package scheduler;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import common.Direction;
import common.ErrorType;
import common.LoggerUtil;
import common.Util;
import communication.MessageSender;
import communication.Port;
import communication.messages.ConfirmInitializeMessage;
import communication.messages.ElevatorActionMessage;
import communication.messages.ElevatorErrorOccuredMessage;
import communication.messages.ElevatorSensorMessage;
import communication.messages.ForceActionConfirmedMessage;
import communication.messages.InitializeMessage;
import communication.messages.StartEventsMessage;
import communication.messages.ConfirmInitializeMessage.Subsystem;
import elevator.Elevator.ElevatorAction;
import error.ExceptionUtil;
import error.UnknownAddressException;

/**
 * This singleton class takes care of ordering SchedulerTasks and sends a
 * message to the elevators telling it where to go
 */
public class Scheduler implements Runnable {
	private static final Logger logger = LoggerUtil.getLoggerForClass(Scheduler.class);
	private static final String CONFIG_FILE_PATH = "data/config";
	private static Scheduler instance;
	private static List<MetaElevator> elevators = new ArrayList<>();
	private static List<MetaElevator> outOfServiceElevators = new ArrayList<>();
	private Queue<SchedulerTask> schedule;
	private MessageSender messageSender;
	private static long floorDelayTime;
	private static boolean calculatePerformance = true;
	private static Set<Subsystem> initializedSystems = new HashSet<>();
	private SystemGUI systemGUI;

	private Scheduler() {
		schedule = new ConcurrentLinkedQueue<>();
		try {
			messageSender = new MessageSender();
		} catch (SocketException e) {
			ExceptionUtil.handleCriticalException(e, "Could not create sending socket", logger);
		}
	}

	/**
	 * Return an instance of the scheduler, create one if there isn't one already
	 *
	 * @return the only instance of the scheduler
	 * @throws SocketException if an exception occured while initing the message
	 *                         sender
	 */
	public static synchronized Scheduler getInstance() {
		if (instance == null) {
			instance = new Scheduler();
		}
		return instance;
	}

	private void initMetaElevators(int numberOfElevators) {
		for (int i = 1; i <= numberOfElevators; i++) {
			elevators.add(new MetaElevator(i, 1));
		}
	}

	/**
	 * Get the first available idle elevator
	 *
	 * @return the first available idle elevator
	 */
	public MetaElevator getIdleElevator() {
		MetaElevator idle = null;
		do {
			synchronized (elevators) {
				List<MetaElevator> idleElevators = elevators.stream().filter(e -> e.getTotalNumberOfDestinations() == 0)
						.collect(Collectors.toList());
				if (!idleElevators.isEmpty()) {
					idle = idleElevators.get(0);
				}
			}
			try {
				Thread.sleep(2000);
			} catch (InterruptedException e1) {
				ExceptionUtil.handleRecoverableException(e1, "Interrupted while waiting for idle elevators", logger);
				Thread.currentThread().interrupt();
			}
		} while (idle == null);
		return idle;
	}

	/**
	 * Get the best elevator for the request floor.
	 *
	 * @param requestFloor
	 * @param direction    the direction the request will go in after
	 * @return the best elevator for the request
	 */
	public synchronized MetaElevator getElevatorForRequest(int requestFloor, Direction direction) {
		MetaElevator elevatorForRequest = null;

		if (elevators == null || elevators.isEmpty()) {
			ExceptionUtil.handleCriticalException(new Exception(), "No working elevators in the system");
		}
		// Check if the requestFloor will already be serviced by an elevator
		elevatorForRequest = elevators.stream().filter(e -> e.checkDestination(requestFloor, direction)).findFirst()
				.orElse(null);
		if (elevatorForRequest != null) {
			return elevatorForRequest;
		}

		boolean allElevatorsIdle = elevators.stream().allMatch(e -> e.getTotalNumberOfDestinations() == 0);
		List<MetaElevator> idleElevators = elevators.stream().filter(e -> e.getTotalNumberOfDestinations() == 0)
				.collect(Collectors.toList());
		List<MetaElevator> passingElevators = null;

		// check if all the elevators are idle
		if (allElevatorsIdle) {
			elevatorForRequest = SchedulerUtil.getClosestElevatorToFloor(elevators, requestFloor);
		} else {
			// check if an elevator will pass by the request floor
			passingElevators = SchedulerUtil.getElevatorsPassingByFloor(elevators, requestFloor, direction);

			if (passingElevators.isEmpty()) {
				// no elevator will pass, check if there's any idle elevators
				if (!idleElevators.isEmpty()) {
					// there are idle elevators, get the closest one
					elevatorForRequest = SchedulerUtil.getClosestElevatorToFloor(idleElevators, requestFloor);
				} else {
					// no idle elevators, find elevator with the least distance to travel
					elevatorForRequest = SchedulerUtil.getElevatorWithLeastTravelDistanceToFloor(elevators,
							requestFloor, direction);
				}
			} else {
				// one or more elevators will pass, determine which to schedule
				elevatorForRequest = SchedulerUtil.getClosestElevatorToFloor(passingElevators, requestFloor);
			}
		}

		return elevatorForRequest;
	}

	/**
	 * Initialize the other subsystems
	 *
	 * @param numFloors
	 * @param numElevators
	 */
	public void initializeSubsystems() {
		logger.config("Initializing subsystems...");
		Map<String, Integer> systemConfigs = null;
		try {
			systemConfigs = Util.parseConfigFile(CONFIG_FILE_PATH, logger);
		} catch (FileNotFoundException e) {
			ExceptionUtil.handleCriticalException(e,
					"A critical error has occured while trying to read the system config file", logger);
		}

		int numElevators = systemConfigs.get("NumberOfElevators");
		int numFloors = systemConfigs.get("NumberOfFloors");
		int floorMoveTime = systemConfigs.get("FloorMoveTimeMs");
		int doorOperationDelay = systemConfigs.get("DoorOperationDelayMs");
		int performanceCalculationEndMs = systemConfigs.get("PerformanceCalculationEndMs");
		
		systemGUI = SystemGUI.createSystemGUI(numElevators, numFloors, true);

		initMetaElevators(numElevators);
		InitializeMessage msg = new InitializeMessage(numFloors, numElevators, floorMoveTime, doorOperationDelay);
		try {
			messageSender.sendMessage(msg, Port.ELEVATOR);
			messageSender.sendMessage(msg, Port.FLOOR);
		} catch (IOException | UnknownAddressException e) {
			ExceptionUtil.handleCriticalException(e, "Could not initialize subsystems", logger);
		}
		floorDelayTime = floorMoveTime + doorOperationDelay + 10000L;

		new Thread(() -> {
			try {
				Thread.sleep(performanceCalculationEndMs);
			} catch (InterruptedException e) {
				ExceptionUtil.handleRecoverableException(e, "Interrupted while waiting for calculation", logger);
				Thread.currentThread().interrupt();
			}
			setCalculatePerformance(false);
			SchedulerTimer.outputSchedulerPerformance();
		}, "Performance Calculation Thread").start();
	}

	public void handleConfirmInitialize(ConfirmInitializeMessage message) {
		initializedSystems.add(message.getSubsystem());

		if (initializedSystems.size() == Subsystem.values().length) {
			try {
				messageSender.sendMessage(new StartEventsMessage(), Port.FLOOR);
			} catch (IOException | UnknownAddressException e) {
				ExceptionUtil.handleRecoverableException(e, "Could not send start events message to floor",
						logger);
			}
		}
	}

	/**
	 * Update the an elevator's current position based on what the floor sensors are
	 * reporting
	 *
	 * @param msg
	 */
	public void updateMetaElevatorPosition(ElevatorSensorMessage msg) {
		MetaElevator elevator = getMetaElevatorById(msg.getElevatorId());
		if (elevator != null) {
			elevator.setCurrentFloor(msg.getCurrentFloor());
		}
	}

	/**
	 * Remove the specified elevator from the list of elevators
	 *
	 * @param elevator
	 */
	public synchronized void removeElevator(MetaElevator elevator) {
		elevators.remove(elevator);
		logger.info("Elevator with id: " + elevator.getElevatorId() + " taken out of service.");
	}

	public synchronized void addTask(SchedulerTask task) {
		schedule.add(task);
	}

	/**
	 * This method handles the occurrence of an elevator door error
	 *
	 * @param msg
	 */
	public synchronized void handleElevatorDoorError(ElevatorErrorOccuredMessage msg) {
		MetaElevator elevator = getMetaElevatorById(msg.getElevatorId());
		int errorFloor = msg.getErrorFloor();
		ErrorType error = msg.getErrorType();

		elevators.remove(elevator);
		outOfServiceElevators.add(elevator);

		try {
			messageSender.sendMessage(new ElevatorActionMessage(elevator.getElevatorId(),
					error == ErrorType.STUCK_OPEN ? ElevatorAction.FORCE_CLOSE : ElevatorAction.FORCE_OPEN, errorFloor),
					Port.ELEVATOR);
		} catch (IOException | UnknownAddressException e) {
			ExceptionUtil.handleCriticalException(e,
					"A critical exception occured while sending a force doors action to elevator "
							+ elevator.getElevatorId(),
					logger);
		}
		
		systemGUI.updateElevatorLocationAfterError(elevator.getElevatorId(), errorFloor);
		systemGUI.updateElevatorStatus(elevator.getElevatorId(), error == ErrorType.STUCK_OPEN ? "STUCK OPEN" : "STUCK CLOSED");
	}

	/**
	 * The method is called when a door error has been fixed
	 *
	 * @param msg
	 */
	public synchronized void handleForcedDoorsActionConfirmed(ForceActionConfirmedMessage msg) {
		int elevatorId = msg.getElevatorId();
		MetaElevator elevator = outOfServiceElevators.stream().filter(e -> e.getElevatorId() == elevatorId).findFirst()
				.orElse(null);

		elevators.add(elevator);
		outOfServiceElevators.remove(elevator);

		// elevator resumes execution
		addTask(new ElevatorMovedTask(new ElevatorSensorMessage(elevator.getCurrentFloor(), elevator.getElevatorId()),
				true, msg.getAction() == ElevatorAction.FORCE_CLOSE));
	}

	@Override
	public void run() {
		while (true) {
			SchedulerTask task = schedule.poll();

			if (task != null) {
				logger.fine("Now executing task type: " + task.getClass());

				try {
					if (calculatePerformance) {
						SchedulerTimer.addSchedulerTask(task);
					}
					task.run(messageSender);
					if (calculatePerformance) {
						SchedulerTimer.setTaskComplete(task);
					}
				} catch (IOException | UnknownAddressException e) {
					ExceptionUtil.handleCriticalException(e,
							"A critical IO exception occured while running the scheduler", logger);
				}
			}
		}
	}

	/**
	 * Get the elevator with the specified elevator id
	 *
	 * @param elevatorId
	 * @return the elevator with elevatorId, null if the elevator cannot be found
	 */
	public static MetaElevator getMetaElevatorById(int elevatorId) {
		for (MetaElevator elevator : elevators) {
			if (elevator.getElevatorId() == elevatorId) {
				return elevator;
			}
		}
		return null;
	}

	public static long getFloorDelayTime() {
		return floorDelayTime;
	}

	private static void setCalculatePerformance(boolean calculatePerformance) {
		Scheduler.calculatePerformance = calculatePerformance;
	}
}
