/*
 ****************************************************************************************
 * Copyright (C) 2009-2015, Google, Inc.; International Business Machines Corporation   *
 * and others. All Rights Reserved.                                                     *
 ****************************************************************************************
 */
package com.ibm.icu.util;

import com.ibm.icu.impl.ICUData;
import com.ibm.icu.impl.ICUResourceBundle;
import com.ibm.icu.impl.Relation;
import com.ibm.icu.impl.Row;
import com.ibm.icu.impl.Row.R3;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Provides a way to match the languages (locales) supported by a product to the languages (locales)
 * acceptable to a user, and get the best match. For example:
 *
 * <pre>
 * LocaleMatcher matcher = new LocaleMatcher("fr, en-GB, en");
 *
 * // afterwards:
 * matcher.getBestMatch("en-US").toLanguageTag() => "en"
 * </pre>
 *
 * It takes into account when languages are close to one another, such as fil and tl, and when
 * language regional variants are close, like en-GB and en-AU. It also handles scripts, like zh-Hant
 * vs zh-TW. For examples, see the test file.
 *
 * <p>All classes implementing this interface should be immutable. Often a product will just need
 * one static instance, built with the languages that it supports. However, it may want multiple
 * instances with different default languages based on additional information, such as the domain.
 *
 * @author markdavis@google.com
 * @stable ICU 4.4
 */
public class LocaleMatcher {

    public static final boolean DEBUG = false;

    private static final ULocale UNKNOWN_LOCALE = new ULocale("und");

    /**
     * Threshold for falling back to the default (first) language. May make this a parameter in the
     * future.
     */
    private static final double DEFAULT_THRESHOLD = 0.5;

    /** The default language, in case the threshold is not met. */
    private final ULocale defaultLanguage;

    /** The default language, in case the threshold is not met. */
    private final double threshold;

    /**
     * Create a new language matcher. The highest-weighted language is the default. That means that
     * if no other language is matches closer than a given threshold, that default language is
     * chosen. Typically the default is English, but it could be different based on additional
     * information, such as the domain of the page.
     *
     * @param languagePriorityList weighted list
     * @stable ICU 4.4
     */
    public LocaleMatcher(LocalePriorityList languagePriorityList) {
        this(languagePriorityList, defaultWritten);
    }

    /**
     * Create a new language matcher from a String form. The highest-weighted language is the
     * default.
     *
     * @param languagePriorityListString String form of LanguagePriorityList
     * @stable ICU 4.4
     */
    public LocaleMatcher(String languagePriorityListString) {
        this(LocalePriorityList.add(languagePriorityListString).build());
    }

    /**
     * Internal testing function; may expose API later.
     *
     * @param languagePriorityList LocalePriorityList to match
     * @param matcherData Internal matching data
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public LocaleMatcher(LocalePriorityList languagePriorityList, LanguageMatcherData matcherData) {
        this(languagePriorityList, matcherData, DEFAULT_THRESHOLD);
    }

    /**
     * Internal testing function; may expose API later.
     *
     * @param languagePriorityList LocalePriorityList to match
     * @param matcherData Internal matching data
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public LocaleMatcher(
            LocalePriorityList languagePriorityList,
            LanguageMatcherData matcherData,
            double threshold) {
        this.matcherData = matcherData == null ? defaultWritten : matcherData.freeze();
        for (final ULocale language : languagePriorityList) {
            add(language, languagePriorityList.getWeight(language));
        }
        processMapping();
        Iterator<ULocale> it = languagePriorityList.iterator();
        defaultLanguage = it.hasNext() ? it.next() : null;
        this.threshold = threshold;
    }

    /**
     * Returns a fraction between 0 and 1, where 1 means that the languages are a perfect match, and
     * 0 means that they are completely different. Note that the precise values may change over
     * time; no code should be made dependent on the values remaining constant.
     *
     * @param desired Desired locale
     * @param desiredMax Maximized locale (using likely subtags)
     * @param supported Supported locale
     * @param supportedMax Maximized locale (using likely subtags)
     * @return value between 0 and 1, inclusive.
     * @stable ICU 4.4
     */
    public double match(
            ULocale desired, ULocale desiredMax, ULocale supported, ULocale supportedMax) {
        return matcherData.match(desired, desiredMax, supported, supportedMax);
    }

    /**
     * Canonicalize a locale (language). Note that for now, it is canonicalizing according to CLDR
     * conventions (he vs iw, etc), since that is what is needed for likelySubtags.
     *
     * @param ulocale language/locale code
     * @return ULocale with remapped subtags.
     * @stable ICU 4.4
     */
    public ULocale canonicalize(ULocale ulocale) {
        // TODO Get the data from CLDR, use Java conventions.
        String lang = ulocale.getLanguage();
        String lang2 = canonicalMap.get(lang);
        String script = ulocale.getScript();
        String script2 = canonicalMap.get(script);
        String region = ulocale.getCountry();
        String region2 = canonicalMap.get(region);
        if (lang2 != null || script2 != null || region2 != null) {
            return new ULocale(
                    lang2 == null ? lang : lang2,
                    script2 == null ? script : script2,
                    region2 == null ? region : region2);
        }
        return ulocale;
    }

    /**
     * Get the best match for a LanguagePriorityList
     *
     * @param languageList list to match
     * @return best matching language code
     * @stable ICU 4.4
     */
    public ULocale getBestMatch(LocalePriorityList languageList) {
        double bestWeight = 0;
        ULocale bestTableMatch = null;
        double penalty = 0;
        OutputDouble matchWeight = new OutputDouble();
        for (final ULocale language : languageList) {
            final ULocale matchLocale = getBestMatchInternal(language, matchWeight);
            final double weight = matchWeight.value * languageList.getWeight(language) - penalty;
            if (weight > bestWeight) {
                bestWeight = weight;
                bestTableMatch = matchLocale;
            }
            penalty += 0.07000001;
        }
        if (bestWeight < threshold) {
            bestTableMatch = defaultLanguage;
        }
        return bestTableMatch;
    }

    /**
     * Convenience method: Get the best match for a LanguagePriorityList
     *
     * @param languageList String form of language priority list
     * @return best matching language code
     * @stable ICU 4.4
     */
    public ULocale getBestMatch(String languageList) {
        return getBestMatch(LocalePriorityList.add(languageList).build());
    }

    /**
     * Get the best match for an individual language code.
     *
     * @param ulocale locale/language code to match
     * @return best matching language code
     * @stable ICU 4.4
     */
    public ULocale getBestMatch(ULocale ulocale) {
        return getBestMatchInternal(ulocale, null);
    }

    /**
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public ULocale getBestMatch(ULocale... ulocales) {
        return getBestMatch(LocalePriorityList.add(ulocales).build());
    }

    /**
     * {@inheritDoc}
     *
     * @stable ICU 4.4
     */
    @Override
    public String toString() {
        return "{" + defaultLanguage + ", " + localeToMaxLocaleAndWeight + "}";
    }

    // ================= Privates =====================

    /**
     * Get the best match for an individual language code.
     *
     * @param languageCode
     * @return best matching language code and weight (as per {@link #match(ULocale, ULocale)})
     */
    private ULocale getBestMatchInternal(ULocale languageCode, OutputDouble outputWeight) {
        languageCode = canonicalize(languageCode);
        final ULocale maximized = addLikelySubtags(languageCode);
        if (DEBUG) {
            System.out.println("\ngetBestMatchInternal: " + languageCode + ";\t" + maximized);
        }
        double bestWeight = 0;
        ULocale bestTableMatch = null;
        String baseLanguage = maximized.getLanguage();
        Set<R3<ULocale, ULocale, Double>> searchTable =
                desiredLanguageToPossibleLocalesToMaxLocaleToData.get(baseLanguage);
        if (searchTable != null) { // we preprocessed the table so as to filter by lanugage
            if (DEBUG) System.out.println("\tSearching: " + searchTable);
            for (final R3<ULocale, ULocale, Double> tableKeyValue : searchTable) {
                ULocale tableKey = tableKeyValue.get0();
                ULocale maxLocale = tableKeyValue.get1();
                Double matchedWeight = tableKeyValue.get2();
                final double match = match(languageCode, maximized, tableKey, maxLocale);
                if (DEBUG) {
                    System.out.println("\t" + tableKeyValue + ";\t" + match + "\n");
                }
                final double weight = match * matchedWeight;
                if (weight > bestWeight) {
                    bestWeight = weight;
                    bestTableMatch = tableKey;
                    if (weight > 0.999d) { // bail on good enough match.
                        break;
                    }
                }
            }
        }
        if (bestWeight < threshold) {
            bestTableMatch = defaultLanguage;
        }
        if (outputWeight != null) {
            outputWeight.value = bestWeight; // only return the weight when needed
        }
        return bestTableMatch;
    }

    public static class OutputDouble { // TODO, move to where OutputInt is
        double value;
    }

    private void add(ULocale language, Double weight) {
        language = canonicalize(language);
        R3<ULocale, ULocale, Double> row = Row.of(language, addLikelySubtags(language), weight);
        row.freeze();
        localeToMaxLocaleAndWeight.add(row);
    }

    /** We preprocess the data to get just the possible matches for each desired base language. */
    private void processMapping() {
        for (Entry<String, Set<String>> desiredToMatchingLanguages :
                matcherData.matchingLanguages().keyValuesSet()) {
            String desired = desiredToMatchingLanguages.getKey();
            Set<String> supported = desiredToMatchingLanguages.getValue();
            for (R3<ULocale, ULocale, Double> localeToMaxAndWeight : localeToMaxLocaleAndWeight) {
                final ULocale key = localeToMaxAndWeight.get0();
                String lang = key.getLanguage();
                if (supported.contains(lang)) {
                    addFiltered(desired, localeToMaxAndWeight);
                }
            }
        }
        // now put in the values directly, since languages always map to themselves
        for (R3<ULocale, ULocale, Double> localeToMaxAndWeight : localeToMaxLocaleAndWeight) {
            final ULocale key = localeToMaxAndWeight.get0();
            String lang = key.getLanguage();
            addFiltered(lang, localeToMaxAndWeight);
        }
    }

    private void addFiltered(String desired, R3<ULocale, ULocale, Double> localeToMaxAndWeight) {
        Set<R3<ULocale, ULocale, Double>> map =
                desiredLanguageToPossibleLocalesToMaxLocaleToData.get(desired);
        if (map == null) {
            desiredLanguageToPossibleLocalesToMaxLocaleToData.put(
                    desired, map = new LinkedHashSet<>());
        }
        map.add(localeToMaxAndWeight);
        if (DEBUG) {
            System.out.println(desired + ", " + localeToMaxAndWeight);
        }
    }

    Set<Row.R3<ULocale, ULocale, Double>> localeToMaxLocaleAndWeight = new LinkedHashSet<>();
    Map<String, Set<Row.R3<ULocale, ULocale, Double>>>
            desiredLanguageToPossibleLocalesToMaxLocaleToData = new LinkedHashMap<>();

    // =============== Special Mapping Information ==============

    /**
     * We need to add another method to addLikelySubtags that doesn't return null, but instead
     * substitutes Zzzz and ZZ if unknown. There are also a few cases where addLikelySubtags needs
     * to have expanded data, to handle all deprecated codes.
     *
     * @param languageCode
     * @return "fixed" addLikelySubtags
     */
    private ULocale addLikelySubtags(ULocale languageCode) {
        // max("und") = "en_Latn_US", and since matching is based on maximized tags, the undefined
        // language would normally match English.  But that would produce the counterintuitive
        // results
        // that getBestMatch("und", LocaleMatcher("it,en")) would be "en", and
        // getBestMatch("en", LocaleMatcher("it,und")) would be "und".
        //
        // To avoid that, we change the matcher's definitions of max (AddLikelySubtagsWithDefaults)
        // so that max("und")="und". That produces the following, more desirable results:
        if (languageCode.equals(UNKNOWN_LOCALE)) {
            return UNKNOWN_LOCALE;
        }
        final ULocale result = ULocale.addLikelySubtags(languageCode);
        // should have method on getLikelySubtags for this
        if (result == null || result.equals(languageCode)) {
            final String language = languageCode.getLanguage();
            final String script = languageCode.getScript();
            final String region = languageCode.getCountry();
            return new ULocale(
                    (language.length() == 0 ? "und" : language)
                            + "_"
                            + (script.length() == 0 ? "Zzzz" : script)
                            + "_"
                            + (region.length() == 0 ? "ZZ" : region));
        }
        return result;
    }

    private static class LocalePatternMatcher {
        // a value of null means a wildcard; matches any.
        private String lang;
        private String script;
        private String region;
        private Level level;
        static Pattern pattern =
                Pattern.compile(
                        "([a-z]{1,8}|\\*)"
                                + "(?:[_-]([A-Z][a-z]{3}|\\*))?"
                                + "(?:[_-]([$]!?[a-zA-Z]+|[A-Z]{2}|[0-9]{3}|\\*))?");

        public LocalePatternMatcher(String toMatch) {
            Matcher matcher = pattern.matcher(toMatch);
            if (!matcher.matches()) {
                throw new IllegalArgumentException("Bad pattern: " + toMatch);
            }
            lang = matcher.group(1);
            script = matcher.group(2);
            region = matcher.group(3);
            level = region != null ? Level.region : script != null ? Level.script : Level.language;

            if (lang.equals("*")) {
                lang = null;
            }
            if (script != null && script.equals("*")) {
                script = null;
            }
            if (region != null && region.equals("*")) {
                region = null;
            }
        }

        boolean matches(ULocale ulocale) {
            if (lang != null && !lang.equals(ulocale.getLanguage())) {
                return false;
            }
            if (script != null && !script.equals(ulocale.getScript())) {
                return false;
            }
            if (region != null && !region.equals(ulocale.getCountry())) {
                return false;
            }
            return true;
        }

        public Level getLevel() {
            return level;
        }

        public String getLanguage() {
            return (lang == null ? "*" : lang);
        }

        public String getScript() {
            return (script == null ? "*" : script);
        }

        public String getRegion() {
            return (region == null ? "*" : region);
        }

        @Override
        public String toString() {
            String result = getLanguage();
            if (level != Level.language) {
                result += "-" + getScript();
                if (level != Level.script) {
                    result += "-" + getRegion();
                }
            }
            return result;
        }

        /* (non-Javadoc)
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            LocalePatternMatcher other = (LocalePatternMatcher) obj;
            return Objects.equals(level, other.level)
                    && Objects.equals(lang, other.lang)
                    && Objects.equals(script, other.script)
                    && Objects.equals(region, other.region);
        }

        /* (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            return level.ordinal()
                    ^ (lang == null ? 0 : lang.hashCode())
                    ^ (script == null ? 0 : script.hashCode())
                    ^ (region == null ? 0 : region.hashCode());
        }
    }

    enum Level {
        language(0.99),
        script(0.2),
        region(0.04);

        final double worst;

        Level(double d) {
            worst = d;
        }
    }

    private static class ScoreData implements Freezable<ScoreData> {
        @SuppressWarnings("unused")
        private static final double maxUnequal_changeD_sameS = 0.5;

        @SuppressWarnings("unused")
        private static final double maxUnequal_changeEqual = 0.75;

        LinkedHashSet<Row.R3<LocalePatternMatcher, LocalePatternMatcher, Double>> scores =
                new LinkedHashSet<>();
        final Level level;

        public ScoreData(Level level) {
            this.level = level;
        }

        void addDataToScores(
                String desired,
                String supported,
                R3<LocalePatternMatcher, LocalePatternMatcher, Double> data) {
            //            Map<String, Set<R3<LocalePatternMatcher,LocalePatternMatcher,Double>>>
            // lang_result = scores.get(desired);
            //            if (lang_result == null) {
            //                scores.put(desired, lang_result = new HashMap());
            //            }
            //            Set<R3<LocalePatternMatcher,LocalePatternMatcher,Double>> result =
            // lang_result.get(supported);
            //            if (result == null) {
            //                lang_result.put(supported, result = new LinkedHashSet());
            //            }
            //            result.add(data);
            boolean added = scores.add(data);
            if (!added) {
                throw new ICUException("trying to add duplicate data: " + data);
            }
        }

        double getScore(
                ULocale dMax,
                String desiredRaw,
                String desiredMax,
                ULocale sMax,
                String supportedRaw,
                String supportedMax) {
            double distance = 0;
            if (!desiredMax.equals(supportedMax)) {
                distance = getRawScore(dMax, sMax);
            } else if (!desiredRaw.equals(supportedRaw)) { // maxes are equal, changes are equal
                distance += 0.001;
            }
            return distance;
        }

        private double getRawScore(ULocale desiredLocale, ULocale supportedLocale) {
            if (DEBUG) {
                System.out.println(
                        "\t\t\t"
                                + level
                                + " Raw Score:\t"
                                + desiredLocale
                                + ";\t"
                                + supportedLocale);
            }
            for (R3<LocalePatternMatcher, LocalePatternMatcher, Double> datum :
                    scores) { // : result
                if (datum.get0().matches(desiredLocale) && datum.get1().matches(supportedLocale)) {
                    if (DEBUG) {
                        System.out.println("\t\t\t\tFOUND\t" + datum);
                    }
                    return datum.get2();
                }
            }
            if (DEBUG) {
                System.out.println("\t\t\t\tNOTFOUND\t" + level.worst);
            }
            return level.worst;
        }

        @Override
        public String toString() {
            StringBuilder result = new StringBuilder().append(level);
            for (R3<LocalePatternMatcher, LocalePatternMatcher, Double> score : scores) {
                result.append("\n\t\t").append(score);
            }
            return result.toString();
        }

        @Override
        @SuppressWarnings("unchecked")
        public ScoreData cloneAsThawed() {
            try {
                ScoreData result = (ScoreData) clone();
                result.scores =
                        (LinkedHashSet<R3<LocalePatternMatcher, LocalePatternMatcher, Double>>)
                                result.scores.clone();
                result.frozen = false;
                return result;
            } catch (CloneNotSupportedException e) {
                throw new ICUCloneNotSupportedException(e); // will never happen
            }
        }

        private volatile boolean frozen = false;

        @Override
        public ScoreData freeze() {
            return this;
        }

        @Override
        public boolean isFrozen() {
            return frozen;
        }

        public Relation<String, String> getMatchingLanguages() {
            Relation<String, String> desiredToSupported =
                    Relation.of(new LinkedHashMap<String, Set<String>>(), HashSet.class);
            for (R3<LocalePatternMatcher, LocalePatternMatcher, Double> item : scores) {
                LocalePatternMatcher desired = item.get0();
                LocalePatternMatcher supported = item.get1();
                if (desired.lang != null
                        && supported.lang
                                != null) { // explicitly mentioned languages must have reasonable
                    // distance
                    desiredToSupported.put(desired.lang, supported.lang);
                }
            }
            desiredToSupported.freeze();
            return desiredToSupported;
        }
    }

    /**
     * Only for testing and use by tools. Interface may change!!
     *
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static class LanguageMatcherData implements Freezable<LanguageMatcherData> {
        private ScoreData languageScores = new ScoreData(Level.language);
        private ScoreData scriptScores = new ScoreData(Level.script);
        private ScoreData regionScores = new ScoreData(Level.region);
        private Relation<String, String> matchingLanguages;
        private volatile boolean frozen = false;

        /**
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public LanguageMatcherData() {}

        /**
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public Relation<String, String> matchingLanguages() {
            return matchingLanguages;
        }

        /**
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Override
        @Deprecated
        public String toString() {
            return languageScores + "\n\t" + scriptScores + "\n\t" + regionScores;
        }

        /**
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public double match(ULocale a, ULocale aMax, ULocale b, ULocale bMax) {
            double diff = 0;
            diff +=
                    languageScores.getScore(
                            aMax,
                            a.getLanguage(),
                            aMax.getLanguage(),
                            bMax,
                            b.getLanguage(),
                            bMax.getLanguage());
            if (diff > 0.999d) { // with no language match, we bail
                return 0.0d;
            }
            diff +=
                    scriptScores.getScore(
                            aMax,
                            a.getScript(),
                            aMax.getScript(),
                            bMax,
                            b.getScript(),
                            bMax.getScript());
            diff +=
                    regionScores.getScore(
                            aMax,
                            a.getCountry(),
                            aMax.getCountry(),
                            bMax,
                            b.getCountry(),
                            bMax.getCountry());

            if (!a.getVariant().equals(b.getVariant())) {
                diff += 0.01;
            }
            if (diff < 0.0d) {
                diff = 0.0d;
            } else if (diff > 1.0d) {
                diff = 1.0d;
            }
            if (DEBUG) {
                System.out.println("\t\t\tTotal Distance\t" + diff);
            }
            return 1.0 - diff;
        }

        /**
         * Add an exceptional distance between languages, typically because regional dialects were
         * given their own language codes. At this point the code is symmetric. We don't bother
         * producing an equivalence class because there are so few cases; this function depends on
         * the other permutations being added specifically.
         *
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @SuppressWarnings("unused")
        @Deprecated
        private LanguageMatcherData addDistance(String desired, String supported, int percent) {
            return addDistance(desired, supported, percent, false, null);
        }

        /**
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public LanguageMatcherData addDistance(
                String desired, String supported, int percent, String comment) {
            return addDistance(desired, supported, percent, false, comment);
        }

        /**
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Deprecated
        public LanguageMatcherData addDistance(
                String desired, String supported, int percent, boolean oneway) {
            return addDistance(desired, supported, percent, oneway, null);
        }

        private LanguageMatcherData addDistance(
                String desired, String supported, int percent, boolean oneway, String comment) {
            if (DEBUG) {
                System.out.println(
                        "\t<languageMatch desired=\""
                                + desired
                                + "\""
                                + " supported=\""
                                + supported
                                + "\""
                                + " percent=\""
                                + percent
                                + "\""
                                + (oneway ? " oneway=\"true\"" : "")
                                + "/>"
                                + (comment == null ? "" : "\t<!-- " + comment + " -->"));
                //                    //     .addDistance("nn", "nb", 4, true)
                //                        System.out.println(".addDistance(\"" + desired + "\"" +
                //                                ", \"" + supported + "\"" +
                //                                ", " + percent + ""
                //                                + (oneway ? "" : ", true")
                //                                + (comment == null ? "" : ", \"" + comment + "\"")
                //                                + ")"
                //                        );

            }
            double score = 1 - percent / 100.0; // convert from percentage
            LocalePatternMatcher desiredMatcher = new LocalePatternMatcher(desired);
            Level desiredLen = desiredMatcher.getLevel();
            LocalePatternMatcher supportedMatcher = new LocalePatternMatcher(supported);
            Level supportedLen = supportedMatcher.getLevel();
            if (desiredLen != supportedLen) {
                throw new IllegalArgumentException(
                        "Lengths unequal: " + desired + ", " + supported);
            }
            R3<LocalePatternMatcher, LocalePatternMatcher, Double> data =
                    Row.of(desiredMatcher, supportedMatcher, score);
            R3<LocalePatternMatcher, LocalePatternMatcher, Double> data2 =
                    oneway ? null : Row.of(supportedMatcher, desiredMatcher, score);
            boolean desiredEqualsSupported = desiredMatcher.equals(supportedMatcher);
            switch (desiredLen) {
                case language:
                    String dlanguage = desiredMatcher.getLanguage();
                    String slanguage = supportedMatcher.getLanguage();
                    languageScores.addDataToScores(dlanguage, slanguage, data);
                    if (!oneway && !desiredEqualsSupported) {
                        languageScores.addDataToScores(slanguage, dlanguage, data2);
                    }
                    break;
                case script:
                    String dscript = desiredMatcher.getScript();
                    String sscript = supportedMatcher.getScript();
                    scriptScores.addDataToScores(dscript, sscript, data);
                    if (!oneway && !desiredEqualsSupported) {
                        scriptScores.addDataToScores(sscript, dscript, data2);
                    }
                    break;
                case region:
                    String dregion = desiredMatcher.getRegion();
                    String sregion = supportedMatcher.getRegion();
                    regionScores.addDataToScores(dregion, sregion, data);
                    if (!oneway && !desiredEqualsSupported) {
                        regionScores.addDataToScores(sregion, dregion, data2);
                    }
                    break;
            }
            return this;
        }

        /**
         * {@inheritDoc}
         *
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Override
        @Deprecated
        public LanguageMatcherData cloneAsThawed() {
            LanguageMatcherData result;
            try {
                result = (LanguageMatcherData) clone();
                result.languageScores = languageScores.cloneAsThawed();
                result.scriptScores = scriptScores.cloneAsThawed();
                result.regionScores = regionScores.cloneAsThawed();
                result.frozen = false;
                return result;
            } catch (CloneNotSupportedException e) {
                throw new ICUCloneNotSupportedException(e); // will never happen
            }
        }

        /**
         * {@inheritDoc}
         *
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Override
        @Deprecated
        public LanguageMatcherData freeze() {
            languageScores.freeze();
            regionScores.freeze();
            scriptScores.freeze();
            matchingLanguages = languageScores.getMatchingLanguages();
            frozen = true;
            return this;
        }

        /**
         * {@inheritDoc}
         *
         * @internal
         * @deprecated This API is ICU internal only.
         */
        @Override
        @Deprecated
        public boolean isFrozen() {
            return frozen;
        }
    }

    LanguageMatcherData matcherData;

    private static final LanguageMatcherData defaultWritten;

    private static HashMap<String, String> canonicalMap = new HashMap<>();

    static {
        canonicalMap.put("iw", "he");
        canonicalMap.put("mo", "ro");
        canonicalMap.put("tl", "fil");

        ICUResourceBundle suppData = getICUSupplementalData();
        ICUResourceBundle languageMatching = suppData.findTopLevel("languageMatching");
        ICUResourceBundle written = (ICUResourceBundle) languageMatching.get("written");
        defaultWritten = new LanguageMatcherData();

        for (UResourceBundleIterator iter = written.getIterator(); iter.hasNext(); ) {
            ICUResourceBundle item = (ICUResourceBundle) iter.next();
            /*
            "*_*_*",
            "*_*_*",
            "96",
             */
            // <languageMatch desired="gsw" supported="de" percent="96" oneway="true" />
            boolean oneway = item.getSize() > 3 && "1".equals(item.getString(3));
            defaultWritten.addDistance(
                    item.getString(0),
                    item.getString(1),
                    Integer.parseInt(item.getString(2)),
                    oneway);
        }
        defaultWritten.freeze();
    }

    /**
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static ICUResourceBundle getICUSupplementalData() {
        ICUResourceBundle suppData =
                (ICUResourceBundle)
                        UResourceBundle.getBundleInstance(
                                ICUData.ICU_BASE_NAME,
                                "supplementalData",
                                ICUResourceBundle.ICU_DATA_CLASS_LOADER);
        return suppData;
    }

    /**
     * @internal
     * @deprecated This API is ICU internal only.
     */
    @Deprecated
    public static double match(ULocale a, ULocale b) {
        final LocaleMatcher matcher = new LocaleMatcher("");
        return matcher.match(a, matcher.addLikelySubtags(a), b, matcher.addLikelySubtags(b));
    }
}
