package com.tinkerpop.frames;

import java.lang.annotation.Annotation;
import java.lang.ref.SoftReference;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import com.tinkerpop.blueprints.Direction;
import com.tinkerpop.blueprints.Element;
import com.tinkerpop.blueprints.Vertex;
import com.tinkerpop.blueprints.util.ElementHelper;
import com.tinkerpop.frames.annotations.AnnotationHandler;
import com.tinkerpop.frames.modules.MethodHandler;

/**
 * The proxy class of a framed element.
 *
 * @author Marko A. Rodriguez (http://markorodriguez.com)
 */
public class FramedElement implements InvocationHandler {

    private final Direction direction;
    protected final FramedGraph framedGraph;

    private final Object id;
    private final boolean isVertex;
    protected SoftReference<Element> elementReference;
    private static Method hashCodeMethod;
    private static Method equalsMethod;
    private static Method toStringMethod;
    private static Method asVertexMethod;
    private static Method asEdgeMethod;
    private static Map<MethodCallEntry, MethodHandlerEntry> methodCallCache = Collections
                .synchronizedMap(new HashMap<MethodCallEntry, MethodHandlerEntry>());

    static {
        try {
            hashCodeMethod = Object.class.getMethod("hashCode");
            equalsMethod = Object.class.getMethod("equals", new Class[]{Object.class});
            toStringMethod = Object.class.getMethod("toString");
            asVertexMethod = VertexFrame.class.getMethod("asVertex");
            asEdgeMethod = EdgeFrame.class.getMethod("asEdge");
        } catch (NoSuchMethodException e) {
            throw new NoSuchMethodError(e.getMessage());
        }
    }

    /**
     * @deprecated The direction field will be dropped in the next major release
     */
    public FramedElement(final FramedGraph framedGraph, final Element element, final Direction direction) {
        if (null == framedGraph) {
            throw new IllegalArgumentException("FramedGraph can not be null");
        }

        if (null == element) {
            throw new IllegalArgumentException("Element can not be null");
        }

        this.id = element.getId();
        this.isVertex = element instanceof Vertex;
        this.elementReference = new SoftReference<Element>(element);
        this.framedGraph = framedGraph;
        this.direction = direction;
    }

    public FramedElement(final FramedGraph framedGraph, final Element element) {
        this(framedGraph, element, Direction.OUT);
    }

    public Object invoke(final Object proxy, final Method originalMethod, final Object[] arguments) {
        MethodCallEntry methodCallEntry = new MethodCallEntry(proxy.getClass(), originalMethod);

        MethodHandlerEntry methodHandlerEntry = methodCallCache.get(methodCallEntry);
        if (methodHandlerEntry != null)
        {
            // hitCount.incrementAndGet();
            if (methodHandlerEntry.methodHandler != null)
                return methodHandlerEntry.methodHandler.processElement(proxy, methodHandlerEntry.method, arguments, methodHandlerEntry.annotation,
                            this.framedGraph,
                            this.getElement());
            else if (methodHandlerEntry.annotationHandler != null)
                return methodHandlerEntry.annotationHandler.processElement(methodHandlerEntry.annotation, methodHandlerEntry.method, arguments,
                            this.framedGraph, this.getElement(),
                            this.direction);
        }
        Method method = null;
        Class<?> methodInterface = null;

        // try to find the method on one of the proxy's interfaces
        // (the passed in Method is often from a superclass or from the {@link Proxy} object itself,
        // so we need to make sure we find the method that the user actually intended)
        for (Class<?> c : proxy.getClass().getInterfaces())
        {
            if (method != null && c.isAssignableFrom(methodInterface))
            {
                // don't search this class if we already have found a method from a subclass of it
                continue;
            }

            for (Method interfaceMethod : c.getMethods())
            {
                if (compareMethods(originalMethod, interfaceMethod))
                {
                    if (interfaceMethod.getAnnotations().length > 0)
                    {
                        method = interfaceMethod;
                        methodInterface = c;
                    }
                    break;
                }
            }
        }
        if (method == null)
        {
            method = originalMethod;
        }

        Annotation[] annotations = method.getAnnotations();
        Map<Class<? extends Annotation>, AnnotationHandler<?>> annotationHandlers = this.framedGraph.getConfig().getAnnotationHandlers();
        Map<Class<? extends Annotation>, MethodHandler<?>> methodHandlers = this.framedGraph.getConfig().getMethodHandlers();
        for (final Annotation annotation : annotations)
        {
            MethodHandler methodHandler = methodHandlers.get(annotation.annotationType());
            if (methodHandler != null)
            {
                methodCallCache.put(methodCallEntry, new MethodHandlerEntry(method, annotation, methodHandler));
                return methodHandler.processElement(proxy, method, arguments, annotation, this.framedGraph, this.getElement());
            }
        }
        for (final Annotation annotation : annotations)
        {
            AnnotationHandler annotationHandler = annotationHandlers.get(annotation.annotationType());
            if (annotationHandler != null)
            {
                methodCallCache.put(methodCallEntry, new MethodHandlerEntry(method, annotation, annotationHandler));
                return annotationHandler.processElement(annotation, method, arguments, this.framedGraph, this.getElement(), this.direction);
            }
        }
        
        // Now that we have checked for annotations, check if it is one of the default methods that we 
        // have builtin support for
        if (originalMethod.equals(hashCodeMethod)) {
            return this.getElement().hashCode();
        } else if (originalMethod.equals(equalsMethod)) {
            return this.proxyEquals(arguments[0]);
        } else if (originalMethod.equals(toStringMethod)) {
            return this.getElement().toString();
        } else if (originalMethod.equals(asVertexMethod) || originalMethod.equals(asEdgeMethod)) {
            return this.getElement();
        }
        
        if(method.getAnnotations().length == 0) {
            throw new UnhandledMethodException("The method " + method.getDeclaringClass().getName() + "." + method.getName() + " has no annotations, therefore frames cannot handle the method.");
        }
        
        throw new UnhandledMethodException("The method " + method.getDeclaringClass().getName() + "." + method.getName() + " was not annotated with any annotations that the framed graph is configured for. Please check your frame interface and/or graph configuration.");
    }

    /**
     * Returns true if the two methods have the same arguments, return types, and method names.
     */
    private boolean compareMethods(Method m1, Method m2) {
	if (!m1.getName().equals(m2.getName())) {
	    return false;
	}
	if (!m1.getReturnType().equals(m2.getReturnType())) {
	    return false;
	}
        Class<?>[] params1 = m1.getParameterTypes();
        Class<?>[] params2 = m2.getParameterTypes();
        if (params1.length == params2.length) {
            for (int i = 0; i < params1.length; i++) {
                if (params1[i] != params2[i])
                    return false;
            }
            return true;
        }
        return false;
    }
    
    private Boolean proxyEquals(final Object other) {
        if (other instanceof VertexFrame) {
            return this.getElement().equals(((VertexFrame) other).asVertex());
        } if (other instanceof EdgeFrame) {
            return this.getElement().equals(((EdgeFrame) other).asEdge());
        } else if (other instanceof Element) {
            return ElementHelper.areEqual(this.getElement(), other);
        } else {
            return Boolean.FALSE;
        }
    }

    public Element getElement() {
        Element element = elementReference.get();
        if (element == null) {
            if (this.isVertex)
                element = framedGraph.getBaseGraph().getVertex(this.id);
            else
                element = framedGraph.getBaseGraph().getEdge(this.id);

            elementReference = new SoftReference<Element>(element);
        }
        return element;
    }

    private class MethodCallEntry
    {
        private Class clazz;
        private Method method;

        public MethodCallEntry(Class clazz, Method method)
        {
            this.clazz = clazz;
            this.method = method;
        }

        @Override
        public boolean equals(Object o)
        {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            MethodCallEntry that = (MethodCallEntry) o;

            if (clazz != null ? !clazz.equals(that.clazz) : that.clazz != null)
                return false;
            return !(method != null ? !method.equals(that.method) : that.method != null);

        }

        @Override
        public int hashCode()
        {
            int result = clazz != null ? clazz.hashCode() : 0;
            result = 31 * result + (method != null ? method.hashCode() : 0);
            return result;
        }
    }

    private class MethodHandlerEntry
    {
        private final Method method;
        private final Annotation annotation;
        private MethodHandler methodHandler;
        private AnnotationHandler annotationHandler;

        public MethodHandlerEntry(Method method, Annotation annotation, MethodHandler<?> methodHandler)
        {
            this.method = method;
            this.annotation = annotation;
            this.methodHandler = methodHandler;
        }

        public MethodHandlerEntry(Method method, Annotation annotation, AnnotationHandler<?> annotationHandler)
        {
            this.method = method;
            this.annotation = annotation;
            this.annotationHandler = annotationHandler;
        }
    }
}
