import javax.swing.*;
import java.awt.*;
import java.io.File;
import java.io.FileNotFoundException;
//feel free to add any other imports you need here...

/**
 * Provides a virtual room for a Vroomba to clean.
 * <p>
 * When a Room is created, it must contain a Vroomba.  The Room's
 * {@link #clean()} method will then instruct that Vroomba to clean
 * the room, monitoring its progress.
 * <p>
 * The details of a room's geography are specified by a map of characters.
 * Room maps are normally read from file, but can be specified directly
 * as a 2D array of appropriate characters.  Acceptable room features
 * (walls, empty floor, etc) are listed by this class as
 * <code>char</code> constants.
 * <p>
 * Every map must include at least one square of floor space.  If the map
 * does not specify the location of the contained Vroomba, the Vroomba will
 * be placed randomly on the floor.  A map does not need to specify its
 * borders with walls; any location off the map is assumed to be a WALL.
 * <p>
 * When instructing a Vroomba to clean, a Room monitors each move of the
 * Vroomba.  It will stop the cleaning if the Vroomba should move over a DROP,
 * crash 3 consecutive times into the same obstruction, fail to move at all,
 * or exceeds the max number of moves allowed.  Max moves are determined from
 * the amount of floor to clean and the {@link #MAX_MOVES_MODIFIER}.
 *
 *
 * @author Zach Tomaszewski
 * @version 08 Nov 2008
 */
public class Room {

  //char constants (acceptable map contents)
  public static final char FLOOR = ' ';
  public static final char DIRT = '.';
  public static final char WALL = '#';
  public static final char DROP = '^';
  public static final char VROOMBA = 'V';

  /** A Vroomba must finish cleaning any room within
     <code>(MAX_MOVES_MODIFIER * this.floorSpaces)</code> moves.
     Current default: 3. */
  public static final int MAX_MOVES_MODIFIER = 3;

  //instance variables
  private char[][] room;   //the map of this room
  private int roomWidth;
  private int roomHeight;
  private int floorSpaces; //number of floor squares on the map

  private Vroomba vroomba;
  private int vroombaRow;  //current vroomba location on room map,
  private int vroombaCol;  // where top-left square of Room is (0,0)

  private int moves = 0;   //state regarding the Vroomba's recent activities
  private int collisions = 0;
  private int repeatCollisions = 0;
  private Direction lastMoveIfCollision = null; //if null, last move was not a crash
  private Direction lastMove;  //direction of last move, regardless of its result
  private Result done = null;  //will be null until cleaning is done

  //CONSTRUCTORS

  /**
   * Construct a new Room with the given map, to be cleaned by
   * the given Vroomba.
   * <p>
   * Verifies that the map is valid:
   * It must not contain any unrecoginzed chars.
   * Every row of the map must be the same length.
   * A map may not contain more than a single 'V' character,
   * which is where the given Vroomba should be initially placed.
   * If no such 'V' is found, the  Vroomba will be placed randomly
   * on the floor somewhere (on either a FLOOR or DIRTy square).
   * <p>
   * Thus, every map must contain at least one
   * VROOMBA, FLOOR, or DIRT character.
   *
   * @throws InvalidMapException  if the given map is invalid
   */
  public Room(char[][] map, Vroomba v) throws InvalidMapException {
    this.loadMap(map);
    this.vroomba = v;
  }

  /**
   * Constructs a room by loading the given file and using
   * the given Vroomba.
   * <p>
   * The contents of the file will be converted to a 2D array of characters.
   * File must exist, be readable, and contain a valid map.
   *
   * @throws FileNotFoundException  if the given file does not exist
   * @throws InvalidMapException    if file does not contain a valid map
   *
   * @see #Room(char[][], Vroomba)
   */
  public Room(String mapFilename, Vroomba v) throws FileNotFoundException,
                                                    InvalidMapException {
//IMPLEMENT THIS:
    // Open the given mapFilename file and read in its contents.
    // Construct a char[][] from the file contents, then call:

    //this.loadMap(map);  //change map here to be the name of your char[][]
    //this.vroomba = v;

    //remove this line below once you've implemented this constructor
    throw new UnsupportedOperationException("You didn't implement " +
                                            "the Room constructor yet.");
  }


  //PUBLIC methods

  /**
   * Has this room cleaned by its occupying Vroomba.
   * Will continue until the Vroomba stops, exceeds its allowed number
   * of moves, or has a mishap.  See {@link #moveVroomba} for more.
   * <p>
   * Once called, additional calls to <code>clean()</code> do nothing
   * and will return null.
   * <p>
   * Returns a transcript of the cleaning session as formed by
   * a call of {@link #getStatus()} every turn.  The transcript includes
   * only moves, however, and does not include the finished result.
   */
  public String clean() {
    if (done == null) {
      //haven't cleaned yet, so go ahead and do it
      String transcript = "";
      while (this.moveVroomba()) {
        transcript += this.getStatus();
      }
      transcript += this.getStatus(true); //get last move
      return transcript;
    }else {
      //already cleaned this room
      return null;
    }
  }

  /**
   * Returns the number of collisions this Room's Vroomba has had so far.
   */
  public int getCollision() {
    return this.collisions;
  }

  /**
   * Returns the maximum number of moves a Vroomba is allowed for this Room.
   */
  public int getMaxMoves() {
    return this.floorSpaces * MAX_MOVES_MODIFIER;
  }

  /**
   * Returns the number of moves this Room's Vroomba has currently made.
   */
  public int getMoves() {
    return this.moves;
  }

  /**
   * Returns the result of cleaning this room.  If cleaning has not
   * started or is still in progress, will return null.
   */
  public Room.Result getResult() {
    return this.done;
  }

  /**
   * Returns the current status of this Room's Vroomba's cleaning
   * performance as a single line of text.
   * <p>
   * If still cleaning, the line returned will begin with "CLEANING:"
   * and the direction of the last move.  If done, the line will instead
   * begin with "RESULT:" and the completion status.  In either case,
   * line also includes the number of moves made over the max allowed moves,
   * as well as the number of collisions.
   */
  public String getStatus() {
    return this.getStatus(false);
  }

  /**
   * Returns the current status, formatted as per {@link #getStatus()}.
   * However, if <code>asMove</code> is true, will print the status
   * as if the Vroomba were still cleaning.  This allows for printing
   * of the last move a Vroomba made before finishing the cleaning.
   */
  public String getStatus(boolean asMove) {
    String status = "";
    if (this.done == null || asMove) {
      status += "CLEANING: " + this.lastMove;
    }else {
      status += "RESULT: " + this.done;
    }
    status += "\t Moves: " + this.moves;
    status += "/" + this.getMaxMoves();
    status += "\t Collisions: " + this.collisions + "\n";
    return status;
  }

  /**
   * Returns whether this room currently contains any DIRT.
   */
  public boolean isClean() {
//IMPLEMENT THIS
    //(Hint: you can implement this with a single line (or two) of code.)

    //remove the line below once you've implemented this method
    throw new UnsupportedOperationException("You didn't implement " +
                                            "Room's isClean() method yet.");
  }

  /**
   * Returns a snapshot of this Room's current map as a string.
   */
  public String toString() {
    String map = "";
    for (int i = 0; i < this.roomHeight; i++) {
      map += new String(room[i]) + "\n";
    }
    return map;
  }


  //HELPER methods

  /**
   * Counts the number of times the given char is found
   * in the current room map.
   */
  protected int count(char feature) {
    int count = 0;
    for (int row = 0; row < roomHeight; row++) {
      for (int col = 0; col < roomWidth; col++) {
        if (this.room[row][col] == feature) {
          count++;
        }
      }
    }
    return count;
  }

  /**
   * Returns the room feature at the given location in this room.
   * If this does not correspond to a space on the map, it is
   * treated as a Room.WALL.
   */
  protected char getFeature(int row, int col) {
    if (row < 0 || row >= this.roomHeight ||
        col < 0 || col >= this.roomWidth) {
      //this is off the map, so treat as a wall
      return WALL;
    }else {
      return this.room[row][col];
    }
  }

  /**
   * Returns the room feature currently in the given direction
   * from this Room's Vroomba.
   * If this does not correspond to a space on the map, it is
   * treated as a Room.WALL.
   */
  protected char getFeature(Direction d) {
    int row = this.vroombaRow + d.getRowModifier();
    int col = this.vroombaCol + d.getColModifier();
    return this.getFeature(row, col);
  }


  /**
   * Given a 2D char array map, validates it and loads it as the map
   * of this room.
   *
   * @see #Room(char[][], Vroomba)
   */
  private void loadMap(char[][] map) throws InvalidMapException {
    //get and check room dimensions
    this.roomHeight = map.length;
    this.roomWidth = (map.length > 0) ? map[0].length : 0;
    if (roomHeight == 0 || roomWidth == 0) {
      throw new InvalidMapException("One of the room dimensions is 0.");
    }

    //going to create a copy of the passed map to encapsulate it
    //(so it can't be changed by caller after constructing a Room)
    this.room = new char[roomHeight][roomWidth];
    this.floorSpaces = 0;

    //load room, validating as we go
    boolean placedVroomba = false;
    for (int row = 0; row < roomHeight; row++) {  //for each row...
      if (map[row].length != roomWidth) {
        throw new InvalidMapException("Not all rows of the map " +
                                      "are the same length as the first.");
      }

      for (int col = 0; col < roomWidth; col++) {  //for each col in this row...
        //ensure this is a supported character
        switch (map[row][col]) {
          case VROOMBA:
            if (placedVroomba) {
              //already have a vroomba!
              throw new InvalidMapException("Map contains more than one Vroomba.");
            }else {
              //note where vroomba is located in the room
              this.vroombaCol = col;
              this.vroombaRow = row;
              placedVroomba = true;
            }
            //fall thru (no break), as Vroomba still counts as a floor space
          case FLOOR:
          case DIRT:
            this.floorSpaces++;
          case WALL:
          case DROP:
            //copy any of the above cases to the local map
            room[row][col] = map[row][col];
            break;
          default:
            throw new InvalidMapException("Map contains an unsupported " +
                                          "character (" + map[row][col] + ")");
        }
      }
    }

    //done loading map; was it all good?
    if (this.floorSpaces == 0) {
      throw new InvalidMapException("Map contains no floor space to clean.");
    }
    /*
     * XXX: Note that we did not check that all floor space was contiguous.
     * Certain maps could hide dirt in certain unreachable squares, making
     * that room uncleanable.  Should probably prevent this in future
     * versions...
     */

    if (!placedVroomba) {
      //<sigh>... going to have to place it randomly...  :(
      //know from above there's at least one floor space somewhere,
      //so pick one of those.
      int toPlace = (int) (Math.random() * this.floorSpaces) + 1;

      for (int row = 0; row < roomHeight; row++) {
        for (int col = 0; col < roomWidth; col++) {
          if (this.room[row][col] == FLOOR || this.room[row][col] == DIRT) {
            toPlace--;
            if (toPlace == 0) {
              //put the Vroomba here
              this.room[row][col]= VROOMBA;
              this.vroombaCol = col;
              this.vroombaRow = row;
              placedVroomba = true;
            }
          }
        }
      }
    }//done placing Vroomba

    return; //done!
  }

  /**
   * Instructs this room's Vroomba to take a turn of cleaning.
   * <p>
   * Passes this room's Vroomba its current surroundings by calling
   * its {@link Vroomba#move(char[])} method and then attempts to move
   * it in its requested direction.
   * <p>
   * After each move, determines whether the Vroomba is done cleaning.
   * It is done if it chooses to move to Direction.HERE, if it has
   * exceeded its max number of moves, if it goes over a drop,
   * or if it crashes into the same obstruction from the same
   * direction for 3 consecutive moves.
   * <p>
   * Returns whether this Vroomba needs to move again because it is
   * not done cleaning.
   *
   * @return  true if needs to continue; false if done cleaning.
   */
  protected boolean moveVroomba() {
    //first, construct a snapshot of the Vroomba's surroundings
    char[] surround = new char[8];
    for (Direction d : Direction.values()) {
      if (d != Direction.HERE) {
        surround[d.ordinal()] = this.getFeature(d);
      }
    }

    //now ask vroomba to move
    Direction move = this.vroomba.move(surround);
    this.lastMove = move;
    this.moves++;

    //and see where that takes us...
    if (move == Direction.HERE) {
      //Vroomba chose to stop
      this.moves--;  //oops, not actually a move
      done = (this.isClean()) ? Result.POWER_OFF_CLEAN : Result.POWER_OFF_DIRTY;
      if (this.moves == 0 && done == Result.POWER_OFF_DIRTY) {
        done = Result.INOPERATIVE;
      }
      return false;

    }else if (this.getFeature(move) == WALL) {
      //Vroomba crashed
      this.collisions++;
      if (this.lastMoveIfCollision == move) {
        //moved this way and crashed last turn too
        this.repeatCollisions++;
        if (this.repeatCollisions >= 3) {
          done = Result.REPEATED_CRASH;
          return false;
        }
      }else {
        //starting a fresh sequence of collisions
        this.repeatCollisions = 1;
      }
      this.lastMoveIfCollision = move;
      //collided, but not enough to be done yet: return below

    }else if (this.getFeature(move) == DROP) {
      done = Result.DROP;
      return false;

    }else {
      //actually moved successfully!
      //first, leaves clean floor behind...
      this.room[vroombaRow][vroombaCol] = FLOOR;
      //...then moves...
      this.vroombaRow += move.getRowModifier();
      this.vroombaCol += move.getColModifier();
      //...to new location on map
      this.room[vroombaRow][vroombaCol] = VROOMBA;

      //clear the collision streak tracking
      this.lastMoveIfCollision = null;
      //valid move, so return below
    }

    if (this.moves >= this.getMaxMoves()) {
      //need to pull the plug
      done = (this.isClean()) ? Result.TIME_OUT_CLEAN : Result.TIME_OUT_DIRTY;
      return false;
    }else {
      return true; //still rolling...
    }
  }


  // INNER classes

  /**
   * Provides a graphical view of a Room and its cleaning process.
   * Specifically, displays a map of the room where each square darkens
   * each time a Vroomba passes over it.  The update() method must be
   * called each time the Vroomba moves.
   * <p>
   * Though perhaps bad practice to make this as an inner class, doing so
   * grants this GUI class access to the a Room's private variables, which
   * must remain private (in the absense of packages) so that a Vroomba
   * cannot directly access details of a Room.
   */
  public class GUI extends JPanel {

    private JLabel[][] guiMap;  //a graphical map of this room

    public GUI() {
      //form a graphical copy of the room map, and load into a board to display
      guiMap = new JLabel[roomHeight][roomWidth];
      this.setLayout(new GridLayout(roomHeight, roomWidth));

      for (int row = 0; row < roomHeight; row++) {
        for (int col = 0; col < roomWidth; col++) {
          JLabel square = new JLabel("" + room[row][col]);
          square.setHorizontalAlignment(SwingConstants.CENTER);
          square.setOpaque(true);
          if (room[row][col] == Room.DIRT) {
            square.setBackground(new Color(0xFF, 0xFF, 0x99));
          }else if (room[row][col] == Room.VROOMBA) {
            square.setBackground(new Color(0xCC, 0xCC, 0xCC));
          }else {
            square.setBackground(Color.white);
          }
          guiMap[row][col] = square;
          this.add(guiMap[row][col]);
        }
      }
    }

    /**
     * Updates this GUI view to represent the state of the underlying Room.
     */
   public void update() {
      for (int row = 0; row < roomHeight; row++) {
        for (int col = 0; col < roomWidth; col++) {
          guiMap[row][col].setText("" + room[row][col]);
          if (room[row][col] == Room.VROOMBA) {
            //darken the color of this square if necessary
            Color color = guiMap[row][col].getBackground();
            if (color.getRed() > 0) {
              //darken, keeping in grey scale
              int darker = color.getRed() - 0x33;
              color = new Color(darker, darker, darker);
              guiMap[row][col].setBackground(color);
              if (darker < 0x66) {
                guiMap[row][col].setForeground(Color.white);
              }
            }
          }
        }
      }
    }
  }


  /**
   * An enumeration of the various possible outcomes of a Vroomba
   * cleaning a room.
   */
  public enum Result {
    /** Room has more than one floor space, but Vroomba never moved at all */
    INOPERATIVE,
    /** Vroomba repeated the same collision 3 times in a row */
    REPEATED_CRASH,
    /** Vroomba fell over a DROP */
    DROP,
    /** Vroomba turned off, but room was not yet clean */
    POWER_OFF_DIRTY,
    /** Vroomba exceeded max number of moves allowed and room still dirty */
    TIME_OUT_DIRTY,
    /** Vroomba exceeded max number of moves allowed, but room is clean */
    TIME_OUT_CLEAN,
    /** Vroomba turned off because room was clean */
    POWER_OFF_CLEAN;
  }
}


/**
 * Indicates that a Room map is invalid.
 */
class InvalidMapException extends Exception {

  public InvalidMapException() {
    super();
  }
  public InvalidMapException(String mesg) {
    super(mesg);
  }
}
