/*
 *  Copyright (c) 2007 Mockito contributors
 *  This program is made available under the terms of the MIT License.
 */
package org.powermock.api.mockito.repackaged.cglib.proxy;

import org.powermock.api.mockito.repackaged.cglib.core.AbstractClassGenerator;
import org.powermock.api.mockito.repackaged.cglib.core.CodeGenerationException;
import org.powermock.api.mockito.repackaged.cglib.core.GeneratorStrategy;
import org.powermock.api.mockito.repackaged.cglib.core.NamingPolicy;
import org.powermock.api.mockito.repackaged.cglib.core.Signature;
import org.powermock.api.mockito.repackaged.cglib.reflect.FastClass;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;


/**
 * Classes generated by {@link Enhancer} pass this object to the
 * registered {@link MethodInterceptor} objects when an intercepted method is invoked. It can
 * be used to either invoke the original method, or call the same method on a different
 * object of the same type.
 * @version $Id: MethodProxy.java,v 1.14 2008/05/26 04:05:50 herbyderby Exp $
 */
public class MethodProxy {
    private final Object initLock = new Object();
    private Signature sig1;
    private Signature sig2;
    private CreateInfo createInfo;
    private volatile FastClassInfo fastClassInfo;
    
    private MethodProxy() {
    }

    /**
     * For internal use by {@link Enhancer} only; see the {@link org.powermock.api.mockito.repackaged.cglib.reflect.FastMethod} class
     * for similar functionality.
     */
    public static MethodProxy create(Class c1, Class c2, String desc, String name1, String name2) {
        MethodProxy proxy = new MethodProxy();
        proxy.sig1 = new Signature(name1, desc);
        proxy.sig2 = new Signature(name2, desc);
        proxy.createInfo = new CreateInfo(c1, c2);
        return proxy;
    }

    private static FastClass helper(CreateInfo ci, Class type) {
        FastClass.Generator g = new FastClass.Generator();
        g.setType(type);
        g.setClassLoader(ci.c2.getClassLoader());
        g.setNamingPolicy(ci.namingPolicy);
        g.setStrategy(ci.strategy);
        g.setAttemptLoad(ci.attemptLoad);
        return g.create();
    }

    /**
     * Return the <code>MethodProxy</code> used when intercepting the method
     * matching the given signature.
     * @param type the class generated by Enhancer
     * @param sig the signature to match
     * @return the MethodProxy instance, or null if no applicable matching method is found
     * @throws IllegalArgumentException if the Class was not created by Enhancer or does not use a MethodInterceptor
     */
    public static MethodProxy find(Class type, Signature sig) {
        try {
            Method m = type.getDeclaredMethod(MethodInterceptorGenerator.FIND_PROXY_NAME,
                                              MethodInterceptorGenerator.FIND_PROXY_TYPES);
            return (MethodProxy)m.invoke(null, new Object[]{ sig });
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException("Class " + type + " does not use a MethodInterceptor");
        } catch (IllegalAccessException e) {
            throw new CodeGenerationException(e);
        } catch (InvocationTargetException e) {
            throw new CodeGenerationException(e);
        }
    }

    private void init()
    {
        /*
         * Using a volatile invariant allows us to initialize the FastClass and
         * method index pairs atomically.
         *
         * Double-checked locking is safe with volatile in Java 5.  Before 1.5 this
         * code could allow fastClassInfo to be instantiated more than once, which
         * appears to be benign.
         */
        if (fastClassInfo == null)
        {
            synchronized (initLock)
            {
                if (fastClassInfo == null)
                {
                    CreateInfo ci = createInfo;

                    FastClassInfo fci = new FastClassInfo();
                    fci.f1 = helper(ci, ci.c1);
                    fci.f2 = helper(ci, ci.c2);
                    fci.i1 = fci.f1.getIndex(sig1);
                    fci.i2 = fci.f2.getIndex(sig2);
                    fastClassInfo = fci;
                }
            }
        }
    }

    /**
     * Return the signature of the proxied method.
     */
    public Signature getSignature() {
        return sig1;
    }

    /**
     * Return the name of the synthetic method created by CGLIB which is
     * used by {@link #invokeSuper} to invoke the superclass
     * (non-intercepted) method implementation. The parameter types are
     * the same as the proxied method.
     */
    public String getSuperName() {
        return sig2.getName();
    }

    /**
     * Return the {@link org.powermock.api.mockito.repackaged.cglib.reflect.FastClass} method index
     * for the method used by {@link #invokeSuper}. This index uniquely
     * identifies the method within the generated proxy, and therefore
     * can be useful to reference external metadata.
     * @see #getSuperName
     */
    public int getSuperIndex() {
        init();
        return fastClassInfo.i2;
    }

    /**
     * Invoke the original method, on a different object of the same type.
     * @param obj the compatible object; recursion will result if you use the object passed as the first
     * argument to the MethodInterceptor (usually not what you want)
     * @param args the arguments passed to the intercepted method; you may substitute a different
     * argument array as long as the types are compatible
     * @see MethodInterceptor#intercept
     * @throws Throwable the bare exceptions thrown by the called method are passed through
     * without wrapping in an <code>InvocationTargetException</code>
     */
    public Object invoke(Object obj, Object[] args) throws Throwable {
        try {
            init();
            FastClassInfo fci = fastClassInfo;
            return fci.f1.invoke(fci.i1, obj, args);
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        } catch (IllegalArgumentException e) {
            if (fastClassInfo.i1 < 0)
                throw new IllegalArgumentException("Protected method: " + sig1);
            throw e;
        }
    }

    /**
     * Invoke the original (super) method on the specified object.
     * @param obj the enhanced object, must be the object passed as the first
     * argument to the MethodInterceptor
     * @param args the arguments passed to the intercepted method; you may substitute a different
     * argument array as long as the types are compatible
     * @see MethodInterceptor#intercept
     * @throws Throwable the bare exceptions thrown by the called method are passed through
     * without wrapping in an <code>InvocationTargetException</code>
     */
    public Object invokeSuper(Object obj, Object[] args) throws Throwable {
        try {
            init();
            FastClassInfo fci = fastClassInfo;
            return fci.f2.invoke(fci.i2, obj, args);
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }
    }

    private static class FastClassInfo
    {
        FastClass f1;
        FastClass f2;
        int i1;
        int i2;
    }

    private static class CreateInfo
    {
        Class c1;
        Class c2;
        NamingPolicy namingPolicy;
        GeneratorStrategy strategy;
        boolean attemptLoad;

        public CreateInfo(Class c1, Class c2)
        {
            this.c1 = c1;
            this.c2 = c2;
            AbstractClassGenerator fromEnhancer = AbstractClassGenerator.getCurrent();
            if (fromEnhancer != null) {
                namingPolicy = fromEnhancer.getNamingPolicy();
                strategy = fromEnhancer.getStrategy();
                attemptLoad = fromEnhancer.getAttemptLoad();
            }
        }
    }
}
