package jdiff;

import java.io.*;
import java.util.*;

/* For SAX parsing in APIHandler */
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Handle the parsing of an XML file and the generation of an API object.
 *
 * See the file LICENSE.txt for copyright details.
 * @author Matthew Doar, mdoar@pobox.com
 */
class APIHandler extends DefaultHandler {

    /** The API object which is populated from the XML file. */
    public API api_;

    /** Default constructor. */
    public APIHandler(API api, boolean createGlobalComments) {
        api_ = api;
        createGlobalComments_ = createGlobalComments;
        tagStack = new LinkedList();
    }

    /** If set, then check that each comment is a sentence. */
    public static boolean checkIsSentence = false;

    /**
     * Contains the name of the current package element type
     * where documentation is being added. Also used as the level
     * at which to add documentation into an element, i.e. class-level
     * or package-level.
     */
    private String currentElement = null;

    /** If set, then create the global list of comments. */
    private boolean createGlobalComments_ = false;

    /** Set if inside a doc element. */
    private boolean inDoc = false;

    /** The current comment text being assembled. */
    private String currentText = null;

    /** The current text from deprecation, null if empty. */
    private String currentDepText = null;

    /**
     * The stack of SingleComment objects awaiting the comment text
     * currently being assembled.
     */
    private LinkedList tagStack = null;

    /** Called at the start of the document. */
    public void startDocument() {
    }

    /** Called when the end of the document is reached. */
    public void endDocument() {
        if (trace)
            api_.dump();
        System.out.println(" finished");
    }

    /** Called when a new element is started. */
    public void startElement(java.lang.String uri, java.lang.String localName,
                             java.lang.String qName, Attributes attributes) {
	 // The change to JAXP compliance produced this change.
	if (localName.equals(""))
	    localName = qName;
        if (localName.compareTo("api") == 0) {
            String apiName = attributes.getValue("name");
            String version = attributes.getValue("jdversion"); // Not used yet
            XMLToAPI.nameAPI(apiName);
        } else if (localName.compareTo("package") == 0) {
            currentElement = localName;
            String pkgName = attributes.getValue("name");
            XMLToAPI.addPackage(pkgName);
        } else if (localName.compareTo("class") == 0) {
            currentElement = localName;
            String className = attributes.getValue("name");
            String parentName = attributes.getValue("extends");
            boolean isAbstract = false;
            if (attributes.getValue("abstract").compareTo("true") == 0)
                isAbstract = true;
            XMLToAPI.addClass(className, parentName, isAbstract, getModifiers(attributes));
        } else if (localName.compareTo("interface") == 0) {
            currentElement = localName;
            String className = attributes.getValue("name");
            String parentName = attributes.getValue("extends");
            boolean isAbstract = false;
            if (attributes.getValue("abstract").compareTo("true") == 0)
                isAbstract = true;
            XMLToAPI.addInterface(className, parentName, isAbstract, getModifiers(attributes));
        } else if (localName.compareTo("implements") == 0) {
            String interfaceName = attributes.getValue("name");
            XMLToAPI.addImplements(interfaceName);
        } else if (localName.compareTo("constructor") == 0) {
            currentElement = localName;
            String ctorName = attributes.getValue("name");
            String ctorType = attributes.getValue("type");
            XMLToAPI.addCtor(ctorName, ctorType, getModifiers(attributes));
        } else if (localName.compareTo("method") == 0) {
            currentElement = localName;
            String methodName = attributes.getValue("name");
            String returnType = attributes.getValue("return");
            boolean isAbstract = false;
            if (attributes.getValue("abstract").compareTo("true") == 0)
                isAbstract = true;
            boolean isNative = false;
            if (attributes.getValue("native").compareTo("true") == 0)
                isNative = true;
            boolean isSynchronized = false;
            if (attributes.getValue("synchronized").compareTo("true") == 0)
                isSynchronized = true;
            XMLToAPI.addMethod(methodName, returnType, isAbstract, isNative,
                               isSynchronized, getModifiers(attributes));
        } else if (localName.compareTo("field") == 0) {
            currentElement = localName;
            String fieldName = attributes.getValue("name");
            String fieldType = attributes.getValue("type");
            boolean isTransient = false;
            if (attributes.getValue("transient").compareTo("true") == 0)
                isTransient = true;
            boolean isVolatile = false;
            if (attributes.getValue("volatile").compareTo("true") == 0)
                isVolatile = true;
            String value = attributes.getValue("value");
            XMLToAPI.addField(fieldName, fieldType, isTransient, isVolatile,
                              value, getModifiers(attributes));
        } else if (localName.compareTo("param") == 0 || localName.compareTo("parameter") == 0) {
            String paramName = attributes.getValue("name");
            String paramType = attributes.getValue("type");
            XMLToAPI.addParam(paramName, paramType, currentElement.compareTo("constructor") == 0);
        } else if (localName.compareTo("exception") == 0) {
            String paramName = attributes.getValue("name");
            String paramType = attributes.getValue("type");
            XMLToAPI.addException(paramName, paramType, currentElement);
        } else if (localName.compareTo("doc") == 0) {
            inDoc = true;
            currentText = null;
        } else {
            if (inDoc) {
                // Start of an element, probably an HTML element
                addStartTagToText(localName, attributes);
            } else {
                System.out.println("Error: unknown element type: " + localName);
                System.exit(-1);
            }
        }
    }

    /** Called when the end of an element is reached. */
    public void endElement(java.lang.String uri, java.lang.String localName,
                           java.lang.String qName) {
        if (localName.equals(""))
            localName = qName;
        // Deal with the end of doc blocks
        if (localName.compareTo("doc") == 0) {
            inDoc = false;
            // Add the assembled comment text to the appropriate current
            // program element, as determined by currentElement.
            addTextToComments();
        } else if (inDoc) {
            // An element was found inside the HTML text
            addEndTagToText(localName);
        } else if (currentElement == null) {
            // No elements were found, nothing to do here
        } else if (currentElement.compareTo("constructor") == 0 &&
                   localName.compareTo("constructor") == 0) {
            currentElement = "class";
        } else if (currentElement.compareTo("method") == 0 &&
                   localName.compareTo("method") == 0) {
            currentElement = "class";
        } else if (currentElement.compareTo("field") == 0 &&
                   localName.compareTo("field") == 0) {
            currentElement = "class";
        } else if (currentElement.compareTo("class") == 0 ||
                   currentElement.compareTo("interface") == 0) {
            // Feature request 510307 and bug 517383: duplicate comment ids.
            // The end of a member element leaves the currentElement at the
            // "class" level, but the next class may in fact be an interface
            // and so the currentElement here will be "interface".
            if (localName.compareTo("class") == 0 ||
                localName.compareTo("interface") == 0) {
                currentElement = "package";
            }
        }
    }

    /** Called to process text. */
    public void characters(char[] ch, int start, int length) {
         if (inDoc) {
            String chunk = new String(ch, start, length);
            if (currentText == null)
                currentText = chunk;
            else
                currentText += chunk;
         }
    }

    /**
     * Trim the current text, check it is a sentence and add it to the
     * current program element.
     */
    public void addTextToComments() {
        // Eliminate any whitespace at each end of the text.
        currentText = currentText.trim();
        // Convert any @link tags to HTML links.
        if (convertAtLinks) {
            currentText = Comments.convertAtLinks(currentText, currentElement,
                                                  api_.currPkg_, api_.currClass_);
        }
        // Check that it is a sentence
        if (checkIsSentence && !currentText.endsWith(".") &&
            currentText.compareTo(Comments.placeHolderText) != 0) {
            System.out.println("Warning: text of comment does not end in a period: " + currentText);
        }
        // The construction of the commentID assumes that the
        // documentation is the final element to be parsed. The format matches
        // the format used in the report generator to look up comments in the
        // the existingComments object.
        String commentID = null;
        // Add this comment to the current API element.
        if (currentElement.compareTo("package") == 0) {
            api_.currPkg_.doc_ = currentText;
            commentID = api_.currPkg_.name_;
        } else if (currentElement.compareTo("class") == 0 ||
                   currentElement.compareTo("interface") == 0) {
            api_.currClass_.doc_ = currentText;
            commentID = api_.currPkg_.name_ + "." + api_.currClass_.name_;
        } else if (currentElement.compareTo("constructor") == 0) {
            api_.currCtor_.doc_ = currentText;
            commentID = api_.currPkg_.name_ + "." + api_.currClass_.name_ +
                ".ctor_changed(";
            if (api_.currCtor_.getSignature().compareTo("void") == 0)
                commentID = commentID + ")";
            else
                commentID = commentID + api_.currCtor_.getSignature() + ")";
        } else if (currentElement.compareTo("method") == 0) {
            api_.currMethod_.doc_ = currentText;
            commentID = api_.currPkg_.name_ + "." + api_.currClass_.name_ +
                "." + api_.currMethod_.name_ + "_changed(" +
                api_.currMethod_.getSignature() + ")";
        } else if (currentElement.compareTo("field") == 0) {
            api_.currField_.doc_ = currentText;
            commentID = api_.currPkg_.name_ + "." + api_.currClass_.name_ +
                "." + api_.currField_.name_;
        }
        // Add to the list of possible comments for use when an
        // element has changed (not removed or added).
        if (createGlobalComments_ && commentID != null) {
            String ct = currentText;
            // Use any deprecation text as the possible comment, ignoring
            // any other comment text.
            if (currentDepText != null) {
                ct = currentDepText;
                currentDepText = null; // Never reuse it. Bug 469794
            }
            String ctOld = (String)(Comments.allPossibleComments.put(commentID, ct));
            if (ctOld != null) {
                System.out.println("Error: duplicate comment id: " + commentID);
                System.exit(5);
            }
        }
    }

    /**
     * Add the start tag to the current comment text.
     */
    public void addStartTagToText(String localName, Attributes attributes) {
        // Need to insert the HTML tag into the current text
        String currentHTMLTag = localName;
        // Save the tag in a stack
        tagStack.add(currentHTMLTag);
        String tag = "<" + currentHTMLTag;
        // Now add all the attributes into the current text
        int len = attributes.getLength();
        for (int i = 0; i < len; i++) {
            String name = attributes.getLocalName(i);
            String value = attributes.getValue(i);
            tag += " " + name + "=\"" + value+ "\"";
        }

        // End the tag
        if (Comments.isMinimizedTag(currentHTMLTag)) {
            tag += "/>";
        } else {
            tag += ">";
        }
        // Now insert the HTML tag into the current text
        if (currentText == null)
            currentText = tag;
        else
            currentText += tag;
    }

    /**
     * Add the end tag to the current comment text.
     */
    public void addEndTagToText(String localName) {
        // Close the current HTML tag
        String currentHTMLTag = (String)(tagStack.removeLast());
        if (!Comments.isMinimizedTag(currentHTMLTag))
            currentText += "</" + currentHTMLTag + ">";
    }

    /** Extra modifiers which are common to all program elements. */
    public Modifiers getModifiers(Attributes attributes) {
        Modifiers modifiers = new Modifiers();
        modifiers.isStatic = false;
        if (attributes.getValue("static").compareTo("true") == 0)
            modifiers.isStatic = true;
        modifiers.isFinal = false;
        if (attributes.getValue("final").compareTo("true") == 0)
            modifiers.isFinal = true;
        modifiers.isDeprecated = false;
        String cdt = attributes.getValue("deprecated");
        if (cdt.compareTo("not deprecated") == 0) {
            modifiers.isDeprecated = false;
            currentDepText = null;
        } else if (cdt.compareTo("deprecated, no comment") == 0) {
            modifiers.isDeprecated = true;
            currentDepText = null;
        } else {
            modifiers.isDeprecated = true;
            currentDepText = API.showHTMLTags(cdt);
        }
        modifiers.visibility = attributes.getValue("visibility");
        return modifiers;
    }

    public void warning(SAXParseException e) {
        System.out.println("Warning (" + e.getLineNumber() + "): parsing XML API file:" + e);
        e.printStackTrace();
    }

    public void error(SAXParseException e) {
        System.out.println("Error (" + e.getLineNumber() + "): parsing XML API file:" + e);
        e.printStackTrace();
        System.exit(1);
    }

    public void fatalError(SAXParseException e) {
        System.out.println("Fatal Error (" + e.getLineNumber() + "): parsing XML API file:" + e);
        e.printStackTrace();
        System.exit(1);
    }

    /**
     * If set, then attempt to convert @link tags to HTML links.
     * A few of the HTML links may be broken links.
     */
    private static boolean convertAtLinks = true;

    /** Set to enable increased logging verbosity for debugging. */
    private static boolean trace = false;

}
