/**************************************************************************************
 * Copyright (c) Jonas Bonér, Alexandre Vasseur. All rights reserved.                 *
 * http://aspectwerkz.codehaus.org                                                    *
 * ---------------------------------------------------------------------------------- *
 * The software in this package is published under the terms of the LGPL license      *
 * a copy of which has been included with this distribution in the license.txt file.  *
 **************************************************************************************/
package org.codehaus.aspectwerkz.definition;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

import org.codehaus.aspectwerkz.ContextClassLoader;
import org.codehaus.aspectwerkz.DeploymentModel;
import org.codehaus.aspectwerkz.util.Strings;
import org.codehaus.aspectwerkz.definition.attribute.AspectAttributeParser;
import org.codehaus.aspectwerkz.definition.attribute.AttributeParser;
import org.codehaus.aspectwerkz.exception.DefinitionException;
import org.codehaus.aspectwerkz.exception.WrappedRuntimeException;
import org.codehaus.aspectwerkz.transform.TransformationUtil;

import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;

/**
 * Parses the attribdef XML definition using <tt>dom4j</tt>.
 *
 * @author <a href="mailto:jboner@codehaus.org">Jonas Bonér</a>
 * @author <a href="mailto:alex@gnilux.com">Alexandre Vasseur</a>
 */
public class DocumentParser {

    private static final String JAVA_VENDOR = "java.vendor";
    private static final String JAVA_VM_VENDOR = "java.vm.vendor";
    private static final String VENDOR_BEA = "BEA";

    /**
     * The attribute parser, retrieves the custom attributes from the bytecode of the classes.
     *
     * @TODO: should use factory, to be able to support othere implementations, f.e. JSR-175
     */
    private static final AttributeParser s_attributeParser = new AspectAttributeParser();

    /**
     * Parses aspect class names.
     *
     * @param document the defintion as a document
     * @return the aspect class names
     */
    public static List parseAspectClassNames(final Document document) {
        final List aspectClassNames = new ArrayList();

        for (Iterator it1 = document.getRootElement().elementIterator("system"); it1.hasNext();) {
            Element system = (Element)it1.next();

            final String packageName = getBasePackage(system);
            for (Iterator it11 = system.elementIterator("aspect"); it11.hasNext();) {

                String className = null;
                Element aspect = (Element)it11.next();
                for (Iterator it2 = aspect.attributeIterator(); it2.hasNext();) {
                    Attribute attribute = (Attribute)it2.next();

                    final String name = attribute.getName().trim();
                    final String value = attribute.getValue().trim();
                    if (name.equalsIgnoreCase("class")) {
                        className = value;
                    }
                }
                String aspectClassName = packageName + className;
                aspectClassNames.add(aspectClassName);
            }

            for (Iterator it11 = system.elementIterator("package"); it11.hasNext();) {
                final Element packageElement = ((Element)it11.next());
                final String packageName1 = getPackage(packageElement);

                for (Iterator it12 = packageElement.elementIterator("aspect"); it12.hasNext();) {

                    String className = null;
                    Element aspect = (Element)it12.next();
                    for (Iterator it2 = aspect.attributeIterator(); it2.hasNext();) {
                        Attribute attribute = (Attribute)it2.next();

                        final String name = attribute.getName().trim();
                        final String value = attribute.getValue().trim();
                        if (name.equalsIgnoreCase("class")) {
                            className = value;
                        }
                    }
                    String aspectClassName = packageName1 + className;
                    aspectClassNames.add(aspectClassName);
                }
            }
        }
        return aspectClassNames;
    }

    /**
     * Parses the definition DOM document.
     *
     * @param loader   the current class loader
     * @param document the defintion as a document
     * @return the definitions
     */
    public static List parse(final ClassLoader loader, final Document document) {
        final Element root = document.getRootElement();

        // parse the transformation scopes
        return parseSystemElements(loader, root);
    }

    /**
     * Parses the <tt>system</tt> elements.
     *
     * @param loader        the current class loader
     * @param systemElement the system element
     * @param basePackage   the base package
     * @return the definition for the system
     */
    public static SystemDefinition parseSystemElement(
            final ClassLoader loader,
            final Element systemElement,
            final String basePackage) {

        final SystemDefinition definition = new SystemDefinition();

        String uuid = systemElement.attributeValue("id");
        if (uuid == null || uuid.equals("")) {
            throw new DefinitionException("system UUID must be specified");
        }
        definition.setUuid(uuid);

        // parse the include, exclude and prepare elements
        parseIncludePackageElements(systemElement, definition, basePackage);
        parseExcludePackageElements(systemElement, definition, basePackage);
        parsePrepareElements(systemElement, definition, basePackage);

        boolean hasDef = false;

        // parse without package elements
        if (parseAspectElements(loader, systemElement, definition, basePackage)) {
            hasDef = true;
        }
        // parse with package elements
        if (parsePackageElements(loader, systemElement, definition, basePackage)) {
            hasDef = true;
        }
        if (hasDef) {
            return definition;
        }
        else {
            return null;
        }
    }

    /**
     * Parses the definition DOM document.
     *
     * @param loader        the current class loader
     * @param systemElement the system element
     * @param definition    the definition
     * @param basePackage   the base package
     * @return flag that says if we have a definition of this kind or not
     */
    private static boolean parsePackageElements(
            final ClassLoader loader,
            final Element systemElement,
            final SystemDefinition definition,
            final String basePackage) {
        boolean hasDef = false;
        for (Iterator it1 = systemElement.elementIterator("package"); it1.hasNext();) {
            final Element packageElement = ((Element)it1.next());
            final String packageName = basePackage + getPackage(packageElement);

            if (parseAspectElements(loader, packageElement, definition, packageName)) {
                hasDef = true;
            }
        }
        return hasDef;
    }

    /**
     * Parses the <tt>aspect</tt> elements.
     *
     * @param loader        the current class loader
     * @param systemElement the system element
     * @param definition    the definition object
     * @param packageName   the package name
     * @return flag that says if we have a definition of this kind or not
     * @TODO: Passing in the CL all the way down here, but uses the context CL anyway. Will not work with AOPC.
     */
    private static boolean parseAspectElements(
            final ClassLoader loader,
            final Element systemElement,
            final SystemDefinition definition,
            final String packageName) {

        boolean hasDef = false;
        for (Iterator it1 = systemElement.elementIterator("aspect"); it1.hasNext();) {

            String aspectName = null;
            String className = null;
            String deploymentModel = null;
            Element aspect = (Element)it1.next();
            for (Iterator it2 = aspect.attributeIterator(); it2.hasNext();) {
                Attribute attribute = (Attribute)it2.next();

                final String name = attribute.getName().trim();
                final String value = attribute.getValue().trim();
                if (name.equalsIgnoreCase("class")) {
                    className = value;
                }
                else if (name.equalsIgnoreCase("deployment-model")) {
                    deploymentModel = value;
                }
                else if (name.equalsIgnoreCase("name")) {
                    aspectName = value;
                }
            }
            String aspectClassName = packageName + className;
            if (aspectName == null) {
                aspectName = aspectClassName;
            }

            // create the aspect definition
            AspectDefinition aspectDef = new AspectDefinition(aspectName, aspectClassName);

            Class aspectClass = null;
            try {
                aspectClass = loadAspectClass(loader, aspectClassName);
            } catch (Exception e) {
                System.err.println("<AspectWerkz> <WARN> could not load " + aspectClassName + " from " + loader);
                continue;
            }

            parsePointcutElements(aspect, aspectDef);//needed to support undefined named pointcut in Attributes AW-152
            s_attributeParser.parse(aspectClass, aspectDef, definition);

            // XML definition settings always overrides attribute definition settings
            aspectDef.setDeploymentModel(deploymentModel);
            aspectDef.setName(aspectName);

            // parse the aspect info
            parseParameterElements(aspect, definition, aspectDef);
            parsePointcutElements(aspect, aspectDef);//reparse pc for XML override (AW-152)
            parseAdviceElements(loader, aspect, aspectDef, aspectClass);
            parseIntroductionElements(loader, aspect, aspectDef, aspectClass, packageName);

            // register introduction of aspect into the system (?? optim for TF ?) TODO check why
            for (Iterator mixins = aspectDef.getInterfaceIntroductions().iterator(); mixins.hasNext();) {
                definition.addInterfaceIntroductionDefinition((InterfaceIntroductionDefinition)mixins.next());
            }
            for (Iterator mixins = aspectDef.getIntroductions().iterator(); mixins.hasNext();) {
                definition.addIntroductionDefinition((IntroductionDefinition)mixins.next());
            }

            definition.addAspect(aspectDef);
            hasDef = true;
        }
        return hasDef;
    }

    /**
     * Loads the aspect class.
     *
     * @param loader          the class loader
     * @param aspectClassName the name of the class implementing the aspect
     * @return the class
     */
    private static Class loadAspectClass(final ClassLoader loader, final String aspectClassName) {
        Class aspectClass;
        try {
            aspectClass = ContextClassLoader.loadClass(aspectClassName);
        }
        catch (Exception e) {
            e.printStackTrace();
            throw new WrappedRuntimeException(e);
        }
        return aspectClass;
    }

    /**
     * Parses the aspectElement parameters.
     * <p/>
     * TODO: should perhaps move the parameters to the aspect def instead of the system def
     *
     * @param aspectElement the aspect element
     * @param def           the system definition
     * @param aspectDef     the aspect def
     */
    private static void parseParameterElements(
            final Element aspectElement,
            final SystemDefinition def,
            final AspectDefinition aspectDef) {
        for (Iterator it2 = aspectElement.elementIterator(); it2.hasNext();) {
            Element parameterElement = (Element)it2.next();
            if (parameterElement.getName().trim().equals("param")) {
                def.addParameter(
                        aspectDef.getName(),
                        parameterElement.attributeValue("name"),
                        parameterElement.attributeValue("value")
                );
            }
        }
    }

    /**
     * Parses the pointcuts.
     *
     * @param aspectElement the aspect element
     * @param aspectDef     the system definition
     */
    private static void parsePointcutElements(final Element aspectElement, final AspectDefinition aspectDef) {
        for (Iterator it2 = aspectElement.elementIterator(); it2.hasNext();) {
            Element pointcutElement = (Element)it2.next();
            if (pointcutElement.getName().trim().equals("pointcut")) {
                String name = pointcutElement.attributeValue("name");
                String expression = pointcutElement.attributeValue("expression");
                DefinitionParserHelper.createAndAddPointcutDefToAspectDef(name, expression, aspectDef);
            }
        }
    }

    /**
     * Parses the advices.
     *
     * @param loader        the current class loader
     * @param aspectElement the aspect element
     * @param aspectDef     the system definition
     * @param aspectClass   the aspect class
     */
    private static void parseAdviceElements(
            final ClassLoader loader,
            final Element aspectElement,
            final AspectDefinition aspectDef,
            final Class aspectClass) {

        List methodList = TransformationUtil.createSortedMethodList(aspectClass);

        for (Iterator it2 = aspectElement.elementIterator(); it2.hasNext();) {
            Element adviceElement = (Element)it2.next();
            if (adviceElement.getName().trim().equals("advice")) {
                String name = adviceElement.attributeValue("name");
                String type = adviceElement.attributeValue("type");
                String bindTo = adviceElement.attributeValue("bind-to");

                String adviceName = aspectClass.getName() + '.' + name;

                int methodIndex = 0;
                Method method = null;
                for (Iterator it3 = methodList.iterator(); it3.hasNext(); methodIndex++) {
                    method = (Method)it3.next();
                    if (method.getName().equals(name)) {
                        break;
                    }
                }
                createAndAddAdviceDefsToAspectDef(type, bindTo, adviceName, method, methodIndex, aspectDef);

                for (Iterator it1 = adviceElement.elementIterator("bind-to"); it1.hasNext();) {
                    Element bindToElement = (Element)it1.next();
                    String pointcut = bindToElement.attributeValue("pointcut");

                    createAndAddAdviceDefsToAspectDef(type, pointcut, adviceName, method, methodIndex, aspectDef);
                }
            }
        }
    }

    /**
     * Parses the introduction.
     *
     * @param loader        the current class loader
     * @param aspectElement the aspect element
     * @param aspectDef     the system definition
     * @param aspectClass   the aspect class
     * @param packageName
     */
    private static void parseIntroductionElements(
            final ClassLoader loader,
            final Element aspectElement,
            final AspectDefinition aspectDef,
            final Class aspectClass,
            final String packageName) {

        for (Iterator it2 = aspectElement.elementIterator(); it2.hasNext();) {
            Element introduceElement = (Element)it2.next();
            if (introduceElement.getName().trim().equals("introduce")) {
                String klass = introduceElement.attributeValue("class");
                String name = introduceElement.attributeValue("name");
                String bindTo = introduceElement.attributeValue("bind-to");
                String deploymentModel = introduceElement.attributeValue("deployment-model");

                // deployment-model defaults to perJVM
                if (deploymentModel == null || deploymentModel.length() <= 0) {
                    deploymentModel = DeploymentModel.getDeploymentModelAsString(DeploymentModel.PER_JVM);
                }

                // default name = FQN
                if (name == null || name.length() <= 0) {
                    name = packageName + klass;
                }

                // load the mixin to determine if it is a pure interface introduction
                Class mixin = null;
                try {
                    mixin = aspectClass.getClassLoader().loadClass(packageName + klass);
                }
                catch (ClassNotFoundException e) {
                    throw new DefinitionException(
                            "could not find mixin implementation: "
                            + packageName + klass + " " + e.getMessage()
                    );
                }

                // pure interface introduction
                if (mixin.isInterface()) {
                    DefinitionParserHelper.createAndAddInterfaceIntroductionDefToAspectDef(
                            bindTo, name, packageName + klass, aspectDef
                    );
                    // handles nested "bind-to" elements
                    for (Iterator it1 = introduceElement.elementIterator("bind-to"); it1.hasNext();) {
                        Element bindToElement = (Element)it1.next();
                        String pointcut = bindToElement.attributeValue("pointcut");
                        DefinitionParserHelper.createAndAddInterfaceIntroductionDefToAspectDef(
                                pointcut, name, packageName + klass, aspectDef
                        );
                    }
                }
                else {
                    // mixin introduction
                    Class[] introduced = mixin.getInterfaces();
                    String[] introducedInterfaceNames = new String[introduced.length];
                    for (int i = 0; i < introduced.length; i++) {
                        introducedInterfaceNames[i] = introduced[i].getName();
                    }
                    Method[] methods = (Method[])TransformationUtil.createSortedMethodList(mixin).toArray(
                            new Method[]{}
                    );
                    DefinitionParserHelper.createAndAddIntroductionDefToAspectDef(
                            bindTo, name, introducedInterfaceNames, methods, deploymentModel, aspectDef
                    );
                    // handles nested "bind-to" elements
                    for (Iterator it1 = introduceElement.elementIterator("bind-to"); it1.hasNext();) {
                        Element bindToElement = (Element)it1.next();
                        String pointcut = bindToElement.attributeValue("pointcut");
                        DefinitionParserHelper.createAndAddIntroductionDefToAspectDef(
                                pointcut, name, introducedInterfaceNames, methods, deploymentModel, aspectDef
                        );
                    }
                }
            }
        }
    }

    /**
     * Creates the advice definitions and adds them to the aspect definition.
     *
     * @param type        the type of advice
     * @param bindTo      the pointcut expresion
     * @param name        the name of the advice
     * @param method      the method implementing the advice
     * @param methodIndex the method index
     * @param aspectDef   the aspect definition
     */
    private static void createAndAddAdviceDefsToAspectDef(
            final String type,
            final String bindTo,
            final String name,
            final Method method,
            final int methodIndex,
            final AspectDefinition aspectDef) {
        if (type.equalsIgnoreCase("around")) {
            DefinitionParserHelper.createAndAddAroundAdviceDefToAspectDef(
                    bindTo, name, aspectDef.getName(), aspectDef.getClassName(),
                    method, methodIndex, aspectDef
            );
        }
        else if (type.equalsIgnoreCase("before")) {
            DefinitionParserHelper.createAndAddBeforeAdviceDefToAspectDef(
                    bindTo, name, aspectDef.getName(), aspectDef.getClassName(),
                    method, methodIndex, aspectDef
            );
        }
        else if (type.equalsIgnoreCase("after")) {
            DefinitionParserHelper.createAndAddAfterAdviceDefToAspectDef(
                    bindTo, name, aspectDef.getName(), aspectDef.getClassName(),
                    method, methodIndex, aspectDef
            );
        }
        else if (type.equalsIgnoreCase("afterFinally")) {
            // TODO: impl. afterFinally
        }
        else if (type.equalsIgnoreCase("afterReturning")) {
            // TODO: impl. afterReturning
        }
        else if (type.equalsIgnoreCase("afterThrowing")) {
            // TODO: impl. afterThrowing
        }
    }

    /**
     * Retrieves and returns the package.
     *
     * @param packageElement the package element
     * @return the package as a string ending with DOT, or empty string
     */
    private static String getPackage(final Element packageElement) {
        String packageName = "";
        for (Iterator it2 = packageElement.attributeIterator(); it2.hasNext();) {
            Attribute attribute = (Attribute)it2.next();
            if (attribute.getName().trim().equalsIgnoreCase("name")) {
                packageName = attribute.getValue().trim();
                if (packageName.endsWith(".*")) {
                    packageName = packageName.substring(0, packageName.length() - 1);
                }
                else if (packageName.endsWith(".")) {
                    ;// skip
                }
                else {
                    packageName += ".";
                }
                break;
            }
            else {
                continue;
            }
        }
        return packageName;
    }

    /**
     * Parses the <tt>include</tt> elements.
     *
     * @param root        the root element
     * @param definition  the definition object
     * @param packageName the package name
     */
    private static void parseIncludePackageElements(
            final Element root,
            final SystemDefinition definition,
            final String packageName) {
        for (Iterator it1 = root.elementIterator("include"); it1.hasNext();) {
            String includePackage = "";
            Element includeElement = (Element)it1.next();
            for (Iterator it2 = includeElement.attributeIterator(); it2.hasNext();) {
                Attribute attribute = (Attribute)it2.next();
                if (attribute.getName().trim().equalsIgnoreCase("package")) {

                    // handle base package
                    if (packageName.endsWith(".*")) {
                        includePackage = packageName.substring(0, packageName.length() - 2);
                    }
                    else if (packageName.endsWith(".")) {
                        includePackage = packageName.substring(0, packageName.length() - 1);
                    }

                    // handle exclude package
                    includePackage = packageName + attribute.getValue().trim();
                    if (includePackage.endsWith(".*")) {
                        includePackage = includePackage.substring(0, includePackage.length() - 2);
                    }
                    else if (includePackage.endsWith(".")) {
                        includePackage = includePackage.substring(0, includePackage.length() - 1);
                    }
                    break;
                }
                else {
                    continue;
                }
            }
            if (includePackage.length() != 0) {
                definition.addIncludePackage(includePackage);
            }
        }
    }

    /**
     * Parses the <tt>exclude</tt> elements.
     *
     * @param root        the root element
     * @param definition  the definition object
     * @param packageName the package name
     */
    private static void parseExcludePackageElements(
            final Element root,
            final SystemDefinition definition,
            final String packageName) {
        for (Iterator it1 = root.elementIterator("exclude"); it1.hasNext();) {
            String excludePackage = "";
            Element excludeElement = (Element)it1.next();
            for (Iterator it2 = excludeElement.attributeIterator(); it2.hasNext();) {
                Attribute attribute = (Attribute)it2.next();
                if (attribute.getName().trim().equalsIgnoreCase("package")) {

                    // handle base package
                    if (packageName.endsWith(".*")) {
                        excludePackage = packageName.substring(0, packageName.length() - 2);
                    }
                    else if (packageName.endsWith(".")) {
                        excludePackage = packageName.substring(0, packageName.length() - 1);
                    }

                    // handle exclude package
                    excludePackage = packageName + attribute.getValue().trim();
                    if (excludePackage.endsWith(".*")) {
                        excludePackage = excludePackage.substring(0, excludePackage.length() - 2);
                    }
                    else if (excludePackage.endsWith(".")) {
                        excludePackage = excludePackage.substring(0, excludePackage.length() - 1);
                    }
                    break;
                }
                else {
                    continue;
                }
            }
            if (excludePackage.length() != 0) {
                definition.addExcludePackage(excludePackage);
            }
        }
    }

    /**
     * Parses the <tt>prepare</tt> elements.
     *
     * @param root        the root element
     * @param definition  the definition object
     * @param packageName the base package name
     */
    public static void parsePrepareElements(
            final Element root,
            final SystemDefinition definition,
            final String packageName) {
        for (Iterator it1 = root.elementIterator("prepare"); it1.hasNext();) {
            String preparePackage = "";
            Element prepareElement = (Element)it1.next();
            for (Iterator it2 = prepareElement.attributeIterator(); it2.hasNext();) {
                Attribute attribute = (Attribute)it2.next();
                if (attribute.getName().trim().equals("package")) {

                    // handle base package
                    if (packageName.endsWith(".*")) {
                        preparePackage = packageName.substring(0, packageName.length() - 2);
                    }
                    else if (packageName.endsWith(".")) {
                        preparePackage = packageName.substring(0, packageName.length() - 1);
                    }

                    // handle prepare package
                    preparePackage = packageName + attribute.getValue().trim();
                    if (preparePackage.endsWith(".*")) {
                        preparePackage = preparePackage.substring(0, preparePackage.length() - 2);
                    }
                    else if (preparePackage.endsWith(".")) {
                        preparePackage = preparePackage.substring(0, preparePackage.length() - 1);
                    }
                    break;
                }
                else {
                    continue;
                }
            }
            if (preparePackage.length() != 0) {
                definition.addPreparePackage(preparePackage);
            }
        }
    }

    /**
     * Parses the <tt>system</tt> elements.
     *
     * @param loader the current class loader
     * @param root   the root element
     */
    private static List parseSystemElements(final ClassLoader loader, final Element root) {
        final List systemDefs = new ArrayList();

        for (Iterator it1 = root.elementIterator("system"); it1.hasNext();) {
            Element system = (Element)it1.next();
            SystemDefinition definition = parseSystemElement(loader, system, getBasePackage(system));
            if (definition != null) {
                systemDefs.add(definition);
            }
        }

        return systemDefs;
    }

    /**
     * Retrieves and returns the base package for a system element
     *
     * @param system a system element
     * @return the base package
     */
    private static String getBasePackage(final Element system) {
        String basePackage = "";
        for (Iterator it2 = system.attributeIterator(); it2.hasNext();) {
            Attribute attribute = (Attribute)it2.next();
            if (attribute.getName().trim().equalsIgnoreCase("base-package")) {
                basePackage = attribute.getValue().trim();
                if (basePackage.endsWith(".*")) {
                    basePackage = basePackage.substring(0, basePackage.length() - 1);
                }
                else if (basePackage.endsWith(".")) {
                    ; // skip
                }
                else {
                    basePackage += ".";
                }
                break;
            }
            else {
                continue;
            }
        }
        return basePackage;
    }
}
