// Copyright 1994, 1995 by Jon Dart.

// This module handles reading and writing board and
// game info.

#include "stdafx.h"
#include "chessio.h" 
#include "strpair.h"
#include "bearing.h"
#include "notation.h"
#include "rmove.h"
#ifdef _WINDOWS
#include "resource.h"
#endif
#include "log.h"
#include "search.h"
#include "globals.h"
#include "options.h"
#include <ctype.h>
#include <string.h>
#include <strstrea.h>
#include <stdio.h>
#include <time.h>

extern Options *global_options; 

const int MAX_TAG = 255;
const int MAX_VAL = 255;
const int PGN_MARGIN = 70; // max length of PGN line for output


static int legal_move(const Board &board, const Square &start,
  const Square &dest)
{
    // do a little basic sanity checking:
      if (!start.OnBoard() || !dest.OnBoard() || board[start].IsEmpty())
          return FALSE;

      ExtendedMove emove(board,start,dest);
      Move moves[Move_Generator::MaxMoves];
      Move_Generator mg( board, 0, Move::NullMove() );
      int found = 0;
          int n = mg.Generate_Moves(moves, FALSE);

      for (int i = 0; i < n; i++)
      {
          if (moves[i].StartSquare() == emove.StartSquare() &&
              moves[i].DestSquare() == emove.DestSquare())
          {
             found++;
             break;
          }
      }
      if (!found)
          return FALSE;
      else        
      {
          // check for king en prise
          Board board_copy(board);
          const ColorType side = board.Side();
          board_copy.MakeMove(emove);
          return board_copy.num_attacks(
            board_copy.KingPos(side),OppositeColor(side)) == 0;
          
      }
}

int skip_space(ifstream &game_file)
{
    int c = EOF;
    while (!game_file.eof())
    {
       c = game_file.get();
       if (!isspace(c) && (c!='\n'))
       {
           break;
       }
    }
    return c;
}

static void get_header(CPtrArray &hdrs, LPCSTR key, CString &val)
{
      const int n = hdrs.GetSize();
      for (int i=0;i<n;i++)
      {
          StringPair *p = (StringPair*)hdrs[i];
          if (strcmp(p->tag,key)==0)
          {
               val = p->val;
               return;
          }
      }
}

static void get_game_description(CPtrArray &hdrs, CString &descr)
{
      CString tmp;
      get_header(hdrs, "White", tmp);
      int comma = tmp.Find(',');
      if (comma != -1)
      {
          tmp = tmp.Mid(0,comma);
      }
      descr = tmp;
      descr += "-";
      get_header(hdrs, "Black", tmp);
      comma = tmp.Find(',');
      if (comma != -1)
      {
          tmp = tmp.Mid(0,comma);
      }
      descr += tmp;
      get_header(hdrs, "Site", tmp);
      descr += ", ";
      descr += tmp;
      get_header(hdrs, "Round", tmp);
      if (tmp.GetLength() >0 && tmp[0] != '?')
      {
          descr += '(';
          descr += tmp;
          descr += ')';
      }
      descr += ' ';
      get_header(hdrs, "Date", tmp);
      descr += tmp.Mid(0,4);
}

void ChessIO::collect_headers(ifstream &game_file, CPtrArray *&hdrs, long &first)
{
        hdrs = new CPtrArray();
        first = -1L;
        int c;
        while (!game_file.eof())
        {
            char tag[MAX_TAG+1];
            char val[MAX_VAL+1];
            c = skip_space(game_file);
            if (c!='[')
            {
                game_file.putback(c);
                break;
            }
            else
            {
                if (first == -1)
                    first = game_file.tellg()-1;
                int t = 0;
                c = skip_space(game_file);
                while (!game_file.eof())
                {
                    if (!isspace(c) && c != '"' && t < MAX_TAG)
                        tag[t++] = c;
                    else
                        break;
                    c = game_file.get();
                } 
                tag[t] = '\0';
                if (isspace(c))
                    c = skip_space(game_file);
                if (c=='"')
                {
                    int v = 0;
                    while (!game_file.eof())
                    {
                        c = game_file.get();
                        if (c != '"' && v < MAX_VAL)
                            val[v++] = c;
                        else
                            break;
                    }
                    val[v] = '\0';
                }
                //else
                //    cerr << "bad tag" << endl;
                StringPair *new_hdr = new StringPair(tag,val);
                hdrs->Add(new_hdr);
                while (!game_file.eof() && c != ']')
                    c = game_file.get();
            }
        }
}

BOOL ChessIO::scan_pgn(ifstream &game_file, CStringList &contents)
{
    Board board;
    ColorType side = White;
    int c;

    while (game_file.good() && !game_file.eof())
    {
        // Collect the header:
        long first;
        CPtrArray *hdrs;
        collect_headers(game_file, hdrs, first);

        // we have the headers, munge them into a single-line game
        // description
        CString descr;
        get_game_description(*hdrs, descr);
        char tmp[20];
        sprintf(tmp,"\t%ld",first);
        descr += tmp;
        contents.AddTail(descr);

        // free the header info we allocated
        const int n = hdrs->GetSize();
        for (int i =0; i<n; i++)
        {
           StringPair *p = (StringPair*)hdrs->GetAt(i);
           delete p;
        }
        hdrs->RemoveAll();
        delete hdrs;
        
        while (game_file.good() && !game_file.eof())
        {
           if ((c = game_file.get()) == '[')
           {
               game_file.putback(c);
               break;
           }
        }
    } 
    return TRUE;
}

ChessIO::Token ChessIO::get_next_token(ifstream &game_file, char *buf, int limit)
{
    Token tok = Unknown;
    int count = 0;
    int c = skip_space(game_file);
    if (c == EOF)
       return Eof;
    else if (c=='{')
    {
        *buf++ = c;
        ++count;
        while (game_file.good() && c != '}')
        {
            c = game_file.get();
            if (count < limit-1)
            {
               *buf++ = c;
               ++count;
            }
        }
        if (c=='}' && count < limit-1)
        {
            *buf++ = c;
        }
        *buf = '\0';
        return Comment;
        //c = game_file.get();
    }
    if (isdigit(c))
    {
        int nextc = game_file.get();
        if (c == '0')
        {
           // peek at next char.
           if (nextc == '-')
           {
               // some so-called PGN files have 0-0 or 0-0-0 for
               // castling.  To handle this, we need to peek ahead
               // one more character.
               int nextc2 = game_file.peek();
               if (toupper(nextc2) == 'O' || nextc2 == '0')
               {            
                  // castling, we presume
                  buf[count++] = c;
                  c = nextc;
                  while (game_file.good() && (c == '-' ||
                    c == '0' || toupper(c) == 'O' || c == '+'))
                  {
                     if (count < limit-1)
                        buf[count++] = c;
                     c = game_file.get();
                  }
                  buf[count] = '\0';
                  game_file.putback(c);
                  return GameMove;
               }
           }
        }
        if (nextc == '-' || nextc == '/') // assume result
        {
            buf[count++] = c;
            c = nextc;
            while (!game_file.eof() && game_file.good() && !isspace(c))
            {
                if (count < limit-1)
                    buf[count++] = c;
                c = game_file.get();
            }
            tok = Result;
        }
        else
        {
            // Assume we have a move number.
            buf[count++] = c;
            c = nextc;
            while (game_file.good() && (isdigit(c) || c == '.'))
            {
               if (count < limit-1)
                  buf[count++] = c;
               c = game_file.get();
            }
            game_file.putback(c);
            return Number;
       }
       buf[count] = '\0';
   }           
   else if (isalpha(c))
   {
       while (game_file.good() && (isalnum(c) 
              || c == '-' || c == '=' || (c == '+')))
       {
           if (count < limit-1)
               buf[count++] = c;
           c = game_file.get();
       }
       buf[count] = '\0';
       game_file.putback(c);
       return GameMove;
   }
   else if (c == '#') // "Checkmate"
   {
       *buf = c; *(buf+1) = '\0';
       tok = Ignore;
   }
   else if (c == '*')
   {
       *buf = c; *(buf+1) = '\0';
       tok = Result;
   }
   else
   {
       *buf = c; *(buf+1) = '\0';
       tok = Unknown;
   }
   return tok;
} 

#ifndef _CONSOLE
BOOL ChessIO::load_pgn(ifstream &game_file, Log &log, DWORD offset)
{
    Board board;
    ColorType side = White;
    int c;
    game_file.seekg(offset); 
    // seekg does not clear the eof bit:
    game_file.clear();
    // skip header, if any:
    while (game_file.good() && (c = game_file.get()) != EOF)
    {
        if (c=='[')
        {
           game_file.ignore(255,']');
           continue;
        }
        else if (c == '1') // first move of game
        {
           game_file.putback(c);
           break;
        }
    }
    for (;;)
    {
        static char buf[256];
        static char num[20];
        Token tok = get_next_token(game_file,buf,256);
        if (tok == Eof)
             break;
        if (tok == Number)
        {
            strncpy(num,buf,20);
        }
        else if (tok == GameMove)
        {
            // parse the move
            Move m = Notation::Value(board,side,buf);
            if (m.IsNull() ||
                !legal_move(board,m.StartSquare(),
                            m.DestSquare()))
            {
#ifdef _WINDOWS
                CString msg;
                AfxFormatString1(msg,IDS_ILLEGAL_MOVE,buf);
                ::MessageBox(NULL,(LPCSTR)msg,"",MB_OK);
                log.clear();
#endif

                break;
            }
            else
            {
                //MessageBox(NULL,m.Image(),"",MB_OK);
                ExtendedMove emove(board,m);
                ReversibleMove rmove(board,emove);
#ifdef _WINDOWS
                log.add_move(board,rmove,buf,NULL,
                             FALSE);
#endif
                board.MakeMove(rmove);
            }
            side = OppositeColor(side);
         }
         else if (tok == Unknown)
         {
#ifdef _WINDOWS
             CString msg;
             AfxFormatString1(msg,IDS_UNREC_TEXT,buf);
             ::MessageBox(NULL,(LPCSTR)msg,"",MB_OK);
#endif
         }
         else if (tok == Comment)
         {
             // ignored for now
         }
         else if (tok == Result)
         {
            if (*buf != '*')
               log.setResult(buf);
            break;
         }            
    } 
    return TRUE;
}
#endif

BOOL ChessIO::load_fen(ifstream &ifs, Board &board)
{                  
    ifs >> board;
    return ifs.good();
}

BOOL ChessIO::store_pgn(ofstream &ofile,Log &log,
                        const ColorType computer_side,
                        LPCSTR eco)
{
    // Write standard PGN header.
    CString result = log.getResult();
    if (result.GetLength() == 0)
       result = "*";
    time_t tm = time(NULL);
    struct tm *t = localtime(&tm);
    char string[15];
    ofile << "[Event \"?\"]" << endl;
    ofile << "[Site \"?\"]" << endl;
    sprintf(string,"%d.%02d.%02d",t->tm_year,t->tm_mon+1,
                        t->tm_mday);
    ofile << "[Date \"" << string << "\"]" << endl;
    ofile << "[Round \"nil\"]" << endl;
    if (computer_side == White)
    {
         ofile << "[White \"" << "Arasan " << 
                    Arasan_Version << "\"]" << endl;
         ofile << "[Black \"?\"]" << endl;
    } 
    else
    {
         ofile << "[White \"?\"]" << endl;
         ofile << "[Black \"" << "Arasan " << 
                  Arasan_Version << "\"]" << endl;
    }
    ofile << "[Result \"" << result << "\"]" << endl;
    if (eco)
       ofile << "[ECO \"" << eco << "\"]" << endl; 
    ofile << endl;

    // Write game moves.
    const int size = log.num_moves();
    char buf[80];
    *buf = '\0';
    char num[10];
    int last = 0;
    int num_image_size;
    for (int i = 0; i < size; i++)
    {
        const Log_Entry &e = log[i];
        num_image_size = 0;
        *num = '\0';
        if (i % 2 == 0)
        {
            sprintf(num,"%d. ",(i/2)+1);
            num_image_size = strlen(num);
        }
        const int image_size = strlen(e.image());
        if (last + image_size + num_image_size + 1 >= PGN_MARGIN)
        {
            buf[last] = '\0';
            ofile << buf << endl;
            last = 0;
            *buf = '\0';
        }
        if (last != 0)
            strcat(buf," ");
        strcat(buf,num);
        strcat(buf,e.image());
        last = strlen(buf);
    }
    if (last + strlen(result) + 1 >= PGN_MARGIN)
    {
        ofile << buf << endl;
        last = 0;
        *buf = '\0';
    }
    strcat(buf," ");
    strcat(buf, result);
    ofile << buf << endl;
    if (!ofile) 
    {
#ifdef _WINDOWS
        ::MessageBox(NULL,"Error saving file!","Save Game",MB_OK);
#endif
        return FALSE;
    }
    return TRUE;
}

BOOL ChessIO::store_fen( ofstream &ofile, Board &board)
{
    ofile << board << endl;
    return ofile.good();
}

#ifdef _WINDOWS
EPD_Record * ChessIO::read_epd_record(ifstream &ifs, Board &board)
{
    // read FEN description
    EPD_Record *rec = new EPD_Record;
    ifs >> board;
    if (ifs.eof())
        return NULL;
    else if (!ifs.good())
    {
        rec->set_error("Bad EPD record: FEN board description missing or invalid");
        ifs.ignore(255,'\n');
        return rec;
    }
    // read EPD commands
    int c;
    while (ifs.good() && (c = ifs.get()) != EOF)
    {
        int saw_eoln = 0;
        while (isspace(c))
        {
            if (c == '\n' || c == '\r')
               saw_eoln++;
            c = ifs.get();
        }
        if (saw_eoln)
        {
            ifs.putback(c);
            break;
        }
        // collect command
        char cmd[20], val[256];
        char *p = cmd;
        int count = 0;
        while (ifs.good() && !isspace(c) && count < 19)
        {
            *p++ = c;
            c = ifs.get();
            ++count;
        }
        *p = '\0';
        while (isspace(c))
        {
            c = ifs.get();
        }
        p = val;
        int quoted = (c == '"');
        count = 0;
        while (ifs.good() && 
               count < 255 &&
               ((!isspace(c) && c != ';') || quoted))
        {
            *p++ = c;
            c = ifs.get();
            if (quoted && c == '"')
            {
               *p++ = c;
               quoted = 0;
               break;
            }
        }
        if (quoted)
        {
            rec->set_error("Missing end quote in EPD record");
            ifs.ignore(255,'\n');
            return rec;
        }
        *p = '\0';
        if (*cmd && *val)
            rec->add(cmd,val);
        while (!ifs.eof() && c != ';') c = ifs.get();
    }
    if (c == '\n') c = ifs.get();
    return rec;
}

void ChessIO::execute_epd_record(EPD_Record *rec, Board &board)
{
    CString moveStr;
    if (rec->get_val("bm",moveStr))
    {
        // collect possible moves
        Move moves[10];
        char *buf = moveStr.GetBuffer(128);
        int move_count = 0;
        for (char *p = buf;*p;)
        {
            while (isspace(*p) && *p != '\0') ++p;
            if (*p == '\0')
               break;
            char tmp[10];
            int i = 0;
            char *q = tmp;
            while (!isspace(*p) && *p != '+' && *p != '\0' && i < 10)
            {
                *q++ = *p++;
                ++i;
            }
            *q = '\0'; 
            Move m = Notation::Value(board,board.Side(),tmp);
            if (!m.IsNull())
            {
                ASSERT(move_count < 10);
                moves[move_count++] = m;
            }
            if (*p == '+' || *p == '#') ++p;
        }
        moveStr.ReleaseBuffer();
        Search::Statistics stats;
        // use the internal ID_COMPUTE_ONLY message to perform
        // the search, w/o updating the board afterwards
        theView->SendMessage(WM_COMMAND,ID_COMPUTE_ONLY,0L);
        Move m = theView->get_last_move();
        int correct = 0;
        for (int i = 0; i < move_count; ++i)
        {
            if (moves[i] == m)
            {
               ++correct; break;
            }
        }    
        if (correct)
           theLog->write("+++ correct");
        else
           theLog->write("+++ error");
        theLog->write_eol();
    }
}

void ChessIO::annotate_epd_record(EPD_Record *rec, Board &board,
                                  ofstream &outfile)
{
    CString moveStr;
    char fenBuf[128];
    char pred_move_image[20];
    char move_buf[20];

    // use the internal ID_COMPUTE_ONLY message to perform
    // the search, w/o updating the board afterwards
    theView->SendMessage(WM_COMMAND,ID_COMPUTE_ONLY,0L);
    const Search::Statistics &stats = theView->get_stats();
    Move m = theView->get_last_move();

    // write 1st 4 parts of a FEN record:
#ifdef _WIN32    
    ZeroMemory(fenBuf, 128);
#else
    memset(fenBuf,'\0', 128);
#endif    
    write_fen(board,fenBuf,FALSE);
    outfile << fenBuf << ' ';

    // Construct centipawn evaluation
    int val;
    if (stats.state == Search::Draw)
    {
       // The EPD standard requires that a draw position be
       // scored as 0
       val = 0;
    }
    else if (stats.value > 9000)
    {
       // EPD standard says a mate score is 32767
       val = 32767-(10000-stats.value);
    }
    else if (stats.value < -9000)
    {
       val = -32767+(10000+stats.value);
    }
    else
    {
       val = stats.value*100/64;
    }
    char valImage[20]; 
    sprintf(valImage,"%d",val);
    rec->add("ce",valImage);
    
    // write principal variation
    // EPD spec says we always need the pv tag even if there
    // are no moves.
    char pvBuf[200];
    *pvBuf = '\0';
    if (!stats.best_line[0].IsNull())
    {
       Board tmp_board(board);
       int j = 0;
       while (strlen(pvBuf) < 190)
       {
           Move m = stats.best_line[j];
           Notation::Image(tmp_board,m,move_buf);
           if (j == 0)
              strcpy(pred_move_image,move_buf);
           strcat(pvBuf,move_buf);
           j++;
           if (j >= Constants::MaxPly || stats.best_line[j].IsNull())
              break;
           strcat(pvBuf," ");
           tmp_board.MakeMove(ExtendedMove(tmp_board,m));
       }
    }
    rec->add("pv",pvBuf);

    // write predicted move if there is one
    if (!stats.best_line[0].IsNull())
    {
       rec->add("pm",pred_move_image);
    }
    // write node count and analysis time
    sprintf(valImage,"%ld",stats.num_nodes);
    rec->add("acn",valImage);
    sprintf(valImage,"%ld",stats.elapsed_time);
    rec->add("acs",valImage);

    // EPD standard says a "normalized" record has sorted operators
    rec->sort();

    // write out the EPD records
    const int n = rec->getSize();
    for (int i=0; i<n; i++)
    {
       CString key,val;
       rec->getData(i,key,val);
       outfile << key << ' ' << val << ';';
       if (i<n-1) outfile << ' ';           
    }
    outfile << endl;
}
#endif

BOOL ChessIO::read_fen(Board &board, char *buf)
{
    for (int j=0;j<64;j++)
    {
       board.my_Contents[j] = Piece::EmptyPiece();
    }
    
    Square sq;
    char *bp = buf;
    char c;
    while (isspace(*bp)) bp++;
    for (int line = 0; line < 8; line++)
    {
       int sqval = line*RankIncr;
       while ((c = *bp) != ' ' && c != '/')
       {
          Piece piece;
          if (isdigit(c))
          {
             sqval += (*bp - '0');
             bp++;
          }
          else if (isalnum(c))
          {
             if (!Square(sqval).OnBoard())
             {
                return FALSE;
             }
             switch(c)
             {
             case 'p': piece = Piece(Piece::Pawn,Black); 
                       break;
             case 'n': piece = Piece(Piece::Knight,Black);
                       break;
             case 'b': piece = Piece(Piece::Bishop,Black);
                       break;
             case 'r': piece = Piece(Piece::Rook,Black);
                       break;
             case 'q': piece = Piece(Piece::Queen,Black); 
                       break;
             case 'k': piece = Piece(Piece::King,Black);
                       break;
             case 'P': piece = Piece(Piece::Pawn,White);
                       break;
             case 'N': piece = Piece(Piece::Knight,White);
                       break;
             case 'B': piece = Piece(Piece::Bishop,White);
                       break;
             case 'R': piece = Piece(Piece::Rook,White);
                       break;
             case 'Q': piece = Piece(Piece::Queen,White);
                       break;
             case 'K': piece = Piece(Piece::King,White);
                       break;
             default:
                return FALSE;
             }
             board.my_Contents[sqval] = piece;
             sqval++;
             bp++;
          }
          else // not a letter or a digit
          {
             return FALSE;
          }
       }
       if (c=='/') ++bp;  // skip delimiter
    }
    while (isspace(*bp)) bp++;
    if (toupper(*bp) == 'W')
       board.my_Side = White;
    else if (toupper(*bp) == 'B')
       board.my_Side = Black;
    else
    {
       return FALSE;
    }
    bp++;
    while (isspace(*bp)) bp++;
    c = *bp;
    if (c == '-')
    {
       board.my_CastleStatus[White] = 
       board.my_CastleStatus[Black] = Board::CantCastleEitherSide;
       bp++;
    }
    else
    {
       int k = 0;
       for (; !isspace(*bp); bp++)
       {
          if (*bp == 'K')
             k += 1;
          else if (*bp == 'Q')
             k += 2;
          else if (*bp == 'k')
             k += 4;
          else if (*bp == 'q')
             k += 8;
          else
          {
             return FALSE;
          }
       }
       static Board::CastleType vals[4] = 
          { Board::CantCastleEitherSide, Board::CanCastleKSide,
            Board::CanCastleQSide, Board::CanCastleEitherSide };
       board.my_CastleStatus[White] = vals[k % 4];
       board.my_CastleStatus[Black] = vals[k / 4];
    }
    while (isspace(*bp)) bp++;
    c = *bp;
    if (c == '-')
    {
       board.my_EnPassantSq[White].SetInvalid();
       board.my_EnPassantSq[Black].SetInvalid();
    }
    else if (isalpha(c))
    {
       char sqbuf[2];
       sqbuf[0] = *bp++;
       sqbuf[1] = *bp++;
       Square epsq = Square::Value(sqbuf);
       if (epsq.IsInvalid())
       {
          return FALSE;
       }
       board.my_EnPassantSq[board.Side()].SetInvalid();
       board.my_EnPassantSq[board.OppositeSide()] = 
            Square::Value(sqbuf) - (RankIncr * Direction[board.Side()]);
    }
    else
    {
        return FALSE;
    }
    board.set_secondary_vars();
    return TRUE;
}

void ChessIO::write_fen( Board &board, char *buf, BOOL addMoveInfo)
{
    strstream  o(buf,128,ios::out);
    // write out the board in Forsythe-Edwards (FEN) notation.
    for (int i=1;i<=8;i++)
    {
      int j = 1;
      while (j <= 8)
      {
         int n = 0;
         Square sq;
         Piece p;
         do
         {
            sq = Square(j,i,Black);
            p = board.my_Contents[sq];
            if (!p.IsEmpty())
                break;
            ++j; ++n;
         } while (j <= 8);
         if (n)
            o << (char)(n + '0');
         if (!p.IsEmpty())
         {
            char img = Piece::Image(p.Type());
            if (p.Color() == Black) img = tolower(img);
            o << img;
            j++;
         }
      }
      if (i != 8) o << '/';
    }
    if (board.Side() == White)
       o << " w";
    else
       o << " b";

    // used in I/O of castling data:
    const BOOL kcastle[6] = { TRUE, TRUE, FALSE, FALSE, FALSE, FALSE };
    const BOOL qcastle[6] = { TRUE, FALSE, TRUE, FALSE, FALSE, FALSE };

    // note : unfortunately FEN doesn't allow recording if castling
    // has taken place, only whether or not it is possible.
    Board::CastleType wcs = board.CastleStatus(White);
    Board::CastleType bcs = board.CastleStatus(Black);
    o << ' ';
    if (!kcastle[(int)wcs] && !qcastle[(int)bcs])
       o << '-';
    else
    {
       if (kcastle[(int)wcs])
          o << 'K';
       if (qcastle[(int)wcs])
          o << 'Q';
       if (kcastle[(int)bcs])
          o << 'k';
       if (qcastle[(int)bcs])
          o << 'q';
    }
    o << ' ';
    Square epsq(board.EnPassantSq(board.OppositeSide()));
    if (epsq.IsInvalid())
       o << '-';
    else
    {
       // FEN stores the destination square for an en passant capture;
       // we store the location of the capturable pawn.
       Square target(epsq + (RankIncr * Direction[board.Side()]));
       o << target.FileImage() << target.RankImage();
    }
    if (addMoveInfo)
    {
       // FEN is supposed to include the halfmove and fullmove numbers,
       // but these are attributes of the game - they are not stored in
       // the board.
       o << " 0 1";
    }
}
