package scheduler;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Timer;
import java.util.TreeMap;
import java.util.logging.Logger;

import common.Direction;
import common.LoggerUtil;
import common.Motor;
import error.ExceptionUtil;

/**
 * This class represents the metadata for an elevator. It tracks an elevator's
 * position and destinations
 */
public class MetaElevator {
	private static final Logger logger = LoggerUtil.getLoggerForClass(MetaElevator.class);
	private int elevatorId;
	private int currentFloor;
	private boolean doorsOpen;
	private Timer timer;
	/**
	 * Not the real state of the motor, it's STOPPED only if it's idle, otherwise
	 * it's the direction it's currently servicing
	 */
	private Motor motor;
	/**
	 * Used so that there is a history of the direction the elevator was going in
	 */
	private Motor prevMotor;
	private Map<Direction, TreeMap<Integer, Boolean>> destinationFloors;
	
	private SystemGUI systemGUI;

	public MetaElevator(int elevatorId, int currentFloor) {
		this.elevatorId = elevatorId;
		this.currentFloor = currentFloor;
		this.doorsOpen = false;
		this.motor = Motor.STOPPED;
		this.prevMotor = Motor.STOPPED;

		this.destinationFloors = new EnumMap<>(Direction.class);
		destinationFloors.put(Direction.UP, new TreeMap<>());
		// automatically put in descending order for DOWN direction
		destinationFloors.put(Direction.DOWN, new TreeMap<>(Collections.reverseOrder()));
		timer = new Timer(String.valueOf(elevatorId));
		
		systemGUI = SystemGUI.getInstance();
	}

	public Motor getMotorStatus() {
		return motor;
	}

	public void setMotorStatus(Motor motor) {
		this.prevMotor = this.motor;
		this.motor = motor;
		systemGUI.updateElevatorStatus(elevatorId, motor.toString());
	}

	private Motor getNonStoppedMotorStatus() {
		return motor != Motor.STOPPED ? motor : prevMotor;
	}

	public int getElevatorId() {
		return elevatorId;
	}

	public void setElevatorId(int elevatorId) {
		this.elevatorId = elevatorId;
	}

	public int getCurrentFloor() {
		return currentFloor;
	}

	public void setCurrentFloor(int currentFloor) {
		this.currentFloor = currentFloor;
		systemGUI.updateElevatorLocation(elevatorId, currentFloor);
	}

	public boolean isDoorsOpen() {
		return doorsOpen;
	}

	public void setDoorsOpen(boolean doorsOpen) {
		this.doorsOpen = doorsOpen;
	}

	public boolean hasDestinations() {
		return hasDestinations(Direction.UP) || hasDestinations(Direction.DOWN);
	}

	public boolean hasDestinations(Direction direction) {
		return !destinationFloors.get(direction).isEmpty();
	}

	/**
	 * Adds destination floor to the specified direction's list
	 *
	 * @param destinationFloor destination floor to add
	 * @param direction        direction to add the floor to
	 */
	public synchronized void addDestinationFloor(int destinationFloor, Direction direction) {
		Direction directionToAdd = direction;
		boolean isFromInside = false;
		// if direction is null then we are adding an input from inside elevator
		if (direction == null) {
			// calculate the direction based on the current floor and floor to go to
			directionToAdd = currentFloor > destinationFloor ? Direction.DOWN : Direction.UP;
			isFromInside = true;
		}

		Map<Integer, Boolean> destinationList = destinationFloors.get(directionToAdd);
		if (destinationList.containsKey(destinationFloor)) {
			return;
		}

		if (needToWait(destinationFloor, currentFloor, direction)) {
			// while there's destinations in the other direction keep waiting
			while (!hasDestinations(direction == Direction.UP ? Direction.DOWN : Direction.UP)) {
				try {
					wait();
				} catch (InterruptedException e) {
					ExceptionUtil.handleRecoverableException(e, "Interrupted wait", logger);
					Thread.currentThread().interrupt();
				}
			}
		}
		destinationList.put(destinationFloor, isFromInside);
		
		systemGUI.updateElevatorDestUp(elevatorId, destinationFloors.get(Direction.UP).keySet());
		systemGUI.updateElevatorDestDown(elevatorId, destinationFloors.get(Direction.DOWN).keySet());
	}

	private boolean needToWait(int destinationFloor, int currentFloor, Direction direction) {
		if (direction == Direction.UP) {
			if (hasDestinations(Direction.DOWN)) {
				if (destinationFloor < currentFloor) {
					return true;
				}
			}
 		} else {
			if (hasDestinations(Direction.UP)) {
				if (destinationFloor > currentFloor) {
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * Remove the next destination floor that the elevator visits
	 */
	public synchronized void removeDestinationFloor() {
		Motor motorStatus = getNonStoppedMotorStatus();
		Direction removedDirection;
		// check if there is a next destination in the direction the elevator is moving
		if (hasDestinations(Direction.mapDirection(motorStatus))) {
			removedDirection = Direction.mapDirection(motorStatus);
			removeNextDestination(removedDirection);
		} else {
			// no destinations in same direction so get destination from other direction
			if (motorStatus == Motor.UP) {
				removedDirection = Direction.DOWN;
			} else {
				removedDirection = Direction.UP;
			}
			removeNextDestination(removedDirection);
		}

		if (!hasDestinations(removedDirection)) {
			// now safe to add destinations in this directions if there are any waiting to add
			notifyAll();
		}

		systemGUI.updateDestinationFloors(elevatorId, destinationFloors);
	}

	/**
	 * Check if the destination exists in the specified direction list
	 *
	 * @param destination
	 * @param direction
	 * @return true if destination is in the list, otherwise false
	 */
	public boolean checkDestination(int destination, Direction direction) {
		return destinationFloors.get(direction).containsKey(destination);
	}

	/**
	 * Get the next destination the the elevator should visit. Will get the destination
	 * from the same direction as the motor if there are any otherwise, get from
	 * other direction
	 * @return the next destination to visit
	 */
	public int getNextDestination() {
		// if motor is stopped use the motor direction from before elevator was stopped
		Motor motorStatus = getNonStoppedMotorStatus();

		// check if there is a next destination in the direction the elevator is moving
		if (hasDestinations(Direction.mapDirection(motorStatus))) {
			return destinationFloors.get(Direction.mapDirection(motorStatus)).firstKey();
		}

		// no destinations in same direction so get destination from other direction
		return destinationFloors.get(motorStatus == Motor.UP ? Direction.DOWN : Direction.UP).firstKey();
	}

	/**
	 * Get the next destination the the elevator should visit. Will get the destination
	 * from the same direction as the motor if there are any otherwise, get from
	 * other direction
	 *
	 * @param defaultFloor default floor to visit if the elevator has not moved ever
	 * @return next destination to visit
	 */
	public int getNextDestination(int defaultFloor) {
		Motor motorStatus = getNonStoppedMotorStatus();
		if (motorStatus == Motor.STOPPED) {
			// case where the elevator has never moved before and is servicing its first request
			// the floor passed in should be its first destination
			return defaultFloor;
		}

		return getNextDestination();
	}

	/**
	 * Returns the first destination for this elevator in a given Direction
	 *
	 * @param direction
	 * @return the next destination
	 */
	public int getNextDestination(Direction direction) {
		return destinationFloors.get(direction).firstKey();
	}

	public List<Integer> getDestinations(Direction direction) {
		return new ArrayList<>(destinationFloors.get(direction).keySet());
	}

	public Integer getLastDestination(Direction direction) {
		return destinationFloors.get(direction).lastKey();
	}

	public int getTotalNumberOfDestinations() {
		return destinationFloors.get(Direction.UP).size() + destinationFloors.get(Direction.DOWN).size();
	}

	protected void clearDestinations() {
		clearDestinations(Direction.UP);
		clearDestinations(Direction.DOWN);
	}

	protected void clearDestinations(Direction direction) {
		destinationFloors.get(direction).clear();
	}

	private void removeNextDestination(Direction removedDirection) {
		Integer nextDestination = destinationFloors.get(removedDirection).firstKey();
		destinationFloors.get(removedDirection).remove(nextDestination);
	}

	/**
	 * Cancel the timer
	 */
	public void cancelTimer() {
		timer.cancel();
		timer = new Timer(String.valueOf(elevatorId));
	}

	/**
	 * Start the timer
	 */
	public void startTimer(long delay) {
		timer.schedule(new ElevatorTimerTask(this), delay);
	}

	/**
	 * Gets the destinations that can be rerouted - the ones that aren't
	 * from the passengers inside the elevator
	 */
	public Map<Integer, Direction> getDestinationsForReroute() {
		Map<Integer, Direction> rerouteFloors = new HashMap<>();
		destinationFloors.forEach((direction, destFloors) -> 
			destFloors.forEach((floor, isFromInside) -> {
				if (!isFromInside) {
					rerouteFloors.put(floor, direction);
				}
			})
		);
		return rerouteFloors;
	}
}
