/* FichothequeLib_Tools - Copyright (c) 2021-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.text.ParseException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import net.fichotheque.Subset;
import net.fichotheque.SubsetItem;
import net.fichotheque.SubsetKey;
import net.fichotheque.corpus.Corpus;
import net.fichotheque.corpus.metadata.FieldKey;
import net.fichotheque.exportation.table.Cell;
import net.fichotheque.exportation.table.CellConverterProvider;
import net.fichotheque.exportation.table.Col;
import net.fichotheque.exportation.table.ColDef;
import net.fichotheque.exportation.table.FormatColDef;
import net.fichotheque.exportation.table.SubsetTable;
import net.fichotheque.exportation.table.SumCellConverter;
import net.fichotheque.exportation.table.SumResult;
import net.fichotheque.exportation.table.TableExportConstants;
import net.fichotheque.exportation.table.TableExportContext;
import net.fichotheque.extraction.ExtractionContext;
import net.fichotheque.extraction.def.ExtractionDef;
import net.fichotheque.format.FichothequeFormatDef;
import net.fichotheque.format.FormatContext;
import net.fichotheque.format.FormatSource;
import net.fichotheque.format.FormatSourceKey;
import net.fichotheque.format.formatters.SourceFormatter;
import net.fichotheque.pointeurs.SubsetItemPointeur;
import net.fichotheque.thesaurus.Thesaurus;
import net.fichotheque.tools.exportation.table.columnsum.ColumnSumEngine;
import net.fichotheque.tools.extraction.dom.ExtractionDOMUtils;
import net.fichotheque.utils.ExtractionUtils;
import net.fichotheque.utils.FormatterUtils;
import net.fichotheque.utils.TableExportUtils;
import net.fichotheque.utils.pointeurs.PointeurFactory;
import net.mapeadores.util.annotation.Nullable;
import net.mapeadores.util.format.Calcul;
import net.mapeadores.util.format.FormatConstants;
import net.mapeadores.util.format.SumEngine;
import net.mapeadores.util.localisation.Lang;
import net.mapeadores.util.localisation.LangsUtils;
import net.mapeadores.util.localisation.ListLangContextBuilder;
import net.mapeadores.util.logging.SourceLog;
import net.mapeadores.util.logging.SourceLogs;
import net.mapeadores.util.logging.SourceLogsBuilder;
import net.mapeadores.util.logging.SourceMessage;
import net.mapeadores.util.money.Amount;
import net.mapeadores.util.primitives.Decimal;
import net.mapeadores.util.primitives.FuzzyDate;
import net.mapeadores.util.text.StringUtils;
import net.mapeadores.util.xml.XMLUtils;


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

    private SumCellConverterFactory() {

    }

    public static SumCellConverter newInstance(SubsetTable subsetTable, TableExportContext tableExportContext, ExtractionContext extractionContext, CellConverterProvider cellConverterProvider, @Nullable Predicate<SubsetItem> globalPredicate) {
        InternalSumCellConverter cellConverter = new InternalSumCellConverter(tableExportContext, extractionContext, cellConverterProvider, globalPredicate);
        if (extractionContext.getSubsetAccessPredicate().test(subsetTable.getSubset())) {
            cellConverter.add(subsetTable);
        }
        return cellConverter;
    }

    public static SumCellConverter newInstance(List<SubsetTable> subsetTableList, TableExportContext tableExportContext, ExtractionContext extractionContext, CellConverterProvider cellConverterProvider, @Nullable Predicate<SubsetItem> globalPredicate) {
        InternalSumCellConverter cellConverter = new InternalSumCellConverter(tableExportContext, extractionContext, cellConverterProvider, globalPredicate);
        for (SubsetTable subsetTable : subsetTableList) {
            if (extractionContext.getSubsetAccessPredicate().test(subsetTable.getSubset())) {
                cellConverter.add(subsetTable);
            }
        }
        return cellConverter;
    }

    private static Decimal getDecimal(String value, Calcul calcul) {
        Decimal decimal = StringUtils.parseDecimal(value);
        if (calcul != null) {
            double d = decimal.toDouble();
            d = calcul.execute(d);
            decimal = new Decimal(d);
        }
        return decimal;
    }


    private static class InternalSumCellConverter implements SumCellConverter {

        private final Map<SubsetKey, CellByTable> map = new HashMap<SubsetKey, CellByTable>();
        private final TableExportContext tableExportContext;
        private final FormatContext formatContext;
        private final ExtractionContext extractionContext;
        private final CellConverterProvider cellConverterProvider;
        private final Predicate<SubsetItem> globalPredicate;

        private InternalSumCellConverter(TableExportContext tableExportContext, ExtractionContext extractionContext, CellConverterProvider cellConverterProvider, Predicate<SubsetItem> globalPredicate) {
            this.tableExportContext = tableExportContext;
            this.formatContext = tableExportContext.getFormatContext();
            this.cellConverterProvider = cellConverterProvider;
            this.extractionContext = extractionContext;
            this.globalPredicate = globalPredicate;
        }

        @Override
        public Cell[] toCellArray(SubsetItem subsetItem) {
            CellByTable cellByTable = map.get(subsetItem.getSubsetKey());
            if (cellByTable == null) {
                return null;
            }
            return cellByTable.getCellArray(subsetItem);
        }

        @Override
        public SumResult getSumResult(SubsetKey subsetKey) {
            CellByTable cellByTable = map.get(subsetKey);
            if (cellByTable == null) {
                return null;
            }
            return cellByTable.getResult();
        }

        private void add(SubsetTable subsetTable) {
            map.put(subsetTable.getSubset().getSubsetKey(), new CellByTable(subsetTable));
        }


        private class CellByTable {

            private final ColDef[] colDefArray;
            private final ColInfo[] colInfoArray;
            private final SubsetItemPointeur subsetItemPointeur;
            private final ColumnSumEngine columnSumEngine;
            private final ExtractionContext customExtractionContext;
            private final FormatSource.ExtractionInfo extractionInfo;


            private CellByTable(SubsetTable subsetTable) {
                boolean withSection = false;
                List<Col> colList = subsetTable.getColList();
                int colLength = colList.size();
                colDefArray = new ColDef[colLength];
                colInfoArray = new ColInfo[colLength];
                for (int i = 0; i < colLength; i++) {
                    Col col = colList.get(i);
                    ColDef colDef = col.getColDef();
                    colDefArray[i] = colDef;
                    colInfoArray[i] = new ColInfo(i, colDef, col.getSourceFormatter());
                    if (colDef instanceof FormatColDef) {
                        FichothequeFormatDef ffd = ((FormatColDef) colDef).getFichothequeFormatDef();
                        for (FormatSourceKey formatSourceKey : ffd.getFormatSourceKeyList()) {
                            if (formatSourceKey.getSourceType() == FormatSourceKey.FIELDKEY_TYPE) {
                                if (((FieldKey) formatSourceKey.getKeyObject()).isSection()) {
                                    withSection = true;
                                }
                            }
                        }
                    }
                }
                this.columnSumEngine = new ColumnSumEngine(colDefArray);
                Subset subset = subsetTable.getSubset();
                ExtractionContext ec = extractionContext;
                String langs = subsetTable.getParameterValue(TableExportConstants.LANGS_PARAMETER);
                if (langs != null) {
                    Lang[] array = LangsUtils.toCleanLangArray(langs);
                    if (array.length > 0) {
                        ec = ExtractionUtils.derive(extractionContext, ListLangContextBuilder.build(array));
                    }
                }
                this.customExtractionContext = ec;
                FormatSource.ExtractionInfo ei = null;
                String extractionDefString = subsetTable.getParameterValue(TableExportConstants.EXTRACTION_PARAMETER);
                if (extractionDefString != null) {
                    SourceLogsBuilder logsBuilder = new SourceLogsBuilder();
                    ExtractionDef extractionDef = ExtractionDOMUtils.readExtractionDef(subset.getFichotheque(), extractionDefString, null, logsBuilder);
                    if (extractionDef != null) {
                        ei = FormatterUtils.toExtractionInfo(extractionDef);
                    } else {
                        ei = FormatterUtils.toExtractionInfo(getErrorLogs(logsBuilder.toSourceLogs()));
                    }
                }
                this.extractionInfo = ei;
                if (subset instanceof Corpus) {
                    this.subsetItemPointeur = PointeurFactory.newFichePointeur((Corpus) subset, withSection);
                } else if (subset instanceof Thesaurus) {
                    this.subsetItemPointeur = PointeurFactory.newMotclePointeur((Thesaurus) subset);
                } else {
                    this.subsetItemPointeur = PointeurFactory.newSubsetItemPointeur(subset);
                }
            }

            private Cell[] getCellArray(SubsetItem subsetItem) {
                subsetItemPointeur.setCurrentSubsetItem(subsetItem);
                InternalHistory history = new InternalHistory();
                FormatSource formatSource = FormatterUtils.toFormatSource(subsetItemPointeur, customExtractionContext, globalPredicate, formatContext, cellConverterProvider, history, extractionInfo);
                int length = colInfoArray.length;
                Cell[] cellArray = new Cell[length];
                for (int i = 0; i < length; i++) {
                    ColInfo colInfo = colInfoArray[i];
                    Object obj = colInfo.format(formatSource, columnSumEngine);
                    history.add(obj);
                    cellArray[i] = TableExportUtils.toCell(colInfo.castType, obj, colInfo.colDef);
                }
                return cellArray;
            }


            private SumResult getResult() {
                SumResult.ColumnSum[] columnSumArray = null;
                if (columnSumEngine.hasColumnSum()) {
                    columnSumArray = columnSumEngine.getResult();
                }
                return TableExportUtils.toSumResult(colDefArray, columnSumArray);
            }


            private class ColInfo {

                private final ColDef colDef;
                private final int colIndex;
                private final SourceFormatter formatter;
                private final String castType;
                private final boolean withSum;
                private final String sumCastType;
                private final boolean convertEmptyToNull;
                private final Calcul calcul;

                private ColInfo(int colIndex, ColDef colDef, SourceFormatter formatter) {
                    this.colDef = colDef;
                    this.colIndex = colIndex;
                    this.formatter = formatter;
                    this.castType = colDef.getCastType();
                    this.withSum = colDef.isSum();
                    if (withSum) {
                        this.sumCastType = colDef.getSumCastType();
                    } else {
                        this.sumCastType = "";
                    }
                    this.calcul = colDef.getCalcul();
                    this.convertEmptyToNull = colDef.isConvertEmptyToNull();
                }

                private Object format(FormatSource formatSource, ColumnSumEngine columnSumEngine) {
                    String formatResult = formatter.formatSource(formatSource);
                    if (formatResult == null) {
                        return null;
                    }
                    if (withSum) {
                        formatResult = SumEngine.compute(formatResult, sumCastType);
                    }
                    switch (castType) {
                        case FormatConstants.INTEGER_CAST:
                    try {
                            Decimal decimal = StringUtils.parseDecimal(formatResult);
                            long l = decimal.getPartieEntiere();
                            if (calcul != null) {
                                l = calcul.execute(l);
                            }
                            columnSumEngine.sumInteger(colIndex, l);
                            return l;
                        } catch (NumberFormatException nfe) {
                            return null;
                        }
                        case FormatConstants.DECIMAL_CAST:
                    try {
                            Decimal decimal = getDecimal(formatResult, calcul);
                            columnSumEngine.sumDecimal(colIndex, decimal);
                            return decimal;
                        } catch (NumberFormatException nfe) {
                            return null;
                        }
                        case FormatConstants.DATE_CAST:
                    try {
                            FuzzyDate date = FuzzyDate.parse(formatResult);
                            return date;
                        } catch (ParseException e) {
                            return null;
                        }
                        case FormatConstants.MONEY_CAST:
                    try {
                            Amount amount = Amount.parse(formatResult);
                            columnSumEngine.sumMoney(colIndex, amount);
                            return amount;
                        } catch (ParseException e) {
                            return null;
                        }
                        case FormatConstants.PERCENTAGE_CAST:
                    try {
                            Decimal decimal = getDecimal(formatResult, calcul);
                            columnSumEngine.sumPercentage(colIndex, decimal);
                            return decimal;
                        } catch (NumberFormatException nfe) {
                            return null;
                        }
                        default:
                            if (formatResult.length() == 0) {
                                if (convertEmptyToNull) {
                                    return null;
                                } else {
                                    return "";
                                }
                            } else {
                                return formatResult;
                            }
                    }
                }

            }

        }

    }

    private static String getErrorLogs(SourceLogs logs) {
        StringBuilder buf = new StringBuilder();
        buf.append("<extraction>");
        for (SourceLog log : logs) {
            for (SourceMessage sourceMessage : log.getSourceMessageList()) {
                buf.append("<message category=\"");
                buf.append(sourceMessage.getCategory());
                buf.append("\" key=\"");
                buf.append(sourceMessage.getMessageKey());
                buf.append("\">");
                Object[] values = sourceMessage.getMessageValues();
                for (Object value : values) {
                    buf.append("<value>");
                    try {
                        XMLUtils.writeEscape(buf, value.toString());
                    } catch (IOException ioe) {

                    }
                    buf.append("</value>");
                }
                buf.append("</message>");
            }
        }

        buf.append("</extraction>");
        return buf.toString();
    }


    private static class InternalHistory implements FormatSource.History {

        private final List<String> previousList = new ArrayList<String>();
        private final List<String> unmodifiablePreviousList = Collections.unmodifiableList(previousList);

        private InternalHistory() {

        }

        @Override
        public List<String> getPreviousFormatList() {
            return unmodifiablePreviousList;
        }

        private void add(Object obj) {
            if (obj == null) {
                this.previousList.add(null);
            } else {
                this.previousList.add(obj.toString());
            }
        }

    }

}
