/*
 * Copyright 2004-2015 the Seasar Foundation and the Others.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
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;

import org.seasar.framework.aop.InterType;
import org.seasar.framework.util.MethodUtil;

/**
 * コンポーネントのバイトコードをエンハンスするクラスです。
 * 
 * @author koichik
 */
public class EnhancedClassGenerator extends AbstractGenerator {

    /**
     * ターゲットクラス
     */
    protected final Class targetClass;

    /**
     * エンハンスされるクラス名
     */
    protected final String enhancedClassName;

    /**
     * エンハンスされるクラス
     */
    protected CtClass enhancedClass;

    /**
     * {@link EnhancedClassGenerator}を作成します。
     * 
     * @param classPool
     * @param targetClass
     * @param enhancedClassName
     */
    public EnhancedClassGenerator(final ClassPool classPool,
            final Class targetClass, final String enhancedClassName) {
        super(classPool);
        this.targetClass = targetClass;
        this.enhancedClassName = enhancedClassName;

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

    /**
     * ターゲットのメソッドを作成します。
     * 
     * @param method
     * @param methodInvocationClassName
     */
    public void createTargetMethod(final Method method,
            final String methodInvocationClassName) {
        createMethod(enhancedClass, method,
                createTargetMethodSource(method, methodInvocationClassName));
    }

    /**
     * superクラスのメソッドを呼び出すためのメソッドを作成します。
     * 
     * @param method
     * @param invokeSuperMethodName
     */
    public void createInvokeSuperMethod(final Method method,
            final String invokeSuperMethodName) {
        createMethod(enhancedClass, method.getModifiers(),
                method.getReturnType(), invokeSuperMethodName,
                method.getParameterTypes(), method.getExceptionTypes(),
                createInvokeSuperMethodSource(method));
    }

    /**
     * {@link InterType}を適用します。
     * 
     * @param interType
     */
    public void applyInterType(final InterType interType) {
        interType.introduce(targetClass, enhancedClass);
    }

    /**
     * CtClassをClassに変換します。
     * 
     * @param classLoader
     * @return
     */
    public Class toClass(final ClassLoader classLoader) {
        final Class clazz = toClass(classLoader, enhancedClass);
        enhancedClass.detach();
        enhancedClass = null;
        return clazz;
    }

    /**
     * CtClassをセットアップします。
     */
    public void setupClass() {
        final Class superClass = (targetClass.isInterface()) ? Object.class
                : targetClass;
        enhancedClass = createCtClass(enhancedClassName, superClass);
    }

    /**
     * インターフェース用のセットアップを行ないます。
     */
    public void setupInterface() {
        if (targetClass.isInterface()) {
            setInterface(enhancedClass, targetClass);
        }
    }

    /**
     * {@link Constructor}のセットアップを行ないます。
     */
    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();
                final Package pkg = targetClass.getPackage();
                if (Modifier.isPublic(modifier)
                        || Modifier.isProtected(modifier)
                        || (!Modifier.isPrivate(modifier)
                                && !targetClass.getName().startsWith("java.") && (pkg == null || !pkg
                                .isSealed()))) {
                    createConstructor(enhancedClass, constructors[i]);
                }
            }
        }
    }

    /**
     * ターゲットメソッド用のソースコードを作成します。
     * 
     * @param method
     * @param methodInvocationClassName
     * @return ターゲットメソッド用のソースコード
     */
    public static String createTargetMethodSource(final Method method,
            final String methodInvocationClassName) {
        final StringBuffer buf = new StringBuffer(200);
        buf.append("Object result = new ").append(methodInvocationClassName)
                .append("(this, $args).proceed();");
        final Class returnType = method.getReturnType();
        if (returnType.equals(void.class)) {
            buf.append("return;");
        } else if (returnType.isPrimitive()) {
            buf.append("return ($r) ((result == null) ? ");
            if (returnType.equals(boolean.class)) {
                buf.append("false : ");
            } else {
                buf.append("0 : ");
            }
            buf.append(fromObject(returnType, "result")).append(");");
        } else {
            buf.append("return ($r) result;");
        }
        String code = new String(buf);

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

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

    /**
     * superクラスのメソッドを呼び出すためのソースコードを作成します。
     * 
     * @param method
     * @return superクラスのメソッドを呼び出すためのソースコード
     */
    public static String createInvokeSuperMethodSource(final Method method) {
        if (MethodUtil.isDefaultMethod(method)) {
            System.out.println("{" + "return ($r) " + method.getDeclaringClass().getName()
                    + ".super." + method.getName() + "($$);" + "}");
            return "{" + "return ($r) " + method.getDeclaringClass().getName()
                    + ".super." + method.getName() + "($$);" + "}";
        }
        return "{" + "return ($r) super." + method.getName() + "($$);" + "}";
    }

    /**
     * 例外の型を正規化します。
     * 
     * @param exceptionTypes
     * @return
     */
    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()]);
    }

    /**
     * 元のソースコードをtry, cacheで囲んだソースコードを返します。
     * 
     * @param exceptionTypes
     * @param code
     * @return 元のソースコードをtry, cacheで囲んだソースコード
     */
    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();
    }
}