/*
 * @author Ranides Atterwim <ranides@gmail.com>
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.reflection;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.Callable;
import net.ranides.assira.asm.MethodInspectorVM;
import net.ranides.assira.collection.ArrayUtils;
import net.ranides.assira.generic.Function;
import net.ranides.assira.generic.LazyRef;
import net.ranides.assira.generic.Serializer;
import net.ranides.assira.reflection.inspect.MethodInspector;
import net.ranides.assira.reflection.util.ClassUtils;
import net.ranides.assira.reflection.util.MemberUtils;
import net.ranides.assira.reflection.util.MethodUtils;
import net.ranides.assira.reflection.util.ReflectFormat;
import net.ranides.assira.reflection.util.ReflectUtils;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public final class MethodFactory {
    
    private MethodFactory() {
        // utility class
    }
    
    public static <T> ConstructorPointer<T> wrap(Constructor<T> ctor) {
        return new ConstructorPointer<>(ctor);
    }
    
    public static MethodPointer<?> wrap(Method method) {
        return new MethodPointer(method);
    }
    
    public static <R> FunctionPointer<R> wrap(Function<R,?> function) {
        return new FunctionPointer<>(function);
    }
    
    public static abstract class ReflectivePointer<R> implements GenericMethod<R> {
        
        protected final LazyRef<List<GenericParam>> params = LazyRef.lockfree(new Callable<List<GenericParam>>() {
            @Override
            public List<GenericParam> call() throws Exception {
                return iparams();
            }
        });
        
        protected final LazyRef<GenericClass> type = LazyRef.lockfree(new Callable<GenericClass>() {
            @Override
            public GenericClass call() throws Exception {
                return itype();
            }
        });
        
        protected abstract GenericClass itype();
        
        protected abstract List<GenericParam> iparams();

        @Override
        public final GenericClass type() {
            return type.get();
        }

        @Override
        public final List<GenericParam> params() {
            return params.get();
        }
        
        @Override
        public final void run() {
            call();
        }
        
        @Override
        public final R call() {
            return invoke(null);
        }
        
    }
    
    public static class ConstructorPointer<R> extends ReflectivePointer<R> {
        
        protected final Constructor<R> method;

        protected ConstructorPointer(ConstructorPointer<R> pointer) {
            this.method = pointer.method;
        }
        
        protected ConstructorPointer(Constructor<R> ctor) {
            this.method = ReflectUtils.access(ctor);
        }
        
        @Override
        protected GenericClass itype() {
            return TypeFactory.construct( method.getDeclaringClass() );
        }

        @Override
        protected List<GenericParam> iparams() {
            return MethodInspectorVM.params(method);
        }

        @Override
        public int paramsize() {
            return method.getParameterTypes().length;
        }
                
        @Override
        public String name() {
            return method.getName();
        }

        @Override
        public GenericMethod bind(Object that) {
            throw new UnsupportedOperationException("Constructor can't bind this.");
        }
        
        @Override
        public R call(Object... arguments) {
            return invoke(null, arguments);
        }
        
        @Override
        public R invoke(Object that, Object... arguments) {
            try {
                return method.newInstance(arguments);
            } catch (InstantiationException | IllegalAccessException | IllegalArgumentException ex) {
                throw new InspectException(ex);
            } catch (InvocationTargetException ex) {
                throw new InvokeException(ex);
            }
        }
        
        @Override
        public String toString() {
            return ReflectFormat.signature(method);
        }

        @Override
        public boolean canCall(Object... object) {
            return ClassUtils.isInstance(method.getParameterTypes(), object);
        }

        @Override
        public boolean canInvoke(Object that, Object... arguments) {
            return (null == that) && canCall(arguments);
        }
        
        public Constructor<?> reflective() {
            return method;
        }
        
    }
    
    public static class MethodPointer<R> extends ReflectivePointer<R> implements Serializable {
		
		private static final long serialVersionUID = 1L;
        
        protected final Method method;
		
		protected MethodPointer(Class<?> clazz, String name, Class[] params) {
			try {
				this.method = ReflectUtils.access(clazz.getDeclaredMethod(name, params));
			} catch (ReflectiveOperationException ex) {
				throw new IllegalArgumentException("Method or class change after serialization: " + clazz + "." + name);
			}
		}

        protected MethodPointer(Method method) {
            this.method = ReflectUtils.access(method);
        }
        
        protected MethodPointer(MethodPointer<R> pointer) {
            this.method = pointer.method;
        }

        @Override
        protected GenericClass itype() {
            return MethodUtils.typeof(method);
        }

        @Override
        protected List<GenericParam> iparams() {
            return MethodUtils.params(method);
        }
        
        @Override
        public int paramsize() {
            return method.getParameterTypes().length;
        }
        
        @Override
        public String name() {
            return method.getName();
        }

        @Override
        public GenericMethod bind(Object that) {
            if( MemberUtils.isStatic(method) ) {
                throw new UnsupportedOperationException("Static method can't bind this.");
            }
            return new BindPointer(this, that);
        }
        
        @Override
        public R call(Object... arguments) {
            if( MemberUtils.isStatic(method) ) {
                return (R)invoke(null, arguments);
            } else {
                return (R)invoke(arguments[0], ArrayUtils.slice(arguments, 1, arguments.length));
            }
        }
        
        @Override
        public R invoke(Object that, Object... arguments) {
            try {
                return (R)method.invoke(that, arguments);
            } catch (IllegalAccessException | IllegalArgumentException ex) {
                throw new InspectException(ex);
            } catch (InvocationTargetException ex) {
                throw new InvokeException(ex);
            }
        }

        @Override
        public boolean canCall(Object... arguments) {
            if( MemberUtils.isStatic(method)) {
                return ClassUtils.isInstance(method.getParameterTypes(), arguments);
            }
            if( !method.getDeclaringClass().isInstance(arguments[0]) ) {
                return false;
            }
            Class[] array = method.getParameterTypes();
            if( arguments.length-1 != array.length) {
                return false;
            }
            for(int i=0; i<array.length; i++) {
                if(!ClassUtils.isInstance(array[i], arguments[i+1])) {
                    return false;
                }
            }
            return true;
        }

        @Override
        public boolean canInvoke(Object that, Object... arguments) {
            if( !MemberUtils.isStatic(method) && !method.getDeclaringClass().isInstance(that) ) {
                return false;
            }
            return ClassUtils.isInstance(method.getParameterTypes(), arguments);
        }

		@Override
		public int hashCode() {
			return method.hashCode();
		}

		@Override
		public boolean equals(Object object) {
			if (object == null) {
				return false;
			}
			if (getClass() != object.getClass()) {
				return false;
			}
			return method.equals(((MethodPointer<?>) object).method);
		}
        
        @Override
        public String toString() {
            return ReflectFormat.signature(method);
        }
        
        public Method reflective() {
            return method;
        }
		
		private Object writeReplace() {
			return Serializer.proxy(this, method.getDeclaringClass(), method.getName(), method.getParameterTypes());
		}
        
    }
    
    public static class FunctionPointer<R> implements GenericMethod<R> {
        
        protected final Function<R,?> function;
        
        protected final LazyRef<List<GenericParam>> params = LazyRef.lockfree(new Callable<List<GenericParam>>() {
            @Override
            public List<GenericParam> call() throws Exception {
                return MethodInspector.inobject(function).published().name("apply").first().params();
            }
        });
        
        protected final LazyRef<GenericClass> type = LazyRef.lockfree(new Callable<GenericClass>() {
            @Override
            public GenericClass call() throws Exception {
                return MethodInspector.inobject(function).published().name("apply").first().type();
            }
        });

        protected FunctionPointer(Function<R, ?> function) {
            this.function = function;
        }
        
        @Override
        public GenericClass type() {
            return type.get();
        }

        @Override
        public List<GenericParam> params() {
            return params.get();
        }
        
        @Override
        public int paramsize() {
            return 1;
        }
        
        @Override
        public String name() {
            return function.getClass().getName();
        }

        @Override
        public GenericMethod bind(Object that) {
            if(that != null) {
                throw new UnsupportedOperationException("Function can't use 'this' argument.");
            }
            return this;
        }
        
        @Override
        public void run() {
            throw new UnsupportedOperationException("Function requires exactly one argument");
        }
        
        @Override
        public R call() {
            throw new UnsupportedOperationException("Function requires exactly one argument");
        }

        @Override
        public R call(Object... arguments) {
            return invoke(null, arguments);
        }
        
        @Override
        public R invoke(Object that, Object... params) {
            if(that != null) {
                throw new UnsupportedOperationException("Function can't use 'this' argument.");
            }
            return ((Function<R,Object>)function).apply(params[0]);
        }

        @Override
        public boolean canCall(Object... arguments) {
            if( arguments.length != 1) {
                return false;
            }
            return params().get(0).type().rawType().isInstance(arguments[0]);
        }

        @Override
        public boolean canInvoke(Object that, Object... arguments) {
            return (null == that) && canCall(arguments);
        }
        
        @Override
        public String toString() {
            return function.toString();
        }
        
    }
    
    private static final class BindPointer implements GenericMethod {
        
        private final GenericMethod pointer;
        private final Object that;

        BindPointer(GenericMethod pointer, Object that) {
            this.pointer = pointer;
            this.that = that;
        }
        
        @Override
        public String name() {
            return pointer.name();
        }

        @Override
        public GenericClass type() {
            return pointer.type();
        }

        @Override
        public List<GenericParam> params() {
            return pointer.params();
        }
        
        @Override
        public int paramsize() {
            return pointer.paramsize();
        }

        @Override
        public GenericMethod bind(Object that) {
            return new BindPointer(pointer, that);
        }
        
        @Override
        public void run() {
            pointer.invoke(that);
        }
        
        @Override
        public Object call() {
            return pointer.invoke(that);
        }
        
        @Override
        public Object call(Object... arguments) {
            return pointer.invoke(that, arguments);
        }

        @Override
        public Object invoke(Object that, Object... arguments) {
            throw new UnsupportedOperationException("Already bound.");
        }

        @Override
        public boolean canCall(Object... arguments) {
            return pointer.canInvoke(that, arguments);
        }

        @Override
        public boolean canInvoke(Object that, Object... arguments) {
            return false;
        }
        
        @Override
        public String toString() {
            return pointer.toString();
        }
        
    }
}
