package org.unicode.cldr.util;

import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
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.TreeMap;
import java.util.TreeSet;
import java.util.regex.Pattern;

import org.unicode.cldr.util.LanguageInfo.CldrDir;
import org.unicode.cldr.util.StandardCodes.LstrType;
import org.unicode.cldr.util.SupplementalDataInfo.AttributeValidityInfo;

import com.google.common.base.Splitter;
import com.google.common.collect.ComparisonChain;
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.R3;
import com.ibm.icu.text.UnicodeSet;
import com.ibm.icu.util.ICUException;
import com.ibm.icu.util.Output;

public class AttributeValueValidity {

    public enum Status {
        ok, deprecated, illegal, noTest
    }

    public enum LocaleSpecific {
        pluralCardinal, pluralOrdinal, dayPeriodFormat, dayPeriodSelection
    }

    static final Splitter BAR = Splitter.on('|').trimResults().omitEmptyStrings();
    static final Splitter SPACE = Splitter.on(PatternCache.get("\\s+")).trimResults().omitEmptyStrings();

    private static final Set<DtdType> ALL_DTDs = Collections.unmodifiableSet(EnumSet.allOf(DtdType.class));

    private static final SupplementalDataInfo supplementalData = CLDRConfig.getInstance().getSupplementalDataInfo();

    private static Map<DtdType, Map<String, Map<String, MatcherPattern>>> dtd_element_attribute_validity = new EnumMap<>(DtdType.class);
    private static Map<String, MatcherPattern> common_attribute_validity = new LinkedHashMap<String, MatcherPattern>();
    private static Map<String, MatcherPattern> variables = new LinkedHashMap<String, MatcherPattern>();
    private static final RegexMatcher NOT_DONE_YET = new RegexMatcher(".*", Pattern.COMMENTS);
    private static final Map<AttributeValidityInfo, String> failures = new LinkedHashMap<>();
    private static final boolean DEBUG = false;

    static {

        Relation<R2<String, String>, String> bcp47Aliases = supplementalData.getBcp47Aliases();
        Set<String> bcp47Keys = new LinkedHashSet<>();
        Set<String> bcp47Values = new LinkedHashSet<>();
        for (Entry<String, Set<String>> keyValues : supplementalData.getBcp47Keys().keyValuesSet()) {
            Set<String> fullValues = new TreeSet<>();
            String key = keyValues.getKey();
            bcp47Keys.add(key);

            Set<String> rawValues = keyValues.getValue();

            for (String value : rawValues) {
                if (key.equals("cu")) { // Currency codes are in upper case.
                    fullValues.add(value.toUpperCase());
                } else {
                    fullValues.add(value);
                }
                R2<String, String> keyValue = R2.of(key, value);
                Set<String> aliases = bcp47Aliases.getAll(keyValue);
                if (aliases != null) {
                    fullValues.addAll(aliases);
                }
            }
            // Special case exception for generic calendar, since we don't want to expose it in bcp47
            if (key.equals("ca")) {
                fullValues.add("generic");
            }
            fullValues = Collections.unmodifiableSet(fullValues);
            addCollectionVariable("$_bcp47_" + key, fullValues);

            // add aliased keys
            Set<String> aliases = supplementalData.getBcp47Aliases().getAll(Row.of(key, ""));
            if (aliases != null) {
                for (String aliasKey : aliases) {
                    bcp47Keys.add(aliasKey);
                    addCollectionVariable("$_bcp47_" + aliasKey, fullValues);
                }
            }
            bcp47Values.addAll(fullValues);
        }
        bcp47Keys.add("x"); // special-case private use
        bcp47Keys.add("x0"); // special-case, has no subtypes
        addCollectionVariable("$_bcp47_keys", bcp47Keys);
        addCollectionVariable("$_bcp47_value", bcp47Values);

        Validity validity = Validity.getInstance();
        for (LstrType key : LstrType.values()) { 
            final Map<Validity.Status, Set<String>> statusToCodes = validity.getStatusToCodes(key);
            if (statusToCodes == null) {
                continue;
            }
            String keyName = "$_" + key;
            Set<String> all = new LinkedHashSet<>();
            Set<String> prefix = new LinkedHashSet<>();
            Set<String> suffix = new LinkedHashSet<>();
            Set<String> regularAndUnknown = new LinkedHashSet<>();
            for (Entry<Validity.Status, Set<String>> item2 : statusToCodes.entrySet()) {
                Validity.Status status = item2.getKey();
                Set<String> validItems = item2.getValue();
                if (key == LstrType.variant) { // uppercased in CLDR
                    Set<String> temp2 = new LinkedHashSet<>(validItems);
                    for (String item : validItems) {
                        temp2.add(item.toUpperCase(Locale.ROOT));
                    }
                    validItems = temp2;
                } else if (key == LstrType.subdivision) {
                    for (String item : validItems) {
                        if (item.contains("-")) {
                            List<String> parts = Splitter.on('-').splitToList(item);
                            prefix.add(parts.get(0));
                            suffix.add(parts.get(1)); }
                        else {
                            int prefixWidth = item.charAt(0) < 'A' ? 3 : 2;
                            prefix.add(item.substring(0,prefixWidth));
                            suffix.add(item.substring(prefixWidth));
                        }
                    }
                }
                all.addAll(validItems);
                if (status == Validity.Status.regular || status == Validity.Status.special || status == Validity.Status.unknown) {
                    regularAndUnknown.addAll(validItems);
                }
                addCollectionVariable(keyName + "_" + status, validItems);
//                MatcherPattern m = new MatcherPattern(key.toString(), validItems.toString(), new CollectionMatcher(validItems));
//                variables.put(keyName+"_"+status, m);
            }
            if (key == LstrType.subdivision) {
                addCollectionVariable(keyName + "_prefix", prefix);
                addCollectionVariable(keyName + "_suffix", suffix);
            }
            addCollectionVariable(keyName, all);
            addCollectionVariable(keyName + "_plus", regularAndUnknown);

//            MatcherPattern m = new MatcherPattern(key.toString(), all.toString(), new CollectionMatcher(all));
//            variables.put(keyName, m);
//            MatcherPattern m2 = new MatcherPattern(key.toString(), regularAndUnknown.toString(), new CollectionMatcher(regularAndUnknown));
//            variables.put(keyName + "_plus", m2);
        }

        Set<String> main = new LinkedHashSet<>();
        main.addAll(StandardCodes.LstrType.language.specials);
        Set<String> coverage = new LinkedHashSet<>();
        Set<String> large_official = new LinkedHashSet<>();
        final LocaleIDParser lip = new LocaleIDParser();

        for (String language : LanguageInfo.getAvailable()) {
            LanguageInfo info = LanguageInfo.get(language);
            CldrDir cldrDir = info.getCldrDir();
            String base = lip.set(language).getLanguage();
            if (cldrDir == CldrDir.main || cldrDir == CldrDir.base) {
                main.add(base);
            }
            if (info.getCldrLevel() == Level.MODERN) {
                coverage.add(base);
            }
            if (info.getLiteratePopulation() > 1000000 && !info.getStatusToRegions().isEmpty()) {
                large_official.add(base);
            }
        }
        addCollectionVariable("$_language_main", main);
        addCollectionVariable("$_language_coverage", coverage);
        addCollectionVariable("$_language_large_official", large_official);
        Set<String> cldrLang = new TreeSet<>(main);
        cldrLang.addAll(coverage);
        cldrLang.addAll(large_official);
        addCollectionVariable("$_language_cldr", large_official);
        // System.out.println("\ncldrLang:\n" + Joiner.on(' ').join(cldrLang));

        Map<String, R2<String, String>> rawVariables = supplementalData.getValidityInfo();
        for (Entry<String, R2<String, String>> item : rawVariables.entrySet()) {
            String id = item.getKey();
            String type = item.getValue().get0();
            String value = item.getValue().get1();
            MatcherPattern mp = getMatcherPattern2(type, value);
            if (mp != null) {
                variables.put(id, mp);
                // variableReplacer.add(id, value);
            } else {
                throw new IllegalArgumentException("Duplicate element " + mp);
            }
        }
        //System.out.println("Variables: " + variables.keySet());

        Map<AttributeValidityInfo, String> rawAttributeValueInfo = supplementalData.getAttributeValidity();
        int x = 0;
        for (Entry<AttributeValidityInfo, String> entry : rawAttributeValueInfo.entrySet()) {
            AttributeValidityInfo item = entry.getKey();
            String value = entry.getValue();
            //System.out.println(item);
            MatcherPattern mp = getMatcherPattern2(item.getType(), value);
            if (mp == null) {
                getMatcherPattern2(item.getType(), value); // for debugging
                failures.put(item, value);
                continue;
            }
            Set<DtdType> dtds = item.getDtds();
            if (dtds == null) {
                dtds = ALL_DTDs;
            }
            for (DtdType dtdType : dtds) {
                DtdData data = DtdData.getInstance(dtdType);
                Map<String, Map<String, MatcherPattern>> element_attribute_validity = dtd_element_attribute_validity.get(dtdType);
                if (element_attribute_validity == null) {
                    dtd_element_attribute_validity.put(dtdType, element_attribute_validity = new TreeMap<String, Map<String, MatcherPattern>>());
                }

                //             <attributeValues dtds="supplementalData" elements="currency" attributes="before from to">$currencyDate</attributeValues>

                Set<String> attributeList = item.getAttributes();
                Set<String> elementList = item.getElements();
                if (elementList.size() == 0) {
                    addAttributes(attributeList, common_attribute_validity, mp);
                } else {
                    for (String element : elementList) {
                        // check if unnecessary
                        DtdData.Element elementInfo = data.getElementFromName().get(element);
                        if (elementInfo == null) {
                            throw new ICUException(
                                "Illegal <attributeValues>, element not valid: "
                                    + dtdType
                                    + ", element: " + element);
                        } else {
                            for (String attribute : attributeList) {
                                DtdData.Attribute attributeInfo = elementInfo.getAttributeNamed(attribute);
                                if (attributeInfo == null) {
                                    throw new ICUException(
                                        "Illegal <attributeValues>, attribute not valid: "
                                            + dtdType
                                            + ", element: " + element
                                            + ", attribute: " + attribute);
                                } else if (!attributeInfo.values.isEmpty()) {
//                                    if (false) {
//                                        System.out.println("Unnecessary <attributeValues …>, the DTD has specific list: element: " + element + ", attribute: " + attribute + ", " + attributeInfo.values);
//                                    }
                                }
                            }
                        }
                        // System.out.println("\t" + element);
                        Map<String, MatcherPattern> attribute_validity = element_attribute_validity.get(element);
                        if (attribute_validity == null) {
                            element_attribute_validity.put(element, attribute_validity = new TreeMap<String, MatcherPattern>());
                        }
                        addAttributes(attributeList, attribute_validity, mp);
                    }
                }
            }
        }
        // show values
//        for (Entry<DtdType, Map<String, Map<String, MatcherPattern>>> entry1 : dtd_element_attribute_validity.entrySet()) {
//            final DtdType dtdType = entry1.getKey();
//            Map<String, Map<String, MatcherPattern>> element_attribute_validity = entry1.getValue();
//            DtdData dtdData2 = DtdData.getInstance(dtdType);
//            for (Element element : dtdData2.getElements()) {
//                Set<Attribute> attributes = element.getAttributes().keySet();
//
//            }
//            for (Entry<String, Map<String, MatcherPattern>> entry2 : entry1.getValue().entrySet()) {
//                for (Entry<String, MatcherPattern> entry3 : entry2.getValue().entrySet()) {
//                    System.out.println(dtdType + "\t" + entry2.getKey() + "\t" + entry3.getKey() + "\t" + entry3.getValue());
//                }
//            }
//        }

//        private LocaleIDParser localeIDParser = new LocaleIDParser();
        //
//        @Override
//        public CheckCLDR setCldrFileToCheck(CLDRFile cldrFileToCheck, Options options,
//            List<CheckStatus> possibleErrors) {
//            if (cldrFileToCheck == null) return this;
//            if (Phase.FINAL_TESTING == getPhase() || Phase.BUILD == getPhase()) {
//                setSkipTest(false); // ok
//            } else {
//                setSkipTest(true);
//                return this;
//            }
        //
//            pluralInfo = supplementalData.getPlurals(PluralType.cardinal, cldrFileToCheck.getLocaleID());
//            super.setCldrFileToCheck(cldrFileToCheck, options, possibleErrors);
//            isEnglish = "en".equals(localeIDParser.set(cldrFileToCheck.getLocaleID()).getLanguage());
//            synchronized (elementOrder) {
//                if (!initialized) {
//                    getMetadata();
//                    initialized = true;
//                    localeMatcher = LocaleMatcher.make();
//                }
//            }
//            if (!localeMatcher.matches(cldrFileToCheck.getLocaleID())) {
//                possibleErrors.add(new CheckStatus()
//                .setCause(null).setMainType(CheckStatus.errorType).setSubtype(Subtype.invalidLocale)
//                .setMessage("Invalid Locale {0}",
//                    new Object[] { cldrFileToCheck.getLocaleID() }));
        //
//            }
//            return this;
//        }
    }

    private static void addCollectionVariable(String name, Set<String> validItems) {
        variables.put(name, new CollectionMatcher(validItems));
    }

    public static Relation<String, String> getAllPossibleMissing(DtdType dtdType) {
        Relation<String, String> missing = Relation.of(new TreeMap<String, Set<String>>(), LinkedHashSet.class);

        if (dtdType == DtdType.ldmlICU) {
            return missing;
        }

        DtdData dtdData2 = DtdData.getInstance(dtdType);
        Map<String, Map<String, MatcherPattern>> element_attribute_validity = CldrUtility.ifNull(
            dtd_element_attribute_validity.get(dtdType),
            Collections.<String, Map<String, MatcherPattern>> emptyMap());

        for (DtdData.Element element : dtdData2.getElements()) {
            if (element.isDeprecated()) {
                continue;
            }
            Map<String, MatcherPattern> attribute_validity = CldrUtility.ifNull(
                element_attribute_validity.get(element.name),
                Collections.<String, MatcherPattern> emptyMap());
            for (DtdData.Attribute attribute : element.getAttributes().keySet()) {
                if (attribute.isDeprecated()) {
                    continue;
                }
                if (!attribute.values.isEmpty()) {
                    continue;
                }
                MatcherPattern validity = attribute_validity.get(attribute.name);
                if (validity != null) {
                    continue;
                }
                //            <attributeValues attributes="alt" type="choice">$alt</attributeValues>
                //             <attributeValues dtds="supplementalData" elements="character" attributes="value" type="regex">.</attributeValues>
                missing.put(attribute.name,
                    new AttributeValueSpec(dtdType, element.name, attribute.name, "$xxx").toString());
            }
        }
        return missing;
    }

    public static abstract class MatcherPattern {

        public abstract boolean matches(String value, Output<String> reason);

        public String getPattern() {
            String temp = _getPattern();
            return temp.length() <= MAX_STRING ? temp : temp.substring(0, MAX_STRING) + "…";
        }

        public abstract String _getPattern();

        public String toString() {
            return getClass().getName() + "\t" + getPattern();
        }
    }

//    private static MatcherPattern getBcp47MatcherPattern(String key) {
//        // <key type="calendar">Calendar</key>
//        // <type key="calendar" type="chinese">Chinese Calendar</type>
//
//        //<attributeValues elements="key" attributes="type" type="bcp47">key</attributeValues>
//        //<attributeValues elements="type" attributes="key" type="bcp47">key</attributeValues>
//        //<attributeValues elements="type" attributes="type" type="bcp47">use-key</attributeValues>
//
//        Set<String> values;
//        if (key.equals("key")) {
//            values = BCP47_KEY_VALUES.keySet();
//        } else {
//            values = BCP47_KEY_VALUES.get(key);
//        }
//        return new CollectionMatcher(values);
//    }

    enum MatcherTypes {
        single,
        choice,
        list,
        unicodeSet,
        regex,
        locale,
        bcp47,
        subdivision,
        localeSpecific,
        TODO;
    }

    private static MatcherPattern getMatcherPattern2(String type, String value) {
        final MatcherTypes matcherType = type == null ? MatcherTypes.single : MatcherTypes.valueOf(type);

        if (matcherType != MatcherTypes.TODO && value.startsWith("$")) {
            MatcherPattern result = getVariable(matcherType, value);
            if (result != null) {
                return result;
            }
            throw new IllegalArgumentException("Unknown variable: " + value);
        }

        MatcherPattern result;

        switch (matcherType) {
        case single:
            result = new CollectionMatcher(Collections.singleton(value.trim()));
            break;
        case choice:
            result = new CollectionMatcher(SPACE.splitToList(value));
            break;
        case unicodeSet:
            result = new UnicodeSetMatcher(new UnicodeSet(value));
            break;
//        case bcp47:
//            return getBcp47MatcherPattern(value);
        case regex:
            result = new RegexMatcher(value, Pattern.COMMENTS); // Pattern.COMMENTS to get whitespace
            break;
        case locale:
            result = value.equals("all") ? LocaleMatcher.ALL_LANGUAGES : LocaleMatcher.REGULAR;
            break;
        case localeSpecific:
            result = LocaleSpecificMatcher.getInstance(value);
            break;
        case TODO:
            result = NOT_DONE_YET;
            break;
        case list:
            result = new ListMatcher(new CollectionMatcher(SPACE.splitToList(value)));
            break;
        default:
            return null;
        }

        return result;
    }

    private static MatcherPattern getVariable(final MatcherTypes matcherType, String value) {
        List<String> values = BAR.splitToList(value); //value.trim().split("|");
        MatcherPattern[] reasons = new MatcherPattern[values.size()];
        for (int i = 0; i < values.size(); ++i) {
            reasons[i] = getNonNullVariable(values.get(i));
        }
        MatcherPattern result;

        if (reasons.length == 1) {
            result = reasons[0];
        } else {
            result = new OrMatcher(reasons);
        }
        if (matcherType == MatcherTypes.list) {
            result = new ListMatcher(result);
        }
        return result;
    }

    private static void addAttributes(Set<String> attributes, Map<String, MatcherPattern> attribute_validity, MatcherPattern mp) {
        for (String attribute : attributes) {
            MatcherPattern old = attribute_validity.get(attribute);
            if (old != null) {
                mp = new OrMatcher(old, mp);
            }
            attribute_validity.put(attribute, mp);
        }
    }

    public static class RegexMatcher extends MatcherPattern {

        private java.util.regex.Matcher matcher;

        public RegexMatcher(String pattern, int flags) {
            matcher = Pattern.compile(pattern, flags).matcher("");
        }

        public boolean matches(String value, Output<String> reason) {
            matcher.reset(value.toString());
            boolean result = matcher.matches();
            if (!result && reason != null) {
                reason.value = RegexUtilities.showMismatch(matcher, value.toString());
            }
            return result;
        }

        @Override
        public String _getPattern() {
            return matcher.toString();
        }
    }

    private static EnumMap<LocaleSpecific, Set<String>> LOCALE_SPECIFIC = null;

    /** WARNING, not thread-safe. Needs cleanup **/
    public static void setLocaleSpecifics(EnumMap<LocaleSpecific, Set<String>> newValues) {
        LOCALE_SPECIFIC = newValues;
    }

    public static class LocaleSpecificMatcher extends MatcherPattern {
        final LocaleSpecific ls;

        public LocaleSpecificMatcher(LocaleSpecific ls) {
            this.ls = ls;
        }

        public static LocaleSpecificMatcher getInstance(String value) {
            return new LocaleSpecificMatcher(LocaleSpecific.valueOf(value));
        }

        public boolean matches(String value) {
            return LOCALE_SPECIFIC.get(ls).contains(value);
        }

        static final int MAX_STRING = 64;

        public boolean matches(String value, Output<String> reason) {
            boolean result = LOCALE_SPECIFIC.get(ls).contains(value);
            if (!result && reason != null) {
                reason.value = "∉ " + getPattern();
            }
            return result;
        }

        @Override
        public String _getPattern() {
            return LOCALE_SPECIFIC.get(ls).toString();
        }
    }

    static final int MAX_STRING = 64;

    public static class CollectionMatcher extends MatcherPattern {
        private final Collection<String> collection;

        public CollectionMatcher(Collection<String> collection) {
            this.collection = Collections.unmodifiableCollection(new LinkedHashSet<>(collection));
        }

        @Override
        public boolean matches(String value, Output<String> reason) {
            boolean result = collection.contains(value);
            if (!result && reason != null) {
                reason.value = "∉ " + getPattern();
            }
            return result;
        }

        @Override
        public String _getPattern() {
            return collection.toString();
        }
    }

    public static class UnicodeSetMatcher extends MatcherPattern {
        private final UnicodeSet collection;

        public UnicodeSetMatcher(UnicodeSet collection) {
            this.collection = collection.freeze();
        }

        @Override
        public boolean matches(String value, Output<String> reason) {
            final UnicodeSet valueSet = new UnicodeSet(value);
            boolean result = collection.containsAll(valueSet);
            if (!result && reason != null) {
                reason.value = "∉ " + getPattern();
            }
            return result;
        }

        @Override
        public String _getPattern() {
            return collection.toPattern(false);
        }
    }

    public static class OrMatcher extends MatcherPattern {
        private final MatcherPattern[] operands;

        public OrMatcher(MatcherPattern... operands) {
            for (MatcherPattern operand : operands) {
                if (operand == null) {
                    throw new NullPointerException();
                }
            }
            this.operands = operands;
        }

        public boolean matches(String value, Output<String> reason) {
            StringBuilder fullReason = reason == null ? null : new StringBuilder();
            for (MatcherPattern operand : operands) {
                if (operand.matches(value, reason)) {
                    return true;
                }
                if (fullReason != null) {
                    if (fullReason.length() != 0) {
                        fullReason.append("&");
                    }
                    fullReason.append(reason.value);
                }
            }
            if (fullReason != null) {
                reason.value = fullReason.toString();
            }
            return false;
        }

        @Override
        public String _getPattern() {
            StringBuffer result = new StringBuffer();
            for (MatcherPattern operand : operands) {
                if (result.length() != 0) {
                    result.append('|');
                }
                result.append(operand._getPattern());
            }
            return result.toString();
        }
    }

    public static class ListMatcher extends MatcherPattern {
        private MatcherPattern other;

        public ListMatcher(MatcherPattern other) {
            this.other = other;
        }

        public boolean matches(String value, Output<String> reason) {
            List<String> values = SPACE.splitToList(value);
            if (values.isEmpty()) return true;
            for (String valueItem : values) {
                if (!other.matches(valueItem, reason)) {
                    if (reason != null) {
                        reason.value = "«" + valueItem + "» ∉ " + other.getPattern();
                    }
                    return false;
                }
            }
            return true;
        }

        @Override
        public String _getPattern() {
            return "List of " + other._getPattern();
        }
    }

    public static class LocaleMatcher extends MatcherPattern {
        //final ObjectMatcherReason grandfathered = getNonNullVariable("$grandfathered").matcher;
        final MatcherPattern language;
        final MatcherPattern script = getNonNullVariable("$_script");
        final MatcherPattern territory = getNonNullVariable("$_region");
        final MatcherPattern variant = getNonNullVariable("$_variant");
        final LocaleIDParser lip = new LocaleIDParser();

        public static LocaleMatcher REGULAR = new LocaleMatcher("$_language_plus");
        public static LocaleMatcher ALL_LANGUAGES = new LocaleMatcher("$_language");

        private LocaleMatcher(String variable) {
            language = getNonNullVariable(variable);
        }

        public boolean matches(String value, Output<String> reason) {
//            if (grandfathered.matches(value, reason)) {
//                return true;
//            }
            lip.set((String) value);
            String field = lip.getLanguage();
            if (!language.matches(field, reason)) {
                if (reason != null) {
                    reason.value = "invalid base language";
                }
                return false;
            }
            field = lip.getScript();
            if (field.length() != 0 && !script.matches(field, reason)) {
                if (reason != null) {
                    reason.value = "invalid script";
                }
                return false;
            }
            field = lip.getRegion();
            if (field.length() != 0 && !territory.matches(field, reason)) {
                if (reason != null) {
                    reason.value = "invalid region";
                }
                return false;
            }
            String[] fields = lip.getVariants();
            for (int i = 0; i < fields.length; ++i) {
                if (!variant.matches(fields[i], reason)) {
                    if (reason != null) {
                        reason.value = "invalid variant";
                    }
                    return false;
                }
            }
            return true;
        }

        @Override
        public String _getPattern() {
            return "Unicode_Language_Subtag";
        }
    }

    public static final class AttributeValueSpec implements Comparable<AttributeValueSpec> {
        public AttributeValueSpec(DtdType type, String element, String attribute, String attributeValue) {
            this.type = type;
            this.element = element;
            this.attribute = attribute;
            this.attributeValue = attributeValue;
        }

        public final DtdType type;
        public final String element;
        public final String attribute;
        public final String attributeValue;

        @Override
        public int hashCode() {
            return Objects.hash(type, element, attribute, attributeValue);
        }

        @Override
        public boolean equals(Object obj) {
            AttributeValueSpec other = (AttributeValueSpec) obj;
            return CldrUtility.deepEquals(
                type, other.type,
                element, other.element,
                attribute, other.attribute,
                attributeValue, other.attributeValue
                );
        }

        @Override
        public int compareTo(AttributeValueSpec other) {
            return ComparisonChain.start()
                .compare(type, other.type)
                .compare(element, other.element)
                .compare(attribute, other.attribute)
                .compare(attributeValue, other.attributeValue)
                .result();
        }

        @Override
        public String toString() {
            return "<attributeValues"
                + " dtds='" + type + "\'"
                + " elements='" + element + "\'"
                + " attributes='" + attribute + "\'"
                + " type='TODO\'>"
                + attributeValue
                + "</attributeValues>";
        }
    }

    /**
     * return Status
     * @param attribute_validity
     * @param attribute
     * @param attributeValue
     * @param result
     * @return
     */
    private static Status check(Map<String, MatcherPattern> attribute_validity,
        String element, String attribute, String attributeValue,
        Output<String> reason) {

        if (attribute_validity == null) {
            return Status.noTest; // no test
        }
        MatcherPattern matcherPattern = attribute_validity.get(attribute);
        if (matcherPattern == null) {
            return Status.noTest; // no test
        }
        if (matcherPattern.matches(attributeValue, reason)) {
            return Status.ok;
        }
        return Status.illegal;
    }

    public static Status check(DtdData dtdData, String element, String attribute, String attributeValue, Output<String> reason) {
        if (dtdData.isDeprecated(element, attribute, attributeValue)) {
            return Status.deprecated;
        }
        Status haveTest = check(common_attribute_validity, element, attribute, attributeValue, reason);

        if (haveTest == Status.noTest) {
            final Map<String, Map<String, MatcherPattern>> element_attribute_validity = dtd_element_attribute_validity.get(dtdData.dtdType);
            if (element_attribute_validity == null) {
                return Status.noTest;
            }

            Map<String, MatcherPattern> attribute_validity = element_attribute_validity.get(element);
            if (attribute_validity == null) {
                return Status.noTest;
            }

            haveTest = check(attribute_validity, element, attribute, attributeValue, reason);
        }
        return haveTest;
    }

    public static Set<R3<DtdType, String, String>> getTodoTests() {
        Set<Row.R3<DtdType, String, String>> result = new LinkedHashSet<>();
        for (Entry<DtdType, Map<String, Map<String, MatcherPattern>>> entry1 : dtd_element_attribute_validity.entrySet()) {
            for (Entry<String, Map<String, MatcherPattern>> entry2 : entry1.getValue().entrySet()) {
                for (Entry<String, MatcherPattern> entry3 : entry2.getValue().entrySet()) {
                    if (entry3.getValue() == NOT_DONE_YET) {
                        result.add(Row.of(entry1.getKey(), entry2.getKey(), entry3.getKey()));
                    }
                }
            }
        }
        return result;
    }

    public static Map<AttributeValidityInfo, String> getReadFailures() {
        return Collections.unmodifiableMap(failures);
    }

    public static MatcherPattern getMatcherPattern(String variable) {
        return variables.get(variable);
    }

    private static MatcherPattern getNonNullVariable(String variable) {
        MatcherPattern result = variables.get(variable);
        if (result == null) {
            throw new NullPointerException();
        }
        return result;
    }

    public static Set<String> getMatcherPatternIds() {
        return Collections.unmodifiableSet(variables.keySet());
    }
}
