/*******************************************************************************
 * Copyright (c) 2012, 2020 Original authors and others.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Original authors and others - initial API and implementation
 *     Dirk Fauth <dirk.fauth@googlemail.com> - Bug 450443
 *     Jan Haensli <jan.haensli@inventage.com> - Bug 452453
 *     Uwe Peuker <dev@upeuker.net> - Bug 500787
 ******************************************************************************/
package org.eclipse.nebula.widgets.nattable.export.excel;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.Charset;

import org.eclipse.nebula.widgets.nattable.config.IConfigRegistry;
import org.eclipse.nebula.widgets.nattable.export.FileOutputStreamProvider;
import org.eclipse.nebula.widgets.nattable.export.ILayerExporter;
import org.eclipse.nebula.widgets.nattable.export.IOutputStreamProvider;
import org.eclipse.nebula.widgets.nattable.layer.cell.ILayerCell;
import org.eclipse.nebula.widgets.nattable.style.CellStyleAttributes;
import org.eclipse.nebula.widgets.nattable.style.CellStyleProxy;
import org.eclipse.nebula.widgets.nattable.style.DisplayMode;
import org.eclipse.nebula.widgets.nattable.util.GUIHelper;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.widgets.Shell;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class is used to export a NatTable to an Excel spreadsheet by using a
 * XML format.
 */
public class ExcelExporter implements ILayerExporter {

    private static final Logger LOG = LoggerFactory.getLogger(ExcelExporter.class);

    private static final String EXCEL_HEADER_FILE = "excelExportHeader.txt"; //$NON-NLS-1$

    private static final String CHARSET_PLACEHOLDER = "${charset}"; //$NON-NLS-1$
    private static final String SHEETNAME_PLACEHOLDER = "${sheetname}"; //$NON-NLS-1$

    private String charset = "windows-1252"; //$NON-NLS-1$
    private Charset exportCharSet;
    private String sheetname = "Sheet1"; //$NON-NLS-1$

    /**
     * The IOutputStreamProvider that is used to create new OutputStreams on
     * beginning new export operations.
     */
    private final IOutputStreamProvider outputStreamProvider;

    /**
     * Creates a new ExcelExporter using a FileOutputStreamProvider with default
     * values.
     */
    public ExcelExporter() {
        this(new FileOutputStreamProvider("table_export.xls", //$NON-NLS-1$
                new String[] { "Excel Workbook (*.xls)" }, new String[] { "*.xls" })); //$NON-NLS-1$ //$NON-NLS-2$
    }

    /**
     * Creates a new ExcelExporter that uses the given IOutputStreamProvider for
     * retrieving the OutputStream to write the export to.
     *
     * @param outputStreamProvider
     *            The IOutputStreamProvider that is used to retrieve the
     *            OutputStream to write the export to.
     */
    public ExcelExporter(IOutputStreamProvider outputStreamProvider) {
        this.outputStreamProvider = outputStreamProvider;
    }

    @Override
    public OutputStream getOutputStream(Shell shell) {
        return this.outputStreamProvider.getOutputStream(shell);
    }

    @Override
    public void exportBegin(OutputStream outputStream) throws IOException {
        this.exportCharSet = Charset.forName(this.charset);
    }

    @Override
    public void exportEnd(OutputStream outputStream) throws IOException {
        // no specific action needed on export end in this exporter
    }

    @Override
    public void exportLayerBegin(OutputStream outputStream, String layerName) throws IOException {
        writeHeader(outputStream);
        outputStream.write(asBytes("<body><table border='1'>")); //$NON-NLS-1$
    }

    /**
     * Writes the Excel header informations that are stored locally in the
     * package structure.
     *
     * @throws IOException
     *             if an I/O error occurs on closing the stream to the header
     *             content file
     */
    private void writeHeader(OutputStream outputStream) throws IOException {
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(this.getClass().getResourceAsStream(EXCEL_HEADER_FILE)));) {

            String line = null;
            while ((line = reader.readLine()) != null) {
                // don not export comments in export header template
                if (!line.startsWith("#")) { //$NON-NLS-1$
                    line = line.replace(CHARSET_PLACEHOLDER, this.charset);
                    line = line.replace(SHEETNAME_PLACEHOLDER, this.sheetname);
                    outputStream.write(line.getBytes());
                    outputStream.write(System.getProperty("line.separator").getBytes()); //$NON-NLS-1$
                }
            }
        } catch (Exception e) {
            LOG.error("Excel Exporter failed: {}", e.getMessage(), e); //$NON-NLS-1$
        }
    }

    @Override
    public void exportLayerEnd(OutputStream outputStream, String layerName) throws IOException {
        outputStream.write(asBytes("</table></body></html>")); //$NON-NLS-1$
    }

    @Override
    public void exportRowBegin(OutputStream outputStream, int rowPosition) throws IOException {
        outputStream.write(asBytes("<tr>\n")); //$NON-NLS-1$
    }

    @Override
    public void exportRowEnd(OutputStream outputStream, int rowPosition) throws IOException {
        outputStream.write(asBytes("</tr>\n")); //$NON-NLS-1$
    }

    @Override
    public void exportCell(OutputStream outputStream,
            Object exportDisplayValue, ILayerCell cell,
            IConfigRegistry configRegistry) throws IOException {

        if (cell.getBounds().width == 0 || cell.getBounds().height == 0) {
            // if the cell is not visible to the user, it should not be exported
            return;
        }

        CellStyleProxy cellStyle = new CellStyleProxy(
                configRegistry,
                DisplayMode.NORMAL,
                cell.getConfigLabels());
        Color fg = cellStyle.getAttributeValue(CellStyleAttributes.FOREGROUND_COLOR);
        Color bg = cellStyle.getAttributeValue(CellStyleAttributes.BACKGROUND_COLOR);
        Font font = cellStyle.getAttributeValue(CellStyleAttributes.FONT);

        if (fg == null) {
            fg = GUIHelper.COLOR_BLACK;
        }
        if (bg == null) {
            bg = GUIHelper.COLOR_WHITE;
        }
        if (font == null) {
            font = GUIHelper.DEFAULT_FONT;
        }

        String htmlAttributes = String.format("style='color: %s; background-color: %s; %s;'", //$NON-NLS-1$
                getColorInCSSFormat(fg), getColorInCSSFormat(bg),
                getFontInCSSFormat(font));

        String htmlText = exportDisplayValue != null ? exportDisplayValue.toString() : ""; //$NON-NLS-1$

        if (htmlText.startsWith(" ")) { //$NON-NLS-1$
            htmlAttributes += " x:str=\"'" + htmlText + "\";"; //$NON-NLS-1$ //$NON-NLS-2$
            htmlText = htmlText.replaceFirst("^(\\ *)", "<span style='mso-spacerun:yes'>$1</span>"); //$NON-NLS-1$ //$NON-NLS-2$
        }

        outputStream.write(asBytes(String.format("\t<td %s>%s</td>\n", htmlAttributes, htmlText))); //$NON-NLS-1$
    }

    private byte[] asBytes(String string) {
        return string.getBytes(this.exportCharSet);
    }

    private String getFontInCSSFormat(Font font) {
        FontData fontData = font.getFontData()[0];
        String fontName = fontData.getName();
        int fontStyle = fontData.getStyle();
        String HTML_STYLES[] = new String[] { "NORMAL", "BOLD", "ITALIC" }; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$

        return String.format("font: %s; font-family: %s", //$NON-NLS-1$
                fontStyle <= 2 ? HTML_STYLES[fontStyle] : HTML_STYLES[0],
                fontName);
    }

    private String getColorInCSSFormat(Color color) {
        return String.format("rgb(%d,%d,%d)", //$NON-NLS-1$
                Integer.valueOf(color.getRed()),
                Integer.valueOf(color.getGreen()),
                Integer.valueOf(color.getBlue()));
    }

    @Override
    public Object getResult() {
        return this.outputStreamProvider.getResult();
    }

    /**
     * @param charset
     *            The charset that should be used as replacement for the charset
     *            value in the export header. Default is <i>windows-1252</i>
     */
    public void setCharset(String charset) {
        this.charset = charset;
    }

    /**
     *
     * @param sheetname
     *            The name that should be set as sheet name in the resulting
     *            Excel file. Default is <i>Sheet1</i>
     */
    public void setSheetname(String sheetname) {
        this.sheetname = sheetname;
    }
}
