/*
  SAN Kit Minimal Engine
  ======================
  
  Minimal CECP engine built on Steven Edwards' SAN Kit library */

/*
 * Protocol coverage and status handling (ReportStatus() below) also
 * work around two quirks in the underlying SAN Kit library itself,
 * both explained in full where they're used further down: a result
 * inversion for checkmate (gs_wcmt/gs_bcmt) in the library's own
 * cv_gtc_gsv[]/gtcstrv[] tables (sanint.c), and a fixed 200-move
 * capacity in its game-history array (gs_over, fmpL in sandef.h)
 * that the library mislabels as a drawn game once exceeded.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>

#include "sandef.h"
#include "sanvar.h"

#include "sanint.h"
#include "sangen.h"
#include "sanmer.h"
#include "sanmpu.h"
#include "sanfen.h"
#include "sanpce.h"
#include "sanrst.h"

/* controls whether the engine auto-replies after the opponent's
   move; toggled by force/go for entirely ordinary reasons, so it
   does NOT by itself mean the game is unrecoverably over */
static siT playing;

/* set once, permanently, when the move-history capacity safety net
   in ReportStatus() fires (see there): unlike "playing", this must
   never be turned back on short of a fresh "new"/"setboard", since
   it exists specifically to stop hn from ever growing again -- see
   the usermove handler in main(), which refuses to apply *any*
   further move once this is set, even if the GUI or the opponent's
   engine keeps sending them after our "resign" */
static siT capacity_resigned;

/* the largest number of legal moves possible in any reachable chess
   position is 218; this is comfortable headroom for the per-ply
   "is this candidate safe" scratch array in ChooseMove() below */
#define MAX_MOVES 256

/*--> MoveToCoord: format a move as plain coordinate notation
      ("e2e4", or "e7e8q" for a promotion) rather than SAN, since
      that is what the CECP protocol expects back */
static void
MoveToCoord (mptrT mptr, char *buf)
{
  fileT ffile, tfile;
  rankT frank, trank;
  pT promo;

  ffile = map_file_sq (mptr->m_frsq);
  frank = map_rank_sq (mptr->m_frsq);
  tfile = map_file_sq (mptr->m_tosq);
  trank = map_rank_sq (mptr->m_tosq);

  buf[0] = acfv[ffile];
  buf[1] = acrv[frank];
  buf[2] = acfv[tfile];
  buf[3] = acrv[trank];
  buf[4] = '\0';

  promo = cv_p_scmvv[mptr->m_scmv];
  if (promo != p_x)
    {
      buf[4] = (char) tolower ((unsigned char) acpv[promo]);
      buf[5] = '\0';
    };

  return;
}

/*--> LooksLikeCoordMove: strict syntax check for "e2e4"-style text,
      so protocol keywords (e.g. "easy", "draw", "hint") are never
      mistaken for a badly-typed move just because they start with a
      letter that happens to also be a file letter */
static siT
LooksLikeCoordMove (const char *s)
{
  size_t len;

  len = strlen (s);
  if ((len != 4) && (len != 5))
    return (0);
  if (!SANCheckFile (s[0]) || !SANCheckRank (s[1]))
    return (0);
  if (!SANCheckFile (s[2]) || !SANCheckRank (s[3]))
    return (0);
  if (len == 5)
    {
      char pc = (char) tolower ((unsigned char) s[4]);
      if ((pc != 'q') && (pc != 'r') && (pc != 'b') && (pc != 'n'))
        return (0);
    };

  return (1);
}

/*--> FindMoveByCoord: locate the legal move matching a coordinate
      string, by generating the legal move list and comparing
      from-square/to-square/promotion piece against each entry */
static mptrT
FindMoveByCoord (const char *s)
{
  siT i;
  mptrT mptr, found;
  fileT ffile, tfile;
  rankT frank, trank;
  sqT frsq, tosq;
  pT promo;

  if (!LooksLikeCoordMove (s))
    return (NULL);

  ffile = SANEvalFile (s[0]);
  frank = SANEvalRank (s[1]);
  tfile = SANEvalFile (s[2]);
  trank = SANEvalRank (s[3]);
  frsq = map_sq_rank_file (frank, ffile);
  tosq = map_sq_rank_file (trank, tfile);

  promo = p_x;
  if (strlen (s) == 5)
    {
      char pc = (char) tolower ((unsigned char) s[4]);
      if (pc == 'q') promo = p_q;
      else if (pc == 'r') promo = p_r;
      else if (pc == 'b') promo = p_b;
      else if (pc == 'n') promo = p_n;
    };

  SANGenMoves ();

  found = NULL;
  for (i = 0; (found == NULL) && (i < curr_g.g_gmvc); i++)
    {
      mptr = &curr_g.g_base[i];
      if ((mptr->m_frsq == frsq) && (mptr->m_tosq == tosq))
        {
          if (promo == p_x)
            found = mptr;
          else if (cv_p_scmvv[mptr->m_scmv] == promo)
            found = mptr;
        };
    };

  return (found);
}

/*--> ReportStatus: inspect SANStatus() and react to it.
 *
 * Every one of the thirteen gsT values SANStatus() can return is
 * handled explicitly below, and every branch writes at least one
 * '#' comment line to stderr explaining what that code means and
 * why this engine reacts the way it does -- CECP GUIs ignore lines
 * starting with '#' on stdout, but since these are diagnostic
 * rather than protocol output they go to stderr instead, so they
 * never risk being misread as part of the protocol stream.
 *
 * Two things are deliberately NOT taken from the SAN Kit's own
 * cv_gtc_gsv[]/gtcstrv[] result tables (sanint.c), and are computed
 * here instead:
 *
 *  - gs_wcmt/gs_bcmt (checkmate): cv_gtc_gsv[] maps these backwards
 *    (gs_wcmt, "White is checkmated" i.e. Black wins, is mapped to
 *    gtc_wwin, "White win" -- and gs_bcmt is mapped the same wrong
 *    way round). This is a genuine, reproducible bug in the
 *    original 1994 sanint.c, not a misunderstanding on our part:
 *    feed this engine the moves for the "fool's mate" (Black mates)
 *    and it will report "1-0" if it trusts that table, and "0-1"
 *    for the "scholar's mate" (White mates). Since SANPGNEventReport
 *    (sanpgn.c) uses the very same table, this also affects PGN
 *    export from the stock "san" tool. We simply don't use the
 *    table for these two codes.
 *
 *  - gs_over: see below -- not a chess result at all.
 *
 * Returns the gsT value it examined, so callers can decide whether
 * to keep playing without querying SANStatus() a second time.
 */
static gsT
ReportStatus (void)
{
  gsT gs;

  /* SANStatus()'s own gs_over case (see below) checks fmvn *after*
     several other statuses in a strict priority chain (gs_bust,
     gs_dhmc, gs_wcmt/gs_bcmt, gs_wsmt/gs_bsmt, gs_dmat, *then*
     gs_over -- see sanrst.c), each guarded by "if (gs == gs_norm)".
     Once an earlier one of those becomes permanently true --
     gs_dmat is the practical case, since captured material never
     comes back -- gs_over can never be reached again, no matter
     how far fmvn climbs afterwards: a self-play game was observed
     reaching insufficient material at move 198 (two moves *before*
     the 200-move mark), after which SANStatus() reported gs_dmat
     continuously all the way past the intended resignation point,
     right up to the move-history array's true hard capacity
     (hn == hL), which aborted the process via SANFatal()/exit(1)
     instead of ever resigning -- reported by the GUI as a raw
     disconnect, not a clean game result.
     So this check is done directly against the live history
     counter hn, before SANStatus() runs at all, precisely so nothing
     else can ever mask it. hmpL (fmpL full moves' worth of plies)
     is used rather than the library's own hL, to resign at the
     originally-intended 200-move mark with comfortable margin
     below the true crash point (hL = hmpL + a small fixed-size
     safety cushion the original 1994 code reserved, not meant to
     be played into). */
  if (hn >= hmpL)
    {
      fprintf (stderr,
        "# hn=%d has reached the SAN Kit's intended move-history "
        "capacity (hmpL=%d, i.e. %d full moves -- see sandef.h); "
        "resigning now regardless of what SANStatus() reports, "
        "since that check can be permanently masked (see the "
        "comment above) and this must never be allowed to run into "
        "the library's true hard limit and abort the process.\n",
        (int) hn, (int) hmpL, (int) fmpL);
      printf ("resign\n");
      playing = 0;
      capacity_resigned = 1;
      return (gs_over);
    };

  gs = SANStatus ();

  switch (gs)
    {
    case gs_norm:
      fprintf (stderr, "# status: gs_norm -- normal, game continues\n");
      break;

    case gs_bust:
      fprintf (stderr,
        "# status: gs_bust -- illegal position (SANCensus(), see "
        "sanmsc.c, failed: wrong king count, castling rights "
        "inconsistent with the actual king/rook placement, a pawn "
        "on the back rank, a piece surplus, or the side not on "
        "move left in check -- in this engine, only reachable via "
        "a syntactically-valid but chess-illegal 'setboard' FEN). "
        "Move generation is skipped entirely for an illegal "
        "position (see SANUpdate() in sanval.c), so curr_g may be "
        "stale; no move can safely be selected from here.\n");
      printf ("tellusererror illegal position (see setboard)\n");
      playing = 0;
      break;

    case gs_wcmt:
      fprintf (stderr,
        "# status: gs_wcmt -- checkmate, White is mated (Black "
        "wins). Reported directly rather than via cv_gtc_gsv[] / "
        "gtcstrv[] (sanint.c), which map this the wrong way "
        "round -- see the ReportStatus() header comment above.\n");
      printf ("0-1 {Black mates}\n");
      break;

    case gs_bcmt:
      fprintf (stderr,
        "# status: gs_bcmt -- checkmate, Black is mated (White "
        "wins). Same cv_gtc_gsv[] inversion as gs_wcmt, reported "
        "directly for the same reason.\n");
      printf ("1-0 {White mates}\n");
      break;

    case gs_wsmt:
      fprintf (stderr,
        "# status: gs_wsmt -- stalemate, White has no legal move "
        "(draw)\n");
      printf ("1/2-1/2 {White stalemated}\n");
      break;

    case gs_bsmt:
      fprintf (stderr,
        "# status: gs_bsmt -- stalemate, Black has no legal move "
        "(draw)\n");
      printf ("1/2-1/2 {Black stalemated}\n");
      break;

    case gs_dhmc:
      fprintf (stderr,
        "# status: gs_dhmc -- draw, fifty-move rule (halfmove "
        "clock reached hmvcL, see sandef.h)\n");
      printf ("1/2-1/2 {Draw by fifty-move rule}\n");
      break;

    case gs_dmat:
      fprintf (stderr,
        "# status: gs_dmat -- draw, insufficient mating material. "
        "Chess-theoretically correct, but not always accepted by "
        "cutechess-cli's own stricter referee, which has been seen "
        "forfeiting the claiming engine over it ('Invalid result "
        "claim'); this engine deliberately does not volunteer this "
        "claim -- see the README -- but still has plenty of legal "
        "moves available, so the usermove handler keeps play going "
        "for this code exactly as for gs_norm; stopping here "
        "instead would leave the engine silent forever and get it "
        "killed as a stalled connection.\n");
      break;

    case gs_drep:
      fprintf (stderr,
        "# status: gs_drep -- draw, position repetition. "
        "SANStatus() never actually produces this on its own (no "
        "repetition test exists in sanrst.c); only reachable "
        "through manual result-declaration bookkeeping (sangtc.c) "
        "this engine doesn't use.\n");
      break;

    case gs_dagr:
      fprintf (stderr,
        "# status: gs_dagr -- draw, agreed. Like gs_drep, never "
        "produced by SANStatus() itself in this engine.\n");
      break;

    case gs_wres:
      fprintf (stderr,
        "# status: gs_wres -- White resigns. Never produced by "
        "SANStatus() itself in this engine.\n");
      break;

    case gs_bres:
      fprintf (stderr,
        "# status: gs_bres -- Black resigns. Never produced by "
        "SANStatus() itself in this engine.\n");
      break;

    case gs_over:
      /* kept for defensive completeness; the hn >= hmpL check at
         the top of this function fires first and resigns before
         SANStatus() itself would ever reach this case in practice
         (see that check's comment for why relying on this branch
         alone isn't reliable) */
      fprintf (stderr,
        "# status: gs_over -- SAN Kit move-history capacity (200 "
        "full moves, fmpL in sandef.h) reached. NOT a chess draw "
        "-- see the header comment in "
        "../xboard-engine/xboard-engine.c. Resigning instead of "
        "repeating the library's own false '1/2-1/2' claim.\n");
      printf ("resign\n");
      playing = 0;
      capacity_resigned = 1;
      break;

    default:
      fprintf (stderr, "# status: unrecognized gsT value %d\n", (int) gs);
      break;
    };

  return (gs);
}

/*--> ChooseMove: pick a move for the side currently to move.
 *
 * Priority order:
 *
 *   1. Deliver checkmate immediately if any legal move does so.
 *      This needs no search of our own at all: SANGenMoves()
 *      already flags every candidate move with mf_chmt whenever
 *      playing it would checkmate the opponent (see
 *      SANMLScanMate() in sanmlm.c, which SANGenMoves() always
 *      calls) -- we just scan for the flag.
 *
 *   2. Otherwise, avoid moves that let the opponent deliver
 *      checkmate on their very next reply. For every candidate:
 *      play it with SANExecute() (the same lightweight,
 *      history-free primitive SANEnumerate() itself uses --
 *      SANPlayMove() would be wrong here, since it would pollute
 *      the *real* game history with moves we're only trying out),
 *      regenerate the opponent's replies with SANGenMoves() -- which
 *      again come back pre-flagged for any mate-in-one of theirs --
 *      and note whether one exists, then undo with SANRetract().
 *      This is a plain one-ply-deeper safety check, not a real
 *      search: it only ever looks at the two half-moves directly in
 *      front of it. SANExecute()/SANRetract() save and restore
 *      curr_e/curr_g through the av[an] analysis stack, and each
 *      depth's move list lives in its own slice of the shared Tree[]
 *      array (see g_strt in sangen.c), so this scan never disturbs
 *      the root candidate list it's iterating over.
 *
 *   3. Among whatever candidates are left (the "safe" ones, or
 *      every legal move if none turned out to be safe -- i.e. the
 *      position is simply lost against best defense), prefer the
 *      move that captures the most valuable piece (using the SAN
 *      Kit's own valcpv[] material table), breaking ties in favor
 *      of a checking move, and otherwise at random.
 *
 * With at most a few dozen legal moves at each of the two plies
 * step 2 looks at, this whole scan finishes in a few milliseconds
 * on any real position. The clock check inside step 2's loop is a
 * purely defensive fallback for the 0.5s budget asked for -- it is
 * not expected to ever actually trigger, since even the theoretical
 * maximum of 218 legal moves in a single position, squared, is
 * still a trivial amount of work for a modern CPU; any candidate
 * left unexamined because of it is optimistically treated as safe
 * rather than leaving the engine without a move to play.
 */
static mptrT
ChooseMove (void)
{
  siT i, j, n;
  siT safe[MAX_MOVES];
  siT safe_count;
  clock_t deadline;
  mptrT cand, best;
  cpevT score, best_score;
  siT is_check, best_is_check;

  SANGenMoves ();
  n = curr_g.g_gmvc;

  /* priority 1: checkmate in one */

  for (i = 0; i < n; i++)
    if (curr_g.g_base[i].m_flag & mf_chmt)
      return (&curr_g.g_base[i]);

  /* priority 2: mark candidates that do NOT allow an opponent
     mate-in-one reply; default every candidate to "safe" so that
     the defensive time-budget cutoff below degrades gracefully */

  for (i = 0; i < n; i++)
    safe[i] = 1;
  safe_count = n;

  deadline = clock () + (CLOCKS_PER_SEC / 2);

  for (i = 0; i < n; i++)
    {
      if (clock () >= deadline)
        break;                  /* defensive only -- see header comment */

      cand = &curr_g.g_base[i];
      if (cand->m_flag & mf_bust)
        continue;                /* defensive; curr_g is already
                                     policed by SANGenMoves(), so this
                                     should never actually be set */

      curr_g.g_curr = cand;
      SANExecute ();

      SANGenMoves ();
      for (j = 0; j < curr_g.g_gmvc; j++)
        if (curr_g.g_base[j].m_flag & mf_chmt)
          {
            safe[i] = 0;
            safe_count--;
            break;
          };

      SANRetract ();
    };

  /* priority 3: among the safe candidates (or every candidate, if
     none turned out to be safe), prefer the best capture, then a
     checking move, then break remaining ties at random */

  best = NULL;
  best_score = -1;
  best_is_check = 0;

  for (i = 0; i < n; i++)
    {
      if ((safe_count > 0) && !safe[i])
        continue;

      cand = &curr_g.g_base[i];
      score = valcpv[cand->m_tocp];       /* 0 if not a capture */
      is_check = (cand->m_flag & mf_chec) ? 1 : 0;

      if ((best == NULL) ||
          (score > best_score) ||
          ((score == best_score) && is_check && !best_is_check) ||
          ((score == best_score) && (is_check == best_is_check) &&
           ((rand () % 2) == 0)))
        {
          best = cand;
          best_score = score;
          best_is_check = is_check;
        };
    };

  return (best);
}

/*--> EngineMove: pick a move, play it, and report it to the GUI in
      coordinate notation
 *
 * ChooseMove() returns a pointer into the shared Tree[] move-tree
 * storage (see its header comment). That pointer is only valid
 * until the next move-list generation -- and SANPlayMove() below
 * triggers exactly that internally, to compute the *new* position's
 * own move list (via SANUpdate(), see sanval.c), reusing the very
 * same Tree[] memory the just-played move's data lived in. So the
 * chosen move is copied by value into a local mT before
 * SANPlayMove() ever runs, rather than kept as a pointer across that
 * call.
 */
static void
EngineMove (void)
{
  char coord[8];
  mptrT mptr;
  mT chosen;

  mptr = ChooseMove ();
  if (mptr == NULL)
    {
      /* should not happen: EngineMove() is only ever called when
         ReportStatus() has just confirmed gs_norm, which guarantees
         at least one legal move exists */
      fprintf (stderr, "# ChooseMove() unexpectedly found no move\n");
      printf ("resign\n");
      playing = 0;
      return;
    };
  chosen = *mptr;

  SANPlayMove (&chosen);
  MoveToCoord (&chosen, coord);
  printf ("move %s\n", coord);
  (void) ReportStatus ();

  return;
}

static void
Usage (const char *progname)
{
  fprintf (stderr, "usage: %s [-h]\n", progname);
  fprintf (stderr, "  runs a CECP (xboard/WinBoard) engine on "
                    "stdin/stdout\n");
  fprintf (stderr, "  plays mate-in-one when available, and avoids "
                    "moves that\n");
  fprintf (stderr, "  allow the opponent to mate in one, otherwise\n");

  return;
}

int
main (int argc, char *argv[])
{
  char line[256];
  mptrT mptr;

  /* CECP is a line-oriented protocol over stdin/stdout: never let
     output buffering delay a reply */
  setvbuf (stdout, NULL, _IONBF, 0);

  if ((argc > 1) && ((strcmp (argv[1], "-h") == 0) ||
                      (strcmp (argv[1], "--help") == 0)))
    {
      Usage (argv[0]);
      return (0);
    };

  srand ((unsigned) time (NULL));

  SANInit ();

  /* the CECP protocol has its own move/board reporting; suppress the
     SAN Kit's own automatic board and move display so it doesn't get
     mixed into the protocol stream */
  optnv[optn_nabd] = 1;
  optnv[optn_namd] = 1;

  playing = 1;          /* auto-reply to the opponent's moves */

  while (fgets (line, sizeof (line), stdin) != NULL)
    {
      line[strcspn (line, "\r\n")] = '\0';

      if (strcmp (line, "xboard") == 0)
        {
          /* nothing to do */
        }
      else if (strncmp (line, "protover", 8) == 0)
        {
          printf ("feature myname=\"SME-D\" ");
          printf ("setboard=1 usermove=1 sigint=0 sigterm=0 ");
          printf ("colors=0 draw=0 analyze=0 variants=\"normal\" done=1\n");
        }
      else if (strcmp (line, "new") == 0)
        {
          SANInitGround ();
          playing = 1;
          capacity_resigned = 0;
          (void) ReportStatus ();
        }
      else if (strncmp (line, "setboard ", 9) == 0)
        {
          if (!SANFENSet (line + 9))
            fprintf (stderr, "tellusererror illegal FEN\n");
          else
            {
              capacity_resigned = 0;
              (void) ReportStatus ();
            };
        }
      else if (strcmp (line, "force") == 0)
        {
          playing = 0;
        }
      else if (strcmp (line, "go") == 0)
        {
          if (capacity_resigned)
            fprintf (stderr,
              "# ignoring 'go': already resigned for move-history "
              "capacity; send 'new' or 'setboard' to start over\n");
          else
            {
              playing = 1;
              EngineMove ();
            };
        }
      else if (strcmp (line, "undo") == 0)
        {
          if (hn > 0)
            SANUnplayMove ();
        }
      else if (strcmp (line, "remove") == 0)
        {
          if (hn > 0) SANUnplayMove ();
          if (hn > 0) SANUnplayMove ();
        }
      else if ((strncmp (line, "usermove ", 9) == 0) ||
               LooksLikeCoordMove (line))
        {
          char *mv;

          mv = (strncmp (line, "usermove ", 9) == 0) ? line + 9 : line;

          if (capacity_resigned)
            {
              /* refuse to apply *any* further move once resigned for
                 move-history capacity, no matter how many more the
                 GUI or the opponent's engine sends: SANPlayMove()
                 unconditionally pushes to that same history array
                 regardless of "playing", so continuing to accept
                 moves here (as this engine used to) can still walk
                 straight into the library's true hard capacity and
                 abort the process, exactly as if this check weren't
                 here at all */
              fprintf (stderr,
                "# ignoring usermove %s: already resigned for "
                "move-history capacity; send 'new' or 'setboard' to "
                "start over\n", mv);
            }
          else
            {
              mptr = FindMoveByCoord (mv);
              if (mptr == NULL)
                printf ("Illegal move: %s\n", mv);
              else
                {
                  gsT gs;

                  SANPlayMove (mptr);
                  gs = ReportStatus ();

                  /* gs_dmat is deliberately never *announced* above
                     (see ReportStatus()), but the game is still very
                     much ongoing -- plenty of legal moves remain in
                     a bare king+bishop vs king+bishop ending, say,
                     just no way to force mate. Treating it the same
                     as "not gs_norm, stop responding" here would
                     leave this engine replying to absolutely nothing
                     until the GUI's own timeout kills it as a
                     stalled connection -- exactly as harmful as the
                     false claim this was written to avoid in the
                     first place (this was observed to actually
                     happen, in the sibling xboard-engine, in a
                     self-play tournament game). So gs_dmat keeps
                     play going, same as gs_norm; only the truly
                     final codes (checkmate, stalemate, the
                     fifty-move rule, gs_over, gs_bust) actually stop
                     it. */
                  if (((gs == gs_norm) || (gs == gs_dmat)) && playing)
                    EngineMove ();
                };
            };
        }
      else if (strncmp (line, "ping ", 5) == 0)
        {
          printf ("pong %s\n", line + 5);
        }
      else if (strcmp (line, "quit") == 0)
        {
          break;
        }
      else
        {
          /* silently ignore anything else: level, time, otim, st,
             sd, hard, easy, post, nopost, hint, draw, result,
             black, white, computer, name, rating, ics, accepted,
             rejected, etc. */
        };
    };

  return (0);
}
