package at.ac.tuwien.lsdc.sched;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.SortedMap;
import java.util.TreeMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import at.ac.tuwien.lsdc.federation.Federation;
import at.ac.tuwien.lsdc.management.MachineManager;
import at.ac.tuwien.lsdc.types.Application;
import at.ac.tuwien.lsdc.types.ScenarioData;
import at.ac.tuwien.lsdc.types.ScenarioType;
import at.ac.tuwien.lsdc.types.SchedulerData;
import at.ac.tuwien.lsdc.types.SchedulerEvent;
import at.ac.tuwien.lsdc.types.SchedulerEvent.EventType;
import at.ac.tuwien.lsdc.types.VirtualMachine.VMType;
import at.ac.tuwien.lsdc.util.CSVLogger;
import at.ac.tuwien.lsdc.util.NumberUtils;

/**
 * Class doing all the generic scheduling work.
 */
public abstract class AbstractScheduler {

	private static final Logger log = LoggerFactory.getLogger(AbstractScheduler.class);

	// the following types are only needed for correct
	// log output
	protected ScenarioType scenario;
	protected int numTotalInSourced;
	protected int numTotalOutSourced;
	protected int numCurrInSourced;
	protected int numCurrOutSourced;
	protected int numStarted;
	protected int numStopped;
	protected int numTotalStarted;
	protected int numTotalStopped;
	protected double totalConsumption;

	protected MachineManager manager;
	protected File schedulerLog;
	protected CSVLogger logger;
	protected VMType vmType;

	// List of Apps that were delayed due to not enough free resources.
	protected ArrayList<Application> delayedApps;

	private int totalDelayedApps;
	/**
	 * this map saves the following Type of Events: start of an Application, end of an
	 * Application(start outSourced, end outSourced, start inSourced, end inSourced)
	 */
	protected SortedMap<Long, HashMap<EventType, LinkedList<SchedulerEvent>>> eventMap;

	/**
	 * Scheduler has an internal Time "Abstraction" at every point in time it checks for Events in
	 * his "EventList" and handles them (via the individual scheduling algorithm)
	 */
	protected static long currTime = 0;

	/**
	 * the timestamp at which the Scheduler is finished it is updated with every added "EndEvent"
	 */
	protected long endTime;

	/**
	 * All our cloud partners.
	 */
	protected Federation federation;

	public AbstractScheduler(int numPMs, int numCloudPartners, File schedulerLog,
			ScenarioType scenario) throws IOException {
		manager = new MachineManager(numPMs);
		this.schedulerLog = schedulerLog;
		this.scenario = scenario;
		eventMap = new TreeMap<Long, HashMap<EventType, LinkedList<SchedulerEvent>>>();
		logger = new CSVLogger(schedulerLog);
		federation = new Federation(numCloudPartners);
		delayedApps = new ArrayList<Application>();
	}

	/**
	 * Initialize Scheduler with Data from CSV CSV will be parsed and sent as List<Application> to
	 * Scheduler
	 */
	public ScenarioData initAndStart(LinkedList<Application> apps) {
		for (Application app : apps) {
			insertStartEvent(app.getTimestamp(), app);
		}

		endTime = apps.getLast().getTimestamp() + 1;

		startScheduling();
		return doEndLogging();
	}

	/**
	 * Insert a start event into the map, at timestamp when the application should start
	 *
	 * @param timestamp the timestamp when the application should start
	 * @param app the application to start
	 */
	protected void insertStartEvent(long timestamp, Application app) {
		SchedulerEvent evt = new SchedulerEvent(timestamp, EventType.startApplication, app);
		insertEvent(evt);
	}

	/**
	 * Insert a start outsourced event into the map, at timestamp when the application should start
	 *
	 * @param timestamp the timestamp when the application should start
	 * @param app the application to start
	 */
	protected void insertOutsourcedStartEvent(long timestamp, Application app) {
		SchedulerEvent evt = new SchedulerEvent(timestamp, EventType.startOutsourcedApplication,
				app);
		insertEvent(evt);
	}

	/**
	 * Insert a start insourced event into the map, at timestamp when the application should start
	 *
	 * @param timestamp the timestamp when the application should start
	 * @param app the application to start
	 */
	protected void insertInsourcedStartEvent(long timestamp, Application app) {
		SchedulerEvent evt = new SchedulerEvent(timestamp, EventType.startInsourcedApplication,
				app);
		insertEvent(evt);
	}

	/**
	 * Insert a stop event into the map, at timestamp when the application should stop.
	 *
	 * @param timestamp the timestamp when the application should stop
	 * @param app the application to stop
	 */
	protected void insertStopEvent(long timestamp, Application app) {
		SchedulerEvent evt = new SchedulerEvent(timestamp, EventType.endApplication, app);
		insertEvent(evt);
		if (endTime < timestamp) {
			endTime = timestamp;
		}
	}

	/**
	 * Insert a stop event into the map, at timestamp when the application should stop.
	 *
	 * @param timestamp the timestamp when the application should stop
	 * @param app the application to stop
	 */
	protected void insertOutsourcedStopEvent(long timestamp, Application app) {
		SchedulerEvent evt = new SchedulerEvent(timestamp, EventType.endOutsourcedApplication, app);
		insertEvent(evt);
		if (endTime < timestamp) {
			endTime = timestamp;
		}
	}

	/**
	 * Insert a stop event into the map, at timestamp when the application should stop.
	 *
	 * @param timestamp the timestamp when the application should stop
	 * @param app the application to stop
	 */
	protected void insertInsourcedStopEvent(long timestamp, Application app) {
		SchedulerEvent evt = new SchedulerEvent(timestamp, EventType.endInsourcedApplication, app);
		insertEvent(evt);
		if (endTime < timestamp) {
			endTime = timestamp;
		}
	}

	private void insertEvent(SchedulerEvent evt) {
		LinkedList<SchedulerEvent> list;
		if (!eventMap.containsKey(evt.getTimestamp())) {
			HashMap<EventType, LinkedList<SchedulerEvent>> map = new HashMap<EventType, LinkedList<SchedulerEvent>>();
			eventMap.put(evt.getTimestamp(), map);
		}
		if (!eventMap.get(evt.getTimestamp()).containsKey(evt.getType())) {
			list = new LinkedList<SchedulerEvent>();
			eventMap.get(evt.getTimestamp()).put(evt.getType(), list);
		} else {
			list = eventMap.get(evt.getTimestamp()).get(evt.getType());
		}
		list.add(evt);
	}

	/**
	 * Start the actual scheduling algorithm. Each scheduler will implement the method handleEvents
	 * differently
	 */
	protected void startScheduling() {
		while (true) {
			if (eventMap.containsKey(currTime)) {
				// log.info(events.size() + " events at timestamp " + currTime);
				handleEvents(eventMap.get(currTime));
			}
			doStateLogging();
			// advance Time to next step
			currTime++;

			if (currTime > endTime  &&  delayedApps.size() == 0) {
				// reached last Event, Scheduler will shut down
				log.info("Last event reached at time " + currTime);
				break;
			}
		}
	}

	/**
	 * this method is where the Scheduling Algorithm resides it reads the Events (start & stop of
	 * applications)
	 *
	 * @param events the events to be read and used by the scheduler
	 */
	protected void handleEvents(HashMap<EventType, LinkedList<SchedulerEvent>> events) {
		numStarted = 0;
		numStopped = 0;
		
		if (events.containsKey(EventType.endApplication))
			handleEndEvents(events.get(EventType.endApplication));
		if (events.containsKey(EventType.endOutsourcedApplication))
			handleOutsourcedEndEvents(events.get(EventType.endOutsourcedApplication));
		if (events.containsKey(EventType.endInsourcedApplication))
			handleInsourcedEndEvents(events.get(EventType.endInsourcedApplication));
		if(delayedApps.size() > 0)
			handleDelayedApps();
		
		if (events.containsKey(EventType.startOutsourcedApplication))
			handleOutsourcedStartEvents(events.get(EventType.startOutsourcedApplication));
		if (events.containsKey(EventType.startApplication))
			handleStartEvents(events.get(EventType.startApplication));
		if (events.containsKey(EventType.startInsourcedApplication))
			handleInsourcedStartEvents(events.get(EventType.startInsourcedApplication));
		
		numTotalStarted += numStarted;
		numTotalStopped += numStopped;
	}

	protected void handleDelayedApps() {
		LinkedList<SchedulerEvent> delayedStartEvents = new LinkedList<SchedulerEvent>();
		for (Application app : delayedApps) {
			SchedulerEvent evt = new SchedulerEvent(currTime, EventType.startApplication, app);
			delayedStartEvents.add(evt);
			totalDelayedApps ++;
		}

		delayedApps.clear();
		handleStartEvents(delayedStartEvents);
	}

	protected abstract void handleEndEvents(LinkedList<SchedulerEvent> events);

	protected abstract void handleStartEvents(LinkedList<SchedulerEvent> events);

	/**
	 * handle running of outsourced apps.
	 *
	 * @param events list of all events that happened in this timeslot.
	 */
	protected void handleOutsourcedStartEvents(LinkedList<SchedulerEvent> events) {
		for (SchedulerEvent evt : events) {
			if (evt.getType() == EventType.startOutsourcedApplication) {
				insertOutsourcedStopEvent(currTime + evt.getApp().getDuration(), evt.getApp());
				numCurrOutSourced++;
				numTotalOutSourced++;
			}
		}
	}

	/**
	 * handle running of outsourced apps.
	 *
	 * @param events list of all events that happened in this timeslot.
	 */
	protected void handleInsourcedStartEvents(LinkedList<SchedulerEvent> events) {
		for (SchedulerEvent evt : events) {
			if (evt.getType() == EventType.startInsourcedApplication) {
				insertInsourcedStopEvent(currTime + evt.getApp().getDuration(), evt.getApp());
				numCurrInSourced++;
				numTotalInSourced++;
			}
		}
	}

	/**
	 * handle stopping of outsourced apps.
	 *
	 * @param events list of all events that happened in this timeslot.
	 */
	protected void handleOutsourcedEndEvents(LinkedList<SchedulerEvent> events) {
		for (SchedulerEvent evt : events) {
			if (evt.getType() == EventType.endOutsourcedApplication) {
				numCurrOutSourced--;
			}
		}
	}

	/**
	 * handle stopping of outsourced apps.
	 *
	 * @param events list of all events that happened in this timeslot.
	 */
	protected void handleInsourcedEndEvents(LinkedList<SchedulerEvent> events) {
		for (SchedulerEvent evt : events) {
			if (evt.getType() == EventType.endInsourcedApplication) {
				numCurrInSourced--;
			}
		}
	}

	/**
	 * Does the logging for each timestamp. It uses the CSVLogger to write directly to the specific
	 * scheduler/scenario csv file
	 */
	protected void doStateLogging() {
		SchedulerData data = new SchedulerData(currTime, manager.getTotalRam(),
				manager.getTotalCpu(), manager.getTotalSize(), manager.countCurrentlyRunningPMs(),
				manager.countCurrentlyRunningVMs(), manager.getCurrentConsumption(),
				numCurrInSourced, numCurrOutSourced, MachineManager.getTotalResizeCalls(), delayedApps.size(), 
				numStarted, numStopped);
		totalConsumption += manager.getCurrentConsumption();
		try {
			logger.logSchedulerData(data);
		} catch (IOException e) {
			log.error("error logging data");
		}
	}

	/**
	 * this creates the total summary which should be written to a CSV at the end
	 *
	 * @return a ScenarioData Object that holds the values to be logged
	 */
	protected ScenarioData doEndLogging() {
		return new ScenarioData(getSchedulerType(), scenario.toString(), manager.getTotalPMs(),
				manager.getTotalVMs(), endTime, NumberUtils.roundDouble(totalConsumption),
				numTotalInSourced, numTotalOutSourced, MachineManager.getTotalResizeCalls(), totalDelayedApps, 
				numTotalStarted, numTotalStopped);
	}

	protected abstract String getSchedulerType();

	public static long getCurrentTime() {
		return currTime;
	}

}
