/*
 * Atom-4 AI player
 * Implementation file
 *
 * $Id: ai.cc,v 1.23 2003/04/10 15:39:54 hsteoh Exp hsteoh $
 */


#include <sys/types.h>
#include <sys/wait.h>

#include <stdlib.h>
#include "ai.h"
#include "color4.h"
#include "exception.h"


/*
 * AI behaviour settings
 */

#define DEFAULT_SEARCH_DEPTH	2

// Heuristics for nodes at maxdepth=0
#define COLOR_CHANGE_WEIGHT	1.0
#define PCREATED_WEIGHT		1.7
#define PDESTROYED_WEIGHT	1.5
#define WINNING_WEIGHT		1000.0


#ifdef DEBUG_AI
void atom4ai::reopen_log() {
  fclose(logfile);			// reopen log file per round
  logfile=fopen("/tmp/atom4ai.log","w");
  if (!logfile) throw exception("atom4ai: failed to open logfile\n");
}
#endif //DEBUG_AI

#ifdef PROFILE_AI
#include <sys/gmon.h>
extern "C" {
  extern void _start(void), etext(void);
}
#endif //PROFILE_AI

void atom4ai::childmon::read_ready(eventloop *loop, int fd) {
  int x,y;
  int count;
  int status;				// child status

  if (read(fd, &x, sizeof(x))==-1)
    throw exception("Error while reading from AI subprocess");
  if (read(fd, &y, sizeof(y))==-1)
    throw exception("Error while reading from AI subprocess");

  // Remove handler from event loop
  loop->unregister_handler(eventloop::READER, fd);
  close(fd);
  waitpid(ai->child, &status, 0);	// wait for child to terminate
  ai->child = -1;

  if (!ai->move(ai->playernum, x,y)) {
    throw exception("@Unable to move to (%d,%d) as requested by AI subprocess",
                    x,y);
  }
}

void atom4ai::childmon::write_ready(eventloop *loop, int fd) {
  throw exception("atom4ai::childmon registered as a write handler??\n");
}

void atom4ai::find_legal_moves(board4 &board, elist<atom4ai::move_t> &moves) {
  move_t m;

  // Scan for all possible legal moves
  moves.clear();
  for (m.y=0; m.y<board.height(); m.y++) {
    for (m.x=0; m.x<board.width(); m.x++) {
      if (board.check_legal(m.x, m.y)) {
        moves.append(m);
      }
    } // endfor(x)
  } // endfor(y)
}

float atom4ai::score_opponent_turn(board4 &board, color4 nexttile, float min,
                                   float max, hdelta h, int maxdepth) {
  elist<move_t> moves;
  elistiter<move_t> it;

  // Find opponent's best response to our move
  find_legal_moves(board, moves);
  for (it=moves.headp(); it; it++) {

#ifdef DEBUG_AI
    fprintf(logfile, "  (%d) Considering: (%d,%d), min=%4.3f max=%4.3f <<\n",
            maxdepth, (*it).x, (*it).y, min, max);
#endif //DEBUG_AI

    float score = score_move(board, nexttile, *it, min, max, h, maxdepth);
    if (score > min) min = score;

#ifdef DEBUG_AI
    fprintf(logfile, "  >>(%d) move(%d,%d): %4.3f\n", maxdepth, (*it).x,
            (*it).y, score);
#endif //DEBUG_AI

    // Alpha prune
    if (score > max) {
#ifdef DEBUG_AI
      fprintf(logfile, "  [(%d) PRUNED: %4.3f > (%4.3f)]\n", maxdepth, score,
              max);
#endif //DEBUG_AI
      return 1e9;			// anything > max will do, actually.
    }
  }

  return min;
}

void atom4ai::update_heuristic(board4 &board, color4 our_prop,
                               elist<boardchange> &undo, hdelta &h) {
  elistiter<boardchange> it;
  color4 enemy_prop = our_prop.complement();

  for (it=undo.headp(); it; it++, h.changes++) {
    color4 newcell = board.getcell((*it).x, (*it).y);
    color4 oldcell = (*it).cell;

    if (newcell==our_prop) h.pcreated++;
    if (oldcell==enemy_prop) h.pdestroyed++;
  }
}

float atom4ai::calc_heuristic(hdelta h) {
  return COLOR_CHANGE_WEIGHT*h.changes +
         PCREATED_WEIGHT*h.pcreated +
         PDESTROYED_WEIGHT*h.pdestroyed;
}

float atom4ai::score_move(board4 &board, color4 tile, move_t m, float min,
                          float max, hdelta h, int maxdepth) {
  color4 our_prop = tile.propagator();
  elist<boardchange> splash, unsplash;
  float score;
  int win;

  // Compute changes caused by move
  board.compute_splash(m.x, m.y, tile, splash);
  board.applychanges(splash, &unsplash);
  update_heuristic(board, our_prop, unsplash, h);

  if (board.check_win(splash, our_prop, WIN_ROW)) {
    // Adjust based on real, hard data :-)
    score = WINNING_WEIGHT + calc_heuristic(h);
  } else if (maxdepth >0) {
    // Swap and negate min and max: if opponent is able to make a move that
    // scores higher than current (-min), we won't make that choice.
    score = -score_opponent_turn(board, nextcolor(tile), -1e9, -min, -h,
                                 maxdepth-1);
  } else {
    score = calc_heuristic(h);		// exhausted search depth
  }

  board.applychanges(unsplash, NULL);	// restore original board
  return score;
}

atom4ai::move_t atom4ai::pick_random(elist<atom4ai::move_t> &list) {
  int p;				// 1/probability
  elistiter<move_t> it;
  move_t pick;

  for (p=1, it=list.headp(); it; p++, it++) {
    if (rand()%p == 0) {
      pick = *it;
    }
  }
  return pick;
}

void atom4ai::pick_best_move(int fd) {
  elist<move_t> moves;
  elist<move_t> bestmoves;		// in case >1 moves have equal score
  elistiter<move_t> it;
  float min=-1e9, max=+1e9;
  hdelta h = { 0, 0, 0 };		// heuristic function accumulator

  find_legal_moves(*get_board(), moves);

  // compute score for each move
  for (it=moves.headp(); it; it++) {
    move_t m = *it;

#ifdef DEBUG_AI
    fprintf(logfile, "AI: considering move (%d,%d), max depth=%d {{\n",
            m.x, m.y, search_depth);
#endif //DEBUG_AI

    float score = score_move(*get_board(), current_tile(), m, min, max, h,
                             search_depth);

#ifdef DEBUG_AI
    fprintf(logfile, "}} move (%d,%d): %.3f\n", m.x, m.y, score);
#endif //DEBUG_AI

    // Update list of good moves
    if (score > min) {
      min = score;
      bestmoves.clear();		// prev moves are worse than this one
      bestmoves.append(m);		// this move is now the best
    } else if (score==min) {
      bestmoves.append(m);		// more than one best move so far
    }
  }

  // randomly pick a move from the list of highest-scored moves
  if (bestmoves.num_elem() < 1)
    throw exception("atom4ai: unable to find valid move?");
  move_t m = pick_random(bestmoves);

#ifdef DEBUG_AI
  fprintf(logfile, "AI: moving (%d,%d) --> %.3f\n", m.x, m.y, min);
#endif //DEBUG_AI

  // Send move back to parent process
  write(fd, &m.x, sizeof(m.x));
  write(fd, &m.y, sizeof(m.y));

#ifdef PROFILE_AI
  _mcleanup();
#endif //PROFILE_AI
}

// TESTING: spawn subprocess to do the computation, so that the event loop
// can continue processing events.
void atom4ai::make_move() {
  int pipefd[2];

  if (current_player()!=playernum || child != -1)
    return;				// not our turn

  if (pipe(pipefd)==-1)
    throw exception("Unable to create communications channel with AI "
                    "subprocess");

  switch(child=fork()) {
  case -1:
    throw exception("Unable to create AI subprocess\n");
  case 0:				// in child
    close(pipefd[0]);			// close read fd

#ifdef PROFILE_AI
    monstartup((u_long)&_start, (u_long)&etext);
#endif //PROFILE_AI

    pick_best_move(pipefd[1]);
    exit(0);				// never return
  default:				// in parent
    close(pipefd[1]);			// close write fd
    childpipe = pipefd[0];		// remember read fd
    loop->register_handler(eventloop::READER, childpipe, &babysitter);
    break;
  }
}

void atom4ai::notify_move(int player, elist<boardchange> &chg) {
  atom4::notify_move(player, chg);	// forward notification first
  make_move();				// if it's now our turn, make a move
}

void atom4ai::notify_clear() {
  atom4::notify_clear();		// forward notification
  make_move();				// move if it's our turn
}

atom4ai::atom4ai(eventloop *eloop, unsigned int width, unsigned int height,
                 int which_player) :
	atom4local(width,height), playernum(which_player),
	search_depth(DEFAULT_SEARCH_DEPTH),
	loop(eloop), babysitter(this) {

#ifdef DEBUG_AI
  logfile=fopen("/tmp/atom4ai.log","w");
  if (!logfile) throw exception("atom4ai: failed to open logfile\n");
#endif //DEBUG_AI

  child = -1;				// indicate not waiting for process

  // Make first move if we're starting
  make_move();
}

atom4ai::~atom4ai() {
  if (child != -1) {
    loop->unregister_handler(eventloop::READER, childpipe);

    // TODO: kill child process
  }

#ifdef DEBUG_AI
  fclose(logfile);
#endif //DEBUG_AI
}

void atom4ai::reset() {
  // NOTE: may want to insert resetting code here if we keep state between
  // turns
  atom4local::reset();

#ifdef DEBUG_AI
  reopen_log();
#endif //DEBUG_AI

  make_move();				// move if it's our turn after reset
}

atom4::mode_t atom4ai::game_mode() {
  return atom4::PEER;
}

int atom4ai::local_playernum() {
  // FIXME: can be more general
  return playernum==1 ? 2 : 1;
}

int atom4ai::is_local_turn() {
  return current_player()!=playernum && !round_over();
}

int atom4ai::newround() {
  atom4local::newround();

#ifdef DEBUG_AI
  reopen_log();
#endif //DEBUG_AI

  make_move();				// move if it's our turn after reset
}
