package de.tu_darmstadt.tk.SmartHomeNetworkSim.core;

import java.util.Collection;
import java.util.Random;

import org.apache.commons.math3.distribution.AbstractRealDistribution;
import org.apache.commons.math3.distribution.LaplaceDistribution;

import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.distributionHandler.ConstantValueDistributionHandler;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.scheduler.Schedulable;

/**
 * Representation of connection EndPoints, which allows configuration of timings
 * and if it reacts to incoming traffic or even triggers new connections.
 * 
 * @author Andreas T. Meyer-Berg
 */
public class Port implements Schedulable {

	/**
	 * A closed Port which does not react to incoming traffic
	 */
	public static final short CLOSED = 0;

	/**
	 * Open Port which reacts to incoming traffic
	 */
	public static final short OPEN = 1;

	/**
	 * Open Port which reacts to incoming traffic and sends packets every
	 * triggerInterval
	 */
	public static final short SENDING = 2;

	/**
	 * Status of the Port, whether it is CLOSED, OPEN or SENDING
	 */
	private short status;

	/**
	 * SmartDevice which owns the port
	 */
	private SmartDevice owner;

	/**
	 * Connection this Port listens to
	 */
	private Connection connection;

	/**
	 * Last timestep traffic was triggered
	 */
	private long lastTrigger;

	/**
	 * Time in milliseconds needed for responds to incoming 
	 */
	private short responseTime;

	/**
	 * Jitter of the connection of this port. Random fluctuation of delivery times. 
	 */
	private short jitter;
	
	/**
	 * Current Jitter, updated after every simulation
	 */
	private short currentJitter;
	
	/**
	 * Port number of this Port.
	 */
	private short portNumber;

	/**
	 * Constant time interval, which triggers new traffic every
	 * {@code triggerInterval} milliseconds.
	 */
	private long triggerInterval;

	/**
	 * Probability Distribution for calculating the next Trigger
	 */
	private ProbabilityDistributionHandler triggerIntervalStat;
	
	/**
	 * Creates a new Port for the given Device with the specified PortNumber
	 * @param device SmartDevice this port listens on
	 * @param portNumber Number of the Port
	 */
	public Port(SmartDevice device, short portNumber){
		status = SENDING;
		owner = device;
		connection = null;
		triggerIntervalStat = new ConstantValueDistributionHandler(new Random().nextInt(1000)+1);
		triggerInterval = Math.max(1, triggerIntervalStat.sampleNextValue());
		//setTriggerInterval(new Random().nextInt(1000)+1);
		lastTrigger = 0;
		jitter = (short)(new Random().nextInt(5)+1);
		currentJitter = (short)Math.round(Math.random()*jitter);
		responseTime = 0;
		this.portNumber = portNumber;
	}
	
	/**
	 * Creates a new Port for the given Device with the specified PortNumber
	 * @param device SmartDevice this port listens on
	 * @param portNumber Number of the Port
	 * @param triggerInterval frequency the port should trigger
	 */
	public Port(SmartDevice device, short portNumber, long triggerInterval){
		status = SENDING;
		owner = device;
		connection = null;
		triggerIntervalStat = new ConstantValueDistributionHandler(triggerInterval);
		this.triggerInterval = Math.max(1, triggerIntervalStat.sampleNextValue());
		lastTrigger = 0;
		jitter = 0;
		currentJitter = (short)Math.round(Math.random()*jitter);
		responseTime = 0;
		this.portNumber = portNumber;
	}
	
	/**
	 * Creates a new Port for the given Device with the specified PortNumber
	 * @param device SmartDevice this port listens on
	 * @param portNumber Number of the Port
	 * @param triggerInterval frequency the port should trigger
	 * @param jitter Jitter
	 * @param lastTrigger last time a packet was sent
	 * @param responseTime response time
	 */
	public Port(SmartDevice device, short portNumber, long triggerInterval, short jitter, long lastTrigger, short responseTime){
		status = SENDING;
		owner = device;
		connection = null;
		triggerIntervalStat = new ConstantValueDistributionHandler(triggerInterval);
		this.triggerInterval = Math.max(1, triggerIntervalStat.sampleNextValue());
		this.lastTrigger = lastTrigger;
		this.jitter = jitter;
		currentJitter = (short)Math.round(Math.random()*jitter);
		this.responseTime = responseTime;
		this.portNumber = portNumber;
	}
	
	
	/**
	 * @return the status
	 */
	public short getStatus() {
		return status;
	}

	/**
	 * @param status
	 *            the status to set
	 */
	public void setStatus(short status) {
		this.status = status;
	}

	/**
	 * @return the owner
	 */
	public SmartDevice getOwner() {
		return owner;
	}

	/**
	 * @param owner
	 *            the owner to set
	 */
	public void setOwner(SmartDevice owner) {
		this.owner = owner;
	}

	/**
	 * TriggerInterval is sampled from distribution after previous simulation
	 * @return the triggerInterval
	 */
	public long getTriggerInterval() {
		return triggerInterval;
	}

	/**
	 * Returns the Probability Handler for the trigger interval, which allows configuration of the Distribution
	 * @return Distribution Handler for configuration
	 */
	public ProbabilityDistributionHandler getTriggerHandler(){
		return triggerIntervalStat;
	}
	
	/**
	 * Set a new Probability Distribution Handler to sample the trigger interval
	 * @param triggerHandler Sets the Probability Distribution to generate the trigger interval
	 */
	public void setTriggerHandler(ProbabilityDistributionHandler triggerHandler){
		this.triggerIntervalStat = triggerHandler;
		boolean removed = SimulationManager.removeEvent(this);
		this.triggerInterval = Math.max(1, triggerHandler.sampleNextValue());
		if(removed)
			SimulationManager.scheduleEvent(this);
	}
	
	/**
	 * Samples a new Value for the TriggerInterval
	 */
	public void resampleTriggerInterval(){
		boolean removed = SimulationManager.removeEvent(this);
		this.triggerInterval = Math.max(triggerIntervalStat.sampleNextValue(),1);
		if(removed)
			SimulationManager.scheduleEvent(this);
	}

	/**
	 * @return the responseTime
	 */
	public short getResponseTime() {
		return responseTime;
	}

	/**
	 * @param responseTime
	 *            the responseTime to set
	 */
	public void setResponseTime(short responseTime) {
		if(responseTime<=0)
			this.responseTime = 0;
		else
			this.responseTime = responseTime;
	}

	/**
	 * @return the portNumber
	 */
	public short getPortNumber() {
		return portNumber;
	}

	/**
	 * @param portNumber
	 *            the portNumber to set
	 */
	public void setPortNumber(short portNumber) {
		this.portNumber = portNumber;
	}

	/**
	 * @return the last timestep the port sent a package
	 */
	public long getLastTrigger() {
		return lastTrigger;
	}

	/**
	 * @param lastTrigger the last timestep the port sent
	 */
	public void setLastTrigger(long lastTrigger) {
		this.lastTrigger = lastTrigger;
	}

	/**
	 * @return the connection
	 */
	public Connection getConnection() {
		return connection;
	}

	/**
	 * @param connection the connection to set
	 */
	public void setConnection(Connection connection) {
		this.connection = connection;
	}	
	
	/**
	 * Return the Jitter of the port, which represents random fluctuations of the delivery times
	 * @return the Jitter
	 */
	public short getJitter() {
		return jitter;
	}
	
	
	/**
	 * Set Jitter of the port, which represents random fluctuations of the delivery times
	 * @param jitter the Jitter to set
	 */
	public void setJitter(short jitter) {
		this.jitter = jitter;
	}
	
	/**
	 * Returns a String representation for the status byte
	 * Returns 0:closed, 1:open, 2:sending
	 * 
	 * @param s as specified by {@link Port}
	 * @return textual representation of the status
	 */
	public static String statusToString(short s){
		switch (s) {
		case CLOSED:
			return "closed";
		case OPEN:
			return "open";
		case SENDING:
			return "sending";
		default:
			return "unknown";
		}
	}
	
	@Override
	public String toString() {
		String protocol = "";
		if(connection!=null&&connection.getProtocol()!=null)
			protocol=connection.getProtocol().getName();
		return "Port: " + portNumber + protocol + " Status:" + statusToString(status) + " " + "TriggerInterval:"+triggerInterval+" "+(owner == null ? "" : " on " + owner.getName());
	}
	
	/**
	 * Returns the transmission delay in milliseconds to the other port, which is specified in the Link of the Connection.
	 * @param to Port, which should receive the transmission
	 * @return delay between this port and the to Port.
	 */
	public long getTransmissionDelayTo(Port to){
		if(connection != null && connection.getLink() != null)
				if(owner != null && to != null && to.getOwner()!=null)
					return connection.getLink().getTransmissionDelayFrom(owner, to.getOwner());
		return Long.MAX_VALUE;
	}

	@Override
	public long getEventTime() {
		return lastTrigger+triggerInterval+currentJitter;
	}

	@Override
	public void simulateEvent(long time) {
		if(connection==null || connection.getProtocol()==null || connection.getLink()==null || owner == null)
			return;
		
		/**
		 * Packets of the transfer initiated by this port
		 */
		Collection<Packet> packets = connection.getProtocol().generateNextPackets(this, time, false);
		
		//Packets encapsulated by Connection
		packets = connection.encapsulatePackages(packets);
		
		//Encapsulate packets in Links
		packets = connection.getLink().encapsulatePackages(packets);
		
		connection.getLink().addPackets(packets);
		
		//TODO: Sample next game
		triggerInterval = Math.max(1,triggerIntervalStat.sampleNextValue());
		
		if(status==Port.SENDING && connection.getStatus()==Connection.ACTIVE)
			SimulationManager.scheduleEvent(this);
		
	}
	
	public static void main(String[] args) {
		AbstractRealDistribution dist = new LaplaceDistribution(30, 50);
		for(int i = 0; i<100; i++)
			System.out.println(dist.sample());
	}
}
