package org.unicode.cldr.test;

import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.unicode.cldr.test.CheckCLDR.CheckStatus.Subtype;
import org.unicode.cldr.util.CLDRFile;
import org.unicode.cldr.util.CldrUtility;
import org.unicode.cldr.util.Counter;
import org.unicode.cldr.util.Factory;
import org.unicode.cldr.util.PathStarrer;
import org.unicode.cldr.util.RegexLookup;
import org.unicode.cldr.util.XPathParts;

import com.ibm.icu.dev.util.CollectionUtilities;
import com.ibm.icu.lang.UCharacter;
import com.ibm.icu.text.BreakIterator;
import com.ibm.icu.util.ULocale;

public class CheckConsistentCasing extends FactoryCheckCLDR {

    private static final boolean DEBUG = CldrUtility.getProperty("DEBUG", false);

    private static final double MIN_FACTOR = 2.5;
    // remember to add this class to the list in CheckCLDR.getCheckAll
    // to run just this test, on just locales starting with 'nl', use CheckCLDR with -fnl.* -t.*Currencies.*

    XPathParts parts = new XPathParts(); // used to parse out a path
    ULocale uLocale = null;
    BreakIterator breaker = null;
    private String locale;
    CasingInfo casingInfo;
    private boolean hasCasingInfo;

    public CheckConsistentCasing(Factory factory) {
        super(factory);
        casingInfo = new CasingInfo(factory.getSupplementalDirectory().getAbsolutePath() + "/../casing"); // TODO: fix.
    }

    public CheckCLDR setCldrFileToCheck(CLDRFile cldrFileToCheck, Map<String, String> options,
        List<CheckStatus> possibleErrors) {
        if (cldrFileToCheck == null) return this;
        super.setCldrFileToCheck(cldrFileToCheck, options, possibleErrors);
        locale = cldrFileToCheck.getLocaleID();
        types = casingInfo.getLocaleCasing(locale);
        if (types == null) {
            possibleErrors.add(new CheckStatus().setCause(this)
                .setMainType(CheckStatus.warningType)
                .setSubtype(Subtype.incorrectCasing)
                .setMessage("Could not load casing info for {0}", locale));
            hasCasingInfo = false;
        }
        hasCasingInfo = types.size() > 0;
        return this;
    }

    // If you don't need any file initialization or postprocessing, you only need this one routine
    public CheckCLDR handleCheck(String path, String fullPath, String value, Map<String, String> options,
        List<CheckStatus> result) {
        // it helps performance to have a quick reject of most paths
        if (fullPath == null) return this; // skip paths that we don't have
        if (!hasCasingInfo) return this;

        String locale2 = getCldrFileToCheck().getSourceLocaleID(path, null);
        if (locale2.equals(locale) && value != null && value.length() > 0) {
            Category category = getCategory(path);
            if (category != null) {
                checkConsistentCasing(category, path, fullPath, value, options, result);
            }
        }
        return this;
    }

    static final Matcher placeholder = Pattern.compile("\\{\\d+\\}").matcher("");

    /**
     * The casing type of a given string.
     */
    enum CasingType {
        titlecase, lowercase, other;
        public static CasingType from(String s) {
            if (s == null || s.length() == 0) {
                return other;
            }
            int cp;
            // Look for the first meaningful character in the string to determine case.
            for (int i = 0; i < s.length(); i += Character.charCount(cp)) {
                cp = s.codePointAt(i);
                // used to skip the placeholders, but works better to have them be 'other'
                // if (cp == '{') {
                // if (placeholder.reset(s).region(i,s.length()).lookingAt()) {
                // i = placeholder.end() - 1; // skip
                // continue;
                // }
                // }
                int type = UCharacter.getType(cp);
                switch (type) {

                case UCharacter.LOWERCASE_LETTER:
                    return lowercase;

                case UCharacter.UPPERCASE_LETTER:
                case UCharacter.TITLECASE_LETTER:
                    return titlecase;

                    // for other letters / numbers / symbols, return other
                case UCharacter.OTHER_LETTER:
                case UCharacter.DECIMAL_DIGIT_NUMBER:
                case UCharacter.LETTER_NUMBER:
                case UCharacter.OTHER_NUMBER:
                case UCharacter.MATH_SYMBOL:
                case UCharacter.CURRENCY_SYMBOL:
                case UCharacter.MODIFIER_SYMBOL:
                case UCharacter.OTHER_SYMBOL:
                    return other;
                    // ignore everything else (whitespace, punctuation, etc) and keep going
                }
            }
            return other;
        }

        /**
         * Return true if either is other, or they are identical.
         */
        public boolean worksWith(CasingType otherType) {
            return otherType == null || this == otherType || this == CasingType.other || otherType == CasingType.other;
        }
    }

    static final RegexLookup<Category> pathToBucket = new RegexLookup<Category>()
        .add("//ldml/localeDisplayNames/languages/language", Category.language)
        .add("//ldml/localeDisplayNames/scripts/script", Category.script)
        .add("//ldml/localeDisplayNames/territories/territory", Category.territory)
        .add("//ldml/localeDisplayNames/variants/variant", Category.variant)
        .add("//ldml/localeDisplayNames/keys/key", Category.key)
        .add("//ldml/localeDisplayNames/types/type", Category.type)
        .add("//ldml/dates/calendars/calendar.*/months.*narrow", Category.month_narrow)
        .add("//ldml/dates/calendars/calendar.*/months.*format", Category.month_format_except_narrow)
        .add("//ldml/dates/calendars/calendar.*/months", Category.month_standalone_except_narrow)
        .add("//ldml/dates/calendars/calendar.*/days.*narrow", Category.day_narrow)
        .add("//ldml/dates/calendars/calendar.*/days.*format", Category.day_format_except_narrow)
        .add("//ldml/dates/calendars/calendar.*/days", Category.day_standalone_except_narrow)
        .add("//ldml/dates/calendars/calendar.*/eras/eraNarrow", Category.era_narrow)
        .add("//ldml/dates/calendars/calendar.*/eras/eraAbbr", Category.era_abbr)
        .add("//ldml/dates/calendars/calendar.*/eras/", Category.era_name)
        .add("//ldml/dates/calendars/calendar.*/quarters.*narrow", Category.quarter_narrow)
        .add("//ldml/dates/calendars/calendar.*/quarters.*abbreviated", Category.quarter_abbreviated)
        .add("//ldml/dates/calendars/calendar.*/quarters.*format", Category.quarter_format_wide)
        .add("//ldml/dates/calendars/calendar.*/quarters", Category.quarter_standalone_wide)
        .add("//ldml/.*/relative", Category.tense)
        .add("//ldml/dates/fields", Category.calendar_field)
        .add("//ldml/dates/timeZoneNames/zone.*/exemplarCity", Category.zone_exemplarCity)
        .add("//ldml/dates/timeZoneNames/zone.*/short", Category.zone_short)
        .add("//ldml/dates/timeZoneNames/zone", Category.zone_long)
        .add("//ldml/dates/timeZoneNames/metazone.*/commonlyUsed", Category.NOT_USED) // just to remove them from the other cases
        .add("//ldml/dates/timeZoneNames/metazone.*/short", Category.metazone_long)
        .add("//ldml/dates/timeZoneNames/metazone", Category.metazone_long)
        .add("//ldml/numbers/currencies/currency.*/symbol", Category.symbol)
        .add("//ldml/numbers/currencies/currency.*/displayName.*@count", Category.displayName_count)
        .add("//ldml/numbers/currencies/currency.*/displayName", Category.displayName)
        .add("//ldml/units/unit.*/unitPattern.*(past|future)", Category.tense)
        .add("//ldml/units/unit.*/unitPattern", Category.unit_pattern)
    // ldml/localeDisplayNames/keys/key[@type=".*"]
    // ldml/localeDisplayNames/measurementSystemNames/measurementSystemName[@type=".*"]
    // ldml/localeDisplayNames/transformNames/transformName[@type=".*"]
    ;

    Map<Category, CasingType> types = new EnumMap<Category, CasingType>(Category.class);

    public enum Category {
        language, script, territory, variant, type,
        month_narrow, month_format_except_narrow, month_standalone_except_narrow,
        day_narrow, day_format_except_narrow, day_standalone_except_narrow,
        era_narrow, era_abbr, era_name,
        quarter_narrow, quarter_abbreviated, quarter_format_wide, quarter_standalone_wide,
        calendar_field,
        zone_exemplarCity, zone_short, zone_long,
        NOT_USED,
        metazone_short, metazone_long,
        symbol,
        displayName_count, displayName,
        tense, unit_pattern,
        key;
    }

    // //ldml/numbers/currencies/currency[@type="ADP"]/displayName
    // //ldml/numbers/currencies/currency[@type="RON"]/displayName[@count="other"]
    // //ldml/numbers/currencies/currency[@type="BYB"]/symbol

    static Category getCategory(String path) {
        return pathToBucket.get(path);
    }

    /**
     * Calculates casing information using data from the specified CLDRFile.
     * 
     * @param resolved
     *            the resolved CLDRFile to calculate casing information from
     * @return
     */
    public static Map<Category, CasingType> getSamples(CLDRFile resolved) {
        // Use EnumMap instead of an array for type safety.
        Map<Category, Counter<CasingType>> counters = new EnumMap<Category, Counter<CasingType>>(Category.class);

        for (Category category : Category.values()) {
            counters.put(category, new Counter<CasingType>());
        }
        PathStarrer starrer = new PathStarrer();
        boolean isRoot = "root".equals(resolved.getLocaleID());
        Set<String> missing = !DEBUG ? null : new TreeSet<String>();

        for (String path : resolved) {
            if (!isRoot) {
                String locale2 = resolved.getSourceLocaleID(path, null);
                if (locale2.equals("root") || locale2.equals("code-fallback")) {
                    continue;
                }
            }
            String winningPath = resolved.getWinningPath(path);
            if (!winningPath.equals(path)) {
                continue;
            }
            Category category = getCategory(path);
            if (category != null) {
                String value = resolved.getStringValue(path);
                if (value == null || value.length() == 0) continue;
                CasingType ft = CasingType.from(value);
                counters.get(category).add(ft, 1);
            } else if (DEBUG) {
                String starred = starrer.set(path);
                missing.add(starred);
            }
        }

        Map<Category, CasingType> info = new EnumMap<Category, CasingType>(Category.class);
        for (Category category : Category.values()) {
            if (category == Category.NOT_USED) continue;
            Counter<CasingType> counter = counters.get(category);
            long countLower = counter.getCount(CasingType.lowercase);
            long countUpper = counter.getCount(CasingType.titlecase);
            long countOther = counter.getCount(CasingType.other);
            CasingType type;
            if (countLower + countUpper == 0) {
                type = CasingType.other;
            } else if (countLower >= countUpper * MIN_FACTOR && countLower >= countOther) {
                type = CasingType.lowercase;
            } else if (countUpper >= countLower * MIN_FACTOR && countUpper >= countOther) {
                type = CasingType.titlecase;
            } else {
                type = CasingType.other;
            }
            info.put(category, type);
        }
        if (DEBUG && missing.size() != 0) {
            System.out.println("Paths skipped:\n" + CollectionUtilities.join(missing, "\n"));
        }
        return info;
    }

    private void checkConsistentCasing(Category category, String path, String fullPath, String value,
        Map<String, String> options, List<CheckStatus> result) {
        CasingType ft = CasingType.from(value);
        if (!ft.worksWith(types.get(category))) {
            result.add(new CheckStatus().setCause(this)
                .setMainType(CheckStatus.warningType)
                .setSubtype(Subtype.incorrectCasing) // typically warningType or errorType
                .setMessage("The first letter of 〈{0}〉 is {1}, which differs from the majority of this type: {2}",
                    value, ft, types.get(category))); // the message; can be MessageFormat with arguments
        }
    }
}