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

import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.conf.Configuration;
import org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.InputSizeReducerEstimator;
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.optimizer.ParallelismSetter;
import org.apache.pig.backend.hadoop.executionengine.tez.plan.optimizer.TezEstimatedParallelismClearer;
import org.apache.pig.impl.PigContext;
import org.apache.pig.impl.PigImplConstants;
import org.apache.pig.impl.plan.OperatorKey;
import org.apache.pig.impl.plan.VisitorException;
import org.apache.pig.impl.util.ObjectSerializer;
import org.apache.tez.common.TezUtils;
import org.apache.tez.dag.api.EdgeProperty;
import org.apache.tez.dag.api.EdgeProperty.DataMovementType;
import org.apache.tez.dag.api.TezUncheckedException;
import org.apache.tez.dag.api.VertexManagerPluginContext;
import org.apache.tez.dag.api.event.VertexState;
import org.apache.tez.dag.api.event.VertexStateUpdate;
import org.apache.tez.dag.library.vertexmanager.ShuffleVertexManager;

import com.google.common.base.Function;
import com.google.common.collect.Lists;

public class PigGraceShuffleVertexManager extends ShuffleVertexManager {

    private TezOperPlan tezPlan;
    private List<String> grandParents = new ArrayList<String>();
    private List<String> finishedGrandParents = new ArrayList<String>();
    private long bytesPerTask;
    private Configuration conf;
    private PigContext pc;
    private int thisParallelism = -1;
    private boolean parallelismSet = false;

    private static final Log LOG = LogFactory.getLog(PigGraceShuffleVertexManager.class);

    public PigGraceShuffleVertexManager(VertexManagerPluginContext context) {
        super(context);
    }

    @Override
    public synchronized void initialize() {
        try {
            conf = TezUtils.createConfFromUserPayload(getContext().getUserPayload());
            bytesPerTask = conf.getLong(InputSizeReducerEstimator.BYTES_PER_REDUCER_PARAM,
                    InputSizeReducerEstimator.DEFAULT_BYTES_PER_REDUCER);
            pc = (PigContext)ObjectSerializer.deserialize(conf.get(PigImplConstants.PIG_CONTEXT));
            tezPlan = (TezOperPlan)ObjectSerializer.deserialize(conf.get("pig.tez.plan"));
            TezEstimatedParallelismClearer clearer = new TezEstimatedParallelismClearer(tezPlan);
            try {
                clearer.visit();
            } catch (VisitorException e) {
                throw new TezUncheckedException(e);
            }
            TezOperator op = tezPlan.getOperator(OperatorKey.fromString(getContext().getVertexName()));

            // Collect grandparents of the vertex
            Function<TezOperator, String> tezOpToString = new Function<TezOperator, String>() {
                @Override
                public String apply(TezOperator op) { return op.getOperatorKey().toString(); }
            };
            grandParents = Lists.transform(TezOperPlan.getGrandParentsForGraceParallelism(tezPlan, op), tezOpToString);
        } catch (IOException e) {
            throw new TezUncheckedException(e);
        }

        // Register notification for grandparents
        for (String grandParent : grandParents) {
            getContext().registerForVertexStateUpdates(grandParent, EnumSet.of(VertexState.SUCCEEDED));
        }
        super.initialize();
    }

    @Override
    public synchronized void onVertexStateUpdated(VertexStateUpdate stateUpdate) {
        super.onVertexStateUpdated(stateUpdate);
        if (parallelismSet) {
            return;
        }
        String vertexName = stateUpdate.getVertexName();
        if (grandParents.contains(vertexName)) {
            if (!finishedGrandParents.contains(vertexName)) {
                finishedGrandParents.add(vertexName);
            }
        }

        TezOperator op = tezPlan.getOperator(OperatorKey.fromString(getContext().getVertexName()));

        List<TezOperator> preds = tezPlan.getPredecessors(op);
        boolean anyPredAboutToStart = false;
        for (TezOperator pred : preds) {
            List<TezOperator> predPreds = tezPlan.getPredecessors(pred);
            boolean predAboutToStart = true;
            for (TezOperator predPred : predPreds) {
                if (!finishedGrandParents.contains(predPred.getOperatorKey().toString())) {
                    predAboutToStart = false;
                    break;
                }
            }
            if (predAboutToStart) {
                LOG.info("All predecessors for " + pred.getOperatorKey().toString() + " are finished, time to " +
                        "set parallelism for " + getContext().getVertexName());
                anyPredAboutToStart = true;
                break;
            }
        }

        // Now one of the predecessor is about to start, we need to make a decision now
        if (anyPredAboutToStart) {
            // All grandparents finished, start parents with right parallelism

            for (TezOperator pred : preds) {
                if (pred.getRequestedParallelism()==-1) {
                    List<TezOperator> predPreds = tezPlan.getPredecessors(pred);
                    if (predPreds!=null) {
                        for (TezOperator predPred : predPreds) {
                            String predPredVertexName = predPred.getOperatorKey().toString();
                            if (finishedGrandParents.contains(predPredVertexName)) {
                                // We shall get precise output size since all those nodes are finished
                                long outputSize = getContext().getVertexStatistics(predPredVertexName).getOutputStatistics(pred.getOperatorKey().toString()).getDataSize();
                                int desiredNumReducers = (int)Math.ceil((double)outputSize/bytesPerTask);
                                predPred.setEstimatedParallelism(desiredNumReducers);
                                LOG.info(getContext().getVertexName() +  ": Grandparent " + predPred.getOperatorKey().toString() +
                                        " finished with actual output " + outputSize + " (desired parallelism " + desiredNumReducers + ")");
                            }
                        }
                    }
                }
            }
            try {
                ParallelismSetter parallelismSetter = new ParallelismSetter(tezPlan, pc);
                parallelismSetter.visit();
                thisParallelism = op.getEstimatedParallelism();
            } catch (IOException e) {
                throw new TezUncheckedException(e);
            }
            Map<String, EdgeProperty> edgeManagers = new HashMap<String, EdgeProperty>();
            for(Map.Entry<String,EdgeProperty> entry : getContext().getInputVertexEdgeProperties().entrySet()) {
                EdgeProperty edge = entry.getValue();
                DataMovementType movementType = edge.getDataMovementType() == DataMovementType.BROADCAST? DataMovementType.BROADCAST: DataMovementType.SCATTER_GATHER;
                edge = EdgeProperty.create(movementType, edge.getDataSourceType(), edge.getSchedulingType(),
                        edge.getEdgeSource(), edge.getEdgeDestination());
                edgeManagers.put(entry.getKey(), edge);
            }
            getContext().reconfigureVertex(thisParallelism, null, edgeManagers);
            parallelismSet = true;
            LOG.info("Initialize parallelism for " + getContext().getVertexName() + " to " + thisParallelism);
        }
    }
}
