/**
 * @file HeuristicPlayer.java
 *
 * @author
 *    Anastasia Foti AEM:8959
 *    <anastaskf@ece.auth.gr>
 *
 * @author
 *    Christos Choutouridis AEM:8997
 *    <cchoutou@ece.auth.gr>
 */

package host.labyrinth;

/**
 * @brief
 *    This class represents the game's player who cheats.
 */
class HeuristicPlayer extends Player {

   /** @name Constructors */
   /** @{ */

   /**
    * Create a new player and put him at the row-column coordinates 
    * @param name       The name of the player
    * @param champion   Flag to indicate if a player is a `champion`
    * @param board      Reference to the board of the game
    * @param row        The row coordinate of initial player position
    * @param column     The column coordinate of initial player's position
    */
   public HeuristicPlayer(String name, boolean champion, Board board, int row, int column) throws Exception {
      super(name, champion, board, row, column);
   }

   /**
    * Create a new player and put him at the row-column coordinates 
    * @param name       The name of the player
    * @param champion   Flag to indicate if a player is a `champion`
    * @param board      Reference to the board of the game
    * @param tileId     The tileId coordinate of player's initial position
    */
   public HeuristicPlayer(String name, boolean champion, Board board, int tileId) throws Exception {
      super(name, champion, board, tileId);
   }
   /** @} */

   /** @name Board's main application interface */
   /** @{ */
   
   /**
    * Utility to get the distance of a possible supply in some direction
    * @param currentPos    The current position of the player
    * @param direction     The direction to check
    * @return           The distance or Const.noSupply
    */
   int supplyInDirection(int currentPos, int direction) {
      Position pos = new Position(Position.toRow(currentPos), Position.toCol(currentPos));

      for (int i=0 ; board.isWalkable(pos.getId(), direction) && i<Const.viewDistance ; ++i) {
         pos = new Position(pos.getRow(), pos.getCol(), direction);
         if (board.hasSupply(pos.getId()))
            return i+1;
      }
      return Const.noSupply;
   }

   /**
    * Utility to get the distance of a possible opponent in some direction
    * @param currentPos    The current position of the player
    * @param direction     The direction to check
    * @return           The distance or Const.noOpponent
    */
   int opponetInDirection(int currentPos, int direction) {
      Position pos  = new Position(Position.toRow(currentPos), Position.toCol(currentPos));
      int [] opp = board.getOpponentMove(playerId);

      for (int i=0 ; board.isWalkable(pos.getId(), direction) && i<Const.viewDistance ; ++i) {
         pos = new Position(pos.getRow(), pos.getCol(), direction);
         if (opp[MOVE_TILE_ID] == pos.getId())
            return i+1;
      }
      return Const.noOpponent;
   }

   /**
    * This is the main move evaluation function.
    * @param currentPos    The current position of the player (before the move to evaluate)
    * @param direction     The direction (a.k.a. the move) to evaluate
    * @return              A signed real number. The higher the output, the higher the evaluation.
    */
   double evaluate (int currentPos, int direction) {
      int opDist  = opponetInDirection (currentPos, direction);
      int supDist = supplyInDirection(currentPos, direction);

      // saturate
      opDist  = (opDist == Const.noOpponent) ? 0: opDist;
      supDist = (supDist == Const.noSupply)  ? 0 : supDist;
      return ((supDist != 0)? (1.0/supDist * Const.supplyFactor) : 0)
           - ((opDist != 0) ? (1.0/opDist  * Const.opponentFactor) : 0); 
   }

   /**
    * Selects the best possible move to return
    * @param   currentPos  Player's current position to the board
    * @return  The move array
    *
    * @note
    *    This function always return a new move.
    */
   int[] getNextMove(int currentPos) {
      Range dirs        = new Range(DirRange.Begin, DirRange.End, DirRange.Step);
      int N             = dirs.size();
      double[] eval     = new double[N];
      int [] eval_dir   = new int[N];

      for (int i =0, dir = dirs.get() ; dir != Const.EOR ; dir = dirs.get(), ++i) {
         if (board.isWalkable(currentPos, dir))
            eval[i]  = evaluate(currentPos, dir);
         else
            eval[i]  = Double.NEGATIVE_INFINITY;
         eval_dir[i] = dir;
      }
      int dir;
      if (isUnevaluated(eval, N)) {
         ShuffledRange r = new ShuffledRange(DirRange.Begin, DirRange.End, DirRange.Step);
         do
            dir = r.get();
         while (!board.isWalkable(currentPos, dir));
      }
      else {
         dir = directionOfMax (eval, eval_dir, N);
      }
      Position new_pos = new Position( Position.toRow(currentPos), Position.toCol(currentPos), dir );
      int [] ret = {new_pos.getId(), new_pos.getRow(), new_pos.getCol(), dir};
      return ret;
   }

   /**
    * HeuristicPlayer's move.
    *
    * A player of this kind cheats. He does not throw a dice to get a direction. In contrary he
    * calculates his next move very carefully.
    * If the player is a champion then he also picks up a possible supply from the tile.
    *
    * @param id   The id of the starting tile.
    * @return     An array containing player's final position and possible supply of that position.
    *             The array format is:
    * <ul>
    * <li> int[0]: The tileId of the final player's position.
    * <li> int[1]: The row of the final player's position.
    * <li> int[2]: The column of the final player's position.
    * <li> int[3]: The dice/direction of the move.
    * </ul>
    */
   @Override
   int[] move(int id) {
      // Initialize return array with the current data
      int[] ret = getNextMove(id);
      y = Position.toRow(ret[MOVE_TILE_ID]);
      x = Position.toCol(ret[MOVE_TILE_ID]);

      int supplyFlag =0, moveFlag =1;
      // In case of a champion player, try also to pick a supply
      if (champion && (board.tryPickSupply(ret[MOVE_TILE_ID]) != Const.noSupply)) {
         ++score;                               // keep score
         ++supplyFlag;
      }
      ++dirCounter[ret[MOVE_DICE]];             // update direction counters
      board.updateMove(ret, playerId);

      // Update supply and opponent distance
      int smin =DirRange.End, omin =DirRange.End;
      for (int d = DirRange.Begin ; d<DirRange.End ; d += DirRange.Step) {
         int s = supplyInDirection (ret[MOVE_TILE_ID], d);
         int o = opponetInDirection(ret[MOVE_TILE_ID], d);
         if (s >= 0 && s < smin)  smin = s;
         if (o >= 0 && o < omin)  omin = o;
      }
      // update path
      Integer[] p = {
            ret[MOVE_TILE_ID], ret[MOVE_DICE], moveFlag, supplyFlag,
            dirCounter[Direction.UP], dirCounter[Direction.RIGHT], dirCounter[Direction.DOWN], dirCounter[Direction.LEFT],
            (smin != DirRange.End)? smin:Const.noSupply, (omin != DirRange.End)? omin:Const.noOpponent
      };
      path.add(p);
      return ret;
   }

   /**
    * Prints round information for the player
    */
   void statistics() {
      if (!path.isEmpty()) {
         Integer[] last = path.get(path.size()-1);
         String who = String.format("%12s", name);
         System.out.print(who + ": score[" + score + "]" + ", dice =" + last[1] + ", tileId =" + last[0] + " (" + Position.toRow(last[0]) + ", " + Position.toCol(last[0]) + ")");
         if (last[2] == 0)
            System.out.println(" *Can not move.");
         else if (last[3] != 0)
            System.out.println(" *Found a supply.");
         else
            System.out.println("");
         // extra prints for heuristic
         if (last[8] != Const.noSupply)   System.out.println("              supply   =" + last[8]);
         else                             System.out.println("              supply   = blind");
         if (last[9] != Const.noOpponent) System.out.println("              opponent =" + last[9]);
         else                             System.out.println("              opponent = blind");
      }
   }
   
   /**
    * Prints final statistics for the player
    */
   void final_statistics () {
      String who = String.format("%12s", name);
      System.out.println();
      System.out.println(who + ": score[" + score + "]");
      System.out.println("    Moves up: " + dirCounter[Direction.UP]);
      System.out.println(" Moves right: " + dirCounter[Direction.RIGHT]);
      System.out.println("  Moves down: " + dirCounter[Direction.DOWN]);
      System.out.println("  Moves left: " + dirCounter[Direction.LEFT]);
   }

   /** @} */

   /**
    * A small utility to extract the direction of maximum evaluation result.
    *
    * We search into the \c eval results and find the index of the maximum evaluation.
    * Then we return the direction of \c eval_dir matrix at the same index we found.
    *
    * @param eval       Array with evaluation results for each direction
    * @param eval_dir   Array with the matching direction to \c eval array
    * @param N          The size of both arrays
    * @return           The direction of maximum evaluation. If \c eval is empty returns the first item \c eval[0]
    * @note
    *    This function should not be called if there is at least one evaluation result in \c eval
    */
   private int directionOfMax (double[] eval, int[] eval_dir, int N) {
      double M = Double.NEGATIVE_INFINITY;
      int M_idx = 0;
      for (int i =0; i < N ; ++i) {
         if (eval[i] > M) {
            M = eval[i];
            M_idx = i;
         }
      }
      return eval_dir[M_idx];
   }

   /**
    * A small utility to check if there is at least one evaluation result in the \c eval array
    * @param eval    The array to check
    * @param N       The size of the array
    * @return        True if there is none, false otherwise
    */
   private boolean isUnevaluated (double[] eval, int N) {
      for (int i =0 ; i<N ; ++i)
         if (eval[i] != 0 && eval[i] != Double.NEGATIVE_INFINITY)
            return false;
      return true;
   }
   
   /** @name Class data */
   /** @{ */


   /** @} */
}
