/**
 * @file Board.java
 *
 * @author
 *    Christos Choutouridis
 *    <cchoutou@ece.auth.gr>
 *    AEM:8997
 */

package host.labyrinth;

import java.util.ArrayList;
import java.util.function.IntFunction;

/**
 * @brief
 *    This class is the representation of the games's board
 *
 * The board is the square arrangement of the tiles. This class is also
 * the owner of the tile and supply objects.
 */
class Board {
   /** @name Constructors */
   /** @{ */

   /**
    * The empty constructor for default initialization
    */
   Board() {
      this.N   = 0;
      this.S   = 0;
      this.W   = 0;
      tiles    = null;
      supplies =null;
      walls    = new ArrayList<Edge>();
   }

   /**
    * The main constructor for the application
    * @param N    The size of each edge of the board
    * @param S    The number of supplies on the board
    */
   Board(int N, int S) {
      assert (N%2 != 0) : "Board's size has to be an odd number.";
      this.N   = Session.boardSize = N;
      this.S   = S;
      this.W   = 0;
      tiles    = new Tile[N*N];
      supplies = new Supply[S];
      walls    = new ArrayList<Edge>();
   }

   /**
    * Deep copy constructor
    * @param b    The board to copy
    *
    * @note
    *    The lack of value semantics in java is (in author's opinion) one of the greatest
    *    weakness of the language and one of the reasons why it will never be a language
    *    to care about. To quote Alexander Stepanof's words in "elements of programming" section 1.5:
    *    "Assignment is a procedure that takes two objects of the same type and makes the first
    *    object equal to the second without modifying the second".
    *    In this class we try to cope with this situation knowing that we can not do anything about
    *    assignment operator. We just add value semantics to the copy constructor and go on with our lifes...
    */
   Board(Board b) {
      // Copy primitives
      this.N   = b.N;
      this.S   = b.S;
      this.W   = b.W;
      // Clone arrays
      this.tiles     = b.tiles.clone();
      this.supplies  = b.supplies.clone();
      this.walls     = b.walls;
   }
   /** @} */

   /** @name Board's main application interface */
   /** @{ */

   /**
    * Creates the board with all the requested walls and supplies.
    *
    * @param theseusTile
    * @param minotaurTile
    */
   void createBoard(int theseusTile, int minotaurTile) {
      createTiles();
      createSupplies(theseusTile, minotaurTile);
   }

   /**
    * Returns a 2-D array with the string representation of the board.
    *
    * The rows of the array represent the Y-coordinate and the columns the X-coordinate.
    * The only difference is that between each row there is an extra row with the possible
    * walls. This way the number of rows of the returning array are 2N+1 and the number of
    * columns N+1.\n
    * So each tile of the board is represented by 3 strings. One for the north wall, one for
    * the body and one for the south wall.
    *
    * @param theseusTile   The current Theseus tile
    * @param minotaurTile  The current Minotaur tile
    * @return  The string representation of the board
    */
   String[][] getStringRepresentation(int theseusTile, int minotaurTile) {
      String[][] frame = new String[2*N+1][N];

      for (int row=0 ; row<N ; ++row) {
         int col;
         for (col =0 ; col<N-1 ; ++col)
            renderTile(frame, row, col, theseusTile, minotaurTile);
         renderSentinelTile(frame, row, col, theseusTile, minotaurTile);
      }
      return frame;
   }

   /**
    * Print board utility.
    * @param sBoard  Reference to string representation of the board to print.
    *
    * @note
    *    As the lower row addresses of the string representation of the board contain
    *    the south rows, in order to view the board correctly we have to print the rows
    *    in the opposite order. 
    */
   void printBoard (String[][] sBoard) {
      for (int i=sBoard.length-1 ; i>=0 ; --i) {
         for (String it : sBoard[i])
            System.out.print(it);
         System.out.println();
      }
   }

   /**
    * Predicate to check if a direction is Walkable.
    * 
    * A `walkable` direction is a tile direction where:
    * <ul>
    * <li>The wall is not the DOWN wall from tile (0, 0).
    * <li>There is not already a wall in the desired direction. (Implies no sentinel tile).
    * </ul>
    *
    * @param tileId     The starting tileId.
    * @param direction  The desired direction.
    * @return           True if it is walkable.
    */
   boolean isWalkable(int tileId, int direction) {
      return !tiles[tileId].hasWall(direction)
          && !(tileId == 0 && direction == Direction.DOWN);
   }

   /**
    * Predicate to check if a direction is Walkable.
    *
    * A `walkable` direction is a tile direction where:
    * <ul>
    * <li>The wall is not the DOWN wall from tile (0, 0).
    * <li>There is not already a wall in the desired direction. (Implies no sentinel tile).
    * </ul>
    *
    * @param row        Row position of the starting tile.
    * @param col        Column position of the starting tile.
    * @param direction  The desired direction.
    * @return           True if it is walkable.
    */
   boolean isWalkable(int row, int col, int direction) {
      return !tiles[Position.toID(row, col)].hasWall(direction)
          && !(Position.toID(row, col) == 0 && direction == Direction.DOWN);
   }

   /**
    * Try to pick supply from a tile. If succeed it also erases the
    * supply from the board.
    *
    * @param tileId     The tile to check
    * @return     The id of supply.
    *    @arg     Const.noSupply    if there is none
    *    @arg     The ID of supply if there is one.
    */
   int tryPickSupply(int tileId) {
      int supplyId = tiles[tileId].hasSupply(supplies);
      if (supplyId != Const.noSupply) {
         tiles[tileId].pickSupply(supplies, supplyId);
      }
      return supplyId;
   }

   /**
    * A plain fair dice functionality provided by the board.
    * @return  A random direction;
    */
   int dice () {
      ShuffledRange d = new ShuffledRange(DirRange.Begin, DirRange.End, DirRange.Step);
      return d.get();
   }

   /** @return the size of each site of the board.  */
   int size ()    { return N; }
   /** @} */

   /**
    * @name Accessor/Mutator interface
    * @note
    *    Please consider not to use mutator interface. Its the abstraction killer :(
    */
   /** @{ */
   int getN()    { return N; }
   int getS()    { return S; }
   int getW()    { return W; }
   /**
    * @note Use it with care. Any use of this function results to what Sean Parent calls "incidental data-structure".
    * <a href="https://github.com/sean-parent/sean-parent.github.io/blob/master/better-code/03-data-structures.md"> see also here</a>
    * @return  Reference to inner tiles array.
    */
   Tile[] getTiles()      { return tiles; }
   /**
    * @note Use it with care. Any use of this function results to what Sean Parent calls "incidental data-structure".
    * <a href="https://github.com/sean-parent/sean-parent.github.io/blob/master/better-code/03-data-structures.md"> see also here</a>
    * @return  Reference to inner supplies array.
    */
   Supply[] getSupplies() { return supplies; }

   /**
    * @note Use it with care. Any use of this function results to what Sean Parent calls "incidental data-structure".
    * <a href="https://github.com/sean-parent/sean-parent.github.io/blob/master/better-code/03-data-structures.md"> see also here</a>
    * @return  Reference to inner walls array.
    */
   ArrayList<Edge> getWalls() { return walls; }

   void setN(int N)  { this.N = N; }
   void setS(int S)  { this.S = S; }
   void setW(int W)  { this.W = W; }

   /**
    * @param tiles   Reference to tiles that we want to act as replacement for the inner tiles array.
    * @note    Use with care.
    * Any call to this function will probably add memory for the garbage collector.
    */
   void setTiles(Tile[] tiles)           { this.tiles = tiles; }
   /**
    * @param supplies   Reference to supplies that we want to act as replacement for the inner supplies array.
    * @note    Use with care.
    * Any call to this function will probably add memory for the garbage collector.
    */
   void setSupplies(Supply[] supplies)   { this.supplies= supplies; }

   /**
    * @param walls   Reference to walls that we want to act as replacement for the inner walls vector.
    * @note    Use with care.
    * Any call to this function will probably add memory for the garbage collector.
    */
   void setWalls (ArrayList<Edge> walls) { this.walls= walls; }

   /** @} */


   /** @name Sentinel predicates */
   /** @{ */
   private boolean isLeftSentinel (int tileId)  { return (Position.toCol(tileId) == 0); }
   private boolean isRightSentinel (int tileId) { return (Position.toCol(tileId) == N-1); }
   private boolean isUpSentinel (int tileId)    { return (Position.toRow(tileId) == N-1); }
   private boolean isDownSentinel (int tileId)  { return (Position.toRow(tileId) == 0); }
   /** @} */

   /**
    * @name private functionality of the object
    */
   /** @{ */

   /**
    * This function creates randomly all the tiles of the board
    */
   private void createTiles() {
      int wallCount;
      wallCount  = createBasicTileWalls ();  // First create tiles with outer walls
      wallCount += createInnerWalls();       // Greedy create as many inner walls we can
      W = wallCount;
   }

   /**
    * This function create randomly the board's supplies.
    *
    * The supplies has to be in separate tiles and in tiles with no player
    *
    * @param theseusTile   The tile of the Theseus
    * @param minotaurTile  The tile of the Minotaur
    */
   private void createSupplies(int theseusTile, int minotaurTile) {
      ShuffledRange rand = new ShuffledRange(0, N*N);    // Make a shuffled range of all tiles
      for (int tileId, i=0 ; i<supplies.length ; ++i) {
         // Pick a tile as long as there is no player in it
         do
            tileId = rand.get();
         while (tileId == theseusTile || tileId == minotaurTile);
         supplies[i] = new Supply(i, tileId);
      }
   }

   /**
    * Predicate to check if a wall creates a closed room.
    *
    * This algorithm has a complexity of @f$ O(N^2logN) @f$ where N represents the total
    * number of tiles.
    * It should be used with care.
    *
    * @param tileId     The tileId of the wall.
    * @param direction  The wall's relative direction. 
    * @return           True if the wall creates a closed room, false otherwise.
    */
   private boolean isRoomCreator (int tileId, int direction) {
      // Clone the list of all the walls locally.
      ArrayList<Edge> w = new ArrayList<Edge>();
      for (Edge it : walls)
         w.add(new Edge(it));
      // Create the largest possible coherent graph from the list of walls(edges)
      Graph g = new Graph(new Edge(tileId, direction));
      int size;
      do {
         size = w.size();                       // mark the size (before the pass)
         for (int i =0, S=w.size() ; i<S ; ++i) // for each edge(wall) on the local wall list
            if (g.attach(w.get(i))) {           // can we attach the edge(wall) to the graph ?
               w.remove(i);                     // if yes remove it from the local wall list
               --i; --S;                        // decrease iterator and size to match ArrayList's new values
            }
      } while (size != w.size());               // If the size hasn't change(no new graph leafs) exit

      // Search if a vertex is attached to the graph more than once.
      // This means that there is at least 2 links to the same node
      // so the graph has a closed loop
      for (Edge it : walls) {
         if (g.count(it.getV1()) > 1)  return true;
         if (g.count(it.getV2()) > 1)  return true;
      }
      return false;
   }

   /**
    * Predicate to check if a tile direction is `Wallable`.
    *
    * A `wallable` direction is a tile direction where:
    * <ul>
    * <li>The wall is not the DOWN wall from tile (0, 0).
    * <li>There is not already a wall in the desired direction. (Implies no sentinel tile).
    * <li>The neighbor in this direction has at most `Const.maxTileWalls -1` walls.
    * <li>The wall does not create a closed room (Optional requirement).
    * </ul>
    *
    * @note
    *    A wallable direction automatically implies that the direction in not an outer wall.
    *
    * @param   tileId      The tile to check.
    * @param   direction   The direction to check
    * @return  True if the direction is wallable.
    */
   private boolean isWallableDir (int tileId, int direction) {
      // Check list
      if (!isWalkable(tileId, direction))
         return false;
      switch (direction) {
         case Direction.UP:
            if (tiles[upTileId.apply(tileId)].hasWalls() >= Const.maxTileWalls)  return false;
            break;
         case Direction.DOWN:
            if (tiles[downTileId.apply(tileId)].hasWalls() >= Const.maxTileWalls)  return false;
            break;
         case Direction.LEFT:
            if (tiles[leftTileId.apply(tileId)].hasWalls() >= Const.maxTileWalls)  return false;
            break;
         case Direction.RIGHT:
            if (tiles[rightTileId.apply(tileId)].hasWalls() >= Const.maxTileWalls)  return false;
            break;
      }
      if (Session.loopGuard && isRoomCreator(tileId, direction))
         return false;
      return true;
   }

   /**
    * Predicate to check if a tile is `Wallable`.
    *
    * A `wallable` tile is a tile where:
    * <ul>
    * <li>The tile has at most `Const.maxTileWalls -1` walls.
    * <li>There is at least one wallable direction on the tile.
    * </ul>
    * @param   tileId   The tile to check
    * @return  True if the tile is wallable.
    */
   private boolean isWallable (int tileId) {
      // Check list
      if (tileId == Const.noTileId)
         return false;
      if (tiles[tileId].hasWalls() >= Const.maxTileWalls)
         return false;
      Range dirs = new Range(DirRange.Begin, DirRange.End, DirRange.Step);
      for (int dir ; (dir = dirs.get()) != Const.noTileId ; )
         if (isWallableDir(tileId, dir))
            return true;
      return false;
   }


   /**
    * This utility function create/allocate the tiles of the board and create
    * the outer walls at the same time.
    *
    * @return  The number of walls created from the utility.
    */
   private int createBasicTileWalls () {
      int wallCount =0;
      for (int i =0 ; i< tiles.length ; ++i) {
         boolean up    = isUpSentinel(i);
         boolean down  = isDownSentinel(i) && (i != 0);
         boolean left  = isLeftSentinel(i);
         boolean right = isRightSentinel(i);
         wallCount += ((up?1:0) + (down?1:0) + (left?1:0) + (right?1:0));
         tiles[i] = new Tile (i, up, down, left, right);
         // If we have loopGuard enable we populate walls also.
         if (Session.loopGuard) {
            if (up)     walls.add(new Edge(i, Direction.UP));
            if (down)   walls.add(new Edge(i, Direction.DOWN));
            if (left)   walls.add(new Edge(i, Direction.LEFT));
            if (right)  walls.add(new Edge(i, Direction.RIGHT));
         }
      }
      return wallCount;
   }

   /**
    * Create randomly a wall in the wallable selected tile.
    * @param tileId     The wallable tile to create the wall
    */
   private void createInnerWall(int tileId) {
      // Randomly pick a wallable direction in that tile.
      ShuffledRange randDirections = new ShuffledRange(DirRange.Begin, DirRange.End, DirRange.Step);
      int dir;
      do
         dir = randDirections.get();
      while (!isWallableDir(tileId, dir));
      // Add wall to tileId and the adjacent tileId
      Position neighbor = new Position(Position.toRow(tileId), Position.toCol(tileId), dir);
      tiles[tileId].setWall(dir);
      tiles[neighbor.getId()].setWall(Direction.opposite(dir));
      // If we have loopGuard enable we populate walls also.
      if (Session.loopGuard)
         walls.add(new Edge(tileId, dir));
   }

   /**
    * This utility creates the inner walls of the board.
    *
    * @return        The number of walls failed to create.
    */
   private int createInnerWalls () {
      ShuffledRange randTiles = new ShuffledRange(0, N*N);
      for (int tileId, i =0, walls =0, shuffleMark =0 ; true ; ) {
         // randomly pick a wallable tile.
         do {
            if ((tileId = randTiles.get())== Const.noTileId) {
               if (i == shuffleMark)   // Wallable tiles exhausted.
                  return walls;
               else {                  // Re-shuffle and continue.
                  randTiles = new ShuffledRange(0, N*N);
                  shuffleMark =i;
               }
            }
         } while (!isWallable(tileId));
         ++walls;
         createInnerWall(tileId);
      }
   }

   /**
    * Utility to get the body (center line) of the string representation of the tile.
    *
    * @param row           What board's row to get.
    * @param col           What board's column to get.
    * @param theseusTile   The current tile of the Theseus.
    * @param minotaurTile  The current tile of the Minotaur.
    * @return  The body string
    */
   private String getTileBody (int row, int col, int theseusTile, int minotaurTile) {
      int tileId = Position.toID(row, col);
      boolean T = (tileId == theseusTile) ? true : false;
      boolean M = (tileId == minotaurTile) ? true : false;
      int S = tiles[tileId].hasSupply(supplies);

      if      (T && !M)    return " T ";
      else if (T && M)     return "T+M";
      else if (M) {
         if (S == Const.noSupply)   return " M ";
         else                       return "M+s";
      }
      else if (S != Const.noSupply)
         return String.format("s%02d", S+1);
      else                 return "   ";
   }

   /**
    * Utility to render the 3 strings of the tile in the representation frame.
    *
    * @param frame         Reference to the frame to print into.
    * @param row           The board's row to print.
    * @param col           The board's column to print.
    * @param theseusTile   The current tile of the Theseus.
    * @param minotaurTile  The current tile of the Minotaur.
    */
   private void renderTile(String[][] frame, int row, int col, int theseusTile, int minotaurTile) {
      IntFunction<Integer> toframe = (r)->{ return 2*r+1; };

      int tileId = Position.toID(row, col);
      frame[toframe.apply(row)+1][col] = tiles[tileId].hasWall(Direction.UP)   ? "+---" : "+   ";
      frame[toframe.apply(row)  ][col] = (tiles[tileId].hasWall(Direction.LEFT)? "|" : " ")
                                       + getTileBody(row, col, theseusTile, minotaurTile);
      frame[toframe.apply(row)-1][col] = tiles[tileId].hasWall(Direction.DOWN) ? "+---" : "+   ";
   }

   /**
    * Utility to render the 3 strings of the tile in the representation frame in
    * the case the tile lies in the east wall. We call these tiles `sentinel tiles`
    *
    * @param frame         Reference to the frame to print into.
    * @param row           The board's row to print.
    * @param col           The board's column to print.
    * @param theseusTile   The current tile of the Theseus.
    * @param minotaurTile  The current tile of the Minotaur.
    */
   private void renderSentinelTile(String[][] frame, int row, int col, int theseusTile, int minotaurTile ) {
      IntFunction<Integer> toframe = (r)->{ return 2*r+1; };

      int tileId = Position.toID(row, col);
      frame[toframe.apply(row)+1][col] = tiles[tileId].hasWall(Direction.UP)   ? "+---+" : "+   +";
      frame[toframe.apply(row)  ][col] = (tiles[tileId].hasWall(Direction.LEFT)? "|" : " ")
                                       + getTileBody(row, col, theseusTile, minotaurTile)
                                       + (tiles[tileId].hasWall(Direction.RIGHT)? "|" : " ");
      frame[toframe.apply(row)-1][col] = tiles[tileId].hasWall(Direction.DOWN) ? "+---+" : "+   +";
   }
   /** @} */

   /** @name Neighbor access lambdas */
   /** @{ */
   private IntFunction<Integer> leftTileId  = (id) -> { return Position.toID(Position.toRow(id),   Position.toCol(id)-1); };
   private IntFunction<Integer> rightTileId = (id) -> { return Position.toID(Position.toRow(id),   Position.toCol(id)+1); };
   private IntFunction<Integer> upTileId    = (id) -> { return Position.toID(Position.toRow(id)+1, Position.toCol(id)  ); };
   private IntFunction<Integer> downTileId  = (id) -> { return Position.toID(Position.toRow(id)-1, Position.toCol(id)  ); };
   /** @} */

   /** @name Class data */
   /** @{ */
   private int    N;                /**< The size of each edge of the board */
   private int    S;                /**< The number of the supplies on the board */
   private int    W;                /**< The number of walls on the board */
   private Tile[] tiles;            /**< Array to hold all the tiles for the board */
   private Supply[] supplies;       /**< Array to hold all the supplies on the board */
   private ArrayList<Edge> walls;   /**<
                                     *  Array to hold all the walls using the edge representation 
                                     *  required by the closed room preventing algorithm. 
                                     */
   /** @} */
}
