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

import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.fichotheque.FichothequeQuestioner;
import net.fichotheque.SubsetKey;
import net.fichotheque.corpus.fiche.AmountItem;
import net.fichotheque.corpus.fiche.FicheBlock;
import net.fichotheque.corpus.fiche.FicheBlocks;
import net.fichotheque.corpus.fiche.FicheItem;
import net.fichotheque.corpus.fiche.FicheItems;
import net.fichotheque.corpus.fiche.GeopointItem;
import net.fichotheque.corpus.fiche.ImageItem;
import net.fichotheque.corpus.fiche.Item;
import net.fichotheque.corpus.fiche.LanguageItem;
import net.fichotheque.corpus.fiche.PersonItem;
import net.fichotheque.corpus.metadata.AmountSubfieldKey;
import net.fichotheque.corpus.metadata.CorpusField;
import net.fichotheque.corpus.metadata.FieldKey;
import net.fichotheque.corpus.metadata.SubfieldKey;
import net.fichotheque.sphere.Redacteur;
import net.fichotheque.sphere.UserLoginException;
import net.fichotheque.tools.corpus.FicheChangeBuilder;
import net.fichotheque.tools.parsers.ficheblock.FicheBlockParser;
import net.fichotheque.utils.FicheUtils;
import net.mapeadores.util.html.HtmlCleaner;
import net.mapeadores.util.localisation.CodeCatalog;
import net.mapeadores.util.localisation.Lang;
import net.mapeadores.util.localisation.UserLangContext;
import net.mapeadores.util.models.PersonCoreUtils;
import net.mapeadores.util.money.Currencies;
import net.mapeadores.util.money.ExtendedCurrency;
import net.mapeadores.util.primitives.Decimal;
import net.mapeadores.util.primitives.DegreDecimal;
import net.mapeadores.util.text.CleanedString;
import net.mapeadores.util.text.StringUtils;
import net.mapeadores.util.text.TypoOptions;


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

    private final Parameters parameters;
    private final FicheBlockParser ficheBlockParser;
    private final TypoOptions typoOptions;
    private final TextContentParser textContentParser;
    private final SubsetKey defaultSphereKey;

    public FicheParser(Parameters parameters) {
        this.parameters = parameters;
        Redacteur userRedacteur = parameters.userRedacteur();
        if (userRedacteur != null) {
            this.defaultSphereKey = userRedacteur.getSubsetKey();
        } else {
            this.defaultSphereKey = null;
        }
        this.typoOptions = parameters.typoOptions;
        textContentParser = new TextContentParser(typoOptions);
        ficheBlockParser = new FicheBlockParser(parameters.htmlCleaner(), typoOptions, false);
    }

    public Lang getWorkingLang() {
        return parameters.userLangContext().getWorkingLang();
    }

    public Buffer getBuffer(FicheChangeBuilder ficheChangeBuilder) {
        return new Buffer(ficheChangeBuilder);
    }


    public class Buffer {

        private final Map<FieldKey, SubfieldBuffer> subfieldBufferMap = new HashMap<FieldKey, SubfieldBuffer>();
        private final FicheChangeBuilder ficheChangeBuilder;

        private Buffer(FicheChangeBuilder ficheChangeBuilder) {
            this.ficheChangeBuilder = ficheChangeBuilder;
        }

        public void flushParsedSubfields() {
            for (SubfieldBuffer subfieldBuffer : subfieldBufferMap.values()) {
                subfieldBuffer.flush();
            }
        }

        public void parseCorpusField(CorpusField corpusField, String value) {
            switch (corpusField.getCategory()) {
                case FieldKey.PROP_CATEGORY:
                    parseProp(corpusField, cleanPropToken(value, parameters.itemListSeparator()));
                    break;
                case FieldKey.INFO_CATEGORY:
                    String[] tokens = StringUtils.getTechnicalTokens(value, parameters.itemListSeparator(), false);
                    parseInfo(corpusField, tokens);
                    break;
                case FieldKey.SECTION_CATEGORY:
                    parseSection(corpusField, value);
                    break;
                case FieldKey.SPECIAL_CATEGORY:
                    switch (corpusField.getFieldString()) {
                        case FieldKey.SPECIAL_TITLE:
                            parseTitle(value);
                            break;
                        case FieldKey.SPECIAL_SUBTITLE:
                            parseSubtitle(value);
                            break;
                        case FieldKey.SPECIAL_LANG:
                            parseFicheLang(value);
                            break;
                        case FieldKey.SPECIAL_OWNERS: {
                            String[] ownerTokens = StringUtils.getTechnicalTokens(value, parameters.itemListSeparator(), false);
                            parseOwners(ownerTokens);
                            break;
                        }
                    }
                    break;
            }
        }

        public void parseSubfield(CorpusField corpusField, SubfieldKey subfieldKey, String value) {
            if (SubfieldKey.isLegalSubfield(corpusField, subfieldKey.getSubtype())) {
                SubfieldBuffer fieldBuffer = subfieldBufferMap.get(corpusField.getFieldKey());
                if (fieldBuffer == null) {
                    fieldBuffer = new SubfieldBuffer(corpusField);
                    subfieldBufferMap.put(corpusField.getFieldKey(), fieldBuffer);
                }
                fieldBuffer.putSubfield(subfieldKey, value);
            }
        }

        private void parseFicheLang(String s) {
            if (s == null) {
                return;
            }
            try {
                LanguageItem languageItem = FicheItemParser.parseLanguage(s, parameters.codeCatalog(), parameters.userLangContext().getLangPreference());
                ficheChangeBuilder.setLang(languageItem.getLang());
            } catch (ParseException pe) {
                ficheChangeBuilder.setLang(null);
            }
        }

        private void parseTitle(String s) {
            ficheChangeBuilder.setTitle(CleanedString.newInstance(TypoParser.parseTypo(s, typoOptions)));
        }

        private void parseSubtitle(String s) {
            CleanedString cs = CleanedString.newInstance(s);
            if (cs != null) {
                ficheChangeBuilder.setSubtitle(FicheItemParser.parsePara(cs.toString(), textContentParser));
            } else {
                ficheChangeBuilder.setSubtitle(null);
            }
        }

        private void parseOwners(String[] tokens) {
            FicheItems ficheItems = FicheItemParser.parsePersonList(tokens, parameters.fichothequeQuestioner(), defaultSphereKey, typoOptions);
            ficheChangeBuilder.appendOwners(ficheItems);
        }

        private void parseProp(CorpusField propField, String s) {
            s = StringUtils.cleanString(s);
            FicheItem ficheItem = null;
            if (s.length() > 0) {
                ficheItem = parseFicheItem(s, propField);
            }
            ficheChangeBuilder.setProp(propField.getFieldKey(), ficheItem);
        }

        private void parsePersonProp(CorpusField propField, String surname, String forename, String nonlatin, String surnameFirstString) {
            surname = StringUtils.cleanString(surname);
            forename = StringUtils.cleanString(forename);
            nonlatin = StringUtils.cleanString(nonlatin);
            boolean surnameFirst = StringUtils.isTrue(surnameFirstString);
            FicheItem redacteurItem = parsePerson(surname, forename, nonlatin, surnameFirst);
            ficheChangeBuilder.setProp(propField.getFieldKey(), redacteurItem);
        }

        private void parseAmountProp(CorpusField propField, String value, String cur) {
            FicheItem ficheItem = null;
            boolean itemAllowed = true;
            value = StringUtils.cleanString(value);
            if (value.length() > 0) {
                try {
                    ficheItem = FicheItemParser.parseAmount(value);
                } catch (ParseException pe1) {
                    cur = StringUtils.cleanString(cur);
                    if (cur.length() == 0) {
                        Currencies currencies = propField.getCurrencies();
                        cur = currencies.get(0).getCurrencyCode();
                    }
                    try {
                        ficheItem = FicheItemParser.parseAmount(value, cur);
                    } catch (ParseException pe2) {
                        if (itemAllowed) {
                            if (!value.startsWith("?")) {
                                value = "?" + value;
                            }
                        }
                        ficheItem = parseItem(value, itemAllowed);
                    }
                }
            }
            ficheChangeBuilder.setProp(propField.getFieldKey(), ficheItem);
        }

        private void parseImageProp(CorpusField propField, String src, String alt, String title) {
            ImageItem image = null;
            src = StringUtils.cleanString(src);
            if (src.length() > 0) {
                alt = StringUtils.cleanString(alt);
                title = StringUtils.cleanString(title);
                image = new ImageItem(src, alt, title);
            }
            ficheChangeBuilder.setProp(propField.getFieldKey(), image);
        }

        private void parseGeopointProp(CorpusField propField, String latitude, String longitude) {
            latitude = StringUtils.cleanString(latitude);
            longitude = StringUtils.cleanString(longitude);
            boolean itemAllowed = true;
            FicheItem ficheItem = null;
            try {
                DegreDecimal latitudeDecimal = FicheItemParser.parseDegreDecimal(latitude);
                DegreDecimal longitudeDecimal = FicheItemParser.parseDegreDecimal(longitude);
                ficheItem = new GeopointItem(latitudeDecimal, longitudeDecimal);
            } catch (ParseException pe) {
                if ((latitude.length() > 0) || (longitude.length() > 0)) {
                    if (itemAllowed) {
                        if (!latitude.startsWith("?? ")) {
                            latitude = "?? " + latitude;
                        }
                    }
                    ficheItem = parseItem(latitude + " ?? " + longitude, itemAllowed);
                }
            }
            ficheChangeBuilder.setProp(propField.getFieldKey(), ficheItem);
        }

        private void parseInfo(CorpusField infoField, String[] tokens) {
            List<FicheItem> ficheItemCollection = new ArrayList<FicheItem>();
            for (String token : tokens) {
                FicheItem ficheItem = parseFicheItem(token, infoField);
                if (ficheItem != null) {
                    ficheItemCollection.add(ficheItem);
                }
            }
            addInInfo(infoField, FicheUtils.toFicheItems(ficheItemCollection));
        }

        private void addInInfo(CorpusField infoField, FicheItems ficheItems) {
            ficheChangeBuilder.appendInfo(infoField.getFieldKey(), ficheItems);
        }

        private void parseSection(CorpusField sectionField, String s) {
            List<FicheBlock> list = new ArrayList<FicheBlock>();
            ficheBlockParser.parseFicheBlockList(s, list);
            FicheBlocks ficheBlocks = FicheUtils.toFicheBlocks(list);
            ficheChangeBuilder.appendSection(sectionField.getFieldKey(), ficheBlocks);
        }

        private FicheItem parseFicheItem(String token, CorpusField corpusField) {
            boolean itemAllowed = true;
            switch (corpusField.getFicheItemType()) {
                case CorpusField.LANGUAGE_FIELD: {
                    try {
                        return FicheItemParser.parseLanguage(token, parameters.codeCatalog(), parameters.userLangContext().getLangPreference());
                    } catch (ParseException pe) {
                        return parseItem(token, itemAllowed);
                    }
                }
                case CorpusField.COUNTRY_FIELD: {
                    try {
                        return FicheItemParser.parseCountry(token, parameters.codeCatalog(), parameters.userLangContext().getLangPreference());
                    } catch (ParseException pe) {
                        return parseItem(token, itemAllowed);
                    }
                }
                case CorpusField.DATE_FIELD: {
                    try {
                        return FicheItemParser.parseDate(token, parameters.userLangContext().getFormatLocale());
                    } catch (ParseException pe) {
                        return parseItem(token, itemAllowed);
                    }
                }
                case CorpusField.LINK_FIELD: {
                    try {
                        return FicheItemParser.parseLink(token, typoOptions);
                    } catch (ParseException pe) {
                        return parseItem(token, itemAllowed);
                    }
                }
                case CorpusField.NUMBER_FIELD: {
                    try {
                        return FicheItemParser.parseNumber(token);
                    } catch (ParseException pe) {
                        return parseItem(checkToken(token), itemAllowed);
                    }
                }
                case CorpusField.AMOUNT_FIELD: {
                    try {
                        return FicheItemParser.parseAmount(token);
                    } catch (ParseException pe) {
                        return parseItem(checkToken(token), itemAllowed);
                    }
                }
                case CorpusField.EMAIL_FIELD: {
                    try {
                        return FicheItemParser.parseEmail(token);
                    } catch (ParseException pe) {
                        return parseItem(token, itemAllowed);
                    }
                }
                case CorpusField.PERSON_FIELD: {
                    return FicheItemParser.parsePerson(token, parameters.fichothequeQuestioner(), getDefaultSphereKey(corpusField), typoOptions);
                }
                case CorpusField.ITEM_FIELD: {
                    return FicheItemParser.parseItem(token, typoOptions);
                }
                case CorpusField.GEOPOINT_FIELD: {
                    try {
                        return FicheItemParser.parseGeopoint(token);
                    } catch (ParseException pe) {
                        return parseItem(token, itemAllowed);
                    }
                }
                case CorpusField.PARA_FIELD: {
                    return FicheItemParser.parsePara(token, textContentParser);
                }
                case CorpusField.IMAGE_FIELD: {
                    return FicheItemParser.parseImage(token, typoOptions);
                }
                default:
                    return null;
            }
        }


        private FicheItem parsePerson(String surname, String forename, String nonlatin, boolean surnameFirst) {
            if ((forename.length() == 0) && (nonlatin.length() == 0)) {
                try {
                    String userGlobalId = parameters.fichothequeQuestioner().getUserGlobalId(surname);
                    return new PersonItem(userGlobalId);
                } catch (UserLoginException e) {
                }
                if (surname.length() == 0) {
                    return null;
                }
                if (!surnameFirst) {
                    return new Item(TypoParser.parseTypo(surname, typoOptions));
                }
            }
            surname = TypoParser.parseTypo(surname, typoOptions);
            forename = TypoParser.parseTypo(forename, typoOptions);
            nonlatin = TypoParser.parseTypo(nonlatin, typoOptions);
            return new PersonItem(PersonCoreUtils.toPersonCore(surname, forename, nonlatin, surnameFirst), null);
        }

        private FicheItem parseAmountInfoSubfield(CorpusField corpusField, ExtendedCurrency currency, String value) {
            value = StringUtils.cleanString(value);
            boolean itemAllowed = true;
            if (value.isEmpty()) {
                return null;
            }
            try {
                Decimal decimal = StringUtils.parseDecimal(value);
                return new AmountItem(decimal, currency);
            } catch (NumberFormatException nfe) {
                if (itemAllowed) {
                    if (!value.startsWith("?")) {
                        value = "?" + value;
                    }
                }
                return parseItem(value, itemAllowed);
            }
        }


        private class SubfieldBuffer {

            private final CorpusField corpusField;
            private final Map<SubfieldKey, String> subfieldMap = new LinkedHashMap<SubfieldKey, String>();

            private SubfieldBuffer(CorpusField corpusField) {
                this.corpusField = corpusField;
            }

            private void putSubfield(SubfieldKey subfieldKey, String value) {
                subfieldMap.put(subfieldKey, value);
            }

            private void flush() {
                switch (corpusField.getFicheItemType()) {
                    case CorpusField.AMOUNT_FIELD:
                        if (corpusField.isProp()) {
                            flushAmountProp();
                        } else {
                            flushAmountInfo();
                        }
                        break;
                    case CorpusField.PERSON_FIELD:
                        flushPerson();
                        break;
                    case CorpusField.GEOPOINT_FIELD:
                        flushGeopoint();
                        break;
                    case CorpusField.IMAGE_FIELD:
                        flushImage();
                        break;
                }
            }

            private void flushAmountProp() {
                String num = "";
                String cur = "";
                for (Map.Entry<SubfieldKey, String> entry : subfieldMap.entrySet()) {
                    String value = entry.getValue();
                    switch (entry.getKey().getSubtype()) {
                        case SubfieldKey.CURRENCY_SUBTYPE:
                            cur = value;
                            break;
                        case SubfieldKey.VALUE_SUBTYPE:
                            num = value;
                            break;
                    }
                }
                parseAmountProp(corpusField, num, cur);
            }

            private void flushAmountInfo() {
                Set<ExtendedCurrency> currencySet = new HashSet<ExtendedCurrency>();
                List<FicheItem> ficheItemCollection = new ArrayList<FicheItem>();
                String othersValue = null;
                for (Map.Entry<SubfieldKey, String> entry : subfieldMap.entrySet()) {
                    String value = entry.getValue();
                    SubfieldKey subfieldKey = entry.getKey();
                    switch (entry.getKey().getSubtype()) {
                        case SubfieldKey.AMOUNT_SUBTYPE:
                            FicheItem ficheItem = parseAmountInfoSubfield(corpusField, ((AmountSubfieldKey) subfieldKey).getCurrency(), value);
                            if (ficheItem != null) {
                                if (ficheItem instanceof AmountItem) {
                                    AmountItem amountItem = (AmountItem) ficheItem;
                                    if (!currencySet.contains(amountItem.getCurrency())) {
                                        ficheItemCollection.add(amountItem);
                                        currencySet.add(amountItem.getCurrency());
                                    }
                                } else {
                                    ficheItemCollection.add(ficheItem);
                                }
                            }
                            break;
                        case SubfieldKey.OTHERS_SUBTYPE:
                            othersValue = value;
                            break;

                    }
                }
                if (othersValue != null) {
                    String[] tokens = StringUtils.getTechnicalTokens(othersValue, parameters.itemListSeparator(), false);
                    int length = tokens.length;
                    for (int i = 0; i < length; i++) {
                        FicheItem ficheItem = parseFicheItem(tokens[i], corpusField);
                        if (ficheItem != null) {
                            if (ficheItem instanceof AmountItem) {
                                AmountItem amountItem = (AmountItem) ficheItem;
                                if (!currencySet.contains(amountItem.getCurrency())) {
                                    ficheItemCollection.add(amountItem);
                                    currencySet.add(amountItem.getCurrency());
                                }
                            } else {
                                ficheItemCollection.add(ficheItem);
                            }
                        }
                    }
                }
                addInInfo(corpusField, FicheUtils.toFicheItems(ficheItemCollection));
            }

            private void flushGeopoint() {
                String latitude = "";
                String longitude = "";
                for (Map.Entry<SubfieldKey, String> entry : subfieldMap.entrySet()) {
                    String value = entry.getValue();
                    switch (entry.getKey().getSubtype()) {
                        case SubfieldKey.LATITUDE_SUBTYPE:
                            latitude = value;
                            break;
                        case SubfieldKey.LONGITUDE_SUBTYPE:
                            longitude = value;
                            break;
                    }
                }
                parseGeopointProp(corpusField, latitude, longitude);
            }

            private void flushPerson() {
                String surname = "";
                String forename = "";
                String nonlatin = "";
                String surnameFirstString = "";
                for (Map.Entry<SubfieldKey, String> entry : subfieldMap.entrySet()) {
                    String value = entry.getValue();
                    switch (entry.getKey().getSubtype()) {
                        case SubfieldKey.SURNAME_SUBTYPE:
                            surname = value;
                            break;
                        case SubfieldKey.FORENAME_SUBTYPE:
                            forename = value;
                            break;
                        case SubfieldKey.NONLATIN_SUBTYPE:
                            nonlatin = value;
                            break;
                        case SubfieldKey.SURNAMEFIRST_SUBTYPE:
                            surnameFirstString = value;
                            break;
                    }
                }
                parsePersonProp(corpusField, surname, forename, nonlatin, surnameFirstString);
            }

            private void flushImage() {
                String src = "";
                String alt = "";
                String title = "";
                for (Map.Entry<SubfieldKey, String> entry : subfieldMap.entrySet()) {
                    String value = entry.getValue();
                    switch (entry.getKey().getSubtype()) {
                        case SubfieldKey.SRC_SUBTYPE:
                            src = value;
                            break;
                        case SubfieldKey.ALT_SUBTYPE:
                            alt = value;
                            break;
                        case SubfieldKey.TITLE_SUBTYPE:
                            title = value;
                            break;
                    }
                }
                parseImageProp(corpusField, src, alt, title);
            }

        }

    }

    private Item parseItem(String token, boolean itemAllowed) {
        if (itemAllowed) {
            return FicheItemParser.parseItem(token, typoOptions);
        } else {
            return null;
        }
    }

    private SubsetKey getDefaultSphereKey(CorpusField corpusField) {
        SubsetKey customSphereKey = corpusField.getDefaultSphereKey();
        if (customSphereKey == null) {
            customSphereKey = defaultSphereKey;
        }
        return customSphereKey;
    }

    private static String cleanPropToken(String s, char itemListSeparateur) {
        int length = s.length();
        int p = length;
        for (int i = (length - 1); i >= 0; i--) {
            char carac = s.charAt(i);
            if ((carac == itemListSeparateur) || (Character.isWhitespace(carac))) {
                p--;
            } else {
                break;
            }
        }
        if (p == 0) {
            return "";
        } else if (p == length) {
            return s;
        } else {
            return s.substring(0, p);
        }
    }

    private static String checkToken(String token) {
        if (!token.startsWith("?")) {
            token = "?" + token;
        }
        return token;
    }

    public static FicheParser.Parameters initParameters(FichothequeQuestioner fichothequeQuestioner, UserLangContext userLangContext, CodeCatalog codeCatalog, HtmlCleaner htmlCleaner, TypoOptions typoOptions) {
        return new FicheParser.Parameters(fichothequeQuestioner, userLangContext, codeCatalog, htmlCleaner, typoOptions);
    }


    public static class Parameters {

        private final FichothequeQuestioner fichothequeQuestioner;
        private final UserLangContext userLangContext;
        private final CodeCatalog codeCatalog;
        private final HtmlCleaner htmlCleaner;
        private final TypoOptions typoOptions;
        private char itemListSeparator = ';';
        private Redacteur userRedacteur = null;

        private Parameters(FichothequeQuestioner fichothequeQuestioner, UserLangContext userLangContext, CodeCatalog codeCatalog, HtmlCleaner htmlCleaner, TypoOptions typoOptions) {
            this.fichothequeQuestioner = fichothequeQuestioner;
            this.userLangContext = userLangContext;
            this.codeCatalog = codeCatalog;
            this.htmlCleaner = htmlCleaner;
            this.typoOptions = typoOptions;
        }

        public FichothequeQuestioner fichothequeQuestioner() {
            return fichothequeQuestioner;
        }

        public UserLangContext userLangContext() {
            return userLangContext;
        }

        public CodeCatalog codeCatalog() {
            return codeCatalog;
        }

        public HtmlCleaner htmlCleaner() {
            return htmlCleaner;
        }

        public TypoOptions typoOptions() {
            return typoOptions;
        }

        public char itemListSeparator() {
            return itemListSeparator;
        }

        public Redacteur userRedacteur() {
            return userRedacteur;
        }

        public Parameters itemListSeparator(char itemListSeparator) {
            this.itemListSeparator = itemListSeparator;
            return this;
        }

        public Parameters userRedacteur(Redacteur userRedacteur) {
            this.userRedacteur = userRedacteur;
            return this;
        }

    }

}
