/*
 * Copyright (c) 2005-2010 KOM – Multimedia Communications Lab
 *
 * This file is part of PeerfactSim.KOM.
 * 
 * PeerfactSim.KOM is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 * 
 * PeerfactSim.KOM 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with PeerfactSim.KOM.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package de.tud.kom.p2psim.impl.topology.views.fiveg;

import java.util.HashMap;
import java.util.HashSet;

import de.tud.kom.p2psim.api.linklayer.mac.MacAddress;
import de.tud.kom.p2psim.api.scenario.ConfigurationException;
import de.tud.kom.p2psim.impl.topology.views.VisualizationTopologyView.VisualizationInjector;
import de.tud.kom.p2psim.impl.topology.views.fiveg.models.AbstractModel;
import de.tud.kom.p2psim.impl.topology.views.fiveg.utils.Direction;
import de.tud.kom.p2psim.impl.topology.views.fiveg.utils.ParameterType;
import de.tud.kom.p2psim.impl.topology.views.visualization.world.ModelFiveGVisualization;
import de.tudarmstadt.maki.simonstrator.api.Event;
import de.tudarmstadt.maki.simonstrator.api.EventHandler;
import de.tudarmstadt.maki.simonstrator.api.Time;

/**
 * A grid based FiveG model-based database.
 * 
 * This module allows to add transmission models to each segment of the map to
 * accommodate different behaviors of wireless network transmission
 * technologies. It enables the developer to define different models for
 * **Latency**, **Bandwidth** and **Drop Rate** (called *characteristics*), for
 * **Upload** and **Download** and for every **Segment**.
 * 
 * For more information see README.md in
 * de.tud.kom.p2psim.impl.topology.views.fiveg.models.README.md
 * 
 * @author Nils Richerzhagen
 * @version 1.0, Jul 7, 2017
 */
public class ModelBasedSegmentDatabase
		extends AbstractGridBasedTopologyDatabase {

	final Integer DEFAULT_SEGMENT_ID = new Integer(-1);

	// Models[Type][SegID][Up/Down/Both]
	HashMap<ParameterType, HashMap<Integer, HashMap<Direction, AbstractModel>>> models = new HashMap<>();

	public ModelBasedSegmentDatabase() {
		super(100, true);
		super.setSupportCloudlets(true);
	}

	@Override
	protected Entry createEntryFor(int segmentID, boolean isCloudlet) {
		return new ModelBasedEntry(segmentID);
	}
	
	@Override
	public void setEnableVis(boolean enableVis) {
		if (enableVis == true) {
			Event.scheduleImmediately(new EventHandler() {

				@Override
				public void eventOccurred(Object content, int type) {
					ModelFiveGVisualization viz = ModelFiveGVisualization.getInstance();
					viz.setDatabase(ModelBasedSegmentDatabase.this);
					VisualizationInjector.injectComponent(viz);
				}
			}, null, 0);
		}
	}

	/**
	 * Add a model to Database
	 * 
	 * @param model
	 *            the model to add
	 */
	public void setModel(AbstractModel model) {
		// Enable debugging
		model.debug();

		// Check if model is valid
		ParameterType type = model.getParameterType();
		if (type == null) {
			throw new ConfigurationException(
					"Model is not defined for any Parameter Type. Please specify the Type for this Model.");
		}

		Direction dir = model.getDirection();
		if (dir == null) {
			throw new ConfigurationException(
					"Model is not defined for any Direction. Please specify the Direction for this Model.");
		}

		Integer segmentID = new Integer(model.getSegmentID());

		// Check if there is no contradicting model
		if (models.containsKey(type)) {
			// There is already this Type
			if (models.get(type).containsKey(segmentID)) {
				// There is already this SegID
				if (models.get(type).get(segmentID).containsKey(dir)
						|| (models.get(type).get(segmentID)
								.containsKey(Direction.BOTH)
								&& (dir.equals(Direction.DOWNLOAD)
										|| dir.equals(Direction.UPLOAD)))
						|| (dir.equals(Direction.BOTH) && (models.get(type)
								.get(segmentID).containsKey(Direction.DOWNLOAD)
								|| models.get(type).get(segmentID)
										.containsKey(Direction.UPLOAD)))) {
					// There is already a model defined
					throw new ConfigurationException(
							"Conflicting Models for SegmentID " + segmentID
									+ ", Type " + type.toString()
									+ " and Direction " + dir.toString());
				} else {
					// Add new model
					models.get(type).get(segmentID).put(dir, model);
				}
			} else {
				// Add new SegmentID
				// Direction => Model
				HashMap<Direction, AbstractModel> tmp1 = new HashMap<>();
				tmp1.put(dir, model);

				models.get(type).put(segmentID, tmp1);
			}

		} else {
			// Add new model
			// Direction => Model
			HashMap<Direction, AbstractModel> tmp1 = new HashMap<>();
			tmp1.put(dir, model);

			// SegID => [Direction => Model]
			HashMap<Integer, HashMap<Direction, AbstractModel>> tmp2 = new HashMap<>();
			tmp2.put(segmentID, tmp1);

			models.put(type, tmp2);
		}
	}

	/**
	 * Get the model for the specific combination
	 * 
	 * @param segID
	 *            The SegmentID to get the model for
	 * @param type
	 *            The Type of the Parameter
	 * @param isUpload
	 *            Is it a Up or Downlink
	 * @return The Model for the given segment
	 */
	AbstractModel getModel(int segID, ParameterType type, Boolean isUpload) {

		Integer segmentID = new Integer(segID);
		Direction dir = Direction.DOWNLOAD;
		
		if (isUpload) {
			dir = Direction.UPLOAD;
		}
		
		// Check if type exists
		if (!this.models.containsKey(type)) {
			throw new ConfigurationException(
					"No Model is defined for " + type + ".");
		}
		
		// Check if there is a specific model 
		if(this.models.get(type).containsKey(segmentID) && this.models.get(type).get(segmentID).containsKey(dir)) {
			return this.models.get(type).get(segmentID).get(dir);
		}
		
		// Check if there is a BOTH model in this segment
		if(this.models.get(type).containsKey(segmentID) && this.models.get(type).get(segmentID).containsKey(Direction.BOTH)) {
			return this.models.get(type).get(segmentID).get(Direction.BOTH);
		}
		
		// Check default model for specific model
		if(this.models.get(type).containsKey(DEFAULT_SEGMENT_ID) && this.models.get(type).get(DEFAULT_SEGMENT_ID).containsKey(dir)) {
			return this.models.get(type).get(DEFAULT_SEGMENT_ID).get(dir);
		}
		
		// Check if there is a default BOTH model
		if(this.models.get(type).containsKey(DEFAULT_SEGMENT_ID) && this.models.get(type).get(DEFAULT_SEGMENT_ID).containsKey(Direction.BOTH)) {
			return this.models.get(type).get(DEFAULT_SEGMENT_ID).get(Direction.BOTH);
		}

		throw new ConfigurationException(
				"No Model is defined for Type " + type + ", Segment ID "
						+ segmentID + " and Direction " + dir + ".");
	}

	public class ModelBasedEntry implements FiveGTopologyDatabase.Entry {

		// How is a overload defined
		// FIXME Can the time in the Latency result in problems if the drop-rate is set to 100%?
		private final long OVERLOAD_LATENCY = 9999 * Time.MILLISECOND;

		private final long OVERLOAD_BANDWIDTH = 1;

		private final double OVERLOAD_DROPRATE = 1;

		// When is a node considered overloaded
		private final long THRESHOLD_LATENCY = Time.SECOND;

		private final long THRESHOLD_BANDWIDTH = 10;

		private final double THRESHOLD_DROPRATE = 1;
		
		private final double YELLOW_INDICATOR = .8;

		// Other storage
		private final int segment;

		private boolean isAvailable = true;

		private HashSet<MacAddress> hostsInSegment = new HashSet<>();

		private boolean overload;

		// The current metrics of this segment
		private long bandUp;

		private long bandDown;

		private long latUp;

		private long latDown;

		private double dropUp;

		private double dropDown;

		/**
		 * Create a new entry for the given segment
		 * 
		 * @param segment
		 */
		public ModelBasedEntry(int segment) {
			this.segment = segment;
			calc();
		}

		@Override
		public int getSegmentID() {
			return segment;
		}

		/**
		 * A host leaves this segment
		 */
		public void onHostLeavesSegment(MacAddress hostAddr) {
			// Remove MAC from current users
			this.hostsInSegment.remove(hostAddr);

			calc();
		}

		/**
		 * A host enters this segment
		 */
		public void onHostEntersSegment(MacAddress hostAddr) {
			// Add MAC to current users
			this.hostsInSegment.add(hostAddr);

			calc();
		}

		@Override
		public double getDropProbability(boolean isUpload) {
			// Segment is overloaded or not available return overloaded drop
			// probability
			if (!isAvailable || overload) {
				return OVERLOAD_DROPRATE;
			}

			// Return calculated drop probability
			if (isUpload) {
				return dropUp;
			} else {
				return dropDown;
			}
		}

		@Override
		public long getLatency(boolean isUpload) {
			// Segment is overloaded or not available return overloaded latency
			if (!isAvailable || overload) {
				return OVERLOAD_LATENCY;
			}

			// Return calculated latency
			if (isUpload) {
				return latUp;
			} else {
				return latDown;
			}
		}

		@Override
		public long getBandwidth(boolean isUpload) {
			// Segment is overloaded or not available return overloaded
			// bandwidth
			if (!isAvailable || overload) {
				return OVERLOAD_BANDWIDTH;
			}

			// Return calculated bandwidth
			if (isUpload) {
				return bandUp;
			} else {
				return bandDown;
			}
		}

		@Override
		public boolean isAvailable() {
			return isAvailable;
		}

		@Override
		public void setAvailability(boolean isAvailable) {
			this.isAvailable = isAvailable;
		}
		
		public int getStatus(ParameterType type, Direction dir) {
			
			if(type.equals(ParameterType.BANDWIDTH) && dir.equals(Direction.UPLOAD)) {
				if(this.bandUp <= THRESHOLD_BANDWIDTH) {
					return 2;
				}
				
				if(this.bandUp < ((2 - YELLOW_INDICATOR) * THRESHOLD_BANDWIDTH)) {
					return 1;
				}
				
				return 0;
			}
			
			if(type.equals(ParameterType.BANDWIDTH) && dir.equals(Direction.DOWNLOAD)) {
				if(this.bandDown <= THRESHOLD_BANDWIDTH) {
					return 2;
				}
				
				if(this.bandDown < ((2 - YELLOW_INDICATOR) * THRESHOLD_BANDWIDTH)) {
					return 1;
				}
				
				return 0;
			}
			
			if(type.equals(ParameterType.DROPRATE) && dir.equals(Direction.UPLOAD)) {
				if(this.dropUp >= THRESHOLD_DROPRATE) {
					return 2;
				}
				
				if(this.dropUp > (YELLOW_INDICATOR * THRESHOLD_DROPRATE)) {
					return 1;
				}
				
				return 0;
			}
			
			if(type.equals(ParameterType.DROPRATE) && dir.equals(Direction.DOWNLOAD)) {
				if(this.dropDown >= THRESHOLD_DROPRATE) {
					return 2;
				}
				
				if(this.dropDown > (YELLOW_INDICATOR * THRESHOLD_DROPRATE)) {
					return 1;
				}
				
				return 0;
			}
			
			if(type.equals(ParameterType.LATENCY) && dir.equals(Direction.UPLOAD)) {
				if(this.latUp >= THRESHOLD_LATENCY) {
					return 2;
				}
				
				if(this.latUp > ((2 - YELLOW_INDICATOR) * THRESHOLD_LATENCY)) {
					return 1;
				}
				return 0;
			}
			
			if(type.equals(ParameterType.LATENCY) && dir.equals(Direction.DOWNLOAD)) {
				if(this.latDown >= THRESHOLD_LATENCY) {
					return 2;
				}
				
				if(this.latDown > ((2 - YELLOW_INDICATOR) * THRESHOLD_LATENCY)) {
					return 1;
				}
				return 0;
			}
			
			return 0;
		}
		
		public boolean isOverloaded() {
			return overload;
		}
		
		public int getHostsInSegment() {
			return this.hostsInSegment.size();
		}

		/**
		 * Recalculate every metric when a host leaves or enters
		 */
		private void calc() {
			int users = this.hostsInSegment.size();

			// Assume not overloaded
			this.overload = false;

			// Calc Bandwidth
			this.bandUp = getModel(getSegmentID(), ParameterType.BANDWIDTH,
					true).getLong(users);
			this.bandDown = getModel(getSegmentID(), ParameterType.BANDWIDTH,
					false).getLong(users);

			if (bandDown <= THRESHOLD_BANDWIDTH
					|| bandUp <= THRESHOLD_BANDWIDTH) {
				overload = true;
			}

			// Calc Latency
			this.latUp = getModel(getSegmentID(), ParameterType.LATENCY, true)
					.getLong(users);
			this.latDown = getModel(getSegmentID(), ParameterType.LATENCY,
					false).getLong(users);

			if (latUp >= THRESHOLD_LATENCY || latDown >= THRESHOLD_LATENCY) {
				overload = true;
			}

			// Calc Droprate
			this.dropUp = getModel(getSegmentID(), ParameterType.DROPRATE, true)
					.getDouble(users);
			this.dropDown = getModel(getSegmentID(), ParameterType.DROPRATE,
					false).getDouble(users);

			if (dropUp >= THRESHOLD_DROPRATE
					|| dropDown >= THRESHOLD_DROPRATE) {
				overload = true;
			}
		}
	}
}
