/*
 * Copyright (c) 2008, 2015, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 *
 */
package com.sun.hotspot.igv.data.serialization;

import com.sun.hotspot.igv.data.*;
import com.sun.hotspot.igv.data.serialization.XMLParser.ElementHandler;
import com.sun.hotspot.igv.data.serialization.XMLParser.HandoverElementHandler;
import com.sun.hotspot.igv.data.serialization.XMLParser.TopElementHandler;
import com.sun.hotspot.igv.data.services.GroupCallback;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import javax.swing.SwingUtilities;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.SchemaFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;

/**
 *
 * @author Thomas Wuerthinger
 */
public class Parser implements GraphParser {

    public static final String INDENT = "  ";
    public static final String TOP_ELEMENT = "graphDocument";
    public static final String GROUP_ELEMENT = "group";
    public static final String GRAPH_ELEMENT = "graph";
    public static final String ROOT_ELEMENT = "graphDocument";
    public static final String PROPERTIES_ELEMENT = "properties";
    public static final String EDGES_ELEMENT = "edges";
    public static final String PROPERTY_ELEMENT = "p";
    public static final String EDGE_ELEMENT = "edge";
    public static final String NODE_ELEMENT = "node";
    public static final String NODES_ELEMENT = "nodes";
    public static final String REMOVE_EDGE_ELEMENT = "removeEdge";
    public static final String REMOVE_NODE_ELEMENT = "removeNode";
    public static final String METHOD_NAME_PROPERTY = "name";
    public static final String GROUP_NAME_PROPERTY = "name";
    public static final String METHOD_IS_PUBLIC_PROPERTY = "public";
    public static final String METHOD_IS_STATIC_PROPERTY = "static";
    public static final String TRUE_VALUE = "true";
    public static final String NODE_NAME_PROPERTY = "name";
    public static final String EDGE_NAME_PROPERTY = "name";
    public static final String NODE_ID_PROPERTY = "id";
    public static final String FROM_PROPERTY = "from";
    public static final String TO_PROPERTY = "to";
    public static final String TYPE_PROPERTY = "type";
    public static final String PROPERTY_NAME_PROPERTY = "name";
    public static final String GRAPH_NAME_PROPERTY = "name";
    public static final String FROM_INDEX_PROPERTY = "fromIndex";
    public static final String TO_INDEX_PROPERTY = "toIndex";
    public static final String TO_INDEX_ALT_PROPERTY = "index";
    public static final String LABEL_PROPERTY = "label";
    public static final String METHOD_ELEMENT = "method";
    public static final String INLINE_ELEMENT = "inline";
    public static final String BYTECODES_ELEMENT = "bytecodes";
    public static final String METHOD_BCI_PROPERTY = "bci";
    public static final String METHOD_SHORT_NAME_PROPERTY = "shortName";
    public static final String CONTROL_FLOW_ELEMENT = "controlFlow";
    public static final String BLOCK_NAME_PROPERTY = "name";
    public static final String BLOCK_ELEMENT = "block";
    public static final String SUCCESSORS_ELEMENT = "successors";
    public static final String SUCCESSOR_ELEMENT = "successor";
    public static final String ASSEMBLY_ELEMENT = "assembly";
    public static final String DIFFERENCE_PROPERTY = "difference";
    private TopElementHandler<GraphDocument> xmlDocument = new TopElementHandler<>();
    private Map<Group, Boolean> differenceEncoding = new HashMap<>();
    private Map<Group, InputGraph> lastParsedGraph = new HashMap<>();
    private GroupCallback groupCallback;
    private HashMap<String, Integer> idCache = new HashMap<>();
    private ArrayList<Pair<String, String>> blockConnections = new ArrayList<>();
    private int maxId = 0;
    private GraphDocument graphDocument;
    private final ParseMonitor monitor;
    private final ReadableByteChannel channel;

    private int lookupID(String i) {
        try {
            return Integer.parseInt(i);
        } catch (NumberFormatException nfe) {
            // ignore
        }
        Integer id = idCache.get(i);
        if (id == null) {
            id = maxId++;
            idCache.put(i, id);
        }
        return id.intValue();
    }

    // <graphDocument>
    private ElementHandler<GraphDocument, Object> topHandler = new ElementHandler<GraphDocument, Object>(TOP_ELEMENT) {

        @Override
        protected GraphDocument start() throws SAXException {
            graphDocument = new GraphDocument();
            return graphDocument;
        }
    };
    // <group>
    private ElementHandler<Group, Folder> groupHandler = new XMLParser.ElementHandler<Group, Folder>(GROUP_ELEMENT) {

        @Override
        protected Group start() throws SAXException {
            final Group group = new Group(this.getParentObject());

            String differenceProperty = this.readAttribute(DIFFERENCE_PROPERTY);
            Parser.this.differenceEncoding.put(group, (differenceProperty != null && (differenceProperty.equals("1") || differenceProperty.equals("true"))));

            ParseMonitor monitor = getMonitor();
            if (monitor != null) {
                monitor.setState(group.getName());
            }

            final Folder parent = getParentObject();
            if (groupCallback == null || parent instanceof Group) {
                SwingUtilities.invokeLater(new Runnable(){
                    @Override
                    public void run() {
                        parent.addElement(group);
                    }
                });
            }

            return group;
        }

        @Override
        protected void end(String text) throws SAXException {
        }
    };
    // <method>
    private ElementHandler<InputMethod, Group> methodHandler = new XMLParser.ElementHandler<InputMethod, Group>(METHOD_ELEMENT) {

        @Override
        protected InputMethod start() throws SAXException {

            InputMethod method = parseMethod(this, getParentObject());
            getParentObject().setMethod(method);
            return method;
        }
    };

    private InputMethod parseMethod(XMLParser.ElementHandler<?,?> handler, Group group) throws SAXException {
        String s = handler.readRequiredAttribute(METHOD_BCI_PROPERTY);
        int bci = 0;
        try {
            bci = Integer.parseInt(s);
        } catch (NumberFormatException e) {
            throw new SAXException(e);
        }
        InputMethod method = new InputMethod(group, handler.readRequiredAttribute(METHOD_NAME_PROPERTY), handler.readRequiredAttribute(METHOD_SHORT_NAME_PROPERTY), bci);
        return method;
    }
    // <bytecodes>
    private HandoverElementHandler<InputMethod> bytecodesHandler = new XMLParser.HandoverElementHandler<InputMethod>(BYTECODES_ELEMENT, true) {

        @Override
        protected void end(String text) throws SAXException {
            getParentObject().setBytecodes(text);
        }
    };
    // <inlined>
    private HandoverElementHandler<InputMethod> inlinedHandler = new XMLParser.HandoverElementHandler<>(INLINE_ELEMENT);
    // <inlined><method>
    private ElementHandler<InputMethod, InputMethod> inlinedMethodHandler = new XMLParser.ElementHandler<InputMethod, InputMethod>(METHOD_ELEMENT) {

        @Override
        protected InputMethod start() throws SAXException {
            InputMethod method = parseMethod(this, getParentObject().getGroup());
            getParentObject().addInlined(method);
            return method;
        }
    };
    // <graph>
    private ElementHandler<InputGraph, Group> graphHandler = new XMLParser.ElementHandler<InputGraph, Group>(GRAPH_ELEMENT) {

        @Override
        protected InputGraph start() throws SAXException {
            String name = readAttribute(GRAPH_NAME_PROPERTY);
            InputGraph curGraph = new InputGraph(name);
            if (differenceEncoding.get(getParentObject())) {
                InputGraph previous = lastParsedGraph.get(getParentObject());
                lastParsedGraph.put(getParentObject(), curGraph);
                if (previous != null) {
                    for (InputNode n : previous.getNodes()) {
                        curGraph.addNode(n);
                    }
                    for (InputEdge e : previous.getEdges()) {
                        curGraph.addEdge(e);
                    }
                }
            }
            ParseMonitor monitor = getMonitor();
            if (monitor != null) {
                monitor.updateProgress();
            }
            return curGraph;
        }

        @Override
        protected void end(String text) throws SAXException {
            // NOTE: Some graphs intentionally don't provide blocks. Instead
            //       they later generate the blocks from other information such
            //       as node properties (example: ServerCompilerScheduler).
            //       Thus, we shouldn't assign nodes that don't belong to any
            //       block to some artificial block below unless blocks are
            //       defined and nodes are assigned to them.

            final InputGraph graph = getObject();
            final Group parent = getParentObject();
            if (graph.getBlocks().size() > 0) {
                boolean blocksContainNodes = false;
                for (InputBlock b : graph.getBlocks()) {
                    if (b.getNodes().size() > 0) {
                        blocksContainNodes = true;
                        break;
                    }
                }

                if (!blocksContainNodes) {
                    graph.clearBlocks();
                    blockConnections.clear();
                } else {
                    // Blocks and their nodes defined: add other nodes to an
                    //  artificial "no block" block
                    InputBlock noBlock = null;
                    for (InputNode n : graph.getNodes()) {
                        if (graph.getBlock(n) == null) {
                            if (noBlock == null) {
                                noBlock = graph.addBlock("(no block)");
                            }

                            noBlock.addNode(n.getId());
                        }

                        assert graph.getBlock(n) != null;
                    }
                }
            }

            // Resolve block successors
            for (Pair<String, String> p : blockConnections) {
                final InputBlock left = graph.getBlock(p.getLeft());
                assert left != null;
                final InputBlock right = graph.getBlock(p.getRight());
                assert right != null;
                graph.addBlockEdge(left, right);
            }
            blockConnections.clear();

            SwingUtilities.invokeLater(new Runnable(){

                @Override
                public void run() {
                    // Add to group
                    parent.addElement(graph);
                }
            });
        }
    };
    // <nodes>
    private HandoverElementHandler<InputGraph> nodesHandler = new HandoverElementHandler<>(NODES_ELEMENT);
    // <controlFlow>
    private HandoverElementHandler<InputGraph> controlFlowHandler = new HandoverElementHandler<>(CONTROL_FLOW_ELEMENT);
    // <block>
    private ElementHandler<InputBlock, InputGraph> blockHandler = new ElementHandler<InputBlock, InputGraph>(BLOCK_ELEMENT) {

        @Override
        protected InputBlock start() throws SAXException {
            InputGraph graph = getParentObject();
            String name = readRequiredAttribute(BLOCK_NAME_PROPERTY);
            InputBlock b = graph.addBlock(name);
            for (InputNode n : b.getNodes()) {
                assert graph.getBlock(n).equals(b);
            }
            return b;
        }
    };
    // <nodes>
    private HandoverElementHandler<InputBlock> blockNodesHandler = new HandoverElementHandler<>(NODES_ELEMENT);
    // <node>
    private ElementHandler<InputBlock, InputBlock> blockNodeHandler = new ElementHandler<InputBlock, InputBlock>(NODE_ELEMENT) {

        @Override
        protected InputBlock start() throws SAXException {
            String s = readRequiredAttribute(NODE_ID_PROPERTY);

            int id = 0;
            try {
                id = lookupID(s);
            } catch (NumberFormatException e) {
                throw new SAXException(e);
            }
            getParentObject().addNode(id);
            return getParentObject();
        }
    };
    // <successors>
    private HandoverElementHandler<InputBlock> successorsHandler = new HandoverElementHandler<>(SUCCESSORS_ELEMENT);
    // <successor>
    private ElementHandler<InputBlock, InputBlock> successorHandler = new ElementHandler<InputBlock, InputBlock>(SUCCESSOR_ELEMENT) {

        @Override
        protected InputBlock start() throws SAXException {
            String name = readRequiredAttribute(BLOCK_NAME_PROPERTY);
            blockConnections.add(new Pair<>(getParentObject().getName(), name));
            return getParentObject();
        }
    };
    // <node>
    private ElementHandler<InputNode, InputGraph> nodeHandler = new ElementHandler<InputNode, InputGraph>(NODE_ELEMENT) {

        @Override
        protected InputNode start() throws SAXException {
            String s = readRequiredAttribute(NODE_ID_PROPERTY);
            int id = 0;
            try {
                id = lookupID(s);
            } catch (NumberFormatException e) {
                throw new SAXException(e);
            }
            InputNode node = new InputNode(id);
            getParentObject().addNode(node);
            return node;
        }
    };
    // <removeNode>
    private ElementHandler<InputNode, InputGraph> removeNodeHandler = new ElementHandler<InputNode, InputGraph>(REMOVE_NODE_ELEMENT) {

        @Override
        protected InputNode start() throws SAXException {
            String s = readRequiredAttribute(NODE_ID_PROPERTY);
            int id = 0;
            try {
                id = lookupID(s);
            } catch (NumberFormatException e) {
                throw new SAXException(e);
            }
            return getParentObject().removeNode(id);
        }
    };
    // <graph>
    private HandoverElementHandler<InputGraph> edgesHandler = new HandoverElementHandler<>(EDGES_ELEMENT);

    // Local class for edge elements
    private class EdgeElementHandler extends ElementHandler<InputEdge, InputGraph> {

        public EdgeElementHandler(String name) {
            super(name);
        }

        @Override
        protected InputEdge start() throws SAXException {
            int fromIndex = 0;
            int toIndex = 0;
            int from = -1;
            int to = -1;
            String label = null;
            String type = null;

            try {
                String fromIndexString = readAttribute(FROM_INDEX_PROPERTY);
                if (fromIndexString != null) {
                    fromIndex = Integer.parseInt(fromIndexString);
                }

                String toIndexString = readAttribute(TO_INDEX_PROPERTY);
                if (toIndexString == null) {
                    toIndexString = readAttribute(TO_INDEX_ALT_PROPERTY);
                }
                if (toIndexString != null) {
                    toIndex = Integer.parseInt(toIndexString);
                }

                label = readAttribute(LABEL_PROPERTY);
                type = readAttribute(TYPE_PROPERTY);

                from = lookupID(readRequiredAttribute(FROM_PROPERTY));
                to = lookupID(readRequiredAttribute(TO_PROPERTY));
            } catch (NumberFormatException e) {
                throw new SAXException(e);
            }

            InputEdge conn = new InputEdge((char) fromIndex, (char) toIndex, from, to, label, type == null ? "" : type);
            return start(conn);
        }

        protected InputEdge start(InputEdge conn) throws SAXException {
            return conn;
        }
    }
    // <edge>
    private EdgeElementHandler edgeHandler = new EdgeElementHandler(EDGE_ELEMENT) {

        @Override
        protected InputEdge start(InputEdge conn) throws SAXException {
            getParentObject().addEdge(conn);
            return conn;
        }
    };
    // <removeEdge>
    private EdgeElementHandler removeEdgeHandler = new EdgeElementHandler(REMOVE_EDGE_ELEMENT) {

        @Override
        protected InputEdge start(InputEdge conn) throws SAXException {
            getParentObject().removeEdge(conn);
            return conn;
        }
    };
    // <properties>
    private HandoverElementHandler<Properties.Provider> propertiesHandler = new HandoverElementHandler<>(PROPERTIES_ELEMENT);
    // <properties>
    private HandoverElementHandler<Group> groupPropertiesHandler = new HandoverElementHandler<Group>(PROPERTIES_ELEMENT) {

        @Override
        public void end(String text) throws SAXException {
            if (groupCallback != null && getParentObject().getParent() instanceof GraphDocument) {
                final Group group = getParentObject();
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        groupCallback.started(group);
                    }
                });
            }
        }
    };
    // <property>
    private ElementHandler<String, Properties.Provider> propertyHandler = new XMLParser.ElementHandler<String, Properties.Provider>(PROPERTY_ELEMENT, true) {

        @Override
        public String start() throws SAXException {
            return readRequiredAttribute(PROPERTY_NAME_PROPERTY);
         }

        @Override
        public void end(String text) {
            getParentObject().getProperties().setProperty(getObject(), text.trim());
        }
    };

    public Parser(ReadableByteChannel channel) {
        this(channel, null, null);
    }

    public Parser(ReadableByteChannel channel, ParseMonitor monitor, GroupCallback groupCallback) {

        this.groupCallback = groupCallback;
        this.monitor = monitor;
        this.channel = channel;

        // Initialize dependencies
        xmlDocument.addChild(topHandler);
        topHandler.addChild(groupHandler);

        groupHandler.addChild(methodHandler);
        groupHandler.addChild(graphHandler);
        groupHandler.addChild(groupHandler);

        methodHandler.addChild(inlinedHandler);
        methodHandler.addChild(bytecodesHandler);

        inlinedHandler.addChild(inlinedMethodHandler);
        inlinedMethodHandler.addChild(bytecodesHandler);
        inlinedMethodHandler.addChild(inlinedHandler);

        graphHandler.addChild(nodesHandler);
        graphHandler.addChild(edgesHandler);
        graphHandler.addChild(controlFlowHandler);

        controlFlowHandler.addChild(blockHandler);

        blockHandler.addChild(successorsHandler);
        successorsHandler.addChild(successorHandler);
        blockHandler.addChild(blockNodesHandler);
        blockNodesHandler.addChild(blockNodeHandler);

        nodesHandler.addChild(nodeHandler);
        nodesHandler.addChild(removeNodeHandler);
        edgesHandler.addChild(edgeHandler);
        edgesHandler.addChild(removeEdgeHandler);

        methodHandler.addChild(propertiesHandler);
        inlinedMethodHandler.addChild(propertiesHandler);
        topHandler.addChild(propertiesHandler);
        groupHandler.addChild(groupPropertiesHandler);
        graphHandler.addChild(propertiesHandler);
        nodeHandler.addChild(propertiesHandler);
        propertiesHandler.addChild(propertyHandler);
        groupPropertiesHandler.addChild(propertyHandler);
    }

    // Returns a new GraphDocument object deserialized from an XML input source.
    @Override
    public GraphDocument parse() throws IOException {
        if (monitor != null) {
            monitor.setState("Starting parsing");
        }
        try {
            XMLReader reader = createReader();
            reader.setContentHandler(new XMLParser(xmlDocument, monitor));
            reader.parse(new InputSource(Channels.newInputStream(channel)));
        } catch (SAXException ex) {
            if (!(ex instanceof SAXParseException) || !"XML document structures must start and end within the same entity.".equals(ex.getMessage())) {
                throw new IOException(ex);
            }
        }
        if (monitor != null) {
            monitor.setState("Finished parsing");
        }
        return graphDocument;
    }

    private XMLReader createReader() throws SAXException {
        try {
            SAXParserFactory pfactory = SAXParserFactory.newInstance();
            pfactory.setValidating(true);
            pfactory.setNamespaceAware(true);

            // Enable schema validation
            SchemaFactory sfactory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
            InputStream stream = Parser.class.getResourceAsStream("graphdocument.xsd");
            pfactory.setSchema(sfactory.newSchema(new Source[]{new StreamSource(stream)}));

            return pfactory.newSAXParser().getXMLReader();
        } catch (ParserConfigurationException ex) {
            throw new SAXException(ex);
        }
    }
}
