package org.unicode.cldr.util;

import java.io.File;
import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Deque;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.unicode.cldr.test.CoverageLevel2;
import org.unicode.cldr.tool.LikelySubtags;
import org.unicode.cldr.tool.SubdivisionNames;
import org.unicode.cldr.util.Builder.CBuilder;
import org.unicode.cldr.util.CldrUtility.VariableReplacer;
import org.unicode.cldr.util.DayPeriodInfo.DayPeriod;
import org.unicode.cldr.util.StandardCodes.LstrType;
import org.unicode.cldr.util.SupplementalDataInfo.BasicLanguageData.Type;
import org.unicode.cldr.util.SupplementalDataInfo.NumberingSystemInfo.NumberingSystemType;
import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count;
import org.unicode.cldr.util.Validity.Status;

import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.ibm.icu.dev.util.CollectionUtilities;
import com.ibm.icu.impl.IterableComparator;
import com.ibm.icu.impl.Relation;
import com.ibm.icu.impl.Row;
import com.ibm.icu.impl.Row.R2;
import com.ibm.icu.impl.Row.R4;
import com.ibm.icu.text.DateFormat;
import com.ibm.icu.text.MessageFormat;
import com.ibm.icu.text.NumberFormat;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.text.PluralRules.FixedDecimal;
import com.ibm.icu.text.PluralRules.FixedDecimalRange;
import com.ibm.icu.text.PluralRules.FixedDecimalSamples;
import com.ibm.icu.text.PluralRules.SampleType;
import com.ibm.icu.text.SimpleDateFormat;
import com.ibm.icu.text.UnicodeSet;
import com.ibm.icu.util.Freezable;
import com.ibm.icu.util.ICUUncheckedIOException;
import com.ibm.icu.util.Output;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.VersionInfo;

/**
 * Singleton class to provide API access to supplemental data -- in all the supplemental data files.
 * <p>
 * To create, use SupplementalDataInfo.getInstance
 * <p>
 * To add API for new structure, you will generally:
 * <ul>
 * <li>add a Map or Relation as a data member,
 * <li>put a check and handler in MyHandler for the paths that you consume,
 * <li>make the data member immutable in makeStuffSave, and
 * <li>add a getter for the data member
 * </ul>
 *
 * @author markdavis
 */

public class SupplementalDataInfo {
    private static final boolean DEBUG = false;
    private static final StandardCodes sc = StandardCodes.make();
    private static final String UNKNOWN_SCRIPT = "Zzzz";

    // TODO add structure for items shown by TestSupplementalData to be missing
    /*
     * [calendarData/calendar,
     * characters/character-fallback,
     * measurementData/measurementSystem, measurementData/paperSize,
     * metadata/attributeOrder, metadata/blocking, metadata/deprecated,
     * metadata/distinguishing, metadata/elementOrder, metadata/serialElements, metadata/skipDefaultLocale,
     * metadata/suppress, metadata/validity, metazoneInfo/timezone,
     * timezoneData/mapTimezones,
     * weekData/firstDay, weekData/minDays, weekData/weekendEnd, weekData/weekendStart]
     */
    // TODO: verify that we get everything by writing the files solely from the API, and verifying identity.

    /**
     * Official status of languages
     */
    public enum OfficialStatus {
        unknown("U", 1),
        recognized("R", 1),
        official_minority("OM", 2),
        official_regional("OR", 3),
        de_facto_official("OD", 10),
        official("O", 10);

        private final String shortName;
        private final int weight;

        private OfficialStatus(String shortName, int weight) {
            this.shortName = shortName;
            this.weight = weight;
        }

        public String toShortString() {
            return shortName;
        }

        public int getWeight() {
            return weight;
        }

        public boolean isMajor() {
            return compareTo(OfficialStatus.de_facto_official) >= 0;
        }

        public boolean isOfficial() {
            return compareTo(OfficialStatus.official_regional) >= 0;
        }
    };

    /**
     * Population data for different languages.
     */
    public static final class PopulationData implements Freezable<PopulationData> {
        private double population = Double.NaN;

        private double literatePopulation = Double.NaN;

        private double writingPopulation = Double.NaN;

        private double gdp = Double.NaN;

        private OfficialStatus officialStatus = OfficialStatus.unknown;

        public double getGdp() {
            return gdp;
        }

        public double getLiteratePopulation() {
            return literatePopulation;
        }

        public double getLiteratePopulationPercent() {
            return 100*literatePopulation/population;
        }

        public double getWritingPopulation() {
            return writingPopulation;
        }

        public double getWritingPercent() {
            return writingPopulation/population;
        }

        public double getPopulation() {
            return population;
        }

        public PopulationData setGdp(double gdp) {
            if (frozen) {
                throw new UnsupportedOperationException(
                    "Attempt to modify frozen object");
            }
            this.gdp = gdp;
            return this;
        }

        public PopulationData setLiteratePopulation(double literatePopulation) {
            if (frozen) {
                throw new UnsupportedOperationException(
                    "Attempt to modify frozen object");
            }
            this.literatePopulation = literatePopulation;
            return this;
        }

        public PopulationData setPopulation(double population) {
            if (frozen) {
                throw new UnsupportedOperationException(
                    "Attempt to modify frozen object");
            }
            this.population = population;
            return this;
        }

        public PopulationData set(PopulationData other) {
            if (frozen) {
                throw new UnsupportedOperationException(
                    "Attempt to modify frozen object");
            }
            if (other == null) {
                population = literatePopulation = gdp = Double.NaN;
            } else {
                population = other.population;
                literatePopulation = other.literatePopulation;
                writingPopulation = other.writingPopulation;
                gdp = other.gdp;
            }
            return this;
        }

        public void add(PopulationData other) {
            if (frozen) {
                throw new UnsupportedOperationException(
                    "Attempt to modify frozen object");
            }
            population += other.population;
            literatePopulation += other.literatePopulation;
            writingPopulation += other.writingPopulation;
            gdp += other.gdp;
        }

        public String toString() {
            return MessageFormat
                .format(
                    "[pop: {0,number,#,##0},\t lit: {1,number,#,##0.00},\t gdp: {2,number,#,##0},\t status: {3}]",
                    new Object[] { population, literatePopulation, gdp, officialStatus });
        }

        private boolean frozen;

        public boolean isFrozen() {
            return frozen;
        }

        public PopulationData freeze() {
            frozen = true;
            return this;
        }

        public PopulationData cloneAsThawed() {
            throw new UnsupportedOperationException("not yet implemented");
        }

        public OfficialStatus getOfficialStatus() {
            return officialStatus;
        }

        public PopulationData setOfficialStatus(OfficialStatus officialStatus) {
            if (frozen) {
                throw new UnsupportedOperationException(
                    "Attempt to modify frozen object");
            }
            this.officialStatus = officialStatus;
            return this;
        }

        public PopulationData setWritingPopulation(double writingPopulation) {
            if (frozen) {
                throw new UnsupportedOperationException(
                    "Attempt to modify frozen object");
            }
            this.writingPopulation = writingPopulation;
            return this;
        }
    }

    static final Pattern WHITESPACE_PATTERN = PatternCache.get("\\s+");

    /**
     * Simple language/script/region information
     */
    public static class BasicLanguageData implements Comparable<BasicLanguageData>,
    com.ibm.icu.util.Freezable<BasicLanguageData> {
        public enum Type {
            primary, secondary
        };

        private Type type = Type.primary;

        private Set<String> scripts = Collections.emptySet();

        private Set<String> territories = Collections.emptySet();

        public Type getType() {
            return type;
        }

        public BasicLanguageData setType(Type type) {
            this.type = type;
            return this;
        }

        public BasicLanguageData setScripts(String scriptTokens) {
            return setScripts(scriptTokens == null ? null : Arrays
                .asList(WHITESPACE_PATTERN.split(scriptTokens)));
        }

        public BasicLanguageData setTerritories(String territoryTokens) {
            return setTerritories(territoryTokens == null ? null : Arrays
                .asList(WHITESPACE_PATTERN.split(territoryTokens)));
        }

        public BasicLanguageData setScripts(Collection<String> scriptTokens) {
            if (frozen) {
                throw new UnsupportedOperationException();
            }
            // TODO add error checking
            scripts = Collections.emptySet();
            if (scriptTokens != null) {
                for (String script : scriptTokens) {
                    addScript(script);
                }
            }
            return this;
        }

        public BasicLanguageData setTerritories(Collection<String> territoryTokens) {
            if (frozen) {
                throw new UnsupportedOperationException();
            }
            territories = Collections.emptySet();
            if (territoryTokens != null) {
                for (String territory : territoryTokens) {
                    addTerritory(territory);
                }
            }
            return this;
        }

        public BasicLanguageData set(BasicLanguageData other) {
            scripts = other.scripts;
            territories = other.territories;
            return this;
        }

        public Set<String> getScripts() {
            return scripts;
        }

        public Set<String> getTerritories() {
            return territories;
        }

        public String toString(String languageSubtag) {
            if (scripts.size() == 0 && territories.size() == 0)
                return "";
            return "\t\t<language type=\""
            + languageSubtag
            + "\""
            + (scripts.size() == 0 ? "" : " scripts=\""
                + CldrUtility.join(scripts, " ") + "\"")
            + (territories.size() == 0 ? "" : " territories=\""
                + CldrUtility.join(territories, " ") + "\"")
            + (type == Type.primary ? "" : " alt=\"" + type + "\"") + "/>";
        }

        public String toString() {
            return "[" + type
                + (scripts.isEmpty() ? "" : "; scripts=" + CollectionUtilities.join(scripts, " "))
                + (scripts.isEmpty() ? "" : "; territories=" + CollectionUtilities.join(territories, " "))
                + "]";
        }

        public int compareTo(BasicLanguageData o) {
            int result;
            if (0 != (result = type.compareTo(o.type)))
                return result;
            if (0 != (result = IterableComparator.compareIterables(scripts, o.scripts)))
                return result;
            if (0 != (result = IterableComparator.compareIterables(territories, o.territories)))
                return result;
            return 0;
        }

        public boolean equals(Object input) {
            return compareTo((BasicLanguageData) input) == 0;
        }

        @Override
        public int hashCode() {
            // TODO Auto-generated method stub
            return ((type.ordinal() * 37 + scripts.hashCode()) * 37) + territories.hashCode();
        }

        public BasicLanguageData addScript(String script) {
            // simple error checking
            if (script.length() != 4) {
                throw new IllegalArgumentException("Illegal Script: " + script);
            }
            if (scripts == Collections.EMPTY_SET) {
                scripts = new TreeSet<String>();
            }
            scripts.add(script);
            return this;
        }

        public BasicLanguageData addTerritory(String territory) {
            // simple error checking
            if (territory.length() != 2) {
                throw new IllegalArgumentException("Illegal Territory: " + territory);
            }
            if (territories == Collections.EMPTY_SET) {
                territories = new TreeSet<String>();
            }
            territories.add(territory);
            return this;
        }

        boolean frozen = false;

        public boolean isFrozen() {
            return frozen;
        }

        public BasicLanguageData freeze() {
            frozen = true;
            if (scripts != Collections.EMPTY_SET) {
                scripts = Collections.unmodifiableSet(scripts);
            }
            if (territories != Collections.EMPTY_SET) {
                territories = Collections.unmodifiableSet(territories);
            }
            return this;
        }

        public BasicLanguageData cloneAsThawed() {
            throw new UnsupportedOperationException();
        }

        public void addScripts(Set<String> scripts2) {
            for (String script : scripts2) {
                addScript(script);
            }
        }
    }

    /**
     * Information about currency digits and rounding.
     */
    public static class CurrencyNumberInfo {
        public final int digits;
        public final int rounding;
        public final double roundingIncrement;
        public final int cashDigits;
        public final int cashRounding;
        public final double cashRoundingIncrement;

        public int getDigits() {
            return digits;
        }

        public int getRounding() {
            return rounding;
        }

        public double getRoundingIncrement() {
            return roundingIncrement;
        }

        public CurrencyNumberInfo(int _digits, int _rounding, int _cashDigits, int _cashRounding) {
            digits = _digits;
            rounding = _rounding < 0 ? 0 : _rounding;
            roundingIncrement = rounding * Math.pow(10.0, -digits);
            // if the values are not set, use the above values
            cashDigits = _cashDigits < 0 ? digits : _cashDigits;
            cashRounding = _cashRounding < 0 ? rounding : _cashRounding;
            cashRoundingIncrement = this.cashRounding * Math.pow(10.0, -digits);
        }
    }

    public static class NumberingSystemInfo {
        public enum NumberingSystemType {
            algorithmic, numeric, unknown
        };

        public final String name;
        public final NumberingSystemType type;
        public final String digits;
        public final String rules;

        public NumberingSystemInfo(XPathParts parts) {
            name = parts.getAttributeValue(-1, "id");
            digits = parts.getAttributeValue(-1, "digits");
            rules = parts.getAttributeValue(-1, "rules");
            type = NumberingSystemType.valueOf(parts.getAttributeValue(-1, "type"));
        }

    }

    /**
     * Class for a range of two dates, refactored to share code.
     *
     * @author markdavis
     */
    public static final class DateRange implements Comparable<DateRange> {
        public static final long START_OF_TIME = Long.MIN_VALUE;
        public static final long END_OF_TIME = Long.MAX_VALUE;
        public final long from;
        public final long to;

        public DateRange(String fromString, String toString) {
            from = parseDate(fromString, START_OF_TIME);
            to = parseDate(toString, END_OF_TIME);
        }

        public long getFrom() {
            return from;
        }

        public long getTo() {
            return to;
        }

        static final DateFormat[] simpleFormats = {
            new SimpleDateFormat("yyyy-MM-dd HH:mm"),
            new SimpleDateFormat("yyyy-MM-dd"),
            new SimpleDateFormat("yyyy-MM"),
            new SimpleDateFormat("yyyy"),
        };
        static {
            TimeZone gmt = TimeZone.getTimeZone("GMT");
            for (DateFormat format : simpleFormats) {
                format.setTimeZone(gmt);
            }
        }

        long parseDate(String dateString, long defaultDate) {
            if (dateString == null) {
                return defaultDate;
            }
            ParseException e2 = null;
            for (int i = 0; i < simpleFormats.length; ++i) {
                try {
                    synchronized (simpleFormats[i]) {
                        Date result = simpleFormats[i].parse(dateString);
                        return result.getTime();
                    }
                } catch (ParseException e) {
                    if (e2 == null) {
                        e2 = e;
                    }
                }
            }
            throw new IllegalArgumentException(e2);
        }

        public String toString() {
            return "{" + formatDate(from)
            + ", "
            + formatDate(to) + "}";
        }

        public static String formatDate(long date) {
            if (date == START_OF_TIME) {
                return "-∞";
            }
            if (date == END_OF_TIME) {
                return "∞";
            }
            synchronized (simpleFormats[0]) {
                return simpleFormats[0].format(date);
            }
        }

        @Override
        public int compareTo(DateRange arg0) {
            return to > arg0.to ? 1 : to < arg0.to ? -1 : from > arg0.from ? 1 : from < arg0.from ? -1 : 0;
        }
    }

    /**
     * Information about when currencies are in use in territories
     */
    public static class CurrencyDateInfo implements Comparable<CurrencyDateInfo> {

        public static final Date END_OF_TIME = new Date(DateRange.END_OF_TIME);
        public static final Date START_OF_TIME = new Date(DateRange.START_OF_TIME);

        private String currency;
        private DateRange dateRange;
        private boolean isLegalTender;
        private String errors = "";

        public CurrencyDateInfo(String currency, String startDate, String endDate, String tender) {
            this.currency = currency;
            this.dateRange = new DateRange(startDate, endDate);
            this.isLegalTender = (tender == null || !tender.equals("false"));
        }

        public String getCurrency() {
            return currency;
        }

        public Date getStart() {
            return new Date(dateRange.getFrom());
        }

        public Date getEnd() {
            return new Date(dateRange.getTo());
        }

        public String getErrors() {
            return errors;
        }

        public boolean isLegalTender() {
            return isLegalTender;
        }

        public int compareTo(CurrencyDateInfo o) {
            int result = dateRange.compareTo(o.dateRange);
            if (result != 0) return result;
            return currency.compareTo(o.currency);
        }

        public String toString() {
            return "{" + dateRange + ", " + currency + "}";
        }

        public static String formatDate(Date date) {
            return DateRange.formatDate(date.getTime());
        }

    }

    public static final class MetaZoneRange implements Comparable<MetaZoneRange> {
        public final DateRange dateRange;
        public final String metazone;

        /**
         * @param metazone
         * @param from
         * @param to
         */
        public MetaZoneRange(String metazone, String fromString, String toString) {
            super();
            this.metazone = metazone;
            dateRange = new DateRange(fromString, toString);
        }

        @Override
        public int compareTo(MetaZoneRange arg0) {
            int result;
            if (0 != (result = dateRange.compareTo(arg0.dateRange))) {
                return result;
            }
            return metazone.compareTo(arg0.metazone);
        }

        public String toString() {
            return "{" + dateRange + ", " + metazone + "}";
        }
    }

    /**
     * Information about telephone code(s) for a given territory
     */
    public static class TelephoneCodeInfo implements Comparable<TelephoneCodeInfo> {
        public static final Date END_OF_TIME = new Date(Long.MAX_VALUE);
        public static final Date START_OF_TIME = new Date(Long.MIN_VALUE);
        private static final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

        private String code;
        private Date start;
        private Date end;
        private String alt;
        private String errors = "";

        // code must not be null, the others can be
        public TelephoneCodeInfo(String code, String startDate, String endDate, String alt) {
            if (code == null)
                throw new NullPointerException();
            this.code = code; // code will not be null
            this.start = parseDate(startDate, START_OF_TIME); // start will not be null
            this.end = parseDate(endDate, END_OF_TIME); // end willl not be null
            this.alt = (alt == null) ? "" : alt; // alt will not be null
        }

        static DateFormat[] simpleFormats = {
            new SimpleDateFormat("yyyy-MM-dd"),
            new SimpleDateFormat("yyyy-MM"),
            new SimpleDateFormat("yyyy"), };

        Date parseDate(String dateString, Date defaultDate) {
            if (dateString == null) {
                return defaultDate;
            }
            ParseException e2 = null;
            for (int i = 0; i < simpleFormats.length; ++i) {
                try {
                    Date result = simpleFormats[i].parse(dateString);
                    return result;
                } catch (ParseException e) {
                    if (i == 0) {
                        errors += dateString + " ";
                    }
                    if (e2 == null) {
                        e2 = e;
                    }
                }
            }
            throw (IllegalArgumentException) new IllegalArgumentException().initCause(e2);
        }

        public String getCode() {
            return code;
        }

        public Date getStart() {
            return start;
        }

        public Date getEnd() {
            return end;
        }

        public String getAlt() {
            return alt; // may return null
        }

        public String getErrors() {
            return errors;
        }

        public boolean equals(Object o) {
            if (!(o instanceof TelephoneCodeInfo))
                return false;
            TelephoneCodeInfo tc = (TelephoneCodeInfo) o;
            return tc.code.equals(code) && tc.start.equals(start) && tc.end.equals(end) && tc.alt.equals(alt);
        }

        public int hashCode() {
            return 31 * code.hashCode() + start.hashCode() + end.hashCode() + alt.hashCode();
        }

        public int compareTo(TelephoneCodeInfo o) {
            int result = code.compareTo(o.code);
            if (result != 0) return result;
            result = start.compareTo(o.start);
            if (result != 0) return result;
            result = end.compareTo(o.end);
            if (result != 0) return result;
            return alt.compareTo(o.alt);
        }

        public String toString() {
            return "{" + code + ", " + formatDate(start) + ", " + formatDate(end) + ", " + alt + "}";
        }

        public static String formatDate(Date date) {
            if (date.equals(START_OF_TIME)) return "-∞";
            if (date.equals(END_OF_TIME)) return "∞";
            return dateFormat.format(date);
        }
    }

    public static class CoverageLevelInfo implements Comparable<CoverageLevelInfo> {
        public final String match;
        public final Level value;
        public final Pattern inLanguage;
        public final String inScript;
        public final Set<String> inScriptSet;
        public final String inTerritory;
        public final Set<String> inTerritorySet;
        private Set<String> inTerritorySetInternal;

        public CoverageLevelInfo(String match, int value, String language, String script, String territory) {
            this.inLanguage = language != null ? PatternCache.get(language) : null;
            this.inScript = script;
            this.inTerritory = territory;
            this.inScriptSet = toSet(script);
            this.inTerritorySet = toSet(territory); // MUST BE LAST, sets inTerritorySetInternal
            this.match = match;
            this.value = Level.fromLevel(value);
        }

        public static final Pattern NON_ASCII_LETTER = PatternCache.get("[^A-Za-z]+");

        private Set<String> toSet(String source) {
            if (source == null) {
                return null;
            }
            Set<String> result = new HashSet<String>(Arrays.asList(NON_ASCII_LETTER.split(source)));
            result.remove("");
            inTerritorySetInternal = result;
            return Collections.unmodifiableSet(result);
        }

        public int compareTo(CoverageLevelInfo o) {
            if (value == o.value) {
                return match.compareTo(o.match);
            } else {
                return value.compareTo(o.value);
            }
        }

        static void fixEU(Collection<CoverageLevelInfo> targets, SupplementalDataInfo info) {
            Set<String> euCountries = info.getContained("EU");
            for (CoverageLevelInfo item : targets) {
                if (item.inTerritorySet != null
                    && item.inTerritorySet.contains("EU")) {
                    item.inTerritorySetInternal.addAll(euCountries);
                }
            }
        }
    }

    public static final String STAR = "*";
    public static final Set<String> STAR_SET = Builder.with(new HashSet<String>()).add("*").freeze();

    private VersionInfo cldrVersion;

    private Map<String, PopulationData> territoryToPopulationData = new TreeMap<String, PopulationData>();

    private Map<String, Map<String, PopulationData>> territoryToLanguageToPopulationData = new TreeMap<String, Map<String, PopulationData>>();

    private Map<String, PopulationData> languageToPopulation = new TreeMap<String, PopulationData>();

    private Map<String, PopulationData> baseLanguageToPopulation = new TreeMap<String, PopulationData>();

    private Relation<String, String> languageToScriptVariants = Relation.of(new TreeMap<String, Set<String>>(),
        TreeSet.class);

    private Relation<String, String> languageToTerritories = Relation.of(new TreeMap<String, Set<String>>(),
        LinkedHashSet.class);

    transient private Relation<String, Pair<Boolean, Pair<Double, String>>> languageToTerritories2 =
        Relation.of(new TreeMap<String, Set<Pair<Boolean, Pair<Double, String>>>>(), TreeSet.class);

    private Map<String, Map<BasicLanguageData.Type, BasicLanguageData>> languageToBasicLanguageData =
        new TreeMap<String, Map<BasicLanguageData.Type, BasicLanguageData>>();

    // private Map<String, BasicLanguageData> languageToBasicLanguageData2 = new
    // TreeMap();

    // Relation(new TreeMap(), TreeSet.class, null);

    private Set<String> allLanguages = new TreeSet<String>();
    final private List<String> approvalRequirements = new LinkedList<String>(); // xpath array

    private Relation<String, String> containment = Relation.of(new LinkedHashMap<String, Set<String>>(),
        LinkedHashSet.class);
    private Relation<String, String> containmentCore = Relation.of(new LinkedHashMap<String, Set<String>>(),
        LinkedHashSet.class);
    //    private Relation<String, String> containmentNonDeprecated = Relation.of(new LinkedHashMap<String, Set<String>>(),
    //        LinkedHashSet.class);
    private Relation<String, String> containmentGrouping = Relation.of(new LinkedHashMap<String, Set<String>>(),
        LinkedHashSet.class);
    private Relation<String, String> containmentDeprecated = Relation.of(new LinkedHashMap<String, Set<String>>(),
        LinkedHashSet.class);
    private Relation<String, String> containerToSubdivision = Relation.of(new LinkedHashMap<String, Set<String>>(),
        LinkedHashSet.class);

    private Map<String, CurrencyNumberInfo> currencyToCurrencyNumberInfo = new TreeMap<String, CurrencyNumberInfo>();

    private Relation<String, CurrencyDateInfo> territoryToCurrencyDateInfo = Relation.of(
        new TreeMap<String, Set<CurrencyDateInfo>>(), LinkedHashSet.class);

    // private Relation<String, TelephoneCodeInfo> territoryToTelephoneCodeInfo = new Relation(new TreeMap(),
    // LinkedHashSet.class);
    private Map<String, Set<TelephoneCodeInfo>> territoryToTelephoneCodeInfo = new TreeMap<String, Set<TelephoneCodeInfo>>();

    private Set<String> multizone = new TreeSet<String>();

    private Map<String, String> zone_territory = new TreeMap<String, String>();

    private Relation<String, String> zone_aliases = Relation
        .of(new TreeMap<String, Set<String>>(), LinkedHashSet.class);

    private Map<String, Map<String, Map<String, String>>> typeToZoneToRegionToZone = new TreeMap<String, Map<String, Map<String, String>>>();
    private Relation<String, MetaZoneRange> zoneToMetaZoneRanges = Relation.of(
        new TreeMap<String, Set<MetaZoneRange>>(), TreeSet.class);
//    private Map<String, Map<String, Relation<String, String>>> deprecated = new HashMap<String, Map<String, Relation<String, String>>>();

    private Map<String, String> metazoneContinentMap = new HashMap<String, String>();
    private Set<String> allMetazones = new TreeSet<String>();

    private Map<String, String> alias_zone = new TreeMap<String, String>();

    public Relation<String, Integer> numericTerritoryMapping = Relation.of(new HashMap<String, Set<Integer>>(),
        HashSet.class);

    public Relation<String, String> alpha3TerritoryMapping = Relation.of(new HashMap<String, Set<String>>(),
        HashSet.class);

    public Relation<String, Integer> numericCurrencyCodeMapping = Relation.of(new HashMap<String, Set<Integer>>(),
        HashSet.class);

    static Map<String, SupplementalDataInfo> directory_instance = new HashMap<String, SupplementalDataInfo>();

    public Map<String, Map<String, Row.R2<List<String>, String>>> typeToTagToReplacement = new TreeMap<String, Map<String, Row.R2<List<String>, String>>>();

    Map<String, List<Row.R4<String, String, Integer, Boolean>>> languageMatch = new HashMap<String, List<Row.R4<String, String, Integer, Boolean>>>();

    public Relation<String, String> bcp47Key2Subtypes = Relation.of(new TreeMap<String, Set<String>>(), TreeSet.class);
    public Relation<String, String> bcp47Extension2Keys = Relation
        .of(new TreeMap<String, Set<String>>(), TreeSet.class);
    public Relation<Row.R2<String, String>, String> bcp47Aliases = Relation.of(
        new TreeMap<Row.R2<String, String>, Set<String>>(), LinkedHashSet.class);
    public Map<Row.R2<String, String>, String> bcp47Descriptions = new TreeMap<Row.R2<String, String>, String>();
    public Map<Row.R2<String, String>, String> bcp47Since = new TreeMap<Row.R2<String, String>, String>();
    public Map<Row.R2<String, String>, String> bcp47Preferred = new TreeMap<Row.R2<String, String>, String>();
    public Map<Row.R2<String, String>, String> bcp47Deprecated = new TreeMap<Row.R2<String, String>, String>();

    public Map<String, Row.R2<String, String>> validityInfo = new LinkedHashMap<String, Row.R2<String, String>>();
    public Map<AttributeValidityInfo, String> attributeValidityInfo = new LinkedHashMap<>();

    public enum MeasurementType {
        measurementSystem, paperSize
    }

    Map<MeasurementType, Map<String, String>> measurementData = new HashMap<MeasurementType, Map<String, String>>();
    Map<String, PreferredAndAllowedHour> timeData = new HashMap<String, PreferredAndAllowedHour>();

    public Relation<String, String> getAlpha3TerritoryMapping() {
        return alpha3TerritoryMapping;
    }

    public Relation<String, Integer> getNumericTerritoryMapping() {
        return numericTerritoryMapping;
    }

    public Relation<String, Integer> getNumericCurrencyCodeMapping() {
        return numericCurrencyCodeMapping;
    }

    /**
     * Returns type -> tag -> <replacementList, reason>, like "language" -> "sh" -> <{"sr_Latn"}, reason>
     *
     * @return
     */
    public Map<String, Map<String, R2<List<String>, String>>> getLocaleAliasInfo() {
        return typeToTagToReplacement;
    }

    public R2<List<String>, String> getDeprecatedInfo(String type, String code) {
        return typeToTagToReplacement.get(type).get(code);
    }

    public static SupplementalDataInfo getInstance(File supplementalDirectory) {
        try {
            return getInstance(supplementalDirectory.getCanonicalPath());
        } catch (IOException e) {
            throw new ICUUncheckedIOException(e);
//            throw (IllegalArgumentException) new IllegalArgumentException()
//            .initCause(e);
        }
    }

    static private SupplementalDataInfo defaultInstance = null;
    /**
     * Which directory did we come from?
     */
    final private File directory;

    /**
     * Get an instance chosen using setAsDefaultInstance(), otherwise return an instance using the default directory
     * CldrUtility.SUPPLEMENTAL_DIRECTORY
     *
     * @return
     */
    public static SupplementalDataInfo getInstance() {
        if (defaultInstance != null) return defaultInstance;
        return CLDRConfig.getInstance().getSupplementalDataInfo();
        // return getInstance(CldrUtility.SUPPLEMENTAL_DIRECTORY);
    }

    /**
     * Mark this as the default instance to be returned by getInstance()
     */
    public void setAsDefaultInstance() {
        defaultInstance = this;
    }

    public static SupplementalDataInfo getInstance(String supplementalDirectory) {
        synchronized (SupplementalDataInfo.class) {
            // Sanity checks - not null, not empty
            if (supplementalDirectory == null) {
                throw new IllegalArgumentException("Error: null supplemental directory.");
            }
            if (supplementalDirectory.isEmpty()) {
                throw new IllegalArgumentException("Error: The string passed as a parameter resolves to the empty string.");
            }
            // canonicalize path
            String canonicalpath = null;
            try {
                canonicalpath = new File(supplementalDirectory).getCanonicalPath();
            } catch (IOException e) {
                throw new ICUUncheckedIOException(e);
            }
            SupplementalDataInfo instance = directory_instance.get(canonicalpath);
            if (instance != null) {
                return instance;
            }
            //            if (!canonicalpath.equals(supplementalDirectory)) {
            //                instance = directory_instance.get(canonicalpath);
            //                if (instance != null) {
            //                    directory_instance.put(supplementalDirectory, instance);
            //                    directory_instance.put(canonicalpath,instance);
            //                    return instance;
            //                }
            //            }
            // reaching here means we have not cached the entry
            File directory = new File(canonicalpath);
            instance = new SupplementalDataInfo(directory);
            MyHandler myHandler = instance.new MyHandler();
            XMLFileReader xfr = new XMLFileReader().setHandler(myHandler);
            File files1[] = directory.listFiles();
            if (files1 == null) {
                throw new InternalError("Could not list XML Files from " + canonicalpath);
            }
            if (files1.length == 0) {
                throw new InternalError("Error: Supplemental files missing from " + directory.getAbsolutePath());
            }
            // get bcp47 files also
            File bcp47dir = instance.getBcp47Directory();
            if (!bcp47dir.isDirectory()) {
                throw new InternalError("Error: BCP47 dir is not a directory: " + bcp47dir.getAbsolutePath());
            }
            File files2[] = bcp47dir.listFiles();
            if (files2 == null || files2.length == 0) {
                throw new InternalError("Error: BCP47 files missing from " + bcp47dir.getAbsolutePath());
            }

            CBuilder<File, ArrayList<File>> builder = Builder.with(new ArrayList<File>());
            builder.addAll(files1);
            builder.addAll(files2);
            for (File file : builder.get()) {
                if (DEBUG) {
                    try {
                        System.out.println(file.getCanonicalPath());
                    } catch (IOException e) {
                    }
                }
                String name = file.toString();
                String shortName = file.getName();
                if (!shortName.endsWith(".xml") || // skip non-XML
                    shortName.startsWith("#") || // skip other junk files
                    shortName.startsWith(".")) continue; // skip dot files (backups, etc)
                xfr.read(name, -1, true);
                myHandler.cleanup();
            }

            // xfr = new XMLFileReader().setHandler(instance.new MyHandler());
            // .xfr.read(canonicalpath + "/supplementalMetadata.xml", -1, true);

            instance.makeStuffSafe();
            // cache
            //            directory_instance.put(supplementalDirectory, instance);
            directory_instance.put(canonicalpath, instance);
            //            if (!canonicalpath.equals(supplementalDirectory)) {
            //                directory_instance.put(canonicalpath, instance);
            //            }
            return instance;
        }
    }

    private File getBcp47Directory() {
        return new File(getDirectory().getParent(), "bcp47");
    }

    private SupplementalDataInfo(File directory) {
        this.directory = directory;
    }; // hide

    private void makeStuffSafe() {
        // now make stuff safe
        allLanguages.addAll(languageToPopulation.keySet());
        allLanguages.addAll(baseLanguageToPopulation.keySet());
        allLanguages = Collections.unmodifiableSet(allLanguages);
        skippedElements = Collections.unmodifiableSet(skippedElements);
        multizone = Collections.unmodifiableSet(multizone);
        zone_territory = Collections.unmodifiableMap(zone_territory);
        alias_zone = Collections.unmodifiableMap(alias_zone);
        references = Collections.unmodifiableMap(references);
        likelySubtags = Collections.unmodifiableMap(likelySubtags);
        currencyToCurrencyNumberInfo = Collections.unmodifiableMap(currencyToCurrencyNumberInfo);
        territoryToCurrencyDateInfo.freeze();
        // territoryToTelephoneCodeInfo.freeze();
        territoryToTelephoneCodeInfo = Collections.unmodifiableMap(territoryToTelephoneCodeInfo);

        typeToZoneToRegionToZone = CldrUtility.protectCollection(typeToZoneToRegionToZone);
        typeToTagToReplacement = CldrUtility.protectCollection(typeToTagToReplacement);

        zoneToMetaZoneRanges.freeze();

        containment.freeze();
        containmentCore.freeze();
        //        containmentNonDeprecated.freeze();
        containmentGrouping.freeze();
        containmentDeprecated.freeze();

        containerToSubdivision.freeze();

        CldrUtility.protectCollection(languageToBasicLanguageData);
        for (String language : languageToTerritories2.keySet()) {
            for (Pair<Boolean, Pair<Double, String>> pair : languageToTerritories2.getAll(language)) {
                languageToTerritories.put(language, pair.getSecond().getSecond());
            }
        }
        languageToTerritories2 = null; // free up the memory.
        languageToTerritories.freeze();
        zone_aliases.freeze();
        languageToScriptVariants.freeze();

        numericTerritoryMapping.freeze();
        alpha3TerritoryMapping.freeze();
        numericCurrencyCodeMapping.freeze();

        // freeze contents
        for (String language : languageToPopulation.keySet()) {
            languageToPopulation.get(language).freeze();
        }
        for (String language : baseLanguageToPopulation.keySet()) {
            baseLanguageToPopulation.get(language).freeze();
        }
        for (String territory : territoryToPopulationData.keySet()) {
            territoryToPopulationData.get(territory).freeze();
        }
        for (String territory : territoryToLanguageToPopulationData.keySet()) {
            Map<String, PopulationData> languageToPopulationDataTemp = territoryToLanguageToPopulationData
                .get(territory);
            for (String language : languageToPopulationDataTemp.keySet()) {
                languageToPopulationDataTemp.get(language).freeze();
            }
        }
        localeToPluralInfo2.put(PluralType.cardinal, Collections.unmodifiableMap(localeToPluralInfo2.get(PluralType.cardinal)));
        localeToPluralInfo2.put(PluralType.ordinal, Collections.unmodifiableMap(localeToPluralInfo2.get(PluralType.ordinal)));

        localeToPluralRanges = Collections.unmodifiableMap(localeToPluralRanges);
        for (PluralRanges pluralRanges : localeToPluralRanges.values()) {
            pluralRanges.freeze();
        }

        if (lastDayPeriodLocales != null) {
            addDayPeriodInfo();
        }
        typeToLocaleToDayPeriodInfo = CldrUtility.protectCollection(typeToLocaleToDayPeriodInfo);
        languageMatch = CldrUtility.protectCollection(languageMatch);
        bcp47Key2Subtypes.freeze();
        bcp47Extension2Keys.freeze();
        bcp47Aliases.freeze();
        if (bcp47Key2Subtypes.isEmpty()) {
            throw new InternalError("No BCP47 key 2 subtype data was loaded from bcp47 dir " + getBcp47Directory().getAbsolutePath());
        }
        CldrUtility.protectCollection(bcp47Descriptions);

        CoverageLevelInfo.fixEU(coverageLevels, this);
        coverageLevels = Collections.unmodifiableSortedSet(coverageLevels);

        measurementData = CldrUtility.protectCollection(measurementData);
        timeData = CldrUtility.protectCollection(timeData);

        validityInfo = CldrUtility.protectCollection(validityInfo);
        attributeValidityInfo = CldrUtility.protectCollection(attributeValidityInfo);
        parentLocales = Collections.unmodifiableMap(parentLocales);

        ImmutableSet.Builder<String> newScripts = ImmutableSet.<String>builder();
        Map<Validity.Status, Set<String>> scripts = Validity.getInstance().getStatusToCodes(LstrType.script);
        for (Entry<Status, Set<String>> e : scripts.entrySet()) {
            switch(e.getKey()) {
            case regular:
            case special:
            case unknown:
                newScripts.addAll(e.getValue());
                break;
            default:
                break; // do nothing
            }
        }
        CLDRScriptCodes = newScripts.build();
    }

    // private Map<String, Map<String, String>> makeUnmodifiable(Map<String, Map<String, String>>
    // metazoneToRegionToZone) {
    // Map<String, Map<String, String>> temp = metazoneToRegionToZone;
    // for (String mzone : metazoneToRegionToZone.keySet()) {
    // temp.put(mzone, Collections.unmodifiableMap(metazoneToRegionToZone.get(mzone)));
    // }
    // return Collections.unmodifiableMap(temp);
    // }


    /**
     * Core function used to process each of the paths, and add the data to the appropriate data member.
     */
    class MyHandler extends XMLFileReader.SimpleHandler {
        private static final double MAX_POPULATION = 3000000000.0;

        XPathParts parts = new XPathParts();

        LanguageTagParser languageTagParser = new LanguageTagParser();

        /**
         * Finish processing anything left hanging in the file.
         */
        public void cleanup() {
            if (lastPluralMap.size() > 0) {
                addPluralInfo(lastPluralWasOrdinal);
            }
            lastPluralLocales = "";
        }

        public void handlePathValue(String path, String value) {
            try {
                parts.set(path);
                String level0 = parts.getElement(0);
                String level1 = parts.size() < 2 ? null : parts.getElement(1);
                String level2 = parts.size() < 3 ? null : parts.getElement(2);
                String level3 = parts.size() < 4 ? null : parts.getElement(3);
                // String level4 = parts.size() < 5 ? null : parts.getElement(4);
                if (level1.equals("generation")) {
                    // skip
                    return;
                }
                if (level1.equals("version")) {
                    if (cldrVersion == null) {
                        String version = parts.getAttributeValue(1, "cldrVersion");
                        if (version == null) {
                            // old format
                            version = parts.getAttributeValue(0, "version");
                        }
                        cldrVersion = VersionInfo.getInstance(version);
                    }
                    return;
                }

                // copy the rest from ShowLanguages later
                if (level0.equals("ldmlBCP47")) {
                    if (handleBcp47(level1, level2)) {
                        return;
                    }
                } else if (level1.equals("territoryInfo")) {
                    if (handleTerritoryInfo()) {
                        return;
                    }
                } else if (level1.equals("calendarPreferenceData")) {
                    handleCalendarPreferenceData();
                    return;
                } else if (level1.equals("languageData")) {
                    handleLanguageData();
                    return;
                } else if (level1.equals("territoryContainment")) {
                    handleTerritoryContainment();
                    return;
                } else if (level1.equals("subdivisionContainment")) {
                    handleSubdivisionContainment();
                    return;
                } else if (level1.equals("currencyData")) {
                    if (handleCurrencyData(level2)) {
                        return;
                    }
                    // } else if (level1.equals("timezoneData")) {
                    // if (handleTimezoneData(level2)) {
                    // return;
                    // }
                } else if ("metazoneInfo".equals(level2)) {
                    if (handleMetazoneInfo(level2, level3)) {
                        return;
                    }
                } else if ("mapTimezones".equals(level2)) {
                    if (handleMetazoneData(level2, level3)) {
                        return;
                    }
                } else if (level1.equals("plurals")) {
                    if (addPluralPath(parts, value)) {
                        return;
                    }
                } else if (level1.equals("dayPeriodRuleSet")) {
                    addDayPeriodPath(parts, value);
                    return;
                } else if (level1.equals("telephoneCodeData")) {
                    handleTelephoneCodeData(parts);
                    return;
                } else if (level1.equals("references")) {
                    String type = parts.getAttributeValue(-1, "type");
                    String uri = parts.getAttributeValue(-1, "uri");
                    references.put(type, new Pair<String, String>(uri, value).freeze());
                    return;
                } else if (level1.equals("likelySubtags")) {
                    handleLikelySubtags();
                    return;
                } else if (level1.equals("numberingSystems")) {
                    handleNumberingSystems();
                    return;
                } else if (level1.equals("coverageLevels")) {
                    handleCoverageLevels();
                    return;
                } else if (level1.equals("parentLocales")) {
                    handleParentLocales();
                    return;
                } else if (level1.equals("metadata")) {
                    if (handleMetadata(level2, value)) {
                        return;
                    }
                } else if (level1.equals("codeMappings")) {
                    if (handleCodeMappings(level2)) {
                        return;
                    }
                } else if (level1.equals("languageMatching")) {
                    if (handleLanguageMatcher(level2)) {
                        return;
                    }
                } else if (level1.equals("measurementData")) {
                    if (handleMeasurementData(level2)) {
                        return;
                    }
                } else if (level1.equals("timeData")) {
                    if (handleTimeData(level2)) {
                        return;
                    }
                }

                // capture elements we didn't look at, since we should cover everything.
                // this helps for updates

                final String skipKey = level1 + (level2 == null ? "" : "/" + level2);
                if (!skippedElements.contains(skipKey)) {
                    skippedElements.add(skipKey);
                }
                // System.out.println("Skipped Element: " + path);
            } catch (Exception e) {
                throw (IllegalArgumentException) new IllegalArgumentException("Exception while processing path: "
                    + path + ",\tvalue: " + value).initCause(e);
            }
        }

        private boolean handleMeasurementData(String level2) {
            /**
             * <measurementSystem type="US" territories="LR MM US"/>
             * <paperSize type="A4" territories="001"/>
             */
            MeasurementType measurementType = MeasurementType.valueOf(level2);
            String type = parts.getAttributeValue(-1, "type");
            String territories = parts.getAttributeValue(-1, "territories");
            Map<String, String> data = measurementData.get(measurementType);
            if (data == null) {
                measurementData.put(measurementType, data = new HashMap<String, String>());
            }
            for (String territory : territories.trim().split("\\s+")) {
                data.put(territory, type);
            }
            return true;
        }

        private boolean handleTimeData(String level2) {
            /**
             * <hours preferred="H" allowed="H" regions="IL RU"/>
             */
            String preferred = parts.getAttributeValue(-1, "preferred");
            // String[] allowed = parts.getAttributeValue(-1, "allowed").trim().split("\\s+");
            PreferredAndAllowedHour preferredAndAllowedHour = new PreferredAndAllowedHour(preferred,
                parts.getAttributeValue(-1, "allowed"));
            for (String region : parts.getAttributeValue(-1, "regions").trim().split("\\s+")) {
                PreferredAndAllowedHour oldValue = timeData.put(region, preferredAndAllowedHour);
                if (oldValue != null) {
                    throw new IllegalArgumentException("timeData/hours must not have duplicate regions: " + region);
                }
            }
            return true;
        }

        private boolean handleBcp47(String level1, String level2) {
            if (level1.equals("version") || level1.equals("generation") || level1.equals("cldrVersion")) {
                return true; // skip
            }
            if (!level1.equals("keyword")) {
                throw new IllegalArgumentException("Unexpected level1 element: " + level1);
            }

            String finalElement = parts.getElement(-1);
            String key = parts.getAttributeValue(2, "name");
            String extension = parts.getAttributeValue(2, "extension");
            if (extension == null) {
                extension = "u";
            }
            bcp47Extension2Keys.put(extension, key);

            String keyAlias = parts.getAttributeValue(2, "alias");
            String keyDescription = parts.getAttributeValue(2, "description");
            String deprecated = parts.getAttributeValue(2, "deprecated");
            // TODO add preferred, valueType, since

            final R2<String, String> key_empty = (R2<String, String>) Row.of(key, "").freeze();

            if (keyAlias != null) {
                bcp47Aliases.putAll(key_empty, Arrays.asList(keyAlias.trim().split("\\s+")));
            }

            if (keyDescription != null) {
                bcp47Descriptions.put(key_empty, keyDescription);
            }
            if (deprecated != null && deprecated.equals("true")) {
                bcp47Deprecated.put(key_empty, deprecated);
            }

            switch (finalElement) {
            case "key":
                break; // all actions taken above

            case "type":
                String subtype = parts.getAttributeValue(3, "name");
                String subtypeAlias = parts.getAttributeValue(3, "alias");
                String subtypeDescription = parts.getAttributeValue(3, "description").replaceAll("\\s+", " ");
                String subtypeSince = parts.getAttributeValue(3, "since");
                String subtypePreferred = parts.getAttributeValue(3, "preferred");
                String subtypeDeprecated = parts.getAttributeValue(3, "deprecated");

                Set<String> set = bcp47Key2Subtypes.get(key);
                if (set != null && set.contains(key)) {
                    throw new IllegalArgumentException("Collision with bcp47 key-value: " + key + "," + subtype);
                }
                bcp47Key2Subtypes.put(key, subtype);

                final R2<String, String> key_subtype = (R2<String, String>) Row.of(key, subtype).freeze();

                if (subtypeAlias != null) {
                    bcp47Aliases.putAll(key_subtype, Arrays.asList(subtypeAlias.trim().split("\\s+")));
                }
                if (subtypeDescription != null) {
                    bcp47Descriptions.put(key_subtype, subtypeDescription.replaceAll("\\s+", " "));
                }
                if (subtypeDescription != null) {
                    bcp47Since.put(key_subtype, subtypeSince);
                }
                if (subtypePreferred != null) {
                    bcp47Preferred.put(key_subtype, subtypePreferred);
                }
                if (subtypeDeprecated != null) {
                    bcp47Deprecated.put(key_subtype, subtypeDeprecated);
                }
                break;
            default:
                throw new IllegalArgumentException("Unexpected element: " + finalElement);
            }

            return true;
        }

        private boolean handleLanguageMatcher(String level2) {
            String type = parts.getAttributeValue(2, "type");
            String alt = parts.getAttributeValue(2, "alt");
            if (alt != null) {
                type += "_" + alt;
            }
            switch(parts.getElement(3)) {
            case "paradigmLocales":
                List<String> locales = WHITESPACE_SPLTTER.splitToList(parts.getAttributeValue(3, "locales"));
                // TODO
//                LanguageMatchData languageMatchData = languageMatchData.get(type);
//                if (languageMatchData == null) {
//                    languageMatch.put(type, languageMatchData = new LanguageMatchData());
//                }
                break;
            case "matchVariable":
                String id = parts.getAttributeValue(3, "id");
                String value = parts.getAttributeValue(3, "value");
                // TODO
                break;
            case "languageMatch":
                List<R4<String, String, Integer, Boolean>> matches = languageMatch.get(type);
                if (matches == null) {
                    languageMatch.put(type, matches = new ArrayList<R4<String, String, Integer, Boolean>>());
                }
                String percent = parts.getAttributeValue(3, "percent");
                String distance = parts.getAttributeValue(3, "distance");
                matches.add(Row.of(
                    parts.getAttributeValue(3, "desired"), 
                    parts.getAttributeValue(3, "supported"),
                    percent != null ? Integer.parseInt(percent) 
                        : 100-Integer.parseInt(distance),
                        "true".equals(parts.getAttributeValue(3, "oneway"))));
                break;
                default: throw new IllegalArgumentException("Unknown element");
            }
            return true;
        }

        private boolean handleCodeMappings(String level2) {
            if (level2.equals("territoryCodes")) {
                // <territoryCodes type="VU" numeric="548" alpha3="VUT"/>
                String type = parts.getAttributeValue(-1, "type");
                final String numeric = parts.getAttributeValue(-1, "numeric");
                if (numeric != null) {
                    numericTerritoryMapping.put(type, Integer.parseInt(numeric));
                }
                final String alpha3 = parts.getAttributeValue(-1, "alpha3");
                if (alpha3 != null) {
                    alpha3TerritoryMapping.put(type, alpha3);
                }
                return true;
            } else if (level2.equals("currencyCodes")) {
                // <currencyCodes type="BBD" numeric="52"/>
                String type = parts.getAttributeValue(-1, "type");
                final String numeric = parts.getAttributeValue(-1, "numeric");
                if (numeric != null) {
                    numericCurrencyCodeMapping.put(type, Integer.parseInt(numeric));
                }
                return true;
            }
            return false;
        }

        private void handleNumberingSystems() {
            NumberingSystemInfo ns = new NumberingSystemInfo(parts);
            numberingSystems.put(ns.name, ns);
            if (ns.type == NumberingSystemType.numeric) {
                numericSystems.add(ns.name);
            }
        }

        private void handleCoverageLevels() {
            if (parts.containsElement("approvalRequirement")) {
                approvalRequirements.add(parts.toString());
            } else if (parts.containsElement("coverageLevel")) {
                String match = parts.containsAttribute("match") ? coverageVariables.replace(parts.getAttributeValue(-1,
                    "match")) : null;
                String valueStr = parts.getAttributeValue(-1, "value");
                // Ticket 7125: map the number to English. So switch from English to number for construction
                valueStr = Integer.toString(Level.get(valueStr).getLevel());

                String inLanguage = parts.containsAttribute("inLanguage") ? coverageVariables.replace(parts
                    .getAttributeValue(-1, "inLanguage")) : null;
                String inScript = parts.containsAttribute("inScript") ? coverageVariables.replace(parts
                    .getAttributeValue(-1, "inScript")) : null;
                String inTerritory = parts.containsAttribute("inTerritory") ? coverageVariables.replace(parts
                    .getAttributeValue(-1, "inTerritory")) : null;
                Integer value = (valueStr != null) ? Integer.valueOf(valueStr) : Integer.valueOf("101");
                if (cldrVersion.getMajor() < 2) {
                    value = 40;
                }
                CoverageLevelInfo ci = new CoverageLevelInfo(match, value, inLanguage, inScript, inTerritory);
                coverageLevels.add(ci);
            } else if (parts.containsElement("coverageVariable")) {
                String key = parts.getAttributeValue(-1, "key");
                String value = parts.getAttributeValue(-1, "value");
                coverageVariables.add(key, value);
            }
        }

        private void handleParentLocales() {
            String parent = parts.getAttributeValue(-1, "parent");
            String locales = parts.getAttributeValue(-1, "locales");
            String[] pl = locales.split(" ");
            for (int i = 0; i < pl.length; i++) {
                parentLocales.put(pl[i], parent);
            }
        }

        private void handleCalendarPreferenceData() {
            String territoryString = parts.getAttributeValue(-1, "territories");
            String orderingString = parts.getAttributeValue(-1, "ordering");
            String[] calendars = orderingString.split(" ");
            String[] territories = territoryString.split(" ");
            List<String> calendarList = Arrays.asList(calendars);
            for (int i = 0; i < territories.length; i++) {
                calendarPreferences.put(territories[i], calendarList);
            }
        }

        private void handleLikelySubtags() {
            String from = parts.getAttributeValue(-1, "from");
            String to = parts.getAttributeValue(-1, "to");
            likelySubtags.put(from, to);
        }

        /**
         * Only called if level2 = mapTimezones. Level 1 might be metaZones or might be windowsZones
         */
        private boolean handleMetazoneData(String level2, String level3) {
            if (level3.equals("mapZone")) {
                String maintype = parts.getAttributeValue(2, "type");
                if (maintype == null) {
                    maintype = "windows";
                }
                String mzone = parts.getAttributeValue(3, "other");
                String region = parts.getAttributeValue(3, "territory");
                String zone = parts.getAttributeValue(3, "type");

                Map<String, Map<String, String>> zoneToRegionToZone = typeToZoneToRegionToZone.get(maintype);
                if (zoneToRegionToZone == null) {
                    typeToZoneToRegionToZone.put(maintype,
                        zoneToRegionToZone = new TreeMap<String, Map<String, String>>());
                }
                Map<String, String> regionToZone = zoneToRegionToZone.get(mzone);
                if (regionToZone == null) {
                    zoneToRegionToZone.put(mzone, regionToZone = new TreeMap<String, String>());
                }
                if (region != null) {
                    regionToZone.put(region, zone);
                }
                if (maintype.equals("metazones")) {
                    if (mzone != null && region.equals("001")) {
                        metazoneContinentMap.put(mzone, zone.substring(0, zone.indexOf("/")));
                    }
                    allMetazones.add(mzone);
                }
                return true;
            }
            return false;
        }

        private Collection<String> getSpaceDelimited(int index, String attribute, Collection<String> defaultValue) {
            String temp = parts.getAttributeValue(index, attribute);
            Collection<String> elements = temp == null ? defaultValue : Arrays.asList(temp.split("\\s+"));
            return elements;
        }

        /*
         *
         * <supplementalData>
         * <metaZones>
         * <metazoneInfo>
         * <timezone type="Asia/Yerevan">
         * <usesMetazone to="1991-09-22 20:00" mzone="Yerevan"/>
         * <usesMetazone from="1991-09-22 20:00" mzone="Armenia"/>
         */

        private boolean handleMetazoneInfo(String level2, String level3) {
            if (level3.equals("timezone")) {
                String zone = parts.getAttributeValue(3, "type");
                String mzone = parts.getAttributeValue(4, "mzone");
                String from = parts.getAttributeValue(4, "from");
                String to = parts.getAttributeValue(4, "to");
                MetaZoneRange mzoneRange = new MetaZoneRange(mzone, from, to);
                zoneToMetaZoneRanges.put(zone, mzoneRange);
                return true;
            }
            return false;
        }

        private boolean handleMetadata(String level2, String value) {
            if (parts.contains("defaultContent")) {
                String defContent = parts.getAttributeValue(-1, "locales").trim();
                String[] defLocales = defContent.split("\\s+");
                defaultContentLocales = Collections.unmodifiableSet(new TreeSet<String>(Arrays.asList(defLocales)));
                return true;
            }
            if (level2.equals("alias")) {
                // <alias>
                // <!-- grandfathered 3066 codes -->
                // <languageAlias type="art-lojban" replacement="jbo"/> <!-- Lojban -->
                String level3 = parts.getElement(3);
                if (!level3.endsWith("Alias")) {
                    throw new IllegalArgumentException();
                }
                level3 = level3.substring(0, level3.length() - "Alias".length());
                boolean isSubdivision = level3.equals("subdivision");
                Map<String, R2<List<String>, String>> tagToReplacement = typeToTagToReplacement.get(level3);
                if (tagToReplacement == null) {
                    typeToTagToReplacement.put(level3,
                        tagToReplacement = new TreeMap<String, R2<List<String>, String>>());
                }
                final String replacement = parts.getAttributeValue(3, "replacement");
                List<String> replacementList = null;
                if (replacement != null) {
                    Set<String> builder = new LinkedHashSet<>();
                    for (String item : replacement.split("\\s+")) {
                        String cleaned = SubdivisionNames.isRegionCode(item) ? item : replacement.replace("-","").toLowerCase(Locale.ROOT); 
//                            : replacement.replace("-", "_");
                        builder.add(cleaned);
                    }
                    replacementList = ImmutableList.copyOf(builder);
                }
                final String reason = parts.getAttributeValue(3, "reason");
                String cleanTag = parts.getAttributeValue(3, "type");
                tagToReplacement.put(cleanTag, (R2<List<String>, String>) Row.of(replacementList, reason).freeze());
                return true;
            } else if (level2.equals("validity")) {
                // <variable id="$grandfathered" type="choice">
                String level3 = parts.getElement(3);
                if (level3.equals("variable")) {
                    Map<String, String> attributes = parts.getAttributes(-1);
                    validityInfo.put(attributes.get("id"), Row.of(attributes.get("type"), value));
                    String idString = attributes.get("id");
                    if (("$language".equals(idString) || "$languageExceptions".equals(attributes.get("id")))
                        && "choice".equals(attributes.get("type"))) {
                        String[] validCodeArray = value.trim().split("\\s+");
                        CLDRLanguageCodes.addAll(Arrays.asList(validCodeArray));
                    }
//                    if ("$script".equals(attributes.get("id")) && "choice".equals(attributes.get("type"))) {
//                        String[] validCodeArray = value.trim().split("\\s+");
//                        CLDRScriptCodes = Collections
//                            .unmodifiableSet(new TreeSet<String>(Arrays.asList(validCodeArray)));
//                    }
                    return true;
                } else if (level3.equals("attributeValues")) {
                    AttributeValidityInfo.add(parts.getAttributes(-1), value, attributeValidityInfo);
                    return true;
                }
            } else if (level2.equals("serialElements")) {
                serialElements = Arrays.asList(value.trim().split("\\s+"));
                return true;
            } else if (level2.equals("distinguishing")) {
                String level3 = parts.getElement(3);
                if (level3.equals("distinguishingItems")) {
                    Map<String, String> attributes = parts.getAttributes(-1);
                    // <distinguishingItems
                    // attributes="key request id _q registry alt iso4217 iso3166 mzone from to type numberSystem"/>
                    // <distinguishingItems exclude="true"
                    // elements="default measurementSystem mapping abbreviationFallback preferenceOrdering"
                    // attributes="type"/>

                    if (attributes.containsKey("exclude") && "true".equals(attributes.get("exclude"))) {
                        return false; // don't handle the excludes -yet.
                    } else {
                        distinguishingAttributes = Collections.unmodifiableCollection(getSpaceDelimited(-1,
                            "attributes", STAR_SET));
                        return true;
                    }
                }
            }
            return false;
        }

        private boolean handleTerritoryInfo() {

            // <territoryInfo>
            // <territory type="AD" gdp="1840000000" literacyPercent="100"
            // population="66000"> <!--Andorra-->
            // <languagePopulation type="ca" populationPercent="50"/>
            // <!--Catalan-->

            Map<String, String> territoryAttributes = parts.getAttributes(2);
            String territory = territoryAttributes.get("type");
            double territoryPopulation = parseDouble(territoryAttributes.get("population"));
            if (failsRangeCheck("population", territoryPopulation, 0, MAX_POPULATION)) {
                return true;
            }

            double territoryLiteracyPercent = parseDouble(territoryAttributes.get("literacyPercent"));
            double territoryGdp = parseDouble(territoryAttributes.get("gdp"));
            if (territoryToPopulationData.get(territory) == null) {
                territoryToPopulationData.put(territory, new PopulationData()
                    .setPopulation(territoryPopulation)
                    .setLiteratePopulation(territoryLiteracyPercent * territoryPopulation / 100)
                    .setGdp(territoryGdp));
            }
            if (parts.size() > 3) {

                Map<String, String> languageInTerritoryAttributes = parts
                    .getAttributes(3);
                String language = languageInTerritoryAttributes.get("type");
                double languageLiteracyPercent = parseDouble(languageInTerritoryAttributes.get("literacyPercent"));
                if (Double.isNaN(languageLiteracyPercent)) {
                    languageLiteracyPercent = territoryLiteracyPercent;
                }
                double writingPercent = parseDouble(languageInTerritoryAttributes.get("writingPercent"));
                if (Double.isNaN(writingPercent)) {
                    writingPercent = languageLiteracyPercent;
                }
                // else {
                // System.out.println("writingPercent\t" + languageLiteracyPercent
                // + "\tterritory\t" + territory
                // + "\tlanguage\t" + language);
                // }
                double languagePopulationPercent = parseDouble(languageInTerritoryAttributes.get("populationPercent"));
                double languagePopulation = languagePopulationPercent * territoryPopulation / 100;
                // double languageGdp = languagePopulationPercent * territoryGdp;

                // store
                Map<String, PopulationData> territoryLanguageToPopulation = territoryToLanguageToPopulationData
                    .get(territory);
                if (territoryLanguageToPopulation == null) {
                    territoryToLanguageToPopulationData.put(territory,
                        territoryLanguageToPopulation = new TreeMap<String, PopulationData>());
                }
                OfficialStatus officialStatus = OfficialStatus.unknown;
                String officialStatusString = languageInTerritoryAttributes.get("officialStatus");
                if (officialStatusString != null) officialStatus = OfficialStatus.valueOf(officialStatusString);

                PopulationData newData = new PopulationData()
                    .setPopulation(languagePopulation)
                    .setLiteratePopulation(languageLiteracyPercent * languagePopulation / 100)
                    .setWritingPopulation(writingPercent * languagePopulation / 100)
                    .setOfficialStatus(officialStatus)
                    // .setGdp(languageGdp)
                    ;
                newData.freeze();
                if (territoryLanguageToPopulation.get(language) != null) {
                    System.out
                    .println("Internal Problem in supplementalData: multiple data items for "
                        + language + ", " + territory + "\tSkipping " + newData);
                    return true;
                }

                territoryLanguageToPopulation.put(language, newData);
                // add the language, using the Pair fields to get the ordering right
                languageToTerritories2.put(language,
                    Pair.of(newData.getOfficialStatus().isMajor() ? false : true,
                        Pair.of(-newData.getLiteratePopulation(), territory)));

                // now collect data for languages globally
                PopulationData data = languageToPopulation.get(language);
                if (data == null) {
                    languageToPopulation.put(language, data = new PopulationData().set(newData));
                } else {
                    data.add(newData);
                }
                // if (language.equals("en")) {
                // System.out.println(territory + "\tnewData:\t" + newData + "\tdata:\t" + data);
                // }
                String baseLanguage = languageTagParser.set(language).getLanguage();
                data = baseLanguageToPopulation.get(baseLanguage);
                if (data == null) {
                    baseLanguageToPopulation.put(baseLanguage, data = new PopulationData().set(newData));
                } else {
                    data.add(newData);
                }
                if (!baseLanguage.equals(language)) {
                    languageToScriptVariants.put(baseLanguage, language);
                }
            }
            return true;
        }

        private boolean handleCurrencyData(String level2) {
            if (level2.equals("fractions")) {
                // <info iso4217="ADP" digits="0" rounding="0" cashRounding="5"/>
                currencyToCurrencyNumberInfo.put(parts.getAttributeValue(3, "iso4217"),
                    new CurrencyNumberInfo(
                        parseIntegerOrNull(parts.getAttributeValue(3, "digits")),
                        parseIntegerOrNull(parts.getAttributeValue(3, "rounding")),
                        parseIntegerOrNull(parts.getAttributeValue(3, "cashDigits")),
                        parseIntegerOrNull(parts.getAttributeValue(3, "cashRounding")))
                    );
                return true;
            }
            /*
             * <region iso3166="AD">
             * <currency iso4217="EUR" from="1999-01-01"/>
             * <currency iso4217="ESP" from="1873" to="2002-02-28"/>
             */
            if (level2.equals("region")) {
                territoryToCurrencyDateInfo.put(parts.getAttributeValue(2, "iso3166"),
                    new CurrencyDateInfo(parts.getAttributeValue(3, "iso4217"),
                        parts.getAttributeValue(3, "from"),
                        parts.getAttributeValue(3, "to"),
                        parts.getAttributeValue(3, "tender")));
                return true;
            }

            return false;
        }

        private void handleTelephoneCodeData(XPathParts parts) {
            // element 2: codesByTerritory territory [draft] [references]
            String terr = parts.getAttributeValue(2, "territory");
            // element 3: telephoneCountryCode code [from] [to] [draft] [references] [alt]
            TelephoneCodeInfo tcInfo = new TelephoneCodeInfo(parts.getAttributeValue(3, "code"),
                parts.getAttributeValue(3, "from"),
                parts.getAttributeValue(3, "to"),
                parts.getAttributeValue(3, "alt"));

            Set<TelephoneCodeInfo> tcSet = territoryToTelephoneCodeInfo.get(terr);
            if (tcSet == null) {
                tcSet = new LinkedHashSet<TelephoneCodeInfo>();
                territoryToTelephoneCodeInfo.put(terr, tcSet);
            }
            tcSet.add(tcInfo);
        }

        private void handleTerritoryContainment() {
            // <group type="001" contains="002 009 019 142 150"/>
            final String container = parts.getAttributeValue(-1, "type");
            final List<String> contained = Arrays
                .asList(parts.getAttributeValue(-1, "contains").split("\\s+"));
            // everything!
            containment.putAll(container, contained);

            String status = parts.getAttributeValue(-1, "status");
            String grouping = parts.getAttributeValue(-1, "grouping");
            if (status == null && grouping == null) {
                containmentCore.putAll(container, contained);
            }
            if (status != null && status.equals("deprecated")) {
                containmentDeprecated.putAll(container, contained);
            }
            if (grouping != null) {
                containmentGrouping.putAll(container, contained);
            }
        }

        private void handleSubdivisionContainment() {
            //      <subgroup type="AL" subtype="04" contains="FR MK LU"/>
            final String country = parts.getAttributeValue(-1, "type");
            final String subtype = parts.getAttributeValue(-1, "subtype");
            final String container = subtype == null ? country : (country + subtype).toLowerCase(Locale.ROOT);
            for (String contained : parts.getAttributeValue(-1, "contains").split("\\s+")) {
                String newContained = contained.charAt(0) >= 'a' ? contained : (country + contained).toLowerCase(Locale.ROOT);
                containerToSubdivision.put(container, newContained);
            }
        }

        private void handleLanguageData() {
            // <languageData>
            // <language type="aa" scripts="Latn" territories="DJ ER ET"/> <!--
            // Reflecting submitted data, cldrbug #1013 -->
            // <language type="ab" scripts="Cyrl" territories="GE"
            // alt="secondary"/>
            String language = (String) parts.getAttributeValue(2, "type");
            BasicLanguageData languageData = new BasicLanguageData();
            languageData
            .setType(parts.getAttributeValue(2, "alt") == null ? BasicLanguageData.Type.primary
                : BasicLanguageData.Type.secondary);
            languageData.setScripts(parts.getAttributeValue(2, "scripts"))
            .setTerritories(parts.getAttributeValue(2, "territories"));
            Map<Type, BasicLanguageData> map = languageToBasicLanguageData.get(language);
            if (map == null) {
                languageToBasicLanguageData.put(language, map = new EnumMap<Type, BasicLanguageData>(
                    BasicLanguageData.Type.class));
            }
            if (map.containsKey(languageData.type)) {
                throw new IllegalArgumentException("Duplicate value:\t" + parts);
            }
            map.put(languageData.type, languageData);
        }

        private boolean failsRangeCheck(String path, double input, double min, double max) {
            if (input >= min && input <= max) {
                return false;
            }
            System.out
            .println("Internal Problem in supplementalData: range check fails for "
                + input + ", min: " + min + ", max:" + max + "\t" + path);

            return false;
        }

        private double parseDouble(String literacyString) {
            return literacyString == null ? Double.NaN : Double
                .parseDouble(literacyString);
        }
    }

    public class CoverageVariableInfo {
        public Set<String> targetScripts;
        public Set<String> targetTerritories;
        public Set<String> calendars;
        public Set<String> targetCurrencies;
        public Set<String> targetTimeZones;
        public Set<String> targetPlurals;
    }

    public static String toRegexString(Set<String> s) {
        Iterator<String> it = s.iterator();
        StringBuilder sb = new StringBuilder("(");
        int count = 0;
        while (it.hasNext()) {
            if (count > 0) {
                sb.append("|");
            }
            sb.append(it.next());
            count++;
        }
        sb.append(")");
        return sb.toString();

    }

    public int parseIntegerOrNull(String attributeValue) {
        return attributeValue == null ? -1 : Integer.parseInt(attributeValue);
    }

    Set<String> skippedElements = new TreeSet<String>();

    private Map<String, Pair<String, String>> references = new TreeMap<String, Pair<String, String>>();
    private Map<String, String> likelySubtags = new TreeMap<String, String>();
    // make public temporarily until we resolve.
    private SortedSet<CoverageLevelInfo> coverageLevels = new TreeSet<CoverageLevelInfo>();
    private Map<String, String> parentLocales = new HashMap<String, String>();
    private Map<String, List<String>> calendarPreferences = new HashMap<String, List<String>>();
    private Map<String, CoverageVariableInfo> localeSpecificVariables = new TreeMap<String, CoverageVariableInfo>();
    private VariableReplacer coverageVariables = new VariableReplacer();
    private Map<String, NumberingSystemInfo> numberingSystems = new HashMap<String, NumberingSystemInfo>();
    private Set<String> numericSystems = new TreeSet<String>();
    private Set<String> defaultContentLocales;
    public Map<CLDRLocale, CLDRLocale> baseToDefaultContent; // wo -> wo_Arab_SN
    public Map<CLDRLocale, CLDRLocale> defaultContentToBase; // wo_Arab_SN -> wo
    private Set<String> CLDRLanguageCodes = new TreeSet<String>();;
    private Set<String> CLDRScriptCodes;

    /**
     * Get the population data for a language. Warning: if the language has script variants, cycle on those variants.
     *
     * @param language
     * @param output
     * @return
     */
    public PopulationData getLanguagePopulationData(String language) {
        return languageToPopulation.get(language);
    }

    public PopulationData getBaseLanguagePopulationData(String language) {
        return baseLanguageToPopulation.get(language);
    }

    public Set<String> getLanguages() {
        return allLanguages;
    }

    public Set<String> getTerritoryToLanguages(String territory) {
        Map<String, PopulationData> result = territoryToLanguageToPopulationData
            .get(territory);
        if (result == null) {
            return Collections.emptySet();
        }
        return result.keySet();
    }

    public PopulationData getLanguageAndTerritoryPopulationData(String language,
        String territory) {
        Map<String, PopulationData> result = territoryToLanguageToPopulationData
            .get(territory);
        if (result == null) {
            return null;
        }
        return result.get(language);
    }

    public Set<String> getTerritoriesWithPopulationData() {
        return territoryToLanguageToPopulationData.keySet();
    }

    public Set<String> getLanguagesForTerritoryWithPopulationData(String territory) {
        return territoryToLanguageToPopulationData.get(territory).keySet();
    }

    public Set<BasicLanguageData> getBasicLanguageData(String language) {
        Map<Type, BasicLanguageData> map = languageToBasicLanguageData.get(language);
        if (map == null) {
            throw new IllegalArgumentException("Bad language code: " + language);
        }
        return new LinkedHashSet<BasicLanguageData>(map.values());
    }

    public Map<Type, BasicLanguageData> getBasicLanguageDataMap(String language) {
        return languageToBasicLanguageData.get(language);
    }

    public Set<String> getBasicLanguageDataLanguages() {
        return languageToBasicLanguageData.keySet();
    }

    public Relation<String, String> getContainmentCore() {
        return containmentCore;
    }

    public Set<String> getContained(String territoryCode) {
        return containment.getAll(territoryCode);
    }

    public Set<String> getContainers() {
        return containment.keySet();
    }

    public Set<String> getContainedSubdivisions(String territoryOrSubdivisionCode) {
        return containerToSubdivision.getAll(territoryOrSubdivisionCode);
    }

    public Set<String> getContainersForSubdivisions() {
        return containerToSubdivision.keySet();
    }

    public Relation<String, String> getTerritoryToContained() {
        return getTerritoryToContained(ContainmentStyle.all); // true
    }

    //    public Relation<String, String> getTerritoryToContained(boolean allowDeprecated) {
    //        return allowDeprecated ? containment : containmentNonDeprecated;
    //    }
    //
    public enum ContainmentStyle {
        all, core, grouping, deprecated
    }

    public Relation<String, String> getTerritoryToContained(ContainmentStyle containmentStyle) {
        switch (containmentStyle) {
        case all:
            return containment;
        case core:
            return containmentCore;
        case grouping:
            return containmentGrouping;
        case deprecated:
            return containmentDeprecated;
        }
        throw new IllegalArgumentException("internal error");
    }

    public Set<String> getSkippedElements() {
        return skippedElements;
    }

    public Set<String> getZone_aliases(String zone) {
        Set<String> result = zone_aliases.getAll(zone);
        if (result == null) {
            return Collections.emptySet();
        }
        return result;
    }

    public String getZone_territory(String zone) {
        return zone_territory.get(zone);
    }

    public Set<String> getCanonicalZones() {
        return zone_territory.keySet();
    }

    /**
     * Return the multizone countries (should change name).
     *
     * @return
     */
    public Set<String> getMultizones() {
        // TODO Auto-generated method stub
        return multizone;
    }

    private Set<String> singleRegionZones;

    public Set<String> getSingleRegionZones() {
        synchronized (this) {
            if (singleRegionZones == null) {
                singleRegionZones = new HashSet<String>();
                SupplementalDataInfo supplementalData = this; // TODO: this?
                Set<String> multizoneCountries = supplementalData.getMultizones();
                for (String zone : supplementalData.getCanonicalZones()) {
                    String region = supplementalData.getZone_territory(zone);
                    if (!multizoneCountries.contains(region) || zone.startsWith("Etc/")) {
                        singleRegionZones.add(zone);
                    }
                }
                singleRegionZones.remove("Etc/Unknown"); // remove special case
                singleRegionZones = Collections.unmodifiableSet(singleRegionZones);
            }
        }
        return singleRegionZones;
    }

    public Set<String> getTerritoriesForPopulationData(String language) {
        return languageToTerritories.getAll(language);
    }

    public Set<String> getLanguagesForTerritoriesPopulationData() {
        return languageToTerritories.keySet();
    }

    /**
     * Return the list of default content locales.
     *
     * @return
     */
    public Set<String> getDefaultContentLocales() {
        return defaultContentLocales;
    }

    public static Map<String, String> makeLocaleToDefaultContents(Set<String> defaultContents,
        Map<String, String> result, Set<String> errors) {
        for (String s : defaultContents) {
            String simpleParent = LanguageTagParser.getSimpleParent(s);
            String oldValue = result.get(simpleParent);
            if (oldValue != null) {
                errors.add("*** Error: Default contents cannot contain two children for the same parent:\t"
                    + oldValue + ", " + s + "; keeping " + oldValue);
                continue;
            }
            result.put(simpleParent, s);
        }
        return result;
    }

    /**
     * Return the list of default content locales.
     *
     * @return
     */
    public Set<CLDRLocale> getDefaultContentCLDRLocales() {
        initCLDRLocaleBasedData();
        return defaultContentToBase.keySet();
    }

    /**
     * Get the default content locale for a specified language
     *
     * @param language
     *            language to search
     * @return default content, or null if none
     */
    public String getDefaultContentLocale(String language) {
        for (String dc : defaultContentLocales) {
            if (dc.startsWith(language + "_")) {
                return dc;
            }
        }
        return null;
    }

    /**
     * Get the default content locale for a specified language and script.
     * If script is null, delegates to {@link #getDefaultContentLocale(String)}
     *
     * @param language
     * @param script
     *            if null, delegates to {@link #getDefaultContentLocale(String)}
     * @return default content, or null if none
     */
    public String getDefaultContentLocale(String language, String script) {
        if (script == null) return getDefaultContentLocale(language);
        for (String dc : defaultContentLocales) {
            if (dc.startsWith(language + "_" + script + "_")) {
                return dc;
            }
        }
        return null;
    }

    /**
     * Given a default locale (such as 'wo_Arab_SN') return the base locale (such as 'wo'), or null if the input wasn't
     * a default conetnt locale.
     *
     * @param baseLocale
     * @return
     */
    public CLDRLocale getBaseFromDefaultContent(CLDRLocale dcLocale) {
        initCLDRLocaleBasedData();
        return defaultContentToBase.get(dcLocale);
    }

    /**
     * Given a base locale (such as 'wo') return the default content locale (such as 'wo_Arab_SN'), or null.
     *
     * @param baseLocale
     * @return
     */
    public CLDRLocale getDefaultContentFromBase(CLDRLocale baseLocale) {
        initCLDRLocaleBasedData();
        return baseToDefaultContent.get(baseLocale);
    }

    /**
     * Is this a default content locale?
     *
     * @param dcLocale
     * @return
     */
    public boolean isDefaultContent(CLDRLocale dcLocale) {
        initCLDRLocaleBasedData();
        if (dcLocale == null) throw new NullPointerException("null locale");
        return (defaultContentToBase.get(dcLocale) != null);
    }

    public Set<String> getNumberingSystems() {
        return numberingSystems.keySet();
    }

    public Set<String> getNumericNumberingSystems() {
        return Collections.unmodifiableSet(numericSystems);
    }

    public String getDigits(String numberingSystem) {
        return numberingSystems.get(numberingSystem).digits;
    }

    public NumberingSystemType getNumberingSystemType(String numberingSystem) {
        return numberingSystems.get(numberingSystem).type;
    }

    public SortedSet<CoverageLevelInfo> getCoverageLevelInfo() {
        return coverageLevels;
    }

    /**
     * Used to get the coverage value for a path. This is generally the most
     * efficient way for tools to get coverage.
     *
     * @param xpath
     * @param loc
     * @return
     */
    public Level getCoverageLevel(String xpath, String loc) {
        Level result = null;
        result = coverageCache.get(xpath, loc);
        if (result == null) {
            CoverageLevel2 cov = localeToCoverageLevelInfo.get(loc);
            if (cov == null) {
                cov = CoverageLevel2.getInstance(this, loc);
                localeToCoverageLevelInfo.put(loc, cov);
            }

            result = cov.getLevel(xpath);
            coverageCache.put(xpath, loc, result);
        }
        return result;
    }

    /**
     * Cache Data structure with object expiry,
     * List that can hold up to MAX_LOCALES caches of locales, when one locale hasn't been used for a while it will removed and GC'd
     */
    private class CoverageCache {
        private final Deque<Node> localeList = new LinkedList<>();
        private final int MAX_LOCALES = 10;

        /**
         * Object to sync on for modifying the locale list
         */
        private final Object LOCALE_LIST_ITER_SYNC = new Object();

        /*
         * constructor
         */
        public CoverageCache() {
//            localeList = new LinkedList<Node>();
        }

        /*
         * retrieves coverage level associated with two keys if it exists in the cache, otherwise returns null
         * @param xpath
         * @param loc
         * @return the coverage level of the above two keys
         */
        public Level get(String xpath, String loc) {
            synchronized (LOCALE_LIST_ITER_SYNC) {
                Iterator<Node> it = localeList.iterator();
                Node reAddNode = null;
                while (it.hasNext()) {
//            for (Iterator<Node> it = localeList.iterator(); it.hasNext();) {
                    Node node = it.next();
                    if (node.loc.equals(loc)) {
                        reAddNode = node;
                        it.remove();
                        break;

                    }
                }
                if (reAddNode != null) {
                    localeList.addFirst(reAddNode);
                    return reAddNode.map.get(xpath);
                }
                return null;
            }
        }

        /*
         * places a coverage level into the cache, with two keys
         * @param xpath
         * @param loc
         * @param covLevel    the coverage level of the above two keys
         */
        public void put(String xpath, String loc, Level covLevel) {
            synchronized (LOCALE_LIST_ITER_SYNC) {
                //if locale's map is already in the cache add to it
//            for (Iterator<Node> it = localeList.iterator(); it.hasNext();) {
                for (Node node : localeList) {
//                Node node = it.next();
                    if (node.loc.equals(loc)) {
                        node.map.put(xpath, covLevel);
                        return;
                    }
                }

                //if it is not, add a new map with the coverage level, and remove the last map in the list (used most seldom) if the list is too large
                Map<String, Level> newMap = new ConcurrentHashMap<String, Level>();
                newMap.put(xpath, covLevel);
                localeList.addFirst(new Node(loc, newMap));

                if (localeList.size() > MAX_LOCALES) {
                    localeList.removeLast();
                }
            }
        }

        /*
         * node to hold a location and a Map
         */
        private class Node {
            //public fields to emulate a C/C++ struct
            public String loc;
            public Map<String, Level> map;

            public Node(String _loc, Map<String, Level> _map) {
                loc = _loc;
                map = _map;
            }
        }
    }

    /**
     * Used to get the coverage value for a path. Note, it is more efficient to create
     * a CoverageLevel2 for a language, and keep it around.
     *
     * @param xpath
     * @param loc
     * @return
     */
    public int getCoverageValue(String xpath, String loc) {
        return getCoverageLevel(xpath, loc).getLevel();
    }

    private RegexLookup<Level> coverageLookup = null;

    public synchronized RegexLookup<Level> getCoverageLookup() {
        if (coverageLookup == null) {
            RegexLookup<Level> lookup = new RegexLookup<Level>(RegexLookup.LookupType.STAR_PATTERN_LOOKUP);

            Matcher variable = PatternCache.get("\\$\\{[A-Za-z][\\-A-Za-z]*\\}").matcher("");

            for (CoverageLevelInfo ci : getCoverageLevelInfo()) {
                String pattern = ci.match.replace('\'', '"')
                    .replace("[@", "\\[@") // make sure that attributes are quoted
                    .replace("(", "(?:") // make sure that there are no capturing groups (beyond what we generate
                    .replace("(?:?!", "(?!"); // Allow negative lookahead
                pattern = "^//ldml/" + pattern + "$"; // for now, force a complete match
                String variableType = null;
                variable.reset(pattern);
                if (variable.find()) {
                    pattern = pattern.substring(0, variable.start()) + "([^\"]*)" + pattern.substring(variable.end());
                    variableType = variable.group();
                    if (variable.find()) {
                        throw new IllegalArgumentException("We can only handle a single variable on a line");
                    }
                }

                // .replaceAll("\\]","\\\\]");
                lookup.add(new CoverageLevel2.MyRegexFinder(pattern, variableType, ci), ci.value);
            }
            coverageLookup = lookup;
        }
        return coverageLookup;
    }

    /**
     * This appears to be unused, so didn't provide new version.
     *
     * @param xpath
     * @return
     */
    public int getCoverageValueOld(String xpath) {
        ULocale loc = new ULocale("und");
        return getCoverageValueOld(xpath, loc);
    }

    /**
     * Older version of code.
     *
     * @param xpath
     * @param loc
     * @return
     */
    public int getCoverageValueOld(String xpath, ULocale loc) {
        String targetLanguage = loc.getLanguage();

        CoverageVariableInfo cvi = getCoverageVariableInfo(targetLanguage);
        String targetScriptString = toRegexString(cvi.targetScripts);
        String targetTerritoryString = toRegexString(cvi.targetTerritories);
        String calendarListString = toRegexString(cvi.calendars);
        String targetCurrencyString = toRegexString(cvi.targetCurrencies);
        String targetTimeZoneString = toRegexString(cvi.targetTimeZones);
        String targetPluralsString = toRegexString(cvi.targetPlurals);
        Iterator<CoverageLevelInfo> i = coverageLevels.iterator();
        while (i.hasNext()) {
            CoverageLevelInfo ci = i.next();
            String regex = "//ldml/" + ci.match.replace('\'', '"')
            .replaceAll("\\[", "\\\\[")
            .replaceAll("\\]", "\\\\]")
            .replace("${Target-Language}", targetLanguage)
            .replace("${Target-Scripts}", targetScriptString)
            .replace("${Target-Territories}", targetTerritoryString)
            .replace("${Target-TimeZones}", targetTimeZoneString)
            .replace("${Target-Currencies}", targetCurrencyString)
            .replace("${Target-Plurals}", targetPluralsString)
            .replace("${Calendar-List}", calendarListString);

            // Special logic added for coverage fields that are only to be applicable
            // to certain territories
            if (ci.inTerritory != null) {
                if (ci.inTerritory.equals("EU")) {
                    Set<String> containedTerritories = new HashSet<String>();
                    containedTerritories.addAll(getContained(ci.inTerritory));
                    containedTerritories.retainAll(cvi.targetTerritories);
                    if (containedTerritories.isEmpty()) {
                        continue;
                    }
                }
                else {
                    if (!cvi.targetTerritories.contains(ci.inTerritory)) {
                        continue;
                    }
                }
            }
            // Special logic added for coverage fields that are only to be applicable
            // to certain languages
            if (ci.inLanguage != null && !ci.inLanguage.matcher(targetLanguage).matches()) {
                continue;
            }

            // Special logic added for coverage fields that are only to be applicable
            // to certain scripts
            if (ci.inScript != null && !cvi.targetScripts.contains(ci.inScript)) {
                continue;
            }

            if (xpath.matches(regex)) {
                return ci.value.getLevel();
            }

            if (xpath.matches(regex)) {
                return ci.value.getLevel();
            }
        }
        return Level.OPTIONAL.getLevel(); // If no match then return highest possible value
    }

    public CoverageVariableInfo getCoverageVariableInfo(String targetLanguage) {
        CoverageVariableInfo cvi;
        if (localeSpecificVariables.containsKey(targetLanguage)) {
            cvi = localeSpecificVariables.get(targetLanguage);
        } else {
            cvi = new CoverageVariableInfo();
            cvi.targetScripts = getTargetScripts(targetLanguage);
            cvi.targetTerritories = getTargetTerritories(targetLanguage);
            cvi.calendars = getCalendars(cvi.targetTerritories);
            cvi.targetCurrencies = getCurrentCurrencies(cvi.targetTerritories);
            cvi.targetTimeZones = getCurrentTimeZones(cvi.targetTerritories);
            cvi.targetPlurals = getTargetPlurals(targetLanguage);
            localeSpecificVariables.put(targetLanguage, cvi);
        }
        return cvi;
    }

    private Set<String> getTargetScripts(String language) {
        Set<String> targetScripts = new HashSet<String>();
        try {
            Set<BasicLanguageData> langData = getBasicLanguageData(language);
            Iterator<BasicLanguageData> ldi = langData.iterator();
            while (ldi.hasNext()) {
                BasicLanguageData bl = ldi.next();
                Set<String> addScripts = bl.scripts;
                if (addScripts != null && bl.getType() != BasicLanguageData.Type.secondary) {
                    targetScripts.addAll(addScripts);
                }
            }
        } catch (Exception e) {
            // fall through
        }

        if (targetScripts.size() == 0) {
            targetScripts.add("Zzzz"); // Unknown Script
        }
        return targetScripts;
    }

    private Set<String> getTargetTerritories(String language) {
        Set<String> targetTerritories = new HashSet<String>();
        try {
            Set<BasicLanguageData> langData = getBasicLanguageData(language);
            Iterator<BasicLanguageData> ldi = langData.iterator();
            while (ldi.hasNext()) {
                BasicLanguageData bl = ldi.next();
                Set<String> addTerritories = bl.territories;
                if (addTerritories != null && bl.getType() != BasicLanguageData.Type.secondary) {
                    targetTerritories.addAll(addTerritories);
                }
            }
        } catch (Exception e) {
            // fall through
        }
        if (targetTerritories.size() == 0) {
            targetTerritories.add("ZZ");
        }
        return targetTerritories;
    }

    private Set<String> getCalendars(Set<String> territories) {
        Set<String> targetCalendars = new HashSet<String>();
        Iterator<String> it = territories.iterator();
        while (it.hasNext()) {
            List<String> addCalendars = getCalendars(it.next());
            if (addCalendars == null) {
                continue;
            }
            targetCalendars.addAll(addCalendars);
        }
        return targetCalendars;
    }

    /**
     * @param territory
     * @return a list the calendars used in the specified territorys
     */
    public List<String> getCalendars(String territory) {
        return calendarPreferences.get(territory);
    }

    private Set<String> getCurrentCurrencies(Set<String> territories) {
        Date now = new Date();
        return getCurrentCurrencies(territories, now, now);
    }

    public Set<String> getCurrentCurrencies(Set<String> territories, Date startsBefore, Date endsAfter) {
        Set<String> targetCurrencies = new HashSet<String>();
        Iterator<String> it = territories.iterator();
        while (it.hasNext()) {
            Set<CurrencyDateInfo> targetCurrencyInfo = getCurrencyDateInfo(it.next());
            if (targetCurrencyInfo == null) {
                continue;
            }
            Iterator<CurrencyDateInfo> it2 = targetCurrencyInfo.iterator();
            while (it2.hasNext()) {
                CurrencyDateInfo cdi = it2.next();
                if (cdi.getStart().before(startsBefore) && cdi.getEnd().after(endsAfter) && cdi.isLegalTender()) {
                    targetCurrencies.add(cdi.getCurrency());
                }
            }
        }
        return targetCurrencies;
    }

    private Set<String> getCurrentTimeZones(Set<String> territories) {
        Set<String> targetTimeZones = new HashSet<String>();
        Iterator<String> it = territories.iterator();
        while (it.hasNext()) {
            String[] countryIDs = TimeZone.getAvailableIDs(it.next());
            for (int i = 0; i < countryIDs.length; i++) {
                targetTimeZones.add(countryIDs[i]);
            }
        }
        return targetTimeZones;
    }

    private Set<String> getTargetPlurals(String language) {
        Set<String> targetPlurals = new HashSet<String>();
        targetPlurals.addAll(getPlurals(PluralType.cardinal, language).getCanonicalKeywords());
        // TODO: Kept 0 and 1 specifically until Mark figures out what to do with them.
        // They should be removed once this is done.
        targetPlurals.add("0");
        targetPlurals.add("1");
        return targetPlurals;
    }

    public String getExplicitParentLocale(String loc) {
        return parentLocales.get(loc);
    }

    public Set<String> getExplicitChildren() {
        return parentLocales.keySet();
    }

    public Collection<String> getExplicitParents() {
        return parentLocales.values();
    }

    private final static class ApprovalRequirementMatcher {
        @Override
        public String toString() {
            return locales + " / " + xpathMatcher + " = " + requiredVotes;
        }

        ApprovalRequirementMatcher(String xpath) {
            XPathParts parts = new XPathParts();
            parts.set(xpath);
            if (parts.containsElement("approvalRequirement")) {
                requiredVotes = Integer.parseInt(parts.getAttributeValue(-1, "votes"));
                String localeAttrib = parts.getAttributeValue(-1, "locales");
                if (localeAttrib == null || localeAttrib.equals(STAR) || localeAttrib.isEmpty()) {
                    locales = null; // no locale listed == '*'
                } else {
                    Set<CLDRLocale> localeList = new HashSet<CLDRLocale>();
                    String[] el = localeAttrib.split(" ");
                    for (int i = 0; i < el.length; i++) {
                        if (el[i].indexOf(":") == -1) { // Just a simple locale designation
                            localeList.add(CLDRLocale.getInstance(el[i]));
                        } else { // Org:CoverageLevel
                            String[] coverageLocaleParts = el[i].split(":", 2);
                            String org = coverageLocaleParts[0];
                            String level = coverageLocaleParts[1].toUpperCase();
                            Set<String> coverageLocales = sc.getLocaleCoverageLocales(Organization.fromString(org), EnumSet.of(Level.fromString(level)));
                            for (String cl : coverageLocales) {
                                localeList.add(CLDRLocale.getInstance(cl));
                            }
                        }
                    }
                    locales = Collections.unmodifiableSet(localeList);
                }
                String xpathMatch = parts.getAttributeValue(-1, "paths");
                if (xpathMatch == null || xpathMatch.isEmpty() || xpathMatch.equals(STAR)) {
                    xpathMatcher = null;
                } else {
                    xpathMatcher = PatternCache.get(xpathMatch);
                }
            } else {
                throw new RuntimeException("Unknown approval requirement: " + xpath);
            }
        }

        final private Set<CLDRLocale> locales;
        final private Pattern xpathMatcher;
        final int requiredVotes;

        public static List<ApprovalRequirementMatcher> buildAll(List<String> approvalRequirements) {
            List<ApprovalRequirementMatcher> newList = new LinkedList<ApprovalRequirementMatcher>();

            for (String xpath : approvalRequirements) {
                newList.add(new ApprovalRequirementMatcher(xpath));
            }

            return Collections.unmodifiableList(newList);
        }

        public boolean matches(CLDRLocale loc, PathHeader ph) {
            if (DEBUG) System.err.println(">> testing " + loc + " / " + ph + " vs " + toString());
            if (locales != null) {
                if (!locales.contains(loc)) {
                    return false;
                }
            }
            if (xpathMatcher != null) {
                if (ph != null) {
                    if (!xpathMatcher.matcher(ph.getOriginalPath()).matches()) {
                        return false;
                    } else {
                        return true;
                    }
                } else {
                    return false;
                }
            }
            return true;
        }

        public int getRequiredVotes() {
            return requiredVotes;
        }
    }

    // run these from first to last to get the approval info.
    volatile List<ApprovalRequirementMatcher> approvalMatchers = null;

    /**
     * Only called by VoteResolver.
     * @param loc
     * @param PathHeader - which path this is applied to, or null if unknown.
     * @return
     */
    public int getRequiredVotes(CLDRLocale loc, PathHeader ph) {
        if (approvalMatchers == null) {
            approvalMatchers = ApprovalRequirementMatcher.buildAll(approvalRequirements);
        }

        for (ApprovalRequirementMatcher m : approvalMatchers) {
            if (m.matches(loc, ph)) {
                return m.getRequiredVotes();
            }
        }
        throw new RuntimeException("Error: " + loc + " " + ph + " ran off the end of the approvalMatchers.");
    }

    /**
     * Return the canonicalized zone, or null if there is none.
     *
     * @param alias
     * @return
     */
    public String getZoneFromAlias(String alias) {
        String zone = alias_zone.get(alias);
        if (zone != null)
            return zone;
        if (zone_territory.get(alias) != null)
            return alias;
        return null;
    }

    public boolean isCanonicalZone(String alias) {
        return zone_territory.get(alias) != null;
    }

    /**
     * Return the approximate economic weight of this language, computed by taking
     * all of the languages in each territory, looking at the literate population
     * and dividing up the GDP of the territory (in PPP) according to the
     * proportion that language has of the total. This is only an approximation,
     * since the language information is not complete, languages may overlap
     * (bilingual speakers), the literacy figures may be estimated, and literacy
     * is only a rough proxy for weight of each language in the economy of the
     * territory.
     *
     * @param language
     * @return
     */
    public double getApproximateEconomicWeight(String targetLanguage) {
        double weight = 0;
        Set<String> territories = getTerritoriesForPopulationData(targetLanguage);
        if (territories == null) return weight;
        for (String territory : territories) {
            Set<String> languagesInTerritory = getTerritoryToLanguages(territory);
            double totalLiteratePopulation = 0;
            double targetLiteratePopulation = 0;
            for (String language : languagesInTerritory) {
                PopulationData populationData = getLanguageAndTerritoryPopulationData(
                    language, territory);
                totalLiteratePopulation += populationData.getLiteratePopulation();
                if (language.equals(targetLanguage)) {
                    targetLiteratePopulation = populationData.getLiteratePopulation();
                }
            }
            PopulationData territoryPopulationData = getPopulationDataForTerritory(territory);
            final double gdp = territoryPopulationData.getGdp();
            final double scaledGdp = gdp * targetLiteratePopulation / totalLiteratePopulation;
            if (scaledGdp > 0) {
                weight += scaledGdp;
            } else {
                // System.out.println("?\t" + territory + "\t" + targetLanguage);
            }
        }
        return weight;
    }

    public PopulationData getPopulationDataForTerritory(String territory) {
        return territoryToPopulationData.get(territory);
    }

    public Set<String> getScriptVariantsForPopulationData(String language) {
        return languageToScriptVariants.getAll(language);
    }

    public Map<String, Pair<String, String>> getReferences() {
        return references;
    }

    public Map<String, Map<String, String>> getMetazoneToRegionToZone() {
        return typeToZoneToRegionToZone.get("metazones");
    }

    public String getZoneForMetazoneByRegion(String metazone, String region) {
        String result = null;
        if (getMetazoneToRegionToZone().containsKey(metazone)) {
            Map<String, String> myMap = getMetazoneToRegionToZone().get(metazone);
            if (myMap.containsKey(region)) {
                result = myMap.get(region);
            } else {
                result = myMap.get("001");
            }
        }

        if (result == null) {
            result = "Etc/GMT";
        }

        return result;
    }

    public Map<String, Map<String, Map<String, String>>> getTypeToZoneToRegionToZone() {
        return typeToZoneToRegionToZone;
    }

    /**
     * @deprecated, use PathHeader.getMetazonePageTerritory
     */
    public Map<String, String> getMetazoneToContinentMap() {
        return metazoneContinentMap;
    }

    public Set<String> getAllMetazones() {
        return allMetazones;
    }

    public Map<String, String> getLikelySubtags() {
        return likelySubtags;
    }

    public enum PluralType {
        cardinal(PluralRules.PluralType.CARDINAL),
        ordinal(PluralRules.PluralType.ORDINAL);

        // add some gorp to interwork until we clean things up

        public final PluralRules.PluralType standardType;

        PluralType(PluralRules.PluralType standardType) {
            this.standardType = standardType;
        }

        public static PluralType fromStandardType(PluralRules.PluralType standardType) {
            return standardType == null ? null
                : standardType == PluralRules.PluralType.CARDINAL ? cardinal
                    : ordinal;
        }
    };

    private EnumMap<PluralType, Map<String, PluralInfo>> localeToPluralInfo2 = new EnumMap<>(PluralType.class);
    {
        localeToPluralInfo2.put(PluralType.cardinal, new LinkedHashMap<String, PluralInfo>());
        localeToPluralInfo2.put(PluralType.ordinal, new LinkedHashMap<String, PluralInfo>());
    }
    private Map<String, PluralRanges> localeToPluralRanges = new LinkedHashMap<String, PluralRanges>();

    private Map<DayPeriodInfo.Type, Map<String, DayPeriodInfo>> typeToLocaleToDayPeriodInfo = new EnumMap<DayPeriodInfo.Type, Map<String, DayPeriodInfo>>(
        DayPeriodInfo.Type.class);
    private Map<String, CoverageLevel2> localeToCoverageLevelInfo = new ConcurrentHashMap<String, CoverageLevel2>();
    private CoverageCache coverageCache = new CoverageCache();
    private transient String lastPluralLocales = "";
    private transient PluralType lastPluralWasOrdinal = null;
    private transient Map<Count, String> lastPluralMap = new EnumMap<Count, String>(Count.class);
    private transient String lastDayPeriodLocales = null;
    private transient DayPeriodInfo.Type lastDayPeriodType = null;
    private transient DayPeriodInfo.Builder dayPeriodBuilder = new DayPeriodInfo.Builder();

    private void addDayPeriodPath(XPathParts path, String value) {
        // ldml/dates/calendars/calendar[@type="gregorian"]/dayPeriods/dayPeriodContext[@type="format"]/dayPeriodWidth[@type="wide"]/dayPeriod[@type="am"]
        /*
         * <supplementalData>
         * <version number="$Revision: 13288 $"/>
         * <generation date="$D..e... $"/>
         * <dayPeriodRuleSet>
         * <dayPeriodRules locales = "en"> <!-- default for any locales not listed under other dayPeriods -->
         * <dayPeriodRule type = "am" from = "0:00" before="12:00"/>
         * <dayPeriodRule type = "pm" from = "12:00" to="24:00"/>
         */
        String typeString = path.getAttributeValue(1, "type");
        String locales = path.getAttributeValue(2, "locales").trim();
        DayPeriodInfo.Type type = typeString == null
            ? DayPeriodInfo.Type.format
                : DayPeriodInfo.Type.valueOf(typeString.trim());
        if (!locales.equals(lastDayPeriodLocales) || type != lastDayPeriodType) {
            if (lastDayPeriodLocales != null) {
                addDayPeriodInfo();
            }
            lastDayPeriodLocales = locales;
            lastDayPeriodType = type;
            // System.out.println(type + ", " + locales + ", " + path);
        }
        if (path.size() != 4) {
            if (locales.equals("root")) return; // we allow root to be empty
            throw new IllegalArgumentException(locales + " must have dayPeriodRule elements");
        }
        DayPeriod dayPeriod = DayPeriod.fromString(path.getAttributeValue(-1, "type"));
        String at = path.getAttributeValue(-1, "at");
        String from = path.getAttributeValue(-1, "from");
        String after = path.getAttributeValue(-1, "after");
        String to = path.getAttributeValue(-1, "to");
        String before = path.getAttributeValue(-1, "before");
        if (at != null) {
            if (from != null || after != null || to != null || before != null) {
                throw new IllegalArgumentException();
            }
            from = at;
            to = at;
        } else if ((from == null) == (after == null) || (to == null) == (before == null)) {
            throw new IllegalArgumentException();
        }
//        if (dayPeriodBuilder.contains(dayPeriod)) { // disallow multiple rules with same dayperiod
//            throw new IllegalArgumentException("Multiple rules with same dayperiod are disallowed: "
//                + lastDayPeriodLocales + ", " + lastDayPeriodType + ", " + dayPeriod);
//        }
        boolean includesStart = from != null;
        boolean includesEnd = to != null;
        int start = parseTime(includesStart ? from : after);
        int end = parseTime(includesEnd ? to : before);
        // Check if any periods contain 0, e.g. 1700 - 300
        if (start > end) {
            // System.out.println("start " + start + " end " + end);
            dayPeriodBuilder.add(dayPeriod, start, includesStart, parseTime("24:00"), includesEnd);
            dayPeriodBuilder.add(dayPeriod, parseTime("0:00"), includesStart, end, includesEnd);
        } else {
            dayPeriodBuilder.add(dayPeriod, start, includesStart, end, includesEnd);
        }
    }

    static Pattern PARSE_TIME = PatternCache.get("(\\d\\d?):(\\d\\d)");

    private int parseTime(String string) {
        Matcher matcher = PARSE_TIME.matcher(string);
        if (!matcher.matches()) {
            throw new IllegalArgumentException();
        }
        return (Integer.parseInt(matcher.group(1)) * 60 + Integer.parseInt(matcher.group(2))) * 60 * 1000;
    }

    private void addDayPeriodInfo() {
        String[] locales = lastDayPeriodLocales.split("\\s+");
        DayPeriodInfo temp = dayPeriodBuilder.finish(locales);
        Map<String, DayPeriodInfo> locale2DPI = typeToLocaleToDayPeriodInfo.get(lastDayPeriodType);
        if (locale2DPI == null) {
            typeToLocaleToDayPeriodInfo.put(lastDayPeriodType, locale2DPI = new LinkedHashMap<String, DayPeriodInfo>());
            //System.out.println(lastDayPeriodType + ", " + locale2DPI);
        }
        for (String locale : locales) {
            locale2DPI.put(locale, temp);
        }
    }

    static String lastPluralRangesLocales = null;
    static PluralRanges lastPluralRanges = null;

    private boolean addPluralPath(XPathParts path, String value) {
        /*
         * Adding
         <pluralRanges locales="am">
          <pluralRange start="one" end="one" result="one" />
         </pluralRanges>
         */
        String locales = path.getAttributeValue(2, "locales").trim();
        String element = path.getElement(2);
        if ("pluralRanges".equals(element)) {
            if (!locales.equals(lastPluralRangesLocales)) {
                addPluralRanges(locales);
            }
            if (path.size() == 3) {
                // ok for ranges to be empty
                return true;
            }
            String rangeStart = path.getAttributeValue(-1, "start");
            String rangeEnd = path.getAttributeValue(-1, "end");
            String result = path.getAttributeValue(-1, "result");
            lastPluralRanges.add(rangeStart == null ? null : Count.valueOf(rangeStart),
                rangeEnd == null ? null : Count.valueOf(rangeEnd),
                    Count.valueOf(result)
                );
            return true;
        } else if ("pluralRules".equals(element)) {

            String type = path.getAttributeValue(1, "type");
            PluralType pluralType = type == null ? PluralType.cardinal : PluralType.valueOf(type);
            if (!lastPluralLocales.equals(locales)) {
                addPluralInfo(pluralType);
                lastPluralLocales = locales;
            }
            final String countString = path.getAttributeValue(-1, "count");
            if (countString == null) {
                return false;
            }
            Count count = Count.valueOf(countString);
            if (lastPluralMap.containsKey(count)) {
                throw new IllegalArgumentException("Duplicate plural count: " + count + " in " + locales);
            }
            lastPluralMap.put(count, value);
            lastPluralWasOrdinal = pluralType;
            return true;
        } else {
            return false;
        }
    }

    private void addPluralRanges(String localesString) {
        final String[] locales = localesString.split("\\s+");
        lastPluralRanges = new PluralRanges();
        for (String locale : locales) {
            if (localeToPluralRanges.containsKey(locale)) {
                throw new IllegalArgumentException("Duplicate plural locale: " + locale);
            }
            localeToPluralRanges.put(locale, lastPluralRanges);
        }
        lastPluralRangesLocales = localesString;
    }

    private void addPluralInfo(PluralType pluralType) {
        final String[] locales = lastPluralLocales.split("\\s+");
        PluralInfo info = new PluralInfo(lastPluralMap, pluralType);
        Map<String, PluralInfo> localeToInfo = localeToPluralInfo2.get(pluralType);
        for (String locale : locales) {
            if (localeToInfo.containsKey(locale)) {
                throw new IllegalArgumentException("Duplicate plural locale: " + locale);
            } else if (!locale.isEmpty()) {
                localeToInfo.put(locale, info);
            }
        }
        lastPluralMap.clear();
    }

    public static class SampleList {
        public static final SampleList EMPTY = new SampleList().freeze();

        private UnicodeSet uset = new UnicodeSet();
        private List<FixedDecimal> fractions = new ArrayList<FixedDecimal>(0);

        public String toString() {
            return toString(6, 3);
        }

        public String toString(int intLimit, int fractionLimit) {
            StringBuilder b = new StringBuilder();
            int intCount = 0;
            int fractionCount = 0;
            int limit = uset.getRangeCount();
            for (int i = 0; i < limit; ++i) {
                if (intCount >= intLimit) {
                    b.append(", …");
                    break;
                }
                if (b.length() != 0) {
                    b.append(", ");
                }
                int start = uset.getRangeStart(i);
                int end = uset.getRangeEnd(i);
                if (start == end) {
                    b.append(start);
                    ++intCount;
                } else if (start + 1 == end) {
                    b.append(start).append(", ").append(end);
                    intCount += 2;
                } else {
                    b.append(start).append('-').append(end);
                    intCount += 2;
                }
            }
            if (fractions.size() > 0) {
                for (int i = 0; i < fractions.size(); ++i) {
                    if (fractionCount >= fractionLimit) {
                        break;
                    }
                    if (b.length() != 0) {
                        b.append(", ");
                    }
                    b.append(fractions.get(i));
                    ++fractionCount;
                }
                b.append(", …");
            }
            return b.toString();
        }

        public int getRangeCount() {
            return uset.getRangeCount();
        }

        public int getRangeStart(int index) {
            return uset.getRangeStart(index);
        }

        public int getRangeEnd(int index) {
            return uset.getRangeEnd(index);
        }

        public List<FixedDecimal> getFractions() {
            return fractions;
        }

        public int intSize() {
            return uset.size();
        }

        public SampleList remove(int i) {
            uset.remove(i);
            return this;
        }

        public SampleList add(int i) {
            uset.add(i);
            return this;
        }

        public SampleList freeze() {
            uset.freeze();
            if (fractions instanceof ArrayList) {
                fractions = Collections.unmodifiableList(fractions);
            }
            return this;
        }

        public void add(FixedDecimal i) {
            fractions.add(i);
        }

        public int fractionSize() {
            return fractions.size();
        }
    }

    public static class CountSampleList {
        private final Map<Count, SampleList> countToIntegerSamples9999;
        private final Map<Count, SampleList[]> countToDigitToIntegerSamples9999;

        CountSampleList(PluralRules pluralRules, Set<Count> keywords, PluralType pluralType) {
            // Create the integer counts
            countToIntegerSamples9999 = new EnumMap<Count, SampleList>(Count.class);
            countToDigitToIntegerSamples9999 = new EnumMap<Count, SampleList[]>(Count.class);
            for (Count c : keywords) {
                countToIntegerSamples9999.put(c, new SampleList());
                SampleList[] row = new SampleList[5];
                countToDigitToIntegerSamples9999.put(c, row);
                for (int i = 1; i < 5; ++i) {
                    row[i] = new SampleList();
                }
            }
            for (int ii = 0; ii < 10000; ++ii) {
                int i = ii;
                int digit;
                if (i > 999) {
                    digit = 4;
                } else if (i > 99) {
                    digit = 3;
                } else if (i > 9) {
                    digit = 2;
                } else {
                    digit = 1;
                }
                Count count = Count.valueOf(pluralRules.select(i));
                addSimple(countToIntegerSamples9999, i, count);
                addDigit(countToDigitToIntegerSamples9999, i, count, digit);
                if (haveFractions(keywords, digit)) {
                    continue;
                }
                if (pluralType == PluralType.cardinal) {
                    for (int f = 0; f < 30; ++f) {
                        FixedDecimal ni = new FixedDecimal(i + f / 10.0d, f < 10 ? 1 : 2, f);
                        count = Count.valueOf(pluralRules.select(ni));
                        addSimple(countToIntegerSamples9999, ni, count);
                        addDigit(countToDigitToIntegerSamples9999, ni, count, digit);
                    }
                }
            }
            // HACK for Breton
            addSimple(countToIntegerSamples9999, 1000000, Count.valueOf(pluralRules.select(1000000)));

            for (Count count : keywords) {
                SampleList uset = countToIntegerSamples9999.get(count);
                uset.freeze();
                SampleList[] map = countToDigitToIntegerSamples9999.get(count);
                for (int i = 1; i < map.length; ++i) {
                    map[i].freeze();
                }
            }
        }

        private boolean haveFractions(Set<Count> keywords, int digit) {
            for (Count c : keywords) {
                int size = countToDigitToIntegerSamples9999.get(c)[digit].fractionSize();
                if (size < MAX_COLLECTED_FRACTION) {
                    return false;
                }
            }
            return true;
        }

        static final int MAX_COLLECTED_FRACTION = 5;

        private boolean addDigit(Map<Count, SampleList[]> countToDigitToIntegerSamples9999, FixedDecimal i, Count count, int digit) {
            return addFraction(i, countToDigitToIntegerSamples9999.get(count)[digit]);
        }

        private boolean addFraction(FixedDecimal i, SampleList sampleList) {
            if (sampleList.fractionSize() < MAX_COLLECTED_FRACTION) {
                sampleList.add(i);
                return true;
            } else {
                return false;
            }
        }

        private boolean addSimple(Map<Count, SampleList> countToIntegerSamples9999, FixedDecimal i, Count count) {
            return addFraction(i, countToIntegerSamples9999.get(count));
        }

        private void addDigit(Map<Count, SampleList[]> countToDigitToIntegerSamples9999, int i, Count count, int digit) {
            countToDigitToIntegerSamples9999.get(count)[digit].add(i);
        }

        private void addSimple(Map<Count, SampleList> countToIntegerSamples9999, int i, Count count) {
            countToIntegerSamples9999.get(count).add(i);
        }

        public SampleList get(Count type) {
            return countToIntegerSamples9999.get(type);
        }

        public SampleList get(Count c, int digit) {
            SampleList[] sampleLists = countToDigitToIntegerSamples9999.get(c);
            return sampleLists == null ? null : sampleLists[digit];
        }
    }

    /**
     * Immutable class with plural info for different locales
     *
     * @author markdavis
     */
    public static class PluralInfo implements Comparable<PluralInfo> {
        static final Set<Double> explicits = new HashSet<Double>();
        static {
            explicits.add(0.0d);
            explicits.add(1.0d);
        }

        public enum Count {
            zero, one, two, few, many, other;
            public static final int LENGTH = Count.values().length;
            public static final List<Count> VALUES = Collections.unmodifiableList(Arrays.asList(values()));
        }

        static final Pattern pluralPaths = PatternCache.get(".*pluralRule.*");
        static final int fractDecrement = 13;
        static final int fractStart = 20;

        private final Map<Count, Set<Double>> countToExampleSet;
        private final Map<Count, String> countToStringExample;
        private final Map<Integer, Count> exampleToCount;
        private final PluralRules pluralRules;
        private final String pluralRulesString;
        private final Set<String> canonicalKeywords;
        private final Set<Count> keywords;
        private final Set<Count> integerKeywords;
        private final Set<Count> decimalKeywords;
        private final CountSampleList countSampleList;
        private final Map<Count, String> countToRule;

        private PluralInfo(Map<Count, String> countToRule, PluralType pluralType) {
            EnumMap<Count, String> tempCountToRule = new EnumMap<Count, String>(Count.class);
            tempCountToRule.putAll(countToRule);
            this.countToRule = Collections.unmodifiableMap(tempCountToRule);

            // now build rules
            NumberFormat nf = NumberFormat.getNumberInstance(ULocale.ENGLISH);
            nf.setMaximumFractionDigits(2);
            StringBuilder pluralRuleBuilder = new StringBuilder();
            for (Count count : countToRule.keySet()) {
                if (pluralRuleBuilder.length() != 0) {
                    pluralRuleBuilder.append(';');
                }
                pluralRuleBuilder.append(count).append(':').append(countToRule.get(count));
            }
            pluralRulesString = pluralRuleBuilder.toString();
            try {
                pluralRules = PluralRules.parseDescription(pluralRulesString);
            } catch (ParseException e) {
                throw new IllegalArgumentException("Can't create plurals from <" + pluralRulesString + ">", e);
            }
            EnumSet<Count> _keywords = EnumSet.noneOf(Count.class);
            EnumSet<Count> _integerKeywords = EnumSet.noneOf(Count.class);
            EnumSet<Count> _decimalKeywords = EnumSet.noneOf(Count.class);
            for (String s : pluralRules.getKeywords()) {
                Count c = Count.valueOf(s);
                _keywords.add(c);
                if (pluralRules.getDecimalSamples(s, SampleType.DECIMAL) != null) {
                    _decimalKeywords.add(c);
                } else {
                    int debug = 1;
                }
                if (pluralRules.getDecimalSamples(s, SampleType.INTEGER) != null) {
                    _integerKeywords.add(c);
                } else {
                    int debug = 1;
                }
            }
            keywords = Collections.unmodifiableSet(_keywords);
            decimalKeywords = Collections.unmodifiableSet(_decimalKeywords);
            integerKeywords = Collections.unmodifiableSet(_integerKeywords);

            countSampleList = new CountSampleList(pluralRules, keywords, pluralType);

            Map<Count, Set<Double>> countToExampleSetRaw = new TreeMap<Count, Set<Double>>();
            Map<Integer, Count> exampleToCountRaw = new TreeMap<Integer, Count>();

            Output<Map<Count, SampleList[]>> output = new Output();

            // double check
            // if (!targetKeywords.equals(typeToExamples2.keySet())) {
            // throw new IllegalArgumentException ("Problem in plurals " + targetKeywords + ", " + this);
            // }
            // now fix the longer examples
            String otherFractionalExamples = "";
            List<Double> otherFractions = new ArrayList<Double>(0);

            // add fractional samples
            Map<Count, String> countToStringExampleRaw = new TreeMap<Count, String>();
            for (Count type : keywords) {
                SampleList uset = countSampleList.get(type);
                countToStringExampleRaw.put(type, uset.toString(5, 5));
            }
            final String baseOtherExamples = countToStringExampleRaw.get(Count.other);
            String otherExamples = (baseOtherExamples == null ? "" : baseOtherExamples + "; ")
                + otherFractionalExamples + "...";
            countToStringExampleRaw.put(Count.other, otherExamples);

            // Now do double examples (previously unused & not working).
            // Currently a bit of a hack, we should enhance SampleList to make this easier
            // and then use SampleList directly, see http://unicode.org/cldr/trac/ticket/9813
            for (Count type : countToStringExampleRaw.keySet()) {
                Set<Double> doublesSet = new LinkedHashSet<Double>(0);
                String examples = countToStringExampleRaw.get(type);
                if (examples == null) {
                    examples = "";
                }
                String strippedExamples = examples.replaceAll("(, …)|(; ...)", "");
                String[] exampleArray = strippedExamples.split("(, )|(-)");
                for (String example: exampleArray) {
                    if (example == null || example.length() == 0) {
                        continue;
                    }
                    Double doubleValue = Double.valueOf(example);
                    doublesSet.add(doubleValue);
                }
                doublesSet = Collections.unmodifiableSet(doublesSet);
                countToExampleSetRaw.put(type, doublesSet);
            }

            countToExampleSet = Collections.unmodifiableMap(countToExampleSetRaw);
            countToStringExample = Collections.unmodifiableMap(countToStringExampleRaw);
            exampleToCount = Collections.unmodifiableMap(exampleToCountRaw);
            Set<String> temp = new LinkedHashSet<String>();
            // String keyword = pluralRules.select(0.0d);
            // double value = pluralRules.getUniqueKeywordValue(keyword);
            // if (value == pluralRules.NO_UNIQUE_VALUE) {
            // temp.add("0");
            // }
            // keyword = pluralRules.select(1.0d);
            // value = pluralRules.getUniqueKeywordValue(keyword);
            // if (value == pluralRules.NO_UNIQUE_VALUE) {
            // temp.add("1");
            // }
            Set<String> keywords = pluralRules.getKeywords();
            for (Count count : Count.values()) {
                String keyword = count.toString();
                if (keywords.contains(keyword)) {
                    temp.add(keyword);
                }
            }
            // if (false) {
            // change to this after rationalizing 0/1
            // temp.add("0");
            // temp.add("1");
            // for (Count count : Count.values()) {
            // temp.add(count.toString());
            // KeywordStatus status = org.unicode.cldr.util.PluralRulesUtil.getKeywordStatus(pluralRules,
            // count.toString(), 0, explicits, true);
            // if (status != KeywordStatus.SUPPRESSED && status != KeywordStatus.INVALID) {
            // temp.add(count.toString());
            // }
            // }
            // }
            canonicalKeywords = Collections.unmodifiableSet(temp);
        }

        public String toString() {
            return countToExampleSet + "; " + exampleToCount + "; " + pluralRules;
        }

        public Map<Count, Set<Double>> getCountToExamplesMap() {
            return countToExampleSet;
        }

        public Map<Count, String> getCountToStringExamplesMap() {
            return countToStringExample;
        }

        public Count getCount(double exampleCount) {
            return Count.valueOf(pluralRules.select(exampleCount));
        }

        public Count getCount(PluralRules.FixedDecimal exampleCount) {
            return Count.valueOf(pluralRules.select(exampleCount));
        }

        public PluralRules getPluralRules() {
            return pluralRules;
        }

        public String getRules() {
            return pluralRulesString;
        }

        public Count getDefault() {
            return null;
        }

        public Set<String> getCanonicalKeywords() {
            return canonicalKeywords;
        }

        public Set<Count> getCounts() {
            return keywords;
        }

        public Set<Count> getCounts(SampleType sampleType) {
            return sampleType == SampleType.DECIMAL ? decimalKeywords : integerKeywords;
        }

        /**
         * Return the integer samples from 0 to 9999. For simplicity and compactness, this is a UnicodeSet, but
         * the interpretation is simply as a list of integers. UnicodeSet.EMPTY is returned if there are none.
         * @param c
         * @return
         */
        public SampleList getSamples9999(Count c) {
            return countSampleList.get(c);
        }

        /**
         * Return the integer samples for the specified digit, eg 1 => 0..9. For simplicity and compactness, this is a UnicodeSet, but
         * the interpretation is simply as a list of integers.
         * @param c
         * @return
         */
        public SampleList getSamples9999(Count c, int digit) {
            return countSampleList.get(c, digit);
        }

        public boolean hasSamples(Count c, int digits) {
            SampleList samples = countSampleList.get(c, digits);
            return samples != null && (samples.fractionSize() > 0 || samples.intSize() > 0);
        }

        public String getRule(Count keyword) {
            return countToRule.get(keyword);
        }

        @Override
        public int compareTo(PluralInfo other) {
            int size1 = this.countToRule.size();
            int size2 = other.countToRule.size();
            int diff = size1 - size2;
            if (diff != 0) {
                return diff;
            }
            Iterator<Count> it1 = countToRule.keySet().iterator();
            Iterator<Count> it2 = other.countToRule.keySet().iterator();
            while (it1.hasNext()) {
                Count a1 = it1.next();
                Count a2 = it2.next();
                diff = a1.ordinal() - a2.ordinal();
                if (diff != 0) {
                    return diff;
                }
            }
            return pluralRules.compareTo(other.pluralRules);
        }

        enum MinMax {
            MIN, MAX
        }

        public static final FixedDecimal NEGATIVE_INFINITY = new FixedDecimal(Double.NEGATIVE_INFINITY, 0, 0);
        public static final FixedDecimal POSITIVE_INFINITY = new FixedDecimal(Double.POSITIVE_INFINITY, 0, 0);

        static double doubleValue(FixedDecimal a) {
            return a.doubleValue();
        }

        public boolean rangeExists(Count s, Count e, Output<FixedDecimal> minSample, Output<FixedDecimal> maxSample) {
            if (!getCounts().contains(s) || !getCounts().contains(e)) {
                return false;
            }
            FixedDecimal temp;
            minSample.value = getLeastIn(s, SampleType.INTEGER, NEGATIVE_INFINITY, POSITIVE_INFINITY);
            temp = getLeastIn(s, SampleType.DECIMAL, NEGATIVE_INFINITY, POSITIVE_INFINITY);
            if (lessOrFewerDecimals(temp, minSample.value)) {
                minSample.value = temp;
            }
            maxSample.value = getGreatestIn(e, SampleType.INTEGER, NEGATIVE_INFINITY, POSITIVE_INFINITY);
            temp = getGreatestIn(e, SampleType.DECIMAL, NEGATIVE_INFINITY, POSITIVE_INFINITY);
            if (greaterOrFewerDecimals(temp, maxSample.value)) {
                maxSample.value = temp;
            }
            // if there is no range, just return
            if (doubleValue(minSample.value) >= doubleValue(maxSample.value)) {
                return false;
            }
            // see if we can get a better range, with not such a large end range

            FixedDecimal lowestMax = new FixedDecimal(doubleValue(minSample.value) + 0.00001, 5);
            SampleType bestType = getCounts(SampleType.INTEGER).contains(e) ? SampleType.INTEGER : SampleType.DECIMAL;
            temp = getLeastIn(e, bestType, lowestMax, POSITIVE_INFINITY);
            if (lessOrFewerDecimals(temp, maxSample.value)) {
                maxSample.value = temp;
            }
            if (maxSample.value.source > 100000) {
                temp = getLeastIn(e, bestType, lowestMax, POSITIVE_INFINITY);
                if (lessOrFewerDecimals(temp, maxSample.value)) {
                    maxSample.value = temp;
                }
            }

            return true;
        }

        public boolean greaterOrFewerDecimals(FixedDecimal a, FixedDecimal b) {
            return doubleValue(a) > doubleValue(b)
                || doubleValue(b) == doubleValue(a) && b.decimalDigits > a.decimalDigits;
        }

        public boolean lessOrFewerDecimals(FixedDecimal a, FixedDecimal b) {
            return doubleValue(a) < doubleValue(b)
                || doubleValue(b) == doubleValue(a) && b.decimalDigits > a.decimalDigits;
        }

        private FixedDecimal getLeastIn(Count s, SampleType sampleType, FixedDecimal min, FixedDecimal max) {
            FixedDecimal result = POSITIVE_INFINITY;
            FixedDecimalSamples sSamples1 = pluralRules.getDecimalSamples(s.toString(), sampleType);
            if (sSamples1 != null) {
                for (FixedDecimalRange x : sSamples1.samples) {
                    // overlap in ranges??
                    if (doubleValue(x.start) > doubleValue(max)
                        || doubleValue(x.end) < doubleValue(min)) {
                        continue; // no, continue
                    }
                    // get restricted range
                    FixedDecimal minOverlap = greaterOrFewerDecimals(min, x.start) ? max : x.start;
                    //FixedDecimal maxOverlap = lessOrFewerDecimals(max, x.end) ? max : x.end;

                    // replace if better
                    if (lessOrFewerDecimals(minOverlap, result)) {
                        result = minOverlap;
                    }
                }
            }
            return result;
        }

        private FixedDecimal getGreatestIn(Count s, SampleType sampleType, FixedDecimal min, FixedDecimal max) {
            FixedDecimal result = NEGATIVE_INFINITY;
            FixedDecimalSamples sSamples1 = pluralRules.getDecimalSamples(s.toString(), sampleType);
            if (sSamples1 != null) {
                for (FixedDecimalRange x : sSamples1.samples) {
                    // overlap in ranges??
                    if (doubleValue(x.start) > doubleValue(max)
                        || doubleValue(x.end) < doubleValue(min)) {
                        continue; // no, continue
                    }
                    // get restricted range
                    //FixedDecimal minOverlap = greaterOrFewerDecimals(min, x.start) ? max : x.start;
                    FixedDecimal maxOverlap = lessOrFewerDecimals(max, x.end) ? max : x.end;

                    // replace if better
                    if (greaterOrFewerDecimals(maxOverlap, result)) {
                        result = maxOverlap;
                    }
                }
            }
            return result;
        }

        public static FixedDecimal getNonZeroSampleIfPossible(FixedDecimalSamples exampleList) {
            Set<FixedDecimalRange> sampleSet = exampleList.getSamples();
            FixedDecimal sampleDecimal = null;
            // skip 0 if possible
            for (FixedDecimalRange range : sampleSet) {
                sampleDecimal = range.start;
                if (sampleDecimal.source != 0.0) {
                    break;
                }
                sampleDecimal = range.end;
                if (sampleDecimal.source != 0.0) {
                    break;
                }
            }
            return sampleDecimal;
        }
    }

    /**
     * @deprecated use {@link #getPlurals(PluralType)} instead
     */
    public Set<String> getPluralLocales() {
        return getPluralLocales(PluralType.cardinal);
    }

    /**
     * @param type
     * @return the set of locales that have rules for the specified plural type
     */
    public Set<String> getPluralLocales(PluralType type) {
        return localeToPluralInfo2.get(type).keySet();
    }

    public Set<String> getPluralRangesLocales() {
        return localeToPluralRanges.keySet();
    }

    public PluralRanges getPluralRanges(String locale) {
        return localeToPluralRanges.get(locale);
    }

    /**
     * @deprecated use {@link #getPlurals(PluralType, String)} instead
     */
    public PluralInfo getPlurals(String locale) {
        return getPlurals(locale, true);
    }

    /**
     * Returns the plural info for a given locale.
     *
     * @param locale
     * @return
     */
    public PluralInfo getPlurals(PluralType type, String locale) {
        return getPlurals(type, locale, true);
    }

    /**
     * @deprecated use {@link #getPlurals(PluralType, String, boolean)} instead.
     */
    public PluralInfo getPlurals(String locale, boolean allowRoot) {
        return getPlurals(PluralType.cardinal, locale, allowRoot);
    }

    /**
     * Returns the plural info for a given locale.
     *
     * @param locale
     * @param allowRoot
     * @param type
     * @return
     */
    public PluralInfo getPlurals(PluralType type, String locale, boolean allowRoot) {
        Map<String, PluralInfo> infoMap = localeToPluralInfo2.get(type);
        while (locale != null) {
            if (!allowRoot && locale.equals("root")) {
                break;
            }
            PluralInfo result = infoMap.get(locale);
            if (result != null) {
                return result;
            }
            locale = LocaleIDParser.getSimpleParent(locale);
        }
        return null;
    }

    public DayPeriodInfo getDayPeriods(DayPeriodInfo.Type type, String locale) {
        Map<String, DayPeriodInfo> map1 = typeToLocaleToDayPeriodInfo.get(type);
        while (locale != null) {
            DayPeriodInfo result = map1.get(locale);
            if (result != null) {
                return result;
            }
            locale = LocaleIDParser.getSimpleParent(locale);
        }
        return null;
    }

    public Set<String> getDayPeriodLocales(DayPeriodInfo.Type type) {
        return typeToLocaleToDayPeriodInfo.get(type).keySet();
    }

    private static CurrencyNumberInfo DEFAULT_NUMBER_INFO = new CurrencyNumberInfo(2, -1, -1, -1);

    public CurrencyNumberInfo getCurrencyNumberInfo(String currency) {
        CurrencyNumberInfo result = currencyToCurrencyNumberInfo.get(currency);
        if (result == null) {
            result = DEFAULT_NUMBER_INFO;
        }
        return result;
    }

    /**
     * Returns ordered set of currency data information
     *
     * @param territory
     * @return
     */
    public Set<CurrencyDateInfo> getCurrencyDateInfo(String territory) {
        return territoryToCurrencyDateInfo.getAll(territory);
    }

    /**
     * Returns ordered set of currency data information
     *
     * @param territory
     * @return
     */
    public Set<String> getCurrencyTerritories() {
        return territoryToCurrencyDateInfo.keySet();
    }

    /**
     * Returns the ISO4217 currency code of the default currency for a given
     * territory. The default currency is the first one listed which is legal
     * tender at the present moment.
     *
     * @param territory
     * @return
     */
    public String getDefaultCurrency(String territory) {

        Set<CurrencyDateInfo> targetCurrencyInfo = getCurrencyDateInfo(territory);
        String result = "XXX";
        Date now = new Date();
        for (CurrencyDateInfo cdi : targetCurrencyInfo) {
            if (cdi.getStart().before(now) && cdi.getEnd().after(now) && cdi.isLegalTender()) {
                result = cdi.getCurrency();
                break;
            }
        }
        return result;
    }

    /**
     * Returns the ISO4217 currency code of the default currency for a given
     * CLDRLocale. The default currency is the first one listed which is legal
     * tender at the present moment.
     *
     * @param territory
     * @return
     */
    public String getDefaultCurrency(CLDRLocale loc) {
        return getDefaultCurrency(loc.getCountry());
    }

    public Map<String, Set<TelephoneCodeInfo>> getTerritoryToTelephoneCodeInfo() {
        return territoryToTelephoneCodeInfo;
    }

    public Set<TelephoneCodeInfo> getTelephoneCodeInfoForTerritory(String territory) {
        return territoryToTelephoneCodeInfo.get(territory);
    }

    public Set<String> getTerritoriesForTelephoneCodeInfo() {
        return territoryToTelephoneCodeInfo.keySet();
    }

    private List<String> serialElements;
    private Collection<String> distinguishingAttributes;

    public List<String> getSerialElements() {
        return serialElements;
    }

    public Collection<String> getDistinguishingAttributes() {
        return distinguishingAttributes;
    }

    public List<R4<String, String, Integer, Boolean>> getLanguageMatcherData(String string) {
        return languageMatch.get(string);
    }

    public Set<String> getLanguageMatcherKeys() {
        return languageMatch.keySet();
    }

    /**
     * Return mapping from type to territory to data. 001 is the default.
     */
    public Map<MeasurementType, Map<String, String>> getTerritoryMeasurementData() {
        return measurementData;
    }

    /**
     * Return mapping from keys to subtypes
     */
    public Relation<String, String> getBcp47Keys() {
        return bcp47Key2Subtypes;
    }

    /**
     * Return mapping from extensions to keys
     */
    public Relation<String, String> getBcp47Extension2Keys() {
        return bcp47Extension2Keys;
    }

    /**
     * Return mapping from &lt;key,subtype> to aliases
     */
    public Relation<R2<String, String>, String> getBcp47Aliases() {
        return bcp47Aliases;
    }

    /**
     * Return mapping from &lt;key,subtype> to description
     */
    public Map<R2<String, String>, String> getBcp47Descriptions() {
        return bcp47Descriptions;
    }

    /**
     * Return mapping from &lt;key,subtype> to since
     */
    public Map<R2<String, String>, String> getBcp47Since() {
        return bcp47Since;
    }

    /**
     * Return mapping from &lt;key,subtype> to preferred
     */
    public Map<R2<String, String>, String> getBcp47Preferred() {
        return bcp47Preferred;
    }

    /**
     * Return mapping from &lt;key,subtype> to deprecated
     */
    public Map<R2<String, String>, String> getBcp47Deprecated() {
        return bcp47Deprecated;
    }

    static Set<String> MainTimeZones;;

    /**
     * Return canonical timezones
     *
     * @return
     */
    public Set<String> getCanonicalTimeZones() {
        synchronized (SupplementalDataInfo.class) {
            if (MainTimeZones == null) {
                MainTimeZones = new TreeSet<String>();
                SupplementalDataInfo info = SupplementalDataInfo.getInstance();
                for (Entry<R2<String, String>, Set<String>> entry : info.getBcp47Aliases().keyValuesSet()) {
                    R2<String, String> subtype_aliases = entry.getKey();
                    if (!subtype_aliases.get0().equals("timezone")) {
                        continue;
                    }
                    MainTimeZones.add(entry.getValue().iterator().next());
                }
                MainTimeZones = Collections.unmodifiableSet(MainTimeZones);
            }
            return MainTimeZones;
        }
    }

    public Set<MetaZoneRange> getMetaZoneRanges(String zone) {
        return zoneToMetaZoneRanges.get(zone);
    }

    /**
     * Return the metazone containing this zone at this date
     *
     * @param zone
     * @param date
     * @return
     */
    public MetaZoneRange getMetaZoneRange(String zone, long date) {
        Set<MetaZoneRange> metazoneRanges = zoneToMetaZoneRanges.get(zone);
        if (metazoneRanges != null) {
            for (MetaZoneRange metazoneRange : metazoneRanges) {
                if (metazoneRange.dateRange.getFrom() <= date && date < metazoneRange.dateRange.getTo()) {
                    return metazoneRange;
                }
            }
        }
        return null;
    }

    public boolean isDeprecated(DtdType type, String element, String attribute, String value) {
        return DtdData.getInstance(type).isDeprecated(element, attribute, value);
    }

    public boolean isDeprecated(DtdType type, String path) {

        XPathParts parts = XPathParts.getInstance(path);
        for (int i = 0; i < parts.size(); ++i) {
            String element = parts.getElement(i);
            if (isDeprecated(type, element, "*", "*")) {
                return true;
            }
            for (Entry<String, String> entry : parts.getAttributes(i).entrySet()) {
                String attribute = entry.getKey();
                String value = entry.getValue();
                if (isDeprecated(type, element, attribute, value)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns map of ID/Type/Value, such as id="$integer" type="regex" value=[0-9]+
     * @return
     */
    public Map<String, R2<String, String>> getValidityInfo() {
        return validityInfo;
    }

    public Set<String> getCLDRLanguageCodes() {
        return CLDRLanguageCodes;
    }

    public boolean isCLDRLanguageCode(String code) {
        return CLDRLanguageCodes.contains(code);
    }

    public Set<String> getCLDRScriptCodes() {
        return CLDRScriptCodes;
    }

    public boolean isCLDRScriptCode(String code) {
        return CLDRScriptCodes.contains(code);
    }

    private synchronized void initCLDRLocaleBasedData() throws InternalError {
        // This initialization depends on SDI being initialized.
        if (defaultContentToBase == null) {
            Map<CLDRLocale, CLDRLocale> p2c = new TreeMap<CLDRLocale, CLDRLocale>();
            Map<CLDRLocale, CLDRLocale> c2p = new TreeMap<CLDRLocale, CLDRLocale>();
            TreeSet<CLDRLocale> tmpAllLocales = new TreeSet<CLDRLocale>();
            // copied from SupplementalData.java - CLDRLocale based
            for (String l : defaultContentLocales) {
                CLDRLocale child = CLDRLocale.getInstance(l);
                tmpAllLocales.add(child);
            }

            for (CLDRLocale child : tmpAllLocales) {
                // Find a parent of this locale which is NOT itself also a defaultContent
                CLDRLocale nextParent = child.getParent();
                // /System.err.println(">> considering " + child + " with parent " + nextParent);
                while (nextParent != null) {
                    if (!tmpAllLocales.contains(nextParent)) { // Did we find a parent that's also not itself a
                        // defaultContent?
                        // /System.err.println(">>>> Got 1? considering " + child + " with parent " + nextParent);
                        break;
                    }
                    // /System.err.println(">>>>> considering " + child + " with parent " + nextParent);
                    nextParent = nextParent.getParent();
                }
                // parent
                if (nextParent == null) {
                    throw new InternalError("SupplementalDataInfo.defaultContentToChild(): No valid parent for "
                        + child);
                } else if (nextParent == CLDRLocale.ROOT || nextParent == CLDRLocale.getInstance("root")) {
                    throw new InternalError(
                        "SupplementalDataInfo.defaultContentToChild(): Parent is root for default content locale "
                            + child);
                } else {
                    c2p.put(child, nextParent); // wo_Arab_SN -> wo
                    CLDRLocale oldChild = p2c.get(nextParent);
                    if (oldChild != null) {
                        CLDRLocale childParent = child.getParent();
                        if (!childParent.equals(oldChild)) {
                            throw new InternalError(
                                "SupplementalData.defaultContentToChild(): defaultContent list in wrong order? Tried to map "
                                    + nextParent + " -> " + child + ", replacing " + oldChild + " (should have been "
                                    + childParent + ")");
                        }
                    }
                    p2c.put(nextParent, child); // wo -> wo_Arab_SN
                }
            }

            // done, save the hashtables..
            baseToDefaultContent = Collections.unmodifiableMap(p2c); // wo -> wo_Arab_SN
            defaultContentToBase = Collections.unmodifiableMap(c2p); // wo_Arab_SN -> wo
        }
    }

    public Map<String, PreferredAndAllowedHour> getTimeData() {
        return timeData;
    }

    public String getDefaultScript(String baseLanguage) {
        String ls = likelySubtags.get(baseLanguage);
        if (ls == null) {
            return UNKNOWN_SCRIPT;
        }
        LocaleIDParser lp = new LocaleIDParser().set(ls);
        String defaultScript = lp.getScript();
        if (defaultScript.length() > 0) {
            return defaultScript;
        } else {
            return UNKNOWN_SCRIPT;
        }
    }

    private XEquivalenceClass<String, String> equivalentLocales = null;

    public Set<String> getEquivalentsForLocale(String localeId) {
        if (equivalentLocales == null) {
            equivalentLocales = getEquivalentsForLocale();
        }
        Set<String> result = new TreeSet(LENGTH_FIRST);
        result.add(localeId);
        Set<String> equiv = equivalentLocales.getEquivalences(localeId);
        //        if (equiv == null) {
        //            result.add(localeId);
        //            return result;
        //        }
        if (equiv != null) {
            result.addAll(equivalentLocales.getEquivalences(localeId));
        }
        Map<String, String> likely = getLikelySubtags();
        String newMax = LikelySubtags.maximize(localeId, likely);
        if (newMax != null) {
            result.add(newMax);
            newMax = LikelySubtags.minimize(localeId, likely, true);
            if (newMax != null) {
                result.add(newMax);
            }
            newMax = LikelySubtags.minimize(localeId, likely, false);
            if (newMax != null) {
                result.add(newMax);
            }
        }

        //        if (result.size() == 1) {
        //            LanguageTagParser ltp = new LanguageTagParser().set(localeId);
        //            if (ltp.getScript().isEmpty()) {
        //                String ds = getDefaultScript(ltp.getLanguage());
        //                if (ds != null) {
        //                    ltp.setScript(ds);
        //                    result.add(ltp.toString());
        //                }
        //            }
        //        }
        return result;
    }

    public final static class LengthFirstComparator<T> implements Comparator<T> {
        public int compare(T a, T b) {
            String as = a.toString();
            String bs = b.toString();
            if (as.length() < bs.length())
                return -1;
            if (as.length() > bs.length())
                return 1;
            return as.compareTo(bs);
        }
    }

    public static final LengthFirstComparator LENGTH_FIRST = new LengthFirstComparator();

    private synchronized XEquivalenceClass<String, String> getEquivalentsForLocale() {
        SupplementalDataInfo sdi = this;
        Relation<String, String> localeToDefaultContents = Relation.of(new HashMap<String, Set<String>>(),
            LinkedHashSet.class);

        Set<String> dcl = sdi.getDefaultContentLocales();
        Map<String, String> likely = sdi.getLikelySubtags();
        XEquivalenceClass<String, String> locales = new XEquivalenceClass<String, String>();
        LanguageTagParser ltp = new LanguageTagParser();
        Set<String> temp = new HashSet<String>();
        for (Entry<String, String> entry : likely.entrySet()) {
            String source = entry.getKey();
            if (source.startsWith("und")) {
                continue;
            }
            for (String s : getCombinations(source, ltp, likely, temp)) {
                locales.add(source, s);
            }
            for (String s : getCombinations(entry.getValue(), ltp, likely, temp)) {
                locales.add(source, s);
            }
        }
        //        Set<String> sorted = new TreeSet(locales.getExplicitItems());
        //        for (String s : sorted) {
        //            System.out.println(locales.getEquivalences(s));
        //        }
        for (String defaultContentLocale : dcl) {
            if (defaultContentLocale.startsWith("zh")) {
                int x = 0;
            }
            Set<String> set = locales.getEquivalences(defaultContentLocale);

            String parent = LocaleIDParser.getSimpleParent(defaultContentLocale);
            if (!set.contains(parent)) {
                localeToDefaultContents.put(parent, defaultContentLocale);
                //System.out.println("Mismatch " + parent + ", " + set);
            }
            if (parent.contains("_")) {
                continue;
            }
            // only base locales after this point
            String ds = sdi.getDefaultScript(parent);
            if (ds != null) {
                ltp.set(parent);
                ltp.setScript(ds);
                String trial = ltp.toString();
                if (!set.contains(trial)) {
                    //System.out.println("Mismatch " + trial + ", " + set);
                    localeToDefaultContents.put(parent, trial);
                }
            }
        }
        return locales;
    }

    private Set<String> getCombinations(String source, LanguageTagParser ltp, Map<String, String> likely,
        Set<String> locales) {
        locales.clear();

        String max = LikelySubtags.maximize(source, likely);
        locales.add(max);

        ltp.set(source);
        ltp.setScript("");
        String trial = ltp.toString();
        String newMax = LikelySubtags.maximize(trial, likely);
        if (Objects.equals(newMax,max)) {
            locales.add(trial);
        }

        ltp.set(source);
        ltp.setRegion("");
        trial = ltp.toString();
        newMax = LikelySubtags.maximize(trial, likely);
        if (Objects.equals(newMax,max)) {
            locales.add(trial);
        }

        return locales;
    }

    public VersionInfo getCldrVersion() {
        return cldrVersion;
    }

    public File getDirectory() {
        return directory;
    }

    public final static Splitter WHITESPACE_SPLTTER = Splitter.on(PatternCache.get("\\s+")).omitEmptyStrings();

    public static final class AttributeValidityInfo {
        //<attributeValues elements="alias" attributes="path" type="path">notDoneYet</attributeValues>

        final String type;
        final Set<DtdType> dtds;
        final Set<String> elements;
        final Set<String> attributes;
        final String order;

        @Override
        public String toString() {
            return "type:" + type
                + ", elements:" + elements
                + ", attributes:" + attributes
                + ", order:" + order;
        }

        static void add(Map<String, String> inputAttibutes, String inputValue, Map<AttributeValidityInfo, String> data) {
            final AttributeValidityInfo key = new AttributeValidityInfo(
                inputAttibutes.get("dtds"),
                inputAttibutes.get("type"),
                inputAttibutes.get("attributes"),
                inputAttibutes.get("elements"),
                inputAttibutes.get("order"));
            if (data.containsKey(key)) {
                throw new IllegalArgumentException(key + " declared twice");
            }
            data.put(key, inputValue);
        }

        public AttributeValidityInfo(String dtds, String type, String attributes, String elements, String order) {
            if (dtds == null) {
                this.dtds = Collections.singleton(DtdType.ldml);
            } else {
                Set<DtdType> temp = EnumSet.noneOf(DtdType.class);
                for (String s : WHITESPACE_SPLTTER.split(dtds)) {
                    temp.add(DtdType.valueOf(s));
                }
                this.dtds = Collections.unmodifiableSet(temp);
            }
            this.type = type != null ? type : order != null ? "choice" : null;
            this.elements = elements == null ? Collections.EMPTY_SET
                : With.in(WHITESPACE_SPLTTER.split(elements)).toUnmodifiableCollection(new HashSet<String>());
            this.attributes = With.in(WHITESPACE_SPLTTER.split(attributes)).toUnmodifiableCollection(new HashSet<String>());
            this.order = order;
        }

        public String getType() {
            return type;
        }

        public Set<DtdType> getDtds() {
            return dtds;
        }

        public Set<String> getElements() {
            return elements;
        }

        public Set<String> getAttributes() {
            return attributes;
        }

        public String getOrder() {
            return order;
        }

        @Override
        public boolean equals(Object obj) {
            AttributeValidityInfo other = (AttributeValidityInfo) obj;
            return CldrUtility.deepEquals(
                type, other.type,
                dtds, other.dtds,
                elements, other.elements,
                attributes, other.attributes,
                order, other.order);
        }

        @Override
        public int hashCode() {
            return Objects.hash(type, dtds, elements, attributes, order);
        }
    }

    public Map<AttributeValidityInfo, String> getAttributeValidity() {
        return attributeValidityInfo;
    }
}
