/* FichothequeLib_Tools - Copyright (c) 2020-2025 Vincent Calame - Exemole
 * Logiciel libre donné sous triple licence :
 * 1) selon les termes de la CeCILL V2
 * 2) selon les termes de l’EUPL V.1.1
 * 3) selon les termes de la GNU GPLv3
 * Voir le fichier licences.txt
 */


package net.fichotheque.tools.exportation.table;

import java.io.IOException;
import java.io.OutputStream;
import java.time.format.FormatStyle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import net.fichotheque.Subset;
import net.fichotheque.SubsetItem;
import net.fichotheque.SubsetKey;
import net.fichotheque.corpus.Corpus;
import net.fichotheque.corpus.Fiches;
import net.fichotheque.exportation.table.ColDef;
import net.fichotheque.exportation.table.SubsetTable;
import net.fichotheque.exportation.table.TableExport;
import net.fichotheque.exportation.table.TableExportConstants;
import net.fichotheque.utils.FichothequeUtils;
import net.mapeadores.opendocument.io.OdSource;
import net.mapeadores.opendocument.io.OdUtils;
import net.mapeadores.opendocument.io.OdXML;
import net.mapeadores.opendocument.io.SheetNameChecker;
import net.mapeadores.opendocument.io.odtable.OdTableDef;
import net.mapeadores.opendocument.io.odtable.OdTableDefBuilder;
import net.mapeadores.opendocument.io.odtable.OdsOptions;
import net.mapeadores.opendocument.io.odtable.SheetHandler;
import net.mapeadores.opendocument.io.odtable.StyleManager;
import net.mapeadores.opendocument.io.odtable.StyleManagerBuilder;
import net.mapeadores.util.date.DateUtils;
import net.mapeadores.util.format.FormatConstants;
import net.mapeadores.util.io.TempStorageAppendable;
import net.mapeadores.util.localisation.UserLangContext;
import net.mapeadores.util.xml.XMLUtils;
import net.mapeadores.util.xml.XMLWriter;


/**
 *
 * @author Vincent Calame
 */
public final class TableExportOdsFactory {

    private TableExportOdsFactory() {

    }

    public static TableExportOds multi(TableExport tableExport, Fiches fiches, TableExportOdsParameters tableExportOdsParameters, OdsOptions odsOptions) {
        SelectionCheck selectionCheck = check(tableExport, fiches, tableExportOdsParameters.isWithThesaurusTable());
        return toTableExportOds(new MultiContentOdSource(tableExportOdsParameters, selectionCheck, odsOptions));
    }

    public static TableExportOds merge(String sheetName, TableExport tableExport, Fiches fiches, TableExportOdsParameters tableExportOdsParameters, OdsOptions odsOptions) {
        SelectionCheck selectionCheck = check(tableExport, fiches, tableExportOdsParameters.isWithThesaurusTable());
        return toTableExportOds(new MergeContentOdSource(sheetName, tableExportOdsParameters, selectionCheck, odsOptions));
    }

    public static TableExportOds unique(SubsetTable subsetTable, Collection<SubsetItem> subsetItems, TableExportOdsParameters tableExportOdsParameters, OdsOptions odsOptions) {
        return toTableExportOds(new UniqueContentOdSource(subsetTable, subsetItems, tableExportOdsParameters, odsOptions));
    }

    private static TableExportOds toTableExportOds(ContentOdSource contentOdSource) {
        return new TableExportOds(Collections.unmodifiableList(contentOdSource.tableDefList), contentOdSource, contentOdSource.odsOptions);
    }

    private static SelectionCheck check(TableExport tableExport, Fiches fiches, boolean withThesaurusTable) {
        SelectionCheck selectionCheck = new SelectionCheck();
        Map<SubsetKey, SubsetTable> corpusTableMap = new HashMap<SubsetKey, SubsetTable>();
        for (SubsetTable subsetTable : tableExport.getSubsetTableList()) {
            SubsetKey subsetKey = subsetTable.getSubsetKey();
            if (subsetKey.isCorpusSubset()) {
                corpusTableMap.put(subsetKey, subsetTable);
            } else if ((subsetKey.isThesaurusSubset()) && (withThesaurusTable)) {
                selectionCheck.addThesaurus(subsetTable);
            }
        }
        for (Fiches.Entry entry : fiches.getEntryList()) {
            Corpus corpus = entry.getCorpus();
            SubsetTable subsetTable = corpusTableMap.get(corpus.getSubsetKey());
            if (subsetTable != null) {
                selectionCheck.addCorpus(subsetTable, entry.getSubsetItemList());
            }
        }
        return selectionCheck;
    }


    private static String getTableName(SubsetTable subsetTable, TableExportOdsParameters tableExportOdsParameters, SheetNameChecker sheetNameChecker) {
        String tableName = subsetTable.getParameterValue(TableExportConstants.TABLENAME_PARAMETER);
        if (tableName == null) {
            Subset subset = subsetTable.getSubset();
            switch (tableExportOdsParameters.getHeaderType()) {
                case TableExportConstants.COLUMNTITLE_HEADER:
                    tableName = FichothequeUtils.getTitle(subset, tableExportOdsParameters.getWorkingLang());
                    break;
                default:
                    tableName = subset.getSubsetKeyString();
            }
        }
        tableName = sheetNameChecker.checkName(tableName);
        return tableName;
    }

    private static OdTableDef getOdTableDef(String tableName, List<ColDef> colDefList, UserLangContext userLangContext) {
        OdTableDefBuilder odTableDefBuilder = new OdTableDefBuilder(tableName);
        for (ColDef colDef : colDefList) {
            if (colDef.getCastType().equals(FormatConstants.DATE_CAST)) {
                odTableDefBuilder.addDate(getDatePattern(colDef, userLangContext));
            } else {
                odTableDefBuilder.addStandard();
            }
        }
        return odTableDefBuilder.toOdTableDef();
    }

    private static String getDatePattern(ColDef colDef, UserLangContext userLangContext) {
        Object dateStyleObject = colDef.getParameterValue(FormatConstants.DATESTYLE_PARAMKEY);
        if ((dateStyleObject == null) || (!(dateStyleObject instanceof String))) {
            return DateUtils.ISO_JAVAPATTERN;
        }
        String dateStyle = (String) dateStyleObject;
        Locale locale = userLangContext.getFormatLocale();
        FormatStyle formatStyle = FormatConstants.getMatchingFormatStyle(dateStyle);
        if (formatStyle != null) {
            return DateUtils.getLocalizedPattern(formatStyle, locale);
        } else {
            return DateUtils.ISO_JAVAPATTERN;
        }
    }


    private abstract static class ContentOdSource implements OdSource {

        protected final SheetNameChecker sheetNameChecker = new SheetNameChecker();
        private final OdsOptions odsOptions;
        private final SheetHandler supplementarySheetHandler;
        protected final List<OdTableDef> tableDefList = new ArrayList<OdTableDef>();
        protected final NamedRangeHandler namedRangeHandler = new NamedRangeHandler();

        protected ContentOdSource(OdsOptions odsOptions) {
            this.odsOptions = odsOptions;
            if (odsOptions.supplementarySheetWriter() != null) {
                this.supplementarySheetHandler = new SheetHandler(odsOptions.supplementarySheetWriter());
            } else {
                this.supplementarySheetHandler = null;
            }
        }

        protected StyleManagerBuilder initBuilder() {
            StyleManagerBuilder styleManagerBuilder = new StyleManagerBuilder();
            if (supplementarySheetHandler != null) {
                supplementarySheetHandler.addSupplementary(styleManagerBuilder, sheetNameChecker);
            }
            styleManagerBuilder.setElementMaps(odsOptions.elementMaps());
            return styleManagerBuilder;
        }

        protected StyleManager initStyleManager() {
            StyleManagerBuilder styleManagerBuilder = new StyleManagerBuilder();
            if (supplementarySheetHandler != null) {
                supplementarySheetHandler.addSupplementary(styleManagerBuilder, sheetNameChecker);
            }
            styleManagerBuilder.setElementMaps(odsOptions.elementMaps());
            for (OdTableDef odTableDef : tableDefList) {
                styleManagerBuilder.addTableDef(odTableDef);
            }
            return styleManagerBuilder.toStyleManager();
        }

        protected void writeSupplementary(SubsetTableOdsXMLPart xmlPart) throws IOException {
            if (supplementarySheetHandler != null) {
                supplementarySheetHandler.writeSupplementary(xmlPart);
            }
        }

    }


    private static class MergeContentOdSource extends ContentOdSource {

        private final String tableName;
        private final TableExportOdsParameters tableExportOdsParameters;
        private final SelectionCheck selectionCheck;
        private int rowNumber = 1;

        private MergeContentOdSource(String uniqueSheetName, TableExportOdsParameters tableExportOdsParameters, SelectionCheck selectionCheck, OdsOptions odsOptions) {
            super(odsOptions);
            this.tableExportOdsParameters = tableExportOdsParameters;
            this.selectionCheck = selectionCheck;
            this.tableName = sheetNameChecker.checkName(uniqueSheetName);
            tableDefList.add(OdTableDefBuilder.buildStandard(tableName, selectionCheck.getMaxColDefCount()));
        }

        @Override
        public void writeStream(OutputStream outputStream) throws IOException {
            StyleManager styleManager = initStyleManager();
            TempStorageAppendable bodyBuffer = new TempStorageAppendable();
            XMLWriter bufferXMLWriter = XMLUtils.toXMLWriter(bodyBuffer, 3);
            namedRangeHandler.setCurrentTableName(tableName);
            boolean first = true;
            int corpusCount = selectionCheck.getCorpusCount();
            SubsetTableOdsXMLPart subsetTableOdsXMLPart = new SubsetTableOdsXMLPart(bufferXMLWriter, styleManager, tableExportOdsParameters);
            subsetTableOdsXMLPart.tableStart(tableName);
            for (int i = 0; i < corpusCount; i++) {
                writeSubsetRows(subsetTableOdsXMLPart, selectionCheck.getCorpusSubsetTable(i), selectionCheck.getCorpusFicheMetaList(i), first);
                first = false;
            }
            for (SubsetTable thesaurusSubsetTable : selectionCheck.thesaurusList) {
                writeSubsetRows(subsetTableOdsXMLPart, thesaurusSubsetTable, thesaurusSubsetTable.getSubset().getSubsetItemList(), first);
                first = false;
            }
            subsetTableOdsXMLPart.tableEnd();
            writeSupplementary(subsetTableOdsXMLPart);
            namedRangeHandler.toXML(bufferXMLWriter);
            OdUtils.writeSpreadSheetDocumentContent(outputStream, bodyBuffer, styleManager);
        }

        private void writeSubsetRows(SubsetTableOdsXMLPart subsetTableOdsXMLPart, SubsetTable subsetTable, Collection<SubsetItem> subsetItems, boolean first) throws IOException {
            String headerType = tableExportOdsParameters.getHeaderType();
            if (!headerType.equals(TableExportConstants.NONE_HEADER)) {
                if (!first) {
                    subsetTableOdsXMLPart
                            .rowStart()
                            .rowEnd()
                            .rowStart()
                            .rowEnd();
                    rowNumber = rowNumber + 2;
                }
                String title;
                if (headerType.equals(TableExportConstants.COLUMNKEY_HEADER)) {
                    title = subsetTable.getSubset().getSubsetKeyString();
                } else {
                    title = FichothequeUtils.getTitle(subsetTable.getSubset(), tableExportOdsParameters.getWorkingLang());
                }
                int rowAdded = subsetTableOdsXMLPart.addTitleRows(title);
                rowNumber = rowNumber + rowAdded;
            }
            int rowCount = subsetTableOdsXMLPart.addSubsetTable(tableName, subsetTable, subsetItems, rowNumber, namedRangeHandler);
            rowNumber = rowNumber + rowCount;
        }

    }


    private static class MultiContentOdSource extends ContentOdSource {

        private final TableExportOdsParameters tableExportOdsParameters;
        private final SelectionCheck selectionCheck;
        private final String[] corpusTableNameArray;
        private final String[] thesaurusTableNameArray;

        private MultiContentOdSource(TableExportOdsParameters tableExportOdsParameters, SelectionCheck selectionCheck, OdsOptions odsOptions) {
            super(odsOptions);
            this.tableExportOdsParameters = tableExportOdsParameters;
            this.selectionCheck = selectionCheck;
            int corpusCount = selectionCheck.getCorpusCount();
            this.corpusTableNameArray = new String[corpusCount];
            for (int i = 0; i < corpusCount; i++) {
                SubsetTable subsetTable = selectionCheck.getCorpusSubsetTable(i);
                String tableName = getTableName(subsetTable, tableExportOdsParameters, sheetNameChecker);
                corpusTableNameArray[i] = tableName;
                tableDefList.add(getOdTableDef(tableName, subsetTable.getColDefList(), tableExportOdsParameters.getUserLangContext()));
            }
            int thesaurusCount = selectionCheck.getThesaurusCount();
            this.thesaurusTableNameArray = new String[thesaurusCount];
            for (int i = 0; i < thesaurusCount; i++) {
                SubsetTable subsetTable = selectionCheck.getThesaurusSubsetTable(i);
                String tableName = getTableName(subsetTable, tableExportOdsParameters, sheetNameChecker);
                thesaurusTableNameArray[i] = tableName;
                tableDefList.add(getOdTableDef(tableName, subsetTable.getColDefList(), tableExportOdsParameters.getUserLangContext()));
            }
        }

        @Override
        public void writeStream(OutputStream outputStream) throws IOException {
            StyleManager styleManager = initStyleManager();
            TempStorageAppendable bodyBuffer = new TempStorageAppendable();
            XMLWriter bufferXMLWriter = XMLUtils.toXMLWriter(bodyBuffer, 3);
            SubsetTableOdsXMLPart subsetTableOdsXMLPart = new SubsetTableOdsXMLPart(bufferXMLWriter, styleManager, tableExportOdsParameters);
            if (selectionCheck.isEmpty()) {
                OdXML.openTable(bufferXMLWriter, "_empty");
                OdXML.closeTable(bufferXMLWriter);
            } else {
                int corpusLength = corpusTableNameArray.length;
                for (int i = 0; i < corpusLength; i++) {
                    writeSubsetTable(subsetTableOdsXMLPart, corpusTableNameArray[i], selectionCheck.getCorpusSubsetTable(i), selectionCheck.getCorpusFicheMetaList(i));
                }
                int thesaurusLength = thesaurusTableNameArray.length;
                for (int i = 0; i < thesaurusLength; i++) {
                    writeSubsetTable(subsetTableOdsXMLPart, thesaurusTableNameArray[i], selectionCheck.getThesaurusSubsetTable(i), null);
                }
            }
            writeSupplementary(subsetTableOdsXMLPart);
            namedRangeHandler.toXML(bufferXMLWriter);
            OdUtils.writeSpreadSheetDocumentContent(outputStream, bodyBuffer, styleManager);
        }

        private void writeSubsetTable(SubsetTableOdsXMLPart subsetTableOdsXMLPart, String tableName, SubsetTable subsetTable, Collection<SubsetItem> subsetItems) throws IOException {
            namedRangeHandler.setCurrentTableName(tableName);
            subsetTableOdsXMLPart.tableStart(tableName);
            subsetTableOdsXMLPart.addSubsetTable(tableName, subsetTable, subsetItems, 1, namedRangeHandler);
            subsetTableOdsXMLPart.tableEnd();
        }

    }


    private static class UniqueContentOdSource extends ContentOdSource implements OdSource {

        private final SubsetTable subsetTable;
        private final Collection<SubsetItem> subsetItems;
        private final TableExportOdsParameters tableExportOdsParameters;
        private final String tableName;


        private UniqueContentOdSource(SubsetTable subsetTable, Collection<SubsetItem> subsetItemList, TableExportOdsParameters tableExportOdsParameters, OdsOptions odsOptions) {
            super(odsOptions);
            this.subsetTable = subsetTable;
            this.subsetItems = subsetItemList;
            this.tableExportOdsParameters = tableExportOdsParameters;
            this.tableName = getTableName(subsetTable, tableExportOdsParameters, sheetNameChecker);
            tableDefList.add(getOdTableDef(tableName, subsetTable.getColDefList(), tableExportOdsParameters.getUserLangContext()));
        }

        @Override
        public void writeStream(OutputStream outputStream) throws IOException {
            StyleManager styleManager = initStyleManager();
            TempStorageAppendable bodyBuffer = new TempStorageAppendable();
            XMLWriter bufferXMLWriter = XMLUtils.toXMLWriter(bodyBuffer, 3);
            namedRangeHandler.setCurrentTableName(tableName);
            SubsetTableOdsXMLPart subsetTableOdsXMLPart = new SubsetTableOdsXMLPart(bufferXMLWriter, styleManager, tableExportOdsParameters);
            subsetTableOdsXMLPart.tableStart(tableName);
            subsetTableOdsXMLPart.addSubsetTable(tableName, subsetTable, subsetItems, 1, namedRangeHandler);
            subsetTableOdsXMLPart.tableEnd();
            writeSupplementary(subsetTableOdsXMLPart);
            namedRangeHandler.toXML(bufferXMLWriter);
            OdUtils.writeSpreadSheetDocumentContent(outputStream, bodyBuffer, styleManager);
        }

    }


    private static class SelectionCheck {

        private final List<SubsetTable> corpusList = new ArrayList<SubsetTable>();
        private final List<List<SubsetItem>> ficheList = new ArrayList<List<SubsetItem>>();
        private final List<SubsetTable> thesaurusList = new ArrayList<SubsetTable>();

        private SelectionCheck() {

        }

        private int getCorpusCount() {
            return corpusList.size();
        }

        private int getThesaurusCount() {
            return thesaurusList.size();
        }

        private SubsetTable getCorpusSubsetTable(int i) {
            return corpusList.get(i);
        }

        private SubsetTable getThesaurusSubsetTable(int i) {
            return thesaurusList.get(i);
        }

        private List<SubsetItem> getCorpusFicheMetaList(int i) {
            return ficheList.get(i);
        }

        private void addCorpus(SubsetTable subsetTable, List<SubsetItem> ficheMetaList) {
            corpusList.add(subsetTable);
            ficheList.add(ficheMetaList);
        }

        private void addThesaurus(SubsetTable subsetTable) {
            thesaurusList.add(subsetTable);
        }

        private boolean isEmpty() {
            return (corpusList.isEmpty()) && (thesaurusList.isEmpty());
        }

        private int getMaxColDefCount() {
            int max = 0;
            for (SubsetTable subsetTable : corpusList) {
                max = Math.max(max, subsetTable.getColDefList().size());
            }
            for (SubsetTable subsetTable : thesaurusList) {
                max = Math.max(max, subsetTable.getColDefList().size());
            }
            return max;
        }

    }

}
