package de.tu_darmstadt.tk.SmartHomeNetworkSim.core;

import java.util.LinkedList;
import java.util.Observable;

import javax.swing.Timer;

import de.tu_darmstadt.tk.SmartHomeNetworkSim.control.Controller;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.scheduler.Schedulable;
import de.tu_darmstadt.tk.SmartHomeNetworkSim.core.scheduler.Scheduler;

/**
 * Manages the Simulation by calling the relevant methods.
 *
 * @author Andreas T. Meyer-Berg
 */
public class SimulationManager extends Observable {
	/**
	 * Model which should be simulated
	 */
	private Model model;

	/**
	 * Controller for manipulationAlgorithms
	 */
	private Controller controller;

	/**
	 * Whether the model status changed during the last simulation and the panel
	 * should be repainted
	 */
	private boolean statusChanged = false;
	
	/**
	 * Timer which triggers each simulation step
	 */
	private Timer timer;

	/**
	 * First Timestep of the simulation
	 */
	private long startTime = 0L;
	
	/**
	 * Last Timestep of the simulation
	 */
	private long endTime = 1000L;
	
	/**
	 * Current TimeStep in the simulation
	 */
	private long currentTime = 0L;
	
	/**
	 * Duration of the simulation
	 */
	private long duration = 100L;
	
	/**
	 * ManipulationAlgorithm Instances, which should run in each timestep
	 */
	private LinkedList<NetworkManipulationAlgorithm> algos = new LinkedList<NetworkManipulationAlgorithm>();
	
	/**
	 * PacketCollectionManager which manages the collection of packets
	 */
	private PacketCollectionManager collectionMan;
	
	/**
	 * PacketExportManager which handles packet export
	 */
	private PacketExportManager exportMan;
	
	/*
	 * Peformance Eval
	 */
	//private long perfStart = 0;
	
	/**
	 * Scheduler which stores the event queue and enables scheduling and running of further events
	 */
	private static Scheduler scheduler;
	
	/**
	 * Creates a new Simulationmanager
	 * 
	 * @param model
	 *            Model that should be simulated
	 */
	public SimulationManager(Model model) {
		this.model = model;
		this.controller = null;
		this.collectionMan = new PacketCollectionManager(model);
		this.exportMan = new PacketExportManager(model);
		timer = new Timer(0, t -> simulateTimeStep());
		scheduler = new Scheduler();
	}

	/**
	 * Simulate the next Timestep
	 */
	private void simulateTimeStep() {
		// Just simulate if timer is running
		if (endTime <= currentTime){
			stopSimulation();
			return;
		}
		if (currentTime + duration <= endTime) {
			simulateTimeIntervall(currentTime, duration);
			currentTime += duration;
			notifyPanels();
		} else {
			simulateTimeIntervall(currentTime, endTime - currentTime);
			currentTime = endTime;
			stopSimulation();
		}
	}

	/**
	 * Simulates one time step at a given time for a given duration
	 * 
	 * @param startTime
	 *            Time the simulation interval starts in
	 *            System.currentTimeMillis() time
	 * @param duration
	 *            Duration of the simulation interval in milliseconds
	 */
	public void simulateTimeIntervall(long startTime, long duration) {
		scheduler.setMinTime(startTime);
		//TODO: Fill Queue
		//TODO: Simulate Schedule
		//TODO: Export
		
		//Simulates the network
		boolean oldWay = false;
		if(oldWay)
			simulateNetwork(startTime, duration);
		else{
			long maxTime = startTime + duration;
			scheduler.scheduleAll(model);
			for(Connection con:model.getConnections()){
				Link l = con.getLink();
				if(l!=null){
					l.getPackets().clear();
					l.addPackets(con.getTerminationPackages(startTime));
				}
			}
			while(scheduler.hasNext(maxTime)){
				Schedulable currentEvent = scheduler.getAndRemoveFirst();
				//System.out.println("Event time: "+currentEvent.getEventTime());
				currentEvent.simulateEvent(currentEvent.getEventTime());
			}
			// Simulate SmartDevices - if they need some logic -> TODO: Insert as schedulable
			model.getDevices().forEach(d -> d.simulateTimeStep(startTime, duration));
		}
		runAlgorithms(startTime+duration);
		collectionMan.collectPackets();
		//Run all 
		collectionMan.runPacketAlgorithms();
		//Export Packets
		exportMan.exportPacketsOfLastTimeStep();
	}
	
	/**
	 * Simulates one time step of the network at a given time for a given duration
	 * 
	 * @param startTime
	 *            Time the simulation interval starts in
	 *            System.currentTimeMillis() time
	 * @param duration
	 *            Duration of the simulation interval in milliseconds
	 */
	private void simulateNetwork(long startTime, long duration){
		//TODO: Implement Event Schedule
		// Nothing changed so far
		statusChanged = false;
		// Simulate all Links, and their connections
		model.getConnectionNetworks().forEach(d -> {
			d.simulateTimeInterval(startTime, duration);
			if (d.getStatusChanged()) {
				for (Connection c : d.getConnections()) {
					if (c.getStatus() == Connection.DONE) {
						model.getConnections().remove(c);
					}
				}
				statusChanged = true;
			}
		});
		// Simulate SmartDevices - if they need some logic
		model.getDevices().forEach(d -> d.simulateTimeStep(startTime, duration));
		// Store Packages/Export Packages etc. (for debug purposes)
		if (statusChanged) {
			model.setChanged();
			model.notifyObservers();
		}
	}

	/**
	 * Start the simulation
	 */
	public void startSimulation() {
		//perfStart=System.currentTimeMillis();
		timer.start();
		notifyPanels();
	}

	/**
	 * Stop the simulation
	 */
	public void stopSimulation() {
		timer.stop();
		/*
		long perfEnd = System.currentTimeMillis();
		long duration = perfEnd - perfStart;
		System.out.println("Duration: "+duration/1000.0);
		*/
		notifyPanels();
	}

	/**
	 * Resets the Simulation to the start time
	 */
	public void resetSimulation() {
		timer.stop();
		timer = new Timer(0, a -> simulateTimeStep());
		currentTime = startTime;
		resetSimulation(currentTime);
		notifyPanels();
	}

	/**
	 * Reset Simulation
	 * 
	 * @param timestep new timestep of ports after the reset
	 */
	private void resetSimulation(long timestep) {
		for (SmartDevice d : model.getDevices())
			for (Port p : d.getPorts())
				if (p.getLastTrigger() > timestep)
					p.setLastTrigger(timestep);
		scheduler.reset(timestep);
		/**
		for(Connection c: model.getConnections())
			for(Port p: c.getParticipants())
				if (p.getLastTrigger() > timestep)
					System.out.println("P without owner: "+p.toString());*/
	}

	/**
	 * Returns true if the simulation is running, false if not
	 * 
	 * @return true if running
	 */
	public boolean isRunning() {
		return timer.isRunning();
	}

	/**
	 * Returns the StartTime of the simulation
	 * 
	 * @return startTime
	 */
	public long getStartTime() {
		return startTime;
	}

	/**
	 * Sets the new startTime
	 * 
	 * @param startTime
	 *            time the simulations starts
	 */
	public void setStartTime(long startTime) {
		this.startTime = startTime;
		notifyPanels();
	}

	/**
	 * Returns the end time of the simulation
	 * 
	 * @return End time of the simulation
	 */
	public long getEndTime() {
		return endTime;
	}

	/**
	 * Sets the new startTime
	 * @param endTime new EndTime
	 */
	public void setEndTime(long endTime) {
		this.endTime = endTime;
		notifyPanels();
	}

	/**
	 * @return the currentTime
	 */
	public long getCurrentTime() {
		return currentTime;
	}

	/**
	 * @param currentTime
	 *            the currentTime to set
	 */
	public void setCurrentTime(long currentTime) {
		this.currentTime = currentTime;
		notifyPanels();
	}

	/**
	 * Returns the simulation step duration in milliseconds
	 * 
	 * @return duration of each simulation step in milliseconds
	 */
	public long getStepDuration() {
		return duration;
	}

	/**
	 * Sets the duration of simulation steps
	 * 
	 * @param duration
	 *            duration in milliseconds of a step
	 */
	public void setStepDuration(long duration) {
		if(duration>0)
			this.duration = duration;
		else
			this.duration = 1;
		notifyPanels();
	}

	/**
	 * Notify the panels, which could update their GUI
	 */
	public void notifyPanels() {
		this.setChanged();
		this.notifyObservers();
	}
	
	/**
	 * Runs all registered algorithms at the currentTimeStep
	 * @param time currentTime of the simulation
	 */
	public void runAlgorithms(long time) {
		/**
		 * Run all Algorithms
		 */
		for(NetworkManipulationAlgorithm algo:algos){
			algo.runAlgorithm(controller, time);
		}
	}
	/**
	 * Returns all registered algorithms of the simulation
	 * @return all registered algorithms
	 */
	public LinkedList<NetworkManipulationAlgorithm> getAlgorithms(){
		return algos;
	}
	/**
	 * Adds an algorithm, which should be executed each timestep
	 * @param algo new algorithm
	 */
	public void addAlgorithm(NetworkManipulationAlgorithm algo, Controller controller){
		this.controller = controller;
		if(algo!=null)
			algos.add(algo);
		
	}
	
	/**
	 * Removes algorithms from the simulation
	 * @param algo algorithm to be removed
	 */
	public void removeAlgo(NetworkManipulationAlgorithm algo){
		algos.remove(algo);
	}
	
	/**
	 * Returns the PacketCollectionManager of the simulation
	 * @return PacketCollectionManager, which contains all the different packet collectors
	 */
	public PacketCollectionManager getPacketCollectionManager(){
		return collectionMan;
	}
	
	/**
	 * Returns the PacketExportManager
	 * @return the exportMan
	 */
	public PacketExportManager getPacketExportManager(){
		return exportMan;
	}
	
	/**
	 * Adds an event to the event queue, which will be simulated later. Returns falls, if it could not be added (e.g. timestep to low)
	 * @param event event which should be scheduled
	 * @return true if it was scheduled, false if not
	 */
	public static boolean scheduleEvent(Schedulable event){
		return scheduler.scheduleEvent(event);
	}
	
	/**
	 * Removes an event from the global event queue
	 * @param event Event which should be removed
	 * @return true, if it was removed, false if not
	 */
	public static boolean removeEvent(Schedulable event){
		return scheduler.removeEvent(event);
	}
}
