/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.fesod.sheet.write.metadata.holder;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString.Exclude;
import lombok.extern.slf4j.Slf4j;
import org.apache.fesod.common.util.IoUtils;
import org.apache.fesod.common.util.MapUtils;
import org.apache.fesod.sheet.enums.CellDataTypeEnum;
import org.apache.fesod.sheet.enums.HolderEnum;
import org.apache.fesod.sheet.exception.ExcelGenerateException;
import org.apache.fesod.sheet.metadata.data.DataFormatData;
import org.apache.fesod.sheet.support.ExcelTypeEnum;
import org.apache.fesod.sheet.util.DateUtils;
import org.apache.fesod.sheet.util.FileUtils;
import org.apache.fesod.sheet.util.StyleUtil;
import org.apache.fesod.sheet.write.handler.context.WorkbookWriteHandlerContext;
import org.apache.fesod.sheet.write.metadata.WriteWorkbook;
import org.apache.fesod.sheet.write.metadata.style.WriteCellStyle;
import org.apache.fesod.sheet.write.metadata.style.WriteFont;
import org.apache.poi.hssf.usermodel.HSSFCellStyle;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

/**
 * Workbook holder
 *
 *
 */
@Getter
@Setter
@EqualsAndHashCode
@Slf4j
public class WriteWorkbookHolder extends AbstractWriteHolder {
    /***
     * Current poi Workbook.This is only for writing, and there may be no data in version 07 when template data needs to
     * be read.
     * <ul>
     * <li>03:{@link HSSFWorkbook}</li>
     * <li>07:{@link SXSSFWorkbook}</li>
     * </ul>
     */
    private Workbook workbook;
    /***
     * Current poi Workbook.Be sure to use and this method when reading template data.
     * <ul>
     * <li>03:{@link HSSFWorkbook}</li>
     * <li>07:{@link XSSFWorkbook}</li>
     * </ul>
     */
    private Workbook cachedWorkbook;
    /**
     * current param
     */
    private WriteWorkbook writeWorkbook;
    /**
     * Final output file
     * <p>
     * If 'outputStream' and 'file' all not empty, file first
     */
    private File file;
    /**
     * Final output stream
     */
    private OutputStream outputStream;
    /**
     * charset.
     * Only work on the CSV file
     */
    private Charset charset;

    /**
     * Set the encoding prefix in the csv file, otherwise the office may open garbled characters.
     * Default true.
     */
    private Boolean withBom;

    /**
     * Template input stream
     * <p>
     * If 'inputStream' and 'file' all not empty, file first
     */
    private InputStream templateInputStream;
    /**
     * Template file
     * <p>
     * If 'inputStream' and 'file' all not empty, file first
     */
    private File templateFile;
    /**
     * Temporary template file stream.
     * <p>
     * A temporary file stream needs to be created in order not to modify the original template file.
     */
    private InputStream tempTemplateInputStream;
    /**
     * Default true
     */
    private Boolean autoCloseStream;
    /**
     * Excel type
     */
    private ExcelTypeEnum excelType;
    /**
     * Mandatory use 'inputStream'
     */
    private Boolean mandatoryUseInputStream;
    /**
     * prevent duplicate creation of sheet objects
     */
    private Map<Integer, WriteSheetHolder> hasBeenInitializedSheetIndexMap;
    /**
     * prevent duplicate creation of sheet objects
     */
    private Map<String, WriteSheetHolder> hasBeenInitializedSheetNameMap;
    /**
     * Whether the encryption
     */
    private String password;
    /**
     * Write excel in memory. Default false, the cache file is created and finally written to excel.
     * <p>
     * Comment and RichTextString are only supported in memory mode.
     */
    private Boolean inMemory;
    /**
     * Excel is also written in the event of an exception being thrown.The default false.
     */
    private Boolean writeExcelOnException;

    /**
     * Used to cell style.
     */
    private Map<Short, Map<WriteCellStyle, CellStyle>> cellStyleIndexMap;
    /**
     * Used to font.
     */
    private Map<WriteFont, Font> fontMap;
    /**
     * Used to data format.
     */
    private Map<DataFormatData, Short> dataFormatMap;

    /**
     * handler context
     */
    @Exclude
    private WorkbookWriteHandlerContext workbookWriteHandlerContext;

    public WriteWorkbookHolder(WriteWorkbook writeWorkbook) {
        super(writeWorkbook, null);
        this.writeWorkbook = writeWorkbook;
        this.file = writeWorkbook.getFile();
        if (file != null) {
            try {
                this.outputStream = new FileOutputStream(file);
            } catch (FileNotFoundException e) {
                throw new ExcelGenerateException("Can not found file.", e);
            }
        } else {
            this.outputStream = writeWorkbook.getOutputStream();
        }

        if (writeWorkbook.getCharset() == null) {
            this.charset = Charset.defaultCharset();
        } else {
            this.charset = writeWorkbook.getCharset();
        }

        if (writeWorkbook.getWithBom() == null) {
            this.withBom = Boolean.TRUE;
        } else {
            this.withBom = writeWorkbook.getWithBom();
        }

        if (writeWorkbook.getAutoCloseStream() == null) {
            this.autoCloseStream = Boolean.TRUE;
        } else {
            this.autoCloseStream = writeWorkbook.getAutoCloseStream();
        }
        if (writeWorkbook.getExcelType() == null) {
            boolean isXls = (file != null && file.getName().endsWith(ExcelTypeEnum.XLS.getValue()))
                    || (writeWorkbook.getTemplateFile() != null
                            && writeWorkbook.getTemplateFile().getName().endsWith(ExcelTypeEnum.XLS.getValue()));
            if (isXls) {
                this.excelType = ExcelTypeEnum.XLS;
            } else {
                boolean isCsv = (file != null && file.getName().endsWith(ExcelTypeEnum.CSV.getValue()))
                        || (writeWorkbook.getTemplateFile() != null
                                && writeWorkbook.getTemplateFile().getName().endsWith(ExcelTypeEnum.CSV.getValue()));
                if (isCsv) {
                    this.excelType = ExcelTypeEnum.CSV;
                } else {
                    this.excelType = ExcelTypeEnum.XLSX;
                }
            }
        } else {
            this.excelType = writeWorkbook.getExcelType();
        }

        // init handler
        initHandler(writeWorkbook, null);

        try {
            copyTemplate();
        } catch (IOException e) {
            throw new ExcelGenerateException("Copy template failure.", e);
        }
        if (writeWorkbook.getMandatoryUseInputStream() == null) {
            this.mandatoryUseInputStream = Boolean.FALSE;
        } else {
            this.mandatoryUseInputStream = writeWorkbook.getMandatoryUseInputStream();
        }
        this.hasBeenInitializedSheetIndexMap = new HashMap<>();
        this.hasBeenInitializedSheetNameMap = new HashMap<>();
        this.password = writeWorkbook.getPassword();
        if (writeWorkbook.getInMemory() == null) {
            this.inMemory = Boolean.FALSE;
        } else {
            this.inMemory = writeWorkbook.getInMemory();
        }
        if (writeWorkbook.getWriteExcelOnException() == null) {
            this.writeExcelOnException = Boolean.FALSE;
        } else {
            this.writeExcelOnException = writeWorkbook.getWriteExcelOnException();
        }
        this.cellStyleIndexMap = MapUtils.newHashMap();
        this.fontMap = MapUtils.newHashMap();
        this.dataFormatMap = MapUtils.newHashMap();
    }

    private void copyTemplate() throws IOException {
        if (writeWorkbook.getTemplateFile() == null && writeWorkbook.getTemplateInputStream() == null) {
            return;
        }
        if (this.excelType == ExcelTypeEnum.CSV) {
            throw new ExcelGenerateException("csv cannot use template.");
        }
        byte[] templateFileByte = null;
        if (writeWorkbook.getTemplateFile() != null) {
            templateFileByte = FileUtils.readFileToByteArray(writeWorkbook.getTemplateFile());
        } else if (writeWorkbook.getTemplateInputStream() != null) {
            try {
                templateFileByte = IoUtils.toByteArray(writeWorkbook.getTemplateInputStream());
            } finally {
                if (autoCloseStream) {
                    writeWorkbook.getTemplateInputStream().close();
                }
            }
        }
        this.tempTemplateInputStream = new ByteArrayInputStream(templateFileByte);
    }

    @Override
    public HolderEnum holderType() {
        return HolderEnum.WORKBOOK;
    }

    /**
     * create a cell style.
     *
     * @param writeCellStyle
     * @param originCellStyle
     * @return
     */
    public CellStyle createCellStyle(
            WriteCellStyle writeCellStyle, CellStyle originCellStyle, CellDataTypeEnum cellDataType) {
        if (writeCellStyle == null) {
            return originCellStyle;
        }

        short styleIndex = -1;
        Font originFont = null;
        boolean useCache = true;
        if (originCellStyle != null) {
            styleIndex = originCellStyle.getIndex();
            if (originCellStyle instanceof XSSFCellStyle) {
                originFont = ((XSSFCellStyle) originCellStyle).getFont();
            } else if (originCellStyle instanceof HSSFCellStyle) {
                originFont = ((HSSFCellStyle) originCellStyle).getFont(workbook);
            }
            useCache = false;

            if (CellDataTypeEnum.DATE.equals(cellDataType)
                    && DateUtils.isADateFormat(
                            originCellStyle.getDataFormat(), originCellStyle.getDataFormatString())) {
                DataFormatData dataFormatData = new DataFormatData();
                dataFormatData.setIndex(originCellStyle.getDataFormat());
                dataFormatData.setFormat(originCellStyle.getDataFormatString());

                writeCellStyle.setDataFormatData(dataFormatData);
            }
        }

        Map<WriteCellStyle, CellStyle> cellStyleMap =
                cellStyleIndexMap.computeIfAbsent(styleIndex, key -> MapUtils.newHashMap());
        CellStyle cellStyle = cellStyleMap.get(writeCellStyle);
        if (cellStyle != null) {
            return cellStyle;
        }
        if (log.isDebugEnabled()) {
            log.info("create new style:{},{}", writeCellStyle, originCellStyle);
        }
        WriteCellStyle tempWriteCellStyle = new WriteCellStyle();
        WriteCellStyle.merge(writeCellStyle, tempWriteCellStyle);

        cellStyle = StyleUtil.buildCellStyle(workbook, originCellStyle, tempWriteCellStyle);
        Short dataFormat = createDataFormat(tempWriteCellStyle.getDataFormatData(), useCache);
        if (dataFormat != null) {
            cellStyle.setDataFormat(dataFormat);
        }
        Font font = createFont(tempWriteCellStyle.getWriteFont(), originFont, useCache);
        if (font != null) {
            cellStyle.setFont(font);
        }
        cellStyleMap.put(tempWriteCellStyle, cellStyle);
        return cellStyle;
    }

    /**
     * create a font.
     *
     * @param writeFont
     * @param originFont
     * @param useCache
     * @return
     */
    public Font createFont(WriteFont writeFont, Font originFont, boolean useCache) {
        if (!useCache) {
            return StyleUtil.buildFont(workbook, originFont, writeFont);
        }
        WriteFont tempWriteFont = new WriteFont();
        WriteFont.merge(writeFont, tempWriteFont);

        Font font = fontMap.get(tempWriteFont);
        if (font != null) {
            return font;
        }
        font = StyleUtil.buildFont(workbook, originFont, tempWriteFont);
        fontMap.put(tempWriteFont, font);
        return font;
    }

    /**
     * create a data format.
     *
     * @param dataFormatData
     * @param useCache
     * @return
     */
    public Short createDataFormat(DataFormatData dataFormatData, boolean useCache) {
        if (dataFormatData == null) {
            return null;
        }
        if (!useCache) {
            return StyleUtil.buildDataFormat(workbook, dataFormatData);
        }
        DataFormatData tempDataFormatData = new DataFormatData();
        DataFormatData.merge(dataFormatData, tempDataFormatData);

        Short dataFormat = dataFormatMap.get(tempDataFormatData);
        if (dataFormat != null) {
            return dataFormat;
        }
        dataFormat = StyleUtil.buildDataFormat(workbook, tempDataFormatData);
        dataFormatMap.put(tempDataFormatData, dataFormat);
        return dataFormat;
    }
}
