package de.tu_darmstadt.tk.SmartHomeNetworkSim.core;

import java.util.Collection;
import java.util.LinkedList;

import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.Connection;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.Link;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.Packet;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.SmartDevice;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.util.PacketComparator;

import java.util.ArrayList;

/**
 * Simple Implementation of {@link Link}, which allows connection of multiple
 * devices, with an more precise calculation than {@link de.tu_darmstadt.tk.SmartHomeNetworkSim.core.simpleImplementation.SimpleLink}
 *
 * @author Andreas T. Meyer-Berg
 */
public class PrecisionLink implements Link {

	/**
	 * Name of the connection
	 */
	private String name;

	/**
	 * Devices connected by this Link
	 */
	private ArrayList<SmartDevice> devices;

	/**
	 * Connections running via this Link
	 */
	private ArrayList<Connection> connections;
	/**
	 * List of packages to store packages sent during simulation intervals, or
	 * after termination
	 */
	private LinkedList<Packet> packets;
	/**
	 * Packets which should be returned in the next time step
	 */
	private LinkedList<Packet> outOfBoundsPackets = new LinkedList<Packet>();
	
	/**
	 * whether the status changed during the last simulation step
	 */
	private boolean statusChanged;

	/**
	 * Delay between two devices, fixed
	 */
	private long fixedDelay=3;

	/**
	 * Initializes a simple Link with name and an empty devices list.
	 * 
	 * @param name
	 *            Name the Link should have
	 */
	public PrecisionLink(String name) {
		this.name = name;
		this.devices = new ArrayList<SmartDevice>();
		this.packets = new LinkedList<Packet>();
		this.connections = new ArrayList<Connection>();
	}
	
	/**
	 * Initializes a simple Link with default name and an empty devices list.
	 */
	public PrecisionLink() {
		this.name = "link name";
		this.devices = new ArrayList<SmartDevice>();
		this.packets = new LinkedList<Packet>();
		this.connections = new ArrayList<Connection>();
	}

	/**
	 * Returns the name of this Link
	 * 
	 * @return name of this Link
	 */
	public String getName() {
		return name;
	}

	/**
	 * Sets the name of this Link
	 * 
	 * @param name
	 *            the name to set
	 */
	public void setName(String name) {
		this.name = name;
	}

	/**
	 * Returns the device that are part of this connection
	 * 
	 * @return the devices of this Link
	 */
	public Collection<SmartDevice> getDevices() {
		return devices;
	}

	/**
	 * Adds the SmartDevice to this Link
	 * 
	 * @param device
	 *            the devices to add
	 */
	public void addDevice(SmartDevice device) {
		this.devices.add(device);
	}

	/**
	 * Remove SmartDevice from this Link
	 * 
	 * @param device
	 *            the device to remove
	 */
	public void removeDevice(SmartDevice device) {
		this.devices.remove(device);
	}

	@Override
	public void simulateTimeInterval(long startTime, long duration) {
		packets.clear();
		statusChanged = false;
		packets.addAll(outOfBoundsPackets);
		for (Connection c : connections) {
			if (c.getLink() != this)
				continue;
			// Simulate just if source and link match
			if ((c.getStatus() == Connection.ACTIVE || c.getStatus() == Connection.HALTED))
				packets.addAll(c.simulateTimeInterval(startTime, duration));
			else if (c.getStatus() == Connection.FINISHED
					|| c.getStatus() == Connection.TERMINATED)
				//Produce Termination packages
				packets.addAll(c.getTerminationPackages(startTime));
			statusChanged|=c.getStatusChanged();
		}
		packets.sort(new PacketComparator());

		/**
		 * Remove packets which are not being sent in this time interval
		 */
		outOfBoundsPackets.clear();
		/**
		 * Last package, which should be sent in the next time step
		 */
		Packet last = packets.isEmpty()? null : packets.getLast();
		while (last != null&&last.getTimestamp()>startTime+duration) {
			outOfBoundsPackets.addFirst(packets.removeLast());
			if(packets.isEmpty())
				break;
			last = packets.getLast();
		}
	}
	
	@Override
	public Collection<Packet> encapsulatePackages(Collection<Packet> packets){
		return packets;
	}

	@Override
	public Collection<Packet> getPackets() {
		return packets;
	}

	@Override
	public Collection<Connection> getConnections() {
		return connections;
	}

	@Override
	public void addConnection(Connection connection) {
		connections.add(connection);
	}

	@Override
	public void removeConnection(Connection connection) {
		connections.remove(connection);
	}

	@Override
	public boolean getStatusChanged() {
		return statusChanged;
	}

	@Override
	public long getTransmissionDelayFrom(SmartDevice from, SmartDevice to) {
		return fixedDelay;
	}
	
	/**
	 * Set the delay of this Link
	 * @param delay fixed transmission delay
	 */
	public void setFixedDelay(long delay){
		this.fixedDelay = delay;
	}
	
	/**
	 * Returns the fixed transmission delay
	 * @return transmission delay
	 */
	public long  getFixedDelay(){
		return this.fixedDelay;
	}

	@Override
	public void addPackets(Collection<Packet> packets) {
		this.packets.addAll(packets);
	}

	@Override
	public void initSimulationInterval(long startTime, long duration) {
		/**
		 * Reset packets
		 */
		packets.clear();
		/**
		 * Add out of Bounds packets
		 */
		packets.addAll(outOfBoundsPackets);
	}

	@Override
	public void finalizeSimulationInterval(long startTime, long duration) {
		//Remove out of Bounds Packets
		/**
		 * Remove packets which are not being sent in this time interval
		 */
		outOfBoundsPackets.clear();
		/**
		 * Last package, which should be sent in the next time step
		 */
		Packet last = packets.isEmpty()? null : packets.getLast();
		while (last != null&&last.getTimestamp()>startTime+duration) {
			outOfBoundsPackets.addFirst(packets.removeLast());
			if(packets.isEmpty())
				break;
			last = packets.getLast();
		}
	}
}
