/*
 * This file is part of the OpenKropki project
 * MiniMax AI
 *
 * 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.
 *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 * - picks a selection of "interesting positions" based on last moves
 * - performs minmax computations on each move, up to a limited depth
 */

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

#include <SDL2/SDL_thread.h>

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

/* how many fields should the AI analyze around a given last move */
#define FIELDSLOOKUP 5

/* how deep should the AI perform its playouts */
#define STEPLIMIT 4

/* how many last moves to analyse */
#define LASTMOVES 5

/* do I keep track of my own moves, or should I remember only opp's moves? */
#define REMEMBER_OWN_MOVES 0

/* how many CPU threads to use */
#define THREADSNUM 4

/* make Tigran feed back to OpenKropki the "interesting" fields it looked at */
#ifndef TIGRAN_DEBUGFIELDS
  #define TIGRAN_DEBUGFIELDS 0
#endif

/* field_t is used to store [x,y] coordinates in a single variable */
typedef uint_fast32_t field_t;

/* translate a field value into x,y coordinates and vice-versa */
#define FIELDX(f) ((int)(f >> 16))
#define FIELDY(f) ((int)(f & 0xffff))
#define XYTOFIELD(x,y) ((uint_fast32_t)((x << 16) | y))


struct fieldlist {
  int x;
  int y;
  int best;
  int worst;
  int simcount; /* number of simulations executed (informational, debug data) */
};


/* memory for keeping track of last moves */
static field_t lastmoves[LASTMOVES];
static int lastmoveptr;


/* perform move x/y and analyze all subsequent positions, fills bestworst with best and worst score */
static void ai_think(const struct kropki_game *gamesrc, int movex, int movey, const field_t *fields, int fieldscount, int steplimit, int self, int *worst, int *best, int *simcount, const int *parentworst) {
  struct kropki_game *g;
  int i;
  int stepschecked;
  int worstchild = 0, bestchild = 0;
  const int opp = 1 - self; /* opponent's index */

  /* pre-init worst/best in case I quit early */
  *worst = gamesrc->score[self] - gamesrc->score[opp];
  *best = *worst;

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

  g = kropki_copygame(gamesrc, kropki_NO_WALLS);
  if (g == NULL) {
    printf("ai_tigran: OUT OF MEMORY @%d\n", __LINE__);
    return;
  }

  /* perform the move */
  if (kropki_setpos(g, movex, movey, g->nextplayer, kropki_NO_WALLS) != 0) {
    printf("ERROR: kropki_setpos(%d,%d) FAILED! field %d,%d=%d; nextplayer=%d\n", movex, movey, movex, movey, kropki_getpos(g, movex, movey), g->nextplayer);
    kropki_freegame(g);
    return;
  }

  /* end here if steplimit reached */
  if (steplimit == 0) {
    *worst = g->score[self] - g->score[opp];
    *best = *worst;
    /* printf("edge reached: worst=%d best=%d\n", *worst, *best); */
    kropki_freegame(g);
    return;
  }

  /* analyze possible next moves by calling self */
  stepschecked = 0; /* detect if no simulation could be done due to game full */
  for (i = 0; i < fieldscount; i++) {
    int x, y;
    int b, w;
    int *pworst = NULL;

    x = FIELDX(fields[i]);
    y = FIELDY(fields[i]);

    if (KROPKI_GETPOS(g, x, y) != 0) continue; /* analyze only playable fields */

    /* if at least one simulation was done already, pass current worst to child */
    if (stepschecked != 0) pworst = &worstchild;

    ai_think(g, x, y, fields, fieldscount, steplimit - 1, self, &w, &b, simcount, pworst);
    stepschecked++;

    /* init local best/worst with first result */
    if (stepschecked == 1) {
      worstchild = w;
      bestchild = b;
      continue;
    }
    /* elect localbest (always choose highest score, assuming opp is a moron) */
    if (b > bestchild) bestchild = b;
    /* elect localworst (if opp running: assume he will choose the most damaging move, if I'm running: assume I will play my best move) */
    if (g->lastplayer != (self + 1)) { /* analyzed move was mine */
      if (w > worstchild) worstchild = w;
    } else { /* analyzed move was opp */
      if (w < worstchild) worstchild = w;
      /* stop here if parentworst matched (and set): pruning branches that cannot lead to results better than what I found already */
      if ((parentworst != NULL) && (w < *parentworst)) break;
    }
  }

  if (stepschecked > 0) {
    (*simcount) += stepschecked;
    *worst = worstchild;
    *best = bestchild;
  } else {
    /* printf("ai_tigran: analyzed fields full: could simulate no more\n"); */
    *worst = g->score[self] - g->score[opp];
    *best = *worst;
  }

  kropki_freegame(g);
}


/* elect a list of "interesting" fields that should be analysed around field f,
 * but no more than maxfields. returns the number of fields inserted in fieldlist. */
static int computeinterestingfields(const struct kropki_game *g, field_t *fieldslist, field_t f, int maxfields) {
  const int inc[2] = {1, -1};
  int x, y, i, bok;
  int count = 0;

  x = FIELDX(f);
  y = FIELDY(f);

  /* the algorithm looks at fields moving in a spiral around [x,y] until
   * maxfields playable fields are found or until no more fields are left on
   * the playfield */
  printf("intfields around [%d,%d]:", x, y);
  for (bok = 1;; bok++) {
    for (i = 0; i < bok; i++) {
      x += inc[bok & 1];
      if (kropki_getpos(g, x, y) == 0) {
        fieldslist[count++] = XYTOFIELD(x, y);
        printf(" [%d,%d]", x, y);
        /* do not look further if we found all we needed */
        if (count >= maxfields) return(count);
      }
    }
    for (i = 0; i < bok; i++) {
      y += inc[bok & 1];
      if (kropki_getpos(g, x, y) == 0) {
        fieldslist[count++] = XYTOFIELD(x, y);
        printf(" [%d,%d]", x, y);
        /* do not look further if we found all we needed */
        if (count >= maxfields) return(count);
      }
    }
    /* stop looking if bok is completely outside game */
    if ((bok > g->field_width * 2) && (bok > g->field_height * 2)) {
      printf("\nfailed to find %d interesting points (game full)", maxfields);
      return(count);
    }
  }
}


struct threaddata_t {
  struct kropki_game *g;
  struct fieldlist *f;
  field_t *interestingfields;
  int interestingfields_count;
};


static int start_ai_thinking(void *param) {
  struct threaddata_t *d = param;

  ai_think(d->g, d->f->x, d->f->y, d->interestingfields, d->interestingfields_count, STEPLIMIT, d->g->nextplayer - 1, &(d->f->worst), &(d->f->best), &(d->f->simcount), NULL);

  printf("choice %d.%d\t", d->f->x, d->f->y);
  printf("-> worst=%d best=%d (simcount=%d)\n", d->f->worst, d->f->best, d->f->simcount);

  return(0);
}


/* reset memory of the AI */
void ai_tigran_reset(void) {
  memset(lastmoves, 0, sizeof(lastmoves));
  lastmoveptr = 0;
}


void ai_tigran(const char *okp, int *movex, int *movey, int **debfields) {
  int mymoves[FIELDSLOOKUP * LASTMOVES];
  int mymoves_count;
  int mymove, i, n;
  struct kropki_game *g = NULL;
  struct fieldlist flist[FIELDSLOOKUP * LASTMOVES];
  int flistlen;
  field_t interestingmoves[FIELDSLOOKUP * LASTMOVES];
  int interestingmoves_count;
#if TIGRAN_DEBUGFIELDS > 0
  static int debugfields[FIELDSLOOKUP * LASTMOVES * 2 + 1];
  debugfields[0] = -1;
  *debfields = debugfields;
#else
  *debfields = NULL;
#endif

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

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

  if (g->playablefields == 0) {
    printf("ERROR: ai_tigran() called on a game with playablefields == 0\n");
    goto QUIT;
  }

  /* empty game? return a move in the middle of the screen and reset lastmoves */
  if (g->lastplayer == 0) {
    printf("ai_tigran: empty game detected\n");
    *movex = g->field_width / 2;
    *movey = g->field_height / 2;
    memset(lastmoves, 0, sizeof(lastmoves));
    goto QUIT;
  }

  /* add last move to lastmove list */
  printf("ai_tigran: opp move was [%d,%d]\n", g->lastmovex, g->lastmovey);
  lastmoves[lastmoveptr++] = XYTOFIELD(g->lastmovex, g->lastmovey);
  lastmoveptr %= LASTMOVES;

  /* compute the list of interesting fields (to be analyzed) around last n
   * positions */
  interestingmoves_count = 0;
  for (n = 0; n < LASTMOVES; n++) {
    if (lastmoves[n] == 0) continue; /* ignore last moves in 0,0: it is never an interesting location, plus it is likely to be false data (zero-inited array) */
    interestingmoves_count += computeinterestingfields(g, &(interestingmoves[interestingmoves_count]), lastmoves[n], FIELDSLOOKUP);
    printf("\n");
  }
  /* if no last move found, then add playablefield */
  if (interestingmoves_count == 0) {
    interestingmoves[interestingmoves_count++] = XYTOFIELD(g->playablefieldx, g->playablefieldy);
  }
  /* simplify the list (remove double entries) */
  printf("interesting_count before simplification = %d\n", interestingmoves_count);
  for (n = 0; n < interestingmoves_count; n++) {
    for (i = interestingmoves_count - 1; i > n; i--) { /* run backward to avoid skipping fields when a move is removed */
      if ((interestingmoves[n] != interestingmoves[i]) || (n == i)) continue;
      /* printf("removing [%d,%d] at pos %d (same as pos %d)\n", FIELDX(interestingmoves[n]), FIELDY(interestingmoves[n]), i, n); */
      interestingmoves_count--;
      if (interestingmoves_count > i) memmove(&(interestingmoves[i]), &(interestingmoves[i + 1]), sizeof(field_t) * (interestingmoves_count - i));
    }
  }
  printf("computed a list of %d interesting points:\n", interestingmoves_count);

  /* remove interesting fields that are probably not so interesting (no liberties and no immediate win) */
  for (i = interestingmoves_count - 1; (i >= 0) && (interestingmoves_count > 1); i--) {
    int x = FIELDX(interestingmoves[i]);
    int y = FIELDY(interestingmoves[i]);
    int count = 0;
    if ((x == 0) || (y == 0)) continue;
    if (x == (g->field_width - 1)) continue;
    if (y == (g->field_height - 1)) continue;
    if ((KROPKI_GETPOS(g, x - 1, y) & 3) == g->lastplayer) count++;
    if ((KROPKI_GETPOS(g, x + 1, y) & 3) == g->lastplayer) count++;
    if ((KROPKI_GETPOS(g, x, y - 1) & 3) == g->lastplayer) count++;
    if ((KROPKI_GETPOS(g, x, y + 1) & 3) == g->lastplayer) count++;
    if (count < 4) continue;
    /* no liberties - will such move lead to an immediate win? */
    {
      struct kropki_game *g2;
      g2 = kropki_copygame(g, kropki_NO_WALLS);
      if (g2 == NULL) continue; /* OOM: keep the field rather than crash */
      kropki_setpos(g2, x, y, g2->nextplayer, kropki_NO_WALLS);
      count = g2->score[g->nextplayer - 1] - g->score[g->nextplayer - 1];
      kropki_freegame(g2);
      if (count > 0) continue; /* immediate win */
    }
    printf("UNINTERESTING FIELD REMOVED [%d,%d]\n", FIELDX(interestingmoves[i]), FIELDY(interestingmoves[i]));
    interestingmoves_count--;
    if (interestingmoves_count > i) memmove(&(interestingmoves[i]), &(interestingmoves[i + 1]), sizeof(field_t) * (interestingmoves_count - i));
  }

#if TIGRAN_DEBUGFIELDS > 0
  /* add field to debugfields for displaying it in the UI if needed */
  for (n = 0; n < interestingmoves_count; n++) {
    debugfields[n * 2 + 2] = -1; /* start with this to avoid crashing openkropki (it works in parallel!) */
    debugfields[n * 2] = FIELDX(interestingmoves[n]);
    debugfields[n * 2 + 1] = FIELDY(interestingmoves[n]);
  }
#endif

  /* compute a fieldlist based on the list of interesting fields */
  memset(flist, 0, sizeof(struct fieldlist) * interestingmoves_count);
  for (flistlen = 0; flistlen < interestingmoves_count; flistlen++) {
    flist[flistlen].x = FIELDX(interestingmoves[flistlen]);
    flist[flistlen].y = FIELDY(interestingmoves[flistlen]);
  }

  /* launch simulations */
  {
    int nextthread = 0;
    SDL_Thread *threadid[THREADSNUM] = {NULL};
    struct threaddata_t threadparam[THREADSNUM];

    /* simulation starts - do it for every field of the box (multithreaded) */
    for (i = 0; i < flistlen; i++) {
      /* wait for thread to finish */
      if (threadid[nextthread] != NULL) SDL_WaitThread(threadid[nextthread], NULL);
      threadparam[nextthread].g = g;
      threadparam[nextthread].f = &(flist[i]);
      threadparam[nextthread].interestingfields = interestingmoves;
      threadparam[nextthread].interestingfields_count = interestingmoves_count;
      threadid[nextthread] = SDL_CreateThread(start_ai_thinking, "ai_tigran", &threadparam[nextthread]);
      nextthread++;
      nextthread %= THREADSNUM;
    }
    /* wait for all threads to finish */
    for (nextthread = 0; nextthread < THREADSNUM; nextthread++) {
      if (threadid[nextthread] == NULL) continue;
      SDL_WaitThread(threadid[nextthread], NULL);
    }
  }

  /* choose the best move: highest worst, then highest best, then highest simcount */
  mymove = 0; /* preselect first move */
  for (i = 0; i < flistlen; i++) {
    /* select move if it is "less worse" than the current selection */
    /* printf("choice #%d [%d,%d] leads to worst=%d best=%d (simcount=%d)\n", i, flist[i].x, flist[i].y, flist[i].worst, flist[i].best, flist[i].simcount); */
    if (flist[i].worst > flist[mymove].worst) {
      mymove = i;
    } else if ((flist[i].worst == flist[mymove].worst)
            && (flist[i].best > flist[mymove].best)) {
      /* if it's worsness is same, then look if best-case scenario is better */
      mymove = i;
    } else if ((flist[i].worst == flist[mymove].worst)
            && (flist[i].best == flist[mymove].best)
            && (flist[i].simcount > flist[mymove].simcount)) {
      /* if it's worsness and bestest is same, then look for higher simcount */
      mymove = i;
    }
  }

  /* do a list of all propositions with same score as mymove */
  mymoves_count = 0;
  for (i = 0; i < flistlen; i++) {
    if ((flist[i].worst != flist[mymove].worst) || (flist[i].best != flist[mymove].best) || (flist[i].simcount != flist[mymove].simcount)) continue;
    mymoves[mymoves_count++] = i;
    printf("acceptable choice: [%d,%d] (worst=%d, best=%d, simcount=%d)\n", flist[i].x, flist[i].y, flist[i].worst, flist[i].best, flist[i].simcount);
  }
  /* select one (random) move from the list of best scores */
  mymove = mymoves[rand() % mymoves_count];

  /* translate mymove to actual x/y coordinates */
  *movex = flist[mymove].x;
  *movey = flist[mymove].y;

#if REMEMBER_OWN_MOVES > 0
  /* remember my choice in last moves */
  lastmoves[lastmoveptr++] = XYTOFIELD(*movex, *movey);
  lastmoveptr %= LASTMOVES;
#endif

  QUIT:
  kropki_freegame(g);
}
