/*
 * 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.flink.optimizer.plan;

import org.apache.flink.api.common.distributions.DataDistribution;
import org.apache.flink.api.common.functions.Partitioner;
import org.apache.flink.api.common.operators.util.FieldList;
import org.apache.flink.api.common.typeutils.TypeComparatorFactory;
import org.apache.flink.api.common.typeutils.TypeSerializerFactory;
import org.apache.flink.optimizer.CompilerException;
import org.apache.flink.optimizer.dag.EstimateProvider;
import org.apache.flink.optimizer.dag.TempMode;
import org.apache.flink.optimizer.dataproperties.GlobalProperties;
import org.apache.flink.optimizer.dataproperties.LocalProperties;
import org.apache.flink.optimizer.dataproperties.RequestedGlobalProperties;
import org.apache.flink.optimizer.dataproperties.RequestedLocalProperties;
import org.apache.flink.optimizer.plandump.DumpableConnection;
import org.apache.flink.optimizer.util.Utils;
import org.apache.flink.runtime.io.network.DataExchangeMode;
import org.apache.flink.runtime.operators.shipping.ShipStrategyType;
import org.apache.flink.runtime.operators.util.LocalStrategy;

import static org.apache.flink.util.Preconditions.checkNotNull;

/**
 * A Channel represents the result produced by an operator and the data exchange
 * before the consumption by the target operator.
 *
 * The channel defines and tracks various properties and characteristics of the
 * data set and data exchange.
 *
 * Data set characteristics:
 * <ul>
 *     <li>The "global properties" of the data, i.e., how the data is distributed across
 *         partitions</li>
 *     <li>The "required global properties" of the data, i.e., the global properties that, if absent,
 *         would cause the program to return a wrong result.</li>
 *     <li>The "local properties" of the data, i.e., how the data is organized within a partition</li>
 *     <li>The "required local properties" of the data, i.e., the local properties that, if absent,
 *         would cause the program to return a wrong result.</li>
 * </ul>
 *
 * Data exchange parameters:
 * <ul>
 *     <li>The "ship strategy", i.e., whether to forward the data, shuffle it, broadcast it, ...</li>
 *     <li>The "ship keys", which are the positions of the key fields in the exchanged records.</li>
 *     <li>The "data exchange mode", which defines whether to pipeline or batch the exchange</li>
 *     <li>Several more...</li>
 * </ul>
 */
public class Channel implements EstimateProvider, Cloneable, DumpableConnection<PlanNode> {
	
	private PlanNode source;
	
	private PlanNode target;

	private ShipStrategyType shipStrategy = ShipStrategyType.NONE;

	private DataExchangeMode dataExchangeMode;
	
	private LocalStrategy localStrategy = LocalStrategy.NONE;
	
	private FieldList shipKeys;
	
	private FieldList localKeys;
	
	private boolean[] shipSortOrder;
	
	private boolean[] localSortOrder;
	
	private RequestedGlobalProperties requiredGlobalProps;
	
	private RequestedLocalProperties requiredLocalProps;
	
	private GlobalProperties globalProps;
	
	private LocalProperties localProps;
	
	private TypeSerializerFactory<?> serializer;
	
	private TypeComparatorFactory<?> shipStrategyComparator;
	
	private TypeComparatorFactory<?> localStrategyComparator;
	
	private DataDistribution dataDistribution;
	
	private Partitioner<?> partitioner;
	
	private TempMode tempMode;
	
	private double relativeTempMemory;
	
	private double relativeMemoryLocalStrategy;
	
	private int replicationFactor = 1;
	
	// --------------------------------------------------------------------------------------------
	
	public Channel(PlanNode sourceNode) {
		this(sourceNode, null);
	}
	
	public Channel(PlanNode sourceNode, TempMode tempMode) {
		this.source = sourceNode;
		this.tempMode = (tempMode == null ? TempMode.NONE : tempMode);
	}
	
	// --------------------------------------------------------------------------------------------
	//                                         Accessors
	// --------------------------------------------------------------------------------------------
	
	/**
	 * Gets the source of this Channel.
	 *
	 * @return The source.
	 */
	@Override
	public PlanNode getSource() {
		return this.source;
	}

	public void setSource(PlanNode source) {
		this.source = source;
	}
	
	/**
	 * Sets the target of this Channel.
	 *
	 * @param target The target.
	 */
	public void setTarget(PlanNode target) {
		this.target = target;
	}
	
	/**
	 * Gets the target of this Channel.
	 *
	 * @return The target.
	 */
	public PlanNode getTarget() {
		return this.target;
	}

	public void setShipStrategy(ShipStrategyType strategy, DataExchangeMode dataExchangeMode) {
		setShipStrategy(strategy, null, null, null, dataExchangeMode);
	}
	
	public void setShipStrategy(ShipStrategyType strategy, FieldList keys, DataExchangeMode dataExchangeMode) {
		setShipStrategy(strategy, keys, null, null, dataExchangeMode);
	}
	
	public void setShipStrategy(ShipStrategyType strategy, FieldList keys,
								boolean[] sortDirection, DataExchangeMode dataExchangeMode) {
		setShipStrategy(strategy, keys, sortDirection, null, dataExchangeMode);
	}
	
	public void setShipStrategy(ShipStrategyType strategy, FieldList keys,
								Partitioner<?> partitioner, DataExchangeMode dataExchangeMode) {
		setShipStrategy(strategy, keys, null, partitioner, dataExchangeMode);
	}
	
	public void setShipStrategy(ShipStrategyType strategy, FieldList keys,
								boolean[] sortDirection, Partitioner<?> partitioner,
								DataExchangeMode dataExchangeMode) {
		this.shipStrategy = strategy;
		this.shipKeys = keys;
		this.shipSortOrder = sortDirection;
		this.partitioner = partitioner;
		this.dataExchangeMode = dataExchangeMode;
		this.globalProps = null;		// reset the global properties
	}

	/**
	 * Sets the data exchange mode (batch / pipelined) to use for the data
	 * exchange of this channel.
	 *
	 */
	public void setDataExchangeMode(DataExchangeMode dataExchangeMode) {
		this.dataExchangeMode = checkNotNull(dataExchangeMode);
	}

	/**
	 * Gets the data exchange mode (batch / pipelined) to use for the data
	 * exchange of this channel.
	 *
	 * @return The data exchange mode of this channel.
	 */
	public DataExchangeMode getDataExchangeMode() {
		return dataExchangeMode;
	}

	public ShipStrategyType getShipStrategy() {
		return this.shipStrategy;
	}
	
	public FieldList getShipStrategyKeys() {
		return this.shipKeys;
	}
	
	public boolean[] getShipStrategySortOrder() {
		return this.shipSortOrder;
	}
	
	public void setLocalStrategy(LocalStrategy strategy) {
		setLocalStrategy(strategy, null, null);
	}
	
	public void setLocalStrategy(LocalStrategy strategy, FieldList keys, boolean[] sortDirection) {
		this.localStrategy = strategy;
		this.localKeys = keys;
		this.localSortOrder = sortDirection;
		this.localProps = null;		// reset the local properties
	}
	
	public LocalStrategy getLocalStrategy() {
		return this.localStrategy;
	}
	
	public FieldList getLocalStrategyKeys() {
		return this.localKeys;
	}
	
	public boolean[] getLocalStrategySortOrder() {
		return this.localSortOrder;
	}
	
	public void setDataDistribution(DataDistribution dataDistribution) {
		this.dataDistribution = dataDistribution;
	}
	
	public DataDistribution getDataDistribution() {
		return this.dataDistribution;
	}
	
	public Partitioner<?> getPartitioner() {
		return partitioner;
	}
	
	public TempMode getTempMode() {
		return this.tempMode;
	}

	/**
	 * Sets the temp mode of the connection.
	 * 
	 * @param tempMode
	 *        The temp mode of the connection.
	 */
	public void setTempMode(TempMode tempMode) {
		this.tempMode = tempMode;
	}
	
	/**
	 * Gets the memory for materializing the channel's result from this Channel.
	 *
	 * @return The temp memory.
	 */
	public double getRelativeTempMemory() {
		return this.relativeTempMemory;
	}
	
	/**
	 * Sets the memory for materializing the channel's result from this Channel.
	 *
	 * @param relativeTempMemory The memory for materialization.
	 */
	public void setRelativeTempMemory(double relativeTempMemory) {
		this.relativeTempMemory = relativeTempMemory;
	}
	
	/**
	 * Sets the replication factor of the connection.
	 * 
	 * @param factor The replication factor of the connection.
	 */
	public void setReplicationFactor(int factor) {
		this.replicationFactor = factor;
	}
	
	/**
	 * Returns the replication factor of the connection.
	 * 
	 * @return The replication factor of the connection.
	 */
	public int getReplicationFactor() {
		return this.replicationFactor;
	}
	
	/**
	 * Gets the serializer from this Channel.
	 *
	 * @return The serializer.
	 */
	public TypeSerializerFactory<?> getSerializer() {
		return serializer;
	}
	
	/**
	 * Sets the serializer for this Channel.
	 *
	 * @param serializer The serializer to set.
	 */
	public void setSerializer(TypeSerializerFactory<?> serializer) {
		this.serializer = serializer;
	}
	
	/**
	 * Gets the ship strategy comparator from this Channel.
	 *
	 * @return The ship strategy comparator.
	 */
	public TypeComparatorFactory<?> getShipStrategyComparator() {
		return shipStrategyComparator;
	}
	
	/**
	 * Sets the ship strategy comparator for this Channel.
	 *
	 * @param shipStrategyComparator The ship strategy comparator to set.
	 */
	public void setShipStrategyComparator(TypeComparatorFactory<?> shipStrategyComparator) {
		this.shipStrategyComparator = shipStrategyComparator;
	}
	
	/**
	 * Gets the local strategy comparator from this Channel.
	 *
	 * @return The local strategy comparator.
	 */
	public TypeComparatorFactory<?> getLocalStrategyComparator() {
		return localStrategyComparator;
	}
	
	/**
	 * Sets the local strategy comparator for this Channel.
	 *
	 * @param localStrategyComparator The local strategy comparator to set.
	 */
	public void setLocalStrategyComparator(TypeComparatorFactory<?> localStrategyComparator) {
		this.localStrategyComparator = localStrategyComparator;
	}
	
	public double getRelativeMemoryLocalStrategy() {
		return relativeMemoryLocalStrategy;
	}
	
	public void setRelativeMemoryLocalStrategy(double relativeMemoryLocalStrategy) {
		this.relativeMemoryLocalStrategy = relativeMemoryLocalStrategy;
	}
	
	public boolean isOnDynamicPath() {
		return this.source.isOnDynamicPath();
	}
	
	public int getCostWeight() {
		return this.source.getCostWeight();
	}

	// --------------------------------------------------------------------------------------------
	//                                Statistic Estimates
	// --------------------------------------------------------------------------------------------
	

	@Override
	public long getEstimatedOutputSize() {
		long estimate = this.source.template.getEstimatedOutputSize();
		return estimate < 0 ? estimate : estimate * this.replicationFactor;
	}

	@Override
	public long getEstimatedNumRecords() {
		long estimate =  this.source.template.getEstimatedNumRecords();
		return estimate < 0 ? estimate : estimate * this.replicationFactor;
	}
	
	@Override
	public float getEstimatedAvgWidthPerOutputRecord() {
		return this.source.template.getEstimatedAvgWidthPerOutputRecord();
	}
	
	// --------------------------------------------------------------------------------------------
	//                                Data Property Handling
	// --------------------------------------------------------------------------------------------
	

	public RequestedGlobalProperties getRequiredGlobalProps() {
		return requiredGlobalProps;
	}

	public void setRequiredGlobalProps(RequestedGlobalProperties requiredGlobalProps) {
		this.requiredGlobalProps = requiredGlobalProps;
	}

	public RequestedLocalProperties getRequiredLocalProps() {
		return requiredLocalProps;
	}

	public void setRequiredLocalProps(RequestedLocalProperties requiredLocalProps) {
		this.requiredLocalProps = requiredLocalProps;
	}

	public GlobalProperties getGlobalProperties() {
		if (this.globalProps == null) {
			this.globalProps = this.source.getGlobalProperties().clone();
			switch (this.shipStrategy) {
				case BROADCAST:
					this.globalProps.clearUniqueFieldCombinations();
					this.globalProps.setFullyReplicated();
					break;
				case PARTITION_HASH:
					this.globalProps.setHashPartitioned(this.shipKeys);
					break;
				case PARTITION_RANGE:
					this.globalProps.setRangePartitioned(Utils.createOrdering(this.shipKeys, this.shipSortOrder), this.dataDistribution);
					break;
				case FORWARD:
					break;
				case PARTITION_RANDOM:
					this.globalProps.reset();
					break;
				case PARTITION_FORCED_REBALANCE:
					this.globalProps.setForcedRebalanced();
					break;
				case PARTITION_CUSTOM:
					this.globalProps.setCustomPartitioned(this.shipKeys, this.partitioner);
					break;
				case NONE:
					throw new CompilerException("Cannot produce GlobalProperties before ship strategy is set.");
			}
		}
		
		return this.globalProps;
	}
	
	public LocalProperties getLocalProperties() {
		if (this.localProps == null) {
			computeLocalPropertiesAfterShippingOnly();
			switch (this.localStrategy) {
				case NONE:
					break;
				case SORT:
				case COMBININGSORT:
					this.localProps = LocalProperties.forOrdering(Utils.createOrdering(this.localKeys, this.localSortOrder));
					break;
				default:
					throw new CompilerException("Unsupported local strategy for channel.");
			}
		}
		
		return this.localProps;
	}
	
	private void computeLocalPropertiesAfterShippingOnly() {
		switch (this.shipStrategy) {
			case BROADCAST:
			case PARTITION_HASH:
			case PARTITION_CUSTOM:
			case PARTITION_RANGE:
			case PARTITION_RANDOM:
			case PARTITION_FORCED_REBALANCE:
				this.localProps = new LocalProperties();
				break;
			case FORWARD:
				this.localProps = this.source.getLocalProperties();
				break;
			case NONE:
				throw new CompilerException("ShipStrategy has not yet been set.");
			default:
				throw new CompilerException("Unknown ShipStrategy.");
		}
	}
	
	public void adjustGlobalPropertiesForFullParallelismChange() {
		if (this.shipStrategy == null || this.shipStrategy == ShipStrategyType.NONE) {
			throw new IllegalStateException("Cannot adjust channel for parallelism " +
					"change before the ship strategy is set.");
		}
		
		// make sure the properties are acquired
		if (this.globalProps == null) {
			getGlobalProperties();
		}
		
		// some strategies globally reestablish properties
		switch (this.shipStrategy) {
		case FORWARD:
			throw new CompilerException("Cannot use FORWARD strategy between operations " +
					"with different number of parallel instances.");
		case NONE: // excluded by sanity check. left here for verification check completion
		case BROADCAST:
		case PARTITION_HASH:
		case PARTITION_RANGE:
		case PARTITION_RANDOM:
		case PARTITION_FORCED_REBALANCE:
		case PARTITION_CUSTOM:
			return;
		}
		throw new CompilerException("Unrecognized Ship Strategy Type: " + this.shipStrategy);
	}

	// --------------------------------------------------------------------------------------------
	
	/**
	 * Utility method used while swapping binary union nodes for n-ary union nodes.
	 */
	public void swapUnionNodes(PlanNode newUnionNode) {
		if (!(this.source instanceof BinaryUnionPlanNode)) {
			throw new IllegalStateException();
		} else {
			this.source = newUnionNode;
		}
	}
	
	// --------------------------------------------------------------------------------------------
	
	public int getMaxDepth() {
		return this.source.getOptimizerNode().getMaxDepth() + 1;
	}

	// --------------------------------------------------------------------------------------------

	@Override
	public String toString() {
		return "Channel (" + this.source + (this.target == null ? ')' : ") -> (" + this.target + ')') +
				'[' + this.shipStrategy + "] [" + this.localStrategy + "] " +
				(this.tempMode == null || this.tempMode == TempMode.NONE ? "{NO-TEMP}" : this.tempMode);
	}

	@Override
	public Channel clone() {
		try {
			return (Channel) super.clone();
		} catch (CloneNotSupportedException cnsex) {
			throw new RuntimeException(cnsex);
		}
	}
}