/*
 * 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.walker;

import java.util.Iterator ;

import org.apache.jena.sparql.algebra.Op ;
import org.apache.jena.sparql.algebra.OpVisitor ;
import org.apache.jena.sparql.algebra.OpVisitorBase ;
import org.apache.jena.sparql.algebra.op.* ;
import org.apache.jena.sparql.core.VarExprList ;
import org.apache.jena.sparql.expr.* ;

/** Walk algebra and expressions */
public class WalkerVisitor implements OpVisitorByTypeAndExpr, ExprVisitorFunction {
    protected final ExprVisitor exprVisitor ;
    protected final OpVisitor   opVisitor ;
    protected int               opDepthLimit      = Integer.MAX_VALUE ;
    protected int               exprDepthLimit    = Integer.MAX_VALUE ;

    protected int               opDepth      = 0 ;
    protected int               exprDepth    = 0 ;
    
    private final OpVisitor     beforeVisitor ;
    private final OpVisitor     afterVisitor ;

    /**
     * A walker. If a visitor is null, then don't walk in. For
     * "no action but keep walking inwards", use {@link OpVisitorBase} and
     * {@link ExprVisitorBase}.
     * 
     * @see OpVisitorBase
     * @see ExprVisitorBase
     */
    public WalkerVisitor(OpVisitor opVisitor, ExprVisitor exprVisitor, OpVisitor before, OpVisitor after) {
        this.opVisitor = opVisitor ;
        this.exprVisitor = exprVisitor ;
        if ( opDepthLimit < 0 )
            opDepthLimit = Integer.MAX_VALUE ;
        if ( exprDepth < 0 )
            exprDepthLimit = Integer.MAX_VALUE ;
        opDepth = 0 ;
        exprDepth = 0 ;
        beforeVisitor = before ;
        afterVisitor = after ;
    }

    protected final void before(Op op) {
        if ( beforeVisitor != null )
            op.visit(beforeVisitor) ;
    }

    protected final void after(Op op) {
        if ( afterVisitor != null )
            op.visit(afterVisitor) ;
    }
    
    public void walk(Op op) {
        if ( op == null )
            return ;
        if ( opDepth == opDepthLimit )
            // No deeper.
            return ;
        opDepth++ ; 
        try { op.visit(this); }
        finally { opDepth-- ; }
    }
    
    public void walk(Expr expr) {
        if ( expr == null )
            return ;
        if ( exprDepth == exprDepthLimit )
            return ;
        exprDepth++ ;
        try { expr.visit(this) ; }
        finally { exprDepth-- ; }
    }
    
    public void walk(ExprList exprList) {
        if ( exprList == null )
            return ;
        exprList.forEach(e->walk(e));
    }

    public void walk(VarExprList varExprList) {
        if ( varExprList == null )
            return ;
        // retains order.
        varExprList.forEachVarExpr((v,e) -> {
            Expr expr = (e!=null) ? e : Expr.NONE ;
            walk(expr) ;
        });
    }

    // ---- Mode swapping between op and expr. visit=>walk
    @Override
    public void visitExpr(ExprList exprList) {
        if ( exprVisitor != null )
            walk(exprList) ;
    }

    @Override
    public void visitVarExpr(VarExprList varExprList) {
        if ( exprVisitor != null )
            walk(varExprList);
    }
    
    // ----
    
    public void visitOp(Op op) {
        before(op) ;
        if ( opVisitor != null )
            op.visit(this);
        after(op) ;
    }

    @Override
    public void visit0(Op0 op) {
        before(op) ;
        if ( opVisitor != null )
            op.visit(opVisitor) ;
        after(op) ;
    }

    @Override
    public void visit1(Op1 op) {
        before(op) ;
        visit1$(op) ;
        after(op) ;
    }
    
    // Can be called via different routes. 
    private void visit1$(Op1 op) {
        if ( op.getSubOp() != null )
            op.getSubOp().visit(this) ;
        if ( opVisitor != null )
            op.visit(opVisitor) ;
    }

    @Override
    public void visit2(Op2 op) {
        before(op) ;
        if ( op.getLeft() != null )
            op.getLeft().visit(this) ;
        if ( op.getRight() != null )
            op.getRight().visit(this) ;
        if ( opVisitor != null )
            op.visit(opVisitor) ;
        after(op) ;
    }

    @Override
    public void visitN(OpN op) {
        before(op) ;
        for (Iterator<Op> iter = op.iterator(); iter.hasNext();) {
            Op sub = iter.next() ;
            sub.visit(this) ;
        }
        if ( opVisitor != null )
            op.visit(opVisitor) ;
        after(op) ;
    }
    
    @Override
    public void visitExt(OpExt op) {
        before(op) ;
        if ( opVisitor != null )
            op.visit(opVisitor) ;
        after(op) ;
    }

    @Override
    public void visit(OpOrder opOrder) {
        // XXX Why not this?
        // ApplyTransformVisitor handles the parts of OpOrder.  
//        before(opOrder) ;
//        visitSortConditions(opOrder.getConditions()) ;
//        visitModifer(opOrder);
//        visit1$(opOrder);
//        after(opOrder) ;
        visit1(opOrder) ;
    }

    @Override
    public void visit(OpAssign opAssign) {
        before(opAssign) ;
        VarExprList varExpr = opAssign.getVarExprList() ;
        visitVarExpr(varExpr); 
        visit1$(opAssign) ;
        after(opAssign) ;
    }

    @Override
    public void visit(OpExtend opExtend) {
        before(opExtend) ;
        VarExprList varExpr = opExtend.getVarExprList() ;
        visitVarExpr(varExpr); 
        visit1$(opExtend) ;
        after(opExtend) ;
    }

    
    // Transforming to quads needs the graph node handled before doing the sub-algebra ops
    // so it has to be done as before/after by the Walker. By the time visit(OpGraph) is called,
    // the sub-tree has already been visited. 
    
    
//    @Override
//    public void visit(OpGraph op) {
//        pushGraph(op.getNode()) ;
//        OpVisitorByTypeAndExpr.super.visit(op) ;
//        popGraph() ;
//    }
//    
//    private Deque<Node> stack = new ArrayDeque<>() ;
//    
//    public Node getCurrentGraph() { return stack.peek() ; }
//    
//    private void pushGraph(Node node) {
//        stack.push(node) ;   
//    }
//
//    private void popGraph() {
//        stack.pop() ;
//    }

    @Override
    public void visit(ExprFunction0 func) { visitExprFunction(func) ; }
    @Override
    public void visit(ExprFunction1 func) { visitExprFunction(func) ; }
    @Override
    public void visit(ExprFunction2 func) { visitExprFunction(func) ; }
    @Override
    public void visit(ExprFunction3 func) { visitExprFunction(func) ; }
    @Override
    public void visit(ExprFunctionN func) { visitExprFunction(func) ; }
    
    @Override
    public void visitExprFunction(ExprFunction func) {
        for ( int i = 1 ; i <= func.numArgs() ; i++ ) {
            Expr expr = func.getArg(i) ;
            if ( expr == null )
                // Put a dummy in, e.g. to keep the transform stack aligned.
                Expr.NONE.visit(this) ;
            else
                expr.visit(this) ;
        }
        if ( exprVisitor != null )
            func.visit(exprVisitor) ;
    }
    
    @Override
    public void visit(ExprFunctionOp funcOp) {
        walk(funcOp.getGraphPattern());
        if ( exprVisitor != null )
            funcOp.visit(exprVisitor) ;
    }
    
    @Override
    public void visit(NodeValue nv) {
        if ( exprVisitor != null )
            nv.visit(exprVisitor) ;
    }

    @Override
    public void visit(ExprVar v) {
        if ( exprVisitor != null )
            v.visit(exprVisitor) ;
    }

    @Override
    public void visit(ExprNone none) {
        if ( exprVisitor != null )
            none.visit(exprVisitor) ;
    }

    @Override
    public void visit(ExprAggregator eAgg) {
        // This is the assignment variable of the aggregation
        // not a normal variable of an expression.
        visitAssignVar(eAgg.getAggVar().asVar()) ;
        if ( exprVisitor != null )
            eAgg.visit(exprVisitor) ;
    }
}
