/************************************************
 * Copyright (c) Shen Li. All rights reserved.  *
 * http://joyaop.sourceforge.net                *
 * -------------------------------------------- *
 * Distributable under LGPL license.            *
 * See terms of license at gnu.org.             *
 ************************************************/
package net.sf.joyaop.impl;

import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.Factory;
import net.sf.joyaop.AspectRuntimeException;
import net.sf.joyaop.framework.AspectizedClass;
import net.sf.joyaop.framework.InterceptorAspect;
import net.sf.joyaop.framework.MixinAspect;
import net.sf.joyaop.framework.InterfaceAspect;
import net.sf.joyaop.framework.AspectizedClass;
import net.sf.joyaop.util.ClassUtils;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;

/**
 * @author Shen Li
 */
class AspectizedClassImpl implements AspectizedClass {
//    private static final Log log = LogFactory.getLog(AspectizedClassImpl.class);

    private final Map interceptors = new HashMap();
    private final Map mixins = new HashMap();
    private final Set interfaceClasses = new HashSet();
    private final Set mixinInterfaceClasses = new HashSet();
    private final Class originalClass;

    private CallbackFactory[] callbackFactories;
    private CallbackFilterImpl callbackFilter;

    public AspectizedClassImpl(Class originalClass) {
        this.originalClass = originalClass;
        interfaceClasses.add(WriteReplace.class);
    }

    public void addInterceptor(Method method, InterceptorAspect interceptorAspect) {
        if (method.getDeclaringClass() == WriteReplace.class) {
            return;
        }
        Set chain = (Set) interceptors.get(method);
        if (chain == null) {
            chain = new TreeSet();
            interceptors.put(method, chain);
        }
        chain.add(interceptorAspect);
    }

    public void addInterface(InterfaceAspect interfaceAspect) {
        interfaceClasses.add(interfaceAspect.getAspectClass());
    }

    public void addMixin(MixinAspect mixinAspect) {
        interfaceClasses.add(mixinAspect.getInterfaceClass());
        mixinInterfaceClasses.add(mixinAspect.getInterfaceClass());
        doAddMixin(mixinAspect.getInterfaceClass(), mixinAspect);
    }

    public Object newInstance() {
        return newInstance(new Class[0], new Object[0]);
    }

    public Object newInstance(Class[] argTypes, Object[] args) {
        if (!weaved()) {
            if (Modifier.isAbstract(originalClass.getModifiers())) {
                throw new AspectRuntimeException(
                        "Class " + originalClass.getName() + " is interface or abstract class, "
                        + "and doesn't weaved by the mixins, so it can't be instantiated.");
            }
            return ClassUtils.newInstance(originalClass, argTypes, args);
        }
        initialize();
        Factory factory = ClassUtils.getProxyFactory(originalClass);
        if (factory == null) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(originalClass);
            enhancer.setInterfaces((Class[]) interfaceClasses.toArray(new Class[interfaceClasses.size()]));
            enhancer.setCallbackFilter(callbackFilter);
            enhancer.setCallbacks(createCallbacks());
            enhancer.setClassLoader(ClassUtils.getClassLoader());
            factory = (Factory) enhancer.create(argTypes, args);
            ClassUtils.putProxyFactory(originalClass, factory);
            return factory;
        }
        return factory.newInstance(argTypes, args, createCallbacks());
    }

    public Class getOriginalClass() {
        return originalClass;
    }

    public Set getInterceptors(Method method) {
        return (Set) interceptors.get(method);
    }

    public MixinAspect getMixin(Class interfaceClass) {
        return (MixinAspect) mixins.get(interfaceClass);
    }

    // not used now
    public Class getDeclaringClass(Method method) {
        Class clazz = method.getDeclaringClass();
        if (Object.class == clazz && originalClass.isInterface()) {
            try {
                clazz = originalClass.getMethod(method.getName(), method.getParameterTypes()).getDeclaringClass();
            } catch (NoSuchMethodException e) {
            }
        }
        return clazz;
    }

    public Set getMethods() {
        Set methods = new HashSet(Arrays.asList(originalClass.getMethods()));
        for (Iterator i = mixinInterfaceClasses.iterator(); i.hasNext();) {
            Class interfaceClass = (Class) i.next();
            if (!interfaceClass.isAssignableFrom(originalClass)) {
                methods.addAll(Arrays.asList(interfaceClass.getMethods()));
            }
        }
        return methods;
    }

    private synchronized void initialize() {
        if (callbackFactories == null) {
            callbackFactories = new CallbackFactory[interceptors.size() + mixins.size() + 2];
            callbackFilter = new CallbackFilterImpl();

            int index = 0;
            // interceptor callback
            for (Iterator i = interceptors.entrySet().iterator(); i.hasNext(); index++) {
                Map.Entry entry = (Map.Entry) i.next();
                callbackFilter.addMethod((Method) entry.getKey(), index);
                callbackFactories[index] =
                        new InterceptorCallbackFactory(this, (Method) entry.getKey(), (Set) entry.getValue());
            }
            // mixin callback
            for (Iterator i = mixins.entrySet().iterator(); i.hasNext(); index++) {
                Map.Entry entry = (Map.Entry) i.next();
                callbackFilter.addClass((Class) entry.getKey(), index);
                callbackFactories[index] = new MixinCallbackFactory(this, (MixinAspect) entry.getValue());
            }
            // original class callback
            for (Class clazz = originalClass; clazz != null; clazz = clazz.getSuperclass()) {
                callbackFilter.addClass(clazz, index);
            }
            callbackFactories[index] = new OriginalClassCallbackFactory(this);
            // writeReplace callback
            index++;
            callbackFilter.addClass(WriteReplace.class, index);
            callbackFactories[index] = new WriteReplaceCallbackFactory(interfaceClasses, callbackFilter);
        }
    }

    private void doAddMixin(Class interfaceClass, MixinAspect mixinAspect) {
        if (mixins.containsKey(interfaceClass)) {
            throw new AspectRuntimeException(
                    "The mixin with interface "
                    + mixinAspect.getInterfaceClass().getName()
                    + " has been added.");
        }
        mixins.put(interfaceClass, mixinAspect);
        Class[] interfaces = interfaceClass.getInterfaces();
        for (int i = 0; i < interfaces.length; i++) {
            doAddMixin(interfaces[i], mixinAspect);
        }
    }

    private Callback[] createCallbacks() {
        RuntimeAspectInstanceFactory factory = new RuntimeAspectInstanceFactory(originalClass);
        Callback[] callbacks = new Callback[callbackFactories.length];
        for (int i = 0; i < callbackFactories.length; i++) {
            callbacks[i] = callbackFactories[i].getCallback(factory);
        }
        return callbacks;
    }

    private boolean weaved() {
        return (interceptors.size() > 0 || interfaceClasses.size() > 1); // remember WriteReplace interface
    }
}