/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */

package net.ranides.assira.reflection.impl;

import net.ranides.assira.collection.arrays.ArrayUtils;
import net.ranides.assira.functional.special.AnyFunction;
import net.ranides.assira.reflection.*;
import net.ranides.assira.trace.ExceptionUtils;

import java.io.Serializable;
import java.lang.invoke.CallSite;
import java.lang.invoke.LambdaConversionException;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.invoke.WrongMethodTypeException;

import static net.ranides.assira.reflection.IAttribute.*;

/**
 * Base abstract class for IExecutable implementations.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public abstract class AExecutable extends AElement implements IExecutable, Serializable {

    private static final long serialVersionUID = 1L;

    private final IContext context;

    protected AExecutable(IContext context) {
        this.context = context;
    }

    @Override
    public final IContext context() {
        return context;
    }

    @Override
    public final boolean declares(IArguments arguments) {
        return declares(arguments.types());
    }

    @Override
    public final boolean declares(IClasses arguments) {
        return this.arguments().types().raw().list().equals(arguments.raw().list());
    }

    @Override
    public final boolean declares(IClass<?>... arguments) {
        return declares(IClasses.typeinfo(arguments));
    }

    @Override
    public final boolean declares(Class<?>... arguments) {
        return declares(IClasses.typeinfo(arguments));
    }

    @Override
    public final boolean declares() {
        return declares(IClasses.empty());
    }

    @Override
    public final boolean matches(IArguments arguments) {
        return this.arguments().types().isSuper(arguments.types());
    }

    @Override
    public final boolean matches(IClasses arguments) {
        return this.arguments().types().isSuper(arguments);
    }
    
    @Override
    public final boolean matches(IClass<?>... arguments) {
        return matches(IClasses.typeinfo(arguments));
    }
    
    @Override
    public final boolean matches(Class<?>... arguments) {
        return matches(IClasses.typeinfo(arguments));
    }
    
    @Override
    public final boolean matches() {
        return matches(IClasses.empty());
    }

    @Override
    public final boolean accepts(Object... arguments) {
        return this.arguments().types().isInstance(arguments);
    }
    
    @Override
    public final boolean $accepts1(Object... arguments) {
        return this.arguments().types().$isInstance1(arguments);
    }

    @SuppressWarnings("unchecked")
    @Override
    public final <T> T $invoke() {
        return (T)invoke();
    }

    @SuppressWarnings("unchecked")
    @Override
    public final <T> T $invoke(Object that) {
        return (T)invoke(that);
    }

    @SuppressWarnings("unchecked")
    @Override
    public final <T> T $invoke(Object that, Object... arguments) {
        return (T)invoke(that, arguments);
    }

    @SuppressWarnings("unchecked")
    @Override
    public final <T> T $call(Object... arguments) {
        return (T)call(arguments);
    }

    @SuppressWarnings("unchecked")
    @Override
    public final <T> T $apply(Object[] arguments) {
        return (T)apply(arguments);
    }

    @SuppressWarnings("unchecked")
    @Override
    public final <T> T $apply(Object that, Object[] arguments) {
        return (T)apply(that, arguments);
    }

	@Override
	public AnyFunction<Object> bind(Object that) {
        MethodHandle mh = handle().bindTo(that);
        return (args) -> {
            try {
                return mh.invokeWithArguments(args);
            } catch (WrongMethodTypeException cause) {
                throw new IReflectiveException(cause);
            } catch (Throwable cause) {
                throw ExceptionUtils.rethrow(cause);
            }
        };
	}
    
    @Override
    public Object apply(Object[] arguments) {
        return call(arguments);
    }

    @Override
    public Object apply(Object that, Object[] arguments) {
        return call(ArrayUtils.prepend(arguments, that));
    }

    @Override
    public <F> F handle(Class<F> functor) {
        return handle(IClass.typeinfo(functor));
    }

    @Override
    public <F> F handle(IClass<F> functor) {
        try {
            Class<?> rfunctor = functor.raw();
            
            IExecutable method = IClass.typeinfo(rfunctor).methods()
                .require(DECLARED)
                .discard(SYNTHETIC)
                .discard(BRIDGE)
                .first()
                .orElseThrow(() -> new IReflectiveException("Method handle for " + this + " not found"));

            // invoke vs invokeExact:
            // http://stackoverflow.com/questions/28196829/java-8-generic-lambdametafactory
            @SuppressWarnings("unchecked")
            F result = (F) site(method, functor.raw()).getTarget().invoke();
            return result;
            
        } catch (ResolveException | WrongMethodTypeException cause) {
            throw new IReflectiveException(cause);
        } catch (IReflectiveException cause) {
            throw cause;
        } catch (Throwable cause) {
            throw new IReflectiveException.CallException(cause);
        }
    }
    
    private CallSite site(IExecutable method, Class<?> functor) throws LambdaConversionException {
        MethodHandle handle = handle();
        
        MethodType signature = MethodType.methodType(
            method.returns().raw(), 
            method.arguments().types().raw().array(Class[]::new)
        );
        
        return LambdaMetafactory.metafactory(
            MethodHandles.lookup(), 
            method.name(), 
            MethodType.methodType(functor), 
            signature, 
            handle, 
            handle.type()
        );
    }

    @Override
    public final boolean isAbstract() {
        return attribute(IAttribute.ABSTRACT);
    }

    @Override
    public final boolean isSynchronized() {
        return attribute(IAttribute.SYNCHRONIZED);
    }

    @Override
    public final boolean isBridge() {
        return attribute(IAttribute.BRIDGE);
    }

    @Override
    public final boolean isNative() {
        return attribute(IAttribute.NATIVE);
    }

    @Override
    public final boolean isLambda() {
        return attribute(IAttribute.LAMBDA);
    }
  
    @Override
    public final int hashCode() {
        return name().hashCode();
    }

    @Override
    public final boolean equals(Object object) {
        if(object == this) {
            return true;
        }
        if(!(object instanceof IExecutable)) {
            return false;
        }
        IExecutable that = (IExecutable)object;
        return this.name().equals(that.name())
            && this.arguments().equals(that.arguments())
            && this.returns().equals(that.returns());
    }

    protected abstract String[] $debugParams();

}
