/*
 * 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.sysds.hops.cost;

import org.apache.sysds.api.DMLException;
import org.apache.sysds.common.Types;
import org.apache.sysds.common.Types.ExecType;
import org.apache.sysds.hops.Hop;
import org.apache.sysds.hops.fedplanner.FTypes;
import org.apache.sysds.hops.fedplanner.FTypes.FType;
import org.apache.sysds.hops.fedplanner.MemoTable;
import org.apache.sysds.hops.rewrite.HopRewriteUtils;
import org.apache.sysds.runtime.instructions.fed.FEDInstruction;
import org.apache.sysds.runtime.instructions.fed.FEDInstruction.FederatedOutput;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * HopRel provides a representation of the relation between a hop, the cost of setting a given FederatedOutput value,
 * and the input dependency with the given FederatedOutput value.
 * The HopRel class is used when building and selecting an optimal federated execution plan in IPAPassRewriteFederatedPlan.
 * The input dependency is needed to hold the valid and optimal FederatedOutput values for the inputs.
 */
public class HopRel {
	protected final Hop hopRef;
	protected final FEDInstruction.FederatedOutput fedOut;
	protected ExecType execType;
	protected FTypes.FType fType;
	protected FederatedCost cost;
	protected final Set<Long> costPointerSet = new HashSet<>();
	protected List<Hop> inputHops;
	protected List<HopRel> inputDependency = new ArrayList<>();

	/**
	 * Constructs a HopRel with input dependency and cost estimate based on entries in hopRelMemo.
	 * @param associatedHop hop associated with this HopRel
	 * @param fedOut FederatedOutput value assigned to this HopRel
	 * @param hopRelMemo memo table storing other HopRels including the inputs of associatedHop
	 */
	public HopRel(Hop associatedHop, FEDInstruction.FederatedOutput fedOut, MemoTable hopRelMemo){
		this(associatedHop, fedOut, null, hopRelMemo,associatedHop.getInput());
	}

	/**
	 * Constructs a HopRel with input dependency and cost estimate based on entries in hopRelMemo.
	 * @param associatedHop hop associated with this HopRel
	 * @param fedOut FederatedOutput value assigned to this HopRel
	 * @param hopRelMemo memo table storing other HopRels including the inputs of associatedHop
	 * @param inputs hop inputs which input dependencies and cost is based on
	 */
	public HopRel(Hop associatedHop, FEDInstruction.FederatedOutput fedOut, MemoTable hopRelMemo, ArrayList<Hop> inputs){
		this(associatedHop, fedOut, null, hopRelMemo, inputs);
	}

	private HopRel(Hop associatedHop, FEDInstruction.FederatedOutput fedOut, FType fType, List<Hop> inputs){
		hopRef = associatedHop;
		this.fedOut = fedOut;
		this.fType = fType;
		this.inputHops = inputs;
	}

	/**
	 * Constructs a HopRel with input dependency and cost estimate based on entries in hopRelMemo.
	 * @param associatedHop hop associated with this HopRel
	 * @param fedOut FederatedOutput value assigned to this HopRel
	 * @param fType Federated Type of the output of this hopRel
	 * @param hopRelMemo memo table storing other HopRels including the inputs of associatedHop
	 * @param inputs hop inputs which input dependencies and cost is based on
	 */
	public HopRel(Hop associatedHop, FEDInstruction.FederatedOutput fedOut, FType fType, MemoTable hopRelMemo, ArrayList<Hop> inputs){
		this(associatedHop, fedOut, fType, inputs);
		setInputDependency(hopRelMemo);
		cost = FederatedCostEstimator.costEstimate(this, hopRelMemo);
		setExecType();
	}

	public HopRel(Hop associatedHop, FEDInstruction.FederatedOutput fedOut, FType fType, MemoTable hopRelMemo, List<Hop> inputs, List<FType> inputDependency){
		this(associatedHop, fedOut, fType, inputs);
		setInputFTypeDependency(inputs, inputDependency, hopRelMemo);
		cost = FederatedCostEstimator.costEstimate(this, hopRelMemo);
		setExecType();
	}

	private void setInputFTypeDependency(List<Hop> inputs, List<FType> inputDependency, MemoTable hopRelMemo){
		for ( int i = 0; i < inputs.size(); i++ ){
			this.inputDependency.add(hopRelMemo.getHopRel(inputs.get(i), inputDependency.get(i)));
		}
		validateInputDependency();
	}

	private void setExecType(){
		if ( inputDependency.stream().anyMatch(HopRel::hasFederatedOutput)
			|| HopRewriteUtils.isData(hopRef, Types.OpOpData.FEDERATED))
			execType = ExecType.FED;
	}

	/**
	 * Adds hopID to set of hops pointing to this HopRel.
	 * By storing the hopID it can later be determined if the cost
	 * stored in this HopRel is already used as input cost in another HopRel.
	 * @param hopID added to set of stored cost pointers
	 */
	public void addCostPointer(long hopID){
		costPointerSet.add(hopID);
	}

	/**
	 * Checks if another Hop is refering to this HopRel in memo table.
	 * A reference from a HopRel with same Hop ID is allowed, so this
	 * ID is ignored when checking references.
	 * @param currentHopID to ignore when checking references
	 * @return true if another Hop refers to this HopRel in memo table
	 */
	public boolean existingCostPointer(long currentHopID){
		if ( costPointerSet.contains(currentHopID) )
			return costPointerSet.size() > 1;
		else return costPointerSet.size() > 0;
	}

	public boolean hasLocalOutput(){
		return fedOut == FederatedOutput.LOUT;
	}

	public boolean hasFederatedOutput(){
		return fedOut == FederatedOutput.FOUT;
	}

	public FederatedOutput getFederatedOutput(){
		return fedOut;
	}

	public List<HopRel> getInputDependency(){
		return inputDependency;
	}

	public Hop getHopRef(){
		return hopRef;
	}

	public FType getFType(){
		return fType;
	}

	public void setFType(FType fType){
		this.fType = fType;
	}

	public ExecType getExecType(){
		return execType;
	}

	/**
	 * Returns FOUT HopRel for given hop found in hopRelMemo or returns null if HopRel not found.
	 * @param hop to look for in hopRelMemo
	 * @param hopRelMemo memo table storing HopRels
	 * @return FOUT HopRel found in hopRelMemo
	 */
	private HopRel getFOUTHopRel(Hop hop, MemoTable hopRelMemo){
		return hopRelMemo.getFederatedOutputAlternativeOrNull(hop);
	}

	/**
	 * Set valid and optimal input dependency for this HopRel as a field.
	 * @param hopRelMemo memo table storing input HopRels
	 */
	private void setInputDependency(MemoTable hopRelMemo){
		if (inputHops != null && inputHops.size() > 0) {
			if ( fedOut == FederatedOutput.FOUT && !hopRef.isFederatedDataOp() ) {
				int lowestFOUTIndex = 0;
				HopRel lowestFOUTHopRel = getFOUTHopRel(inputHops.get(0), hopRelMemo);
				for(int i = 1; i < inputHops.size(); i++) {
					Hop input = inputHops.get(i);
					HopRel foutHopRel = getFOUTHopRel(input, hopRelMemo);
					if(lowestFOUTHopRel == null) {
						lowestFOUTHopRel = foutHopRel;
						lowestFOUTIndex = i;
					}
					else if(foutHopRel != null) {
						if(foutHopRel.getCost() < lowestFOUTHopRel.getCost()) {
							lowestFOUTHopRel = foutHopRel;
							lowestFOUTIndex = i;
						}
					}
				}

				HopRel[] inputHopRels = new HopRel[inputHops.size()];
				for(int i = 0; i < inputHops.size(); i++) {
					if(i != lowestFOUTIndex) {
						Hop input = inputHops.get(i);
						inputHopRels[i] = hopRelMemo.getMinCostAlternative(input);
					}
					else {
						inputHopRels[i] = lowestFOUTHopRel;
					}
				}
				inputDependency.addAll(Arrays.asList(inputHopRels));
			} else {
				inputDependency.addAll(
					inputHops.stream()
						.map(hopRelMemo::getMinCostAlternative)
						.collect(Collectors.toList()));
			}
		}
		validateInputDependency();
	}

	/**
	 * Throws exception if any input dependency is null.
	 * If any of the input dependencies are null, it is not possible to build a federated execution plan.
	 * If this null-state is not found here, an exception will be thrown at another difficult-to-debug place.
	 */
	private void validateInputDependency(){
		for ( int i = 0; i < inputDependency.size(); i++){
			if ( inputDependency.get(i) == null)
				throw new DMLException("HopRel input number " + i + " (" + hopRef.getInput(i) + ")"
					+ " is null for root: \n" + this);
		}
	}

	/**
	 * Get total cost as double
	 * @return cost as double
	 */
	public double getCost(){
		return cost.getTotal();
	}

	/**
	 * Get cost object
	 * @return cost object
	 */
	public FederatedCost getCostObject(){
		return cost;
	}

	@Override
	public String toString(){
		StringBuilder strB = new StringBuilder();
		strB.append(this.getClass().getSimpleName());
		strB.append(" {HopID: ");
		strB.append(hopRef.getHopID());
		strB.append(", Opcode: ");
		strB.append(hopRef.getOpString());
		strB.append(", FedOut: ");
		strB.append(fedOut);
		strB.append(", Cost: ");
		strB.append(cost.getTotal());
		strB.append(", Inputs: ");
		strB.append(inputDependency.stream().map(i -> "{" + i.getHopRef().getHopID() +
			", " + i.getFederatedOutput() + "}").collect(Collectors.toList()));
		strB.append("}");
		return strB.toString();
	}
}
