package org.unicode.cldr.tool;

import java.io.File;
import java.io.PrintWriter;
import java.util.EnumMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Matcher;

import org.unicode.cldr.util.AttributeValueValidity;
import org.unicode.cldr.util.AttributeValueValidity.AttributeValueSpec;
import org.unicode.cldr.util.AttributeValueValidity.LocaleSpecific;
import org.unicode.cldr.util.AttributeValueValidity.Status;
import org.unicode.cldr.util.CLDRConfig;
import org.unicode.cldr.util.CLDRPaths;
import org.unicode.cldr.util.ChainedMap;
import org.unicode.cldr.util.DayPeriodInfo;
import org.unicode.cldr.util.DayPeriodInfo.DayPeriod;
import org.unicode.cldr.util.DayPeriodInfo.Type;
import org.unicode.cldr.util.DtdData;
import org.unicode.cldr.util.DtdData.Attribute;
import org.unicode.cldr.util.DtdData.Element;
import org.unicode.cldr.util.DtdType;
import org.unicode.cldr.util.SupplementalDataInfo;
import org.unicode.cldr.util.SupplementalDataInfo.PluralType;
import org.unicode.cldr.util.XMLFileReader;
import org.unicode.cldr.util.XMLFileReader.SimpleHandler;
import org.unicode.cldr.util.XPathParts;

import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.ibm.icu.impl.Row.R3;
import com.ibm.icu.util.Output;

public class VerifyAttributeValues extends SimpleHandler {
    private static final File BASE_DIR = new File(CLDRPaths.BASE_DIRECTORY);
    private static final SupplementalDataInfo supplementalData = CLDRConfig.getInstance().getSupplementalDataInfo();

    public static final Joiner SPACE_JOINER = Joiner.on(' ');
    public static final Splitter SPACE_SPLITTER = Splitter.on(' ').trimResults().omitEmptyStrings();

    public final static class Errors {

        @SuppressWarnings({ "unchecked", "rawtypes" })
        final ChainedMap.M3<String, AttributeValueSpec, String> file_element_attribute
        = ChainedMap.of(new TreeMap(), new TreeMap(), String.class);

        public void put(String file, DtdType dtdType, String element, String attribute, String attributeValue, String problem) {
            file_element_attribute.put(file, new AttributeValueSpec(dtdType, element, attribute, attributeValue), problem);
        }

        public Iterable<R3<String, AttributeValueSpec, String>> getRows() {
            return file_element_attribute.rows();
        }
    }

    private DtdData dtdData; // set from first element read
    private final Errors file_element_attribute;
    private final String file;
    private final EnumMap<LocaleSpecific,Set<String>> localeSpecific = new EnumMap<>(LocaleSpecific.class);
    private final Set<AttributeValueSpec> missing;

    private VerifyAttributeValues(String fileName, Errors file_element_attribute, Set<AttributeValueSpec> missing) {
        this.file_element_attribute = file_element_attribute;
        this.file = fileName.startsWith(BASE_DIR.toString()) ? fileName.substring(BASE_DIR.toString().length()) : fileName;
        this.missing = missing;
    }

    /**
     * Check the filename—note that the errors and missing are <b>added to<b>, so clear if you want a fresh start!
     * @param fileName
     * @param errors
     * @param missing
     */
    public static void check(String fileName, Errors errors, Set<AttributeValueSpec> missing) {
        try {
            final VerifyAttributeValues platformHandler = new VerifyAttributeValues(fileName, errors, missing);
            new XMLFileReader()
            .setHandler(platformHandler)
            .read(fileName, -1, true);
        } catch (Exception e) {
            throw new IllegalArgumentException(fileName, e);
        }
    }

    public void handlePathValue(String path, String value) {
        XPathParts parts = XPathParts.getFrozenInstance(path);
        if (dtdData == null) {
            dtdData = DtdData.getInstance(DtdType.valueOf(parts.getElement(0)));
            if (dtdData.dtdType == DtdType.ldml) {
                String name = file;
                String locale = name.substring(name.lastIndexOf('/')+1, name.lastIndexOf('.'));
                localeSpecific.put(LocaleSpecific.pluralCardinal, supplementalData.getPlurals(PluralType.cardinal, locale).getPluralRules().getKeywords());
                localeSpecific.put(LocaleSpecific.pluralOrdinal, supplementalData.getPlurals(PluralType.ordinal, locale).getPluralRules().getKeywords());
                localeSpecific.put(LocaleSpecific.dayPeriodFormat, getPeriods(Type.format, locale));
                localeSpecific.put(LocaleSpecific.dayPeriodSelection, getPeriods(Type.selection, locale));
            } else {
                localeSpecific.clear();
            }
            AttributeValueValidity.setLocaleSpecifics(localeSpecific);
        }

        for (int i = 0; i < parts.size(); ++i) {
            if (parts.getAttributeCount(i) == 0) continue;
            Map<String, String> attributes = parts.getAttributes(i);
            String element = parts.getElement(i);
            if (element.equals("attributeValues")) {
                continue; // don't look at ourselves in the mirror
            }
            Element elementInfo = dtdData.getElementFromName().get(element);

            for (String attribute : attributes.keySet()) {
                Attribute attributeInfo = elementInfo.getAttributeNamed(attribute);
                if (!attributeInfo.values.isEmpty()) {
                    // we don't need to check, since the DTD will enforce values
                    continue;
                }
                String attributeValue = attributes.get(attribute);
                if (dtdData.isDeprecated(element, attribute, attributeValue)) {
                    file_element_attribute.put(file, dtdData.dtdType, element, attribute, attributeValue, "deprecated");
                    continue;
                }

                Output<String> reason = new Output<>();
                Status haveTest = AttributeValueValidity.check(dtdData, element, attribute, attributeValue, reason);
                switch(haveTest) {
                case ok:
                    break;
                case deprecated: 
                case illegal:
                    file_element_attribute.put(file, dtdData.dtdType, element, attribute, attributeValue, reason.value);
                    break;
                case noTest:
                    missing.add(new AttributeValueSpec(dtdData.dtdType, element, attribute, attributeValue));
                    break;
                }
            }
        }
    }

    private Set<String> getPeriods(Type selection, String locale) {
        Set<String> result = new TreeSet<>();
        final DayPeriodInfo dayPeriods = supplementalData.getDayPeriods(Type.format, locale);
        for (DayPeriod period : dayPeriods.getPeriods()) {
            result.add(period.toString());
        };
        result.add("am");
        result.add("pm");
        return new LinkedHashSet<>(result);
    }

    public static int findAttributeValues(File file, int max, Matcher fileMatcher, Errors errors, Set<AttributeValueSpec> allMissing, PrintWriter out) {
        final String name = file.getName();
        if (file.isDirectory() 
            && !name.equals("specs") 
            && !name.equals("tools")
            && !file.toString().contains(".svn")
            // && !name.equals("keyboards") // TODO reenable keyboards
            ) {
            int processed = 0;
            int count = max;
            for (File subfile : file.listFiles()) {
                final String subname = subfile.getName();
                if (--count < 0 
                    && !"en.xml".equals(subname)
                    && !"root.xml".equals(subname)
                    ) {
                    continue;
                }
                processed += findAttributeValues(subfile, max, fileMatcher, errors, allMissing, out);
            }
            if (out != null) {
                out.println("Processed files: " + processed + " \tin " + file);
                out.flush();
            }
            return processed;
        } else if (name.endsWith(".xml")) {
            if (fileMatcher == null || fileMatcher.reset(name.substring(0, name.length()-4)).matches()) {
                check(file.toString(), errors, allMissing);
                return 1;
            }
        }
        return 0;
    }

}