// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
/*
 *******************************************************************************
 * Copyright (C) 2008-2016, International Business Machines Corporation and
 * others. All Rights Reserved.
 *******************************************************************************
 */
package com.ibm.icu.impl;

import java.text.ParseException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Set;
import java.util.TreeMap;

import com.ibm.icu.text.PluralRanges;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.text.PluralRules.PluralType;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.UResourceBundle;

/**
 * Loader for plural rules data.
 */
public class PluralRulesLoader extends PluralRules.Factory {
    private final Map<String, PluralRules> rulesIdToRules;
    // lazy init, use getLocaleIdToRulesIdMap to access
    private Map<String, String> localeIdToCardinalRulesId;
    private Map<String, String> localeIdToOrdinalRulesId;
    private Map<String, ULocale> rulesIdToEquivalentULocale;
    private static Map<String, PluralRanges> localeIdToPluralRanges;


    /**
     * Access through singleton.
     */
    private PluralRulesLoader() {
        rulesIdToRules = new HashMap<String, PluralRules>();
    }

    /**
     * Returns the locales for which we have plurals data. Utility for testing.
     */
    public ULocale[] getAvailableULocales() {
        Set<String> keys = getLocaleIdToRulesIdMap(PluralType.CARDINAL).keySet();
        ULocale[] locales = new ULocale[keys.size()];
        int n = 0;
        for (Iterator<String> iter = keys.iterator(); iter.hasNext();) {
            locales[n++] = ULocale.createCanonical(iter.next());
        }
        return locales;
    }

    /**
     * Returns the functionally equivalent locale.
     */
    public ULocale getFunctionalEquivalent(ULocale locale, boolean[] isAvailable) {
        if (isAvailable != null && isAvailable.length > 0) {
            String localeId = ULocale.canonicalize(locale.getBaseName());
            Map<String, String> idMap = getLocaleIdToRulesIdMap(PluralType.CARDINAL);
            isAvailable[0] = idMap.containsKey(localeId);
        }

        String rulesId = getRulesIdForLocale(locale, PluralType.CARDINAL);
        if (rulesId == null || rulesId.trim().length() == 0) {
            return ULocale.ROOT; // ultimate fallback
        }

        ULocale result = getRulesIdToEquivalentULocaleMap().get(
                rulesId);
        if (result == null) {
            return ULocale.ROOT; // ultimate fallback
        }

        return result;
    }

    /**
     * Returns the lazily-constructed map.
     */
    private Map<String, String> getLocaleIdToRulesIdMap(PluralType type) {
        checkBuildRulesIdMaps();
        return (type == PluralType.CARDINAL) ? localeIdToCardinalRulesId : localeIdToOrdinalRulesId;
    }

    /**
     * Returns the lazily-constructed map.
     */
    private Map<String, ULocale> getRulesIdToEquivalentULocaleMap() {
        checkBuildRulesIdMaps();
        return rulesIdToEquivalentULocale;
    }

    /**
     * Lazily constructs the localeIdToRulesId and rulesIdToEquivalentULocale
     * maps if necessary. These exactly reflect the contents of the locales
     * resource in plurals.res.
     */
    private void checkBuildRulesIdMaps() {
        boolean haveMap;
        synchronized (this) {
            haveMap = localeIdToCardinalRulesId != null;
        }
        if (!haveMap) {
            Map<String, String> tempLocaleIdToCardinalRulesId;
            Map<String, String> tempLocaleIdToOrdinalRulesId;
            Map<String, ULocale> tempRulesIdToEquivalentULocale;
            try {
                UResourceBundle pluralb = getPluralBundle();
                // Read cardinal-number rules.
                UResourceBundle localeb = pluralb.get("locales");

                // sort for convenience of getAvailableULocales
                tempLocaleIdToCardinalRulesId = new TreeMap<String, String>();
                // not visible
                tempRulesIdToEquivalentULocale = new HashMap<String, ULocale>();

                for (int i = 0; i < localeb.getSize(); ++i) {
                    UResourceBundle b = localeb.get(i);
                    String id = b.getKey();
                    String value = b.getString().intern();
                    tempLocaleIdToCardinalRulesId.put(id, value);

                    if (!tempRulesIdToEquivalentULocale.containsKey(value)) {
                        tempRulesIdToEquivalentULocale.put(value, new ULocale(id));
                    }
                }

                // Read ordinal-number rules.
                localeb = pluralb.get("locales_ordinals");
                tempLocaleIdToOrdinalRulesId = new TreeMap<String, String>();
                for (int i = 0; i < localeb.getSize(); ++i) {
                    UResourceBundle b = localeb.get(i);
                    String id = b.getKey();
                    String value = b.getString().intern();
                    tempLocaleIdToOrdinalRulesId.put(id, value);
                }
            } catch (MissingResourceException e) {
                // dummy so we don't try again
                tempLocaleIdToCardinalRulesId = Collections.emptyMap();
                tempLocaleIdToOrdinalRulesId = Collections.emptyMap();
                tempRulesIdToEquivalentULocale = Collections.emptyMap();
            }

            synchronized(this) {
                if (localeIdToCardinalRulesId == null) {
                    localeIdToCardinalRulesId = tempLocaleIdToCardinalRulesId;
                    localeIdToOrdinalRulesId = tempLocaleIdToOrdinalRulesId;
                    rulesIdToEquivalentULocale = tempRulesIdToEquivalentULocale;
                }
            }
        }
    }

    /**
     * Gets the rulesId from the locale,with locale fallback. If there is no
     * rulesId, return null. The rulesId might be the empty string if the rule
     * is the default rule.
     */
    public String getRulesIdForLocale(ULocale locale, PluralType type) {
        Map<String, String> idMap = getLocaleIdToRulesIdMap(type);
        String localeId = ULocale.canonicalize(locale.getBaseName());
        String rulesId = null;
        while (null == (rulesId = idMap.get(localeId))) {
            int ix = localeId.lastIndexOf("_");
            if (ix == -1) {
                break;
            }
            localeId = localeId.substring(0, ix);
        }
        return rulesId;
    }

    /**
     * Gets the rule from the rulesId. If there is no rule for this rulesId,
     * return null.
     */
    public PluralRules getRulesForRulesId(String rulesId) {
        // synchronize on the map.  release the lock temporarily while we build the rules.
        PluralRules rules = null;
        boolean hasRules;  // Separate boolean because stored rules can be null.
        synchronized (rulesIdToRules) {
            hasRules = rulesIdToRules.containsKey(rulesId);
            if (hasRules) {
                rules = rulesIdToRules.get(rulesId);  // can be null
            }
        }
        if (!hasRules) {
            try {
                UResourceBundle pluralb = getPluralBundle();
                UResourceBundle rulesb = pluralb.get("rules");
                UResourceBundle setb = rulesb.get(rulesId);

                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < setb.getSize(); ++i) {
                    UResourceBundle b = setb.get(i);
                    if (i > 0) {
                        sb.append("; ");
                    }
                    sb.append(b.getKey());
                    sb.append(": ");
                    sb.append(b.getString());
                }
                rules = PluralRules.parseDescription(sb.toString());
            } catch (ParseException e) {
            } catch (MissingResourceException e) {
            }
            synchronized (rulesIdToRules) {
                if (rulesIdToRules.containsKey(rulesId)) {
                    rules = rulesIdToRules.get(rulesId);
                } else {
                    rulesIdToRules.put(rulesId, rules);  // can be null
                }
            }
        }
        return rules;
    }

    /**
     * Return the plurals resource. Note MissingResourceException is unchecked,
     * listed here for clarity. Callers should handle this exception.
     */
    public UResourceBundle getPluralBundle() throws MissingResourceException {
        return ICUResourceBundle.getBundleInstance(
                ICUData.ICU_BASE_NAME, "plurals",
                ICUResourceBundle.ICU_DATA_CLASS_LOADER, true);
    }

    /**
     * Returns the plural rules for the the locale. If we don't have data,
     * com.ibm.icu.text.PluralRules.DEFAULT is returned.
     */
    public PluralRules forLocale(ULocale locale, PluralRules.PluralType type) {
        String rulesId = getRulesIdForLocale(locale, type);
        if (rulesId == null || rulesId.trim().length() == 0) {
            return PluralRules.DEFAULT;
        }
        PluralRules rules = getRulesForRulesId(rulesId);
        if (rules == null) {
            rules = PluralRules.DEFAULT;
        }
        return rules;
    }

    /**
     * The only instance of the loader.
     */
    public static final PluralRulesLoader loader = new PluralRulesLoader();

    /* (non-Javadoc)
     * @see com.ibm.icu.text.PluralRules.Factory#hasOverride(com.ibm.icu.util.ULocale)
     */
    @Override
    public boolean hasOverride(ULocale locale) {
        return false;
    }
    
    private static final PluralRanges UNKNOWN_RANGE = new PluralRanges().freeze();

    public PluralRanges getPluralRanges(ULocale locale) {
        // TODO markdavis Fix the bad fallback, here and elsewhere in this file.
        String localeId = ULocale.canonicalize(locale.getBaseName());
        PluralRanges result;
        while (null == (result = localeIdToPluralRanges.get(localeId))) {
            int ix = localeId.lastIndexOf("_");
            if (ix == -1) {
                result = UNKNOWN_RANGE;
                break;
            }
            localeId = localeId.substring(0, ix);
        }
        return result;
    }

    public boolean isPluralRangesAvailable(ULocale locale) {
        return getPluralRanges(locale) == UNKNOWN_RANGE;
    }

    // TODO markdavis FIX HARD-CODED HACK once we have data from CLDR in the bundles
    static {
        String[][] pluralRangeData = {
                {"locales", "id ja km ko lo ms my th vi zh"},
                {"other", "other", "other"},

                {"locales", "am bn fr gu hi hy kn mr pa zu"},
                {"one", "one", "one"},
                {"one", "other", "other"},
                {"other", "other", "other"},

                {"locales", "fa"},
                {"one", "one", "other"},
                {"one", "other", "other"},
                {"other", "other", "other"},

                {"locales", "ka"},
                {"one", "other", "one"},
                {"other", "one", "other"},
                {"other", "other", "other"},

                {"locales", "az de el gl hu it kk ky ml mn ne nl pt sq sw ta te tr ug uz"},
                {"one", "other", "other"},
                {"other", "one", "one"},
                {"other", "other", "other"},

                {"locales", "af bg ca en es et eu fi nb sv ur"},
                {"one", "other", "other"},
                {"other", "one", "other"},
                {"other", "other", "other"},

                {"locales", "da fil is"},
                {"one", "one", "one"},
                {"one", "other", "other"},
                {"other", "one", "one"},
                {"other", "other", "other"},

                {"locales", "si"},
                {"one", "one", "one"},
                {"one", "other", "other"},
                {"other", "one", "other"},
                {"other", "other", "other"},

                {"locales", "mk"},
                {"one", "one", "other"},
                {"one", "other", "other"},
                {"other", "one", "other"},
                {"other", "other", "other"},

                {"locales", "lv"},
                {"zero", "zero", "other"},
                {"zero", "one", "one"},
                {"zero", "other", "other"},
                {"one", "zero", "other"},
                {"one", "one", "one"},
                {"one", "other", "other"},
                {"other", "zero", "other"},
                {"other", "one", "one"},
                {"other", "other", "other"},

                {"locales", "ro"},
                {"one", "few", "few"},
                {"one", "other", "other"},
                {"few", "one", "few"},
                {"few", "few", "few"},
                {"few", "other", "other"},
                {"other", "few", "few"},
                {"other", "other", "other"},

                {"locales", "hr sr bs"},
                {"one", "one", "one"},
                {"one", "few", "few"},
                {"one", "other", "other"},
                {"few", "one", "one"},
                {"few", "few", "few"},
                {"few", "other", "other"},
                {"other", "one", "one"},
                {"other", "few", "few"},
                {"other", "other", "other"},

                {"locales", "sl"},
                {"one", "one", "few"},
                {"one", "two", "two"},
                {"one", "few", "few"},
                {"one", "other", "other"},
                {"two", "one", "few"},
                {"two", "two", "two"},
                {"two", "few", "few"},
                {"two", "other", "other"},
                {"few", "one", "few"},
                {"few", "two", "two"},
                {"few", "few", "few"},
                {"few", "other", "other"},
                {"other", "one", "few"},
                {"other", "two", "two"},
                {"other", "few", "few"},
                {"other", "other", "other"},

                {"locales", "he"},
                {"one", "two", "other"},
                {"one", "many", "many"},
                {"one", "other", "other"},
                {"two", "many", "other"},
                {"two", "other", "other"},
                {"many", "many", "many"},
                {"many", "other", "many"},
                {"other", "one", "other"},
                {"other", "two", "other"},
                {"other", "many", "many"},
                {"other", "other", "other"},

                {"locales", "cs pl sk"},
                {"one", "few", "few"},
                {"one", "many", "many"},
                {"one", "other", "other"},
                {"few", "few", "few"},
                {"few", "many", "many"},
                {"few", "other", "other"},
                {"many", "one", "one"},
                {"many", "few", "few"},
                {"many", "many", "many"},
                {"many", "other", "other"},
                {"other", "one", "one"},
                {"other", "few", "few"},
                {"other", "many", "many"},
                {"other", "other", "other"},

                {"locales", "lt ru uk"},
                {"one", "one", "one"},
                {"one", "few", "few"},
                {"one", "many", "many"},
                {"one", "other", "other"},
                {"few", "one", "one"},
                {"few", "few", "few"},
                {"few", "many", "many"},
                {"few", "other", "other"},
                {"many", "one", "one"},
                {"many", "few", "few"},
                {"many", "many", "many"},
                {"many", "other", "other"},
                {"other", "one", "one"},
                {"other", "few", "few"},
                {"other", "many", "many"},
                {"other", "other", "other"},

                {"locales", "cy"},
                {"zero", "one", "one"},
                {"zero", "two", "two"},
                {"zero", "few", "few"},
                {"zero", "many", "many"},
                {"zero", "other", "other"},
                {"one", "two", "two"},
                {"one", "few", "few"},
                {"one", "many", "many"},
                {"one", "other", "other"},
                {"two", "few", "few"},
                {"two", "many", "many"},
                {"two", "other", "other"},
                {"few", "many", "many"},
                {"few", "other", "other"},
                {"many", "other", "other"},
                {"other", "one", "one"},
                {"other", "two", "two"},
                {"other", "few", "few"},
                {"other", "many", "many"},
                {"other", "other", "other"},

                {"locales", "ar"},
                {"zero", "one", "zero"},
                {"zero", "two", "zero"},
                {"zero", "few", "few"},
                {"zero", "many", "many"},
                {"zero", "other", "other"},
                {"one", "two", "other"},
                {"one", "few", "few"},
                {"one", "many", "many"},
                {"one", "other", "other"},
                {"two", "few", "few"},
                {"two", "many", "many"},
                {"two", "other", "other"},
                {"few", "few", "few"},
                {"few", "many", "many"},
                {"few", "other", "other"},
                {"many", "few", "few"},
                {"many", "many", "many"},
                {"many", "other", "other"},
                {"other", "one", "other"},
                {"other", "two", "other"},
                {"other", "few", "few"},
                {"other", "many", "many"},
                {"other", "other", "other"},     
        };
        PluralRanges pr = null;
        String[] locales = null;
        HashMap<String, PluralRanges> tempLocaleIdToPluralRanges = new HashMap<String, PluralRanges>();
        for (String[] row : pluralRangeData) {
            if (row[0].equals("locales")) {
                if (pr != null) {
                    pr.freeze();
                    for (String locale : locales) {
                        tempLocaleIdToPluralRanges.put(locale, pr);
                    }
                }
                locales = row[1].split(" ");
                pr = new PluralRanges();
            } else {
                pr.add(
                        StandardPlural.fromString(row[0]),
                        StandardPlural.fromString(row[1]),
                        StandardPlural.fromString(row[2]));
            }
        }
        // do last one
        for (String locale : locales) {
            tempLocaleIdToPluralRanges.put(locale, pr);
        }
        // now make whole thing immutable
        localeIdToPluralRanges = Collections.unmodifiableMap(tempLocaleIdToPluralRanges);
    }
}
