/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
 * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package edu.mit.csail.sdg.alloy4viz;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import edu.mit.csail.sdg.alloy4.Util;
import edu.mit.csail.sdg.alloy4.XMLNode;
import edu.mit.csail.sdg.alloy4graph.DotColor;
import edu.mit.csail.sdg.alloy4graph.DotPalette;
import edu.mit.csail.sdg.alloy4graph.DotShape;
import edu.mit.csail.sdg.alloy4graph.DotStyle;

/**
 * This utility class contains methods to read and write VizState
 * customizations.
 * <p>
 * <b>Thread Safety:</b> Can be called only by the AWT event thread.
 */

public final class StaticThemeReaderWriter {

    /**
     * Constructor is private, since this utility class never needs to be
     * instantiated.
     */
    private StaticThemeReaderWriter() {}

    /**
     * Read the XML file and merge its settings into an existing VizState object.
     */
    public static void readAlloy(String filename, VizState theme) throws IOException {
        File file = new File(filename);
        try {
            XMLNode elem = new XMLNode(file);
            for (XMLNode sub : elem.getChildren("view"))
                parseView(sub, theme);
        } catch (Throwable e) {
            throw new IOException("The file \"" + file.getPath() + "\" is not a valid XML file, or an error occurred in reading.");
        }
    }

    /**
     * Write the VizState's customizations into a new file (which will be
     * overwritten if it exists).
     */
    public static void writeAlloy(String filename, VizState theme) throws IOException {
        PrintWriter bw = new PrintWriter(filename, "UTF-8");
        bw.write("<?xml version=\"1.0\"?>\n<alloy>\n\n");
        if (theme != null) {
            try {
                writeView(bw, theme);
            } catch (IOException ex) {
                Util.close(bw);
                throw new IOException("Error writing to the file \"" + filename + "\"");
            }
        }
        bw.write("\n</alloy>\n");
        if (!Util.close(bw))
            throw new IOException("Error writing to the file \"" + filename + "\"");
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /** Does nothing if the element is malformed. */
    private static void parseView(final XMLNode x, VizState now) {
        /*
         * <view orientation=".." nodetheme=".." edgetheme=".." hidePrivate="yes/no"
         * hideMeta="yes/no" useOriginalAtomNames="yes/no" fontsize="12"> <projection>
         * .. </projection> <defaultnode../> <defaultedge../> 0 or more NODE or EDGE
         * </view>
         */
        if (!x.is("view"))
            return;
        for (XMLNode xml : x) {
            if (xml.is("projection")) {
                now.deprojectAll();
                for (AlloyType t : parseProjectionList(now, xml))
                    now.project(t);
            }
        }
        if (has(x, "useOriginalAtomNames"))
            now.useOriginalName(getbool(x, "useOriginalAtomNames"));
        if (has(x, "hidePrivate"))
            now.hidePrivate(getbool(x, "hidePrivate"));
        if (has(x, "hideMeta"))
            now.hideMeta(getbool(x, "hideMeta"));
        if (has(x, "fontsize"))
            now.setFontSize(getint(x, "fontsize"));
        if (has(x, "nodetheme"))
            now.setNodePalette(parseDotPalette(x, "nodetheme"));
        if (has(x, "edgetheme"))
            now.setEdgePalette(parseDotPalette(x, "edgetheme"));
        for (XMLNode xml : x) {
            if (xml.is("defaultnode"))
                parseNodeViz(xml, now, null);
            else if (xml.is("defaultedge"))
                parseEdgeViz(xml, now, null);
            else if (xml.is("node")) {
                for (XMLNode sub : xml.getChildren("type")) {
                    AlloyType t = parseAlloyType(now, sub);
                    if (t != null)
                        parseNodeViz(xml, now, t);
                }
                for (XMLNode sub : xml.getChildren("set")) {
                    AlloySet s = parseAlloySet(now, sub);
                    if (s != null)
                        parseNodeViz(xml, now, s);
                }
            } else if (xml.is("edge")) {
                for (XMLNode sub : xml.getChildren("relation")) {
                    AlloyRelation r = parseAlloyRelation(now, sub);
                    if (r != null)
                        parseEdgeViz(xml, now, r);
                }
            }
        }
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /** Writes nothing if the argument is null. */
    private static void writeView(PrintWriter out, VizState view) throws IOException {
        if (view == null)
            return;
        VizState defaultView = new VizState(view.getOriginalInstance());
        out.write("<view");
        writeDotPalette(out, "nodetheme", view.getNodePalette(), defaultView.getNodePalette());
        writeDotPalette(out, "edgetheme", view.getEdgePalette(), defaultView.getEdgePalette());
        if (view.useOriginalName() != defaultView.useOriginalName()) {
            out.write(" useOriginalAtomNames=\"");
            out.write(view.useOriginalName() ? "yes" : "no");
            out.write("\"");
        }
        if (view.hidePrivate() != defaultView.hidePrivate()) {
            out.write(" hidePrivate=\"");
            out.write(view.hidePrivate() ? "yes" : "no");
            out.write("\"");
        }
        if (view.hideMeta() != defaultView.hideMeta()) {
            out.write(" hideMeta=\"");
            out.write(view.hideMeta() ? "yes" : "no");
            out.write("\"");
        }
        if (view.getFontSize() != defaultView.getFontSize()) {
            out.write(" fontsize=\"" + view.getFontSize() + "\"");
        }
        out.write(">\n");
        if (view.getProjectedTypes().size() > 0)
            writeProjectionList(out, view.getProjectedTypes());
        out.write("\n<defaultnode" + writeNodeViz(view, defaultView, null));
        out.write("/>\n\n<defaultedge" + writeEdgeViz(view, defaultView, null));
        out.write("/>\n");
        // === nodes ===
        Set<AlloyNodeElement> types = new TreeSet<AlloyNodeElement>();
        types.addAll(view.getOriginalModel().getTypes());
        types.addAll(view.getCurrentModel().getTypes());
        types.addAll(view.getOriginalModel().getSets());
        types.addAll(view.getCurrentModel().getSets());
        Map<String,Set<AlloyNodeElement>> viz2node = new TreeMap<String,Set<AlloyNodeElement>>();
        for (AlloyNodeElement t : types) {
            String str = writeNodeViz(view, defaultView, t);
            Set<AlloyNodeElement> nodes = viz2node.get(str);
            if (nodes == null)
                viz2node.put(str, nodes = new TreeSet<AlloyNodeElement>());
            nodes.add(t);
        }
        for (Map.Entry<String,Set<AlloyNodeElement>> e : viz2node.entrySet()) {
            out.write("\n<node" + e.getKey() + ">\n");
            for (AlloyNodeElement ts : e.getValue()) {
                if (ts instanceof AlloyType)
                    writeAlloyType(out, (AlloyType) ts);
                else if (ts instanceof AlloySet)
                    writeAlloySet(out, (AlloySet) ts);
            }
            out.write("</node>\n");
        }
        // === edges ===
        Set<AlloyRelation> rels = new TreeSet<AlloyRelation>();
        rels.addAll(view.getOriginalModel().getRelations());
        rels.addAll(view.getCurrentModel().getRelations());
        Map<String,Set<AlloyRelation>> viz2edge = new TreeMap<String,Set<AlloyRelation>>();
        for (AlloyRelation r : rels) {
            String str = writeEdgeViz(view, defaultView, r);
            if (str.length() == 0)
                continue;
            Set<AlloyRelation> edges = viz2edge.get(str);
            if (edges == null)
                viz2edge.put(str, edges = new TreeSet<AlloyRelation>());
            edges.add(r);
        }
        for (Map.Entry<String,Set<AlloyRelation>> e : viz2edge.entrySet()) {
            out.write("\n<edge" + e.getKey() + ">\n");
            for (AlloyRelation r : e.getValue())
                writeAlloyRelation(out, r);
            out.write("</edge>\n");
        }
        // === done ===
        out.write("\n</view>\n");
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /** Return null if the element is malformed. */
    private static AlloyType parseAlloyType(VizState now, XMLNode x) {
        /*
         * class AlloyType implements AlloyNodeElement { String name; } <type
         * name="the type name"/>
         */
        if (!x.is("type"))
            return null;
        String name = x.getAttribute("name");
        if (name.length() == 0)
            return null;
        else
            return now.getCurrentModel().hasType(name);
    }

    /** Writes nothing if the argument is null. */
    private static void writeAlloyType(PrintWriter out, AlloyType x) throws IOException {
        if (x != null)
            Util.encodeXMLs(out, "   <type name=\"", x.getName(), "\"/>\n");
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /** Return null if the element is malformed. */
    private static AlloySet parseAlloySet(VizState now, XMLNode x) {
        /*
         * class AlloySet implements AlloyNodeElement { String name; AlloyType type; }
         * <set name="name" type="name"/>
         */
        if (!x.is("set"))
            return null;
        String name = x.getAttribute("name"), type = x.getAttribute("type");
        if (name.length() == 0 || type.length() == 0)
            return null;
        AlloyType t = now.getCurrentModel().hasType(type);
        if (t == null)
            return null;
        else
            return now.getCurrentModel().hasSet(name, t);
    }

    /** Writes nothing if the argument is null. */
    private static void writeAlloySet(PrintWriter out, AlloySet x) throws IOException {
        if (x != null)
            Util.encodeXMLs(out, "   <set name=\"", x.getName(), "\" type=\"", x.getType().getName(), "\"/>\n");
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /** Return null if the element is malformed. */
    private static AlloyRelation parseAlloyRelation(VizState now, XMLNode x) {
        /*
         * <relation name="name"> 2 or more <type name=".."/> </relation>
         */
        List<AlloyType> ans = new ArrayList<AlloyType>();
        if (!x.is("relation"))
            return null;
        String name = x.getAttribute("name");
        if (name.length() == 0)
            return null;
        for (XMLNode sub : x.getChildren("type")) {
            String typename = sub.getAttribute("name");
            if (typename.length() == 0)
                return null;
            AlloyType t = now.getCurrentModel().hasType(typename);
            if (t == null)
                return null;
            ans.add(t);
        }
        if (ans.size() < 2)
            return null;
        else
            return now.getCurrentModel().hasRelation(name, ans);
    }

    /** Writes nothing if the argument is null. */
    private static void writeAlloyRelation(PrintWriter out, AlloyRelation x) throws IOException {
        if (x == null)
            return;
        Util.encodeXMLs(out, "   <relation name=\"", x.getName(), "\">");
        for (AlloyType t : x.getTypes())
            Util.encodeXMLs(out, " <type name=\"", t.getName(), "\"/>");
        out.write(" </relation>\n");
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /**
     * Always returns a nonnull (though possibly empty) set of AlloyType.
     */
    private static Set<AlloyType> parseProjectionList(VizState now, XMLNode x) {
        /*
         * <projection> 0 or more <type name=".."/> </projection>
         */
        Set<AlloyType> ans = new TreeSet<AlloyType>();
        if (x.is("projection"))
            for (XMLNode sub : x.getChildren("type")) {
                String name = sub.getAttribute("name");
                if (name.length() == 0)
                    continue;
                AlloyType t = now.getOriginalModel().hasType(name);
                if (t != null)
                    ans.add(t);
            }
        return ans;
    }

    /**
     * Writes an empty Projection tag if the argument is null or empty
     */
    private static void writeProjectionList(PrintWriter out, Set<AlloyType> types) throws IOException {
        if (types == null || types.size() == 0) {
            out.write("\n<projection/>\n");
            return;
        }
        out.write("\n<projection>");
        for (AlloyType t : types)
            Util.encodeXMLs(out, " <type name=\"", t.getName(), "\"/>");
        out.write(" </projection>\n");
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /**
     * Do nothing if the element is malformed; note: x can be null.
     */
    private static void parseNodeViz(XMLNode xml, VizState view, AlloyNodeElement x) {
        /*
         * <node visible="inherit/yes/no" label=".." color=".." shape=".." style=".."
         * showlabel="inherit/yes/no" showinattr="inherit/yes/no"
         * hideunconnected="inherit/yes/no" nubmeratoms="inherit/yes/no"> zero or more
         * SET or TYPE </node> Each attribute, if omitted, means "no change". Note:
         * BOOLEAN is tristate.
         */
        if (has(xml, "visible"))
            view.nodeVisible.put(x, getbool(xml, "visible"));
        if (has(xml, "hideunconnected"))
            view.hideUnconnected.put(x, getbool(xml, "hideunconnected"));
        if (x == null || x instanceof AlloySet) {
            AlloySet s = (AlloySet) x;
            if (has(xml, "showlabel"))
                view.showAsLabel.put(s, getbool(xml, "showlabel"));
            if (has(xml, "showinattr"))
                view.showAsAttr.put(s, getbool(xml, "showinattr"));
        }
        if (x == null || x instanceof AlloyType) {
            AlloyType t = (AlloyType) x;
            if (has(xml, "numberatoms"))
                view.number.put(t, getbool(xml, "numberatoms"));
        }
        if (has(xml, "style"))
            view.nodeStyle.put(x, parseDotStyle(xml));
        if (has(xml, "color"))
            view.nodeColor.put(x, parseDotColor(xml));
        if (has(xml, "shape"))
            view.shape.put(x, parseDotShape(xml));
        if (has(xml, "label"))
            view.label.put(x, xml.getAttribute("label"));
    }

    /**
     * Returns the String representation of an AlloyNodeElement's settings.
     */
    private static String writeNodeViz(VizState view, VizState defaultView, AlloyNodeElement x) throws IOException {
        StringWriter sw = new StringWriter();
        PrintWriter out = new PrintWriter(sw);
        writeBool(out, "visible", view.nodeVisible.get(x), defaultView.nodeVisible.get(x));
        writeBool(out, "hideunconnected", view.hideUnconnected.get(x), defaultView.hideUnconnected.get(x));
        if (x == null || x instanceof AlloySet) {
            AlloySet s = (AlloySet) x;
            writeBool(out, "showlabel", view.showAsLabel.get(s), defaultView.showAsLabel.get(s));
            writeBool(out, "showinattr", view.showAsAttr.get(s), defaultView.showAsAttr.get(s));
        }
        if (x == null || x instanceof AlloyType) {
            AlloyType t = (AlloyType) x;
            writeBool(out, "numberatoms", view.number.get(t), defaultView.number.get(t));
        }
        writeDotStyle(out, view.nodeStyle.get(x), defaultView.nodeStyle.get(x));
        writeDotShape(out, view.shape.get(x), defaultView.shape.get(x));
        writeDotColor(out, view.nodeColor.get(x), defaultView.nodeColor.get(x));
        if (x != null && !view.label.get(x).equals(defaultView.label.get(x)))
            Util.encodeXMLs(out, " label=\"", view.label.get(x), "\"");
        if (out.checkError())
            throw new IOException("PrintWriter IO Exception!");
        return sw.toString();
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /**
     * Do nothing if the element is malformed; note: x can be null.
     */
    private static void parseEdgeViz(XMLNode xml, VizState view, AlloyRelation x) {
        /*
         * <edge visible="inherit/yes/no" label=".." color=".." style=".." weight=".."
         * constraint=".." attribute="inherit/yes/no" merge="inherit/yes/no"
         * layout="inherit/yes/no"> zero or more RELATION </edge> Each attribute, if
         * omitted, means "no change". Note: BOOLEAN is tristate.
         */
        if (has(xml, "visible"))
            view.edgeVisible.put(x, getbool(xml, "visible"));
        if (has(xml, "attribute"))
            view.attribute.put(x, getbool(xml, "attribute"));
        if (has(xml, "merge"))
            view.mergeArrows.put(x, getbool(xml, "merge"));
        if (has(xml, "layout"))
            view.layoutBack.put(x, getbool(xml, "layout"));
        if (has(xml, "constraint"))
            view.constraint.put(x, getbool(xml, "constraint"));
        if (has(xml, "style"))
            view.edgeStyle.put(x, parseDotStyle(xml));
        if (has(xml, "color"))
            view.edgeColor.put(x, parseDotColor(xml));
        if (has(xml, "weight"))
            view.weight.put(x, getint(xml, "weight"));
        if (has(xml, "label"))
            view.label.put(x, xml.getAttribute("label"));
    }

    /**
     * Returns the String representation of an AlloyRelation's settings.
     */
    private static String writeEdgeViz(VizState view, VizState defaultView, AlloyRelation x) throws IOException {
        StringWriter sw = new StringWriter();
        PrintWriter out = new PrintWriter(sw);
        writeDotColor(out, view.edgeColor.get(x), defaultView.edgeColor.get(x));
        writeDotStyle(out, view.edgeStyle.get(x), defaultView.edgeStyle.get(x));
        writeBool(out, "visible", view.edgeVisible.get(x), defaultView.edgeVisible.get(x));
        writeBool(out, "merge", view.mergeArrows.get(x), defaultView.mergeArrows.get(x));
        writeBool(out, "layout", view.layoutBack.get(x), defaultView.layoutBack.get(x));
        writeBool(out, "attribute", view.attribute.get(x), defaultView.attribute.get(x));
        writeBool(out, "constraint", view.constraint.get(x), defaultView.constraint.get(x));
        if (view.weight.get(x) != defaultView.weight.get(x))
            out.write(" weight=\"" + view.weight.get(x) + "\"");
        if (x != null && !view.label.get(x).equals(defaultView.label.get(x)))
            Util.encodeXMLs(out, " label=\"", view.label.get(x), "\"");
        if (out.checkError())
            throw new IOException("PrintWriter IO Exception!");
        return sw.toString();
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /**
     * Returns null if the attribute doesn't exist, or is malformed.
     */
    private static DotPalette parseDotPalette(XMLNode x, String key) {
        return DotPalette.parse(x.getAttribute(key));
    }

    /** Writes nothing if value==defaultValue. */
    private static void writeDotPalette(PrintWriter out, String key, DotPalette value, DotPalette defaultValue) throws IOException {
        if (value != defaultValue)
            Util.encodeXMLs(out, " " + key + "=\"", value == null ? "inherit" : value.toString(), "\"");
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /**
     * Returns null if the attribute doesn't exist, or is malformed.
     */
    private static DotColor parseDotColor(XMLNode x) {
        return DotColor.parse(x.getAttribute("color"));
    }

    /** Writes nothing if value==defaultValue. */
    private static void writeDotColor(PrintWriter out, DotColor value, DotColor defaultValue) throws IOException {
        if (value != defaultValue)
            Util.encodeXMLs(out, " color=\"", value == null ? "inherit" : value.toString(), "\"");
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /**
     * Returns null if the attribute doesn't exist, or is malformed.
     */
    private static DotShape parseDotShape(XMLNode x) {
        return DotShape.parse(x.getAttribute("shape"));
    }

    /** Writes nothing if value==defaultValue. */
    private static void writeDotShape(PrintWriter out, DotShape value, DotShape defaultValue) throws IOException {
        if (value != defaultValue)
            Util.encodeXMLs(out, " shape=\"", value == null ? "inherit" : value.toString(), "\"");
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /**
     * Returns null if the attribute doesn't exist, or is malformed.
     */
    private static DotStyle parseDotStyle(XMLNode x) {
        return DotStyle.parse(x.getAttribute("style"));
    }

    /** Writes nothing if value==defaultValue. */
    private static void writeDotStyle(PrintWriter out, DotStyle value, DotStyle defaultValue) throws IOException {
        if (value != defaultValue)
            Util.encodeXMLs(out, " style=\"", value == null ? "inherit" : value.toString(), "\"");
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /**
     * Returns null if the attribute doesn't exist, or is malformed.
     */
    private static Boolean getbool(XMLNode x, String attr) {
        String value = x.getAttribute(attr);
        if (value.equalsIgnoreCase("yes") || value.equalsIgnoreCase("true"))
            return Boolean.TRUE;
        if (value.equalsIgnoreCase("no") || value.equalsIgnoreCase("false"))
            return Boolean.FALSE;
        return null;
    }

    /**
     * Writes nothing if the value is equal to the default value.
     */
    private static void writeBool(PrintWriter out, String key, Boolean value, Boolean defaultValue) throws IOException {
        if (value == null && defaultValue == null)
            return;
        if (value != null && defaultValue != null && value.booleanValue() == defaultValue.booleanValue())
            return;
        out.write(' ');
        out.write(key);
        if (value == null)
            out.write("=\"inherit\"");
        else
            out.write(value ? "=\"yes\"" : "=\"no\"");
    }

    /*
     * ========================================================= ================
     * ===================
     */

    /**
     * Returns true if the XML element has the given attribute.
     */
    private static boolean has(XMLNode x, String attr) {
        return x.getAttribute(attr, null) != null;
    }

    /**
     * Returns 0 if the attribute doesn't exist, or is malformed.
     */
    private static int getint(XMLNode x, String attr) {
        String value = x.getAttribute(attr);
        int i;
        try {
            i = Integer.parseInt(value);
        } catch (NumberFormatException ex) {
            i = 0;
        }
        return i;
    }
}