/*
 * This file is part of the Deterministic Network Calculator (DNC).
 *
 * Copyright (C) 2013 - 2018 Steffen Bondorf
 * Copyright (C) 2017 - 2018 The DiscoDNC contributors
 * Copyright (C) 2019+ The DNC contributors
 *
 * http://networkcalculus.org
 *
 *
 * The Deterministic Network Calculator (DNC) is free software;
 * you can redistribute it and/or modify it under the terms of the 
 * GNU Lesser General Public License as published by the Free Software Foundation; 
 * either version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 */

package org.networkcalculus.dnc.feedforward;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import org.networkcalculus.dnc.AnalysisConfig;
import org.networkcalculus.dnc.Calculator;
import org.networkcalculus.dnc.AnalysisConfig.ArrivalBoundMethod;
import org.networkcalculus.dnc.AnalysisConfig.MultiplexingEnforcement;
import org.networkcalculus.dnc.curves.ArrivalCurve;
import org.networkcalculus.dnc.curves.Curve;
import org.networkcalculus.dnc.curves.Curve_ConstantPool;
import org.networkcalculus.dnc.curves.ServiceCurve;
import org.networkcalculus.dnc.feedforward.arrivalbounds.AggregatePboo_Concatenation;
import org.networkcalculus.dnc.feedforward.arrivalbounds.AggregatePboo_PerServer;
import org.networkcalculus.dnc.feedforward.arrivalbounds.AggregatePmoo;
import org.networkcalculus.dnc.feedforward.arrivalbounds.AggregateTandemMatching;
import org.networkcalculus.dnc.network.server_graph.Flow;
import org.networkcalculus.dnc.network.server_graph.Server;
import org.networkcalculus.dnc.network.server_graph.ServerGraph;
import org.networkcalculus.dnc.network.server_graph.Turn;
import org.networkcalculus.dnc.tandem.analyses.PmooAnalysis;
import org.networkcalculus.dnc.tandem.analyses.SeparateFlowAnalysis;
import org.networkcalculus.dnc.tandem.analyses.TandemMatchingAnalysis;
import org.networkcalculus.dnc.utils.SetUtils;

public abstract class ArrivalBoundDispatch {
	// --------------------------------------------------------------------------------------------------------------
	// Arrival Bound Cache
	// --------------------------------------------------------------------------------------------------------------
	protected static Map<Set<ArrivalBoundMethod>,ArrivalBoundCache> ab_caches = new HashMap<Set<ArrivalBoundMethod>,ArrivalBoundCache>();
	
	private static ArrivalBoundCache getCache( Set<ArrivalBoundMethod> ab_methods ) {
		for ( Entry<Set<ArrivalBoundMethod>,ArrivalBoundCache> cache_entry : ab_caches.entrySet() ) {
			if( cache_entry.getKey().size() == ab_methods.size()
					&& cache_entry.getKey().containsAll( ab_methods ) ) {
				return cache_entry.getValue();
			}
		}
		
		// Reaching this code here means that there is
		// no cache for this set of ab_methods in the set of caches yet.
		// So we create one, add it to the map and return it.
		ArrivalBoundCache new_ab_cache = new ArrivalBoundCache();
		ab_caches.put( ab_methods, new_ab_cache );
		
		return new_ab_cache;
	}
	
	public static void clearAllCaches() {
		ab_caches.clear();
	}
	
	// --------------------------------------------------------------------------------------------------------------
	// Arrival Bound Dispatching
	// --------------------------------------------------------------------------------------------------------------

	public static Set<ArrivalCurve> computeArrivalBounds(ServerGraph server_graph, AnalysisConfig configuration, Server server)
			throws Exception {
		return computeArrivalBounds(server_graph, configuration, server, server_graph.getFlows(server), Flow.NULL_FLOW);
	}

	/**
	 * The flow_of_interest low priority supersedes the wish to bound all flows in
	 * flows_to_bound, i.e., if flow_of_interest will be removed from flows_to_bound
	 * before bounding the arrivals such that the result will always hold (only) for
	 * {flows_to_bound} \ {flow_of_interest}.
	 * <p>
	 * To bound all flows in flows_to_bound, please call, e.g.,
	 * computeArrivalBounds( server_graph, flows_to_bound, Flow.NULL_FLOW )
	 *
	 * @param server
	 *            The server seeing the arrival bound.
	 * @param flows_to_bound
	 *            The flows to be bounded.
	 * @param flow_of_interest
	 *            The flow of interest to get a lower priority.
	 * @return The arrival bound.
	 * @throws Exception
	 *             Potential exception raised in the called function
	 *             computeArrivalBounds.
	 */
	public static Set<ArrivalCurve> computeArrivalBounds(ServerGraph server_graph, AnalysisConfig configuration, Server server,
															Set<Flow> flows_to_bound, Flow flow_of_interest) throws Exception {
		flows_to_bound.remove(flow_of_interest);
		Set<ArrivalCurve> arrival_bounds = new HashSet<ArrivalCurve>(
				Collections.singleton(Curve_ConstantPool.ZERO_ARRIVAL_CURVE.get()));
		if (flows_to_bound.isEmpty()) {
			return arrival_bounds;
		}

		Set<Flow> f_server = server_graph.getFlows(server);
		Set<Flow> f_xfcaller_server = SetUtils.getIntersection(f_server, flows_to_bound);
		if (f_xfcaller_server.isEmpty()) {
			return arrival_bounds;
		}
		
		if( configuration.useArrivalBoundsCache() 
				&& configuration.enforceMultiplexing() != MultiplexingEnforcement.SERVER_LOCAL ) { // Do not cache in that case. Too many variables, the cache does not check all of them.
			ArrivalBoundCache.CacheEntryServer entry = getCache( configuration.arrivalBoundMethods() ).getCacheEntry( configuration, server, flows_to_bound, flow_of_interest );
			if( entry != null && !entry.arrival_bounds.isEmpty()
					&& !(entry.arrival_bounds.size() > 1 && configuration.convolveAlternativeArrivalBounds()) // Inconsistency between current cache content and current setting.
					) {
				// Be cautious here! By using the original cache entry instead of the getArrivalBounds function, we need to repack the result in a new set manually!
				return new HashSet<ArrivalCurve>( entry.arrival_bounds );
			}
		}

		// Get cross-traffic originating in server
		Set<Flow> f_xfcaller_sourceflows_server = SetUtils.getIntersection(f_xfcaller_server,
				server_graph.getSourceFlows(server));
		if( !f_xfcaller_sourceflows_server.isEmpty() ) {
			f_xfcaller_sourceflows_server.remove(flow_of_interest);
			ArrivalCurve alpha_xfcaller_sourceflows_server = server_graph.getSourceFlowArrivalCurve(server,f_xfcaller_sourceflows_server); // Will at least be a zeroArrivalCurve
			arrival_bounds = new HashSet<ArrivalCurve>(Collections.singleton(alpha_xfcaller_sourceflows_server));

			if (f_xfcaller_sourceflows_server.containsAll(f_xfcaller_server)) {
				return arrival_bounds;
			}
		}

		// Get cross-traffic from each predecessor. Call per turn in order to get
		// splitting points.
		Set<ArrivalCurve> arrival_bounds_turn;
		Set<ArrivalCurve> arrival_bounds_turn_permutations = new HashSet<ArrivalCurve>();

		Iterator<Turn> in_turn_iter = server_graph.getInTurns(server).iterator();
		while (in_turn_iter.hasNext()) {

			Turn in_l = in_turn_iter.next();
			Set<Flow> f_xfcaller_in_l = SetUtils.getIntersection(server_graph.getFlows(in_l), f_xfcaller_server);
			f_xfcaller_in_l.remove(flow_of_interest);

			if (f_xfcaller_in_l.isEmpty()) { // Do not check turns without flows of interest
				continue;
			}

			arrival_bounds_turn = computeArrivalBounds(server_graph, configuration, in_l, f_xfcaller_in_l, flow_of_interest);

			// Add the new bounds to the others:
			// * Consider all the permutations of different bounds per in turn.
			// * Care about the configuration.convolveAlternativeArrivalBounds()-flag later.
			for (ArrivalCurve arrival_bound_turn : arrival_bounds_turn) {
				Curve.getUtils().beautify(arrival_bound_turn);

				for (ArrivalCurve arrival_bound_exiting : arrival_bounds) {
					arrival_bounds_turn_permutations.add(Curve.getUtils().add(arrival_bound_turn, arrival_bound_exiting));
				}
			}

			arrival_bounds.clear();
			arrival_bounds.addAll(arrival_bounds_turn_permutations);
			arrival_bounds_turn_permutations.clear();
		}

		if( configuration.convolveAlternativeArrivalBounds() ) {
			arrival_bounds = new HashSet<ArrivalCurve>( Collections.singleton( Calculator.getInstance().getMinPlus().convolve( arrival_bounds ) ) );
		} 
		
		if( configuration.useArrivalBoundsCache() 
				&& configuration.enforceMultiplexing() != MultiplexingEnforcement.SERVER_LOCAL ) { // Do not cache in that case. Too many variables, the cache does not check all of them.
			
			// As we checked for an existing cache entry at the beginning (and returned it of present), we do not hav to care about the potential overwriting of a cache entry here. 
			getCache( configuration.arrivalBoundMethods() ).addArrivalBounds( configuration, server, flows_to_bound, flow_of_interest, arrival_bounds );
		}
		
		return new HashSet<ArrivalCurve>( arrival_bounds );
	}

	public static Set<ArrivalCurve> computeArrivalBounds(ServerGraph server_graph, AnalysisConfig configuration, Turn turn,
			Set<Flow> flows_to_bound, Flow flow_of_interest) throws Exception {
		flows_to_bound.remove(flow_of_interest);
		if (flows_to_bound.isEmpty()) {
			return new HashSet<ArrivalCurve>(Collections.singleton(Curve_ConstantPool.ZERO_ARRIVAL_CURVE.get()));
		}
		
		if( configuration.useArrivalBoundsCache() && configuration.enforceMultiplexing() != MultiplexingEnforcement.SERVER_LOCAL ) { // Do not cache in that case. Too many variables.
			ArrivalBoundCache.CacheEntryTurn entry = getCache( configuration.arrivalBoundMethods() ).getCacheEntry( configuration, turn, flows_to_bound, flow_of_interest );
			if( entry != null && !entry.arrival_bounds.isEmpty()
					&& !(entry.arrival_bounds.size() > 1 && configuration.convolveAlternativeArrivalBounds()) // Inconsistency between current cache content and current setting.
					) {
				// Be cautious here! By using the original cache entry instead of the getArrivalBounds function, we need to repack the result in a new set manually!
				return new HashSet<ArrivalCurve>( entry.arrival_bounds );
			}
		}

		Set<ArrivalCurve> arrival_bounds_xfcaller = new HashSet<ArrivalCurve>();
		
		for (AnalysisConfig.ArrivalBoundMethod arrival_bound_method : configuration.arrivalBoundMethods()) {
			Set<ArrivalCurve> arrival_bounds_tmp = new HashSet<ArrivalCurve>();

			switch (arrival_bound_method) {
			case AGGR_PBOO_PER_SERVER:
				AggregatePboo_PerServer aggr_pboo_per_server = AggregatePboo_PerServer.getInstance();
				aggr_pboo_per_server.setServerGraph(server_graph);
				aggr_pboo_per_server.setConfiguration(configuration);
				arrival_bounds_tmp = aggr_pboo_per_server.computeArrivalBound(turn, flows_to_bound, flow_of_interest);
				break;

			case AGGR_PBOO_CONCATENATION:
				AggregatePboo_Concatenation aggr_pboo_concatenation = AggregatePboo_Concatenation.getInstance();
				aggr_pboo_concatenation.setServerGraph(server_graph);
				aggr_pboo_concatenation.setConfiguration(configuration);
				arrival_bounds_tmp = aggr_pboo_concatenation.computeArrivalBound(turn, flows_to_bound, flow_of_interest);
				break;

			case AGGR_PMOO:
				AggregatePmoo aggr_pmoo = AggregatePmoo.getInstance();
				aggr_pmoo.setServerGraph(server_graph);
				aggr_pmoo.setConfiguration(configuration);
				arrival_bounds_tmp = aggr_pmoo.computeArrivalBound(turn, flows_to_bound, flow_of_interest);
				break;

			/* 
			 * There are no functional tests for Tandem Matching-based arrival bounding
			 * or segregate arrival bounding methods. 
			 */
				
			case AGGR_TM:
				AggregateTandemMatching aggr_tm = AggregateTandemMatching.getInstance();
				aggr_tm.setServerGraph(server_graph);
				aggr_tm.setConfiguration(configuration);
				arrival_bounds_tmp = aggr_tm.computeArrivalBound(turn, flows_to_bound, flow_of_interest);
				break;

			// This arrival bound is known to be inferior to PMOO and the PBOO_* variants.
			case SEGR_PBOO:
				for (Flow flow : flows_to_bound) {
					SeparateFlowAnalysis sfa = new SeparateFlowAnalysis(server_graph);
					sfa.performAnalysis(flow, flow.getSubPath(flow.getSource(), turn.getSource()));

					arrival_bounds_tmp = getPermutations(arrival_bounds_tmp,
							singleFlowABs(configuration, flow.getArrivalCurve(), sfa.getLeftOverServiceCurves()));
				}
				break;

			// This arrival bound can yield better results than PMOO and the PBOO_* variants. See:
			/*
			 * Catching Corner Cases in Network Calculus - Flow Segregation Can Improve Accuracy.
			 * Steffen Bondorf, Paul Nikolaus and Jens B. Schmitt,
			 * In Proceedings of 19th International GI/ITG Conference on 
			 * Measurement, Modelling and Evaluation of Computing Systems (MMB), 2018.
			 */
			case SEGR_PMOO:
				for (Flow flow : flows_to_bound) {
					PmooAnalysis pmoo = new PmooAnalysis(server_graph);
					pmoo.performAnalysis(flow, flow.getSubPath(flow.getSource(), turn.getSource()));

					arrival_bounds_tmp = getPermutations(arrival_bounds_tmp,
							singleFlowABs(configuration, flow.getArrivalCurve(), pmoo.getLeftOverServiceCurves()));
				}
				break;

			case SEGR_TM:
				for (Flow flow : flows_to_bound) {
					TandemMatchingAnalysis tma = new TandemMatchingAnalysis(server_graph);
					tma.performAnalysis(flow, flow.getSubPath(flow.getSource(), turn.getSource()));

					arrival_bounds_tmp = getPermutations(arrival_bounds_tmp,
							singleFlowABs(configuration, flow.getArrivalCurve(), tma.getLeftOverServiceCurves()));
				}
				break;

			default:
				System.out.println("Executing default arrival bounding: AGGR_PBOO_CONCATENATION");
				AggregatePboo_Concatenation default_ab = new AggregatePboo_Concatenation(server_graph, configuration);
				arrival_bounds_tmp = default_ab.computeArrivalBound(turn, flows_to_bound, flow_of_interest);
				break;
			}

			arrival_bounds_xfcaller.addAll( arrival_bounds_tmp );
		}
		
		if( configuration.convolveAlternativeArrivalBounds() ) {
			arrival_bounds_xfcaller = new HashSet<ArrivalCurve>( Collections.singleton( Calculator.getInstance().getMinPlus().convolve( arrival_bounds_xfcaller ) ) );
		}
		
		if( configuration.useArrivalBoundsCache() 
				&& configuration.enforceMultiplexing() != MultiplexingEnforcement.SERVER_LOCAL ) { // Do not cache in that case. Too many variables, the cache does not check all of them.
			
			// As we checked for an existing cache entry before the for-loop (and returned it of present), we do not hav to care about the potential overwriting of a cache entry here. 
			getCache( configuration.arrivalBoundMethods() ).addArrivalBounds( configuration, turn, flows_to_bound, flow_of_interest, arrival_bounds_xfcaller );
		}
		
		return arrival_bounds_xfcaller;
	}

	private static Set<ArrivalCurve> singleFlowABs(AnalysisConfig configuration, ArrivalCurve alpha,
			Set<ServiceCurve> betas_lo) throws Exception {
		Set<ArrivalCurve> arrival_bounds_f = new HashSet<ArrivalCurve>();

		// All permutations of single flow results
		for (ServiceCurve beta_lo : betas_lo) {
			arrival_bounds_f.add(Calculator.getInstance().getMinPlus().deconvolve(alpha, beta_lo));
		}

		return arrival_bounds_f;
	}

	private static Set<ArrivalCurve> getPermutations(Set<ArrivalCurve> arrival_curves_1,
			Set<ArrivalCurve> arrival_curves_2) {
		if (arrival_curves_1.isEmpty()) {
			return new HashSet<ArrivalCurve>(arrival_curves_2);
		}
		if (arrival_curves_2.isEmpty()) {
			return new HashSet<ArrivalCurve>(arrival_curves_1);
		}

		Set<ArrivalCurve> arrival_bounds_merged = new HashSet<ArrivalCurve>();

		for (ArrivalCurve alpha_1 : arrival_curves_1) {
			for (ArrivalCurve alpha_2 : arrival_curves_2) {
				arrival_bounds_merged.add(Curve.getUtils().add(alpha_1, alpha_2));
			}
		}

		return arrival_bounds_merged;
	}
}