/* -*- tab-width: 4 -*-
 *
 * Electric(tm) VLSI Design System
 *
 * File: XMLIO.java
 * Written by Eric Kim, Sun Microsystems.
 *
 * Copyright (c) 2004, Oracle and/or its affiliates. All rights reserved.
 *
 * Electric(tm) is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * Electric(tm) 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 for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.sun.electric.tool.simulation.test;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * XML input helper class, used by ChainModel to read in the scan chain XML
 * file. For more information on the XML file, see <code>ChainG.dtd</code> and
 * ``The Scan Chain XML File Format'' by Tom O'Neill.
 */
class XMLIO {

    /**
     * Character string that identifies readable chain elements when it occurs
     * in the access attribute
     */
    public final static String READ_ACCESS_STRING = "R";

    /**
     * Character string that identifies readable chain elements when it occurs
     * in the access attribute
     */
    public final static String WRITE_ACCESS_STRING = "W";

    /**
     * Character string that identifies chain elements that are neither readable
     * nor writeable when it occurs in the access attribute.
     */
    public final static String NO_ACCESS_STRING = "-";

    /**
     * Character string that identifies chain elements that read and write to
     * and from the same shadow register. If the element is master clearable,
     * master clear writes to this register. No other circuits write to this
     * register.
     */
    public final static String SHADOW_ACCESS_STRING = "S";

    /**
     * Character string that identifies chain elements with unknown read/write
     * access
     */
    public final static String UNKNOWN_ACCESS_STRING = "?";

    /**
     * Character string that identifies chain elements whose scan out bits
     * cannot be predicted.
     */
    public final static String UNPREDICTABLE_ACCESS_STRING = "U";

    /**
     * Character string that indentifies chain elements that write to
     * a shadow register, but read from a different location. If the
     * element is master clearable, master clear writes to the shadow
     * register. No other circuits write to this register.
     */
    public final static String DUAL_PORTED_SHADOW_ACCESS_STRING = "D";

    /** Character string that identifies chain elements that clear HI */
    public final static String CLEARS_HI_STRING = "H";

    /** Character string that identifies chain elements that clear LO */
    public final static String CLEARS_LO_STRING = "L";

    /** Character string that identifies chain elements that clear NOT */
    public final static String CLEARS_NOT_STRING = "-";

    /** Character string that identifies chain elements that clear NOT */
    public final static String CLEARS_UNKNOWN_STRING = "?";

    protected final static String SCAN_CHAIN_DATA_NETS = "scanChainDataNets";

    public XMLIO() {
    }

    /**
     * Read a scan chain xml file, creating a TestNode tree representing the
     * scan chain. Return the system node at the root of the tree.
     * 
     * @return the TestNode object created to represent the <tt>system</tt>
     *         tag
     */
    public static TestNode read(String filename) throws IOException,
            SAXException, ParserConfigurationException, Exception {

        // Make a Document Object Model (DOM) of the XML file. The dbf
        // is used to obtain a parser db, which is then used to create the
        // DOM document object.
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setValidating(true);
        DocumentBuilder db = null;
        try {
            db = dbf.newDocumentBuilder();
        } catch (ParserConfigurationException pce) {
            pce.printStackTrace();
        }
        DefaultHandler er = new XMLEntityResolver();
        db.setEntityResolver(er);
        db.setErrorHandler(er);
        Document doc = db.parse(filename);


        // Extract scan chain tree from DOM. Start at system tag, then
        // descend.
        Element root = doc.getDocumentElement();
        Element element = (Element) root.getElementsByTagName("system").item(0);
        TestNode system = readSystem(element, null);

        Logger.logInit("Read XML file " + filename + ", system=" + system);

        return system;
    }

    private static class XMLEntityResolver extends DefaultHandler
    {
        public InputSource resolveEntity (String publicId,
					       String systemId) throws SAXException
        {
            InputStream inputStream  = null;

            try
            {
                URL fileURL = this.getClass().getResource("ChainG.dtd");
                URLConnection urlCon = fileURL.openConnection();
                inputStream = urlCon.getInputStream();
            }
            catch (IOException e)
            {
                e.printStackTrace();
            }
            return new InputSource(inputStream);
        }

        public void fatalError(SAXParseException e)
        {
            System.out.println("Parser Fatal Error on line "+e.getLineNumber()+
                    ", column "+e.getColumnNumber()+
                    ". Check Validation rules in ChainG.dtd");
            e.printStackTrace();
            System.exit(1);
        }

        public void warning(SAXParseException e)
        {
            System.out.println("Parser Warning");
            e.printStackTrace();
        }

        public void error(SAXParseException e)
        {
            System.out.println("Parser Error on line "+e.getLineNumber()+
                    ", column "+e.getColumnNumber()+
                    ". Check Validation rules in ChainG.dtd");
            e.printStackTrace();
            System.exit(1);
        }
    }

    /**
     * Create the TestNode (scan chain hierarchy) object corresponding to the
     * specified element in the DOM tree. Adds it as a child to the parent
     * TestNode object.
     * 
     * Recursively traverses the Document Obeject Model tree starting at
     * specified element and constructs our own TestNode representation of the
     * scan chain hieararchy.
     * 
     * @param element
     *            A Document Element in the XML file's DOM tree
     * @param parentNode
     *            Parent node in TestNode hierarchy
     * @return The TestNode object created to represent the element
     */
    private static TestNode readSystem(Element element, TestNode parentNode)
            throws Exception {

        // Create the TestNode object, if any, corresponding to the element.
        // Add the new node to its parent
        TestNode newNode = createTreeNode(element, parentNode);
        if (newNode == null) {
            return newNode;
        }
        if (parentNode != null)
            parentNode.addChild(newNode);

        // Add all of the children for this node to the TestNode
        // hierarchy. Note they will add their own children, and the
        // current node will be added when it returns to the calling
        // instance of readSystem(). Mmmmm....recursion.
        NodeList children = element.getChildNodes();
        for (int ind = 0; ind < children.getLength(); ind++) {
            Node childElement = children.item(ind);

            // If candidate element is an element node (insted of, e.g.,
            // a text node), add it and its children to parentNode.
            if (childElement.getNodeType() == Node.ELEMENT_NODE) {
                readSystem((Element) childElement, newNode);
            }
        }

        return newNode;
    }

    /**
     * If appropriate, create a new node in the <code>TestNode</code>
     * hierarchy representing element e in the Document Obeject Model of the
     * scan chain XML file.
     * 
     * @param element
     *            A Document Element in the XML file's DOM tree
     * @param parentNode
     *            Parent node in TestNode hierarchy
     * @return The TestNode object, if any, created to represent element e
     */
    protected static TestNode createTreeNode(Element element, TestNode parentNode)
            throws Exception {
        String name; //node name
        int length; //number of scan chain elements in node
        String comment; //optional comment for node

        // If element is null, no object to create
        if (element == null) {
            System.err.println("Warning null element");
            return null;
        }

        TestNode newNode = null; //Return value; no new node identified yet

        // Create new node for the TestNode object tree, based on the XML tag
        // name and parameters
        String tagName = element.getNodeName();
        if (tagName.equals("system")) {
            comment = getField(element, "comment");
            if (comment == null || comment.length() < 1)
                comment = "System Top Level";
            newNode = new TestNode("System", comment);
        } else if (tagName.equals("chip")) {
            name = element.getAttribute("name").trim();
            int lenIR = Integer.parseInt(element.getAttribute("lengthIR"));
            comment = getField(element, "comment");
            newNode = new ChipNode(name, lenIR, comment);
        } else if (tagName.equals("chain")) {
            newNode = processChain(element);
        } else if (tagName.equals("subchain")) {
            name = element.getAttribute("name").trim();
            String pin = element.getAttribute("pin");
            length = parseLength(element);
            comment = getField(element, "comment");
            String dataNet = element.getAttribute("dataNet");
            String dataNetBar = element.getAttribute("dataNet2");
            newNode = new SubchainNode(name, length, pin, comment, dataNet, dataNetBar);
        } else if (tagName.equals("duplicatechain")) {
            newNode = processDuplicateChain(element, parentNode);
        } else if (tagName.equals("forloop")) {
            newNode = processForLoop(element, parentNode);
        } else if (tagName.equals("comment")) {
            newNode = null;
        } else if (tagName.equals("scandatanets")) {
            comment = getField(element, "comment");
            newNode = new TestNode(SCAN_CHAIN_DATA_NETS, comment);
        } else if (tagName.equals("datachain")) {
            newNode = processChain(element);
        } else if (tagName.equals("datanet")) {
            comment = getField(element, "comment");
            name = element.getAttribute("name").trim();
            String dataNet = element.getAttribute("net");
            String dataNet2 = element.getAttribute("net2");
            newNode = new SubchainNode(name, 1, "", comment, dataNet, dataNet2);
        } else {
            Infrastructure.fatal("Unrecognized tag name " + tagName
                    + " in XML file\nparent node: " + parentNode);
        }

        if (newNode != null) {
            setAccess(element, parentNode, newNode);
            setClearBehavior(element, parentNode, newNode);
        }

        return newNode;
    }

    /** Creates <code>ChainNode</code> object corresponding to the element */
    private static TestNode processChain(Element element) {
        String name = element.getAttribute("name").trim();
        String opcode = element.getAttribute("opcode").trim();
        int length = parseLength(element);
        String comment = getField(element, "comment");

        return new ChainNode(name, opcode, length, comment);
    }

    private static TestNode processDuplicateChain(Element element, TestNode parentNode) {
        String name = element.getAttribute("name").trim();
        String opcode = element.getAttribute("opcode").trim();
        String reference = element.getAttribute("sameas").trim();
        TestNode pp = parentNode;
        while (!(parentNode instanceof ChipNode)) {
            parentNode = (TestNode)parentNode.getParent();
        }
        for (int i=0; i<parentNode.getChildCount(); i++) {
            MyTreeNode node = parentNode.getChildAt(i);
            if (node instanceof ChainNode) {
                if (node.getName().equals(reference)) {
                    return new ChainNodeDuplicate(name, opcode, (ChainNode)node);
                }
            }
        }
        Infrastructure.fatal("Could not find reference chain " + reference
                    + " in XML file\nparent node: " + pp + " for duplicate chain: "+name);
        return null;
    }

    /**
     * Adds children of a forloop element multiple times, directly to the
     * parentNode TestNode object. Note that forloop nodes do not appear in the
     * TestNode hierarchy, so this routine must return null.
     * 
     * @param element
     *            A Document Element in the XML file's DOM tree
     * @param parentNode
     *            Parent node in TestNode hierarchy
     * @return null (for loop node disappears in TestNode hierarchy)
     */
    private static TestNode processForLoop(Element element, TestNode parentNode)
            throws Exception {

        int start = Integer.parseInt(element.getAttribute("initial"));
        int end = Integer.parseInt(element.getAttribute("final"));
        int increment = Integer.parseInt(element.getAttribute("increment"));

        if (end >= start && increment > 0) {
            for (int ind = start; ind <= end; ind += increment) {
                addForLoopChildren(element, parentNode, ind);
            }
        } else if (end <= start && increment < 0) {
            for (int ind = start; ind >= end; ind += increment) {
                addForLoopChildren(element, parentNode, ind);
            }
        } else {
            Infrastructure.fatal("Infinite <forloop> tag specified."
                    + " Bad tag, bad!\n  parent='" + parentNode
                    + "'\n  element='" + element + "'");
        }

        return null;
    }

    /**
     * Add the children of the a forloop tag, during a single iteration of the
     * loop. The children are added directly to <code>parentNode</code> (no
     * forloop node is created), and will add their own children.
     * 
     * @param element
     *            The forloop Document Element in the XML file's DOM tree
     * @param parentNode
     *            The forloop's parent node
     * @param ind
     *            The loop index to append to the child nodes' names
     * @throws Exception
     */
    private static void addForLoopChildren(Element element,
            TestNode parentNode, int ind) throws Exception {
        NodeList children = element.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            Node childElement = children.item(i);
            if (childElement.getNodeType() != Node.ELEMENT_NODE)
                continue;
            TestNode childNode = readSystem((Element) childElement, parentNode);
            if (childNode == null)
                continue;

            //Append index value to name of child tree node
            String name = childNode.getName() + Integer.toString(ind);
            childNode.setName(name);
        }
    }

    /**
     * Extracts length from current element
     * 
     * @return integer parsed from attribute length or -1 on error
     */
    private static int parseLength(Element e) {
        int length = -1;
        try {
            String lengthS = e.getAttribute("length");
            length = Integer.parseInt(lengthS);
        } catch (Exception ignore) {
        }
        return length;
    }

    /**
     * Return the text from the specified element. Non recursive. Find only one
     * level. Assume element has one field element, one text
     */
    private static String getField(Node n, String field) {
        NodeList children = n.getChildNodes();
        boolean found = false;
        String ret = null;

        for (int i = 0; i < children.getLength(); i++) {
            Node childNode = children.item(i);
            if (childNode.getNodeType() == Node.ELEMENT_NODE
                    && childNode.getNodeName().equals(field)) {
                if (found) {
                    Infrastructure.fatal("getField: node '" + n
                            + "' has more than one child named " + field);
                } else {
                    found = true;
                    ret = childNode.getFirstChild().getNodeValue();
                }
            }
        }

        return ret;
    }

    /**
     * Sets the readable, writeable characteristics of newNode according to the
     * "access" attribute in the corresponding element of the DOM. If unknown
     * access is specified, access is set to RW (this is the most conservative
     * selection, because it disables tests that depend on the access
     * attribute). If the access attribute is not provided, default to access of
     * parentNode.
     * 
     * @param element
     * @param parentNode
     * @param newNode
     */
    private static void setAccess(Element element, TestNode parentNode,
            TestNode newNode) {
        boolean unpredictable, readable, writeable, usesShadow, usesDualPortedShadow;
        String access = element.getAttribute("access").trim().toUpperCase();

        // If the access was not specified, then use the parent's
        // accessability as the default.
        if (access.length() == 0) {
            unpredictable = parentNode.isUnpredictable();
            readable = parentNode.isReadable();
            writeable = parentNode.isWriteable();
            usesShadow = parentNode.usesShadow();
            usesDualPortedShadow = parentNode.usesDualPortedShadow();
        } else if (access.equals(UNKNOWN_ACCESS_STRING)) {
            readable = writeable = true;
            unpredictable = usesShadow = usesDualPortedShadow = false;
        } else if (access.equals(NO_ACCESS_STRING)) {
            unpredictable = readable = writeable = usesShadow = usesDualPortedShadow = false;
        } else if (access.equals(UNPREDICTABLE_ACCESS_STRING)) {
            unpredictable = true;
            readable = writeable = usesShadow = usesDualPortedShadow = false;
        } else {
            int goodLength = 0;

            unpredictable = false;
            readable = access.indexOf(READ_ACCESS_STRING) >= 0;
            if (readable) {
                goodLength += READ_ACCESS_STRING.length();
            }
            writeable = access.indexOf(WRITE_ACCESS_STRING) >= 0;
            if (writeable) {
                goodLength += WRITE_ACCESS_STRING.length();
            }
            usesShadow = access.indexOf(SHADOW_ACCESS_STRING) >= 0;
            if (usesShadow) {
                goodLength += SHADOW_ACCESS_STRING.length();
            }
            usesDualPortedShadow = access.indexOf(DUAL_PORTED_SHADOW_ACCESS_STRING) >= 0;
            if (usesDualPortedShadow) {
                goodLength += DUAL_PORTED_SHADOW_ACCESS_STRING.length();
            }

            if (usesShadow && usesDualPortedShadow) {
                Infrastructure.fatal("Bad access string '"
                        + element.getAttribute("access") + "' in XML file."
                        + "\nCannot have S and D specified at the same time."
                        + "\nerror node: '" + newNode
                        + "'\nparent node: '" + parentNode + "'");
            }

            // READ_ACCESS_STRING and WRITE_ACCESS string may appear
            // together, but not with any other characters.
            if (access.length() != goodLength) {
                Infrastructure.fatal("Bad access string '"
                        + element.getAttribute("access") + "' in XML file."
                        + "\nAllowed values are "
                        + "?, U, -, R, W, RW, RWS, or RWD.\nerror node: '" + newNode
                        + "'\nparent node: '" + parentNode + "'");
            }
        }

        newNode.setUnpredictable(unpredictable);
        newNode.setReadable(readable);
        newNode.setWriteable(writeable);
        newNode.setUsesShadow(usesShadow);
        newNode.setUsesDualPortedShadow(usesDualPortedShadow);
    }

    /**
     * Sets the clearBehavior property of newNode according to the "clears"
     * attribute in the corresponding element of the DOM. If the clears
     * attribute is not provided, default to clearing behavior of parentNode.
     * 
     * @param element
     * @param parentNode
     * @param newNode
     */
    private static void setClearBehavior(Element element, TestNode parentNode,
            TestNode newNode) {
        int clearBehavior = TestNode.CLEARS_UNKNOWN;
        String clears = element.getAttribute("clears").trim().toUpperCase();

        // If the clears was not specified, then use the parent's
        // clear behavior as the default.
        if (clears.length() == 0) {
            clearBehavior = parentNode.getClearBehavior();
        } else if (clears.equals(CLEARS_LO_STRING)) {
            clearBehavior = TestNode.CLEARS_LO;
        } else if (clears.equals(CLEARS_HI_STRING)) {
            clearBehavior = TestNode.CLEARS_HI;
        } else if (clears.equals(CLEARS_NOT_STRING)) {
            clearBehavior = TestNode.CLEARS_NOT;
        } else if (clears.equals(CLEARS_UNKNOWN_STRING)) {
            clearBehavior = TestNode.CLEARS_UNKNOWN;
        } else {
            Infrastructure.fatal("Bad clears string '"
                    + element.getAttribute("clears") + "' in XML file."
                    + "\nAllowed values are ?, -, L, or H." + "\nerror node: "
                    + newNode + "\nparent node: " + parentNode);
        }

        newNode.setClearBehavior(clearBehavior);
    }
}
