package elevator;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;

import common.Direction;
import common.ErrorType;
import common.Motor;

/**
 * Elevator class
 *
 * Consists:
 * Floor buttons: receive this info from scheduler for indicating the selected floor/ destination
 * Floor lamps: indicate the floor(s) which will be visited by the elevator
 * Motor: controlled by scheduler
 * Door: controlled by scheduler
 *
 */
public class Elevator {

	private int currentFloor;
	private int elevatorId;
	private Map<Direction, List<Integer>> destinationFloors;
	private Motor motor;
	private Light[] elevatorLamps;
	private Light[] elevatorButtons;
	private Door door;
	private Map<Integer, ErrorType> errorsPerFloor;
	private static final int STARTING_FLOOR = 1;

	/**
	 * Creating elevator
	 *
	 * @param elevatorId
	 * @param numFloors
	 */
	public Elevator(int elevatorId, int numFloors) {
		this.elevatorId = elevatorId;
		this.elevatorLamps = new Light[numFloors];
		this.elevatorButtons = new Light[numFloors];
		for (int i = 0; i < numFloors; i++) {
			elevatorButtons[i] = Light.OFF;
			elevatorLamps[i] = i == STARTING_FLOOR - 1 ? Light.ON : Light.OFF; // elevators start on the first floor
		}
		this.door = Door.CLOSED;
		this.currentFloor = STARTING_FLOOR;
		this.destinationFloors = new EnumMap<>(Direction.class);
		destinationFloors.put(Direction.UP, new ArrayList<>());
		destinationFloors.put(Direction.DOWN, new ArrayList<>());
		this.motor = Motor.STOPPED;
		errorsPerFloor = new HashMap<>();
	}

	public int getCurrentFloor() {
		return currentFloor;
	}

	/**
	 * set currentFloor when it reaches to that floor
	 * turn on lamp on that floor and turn off lamp of prev floor
	 *
	 * @param currentFloor
	 */
	public void setCurrentFloor(int currentFloor) {
		this.currentFloor = currentFloor;
		if (this.motor == Motor.UP) {
			this.elevatorLamps[currentFloor - 2] = Light.OFF;
			this.elevatorLamps[currentFloor - 1] = Light.ON;
		} else {
			this.elevatorLamps[currentFloor] = Light.OFF;
			this.elevatorLamps[currentFloor - 1] = Light.ON;
		}
	}

	/**
	 * Add floor to the destinations in the specified direction
	 * @param floor
	 * @param direction
	 */
	public void addDestinationFloor(int floor, Direction direction) {
		if (direction == null) {
			direction = floor < currentFloor ? Direction.DOWN : Direction.UP;
		}
		this.destinationFloors.get(direction).add(floor);
	}

	/**
	 * turn on elevator button
	 * @param floor
	 */
	public void turnElevatorButtonOn(int floor) {
		this.elevatorButtons[floor - 1] = Light.ON;
	}

	/**
	 * Turn off the button at the destination floor as indicated and remove
	 * the floor from the destinations
	 * @param floor
	 */
	public void removeDestinationFloor(int floor) {
		Direction directionToRemove;
		if (containsDestinationFloor(Direction.UP, floor) && containsDestinationFloor(Direction.DOWN, floor)) {
			// remove in the direction of the motor
			directionToRemove = Direction.mapDirection(motor);
		} else {
			// the destination floor should be in one of the two directions
			directionToRemove = containsDestinationFloor(Direction.UP, floor) ? Direction.UP : Direction.DOWN;
		}
		destinationFloors.get(directionToRemove).remove(Integer.valueOf(floor));
		this.elevatorButtons[floor - 1] = Light.OFF;
	}

	/**
	 * Check if the specified floor is in the destinations for the
	 * specified direction
	 *
	 * @param direction
	 * @param floor
	 * @return true if the floor is in the destinations
	 */
	public boolean containsDestinationFloor(Direction direction, int floor) {
		return destinationFloors.get(direction).contains(floor);
	}

	public Light getElevatorButton(int floor) {
		return elevatorButtons[floor - 1];
	}

	public List<Integer> getDestinationFloors(Direction direction) {
		return destinationFloors.get(direction);
	}

	public int getElevatorId() {
		return elevatorId;
	}

	public Motor getMotorStatus() {
		return motor;
	}

	public void setMotorStatus(Motor motor) {
		this.motor = motor;
	}

	public void openDoors() {
		this.door = Door.OPEN;
	}

	public void closeDoors() {
		this.door = Door.CLOSED;
	}

	public boolean isDoorClosed() {
		return this.door == Door.CLOSED;
	}

	public Door getDoor() {
		return this.door;
	}

	/**
	 * Set the error parameters will occur for this elevator
	 * @param errorType
	 * @param errorFloor
	 */
	public void setError(ErrorType errorType, int errorFloor) {
		errorsPerFloor.put(errorFloor, errorType);
	}

	/**
	 * Get the error that occurs on this floor
	 * @param floor
	 * @return the error that occurs on this floor, null if none
	 */
	public ErrorType getErrorForFloor(int floor) {
		return errorsPerFloor.get(floor);
	}

	public void removeError(int errorFloor) {
		errorsPerFloor.remove(errorFloor);
	}

	/**
	 * Actions that can be performed in an ElevatorActionMessage
	 */
	public enum ElevatorAction {
		ARRIVED_AT_DESTINATION,
		MOVE_UP,
		MOVE_DOWN,
		CONTINUE_MOVING,
		ADD_DESTINATION_FLOOR,
		ADD_AND_ARRIVE_AT_DESTINATION,
		FORCE_OPEN,
		FORCE_CLOSE
	}

	/**
	 * If a light is on or off for elevatorLamps and elevatorButtons
	 */
	public enum Light {
		ON,
		OFF
	}

	/**
	 * Represents the state of the door
	 */
	public enum Door {
		OPEN,
		CLOSED
	}

	/**
	 *
	 * @return string representation of the floors at which the elevator lights are on
	 */
	public String getLightsToString() {
		return Arrays.toString(IntStream.range(0 , elevatorLamps.length)
			.filter(i -> elevatorLamps[i] == Light.ON).map(index -> index + 1).toArray());
	}

	/**
	 * @return a string representation of the floors at which the elevator buttons are on.
	 * If there are no buttons on, then null is returned
	 */
	public String getButtonsToString() {
		int[] buttons = IntStream.range(0 , elevatorButtons.length)
			.filter(i -> elevatorButtons[i] == Light.ON).map(index -> index + 1).toArray();
		return buttons.length > 0 ? Arrays.toString(buttons) : null;
	}
}
