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

import java.util.ArrayList;
import java.util.List;

import org.apache.jena.sparql.algebra.Op;
import org.apache.jena.sparql.algebra.TransformCopy;
import org.apache.jena.sparql.algebra.op.OpAssign;
import org.apache.jena.sparql.algebra.op.OpExtend;
import org.apache.jena.sparql.algebra.op.OpExtendAssign;
import org.apache.jena.sparql.algebra.op.OpProject;
import org.apache.jena.sparql.core.Var;
import org.apache.jena.sparql.core.VarExprList;
import org.apache.jena.sparql.expr.Expr;

/**
 * A transform capable of removing assignments from the algebra tree
 * 
 */
public class TransformRemoveAssignment extends TransformCopy {

    private Var var;
    private Expr expr;
    private boolean topmostOnly = true;
    private boolean aboveExtend = false;

    public TransformRemoveAssignment(Var var, Expr expr, boolean topmostOnly) {
        this.var = var;
        this.expr = expr;
        this.topmostOnly = topmostOnly;
    }

    public TransformRemoveAssignment(Var var, Expr expr) {
        this(var, expr, true);
    }

    @Override
    public Op transform(OpAssign opAssign, Op subOp) {
        VarExprList assignments = processAssignments(opAssign);
        if (assignments == null)
            return super.transform(opAssign, subOp);

        // Rewrite appropriately
        if (this.topmostOnly) {
            // If topmost only ignore any transformations lower down the tree
            // hence call getSubOp() rather than using the provided subOp
            if (assignments.size() > 0) {
                return OpAssign.assign(opAssign.getSubOp(), assignments);
            } else {
                return opAssign.getSubOp();
            }
        } else {
            // Otherwise preserve any transformations from lower down the tree
            if (assignments.size() > 0) {
                return OpAssign.assign(subOp, assignments);
            } else {
                return subOp;
            }
        }
    }

    private VarExprList processAssignments(OpExtendAssign opAssign) {
        VarExprList orig = opAssign.getVarExprList();
        if (!orig.contains(this.var))
            return null;
        if (!orig.getExpr(this.var).equals(this.expr))
            return null;

        VarExprList modified = new VarExprList();
        for (Var v : orig.getVars()) {
            if (!v.equals(this.var)) {
                modified.add(v, orig.getExpr(v));
            }
        }
        if (modified.size() > 0 && modified.size() == orig.size())
            return null;
        
        return modified;
    }

    @Override
    public Op transform(OpExtend opExtend, Op subOp) {
        VarExprList assignments = processAssignments(opExtend);
        if (assignments == null)
            return super.transform(opExtend, subOp);
        
        this.aboveExtend = true;

        // Rewrite appropriately
        if (this.topmostOnly) {
            // If topmost only ignore any transformations lower down the tree
            // hence call getSubOp() rather than using the provided subOp
            if (assignments.size() > 0) {
                return OpExtend.create(opExtend.getSubOp(), assignments);
            } else {
                return opExtend.getSubOp();
            }
        } else {
            // Otherwise preserve any transformations from lower down the tree
            if (assignments.size() > 0) {
                return OpExtend.create(subOp, assignments);
            } else {
                return subOp;
            }
        }
    }

    @Override
    public Op transform(OpProject opProject, Op subOp) {
        if (!opProject.getVars().contains(this.var))
            return super.transform(opProject, subOp);
        
        List<Var> newVars = new ArrayList<>(opProject.getVars());
        newVars.remove(this.var);
        if (this.topmostOnly) {
            if (this.aboveExtend) {
                return new OpProject(subOp, newVars);
            } else {
                return opProject;
            }
        } else {
            return new OpProject(subOp, newVars);
        }
    }
}
