package com.tinkerpop.frames;

import com.tinkerpop.blueprints.Direction;
import com.tinkerpop.blueprints.Edge;
import com.tinkerpop.blueprints.Features;
import com.tinkerpop.blueprints.Graph;
import com.tinkerpop.blueprints.Vertex;
import com.tinkerpop.blueprints.util.StringFactory;
import com.tinkerpop.blueprints.util.wrappers.WrapperGraph;
import com.tinkerpop.frames.annotations.AdjacencyAnnotationHandler;
import com.tinkerpop.frames.annotations.AnnotationHandler;
import com.tinkerpop.frames.annotations.DomainAnnotationHandler;
import com.tinkerpop.frames.annotations.InVertexAnnotationHandler;
import com.tinkerpop.frames.annotations.IncidenceAnnotationHandler;
import com.tinkerpop.frames.annotations.OutVertexAnnotationHandler;
import com.tinkerpop.frames.annotations.PropertyAnnotationHandler;
import com.tinkerpop.frames.annotations.RangeAnnotationHandler;
import com.tinkerpop.frames.core.FramedGraphQueryImpl;
import com.tinkerpop.frames.modules.Module;
import com.tinkerpop.frames.modules.TypeResolver;
import com.tinkerpop.frames.structures.FramedEdgeIterable;
import com.tinkerpop.frames.structures.FramedVertexIterable;

import java.lang.annotation.Annotation;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;

/**
 * The primary class for interpreting/framing elements of a graph in terms of
 * particulate annotated interfaces. This is a wrapper graph in
 * that it requires an underlying graph from which to add functionality. The
 * standard Blueprints graph methods are exposed along with extra
 * methods to make framing easy.
 * 
 * @author Marko A. Rodriguez (http://markorodriguez.com)
 */
public class FramedGraph<T extends Graph> implements Graph, WrapperGraph<T> {

	protected final T baseGraph;

	private FramedGraphConfiguration config;
	private boolean configViaFactory;

	/**
	 * @param baseGraph
	 *            The original graph being framed.
	 * @param config
	 *            The configuration for the framed graph.
	 * @param config
	 *            .getConfiguredGraph() The graph being framed after module
	 *            configuration.
	 */
	protected FramedGraph(final T baseGraph, final FramedGraphConfiguration config) {
		this.config = config;
		this.baseGraph = baseGraph;
		configViaFactory = true;
	}

	/**
	 * Construct a FramedGraph that will frame the elements of the underlying
	 * graph.
	 * 
	 * @param baseGraph
	 *            the graph whose elements to frame
	 * @deprecated Use {@link FramedGraphFactory}.
	 */
	@Deprecated
	public FramedGraph(final T baseGraph) {
		this.baseGraph = baseGraph;
		config = new FramedGraphConfiguration();
		config.setConfiguredGraph(baseGraph);
		configViaFactory = false;
		registerAnnotationHandler(new PropertyAnnotationHandler());
		registerAnnotationHandler(new AdjacencyAnnotationHandler());
		registerAnnotationHandler(new IncidenceAnnotationHandler());
		registerAnnotationHandler(new DomainAnnotationHandler());
		registerAnnotationHandler(new RangeAnnotationHandler());
		registerAnnotationHandler(new InVertexAnnotationHandler());
		registerAnnotationHandler(new OutVertexAnnotationHandler());
	}

	private int countParentInterfaces(Class<?> klazz) {
		int result = 0;
		Class<?>[] parents = klazz.getInterfaces();
		if (parents.length > 0) {
			result = result + parents.length;
			for (Class<?> parent : parents) {
				result = result + countParentInterfaces(parent);
			}
		}
		return result;
	}

	private ClassLoader findProxyClassLoader(Collection<Class<?>> classes) {
		ClassLoader result = null;
		int deepest = 0;
		for (Class<?> curClass : classes) {
			int curDepth = countParentInterfaces(curClass);
			// System.out.println("TEMP DEBUG: testing class depth from " +
			// curClass.getName() + " " + (curDepth));
			if (curDepth > deepest) {
				deepest = curDepth;
				result = curClass.getClassLoader();
				// System.out.println("TEMP DEBUG: Setting new depth classloader: "
				// + curDepth + " from "
				// + curClass.getName());
			}
		}
		return result;
	}

	/**
	 * A helper method for framing a vertex. Note that all framed vertices
	 * implement {@link VertexFrame} to allow access to the underlying
	 * element
	 * 
	 * @param vertex
	 *            the vertex to frame
	 * @param kind
	 *            the default annotated interface to frame the vertex as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return a proxy objects backed by a vertex and interpreted from the
	 *         perspective of the annotate interface or null if the vertex
	 *         parameter was null
	 */
	public <F> F frame(final Vertex vertex, final Class<F> kind) {
		if (vertex == null) {
			return null;
		}

		Collection<Class<?>> resolvedTypes = new LinkedHashSet<Class<?>>();
		resolvedTypes.add(VertexFrame.class);
		resolvedTypes.add(kind);

		for (TypeResolver typeResolver : config.getTypeResolvers()) {
			resolvedTypes.addAll(Arrays.asList(typeResolver.resolveTypes(vertex, kind)));
		}
		ClassLoader cl = findProxyClassLoader(resolvedTypes);
		F result = null;
		result = (F) Proxy.newProxyInstance(cl, resolvedTypes.toArray(new Class[resolvedTypes.size()]),
				new FramedElement(this, vertex));
		return result;
	}

	/**
	 * A helper method for framing an edge. Note that all framed edges implement
	 * {@link EdgeFrame} to allow access to the underlying element
	 * 
	 * @param edge
	 *            the edge to frame
	 * @param direction
	 *            the direction of the edges
	 * @param kind
	 *            the default annotated interface to frame the edges as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return a proxy objects backed by an edge and interpreted from the
	 *         perspective of the annotate interface or null if the edge
	 *         paramenter was null
	 * 
	 * @deprecated Use {@link #frame(Edge, Class)}, in combination with
	 *             {@link InVertex} and {@link OutVertex}.
	 */
	@Deprecated
	public <F> F frame(final Edge edge, final Direction direction, final Class<F> kind) {

		if (edge == null) {
			return null;
		}

		Collection<Class<?>> resolvedTypes = new LinkedHashSet<Class<?>>();
		resolvedTypes.add(EdgeFrame.class);
		resolvedTypes.add(kind);
		for (TypeResolver typeResolver : config.getTypeResolvers()) {
			Class<?>[] types = typeResolver.resolveTypes(edge, kind);
			resolvedTypes.addAll(Arrays.asList(types));
		}
		ClassLoader cl = findProxyClassLoader(resolvedTypes);
		return (F) Proxy.newProxyInstance(cl, resolvedTypes.toArray(new Class[resolvedTypes.size()]),
				new FramedElement(this, edge, direction));
	}

	/**
	 * A helper method for framing an edge. Note that all framed edges implement
	 * {@link EdgeFrame} to allow access to the underlying
	 * element.
	 * 
	 * @param edge
	 *            the edge to frame
	 * @param kind
	 *            the default annotated interface to frame the edges as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return a proxy objects backed by an edge and interpreted from the
	 *         perspective of the annotate interface or null if the edge
	 *         paramenter was null
	 */
	public <F> F frame(final Edge edge, final Class<F> kind) {
		return frame(edge, Direction.OUT, kind);
	}

	/**
	 * A helper method for framing an iterable of vertices.
	 * 
	 * @param vertices
	 *            the vertices to frame
	 * @param kind
	 *            the default annotated interface to frame the vertices as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return an iterable of proxy objects backed by a vertex and interpreted
	 *         from the perspective of the annotate interface
	 */
	public <F> Iterable<F> frameVertices(final Iterable<Vertex> vertices, final Class<F> kind) {
		return new FramedVertexIterable<F>(this, vertices, kind);
	}

	/**
	 * A helper method for framing an iterable of edges.
	 * 
	 * @param edges
	 *            the edges to frame
	 * @param direction
	 *            the direction of the edges
	 * @param kind
	 *            the default annotated interface to frame the edges as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return an iterable of proxy objects backed by an edge and interpreted
	 *         from the perspective of the annotate interface
	 * 
	 * @deprecated Use {@link #frameEdges(Iterable, Class)}, in combination with
	 *             {@link InVertex} and {@link OutVertex}.
	 */
	@Deprecated
	public <F> Iterable<F> frameEdges(final Iterable<Edge> edges, final Direction direction, final Class<F> kind) {
		return new FramedEdgeIterable<F>(this, edges, direction, kind);
	}

	/**
	 * A helper method for framing an iterable of edges.
	 * 
	 * @param edges
	 *            the edges to frame
	 * @param direction
	 *            the direction of the edges
	 * @param kind
	 *            the default annotated interface to frame the edges as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return an iterable of proxy objects backed by an edge and interpreted
	 *         from the perspective of the annotate interface
	 */
	public <F> Iterable<F> frameEdges(final Iterable<Edge> edges, final Class<F> kind) {
		return new FramedEdgeIterable<F>(this, edges, kind);
	}

	@Override
	public Vertex getVertex(final Object id) {
		return config.getConfiguredGraph().getVertex(id);
	}

	/**
	 * Frame a vertex according to a particular kind of annotated interface.
	 * 
	 * @param id
	 *            the id of the vertex
	 * @param kind
	 *            the default annotated interface to frame the vertex as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return a proxy object backed by the vertex and interpreted from the
	 *         perspective of the annotate interface
	 */
	public <F> F getVertex(final Object id, final Class<F> kind) {
		return this.frame(getVertex(id), kind);
	}

	@Override
	public Vertex addVertex(final Object id) {
		return config.getConfiguredGraph().addVertex(id);
	}

	/**
	 * Add a vertex to the underlying graph and return it as a framed vertex.
	 * 
	 * @param id
	 *            the id of the newly created vertex
	 * @param kind
	 *            the default annotated interface to frame the vertex as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return a proxy object backed by the vertex and interpreted from the
	 *         perspective of the annotate interface
	 */
	public <F> F addVertex(final Object id, final Class<F> kind) {
		Vertex vertex = addVertex(id);
		for (FrameInitializer initializer : config.getFrameInitializers()) {
			initializer.initElement(kind, this, vertex);
		}
		return this.frame(vertex, kind);
	}

	@Override
	public Edge getEdge(final Object id) {
		return config.getConfiguredGraph().getEdge(id);
	}

	/**
	 * Frame an edge according to a particular kind of annotated interface.
	 * 
	 * @param id
	 *            the id of the edge
	 * @param direction
	 *            the direction of the edge
	 * @param kind
	 *            the default annotated interface to frame the edge as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return a proxy object backed by the edge and interpreted from the
	 *         perspective of the annotate interface
	 * 
	 * @deprecated Use {@link #getEdges(Object, Class)}, in combination with
	 *             {@link InVertex} and {@link OutVertex}.
	 */
	@Deprecated
	public <F> F getEdge(final Object id, final Direction direction, final Class<F> kind) {
		return this.frame(getEdge(id), direction, kind);
	}

	/**
	 * Frame an edge according to a particular kind of annotated interface.
	 * 
	 * @param id
	 *            the id of the edge
	 * @param direction
	 *            the direction of the edge
	 * @param kind
	 *            the default annotated interface to frame the edge as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return a proxy object backed by the edge and interpreted from the
	 *         perspective of the annotate interface
	 */
	public <F> F getEdge(final Object id, final Class<F> kind) {
		return this.frame(getEdge(id), kind);
	}

	@Override
	public Edge addEdge(final Object id, final Vertex outVertex, final Vertex inVertex, final String label) {
		return config.getConfiguredGraph().addEdge(id, outVertex, inVertex, label);
	}

	/**
	 * Add an edge to the underlying graph and return it as a framed edge.
	 * 
	 * @param id
	 *            the id of the newly created edge
	 * @param outVertex
	 *            the outgoing vertex
	 * @param inVertex
	 *            the incoming vertex
	 * @param label
	 *            the label of the edge
	 * @param direction
	 *            the direction of the edge
	 * @param kind
	 *            the default annotated interface to frame the edge as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return a proxy object backed by the edge and interpreted from the
	 *         perspective of the annotate interface
	 * 
	 * @deprecated Use {@link #addEdge(Object, Vertex, Vertex, String, Class)},
	 *             in combination with {@link InVertex} and {@link OutVertex}.
	 */
	@Deprecated
	public <F> F addEdge(final Object id, final Vertex outVertex, final Vertex inVertex, final String label,
			final Direction direction, final Class<F> kind) {
		Edge edge = addEdge(id, outVertex, inVertex, label);
		for (FrameInitializer initializer : config.getFrameInitializers()) {
			initializer.initElement(kind, this, edge);
		}
		return this.frame(edge, direction, kind);
	}

	public <F> F addEdge(final Object id, final Vertex outVertex, final Vertex inVertex, final String label,
			final Class<F> kind) {
		return addEdge(id, outVertex, inVertex, label, Direction.OUT, kind);
	}

	@Override
	public void removeVertex(final Vertex vertex) {
		config.getConfiguredGraph().removeVertex(vertex);
	}

	@Override
	public void removeEdge(final Edge edge) {
		config.getConfiguredGraph().removeEdge(edge);
	}

	@Override
	public Iterable<Vertex> getVertices() {
		return config.getConfiguredGraph().getVertices();
	}

	@Override
	public Iterable<Vertex> getVertices(final String key, final Object value) {
		return config.getConfiguredGraph().getVertices(key, value);
	}

	/**
	 * Frame vertices according to a particular kind of annotated interface.
	 * 
	 * @param key
	 *            the key of the vertices to get
	 * @param value
	 *            the value of the vertices to get
	 * @param kind
	 *            the default annotated interface to frame the vertices as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return an iterable of proxy objects backed by the vertices and
	 *         interpreted from the perspective of the annotate interface
	 */
	public <F> Iterable<F> getVertices(final String key, final Object value, final Class<F> kind) {
		return new FramedVertexIterable<F>(this, config.getConfiguredGraph().getVertices(key, value), kind);
	}

	@Override
	public Iterable<Edge> getEdges() {
		return config.getConfiguredGraph().getEdges();
	}

	@Override
	public Iterable<Edge> getEdges(final String key, final Object value) {
		return config.getConfiguredGraph().getEdges(key, value);
	}

	/**
	 * Frame edges according to a particular kind of annotated interface.
	 * 
	 * @param key
	 *            the key of the edges to get
	 * @param value
	 *            the value of the edges to get
	 * @param direction
	 *            the direction of the edges
	 * @param kind
	 *            the default annotated interface to frame the edges as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return an iterable of proxy objects backed by the edges and interpreted
	 *         from the perspective of the annotate interface
	 * 
	 * @deprecated Use {@link #getEdges(String, Object, Class)}, in combination
	 *             with {@link InVertex} and {@link OutVertex}.
	 */
	@Deprecated
	public <F> Iterable<F> getEdges(final String key, final Object value, final Direction direction, final Class<F> kind) {
		return new FramedEdgeIterable<F>(this, config.getConfiguredGraph().getEdges(key, value), direction, kind);
	}

	/**
	 * Frame edges according to a particular kind of annotated interface.
	 * 
	 * @param key
	 *            the key of the edges to get
	 * @param value
	 *            the value of the edges to get
	 * @param direction
	 *            the direction of the edges
	 * @param kind
	 *            the default annotated interface to frame the edges as
	 * @param <F>
	 *            the default type of the annotated interface
	 * @return an iterable of proxy objects backed by the edges and interpreted
	 *         from the perspective of the annotate interface
	 */
	public <F> Iterable<F> getEdges(final String key, final Object value, final Class<F> kind) {
		return new FramedEdgeIterable<F>(this, config.getConfiguredGraph().getEdges(key, value), kind);
	}

	@Override
	public Features getFeatures() {
		Features features = config.getConfiguredGraph().getFeatures().copyFeatures();
		features.isWrapper = true;
		return features;
	}

	@Override
	public void shutdown() {
		config.getConfiguredGraph().shutdown();
	}

	@Override
	public T getBaseGraph() {
		return this.baseGraph;
	}

	@Override
	public String toString() {
		return StringFactory.graphString(this, this.baseGraph.toString());
	}

	/**
	 * The method used to register a new annotation handler for every new
	 * annotation a new annotation handler has to be registered in the
	 * framed graph
	 * 
	 * @param handler
	 *            the annotation handler
	 * @deprecated Use {@link Module}s via {@link FramedGraphFactory}.
	 */
	@Deprecated
	public void registerAnnotationHandler(final AnnotationHandler<? extends Annotation> handler) {
		checkFactoryConfig();
		config.addAnnotationHandler(handler);
	}

	/**
	 * @param annotationType
	 *            the type of annotation handled by the annotation handler
	 * @return the annotation handler associated with the specified type
	 * @deprecated Use {@link Module}s via {@link FramedGraphFactory}.
	 */
	@Deprecated
	public AnnotationHandler<?> getAnnotationHandler(final Class<? extends Annotation> annotationType) {
		checkFactoryConfig();
		return config.getAnnotationHandlers().get(annotationType);
	}

	/**
	 * @param annotationType
	 *            the type of annotation handled by the annotation handler
	 * @return a boolean indicating if the framedGraph has registered an
	 *         annotation handler for the specified type
	 * @deprecated Use {@link Module}s via {@link FramedGraphFactory}.
	 */
	@Deprecated
	public boolean hasAnnotationHandler(final Class<? extends Annotation> annotationType) {
		checkFactoryConfig();
		return config.getAnnotationHandlers().containsKey(annotationType);
	}

	/**
	 * @param annotationType
	 *            the type of the annotation handler to remove
	 * @deprecated Use {@link Module}s via {@link FramedGraphFactory}.
	 */
	@Deprecated
	public void unregisterAnnotationHandler(final Class<? extends Annotation> annotationType) {
		checkFactoryConfig();
		config.getAnnotationHandlers().remove(annotationType);
	}

	/**
	 * @return
	 * @deprecated Use {@link Module}s via {@link FramedGraphFactory}.
	 */
	@Deprecated
	public Collection<AnnotationHandler<? extends Annotation>> getAnnotationHandlers() {
		checkFactoryConfig();
		return config.getAnnotationHandlers().values();
	}

	/**
	 * Register a <code>FrameInitializer</code> that will be called whenever a
	 * new vertex or edge is added to the graph. The initializer may
	 * mutate the vertex (or graph) before returning the framed element to the
	 * user.
	 * 
	 * @param frameInitializer
	 *            the frame initializer
	 * @deprecated Use {@link Module}s via {@link FramedGraphFactory}.
	 */
	@Deprecated
	public void registerFrameInitializer(final FrameInitializer frameInitializer) {
		checkFactoryConfig();
		config.addFrameInitializer(frameInitializer);
	}

	private void checkFactoryConfig() {
		if (configViaFactory) {
			throw new UnsupportedOperationException("Unsupported for FramedGraph configured by factory");
		}
	}

	protected FramedGraphConfiguration getConfig() {
		return config;
	}

	/**
	 * Generate a query object that can be used to fine tune which
	 * edges/vertices are retrieved from the graph.
	 * 
	 * @return a graph query object with methods for constraining which data is
	 *         pulled from the underlying graph
	 */
	@Override
	public FramedGraphQuery query() {
		return new FramedGraphQueryImpl(this, config.getConfiguredGraph().query());
	}
}