package org.jboss.windup.config.query;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import com.google.common.collect.Iterables;
import org.jboss.forge.furnace.util.Predicate;
import org.jboss.windup.config.GraphRewrite;
import org.jboss.windup.config.Variables;
import org.jboss.windup.config.condition.GraphCondition;
import org.jboss.windup.config.operation.Iteration;
import org.jboss.windup.config.selectors.FramesSelector;
import org.jboss.windup.graph.GraphTypeManager;
import org.jboss.windup.graph.frames.VertexFromFramedIterable;
import org.jboss.windup.graph.model.WindupVertexFrame;
import org.jboss.windup.util.ExecutionStatistics;
import org.jboss.windup.util.Task;
import org.ocpsoft.rewrite.config.ConditionBuilder;
import org.ocpsoft.rewrite.context.EvaluationContext;

import com.tinkerpop.blueprints.Vertex;
import com.tinkerpop.frames.FramedGraphQuery;
import com.tinkerpop.frames.structures.FramedVertexIterable;
import com.tinkerpop.gremlin.java.GremlinPipeline;
import com.tinkerpop.pipes.PipeFunction;

public class Query extends GraphCondition implements QueryBuilderFind, QueryBuilderFrom, QueryBuilderWith,
            QueryBuilderPiped
{
    private String outputVar = Iteration.DEFAULT_VARIABLE_LIST_STRING;

    private final List<QueryGremlinCriterion> pipelineCriteria = new ArrayList<>();

    private Class<? extends WindupVertexFrame> searchType;

    private FramesSelector framesSelector;

    private Predicate<WindupVertexFrame> resultFilter;

    private Query()
    {
    }

    /**
     * Begin this {@link Query} with all Frame instances that are the result of the provided GremlinQueryCriterion.
     */
    public static QueryBuilderPiped gremlin(final QueryGremlinCriterion criterion)
    {
        return new Query().piped(criterion);
    }

    /**
     * Begin this {@link Query} with all {@link WindupVertexFrame} instances of the given type.
     */
    public static QueryBuilderFind fromType(Class<? extends WindupVertexFrame> type)
    {
        final Query query = new Query();
        // this query is going to be added after evaluate() method, because in some cases we need gremlin and in some
        // frames
        query.searchType = type;
        return query;
    }

    /**
     * Excludes Vertices that are of the provided type.
     */
    @Override
    public QueryBuilderFind excludingType(final Class<? extends WindupVertexFrame> type)
    {
        pipelineCriteria.add(new QueryGremlinCriterion()
        {
            @Override
            public void query(GraphRewrite event, GremlinPipeline<Vertex, Vertex> pipeline)
            {
                pipeline.filter(new PipeFunction<Vertex, Boolean>()
                {
                    @Override
                    public Boolean compute(Vertex argument)
                    {
                        return !GraphTypeManager.hasType(type, argument);
                    }
                });
            }
        });
        return this;
    }

    /**
     * Includes Vertices that are of the provided type.
     */
    @Override
    public QueryBuilderFind includingType(final Class<? extends WindupVertexFrame> type)
    {
        pipelineCriteria.add(new QueryGremlinCriterion()
        {
            @Override
            public void query(GraphRewrite event, GremlinPipeline<Vertex, Vertex> pipeline)
            {
                pipeline.filter(new PipeFunction<Vertex, Boolean>()
                {
                    @Override
                    public Boolean compute(Vertex argument)
                    {
                        return GraphTypeManager.hasType(type, argument);
                    }
                });
            }
        });
        return this;
    }

    /**
     * Begin this {@link Query} with results of a prior {@link Query}, read from the variable with the given name.
     */
    public static QueryBuilderFrom from(final String sourceVarName)
    {
        final Query query = new Query();
        query.setInputVariablesName(sourceVarName);
        return query;
    }

    @Override
    public ConditionBuilder as(String outputVarName)
    {
        outputVar = outputVarName;
        return this;
    }

    /*
     * Evaluators
     */

    @Override
    public boolean evaluate(final GraphRewrite event, final EvaluationContext context)
    {
        final String queryStr = toString();
        return ExecutionStatistics.performBenchmarked(queryStr, new Task<Boolean>()
        {
            public Boolean execute()
            {
                Query.this.setInitialFramesSelector(createInitialFramesSelector(Query.this));
                Iterable<? extends WindupVertexFrame> result = framesSelector.getFrames(event, context);
                if (resultFilter != null)
                {
                    com.google.common.base.Predicate<WindupVertexFrame> guavaPred= new com.google.common.base.Predicate<WindupVertexFrame>() {
                        @Override public boolean apply(WindupVertexFrame input)
                        {
                            return resultFilter.accept(input);
                        }
                    };
                    result = Iterables.filter(result, guavaPred);
                }

                setResults(event, outputVar, result);
                return result.iterator().hasNext();
            }
        });
    }

    /*
     * Criteria
     */

    @Override
    public QueryBuilderWith withProperty(String property, Object searchValue)
    {
        return withProperty(property, QueryPropertyComparisonType.EQUALS, searchValue);
    }

    @Override
    public QueryBuilderWith withProperty(String property, Iterable<?> values)
    {
        pipelineCriteria.add(new QueryPropertyCriterion(property, QueryPropertyComparisonType.CONTAINS_ANY_TOKEN, values));
        return this;
    }

    @Override
    public QueryBuilderWith withProperty(String property)
    {
        pipelineCriteria.add(new QueryPropertyCriterion(property, QueryPropertyComparisonType.DEFINED, null));
        return this;
    }

    @Override
    public QueryBuilderWith withoutProperty(String property)
    {
        pipelineCriteria.add(new QueryPropertyCriterion(property, QueryPropertyComparisonType.NOT_DEFINED, null));
        return this;
    }

    private static FramesSelector createInitialFramesSelector(final Query query)
    {
        return new FramesSelector()
        {
            @Override
            public Iterable<WindupVertexFrame> getFrames(GraphRewrite event, EvaluationContext context)
            {
                Iterable<Vertex> startingVertices = getStartingVertices(event);
                GremlinPipeline<Vertex, Vertex> pipeline = new GremlinPipeline<>(startingVertices);
                Set<WindupVertexFrame> frames = new HashSet<>();
                for (QueryGremlinCriterion c : query.getPipelineCriteria())
                {
                    c.query(event, pipeline);
                }

                FramedVertexIterable<WindupVertexFrame> framedVertexIterable = new FramedVertexIterable<>(
                            event.getGraphContext().getFramed(), pipeline,
                            WindupVertexFrame.class);
                for (WindupVertexFrame frame : framedVertexIterable)
                {
                    frames.add(frame);
                }
                return frames;
            }

            private Iterable<Vertex> getStartingVertices(GraphRewrite event)
            {
                boolean hasStartingVerticesVariable = query.getInputVariablesName() != null
                            && !query.getInputVariablesName().isEmpty();
                Iterable<Vertex> startingVertices;
                if (hasStartingVerticesVariable)
                {
                    // save the type as a gremlin criterion
                    if (query.searchType != null)
                    {
                        query.piped(new QueryTypeCriterion(query.searchType));
                    }
                    Variables variables = (Variables) event.getRewriteContext().get(Variables.class);
                    Iterable<? extends WindupVertexFrame> frames = variables.findVariable(query.getInputVariablesName());
                    return new VertexFromFramedIterable(frames);
                }
                else
                {
                    FramedGraphQuery framesQueryType = event.getGraphContext().getFramed().query();
                    if (query.searchType != null)
                    {
                        new QueryTypeCriterion(query.searchType).query(framesQueryType);
                        startingVertices = framesQueryType.vertices();
                        return startingVertices;
                    }

                }
                return event.getGraphContext().getGraph().getVertices();
            }
        };
    }

    @Override
    public QueryBuilderWith withProperty(String property, Object searchValue, Object... searchValues)
    {
        List<Object> values = new LinkedList<>();
        values.add(searchValue);
        values.addAll(Arrays.asList(searchValues));

        return withProperty(property, values);
    }

    @Override
    public QueryBuilderWith withProperty(String property, QueryPropertyComparisonType searchType,
                Object searchValue)
    {
        pipelineCriteria.add(new QueryPropertyCriterion(property, searchType, searchValue));
        return this;
    }

    @Override
    public QueryBuilderPiped piped(QueryGremlinCriterion criterion)
    {
        pipelineCriteria.add(criterion);
        return this;
    }

    private void setInitialFramesSelector(FramesSelector selector)
    {
        this.framesSelector = selector;
    }

    public Collection<QueryGremlinCriterion> getPipelineCriteria()
    {
        return pipelineCriteria;
    }

    @Override
    @SuppressWarnings("unchecked")
    public <FRAMETYPE extends WindupVertexFrame> QueryBuilderAs filteredBy(Predicate<FRAMETYPE> predicate)
    {
        this.resultFilter = (Predicate<WindupVertexFrame>) predicate;
        return this;
    }

    public String toString()
    {
        StringBuilder builder = new StringBuilder();
        builder.append("Query");
        if (searchType != null)
        {
            builder.append(".fromType(").append(searchType.getName()).append(")");
        }

        if (!pipelineCriteria.isEmpty())
        {
            builder.append(".gremlin()");
            for (QueryGremlinCriterion criterion : pipelineCriteria)
            {
                builder.append(criterion);
            }
        }
        builder.append(".as(" + outputVar + ")");
        return builder.toString();
    }

}
