package tc.oc.commons.core.reflect;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

import com.google.common.collect.ImmutableList;
import com.google.common.reflect.Invokable;
import com.google.common.reflect.Parameter;
import com.google.common.reflect.TypeToken;
import tc.oc.commons.core.stream.Collectors;
import tc.oc.commons.core.util.Utils;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Equivalent of {@link java.lang.invoke.MethodType} using {@link TypeToken}s
 * in order to support generics.
 */
class GenericMethodType<T> {

    private final TypeToken<T> returnType;
    private final ImmutableList<TypeToken<?>> parameterTypes;

    public GenericMethodType(TypeToken<T> returnType, List<TypeToken<?>> parameterTypes) {
        this.returnType = checkNotNull(returnType);
        this.parameterTypes = ImmutableList.copyOf(parameterTypes);
    }

    public static GenericMethodType<?> of(Method method) {
        return new GenericMethodType<>(TypeToken.of(method.getGenericReturnType()),
                                       Stream.of(method.getGenericParameterTypes())
                                             .map(TypeToken::of)
                                             .collect(Collectors.toImmutableList()));
    }

    public static GenericMethodType<?> of(Invokable<?, ?> invokable) {
        return new GenericMethodType<>(invokable.getReturnType(),
                                       invokable.getParameters()
                                                .stream()
                                                .map(Parameter::getType)
                                                .collect(Collectors.toImmutableList()));
    }

    public TypeToken<T> returnType() {
        return returnType;
    }

    public List<TypeToken<?>> parameterTypes() {
        return parameterTypes;
    }

    @Override
    public int hashCode() {
        return Objects.hash(returnType, parameterTypes);
    }

    @Override
    public boolean equals(Object obj) {
        return Utils.equals(GenericMethodType.class, this, obj, that ->
            this.returnType.equals(that.returnType) && this.parameterTypes.equals(that.parameterTypes)
        );
    }

    @Override
    public String toString() {
        return returnType.toString() +
               "(" +
               parameterTypes.stream()
                             .map(Object::toString)
                             .collect(java.util.stream.Collectors.joining(", ")) +
               ")";
    }

    public GenericMethodType<? extends T> resolveIn(TypeToken<?> context) {
        final TypeToken<? extends T> returnType = (TypeToken<? extends T>) context.resolveType(this.returnType.getType());
        final ImmutableList<TypeToken<?>> parameterTypes = this.parameterTypes.stream()
                                                                              .map(t -> context.resolveType(t.getType()))
                                                                              .collect(Collectors.toImmutableList());
        return returnType.equals(this.returnType) && parameterTypes.equals(this.parameterTypes)
               ? this
               : new GenericMethodType<>(returnType, parameterTypes);
    }

    public boolean canInvoke(GenericMethodType<?> method) {
        if(!returnType.isAssignableFrom(method.returnType)) return false;
        if(parameterTypes.size() != method.parameterTypes.size()) return false;
        for(int i = 0; i < parameterTypes.size(); i++) {
            if(!method.parameterTypes.get(i).isAssignableFrom(parameterTypes.get(i))) return false;
        }
        return true;
    }

    public boolean canInvoke(Invokable<?, ?> invokable) {
        return canInvoke(of(invokable));
    }

    public boolean canInvoke(TypeToken<?> target, Method method) {
        return canInvoke(target.method(method));
    }
}