package org.jgroups.blocks.executor;

import org.jgroups.util.Streamable;
import org.jgroups.util.Util;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.concurrent.Callable;

public class Executions {
    /**
     * This method should be used to convert a callable that would not normally
     * be serializable, externalizable or streamable but has serializable,
     * externalizable or streamable arguments to a constructor to construct it.
     * <p>
     * When the call method is called on the callable it will call the provided
     * constructor passing in the given arguments. It will then invoke the call
     * method on resulting callable that was created.
     * <p>
     * The amount of arguments cannot exceed {@link Byte#MAX_VALUE}. Also the
     * constructor cannot exceed {@link Byte#MAX_VALUE} position in the
     * constructor array returned from {@link Class#getConstructors()}
     * <p>
     * The amount of arguments must match the amount of arguments required
     * by the constructor.  Also the arguments must be compatibile with the
     * types required of the constructor.
     * <p>
     * Unfortunately it isn't easy to pass a Constructor<? extends Callable<T>>
     * so we can't pass back a callable that is properly typed.  Also this
     * forces the caller to cast their callable or returned value to the correct 
     * type manually.
     * 
     * @param constructorToUse The constructor to use when creating the callable
     * @param args The arguments to pass to the constructor
     * @return The callable that will upon being called will instantiate the
     *         given callable using the constructor with the provided arguments
     *         and calls the call method
     * @throws IllegalArgumentException This is thrown if the arguments are
     *         not serializable, externalizable or streamable.  It can be thrown
     *         if the constructo is not accessible.  It can also be thrown
     *         if too many arguments or the constructor is to high up in the
     *         constructo array returned by the class.
     */
    public static Callable<?> serializableCallable(@SuppressWarnings("rawtypes") 
                Constructor<? extends Callable> constructorToUse, Object... args)
            throws IllegalArgumentException {
        if (args.length > (int)Byte.MAX_VALUE) {
            throw new IllegalArgumentException(
                "Max number of arguments exceeded: " + Byte.MAX_VALUE);
        }
        Class<?>[] params = constructorToUse.getParameterTypes();
        
        if (params.length != args.length) {
            throw new IllegalArgumentException("Number of arguments ["
                    + args.length + "] doesn't match number of arguments for "
                    + "constructor [" + params.length + "]");
        }
        
        for (int i = 0; i < args.length; ++i) {
            Object arg = args[i];
            if (arg instanceof Serializable ||
                    arg instanceof Streamable) {
                Class<?> classArg = params[i];
                
                if (!classArg.isInstance(arg)) {
                    throw new IllegalArgumentException("Argument [" + arg + 
                        "] is not an instance of [" + classArg + "]");
                }
            }
            else {
                throw new IllegalArgumentException(
                    "Argument is not serializable, externalizable or streamable: " + arg);
            }
        }
        @SuppressWarnings("unchecked")
        Class<? extends Callable<?>> classToUse = 
            (Class<? extends Callable<?>>)constructorToUse.getDeclaringClass();
        Constructor<?>[] constructors = classToUse.getConstructors();
        byte constructorPosition = -1;
        for (int i = 0; i < constructors.length; ++i) {
            Constructor<?> constructor = constructors[i];
            if (constructor.equals(constructorToUse)) {
                if (i > (int)Byte.MAX_VALUE) {
                    throw new IllegalArgumentException(
                        "Constructor position in array cannot be higher than "
                                + Byte.MAX_VALUE);
                }
                constructorPosition = (byte)i;
            }
        }
        if (constructorPosition == -1) {
            throw new IllegalArgumentException(
                "Constructor was not found in public constructor array on class");
        }
        return new StreamableCallable(classToUse, constructorPosition, args);
    }
    
    protected static class StreamableCallable implements Callable<Object>, Streamable {

        protected Class<? extends Callable<?>> _classCallable;
        protected short _constructorNumber;
        protected Object[] _args;
        
        public StreamableCallable() {
            
        }
        
        public StreamableCallable(Class<? extends Callable<?>> classCallable, 
                                    byte constructorNumber, Object... args) {
            _classCallable = classCallable;
            _constructorNumber = constructorNumber;
            _args = args;
        }

        @Override
        public Object call() throws Exception {
            @SuppressWarnings("unchecked")
            // Unfortunately getConstructors doesn't return typed constructors
            // correctly so we have to cast
            Constructor<? extends Callable<?>> constructor = 
                (Constructor<? extends Callable<?>>) _classCallable
                .getConstructors()[_constructorNumber];
            Callable<?> callable = constructor.newInstance(_args);
            return callable.call();
        }

        @Override
        public void writeTo(DataOutput out) throws Exception {
            Util.writeClass(_classCallable, out);
            out.writeByte(_constructorNumber);
            out.writeByte(_args.length);
            for (Object arg : _args) {
                try {
                    Util.writeObject(arg, out);
                }
                catch (Exception e) {
                    throw new IOException("failed to write arg " + arg);
                }
            }
        }

        @SuppressWarnings("unchecked")
        @Override
        public void readFrom(DataInput in) throws Exception {
            try {
                _classCallable = (Class<? extends Callable<?>>)Util.readClass(in);
            }
            catch (ClassNotFoundException e) {
                throw new IOException("failed to read class from classname", e);
            }
            _constructorNumber = in.readByte();
            short numberOfArgs = in.readByte();
            _args = new Object[numberOfArgs];
            for (int i = 0; i < numberOfArgs; ++i) {
                try {
                    _args[i] = Util.readObject(in);
                }
                catch (Exception e) {
                    throw new IOException("failed to read arg", e);
                }
            }
        }

        // @see java.lang.Object#toString()
        @Override
        public String toString() {
            return "StreamableCallable [class=" + _classCallable
                    + ", constructor=" + _constructorNumber + ", arguments="
                    + Arrays.toString(_args) + "]";
        }
    }
}
