/*
 * This file is part of the OpenKropki project
 * Bender - surround-efficiency AI
 *
 * Copyright (C) 2026 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.
 *
 * Algorithm outline:
 *  1. For every live friendly and enemy stone, grow a virtual surround pocket
 *     by greedily expanding through liberties, scoring
 *        (prey_value) / (fence_length ^ 3)
 *     and painting that score onto candidate empty intersections.
 *  2. Keep the max score seen at each empty cell across all seeds.
 *  3. Play the hottest empty cell (with tactical capture/defend overrides).
 *
 * extras:
 *  - hard short-circuit for immediate captures and real defenses
 *  - prey value weights enemy dots more than sealed empties (matches scoring)
 *  - atari (1-liberty) and near-atari (2-liberty) urgency boosts
 *  - sensible opening + tie-break toward contact / last move
 *  - soft veto against dumping a lonely stone into 3+ enemy neighbours
 */

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

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

#define ORTH_N 4
static const int ORTH[ORTH_N][2] = {{0, -1}, {1, 0}, {0, 1}, {-1, 0}};

struct bcell {
  double cost;
  double lastCost;
  unsigned char checked;
  unsigned char inreg;
};

struct bctx {
  const struct kropki_game *g;
  int w, h, n;
  struct bcell *cell;     /* [n] */
  int *queue;             /* [n] BFS scratch */
  int *group_buf;         /* [n] */
  int *free_buf;          /* [n] */
  int *check_buf;         /* [n] cellsToCheck */
  int *tmp_free;          /* [n] */
  int *tmp_friend;        /* [n] */
  int *opt_free;          /* [n] */
  int *opt_friend;        /* [n] */
  int *captured_buf;      /* [n] */
};


static int idx_of(const struct bctx *b, int x, int y) {
  return(y * b->w + x);
}

static int in_bounds(const struct bctx *b, int x, int y) {
  return(x >= 0 && y >= 0 && x < b->w && y < b->h);
}

static unsigned char raw_at(const struct bctx *b, int x, int y) {
  return(KROPKI_GETPOS(b->g, x, y));
}

static int is_playable(const struct bctx *b, int x, int y) {
  return(in_bounds(b, x, y) && raw_at(b, x, y) == 0);
}

static int is_captured(const struct bctx *b, int x, int y) {
  return(in_bounds(b, x, y) && ((raw_at(b, x, y) & KROPKI_CAPX) != 0));
}

static int dot_at(const struct bctx *b, int x, int y) {
  if (!in_bounds(b, x, y)) return(-1);
  return(raw_at(b, x, y) & 3);
}

static void clear_checked(struct bctx *b) {
  int i;
  for (i = 0; i < b->n; i++) b->cell[i].checked = 0;
}

static void clear_inreg(struct bctx *b) {
  int i;
  for (i = 0; i < b->n; i++) b->cell[i].inreg = 0;
}

static void reset_costs(struct bctx *b) {
  int i;
  for (i = 0; i < b->n; i++) {
    b->cell[i].cost = 0.0;
    b->cell[i].lastCost = 0.0;
    b->cell[i].checked = 0;
    b->cell[i].inreg = 0;
  }
}

static void summarize_cost(struct bctx *b) {
  int i;
  for (i = 0; i < b->n; i++) {
    if (b->cell[i].lastCost > b->cell[i].cost)
      b->cell[i].cost = b->cell[i].lastCost;
    b->cell[i].lastCost = 0.0;
  }
}


/* Flood a same-color (plus already-captured) pocket. Returns 0 on success.
 * group_buf/free_buf filled; *group_n / *free_n set; *near_border set. */
static int find_same_group(struct bctx *b, int sx, int sy,
                           int *group_n, int *free_n, int *near_border) {
  int start_dot = dot_at(b, sx, sy);
  int qh = 0, qt = 0;
  int i;

  *group_n = 0;
  *free_n = 0;
  *near_border = 0;
  clear_inreg(b);

  if (start_dot != 1 && start_dot != 2) return(-1);

  b->queue[qt++] = idx_of(b, sx, sy);
  b->cell[idx_of(b, sx, sy)].inreg = 1;

  while (qh < qt) {
    int cur = b->queue[qh++];
    int cx = cur % b->w, cy = cur / b->w;
    b->group_buf[(*group_n)++] = cur;

    for (i = 0; i < ORTH_N; i++) {
      int nx = cx + ORTH[i][0], ny = cy + ORTH[i][1];
      int ni, nd;
      if (!in_bounds(b, nx, ny)) {
        *near_border = 1;
        continue;
      }
      ni = idx_of(b, nx, ny);
      if (b->cell[ni].inreg || b->cell[ni].checked) continue;
      b->cell[ni].inreg = 1;
      if (is_playable(b, nx, ny)) {
        b->free_buf[(*free_n)++] = ni;
        continue;
      }
      nd = dot_at(b, nx, ny);
      /* same colour OR any captured cell joins the flood (dots4all model) */
      if (nd == start_dot || is_captured(b, nx, ny))
        b->queue[qt++] = ni;
    }
  }
  return(0);
}


/* Greedy surround-efficiency expansion from one seed stone.
 * by_color = the colour that would build the fence (attacker). */
static void paint_from_seed(struct bctx *b, int sx, int sy, int by_color) {
  int group_n = 0, free_n = 0, near_border = 0;
  int check_n = 0, captured_n = 0;
  int filled_empties = -1;
  int opt_free_n, opt_friend_n, opt_cell;
  double opt_cost;
  int i, k;

  if (find_same_group(b, sx, sy, &group_n, &free_n, &near_border) != 0) return;
  if (near_border) return;
  if (free_n < 1) return;

  {
    double libs = (double)free_n;
    opt_cost = (double)group_n / (libs * libs * libs);
  }
  opt_free_n = free_n;
  opt_friend_n = group_n;
  for (i = 0; i < free_n; i++) b->opt_free[i] = b->free_buf[i];
  for (i = 0; i < group_n; i++) b->opt_friend[i] = b->group_buf[i];
  opt_cell = idx_of(b, sx, sy);

  for (;;) {
    if (opt_cell < 0) break;

    filled_empties++;
    /* drop opt_cell from check list if present */
    for (i = check_n - 1; i >= 0; i--) {
      if (b->check_buf[i] == opt_cell) {
        b->check_buf[i] = b->check_buf[--check_n];
        break;
      }
    }
    for (i = 0; i < opt_free_n; i++)
      b->cell[b->opt_free[i]].checked = 1;
    for (i = 0; i < opt_friend_n; i++)
      b->cell[b->opt_friend[i]].checked = 1;

    for (i = 0; i < opt_free_n; i++) {
      int fi = b->opt_free[i];
      b->check_buf[check_n++] = fi;
      if (b->cell[fi].lastCost < opt_cost)
        b->cell[fi].lastCost = opt_cost;
    }
    for (i = 0; i < opt_friend_n; i++)
      b->captured_buf[captured_n++] = b->opt_friend[i];

    if (check_n == 0) break;

    /* pick next free cell that maximises prey / fence^3 */
    opt_free_n = 0;
    opt_friend_n = 0;
    opt_cell = -1;
    opt_cost = 0.0;

    for (k = 0; k < check_n; k++) {
      int ci = b->check_buf[k];
      int cx = ci % b->w, cy = ci / b->w;
      int tmp_free_n = 0, tmp_friend_n = 0;
      int ok = 1;
      double wall, cost;
      int prey_dots, j;

      for (i = 0; i < ORTH_N && ok; i++) {
        int nx = cx + ORTH[i][0], ny = cy + ORTH[i][1];
        int ni, nd;
        if (!in_bounds(b, nx, ny)) {
          ok = 0;
          break;
        }
        ni = idx_of(b, nx, ny);
        if (b->cell[ni].checked) continue;

        if (is_playable(b, nx, ny)) {
          /* dedup into tmp_free */
          for (j = 0; j < tmp_free_n; j++) if (b->tmp_free[j] == ni) break;
          if (j == tmp_free_n) b->tmp_free[tmp_free_n++] = ni;
          continue;
        }

        nd = dot_at(b, nx, ny);
        /* Match dots4all: merge uncaptured non-attacker stones, or
         * attacker stones that are already inside a base. */
        if (((nd != by_color) && (nd != 0) && !is_captured(b, nx, ny))
         || ((nd == by_color) && is_captured(b, nx, ny))) {
          int gn = 0, fn = 0, nb = 0;
          int t;
          if (find_same_group(b, nx, ny, &gn, &fn, &nb) != 0 || nb) {
            ok = 0;
            break;
          }
          for (t = 0; t < fn; t++) {
            int fi = b->free_buf[t];
            for (j = 0; j < tmp_free_n; j++) if (b->tmp_free[j] == fi) break;
            if (j == tmp_free_n) b->tmp_free[tmp_free_n++] = fi;
          }
          for (t = 0; t < gn; t++) {
            int gi = b->group_buf[t];
            for (j = 0; j < tmp_friend_n; j++) if (b->tmp_friend[j] == gi) break;
            if (j == tmp_friend_n) b->tmp_friend[tmp_friend_n++] = gi;
          }
        }
      }
      if (!ok) continue;

      wall = (double)(check_n - 1 + tmp_free_n);
      if (wall < 1.0) wall = 1.0;

      /* OpenKropki scores enemy dots, not empty interiors -- weight accordingly */
      prey_dots = 0;
      for (j = 0; j < captured_n; j++) {
        int d = raw_at(b, b->captured_buf[j] % b->w, b->captured_buf[j] / b->w) & 3;
        if (d != 0 && d != by_color) prey_dots++;
      }
      for (j = 0; j < tmp_friend_n; j++) {
        int d = raw_at(b, b->tmp_friend[j] % b->w, b->tmp_friend[j] / b->w) & 3;
        if (d != 0 && d != by_color) prey_dots++;
      }

      cost = ((double)prey_dots * 1.0
            + (double)(captured_n + tmp_friend_n - prey_dots) * 0.15
            + (double)(filled_empties + 1) * 0.25)
           / (wall * wall * wall);

      if (opt_cell < 0 || cost > opt_cost) {
        opt_cost = cost;
        opt_cell = ci;
        opt_free_n = tmp_free_n;
        opt_friend_n = tmp_friend_n;
        for (j = 0; j < tmp_free_n; j++) b->opt_free[j] = b->tmp_free[j];
        for (j = 0; j < tmp_friend_n; j++) b->opt_friend[j] = b->tmp_friend[j];
      }
    }
  }
}


/* Urgency: liberties of groups with few escapes are extremely hot. */
static void paint_liberty_urgency(struct bctx *b, int me, int opp) {
  int x, y;
  unsigned char *seen;
  seen = calloc((size_t)b->n, 1);
  if (seen == NULL) return;

  for (y = 0; y < b->h; y++) {
    for (x = 0; x < b->w; x++) {
      int d = dot_at(b, x, y);
      int gn, fn, nb, i;
      double boost;
      if (d != me && d != opp) continue;
      if (is_captured(b, x, y)) continue;
      if (seen[idx_of(b, x, y)]) continue;

      clear_checked(b);
      if (find_same_group(b, x, y, &gn, &fn, &nb) != 0) continue;
      for (i = 0; i < gn; i++) seen[b->group_buf[i]] = 1;
      if (nb || fn < 1) continue;

      if (fn == 1) boost = 20.0 + (double)gn * 1.5;      /* atari: must act */
      else if (fn == 2) boost = 0.8 + (double)gn * 0.08;  /* hint, don't drown the map */
      else if (fn == 3) boost = 0.15 + (double)gn * 0.02;
      else continue;

      /* defending own group is slightly more urgent than attacking */
      if (d == me) boost *= 1.1;

      for (i = 0; i < fn; i++) {
        int fi = b->free_buf[i];
        b->cell[fi].cost += boost;
      }
    }
  }
  free(seen);
}


static int move_gain(const struct kropki_game *src, int x, int y, int player) {
  struct kropki_game *g;
  int before, after;
  if (KROPKI_GETPOS(src, x, y) != 0) return(-1);
  g = kropki_copygame(src, kropki_NO_WALLS);
  if (g == NULL) return(-1);
  before = g->score[player - 1];
  if (kropki_setpos(g, x, y, player, kropki_NO_WALLS) != 0) {
    kropki_freegame(g);
    return(-1);
  }
  after = g->score[player - 1];
  kropki_freegame(g);
  return(after - before);
}


static int soft_veto(const struct bctx *b, int x, int y, int me, int opp) {
  int i, ene = 0, fri = 0;
  for (i = 0; i < ORTH_N; i++) {
    int p = dot_at(b, x + ORTH[i][0], y + ORTH[i][1]);
    if (p == opp) ene++;
    else if (p == me) fri++;
  }
  return((ene >= 3) && (fri == 0));
}


static int abs_i(int v) { return(v < 0 ? -v : v); }


static int bctx_alloc(struct bctx *b, const struct kropki_game *g) {
  size_t bytes;
  memset(b, 0, sizeof(*b));
  b->g = g;
  b->w = g->field_width;
  b->h = g->field_height;
  b->n = b->w * b->h;
  bytes = (size_t)b->n;
  b->cell = calloc(bytes, sizeof(struct bcell));
  b->queue = malloc(bytes * sizeof(int));
  b->group_buf = malloc(bytes * sizeof(int));
  b->free_buf = malloc(bytes * sizeof(int));
  b->check_buf = malloc(bytes * sizeof(int));
  b->tmp_free = malloc(bytes * sizeof(int));
  b->tmp_friend = malloc(bytes * sizeof(int));
  b->opt_free = malloc(bytes * sizeof(int));
  b->opt_friend = malloc(bytes * sizeof(int));
  b->captured_buf = malloc(bytes * sizeof(int));
  if (!b->cell || !b->queue || !b->group_buf || !b->free_buf || !b->check_buf
   || !b->tmp_free || !b->tmp_friend || !b->opt_free || !b->opt_friend
   || !b->captured_buf) {
    return(-1);
  }
  return(0);
}


static void bctx_free(struct bctx *b) {
  free(b->cell);
  free(b->queue);
  free(b->group_buf);
  free(b->free_buf);
  free(b->check_buf);
  free(b->tmp_free);
  free(b->tmp_friend);
  free(b->opt_free);
  free(b->opt_friend);
  free(b->captured_buf);
  memset(b, 0, sizeof(*b));
}


void ai_bender(const char *okp, int *movex, int *movey) {
  struct kropki_game *g = NULL;
  struct bctx b;
  int me, opp;
  int x, y;
  int best_x = -1, best_y = -1;
  double best_cost = -1.0;
  int best_tie = 0;
  int maxcap = 0, bestcap_x = -1, bestcap_y = -1;
  int maxthreat = 0, bestthr_x = -1, bestthr_y = -1;

  *movex = -1;
  *movey = -1;
  memset(&b, 0, sizeof(b));

  g = kropki_okp2struct(okp, NULL, NULL, 0);
  if (g == NULL) goto DONE;
  if (g->playablefields == 0) goto DONE;

  me = g->nextplayer;
  opp = 3 - me;

  /* opening */
  if (g->lastplayer == 0) {
    *movex = g->field_width / 2;
    *movey = g->field_height / 2;
    goto DONE;
  }

  if (bctx_alloc(&b, g) != 0) {
    *movex = g->playablefieldx;
    *movey = g->playablefieldy;
    goto DONE;
  }
  reset_costs(&b);

  /* --- tactical scan: captures & threats --- */
  {
    struct kropki_game *passg = kropki_copygame(g, kropki_NO_WALLS);
    if (passg != NULL) kropki_pass(passg, passg->nextplayer);

    for (y = 0; y < b.h; y++) {
      for (x = 0; x < b.w; x++) {
        int gain, thr;
        if (!is_playable(&b, x, y)) continue;
        gain = move_gain(g, x, y, me);
        if (gain > maxcap) {
          maxcap = gain;
          bestcap_x = x;
          bestcap_y = y;
        }
        if (passg != NULL) {
          thr = move_gain(passg, x, y, opp);
          if (thr > maxthreat) {
            maxthreat = thr;
            bestthr_x = x;
            bestthr_y = y;
          }
        }
      }
    }
    kropki_freegame(passg);
  }

  if (maxcap > 0 && maxcap >= maxthreat) {
    *movex = bestcap_x;
    *movey = bestcap_y;
    printf("ai_bender: CAPTURE [%d,%d] +%d\n", *movex, *movey, maxcap);
    goto DONE;
  }
  if (maxthreat > 0) {
    /* defend on the threatened intersection if it is playable for us;
     * otherwise grab our own capture if any */
    if (maxcap > 0) {
      *movex = bestcap_x;
      *movey = bestcap_y;
      printf("ai_bender: COUNTER-CAPTURE [%d,%d] +%d (threat=%d)\n",
             *movex, *movey, maxcap, maxthreat);
      goto DONE;
    }
    *movex = bestthr_x;
    *movey = bestthr_y;
    printf("ai_bender: DEFEND [%d,%d] threat=%d\n", *movex, *movey, maxthreat);
    goto DONE;
  }

  /* --- surround-efficiency influence map (dots4all core) --- */
  for (y = 0; y < b.h; y++) {
    for (x = 0; x < b.w; x++) {
      int d = dot_at(&b, x, y);
      if (d != me || is_captured(&b, x, y)) continue;
      clear_checked(&b);
      paint_from_seed(&b, x, y, opp); /* grow around our groups */
      summarize_cost(&b);
    }
  }
  for (y = 0; y < b.h; y++) {
    for (x = 0; x < b.w; x++) {
      int d = dot_at(&b, x, y);
      if (d != opp || is_captured(&b, x, y)) continue;
      clear_checked(&b);
      paint_from_seed(&b, x, y, me); /* grow around enemy groups */
      summarize_cost(&b);
    }
  }

  paint_liberty_urgency(&b, me, opp);

  /* pick hottest playable cell */
  for (y = 0; y < b.h; y++) {
    for (x = 0; x < b.w; x++) {
      double c;
      int tie, dlast, contact, i;
      if (!is_playable(&b, x, y)) continue;
      if (soft_veto(&b, x, y, me, opp)) continue;
      c = b.cell[idx_of(&b, x, y)].cost;

      dlast = 0;
      if (g->lastplayer != 0)
        dlast = abs_i(x - (int)g->lastmovex) + abs_i(y - (int)g->lastmovey);
      contact = 0;
      for (i = 0; i < ORTH_N; i++) {
        int p = dot_at(&b, x + ORTH[i][0], y + ORTH[i][1]);
        if (p == me) contact += 2;
        else if (p == opp) contact += 3;
      }
      /* tie-break key: closer to fight, more contact, prefer lower scan index as last resort */
      tie = contact * 100 - dlast;

      if (best_x < 0 || c > best_cost + 1e-12
       || (fabs(c - best_cost) <= 1e-12 && tie > best_tie)) {
        best_cost = c;
        best_tie = tie;
        best_x = x;
        best_y = y;
      }
    }
  }

  if (best_x < 0 || best_cost <= 0.0) {
    /* quiet board: stay near the last stone */
    int bx = (int)g->lastmovex, by = (int)g->lastmovey;
    int rad, found = 0;
    for (rad = 1; rad < b.w + b.h && !found; rad++) {
      for (y = by - rad; y <= by + rad; y++) {
        for (x = bx - rad; x <= bx + rad; x++) {
          if (!is_playable(&b, x, y)) continue;
          if (soft_veto(&b, x, y, me, opp)) continue;
          if (abs_i(x - bx) + abs_i(y - by) != rad) continue;
          best_x = x;
          best_y = y;
          found = 1;
          break;
        }
        if (found) break;
      }
    }
    if (!found) {
      best_x = g->playablefieldx;
      best_y = g->playablefieldy;
    }
  }

  *movex = best_x;
  *movey = best_y;
  printf("ai_bender: [%d,%d] cost=%.6f\n", *movex, *movey, best_cost);

DONE:
  bctx_free(&b);
  kropki_freegame(g);
}
