// xboard232.c, Peter McGavin, 1 July 2002
//
// This file was originally based on Rémi Coulom's win_a232.cpp, then
// extensively modified.  I would not recommend using this file as a
// base for new Auto232 programs.  Find a copy of the original code
// instead.  Rémi's code contained the following conditions of use:
//
//-----------------------------------------------------------------------
//-  Module:  win_a232.cpp                                              -
//-  Project: Windows-95 Autoplayer.                                    -
//-  Purpose: Reference Implementation of Auto232-Driver for Windows-95 -
//-  History:                                                           -
//-  ??.??.1994: Auto232-Specification by Ch.Donninger.                 -
//-  ??.??.1997 Windows-95/NT C-Version by Stefan Meyer-Kahlen.         -
//-  08.07.1997 Modifications for MFC-use and better separation of      -
//-             autoplayer-library and chess-program by Ch.Donninger.   -
//-             Windows-NT specific named pipe code removed.            -
//-  08.12.1998 Changes by Rémi Coulom                                  -
//-              * MFC-specific code removed                            -
//-              * Extended mode bug corrected                          -
//-              * Some comments added and spelling corrected           -
//-  09.06.1998  * Extended mode bug correction corrected               -
//-              * Option to trace data exchange                        -
//-              * Bug of savegame message for slave corrected          -
//-  10.31.1998  * fAlternate flag added                                -
//-              * Setable Baud rate                                    -
//-  11.29.1998  * Mutex added to avoid simultaneous ReadPacket and     -
//-              SendPacket that could cause bugs when the first move   -
//-              of the master could collide with the extended mode     -
//-              reply from the slave.                                  -
//-  07.17.1999  * Option added to not use extended mode                -
//-              * Baudrate can not be changed any more.                -
//-              * Time interval between bytes sent can be changed      -
//-  05.18.2000  * Upgrade with new extensions of the protocol          -
//-              * Support for connection via named pipes by Stefan     -
//-              Meyer-Kahlen added (it had been remove by Ch.          -
//-              Donninger)                                             -
//-  07.16.2000  * crt-specific _beginthread removed for cygwin         -
//-              compatibility                                          -
//-  (c) 1997 by Stefan Meyer-Kahlen (Original Code).                   -
//-  (c) 1997 by Ch.Donninger (Auto232 Specification, Modifications,    -
//-                            Documentation).                          -
//-  Copyright Conditions:                                              -
//-  This code can be used freely by other chess programmers under the  -
//-  following conditions:                                              -
//-  A) The History and Copyright-comments must be left in the code.    -
//-  B) The code or any executables produced by it must not be sold     -
//-     (this is also valid for modifications of this code).            -
//-  C) Executables/Object-code can be distributed in a bundled form    -
//-     with chess-programs. There must be NO extra price tag for the   -
//-     the autoplayer-version of the chess program.                    -
//-  D) Credit should be given in the About-Box of the program to       -
//-     Stefan Meyer-Kahlen and Ch.Donninger. (This point is only a     -
//-     question of etiquette).                                         -
//-  E) The authors can not be blamed for bugs. It is the duty of each  -
//-     chess programmer to test the code and fix bugs. Serious bugs    -
//-     should be reported to the authors.                              -
//-  F) This code can be distributed freely to other chess programmers  -
//-     if the copyright conditions above are NOT changed.              -
//-  Note: Please consider this implementation as a "Standard". The     -
//-     autoplayer-specification has due to historic reasons some       -
//-     limitations, but it works. Refrain from incompatible extensions -
//-     and changes.                                                    -
//-----------------------------------------------------------------------

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/time.h>
#include <unistd.h>
#include <ctype.h>

#include "util.h"
#include "xboard232.h"
#include "unix_serial.h"

/**********************************************************************/
static int Auto232Quit = 0;        // If TRUE, disconnect from serial port.
static int Auto232GameNum = 1;     // Number of current game
static int Auto232NumGames = 1;    // Number of games for the whole autoplayer session.
static int Auto232HandShake = 0;   // If TRUE, connection established.
static int Auto232ComOn = 0;       // If TRUE, serial port initialized and serial
static int Auto232Master = 0;      // If TRUE, chess program is master (otherwise slave).
static int fExtendedOpponent = 0;

static const char *movetypestr[] = {"N", "CAP", "EP", "OO", "OOO"};

/**********************************************************************/
//--------------------------------------------------------------------------
//- A232 protocol assumes the following square enumeration:                -
//- A1==0, B1==1, ... ,A2==8, ... H8==63.                                  -
//- Parameters:                                                            -
//-    from - From Square of Move.                                         -
//-    to   - To Square of Move. In case of promotion the highest two bits -
//-           have the following meaning:                                  -
//-           0x00 Promotion to Queen (just plain to-square).              -
//-           0x40 Promotion to Rook.                                      -
//-           0x80 Promotion to Bishop.                                    -
//-           0xC0 Promotion to Knight.                                    -
//-   Note: Underpromotion is only handled if both drivers support         -
//-         Extended Mode (Windows-95 Driver should support this,          -
//-         DOS-Drivers do not).                                           -
//-   movetype - Type of chess-move.                                       -
//-              A232-CAPMOVE for Capture-Move.                            -
//-              A232_EPMOVE for Enpassant-Move.                           -
//-              A232_OOMOVE for Short-Castling-Move.                      -
//-              A232_OOOMOVE for Long-Castling-Move.                      -
//-              A232_NMOVE   for all other moves.                         -
//-   Note: There is for historic reasons no own flag for Promotion.       -
//-         Promotion is a normal move (A232_NMOVE).                       -
//- Return Value: TRUE, if move could be sent, otherwise FALSE.            -
//--------------------------------------------------------------------------
static void ConvertXboardMoveToA232 (char *move, unsigned char *from,
				     unsigned char *to,
				     unsigned char *movetype)
{
  if (tolower(move[0]) < 'a' || tolower(move[0]) > 'h' ||
      move[1] < '1' || move[1] > '8' ||
      tolower(move[2]) < 'a' || tolower(move[2]) > 'h' ||
      move[3] < '1' || move[3] > '8')
    error ("Invalid format for xboard move: %s", move);
  *from = ((move[1] - '1') << 3) + (tolower(move[0]) - 'a');
  *to = ((move[3] - '1') << 3) + (tolower(move[2]) - 'a');
  *movetype = A232_NMOVE;
  switch (tolower(move[4])) {
  case 'r':
    *to |= 0x40;
    break;
  case 'b':
    *to |= 0x80;
    break;
  case 'n':
    *to |= 0xc0;
    break;
  default:
    break;
  }
}

/**********************************************************************/
static void ConvertA232MoveToXboard (unsigned char from, unsigned char to,
				     unsigned char movetype, char *move)
{
  move[0] = 'a' + (from & 7);
  move[1] = '1' + (from >> 3);
  move[2] = 'a' + (to & 7);
  move[3] = '1' + ((to & 0x3f) >> 3);
  switch (to & 0xc0) {
  case 0x40:
    move[4] = 'r';
    break;
  case 0x80:
    move[4] = 'b';
    break;
  case 0xc0:
    move[4] = 'n';
    break;
  default:
    move[4] = '\0';
    break;
  }
  move[5] = '\0';
}

/**********************************************************************/
static void DumpPacket (unsigned char packet[5])
{
  if (packet[0] == A232_STX)
    debug ("%02x %02x %02x %02x %02x\n",
	   packet[0] & 0xff, packet[1] & 0xff, packet[2] & 0xff,
	   packet[3] & 0xff, packet[4] & 0xff);
#if 0
    debug ("%02x %02x %02x %02x %02x (\"%c%c%c%c%c\")\n",
	   packet[0] & 0xff, packet[1] & 0xff, packet[2] & 0xff,
	   packet[3] & 0xff, packet[4] & 0xff,
	   packet[0], packet[1], packet[2], packet[3], packet[4]);
#endif
  else if (packet[0] == A232_ACK)
#if 0
    debug ("A232_ACK %02x (\"%c\")\n", packet[0] & 0xff, packet[0]);
#endif
    debug ("A232_ACK %02x\n", packet[0] & 0xff);
  else
    debug ("Bad packet: %02x %02x %02x %02x %02x\n",
	   packet[0] & 0xff, packet[1] & 0xff, packet[2] & 0xff,
	   packet[3] & 0xff, packet[4] & 0xff);
}

/**********************************************************************/
static int Auto232SendPacket (unsigned char packet[5], int confirm)
{
  int attempts, n;
  fd_set rfds;    /* file descriptor set */
  struct timeval t;
  unsigned char c;

  if (!Auto232ComOn)
    return FALSE;
  attempts = A232_ATTEMPTS;

  while (attempts--) {
    debug ("Sending packet: ");
    DumpPacket (packet);
    if (write (serial_fd, packet, 5) != 5)
      fatal ("Error writing packet to serial port");
    if (!confirm) {
      return TRUE; // Do not wait for confirmation.
    }
    // wait up to 3 seconds for ACK
    do {
      FD_ZERO (&rfds);
      FD_SET (serial_fd, &rfds);
      t.tv_sec = 3;
      t.tv_usec = 0;
      if ((n = select (serial_fd + 1, &rfds, NULL, NULL, &t)) < 0)
	fatal ("select() failed: %d", n);
      if (FD_ISSET (serial_fd, &rfds) &&
	  read (serial_fd, &c, 1) == 1 &&
	  c == A232_ACK)
	return TRUE;
      if (n > 0)
	debug ("No A232_ACK received in 3 seconds\n");
    } while (n > 0);
  }
  return FALSE;
}

/**********************************************************************/
static int Auto232SendCommand (unsigned char command, int confirm)
{
  unsigned char packet[5];

  packet[0] = A232_STX;
  packet[1] = A232_COMMAND;
  packet[2] = command;
  packet[3] = 0;
  packet[4] = A232_ETX;
  return Auto232SendPacket (packet, confirm);
}

/**********************************************************************/
static int Auto232SendMove (unsigned char from, unsigned char to,
			    unsigned char movetype)
{
  static unsigned char packet[5];

  if (!Auto232ComOn || !Auto232HandShake)
    return FALSE;
  packet[0] = A232_STX;
  packet[1] = movetype;
  packet[2] = from;
  packet[3] = to;
  packet[4] = A232_ETX;
  return Auto232SendPacket (packet, TRUE);
}

/**********************************************************************/
static int Auto232StartMatch (void)
{
  static unsigned char packet[5];

  if(Auto232HandShake || !Auto232ComOn)
    // Match already requested or communication port not yet set.
    return FALSE;
  Auto232GameNum = 0;
  // Send request for match.
  packet[0] = A232_STX;
  packet[1] = A232_REQUESTMATCH;
  packet[2] = Auto232NumGames;
  packet[3] = A232_EXTENDEDCOMMANDMODE;
  packet[4] = A232_ETX;
  Auto232HandShake = Auto232SendPacket (packet, TRUE);
  debug ("Setting Auto232HandShake = %d\n", Auto232HandShake);
  return Auto232HandShake;
}

/**********************************************************************/
static void Auto232Close (void)
{
   if (!Auto232ComOn)
      return;
   Auto232Quit = TRUE;
   sleep (1);
}

/**********************************************************************/
static int Auto232NextGame (void)
{
  static unsigned char packet[5];

  if (Auto232Master && Auto232HandShake)   { // Only Master can cancel a game.
    packet[0] = A232_STX;
    packet[1] = A232_CANCELGAME;
    packet[2] = 0;
    packet[3] = 0;
    packet[4] = A232_ETX;
    Auto232SendPacket (packet, FALSE);
    sleep (1);
    return TRUE;
  }
  return FALSE;
}

/**********************************************************************/
static void ReceiveA232Packet (unsigned char packet[5])
{
  unsigned char c;
  static char move[6] = "";

  //  debug ("Received A232 packet: ");
  //  DumpPacket (packet);
  if (packet[0] == A232_STX) { // Command-Packet
    //    debug ("Sending A232_ACK\n");
    c = A232_ACK;
    if (write (serial_fd, &c, 1) != 1)
      fatal ("write() of A232_ACK to serial failed");
    switch (packet[1])  { // Type of command.
    case A232_ILLEGALMOVE:
      printf ("Illegal move: %s\n", move);
      fflush (stdout);
      break;
    case A232_COMMAND:
      switch(packet[2]) {
      case A232_COM_COMPUTE:
	debug ("Received A232_COM_COMPUTE\n");
	break;
      case A232_COM_STOP:
	debug ("Received A232_COM_STOP\n");
	break;
      case A232_COM_NEWGAME:
	debug ("Received A232_COM_NEWGAME\n");
	break;
      case A232_COM_TAKEBACK:
	debug ("Received A232_COM_TAKEBACK\n");
	break;
      case A232_COM_MEMOON:
	debug ("Received A232_COM_MEMOON\n");
	break;
      case A232_COM_MEMOOFF:
	debug ("Received A232_COM_MEMOOFF\n");
	break;
      case A232_COM_FLIPBOARD:
	debug ("Received A232_COM_FLIPBOARD\n");
	break;
      case A232_COM_OFFERDRAW:
	debug ("Received A232_COM_OFFERDRAW\n");
	printf ("offer draw\n");
	fflush (stdout);
	break;
      case A232_COM_ACCEPTDRAW:
	debug ("Received A232_COM_ACCEPTDRAW\n");
	printf ("offer draw\n");
	fflush (stdout);
	break;
      case A232_COM_REJECTDRAW:
	debug ("Received A232_COM_REJECTDRAW\n");
	break;
      case A232_COM_RESIGN:
	debug ("Received A232_COM_RESIGN\n");
	printf ("resign\n");
	fflush (stdout);
	break;
      case A232_COM_BOOKON:
	debug ("Received A232_COM_BOOKON\n");
	break;
      case A232_COM_BOOKOFF:
	debug ("Received A232_COM_BOOKOFF\n");
	break;
      default:
	error ("Received unexpected case for A232_COMMAND : %02x",
		 packet[2] & 0xff);
      }
      break;
    case A232_CANCELGAME:
      debug ("Received A232_CANCELGAME\n");
      // Master wants to cancel the game
      break;
    case A232_SAVEGAME:
      debug ("Received A232_SAVEGAME\n");
      Auto232GameNum = packet[2];
      break;
    case A232_CONTINUEGAME:
      debug ("Received A232_CONTINUEGAME\n");
      break;
    case A232_FRITZ:
      debug ("Received A232_FRITZ\n");
      printf ("feature myname=\"Fritz Auto232\"\n");
      fflush (stdout);
      break;
    case A232_NMOVE:
    case A232_CAPMOVE:
    case A232_EPMOVE :
    case A232_OOMOVE :
    case A232_OOOMOVE:
      ConvertA232MoveToXboard (packet[2], packet[3], packet[1], move);
      debug ("Received A232_%sMOVE from %02x to %02x (%s)\n",
	     movetypestr[packet[1] - A232_NMOVE], packet[2] & 0xff,
	     packet[3] & 0xff, move);
      printf ("move %s\n", move);
      fflush (stdout);
      break;
    case A232_REQUESTMATCH:
      debug ("Received A232_REQUESTMATCH\n");
      fExtendedOpponent = (packet[3] == A232_EXTENDEDCOMMANDMODE);
      if (!Auto232HandShake) {
	// Driver is slave.
	Auto232NumGames = packet[2];
	packet[0] = A232_STX;
	packet[1] = A232_CONFIRMMATCH;
	packet[2] = 0;
	packet[3] = A232_EXTENDEDCOMMANDMODE;
	packet[4] = A232_ETX;
	Auto232SendPacket (packet, FALSE);
	debug ("Setting Auto232HandShake = TRUE\n");
	Auto232HandShake = TRUE;
      }
      break;
    case A232_CONFIRMMATCH:
      debug ("Received A232_CONFIRMMATCH\n");
      debug ("Setting Auto232Master = TRUE\n");
      Auto232Master = TRUE;
      fExtendedOpponent = (packet[3] == A232_EXTENDEDCOMMANDMODE);
      break;
    case A232_EXTENDEDMODE: // compatibility with old extended mode
      debug ("Received A232_EXTENDEDMODE\n");
      if (!Auto232Master)  {
	packet[0] = A232_STX;
	packet[1] = A232_EXTENDEDMODE;
	packet[2] = 0;
	packet[3] = 0;
	packet[4] = A232_ETX;
	Auto232SendPacket (packet, FALSE);
      }
      break;
    default:
      error ("Received unexpected type of A232 command : %02x",
	       packet[1] & 0xff);
    }
  } else if (packet[0] == A232_ACK) {
    //    debug ("Received A232_ACK\n");
  } else {
    error ("Received unexpected A232 packet[0] = %02x", packet[0] & 0xff);
  }
}

/**********************************************************************/
static int ReceiveXboardLine (char *line)
{
  int len;
  char *tok;
  unsigned char from, to, movetype;
  static const char delim[] = " \n\r";

  len = strlen(line);
  //  if (write (serial_fd, line, len) != len)
  //    fatal ("write() to serial failed");
  debug ("Received xboard command: '%s'\n", line);
  if ((tok = strtok (line, delim)) == NULL) {
    error ("Received line with no tokens from xboard");
  } else if (strcmp (tok, "xboard") == 0) {
  } else if (strcmp (tok, "protover") == 0) {
    debug ("Sending feature list to xboard\n");
    printf ("feature myname=\"Auto232\"\n");
    printf ("feature analyze=0\n");
    printf ("feature sigint=0\n");
    printf ("feature reuse=1\n");
    printf ("feature time=0\n");
    printf ("feature colors=0\n");
    printf ("feature name=0\n");
    printf ("feature done=1\n");
    fflush (stdout);
  } else if (strcmp (tok, "new") == 0) {
  } else if (strcmp (tok, "random") == 0) {
  } else if (strcmp (tok, "level") == 0) {
  } else if (strcmp (tok, "time") == 0) {
  } else if (strcmp (tok, "otim") == 0) {
  } else if (strcmp (tok, "hard") == 0) {
  } else if ((len == 4 || len == 5) &&
	     line[0] >= 'a' && line[0] <= 'h' &&
	     line[1] >= '1' && line[1] <= '8' &&
	     line[2] >= 'a' && line[2] <= 'h' &&
	     line[3] >= '1' && line[3] <= '8') {
    ConvertXboardMoveToA232 (line, &from, &to, &movetype);
    debug ("Sending A232_%sMOVE from %02x to %02x (%s)\n",
	   movetypestr[movetype - A232_NMOVE], from & 0xff,
	   to & 0xff, line);
    Auto232SendMove (from, to, movetype);
  } else if (strcmp (tok, "black") == 0) {
  } else if (strcmp (tok, "white") == 0) {
  } else if (strcmp (tok, "go") == 0) {
    Auto232SendCommand (A232_COM_COMPUTE, FALSE);
  } else if (strcmp (tok, "computer") == 0) {
  } else if (strcmp (tok, "force") == 0) {
    //    Auto232SendCommand (A232_COM_STOP, FALSE);
  } else if (strcmp (tok, "result") == 0) {
    Auto232SendCommand (A232_COM_STOP, FALSE);
    //    Auto232NextGame ();
  } else if (strcmp (tok, "accepted") == 0) {
  } else if (strcmp (tok, "draw") == 0) {
    Auto232SendCommand (A232_COM_OFFERDRAW, FALSE);
  } else if (strcmp (tok, "undo") == 0) {
  } else if (strcmp (tok, "post") == 0) {
  } else if (strcmp (tok, "quit") == 0) {
    return FALSE;
  } else {
    error ("Unrecognised command from xboard: %s", line);
  }
  return TRUE;
}

/**********************************************************************/
int main (int argc, char *argv[])
{
  int maxfd, stdin_fd, i, len;
  unsigned char c;
  fd_set rfds;    /* file descriptor set */
  unsigned char packet[5];
  char line[1024], lines[4096], *p, *q;

  if (argc != 2) {
    fprintf (stderr, "Usage: %s serialdevice\n", argv[0]);
    exit (EXIT_FAILURE);
  }

  if (!OpenSerial (argv[1]))
    fatal ("Unable to open serial port %s", argv[1]);

  Auto232Quit = FALSE;
  Auto232ComOn = TRUE;
  i = 0;
  stdin_fd = fileno (stdin);
  printf ("Chess\n");
  fflush (stdout);
  if (!Auto232StartMatch ())
    fatal ("Auto232StartMatch() failed");
  maxfd = MAX (stdin_fd, serial_fd) + 1;
  for (;;) {
    FD_ZERO (&rfds);
    FD_SET (stdin_fd, &rfds);
    FD_SET (serial_fd, &rfds);
    if (select (maxfd, &rfds, NULL, NULL, NULL) < 0)
      fatal ("select() failed");
    if (FD_ISSET (stdin_fd, &rfds)) {
      if ((len = read (stdin_fd, lines, sizeof(lines) - 1)) > 0) {
	lines[len] = '\0';
	p = lines;
	while (*p != '\0') {
	  q = line;
	  while (*p != '\n' && *p != '\r' && *p != '\0')
	    *q++ = tolower(*p++);
	  *q = '\0';
	  if (!ReceiveXboardLine (line))
	    goto quit;
	  while (*p == '\n' || *p == '\r')
	    p++;
	}
      }
    }
    if (FD_ISSET (serial_fd, &rfds)) {
      if (read (serial_fd, &c, 1) != 1)
	break;
      if (c == A232_STX || c == A232_ACK)
	i = 0;
      if (i < 5)
	packet[i++] = c;
      if (c == A232_ETX || c == A232_ACK) {
	ReceiveA232Packet (packet);
	i = 0;
      }
    }
  }
 quit:
  debug ("Done\n");
  CloseSerial ();
  Auto232Quit = TRUE;
  Auto232ComOn = FALSE;
  Auto232HandShake = FALSE;
  return EXIT_SUCCESS;
}

/**********************************************************************/
