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

import org.codehaus.aspectwerkz.reflect.MethodInfo;
import org.codehaus.aspectwerkz.reflect.ClassInfo;
import org.codehaus.aspectwerkz.reflect.impl.asm.AsmClassInfo;
import org.codehaus.aspectwerkz.exception.WrappedRuntimeException;
import org.objectweb.asm.attrs.*;
import org.objectweb.asm.attrs.Annotation;
import org.objectweb.asm.Type;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.Field;
import java.lang.reflect.Array;
import java.util.List;
import java.util.Collection;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.HashMap;
import java.util.Arrays;
import java.io.Serializable;

/**
 * Dynamic proxy handler for ASM Annotations we extract
 * The handler resolve the LazyClass to a concrete Class so that the proxy creation does not trigger
 * any class loading.
 * <p/>
 *
 * @author <a href="mailto:alex AT gnilux DOT com">Alexandre Vasseur</a>
 */
public class Java5AnnotationInvocationHandler implements InvocationHandler {

    /**
     * The annotation class name
     */
    private final String m_annotationClassName;

    /**
     * A list of AnnotationElement containing the annotation instance element values
     * (including the defaulted value)
     */
    private final List m_annotationElements;

    /**
     * private ctor - see getAnnotationProxy()
     *
     * @param annotationClassName
     * @param annotationElements
     */
    private Java5AnnotationInvocationHandler(String annotationClassName, Collection annotationElements) {
        m_annotationClassName = annotationClassName;
        m_annotationElements = new ArrayList(annotationElements.size());
        for (Iterator iterator = annotationElements.iterator(); iterator.hasNext();) {
            m_annotationElements.add(iterator.next());
        }
    }

    /**
     * Dynamic proxy based implementation
     * toString(), annotationType() and value() have a specific behavior
     *
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String name = method.getName();
        if ("toString".equals(name)) {
            StringBuffer sb = new StringBuffer();
            sb.append('@').append(m_annotationClassName);
            sb.append("(");
            String sep = "";
            for (Iterator iterator = m_annotationElements.iterator(); iterator.hasNext();) {
                AnnotationElement annotationElement = (AnnotationElement) iterator.next();
                sb.append(sep).append(annotationElement.name + "=" + annotationElement.toString());
                sep = ", ";
            }
            sb.append(")");
            return sb.toString();
        } else if ("annotationType".equals(name)) {
            // funny, may explain why 1.5 Annotation intf has annotationType + getClass
            // since a dynamic proxy handler cannot hijack getClass() ..
            return Class.forName(m_annotationClassName, false, proxy.getClass().getClassLoader());
        } else if ("value".equals(name)) {
            if (m_annotationElements.isEmpty()) {
                return null;
            } else {
                //FIXME !!value can be there with other elements !
                // we could check that we don't have more than one element
                return ((AnnotationElement) m_annotationElements.get(0)).resolveValueHolderFrom(
                        proxy.getClass().getClassLoader()
                );
            }
        } else {
            for (Iterator iterator = m_annotationElements.iterator(); iterator.hasNext();) {
                AnnotationElement annotationElement = (AnnotationElement) iterator.next();
                if (name.equals(annotationElement.name)) {
                    return annotationElement.resolveValueHolderFrom(proxy.getClass().getClassLoader());
                }
            }
            // element not found for such a name
            throw new RuntimeException("No such element on Annotation @" + m_annotationClassName + " : " + name);
        }
    }

    /**
     * Build and return a dynamic proxy representing the given ASM Annotation.
     * The proxy implements the AspectWerkz Annotation interface, as well as the user type Annotation.
     * Each elements of the annotation is proxied if needed or agressively created unless Class types to not trigger
     * any nested loading.
     *
     * Note: JSR-175 does not support Annotation value of N-dimensional array. At most 1 dimension is supported and
     * only for a subset of Java types.
     *
     * @param annotation
     * @param loader the classloader of the annotatED component (can be different from the one of the annotation class)
     * @return
     */
    public static org.codehaus.aspectwerkz.annotation.Annotation getAnnotationProxy(org.objectweb.asm.attrs.Annotation annotation, ClassLoader loader) {
        String annotationClassName = Type.getType(annotation.type).getClassName();

        // get the ClassInfo for the annoation class to populate the assigned element values
        // with lazy value holders from the setted value or the default value if defaulted element
        // has been used in the annotation
        ClassInfo annotationClassInfo = AsmClassInfo.getClassInfo(annotationClassName, loader);
        Map annotationElementValueHoldersByName = new HashMap();

        // populate with the default values (might be then overriden by setted values)
        MethodInfo[] annotationMethods = annotationClassInfo.getMethods();
        for (int i = 0; i < annotationMethods.length; i++) {
            MethodInfo annotationMethod = annotationMethods[i];
            for (Iterator iterator = annotationMethod.getAnnotations().iterator(); iterator.hasNext();) {
                AnnotationInfo annotationInfo = (AnnotationInfo) iterator.next();
                // handles AnnotationDefault attribute that we have wrapped. See AnnotationDefault.
                if (annotationInfo.getName().equals(AnnotationDefault.NAME)) {
                    Object value = ((AnnotationDefault)annotationInfo.getAnnotation()).value();
                    Object valueHolder = getAnnotationValueHolder(value, loader);
                    annotationElementValueHoldersByName.put(annotationMethod.getName(),
                                                            new AnnotationElement(annotationMethod.getName(),
                                                                                  valueHolder)
                    );
                }
            }
        }

        // override and populate with the setted values
        List settedElementValues = annotation.elementValues;
        for (int i = 0; i < settedElementValues.size(); i++) {
            Object[] element = (Object[]) settedElementValues.get(i);
            String name = (String) element[0];
            Object valueHolder = getAnnotationValueHolder(element[1], loader);
            annotationElementValueHoldersByName.put(name, new AnnotationElement(name, valueHolder));
        }

        // create a dynamic proxy to embody the annotation instance
        try {
            Class typeClass = Class.forName(annotationClassName, false, loader);
            Object proxy = Proxy.newProxyInstance(
                    loader,
                    new Class[]{org.codehaus.aspectwerkz.annotation.Annotation.class, typeClass},
                    new Java5AnnotationInvocationHandler(annotationClassName,
                                                         annotationElementValueHoldersByName.values()
                    )
            );
            return (org.codehaus.aspectwerkz.annotation.Annotation) proxy;
        } catch (ClassNotFoundException e) {
            throw new WrappedRuntimeException(e);
        }
    }

    /**
     * Turn an ASM Annotation value into a concrete Java value holder, unless the value is of type
     * Class, in which case we wrap it behind a LazyClass() object so that actual loading of the class
     * will be done lazily
     *
     * @param value
     * @param loader
     * @return
     */
    private static Object getAnnotationValueHolder(Object value, ClassLoader loader) {
        if (value instanceof Annotation.EnumConstValue) {
            Annotation.EnumConstValue enumAsmValue = (Annotation.EnumConstValue) value;
            try {
                Class enumClass = Class.forName(Type.getType(enumAsmValue.typeName).getClassName(), false, loader);
                Field enumConstValue = enumClass.getField(enumAsmValue.constName);
                return enumConstValue.get(null);
            } catch (Exception e) {
                throw new WrappedRuntimeException(e);
            }
        } else if (value instanceof Type) {
            // TODO may require additional filtering ?
            return new AnnotationElement.LazyClass(((Type) value).getClassName());
        } else if (value instanceof Annotation) {
            return getAnnotationProxy(((Annotation) value), loader);
        } else if (value instanceof Object[]) {
            Object[] values = (Object[]) value;
            Object[] holders = new Object[values.length];
            boolean isLazyClass = false;
            for (int i = 0; i < values.length; i++) {
                holders[i] = getAnnotationValueHolder(values[i], loader);
                if (!isLazyClass && holders[i] instanceof AnnotationElement.LazyClass) {
                    isLazyClass = true;
                }
            }
            if (isLazyClass) {
                // retype the array
                AnnotationElement.LazyClass[] typedHolders = (AnnotationElement.LazyClass[]) Array.newInstance(AnnotationElement.LazyClass.class, values.length);
                for (int i = 0; i < holders.length; i++) {
                    typedHolders[i] = (AnnotationElement.LazyClass) holders[i];
                }
                return typedHolders;
            } else {
                return holders;
            }
        }
        return value;
    }

}

