/* FichothequeLib_Tools - Copyright (c) 2007-2026 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.LineNumberReader;
import java.io.Reader;
import java.io.StringReader;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.fichotheque.Subset;
import net.fichotheque.exportation.table.FormatColDef;
import net.fichotheque.exportation.table.FormatColDefChecker;
import net.fichotheque.exportation.table.TableDef;
import net.fichotheque.exportation.table.TableExportContext;
import net.fichotheque.exportation.table.TableInclusionDef;
import net.fichotheque.exportation.table.TableInclusionResolver;
import net.fichotheque.format.FichothequeFormatDef;
import net.fichotheque.tools.format.FichothequeFormatDefParser;
import net.fichotheque.utils.TableDefUtils;
import net.mapeadores.util.exceptions.ShouldNotOccurException;
import net.mapeadores.util.format.FormatConstants;
import net.mapeadores.util.logging.LineMessageHandler;
import net.mapeadores.util.logging.LogUtils;
import net.mapeadores.util.text.LabelUtils;
import net.mapeadores.util.text.Labels;


/**
 *
 * @author Vincent Calame
 */
public class TableDefParser {

    private final static short START_BLOCK = 1;
    private final static short ON_BLOCK = 2;
    private final static short ON_PARAMETER = 3;
    private final static String START_PREFIX = "!!start ";
    private final static String END_PREFIX = "!!end";
    private final List<TableDef.Entry> defEntryList = new ArrayList<TableDef.Entry>();
    private final Map<String, String> paramMap = new LinkedHashMap<String, String>();
    private final Set<String> existingColNameSet = new HashSet<String>();
    private final LineMessageHandler lineMessageHandler;
    private final FormatColDefChecker formatColDefChecker;
    private final TableExportContext tableExportContext;
    private final Subset subset;
    private final boolean allowDuplicate;
    private String currentColLine = null;
    private String currentFormatSource = null;
    private String currentFormatPattern = null;
    private String currentFormatInstruction = null;
    private final List<String> inclusionLineList = new ArrayList<String>();
    private String currentParameter = null;
    private StringBuilder currentParameterBuilder = null;
    private int firstLineNumber;
    private boolean ficheFormatLine = false;
    private short state = START_BLOCK;

    private TableDefParser(Parameters parameters) {
        this.subset = parameters.subset;
        this.tableExportContext = parameters.tableExportContext;
        this.lineMessageHandler = parameters.lineMessageHandler;
        this.allowDuplicate = parameters.allowDuplicate;
        if (tableExportContext != null) {
            this.formatColDefChecker = tableExportContext.getFormatColDefChecker(subset);
        } else {
            this.formatColDefChecker = null;
        }
    }

    public static TableDef parse(Reader reader, Parameters parameters) throws IOException {
        LineNumberReader lineNumberReader = new LineNumberReader(reader);
        String line;
        TableDefParser tableDefParser = new TableDefParser(parameters);
        int lineDiff = parameters.lineDif();
        while ((line = lineNumberReader.readLine()) != null) {
            int lineNumber = lineNumberReader.getLineNumber() + lineDiff;
            tableDefParser.parseLine(line, lineNumber);
        }
        tableDefParser.flush();
        tableDefParser.flushParameter();
        return TableDefUtils.toTableDef(tableDefParser.defEntryList, tableDefParser.paramMap);
    }

    public static TableDef parse(String s, Parameters parameters) {
        try {
            StringReader reader = new StringReader(s);
            return parse(reader, parameters);
        } catch (IOException ioe) {
            throw new ShouldNotOccurException();
        }
    }

    private void parseLine(String line, int lineNumber) {
        line = line.trim();
        if (line.length() == 0) {
            switch (state) {
                case ON_BLOCK:
                    flush();
                    state = START_BLOCK;
                    break;
                case ON_PARAMETER:
                    currentParameterBuilder.append('\n');
                    break;
                default:
                    state = START_BLOCK;
            }
            return;
        }
        char carac = line.charAt(0);
        if (carac == '#') {
            return;
        }
        if (carac == '!') {
            switch (state) {
                case START_BLOCK:
                    if (line.startsWith(START_PREFIX)) {
                        currentParameter = line.substring(START_PREFIX.length()).trim();
                        if (currentParameter.length() == 0) {
                            lineMessageHandler.addMessage(FormatConstants.SEVERE_SYNTAX, lineNumber, "_ error.empty.tableexport.parametername");
                            currentParameter = null;
                            return;
                        } else {
                            currentParameterBuilder = new StringBuilder();
                            state = ON_PARAMETER;
                        }
                    } else {
                        addParameter(line, lineNumber);
                    }
                    break;
                case ON_BLOCK:
                    addLine(line, lineNumber);
                    break;
                case ON_PARAMETER:
                    if (line.startsWith(END_PREFIX)) {
                        state = START_BLOCK;
                        flushParameter();
                    } else {
                        currentParameterBuilder.append(line);
                        currentParameterBuilder.append('\n');
                    }
                    break;
            }
            return;
        }
        switch (state) {
            case START_BLOCK:
            case ON_BLOCK:
                addLine(line, lineNumber);
                state = ON_BLOCK;
                break;
            case ON_PARAMETER:
                currentParameterBuilder.append(line);
                currentParameterBuilder.append('\n');
                break;
        }
    }

    private void addParameter(String line, int currentLineNumber) {
        if (line.length() == 1) {
            lineMessageHandler.addMessage(FormatConstants.SEVERE_SYNTAX, currentLineNumber, "_ error.empty.tableexport.parameterline");
            return;
        }
        int idx = line.indexOf('=');
        if (idx == -1) {
            lineMessageHandler.addMessage(FormatConstants.SEVERE_SYNTAX, currentLineNumber, "_ error.empty.tableexport.parametervalue", line);
            return;
        }
        String paramName = line.substring(1, idx).trim();
        if (paramName.length() == 0) {
            lineMessageHandler.addMessage(FormatConstants.SEVERE_SYNTAX, currentLineNumber, "_ error.empty.tableexport.parametername", line);
            return;
        }
        if (paramName.charAt(0) == '!') {
            lineMessageHandler.addMessage(FormatConstants.SEVERE_SYNTAX, currentLineNumber, "_ error.wrong.tableexport.parametername", line);
            return;
        }
        String paramValue = line.substring(idx + 1).trim();
        addParameter(paramName, paramValue);
    }

    private void addLine(String line, int currentLineNumber) {
        if (currentColLine == null) {
            firstLineNumber = currentLineNumber;
            if (line.startsWith("%")) {
                if (line.length() == 1) {
                    line = "NULL";
                } else {
                    line = line.substring(1);
                }
                ficheFormatLine = false;
            } else {
                ficheFormatLine = true;
            }
            currentColLine = line;
        } else {
            if (ficheFormatLine) {
                if (currentFormatSource == null) {
                    currentFormatSource = line;
                } else if (currentFormatPattern == null) {
                    currentFormatPattern = line;
                } else if (currentFormatInstruction == null) {
                    currentFormatInstruction = line;
                } else {
                    lineMessageHandler.addMessage(FormatConstants.WARNING_SYNTAX, currentLineNumber, "_ error.wrong.tableexport.toomanylines", line);
                }
            } else {
                inclusionLineList.add(line);
            }
        }
    }

    private void flushParameter() {
        if (currentParameter != null) {
            addParameter(currentParameter, currentParameterBuilder.toString());
        }
        currentParameter = null;
        currentParameterBuilder = null;
    }

    private void flush() {
        if (currentColLine != null) {
            String colName;
            Labels customLabels;
            int idx = currentColLine.indexOf('>');
            if (idx > 0) {
                colName = currentColLine.substring(0, idx).trim();
                String instruction = currentColLine.substring(idx + 1);
                try {
                    customLabels = LabelUtils.parseInstruction(instruction);
                } catch (ParseException pe) {
                    customLabels = null;
                }
            } else {
                colName = currentColLine;
                customLabels = null;
            }
            if (ficheFormatLine) {
                if (currentFormatSource == null) {
                    lineMessageHandler.addMessage(FormatConstants.SEVERE_SYNTAX, firstLineNumber, "_ error.empty.tableexport.formatsourceline");
                    clear();
                    return;
                }
                if ((currentFormatPattern != null) && (currentFormatPattern.equals("-"))) {
                    currentFormatPattern = null;
                }
                FichothequeFormatDef formatDef = FichothequeFormatDefParser.parse(currentFormatSource, currentFormatPattern, currentFormatInstruction, TableDefUtils.toFormatDefMessageHandler(lineMessageHandler, firstLineNumber));
                if (formatDef != null) {
                    if ((formatColDefChecker == null) || (formatColDefChecker.checkFormatColDef(colName, formatDef, firstLineNumber, lineMessageHandler))) {
                        if ((!allowDuplicate) && (existingColNameSet.contains(colName))) {
                            lineMessageHandler.addMessage(FormatConstants.SEVERE_COLNAME, firstLineNumber, "_ error.existing.tableexport.colname", colName);
                        } else {
                            FormatColDef colDef = TableDefUtils.toFormatColDef(colName, formatDef, customLabels);
                            addDefEntry(firstLineNumber, colDef);
                            existingColNameSet.add(colName);
                        }
                    }
                }
            } else {
                TableInclusionDef tableInclusionDef = TableDefUtils.newTableInclusionDef(colName, customLabels, inclusionLineList.toArray(new String[inclusionLineList.size()]));
                if (checkTableInclusionDef(firstLineNumber, tableInclusionDef)) {
                    addDefEntry(firstLineNumber, tableInclusionDef);
                }
            }
            clear();
        }
    }

    private boolean checkTableInclusionDef(int lineNumber, TableInclusionDef tableInclusionDef) {
        if (tableExportContext == null) {
            return true;
        }
        TableInclusionResolver tableInclusionResolver = tableExportContext.getTableInclusionResolverProvider().getTableInclusionResolver(tableInclusionDef.getNameSpace());
        if (tableInclusionResolver == null) {
            lineMessageHandler.addMessage(FormatConstants.WARNING_FICHOTHEQUE, lineNumber, "_ error.unknown.tableexport.inclusion", tableInclusionDef.getNameSpace());
            return false;
        }
        return tableInclusionResolver.test(tableInclusionDef, subset, tableExportContext, lineNumber, lineMessageHandler);
    }

    private void clear() {
        currentColLine = null;
        currentFormatSource = null;
        currentFormatPattern = null;
        currentFormatInstruction = null;
        inclusionLineList.clear();
    }

    private void addParameter(String paramName, String paramValue) {
        paramMap.put(paramName, paramValue);
    }

    private void addDefEntry(int lineNumber, TableDef.Entry defEntry) {
        defEntryList.add(defEntry);
    }

    public static TableDefParser.Parameters initParameters() {
        return new Parameters(null, null);
    }

    public static TableDefParser.Parameters initParameters(Subset subset, TableExportContext tableExportContext) {
        if (subset == null) {
            tableExportContext = null;
        } else if (tableExportContext == null) {
            subset = null;
        }
        return new Parameters(subset, tableExportContext);
    }


    public static class Parameters {

        private final Subset subset;
        private final TableExportContext tableExportContext;
        private LineMessageHandler lineMessageHandler = LogUtils.NULL_LINEMESSAGEHANDLER;
        private int lineDif = 0;
        private boolean allowDuplicate = false;

        private Parameters(Subset subset, TableExportContext tableExportContext) {
            this.subset = subset;
            this.tableExportContext = tableExportContext;
        }

        public Subset subset() {
            return subset;
        }

        public TableExportContext tableExportContext() {
            return tableExportContext;
        }

        public LineMessageHandler lineMessageHandler() {
            return lineMessageHandler;
        }

        public Parameters lineMessageHandler(LineMessageHandler lineMessageHandler) {
            this.lineMessageHandler = lineMessageHandler;
            return this;
        }

        public int lineDif() {
            return lineDif;
        }

        public Parameters lineDif(int lineDif) {
            this.lineDif = lineDif;
            return this;
        }

        public boolean allowDuplicate() {
            return allowDuplicate;
        }

        public Parameters allowDuplicate(boolean allowDuplicate) {
            this.allowDuplicate = allowDuplicate;
            return this;
        }


    }

}
