/*
 * 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.jena.sparql.algebra ;

import java.util.* ;
import java.util.function.BiConsumer ;
import java.util.stream.Collectors ;

import org.apache.jena.atlas.lib.NotImplemented ;
import org.apache.jena.graph.Node ;
import org.apache.jena.graph.Triple ;
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.ARQInternalErrorException ;
import org.apache.jena.sparql.ARQNotImplemented ;
import org.apache.jena.sparql.algebra.op.* ;
import org.apache.jena.sparql.core.BasicPattern ;
import org.apache.jena.sparql.core.Quad ;
import org.apache.jena.sparql.core.Var ;
import org.apache.jena.sparql.core.VarExprList ;
import org.apache.jena.sparql.engine.QueryIterator ;
import org.apache.jena.sparql.expr.* ;
import org.apache.jena.sparql.expr.aggregate.Aggregator ;
import org.apache.jena.sparql.pfunction.PropFuncArg ;
import org.apache.jena.sparql.syntax.* ;
import org.apache.jena.sparql.syntax.syntaxtransform.*;
import org.apache.jena.sparql.util.graph.GraphList ;
import org.apache.jena.vocabulary.RDF ;

/**
 * Convert an Op expression in SPARQL syntax, that is, the reverse of algebra
 * generation.
 * <p>
 * The contract is to return an "equivalent query" - generates the same answers -
 * to the original query that generated the algebra.
 * That may be the same query (the code aims for this, assuming the original query
 * didn't have additional, unnecessary {}),
 * different queries with the same alegra forms,
 * or different equivalent queries - same answers, different algebra -
 * usually where extra {} are added in and not easiely cleaned out.
 * <p>
 * Some attempt is made to handle algebra expressions with operators from the optimizer.
 * <p>
 * It is possible to build algrebra expressions directly for which there is no SPARQL query
 * that generates that algebra.  This code may produce an equivalent query but that is
 * not guaranteed.
 */
public class OpAsQuery {
    // Some things that can be done further:

    // TODO Optimization formats like OpTopN (which is an optimizer additional algebra operator).
    // TODO More group flattening. This is better presentation, not chanage in algebra.
    //    OPTIONAL (LeftJoin) unbundles the LHS to avoid { { P OPTIONAL{} } OPTIONAL{} }
    //    {   { ... } BIND } -- the inner { } is not necessary
    //       This is actually a general situation.
    //   Adding onto the end of a group when the item added can not merge into the existing last element.
    // e.g. BIND, VALUES.

    public static Query asQuery(Op op) {
        Converter converter = new Converter(op) ;
        return converter.convert() ;
    }

    static class /* struct */ QueryLevelDetails {
        // The stack of processing in a query is:
        // slice-distinct/reduce-project-order-filter[having]-extend*[AS and aggregate naming]-group-pattern

        OpSlice        opSlice    = null ;
        OpDistinct     opDistinct = null ;
        OpReduced      opReduced  = null ;
        OpProject      opProject  = null ;
        OpOrder        opOrder    = null ;
        OpFilter       opHaving   = null ;
        List<OpExtend> opExtends  = new ArrayList<>() ;
        OpGroup        opGroup    = null ;
        // End of the modifiers.
        // The pattern of the group or query itself if not grouped.
        Op             pattern    = null ;

        private QueryLevelDetails() {}

        // Debugging help.
        void info() {
            if ( opSlice != null )
                System.out.printf("slice: (%d, %d)\n", opSlice.getStart(), opSlice.getLength()) ;
            if ( opDistinct != null )
                System.out.printf("distinct\n") ;
            if ( opReduced != null )
                System.out.printf("reduced\n") ;
            if ( opProject != null )
                System.out.printf("project: %s\n", opProject.getVars()) ;
            if ( opOrder != null )
                System.out.printf("order: %s\n", opOrder.getConditions()) ;
            if ( opHaving != null )
                System.out.printf("having: %s\n", opHaving.getExprs()) ;
            if ( opExtends != null && !opExtends.isEmpty() ) {
                List<VarExprList> z = opExtends.stream().map(x -> x.getVarExprList()).collect(Collectors.toList()) ;
                System.out.printf("assigns: %s\n", z) ;
            }
            if ( opGroup != null ) {
                List<ExprAggregator> aggregators = opGroup.getAggregators() ;
                List<Var> aggVars = aggregators.stream().map(x -> x.getAggVar().asVar()).collect(Collectors.toList()) ;
                System.out.printf("group: %s |-| %s\n", opGroup.getGroupVars(), opGroup.getAggregators()) ;
                System.out.printf("group agg vars: %s\n", aggVars) ;
            }
        }

        static QueryLevelDetails analyse(Op operation) {
            // Walk inwards, collecting the query level information.
            // slice-distinct/reduce-project-order-filter[having]-extend*[AS and aggregate naming]-group-pattern
            QueryLevelDetails details = new QueryLevelDetails() ;

            Op op = operation ;
            if ( op instanceof OpSlice ) {
                details.opSlice = (OpSlice)op ;
                op = details.opSlice.getSubOp() ;
            }
            if ( op instanceof OpDistinct ) {
                details.opDistinct = (OpDistinct)op ;
                op = details.opDistinct.getSubOp() ;
            }
            if ( op instanceof OpReduced ) {
                details.opReduced = (OpReduced)op ;
                op = details.opReduced.getSubOp() ;
            }
            if ( op instanceof OpProject ) {
                details.opProject = (OpProject)op ;
                op = details.opProject.getSubOp() ;

            }
            if ( op instanceof OpOrder ) {
                details.opOrder = (OpOrder)op ;
                op = details.opOrder.getSubOp() ;
            }

            // Lookahead to see if an opGroup can be found.
            // If no group, leave and process as in WHERE clause.
            details.opGroup = getGroup(op) ;
            if ( details.opGroup == null ) {
                // If project, deal with as AS.
                // Else as regular WHERE
                if ( details.opProject != null ) {
                    op = processExtend(op, details.opExtends) ;
                    details.pattern = op ;
                } else
                    details.pattern = op ;
                return details ;
            }
            // (group) found.
            details.pattern = details.opGroup.getSubOp() ;
            if ( op instanceof OpFilter ) {
                details.opHaving = (OpFilter)op ;
                op = details.opHaving.getSubOp() ;
            }
            // Can't tell if it's an "aggregation" except by looking at the
            // assignment variables.

            // AS and aggregate renames.
            op = processExtend(op, details.opExtends) ;

            if ( !(op instanceof OpGroup) ) {
                System.out.println("Expected (group), got " + op.getName()) ;
            }

            return details ;
        }
    }

    public static class Converter implements OpVisitor {
        private Query               query ;
        private Op                  queryOp ;
        private Element             element      = null ;
        private ElementGroup        currentGroup = null ;
        private Deque<ElementGroup> stack        = new ArrayDeque<>() ;
        private boolean             hasRun       = false ;

        public Converter(Op op) {
            this.query = null ;
            this.queryOp = op ;
            currentGroup = new ElementGroup() ;
        }

        Query convert() {
            if ( !hasRun )
                try {
                    // Which may be broken, or null, if something went wrong.
                    query = convertInner() ;
                }
                finally {
                    this.hasRun = true ;
                }
            return query ;
        }

        Query convertInner() {
            this.query = QueryFactory.create() ;
            // Special case. SELECT * { ... BIND (  AS ...) }
            if ( queryOp instanceof OpExtend ) {
                List<OpExtend> assignments = new ArrayList<>() ;
                Op op = processExtend(queryOp, assignments) ;
                processQueryPattern(op, assignments) ;
                query.setQueryResultStar(true) ;
                query.resetResultVars();
                return query ;
            }

            // There is a projection.
            QueryLevelDetails level = QueryLevelDetails.analyse(queryOp) ;
            processQueryPattern(level) ;

            // Modifier stack.
            // slice-distinct/reduce-project-order-filter[having]-extend[AS]-extend[agg]-group-pattern
            // Do as executed (the reverse order) because e.g. extends have effects on project.

            // Substitution mapping
            Map<ExprVar, Expr> aggVarExprMap = new HashMap<>() ;

            if ( level.opGroup != null ) {
                query.getGroupBy().addAll(level.opGroup.getGroupVars()) ;
                level.opGroup.getAggregators().forEach(eAgg -> {
                    ExprVar v = eAgg.getAggVar() ;
                    Aggregator agg = eAgg.getAggregator() ;
                    aggVarExprMap.put(v, eAgg) ;
                }) ;
                query.getAggregators().addAll(level.opGroup.getAggregators()) ;
            }

            ExprTransform varToExpr = new ExprTransformCopy() {
                @Override
                public Expr transform(ExprVar nv) {
                    if ( aggVarExprMap.containsKey(nv) )
                        return aggVarExprMap.get(nv) ;
                    return nv ;
                }
            } ;

            // The assignments will become part of the project.
            Map<Var, Expr> assignments = new HashMap<>() ;
            if ( level.opExtends != null ) {
                processExtends(level.opExtends, (var,expr)->{
                    // Internal rename.
                    expr = rewrite(expr, varToExpr) ;
                    assignments.put(var, expr) ;
                }) ;

            }

            if ( level.opHaving != null ) {
                level.opHaving.getExprs().getList().forEach(expr -> {
                    expr = rewrite(expr, varToExpr) ;
                    query.getHavingExprs().add(expr) ;
                }) ;
            }

            if ( level.opOrder != null ) {
                level.opOrder.getConditions().forEach(sc -> {
                    Expr expr = sc.getExpression() ;
                    expr = rewrite(expr, varToExpr) ;
                    if ( expr == sc.getExpression() )
                        query.addOrderBy(sc) ;
                    else
                        query.addOrderBy(new SortCondition(expr, sc.getDirection())) ;
                }) ;
            }

            if ( level.opProject == null ) {
                query.setQueryResultStar(true) ;
                // No project, Make BINDs
                //processQueryPattern(op, assignments) ;


            } else {
                level.opProject.getVars().forEach(v -> {
                    if ( assignments.containsKey(v) ) {
                        query.addResultVar(v, assignments.get(v)) ;
                    } else
                        query.getProjectVars().add(v) ;

                }) ;
            }


            if ( level.opDistinct != null )
                query.setDistinct(true) ;
            if ( level.opReduced != null )
                query.setReduced(true) ;

            if ( level.opSlice != null ) {
                query.setOffset(level.opSlice.getStart()) ;
                query.setLimit(level.opSlice.getLength()) ;
            }
            query.resetResultVars() ;
            return query ;
        }

        /**
         * Collect the OpExtend in a stack of OpExtend into a list for later
         * processing. (Processing only uses opExtend in the list, not inner one
         * which wil also be in the list.)
         */
        private static Op processExtend(Op op, List<OpExtend> assignments) {
            while ( op instanceof OpExtend ) {
                OpExtend opExtend = (OpExtend)op ;
                // JENA-1843
                assignments.add(0, opExtend) ;
                op = opExtend.getSubOp() ;
            }
            return op ;
        }

        private static void processExtends(List<OpExtend> ext, BiConsumer<Var, Expr> action) {
            ext.forEach(extend->{
                extend.getVarExprList().forEachVarExpr(action) ;
            });
        }

        private static void processAssigns(List<OpAssign> assigns, BiConsumer<Var, Expr> action) {
            assigns.forEach(assign->{
                assign.getVarExprList().forEachExpr(action) ;
            });
        }

        private static Expr rewrite(Expr expr, ExprTransform transform) {
            return ExprTransformer.transform(transform, expr) ;
        }

        /**
         * Process for a single pattern below the modifiers.
         * Cleans up the ElementGroup produced.
         */
        private void processQueryPattern(QueryLevelDetails level) {
            Op op = level.pattern ;
            op.visit(this) ;
            ElementGroup eg = this.currentGroup ;
            Element e = fixupGroupsOfOne(eg) ;
            query.setQueryPattern(e) ;
            query.setQuerySelectType() ;
        }

        // Can't distinguish
        //    SELECT * { ... BIND ( ?v AS ...) }
        // from
        //    SELECT ( ?v AS ...) { ... }.
        // They have the same algebra.
        // This code chooses to use the second form.
        private void processQueryPattern(Op op, List<OpExtend> assignments) {
            op.visit(this) ;
            ElementGroup eg = this.currentGroup ;
            processExtends(assignments,(v,e)->eg.addElement(new ElementBind(v, e)) ) ;
            Element e = cleanupGroup(eg) ;
            query.setQueryPattern(e) ;
            query.setQuerySelectType() ;
        }

        private Element cleanupGroup(ElementGroup eg) {
            Element el = fixupGroupsOfOne(eg);
            // Other cleanups.
            return el;
        }

        private Element fixupGroupsOfOne(ElementGroup eg) {
            ElementTransform transform = new ElementTransformCleanGroupsOfOne() ;
            ExprTransform exprTransform = new ExprTransformApplyElementTransform(transform) ;
            Element el2 = ElementTransformer.transform(eg, transform, exprTransform) ;
            // Top level is always a group or a subquery.
            if ( ! ( el2 instanceof ElementGroup ) && ! ( el2 instanceof ElementSubQuery ) ) {
                ElementGroup eg2 = new ElementGroup() ;
                eg2.addElement(el2);
                el2 = eg2 ;
            }
            return el2 ;
        }

        private Element asElement(Op op) {
            ElementGroup g = asElementGroup(op) ;
            if ( g.size() == 1 )
                return g.get(0) ;
            return g ;
        }

        private ElementGroup asElementGroup(Op op) {
            startSubGroup() ;
            op.visit(this) ;
            return endSubGroup() ;
        }

        @Override
        public void visit(OpBGP opBGP) {
            currentGroup().addElement(process(opBGP.getPattern())) ;
        }

        @Override
        public void visit(OpTriple opTriple) {
            currentGroup().addElement(process(opTriple.getTriple())) ;
        }

        @Override
        public void visit(OpQuad opQuad) {
            throw new ARQNotImplemented("OpQuad") ;
        }

        @Override
        public void visit(OpProcedure opProcedure) {
            throw new ARQNotImplemented("OpProcedure") ;
        }

        @Override
        public void visit(OpPropFunc opPropFunc) {
            Node s = processPropFuncArg(opPropFunc.getSubjectArgs()) ;
            Node o = processPropFuncArg(opPropFunc.getObjectArgs()) ;
            Triple t = new Triple(s, opPropFunc.getProperty(), o) ;
            currentGroup().addElement(process(t)) ;
        }

        private Node processPropFuncArg(PropFuncArg args) {
            if ( args.isNode() )
                return args.getArg() ;

            // List ...
            List<Node> list = args.getArgList() ;
            if ( list.size() == 0 )
                return RDF.Nodes.nil ;
            BasicPattern bgp = new BasicPattern() ;
            Node head = GraphList.listToTriples(list, bgp) ;
            currentGroup().addElement(process(bgp)) ;
            return head ;
        }

        // There is one special case to consider:
        // A path expression was expanded into a OpSequence during Algenra
        // generation. The simple path expressions become an OpSequence that could be
        // recombined into an ElementPathBlock.

        @Override
        public void visit(OpSequence opSequence) {
            ElementGroup g = currentGroup() ;
            boolean nestGroup = !g.isEmpty() ;
            if ( nestGroup ) {
                startSubGroup() ;
                g = currentGroup() ;
            }

            for ( Op op : opSequence.getElements() ) {
                Element e = asElement(op) ;
                insertIntoGroup(g, e) ;
            }

            if ( nestGroup )
                endSubGroup() ;
            return ;
        }

        @Override
        public void visit(OpDisjunction opDisjunction) {
            throw new ARQNotImplemented("OpDisjunction") ;
        }

        private Element process(BasicPattern pattern) {
            // The different SPARQL versions (1.0, 1.1) use different internal
            // structures for BGPs.
            if ( query.getSyntax() == Syntax.syntaxSPARQL_10 ) {
                ElementTriplesBlock e = new ElementTriplesBlock() ;
                for ( Triple t : pattern )
                    // Leave bNode variables as they are
                    // Query serialization will deal with them.
                    e.addTriple(t) ;
                return e ;
            }

            if ( query.getSyntax() == Syntax.syntaxSPARQL_11 || query.getSyntax() == Syntax.syntaxARQ ) {
                ElementPathBlock e = new ElementPathBlock() ;
                for ( Triple t : pattern )
                    // Leave bNode variables as they are
                    // Query serialization will deal with them.
                    e.addTriple(t) ;
                return e ;
            }

            throw new ARQInternalErrorException("Unrecognized syntax: " + query.getSyntax()) ;
        }

        private ElementTriplesBlock process(Triple triple) {
            // Unsubtle
            ElementTriplesBlock e = new ElementTriplesBlock() ;
            e.addTriple(triple) ;
            return e ;
        }

        @Override
        public void visit(OpQuadPattern quadPattern) {
            Node graphNode = quadPattern.getGraphNode() ;
            if ( graphNode.equals(Quad.defaultGraphNodeGenerated) ) {
                currentGroup().addElement(process(quadPattern.getBasicPattern())) ;
            } else {
                startSubGroup() ;
                Element e = asElement(new OpBGP(quadPattern.getBasicPattern())) ;
                endSubGroup() ;

                // If not element group make it one
                if ( !(e instanceof ElementGroup) ) {
                    ElementGroup g = new ElementGroup() ;
                    g.addElement(e) ;
                    e = g ;
                }

                Element graphElt = new ElementNamedGraph(graphNode, e) ;
                currentGroup().addElement(graphElt) ;
            }
        }

        @Override
        public void visit(OpQuadBlock quadBlock) {
            // Gather into OpQuadPatterns.
            throw new NotImplemented("OpQuadBlock") ;
        }

        @Override
        public void visit(OpPath opPath) {
            ElementPathBlock epb = new ElementPathBlock() ;
            epb.addTriplePath(opPath.getTriplePath()) ;
            ElementGroup g = currentGroup() ;
            g.addElement(epb) ;
        }

        @Override
        public void visit(OpJoin opJoin) {
            Element eLeft = asElement(opJoin.getLeft()) ;
            ElementGroup eRightGroup = asElementGroup(opJoin.getRight()) ;
            Element eRight = eRightGroup ;
            // Very special case. If the RHS is not something that risks
            // reparsing into a combined element of a group, strip the group-of-one.
            // See also ElementTransformCleanGroupsOfOne
            if ( eRightGroup.size() == 1 ) {
                // This always was a {} around it but it's unnecessary in a group of one.
                if ( eRightGroup.get(0) instanceof ElementSubQuery )
                    eRight = eRightGroup.get(0) ;
            }

            ElementGroup g = currentGroup() ;
            insertIntoGroup(g, eLeft) ;
            insertIntoGroup(g, eRight) ;
            return ;
        }

        @Override
        public void visit(OpLeftJoin opLeftJoin) {
            Element eLeft = asElement(opLeftJoin.getLeft()) ;
            ElementGroup eRight = asElementGroup(opLeftJoin.getRight()) ;

            // If the RHS is (filter) we need to protect it from becoming
            // part of the expr for the LeftJoin.
            // OPTIONAL {{ ?s ?p ?o FILTER (?o>34) }} is not the same as
            // OPTIONAL { ?s ?p ?o FILTER (?o>34) }

            boolean mustProtect = eRight.getElements().stream().anyMatch(el -> el instanceof ElementFilter ) ;

            if ( mustProtect ) {
                ElementGroup eRight2 = new ElementGroup() ;
                eRight2.addElement(eRight);
                eRight = eRight2 ;
            }

            if ( opLeftJoin.getExprs() != null ) {
                for ( Expr expr : opLeftJoin.getExprs() ) {
                    ElementFilter f = new ElementFilter(expr) ;
                    eRight.addElement(f) ;
                }
            }
            ElementGroup g = currentGroup() ;
            if ( !emptyGroup(eLeft) ) {
                if ( eLeft instanceof ElementGroup )
                    g.getElements().addAll(((ElementGroup)eLeft).getElements()) ;
                else
                    g.addElement(eLeft) ;
            }

            ElementOptional opt = new ElementOptional(eRight) ;
            g.addElement(opt) ;
        }

        @Override
        public void visit(OpDiff opDiff) {
            throw new ARQNotImplemented("OpDiff") ;
        }

        @Override
        public void visit(OpMinus opMinus) {
            Element eLeft = asElement(opMinus.getLeft()) ;
            Element eRight = asElementGroup(opMinus.getRight()) ;
            ElementMinus elMinus = new ElementMinus(eRight) ;
            ElementGroup g = currentGroup() ;
            if ( !emptyGroup(eLeft) )
                g.addElement(eLeft) ;
            g.addElement(elMinus) ;
        }

        @Override
        public void visit(OpUnion opUnion) {
            Element eLeft = asElementGroup(opUnion.getLeft()) ;
            Element eRight = asElementGroup(opUnion.getRight()) ;
            if ( eLeft instanceof ElementUnion ) {
                ElementUnion elUnion = (ElementUnion)eLeft ;
                elUnion.addElement(eRight) ;
                return ;
            }
            // Multiple unions.
            // if ( eRight instanceof ElementUnion )
            // {
            // ElementUnion elUnion = (ElementUnion)eRight ;
            // elUnion.getElements().add(0, eLeft) ;
            // return ;
            // }

            ElementUnion elUnion = new ElementUnion() ;
            elUnion.addElement(eLeft) ;
            elUnion.addElement(eRight) ;
            currentGroup().addElement(elUnion) ;
        }

        @Override
        public void visit(OpConditional opCondition) {
            throw new ARQNotImplemented("OpCondition") ;
        }

        @Override
        public void visit(OpFilter opFilter) {
            // (filter .. (filter ( ... )) (non-canonicalizing OpFilters)
            // Inner gets Grouped unnecessarily.
            Element e = asElement(opFilter.getSubOp()) ;
            if ( currentGroup() != e )
                currentGroup().addElement(e) ;
            element = currentGroup() ; // Was cleared by asElement.

            ExprList exprs = opFilter.getExprs() ;
            for ( Expr expr : exprs ) {
                ElementFilter f = new ElementFilter(expr) ;
                currentGroup().addElement(f) ;
            }
        }

        @Override
        public void visit(OpGraph opGraph) {
            startSubGroup() ;
            Element e = asElement(opGraph.getSubOp()) ;
            endSubGroup() ;

            // If not element group make it one
            if ( !(e instanceof ElementGroup) ) {
                ElementGroup g = new ElementGroup() ;
                g.addElement(e) ;
                e = g ;
            }

            Element graphElt = new ElementNamedGraph(opGraph.getNode(), e) ;
            currentGroup().addElement(graphElt) ;
        }

        @Override
        public void visit(OpService opService) {
            // Hmm - if the subnode has been optimized, we may fail.
            Op op = opService.getSubOp() ;
            Element x = asElement(opService.getSubOp()) ;
            Element elt = new ElementService(opService.getService(), x, opService.getSilent()) ;
            currentGroup().addElement(elt) ;
        }

        @Override
        public void visit(OpDatasetNames dsNames) {
            throw new ARQNotImplemented("OpDatasetNames") ;
        }

        @Override
        public void visit(OpTable opTable) {
            // This will go in a group so simply forget it.
            if ( opTable.isJoinIdentity() )
                return ;

            // Put in a VALUES
            // This may be related to the grpup of the overall query.

            ElementData el = new ElementData() ;
            el.getVars().addAll(opTable.getTable().getVars()) ;
            QueryIterator qIter = opTable.getTable().iterator(null) ;
            while (qIter.hasNext())
                el.getRows().add(qIter.next()) ;
            qIter.close() ;
            currentGroup().addElement(el) ;
        }

        @Override
        public void visit(OpExt opExt) {
            // Op op = opExt.effectiveOp() ;
            // // This does not work in all cases.
            // op.visit(this) ;
            throw new ARQNotImplemented("OpExt") ;
        }

        @Override
        public void visit(OpNull opNull) {
            throw new ARQNotImplemented("OpNull") ;
        }

        @Override
        public void visit(OpLabel opLabel) {
            if ( opLabel.hasSubOp() )
                opLabel.getSubOp().visit(this) ;
        }

        @Override
        public void visit(OpAssign opAssign) {
            Element e = asElement(opAssign.getSubOp()) ;
            // If (assign ... (table unit)), and first in group, don't add the empty group.
            insertIntoGroup(currentGroup(), e) ;
            processAssigns(Arrays.asList(opAssign), (var,expr)->{
                currentGroup().addElement(new ElementAssign(var,expr)) ;
            }) ;
        }

        @Override
        public void visit(OpExtend opExtend) {
            Element e = asElement(opExtend.getSubOp()) ;
            // If (extend ... (table unit)), and first in group, don't add the empty group.
            insertIntoGroup(currentGroup(), e) ;
            processExtends(Arrays.asList(opExtend), (var,expr)->{
                currentGroup().addElement(new ElementBind(var,expr)) ;
            }) ;
        }

        @Override
        public void visit(OpList opList) {
            opList.getSubOp().visit(this) ;
        }

        // When some modifers (e.g. OpDistinct) are met in a pattern, they signal
        // a new query level (new inner SELECT), where we switch back to
        // looking for the level start in

        private void newLevel(Op op) {
            convertAsSubQuery(op) ;
        }

        private void convertAsSubQuery(Op op) {
            Converter subConverter = new Converter(op) ;
            ElementSubQuery subQuery = new ElementSubQuery(subConverter.convert()) ;
            ElementGroup g = currentGroup() ;
            g.addElement(subQuery) ;
        }

        @Override
        public void visit(OpOrder opOrder) {
            newLevel(opOrder) ;
        }

        @Override
        public void visit(OpProject opProject) {
            newLevel(opProject) ;
        }

        @Override
        public void visit(OpReduced opReduced) {
            newLevel(opReduced) ;
        }

        @Override
        public void visit(OpDistinct opDistinct) {
            newLevel(opDistinct) ;
        }

        @Override
        public void visit(OpSlice opSlice) {
            newLevel(opSlice) ;
        }

        @Override
        public void visit(OpGroup opGroup) {
            newLevel(opGroup) ;
        }

        @Override
        public void visit(OpTopN opTop) {
            throw new ARQNotImplemented("OpTopN") ;
        }

        private static boolean emptyGroup(Element element) {
            if ( !(element instanceof ElementGroup) )
                return false ;
            ElementGroup eg = (ElementGroup)element ;
            return eg.isEmpty() ;
        }

        private static boolean groupOfOne(Element element) {
            if ( !(element instanceof ElementGroup) )
                return false ;
            ElementGroup eg = (ElementGroup)element ;
            return eg.size() == 1 ;
        }

        /** Insert into a group, skip initial empty subgroups; recombining ElementPathBlock */
        private static void insertIntoGroup(ElementGroup eg, Element e) {
            // Skip initial empty subgroup.
            if ( emptyGroup(e) && eg.isEmpty() )
                return ;

            // Empty group.
            if ( eg.isEmpty() ) {
                eg.addElement(e);
                return ;
            }

            Element eltTop = eg.getLast() ;
            if ( ! ( eltTop instanceof ElementPathBlock ) ) {
                // Not working on a ElementPathBlock - no need to group-of-one
                // when inserting ElementPathBlock.
                e = unwrapGroupOfOnePathBlock(e) ;
                eg.addElement(e);
                return ;
            }
            if ( ! ( e  instanceof ElementPathBlock ) ) {
                eg.addElement(e);
                return ;
            }
            // Combine.
            ElementPathBlock currentPathBlock = (ElementPathBlock)eltTop ;
            ElementPathBlock newPathBlock = (ElementPathBlock)e ;
            currentPathBlock.getPattern().addAll(newPathBlock.getPattern());
        }

        private static Element unwrapGroupOfOnePathBlock(Element e) {
            Element e2 = getElementOfGroupOfOne(e) ;
            if ( e2 != null )
                return e2 ;
            return e ;
        }

        private static Element getElementOfGroupOfOne(Element e) {
            if ( groupOfOne(e) ) {
                ElementGroup eg = (ElementGroup)e ;
                return eg.get(0) ;
            }
            return null ;
        }

        private Element lastElement() {
            ElementGroup g = currentGroup ;
            return g.getLast() ;
        }

        private void startSubGroup() {
            push(currentGroup) ;
            ElementGroup g = new ElementGroup() ;
            currentGroup = g ;
        }

        private ElementGroup endSubGroup() {
            ElementGroup g = pop() ;
            ElementGroup r = currentGroup ;
            currentGroup = g ;
            return r ;
        }

        private ElementGroup currentGroup() {
            // if ( currentGroup == null )
            // startSubGroup() ;
            return currentGroup ;
        }

        private ElementGroup peek() {
            if ( stack.size() == 0 )
                return null ;
            return stack.peek() ;
        }

        private ElementGroup pop() {
            return stack.pop() ;
        }

        private void push(ElementGroup el) {
            stack.push(el) ;
        }
    }


    /**
     * Allows multiple filters and any number of extend
     */
    private static OpGroup getGroup(Op op) {
        // Unwind tail recursion to protected against extreme queries.
        for ( ; ; ) {
            if ( op instanceof OpGroup )
                return (OpGroup)op ;
            if ( op instanceof OpFilter ) {
                OpFilter opFilter = (OpFilter)op ;
                op = opFilter.getSubOp() ;
                continue ;
            }
            if ( op instanceof OpExtend ) { // AS or Aggregate naming
                OpExtend opExtend = (OpExtend)op ;
                op = opExtend.getSubOp() ;
                continue ;
            }
            return null ;
        }
    }

    private static Op processExtend(Op op, List<OpExtend> assignments) {
        while (op instanceof OpExtend) {
            OpExtend opExtend = (OpExtend)op;
            assignments.add(opExtend);
            op = opExtend.getSubOp();
        }
        // JENA-1843
        if ( assignments.size() > 1 )
            Collections.reverse(assignments);

        return op;
    }
}
