/*
 *  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.causeway.extensions.tabular.excel.exporter;

import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.util.function.Function;
import java.util.stream.IntStream;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.jspecify.annotations.Nullable;

import org.apache.causeway.applib.value.Blob;
import org.apache.causeway.applib.value.NamedWithMimeType.CommonMimeType;
import org.apache.causeway.commons.internal.base._Reduction;
import org.apache.causeway.commons.internal.base._Strings;
import org.apache.causeway.commons.io.DataSource;
import org.apache.causeway.commons.tabular.TabularModel;
import org.apache.causeway.commons.tabular.TabularModel.TabularCell;
import org.apache.causeway.commons.tabular.TabularModel.TabularRow;

import lombok.Builder;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;

/**
 * Utility to write a {@link TabularModel} to file.
 */
@Slf4j
public record ExcelFileWriter(@Nullable Options options) {

    @Builder
    public record Options(
            /**
             * Custom style for individual cell based on cell value. Overrules rowStyleFunction if provided.
             * <p>
             * {@link Function} may return {@code null}.
             */
            @Nullable Function<TabularCell, CustomCellStyle> cellStyleFunction,
            /**
             * Custom style for entire row based on row data. Overruled by cellStyleFunction if provided.
             * {@link Function} may return {@code null}.<br>
             */
            @Nullable Function<TabularRow, CustomCellStyle> rowStyleFunction) {

        @RequiredArgsConstructor
        public enum CustomCellStyle {
            DEFAULT(-1),
            BLUE(0xa0c4ff), // light-blue
            GREEN(0x75b798), // green-300
            INDIGO(0xa370f7), // indigo-300
            MINT(0xb2e0d9), // pale-mint
            PINK(0xffb2d1), // light-pink
            GRAY(0xe0e0e0), // light-gray
            WARNING(0xffcd39), // warning yellow-400
            DANGER(0xe35d6a); // danger red-400
            public final int rgb;
            public boolean isDefault() { return this == CustomCellStyle.DEFAULT; }
            static CustomCellStyle nullToDefault(final @Nullable CustomCellStyle customCellStyle) {
                return customCellStyle!=null
                        ? customCellStyle
                        : DEFAULT;
            }
        }
    }

    @SneakyThrows
    public void write(final TabularModel tabular, final File tempFile) {
        try(final Workbook wb = new SXSSFWorkbook()) {
            tabular.sheets().forEach(sheet->writeSheet(wb, sheet));
            try(var fos = new FileOutputStream(tempFile)) {
                wb.write(fos);
            }
        }
    }

    /**
     * Writes given tabular data to a {@link Blob}, using given name as blob name.
     */
    @SneakyThrows
    public Blob writeToBlob(final String name, final TabularModel tabular) {
        var tempFile = File.createTempFile(this.getClass().getCanonicalName(), name);
        try {
            write(tabular, tempFile);
            return Blob.of(name, CommonMimeType.XLSX, DataSource.ofFile(tempFile).bytes());
        } finally {
            Files.deleteIfExists(tempFile.toPath()); // cleanup
        }
    }

    // -- HELPER

    @RequiredArgsConstructor
    private static class RowFactory {
        private final Sheet sheet;
        private int rowNum;
        public Row newRow() {
            return sheet.createRow(rowNum++);
        }
    }

    private void writeSheet(final Workbook wb, final TabularModel.TabularSheet tabularSheet) {

        var sheetName = tabularSheet.sheetName();

        Row row;

        var sheet = wb.createSheet(sheetName);
        if(sheet instanceof SXSSFSheet sxssfSheet) {
            sxssfSheet.trackAllColumnsForAutoSizing();
        }
        var cellWriter = new ExcelCellWriter(5, new ExcelImageHandler(sheet));

        var cellStyleProvider = CellStyleProvider.create(wb, options);
        var rowFactory = new RowFactory(sheet);

        var dataColumns = tabularSheet.columns();

        // primary header row
        row = rowFactory.newRow();
        int i=0;
        for(var column : dataColumns) {
            final Cell cell = row.createCell((short) i++);
            cell.setCellValue(column.columnName());
            cell.setCellStyle(cellStyleProvider.primaryHeaderStyle());
        }

        // secondary header row
        row = rowFactory.newRow();
        i=0;
        var maxLinesInRow = _Reduction.of(1, Math::max); // row auto-size calculation
        for(var column : dataColumns) {
            final Cell cell = row.createCell((short) i++);
            final String columnDescription = column.columnDescription();
            cell.setCellValue(columnDescription);
            maxLinesInRow.accept((int)
                _Strings.splitThenStream(columnDescription, "\n").count());
            cell.setCellStyle(cellStyleProvider.secondaryHeaderStyle());
        }
        autoSizeRow(row, maxLinesInRow.getResult().orElse(1),
            wb.getFontAt(cellStyleProvider.secondaryHeaderStyle().getFontIndex()));

        var dataRows = tabularSheet.rows();

        int writtenCount = 0;

        // detail rows
        for (var dataRow : dataRows) {
            row = rowFactory.newRow();
            i=0;
            maxLinesInRow = _Reduction.of(1, Math::max); // row auto-size calculation
            for(var column : dataColumns) {
                final Cell cell = row.createCell((short) i++);
                final TabularCell tabularCell = dataRow.getCell(column);
                final int linesWritten = cellWriter.setCellValue(
                    column,
                    tabularCell,
                    cell,
                    cellStyleProvider);
                maxLinesInRow.accept(linesWritten);
            }
            cellStyleProvider.applyCustomStyle(dataRow, row);

            autoSizeRow(row, maxLinesInRow.getResult().orElse(1), null);

            ++writtenCount;
            if(writtenCount%1000==0) {
                log.info("rows written {}k of {}k", writtenCount/1000, (dataRows.size()+500)/1000);
            }
        }

        // column auto-size
        autoSizeColumns(sheet, dataColumns.size());

        // freeze panes
        sheet.createFreezePane(0, 2);
    }

    private void autoSizeRow(final Row row, final int numberOfLines, final @Nullable Font fontHint) {
        if(numberOfLines<2) return; // ignore
        final int defaultHeight = fontHint!=null
                ? fontHint.getFontHeight()
                : row.getSheet().getDefaultRowHeight();
        int height = numberOfLines * defaultHeight;
        height = Math.min(height, Short.MAX_VALUE); // upper bound to 32767 'twips' or 1/20th of a point
        row.setHeight((short) height);
    }

    private void autoSizeColumns(final Sheet sheet, final int columnCount) {
        IntStream.range(0, columnCount)
            .forEach(sheet::autoSizeColumn);
    }

}
