/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.tinkerpop.gremlin.sparql;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.jena.query.Query;
import org.apache.jena.query.QueryFactory;
import org.apache.jena.query.SortCondition;
import org.apache.jena.query.Syntax;
import org.apache.jena.sparql.algebra.Algebra;
import org.apache.jena.sparql.algebra.Op;
import org.apache.jena.sparql.algebra.OpVisitorBase;
import org.apache.jena.sparql.algebra.OpWalker;
import org.apache.jena.sparql.algebra.op.OpBGP;
import org.apache.jena.sparql.algebra.op.OpFilter;
import org.apache.jena.sparql.algebra.op.OpLeftJoin;
import org.apache.jena.sparql.algebra.op.OpUnion;
import org.apache.jena.sparql.core.Var;
import org.apache.jena.sparql.core.VarExprList;
import org.apache.jena.sparql.expr.Expr;
import org.apache.jena.sparql.expr.ExprAggregator;
import org.apache.tinkerpop.gremlin.process.traversal.Order;
import org.apache.tinkerpop.gremlin.process.traversal.Scope;
import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversal;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Vertex;

/**
 * The engine that compiles SPARQL to Gremlin traversals thus enabling SPARQL to be executed on any TinkerPop-enabled
 * graph system.
 */
public class SparqlToGremlinCompiler {

    private GraphTraversal<Vertex, ?> traversal;

    private List<Traversal> traversalList = new ArrayList<>();
            List<Traversal> optionalTraversals = new ArrayList<Traversal>();
            List<String> optionalVariable = new ArrayList<String>();
            boolean optionalFlag = false;

    private SparqlToGremlinCompiler(final GraphTraversal<Vertex, ?> traversal) {
        this.traversal = traversal;
    }

    private SparqlToGremlinCompiler(final GraphTraversalSource g) {
        this(g.V());
    }

    /**
     * Converts SPARQL to a Gremlin traversal.
     *
     * @param graph       the {@link Graph} instance to execute the traversal from
     * @param sparqlQuery the query to compile to Gremlin
     */
    public static GraphTraversal<Vertex, ?> compile(final Graph graph, final String sparqlQuery) {
        return compile(graph.traversal(), sparqlQuery);
    }

    /**
     * Converts SPARQL to a Gremlin traversal.
     *
     * @param g           the {@link GraphTraversalSource} instance to execute the traversal from
     * @param sparqlQuery the query to compile to Gremlin
     */
    public static GraphTraversal<Vertex, ?> compile(final GraphTraversalSource g, final String sparqlQuery) {
        return compile(g, QueryFactory.create(Prefixes.prepend(sparqlQuery), Syntax.syntaxSPARQL));
    }

    private GraphTraversal<Vertex, ?> compile(final Query query) {
        final Op op = Algebra.compile(query);
        OpWalker.walk(op, new GremlinOpVisitor());

        int traversalIndex = 0;
        final int numberOfTraversal = traversalList.size();
        final int numberOfOptionalTraversal = optionalTraversals.size();
        final Traversal[] arrayOfAllTraversals = (numberOfOptionalTraversal > 0) ?
            new Traversal[numberOfTraversal - numberOfOptionalTraversal + 1] :
            new Traversal[numberOfTraversal - numberOfOptionalTraversal];

        final Traversal[] arrayOfOptionalTraversals = new Traversal[numberOfOptionalTraversal];

        for (Traversal tempTrav : traversalList) {
            arrayOfAllTraversals[traversalIndex++] = tempTrav;
        }

        traversalIndex = 0;
        for (Traversal tempTrav : optionalTraversals) 
            arrayOfOptionalTraversals[traversalIndex++] = tempTrav;
 
         // creates a map of ordering keys and their ordering direction
        final Map<String, Order> orderingIndex = createOrderIndexFromQuery(query);

        if (traversalList.size() > 0)
            traversal = traversal.match(arrayOfAllTraversals);

        if (optionalTraversals.size() > 0) {
            traversal = traversal.coalesce(__.match(arrayOfOptionalTraversals), (Traversal) __.constant("N/A"));
            for (int i = 0; i < optionalVariable.size(); i++) {
                traversal = traversal.as(optionalVariable.get(i).substring(1));
            }
        }

        final List<String> vars = query.getResultVars();
        if (!query.isQueryResultStar() && !query.hasGroupBy()) {
            final String[] all = new String[vars.size()];
            vars.toArray(all);
            if (query.isDistinct()) {
                traversal = traversal.dedup(all);
            }

            // apply ordering from ORDER BY
            orderingIndex.forEach((k, v) -> traversal = traversal.order().by(__.select(k), v));

            // the result sizes have special handling to get the right signatures of select() called.
            switch (all.length) {
                case 0:
                    throw new IllegalStateException();
                case 1:
                    traversal = traversal.select(all[0]);
                    break;
                case 2:
                    traversal = traversal.select(all[0], all[1]);
                    break;
                default:
                    final String[] others = Arrays.copyOfRange(all, 2, vars.size());
                    traversal = traversal.select(all[0], all[1], others);
                    break;
            }
        }

        if (query.hasGroupBy()) {
            final VarExprList lstExpr = query.getGroupBy();
            String grpVar = "";
            for (Var expr : lstExpr.getVars()) {
                grpVar = expr.getName();
            }

            if (!grpVar.isEmpty())
                traversal = traversal.select(grpVar);
            if (query.hasAggregators()) {
                final List<ExprAggregator> exprAgg = query.getAggregators();
                for (ExprAggregator expr : exprAgg) {
                    if (expr.getAggregator().getName().contains("COUNT")) {
                        if (!query.toString().contains("GROUP")) {
                            if (expr.getAggregator().toString().contains("DISTINCT"))
                                traversal = traversal.dedup(expr.getAggregator().getExprList().get(0).toString().substring(1));
                            else
                                traversal = traversal.select(expr.getAggregator().getExprList().get(0).toString().substring(1));

                            traversal = traversal.count();
                        } else {
                            traversal = traversal.groupCount();
                        }
                    }
                    if (expr.getAggregator().getName().contains("MAX")) {
                        traversal = traversal.max();
                    }
                }
            } else {
                traversal = traversal.group();
            }
        }

        if (query.hasOrderBy() && query.hasGroupBy())
            orderingIndex.forEach((k, v) -> traversal = traversal.order().by(__.select(k), v));

        if (query.hasLimit()) {
            long limit = query.getLimit(), offset = 0;

            if (query.hasOffset())
                offset = query.getOffset();

            if (query.hasGroupBy() && query.hasOrderBy())
                traversal = traversal.range(Scope.local, offset, offset + limit);
            else
                traversal = traversal.range(offset, offset + limit);
        }

        return traversal;
    }

    /**
     * Extracts any {@code SortCondition} instances from the SPARQL query and holds them in an index of their keys
     * where the value is that keys sorting direction.
     */
    private static Map<String, Order> createOrderIndexFromQuery(final Query query) {
        final Map<String, Order> orderingIndex = new HashMap<>();
        if (query.hasOrderBy()) {
            final List<SortCondition> sortingConditions = query.getOrderBy();

            for (SortCondition sortCondition : sortingConditions) {
                final Expr expr = sortCondition.getExpression();

                // by default, the sort will be ascending. getDirection() returns -2 if the DESC/ASC isn't
                // supplied - weird
                orderingIndex.put(expr.getVarName(), sortCondition.getDirection() == -1 ? Order.desc : Order.asc);
            }
        }

        return orderingIndex;
    }

    private static GraphTraversal<Vertex, ?> compile(final GraphTraversalSource g, final Query query) {
        return new SparqlToGremlinCompiler(g).compile(query);
    }

    /**
     * An {@code OpVisitor} implementation that reads SPARQL algebra operations into Gremlin traversals.
     */
    private class GremlinOpVisitor extends OpVisitorBase {

        /**
         * Visiting triple patterns in SPARQL algebra.
         */
        @Override
        public void visit(final OpBGP opBGP) {
        if(optionalFlag)
            {
                opBGP.getPattern().getList().forEach(triple -> optionalTraversals.add(TraversalBuilder.transform(triple)));
                opBGP.getPattern().getList().forEach(triple -> optionalVariable.add(triple.getObject().toString()));
                
            }
            else        
                opBGP.getPattern().getList().forEach(triple -> traversalList.add(TraversalBuilder.transform(triple)));
        }

        /**
         * Visiting filters in SPARQL algebra.
         */
        @Override
        public void visit(final OpFilter opFilter) {
            Traversal traversal;
            for (Expr expr : opFilter.getExprs().getList()) {
                if (expr != null) {
                    traversal = __.where(WhereTraversalBuilder.transform(expr));
                    traversalList.add(traversal);
                }
            }
        }

        
        /**
         * Visiting LeftJoin(Optional) in SPARQL algebra.
         */
        @Override
        public void visit(final OpLeftJoin opLeftJoin) {
            optionalFlag = true;
            optionalVisit(opLeftJoin.getRight());
            if (opLeftJoin.getExprs() != null) {
                for (Expr expr : opLeftJoin.getExprs().getList()) {
                    if (expr != null) {
                        if (optionalFlag)
                            optionalTraversals.add(__.where(WhereTraversalBuilder.transform(expr)));
                    }
                }
            }
        }
        
        /**
         * Walking OP for Optional in SPARQL algebra.
         */
        private void optionalVisit(final Op op) {

            OpWalker.walk(op, this);
        }

        /**
         * Visiting unions in SPARQL algebra.
         */
        @Override
        public void visit(final OpUnion opUnion) {
            final Traversal unionTemp[] = new Traversal[2];
            final Traversal unionTemp1[] = new Traversal[traversalList.size() / 2];
            final Traversal unionTemp2[] = new Traversal[traversalList.size() / 2];

            int count = 0;

            for (int i = 0; i < traversalList.size(); i++) {
                if (i < traversalList.size() / 2)
                    unionTemp1[i] = traversalList.get(i);
                else
                    unionTemp2[count++] = traversalList.get(i);
            }

            unionTemp[1] = __.match(unionTemp2);
            unionTemp[0] = __.match(unionTemp1);

            traversalList.clear();
            traversal = (GraphTraversal<Vertex, ?>) traversal.union(unionTemp);
        }

    }
}
