/* FichothequeLib_Tools - Copyright (c) 2007-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.sql;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;
import net.fichotheque.Subset;
import net.fichotheque.SubsetItem;
import net.fichotheque.exportation.sql.SqlExport;
import net.fichotheque.exportation.table.CellConverter;
import net.fichotheque.exportation.table.ColDef;
import net.fichotheque.exportation.table.JunctionTable;
import net.fichotheque.exportation.table.SubsetTable;
import net.fichotheque.exportation.table.TableExport;
import net.fichotheque.exportation.table.TableExportConstants;
import net.fichotheque.exportation.table.TableExportContext;
import net.fichotheque.extraction.ExtractionContext;
import net.fichotheque.tools.exportation.table.JunctionTableExportSqlWriter;
import net.fichotheque.tools.exportation.table.SqlTableWriter;
import net.fichotheque.tools.exportation.table.TableExportEngine;
import net.fichotheque.utils.FichothequeUtils;
import net.fichotheque.utils.TableDefUtils;
import net.mapeadores.util.format.FormatConstants;
import net.mapeadores.util.localisation.Lang;
import net.mapeadores.util.localisation.ListLangContext;
import net.mapeadores.util.sql.CreateTableWriter;
import net.mapeadores.util.sql.SqlConstants;
import net.mapeadores.util.text.Label;
import net.mapeadores.util.text.Labels;
import net.mapeadores.util.text.StringUtils;


/**
 *
 * @author Vincent Calame
 */
public class DefaultSqlExport implements SqlExport {

    public final static String CHARSET_PARAMNAME = "charset";
    public final static String ENTITIES_PARAMNAME = "entities";
    public final static String DROPTABLE_PARAMNAME = "drop_table";
    public final static String DELETETABLE_PARAMNAME = "delete_table";
    public final static String CREATETABLE_PARAMNAME = "create_table";
    public final static String CREATEOPTIONS_PARAMNAME = "create_options";
    public final static int CREATE_TABLE = 1;
    public final static int DELETE_TABLE = 1;
    private final static Predicate<ColDef> DEFAULT_PRIMARYPREDICATE = new DefaultPrimaryPredicate();
    private final static Predicate<ColDef> EMPTY_PRIMARYPREDICATE = new EmptyPrimaryPredicate();
    private final Map<String, String> paramMap = new HashMap<String, String>();

    private TableExport tableExport;
    private TableExportContext tableExportContext;
    private ExtractionContext extractionContext;
    private CellConverter cellConverter;
    private Predicate<SubsetItem> predicate;

    public DefaultSqlExport() {
    }

    @Override
    public void exportDump(OutputStream outputStream) throws IOException {
        String charset = paramMap.get(CHARSET_PARAMNAME);
        if (charset == null) {
            charset = "UTF-8";
        }
        try (OutputStreamWriter osw = new OutputStreamWriter(outputStream, charset)) {
            Writer bufWriter = new BufferedWriter(osw);
            if (paramMap.get(ENTITIES_PARAMNAME) != null) {
                bufWriter = new NumericEntityWriter(bufWriter);
            }
            ExportProcess exportProcess = new ExportProcess(bufWriter);
            exportProcess.run();
            bufWriter.flush();
        }
    }

    @Override
    public void setTableExport(TableExport tableExport, TableExportContext tableExportContext) {
        this.tableExport = tableExport;
        this.tableExportContext = tableExportContext;
    }

    @Override
    public void setExtractionContext(ExtractionContext extractionContext) {
        this.extractionContext = extractionContext;
    }

    @Override
    public void setParameter(String name, String value) {
        paramMap.put(name, value);
    }

    @Override
    public void setPredicate(Predicate<SubsetItem> predicate) {
        this.predicate = predicate;
    }

    @Override
    public void setCellConverter(CellConverter cellConverter) {
        this.cellConverter = cellConverter;
    }


    private class ExportProcess {

        private final Writer writer;
        private final ModeOption deleteTableOption;
        private final ModeOption dropTableOption;
        private final CreateTableOption createTableOption;


        private ExportProcess(Writer writer) {
            this.writer = writer;
            this.dropTableOption = getModeOption(DROPTABLE_PARAMNAME);
            this.deleteTableOption = getModeOption(DELETETABLE_PARAMNAME);
            this.createTableOption = getCreateTableOption();
        }

        private void run() throws IOException {
            boolean next = false;
            for (SubsetTable table : tableExport.getSubsetTableList()) {
                if (next) {
                    blank();
                } else {
                    next = true;
                }
                Subset subset = table.getSubset();
                String tableName = table.getParameterValue(TableExportConstants.TABLENAME_PARAMETER);
                if (tableName == null) {
                    tableName = subset.getSubsetKeyString();
                }
                testDrop(tableName);
                if (createTableOption != null) {
                    writeCreateTable(tableName, table.getColDefList(), subset, getPrimaryPredicate(table));
                    endLine();
                    blank();
                }
                testDelete(tableName);
                SqlTableWriter sqlTableWriter = new SqlTableWriter(writer, tableName);
                Collection<SubsetItem> subsetItems = FichothequeUtils.filterAndSort(subset, predicate);
                TableExportEngine.exportSubset(table, sqlTableWriter, cellConverter, subsetItems);
            }
            for (JunctionTable junctionTable : tableExport.getJunctionTableList()) {
                if (next) {
                    blank();
                } else {
                    next = true;
                }
                String tableName = junctionTable.getTableName();
                testDrop(tableName);
                testDelete(tableName);
                JunctionTableExportSqlWriter junctionTableExportSqlWriter = new JunctionTableExportSqlWriter(writer, tableName);
                TableExportEngine.exportJunction(junctionTable, junctionTableExportSqlWriter);
            }

        }

        private Predicate<ColDef> getPrimaryPredicate(SubsetTable table) {
            String primaryKeys = table.getParameterValue(TableExportConstants.PRIMARYKEYS_PARAMETER);
            if (primaryKeys != null) {
                String[] tokens = StringUtils.getTechnicalTokens(primaryKeys, true);
                if (tokens.length == 0) {
                    return EMPTY_PRIMARYPREDICATE;
                } else {
                    return new SetPrimaryPredicate(new HashSet<String>(Arrays.asList(tokens)));
                }
            }
            return DEFAULT_PRIMARYPREDICATE;
        }

        private void testDrop(String tableName) throws IOException {
            if (dropTableOption != null) {
                writer.write("DROP TABLE ");
                if (dropTableOption.getMode() == SqlConstants.MYSQL_MODE) {
                    writer.write("IF EXISTS");
                    writer.write(" ");
                }
                writer.write(tableName);
                endLine();
                blank();
            }
        }

        private void testDelete(String tableName) throws IOException {
            if (deleteTableOption != null) {
                writer.write("DELETE FROM ");
                writer.write(tableName);
                endLine();
                blank();
            }
        }

        public void endLine() throws IOException {
            writer.write(";\n");
        }

        public void blank() throws IOException {
            writer.write("\n");
        }

        private void writeCreateTable(String tableName, List<ColDef> colDefList, Subset subset, Predicate<ColDef> primaryPredicate) throws IOException {
            CreateTableWriter createTableWriter = new CreateTableWriter(writer, createTableOption.getMode());
            createTableWriter.startTable(tableName);
            for (ColDef colDef : colDefList) {
                boolean isPrimary = primaryPredicate.test(colDef);
                String colName = colDef.getColName();
                String title = null;
                if ((createTableOption.isWithTitle()) && (extractionContext.getLangContext() instanceof ListLangContext)) {
                    Labels labels = TableDefUtils.getLabels(colDef, tableExportContext.getSourceLabelProvider(), subset);
                    if (labels != null) {
                        StringBuilder titleBuf = new StringBuilder();
                        for (ListLangContext.Unit unit : (ListLangContext) extractionContext.getLangContext()) {
                            Lang lang = unit.getLang();
                            Label label = labels.getLabel(lang);
                            if (label != null) {
                                if (titleBuf.length() > 0) {
                                    titleBuf.append(" / ");
                                }
                                titleBuf.append(label.getLabelString());
                            }
                        }
                        if (titleBuf.length() > 0) {
                            title = titleBuf.toString();
                        }
                    }
                }
                switch (colDef.getCastType()) {
                    case FormatConstants.INTEGER_CAST:
                        createTableWriter.addIntegerField(colName, isPrimary, title);
                        break;
                    case FormatConstants.DECIMAL_CAST:
                    case FormatConstants.PERCENTAGE_CAST:
                        createTableWriter.addDoubleField(colName, isPrimary, title);
                        break;
                    case FormatConstants.DATE_CAST:
                        createTableWriter.addDateField(colName, isPrimary, title);
                        break;
                    default:
                        createTableWriter.addTextField(colName, isPrimary, title);
                }
            }
            createTableWriter.endTable(paramMap.get(CREATEOPTIONS_PARAMNAME));
        }

    }

    private ModeOption getModeOption(String paramName) {
        String value = paramMap.get(paramName);
        if (value == null) {
            return null;
        }
        if (value.length() == 0) {
            return null;
        }
        short mode = SqlConstants.STANDARD_MODE;
        String[] tokens = StringUtils.getTechnicalTokens(value, false);
        int length = tokens.length;
        if (length > 0) {
            for (int i = 0; i < length; i++) {
                String token = tokens[i];
                token = token.toUpperCase();
                switch (token) {
                    case "2":
                    case "IF EXISTS":
                    case "MYSQL":
                        mode = SqlConstants.MYSQL_MODE;
                        break;
                }
            }
        }
        return new ModeOption(mode);
    }

    private CreateTableOption getCreateTableOption() {
        String value = paramMap.get(CREATETABLE_PARAMNAME);
        if (value == null) {
            return null;
        }
        short mode = SqlConstants.STANDARD_MODE;
        boolean withTitle = false;
        String[] tokens = StringUtils.getTechnicalTokens(value, false);
        int length = tokens.length;
        if (length > 0) {
            for (int i = 0; i < length; i++) {
                String token = tokens[i];
                token = token.toUpperCase();
                switch (token) {
                    case "2":
                    case "IF NOT EXISTS":
                    case "MYSQL":
                        mode = SqlConstants.MYSQL_MODE;
                        break;
                    case "COMMENT":
                    case "WITH_TITLE":
                        withTitle = true;
                        break;
                }
            }
        }
        return new CreateTableOption(mode, withTitle);
    }


    private static class ModeOption {

        private final short mode;

        private ModeOption(short mode) {
            this.mode = mode;
        }

        private short getMode() {
            return mode;
        }

    }


    private static class CreateTableOption {

        private final short mode;
        private final boolean withTitle;

        private CreateTableOption(short mode, boolean withTitle) {
            this.mode = mode;
            this.withTitle = withTitle;
        }

        private short getMode() {
            return mode;
        }

        private boolean isWithTitle() {
            return withTitle;
        }

    }


    private static class NumericEntityWriter extends Writer {

        private final Writer destination;

        private NumericEntityWriter(Writer destination) {
            this.destination = destination;
        }

        @Override
        public void write(char[] cbuf, int off, int len) throws IOException {
            int pos = off;
            len = Math.min(cbuf.length, off + len);
            while (pos < len) {
                int codepoint = Character.codePointAt(cbuf, pos);
                if (codepoint < 128) {
                    destination.write(cbuf[pos]);
                } else {
                    destination.write("&#");
                    destination.write(Integer.toString(codepoint, 10));
                    destination.write(';');
                }
                pos += Character.charCount(codepoint);
            }
        }

        @Override
        public void close() throws IOException {
            destination.close();
        }

        @Override
        public void flush() throws IOException {
            destination.flush();
        }

    }


    private static class DefaultPrimaryPredicate implements Predicate<ColDef> {

        private DefaultPrimaryPredicate() {

        }

        @Override
        public boolean test(ColDef colDef) {
            String colName = colDef.getColName();
            switch (colName) {
                case "id":
                    return true;
                default:
                    return false;
            }
        }

    }


    private static class EmptyPrimaryPredicate implements Predicate<ColDef> {

        private EmptyPrimaryPredicate() {

        }

        @Override
        public boolean test(ColDef colDef) {
            return false;
        }

    }


    private static class SetPrimaryPredicate implements Predicate<ColDef> {

        private final Set<String> set;

        private SetPrimaryPredicate(Set<String> set) {
            this.set = set;
        }

        @Override
        public boolean test(ColDef colDef) {
            return set.contains(colDef.getColName());
        }

    }

}
