package elevator;

import java.io.IOException;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import common.Motor;
import common.Direction;
import common.ErrorType;
import common.LoggerUtil;
import communication.MessageSender;
import communication.Port;
import communication.messages.ConfirmInitializeMessage;
import communication.messages.ElevatorActionMessage;
import communication.messages.ElevatorMoveMessage;
import communication.messages.ForceActionConfirmedMessage;
import communication.messages.ElevatorErrorOccuredMessage;
import communication.messages.InitializeMessage;
import communication.messages.ConfirmInitializeMessage.Subsystem;
import elevator.Elevator.Door;
import elevator.Elevator.ElevatorAction;
import error.ExceptionUtil;
import error.UnknownAddressException;
import floor.input.InputEvent;

/**
 * ElevatorList is the elevator subsystem
 */
public class ElevatorSubsystem {
	private static final Logger logger = LoggerUtil.getLoggerForClass(ElevatorSubsystem.class);
	private List<Elevator> elevators;
	private ElevatorSensor elevatorSensor;
	private ElevatorRequestFloorQueue elevatorFloorRequestQueue;
	private Map<Integer, List<Direction>> directionButtonMap;
	private MessageSender messageSender;
	private long doorOperationDelayMs;

	/**
	 * Create an elevator subsystem
	 *
	 * @throws SocketException if the message sender could not be initialized
	 */
	public ElevatorSubsystem() throws SocketException {
		this.elevators = new ArrayList<>();
		this.messageSender = new MessageSender();
		this.elevatorSensor = new ElevatorSensor(messageSender);
		directionButtonMap = new HashMap<>();
	}

	/**
	 * Set up the system
	 *
	 * @param msg initialize message containing number of floors and elevators
	 */
	public void initializeSystem(InitializeMessage msg) {
		int numFloors = msg.getNumFloors();
		doorOperationDelayMs = msg.getDoorOperationDelay();
		elevatorSensor.setFloorMoveTimeMs(msg.getFloorMoveTime());

		elevators = new ArrayList<>();
		for (int i = 1; i <= msg.getNumElevators(); i++) {
			elevators.add(new Elevator(i, numFloors));
		}

		for (int i = 1; i <= numFloors; i++) {
			directionButtonMap.put(i, new ArrayList<>());
		}

		elevatorFloorRequestQueue = new ElevatorRequestFloorQueue(numFloors);

		sendConfirmInitializeMessage();
	}

	private void sendConfirmInitializeMessage() {
		try {
			messageSender.sendMessage(new ConfirmInitializeMessage(Subsystem.ELEVATOR),
					Port.SCHEDULER);
		} catch (IOException | UnknownAddressException e) {
			ExceptionUtil.handleRecoverableException(e, "Could not send confirm initialize message to scheduler",
					logger);
		}
	}

	protected ElevatorSensor getElevatorSensor() {
		return elevatorSensor;
	}

	public Elevator getElevatorById(int elevatorId) {
		return elevators.get(elevatorId - 1);
	}

	/**
	 * Send the message to the proper floor
	 *
	 * @param message the message to send
	 */
	public synchronized void handleMessage(ElevatorActionMessage message) {
		ElevatorAction action = message.getAction();
		Elevator selectedElevator = getElevatorById(message.getElevatorId());
		executeAction(message);
		if (elevatorStateChanged(action, message.getInputEvent())) {
			printElevator(selectedElevator);
		}
	}

	private boolean elevatorStateChanged(ElevatorAction action, InputEvent event) {
		// false when we receive a message and there is no state change
		return action != ElevatorAction.ADD_DESTINATION_FLOOR || event == null;
	}

	private void printElevator(Elevator elevator) {
		String elevatorState = "elevatorId=" + elevator.getElevatorId() + ", currentFloor=" + elevator.getCurrentFloor()
				+ ", elevatorMotor=" + elevator.getMotorStatus().toString() + ", Door=" + elevator.getDoor()
				+ ", lamp on=" + elevator.getLightsToString();

		String buttons = elevator.getButtonsToString();
		if (buttons != null) {
			elevatorState += ", buttons on=" + buttons;
		}

		logger.info("Current Elevator State: " + elevatorState);
	}

	/**
	 * When we arrive at a floor, the elevator needs to decide if it should take in
	 * a request, and if so then which one. Either both buttons, one or no button
	 * was pressed at this floor.
	 *
	 * If no button was pressed, we return null;
	 *
	 * If both buttons are pressed at this floor then: Check the elevator direction
	 * list to see what direction the target floor is and go in that direction.
	 *
	 * If a single button is pressed then we first see if we have a destination in
	 * the same direction as the motor, if so then go that direction. If not, then
	 * we check if we have destinations in the other direction, and can change
	 * direction.
	 *
	 * @param targetFloor current floor
	 * @param elevator    selected elevator
	 * @return direction the elevator should move
	 */
	private Direction getDirectionFromFloor(int targetFloor, Elevator elevator) {
		Motor direction = elevator.getMotorStatus();
		List<Direction> directionList = directionButtonMap.get(targetFloor);
		Direction removedDirection = null;
		if (directionList.size() == 2) {
			removedDirection = elevator.containsDestinationFloor(Direction.UP, targetFloor) ? Direction.UP
					: Direction.DOWN;
		} else if (!directionList.isEmpty()) {
			if (direction == Motor.UP) {
				if ((elevatorFloorRequestQueue.checkNextFloorRequest(targetFloor, Direction.UP)
						|| (!elevator.getDestinationFloors(Direction.DOWN).isEmpty()
								&& targetFloor == Collections.max(elevator.getDestinationFloors(Direction.DOWN))))) {
					removedDirection = directionList.get(0);
				}
			} else {
				if (elevatorFloorRequestQueue.checkNextFloorRequest(targetFloor, Direction.DOWN)
						|| (!elevator.getDestinationFloors(Direction.UP).isEmpty()
								&& targetFloor == Collections.min(elevator.getDestinationFloors(Direction.UP)))) {
					removedDirection = directionList.get(0);
				}
			}
		}
		if (removedDirection != null) {
			directionList.remove(removedDirection);
		}
		return removedDirection;
	}

	private void executeAction(ElevatorActionMessage message) {
		int elevatorId = message.getElevatorId();
		int targetFloor = message.getRequestedFloor();
		ElevatorAction action = message.getAction();
		InputEvent inputEvent = message.getInputEvent();
		Elevator selectedElevator = getElevatorById(elevatorId);

		logger.fine("Servicing elevator action " + action);
		Set<Integer> nextFloorRequests;

		if (inputEvent != null) {
			selectedElevator.setError(inputEvent.getErrorType(), inputEvent.getErrorFloor());
		}

		switch (action) {
			case ADD_AND_ARRIVE_AT_DESTINATION:
				addFloorToRequestQueue(inputEvent);
				nextFloorRequests = elevatorFloorRequestQueue.getNextFloorRequests(targetFloor,
						getDirectionFromFloor(targetFloor, selectedElevator));
				sendElevatorMoveRequest(selectedElevator, nextFloorRequests);
				break;
			case ARRIVED_AT_DESTINATION:
				handleArrive(targetFloor, selectedElevator);
				break;
			case MOVE_UP:
				moveSelectedElevator(selectedElevator, Motor.UP);
				break;
			case MOVE_DOWN:
				moveSelectedElevator(selectedElevator, Motor.DOWN);
				break;
			case ADD_DESTINATION_FLOOR:
				if (inputEvent != null) {
					addFloorToRequestQueue(inputEvent);
					selectedElevator.addDestinationFloor(targetFloor, inputEvent.getDirectionButton());
				} else {
					// request coming from inside elevator so turn on the button lamp
					selectedElevator.addDestinationFloor(targetFloor, null);
					selectedElevator.turnElevatorButtonOn(targetFloor);
				}
				break;
			case CONTINUE_MOVING:
				updateCurrentFloor(targetFloor, selectedElevator);
				// continue moving in the same direction
				moveSelectedElevator(selectedElevator, selectedElevator.getMotorStatus());
				break;
			case FORCE_OPEN:
				selectedElevator.openDoors();
				sendForceActionConfirmedMessage(selectedElevator.getElevatorId(), ElevatorAction.FORCE_OPEN);
				break;
			case FORCE_CLOSE:
				selectedElevator.closeDoors();
				sendForceActionConfirmedMessage(selectedElevator.getElevatorId(), ElevatorAction.FORCE_CLOSE);
				break;
			default:
				break;
		}
	}

	private void sendForceActionConfirmedMessage(int elevatorId, ElevatorAction action) {
		try {
			messageSender.sendMessage(new ForceActionConfirmedMessage(elevatorId, action), Port.SCHEDULER);
		} catch (IOException | UnknownAddressException e) {
			ExceptionUtil.handleCriticalException(e,
					"A critical exception occured while sending a forced doors action confirmation message", logger);
		}
	}

	private void handleArrive(int targetFloor, Elevator selectedElevator) {
		// update current floor, then stop motor and open doors
		updateCurrentFloor(targetFloor, selectedElevator);
		if (!performDoorOperation(Door.OPEN, selectedElevator)) {
			return;
		}

		Set<Integer> nextFloorRequests = elevatorFloorRequestQueue.getNextFloorRequests(targetFloor,
						getDirectionFromFloor(targetFloor, selectedElevator));
		selectedElevator.removeDestinationFloor(targetFloor);
		selectedElevator.setMotorStatus(Motor.STOPPED);

		sendElevatorMoveRequest(selectedElevator, nextFloorRequests);
	}

	private void addFloorToRequestQueue(InputEvent inputEvent) {
		int inputFloor;
		Direction directionButton;

		inputFloor = inputEvent.getInputFloor();
		directionButton = inputEvent.getDirectionButton();
		if (!directionButtonMap.get(inputFloor).contains(directionButton)) {
			directionButtonMap.get(inputFloor).add(directionButton);
		}
		// add the floor that the "user" will request once the elevator reaches their
		// floor to the request queue
		elevatorFloorRequestQueue.addRequest(inputFloor, directionButton, inputEvent.getCarButton());
	}

	private void updateCurrentFloor(int targetFloor, Elevator selectedElevator) {
		selectedElevator.setCurrentFloor(targetFloor);
	}

	private void moveSelectedElevator(Elevator selectedElevator, Motor motorDirection) {
		if (selectedElevator.getDoor() == Door.OPEN) {
			if (!performDoorOperation(Door.CLOSED, selectedElevator)) {
				return;
			}
		}

		int currentFloor = selectedElevator.getCurrentFloor();
		int targetFloor = motorDirection == Motor.UP ? currentFloor + 1 : currentFloor - 1;

		selectedElevator.setMotorStatus(motorDirection);
		elevatorSensor.handleMovementStarted(selectedElevator, targetFloor);
	}

	private void sendElevatorMoveRequest(Elevator elevator, Set<Integer> requestFloors) {
		if (requestFloors != null) {
			requestFloors.forEach(requestFloor -> {
				logger.fine("elevatorId = " + elevator.getElevatorId() + " Sending input from inside of elevator: "
						+ requestFloor);

				try {
					messageSender.sendMessage(new ElevatorMoveMessage(requestFloor, elevator.getElevatorId()),
							Port.SCHEDULER);
				} catch (IOException | UnknownAddressException e) {
					ExceptionUtil.handleRecoverableException(e, "Could not send input request from inside elevator",
							logger);
				}
			});
		} else {
			logger.fine("elevatorId = " + elevator.getElevatorId()
					+ " No inputs inside, sending confirmation to scheduler.");

			try {
				messageSender.sendMessage(
						new ElevatorMoveMessage(ElevatorMoveMessage.NO_FLOOR_REQUESTS, elevator.getElevatorId()),
						Port.SCHEDULER);
			} catch (IOException | UnknownAddressException e) {
				ExceptionUtil.handleRecoverableException(e, "Could not send input request from inside elevator",
						logger);
			}
		}
	}

	private boolean performDoorOperation(Door doorState, Elevator elevator) {
		logger.fine("Changing door state to: " + doorState + " for elevator: " + elevator.getElevatorId());

		try {
			Thread.sleep(doorOperationDelayMs);

			if (doorState == Door.OPEN) {
				if (elevator.getErrorForFloor(elevator.getCurrentFloor()) == ErrorType.STUCK_CLOSE) {
					// prevent the elevator from moving and notify scheduler
					sendAndRemoveError(elevator, elevator.getCurrentFloor());
					return false;
				} else {
					elevator.openDoors();
				}
			} else {
				if (elevator.getErrorForFloor(elevator.getCurrentFloor()) == ErrorType.STUCK_OPEN) {
					// prevent the elevator from moving and notify scheduler
					sendAndRemoveError(elevator, elevator.getCurrentFloor());
					return false;
				} else {
					elevator.closeDoors();
				}
			}
		} catch (InterruptedException e) {
			ExceptionUtil.handleRecoverableException(e, "Interrupt occured while doors were " + doorState, logger);
			Thread.currentThread().interrupt();
		}
		return true;
	}

	private void sendAndRemoveError(Elevator elevator, int floor) {
		try {
			ElevatorErrorOccuredMessage errorMessage = new ElevatorErrorOccuredMessage(elevator.getErrorForFloor(floor),
					floor, elevator.getElevatorId());
			logger.severe("Error occured: " + errorMessage);
			elevator.removeError(elevator.getCurrentFloor());
			messageSender.sendMessage(errorMessage, Port.SCHEDULER);
		} catch (IOException | UnknownAddressException e) {
			ExceptionUtil.handleRecoverableException(e, "Could not send error message", logger);
		}
	}

	protected void setDoorOperationDelayMs(long delay) {
		this.doorOperationDelayMs = delay;
	}
}
