package com.googlecode.totallylazy.reflection;

import com.googlecode.totallylazy.*;
import com.googlecode.totallylazy.functions.Function1;
import com.googlecode.totallylazy.predicates.LogicalPredicate;
import sun.reflect.generics.repository.MethodRepository;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;

import static com.googlecode.totallylazy.Callers.call;
import static com.googlecode.totallylazy.Classes.allClasses;
import static com.googlecode.totallylazy.Exceptions.handleException;
import static com.googlecode.totallylazy.predicates.Predicates.instanceOf;
import static com.googlecode.totallylazy.Sequences.sequence;

public class Methods {
    public static Method method(String internalName, String name, String descriptior) throws ClassNotFoundException {
        Class<?> aClass = Class.forName(internalName.replace('/', '.'));
        MethodRepository repository = MethodRepository.make(descriptior, Signature.genericsFactory(aClass));
        Sequence<Type> parameterTypes = sequence(repository.getParameterTypes());
        Type returnType = repository.getReturnType();
            return sequence(aClass.getMethods()).
                    filter(m -> m.getName().equals(name)).
                    filter(m -> m.getReturnType().equals(returnType)).
                    filter(m -> sequence(m.getParameterTypes()).equals(parameterTypes)).
                    head();
    }

    public static Function1<Method, String> methodName() {
        return Method::getName;
    }

    public static Function1<Method,? extends Class<?>> returnType() {
        return Method::getReturnType;
    }

    public static Function1<Method, Class<?>[]> parameterTypes() {
        return Method::getParameterTypes;
    }

    public static <T extends Annotation> Function1<Method, T> annotation(final Class<T> annotationClass) {
        return method -> method.getAnnotation(annotationClass);
    }

    public static Function1<Method, Type> genericReturnType() {
        return Method::getGenericReturnType;
    }

    public static Function1<Method, Type[]> genericParameterTypes() {
        return Method::getGenericParameterTypes;
    }

    public static LogicalPredicate<Method> modifier(final int modifier) {
        return new LogicalPredicate<Method>() {
            public boolean matches(Method method) {
                return (method.getModifiers() & modifier) != 0;
            }
        };
    }

    public static Function1<Class<?>, Method> method(final String name, final Class<?>... parameters) {
        return aClass -> aClass.getMethod(name, parameters);
    }

    public static <T> Option<Method> method(T instance, final String name, final Class<?>... parameters) {
        return method(instance.getClass(), name, parameters);
    }

    public static Option<Method> method(Class<?> aClass, String name, final Class<?>... parameters) {
        return call(handleException(method(name, parameters), instanceOf(NoSuchMethodException.class)), aClass);
    }

    public static Function1<Class<?>, Iterable<Method>> methods() {
        return aClass -> sequence(aClass.getMethods());
    }

    public static Function1<Class<?>, Iterable<Method>> declaredMethods() {
        return aClass -> sequence(aClass.getDeclaredMethods());
    }

    public static Sequence<Method> allMethods(Class<?> aClass) {
        return allClasses(aClass).flatMap(Methods.declaredMethods());
    }

    public static <T, R> R invoke(Method method, T instance, Object... arguments) {
        try {
            method.setAccessible(true);
            return Unchecked.cast(method.invoke(instance, arguments));
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw LazyException.lazyException(e);
        }
    }

    public static <R> Function1<Method, R> invokeOn(final Object instance, final Object... arguments) {
        return method -> Unchecked.cast(invoke(method, instance, arguments));
    }
}