package org.seasar.framework.aop.proxy;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import org.aopalliance.intercept.MethodInvocation;
import org.seasar.framework.aop.Aspect;
import org.seasar.framework.aop.Pointcut;
import org.seasar.framework.aop.impl.MethodInvocationImpl;
import org.seasar.framework.aop.impl.PointcutImpl;
import org.seasar.framework.beans.BeanDesc;
import org.seasar.framework.beans.factory.BeanDescFactory;
import org.seasar.framework.exception.EmptyRuntimeException;
import org.seasar.framework.log.Logger;

/**
 * @author higa
 * 
 * Aspectを適用したProxyを作成します。
 */
public final class AopProxy implements MethodInterceptor, Serializable {

	static final long serialVersionUID = 0L;

	private static Logger logger_ = Logger.getLogger(AopProxy.class);

	private static final int AOP_PROXY_INDEX = 0;

	private static final int EQUALS_INTERCEPTOR_INDEX = 1;

	private static final int NONE_INTERCEPTOR_INDEX = 2;
	
	private static final MethodInterceptor EQUALS_INTERCEPTOR = new EqualsInterceptor();
	
	private static final MethodInterceptor NO_OP_INTERCEPTOR = new NoOpInterceptor();

	private Class targetClass_;

	private Pointcut defaultPointcut_;

	private Map interceptorsMap_ = new HashMap();

	private Map parameters_;

	public AopProxy(Class targetClass, Aspect[] aspects) {
		this(targetClass, aspects, null);
	}

	public AopProxy(Class targetClass, Aspect[] aspects, Map parameters) {
		setTargetClass(targetClass);
		setAspects(aspects);
		parameters_ = parameters;
	}

	private void setTargetClass(Class targetClass) {
		targetClass_ = targetClass;
		defaultPointcut_ = new PointcutImpl(targetClass);
	}

	private void setAspects(Aspect[] aspects) {
		if (aspects == null || aspects.length == 0) {
			throw new EmptyRuntimeException("aspects");
		}
		for (int i = 0; i < aspects.length; ++i) {
			Aspect aspect = aspects[i];
			if (aspect.getPointcut() == null) {
				aspect.setPointcut(defaultPointcut_);
			}
		}
		BeanDesc beanDesc = BeanDescFactory.getBeanDesc(targetClass_);
		String[] names = beanDesc.getMethodNames();
		outer: for (int i = 0; i < names.length; ++i) {
			String name = names[i];
			boolean applicableAspect = isApplicableAspect(beanDesc
					.getMethods(name));
			List interceptorList = new ArrayList();
			for (int j = 0; j < aspects.length; ++j) {
				Aspect aspect = aspects[j];
				if (aspect.getPointcut().isApplied(name)) {
					if (!applicableAspect) {
						logger_.log("WSSR0009", new Object[] {
								targetClass_.getName(), name });
						continue outer;
					}
					interceptorList.add(aspect.getMethodInterceptor());
				}
			}
			if (interceptorList.size() > 0) {
				interceptorsMap_
						.put(
								name,
								interceptorList
										.toArray(new org.aopalliance.intercept.MethodInterceptor[interceptorList
												.size()]));
			}
		}
	}

	public Object create() {
		Enhancer e = setupEnhancer();
		return e.create();
	}

	public Object create(Class[] argTypes, Object[] args) {
		Enhancer e = setupEnhancer();
		return e.create(argTypes, args);
	}

	private Enhancer setupEnhancer() {
		Enhancer e = new Enhancer();
		e.setSuperclass(targetClass_);
		e.setCallbacks(new Callback[] { this, EQUALS_INTERCEPTOR,
				NO_OP_INTERCEPTOR });
		e.setCallbackFilter(new MyCallbackFilter());
		return e;
	}

	private boolean isApplicableAspect(Method[] methods) {
		for (int i = 0; i < methods.length; ++i) {
			int mod = methods[i].getModifiers();
			if (Modifier.isFinal(mod) || Modifier.isStatic(mod)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * @see net.sf.cglib.proxy.MethodInterceptor#intercept(java.lang.Object,
	 *      java.lang.reflect.Method, java.lang.Object[],
	 *      net.sf.cglib.proxy.MethodProxy)
	 */
	public Object intercept(Object obj, Method method, Object[] args,
			MethodProxy proxy) throws Throwable {

		org.aopalliance.intercept.MethodInterceptor[] interceptors = (org.aopalliance.intercept.MethodInterceptor[]) interceptorsMap_
				.get(method.getName());
		MethodInvocation invocation = new MethodInvocationImpl(obj, method,
				args, proxy, interceptors, targetClass_, parameters_);
		return invocation.proceed();
	}

	private static class EqualsInterceptor implements MethodInterceptor, Serializable {

		static final long serialVersionUID = 0L;

		/**
		 * @see net.sf.cglib.proxy.MethodInterceptor#intercept(java.lang.Object,
		 *      java.lang.reflect.Method, java.lang.Object[],
		 *      net.sf.cglib.proxy.MethodProxy)
		 */
		public Object intercept(Object obj, Method method, Object[] args,
				MethodProxy proxy) throws Throwable {

			if (args[0] == obj) {
				return Boolean.TRUE;
			} else {
				return Boolean.FALSE;
			}
		}
	}
	
	private static class NoOpInterceptor implements MethodInterceptor, Serializable {

		static final long serialVersionUID = 0L;

		/**
		 * @see net.sf.cglib.proxy.MethodInterceptor#intercept(java.lang.Object,
		 *      java.lang.reflect.Method, java.lang.Object[],
		 *      net.sf.cglib.proxy.MethodProxy)
		 */
		public Object intercept(Object obj, Method method, Object[] args,
				MethodProxy proxy) throws Throwable {

			return proxy.invokeSuper(obj, args);
		}
	}

	private class MyCallbackFilter implements CallbackFilter, Serializable {

		static final long serialVersionUID = 0L;

		MyCallbackFilter() {
		}

		/**
		 * @see net.sf.cglib.proxy.CallbackFilter#accept(java.lang.reflect.Method)
		 */
		public int accept(Method method) {
			if (interceptorsMap_.containsKey(method.getName())) {
				return AOP_PROXY_INDEX;
			} else if (isEqualsMethod(method)) {
				return EQUALS_INTERCEPTOR_INDEX;
			} else {
				return NONE_INTERCEPTOR_INDEX;
			}
		}

		private boolean isEqualsMethod(Method method) {
			return "equals".equals(method.getName())
					&& method.getParameterTypes().length == 1
					&& method.getReturnType() == boolean.class;
		}
	}
}