package jdiff;

import java.io.*;
import java.util.*;
import javax.xml.parsers.ParserConfigurationException;

/* 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.InputSource;
import org.xml.sax.helpers.*;

/**
 * Creates an API object from an XML file. The API object is the internal
 * representation of an API.
 * All methods in this class for populating an API object are static.
 *
 * See the file LICENSE.txt for copyright details.
 * @author Matthew Doar, mdoar@pobox.com
 */
public class XMLToAPI {

    /** The instance of the API object which is populated from the file. */
    private static API api_ = null;

    /** Default constructor. */
    private XMLToAPI() {
    }

    /**
     * Read the file where the XML representing the API is stored.
     *
     * @param filename The full name of the file containing the XML
     *                 representing the API
     * @param createGlobalComments If set, then store possible comments
     * @param apiName The simple name of the API file. If -oldapidir and
     *                -newapidir are not used, then this is the same as
     *                the filename parameter
     */
    public static API readFile(String filename, boolean createGlobalComments,
            String apiName) {
        // The instance of the API object which is populated from the file.
        api_ = new API();
        api_.name_ = apiName; // Checked later
        try {
            XMLReader parser = null;
            DefaultHandler handler = new APIHandler(api_, createGlobalComments);
            try {
                parser = javax.xml.parsers.SAXParserFactory.newInstance().newSAXParser().getXMLReader();
            } catch (SAXException saxe) {
                System.out.println("SAXException: " + saxe);
                saxe.printStackTrace();
                System.exit(1);
            } catch (ParserConfigurationException pce) {
                System.out.println("ParserConfigurationException: " + pce);
                pce.printStackTrace();
                System.exit(1);
            }

            if (validateXML) {
                parser.setFeature("https://xml.org/sax/features/namespaces", true);
                parser.setFeature("https://xml.org/sax/features/validation", true);
                parser.setFeature("https://apache.org/xml/features/validation/schema", true);
            }

            parser.setContentHandler(handler);
            parser.setErrorHandler(handler);
            parser.parse(new InputSource(new FileInputStream(new File(filename))));
        } catch(org.xml.sax.SAXNotRecognizedException snre) {
            System.out.println("SAX Parser does not recognize feature: " + snre);
            snre.printStackTrace();
            System.exit(1);
        } catch(org.xml.sax.SAXNotSupportedException snse) {
            System.out.println("SAX Parser feature is not supported: " + snse);
            snse.printStackTrace();
            System.exit(1);
        } catch(org.xml.sax.SAXException saxe) {
            System.out.println("SAX Exception parsing file '" + filename + "' : " + saxe);
            saxe.printStackTrace();
            System.exit(1);
        } catch(java.io.IOException ioe) {
            System.out.println("IOException parsing file '" + filename + "' : " + ioe);
            ioe.printStackTrace();
            System.exit(1);
        }

        // Add the inherited methods and fields to each class
        addInheritedElements();
        return api_;
    } //readFile()

    /**
     * Add the inherited methods and fields to each class in turn.
     */
    public static void addInheritedElements() {
        Iterator iter = api_.packages_.iterator();
        while (iter.hasNext()) {
            PackageAPI pkg = (PackageAPI)(iter.next());
            Iterator iter2 = pkg.classes_.iterator();
            while (iter2.hasNext()) {
                ClassAPI cls = (ClassAPI)(iter2.next());
                // Look up any inherited classes or interfaces
                if (cls.extends_ != null) {
                    ClassAPI parent = (ClassAPI)api_.classes_.get(cls.extends_);
                    if (parent != null)
                        addInheritedElements(cls, parent, cls.extends_);
                }
                if (cls.implements_.size() != 0) {
                    Iterator iter3 = cls.implements_.iterator();
                    while (iter3.hasNext()) {
                        String implName = (String)(iter3.next());
                        ClassAPI parent = (ClassAPI)api_.classes_.get(implName);
                        if (parent != null)
                            addInheritedElements(cls, parent, implName);
                    }
                }
            } //while (iter2.hasNext())
        } //while (iter.hasNext())
    }

    /**
     * Add all the inherited methods and fields in the second class to
     * the first class, marking them as inherited from the second class.
     * Do not add a method or a field if it is already defined locally.
     *
     * Only elements at the specified visibility level or
     * higher appear in the XML file. All that remains to be tested for
     * a private element, which is never inherited.
     *
     * If the parent class inherits any classes or interfaces, call this
     * method recursively with those parents.
     */
    public static void addInheritedElements(ClassAPI child, ClassAPI parent,
                                            String fqParentName) {
        if (parent.methods_.size() != 0) {
            Iterator iter = parent.methods_.iterator();
            while (iter.hasNext()) {
                MethodAPI m = (MethodAPI)(iter.next());
                // See if it the method is overridden locally
                boolean overridden = false;
                Iterator iter2 = child.methods_.iterator();
                while (iter2.hasNext()) {
                    MethodAPI localM = (MethodAPI)(iter2.next());
                    if (localM.name_.compareTo(m.name_) == 0 &&
                        localM.getSignature().compareTo(m.getSignature()) == 0)
                        overridden = true;
                }
                if (!overridden && m.inheritedFrom_ == null &&
                    m.modifiers_.visibility != null &&
                    m.modifiers_.visibility.compareTo("private") != 0) {
                    MethodAPI m2 = new MethodAPI(m);
                    m2.inheritedFrom_ = fqParentName;
                    child.methods_.add(m2);
                }
            }
        }
        if (parent.fields_.size() != 0) {
            Iterator iter = parent.fields_.iterator();
            while (iter.hasNext()) {
                FieldAPI f = (FieldAPI)(iter.next());
                if (child.fields_.indexOf(f) == -1 &&
                    f.inheritedFrom_ == null &&
                    f.modifiers_.visibility != null &&
                    f.modifiers_.visibility.compareTo("private") != 0) {
                    FieldAPI f2 = new FieldAPI(f);
                    f2.inheritedFrom_ = fqParentName;
                    child.fields_.add(f2);
                }
            }
        }

        // Look up any inherited classes or interfaces
        if (parent.extends_ != null) {
            ClassAPI parent2 = (ClassAPI)api_.classes_.get(parent.extends_);
            if (parent2 != null)
                addInheritedElements(child, parent2, parent.extends_);
        }
        if (parent.implements_.size() != 0) {
            Iterator iter3 = parent.implements_.iterator();
            while (iter3.hasNext()) {
                String implName = (String)(iter3.next());
                ClassAPI parent2 = (ClassAPI)api_.classes_.get(implName);
                if (parent2 != null)
                    addInheritedElements(child, parent2, implName);
            }
        }
    }

//
// Methods to add data to an API object. Called by the XML parser.
//

    /**
     * Set the name of the API object.
     *
     * @param name The name of the package.
     */
    public static void nameAPI(String name) {
        if (name == null) {
            System.out.println("Warning: no API identifier found in the XML file '" + api_.name_ + "'");
            String filename = api_.name_.substring(0, api_.name_.lastIndexOf(".xml"));
            // System.out.println(" api level:" + filename);
            System.out.println("Using '" + filename + "' as the API identifier.");
            api_.name_ = filename;
            // System.exit(3);
            return;
        }
        // Check the given name against the filename currently stored in
        // the name_ field
        String filename2 = name.replace(' ','_');
        filename2 += ".xml";
        if (filename2.compareTo(api_.name_) != 0) {
            System.out.println("Warning: API identifier in the XML file (" +
                               name + ") differs from the name of the file '" +
                               api_.name_ + "'");
        }
        api_.name_ = name;
    }

    /**
     * Create a new package and add it to the API. Called by the XML parser.
     *
     * @param name The name of the package.
     */
    public static void addPackage(String name) {
        api_.currPkg_ = new PackageAPI(name);
        api_.packages_.add(api_.currPkg_);
    }

    /**
     * Create a new class and add it to the current package. Called by the XML parser.
     *
     * @param name The name of the class.
     * @param parent The name of the parent class, null if no class is extended.
     * @param modifiers Modifiers for this class.
     */
    public static void addClass(String name, String parent,
                                boolean isAbstract,
                                Modifiers modifiers) {
        api_.currClass_ = new ClassAPI(name, parent, false, isAbstract, modifiers);
        api_.currPkg_.classes_.add(api_.currClass_);
        String fqName = api_.currPkg_.name_ + "." + name;
        ClassAPI caOld = (ClassAPI)api_.classes_.put(fqName, api_.currClass_);
        if (caOld != null) {
            System.out.println("Warning: duplicate class : " + fqName + " found. Using the first instance only.");
        }
    }

    /**
     * Add an new interface and add it to the current package. Called by the
     * XML parser.
     *
     * @param name The name of the interface.
     * @param parent The name of the parent interface, null if no
     *               interface is extended.
     */
    public static void addInterface(String name, String parent,
                                    boolean isAbstract,
                                    Modifiers modifiers) {
        api_.currClass_ = new ClassAPI(name, parent, true, isAbstract, modifiers);
        api_.currPkg_.classes_.add(api_.currClass_);
    }

    /**
     * Add an inherited interface to the current class. Called by the XML
     * parser.
     *
     * @param name The name of the inherited interface.
     */
    public static void addImplements(String name) {
       api_.currClass_.implements_.add(name);
    }

    /**
     * Add a constructor to the current class. Called by the XML parser.
     *
     * @param name The name of the constructor (optional).
     * @param type The type of the constructor.
     * @param modifiers Modifiers for this constructor.
     */
    public static void addCtor(String name, String type, Modifiers modifiers) {
        String t = type;
        if (t == null)
            t = "void";
        api_.currCtor_ = new ConstructorAPI(name, t, modifiers);
        api_.currClass_.ctors_.add(api_.currCtor_);
    }

    /**
     * Add a method to the current class. Called by the XML parser.
     *
     * @param name The name of the method.
     * @param returnType The return type of the method, null if it is void.
     * @param modifiers Modifiers for this method.
     */
    public static void addMethod(String name, String returnType,
                                 boolean isAbstract, boolean isNative,
                                 boolean isSynchronized, Modifiers modifiers) {
        String rt = returnType;
        if (rt == null)
            rt = "void";
        api_.currMethod_ = new MethodAPI(name, rt, isAbstract, isNative,
                                         isSynchronized, modifiers);
        api_.currClass_.methods_.add(api_.currMethod_);
    }

    /**
     * Add a field to the current class. Called by the XML parser.
     *
     * @param name The name of the field.
     * @param type The type of the field, null if it is void.
     * @param modifiers Modifiers for this field.
     */
    public static void addField(String name, String type, boolean isTransient,
                                boolean isVolatile, String value, Modifiers modifiers) {
        String t = type;
        if (t == null)
            t = "void";
        api_.currField_ = new FieldAPI(name, t, isTransient, isVolatile, value, modifiers);
        api_.currClass_.fields_.add(api_.currField_);
    }

    /**
     * Add a parameter to the current method or constructor. Called by the XML parser.
     * Constuctors have their type (signature) in an attribute, since it
     * is often shorter and makes parsing a little easier.
     *
     * @param name The name of the parameter.
     * @param type The type of the parameter, null if it is void.
     * @param isConstructor Whether the given parameter is for a constructor
     */
    public static void addParam(String name, String type, boolean isConstructor) {
        String t = type;
        if (t == null)
            t = "void";
        ParamAPI paramAPI = new ParamAPI(name, t);
        if (isConstructor) {
            api_.currCtor_.params_.add(paramAPI);
        } else {
            api_.currMethod_.params_.add(paramAPI);
        }
    }

    /**
     * Add an exception to the current method or constructor.
     * Called by the XML parser.
     *
     * @param name The name of the parameter.
     * @param type The type of the parameter.
     *             May be null in JDiff1.0.8 and earlier versions.
     * @param currElement Name of the current element.
     */
    public static void addException(String name, String type, String currElement) {
	String exceptionId = type;
	if (type == null || !showExceptionTypes)
	    exceptionId = name;
        if (currElement.compareTo("method") == 0) {
            if (api_.currMethod_.exceptions_.compareTo("no exceptions") == 0)
                api_.currMethod_.exceptions_ = exceptionId;
            else
                api_.currMethod_.exceptions_ += ", " + exceptionId;
        } else {
            if (api_.currCtor_.exceptions_.compareTo("no exceptions") == 0)
                api_.currCtor_.exceptions_ = exceptionId;
            else
                api_.currCtor_.exceptions_ += ", " + exceptionId;
        }
    }

    /**
     * If set, validate the XML which represents an API. By default, this is
     * not set for reasons of efficiency, and also because if JDiff generated
     * the XML, it should not need validating.
     */
    public static boolean validateXML = false;

    /**
     * If set, then store and display the whole qualified name of exceptions.
     * If not set, then store and display just the name of the exception,
     * which is shorter, but may not detect when an exception changes class,
     * but retains the same name.
     */
    private static boolean showExceptionTypes = true;
}
