/*
 * src/gmp.h
 * Copyright (C) 1995-1997 William Shubert.
 *
 * You may use this code in any way you wish as long as you retain the
 *   above copyright notice.
 *
 * This is based on David Fotland's Go Modem Protocol Code and the
 *   "protocol.Z" info file from Bruce Wilcox.  It has been pretty much
 *   completely rewritten now, though.
 */

/* Modification by Daniel Bump for GNU Go: I made all GMP messages
   contingent on gmp_debug. Otherwise this is identical to Bill Shubert's
   version distributed with GoDummy.
*/

/* Modified by Paul Pogonyshev on 10.07.2003.
 * Added support for Simplified GTP.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#define  _GMP_C_  1

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

#if TIME_WITH_SYS_TIME
# include <sys/time.h>
# include <time.h>
#else
# if HAVE_SYS_TIME_H
#  include <sys/time.h>
# else
#  include <time.h>
# endif
#endif

#ifdef HAVE_SYS_TYPES_H
#include <sys/types.h>
#endif

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#ifdef __MINGW32__
#include <windows.h>
#include <winsock.h>
#include <io.h>
#endif

#ifdef HAVE_WINSOCK_IO_H
#include <winsock.h>
#include <io.h>
#endif

/**********************************************************************
 * Constants
 **********************************************************************/

#define  GMP_TIMEOUTRETRIES   60
#define  GMP_RETRYSECS	       1
#define  SGMP_TIMEOUTRETRIES   9
#define  SGMP_RETRYSECS       20
#define  GMP_MAXSENDSQUEUED   16


/**********************************************************************
 * Data types
 **********************************************************************/
 
typedef enum  {
  cmd_ack, cmd_deny, cmd_reset, cmd_query, cmd_respond, cmd_move,
  cmd_undo
} Command;


typedef enum  {
  query_game, query_bufSize, query_protocol, query_stones,
  query_bTime, query_wTime, query_charSet, query_rules, query_handicap,
  query_boardSize, query_timeLimit, query_color, query_who,
  query_max
} Query;


typedef struct Gmp_struct  {
  int  inFile, outFile;
  int  boardSize, sizeVerified;
  int  handicap, handicapVerified;
  float  komi;
  int komiVerified;
  int  chineseRules, rulesVerified;
  int  iAmWhite, colorVerified;
  Query  lastQuerySent;

  int  recvSoFar, sendsQueued;
  int  sendFailures, noResponseSecs;
  int  waitingHighAck;
  time_t  lastSendTime;
  int  myLastSeq, hisLastSeq;
  unsigned char  recvData[4];
  unsigned char  sendData[4];
  struct  {
    int  cmd, val;
  } sendsPending[GMP_MAXSENDSQUEUED];

  int  earlyMovePresent;
  int  earlyMoveX, earlyMoveY;

  int simplified;
} Gmp;


/**********************************************************************
 * Globals
 **********************************************************************/

int  gmp_debug = 0;
static const char  *commandNames[] = {
  "ACK", "DENY", "RESET", "QUERY", "RESPOND", "MOVE", "UNDO"};
static const char  *queryNames[] = {
  "GAME", "BUFFER SIZE", "PROTOCOL", "STONES",
  "BLACK TIME", "WHITE TIME", "CHAR SET", "RULES", "HANDICAP",
  "BOARD SIZE", "TIME LIMIT", "COLOR", "WHO"};


/**********************************************************************
 * Forward Declarations
 **********************************************************************/

/* Get the forward declaration of externally visible functions. */
#include "gmp.h"

static unsigned char  checksum(unsigned char p[4]);
static GmpResult  gotQueryResponse(Gmp *ge, int val, const char **err);
static void  putCommand(Gmp *ge, Command cmd, int val);
static GmpResult  respond(Gmp *ge, Query query);
static void  askQuery(Gmp *ge);
static int  heartbeat(Gmp *ge);
static GmpResult  getPacket(Gmp *ge, int *out1, int *out2,
			    const char **error);
static GmpResult  parsePacket(Gmp *ge, int *out1, int *out2,
			      const char **error);
static GmpResult  processCommand(Gmp *ge, Command command, int val,
				 int *out1, int *out2, const char **error);
static void  processQ(Gmp *ge);


/**********************************************************************
 * Functions
 **********************************************************************/

#define  gmp_verified(ge)  \
  ((ge)->sizeVerified && (ge)->colorVerified && \
   (ge)->handicapVerified && (ge)->rulesVerified)


Gmp  *gmp_create(int inFile, int outFile)  {
  Gmp  *ge;

  ge = malloc(sizeof(Gmp));
  ge->inFile = inFile;
  ge->outFile = outFile;

  ge->boardSize = -1;
  ge->sizeVerified = 0;

  ge->handicap = -1;
  ge->handicapVerified = 0;

  ge->komi = 0.0;

  ge->chineseRules = -1;
  ge->rulesVerified = 0;

  ge->iAmWhite = -1;
  ge->colorVerified = 0;

  ge->lastQuerySent = 0;
  
  ge->recvSoFar = 0;
  ge->sendsQueued = 0;
  ge->sendFailures = 0;
  ge->noResponseSecs = 0;
  ge->waitingHighAck = 0;
  ge->lastSendTime = 0;
  ge->myLastSeq = 0;
  ge->hisLastSeq = 0;

  ge->earlyMovePresent = 0;

  return(ge);
}


void  gmp_destroy(Gmp *ge)  {
  free(ge);
}


GmpResult  gmp_check(Gmp *ge, int gsleep, int *out1, int *out2,
		     const char **error)  {
  fd_set  readReady;
  struct timeval  noTime;
  int  intDummy;
  const char  *charPtrDummy;
  GmpResult  result;

  if (out1 == NULL)
    out1 = &intDummy;
  if (out2 == NULL)
    out2 = &intDummy;
  if (error == NULL)
    error = &charPtrDummy;
  if (gmp_verified(ge) && ge->earlyMovePresent) {
    *out1 = ge->earlyMoveX;
    *out2 = ge->earlyMoveY;
    ge->earlyMovePresent = 0;
    if (gmp_debug) {
      fprintf(stderr, "GMP: Returning early move.\n");
    }
    return(gmp_move);
  }
  *out1 = 0;
  *out2 = 0;
  *error = NULL;
  do  {
    if (time(NULL) != ge->lastSendTime)  {
      if (!heartbeat(ge))  {
	*error = "GMP Timeout";
	return(gmp_err);
      }
    }
    FD_ZERO(&readReady);
    FD_SET(ge->inFile, &readReady);
    noTime.tv_usec = 0;
    if (gsleep)
      noTime.tv_sec = 1;
    else
      noTime.tv_sec = 0;
    select(ge->inFile + 1, &readReady, NULL, NULL, &noTime);
    if (!gsleep && !FD_ISSET(ge->inFile, &readReady))
      return(gmp_nothing);
    result = getPacket(ge, out1, out2, error);
  } while (result == gmp_nothing);
  return(result);
}


static GmpResult  getPacket(Gmp *ge, int *out1, int *out2,
			    const char **error)  {
  unsigned char  charsIn[4], c;
  int  count = 0, cNum;
  static char  errOut[200];

  count = read(ge->inFile, charsIn, 4 - ge->recvSoFar);
  if (count <= 0)  {
    sprintf(errOut, "System error.");
    *error = errOut;
    return(gmp_err);
  }

  for (cNum = 0;  cNum < count;  ++cNum)  {
    c = charsIn[cNum];
    if (!ge->recvSoFar)  {
      /* idle, looking for start of packet */
      if ((c & 0xfc) == 0)  {  /* start of packet */
	ge->recvData[0] = c;
	ge->recvSoFar = 1;
      } else {
	if (gmp_debug) {
	  fprintf(stderr, "GMP: Received invalid packet.\n");
	}
      }
    } else  {
      /* We're in the packet. */
      if ((c & 0x80) == 0)  {  /* error */
	if (gmp_debug) {
	  fprintf(stderr, "GMP: Received invalid packet.\n");
	}
	ge->recvSoFar = 0;
	if ((c & 0xfc) == 0)  {
	  ge->recvData[ge->recvSoFar++] = c;
	}
      } else  {
	/* A valid character for in a packet. */
	ge->recvData[ge->recvSoFar++] = c;
	if (ge->recvSoFar == 4)  {  /* check for extra bytes */
	  assert(cNum + 1 == count);
	  ge->recvSoFar = 0;
	  if (checksum(ge->recvData) == ge->recvData[1])
	    return(parsePacket(ge, out1, out2, error));
	}
      }
    }
  }
  return(gmp_nothing);
}


static unsigned char  checksum(unsigned char p[4])  {
  unsigned char sum;
  sum = p[0] + p[2] + p[3];       
  sum |= 0x80;  /* set sign bit */
  return(sum);
}


static GmpResult  parsePacket(Gmp *ge, int *out1, int *out2,
			      const char **error)  {
  int  seq, ack, val;
  Command  command;
  GmpResult  result;

  seq = ge->recvData[0] & 1;
  ack = (ge->recvData[0] & 2) >> 1;
  if (ge->recvData[2] & 0x08)  {  /* Not-understood command. */
    if (gmp_debug) {
      fprintf(stderr, "GMP: Unknown command byte 0x%x received.\n",
	      (unsigned int) ge->recvData[2]);
    }
    return(gmp_nothing);
  }
  command = (ge->recvData[2] >> 4) & 7;
  val = ((ge->recvData[2] & 7) << 7) | (ge->recvData[3] & 0x7f);
  if (gmp_debug) {
    if (command == cmd_query) {
      if (val >= query_max) {
	if (gmp_debug)
	  fprintf(stderr, "GMP: Read in command: %s unkown value %d\n",
		  commandNames[command], val);
      } else {
	if (gmp_debug)
	  fprintf(stderr, "GMP: Read in command: %s %s\n",
		  commandNames[command], queryNames[val]);
      }
    } else {
      if (gmp_debug)
	fprintf(stderr, "GMP: Read in command: %s\n",
		commandNames[command]);
    }
  }
  if (!ge->waitingHighAck)  {
    if ((command == cmd_ack) ||  /* An ack.  We don't need an ack now. */
	(ack != ge->myLastSeq))  {  /* He missed my last message. */
      if (gmp_debug)
	fprintf(stderr, "GMP: Unexpected ACK.\n");
      return(gmp_nothing);
    } else if (seq == ge->hisLastSeq)  {  /* Seen this one before. */
      if (gmp_debug)
	fprintf(stderr, "GMP: Received repeated message.\n");
      putCommand(ge, cmd_ack, ~0);
    } else  {
      ge->hisLastSeq = seq;
      ge->sendFailures = 0;
      ge->noResponseSecs = 0;
      return(processCommand(ge, command, val, out1, out2, error));
    }
  } else  {
    /* Waiting for OK. */
    if (command == cmd_ack)  {
      if ((ack != ge->myLastSeq) || (seq != ge->hisLastSeq))  {
	/* Sequence error. */
	fprintf(stderr, "Sequence error.\n");
	return(gmp_nothing);
      }
      ge->sendFailures = 0;
      ge->noResponseSecs = 0;
      ge->waitingHighAck = 0;
      if (!gmp_verified(ge)) {
	askQuery(ge);
      }
      processQ(ge);
    } else if ((command == cmd_reset) && (ge->iAmWhite == -1)) {
      if (gmp_debug)
	fprintf(stderr, "gmp/his last seq = %d\n", seq);
      ge->hisLastSeq = seq;
      ge->waitingHighAck = 0;
      return(processCommand(ge, command, val, out1, out2, error));
    } else if (seq == ge->hisLastSeq)  {
      /* His command is old. */
    } else if (ack == ge->myLastSeq)  {
      ge->sendFailures = 0;
      ge->noResponseSecs = 0;
      ge->waitingHighAck = 0;
      ge->hisLastSeq = seq;
      result = processCommand(ge, command, val, out1, out2, error);
      processQ(ge);
      return(result);
    } else  {
      /* Conflict with opponent. */
      if (gmp_debug)
	fprintf(stderr, "Sending conflict.\n");
      ge->myLastSeq = 1 - ge->myLastSeq;
      ge->waitingHighAck = 0;
      processQ(ge);
    }
  }
  return(gmp_nothing);
}


static GmpResult  processCommand(Gmp *ge, Command command, int val,
				 int *out1, int *out2, const char **error)  {
  int  s, x, y;

  switch(command)  {
  case cmd_deny:
    putCommand(ge, cmd_ack, ~0);
    break;
  case cmd_query:
    return(respond(ge, val));
    break;
  case cmd_reset:  /* New game. */
    if (gmp_debug)
      fprintf(stderr, "GMP: Resetted.  New game.\n");
    askQuery(ge);
    return(gmp_reset);
    break;
  case cmd_undo:  /* Take back moves. */
    putCommand(ge, cmd_ack, ~0);
    *out1 = val;
    return(gmp_undo);
    break;
  case cmd_move:
    s = val & 0x1ff;
    if (s == 0)  {
      x = -1;
      y = 0;
    } else if (s == 0x1ff)  {
      x = -2;
      y = 0;
    } else  {
      --s;
      x = (s % ge->boardSize);
      y = ge->boardSize - 1 - (s / ge->boardSize);
    }
    putCommand(ge, cmd_ack, ~0);
    if (x == -1)
      return(gmp_pass);
    else  {
      if (gmp_verified(ge)) {
	*out1 = x;
	*out2 = y;
	return(gmp_move);
      } else {
	assert(ge->earlyMovePresent == 0);
	ge->earlyMovePresent = 1;
	ge->earlyMoveX = x;
	ge->earlyMoveY = y;
	askQuery(ge);
      }
    }
    break;
  case cmd_respond:
    return(gotQueryResponse(ge, val, error));
    break;
  default:  /* Don't understand command. */
    putCommand(ge, cmd_deny, 0);
    break;
  }
  return(gmp_nothing);
}


static void  putCommand(Gmp *ge, Command cmd, int val)  {
  if (ge->waitingHighAck &&
      (cmd != cmd_ack) && (cmd != cmd_respond) && (cmd != cmd_deny))  {
    if (ge->sendsQueued < 1024)  {
      ge->sendsPending[ge->sendsQueued].cmd = cmd;
      ge->sendsPending[ge->sendsQueued].val = val;
      ++ge->sendsQueued;
    } else  {
      if (gmp_debug)
	fprintf(stderr, "GMP: Send buffer full.  Catastrophic error.");
      exit(EXIT_FAILURE);
    }
    return;
  }
  if ((cmd == cmd_ack) && (ge->sendsQueued))  {
    ge->waitingHighAck = 0;
    processQ(ge);
    return;
  }
  if (cmd != cmd_ack)
    ge->myLastSeq ^= 1;
  ge->sendData[0] = ge->myLastSeq | (ge->hisLastSeq << 1);
  ge->sendData[2] = 0x80 | (cmd << 4) | ((val >> 7) & 7);
  ge->sendData[3] = 0x80 | val;
  ge->sendData[1] = checksum(ge->sendData);
  ge->lastSendTime = time(NULL);
  if (gmp_debug) {
    if (cmd == cmd_query) 
      fprintf(stderr, "GMP: Sending command: %s %s\n",
	      commandNames[cmd], queryNames[val]);
    else
      fprintf(stderr, "GMP: Sending command: %s\n", commandNames[cmd]);
  }
  write(ge->outFile, ge->sendData, 4);
  ge->waitingHighAck = (cmd != cmd_ack);
  return;
}


static GmpResult  respond(Gmp *ge, Query query)  {
  int  response;
  int  wasVerified;

  wasVerified = gmp_verified(ge);
  if (query & 0x200)  {
    /* Do you support this extended query? */
    response = 0;  /* No. */
  } else  {
    ge->waitingHighAck = 1;
    switch(query)  {
    case query_game:
      response = 1;  /* GO */
      break;
    case query_rules:
      if (ge->chineseRules == -1) {
	response = 0;
      } else {
	ge->rulesVerified = 1;
	if (ge->chineseRules == 1)
	  response = 2;
	else
	  response = 1;
      }
      break;
    case query_handicap:
      if (ge->handicap == -1)
	response = 0;
      else {
	ge->handicapVerified = 1;
	response = ge->handicap;
	if (response == 0)
	  response = 1;
      }
      break;
    case query_boardSize:
      if (ge->boardSize == -1) {
	response = 0;
      } else {
	response = ge->boardSize;
	ge->sizeVerified = 1;
      }
      break;
    case query_color:
      if (ge->iAmWhite == -1) {
	response = 0;
      } else  {
	ge->colorVerified = 1;
	if (ge->iAmWhite)
	  response = 1;
	else
	  response = 2;
      }
      break;
    default:
      response = 0;
      break;
    }    
  }
  putCommand(ge, cmd_respond, response);
  if (!wasVerified && gmp_verified(ge)) {
    if (gmp_debug)
      fprintf(stderr, "GMP: New game ready.\n");
    return(gmp_newGame);
  } else {
    return(gmp_nothing);
  }
}


static void  askQuery(Gmp *ge)  {
  if (!ge->simplified) {
    if (!ge->rulesVerified) {
      ge->lastQuerySent = query_rules;
    } else if (!ge->sizeVerified) {
      ge->lastQuerySent = query_boardSize;
    } else if (!ge->handicapVerified) {
      ge->lastQuerySent = query_handicap;
    /*  } else if (!ge->komiVerified) {
	  ge->lastQuerySent = query_komi; query komi is not define in GMP !? */
    } else {
      assert(!ge->colorVerified);
      ge->lastQuerySent = query_color;
    }
  }
  else {
    if (!ge->colorVerified)
      ge->lastQuerySent = query_color;
    else if (!ge->handicapVerified)
      ge->lastQuerySent = query_handicap;
  }

  putCommand(ge, cmd_query, ge->lastQuerySent);
}


static GmpResult  gotQueryResponse(Gmp *ge, int val, const char **err)  {
  static const char  *ruleNames[] = {"Japanese", "Chinese"};
  static const char  *colorNames[] = {"Black", "White"};
  static char errOut[200];

  switch(ge->lastQuerySent)  {
  case query_handicap:
    if (val <= 1)
      --val;
    if (ge->handicap == -1) {
      if (val == -1) {
	sprintf(errOut, "Neither player knows what the handicap should be.");
	*err = errOut;
	return(gmp_err);
      } else {
	ge->handicap = val;
	ge->handicapVerified = 1;
      }
    } else {
      ge->handicapVerified = 1;
      if ((val != -1) && (val != ge->handicap))  {
	sprintf(errOut, "Handicaps do not agree; I want %d, he wants %d.",
		ge->handicap, val);
	*err = errOut;
	return(gmp_err);
      }
    }
    break;
  case query_boardSize:
    if (ge->boardSize == -1)  {
      if (val == 0)  {
	sprintf(errOut, "Neither player knows what the board size should be.");
	*err = errOut;
	return(gmp_err);
      } else {
	ge->boardSize = val;
	ge->sizeVerified = 1;
      }
    } else {
      ge->sizeVerified = 1;
      if ((val != 0) && (val != ge->boardSize))  {
	sprintf(errOut, "Board sizes do not agree; I want %d, he wants %d.",
		ge->boardSize, val);
	*err = errOut;
	return(gmp_err);
      }
    }
    break;
  case query_rules:
    if (ge->chineseRules == -1) {
      if (val == 0) {
	sprintf(errOut, "Neither player knows what rule set to use.");
	*err = errOut;
	return(gmp_err);
      } else {
	ge->chineseRules = val - 1;
	ge->rulesVerified = 1;
      }
    } else {
      ge->rulesVerified = 1;
      if (val != 0)  {
	if (ge->chineseRules != (val == 2))  {
	  sprintf(errOut, "Rule sets do not agree; I want %s, he wants %s.",
		  ruleNames[ge->chineseRules], ruleNames[val == 2]);
	  *err = errOut;
	  return(gmp_err);
	}
      }
    }
    break;
  case query_color:
    if (ge->iAmWhite == -1)  {
      if (val == 0)  {
	sprintf(errOut, "Neither player knows who is which color.");
	*err = errOut;
	return(gmp_err);
      } else {
	ge->iAmWhite = !(val == 1);
	ge->colorVerified = 1;
      }
    } else {
      ge->colorVerified = 1;
      if (val != 0)  {
	if (ge->iAmWhite == (val == 1))  {
	  sprintf(errOut, "Colors do not agree; we both want to be %s.",
		  colorNames[ge->iAmWhite]);
	  *err = errOut;
	  return(gmp_err);
	}
      }
    }
    break;
  default:
    break;
  }
  if (!gmp_verified(ge)) {
    askQuery(ge);
    return(gmp_nothing);
  } else {
    putCommand(ge, cmd_ack, ~0);
    if (gmp_debug)
      fprintf(stderr, "GMP: New game ready.\n");
    return(gmp_newGame);
  }
}


static int  heartbeat(Gmp *ge)  {
  Command  cmd;
  
  if (ge->waitingHighAck) {
    if (++ge->noResponseSecs
	> (ge->simplified ? SGMP_RETRYSECS : GMP_RETRYSECS))  {
      if (++ge->sendFailures
	  > (ge->simplified ? SGMP_TIMEOUTRETRIES : GMP_TIMEOUTRETRIES))  {
	return(0);
      } else  {
	if (gmp_debug) {
	  cmd = (ge->sendData[2] >> 4) & 7;
	  if (cmd == cmd_query) {
	    if (gmp_debug)
	      fprintf(stderr, "GMP: Sending command: %s %s (retry)\n",
		      commandNames[cmd],
		      queryNames[ge->sendData[3] & 0x7f]);
	  }
	  else
	    if (gmp_debug)
	      fprintf(stderr, "GMP: Sending command: %s (retry)\n",
		      commandNames[cmd]);
	}
	write(ge->outFile, ge->sendData, 4);
      }
    }
  }
  return(1);
}


void  gmp_startGame(Gmp *ge, int size, int handicap, float komi,
		    int chineseRules, int iAmWhite, int simplified)  {
  assert((size == -1) || ((size > 1) && (size <= 22)));
  assert((handicap >= -1) && (handicap <= 27));
  assert((chineseRules >= -1) && (chineseRules <= 1));
  assert((iAmWhite >= -1) && (iAmWhite <= 1));

  ge->boardSize = size;
  ge->sizeVerified = simplified;

  ge->handicap = handicap;
  ge->handicapVerified = 0;

  ge->komi = komi;

  ge->chineseRules = chineseRules;
  ge->rulesVerified = simplified;

  ge->iAmWhite = iAmWhite;
  ge->colorVerified = 0;

  ge->earlyMovePresent = 0;

  ge->simplified = simplified;

  if (iAmWhite != 1) {
    putCommand(ge, cmd_reset, 0);
  }
}


void  gmp_sendPass(Gmp *ge)  {
  int  arg;

  if (ge->iAmWhite)
    arg = 0x200;
  else
    arg = 0;
  putCommand(ge, cmd_move, arg);
}


void  gmp_sendMove(Gmp *ge, int x, int y)  {
  int  val;

  val = x + ge->boardSize * (ge->boardSize - 1 - y) + 1;
  if (ge->iAmWhite)
    val |= 0x200;
  putCommand(ge, cmd_move, val);
}


void  gmp_sendUndo(Gmp *ge, int numUndos)  {
  putCommand(ge, cmd_undo, numUndos);
}


const char  *gmp_resultString(GmpResult result)  {
  static const char  *names[] = {
    "Nothing", "Move", "Pass", "Reset", "New game", "Undo", "Error"};

  assert(result <= gmp_err);
  return(names[result]);
}


int  gmp_size(Gmp *ge)  {
  return(ge->boardSize);
}


int  gmp_handicap(Gmp *ge)  {
  return(ge->handicap);
}


float  gmp_komi(Gmp *ge)  {
  return(ge->komi);
}


int  gmp_chineseRules(Gmp *ge)  {
  return(ge->chineseRules);
}


int  gmp_iAmWhite(Gmp *ge)  {
  return(ge->iAmWhite);
}


static void  processQ(Gmp *ge)  {
  int  i;

  if (!ge->waitingHighAck && ge->sendsQueued)  {
    putCommand(ge, ge->sendsPending[0].cmd, ge->sendsPending[0].val);
    --ge->sendsQueued;
    for (i = 0;  i < ge->sendsQueued;  ++i)  {
      ge->sendsPending[i] = ge->sendsPending[i + 1];
    }
  }
}

/*
 * Local Variables:
 * tab-width: 8
 * c-basic-offset: 2
 * End:
 */