package skadistats.clarity.event;

import skadistats.clarity.processor.runner.Context;
import skadistats.clarity.util.Predicate;

import java.lang.annotation.Annotation;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Method;

public abstract class AbstractInvocationPoint<A extends Annotation> extends UsagePoint<A> implements InvocationPoint<A> {

    protected final int arity;
    protected MethodHandle methodHandle;
    protected Class[] parameterClasses;
    protected Predicate<Object[]> invocationPredicate;

    public AbstractInvocationPoint(A annotation, Class<?> processorClass, Method method, UsagePointMarker marker) {
        super(annotation, processorClass, method, marker);
        this.arity = marker.parameterClasses().length;
        this.parameterClasses = marker.parameterClasses();
    }

    public int getArity() {
        return arity;
    }

    public void setParameterClasses(Class... classes) {
        if (classes.length != arity){
            throw new IllegalArgumentException("supplied parameter classes have wrong arity");
        }
        this.parameterClasses = classes;
    }

    public void setInvocationPredicate(Predicate<Object[]> invocationPredicate) {
        this.invocationPredicate = invocationPredicate;
    }

    private boolean hasContextParameter() {
        Class<?>[] parameterTypes = method.getParameterTypes();
        if (parameterTypes.length == 0) {
            return false;
        }
        if (parameterTypes[0].isAssignableFrom(Context.class)) {
            return true;
        }
        return false;
    }

    @Override
    public void bind(Context ctx) throws IllegalAccessException {
        log.debug("bind %s to context", method);
        MethodHandle boundHandle = MethodHandles.publicLookup().unreflect(method).bindTo(ctx.getProcessor(processorClass));
        if (hasContextParameter()) {
            boundHandle = boundHandle.bindTo(ctx);
        }
        this.methodHandle = boundHandle.asSpreader(Object[].class, arity);
    }

    @Override
    public boolean isInvokedForParameterClasses(Class... classes) throws IllegalArgumentException {
        if (classes.length != arity){
            throw new IllegalArgumentException("supplied parameter classes have wrong arity");
        }
        for (int a = 0; a < arity; a++){
            if (!parameterClasses[a].isAssignableFrom(classes[a])){
                return false;
            }
        }
        return true;
    }

    @Override
    public boolean isInvokedForArguments(Object... args) throws IllegalArgumentException {
        if (args.length != arity){
            throw new IllegalArgumentException("supplied arguments have wrong arity");
        }
        if (invocationPredicate == null) {
            return true;
        }
        return invocationPredicate.apply(args);
    }

    @Override
    public void invoke(Object... args) throws Throwable {
        methodHandle.invokeExact(args);
    }

}