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

import java.util.Arrays;

import org.apache.flink.api.common.operators.Ordering;
import org.apache.flink.api.common.operators.SemanticProperties;
import org.apache.flink.api.common.operators.util.FieldSet;
import org.apache.flink.optimizer.plan.Channel;
import org.apache.flink.optimizer.util.Utils;
import org.apache.flink.runtime.operators.util.LocalStrategy;

/**
 * This class represents the local properties of the data that are requested by an operator.
 * Local properties are the properties within one partition.
 * Operators request the local properties they need for correct execution. Here are some example local
 * properties requested by certain operators:
 * <ul>
 *     <li>"groupBy/reduce" will request the data to be grouped on the key fields.</li>
 *     <li>A sort-merge join will request the data from each input to be sorted on the respective join key.</li>
 * </ul>
 */
public class RequestedLocalProperties implements Cloneable {
	
	private Ordering ordering;			// order inside a partition, null if not ordered

	private FieldSet groupedFields;		// fields by which the stream is grouped. null if not grouped.

	// --------------------------------------------------------------------------------------------
	
	/**
	 * Default constructor for trivial local properties. No order, no grouping, no uniqueness.
	 */
	public RequestedLocalProperties() {}
	
	/**
	 * Creates interesting properties for the given ordering.
	 * 
	 * @param ordering The interesting ordering.
	 */
	public RequestedLocalProperties(Ordering ordering) {
		this.ordering = ordering;
	}
	
	/**
	 * Creates interesting properties for the given grouping.
	 * 
	 * @param groupedFields The set of fields whose grouping is interesting.
	 */
	public RequestedLocalProperties(FieldSet groupedFields) {
		this.groupedFields = groupedFields;
	}
	
	/**
	 * This constructor is used only for internal copy creation.
	 * 
	 * @param ordering The ordering represented by these local properties.
	 * @param groupedFields The grouped fields for these local properties.
	 */
	private RequestedLocalProperties(Ordering ordering, FieldSet groupedFields) {
		this.ordering = ordering;
		this.groupedFields = groupedFields;
	}

	// --------------------------------------------------------------------------------------------
	
	/**
	 * Gets the key order.
	 * 
	 * @return The key order, or <code>null</code> if nothing is ordered.
	 */
	public Ordering getOrdering() {
		return ordering;
	}
	
	/**
	 * Sets the order for these interesting local properties.
	 * 
	 * @param ordering The order to set.
	 */
	public void setOrdering(Ordering ordering) {
		this.ordering = ordering;
	}
	
	/**
	 * Gets the grouped fields.
	 * 
	 * @return The grouped fields, or <code>null</code> if nothing is grouped.
	 */
	public FieldSet getGroupedFields() {
		return this.groupedFields;
	}
	
	/**
	 * Sets the fields that are grouped in these data properties.
	 * 
	 * @param groupedFields The fields that are grouped in these data properties.
	 */
	public void setGroupedFields(FieldSet groupedFields) {
		this.groupedFields = groupedFields;	
	}
	
	/**
	 * Checks, if the properties in this object are trivial, i.e. only standard values.
	 */
	public boolean isTrivial() {
		return ordering == null && this.groupedFields == null;
	}

	/**
	 * This method resets the local properties to a state where no properties are given.
	 */
	public void reset() {
		this.ordering = null;
		this.groupedFields = null;
	}
	
	// --------------------------------------------------------------------------------------------

	/**
	 * Filters these properties by what can be preserved by the given SemanticProperties when propagated down
	 * to the given input.
	 *
	 * @param props The SemanticProperties which define which fields are preserved.
	 * @param input The index of the operator's input.
	 * @return The filtered RequestedLocalProperties
	 */
	public RequestedLocalProperties filterBySemanticProperties(SemanticProperties props, int input) {

		// no semantic properties, all local properties are filtered
		if (props == null) {
			throw new NullPointerException("SemanticProperties may not be null.");
		}

		if (this.ordering != null) {
			Ordering newOrdering = new Ordering();

			for (int i = 0; i < this.ordering.getInvolvedIndexes().size(); i++) {
				int targetField = this.ordering.getInvolvedIndexes().get(i);
				int sourceField = props.getForwardingSourceField(input, targetField);
				if (sourceField >= 0) {
					newOrdering.appendOrdering(sourceField, this.ordering.getType(i), this.ordering.getOrder(i));
				} else {
					return null;
				}
			}
			return new RequestedLocalProperties(newOrdering);

		} else if (this.groupedFields != null) {
			FieldSet newGrouping = new FieldSet();

			// check, whether the local key grouping is preserved
			for (Integer targetField : this.groupedFields) {
				int sourceField = props.getForwardingSourceField(input, targetField);
				if (sourceField >= 0) {
					newGrouping = newGrouping.addField(sourceField);
				} else {
					return null;
				}
			}
			return new RequestedLocalProperties(newGrouping);
		} else {
			return null;
		}
	}

	/**
	 * Checks, if this set of properties, as interesting properties, is met by the given
	 * properties.
	 * 
	 * @param other
	 *        The properties for which to check whether they meet these properties.
	 * @return True, if the properties are met, false otherwise.
	 */
	public boolean isMetBy(LocalProperties other) {
		if (this.ordering != null) {
			// we demand an ordering
			return other.getOrdering() != null && this.ordering.isMetBy(other.getOrdering());
		} else if (this.groupedFields != null) {
			// check if the other fields are unique
			if (other.getGroupedFields() != null && other.getGroupedFields().isValidUnorderedPrefix(this.groupedFields)) {
				return true;
			} else  {
				return other.areFieldsUnique(this.groupedFields);
			}
		} else {
			return true;
		}
	}
	
	/**
	 * Parametrizes the local strategy fields of a channel such that the channel produces the desired local properties.
	 * 
	 * @param channel The channel to parametrize.
	 */
	public void parameterizeChannel(Channel channel) {
		LocalProperties current = channel.getLocalProperties();
		
		if (isMetBy(current)) {
			// we are met, all is good
			channel.setLocalStrategy(LocalStrategy.NONE);
		}
		else if (this.ordering != null) {
			channel.setLocalStrategy(LocalStrategy.SORT, this.ordering.getInvolvedIndexes(), this.ordering.getFieldSortDirections());
		}
		else if (this.groupedFields != null) {
			boolean[] dirs = new boolean[this.groupedFields.size()];
			Arrays.fill(dirs, true);
			channel.setLocalStrategy(LocalStrategy.SORT, Utils.createOrderedFromSet(this.groupedFields), dirs);
		}
		else {
			channel.setLocalStrategy(LocalStrategy.NONE);
		}
	}
	
	// --------------------------------------------------------------------------------------------

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + (this.ordering == null ? 0 : this.ordering.hashCode());
		result = prime * result + (this.groupedFields == null ? 0 : this.groupedFields.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof RequestedLocalProperties) {
			final RequestedLocalProperties other = (RequestedLocalProperties) obj;
			return (this.ordering == other.ordering || (this.ordering != null && this.ordering.equals(other.ordering))) &&
				(this.groupedFields == other.groupedFields || (this.groupedFields != null && this.groupedFields.equals(other.groupedFields)));
		} else {
			return false;
		}
	}

	@Override
	public String toString() {
		return "Requested Local Properties [ordering=" + this.ordering + ", grouped=" + this.groupedFields + "]";
	}

	@Override
	public RequestedLocalProperties clone() {
		return new RequestedLocalProperties(this.ordering, this.groupedFields);
	}
}