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

#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 "sanmpu.h"
#include "sanfen.h"
#include "sanpce.h"
#include "sanrst.h"
#include "sansel.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;

/*--> 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 --
      SANDecodeAux() only understands SAN text, not coordinates, so
      this is the CECP-side counterpart to it */
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 and from examples/09_pgn_tags.
 *    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 move-history capacity (hmpL=%d, i.e. %d full moves)\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\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[] -- see the ReportStatus() header comment.\n");
      printf ("0-1 {Black mates}\n");
      break;

    case gs_bcmt:
      fprintf (stderr,
        "# status: gs_bcmt -- checkmate, Black is mated (White "
        "wins). Reported directly rather than via cv_gtc_gsv[] / "
        "gtcstrv[] -- see the ReportStatus() header comment.\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 "
        "(SANDrawTestIMat(), sandrw.c: neither side has enough "
        "force left to mate on its own). Chess-theoretically "
        "correct, but cutechess-cli's own stricter referee has "
        "been observed rejecting this exact claim for endings such "
        "as king+bishop vs king+bishop or king+knight vs "
        "king+bishop and forfeiting the claiming engine ('Invalid "
        "result claim').\n");
      break;

    case gs_drep:
      fprintf (stderr,
        "# status: gs_drep -- draw, position repetition. NOTE: "
        "SANStatus() (sanrst.c) never actually produces this code "
        "on its own -- there is no repetition test in it at all; "
        "gs_drep only exists for the SAN Kit's manual "
        "result-declaration bookkeeping (SANGTCDetermine() in "
        "sangtc.c), which this engine never invokes. Seeing this "
        "would indicate a change elsewhere in the code.\n");
      break;

    case gs_dagr:
      fprintf (stderr,
        "# status: gs_dagr -- draw, agreed. Like gs_drep, never "
        "produced by SANStatus() itself; only reachable through "
        "manual draw-agreement bookkeeping this engine doesn't "
        "use.\n");
      break;

    case gs_wres:
      fprintf (stderr,
        "# status: gs_wres -- White resigns. Never produced by "
        "SANStatus() itself; only reachable through manual "
        "resignation bookkeeping this engine doesn't use.\n");
      break;

    case gs_bres:
      fprintf (stderr,
        "# status: gs_bres -- Black resigns. Never produced by "
        "SANStatus() itself; only reachable through manual "
        "resignation bookkeeping this engine doesn't use.\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.\n");
      printf ("resign\n");
      playing = 0;
      break;

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

  return (gs);
}

/*--> EngineMove: ask the active strategy for a move, play it, and
      report it to the GUI in coordinate notation */
static void
EngineMove (void)
{
  char coord[8];

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

  return;
}

static void
Usage (const char *progname)
{
  ssT s;

  fprintf (stderr, "usage: %s [strategy]\n", progname);
  fprintf (stderr, "  runs a CECP (xboard/WinBoard) engine on stdin/stdout\n");
  fprintf (stderr, "  known strategies (default: eat):");
  for (s = 0; s < ssL; s++)
    fprintf (stderr, " %s", ssstrv[s]);
  fprintf (stderr, "\n");

  return;
}

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

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

  SANInit ();

  strategy = ss_eat;
  if ((argc > 1) && ((strcmp (argv[1], "-h") == 0) ||
                      (strcmp (argv[1], "--help") == 0)))
    {
      Usage (argv[0]);
      return (0);
    };
  if (argc > 1)
    {
      if (SANCheckStrategy (argv[1]))
        strategy = SANEvalStrategy (argv[1]);
      else
        {
          fprintf (stderr, "%s: unknown strategy '%s'\n", argv[0], argv[1]);
          Usage (argv[0]);
          return (1);
        };
    };

  ssv[c_w] = ssv[c_b] = strategy;

  /* 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-C (%s)\" ", ssstrv[strategy]);
          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. 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. -- see the file header comment */
        };
    };

  return (0);
}
