/*
 * This file is part of the OpenKropki project
 * MCTS-inspired AI
 *
 * Copyright (C) 2014-2020 Mateusz Viste
 *
 * MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * - look for current playfield position in mcts db: if found, keep it and
 *   drop the rest, otherwise drop everything
 * - choose a random move, in the vicinity of opponent's last 3 moves
 * - play the move or follow the tree until a random leaf
 * - compute leaf balance
 * - go back and set each branch scorebalance to lowest balance of all its
 *   children
 * - time up: play the move with highest scorebalance
 */

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#include "ai.h"
#include "kropki_core.h"
#include "okp_parse.h"


#define MCTS_STEPLIMIT 1000
#define MCTS_PLAYOUTS 1000
#define MCTS_CHOICE_WINLOSS 1
#define MCTS_CHOICE_HIGHESTWORST 0
#define MCTS_CHOICE_HIGHESTBEST 0

struct mcts {
  int scorebalance;  /* (me - opponent), ie. positive = good */
  int winlossbalance; /* (me_won - opp_won) */
  int playablefields; /* how many playable fields are left (0 = game ended) */
  int bestscore;    /* best score found in children nodes */
  int worstscore;   /* worst score found in children nodes */
  struct mcts *prev;
  struct mcts *next[1]; /* number of possible branches, dynamically allocated */
};

struct karl_t {
  struct mcts *tree;
  int tree_branches;
};

static struct karl_t karl;


static void mcts_free(struct mcts *m, int branchescount) {
  int i;
  if (m == NULL) return;
  /* recurse myself into each branch */
  for (i = 0; i < branchescount; i++) {
    if (m->next[i] != NULL) mcts_free(m->next[i], branchescount);
  }
  /* free myself now */
  free(m);
}


static struct mcts *mcts_alloc(const struct kropki_game *g) {
  struct mcts *res;
  res = calloc(sizeof(struct mcts) + (g->field_width * g->field_height) * sizeof(struct mcts *), 1);
  if (res == NULL) printf("ai_mcts.c: FATAL ERROR: OUT OF MEMORY @%d!", __LINE__);
  return(res);
}


/* make any possible move - used as a stupid fallback */
static void play_whatever(const struct kropki_game *g, int *movex, int *movey) {
  *movex = rand() % (g->field_width - 1);
  *movey = rand() % (g->field_height - 1);
  if (KROPKI_GETPOS(g, *movex, *movey) != 0) {
    *movex = g->playablefieldx;
    *movey = g->playablefieldy;
  }
}


/* looks for move in the global mcts root, free everything else (root included) and update the root to be the move branch (or NULL if not found) */
static void advance_mcts_tree(int oppmove) {
  int i;
  struct mcts *res;
  if (karl.tree == NULL) return;
  /* free all moves in the tree, beside oppmove */
  for (i = 0; i < karl.tree_branches; i++) {
    if (karl.tree->next[i] == NULL) continue;
    if (i == oppmove) continue;
    mcts_free(karl.tree->next[i], karl.tree_branches);
    karl.tree->next[i] = NULL;
  }
  res = karl.tree->next[oppmove];
  /* free root and reassign to res (which is possibly NULL) */
  free(karl.tree);
  karl.tree = res;
  if (karl.tree != NULL) karl.tree->prev = NULL;
}


static void moveid2xy(const struct kropki_game *g, int id, int *x, int *y) {
  div_t d;
  d = div(id, g->field_width);
  *y = d.quot;
  *x = d.rem;
}


static int xy2moveid(const struct kropki_game *g, int x, int y) {
  return((y * g->field_width) + x);
}


static int chooserandmoveclosetolast(const struct kropki_game *g) {
  int x, y, i;
  if (g->playablefields == 0) return(-1);
  for (i = 0; i < 30; i++) { /* try up to a few times */
    const int randi[16] = {-3, -2, -2, -1, -1, -1, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3};
    x = g->lastmovex;
    y = g->lastmovey;
    x += randi[rand() & 15];
    y += randi[rand() & 15];
    /* is this move playable? */
    if (kropki_getpos(g, x, y) == 0) goto FOUND; /* using the safe version of getpos: I may ask for out-of-bound positions */
    /* printf("ai_karl_think(): field %d,%d is not playable\n", x, y); */
  }
  /* if here, then no playable field found - choose whatever is possible */
  play_whatever(g, &x, &y);
  /* printf("ai_karl_think(): failed to find a random playable field in the vicinity of last moves, falling back to %d,%d\n", x, y); */
  FOUND:
  return(xy2moveid(g, x, y));
}


/* reset AI state (inits when new game starts, or free memory when game ends) */
void ai_karl_reset(void) {
  mcts_free(karl.tree, karl.tree_branches);
  memset(&karl, 0, sizeof(karl));
}


static void ai_karl_think(const struct kropki_game *gamesrc, int steplimit) {
  int i;
  int winloss;
  int score;
  int me, opp;
  struct kropki_game *g;
  struct mcts *mcts_node;

  /* abort if game ended already */
  if (gamesrc->playablefields == 0) {
    printf("WARN: ai_karl_think() got called on an ended game! weird, right?\n");
    return;
  }

  /* remember who is who (and translate 1,2 indexes to be 0-based (0,1) */
  me = gamesrc->nextplayer - 1;  /* 1 -> 0 ; 2 -> 1 */
  opp = 2 - gamesrc->nextplayer; /* 1 -> 1 ; 2 -> 0 */

  /* alloc tree node if NULL */
  if (karl.tree == NULL) {
    karl.tree = mcts_alloc(gamesrc);
    karl.tree_branches = gamesrc->field_width * gamesrc->field_height;
  }

  mcts_node = karl.tree;
  g = kropki_copygame(gamesrc, kropki_NO_WALLS);

  /* choose a random move in the vicinity of last move until end of game or step limit reached */
  while ((g->playablefields > 0) && (steplimit-- != 0)) {
    int x, y;
    int move;
    move = chooserandmoveclosetolast(g);
    if (move < 0) break;
    moveid2xy(g, move, &x, &y);
    /* printf("ai_karl_think(): will analyze move #%d (%d,%d) as player %d (playablefields=%d)\n", move, x, y, g->nextplayer,g->playablefields); */
    /* play the move */
    if (kropki_setpos(g, x, y, g->nextplayer, kropki_NO_WALLS) != 0) {
      printf("ERROR: kropki_setpos(%d,%d) FAILED! field %d,%d=%d; nextplayer=%d\n", x, y, x, y, kropki_getpos(g, x, y), g->nextplayer);
      /* force a panic quit */
      g->playablefields = 0;
      break;
    }
    /* does the move exist in memory yet? if not create it */
    if (mcts_node->next[move] == NULL) {
      mcts_node->next[move] = mcts_alloc(gamesrc);
      mcts_node->next[move]->prev = mcts_node;
      mcts_node = mcts_node->next[move];
      /* fill scorebalance and playablefields */
      mcts_node->scorebalance = g->score[me] - g->score[opp];
      mcts_node->playablefields = g->playablefields;
    } else { /* not NULL: just follow the branch */
      mcts_node = mcts_node->next[move];
      /* check that scorebalance and playablefields predictions are consistent with simulation - TODO can be removed in production, this is for debug only */
      if (mcts_node->scorebalance != (g->score[me] - g->score[opp])) {
        printf("WOOT: predicted scorebalance mismatch! %d vs %d\n", mcts_node->scorebalance, (g->score[me] - g->score[opp]));
      }
      if (mcts_node->playablefields != g->playablefields) {
        printf("WOOT: predicted playablefields mismatch! %d vs %d\n", mcts_node->playablefields, g->playablefields);
      }
    }
    /* try next move */
  }

  /* if end of game (leaf): propagate result back to root, ie. go back and update every branch winloss balance */
  winloss = 0;
  score = mcts_node->scorebalance;
  if (g->playablefields == 0) {
    if (mcts_node->scorebalance > 0) winloss = 1;
    if (mcts_node->scorebalance < 0) winloss = -1;
    /* printf("ai_karl_think(): end of game reached (leaf score me:%d opp:%d winloss:%d playablefields=%d): propagate balance back to root\n", g->score[me], g->score[opp], winloss, g->playablefields); */
  }

  i = 0; /* just for a log */
  while (mcts_node) {
    /* propagate winloss */
    mcts_node->winlossbalance += winloss;
    /* update best score */
    if (mcts_node->bestscore < score) mcts_node->bestscore = score;
    /* update worst score */
    if (mcts_node->worstscore > score) mcts_node->worstscore = score;
    /* go to next (well, previous) node */
    mcts_node = mcts_node->prev;
    i++;
  }
  /* printf("backward walk iterated over %d branches and updated them\n", i); */
  kropki_freegame(g);
}


/* analyze the playfield and find the best 'immediate' move */
static int findbestimmediatemove(const struct kropki_game *game, int *score, int whoami) {
  struct kropki_game *g;
  int x, y;
  int bestmove = -1;
  int bestscore = 0;
  int s;
  for (y = 0; y < game->field_height; y++) {
    for (x = 0; x < game->field_width; x++) {
      if (KROPKI_GETPOS(game, x, y) != 0) continue;
      /* simulate */
      g = kropki_copygame(game, kropki_NO_WALLS);
      if (kropki_setpos(g, x, y, whoami, kropki_NO_WALLS) != 0) printf("SHITE @%d\n", __LINE__);;
      s = g->score[whoami - 1] - g->score[2 - whoami];
      if ((bestmove < 0) || (s > bestscore)) {
        bestmove = xy2moveid(g, x, y);
        bestscore = s;
      }
      kropki_freegame(g);
    }
  }
  *score = bestscore;
  return(bestmove);
}


void ai_karl(const char *okp, int *movex, int *movey) {
  int oppmove, mymove, i;
  int bestimmmove, bestimmscore;
  struct kropki_game *g = NULL;

  /* pre-set answer with an error condition */
  *movex = -1;
  *movey = -1;

  g = kropki_okp2struct(okp);
  if (g == NULL) {
    printf("ERROR: kropki_okp2struct() FAILURE\n");
    goto QUIT;
  }

  /* is there a last move at all? if not, play whatever */
  if (g->lastplayer == 0) {
    printf("ai: no last move found -> playing whatever\n");
    play_whatever(g, movex, movey);
    goto QUIT;
  }

  /* do I have a simulation of the position played by opponent? */
  oppmove = xy2moveid(g, g->lastmovex, g->lastmovey);
  advance_mcts_tree(oppmove); /* look for oppmove in tree and prune the rest */

  if (karl.tree) {
    printf("opp move found in MCTS (winlossbalance=%d scorebalance=%d)\n", karl.tree->winlossbalance, karl.tree->scorebalance);
  } else {
    printf("opp move not found in db\n");
  }

  for (i = 0; i < MCTS_PLAYOUTS; i++) { /* perform that many MCTS simulations */
    ai_karl_think(g, MCTS_STEPLIMIT);
  }

  /* analyze ALL the playfield for some immediate score changes and choose the best 'immediately' available score */
  bestimmmove = findbestimmediatemove(g, &bestimmscore, g->nextplayer);
  printf("findbestimmediatemove() finds best move to be %d (scorebalance=%d)\n", bestimmmove, bestimmscore);

  /* choose the move with highest winloss balance OR with immediate score change */
  mymove = -1;
  if (karl.tree != NULL) {
    int i;
    for (i = 0; i < karl.tree_branches; i++) {
      /* if branch NULL, skip it */
      if (karl.tree->next[i] == NULL) continue;
      /* if no choice yet - take first available branch */
      if (mymove < 0) {
        mymove = i;
        continue;
      }
#if MCTS_CHOICE_WINLOSS != 0
      /* see if branch is better than current choice (better winlossbalance) */
      if (karl.tree->next[i]->winlossbalance > karl.tree->next[mymove]->winlossbalance) mymove = i;
#elif MCTS_CHOICE_HIGHESTWORST != 0
      /* see if branch is better than current choice (better worstscore) */
      if (karl.tree->next[i]->worstscore > karl.tree->next[mymove]->worstscore) mymove = i;
#elif MCTS_CHOICE_HIGHESTBEST != 0
      /* see if branch is better than current choice (better worstscore) */
      if ((karl.tree->next[i]->bestscore >= karl.tree->next[mymove]->bestscore)
       && (karl.tree->next[i]->worstscore >= karl.tree->next[mymove]->worstscore)
       && (karl.tree->next[i]->winlossbalance >= karl.tree->next[mymove]->winlossbalance)) mymove = i;
#else
      printf("SHITE\n");
#endif
    }
  }

  if (mymove >= 0) {
    moveid2xy(g, mymove, movex, movey);
    printf("best MCTS move is #%d [%d,%d]: winlossbalance=%d, scorebalance=%d, bestscore=%d worstscore=%d\n", mymove, *movex, *movey, karl.tree->next[mymove]->winlossbalance, karl.tree->next[mymove]->scorebalance, karl.tree->next[mymove]->bestscore, karl.tree->next[mymove]->worstscore);
  }

  /* should I go with mcts choice or imm move? */
  if ((mymove < 0) || ((karl.tree->next[mymove]->scorebalance < bestimmscore))) {
    mymove = bestimmmove;
    moveid2xy(g, mymove, movex, movey);
    printf("playing along with IMMEDIATE move #%d [%d,%d]: scorebalance=%d\n", bestimmmove, *movex, *movey, bestimmscore);
  }

  /* advance the tree and purge all options that I did not choose */
  advance_mcts_tree(mymove);

  QUIT:
  kropki_freegame(g);
}
