/**
 * 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.pig.backend.hadoop.executionengine.tez.plan.optimizer;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.pig.PigConfiguration;
import org.apache.pig.StoreFunc;
import org.apache.pig.backend.hadoop.executionengine.physicalLayer.PhysicalOperator;
import org.apache.pig.backend.hadoop.executionengine.physicalLayer.expressionOperators.POUserFunc;
import org.apache.pig.backend.hadoop.executionengine.physicalLayer.plans.PhysicalPlan;
import org.apache.pig.backend.hadoop.executionengine.physicalLayer.relationalOperators.POSplit;
import org.apache.pig.backend.hadoop.executionengine.physicalLayer.relationalOperators.POStore;
import org.apache.pig.backend.hadoop.executionengine.physicalLayer.util.PlanHelper;
import org.apache.pig.backend.hadoop.executionengine.tez.plan.TezEdgeDescriptor;
import org.apache.pig.backend.hadoop.executionengine.tez.plan.TezOpPlanVisitor;
import org.apache.pig.backend.hadoop.executionengine.tez.plan.TezOperPlan;
import org.apache.pig.backend.hadoop.executionengine.tez.plan.TezOperator;
import org.apache.pig.backend.hadoop.executionengine.tez.plan.TezOperator.OPER_FEATURE;
import org.apache.pig.backend.hadoop.executionengine.tez.plan.TezOperator.VertexGroupInfo;
import org.apache.pig.backend.hadoop.executionengine.tez.plan.operator.POStoreTez;
import org.apache.pig.backend.hadoop.executionengine.tez.plan.operator.POValueOutputTez;
import org.apache.pig.backend.hadoop.executionengine.tez.plan.udf.ReadScalarsTez;
import org.apache.pig.backend.hadoop.executionengine.tez.runtime.HashValuePartitioner;
import org.apache.pig.backend.hadoop.executionengine.tez.runtime.TezInput;
import org.apache.pig.backend.hadoop.executionengine.tez.runtime.TezOutput;
import org.apache.pig.backend.hadoop.executionengine.tez.util.TezCompilerUtil;
import org.apache.pig.backend.hadoop.hbase.HBaseStorage;
import org.apache.pig.builtin.AvroStorage;
import org.apache.pig.builtin.JsonStorage;
import org.apache.pig.builtin.OrcStorage;
import org.apache.pig.builtin.PigStorage;
import org.apache.pig.builtin.mock.Storage;
import org.apache.pig.impl.plan.OperatorKey;
import org.apache.pig.impl.plan.PlanException;
import org.apache.pig.impl.plan.ReverseDependencyOrderWalker;
import org.apache.pig.impl.plan.VisitorException;
import org.apache.tez.dag.api.EdgeProperty.DataMovementType;
import org.apache.tez.runtime.library.input.UnorderedKVInput;
import org.apache.tez.runtime.library.output.UnorderedPartitionedKVOutput;

/**
 * Optimizes union by removing the intermediate union vertex and making the
 * successor get input from the predecessor vertices directly using VertexGroup.
 * This should be run after MultiQueryOptimizer so that it handles cases like
 * union followed by split and then store.
 *
 * For eg:
 * 1) Union followed by store
 * Vertex 1 (Load), Vertex 2 (Load) -> Vertex 3 (Union + Store) will be optimized to
 * Vertex 1 (Load + Store), Vertex 2 (Load + Store). Both the vertices will be writing output
 * to same store location directly which is supported by Tez.
 * 2) Union followed by groupby
 * Vertex 1 (Load), Vertex 2 (Load) -> Vertex 3 (Union + POLocalRearrange) -> Vertex 4 (Group by)
 * will be optimized to Vertex 1 (Load + POLR), Vertex 2 (Load + POLR) -> Vertex 4 (Group by)
 *
 */
public class UnionOptimizer extends TezOpPlanVisitor {

    private static final Log LOG = LogFactory.getLog(UnionOptimizer.class);
    private TezOperPlan tezPlan;
    private List<String> supportedStoreFuncs;
    private List<String> unsupportedStoreFuncs;

    public UnionOptimizer(TezOperPlan plan, List<String> supportedStoreFuncs, List<String> unsupportedStoreFuncs) {
        super(plan, new ReverseDependencyOrderWalker<TezOperator, TezOperPlan>(plan));
        tezPlan = plan;
        this.supportedStoreFuncs = supportedStoreFuncs;
        this.unsupportedStoreFuncs = unsupportedStoreFuncs;
    }

    public static boolean isOptimizable(TezOperator tezOp) throws VisitorException {
        if((tezOp.isLimit() || tezOp.isLimitAfterSort()) && tezOp.getRequestedParallelism() == 1) {
            return false;
        }

        // If user has specified a PARALLEL clause with the union operator
        // turn off union optimization
        if (tezOp.getRequestedParallelism() != -1) {
            return false;
        }
        // Two vertices separately ranking with 1 to n and writing to output directly
        // will make each rank repeate twice which is wrong. Rank always needs to be
        // done from single vertex to have the counting correct.
        if (tezOp.isRankCounter()) {
            return false;
        }
        return true;
    }

    public static boolean isOptimizableStoreFunc(TezOperator tezOp,
            List<String> supportedStoreFuncs, List<String> unsupportedStoreFuncs)
            throws VisitorException {
        List<POStoreTez> stores = PlanHelper.getPhysicalOperators(tezOp.plan, POStoreTez.class);

        // If any store function does not support parallel writes, then we cannot use this optimization
        for (POStoreTez store : stores) {
            String name = store.getStoreFunc().getClass().getName();
            Boolean supportsParallelWriteToStoreLocation = store.getStoreFunc().supportsParallelWriteToStoreLocation();

            // We process exclusions first, then inclusions. This way, a user can explicitly disable parallel stores
            // for a UDF that claims to support it, but cannot enable parallel stores for a UDF that claims not to.
            //
            // Logical flow:
            // 1) If the store function is explicitly listed as unsupported, then return false
            // 2) If the store function specifies itself as unsupported, then return false
            // 3) If the store function specifies itself as supported, then continue (true case)
            // 4) If the store function is explicitly listed as support, then continue (true case)
            // 5) Otherwise, return false

            if (unsupportedStoreFuncs != null && unsupportedStoreFuncs.contains(name)) {
                LOG.warn(name + " does not support union optimization."
                         + " Disabling it. There will be some performance degradation.");
                return false;
            }

            if (supportsParallelWriteToStoreLocation != null) {
                if (supportsParallelWriteToStoreLocation) {
                    continue;
                } else {
                    LOG.warn(name + " does not support union optimization."
                             + " Disabling it. There will be some performance degradation.");
                    return false;
                }
            }

            if (supportedStoreFuncs != null && supportedStoreFuncs.contains(name)) {
                continue;
            }

            LOG.warn(PigConfiguration.PIG_TEZ_OPT_UNION_SUPPORTED_STOREFUNCS
                     + " does not contain " + name
                     + " and so disabling union optimization. There will be some performance degradation. "
                     + "If your storefunc does not hardcode part file names and can work with multiple vertices writing to the output location,"
                     + " run pig with -D"
                     + PigConfiguration.PIG_TEZ_OPT_UNION_SUPPORTED_STOREFUNCS
                     + "=<Comma separated list of fully qualified StoreFunc class names> to enable the optimization. Refer PIG-4691");
            return false;
        }

        return true;
    }

    @Override
    public void visitTezOp(TezOperator tezOp) throws VisitorException {
        if (!tezOp.isUnion()) {
            return;
        }

        if (!isOptimizable(tezOp)) {
            return;
        }

        TezOperator unionOp = tezOp;
        String scope = unionOp.getOperatorKey().scope;
        PhysicalPlan unionOpPlan = unionOp.plan;

        Set<OperatorKey> uniqueUnionMembers = new HashSet<OperatorKey>(unionOp.getUnionMembers());
        List<TezOperator> predecessors = new ArrayList<TezOperator>(tezPlan.getPredecessors(unionOp));
        List<TezOperator> successors = tezPlan.getSuccessors(unionOp) == null ? null
                : new ArrayList<TezOperator>(tezPlan.getSuccessors(unionOp));

        if (uniqueUnionMembers.size() != 1) {

            if (!isOptimizableStoreFunc(tezOp, supportedStoreFuncs, unsupportedStoreFuncs)) {
                return;
            }

            if (successors != null) {
                for (TezOperator succ : successors) {
                    for (TezOperator pred : predecessors) {
                        if (succ.inEdges.containsKey(pred.getOperatorKey())) {
                            // Stop here, we cannot convert the node into vertex group
                            // Otherwise, we will end up with a parallel edge between pred
                            // and succ
                            return;
                        }
                    }
                }
            }

            // TODO: PIG-3856 Handle replicated join and skewed join sample.
            // Replicate join small table/skewed join sample that was broadcast to union vertex
            // now needs to be broadcast to all the union predecessors. How do we do that??
            // Wait for shared edge and do it or write multiple times??
            // For now don't optimize except in the case of Split where we need to write only once
            if (predecessors.size() > unionOp.getUnionMembers().size()) {
                return;
            }
        }

        if (uniqueUnionMembers.size() == 1) {
            // We actually don't need VertexGroup in this case. The multiple
            // sub-plans of Split can write to same MROutput or the Tez LogicalOutput
            OperatorKey splitPredKey = uniqueUnionMembers.iterator().next();
            TezOperator splitPredOp = tezPlan.getOperator(splitPredKey);
            PhysicalPlan splitPredPlan = splitPredOp.plan;
            if (splitPredPlan.getLeaves().get(0) instanceof POSplit) { //It has to be. But check anyways

                for( TezOperator op : predecessors ) {
                    if( !op.getOperatorKey().equals(splitPredKey)) {
                        Set<TezOperator> allNonMemberPredecessorsAncestors = new HashSet<TezOperator>();
                        TezCompilerUtil.addAllPredecessors(tezPlan, op, allNonMemberPredecessorsAncestors);
                        // If any of the nonMemberPredecessor's ancestors(recursive predecessor)
                        // is from the single unionmember, then we stop the merge effort to avoid creating
                        // an illegal loop.
                        if( allNonMemberPredecessorsAncestors.contains(splitPredOp) ) {
                            return;
                        }
                    }
                }

                try {
                    connectUnionNonMemberPredecessorsToSplit(unionOp, splitPredOp, predecessors);

                    // Remove POShuffledValueInputTez from union plan root
                    unionOpPlan.remove(unionOpPlan.getRoots().get(0));
                    // Clone union plan into split subplans
                    for (int i=0; i < Collections.frequency(unionOp.getUnionMembers(), splitPredKey); i++ ) {
                        cloneAndMergeUnionPlan(unionOp, splitPredOp);
                    }
                    copyOperatorProperties(splitPredOp, unionOp);
                    tezPlan.disconnect(splitPredOp, unionOp);

                    connectSplitOpToUnionSuccessors(unionOp, splitPredOp, successors);
                } catch (PlanException e) {
                    throw new VisitorException(e);
                }

                //Remove union operator from the plan
                tezPlan.remove(unionOp);
                return;
            } else {
                throw new VisitorException("Expected POSplit but found " + splitPredPlan.getLeaves().get(0));
            }
        }

        // Create vertex group operator for each store. Union followed by Split
        // followed by Store could have multiple stores
        List<POStoreTez> unionStoreOutputs = PlanHelper.getPhysicalOperators(unionOpPlan, POStoreTez.class);
        TezOperator[] storeVertexGroupOps = new TezOperator[unionStoreOutputs.size()];
        for (int i=0; i < storeVertexGroupOps.length; i++) {
            TezOperator existingVertexGroup = null;
            if (successors != null) {
                for (TezOperator succ : successors) {
                    if (succ.isVertexGroup() && unionStoreOutputs.get(i).getSFile().equals(succ.getVertexGroupInfo().getSFile())) {
                        existingVertexGroup = succ;
                        break;
                    }
                }
            }
            if (existingVertexGroup == null) {
                // In the case of union + split + union + store, the different stores in the Split
                // will be writing to same location after second union operator is optimized.
                // So while optimizing the first union, we should just make it write to one vertex group
                for (int j = 0; j < i; j++) {
                    if (unionStoreOutputs.get(i).getSFile().equals(storeVertexGroupOps[j].getVertexGroupInfo().getSFile())) {
                        storeVertexGroupOps[i] = storeVertexGroupOps[j];
                        break;
                    }
                }
                if (storeVertexGroupOps[i] != null) {
                    continue;
                }
                storeVertexGroupOps[i] = new TezOperator(OperatorKey.genOpKey(scope));
                storeVertexGroupOps[i].setVertexGroupInfo(new VertexGroupInfo(unionStoreOutputs.get(i)));
                storeVertexGroupOps[i].getVertexGroupInfo().setSFile(unionStoreOutputs.get(i).getSFile());
                storeVertexGroupOps[i].setVertexGroupMembers(new ArrayList<OperatorKey>(unionOp.getUnionMembers()));
                tezPlan.add(storeVertexGroupOps[i]);
            } else {
                storeVertexGroupOps[i] = existingVertexGroup;
                existingVertexGroup.getVertexGroupMembers().remove(unionOp.getOperatorKey());
                existingVertexGroup.getVertexGroupMembers().addAll(unionOp.getUnionMembers());
                existingVertexGroup.getVertexGroupInfo().removeInput(unionOp.getOperatorKey());
            }
        }

        // Create vertex group operator for each output. Case of split, orderby,
        // skewed join, rank, etc will have multiple outputs
        List<TezOutput> unionOutputs = PlanHelper.getPhysicalOperators(unionOpPlan, TezOutput.class);
        // One TezOutput can write to multiple LogicalOutputs (POCounterTez, POValueOutputTez, etc)
        List<String> unionOutputKeys = new ArrayList<String>();
        for (TezOutput output : unionOutputs) {
            if (output instanceof POStoreTez) {
                continue;
            }
            for (String key : output.getTezOutputs()) {
                unionOutputKeys.add(key);
            }
        }
        TezOperator[] outputVertexGroupOps = new TezOperator[unionOutputKeys.size()];
        String[] newOutputKeys = new String[unionOutputKeys.size()];
        for (int i=0; i < outputVertexGroupOps.length; i++) {
            TezOperator existingVertexGroup = null;
            if (successors != null) {
                for (TezOperator succ : successors) {
                    if (succ.isVertexGroup()
                        && unionOutputKeys.get(i).equals(succ.getVertexGroupInfo().getOutput()) ) {
                        existingVertexGroup = succ;
                        break;
                    }
                }
            }
            if (existingVertexGroup == null) {
                for (int j = 0; j < i; j++) {
                    if (unionOutputKeys.get(i).equals(unionOutputKeys.get(j))) {
                        outputVertexGroupOps[i] = outputVertexGroupOps[j];
                        break;
                    }
                }
                if (outputVertexGroupOps[i] != null) {
                    continue;
                }
                outputVertexGroupOps[i] = new TezOperator(OperatorKey.genOpKey(scope));
                outputVertexGroupOps[i].setVertexGroupInfo(new VertexGroupInfo());
                outputVertexGroupOps[i].getVertexGroupInfo().setOutput(unionOutputKeys.get(i));
                outputVertexGroupOps[i].setVertexGroupMembers(new ArrayList<OperatorKey>(unionOp.getUnionMembers()));
            } else {
                outputVertexGroupOps[i] = existingVertexGroup;
                existingVertexGroup.getVertexGroupMembers().remove(unionOp.getOperatorKey());
                existingVertexGroup.getVertexGroupMembers().addAll(unionOp.getUnionMembers());
                existingVertexGroup.getVertexGroupInfo().removeInput(unionOp.getOperatorKey());
            }
            newOutputKeys[i] = outputVertexGroupOps[i].getOperatorKey().toString();
            tezPlan.add(outputVertexGroupOps[i]);
        }

        // Change plan from Predecessors -> Union -> Successor(s) to
        // Predecessors -> Vertex Group(s) -> Successor(s)
        try {
             // Remove POShuffledValueInputTez from union plan root
            unionOpPlan.remove(unionOpPlan.getRoots().get(0));

            for (OperatorKey predKey : unionOp.getUnionMembers()) {
                TezOperator pred = tezPlan.getOperator(predKey);
                PhysicalPlan clonePlan = cloneAndMergeUnionPlan(unionOp, pred);
                connectPredecessorsToVertexGroups(unionOp, pred, clonePlan,
                        storeVertexGroupOps, outputVertexGroupOps);
            }

            connectVertexGroupsToSuccessors(unionOp, successors,
                    unionOutputKeys, outputVertexGroupOps);

            replaceSuccessorInputsAndDisconnect(unionOp, successors, unionOutputKeys, newOutputKeys);

            //Remove union operator from the plan
            tezPlan.remove(unionOp);
        } catch (VisitorException e) {
            throw e;
        }  catch (Exception e) {
            throw new VisitorException(e);
        }

    }

    /**
     * Connect the predecessors of the union which are not members of the union
     * (usually FRJoin replicated table orSkewedJoin sample) to the Split op
     * which is the only member of the union. Disconnect those predecessors from the union.
     *
     * Replace the output keys of those predecessors with the split operator
     * key instead of the union operator key.
     *
     * @param unionOp Union operator
     * @param splitPredOp Split operator which is the only member of the union and its predecessor
     * @param unionPredecessors Predecessors of the union including the split operator
     * @throws PlanException
     * @throws VisitorException
     */
    private void connectUnionNonMemberPredecessorsToSplit(TezOperator unionOp,
            TezOperator splitPredOp,
            List<TezOperator> unionPredecessors) throws PlanException, VisitorException {
        String unionOpKey = unionOp.getOperatorKey().toString();
        OperatorKey splitPredKey = splitPredOp.getOperatorKey();
        for (TezOperator pred : unionPredecessors) {

            if (!pred.getOperatorKey().equals(splitPredKey)) { //Skip splitPredOp which is also a predecessor
                // Get actual predecessors if predecessor is a vertex group
                TezOperator predVertexGroup = null;
                List<TezOperator> actualPreds = new ArrayList<TezOperator>();
                if (pred.isVertexGroup()) {
                    predVertexGroup = pred;
                    for (OperatorKey opKey : pred.getVertexGroupMembers()) {
                        // There should not be multiple levels of vertex group. So no recursion required.
                        actualPreds.add(tezPlan.getOperator(opKey));
                    }
                    tezPlan.disconnect(predVertexGroup, unionOp);
                    tezPlan.connect(predVertexGroup, splitPredOp);
                } else {
                    actualPreds.add(pred);
                }

                for (TezOperator actualPred : actualPreds) {

                    TezCompilerUtil.replaceOutput(actualPred, unionOpKey, splitPredKey.toString());

                    TezEdgeDescriptor edge = actualPred.outEdges.remove(unionOp.getOperatorKey());
                    if (edge == null) {
                        throw new VisitorException("Edge description is empty");
                    }
                    actualPred.outEdges.put(splitPredKey, edge);
                    splitPredOp.inEdges.put(actualPred.getOperatorKey(), edge);
                    if (predVertexGroup == null) {
                        // Disconnect FRJoin table/SkewedJoin sample edge to
                        // union op and connect to POSplit
                        tezPlan.disconnect(actualPred, unionOp);
                        tezPlan.connect(actualPred, splitPredOp);
                    }
                }
            }
        }
    }

    /**
     * Connect the split operator to the successors of the union operators and update the edges.
     * Also change the inputs of the successor from the union operator to the split operator.
     *
     * @param unionOp Union operator
     * @param splitPredOp Split operator which is the only member of the union
     * @param successors Successors of the union operator
     * @throws PlanException
     * @throws VisitorException
     */
    private void connectSplitOpToUnionSuccessors(TezOperator unionOp,
            TezOperator splitPredOp, List<TezOperator> successors)
            throws PlanException, VisitorException {
        String unionOpKey = unionOp.getOperatorKey().toString();
        String splitPredOpKey = splitPredOp.getOperatorKey().toString();
        List<TezOperator> splitSuccessors = tezPlan.getSuccessors(splitPredOp);
        if (successors != null) {
            for (TezOperator succ : successors) {
                TezOperator successorVertexGroup = null;
                boolean removeSuccessorVertexGroup = false;
                List<TezOperator> actualSuccs = new ArrayList<TezOperator>();
                if (succ.isVertexGroup()) {
                    successorVertexGroup = succ;
                    if (tezPlan.getSuccessors(successorVertexGroup) != null) {
                        // There should not be multiple levels of vertex group. So no recursion required.
                        actualSuccs.addAll(tezPlan.getSuccessors(successorVertexGroup));
                    }
                    int index = succ.getVertexGroupMembers().indexOf(unionOp.getOperatorKey());
                    while (index > -1) {
                        succ.getVertexGroupMembers().set(index, splitPredOp.getOperatorKey());
                        index = succ.getVertexGroupMembers().indexOf(unionOp.getOperatorKey());
                    }
                    // Store vertex group
                    POStore store = successorVertexGroup.getVertexGroupInfo().getStore();
                    if (store != null) {
                        //Clone changes the operator keys
                        List<POStoreTez> storeOutputs = PlanHelper.getPhysicalOperators(splitPredOp.plan, POStoreTez.class);
                        for (POStoreTez storeOut : storeOutputs) {
                            if (storeOut.getOutputKey().equals(store.getOperatorKey().toString())) {
                                splitPredOp.addVertexGroupStore(storeOut.getOperatorKey(), successorVertexGroup.getOperatorKey());
                            }
                        }
                    }
                    tezPlan.disconnect(unionOp, successorVertexGroup);
                    Set<OperatorKey> uniqueVertexGroupMembers = new HashSet<OperatorKey>(succ.getVertexGroupMembers());
                    if (uniqueVertexGroupMembers.size() == 1) {
                        //Only splitPredOp is member of the vertex group. Get rid of the vertex group
                        removeSuccessorVertexGroup = true;
                    } else {
                        // Avoid connecting multiple times in case of union + self join
                        if (splitSuccessors == null || !splitSuccessors.contains(successorVertexGroup)) {
                            tezPlan.connect(splitPredOp, successorVertexGroup);
                        }
                    }
                } else {
                    actualSuccs.add(succ);
                }

                // Store vertex group
                if (actualSuccs.isEmpty() && removeSuccessorVertexGroup) {
                    splitPredOp.removeVertexGroupStore(successorVertexGroup.getOperatorKey());
                    tezPlan.remove(successorVertexGroup);
                }

                for (TezOperator actualSucc : actualSuccs) {

                    TezCompilerUtil.replaceInput(actualSucc, unionOpKey, splitPredOpKey);

                    TezEdgeDescriptor edge = actualSucc.inEdges.remove(unionOp.getOperatorKey());
                    if (edge == null) {
                        throw new VisitorException("Edge description is empty");
                    }
                    actualSucc.inEdges.put(splitPredOp.getOperatorKey(), edge);
                    splitPredOp.outEdges.put(actualSucc.getOperatorKey(), edge);
                    if (successorVertexGroup == null || removeSuccessorVertexGroup) {
                        if (removeSuccessorVertexGroup) {
                            // Changes plan from SplitOp -> Union -> VertexGroup - > Successor
                            // to SplitOp -> Successor
                            tezPlan.disconnect(successorVertexGroup, actualSucc);
                            tezPlan.remove(successorVertexGroup);
                            TezCompilerUtil.replaceInput(actualSucc, successorVertexGroup.getOperatorKey().toString(), splitPredOpKey);
                        } else {
                            // Changes plan from SplitOp -> Union -> Successor
                            // to SplitOp -> Successor
                            tezPlan.disconnect(unionOp, actualSucc);
                        }
                        // Avoid connecting multiple times in case of union + self join
                        if (splitSuccessors == null || !splitSuccessors.contains(actualSucc)) {
                            tezPlan.connect(splitPredOp, actualSucc);
                        }
                    }
                }
            }
        }
    }

    /**
     * Clone plan of union and merge it into the predecessor operator
     *
     * @param unionOp Union operator
     * @param predOp Predecessor operator of union to which union plan should be merged to
     */
    private PhysicalPlan cloneAndMergeUnionPlan(TezOperator unionOp, TezOperator predOp) throws VisitorException {
        try {
            PhysicalPlan predPlan = predOp.plan;
            PhysicalOperator predLeaf = predPlan.getLeaves().get(0);
            // if predLeaf not POValueOutputTez
            if (predLeaf instanceof POSplit) {
                // Find the subPlan that connects to the union operator
                predPlan = getUnionPredPlanFromSplit(predPlan, unionOp.getOperatorKey().toString());
                predLeaf = predPlan.getLeaves().get(0);
            }
            PhysicalPlan clonePlan = unionOp.plan.clone();

            // Remove POValueOutputTez from predecessor leaf
            predPlan.remove(predLeaf);
            boolean isEmptyPlan = predPlan.isEmpty();
            if (!isEmptyPlan) {
                predLeaf = predPlan.getLeaves().get(0);
            }
            predPlan.merge(clonePlan);
            if (!isEmptyPlan) {
                predPlan.connect(predLeaf, clonePlan.getRoots().get(0));
            }
            return clonePlan;
        } catch (Exception e) {
            throw new VisitorException(e);
        }
    }

    /**
     * Connects the unionOp predecessor to the store vertex groups and the output vertex groups
     * and disconnects it from the unionOp.
     *
     * @param pred Predecessor of union which will be made part of the vertex group
     * @param unionOp Union operator
     * @param predClonedUnionPlan Cloned plan of the union merged to the predecessor
     * @param storeVertexGroupOps Store vertex groups to connect to
     * @param outputVertexGroupOps Tez LogicalOutput vertex groups to connect to
     */
    public void connectPredecessorsToVertexGroups(TezOperator unionOp,
            TezOperator pred, PhysicalPlan predClonedUnionPlan,
            TezOperator[] storeVertexGroupOps,
            TezOperator[] outputVertexGroupOps) throws VisitorException,PlanException {

        //Clone changes the operator keys
        List<POStoreTez> clonedUnionStoreOutputs = PlanHelper.getPhysicalOperators(predClonedUnionPlan, POStoreTez.class);

        // Connect predecessor to the storeVertexGroups
        int i = 0;
        for (TezOperator storeVertexGroup : storeVertexGroupOps) {
            // Skip connecting if they are already connected. Can happen in case of
            // union + split + union + store. Because of the split all the stores
            // will be writing to same location
            List<OperatorKey> inputs = storeVertexGroup.getVertexGroupInfo().getInputs();
            if (inputs == null || !inputs.contains(pred.getOperatorKey())) {
                tezPlan.connect(pred, storeVertexGroup);
            }
            storeVertexGroup.getVertexGroupInfo().addInput(pred.getOperatorKey());
            pred.addVertexGroupStore(clonedUnionStoreOutputs.get(i++).getOperatorKey(),
                    storeVertexGroup.getOperatorKey());
        }

        for (TezOperator outputVertexGroup : outputVertexGroupOps) {
            List<OperatorKey> inputs = outputVertexGroup.getVertexGroupInfo().getInputs();
            if (inputs == null || !inputs.contains(pred.getOperatorKey())) {
                tezPlan.connect(pred, outputVertexGroup);
            }
            outputVertexGroup.getVertexGroupInfo().addInput(pred.getOperatorKey());
        }

        copyOperatorProperties(pred, unionOp);
        tezPlan.disconnect(pred, unionOp);
    }

    /**
     * Connect vertexgroup operator to successor operator in the plan.
     *
     * Copy the output edge between union operator and successor to between
     * predecessors and successor. Predecessor output key and output edge points
     * to successor so that we have all the edge configuration, but they are
     * connected to the vertex group in the plan.
     *
     * @param unionOp Union operator
     * @param successors Successors of the union operator
     * @param unionOutputKeys Output keys of union
     * @param outputVertexGroupOp  Tez LogicalOutput vertex groups corresponding to the output keys
     *
     * @throws PlanException
     */
    private void connectVertexGroupsToSuccessors(TezOperator unionOp,
            List<TezOperator> successors, List<String> unionOutputKeys,
            TezOperator[] outputVertexGroupOps) throws PlanException {
        // Connect to outputVertexGroupOps
        for (Entry<OperatorKey, TezEdgeDescriptor> entry : unionOp.outEdges.entrySet()) {
            TezOperator succOp = tezPlan.getOperator(entry.getKey());
            TezEdgeDescriptor edge = entry.getValue();
            // Edge cannot be one to one as it will get input from two or
            // more union predecessors. Change it to SCATTER_GATHER
            if (edge.dataMovementType == DataMovementType.ONE_TO_ONE) {
                edge.dataMovementType = DataMovementType.SCATTER_GATHER;
                edge.partitionerClass = HashValuePartitioner.class;
                edge.outputClassName = UnorderedPartitionedKVOutput.class.getName();
                edge.inputClassName = UnorderedKVInput.class.getName();
            }
            TezOperator vertexGroupOp = outputVertexGroupOps[unionOutputKeys.indexOf(entry.getKey().toString())];
            for (OperatorKey predKey : unionOp.getUnionMembers()) {
                TezOperator pred = tezPlan.getOperator(predKey);
                // Keep the output edge directly to successor
                // Don't need to keep output edge for vertexgroup
                pred.outEdges.put(entry.getKey(), edge);
                succOp.inEdges.put(predKey, edge);
            }
            if(!tezPlan.pathExists(vertexGroupOp, succOp)) {
                tezPlan.connect(vertexGroupOp, succOp);
            }
        }
    }

    private void replaceSuccessorInputsAndDisconnect(TezOperator unionOp,
            List<TezOperator> successors,
            List<String> unionOutputKeys,
            String[] newOutputKeys)
            throws VisitorException {
        if (successors != null) {
            String unionOpKey = unionOp.getOperatorKey().toString();
            // Successor inputs should now point to the vertex groups.
            for (TezOperator succ : successors) {
                LinkedList<TezInput> inputs = PlanHelper.getPhysicalOperators(succ.plan, TezInput.class);
                for (TezInput input : inputs) {
                    for (String key : input.getTezInputs()) {
                        if (key.equals(unionOpKey)) {
                            input.replaceInput(key,
                                    newOutputKeys[unionOutputKeys.indexOf(succ.getOperatorKey().toString())]);
                        }
                    }
                }

                List<POUserFunc> userFuncs = PlanHelper.getPhysicalOperators(succ.plan, POUserFunc.class);
                for (POUserFunc userFunc : userFuncs) {
                    if (userFunc.getFunc() instanceof ReadScalarsTez) {
                        TezInput tezInput = (TezInput)userFunc.getFunc();
                        for (String inputKey : tezInput.getTezInputs()) {
                            if (inputKey.equals(unionOpKey)) {
                                tezInput.replaceInput(inputKey,
                                        newOutputKeys[unionOutputKeys.indexOf(succ.getOperatorKey().toString())]);
                                userFunc.getFuncSpec().setCtorArgs(tezInput.getTezInputs());
                            }
                        }
                    }
                }

                tezPlan.disconnect(unionOp, succ);
            }
        }
    }

    private void copyOperatorProperties(TezOperator pred, TezOperator unionOp) throws VisitorException {
        pred.UDFs.addAll(unionOp.UDFs);
        pred.scalars.addAll(unionOp.scalars);
        // Copy only map side properties. For eg: crossKeys.
        // Do not copy reduce side specific properties. For eg: useSecondaryKey, segmentBelow, sortOrder, etc
        // Also ignore parallelism settings
        if (unionOp.getCrossKeys() != null) {
            for (String key : unionOp.getCrossKeys()) {
                pred.addCrossKey(key);
            }
        }
        pred.copyFeatures(unionOp, Arrays.asList(new OPER_FEATURE[]{OPER_FEATURE.UNION}));

        // For skewed join right input
        if (unionOp.getSampleOperator() !=  null) {
            if (pred.getSampleOperator() == null) {
                pred.setSampleOperator(unionOp.getSampleOperator());
            } else if (!pred.getSampleOperator().equals(unionOp.getSampleOperator())) {
                throw new VisitorException("Conflicting sample operators "
                        + pred.getSampleOperator().toString() + " and "
                        + unionOp.getSampleOperator().toString());
            }
        }
    }

    public static PhysicalPlan getUnionPredPlanFromSplit(PhysicalPlan plan, String unionOpKey) throws VisitorException {
        List<POSplit> splits = PlanHelper.getPhysicalOperators(plan, POSplit.class);
        for (POSplit split : splits) {
            for (PhysicalPlan subPlan : split.getPlans()) {
                if (subPlan.getLeaves().get(0) instanceof POValueOutputTez) {
                    POValueOutputTez out = (POValueOutputTez) subPlan.getLeaves().get(0);
                    if (out.containsOutputKey(unionOpKey)) {
                        return subPlan;
                    }
                }
            }
        }
        throw new VisitorException("Did not find the union predecessor in the split plan");
    }

}
