package org.seasar.framework.aop.javassist;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

import javassist.ClassPool;
import javassist.CtClass;

/**
 * @author koichik
 */
public class EnhancedClassGenerator extends AbstractGenerator {
    //instance fields
    protected final Class targetClass;
    protected final String enhancedClassName;
    protected CtClass enhancedClass;

    public EnhancedClassGenerator(final ClassPool classPool, final Class targetClass,
            final String enhancedClassName) {
        super(classPool);
        this.targetClass = targetClass;
        this.enhancedClassName = enhancedClassName;

        setupClass();
        setupInterface();
        setupConstructor();
    }

    public void createTargetMethod(final Method method, final String methodInvocationClassName) {
        createMethod(enhancedClass, method, createTargetMethodSource(method,
                methodInvocationClassName));
    }

    public void createInvokeSuperMethod(final Method method, final String invokeSuperMethodName) {
        createMethod(enhancedClass, method.getModifiers(), method.getReturnType(),
                invokeSuperMethodName, method.getParameterTypes(), method.getExceptionTypes(),
                createInvokeSuperMethodSource(method));
    }

    public Class toClass(final ClassLoader classLoader) {
        final Class clazz = toClass(classLoader, enhancedClass);
        enhancedClass.detach();
        enhancedClass = null;
        return clazz;
    }

    public void setupClass() {
        final Class superClass = (targetClass.isInterface()) ? Object.class : targetClass;
        enhancedClass = createCtClass(enhancedClassName, superClass);
    }

    public void setupInterface() {
        if (targetClass.isInterface()) {
            setInterface(enhancedClass, targetClass);
        }
    }

    public void setupConstructor() {
        final Constructor[] constructors = targetClass.getDeclaredConstructors();
        if (constructors.length == 0) {
            createDefaultConstructor(enhancedClass);
        }
        else {
            for (int i = 0; i < constructors.length; ++i) {
                final int modifier = constructors[i].getModifiers();
                if (Modifier.isPublic(modifier)
                        || Modifier.isProtected(modifier)
                        || (!Modifier.isPrivate(modifier)
                                && !targetClass.getName().startsWith("java.") && targetClass
                                .getPackage().isSealed())) {
                    createConstructor(enhancedClass, constructors[i]);
                }
            }
        }
    }

    public static String createTargetMethodSource(final Method method,
            final String methodInvocationClassName) {
        String code = "return ($r) "
                + fromObject(method.getReturnType(), "new " + methodInvocationClassName
                        + "(this, $args).proceed()") + ";";

        final Class[] exceptionTypes = normalizeExceptionTypes(method.getExceptionTypes());
        if (exceptionTypes.length != 1 || !exceptionTypes[0].equals(Throwable.class)) {
            code = aroundTryCatchBlock(exceptionTypes, code);
        }

        return "{" + code + "}";
    }

    public static String createInvokeSuperMethodSource(final Method method) {
        return "{" + "return ($r) super." + method.getName() + "($$);" + "}";
    }

    public static Class[] normalizeExceptionTypes(final Class[] exceptionTypes) {
        final List list = new LinkedList();
        outer: for (int i = 0; i < exceptionTypes.length; ++i) {
            final Class currentException = exceptionTypes[i];
            final ListIterator it = list.listIterator();
            while (it.hasNext()) {
                final Class comparisonException = (Class) it.next();
                if (comparisonException.isAssignableFrom(currentException)) {
                    continue outer;
                }
                if (currentException.isAssignableFrom(comparisonException)) {
                    it.remove();
                }
            }
            list.add(currentException);
        }
        return (Class[]) list.toArray(new Class[list.size()]);
    }

    public static String aroundTryCatchBlock(final Class[] exceptionTypes, final String code) {
        final TryBlockSupport tryBlock = new TryBlockSupport(code);

        boolean needRuntimeException = true;
        boolean needError = true;
        for (int i = 0; i < exceptionTypes.length; ++i) {
            final Class exceptionType = exceptionTypes[i];
            tryBlock.addCatchBlock(exceptionType, "throw e;");
            if (exceptionType.equals(RuntimeException.class)) {
                needRuntimeException = false;
            }
            if (exceptionType.equals(Error.class)) {
                needError = false;
            }
        }

        if (needRuntimeException) {
            tryBlock.addCatchBlock(RuntimeException.class, "throw e;");
        }
        if (needError) {
            tryBlock.addCatchBlock(Error.class, "throw e;");
        }
        tryBlock.addCatchBlock(Throwable.class,
                "throw new java.lang.reflect.UndeclaredThrowableException(e);");

        return tryBlock.getSourceCode();
    }
}