package lift;

enum Direction {
  UP,
  DOWN,
}

/** Lift monitor. Handles lift motion and various state. */
public class LiftMonitor {
  /** Number of floors */
  private int nfloors;
  /** number of passengers waiting to enter the lift at the various floors */
  private PassengerQueue waitEntry;
  /** number of passengers (in lift) waiting to leave at the various floors */
  private PassengerQueue waitExit;
  /** view object passed from main */
  private LiftView view;
  /** State (open/closed) of door on current floor */
  private Door door;
  /** Counter of number of passengers currently entering the elevator */
  private int passengersEntering;
  /** Counter of number of passengers currently exiting the elevator */
  private int passengersExiting;
  /** Lift state */
  private LiftState liftState;

  /** Create new LiftMonitor */
  public LiftMonitor(LiftView v, int floors) {
    nfloors = floors;
    door = new Door();
    waitEntry = new PassengerQueue(nfloors);
    waitExit = new PassengerQueue(nfloors);
    view = v;
    passengersEntering = 0;
    passengersExiting = 0;
    liftState = new LiftState(0, Direction.UP);
  }

  /** Increases the number of waiting passengers on given floor */
  public synchronized void increaseWaitEntry(int passengerFloor) {
    waitEntry.inc(passengerFloor);
    notifyAll();
  }

  /** Handles passengers waiting to enter arriving lift */
  public synchronized void enterLift(int passengerFloor,
                                     int passengerDestination) {
    while (liftState.getFloor() != passengerFloor || liftState.getLoad() == 4 ||
           liftState.isMoving() || passengersEntering == 4) {
      try {
        wait();
      } catch (InterruptedException e) {
        throw new Error("Monitor.enterLift interrupted " + e);
      }
    }

    waitEntry.dec(
        liftState.getFloor()); // In practice, passengerFloor == floor here
    waitExit.inc(passengerDestination);
    liftState.incLoad();
    passengersEntering++;
    notifyAll();
  }

  /** Called after passengers has completed entry animation */
  public synchronized void enterCompleted() {
    passengersEntering--;
    notifyAll();
  }

  /** Handles passengers waiting to exit lift */
  public synchronized void exitLift(int passengerDestination) {
    while (liftState.getFloor() != passengerDestination) {
      try {
        wait();
      } catch (InterruptedException e) {
        throw new Error("Monitor.exitLift interrupted " + e);
      }
    }
    waitExit.dec(passengerDestination);
    liftState.decLoad();
    passengersExiting++;
    notifyAll();
  }

  /** Called after passengers has completed exit animation */
  public synchronized void exitCompleted() {
    passengersExiting--;
    notifyAll();
  }

  /**
   * Handles the conditions when the elevator is to wait for passengers
   * entering/exiting the lift and waiting when there are no passengers waiting
   * on any floor.
   */
  public synchronized int[] liftContinue() {
    while (waitEntry.isEmpty() && waitExit.isEmpty()) {
      try {
        wait();
      } catch (InterruptedException e) {
        throw new Error("LiftContinue no passengers interrupted " + e);
      }
    }

    /** Stops the elevator to allow passengers to enter or exit */
    while (waitEntry.get(liftState.getFloor()) > 0 &&
               liftState.getLoad() != 4 ||
           waitExit.get(liftState.getFloor()) > 0 || passengersEntering > 0 ||
           passengersExiting > 0) {
      if (!door.isOpen()) {
        view.openDoors(liftState.getFloor());
        door.open();
      }
      liftState.setMoving(false);
      notifyAll();
      try {
        wait();
      } catch (InterruptedException e) {
        throw new Error("Monitor.liftContinue interrupted " + e);
      }
    }

    if (!liftState.isMoving())
      view.closeDoors();

    if (door.isOpen())
      door.close();

    liftState.setMoving(true);

    calculateDirection();

    /**
     * Sends the elevators current position and next position to the LiftThread
     */
    int[] movingPositions = new int[2];
    movingPositions[0] = liftState.getFloor();
    movingPositions[1] = liftState.getFloor() +
                         (liftState.getDirection() == Direction.UP ? 1 : -1);
    return movingPositions;
  }

  /** Moves the elevator in the given direction */
  public synchronized void incrementFloor() {
    liftState.setFloor(liftState.getFloor() +
                       (liftState.getDirection() == Direction.UP ? 1 : -1));
  }

  /**
   * Handles calculation of elevator direction
   * Changes the direction of the elevator if there are no passengers that wants
   * to enter or exit on floors either above or below the elevator
   */
  private void calculateDirection() {
    int floor = liftState.getFloor();
    boolean noPassengersAbove =
        (waitEntry.totalAbove(floor) == 0 && waitExit.totalAbove(floor) == 0);
    boolean noPassengersBelow =
        (waitEntry.totalBelow(floor) == 0 && waitExit.totalBelow(floor) == 0);

    if (floor == 0 || noPassengersBelow) {
      liftState.setDirection(
          Direction
              .UP); // Always go up if at ground floor or no passengers below
    } else if (floor == nfloors - 1 || noPassengersAbove) {
      liftState.setDirection(Direction.DOWN); // Always go down if at the top
                                              // floor or no passengers above
    }
  }
}
