/* UtilLib - Copyright (c) 2014-2023 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.mapeadores.util.localisation;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.text.ParseException;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import net.mapeadores.util.exceptions.InternalResourceException;
import net.mapeadores.util.ini.IniParser;
import net.mapeadores.util.text.StringUtils;


/**
 * À compléter pour être conforme à la RFC 5646
 * http://www.rfc-editor.org/rfc/rfc5646.txt Gère les écritures, les pays (pas
 * les régions ONU) et le variantes. Reste à gérer les extensions et les usages
 * privés. Vérifie la conformité mais pas l'existence (Voir le registre de
 * l'IANA :
 * http://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
 *
 * @author Vincent Calame
 */
public final class Lang implements Serializable, Comparable<Lang> {

    private static final long serialVersionUID = 1L;
    private final static Map<String, Lang> langMap = new HashMap<String, Lang>();
    private final static Map<String, String> conversionMap = new HashMap<String, String>();

    static {
        try (InputStream langConversionStream = Country.class.getResourceAsStream("conversion_lang.ini")) {
            IniParser.parseIni(langConversionStream, conversionMap);
        } catch (IOException ioe) {
            throw new InternalResourceException(ioe);
        }
    }

    private final String code;
    private final Locale locale;
    private final Lang rootLang;
    private final Country region;
    private final LangScript script;
    private final String[] variantArray;

    private Lang(String rootCode) {
        this.code = rootCode;
        this.rootLang = null;
        this.locale = new Locale(rootCode);
        this.region = null;
        this.script = null;
        this.variantArray = null;
    }

    private Lang(String code, Lang rootLang, Options options) {
        this.code = code;
        this.rootLang = rootLang;
        this.region = options.region;
        this.script = options.script;
        if (region != null) {
            this.locale = new Locale(rootLang.toString(), region.toString());
        } else {
            this.locale = new Locale(rootLang.toString());
        }
        this.variantArray = options.getVariantArray();
    }

    public Country getRegion() {
        return region;
    }

    public LangScript getScript() {
        return script;
    }

    public int getVariantCount() {
        if (variantArray == null) {
            return 0;
        } else {
            return variantArray.length;
        }
    }

    public String getVariant(int index) {
        if (variantArray == null) {
            throw new IndexOutOfBoundsException();
        } else {
            return variantArray[index];
        }
    }

    @Override
    public int hashCode() {
        return code.hashCode();
    }

    @Override
    public boolean equals(Object other) {
        if (other == null) {
            return false;
        }
        if (other == this) {
            return true;
        }
        if (other.getClass() != this.getClass()) {
            return false;
        }
        Lang otherLang = (Lang) other;
        return otherLang.code.equals(this.code);
    }

    @Override
    public String toString() {
        return code;
    }

    @Override
    public int compareTo(Lang otherLang) {
        return this.code.compareTo(otherLang.code);
    }

    public Locale toLocale() {
        return locale;
    }

    public boolean isRootLang() {
        return (rootLang == null);
    }

    public Lang getRootLang() {
        if (rootLang == null) {
            return this;
        } else {
            return rootLang;
        }
    }

    public boolean isRTLScript() {
        if (script != null) {
            return script.isRTLScript();
        } else if (rootLang != null) {
            return rootLang.isRTLScript();
        } else {
            if (code.equals("ar")) {
                return true;
            } else if (code.equals("he")) {
                return true;
            } else {
                return false;
            }
        }
    }

    public static Lang build(String langCode) {
        try {
            return parse(langCode);
        } catch (ParseException pe) {
            throw new IllegalArgumentException(pe.getMessage());
        }
    }

    public static Lang parse(String langCode) throws ParseException {
        if (langCode == null) {
            throw new ParseException("null", 0);
        }
        Lang lang = langMap.get(langCode);
        if (lang != null) {
            return lang;
        }
        synchronized (langMap) {
            langCode = langCode.toLowerCase();
            lang = langMap.get(langCode);
            if (lang != null) {
                return lang;
            }
            int length = langCode.length();
            if (length == 0) {
                throw new ParseException("empty string", 0);
            } else if (length == 1) {
                throw new ParseException("too short string", 0);
            } else if ((length == 2) || (length == 3)) {
                lang = parseRootLang(langCode);
            } else {
                int idx = langCode.indexOf('-');
                if (idx == -1) {
                    throw new ParseException("too long String", langCode.length() - 1);
                } else if ((idx != 2) && (idx != 3)) {
                    throw new ParseException("bad - position", idx);
                }
                String rootLangString = langCode.substring(0, idx).toLowerCase();
                Lang rootLang = langMap.get(rootLangString);
                if (rootLang == null) {
                    rootLang = parseRootLang(rootLangString);
                }
                Options options = parseOptions(langCode.substring(idx + 1));
                langCode = rootLang.toString() + options.toString();
                lang = new Lang(langCode, rootLang, options);
                langMap.put(langCode, lang);
            }
        }
        return lang;
    }

    public static Lang getDefault() {
        try {
            return fromLocale(Locale.getDefault());
        } catch (ParseException pe) {
            return Lang.build("en");
        }
    }

    public static Lang fromLocale(Locale locale) throws ParseException {
        String language = locale.getLanguage();
        if (language.length() == 0) {
            language = "und";
        }
        String country = locale.getCountry();
        if (country.length() > 0) {
            language = language + "-" + country;
        }
        return parse(language);
    }

    public static String toISOString(Locale locale) {
        try {
            return Lang.fromLocale(locale).toString();
        } catch (ParseException pe) {
            return locale.toString().replace('_', '-');
        }
    }

    private static Lang parseRootLang(String langCode) throws ParseException {
        if (!testChar(langCode.charAt(0))) {
            throw new ParseException("wrong char: " + langCode.charAt(0), 0);
        }
        if (!testChar(langCode.charAt(1))) {
            throw new ParseException("wrong char: " + langCode.charAt(1), 1);
        }
        if (langCode.length() == 3) {
            if (!testChar(langCode.charAt(2))) {
                throw new ParseException("wrong char: " + langCode.charAt(2), 2);
            }
        }
        String conversion = conversionMap.get(langCode);
        Lang lang;
        if (conversion != null) {
            lang = langMap.get(conversion);
            if (lang == null) {
                lang = new Lang(conversion);
                langMap.put(conversion, lang);
            }
        } else {
            lang = new Lang(langCode);
        }
        langMap.put(langCode, lang);
        return lang;
    }

    private static boolean testChar(char carac) {
        return ((carac >= 'a') && (carac <= 'z'));
    }

    private static boolean testVariantChar(char carac) {
        if (((carac >= 'a') && (carac <= 'z'))) {
            return true;
        } else if (((carac >= '0') && (carac <= '9'))) {
            return true;
        } else {
            return false;
        }
    }

    private static Options parseOptions(String optionsString) throws ParseException {
        String[] tokens = StringUtils.getTokens(optionsString, '-', StringUtils.NOTCLEAN);
        int length = tokens.length;
        if (length == 0) {
            throw new ParseException("empty options ", 0);
        }
        Options options = new Options();
        options.parseFirstToken(tokens[0]);
        if (length > 1) {
            options.parseFirstToken(tokens[1]);
        }
        if (length > 2) {
            for (int i = 2; i < length; i++) {
                options.parseSupplementaryToken(tokens[i]);
            }
        }
        return options;
    }

    private static void checkVariant(String variant) throws ParseException {
        int length = variant.length();
        if (length < 4) {
            throw new ParseException("Too short variant: " + variant, 0);
        }
        if (length > 8) {
            throw new ParseException("Too long variant: " + variant, 0);
        }
        if (length == 4) {
            char carac = variant.charAt(0);
            if ((carac < '0') || (carac > '9')) {
                throw new ParseException("Variant of length = 4 must start with a digit: " + variant, 0);
            }
        }
        for (int i = 0; i < length; i++) {
            char carac = variant.charAt(i);
            if (!testVariantChar(carac)) {
                throw new ParseException("Invalid character (" + carac + ") in variant: " + variant, i);
            }
        }
    }


    private static class Options {

        private Country region;
        private LangScript script;
        private final Set<String> variantSet = new LinkedHashSet<String>();

        private Options() {

        }

        private void parseFirstToken(String token) throws ParseException {
            int length = token.length();
            if (length == 0) {
                throw new ParseException("empty token", 0);
            } else if (length > 8) {
                throw new ParseException("too long token: " + token, 0);
            } else if (length == 1) {
                throw new ParseException("too short token: " + token, 0);
            } else if (length == 3) {
                throw new ParseException("too short token: " + token, 0);
            } else if (length == 4) {
                try {
                    script = LangScript.parse(token);
                } catch (ParseException pe) {
                    checkVariant(token);
                    variantSet.add(token);
                }
            } else if (length == 2) {
                try {
                    region = Country.parse(token);
                } catch (ParseException pe) {
                    throw new ParseException("wrong region token: " + token, 0);
                }
            } else {
                checkVariant(token);
                variantSet.add(token);
            }
        }

        private void parseSupplementaryToken(String token) throws ParseException {
            checkVariant(token);
            if (variantSet.contains(token)) {
                throw new ParseException("variant defined twice: " + token, 0);
            }
            variantSet.add(token);
        }

        private String[] getVariantArray() {
            if (variantSet.isEmpty()) {
                return null;
            } else {
                return variantSet.toArray(new String[variantSet.size()]);
            }
        }

        @Override
        public String toString() {
            StringBuilder buf = new StringBuilder();
            if (script != null) {
                buf.append('-');
                buf.append(script.toString());
            }
            if (region != null) {
                buf.append('-');
                buf.append(region.toString());
            }
            for (String variant : variantSet) {
                buf.append('-');
                buf.append(variant);
            }
            return buf.toString();
        }


    }


}
