package tc.oc.pgm.utils;

import java.lang.invoke.MethodHandle;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Nullable;

import com.google.api.client.util.Throwables;
import com.google.common.reflect.TypeToken;
import org.jdom2.Element;
import tc.oc.commons.core.reflect.MethodHandleUtils;
import tc.oc.commons.core.reflect.Types;
import tc.oc.commons.core.util.ExceptionUtils;
import tc.oc.commons.core.util.Optionals;
import tc.oc.pgm.xml.InvalidXMLException;
import tc.oc.pgm.xml.Node;

public class MethodParserMap<T> {

    private class Record {
        final Object target;
        final Method method;
        final MethodHandle handle;
        final boolean passElement;
        final boolean optional;

        private Record(Object target, Method method) {
            this.target = target;
            this.method = method;

            final TypeToken<?> returnType;
            if(Optional.class.isAssignableFrom(method.getReturnType())) {
                optional = true;
                returnType = Optionals.elementType(method.getGenericReturnType());
            } else {
                optional = false;
                returnType = TypeToken.of(method.getGenericReturnType());
            }

            if(!type.isAssignableFrom(returnType)) {
                throw new IllegalStateException("Method " + method + " return type " + returnType + " is not assignable to " + type);
            }

            if(method.getParameterTypes().length == 0) {
                passElement = false;
            } else  {
                if(!(method.getParameterTypes().length == 1 && Element.class.isAssignableFrom(method.getParameterTypes()[0]))) {
                    throw new IllegalStateException("Method " + method + " should take no parameters, or a single Element parameter");
                }
                passElement = true;
            }

            try {
                this.handle = MethodHandleUtils.privateLookup(method.getDeclaringClass())
                                               .unreflect(method)
                                               .bindTo(target);
            } catch(IllegalAccessException e) {
                throw Throwables.propagate(e);
            }
        }

        Object invoke0(Element el) throws InvalidXMLException {
            try {
                if(passElement) {
                    return handle.invoke(el);
                } else {
                    return handle.invoke();
                }
            } catch(InvalidXMLException e) {
                e.offerNode(new Node(el));
                throw e;
            } catch(Throwable e) {
                throw ExceptionUtils.propagate(e);
            }
        }

        T invoke(Element el) throws InvalidXMLException {
            if(optional) {
                throw new IllegalStateException("Invoked optional method as required");
            }
            return (T) invoke0(el);
        }

        Optional<T> tryInvoke(Element el) throws InvalidXMLException {
            return optional ? (Optional<T>) invoke0(el)
                            : Optional.of((T) invoke0(el));
        }
    }

    private final TypeToken<T> type;
    private final Map<String, Record> methods = new HashMap<>();

    public MethodParserMap(@Nullable TypeToken<T> type) {
        this.type = type != null ? type : Types.assertFullySpecified(new TypeToken<T>(getClass()){});
    }

    public void register(String name, Object target, Method method) {
        final Record record = methods.get(name);
        if(record == null || record.method.getDeclaringClass().isAssignableFrom(method.getDeclaringClass())) {
            // Method is new, or overrides a superclass method
            methods.put(name, new Record(target, method));
        } else if(!method.getDeclaringClass().isAssignableFrom(record.method.getDeclaringClass())) {
            // Method is not equal to, or overridden by, the existing one
            throw new IllegalStateException("Conflicting parse method '" + name +
                                            "' declared in " + record.method.getDeclaringClass().getName() +
                                            " and " + method.getDeclaringClass().getName());
        }
    }

    public void register(Object target, Method method) {
        final MethodParser annot = method.getAnnotation(MethodParser.class);
        if(annot != null) {
            if(annot.value().length == 0) {
                register(method.getName().replace('_', '-'), target, method);
            } else {
                for(String name : annot.value()) {
                    register(name, target, method);
                }
            }
        }
    }

    public void register(Object target) {
        for(Class<?> cls : Types.ancestors(target.getClass())) {
            for(Method method : cls.getDeclaredMethods()) {
                register(target, method);
            }
        }
    }

    public boolean hasMethod(String name) {
        return methods.containsKey(name);
    }

    public boolean canParse(Element el) {
        return hasMethod(el.getName());
    }

    private Record record(Element el) throws InvalidXMLException {
        final Record record = methods.get(el.getName());
        if(record == null) {
            throw new InvalidXMLException("Unrecognized element", el);
        }
        return record;
    }

    public T parse(Element el) throws InvalidXMLException {
        return record(el).invoke(el);
    }

    public Optional<T> tryParse(Element el) throws InvalidXMLException {
        return record(el).tryInvoke(el);
    }
}