/*
 * 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.arq.querybuilder.rewriters;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.jena.graph.Node ;
import org.apache.jena.query.SortCondition ;
import org.apache.jena.sparql.algebra.Op ;
import org.apache.jena.sparql.algebra.OpVisitor ;
import org.apache.jena.sparql.algebra.Table ;
import org.apache.jena.sparql.algebra.op.* ;
import org.apache.jena.sparql.algebra.table.TableN ;
import org.apache.jena.sparql.core.* ;
import org.apache.jena.sparql.engine.binding.Binding ;
import org.apache.jena.sparql.expr.ExprAggregator ;
import org.apache.jena.sparql.expr.ExprList ;
import org.apache.jena.sparql.pfunction.PropFuncArg ;

/**
 * A rewriter that implements OpVisitor.
 *
 */
class OpRewriter extends AbstractRewriter<Op> implements OpVisitor {

	/**
	 * Constructor.
	 * @param values The values to replace.
	 */
	OpRewriter(Map<Var, Node> values) {
		super(values);
	}

	private Quad rewrite(Quad q) {
		return new Quad(changeNode(q.getGraph()), changeNode(q.getSubject()),
				changeNode(q.getPredicate()), changeNode(q.getObject()));
	}

	private QuadPattern rewrite(QuadPattern pattern) {
		QuadPattern qp = new QuadPattern();
		for (Quad q : pattern.getList()) {
			qp.add(rewrite(q));
		}
		return qp;
	}

	private List<Op> rewriteOpList(List<Op> lst) {
		List<Op> retval = new ArrayList<>();
		for (Op o : lst) {
			o.visit(this);
			retval.add(pop());
		}
		return retval;
	}

	private BasicPattern rewrite(BasicPattern pattern) {
		return BasicPattern.wrap(rewrite(pattern.getList()));
	}

	@Override
	public void visit(OpBGP opBGP) {
		push(new OpBGP(rewrite(opBGP.getPattern())));
	}

	@Override
	public void visit(OpQuadPattern quadPattern) {
		push(new OpQuadPattern(changeNode(quadPattern.getGraphNode()),
				rewrite(quadPattern.getBasicPattern())));
	}

	@Override
	public void visit(OpQuadBlock quadBlock) {
		push(new OpQuadBlock(rewrite(quadBlock.getPattern())));
	}

	@Override
	public void visit(OpTriple opTriple) {
		push(new OpTriple(rewrite(opTriple.getTriple())));
	}

	@Override
	public void visit(OpQuad opQuad) {
		push(new OpQuad(rewrite(opQuad.getQuad())));

	}

	@Override
	public void visit(OpPath opPath) {
		push(new OpPath(rewrite(opPath.getTriplePath())));
	}

	@Override
	public void visit(OpTable opTable) {
		Table tbl = opTable.getTable();
		boolean process = false;
		for (Var v : tbl.getVars()) {
			process = process | values.keySet().contains(v);
		}
		if (!process) {
			push(opTable);
		} else {
			TableN retTbl = new TableN(tbl.getVars());
			Iterator<Binding> iter = tbl.rows();
			while (iter.hasNext()) {
				retTbl.addBinding(rewrite(iter.next()));
			}
			push(OpTable.create(retTbl));
		}
	}

	@Override
	public void visit(OpNull opNull) {
		push(opNull);
	}

	@Override
	public void visit(OpProcedure opProc) {
		opProc.getSubOp().visit(this);
		Op op = pop();
		ExprList args = new ExprRewriter(values).rewrite(opProc.getArgs());
		Node procId = changeNode(opProc.getProcId());
		push(new OpProcedure(procId, args, op));
	}

	private PropFuncArg rewrite(PropFuncArg arg) {
		if (arg.isList()) {
			List<Node> lst = changeNodes(arg.getArgList());
			return new PropFuncArg(lst, null);
		}
		return new PropFuncArg(changeNode(arg.getArg()));
	}

	@Override
	public void visit(OpPropFunc opPropFunc) {
		opPropFunc.getSubOp().visit(this);
		Op op = pop();
		Node uri = changeNode(opPropFunc.getProperty());
		PropFuncArg args1 = rewrite(opPropFunc.getSubjectArgs());
		PropFuncArg args2 = rewrite(opPropFunc.getObjectArgs());
		push(new OpPropFunc(uri, args1, args2, op));
	}

	@Override
	public void visit(OpFilter opFilter) {
		opFilter.getSubOp().visit(this);
		push(OpFilter.filterBy(
				new ExprRewriter(values).rewrite(opFilter.getExprs()), pop()));
	}

	@Override
	public void visit(OpGraph opGraph) {
		opGraph.getSubOp().visit(this);
		push(new OpGraph(changeNode(opGraph.getNode()), pop()));
	}

	@Override
	public void visit(OpService opService) {
		opService.getSubOp().visit(this);
		push(new OpService(changeNode(opService.getService()), pop(),
				opService.getSilent()));
	}

	@Override
	public void visit(OpDatasetNames dsNames) {
		push(new OpDatasetNames(changeNode(dsNames.getGraphNode())));
	}

	@Override
	public void visit(OpLabel opLabel) {
		if (opLabel.hasSubOp()) {
			opLabel.getSubOp().visit(this);
			push(OpLabel.create(opLabel.getObject(), pop()));
		} else {
			push(opLabel);
		}
	}

	@Override
	public void visit(OpAssign opAssign) {
		opAssign.getSubOp().visit(this);
		push(OpAssign.assign(pop(), rewrite(opAssign.getVarExprList())));
	}

	@Override
	public void visit(OpExtend opExtend) {
		opExtend.getSubOp().visit(this);
		push(OpExtend.extend(pop(), rewrite(opExtend.getVarExprList())));
	}

	@Override
	public void visit(OpJoin opJoin) {
		opJoin.getRight().visit(this);
		opJoin.getLeft().visit(this);
		push(OpJoin.create(pop(), pop()));
	}

	@Override
	public void visit(OpLeftJoin opLeftJoin) {
		opLeftJoin.getRight().visit(this);
		opLeftJoin.getLeft().visit(this);
		push(OpLeftJoin.create(pop(), pop(),
				new ExprRewriter(values).rewrite(opLeftJoin.getExprs())));
	}

	@Override
	public void visit(OpUnion opUnion) {
		opUnion.getRight().visit(this);
		opUnion.getLeft().visit(this);
		push(OpUnion.create(pop(), pop()));
	}

	@Override
	public void visit(OpDiff opDiff) {
		opDiff.getRight().visit(this);
		opDiff.getLeft().visit(this);
		push(OpDiff.create(pop(), pop()));
	}

	@Override
	public void visit(OpMinus opMinus) {
		opMinus.getRight().visit(this);
		opMinus.getLeft().visit(this);
		push(OpMinus.create(pop(), pop()));
	}

	@Override
	public void visit(OpConditional opCondition) {
		opCondition.getRight().visit(this);
		opCondition.getLeft().visit(this);
		push(new OpConditional(pop(), pop()));
	}

	@Override
	public void visit(OpSequence opSequence) {
		List<Op> lst = rewriteOpList(opSequence.getElements());
		push(opSequence.copy(lst));
	}

	@Override
	public void visit(OpDisjunction opDisjunction) {
		List<Op> lst = rewriteOpList(opDisjunction.getElements());
		push(opDisjunction.copy(lst));
	}

	@Override
	public void visit(OpExt opExt) {
		push(opExt);
	}

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

	@Override
	public void visit(OpOrder opOrder) {
		List<SortCondition> lst = new ExprRewriter(values)
				.rewriteSortConditionList(opOrder.getConditions());
		opOrder.getSubOp().visit(this);
		push(new OpOrder(pop(), lst));
	}

	@Override
	public void visit(OpProject opProject) {
		opProject.getSubOp().visit(this);
		List<Var> vars = new ArrayList<>();
		for (Var v : opProject.getVars()) {
			Node n = changeNode(v);
			vars.add(Var.alloc(n));
		}
		push(new OpProject(pop(), vars));
	}

	@Override
	public void visit(OpReduced opReduced) {
		opReduced.getSubOp().visit(this);
		push(opReduced.copy(pop()));
	}

	@Override
	public void visit(OpDistinct opDistinct) {
		opDistinct.getSubOp().visit(this);
		push(opDistinct.copy(pop()));
	}

	@Override
	public void visit(OpSlice opSlice) {
		opSlice.getSubOp().visit(this);
		push(opSlice.copy(pop()));
	}

	@Override
	public void visit(OpGroup opGroup) {
		opGroup.getSubOp().visit(this);
		ExprRewriter expRewriter = new ExprRewriter(values);
		VarExprList groupVars = rewrite(opGroup.getGroupVars());
		List<ExprAggregator> aggregators = new ArrayList<>();
		for (ExprAggregator ea : opGroup.getAggregators()) {
			ea.visit(expRewriter);
			aggregators.add((ExprAggregator) expRewriter.pop());
		}
		push(new OpGroup(pop(), groupVars, aggregators));
	}

	@Override
	public void visit(OpTopN opTop) {
		opTop.getSubOp().visit(this);
		ExprRewriter expRewriter = new ExprRewriter(values);
		expRewriter.rewriteSortConditionList(opTop.getConditions());
		push(new OpTopN(pop(), opTop.getLimit(),
				expRewriter.rewriteSortConditionList(opTop.getConditions())));
	}
}