package org.unicode.cldr.icu;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;

import org.unicode.cldr.icu.ICUResourceWriter.Resource;
import org.unicode.cldr.icu.ICUResourceWriter.ResourceAlias;
import org.unicode.cldr.icu.ICUResourceWriter.ResourceString;
import org.unicode.cldr.icu.ICUResourceWriter.ResourceTable;
import org.unicode.cldr.util.LDMLUtilities;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

public class KeyTypeDataConverter {
    private final ICULog log;
    private final String bcp47Dir;
    private final String[] externalTypeKeys;
    private Collection<Document> documents;

    private static final String SOURCE_INFO = "common/bcp47/*.xml";
    private static final String EXTERNAL_TYPES_SUFFIX = "Types";


    private static final boolean DEBUG = false;

    public KeyTypeDataConverter(ICULog log, String bcp47Dir, String[] externalTypeKeys) {
        this.log = log;
        this.bcp47Dir = bcp47Dir;
        this.externalTypeKeys = (externalTypeKeys == null) ? new String[0] : externalTypeKeys;
    }

    public void convert(ICUWriter writer) {
        // creating key mapping data
        Map<String, String> keyMap = new TreeMap<String, String>();
        for (Document doc : getDocuments()) {
            addKeyMap(keyMap, doc);
        }

        if (DEBUG) {
            log.log("Key mappings ---------------------------------------------");
            dumpMap(keyMap);
        }

        // creating type mapping data
        Map<String, Map<String, String>> typeMaps = new TreeMap<String, Map<String, String>>();
        Map<String, Map<String, String>> typeAliases = new TreeMap<String, Map<String, String>>();
        for (Document doc : getDocuments()) {
            addTypeMaps(typeMaps, typeAliases, doc);
        }

        if (DEBUG) {
            for (Map.Entry<String, Map<String, String>> e : typeMaps.entrySet()) {
                log.log("\n\n\nType mappings for " + e.getKey() + " ---------------------------------------------");
                dumpMap(e.getValue());
            }

            for (Map.Entry<String, Map<String, String>> e : typeAliases.entrySet()) {
                log.log("\n\n\nAlias mappings for " + e.getKey() + " ---------------------------------------------");
                dumpMap(e.getValue());
            }
        }

        // write out keyTypeData.txt
        Resource cur;

        ResourceTable keyTypeDataRes = new ResourceTable();
        keyTypeDataRes.name = LDMLBCP47Constants.KEYTYPEDATA;
        keyTypeDataRes.annotation = ResourceTable.NO_FALLBACK;

        // keyMap
        ResourceTable keyMapRes = new ResourceTable();
        keyMapRes.name = LDMLBCP47Constants.KEYMAP;
        keyTypeDataRes.first = keyMapRes;

        cur = null;
        for (Entry<String, String> keyMapItem : keyMap.entrySet()) {
            ResourceString keyRes = new ResourceString();
            keyRes.name = keyMapItem.getKey();
            keyRes.val = keyMapItem.getValue();

            if (cur == null) {
                keyMapRes.first = keyRes;
            } else {
                cur.next = keyRes;
            }
            cur = keyRes;
        }

        // typeMap
        Resource typeMapRes = createTypeMapResource(typeMaps, null);
        keyMapRes.next = typeMapRes;

        // typeAlias
        Resource typeAliasRes = createTypeAliasResource(typeAliases, null);
        typeMapRes.next = typeAliasRes;

        writer.writeResource(keyTypeDataRes, SOURCE_INFO);

        // externalized type/alias map data
        for (String key : externalTypeKeys) {
            ResourceTable extTypeDataRes = new ResourceTable();
            extTypeDataRes.name = key + EXTERNAL_TYPES_SUFFIX;
            extTypeDataRes.annotation = ResourceTable.NO_FALLBACK;

            // typeMap
            Resource extTypeMapRes = createTypeMapResource(typeMaps, key);
            extTypeDataRes.first = extTypeMapRes;

            // typeAlias
            Resource extTypeAliasRes = createTypeAliasResource(typeAliases, key);
            extTypeMapRes.next = extTypeAliasRes;

            writer.writeResource(extTypeDataRes, SOURCE_INFO);
        }

    }

    private Collection<Document> getDocuments() {
        if (documents != null) {
            return documents;
        }

        documents = new ArrayList<Document>();

        FilenameFilter filter = new FilenameFilter() {
            public boolean accept(File dir, String name) {
                if (name.endsWith(".xml")) {
                    return true;
                }
                return false;
            }
        };

        File dir = new File(bcp47Dir);
        String[] files = dir.list(filter);
        if (files == null) {
            String canonicalPath;
            try {
              canonicalPath = dir.getCanonicalPath();
            } catch (IOException e) {
              canonicalPath = e.getMessage();
            }
            log.error("BCP47 files are missing " + canonicalPath);
            System.exit(-1);
        }

        String dirPath = dir.getAbsolutePath();
        for (String fileName : files) {
            try {
                log.info("Parsing document " + fileName);
                String filePath = dirPath + File.separator + fileName;
                Document doc = LDMLUtilities.parse(filePath, false);
                documents.add(doc);
            } catch (Throwable se) {
              log.error("Parsing: " + fileName + " " + se.toString(), se);
              System.exit(1);
            }
        }
        return documents;
    }

    private static void addKeyMap(Map<String, String> keyMap, Document root) {
        for (Node node = root.getFirstChild(); node != null; node = node.getNextSibling()) {
            if (node.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            if (node.getNodeName().equals(LDMLBCP47Constants.LDMLBCP47)) {
                // Stop iterating over top-level elements, restart iterating over elements
                // under ldmlBCP47.
                node = node.getFirstChild();
                continue;
            }

            if (node.getNodeName().equals(LDMLBCP47Constants.KEYWORD)) {
                // iterating into elements under keyword
                node = node.getFirstChild();
                continue;
            }

            if (node.getNodeName().equals(LDMLBCP47Constants.KEY)) {
                String bcpKey = LDMLUtilities.getAttributeValue(node, LDMLBCP47Constants.NAME);
                String key = LDMLUtilities.getAttributeValue(node, LDMLBCP47Constants.ALIAS);
                if (key != null && !bcpKey.equals(key)) {
                    /* keys are case-insensitive */
                    keyMap.put(escapeKey(key.toLowerCase(Locale.ROOT)), bcpKey);
                }
            }
        }
    }

    private static void addTypeMaps(Map<String, Map<String, String>> typeMaps, Map<String, Map<String, String>> typeAliases, Document root) {
        for (Node node = root.getFirstChild(); node != null; node = node.getNextSibling()) {
            if (node.getNodeType() != Node.ELEMENT_NODE) {
                continue;
            }

            if (node.getNodeName().equals(LDMLBCP47Constants.LDMLBCP47)) {
                // Stop iterating over top-level elements, restart iterating over elements
                // under ldmlBCP47.
                node = node.getFirstChild();
                continue;
            }

            if (node.getNodeName().equals(LDMLBCP47Constants.KEYWORD)) {
                // iterating into elements under keyword
                node = node.getFirstChild();
                continue;
            }

            if (node.getNodeName().equals(LDMLBCP47Constants.KEY)) {
                String bcpKey = LDMLUtilities.getAttributeValue(node, LDMLBCP47Constants.NAME);
                String key = LDMLUtilities.getAttributeValue(node, LDMLBCP47Constants.ALIAS);
                if (key == null) {
                    key = bcpKey;
                }
                key = key.toLowerCase(Locale.ROOT); // keys are case-insensitive
                for (Node node2 = node.getFirstChild(); node2 != null; node2 = node2.getNextSibling()) {
                    if (node2.getNodeType() != Node.ELEMENT_NODE) {
                        continue;
                    }
                    if (node2.getNodeName().equals(LDMLBCP47Constants.TYPE)) {
                        String bcpType = LDMLUtilities.getAttributeValue(node2, LDMLBCP47Constants.NAME);
                        String type = LDMLUtilities.getAttributeValue(node2, LDMLBCP47Constants.ALIAS);
                        if (type != null) {
                            // type may contain multiple values delimited by space character
                            String[] types = type.split(" ");
                            if (types.length > 1) {
                                type = types[0];

                                // add 2nd and following type values into the alias map
                                Map<String, String> singleTypeAliases = typeAliases.get(key);
                                if (singleTypeAliases == null) {
                                    singleTypeAliases = new TreeMap<String, String>();
                                    typeAliases.put(key, singleTypeAliases);
                                }
                                for (int i = 1; i < types.length; i++) {
                                    singleTypeAliases.put(escapeKey(types[i]), type);
                                }
                            }
                        }

                        if (type != null && !bcpType.equals(type)) {
                            // only populating mapping data when bcp47 representation is different
                            Map<String, String> singleTypeMap = typeMaps.get(key);
                            if (singleTypeMap == null) {
                                singleTypeMap = new TreeMap<String, String>();
                                typeMaps.put(key, singleTypeMap);
                            }
                            singleTypeMap.put(escapeKey(type), bcpType);
                        }
                    }
                }
            }
        }
    }

    private Resource createTypeMapResource(Map<String, Map<String, String>> typeMaps, String key) {
        ResourceTable typeMapRes = new ResourceTable();
        typeMapRes.name = LDMLBCP47Constants.TYPEMAP;

        Resource cur = null;
        for (Entry<String, Map<String, String>> typesForKeyItem : typeMaps.entrySet()) {
            String itemKey = typesForKeyItem.getKey();
            if (key != null && !itemKey.equals(key)) {
                // skip this key
                continue;
            }

            String aliasName = null;
            if (key == null) {
                for (String extKey : externalTypeKeys) {
                    if (extKey.equals(itemKey)) {
                        aliasName = "/ICUDATA/" + itemKey + EXTERNAL_TYPES_SUFFIX
                                    + "/" + LDMLBCP47Constants.TYPEMAP
                                    + "/" + itemKey;
                        break;
                    }
                }
            }

            Resource res = null;
            if (aliasName != null) {
                // generating alias resource
                ResourceAlias typeMapForKeyResAlias = new ResourceAlias();
                typeMapForKeyResAlias.name = itemKey;
                typeMapForKeyResAlias.val = aliasName;

                res = typeMapForKeyResAlias;
            } else {
                // generating type mapping container table per key
                ResourceTable typeMapForKeyRes = new ResourceTable();
                typeMapForKeyRes.name = itemKey;

                Resource curTypeRes = null;
                for (Entry<String, String> typeItem : typesForKeyItem.getValue().entrySet()) {
                    // generating each type map data
                    ResourceString typeRes = new ResourceString();
                    typeRes.name = typeItem.getKey();
                    typeRes.val = typeItem.getValue();

                    if (curTypeRes == null) {
                        typeMapForKeyRes.first = typeRes;
                    } else {
                        curTypeRes.next = typeRes;
                    }
                    curTypeRes = typeRes;
                }

                res = typeMapForKeyRes;
            }

            if (cur == null) {
                typeMapRes.first = res;
            } else {
                cur.next = res;
            }
            cur = res;
        }

        return typeMapRes;
    }

    private Resource createTypeAliasResource(Map<String, Map<String, String>> typeAliases, String key) {
        ResourceTable typeAliasRes = new ResourceTable();
        typeAliasRes.name = LDMLBCP47Constants.TYPEALIAS;

        Resource cur = null;
        for (Entry<String, Map<String, String>> aliasesForKeyItem: typeAliases.entrySet()) {
            String itemKey = aliasesForKeyItem.getKey();
            if (key != null && !itemKey.equals(key)) {
                // skip this key
                continue;
            }

            String aliasName = null;
            if (key == null) {
                for (String extKey : externalTypeKeys) {
                    if (extKey.equals(itemKey)) {
                        aliasName = "/ICUDATA/" + itemKey + EXTERNAL_TYPES_SUFFIX
                                    + "/" + LDMLBCP47Constants.TYPEALIAS
                                    + "/" + itemKey;
                        break;
                    }
                }
            }

            Resource res = null;

            if (aliasName != null) {
                // generating alias resource
                ResourceAlias aliasesForKeyResAlias = new ResourceAlias();
                aliasesForKeyResAlias.name = itemKey;
                aliasesForKeyResAlias.val = aliasName;

                res = aliasesForKeyResAlias;
            } else {
                // generating alias mapping container table per key
                ResourceTable aliasesForKeyRes = new ResourceTable();
                aliasesForKeyRes.name = itemKey;

                Resource curAliasRes = null;
                for (Entry<String, String> aliasItem : aliasesForKeyItem.getValue().entrySet()) {
                    // generating each alias map data
                    ResourceString aliasRes = new ResourceString();
                    aliasRes.name = aliasItem.getKey();
                    aliasRes.val = aliasItem.getValue();

                    if (curAliasRes == null) {
                        aliasesForKeyRes.first = aliasRes;
                    } else {
                        curAliasRes.next = aliasRes;
                    }
                    curAliasRes = aliasRes;
                }

                res = aliasesForKeyRes;
            }

            if (cur == null) {
                typeAliasRes.first = res;
            } else {
                cur.next = res;
            }
            cur = res;
        }

        return typeAliasRes;
    }

    private static String escapeKey(String key) {
        if (key.contains("/")) {
            key = "\"" + key.replace('/', ':') + "\"";
        }
        return key;
    }

    private void dumpMap(Map<String, String> map) {
        for (Map.Entry<String, String> e : map.entrySet()) {
            log.log(e.getKey() + " -> " + e.getValue());
        }
    }

    private static class LDMLBCP47Constants {
        static final String LDMLBCP47 = "ldmlBCP47";
        static final String KEYWORD = "keyword";
        static final String KEY = "key";
        static final String NAME = "name";
        static final String ALIAS = "alias";
        static final String TYPE = "type";
        static final String KEYTYPEDATA = "keyTypeData";
        static final String KEYMAP = "keyMap";
        static final String TYPEMAP = "typeMap";
        static final String TYPEALIAS = "typeAlias";
    }
}
