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

package net.ranides.assira.reflection.impl;

import java.lang.annotation.Annotation;
import java.lang.invoke.CallSite;
import java.lang.invoke.LambdaConversionException;
import java.lang.invoke.LambdaMetafactory;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandleProxies;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Optional;

import net.ranides.assira.reflection.*;
import net.ranides.assira.text.StrBuilder;
import net.ranides.assira.trace.ExceptionUtils;
import net.ranides.assira.collection.query.CQueryBuilder;
import net.ranides.assira.functional.VarFunction;
import static net.ranides.assira.reflection.IAttribute.*;

/**
 *
 * @author Ranides Atterwim <ranides@gmail.com>
 */
public abstract class AMethod implements IMethod {
    
    private final IContext context;

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

    @Override
    public IContext context() {
        return context;
    }
    	
	@Override
	public final IMethods collect() {
		return FElements.newMethods(AHints.EMPTY, CQueryBuilder.of(this));
	}
    
    @Override
    public <A extends Annotation> Optional<A> annotation(Class<A> type) {
        return annotations().first(type);
    }

    @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 VarFunction bind(Object that) {
        MethodHandle mh = handle().bindTo(that);
        return (args) -> {
            try {
                return mh.invokeWithArguments(args);
            } catch (Throwable cause) { //NOPMD rethrow idiom
                throw ExceptionUtils.rethrow(cause);
            }
        };
	}

    @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();
            
            IMethod method = IClass.typeinfo(rfunctor).methods().require(DECLARED).discard(SYNTHETIC).discard(BRIDGE).first().get();

            // invoke vs invokeExact:
            // http://stackoverflow.com/questions/28196829/java-8-generic-lambdametafactory
            return (F)site(method, functor.raw()).getTarget().invoke();
            
        } catch(Throwable cause) {
            throw ExceptionUtils.rethrow(cause);
        }
    }
    
    private CallSite site(IMethod 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 int hashCode() {
        return name().hashCode();
    }

    @Override
    public final boolean equals(Object object) {
        if(object == this) {
            return true;
        }
        if(!(object instanceof IMethod)) {
            return false;
        }
        IMethod that = (IMethod)object;
        return this.name().equals(that.name())
            && this.arguments().equals(that.arguments())
            && this.returns().equals(that.returns());
    }
    
    @Override
    public String toString() {
        return new StrBuilder()
            .append(returns()).append(" ")
            .append(parent())
            .append(".")
            .append(name())
            .open("(", ")",", ").append(arguments().list()).close()
            .toString();
    }

    protected String[] $debugParams() {
        return CDParameters.getParams(reflective());
    }
	
}
