/*
 * 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.jackrabbit.oak.query.ast;

import static org.apache.jackrabbit.oak.query.ast.AstElementFactory.copyElementAndCheckReference;

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

import org.apache.jackrabbit.oak.api.Result.SizePrecision;
import org.apache.jackrabbit.oak.query.plan.ExecutionPlan;
import org.apache.jackrabbit.oak.query.plan.JoinExecutionPlan;
import org.apache.jackrabbit.oak.spi.query.Filter;
import org.apache.jackrabbit.oak.spi.state.NodeState;

/**
 * A join. This object contains the left hand side source, the right hand side
 * source, the join type, and the join condition.
 */
public class JoinImpl extends SourceImpl {
    private final JoinConditionImpl joinCondition;
    private JoinType joinType;
    private SourceImpl left;
    private SourceImpl right;

    private boolean leftNeedExecute, rightNeedExecute;
    private boolean leftNeedNext;
    private boolean foundJoinedRow;
    private boolean end;
    private NodeState rootState;
    
    private JoinExecutionPlan plan;

    public JoinImpl(SourceImpl left, SourceImpl right, JoinType joinType,
            JoinConditionImpl joinCondition) {
        this.left = left;
        this.right = right;
        this.joinType = joinType;
        this.joinCondition = joinCondition;
    }
    
    @Override
    public ArrayList<SourceImpl> getInnerJoinSelectors() {
        ArrayList<SourceImpl> list = new ArrayList<SourceImpl>();
        switch (joinType) {
        case INNER:
            list.addAll(left.getInnerJoinSelectors());
            list.addAll(right.getInnerJoinSelectors());
            break;
        case LEFT_OUTER:
            list.addAll(left.getInnerJoinSelectors());
            break;
        case RIGHT_OUTER:
            list.addAll(right.getInnerJoinSelectors());
        }
        return list;
    }
    
    @Override
    public List<JoinConditionImpl> getInnerJoinConditions() {
        ArrayList<JoinConditionImpl> set = new ArrayList<JoinConditionImpl>();
        switch (joinType) {
        case INNER:
            set.add(joinCondition);
            set.addAll(left.getInnerJoinConditions());
            set.addAll(right.getInnerJoinConditions());
            break;
        }
        return set;
    }

    public JoinConditionImpl getJoinCondition() {
        return joinCondition;
    }

    public SourceImpl getLeft() {
        return left;
    }

    public SourceImpl getRight() {
        return right;
    }

    @Override
    boolean accept(AstVisitor v) {
        return v.visit(this);
    }

    @Override
    public String getPlan(NodeState rootState) {
        StringBuilder buff = new StringBuilder();
        buff.append(left.getPlan(rootState)).
            append(' ').
            append(joinType).
            append(' ').
            append(right.getPlan(rootState)).
            append(" on ").
            append(joinCondition);
        return buff.toString();
    }

    @Override
    public String getIndexCostInfo(NodeState rootState) {
        StringBuilder buff = new StringBuilder();
        buff.append(toString());
        buff.append("{ ");
        buff.append(left.getIndexCostInfo(rootState));
        buff.append(", ");
        buff.append(right.getIndexCostInfo(rootState));
        buff.append(" }");
        return buff.toString();
    }

    @Override
    public String toString() {
        return left + " " + joinType +
                " " + right + " on " + joinCondition;
    }
    
    @Override
    public void unprepare() {
        left.unprepare();
        right.unprepare();
        plan = null;
    }
    
    private void applyJoinConditions() {
        switch (joinType) {
        case INNER:
            left.addJoinCondition(joinCondition, false);
            right.addJoinCondition(joinCondition, true);
            break;
        case LEFT_OUTER:
            left.setOuterJoin(true, false);
            right.setOuterJoin(false, true);
            left.addJoinCondition(joinCondition, false);
            right.addJoinCondition(joinCondition, true);
            break;
        case RIGHT_OUTER:
            // swap left and right
            // TODO right outer join: verify whether converting
            // to left outer join is always correct (given the current restrictions)
            joinType = JoinType.LEFT_OUTER;
            SourceImpl temp = left;
            left = right;
            right = temp;
            left.setOuterJoin(true, false);
            right.setOuterJoin(false, true);
            left.addJoinCondition(joinCondition, false);
            right.addJoinCondition(joinCondition, true);
            break;
        }
    }
    
    @Override
    public void prepare(ExecutionPlan p) {
        if (!(p instanceof JoinExecutionPlan)) {
            throw new IllegalArgumentException("Not a join plan");
        }
        JoinExecutionPlan joinPlan = (JoinExecutionPlan) p;
        if (joinPlan.getJoin() != this) {
            throw new IllegalArgumentException("Not a plan for this join");
        }
        this.plan = joinPlan;
        applyJoinConditions();
        left.prepare(joinPlan.getLeftPlan());
        right.prepare(joinPlan.getRightPlan());
    }

    @Override
    public ExecutionPlan prepare() {
        if (plan != null) {
            return plan;
        }
        applyJoinConditions();
        // the estimated cost is the cost of the left selector,
        // plus twice the cost of the right selector (we expect
        // two rows for the right selector for each node 
        // on the left selector)
        ExecutionPlan leftPlan = left.prepare();
        ExecutionPlan rightPlan = right.prepare();
        double cost = leftPlan.getEstimatedCost() + 2 * rightPlan.getEstimatedCost();
        plan = new JoinExecutionPlan(this, leftPlan, rightPlan, cost);
        return plan;
    }

    @Override
    public SelectorImpl getSelector(String selectorName) {
        SelectorImpl s = left.getSelector(selectorName);
        if (s == null) {
            s = right.getSelector(selectorName);
        }
        return s;
    }

    @Override
    public void execute(NodeState rootState) {
        this.rootState = rootState;
        leftNeedExecute = true;
        end = false;
    }

    @Override
    public Filter createFilter(boolean preparing) {
        // TODO is a join filter needed?
        return left.createFilter(preparing);
    }

    @Override
    public void setQueryConstraint(ConstraintImpl queryConstraint) {
        left.setQueryConstraint(queryConstraint);
        right.setQueryConstraint(queryConstraint);
    }    

    @Override
    public void setOuterJoin(boolean outerJoinLeftHandSide, boolean outerJoinRightHandSide) {
        left.setOuterJoin(outerJoinLeftHandSide, outerJoinRightHandSide);
        right.setOuterJoin(outerJoinLeftHandSide, outerJoinRightHandSide);
    }
    
    @Override
    public void addJoinCondition(JoinConditionImpl joinCondition, boolean forThisSelector) {
        left.addJoinCondition(joinCondition, forThisSelector);
        right.addJoinCondition(joinCondition, forThisSelector);
    }

    @Override
    public boolean next() {
        if (end) {
            return false;
        }
        if (leftNeedExecute) {
            left.execute(rootState);
            leftNeedExecute = false;
            leftNeedNext = true;
        }
        while (true) {
            if (leftNeedNext) {
                if (!left.next()) {
                    end = true;
                    return false;
                }
                leftNeedNext = false;
                rightNeedExecute = true;
            }
            if (rightNeedExecute) {
                right.execute(rootState);
                foundJoinedRow = false;
                rightNeedExecute = false;
            }
            if (!right.next()) {
                leftNeedNext = true;
            } else {
                if (joinCondition.evaluate()) {
                    foundJoinedRow = true;
                    return true;
                }
            }
            // for an outer join, if no matching result was found,
            // one row returned (with all values set to null)
            if (right.isOuterJoinRightHandSide() && leftNeedNext && !foundJoinedRow) {
                return true;
            }
        }
    }
    
    @Override
    public boolean isOuterJoinRightHandSide() {
        return left.isOuterJoinRightHandSide() || right.isOuterJoinRightHandSide();
    }

    @Override
    public long getSize(NodeState rootState, SizePrecision precision, long max) {
        // we don't know
        return -1;
    }

    @Override
    public AstElement copyOf() {
        return new JoinImpl(
            (SourceImpl) copyElementAndCheckReference(left),
            (SourceImpl) copyElementAndCheckReference(right),
            joinType,
            (JoinConditionImpl) copyElementAndCheckReference(joinCondition)
            );
    }
}
