/*
 * The MIT License
 * 
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package hudson.remoting;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;

/**
 * Sits behind a proxy object and implements the proxy logic.
 *
 * @author Kohsuke Kawaguchi
 */
final class RemoteInvocationHandler implements InvocationHandler, Serializable {
    /**
     * This proxy acts as a proxy to the object of
     * Object ID on the remote {@link Channel}.
     */
    private final int oid;

    /**
     * Represents the connection to the remote {@link Channel}.
     *
     * <p>
     * This field is null when a {@link RemoteInvocationHandler} is just
     * created and not working as a remote proxy. Once tranferred to the
     * remote system, this field is set to non-null. 
     */
    private transient Channel channel;

    /**
     * True if we are proxying an user object.
     */
    private final boolean userProxy;

    /**
     * If true, this proxy is automatically unexported by the calling {@link Channel},
     * so this object won't release the object at {@link #finalize()}.
     * <p>
     * This ugly distinction enables us to keep the # of exported objects low for
     * the typical situation where the calls are synchronous (thus end of the call
     * signifies full unexport of all involved objects.)
     */
    private final boolean autoUnexportByCaller;

    /**
     * If true, indicates that this proxy object is being sent back
     * to where it came from. If false, indicates that this proxy
     * is being sent to the remote peer.
     *
     * Only used in the serialized form of this class.
     */
    private boolean goingHome;

    /**
     * Creates a proxy that wraps an existing OID on the remote.
     */
    RemoteInvocationHandler(Channel channel, int id, boolean userProxy, boolean autoUnexportByCaller) {
        this.channel = channel;
        this.oid = id;
        this.userProxy = userProxy;
        this.autoUnexportByCaller = autoUnexportByCaller;
    }

    /**
     * Wraps an OID to the typed wrapper.
     */
    public static <T> T wrap(Channel channel, int id, Class<T> type, boolean userProxy, boolean autoUnexportByCaller) {
        ClassLoader cl = type.getClassLoader();
        // if the type is a JDK-defined type, classloader should be for IReadResolve
        if(cl==null || cl==ClassLoader.getSystemClassLoader())
            cl = IReadResolve.class.getClassLoader();
        return type.cast(Proxy.newProxyInstance(cl, new Class[]{type,IReadResolve.class},
            new RemoteInvocationHandler(channel,id,userProxy,autoUnexportByCaller)));
    }

    /*package*/ static Class getProxyClass(Class type) {
        return Proxy.getProxyClass(type.getClassLoader(), new Class[]{type,IReadResolve.class});
    }

    /**
     * If the given object is a proxy to a remote object in the specified channel,
     * return its object ID. Otherwise return -1.
     * <p>
     * This method can be used to get back the original reference when
     * a proxy is sent back to the channel it came from. 
     */
    public static int unwrap(Object proxy, Channel src) {
        InvocationHandler h = Proxy.getInvocationHandler(proxy);
        if (h instanceof RemoteInvocationHandler) {
            RemoteInvocationHandler rih = (RemoteInvocationHandler) h;
            if(rih.channel==src)
                return rih.oid;
        }
        return -1;
    }

    /**
     * If the given object is a proxy object, return the {@link Channel}
     * object that it's associated with. Otherwise null.
     */
    public static Channel unwrap(Object proxy) {
        InvocationHandler h = Proxy.getInvocationHandler(proxy);
        if (h instanceof RemoteInvocationHandler) {
            RemoteInvocationHandler rih = (RemoteInvocationHandler) h;
            return rih.channel;
        }
        return null;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(method.getDeclaringClass()==IReadResolve.class) {
            // readResolve on the proxy.
            // if we are going back to where we came from, replace the proxy by the real object
            if(goingHome)   return Channel.current().getExportedObject(oid);
            else            return proxy;
        }

        if(channel==null)
            throw new IllegalStateException("proxy is not connected to a channel");

        if(args==null)  args = EMPTY_ARRAY;

        Class<?> dc = method.getDeclaringClass();
        if(dc ==Object.class) {
            // handle equals and hashCode by ourselves
            try {
                return method.invoke(this,args);
            } catch (InvocationTargetException e) {
                throw e.getTargetException();
            }
        }

        // delegate the rest of the methods to the remote object

        boolean async = method.isAnnotationPresent(Asynchronous.class);
        RPCRequest req = new RPCRequest(oid, method, args, userProxy ? dc.getClassLoader() : null);
        try {
            if(userProxy) {
                if (async)  channel.callAsync(req);
                else        return channel.call(req);
            } else {
                if (async)  req.callAsync(channel);
                else        return req.call(channel);
            }
            return null;
        } catch (Throwable e) {
            for (Class exc : method.getExceptionTypes()) {
                if (exc.isInstance(e))
                    throw e;    // signature explicitly lists this exception
            }
            if (e instanceof RuntimeException || e instanceof Error)
                throw e;    // these can be thrown from any methods

            // if the thrown exception type isn't compatible with the method signature
            // wrap it to RuntimeException to avoid UndeclaredThrowableException
            throw new RemotingSystemException(e);
        }
    }

    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject();
        if (goingHome) {
            // We came back to the side that exported the object.
            // Since this object represents a local object, it shouldn't have non-null channel.
            // (which would cause the finalize() method to try to unexport the object.)
            channel = null;
        } else {
            channel = Channel.current();
        }
    }

    private void writeObject(ObjectOutputStream oos) throws IOException {
        goingHome = channel!=null;
        oos.defaultWriteObject();
    }

    /**
     * Two proxies are the same iff they represent the same remote object. 
     */
    public boolean equals(Object o) {
        if(Proxy.isProxyClass(o.getClass()))
            o = Proxy.getInvocationHandler(o);

        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        RemoteInvocationHandler that = (RemoteInvocationHandler) o;

        return this.oid==that.oid && this.channel==that.channel;

    }

    public int hashCode() {
        return oid;
    }


    protected void finalize() throws Throwable {
        // unexport the remote object
        if (channel!=null && !autoUnexportByCaller) {
            channel.send(new UnexportCommand(oid));
            channel = null;
        }
        super.finalize();
    }

    private static final long serialVersionUID = 1L;

    /**
     * Executes the method call remotely.
     *
     * If used as {@link Request}, this can be used to provide a lower-layer
     * for the use inside remoting, to implement the classloader delegation, and etc.
     * The downside of this is that the classes used as a parameter/return value
     * must be available to both JVMs.
     *
     * If used as {@link Callable} in conjunction with {@link UserRequest},
     * this can be used to send a method call to user-level objects, and
     * classes for the parameters and the return value are sent remotely if needed.
     */
    static final class RPCRequest extends Request<Serializable,Throwable> implements DelegatingCallable<Serializable,Throwable> {
        /**
         * Target object id to invoke.
         */
        private final int oid;

        private final String methodName;
        /**
         * Type name of the arguments to invoke. They are names because
         * neither {@link Method} nor {@link Class} is serializable.
         */
        private final String[] types;
        /**
         * Arguments to invoke the method with.
         */
        private final Object[] arguments;

        /**
         * If this is used as {@link Callable}, we need to remember what classloader
         * to be used to serialize the request and the response.
         */
        private transient ClassLoader classLoader;

        public RPCRequest(int oid, Method m, Object[] arguments) {
            this(oid,m,arguments,null);
        }

        public RPCRequest(int oid, Method m, Object[] arguments, ClassLoader cl) {
            this.oid = oid;
            this.arguments = arguments;
            this.methodName = m.getName();
            this.classLoader = cl;

            this.types = new String[arguments.length];
            Class<?>[] params = m.getParameterTypes();
            for( int i=0; i<arguments.length; i++ )
                types[i] = params[i].getName();
            assert types.length == arguments.length;
        }

        public Serializable call() throws Throwable {
            return perform(Channel.current());
        }

        public ClassLoader getClassLoader() {
            if(classLoader!=null)
                return classLoader;
            else
                return getClass().getClassLoader();
        }

        protected Serializable perform(Channel channel) throws Throwable {
            Object o = channel.getExportedObject(oid);
            try {
                Method m = choose(o);
                if(m==null)
                    throw new IllegalStateException("Unable to call "+methodName+". No matching method found on "+o.getClass());
                m.setAccessible(true);  // in case the class is not public
                Object r;
                try {
                    r = m.invoke(o, arguments);
                } catch (IllegalArgumentException x) {
                    throw new RemotingSystemException("failed to invoke " + m + " on " + o + Arrays.toString(arguments), x);
                }
                if (r==null || r instanceof Serializable)
                    return (Serializable) r;
                else
                    throw new RemotingSystemException(new ClassCastException(r.getClass()+" is returned from "+m+" on "+o.getClass()+" but it's not serializable"));
            } catch (InvocationTargetException e) {
                throw e.getTargetException();
            }
        }

        /**
         * Chooses the method to invoke.
         */
        private Method choose(Object o) {
            OUTER:
            for(Method m : o.getClass().getMethods()) {
                if(!m.getName().equals(methodName))
                    continue;
                Class<?>[] paramTypes = m.getParameterTypes();
                if(paramTypes.length!=arguments.length)
                    continue;
                for( int i=0; i<types.length; i++ ) {
                    if(!types[i].equals(paramTypes[i].getName()))
                        continue OUTER;
                }
                return m;
            }
            return null;
        }

        Object[] getArguments() { // for debugging
            return arguments;
        }

        public String toString() {
            return "RPCRequest("+oid+","+methodName+")";
        }

        private static final long serialVersionUID = 1L; 
    }

    private static final Object[] EMPTY_ARRAY = new Object[0];
}
