/*
 * This file is part of the OpenKropki project.
 *
 * Copyright (C) 2014-2022 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.
 */

#include <stdio.h>   /* printf(), puts() */
#include <stdlib.h>  /* rand(), srand() */
#include <string.h>  /* memset */
#include <time.h>    /* time(), clock_gettime() */
#include <SDL2/SDL_thread.h>

#include "kropcore.h"
#include "okp.h"

#include "ai.h"  /* include self for control */


struct minimax_t {
  int x, y;
  int score;
};

struct threaddata_t {
  struct minimax_t *minimax;
  struct kropki_game *game;
  int ai_player;
  int singlerun;
  int *interesting_box;
};


static int distance(int x1, int y1, int x2, int y2) {
  int xdist, ydist;
  if (x1 > x2) {
    xdist = x1 - x2;
  } else {
    xdist = x2 - x1;
  }
  if (y1 > y2) {
    ydist = y1 - y2;
  } else {
    ydist = y2 - y1;
  }
  return(xdist + ydist);
}


/* there are some specific situations when a simple hard-wired logic can
 * detect things NOT to do - this function provides a VETO against such
 * well-known bad situations. */
static int is_veto_situation(int ai_player, struct kropki_game *game, int x, int y) {
  int i, t, a, b, c, d;
  /* do not put your soldier between three oponents */
  i = 0; /* i will count the number of opponents around */
  t = 0; /* t is the number of my soldiers */
  a = kropki_getpos(game, x, y - 1) & 3; /* use the (slower) getpos version */
  b = kropki_getpos(game, x, y + 1) & 3; /* that performs boundary checks   */
  c = kropki_getpos(game, x - 1, y) & 3;
  d = kropki_getpos(game, x + 1, y) & 3;
  if (a == 3 - ai_player) i++;
  if (b == 3 - ai_player) i++;
  if (c == 3 - ai_player) i++;
  if (d == 3 - ai_player) i++;
  if (a == ai_player) t++;
  if (b == ai_player) t++;
  if (c == ai_player) t++;
  if (d == ai_player) t++;
  if ((i > 2) && (t == 0)) {
    /* printf("%d:%d is a VETO!\n", x, y); */
    return(1);
  }
  return(0);
}


static int ai_minimax(int ai_player, struct kropki_game *game, struct minimax_t *minmax, int recur, const int *interesting_box) {
  int bestscore = 0, bestmovex = -1, bestmovey = -1;
  int x, y, deltascore = 0, orgdelta;
  struct kropki_game *simgame;
  orgdelta = game->score[ai_player - 1] - game->score[(3 - ai_player) - 1];

  for (y = interesting_box[1]; y <= interesting_box[3]; y++) {
    for (x = interesting_box[0]; x <= interesting_box[2]; x++) {
      int vetoflag = 0;
      /* if field not empty -> skip it */
      if (KROPKI_GETPOS(game, x, y) != 0) continue;
      /* do the actual simulation (without wall building: useless, faster) */
      simgame = kropki_copygame(game, kropki_NO_WALLS);
      if (simgame->nextplayer + 1 != ai_player) kropki_pass(simgame, 3 - ai_player); /* opponent always passes his moves */
      kropki_setpos(simgame, x, y, ai_player, kropki_NO_WALLS);
      deltascore = simgame->score[ai_player - 1] - simgame->score[(3 - ai_player) - 1];
      deltascore -= orgdelta;
      vetoflag = is_veto_situation(3 - ai_player, game, x, y); /* check veto positions */
      if (recur < 2) { /* stop at 3 recursions (0, 1, 2) */
        deltascore += ai_minimax(ai_player, simgame, minmax, recur + 1, interesting_box);
      }
      if ((deltascore > bestscore) || (bestmovex < 0)) {
        /* printf("best move for ai %d found at [%d,%d] -> %d pts\n", ai_player, x, y, deltascore); */
        bestmovex = x;
        bestmovey = y;
        bestscore = deltascore;
      } else if ((deltascore == bestscore) && (vetoflag == 0)) { /* if same score, see if closer, unless VETOed */
        int distbest, distthis;
        distbest = distance(bestmovex, bestmovey, game->lastmovex, game->lastmovey);
        distthis = distance(x, y, game->lastmovex, game->lastmovey);
        if ((distthis < distbest) || ((distthis == distbest) && ((rand() & 7) == 0))) {/* for equivalent moves, choose randomly whether to follow it or not */
          /* printf("[%d,%d] is closer to [%d,%d]\n", x, y, game->lastmovex, game->lastmovey); */
          bestmovex = x;
          bestmovey = y;
        }
      }
      kropki_freegame(simgame);
    }
  }
  minmax->x = bestmovex;
  minmax->y = bestmovey;
  minmax->score = bestscore;
  return(bestscore);
}


static int ai_minimax_thread(void *param) {
  struct threaddata_t *p = param;
  int recursion = 0;
  if (p->singlerun != 0) recursion = 99;
  ai_minimax(p->ai_player, p->game, p->minimax, recursion, p->interesting_box);
  return(0);
}


/* find a box around last move with at least minpos available positions */
static void computeinterestingbox(int *box, const struct kropki_game *game, int minpos)  {
  int i, x, y, count;
  /* if no move yet, then interesting box is somewhere at the center of the field */
  if (game->lastplayer == 0) {
    box[0] = game->field_width / 2 - 3;
    box[1] = game->field_height / 2 - 2;
    box[2] = game->field_width / 2 + 3;
    box[3] = game->field_height / 2 + 2;
    return;
  }
  /* */
  for (i = 2; i < (game->field_width + game->field_height); i++) {
    count = 0;
    box[0] = game->lastmovex - i;
    box[1] = game->lastmovey - i;
    box[2] = game->lastmovex + i;
    box[3] = game->lastmovey + i;
    if (box[0] < 0) box[0] = 0;
    if (box[1] < 0) box[1] = 0;
    if (box[2] >= game->field_width) box[2] = game->field_width - 1;
    if (box[3] >= game->field_height) box[3] = game->field_height - 1;
    /* count the amount of available fields in this box */
    for (y = box[1]; y <= box[3]; y++) {
      for (x = box[0]; x <= box[2]; x++) {
        if (KROPKI_GETPOS(game, x, y) == 0) count++;
      }
    }
    /* printf("interesting box [%d,%d]-[%d,%d] yields %d avail. fields\n", box[0], box[1], box[2], box[3], count); */
    if (count >= minpos) break;
  }
}


static void ai_run(int ai_player, struct kropki_game *game, int *movex, int *movey) {
  SDL_Thread* threadid[2];
  struct threaddata_t threadparam[2];
  struct minimax_t mini, maxi;
  struct timespec starttime, stoptime;
  unsigned long mstime;
  int playsafe;
  int i;
  int interesting_box[4]; /* x1,y1,x2,y2 */

  memset(&mini, 0, sizeof(mini));
  memset(&maxi, 0, sizeof(maxi));

  clock_gettime(CLOCK_REALTIME, &starttime); /* CLOCK_REALTIME is the only clock guaranteed to be implemented */

  /* runs minmax 2 times: first time with only a single recursion and analyzing a broader field: if there are any immediate wins or dangers, no need to analyze any further */
  for (i = 0; i < 2; i++) {
    /* find the 'interesting' box of places (at least 100 positions around last move for first iteration, and 20 positions for further iterations) */
    computeinterestingbox(interesting_box, game, 100 - (i * 80));

    /* prepare and run 1st thread */
    threadparam[0].minimax = &maxi;
    threadparam[0].game = game;
    threadparam[0].singlerun = 1 - i;
    threadparam[0].ai_player = ai_player;
    threadparam[0].interesting_box = interesting_box;
    threadid[0] = SDL_CreateThread(ai_minimax_thread, "max", &threadparam[0]);
    /* prepare and run 2nd thread */
    threadparam[1].minimax = &mini;
    threadparam[1].game = game;
    threadparam[1].singlerun = 1 - i;
    threadparam[1].ai_player = 3 - ai_player;
    threadparam[1].interesting_box = interesting_box;
    threadid[1] = SDL_CreateThread(ai_minimax_thread, "min", &threadparam[1]);
    /* wait for the parallell threads to terminate */
    SDL_WaitThread(threadid[0], NULL);
    SDL_WaitThread(threadid[1], NULL);

    if ((mini.score != 0) || (maxi.score != 0)) break;
  }

  clock_gettime(CLOCK_REALTIME, &stoptime);

  mstime = (unsigned long)((stoptime.tv_sec - starttime.tv_sec) * 1000);
  mstime += (unsigned long)((stoptime.tv_nsec - starttime.tv_nsec) / 1000 / 1000);
  printf("max: [%d,%d] -> %d pts\nmin: [%d,%d] -> %d pts\ncomputation time: %lu ms\n\n", mini.x, mini.y, mini.score, maxi.x, maxi.y, maxi.score, mstime);

  /* decide whether to play defensively or offensively */
  if (i == 0) { /* if there is an immediate win, go for it */
    if (maxi.score > 0) {
      printf("immediate win detected! [%d,%d]\n", maxi.x, maxi.y);
      playsafe = 0;
    } else {
      printf("immediate danger detected! [%d,%d]\n", maxi.x, maxi.y);
      playsafe = 1;
    }
  } else if (maxi.score > mini.score) {
    playsafe = 0;
  } else { /* if I'm afraid to loose points, check the bold move anyway */
    struct kropki_game *simgame;
    struct minimax_t newmini;
    memset(&newmini, 0, sizeof(newmini));
    playsafe = 1;
    if (maxi.score > 0) {
      /* simulate the bold move */
      simgame = kropki_copygame(game, kropki_NO_WALLS);
      ai_minimax(3 - ai_player, simgame, &newmini, 0, interesting_box);
      /* recheck min now - is it still bad? */
      if (newmini.score <= maxi.score) {
        printf("it appears that playing boldly might be rewarding!\n\n");
        playsafe = 0;
      }
      kropki_freegame(simgame);
    }
  }

  /* make the move */
  if (playsafe != 0) {
    *movex = mini.x;
    *movey = mini.y;
  } else {
    *movex = maxi.x;
    *movey = maxi.y;
  }
}


void ai_dumbo(const char *okp, int *movex, int *movey) {
  static int rand_init = 0;
  struct kropki_game *game;
  int ai_player = 2;

  /* pre-init move coordinates to 'failed' */
  *movex = -1;
  *movey = -1;

  /* init the random generator, if not inited yet */
  if (rand_init == 0) {
    rand_init = 1;
    srand((unsigned int)time(NULL));
  }

  /* convert OKP into a game struct */
  game = kropki_okp2struct(okp, NULL, NULL, 0);
  if (game == NULL) {
    puts("kropki_okp2struct() FAIL!");
    return;
  }

  /* verify that I can make any move at all */
  if (kropki_findfirstfreefield(game, NULL, NULL) != 0) return;

  /* execute the AI now */
  ai_run(ai_player, game, movex, movey);
}