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 java.util.regex.Matcher;

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.AttributeStatus;
import org.unicode.cldr.util.DtdData.Element;
import org.unicode.cldr.util.DtdType;
import org.unicode.cldr.util.SupplementalDataInfo;

import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.util.VersionInfo;

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

    private static final Splitter SPLITTER_SPACE = Splitter.on(' ');

    private static final String DEPRECATED_PREFIX = "⊖";

    private static final String NEW_PREFIX = "+";
    private static final String ORDERED_SIGN = "⇣";
    private static final String UNORDERED_SIGN = "⇟";


    private static final Set<String> OMITTED_ATTRIBUTES = Collections.singleton("⊕");

    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>Changes to the LDML DTDs over time.</p>\n"
            + "<ul>\n"
            + "<li>New elements or attributes are indicated with a + sign, and newly deprecated ones with a ⊖ sign.</li>\n"
            + "<li>Element attributes are abbreviated as ⊕ where is no change to them, "
            + "but the element is newly the child of another.</li>\n"
            + "<li>LDML DTDs have augmented data:\n"
            + "<ul><li>Attribute status is marked by: "
            + AttributeStatus.distinguished.shortName + "=" + AttributeStatus.distinguished + ", "
            + AttributeStatus.value.shortName + "=" + AttributeStatus.value + ", or "
            + AttributeStatus.metadata.shortName + "=" + AttributeStatus.metadata + ".</li>\n"
            + "<li>Attribute value constraints are marked with ⟨…⟩ (for DTD constraints) and ⟪…⟫ (for augmented constraints, added in v35.0).</li>\n"
            + "<li>Changes in status or constraints are shown with ➠, with identical sections shown with ….</li>\n"
            + "<li>Newly ordered elements are indicated with " + ORDERED_SIGN + "; newly unordered with " + UNORDERED_SIGN + ".</li>\n"
            + "</ul></li></ul>\n"
            + "<p>For more information, see the LDML spec.</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;

        for (String current : ToolConstants.CLDR_RELEASE_AND_DEV_VERSION_SET) {
            System.out.println("DTD delta: " + current);
            final boolean finalVersion = current.equals(ToolConstants.DEV_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;
            if (current.contentEquals(ToolConstants.CHART_VERSION)) {
                break;
            }
        }

        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.emptyMap() : dtdLast.getElementFromName();
        checkNames(prefix, dtdCurrent, dtdLast, oldNameToElement, "/", dtdCurrent.ROOT, new HashSet<Element>(), false);
    }

    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, DtdData dtdLast, Map<String, Element> oldNameToElement, String path, Element element,
        HashSet<Element> seen, boolean showAnyway) {
        String name = element.getName();

        if (SKIP_ELEMENTS.contains(name)) {
            return;
        }
        if (SKIP_TYPE_ELEMENTS.containsEntry(dtdCurrent.dtdType, name)) {
            return;
        }

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

        // if an element is newly a child of another but has already been seen, you'll have special indication
        if (seen.contains(element)) {
            if (showAnyway) {
                addData(dtdCurrent, NEW_PREFIX + name, version, newPath, OMITTED_ATTRIBUTES);
            }
            return;
        }

        seen.add(element);
        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;
        }


        Element oldElement = null;
        boolean ordered = element.isOrdered();

        if (!oldNameToElement.containsKey(name)) {
            Set<String> attributeNames = getAttributeNames(dtdCurrent, dtdLast, name, Collections.emptyMap(), element.getAttributes());
            addData(dtdCurrent, NEW_PREFIX + name + (ordered ? ORDERED_SIGN : ""), version, newPath, attributeNames);
        } else {
            oldElement = oldNameToElement.get(name);
            boolean oldOrdered = oldElement.isOrdered();
            Set<String> attributeNames = getAttributeNames(dtdCurrent, dtdLast, name, oldElement.getAttributes(), element.getAttributes());
            boolean currentDeprecated = element.isDeprecated();
            boolean lastDeprecated = dtdLast == null ? false : oldElement.isDeprecated(); //  + (currentDeprecated ? "ⓓ" : "")
            boolean newlyDeprecated = currentDeprecated && !lastDeprecated;
            String orderingStatus = (ordered == oldOrdered || currentDeprecated) ? "" : ordered ? ORDERED_SIGN : UNORDERED_SIGN;
            if (newlyDeprecated) {
                addData(dtdCurrent, DEPRECATED_PREFIX + name + orderingStatus, version, newPath, Collections.emptySet());
            }
            if (!attributeNames.isEmpty()) {
                addData(dtdCurrent, (newlyDeprecated ? DEPRECATED_PREFIX : "") + name + orderingStatus, version, newPath, attributeNames);
            }
        }
        if (element.getName().equals("coordinateUnit")) {
            System.out.println(version + "\toordinateUnit\t" + element.getChildren().keySet());
        }
        Set<Element> oldChildren = oldElement == null ? Collections.emptySet() : oldElement.getChildren().keySet();
        for (Element child : element.getChildren().keySet()) {
            showAnyway = true;
            for (Element oldChild : oldChildren) {
                if (oldChild.getName().equals(child.getName())) {
                    showAnyway = false;
                    break;
                }
            }
            checkNames(version, dtdCurrent, dtdLast, oldNameToElement, newPath, child, seen, showAnyway);
        }
    }

    enum DiffType {
        Element, Attribute, AttributeValue
    }

    private static class DiffElement {

        private static final String START_ATTR = "<div>";
        private static final String END_ATTR = "</div>";
        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 :
                START_ATTR + Joiner.on(END_ATTR + START_ATTR).join(attributeNames2) + END_ATTR;
            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, DtdData dtdLast, String elementName,
        Map<Attribute, Integer> attributesOld,
        Map<Attribute, Integer> attributes) {
        Set<String> names = new LinkedHashSet<>();
        if (elementName.equals("coordinateUnit")) {
            int debug = 0;
        }

        main:
            // we want to add a name that is new or that becomes deprecated
            for (Attribute attribute : attributes.keySet()) {
                String name = attribute.getName();
                if (SKIP_ATTRIBUTES.contains(name)) {
                    continue;
                }
                String match = attribute.getMatchString();
                AttributeStatus status = attribute.attributeStatus;
                String display = NEW_PREFIX + name;
//            if (isDeprecated(dtdCurrent, elementName, name)) { // SDI.isDeprecated(dtdCurrent, elementName, name, "*")) {
//                continue;
//            }
                String oldMatch = "?";
                String pre, post;
                Attribute attributeOld = attribute.getMatchingName(attributesOld);
                if (attributeOld == null) {
                    display = NEW_PREFIX + name +  " " + AttributeStatus.getShortName(status) + " " + match;
                } else if (attribute.isDeprecated() && !attributeOld.isDeprecated()) {
                    display = DEPRECATED_PREFIX + name;
                } else {
                    oldMatch = attributeOld.getMatchString();
                    AttributeStatus oldStatus = attributeOld.attributeStatus;

                    boolean matchEquals = match.equals(oldMatch);
                    if (status != oldStatus) {
                        pre = AttributeStatus.getShortName(oldStatus);
                        post = AttributeStatus.getShortName(status);
                        if (!matchEquals) {
                            pre += " " + oldMatch;
                            post += " " + match;
                        }
                    } else if (!matchEquals) {
                        pre = oldMatch;
                        post = match;
                    } else {
                        continue main; // skip attribute entirely;
                    }
                    display = name + " " + diff(pre, post);
                }
                names.add(display);
            }
        return names;
    }

    public static String diff(String pre, String post) {
        Matcher matcherPre = Attribute.LEAD_TRAIL.matcher(pre);
        Matcher matcherPost = Attribute.LEAD_TRAIL.matcher(post);
        if (matcherPre.matches() && matcherPost.matches()) {
            List<String> preParts = SPLITTER_SPACE.splitToList(matcherPre.group(2));
            List<String> postParts = SPLITTER_SPACE.splitToList(matcherPost.group(2));
            pre = matcherPre.group(1) + remove(preParts, postParts) + matcherPre.group(3);
            post = matcherPost.group(1) + remove(postParts, preParts) + matcherPost.group(3);
        }
        return pre + "➠" + post;
    }

    private static String remove(List<String> main, List<String> toRemove) {
        List<String> result = new ArrayList<>();
        boolean removed = false;
        for (String s : main) {
            if (toRemove.contains(s)) {
                removed = true;
            } else {
                if (removed) {
                    result.add("…");
                    removed = false;
                }
                result.add(s);
            }
        }
        if (removed) {
            result.add("…");
        }
        return Joiner.on(" ").join(result);
    }

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