package org.jboss.windup.graph.service;

import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Iterator;

import org.jboss.windup.graph.FramedElementInMemory;
import org.jboss.windup.graph.GraphContext;
import org.jboss.windup.graph.model.InMemoryVertexFrame;
import org.jboss.windup.graph.model.WindupVertexFrame;
import org.jboss.windup.graph.service.exception.NonUniqueResultException;
import org.jboss.windup.util.ExecutionStatistics;
import org.jboss.windup.util.Task;

import com.thinkaurelius.titan.core.TitanTransaction;
import com.thinkaurelius.titan.core.attribute.Text;
import com.thinkaurelius.titan.util.datastructures.IterablesUtil;
import com.tinkerpop.blueprints.GraphQuery;
import com.tinkerpop.blueprints.Vertex;
import com.tinkerpop.frames.FramedGraphQuery;
import com.tinkerpop.frames.VertexFrame;
import com.tinkerpop.frames.modules.typedgraph.TypeValue;
import com.tinkerpop.gremlin.java.GremlinPipeline;

public class GraphService<T extends WindupVertexFrame> implements Service<T>
{
    private final Class<T> type;
    private final GraphContext context;

    public GraphService(GraphContext context, Class<T> type)
    {
        this.context = context;
        this.type = type;
    }

    @SuppressWarnings("unchecked")
    public static <T extends WindupVertexFrame> T refresh(GraphContext context, T frame)
    {
        return (T) context.getFramed().frame(frame.asVertex(), WindupVertexFrame.class);
    }

    @Override
    public void commit()
    {
        ExecutionStatistics.performBenchmarked("GraphService.commit", new Task<Void>()
        {
            @Override
            public Void execute()
            {
                getGraphContext().getGraph().getBaseGraph().commit();
                return null;
            }
        });
    }

    @Override
    public long count(final Iterable<?> obj)
    {
        return ExecutionStatistics.performBenchmarked("GraphService.count", new Task<Long>()
        {
            @Override
            public Long execute()
            {
                GremlinPipeline<Iterable<?>, Object> pipe = new GremlinPipeline<>();
                long result = pipe.start(obj).count();
                return result;
            }
        });
    }

    @SuppressWarnings("unchecked")
    @Override
    public T createInMemory()
    {
        Class<?>[] resolvedTypes = new Class<?>[] { VertexFrame.class, InMemoryVertexFrame.class, type };
        return (T) Proxy.newProxyInstance(this.type.getClassLoader(),
                    resolvedTypes, new FramedElementInMemory<>(this.type));
    }

    /**
     * Create a new instance of the given {@link WindupVertexFrame} type. The ID is generated by the underlying graph database.
     */
    @Override
    public T create()
    {
        return ExecutionStatistics.performBenchmarked("GraphService.create", new Task<T>()
        {
            @Override
            public T execute()
            {
                return context.getFramed().addVertex(null, type);
            }
        });
    }

    @Override
    public T addTypeToModel(final WindupVertexFrame model)
    {
        return ExecutionStatistics.performBenchmarked("GraphService.addTypeToModel", new Task<T>()
        {
            @Override
            public T execute()
            {
                return GraphService.addTypeToModel(getGraphContext(), model, type);
            }
        });
    }

    protected FramedGraphQuery findAllQuery()
    {
        return context.getQuery().type(type);
    }

    @Override
    public Iterable<T> findAll()
    {
        return findAllQuery().vertices(type);
    }

    @Override
    public Iterable<T> findAllByProperties(final String[] keys, final String[] vals)
    {
        return ExecutionStatistics.performBenchmarked("GraphService.findAllByProperties(" + Arrays.asList(keys) + ")", new Task<Iterable<T>>()
        {
            @Override
            public Iterable<T> execute()
            {
                FramedGraphQuery query = findAllQuery();

                for (int i = 0, j = keys.length; i < j; i++)
                {
                    String key = keys[i];
                    String val = vals[i];

                    query = query.has(key, val);
                }

                return query.vertices(type);
            }
        });
    }

    @Override
    public Iterable<T> findAllByProperty(final String key, final Object value)
    {
        return ExecutionStatistics.performBenchmarked("GraphService.findAllByProperty(" + key + ")", new Task<Iterable<T>>()
        {
            @Override
            public Iterable<T> execute()
            {
                return context.getFramed().getVertices(key, value, type);
            }
        });
    }

    @Override
    public Iterable<T> findAllWithoutProperty(final String key, final Object value)
    {
        return ExecutionStatistics.performBenchmarked("GraphService.findAllWithoutProperty(" + key + ")", new Task<Iterable<T>>()
        {
            @Override
            public Iterable<T> execute()
            {
                return findAllQuery().hasNot(key, value).vertices(type);
            }
        });
    }

    @Override
    public Iterable<T> findAllWithoutProperty(final String key)
    {
        return ExecutionStatistics.performBenchmarked("GraphService.findAllWithoutProperty(" + key + ")", new Task<Iterable<T>>()
        {
            @Override
            public Iterable<T> execute()
            {
                return findAllQuery().hasNot(key).vertices(type);
            }
        });
    }

    @Override
    public Iterable<T> findAllByPropertyMatchingRegex(final String key, final String... regex)
    {
        return ExecutionStatistics.performBenchmarked("GraphService.findAllByPropertyMatchingRegex(" + key + ")", new Task<Iterable<T>>()
        {
            @Override
            public Iterable<T> execute()
            {
                if (regex.length == 0)
                    return IterablesUtil.emptyIterable();

                final String regexFinal;
                if (regex.length == 1)
                {
                    regexFinal = regex[0];
                }
                else
                {
                    StringBuilder builder = new StringBuilder();
                    builder.append("\\b(");
                    int i = 0;
                    for (String value : regex)
                    {
                        if (i > 0)
                            builder.append("|");
                        builder.append(value);
                        i++;
                    }
                    builder.append(")\\b");
                    regexFinal = builder.toString();
                }
                return findAllQuery().has(key, Text.REGEX, regexFinal).vertices(type);
            }
        });
    }

    /**
     * Returns the vertex with given ID framed into given interface.
     */
    @Override
    public T getById(Object id)
    {
        return context.getFramed().getVertex(id, this.type);
    }

    @Override
    public T frame(Vertex vertex)
    {
        return getGraphContext().getFramed().frame(vertex, this.getType());
    }

    @Override
    public Class<T> getType()
    {
        return this.type;
    }

    protected GraphQuery getTypedQuery()
    {
        return getGraphContext().getQuery().type(type);
    }

    /**
     * Returns what this' frame has in @TypeValue().
     */
    protected String getTypeValueForSearch()
    {
        TypeValue typeValue = this.type.getAnnotation(TypeValue.class);
        if (typeValue == null)
            throw new IllegalArgumentException("Must be annotated with '@TypeValue': " + this.type.getName());
        return typeValue.value();
    }

    @Override
    public T getUnique() throws NonUniqueResultException
    {
        Iterable<T> results = findAll();

        if (!results.iterator().hasNext())
        {
            return null;
        }

        Iterator<T> iterator = results.iterator();
        T result = iterator.next();

        if (iterator.hasNext())
        {
            throw new NonUniqueResultException("Expected unique value, but returned non-unique.");
        }

        return result;
    }

    @Override
    public T getUniqueByProperty(String property, Object value) throws NonUniqueResultException
    {
        Iterable<T> results = findAllByProperty(property, value);

        if (!results.iterator().hasNext())
        {
            return null;
        }

        Iterator<T> iterator = results.iterator();
        T result = iterator.next();

        if (iterator.hasNext())
        {
            throw new NonUniqueResultException("Expected unique value, but returned non-unique.");
        }

        return result;
    }

    protected T getUnique(GraphQuery framedQuery)
    {
        Iterable<Vertex> results = framedQuery.vertices();

        if (!results.iterator().hasNext())
        {
            return null;
        }

        Iterator<Vertex> iter = results.iterator();
        Vertex result = iter.next();

        if (iter.hasNext())
        {
            throw new NonUniqueResultException("Expected unique value, but returned non-unique.");
        }

        return frame(result);
    }

    protected GraphContext getGraphContext()
    {
        return context;
    }

    @Override
    public TitanTransaction newTransaction()
    {
        return context.getGraph().getBaseGraph().newTransaction();
    }

    /**
     * Adds the specified type to this frame, and returns a new object that implements this type.
     */
    public static <T extends WindupVertexFrame> T addTypeToModel(GraphContext graphContext, WindupVertexFrame frame,
                                                           Class<T> type)
    {
        Vertex vertex = frame.asVertex();
        graphContext.getGraphTypeManager().addTypeToElement(type, vertex);
        return graphContext.getFramed().frame(vertex, type);
    }

    /**
     * Removes the specified type from the frame.
     */
    public static <T extends WindupVertexFrame> T removeTypeFromModel(GraphContext graphContext, WindupVertexFrame frame,
                Class<T> type)
    {
        Vertex vertex = frame.asVertex();
        graphContext.getGraphTypeManager().removeTypeFromElement(type, vertex);
        return graphContext.getFramed().frame(vertex, type);
    }

    @Override
    public void remove(final T model)
    {
        ExecutionStatistics.performBenchmarked("GraphService.commit", new Task<Void>()
        {
            @Override
            public Void execute()
            {
                model.asVertex().remove();
                return null;
            }
        });
    }
}
