/**
 * EasyBeans
 * Copyright (C) 2006 Bull S.A.S.
 * Contact: easybeans@objectweb.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id: SignatureCompare.java 5276 2010-01-08 16:22:04Z benoitf $
 * --------------------------------------------------------------------------
 */

package org.ow2.spec.testengine;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.ow2.spec.testengine.metadata.AnnotationMetadata;
import org.ow2.spec.testengine.metadata.ClassMetadata;
import org.ow2.spec.testengine.metadata.EnumAnnotationMetadata;
import org.ow2.spec.testengine.metadata.FieldMetadata;
import org.ow2.spec.testengine.metadata.InnerClassMetadata;
import org.ow2.spec.testengine.metadata.MethodMetadata;
import org.ow2.spec.testengine.xml.DocumentParser;
import org.ow2.spec.testengine.xml.DocumentParserException;
import org.ow2.spec.testengine.xml.XMLUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class SignatureCompare {

    private static final String APICHECK_NS = "http://org.ow2.spec/ns/apicheck";

    private static final String PACKAGE_ELEMENT = "package";

    private static final String CLASS_ELEMENT = "class";

    private static final String INTERFACE_ELEMENT = "interface";

    private static final String METHOD_ELEMENT = "method";

    private static final String ANNOTATION_ELEMENT = "annotation";

    private static final String ENUM_ANNOTATION_ELEMENT = "enum-annotation";

    private static final String FIELD_ELEMENT = "field";

    private static final String INNER_CLASS_ELEMENT = "inner-class";

    private static final String EXCEPTION_ELEMENT = "exception";

    /**
     * Jar File to analyze.
     */
    private JarFile jarFile = null;

    /**
     * URL of the XML data file.
     */
    private URL urlXML = null;

    /**
     * Reference build from XML file<br>
     * Map between package name and Map of <className, classMetadata>.
     */
    private Map<String, Map<String, ClassMetadata>> referencePackageClassesMetadataMap = null;

    /**
     * Map build ith jar file for the check<br>
     * Map between package name and Map of <className, classMetadata>.
     */
    private Map<String, Map<String, ClassMetadata>> packageClassesMetadataMap = null;

    /**
     * Analyzer visitor. (ASM visitor).
     */
    private AnalyzerClassVisitor analyzerClassVisitor = null;

    /**
     * Files to analyze
     */
    private List<File> allFiles = null;

    /**
     * Only for exceptions.
     */
    private File tmpFile;

    public SignatureCompare(String[] args) {
        if (args == null) {
            usage();
            throw new IllegalArgumentException("No arguments");
        }

        if (args.length < 2) {
            usage();
            throw new IllegalArgumentException("Needs at least 2 arguments");
        }

        tmpFile = new File(args[0]);
        if (!tmpFile.exists()) {
            usage();
            throw new IllegalArgumentException("The file '" + tmpFile.getAbsolutePath() + "' doesn't exists");
        }

        // first arg = JAR File
        if (tmpFile.isDirectory()) {
            allFiles = new ArrayList<File>();
            addFilesFromDirectory(allFiles, tmpFile);
        } else {
            try {
                jarFile = new JarFile(tmpFile);
            } catch (IOException e) {
                usage();
                throw new IllegalArgumentException("The argument '" + args[0] + "' is not a valid JAR file.", e);
            }
        }



        // Second arg = output dir
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        urlXML = cl.getResource(args[1]);
        if (urlXML == null) {
            throw new IllegalArgumentException("Resource '" + args[1] + "' not found in the classloader '" + cl + "'.");
        }

        referencePackageClassesMetadataMap = new HashMap<String, Map<String, ClassMetadata>>();
        packageClassesMetadataMap = new HashMap<String, Map<String, ClassMetadata>>();
        analyzerClassVisitor = new AnalyzerClassVisitor(packageClassesMetadataMap);

    }


    public void check() throws IOException {
        readXML(urlXML);
        if (jarFile != null) {
            analyzeJarFile();
        }
        if (allFiles != null) {
            analyzeDirectory();
        }
        SignatureResultSet rs = compare();
        if (rs.hasErrors()) {
            throw new IOException(rs.toString());
        }
    }

    private void addFilesFromDirectory(List<File> allFiles, File directory) {
        File[] files = directory.listFiles();
        for (File f : files) {
            if (f.isFile()) {
                allFiles.add(f);
            } else {
                addFilesFromDirectory(allFiles, f);
            }
        }
    }

    private void analyzeDirectory() throws IOException {
        ClassVisitor classVisitor = new SerialVersionUIDReader(analyzerClassVisitor);
        for (File f : allFiles) {
            if (f.getName().toLowerCase().endsWith(".class")) {
                InputStream inputStream = new FileInputStream(f);
                new ClassReader(inputStream).accept(classVisitor, 0);
                inputStream.close();
            }
        }
    }


    private void analyzeJarFile() throws IOException {
        ClassVisitor classVisitor = new SerialVersionUIDReader(analyzerClassVisitor);
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry jarEntry = entries.nextElement();
            if (jarEntry.getName().toLowerCase().endsWith(".class")) {
                InputStream inputStream = jarFile.getInputStream(jarEntry);
                new ClassReader(inputStream).accept(classVisitor, 0);
            }
        }
    }

    // compare
    private SignatureResultSet compare() {

        SignatureResultSet rs = new SignatureResultSet();

        Set<String> packagesOfFile = packageClassesMetadataMap.keySet();
        for (String packageName : packagesOfFile) {
            if (packageName.startsWith("org.ow2")) {
                continue;
            }
            System.out.println("Checking package '" + packageName + "'.");

            // reference = package loaded from XML
            Map<String, ClassMetadata> referenceClassMetadataMap = referencePackageClassesMetadataMap.get(packageName);

            // analyzed
            Map<String, ClassMetadata> analyzedClassMetadataMap = packageClassesMetadataMap.get(packageName);

            // Ignore it
            if (referenceClassMetadataMap == null) {
                referenceClassMetadataMap = new HashMap<String, ClassMetadata>();
            }
            
            int referenceSize = referenceClassMetadataMap.size();
            int analyzedSite = analyzedClassMetadataMap.size();

            // compare to each other the class
            // first check from reference
            Set<String> referenceClassKeys = referenceClassMetadataMap.keySet();
            for (String className : referenceClassKeys) {
                if (!analyzedClassMetadataMap.containsKey(className)) {
                    rs.addError(new ComparisonError("Missing class '" + className + "' in analyzed jar file '"
                            + tmpFile.getName() + "'."));
                }
            }

            // compare from analyzed to reference
            Set<String> analyzedClassKeys = analyzedClassMetadataMap.keySet();
            for (String className : analyzedClassKeys) {

                if (!referenceClassMetadataMap.containsKey(className)) {
                    rs.addError(new ComparisonError("The class '" + className + "' is present in the jar file '"
                            + tmpFile.getName() + "' but not in the reference."));
                }
            }

            if (referenceSize != analyzedSite) {
                rs.addError(new ComparisonError("Invalid package size"));
            }

            // As each class is present in the both packages, compare each of
            // them
            for (String className : referenceClassKeys) {

                ClassMetadata referenceClassMetadata = referenceClassMetadataMap.get(className);
                ClassMetadata analyzedClassMetadata = analyzedClassMetadataMap.get(className);
                if (analyzedClassMetadata == null) {
                    rs.addError(new ComparisonError("'" + className +"' is missing."));
                } else {
                    referenceClassMetadata.compare(analyzedClassMetadata, rs);
                }
            }

        }

        return rs;
    }

    // read XML and fill struct
    private void readXML(URL url) {
        // Parse the XML file and build all configuration process.
        Document xmlConfigurationDocument = null;
        try {
            xmlConfigurationDocument = DocumentParser.getDocument(url, false, null);
        } catch (DocumentParserException e) {
            throw new IllegalArgumentException("Cannot get a document on the given url '" + url + "'.", e);
        }

        // Get the root element
        Element rootElement = xmlConfigurationDocument.getDocumentElement();

        NodeList packagesList = rootElement.getElementsByTagNameNS(APICHECK_NS, PACKAGE_ELEMENT);

        // Loop on this list
        for (int i = 0; i < packagesList.getLength(); i++) {
            Element packageElement = (Element) packagesList.item(i);

            // Get name
            String packageName = XMLUtils.getAttributeValue(packageElement, "name");

            // Add package if not existing
            Map<String, ClassMetadata> classMetadataMap = referencePackageClassesMetadataMap.get(packageName);
            if (classMetadataMap == null) {
                classMetadataMap = new HashMap<String, ClassMetadata>();
                referencePackageClassesMetadataMap.put(packageName, classMetadataMap);
            }

            // Now analyze class element of package
            NodeList classesList = packageElement.getElementsByTagNameNS(APICHECK_NS, CLASS_ELEMENT);
            // Loop on this list
            for (int c = 0; c < classesList.getLength(); c++) {
                Element classElement = (Element) classesList.item(c);

                String className = XMLUtils.getAttributeValue(classElement, "name");
                String classSuperName = XMLUtils.getAttributeValue(classElement, "super-name");
                String classAccess = XMLUtils.getAttributeValue(classElement, "access");
                String classSignature = XMLUtils.getAttributeValue(classElement, "signature");
                String classUid = XMLUtils.getAttributeValue(classElement, "uid");

                // Interfaces
                String[] interfaces = null;
                NodeList interfacesList = classElement.getElementsByTagNameNS(APICHECK_NS, INTERFACE_ELEMENT);
                if (interfacesList.getLength() > 0) {
                    interfaces = new String[interfacesList.getLength()];
                    for (int j = 0; j < interfacesList.getLength(); j++) {
                        Element interfaceElement = (Element) interfacesList.item(j);
                        String interfaceName = XMLUtils.getAttributeValue(interfaceElement, "name");
                        interfaces[j] = interfaceName;
                    }
                }

                // build metadata
                ClassMetadata classMetadata = new ClassMetadata(Integer.valueOf(classAccess), className, classSignature,
                        classSuperName, interfaces);
                if (classUid != null) {
                    classMetadata.setUID(Long.valueOf(classUid));
                }

                // store
                classMetadataMap.put(className, classMetadata);

                // Analyze class sub elements
                // like methods
                NodeList methodsList = classElement.getElementsByTagNameNS(APICHECK_NS, METHOD_ELEMENT);
                for (int m = 0; m < methodsList.getLength(); m++) {
                    Element methodElement = (Element) methodsList.item(m);

                    String methodName = XMLUtils.getAttributeValue(methodElement, "name");
                    String methodDesc = XMLUtils.getAttributeValue(methodElement, "desc");
                    String methodAccess = XMLUtils.getAttributeValue(methodElement, "access");
                    String methodSignature = XMLUtils.getAttributeValue(methodElement, "signature");

                    // Exceptions
                    String[] methodExceptions = null;
                    NodeList methodExceptionsList = methodElement.getElementsByTagNameNS(APICHECK_NS, EXCEPTION_ELEMENT);

                    // no exceptions --> null value
                    if (methodExceptionsList.getLength() > 0) {
                        methodExceptions = new String[methodExceptionsList.getLength()];
                        for (int j = 0; j < methodExceptionsList.getLength(); j++) {
                            Element methodExceptionElement = (Element) methodExceptionsList.item(j);
                            String methodExceptionName = XMLUtils.getAttributeValue(methodExceptionElement, "name");
                            methodExceptions[j] = methodExceptionName;
                        }
                    }

                    MethodMetadata methodMetadata = new MethodMetadata(Integer.valueOf(methodAccess), methodName, methodDesc,
                            methodSignature, methodExceptions);
                    classMetadata.addMethodMetadata(methodMetadata);

                }

                // Now fields
                NodeList fieldsList = classElement.getElementsByTagNameNS(APICHECK_NS, FIELD_ELEMENT);
                for (int f = 0; f < fieldsList.getLength(); f++) {
                    Element fieldElement = (Element) fieldsList.item(f);

                    String fieldName = XMLUtils.getAttributeValue(fieldElement, "name");
                    String fieldDesc = XMLUtils.getAttributeValue(fieldElement, "desc");
                    String fieldAccess = XMLUtils.getAttributeValue(fieldElement, "access");
                    String fieldValue = XMLUtils.getAttributeValue(fieldElement, "value");
                    String fieldSignature = XMLUtils.getAttributeValue(fieldElement, "signature");

                    FieldMetadata fieldMetadata = new FieldMetadata(Integer.valueOf(fieldAccess), fieldName, fieldDesc,
                            fieldSignature, fieldValue);
                    classMetadata.addFieldMetadata(fieldMetadata);

                }

                // Now inner class
                NodeList innerClassesList = classElement.getElementsByTagNameNS(APICHECK_NS, INNER_CLASS_ELEMENT);
                for (int k = 0; k < innerClassesList.getLength(); k++) {
                    Element innerClassElement = (Element) innerClassesList.item(k);

                    String innerClassName = XMLUtils.getAttributeValue(innerClassElement, "name");
                    String innerClassOuterName = XMLUtils.getAttributeValue(innerClassElement, "outer-name");
                    String innerClassInnerName = XMLUtils.getAttributeValue(innerClassElement, "inner-name");
                    String innerClassAccess = XMLUtils.getAttributeValue(innerClassElement, "access");

                    InnerClassMetadata innerClassMetadata = new InnerClassMetadata(innerClassName, innerClassOuterName,
                            innerClassInnerName, Integer.valueOf(innerClassAccess));
                    classMetadata.addInnerClassMetadata(innerClassMetadata);
                }

                // Annotations
                NodeList annotationsList = classElement.getElementsByTagNameNS(APICHECK_NS, ANNOTATION_ELEMENT);
                for (int a = 0; a < annotationsList.getLength(); a++) {
                    Element annotationElement = (Element) annotationsList.item(a);

                    String annotationDesc = XMLUtils.getAttributeValue(annotationElement, "desc");
                    String annotationVisible = XMLUtils.getAttributeValue(annotationElement, "visible");

                    AnnotationMetadata annotationMetadata = new AnnotationMetadata(annotationDesc, Boolean
                            .valueOf(annotationVisible));
                    classMetadata.addAnnotationMetadata(annotationMetadata);

                    // enum-annotation
                    NodeList enumAnnotationsList = annotationElement.getElementsByTagNameNS(APICHECK_NS, ENUM_ANNOTATION_ELEMENT);
                    for (int e = 0; e < enumAnnotationsList.getLength(); e++) {
                        Element enumAnnotationElement = (Element) enumAnnotationsList.item(e);

                        String enumAnnotationName = XMLUtils.getAttributeValue(enumAnnotationElement, "name");
                        String enumAnnotationDesc = XMLUtils.getAttributeValue(enumAnnotationElement, "desc");
                        String enumAnnotationValue = XMLUtils.getAttributeValue(enumAnnotationElement, "value");

                        EnumAnnotationMetadata enumAnnotationMetadata = new EnumAnnotationMetadata(enumAnnotationName,
                                enumAnnotationDesc, enumAnnotationValue);
                        annotationMetadata.addEnumAnnotationMetadata(enumAnnotationMetadata);
                    }
                }

            }

        }
        System.out.println("packages = " + referencePackageClassesMetadataMap.keySet());

    }

    /**
     * @param args
     */
    public static void main(String[] args) throws IOException {
        String[] newArgs = new String[2];
        newArgs[0] = "/home/test/workspace/ow2-spec/ee/ejb-3.0/target/ow2-ejb-3.0-spec.jar";
        //"ejb-3.0/target/ow2-ejb-3.0-spec.jar"
        newArgs[1] = "/tmp/report";
        SignatureCompare signatureCompare = new SignatureCompare(newArgs);
        signatureCompare.check();


    }

    public void usage() {
        System.out.println("Usage : SignatureCompare <Jar file to check> <input directory>");
        System.out.println("Usage : example : SignatureCompare /tmp/ejb.jar /tmp/data");
    }
}
