/**************************************************************************************
 * 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.aspect.management;

import gnu.trove.TIntObjectHashMap;
import gnu.trove.TObjectIntHashMap;
import org.codehaus.aspectwerkz.ConstructorTuple;
import org.codehaus.aspectwerkz.CrossCuttingInfo;
import org.codehaus.aspectwerkz.AdviceInfo;
import org.codehaus.aspectwerkz.MethodTuple;
import org.codehaus.aspectwerkz.Mixin;
import org.codehaus.aspectwerkz.aspect.AspectContainer;
import org.codehaus.aspectwerkz.aspect.Introduction;
import org.codehaus.aspectwerkz.aspect.IntroductionContainer;
import org.codehaus.aspectwerkz.definition.AdviceDefinition;
import org.codehaus.aspectwerkz.definition.IntroductionDefinition;
import org.codehaus.aspectwerkz.definition.StartupManager;
import org.codehaus.aspectwerkz.definition.SystemDefinition;
import org.codehaus.aspectwerkz.exception.DefinitionException;
import org.codehaus.aspectwerkz.exception.WrappedRuntimeException;
import org.codehaus.aspectwerkz.expression.ExpressionContext;
import org.codehaus.aspectwerkz.transform.ReflectHelper;
import org.codehaus.aspectwerkz.transform.TransformationUtil;
import org.codehaus.aspectwerkz.util.SequencedHashMap;
import org.codehaus.aspectwerkz.util.Strings;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * Stores the aspects, advices, pointcuts etc. Manages the method, advice and aspect indexing.
 * 
 * @author <a href="mailto:jboner@codehaus.org">Jonas BonŽr </a>
 * @author <a href="mailto:alex@gnilux.com">Alexandre Vasseur </a>
 * @TODO Use hashes, aspect=>hashcode for class advice=>hashcode for method signature
 * @TODO Store references to all join points that uses advices from a certain aspect [aspectKey=>joinPoints]
 * @TODO Map all aspects to a key, meaning have a key that maps to a data structure that contains full info about the
 *       aspect and all its advice methods. [aspectKey=>aspectDataStructure].
 */
public class AspectRegistry {
    /**
     * Holds references to the methods to the advised classes in the system.
     */
    private final static Map s_methods = new HashMap(); //WEAK

    /**
     * Holds references to the fields to the advised classes in the system.
     */
    private final static Map s_fields = new HashMap();

    /**
     * Holds references to all the the advised constructors in the system, maps the target Class to a sorted list of all
     * the advised constructors in the class.
     */
    private final static Map s_constructors = new HashMap();

    /**
     * The AspectManager for the system.
     */
    private final AspectManager m_aspectManager;

    /**
     * The definition.
     */
    private final SystemDefinition m_definition;

    /**
     * Marks the system as initialized.
     */
    private boolean m_initialized = false;

    /**
     * Sorted map with PointcutManager instance containing the pointcut instance the aspect, mapped to its name (the
     * name of the class implementing the aspect).
     */
    private final Map m_pointcutManagerMap = new SequencedHashMap();

    /**
     * Holds the indexes for the aspects, maps the aspect name to the index for the aspect.
     */
    private final TObjectIntHashMap m_aspectIndexes = new TObjectIntHashMap();

    /**
     * Holds the index (a tuple of the aspect index and the advice index) for the advices, mapped to its name
     * ([fullyQualifiedClassName].[methodName]).
     */
    private final Map m_adviceIndexes = new HashMap();

    /**
     * An array with all the the aspect containers in the system.
     */
    private AspectContainer[] m_aspectContainers = new AspectContainer[0];

    /**
     * An array of all the mixins in the system, each nested class in aspect has its own index.
     */
    private Mixin[] m_mixins = new Mixin[0];

    /**
     * Creates a new aspect registry.
     * 
     * @param aspectManager the system aspectManager
     * @param definition the system definition
     */
    public AspectRegistry(final AspectManager aspectManager, final SystemDefinition definition) {
        m_aspectManager = aspectManager;
        m_definition = definition;
    }

    /**
     * Initializes the aspect registry. The initialization needs to be separated fromt he construction of the registry,
     * and is triggered by the runtime system.
     */
    public void initialize() {
        synchronized (this) {
            if (m_initialized) {
                return;
            }
            m_initialized = true;
            StartupManager.initializeSystem(m_aspectManager, m_definition);
        }
    }

    /**
     * Registers a new aspect.
     * 
     * @param aspectContainer the aspectContainer for the aspect to register
     * @param pointcutManager the pointcut manager
     */
    public void register(final AspectContainer aspectContainer, final PointcutManager pointcutManager) {
        if (aspectContainer == null) {
            throw new IllegalArgumentException("aspect aspectContainer can not be null");
        }
        if (pointcutManager == null) {
            throw new IllegalArgumentException("pointcut manager can not be null");
        }
        synchronized (m_aspectContainers) {
            synchronized (m_aspectIndexes) {
                synchronized (m_adviceIndexes) {
                    synchronized (m_mixins) {
                        synchronized (m_pointcutManagerMap) {
                            try {
                                CrossCuttingInfo crossCuttingInfo = aspectContainer.getCrossCuttingInfo();
                                m_pointcutManagerMap.put(crossCuttingInfo.getName(), pointcutManager);
                                final int indexAspect = m_aspectContainers.length + 1;
                                m_aspectIndexes.put(crossCuttingInfo.getName(), indexAspect);
                                final Object[] tmpAspects = new Object[m_aspectContainers.length + 1];
                                java.lang.System.arraycopy(
                                    m_aspectContainers,
                                    0,
                                    tmpAspects,
                                    0,
                                    m_aspectContainers.length);
                                tmpAspects[m_aspectContainers.length] = aspectContainer;
                                m_aspectContainers = new AspectContainer[m_aspectContainers.length + 1];
                                java.lang.System.arraycopy(tmpAspects, 0, m_aspectContainers, 0, tmpAspects.length);

                                // retrieve a sorted advices list => matches the sorted method list
                                // in the
                                // aspectContainer
                                List advices = crossCuttingInfo.getAspectDefinition().getAllAdvices();
                                for (Iterator it = advices.iterator(); it.hasNext();) {
                                    final AdviceDefinition adviceDef = (AdviceDefinition) it.next();
                                    AdviceInfo tuple = new AdviceInfo(
                                        indexAspect,
                                        adviceDef.getMethodIndex(),
                                        m_aspectManager);

                                    //prefix AdviceName with AspectName to allow AspectReuse
                                    m_adviceIndexes.put(crossCuttingInfo.getName() + "/" + adviceDef.getName(), tuple);
                                }

                                // mixins
                                List introductions = crossCuttingInfo.getAspectDefinition().getIntroductions();

                                for (Iterator it = introductions.iterator(); it.hasNext();) {
                                    IntroductionDefinition introDef = (IntroductionDefinition) it.next();

                                    // load default mixinPrototype impl from the aspect which
                                    // defines it
                                    Class defaultImplClass = crossCuttingInfo.getAspectClass().getClassLoader()
                                            .loadClass(introDef.getName());

                                    Introduction mixinPrototype = new Introduction(
                                        introDef.getName(),
                                        defaultImplClass,
                                        crossCuttingInfo,
                                        introDef);
                                    IntroductionContainer introductionContainer = new IntroductionContainer(
                                        mixinPrototype,
                                        aspectContainer);
                                    aspectContainer.addIntroductionContainer(introDef.getName(), introductionContainer);

                                    // prepare the aspectContainer
                                    mixinPrototype.setContainer(introductionContainer);
                                    final Mixin[] tmpMixins = new Mixin[m_mixins.length + 1];
                                    java.lang.System.arraycopy(m_mixins, 0, tmpMixins, 0, m_mixins.length);
                                    tmpMixins[m_mixins.length] = mixinPrototype;
                                    m_mixins = new Mixin[m_mixins.length + 1];
                                    java.lang.System.arraycopy(tmpMixins, 0, m_mixins, 0, tmpMixins.length);
                                }
                            } catch (Exception e) {
                                e.printStackTrace();
                                throw new DefinitionException("could not register aspect ["
                                    + aspectContainer.getCrossCuttingInfo().getName()
                                    + "] due to: "
                                    + e.toString());
                            }
                            if (m_aspectContainers.length != m_aspectIndexes.size()) {
                                throw new IllegalStateException("aspect indexing out of synch");
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Retrieves a specific aspect container based on index.
     * 
     * @param index the index of the aspect
     * @return the aspect container
     */
    public AspectContainer getAspectContainer(final int index) {
        AspectContainer aspect;
        try {
            aspect = m_aspectContainers[index - 1];
        } catch (Throwable e) {
            initialize();
            try {
                aspect = m_aspectContainers[index - 1];
            } catch (ArrayIndexOutOfBoundsException e1) {
                throw new DefinitionException("no aspect with index " + index);
            }
        }
        return aspect;
    }

    /**
     * Returns the aspect container for a specific name.
     * 
     * @param name the name of the aspect
     * @return the the aspect container
     */
    public AspectContainer getAspectContainer(final String name) {
        AspectContainer container;
        try {
            container = m_aspectContainers[m_aspectIndexes.get(name) - 1];
        } catch (Throwable e1) {
            initialize();
            try {
                container = m_aspectContainers[m_aspectIndexes.get(name) - 1];
            } catch (ArrayIndexOutOfBoundsException e2) {
                throw new DefinitionException("container for cross-cutting class ["
                    + name
                    + "] is not properly defined");
            }
        }
        return container;
    }

    /**
     * Returns the aspect for a specific name, deployed as perJVM.
     * 
     * @param name the name of the aspect
     * @return the the aspect
     */
    public CrossCuttingInfo getCrossCuttingInfo(final String name) {
        return getAspectContainer(name).getCrossCuttingInfo();
    }

    /**
     * Retrieves a specific mixin based on its index.
     * 
     * @param index the index of the introduction (aspect in this case)
     * @return the the mixin (aspect in this case)
     */
    public Mixin getMixin(final int index) {
        Mixin mixin;
        try {
            mixin = m_mixins[index - 1];
        } catch (Throwable e1) {
            initialize();
            try {
                mixin = m_mixins[index - 1];
            } catch (ArrayIndexOutOfBoundsException e2) {
                throw new DefinitionException("no mixin with index " + index);
            }
        }
        return mixin;
    }

    /**
     * Returns the mixin implementation for a specific name.
     * 
     * @param name the name of the introduction (aspect in this case)
     * @return the the mixin (aspect in this case)
     */
    public Mixin getMixin(final String name) {
        if (name == null) {
            throw new IllegalArgumentException("introduction name can not be null");
        }
        Mixin introduction;
        try {
            introduction = m_mixins[m_definition.getMixinIndexByName(name) - 1];
        } catch (Throwable e1) {
            initialize();
            try {
                introduction = m_mixins[m_definition.getMixinIndexByName(name) - 1];
            } catch (ArrayIndexOutOfBoundsException e2) {
                throw new DefinitionException("no introduction with name " + name);
            }
        }
        return introduction;
    }

    /**
     * Returns the index for a specific name to aspect mapping.
     * 
     * @param name the name of the aspect
     * @return the index of the aspect
     */
    public int getAspectIndexFor(final String name) {
        if (name == null) {
            throw new IllegalArgumentException("aspect name can not be null");
        }
        final int index = m_aspectIndexes.get(name);
        if (index == 0) {
            throw new DefinitionException("aspect " + name + " is not properly defined");
        }
        return index;
    }

    /**
     * Returns the index for a specific name to advice mapping.
     * 
     * @param name the name of the advice
     * @return the index of the advice
     */
    public AdviceInfo getAdviceIndexFor(final String name) {
        if (name == null) {
            throw new IllegalArgumentException("advice name can not be null");
        }
        final AdviceInfo index = (AdviceInfo) m_adviceIndexes.get(name);
        if (index == null) {
            throw new DefinitionException("advice " + name + " is not properly defined");
        }
        return index;
    }

    /**
     * Returns the pointcut managers for the name specified.
     * 
     * @param name the name of the aspect
     * @return the pointcut manager
     */
    public PointcutManager getPointcutManager(final String name) {
        if (name == null) {
            throw new IllegalArgumentException("aspect name can not be null");
        }
        if (m_pointcutManagerMap.containsKey(name)) {
            return (PointcutManager) m_pointcutManagerMap.get(name);
        } else {
            initialize();
            if (m_pointcutManagerMap.containsKey(name)) {
                return (PointcutManager) m_pointcutManagerMap.get(name);
            } else {
                throw new DefinitionException("aspect " + name + " is not properly defined");
            }
        }
    }

    /**
     * Returns a list with all the pointcut managers.
     * 
     * @return the pointcut managers
     */
    public Collection getPointcutManagers() {
        initialize();
        return m_pointcutManagerMap.values();
    }

    /**
     * Returns an array with all the aspect containers.
     * 
     * @return the aspect containers
     */
    public AspectContainer[] getAspectContainers() {
        initialize();
        return m_aspectContainers;
    }

    /**
     * Returns the pointcut list for the context specified.
     * 
     * @param ctx the expression context
     * @return the pointcuts for this join point
     */
    public List getPointcuts(final ExpressionContext ctx) {
        List pointcuts = new ArrayList();
        for (Iterator it = m_pointcutManagerMap.values().iterator(); it.hasNext();) {
            PointcutManager pointcutManager = (PointcutManager) it.next();
            pointcuts.addAll(pointcutManager.getPointcuts(ctx));
        }
        return pointcuts;
    }

    /**
     * Returns the cflow pointcut list for the context specified.
     * 
     * @param ctx the expression context
     * @return the pointcuts for this join point
     */
    public List getCflowPointcuts(final ExpressionContext ctx) {
        List pointcuts = new ArrayList();
        for (Iterator it = m_pointcutManagerMap.values().iterator(); it.hasNext();) {
            PointcutManager pointcutManager = (PointcutManager) it.next();
            pointcuts.addAll(pointcutManager.getCflowPointcuts(ctx));
        }
        return pointcuts;
    }

    /**
     * Checks if a specific class has an aspect defined.
     * 
     * @param name the name of the aspect
     * @return boolean true if the class has an aspect defined
     */
    public boolean hasAspect(final String name) {
        if (name == null) {
            throw new IllegalArgumentException("aspect name can not be null");
        }
        initialize();
        if (m_pointcutManagerMap.containsKey(name)) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * Returns a specific method by the class and the method hash.
     * 
     * @param klass the class housing the method
     * @param methodHash the method hash
     * @return the method tuple
     */
    public static MethodTuple getMethodTuple(final Class klass, final int methodHash) {
        if (klass == null) {
            throw new IllegalArgumentException("class can not be null");
        }
        try {
            // create the method repository lazily
            if (!s_methods.containsKey(klass)) {
                createMethodRepository(klass);
            }
        } catch (Exception e) {
            throw new WrappedRuntimeException(e);
        }
        MethodTuple methodTuple;
        try {
            methodTuple = (MethodTuple) ((TIntObjectHashMap) s_methods.get(klass)).get(methodHash);
        } catch (Throwable e1) {
            throw new WrappedRuntimeException(e1);
        }
        return methodTuple;
    }

    /**
     * Returns a specific constructor by the class and the method hash.
     * 
     * @param klass the class housing the method
     * @param constructorHash the constructor hash
     * @return the constructor
     */
    public static ConstructorTuple getConstructorTuple(final Class klass, final int constructorHash) {
        if (klass == null) {
            throw new IllegalArgumentException("class can not be null");
        }
        try {
            // create the constructor repository lazily
            if (!s_constructors.containsKey(klass)) {
                createConstructorRepository(klass);
            }
        } catch (Exception e) {
            throw new WrappedRuntimeException(e);
        }
        ConstructorTuple constructorTuple;
        try {
            constructorTuple = (ConstructorTuple) ((TIntObjectHashMap) s_constructors.get(klass)).get(constructorHash);
        } catch (Throwable e1) {
            throw new WrappedRuntimeException(e1);
        }
        return constructorTuple;
    }

    /**
     * Returns a specific field by the class and the field hash.
     * 
     * @param klass the class housing the method
     * @param fieldHash the method hash
     * @return the method tuple
     */
    public static Field getField(final Class klass, final int fieldHash) {
        if (klass == null) {
            throw new IllegalArgumentException("class can not be null");
        }
        try {
            // create the fields repository lazily
            if (!s_fields.containsKey(klass)) {
                createFieldRepository(klass);
            }
        } catch (Exception e) {
            throw new WrappedRuntimeException(e);
        }
        Field field;
        try {
            field = (Field) ((TIntObjectHashMap) s_fields.get(klass)).get(fieldHash);
        } catch (Throwable e1) {
            throw new WrappedRuntimeException(e1);
        }
        return field;
    }

    /**
     * Creates a new method repository for the class specified.
     * 
     * @param klass the class
     */
    protected static void createMethodRepository(final Class klass) {
        if (klass == null) {
            throw new IllegalArgumentException("class can not be null");
        }
        Method[] methods = klass.getDeclaredMethods();
        TIntObjectHashMap methodMap = new TIntObjectHashMap(methods.length);
        for (int i = 0; i < methods.length; i++) {
            Method wrapperMethod = methods[i];
            if (!wrapperMethod.getName().startsWith(TransformationUtil.ASPECTWERKZ_PREFIX)) {
                Method prefixedMethod = null;
                for (int j = 0; j < methods.length; j++) {
                    Method method2 = methods[j];
                    if (method2.getName().startsWith(TransformationUtil.ASPECTWERKZ_PREFIX)) {
                        String[] tokens = Strings.splitString(method2.getName(), TransformationUtil.DELIMITER);
                        String methodName = tokens[1];
                        if (!methodName.equals(wrapperMethod.getName())) {
                            continue;
                        }
                        Class[] parameterTypes1 = wrapperMethod.getParameterTypes();
                        Class[] parameterTypes2 = method2.getParameterTypes();
                        if (parameterTypes2.length != parameterTypes1.length) {
                            continue;
                        }
                        boolean match = true;
                        for (int k = 0; k < parameterTypes1.length; k++) {
                            if (parameterTypes1[k] != parameterTypes2[k]) {
                                match = false;
                                break;
                            }
                        }
                        if (!match) {
                            continue;
                        }
                        prefixedMethod = method2;
                        break;
                    }
                }

                // create a method tuple with 'wrapped method' and 'prefixed method'
                MethodTuple methodTuple = new MethodTuple(wrapperMethod, prefixedMethod);

                // map the tuple to the hash for the 'wrapper method'
                int methodHash = ReflectHelper.calculateHash(wrapperMethod);
                methodMap.put(methodHash, methodTuple);
            }
        }
        synchronized (s_methods) {
            s_methods.put(klass, methodMap);
        }
    }

    /**
     * Creates a new constructor repository for the class specified.
     * 
     * @param klass the class
     */
    protected static void createConstructorRepository(final Class klass) {
        if (klass == null) {
            throw new IllegalArgumentException("class can not be null");
        }
        Constructor[] constructors = klass.getDeclaredConstructors();
        TIntObjectHashMap constructorMap = new TIntObjectHashMap(constructors.length);
        for (int i = 0; i < constructors.length; i++) {
            Constructor constructor1 = constructors[i];
            Constructor prefixedConstructor = constructor1;
            Constructor wrapperConstructor = constructor1;
            for (int j = 0; j < constructors.length; j++) {
                Constructor constructor2 = constructors[j];
                Class[] parameterTypes1 = constructor1.getParameterTypes();
                Class[] parameterTypes2 = constructor2.getParameterTypes();
                if (!constructor2.getName().equals(constructor1.getName())) {
                    continue;
                }
                if (parameterTypes1.length == parameterTypes2.length) {
                    continue;
                } else if ((parameterTypes1.length < parameterTypes2.length)
                    && (parameterTypes1.length == (parameterTypes2.length - 1))) {
                    boolean match = true;
                    for (int k = 0; k < parameterTypes1.length; k++) {
                        if (parameterTypes1[k] != parameterTypes2[k]) {
                            match = false;
                            break;
                        }
                    }
                    if (parameterTypes2[parameterTypes1.length].getName().equals(
                        TransformationUtil.JOIN_POINT_MANAGER_CLASS)) {
                        match = true;
                    }
                    if (!match) {
                        continue;
                    }
                    wrapperConstructor = constructor1;
                    prefixedConstructor = constructor2;
                    break;
                } else if ((parameterTypes2.length < parameterTypes1.length)
                    && (parameterTypes2.length == (parameterTypes1.length - 1))) {
                    boolean match = true;
                    for (int k = 0; k < parameterTypes2.length; k++) {
                        if (parameterTypes2[k] != parameterTypes1[k]) {
                            match = false;
                            break;
                        }
                    }
                    if (parameterTypes1[parameterTypes2.length].getName().equals(
                        TransformationUtil.JOIN_POINT_MANAGER_CLASS)) {
                        match = true;
                    }
                    if (!match) {
                        continue;
                    }
                    wrapperConstructor = constructor2;
                    prefixedConstructor = constructor1;
                    break;
                }
            }

            // create a constructor tuple with 'wrapper constructor' and 'prefixed constructor'
            ConstructorTuple constructorTuple = new ConstructorTuple(wrapperConstructor, prefixedConstructor);

            // map the tuple to the hash for the 'wrapper constructor'
            int constructorHash = ReflectHelper.calculateHash(wrapperConstructor);
            constructorMap.put(constructorHash, constructorTuple);
        }
        synchronized (s_constructors) {
            s_constructors.put(klass, constructorMap);
        }
    }

    /**
     * Creates a new field repository for the class specified.
     * 
     * @param klass the class
     */
    protected static void createFieldRepository(final Class klass) {
        if (klass == null) {
            throw new IllegalArgumentException("class can not be null");
        }
        Field[] fields = klass.getDeclaredFields();
        TIntObjectHashMap fieldMap = new TIntObjectHashMap(fields.length);
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            field.setAccessible(true);
            int fieldHash = ReflectHelper.calculateHash(field);
            fieldMap.put(fieldHash, field);
        }
        synchronized (s_fields) {
            s_fields.put(klass, fieldMap);
        }
    }
}