package org.jboss.windup.graph;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import com.tinkerpop.blueprints.Direction;
import com.tinkerpop.blueprints.Edge;
import com.tinkerpop.blueprints.VertexQuery;
import org.jboss.windup.graph.model.InMemoryVertexFrame;
import org.jboss.windup.util.exception.WindupException;

import com.tinkerpop.blueprints.Vertex;
import com.tinkerpop.frames.ClassUtilities;
import com.tinkerpop.frames.Property;
import com.tinkerpop.frames.VertexFrame;

/**
 * This class represents the @Property elements of a framed element in memory.
 * 
 * This in memory representation can be attached to the graph by calling "attach(FramedGraph)"
 * 
 */
public class FramedElementInMemory<T extends VertexFrame> implements InvocationHandler
{
    private final Class<T> type;
    private final Object id;
    private final Map<String, Object> values = new HashMap<>();

    private static final Method attachMethod;
    private static final Method asVertexMethod;
    private static final Method hashCodeMethod;
    private static final Method equalsMethod;
    private static final Method toStringMethod;

    static
    {
        try
        {
            attachMethod = InMemoryVertexFrame.class.getMethod("attachToGraph", new Class[] { GraphContext.class });
            asVertexMethod = VertexFrame.class.getMethod("asVertex");
            hashCodeMethod = Object.class.getMethod("hashCode");
            equalsMethod = Object.class.getMethod("equals", new Class[] { Object.class });
            toStringMethod = Object.class.getMethod("toString");
        }
        catch (NoSuchMethodException e)
        {
            throw new WindupException(e.getMessage(), e);
        }
    }

    public FramedElementInMemory(Class<T> type)
    {
        this(type, null);
    }

    public FramedElementInMemory(Class<T> type, Object id)
    {
        this.type = type;
        this.id = id;
    }

    public Object invoke(final Object proxy, final Method method, final Object[] arguments)
    {
        String methodName = method.getName();
        if (method.equals(hashCodeMethod))
        {
            return this.hashCode();
        }
        else if (method.equals(equalsMethod))
        {
            return this.equals(arguments[0]);
        }
        else if (method.equals(toStringMethod))
        {
            return this.toString();
        }
        else if (method.equals(attachMethod))
        {
            attach((GraphContext)arguments[0]);
            return null;
        }
        else if (method.equals(asVertexMethod))
        {
            return asVertex();
        }

        final String propertyName;
        Property propertyAnnotation = method.getAnnotation(Property.class);
        if (propertyAnnotation == null)
        {
            Property windupPropertyAnnnotation = method.getAnnotation(Property.class);
            if (windupPropertyAnnnotation == null)
            {
                // if this is a getter or setter, try to find the property on the other method
                if (ClassUtilities.isGetMethod(method)) {
                    // get the setter
                    Method setMethod = ClassUtilities.getSetterMethodForGetter(method);
                    windupPropertyAnnnotation = setMethod.getAnnotation(Property.class);
                } else if (ClassUtilities.isSetMethod(method)) {
                    // get the getter
                    Method getMethod = ClassUtilities.getGetterMethodForSetter(method);
                    windupPropertyAnnnotation = getMethod.getAnnotation(Property.class);
                }

                if (windupPropertyAnnnotation == null)
                    throw new WindupException("Method " + methodName
                            + " called, but has no @Property annotation... only @Property methods are supported");
                else
                    propertyName = windupPropertyAnnnotation.value();
            }
            else
            {
                propertyName = windupPropertyAnnnotation.value();
            }
        }
        else
        {
            propertyName = propertyAnnotation.value();
        }

        if (ClassUtilities.isGetMethod(method))
        {
            return values.get(propertyName);
        }
        else if (ClassUtilities.isSetMethod(method))
        {
            Object value = arguments[0];
            if (value == null)
            {
                values.remove(propertyName);
            }
            else
            {
                values.put(propertyName, value);
            }
            return proxy;
        }
        else if (ClassUtilities.isRemoveMethod(method))
        {
            values.remove(propertyName);
            return proxy;
        }
        else
        {
            throw new WindupException("Unrecognized method " + methodName + " called on in-memory Frame!");
        }
    }

    private void attach(GraphContext context)
    {
        T element = context.getFramed().addVertex(null, this.type);
        Vertex v = element.asVertex();
        for (Map.Entry<String, Object> entry : values.entrySet())
        {
            v.setProperty(entry.getKey(), entry.getValue());
        }
    }

    private Vertex asVertex()
    {
        return new Vertex()
        {
            @Override
            public Iterable<Edge> getEdges(Direction direction, String... labels)
            {
                throw new RuntimeException("Method not supported for FramedElementInMemory");
            }

            @Override
            public Iterable<Vertex> getVertices(Direction direction, String... labels)
            {
                throw new RuntimeException("Method not supported for FramedElementInMemory");
            }

            @Override
            public VertexQuery query()
            {
                throw new RuntimeException("Method not supported for FramedElementInMemory");
            }

            @Override
            public Edge addEdge(String label, Vertex inVertex)
            {
                throw new RuntimeException("Method not supported for FramedElementInMemory");
            }

            @Override
            public <T> T getProperty(String key)
            {
                return (T)values.get(key);
            }

            @Override
            public Set<String> getPropertyKeys()
            {
                return values.keySet();
            }

            @Override
            public void setProperty(String key, Object value)
            {
                values.put(key, value);
            }

            @Override
            public <T> T removeProperty(String key)
            {
                return (T)values.remove(key);
            }

            @Override
            public void remove()
            {
                throw new RuntimeException("Method not supported for FramedElementInMemory");
            }

            @Override
            public Object getId()
            {
                return id;
            }
        };
    }

    @Override
    public String toString()
    {
        return "[Proxy for: " + type.getCanonicalName() + ", values: " + values + "]";
    }
}
