package org.unicode.cldr.tool;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.unicode.cldr.util.CLDRConfig;
import org.unicode.cldr.util.CldrUtility;
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 com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.ibm.icu.dev.util.CollectionUtilities;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.util.VersionInfo;

/**
 * Changed ShowDtdDiffs into a chart.
 * @author markdavis
 */
public class ChartDtdDelta extends Chart {

    public static void main(String[] args) {
        new ChartDtdDelta().writeChart(null);
    }

    @Override
    public String getDirectory() {
        return FormattedFileWriter.CHART_TARGET_DIR;
    }

    @Override
    public String getTitle() {
        return "DTD Deltas";
    }

    @Override
    public String getExplanation() {
        return "<p>Shows additions to the LDML dtds over time. New elements or attributes are indicated with a + sign. "
            + "Currently deprecated elements and attributes are omitted."
            + "<p>";
    }

    @Override
    public void writeContents(FormattedFileWriter pw) throws IOException {

        TablePrinter tablePrinter = new TablePrinter()
        .addColumn("Version", "class='source'", CldrUtility.getDoubleLinkMsg(), "class='source'", true)
        .setSortPriority(0)
        .setSortAscending(false)
        .setBreakSpans(true)
        .addColumn("Dtd Type", "class='source'", null, "class='source'", true)
        .setSortPriority(1)

        .addColumn("Intermediate Path", "class='source'", null, "class='target'", true)
        .setSortPriority(2)

        .addColumn("Element", "class='target'", null, "class='target'", true)
        .setSpanRows(false)
        .addColumn("Attributes", "class='target'", null, "class='target'", true)
        .setSpanRows(false);

        String last = null;
        LinkedHashSet<String> allVersions = new LinkedHashSet<>(ToolConstants.CLDR_VERSIONS);
        allVersions.add(ToolConstants.LAST_CHART_VERSION);
        for (String current : allVersions) {
            System.out.println("DTD delta: " + current);
            final boolean finalVersion = current.equals(ToolConstants.LAST_CHART_VERSION);
            String currentName = finalVersion ? ToolConstants.CHART_DISPLAY_VERSION : current;
            for (DtdType type : TYPES) {
                String firstVersion = type.firstVersion; // FIRST_VERSION.get(type);
                if (firstVersion != null && current != null && current.compareTo(firstVersion) < 0) {
                    continue;
                }
                DtdData dtdCurrent = null;
                try {
                    dtdCurrent = DtdData.getInstance(type,
                        finalVersion && ToolConstants.CHART_STATUS != ToolConstants.ChartStatus.release ? null : current);
                } catch (Exception e) {
                    if (!(e.getCause() instanceof FileNotFoundException)) {
                        throw e;
                    }
                    System.out.println(e.getMessage() + ", " + e.getCause().getMessage());
                    continue;
                }
                DtdData dtdLast = null;
                if (last != null) {
                    try {
                        dtdLast = DtdData.getInstance(type, last);
                    } catch (Exception e) {
                        if (!(e.getCause() instanceof FileNotFoundException)) {
                            throw e;
                        }
                    }
                }
                diff(currentName, dtdLast, dtdCurrent);
            }
            last = current;
        }

        for (DiffElement datum : data) {
            tablePrinter.addRow()
            .addCell(datum.getVersionString())
            .addCell(datum.dtdType)
            .addCell(datum.newPath)
            .addCell(datum.newElement)
            .addCell(datum.attributeNames)
            .finishRow();
        }
        pw.write(tablePrinter.toTable());
        pw.write(Utility.repeat("<br>", 50));
    }

    static final String NONE = " ";

    static final SupplementalDataInfo SDI = CLDRConfig.getInstance().getSupplementalDataInfo();

    static Set<DtdType> TYPES = EnumSet.allOf(DtdType.class);
    static {
        TYPES.remove(DtdType.ldmlICU);
    }

    static final Map<DtdType, String> FIRST_VERSION = new EnumMap<>(DtdType.class);
    static {
        FIRST_VERSION.put(DtdType.ldmlBCP47, "1.7.2");
        FIRST_VERSION.put(DtdType.keyboard, "22.1");
        FIRST_VERSION.put(DtdType.platform, "22.1");
    }

    private void diff(String prefix, DtdData dtdLast, DtdData dtdCurrent) {
        Map<String, Element> oldNameToElement = dtdLast == null ? Collections.EMPTY_MAP : dtdLast.getElementFromName();
        checkNames(prefix, dtdCurrent, oldNameToElement, "/", dtdCurrent.ROOT, new HashSet<Element>());
    }

    static final DtdType DEBUG_DTD = null; // set to enable
    static final String DEBUG_ELEMENT = "lias";
    static final boolean SHOW = false;

    @SuppressWarnings("unused")
    private void checkNames(String version, DtdData dtdCurrent, Map<String, Element> oldNameToElement, String path, Element element, HashSet<Element> seen) {
        if (seen.contains(element)) {
            return;
        }
        seen.add(element);
        String name = element.getName();
        if (SHOW && ToolConstants.CHART_DISPLAY_VERSION.equals(version)) {
            System.out.println(dtdCurrent.dtdType + "\t" + name);
        }
        if (DEBUG_DTD == dtdCurrent.dtdType && name.contains(DEBUG_ELEMENT)) {
            int debug = 0;
        }
        if (SKIP_ELEMENTS.contains(name)) {
            return;
        }
        if (SKIP_TYPE_ELEMENTS.containsEntry(dtdCurrent.dtdType, name)) {
            return;
        }

        if (isDeprecated(dtdCurrent.dtdType, name, "*")) { // SDI.isDeprecated(dtdCurrent.dtdType, name, "*", "*")) {
            return;
        }

        String newPath = path + "/" + element.name;

        if (!oldNameToElement.containsKey(name)) {
            Set<String> attributeNames = getAttributeNames(dtdCurrent, name, Collections.EMPTY_MAP, element.getAttributes());
            addData(dtdCurrent, "+" + name, version, newPath, attributeNames);
        } else {
            Element oldElement = oldNameToElement.get(name);
            Set<String> attributeNames = getAttributeNames(dtdCurrent, name, oldElement.getAttributes(), element.getAttributes());
            if (!attributeNames.isEmpty()) {
                addData(dtdCurrent, name, version, newPath, attributeNames);
            }
        }
        for (Element child : element.getChildren().keySet()) {
            checkNames(version, dtdCurrent, oldNameToElement, newPath, child, seen);
        }
    }

    enum DiffType {
        Element, Attribute, AttributeValue
    }

    private static class DiffElement {

        final VersionInfo version;
        final DtdType dtdType;
        final boolean isBeta;
        final String newPath;
        final String newElement;
        final String attributeNames;

        public DiffElement(DtdData dtdCurrent, String version, String newPath, String newElement, Set<String> attributeNames2) {
            isBeta = version.endsWith("β");
            try {
                this.version = isBeta ? VersionInfo.getInstance(version.substring(0, version.length()-1)) : VersionInfo.getInstance(version);
            } catch (Exception e) {
                e.printStackTrace();
                throw e;
            }
            dtdType = dtdCurrent.dtdType;
            this.newPath = fix(newPath);
            this.attributeNames = attributeNames2.isEmpty() ? NONE : "+" + CollectionUtilities.join(attributeNames2, ", +");
            this.newElement = newElement;
        }

        private String fix(String substring) {
            int base = substring.indexOf('/', 2);
            if (base < 0) return "";
            int last = substring.lastIndexOf('/');
            if (last <= base) return "/";
            substring = substring.substring(base, last);
            return substring.replace("/", "\u200B/") + "/";
        }

        @Override
        public String toString() {
            return MoreObjects.toStringHelper(this)
                .add("version", getVersionString())
                .add("dtdType", dtdType)
                .add("newPath", newPath)
                .add("newElement", newElement)
                .add("attributeNames", attributeNames)
                .toString();
        }

        private String getVersionString() {
            return version.getVersionString(2, 4) + (isBeta ? "β" : "");
        }
    }

    List<DiffElement> data = new ArrayList<>();

    private void addData(DtdData dtdCurrent, String element, String prefix, String newPath, Set<String> attributeNames) {
        DiffElement item = new DiffElement(dtdCurrent, prefix, newPath, element, attributeNames);
        data.add(item);
    }

    static final Set<String> SKIP_ELEMENTS = ImmutableSet.of("generation", "identity", "special", "telephoneCodeData");
    static final Multimap<DtdType, String> SKIP_TYPE_ELEMENTS = ImmutableMultimap.of(DtdType.ldml, "alias");

    static final Set<String> SKIP_ATTRIBUTES = ImmutableSet.of("references", "standard", "draft", "alt");

    private static Set<String> getAttributeNames(DtdData dtdCurrent, String elementName, Map<Attribute, Integer> attributesOld,
        Map<Attribute, Integer> attributes) {
        Set<String> names = new LinkedHashSet<>();
        main: for (Attribute attribute : attributes.keySet()) {
            String name = attribute.getName();
            if (SKIP_ATTRIBUTES.contains(name)) {
                continue;
            }
            if (isDeprecated(dtdCurrent.dtdType, elementName, name)) { // SDI.isDeprecated(dtdCurrent, elementName, name, "*")) {
                continue;
            }
            for (Attribute attributeOld : attributesOld.keySet()) {
                if (attributeOld.name.equals(name)) {
                    continue main;
                }
            }
            names.add(name);
        }
        return names;
    }

    private static boolean isDeprecated(DtdType dtdType, String elementName, String attributeName) {
        try {
            return DtdData.getInstance(dtdType).isDeprecated(elementName, attributeName, "*");
        } catch (DtdData.IllegalByDtdException e) {
            return true;
        }
    }
}
