/*
 * @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.functional.FunctionUtils;
import net.ranides.assira.functional.special.AnyFunction;
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.Constructor;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Implementation of IConstructor using reflective data resolution from method.
 *
 * It collects as much reflective data as possible, trying to bypass type erasure.
 *
 * Information about type parameters defined by outer/parent classes are stored inside context.
 *
 * @param <T> T
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
public final class RConstructor<T> extends AConstructor<T> implements Serializable {

    private final Constructor<?> method;
    private final LazySupplier<MethodHandle> handle;

	RConstructor(IContext context, Constructor<?> method) {
        super(context);
		this.method = method;
        this.handle = LazySuppliers.concurrent(() -> {
            try {
				ReflectUtils.access(method);
                return MethodHandles.lookup().unreflectConstructor(method);
            } catch (IllegalAccessException cause) {
                throw new IReflectiveException.AbnormalException(cause);
            }
        });
	}
    
	@Override
	public List<IClass<?>> params() {
        IContext c = context();
        return ListUtils.map(Arrays.asList(method.getTypeParameters()), c::typeinfo);
	}

	@Override
	public IClass<?> returns() {
        return parent();
	}

	@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);
	}

	@Override
	public Object invoke(Object that) {
        if(that != null) {
            throw new IReflectiveException("constructors doesn't support 'this'");
        }
		return MethodUtils.invoke(method);
	}

	@Override
	public Object invoke(Object that, Object... arguments) {
        if(that != null) {
            throw new IReflectiveException("constructors doesn't support 'this'");
        }
		return MethodUtils.invoke(method, arguments);
	}

    @Override
    public Object call(Object... arguments) {
        try {
            return handle.compute().invokeWithArguments(arguments);
        } catch (ResolveException | WrongMethodTypeException cause) {
			throw new IReflectiveException(cause);
		} catch (Throwable cause) {
			throw new IReflectiveException.CallException(cause);
		}
    }
    
	@SuppressWarnings("unchecked")
	@Override
	public Constructor<T> reflective() {
		return (Constructor<T>)method;
	}

	@Override
	public IConstructor<T> accessible() {
		ReflectUtils.access(method);
		return this;
	}

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

	@Override
	public MethodHandle handle() {
        try {
            return MethodHandles.lookup().unreflectConstructor(method);
        } catch (IllegalAccessException cause) {
            throw new IReflectiveException.AbnormalException(cause);
        }
	}

	@Override
	public AnyFunction<T> accessor() {

        Object accessor = IClass.typefor(method)
            .method("acquireConstructorAccessor")
            .orElseThrow(() -> new IReflectiveException("Incompatible JDK: no #acquireConstructorAccessor"))
            .call(method);

        AnyFunction<Object> function = IClass.typefor(accessor)
            .method("newInstance")
            .orElseThrow(() -> new IReflectiveException("Incompatible JDK: no #newInstance"))
            .bind(accessor);

        AnyFunction<Object> wrapper = (Object... args) -> function.call((new Object[]{args}));

        return FunctionUtils.rawAny(wrapper);
	}

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

	@Override
	public IAttributes attributes() {
        return IAttributes.of(method);
	}

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

	@Override
	protected String[] $debugParams() {
		return CDParameters.getParams(method);
	}

	protected Object writeReplace() {
		final IContext cContext = super.context();
		final Constructor<?> cMethod = this.method;
		return SerializationUtils.proxy(() -> FMethod.newConstructor(cContext, cMethod));
	}
	
}
