/*
 * @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.lists.ListUtils;
import net.ranides.assira.collection.query.CQuery;
import net.ranides.assira.generic.LazySupplier;
import net.ranides.assira.generic.LazySuppliers;
import net.ranides.assira.generic.SerializationUtils;
import net.ranides.assira.reflection.*;
import net.ranides.assira.reflection.util.MethodUtils;
import net.ranides.assira.reflection.util.ReflectUtils;

import java.io.Serializable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.WrongMethodTypeException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * This is reflective implementation of IMethod
 *
 * Information about type parameters defined by outer/parent classes are stored inside context.
 *
 * Please note, that the same method can be represented by 2 instances:
 * one if it is retrieved as "declared" from class, second one if it is retrieved as "inherited".
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public class RMethod extends AMethod implements IMethod, Serializable {

	private final boolean declared;
	private final Method method;
	private final LazySupplier<MethodHandle> handle;

    RMethod(boolean declared, IContext context, Method method, Object that) {
        super(context);
		this.method = method;
        this.handle = LazySuppliers.concurrent(() -> {
            try {
				ReflectUtils.access(method);
                MethodHandle m = MethodHandles.lookup().unreflect(method);
                return null!=that ? m.bindTo(that) : m;
            } catch (IllegalAccessException cause) {
                throw new IReflectiveException.AbnormalException(cause);
            }
        });
        this.declared = declared;
	}
    
	@Override
	public List<IClass<?>> params() {
        IContext c = context();
        return ListUtils.map(Arrays.asList(method.getTypeParameters()), c::typeinfo);
	}

	@Override
	public IClass<?> returns() {
        IContext c = context();
        return FClass.newClass(c, method.getGenericReturnType(), method.getAnnotatedReturnType());
	}

	@Override
	public List<IClass<?>> exceptions() {
		IContext c = context();
		return Arrays.stream(method.getExceptionTypes()).map(v -> FClass.newClass(c, v)).collect(Collectors.toList());
	}

	@Override
	public IArguments arguments() {
        IContext c = context();
		return FElements.newArguments(
			IHints.none(),
			CQuery.from().array(method.getParameters()).mapEach((i, v)-> FArgument.newArgument(c, this, v, i))
        );
	}
    
	@Override
	public IClass<?> parent() {
		return context().typeinfo(method.getDeclaringClass());
	}

	@Override
	public Object invoke() {
		return MethodUtils.invoke(method, null);
	}

	@Override
	public Object invoke(Object that) {
		return MethodUtils.invoke(method, that);
	}

	@Override
	public Object invoke(Object that, Object... arguments) {
		return MethodUtils.invoke(method, that, arguments);
	}

    @Override
    public Object call(Object... arguments) {
        try {
            return handle().invokeWithArguments(arguments);
        } catch (ResolveException | WrongMethodTypeException cause) {
			throw new IReflectiveException(cause);
		} catch (Throwable cause) {
			throw new IReflectiveException.CallException(cause);
		}
    }

	@Override
	public Method reflective() {
        return method;
	}

	@Override
	public IMethod accessible() {
		ReflectUtils.access(method);
		return this;
	}

	@Override
	public boolean isAccessible() {
		return ReflectUtils.isAccessible(method);
	}

	@Override
	public MethodHandle handle() {
        return handle.compute();
	}

	@Override
	public String name() {
        return method.getName();
	}

	@Override
	public IAttributes attributes() {
		IAttributes attr = IAttributes.of(method);
        if(declared) {
            attr.add(IAttribute.DECLARED);
        }
        return attr;
	}

	@Override
	public IAnnotations annotations() {
		return FElements.newAnnotations(IHints.none(), CQuery.from().array(method.getAnnotations()));
	}

	protected Object writeReplace() {
		final IContext cContext = super.context();
		final boolean cDeclared = this.declared;
		final Method cMethod = this.method;
		return SerializationUtils.proxy(() -> FMethod.newMethod(cDeclared, cContext, cMethod));
	}

}
