/*
 * OKP parsing library
 *
 * Copyright (C) 2020-2022 Mateusz Viste
 *
 * MIT License
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

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

#include "kropcore.h"
#include "okp.h"


/* read the 2-letter id of s and return its chunktype enum value */
static enum okp_chunktype readid(const char *s) {
  int v;
  if ((s == NULL) || (s[0] == 0) || (s[1] == 0)) return(OKP_ER); /* too short */
  /* compute id value */
  v = s[0];
  v <<= 8;
  v |= s[1];
  /* match it */
  switch (v) {
    case OKP_FS: return(OKP_FS);
    case OKP_FC: return(OKP_FC);
    case OKP_LM: return(OKP_LM);
    case OKP_PA: return(OKP_PA);
    case OKP_PB: return(OKP_PB);
    case OKP_WL: return(OKP_WL);
    case OKP_HE: return(OKP_HE);
    case OKP_MV: return(OKP_MV);
    case OKP_SC: return(OKP_SC);
    case OKP_NP: return(OKP_NP);
    default: /* invalid id */
      return(OKP_ER);
  }
}


/* read a list of cnt numbers (like 45.2.1$). returns the length of the parsed
 * string on success (incl. the $-terminator), 0 otherwise. */
static unsigned int readnums(unsigned int *nums, int cnt, const char *s) {
  int i;
  unsigned int ccount;
  /* arg validation */
  if ((s == NULL) || (nums == NULL) || (cnt < 1)) return(0);
  /* start by zeroing the nums array */
  for (i = 0; i < cnt; i++) nums[i] = 0;
  /* read values */
  i = 0;
  for (ccount = 0;; ccount++) {
    /* next num? */
    if (s[ccount] == '.') {
      i++;
      if (i == cnt) return(0); /* more nums that I expected */
      continue;
    }
    /* end of chunk? */
    if (s[ccount] == '$') {
      if (i + 1 == cnt) return(ccount + 1);
      return(0); /* error: end of chunk while I expected more values */
    }
    /* invalid byte? */
    if ((s[ccount] < '0') || (s[ccount] > '9')) {
      return(0);
    }
    /* account the new digit */
    nums[i] *= 10;
    nums[i] += (unsigned int)(s[ccount] - '0');
  }
}


/* return the length of a chunk's body (counting the $-terminator), or 0 on error (no $ found) */
static unsigned int readclen(const char *s) {
  unsigned int l;
  for (l = 0; s[l] != '$'; l++) {
    if (s[l] == 0) return(0); /* end of string without $-terminator */
  }
  return(l + 1); /* +1 for the dollar */
}


/* parse an OKP string and explodes it into chunks.
 * returns a chunklist on success, NULL on error. */
struct okp_chunklist *okp_parse(const char *okps) {
  struct okp_chunklist *clist = NULL;
  enum okp_chunktype ctype;
  unsigned int nums[8];
  unsigned int l;
  if (okps == NULL) return(NULL);
  /* */
  for (;;) {
    /* test for end of string (0 or \n) */
    if ((*okps == 0) || (*okps == '\n')) return(clist);
    /* prepare new node */
    if (clist == NULL) {
      clist = calloc(sizeof(struct okp_chunklist), 1);
      if (clist == NULL) goto STRING_ERROR;
    } else {
      struct okp_chunklist *newnode;
      newnode = calloc(sizeof(struct okp_chunklist), 1);
      if (newnode == NULL) goto STRING_ERROR;
      newnode->next = clist;
      clist = newnode;
    }
    /* read id */
    ctype = readid(okps);
    okps += 2;
    clist->id = ctype;
    switch (ctype) {
      case OKP_FS: /* field size fs40.55$ */
        l = readnums(nums, 2, okps);
        if (l == 0) goto STRING_ERROR;
        clist->data.fs.w = nums[0];
        clist->data.fs.h = nums[1];
        break;
      case OKP_FC: /* field content */
        l = readclen(okps); /* how long is the chunk? */
        if (l == 0) goto STRING_ERROR; /* error */
        { /* realloc the block to fit field body */
          struct okp_chunklist *newnode;
          newnode = realloc(clist, sizeof(struct okp_chunklist) + l); /* keep counting the $-terminator, it will be replaced by a null terminator soon */
          if (newnode == NULL) goto STRING_ERROR;
          clist = newnode;
        }
        clist->data.fc.slen = l - 1;
        memcpy(clist->data.fc.playfield, okps, l - 1);
        clist->data.fc.playfield[l - 1] = 0;
        break;
      case OKP_LM: /* last move */
        l = readnums(nums, 3, okps);
        if (l == 0) goto STRING_ERROR;
        clist->data.lm.x = nums[0];
        clist->data.lm.y = nums[1];
        clist->data.lm.p = nums[2];
        break;
      case OKP_PA: /* player A definition */
      case OKP_PB: /* player B definition */
        l = readclen(okps); /* how long is the chunk? */
        if (l == 0) goto STRING_ERROR; /* error */
        { /* realloc the block to fit field body */
          struct okp_chunklist *newnode;
          newnode = realloc(clist, sizeof(struct okp_chunklist) + l); /* keep counting the $-terminator, it will be replaced by a null terminator soon */
          if (newnode == NULL) goto STRING_ERROR;
          clist = newnode;
        }
        if (ctype == OKP_PA) {
          memcpy(clist->data.pa.name, okps, l - 1);
          clist->data.pa.name[l - 1] = 0;
        } else {
          memcpy(clist->data.pb.name, okps, l - 1);
          clist->data.pb.name[l - 1] = 0;
        }
        break;
      case OKP_WL: /* wall */
        l = readnums(nums, 4, okps);
        if (l == 0) goto STRING_ERROR;
        clist->data.wl.x1 = nums[0];
        clist->data.wl.y1 = nums[1];
        clist->data.wl.x2 = nums[2];
        clist->data.wl.y2 = nums[3];
        break;
      case OKP_HE: /* hello */
        if (*okps != '$') goto STRING_ERROR;
        l = 1;
        break;
      case OKP_MV: /* move */
        l = readnums(nums, 2, okps);
        if (l == 0) goto STRING_ERROR;
        clist->data.mv.x = nums[0];
        clist->data.mv.y = nums[1];
        break;
      case OKP_SC: /* scores */
        l = readnums(nums, 2, okps);
        if (l == 0) goto STRING_ERROR;
        clist->data.sc.a = nums[0];
        clist->data.sc.b = nums[1];
        break;
      case OKP_NP: /* next player (0=A, 1=B) -- used when the board has no lm yet */
        l = readnums(nums, 1, okps);
        if (l == 0) goto STRING_ERROR;
        clist->data.np.p = nums[0];
        break;
      case OKP_ER: /* error */
        goto STRING_ERROR;
    }
    okps += l;
  }

  /* error: free memory and return NULL */
  STRING_ERROR:
  okp_freelist(clist);
  return(NULL);
}


/* free a chunklist returned by okp_parse() */
void okp_freelist(struct okp_chunklist *c) {
  struct okp_chunklist *cnext;
  while (c != NULL) {
    cnext = c->next;
    free(c);
    c = cnext;
  }
}


/****************************************************************************
 *              BELOW ARE EXPORT-IMPORT OKP-RELATED FUNCTIONS               *
 ****************************************************************************/


/* return first chunk of t type in list l, or NULL if not found */
static struct okp_chunklist *findchunk(struct okp_chunklist *l, enum okp_chunktype t) {
  struct okp_chunklist *c;
  for (c = l; c != NULL; c = c->next) {
    if (c->id == t) return(c);
  }
  return(NULL);
}


struct kropki_game *kropki_okp2struct(const char *okp, char *player1, char *player2, size_t playersz) {
  struct kropki_game *game;
  struct okp_chunklist *okp_list, *okp_chunk;

  okp_list = okp_parse(okp);
  if (okp_list == NULL) {
    return(NULL);
  }

  /* look for field size */
  okp_chunk = findchunk(okp_list, OKP_FS);
  if (okp_chunk == NULL) {
    okp_freelist(okp_list);
    return(NULL);
  }

  /* kropki_newgame takes unsigned char width/height -- reject values that
   * would truncate (e.g. 256 -> 0) or otherwise fail allocation */
  if ((okp_chunk->data.fs.w < 1) || (okp_chunk->data.fs.w > 255)
   || (okp_chunk->data.fs.h < 1) || (okp_chunk->data.fs.h > 255)) {
    okp_freelist(okp_list);
    return(NULL);
  }

  /* init a new game struct */
  game = kropki_newgame((unsigned char)okp_chunk->data.fs.w,
                        (unsigned char)okp_chunk->data.fs.h, 0);
  if (game == NULL) {
    okp_freelist(okp_list);
    return(NULL);
  }

  /* look for field content */
  okp_chunk = findchunk(okp_list, OKP_FC);
  if (okp_chunk != NULL) {
    int x, y, i = 0;
    if (strlen(okp_chunk->data.fc.playfield) != (unsigned long)(game->field_width * game->field_height)) {
      /* inconsistent playfield lenght */
      kropki_freegame(game);
      okp_freelist(okp_list);
      return(NULL);
    }
    for (y = 0; y < game->field_height; y++) {
      for (x = 0; x < game->field_width; x++) {
        char okpchar = okp_chunk->data.fc.playfield[i++];
        unsigned char wallflag = 0;
        unsigned char capturedflag = 0;
        unsigned char fieldval;
        if ((okpchar >= 'A') && (okpchar <= 'Z')) {
          okpchar += 32;
          wallflag = 1;
        }
        if ((okpchar >= 'd') && (okpchar <= 'f')) {
          okpchar -= 3;
          capturedflag = 1 << 2;
          if (wallflag) wallflag = 1 << 4;
        } else if ((okpchar >= 'g') && (okpchar <= 'i')) {
          okpchar -= 6;
          capturedflag = 2 << 2;
          if (wallflag) wallflag = 2 << 4;
        }
        /* the resulting char must be a, b or c */
        if (okpchar == 'a') {
          fieldval = kropki_PL1_DOT;
          if (wallflag) wallflag = 1 << 4;
        } else if (okpchar == 'b') {
          fieldval = kropki_PL2_DOT;
          if (wallflag) wallflag = 2 << 4;
        } else if (okpchar == 'c') {
          fieldval = kropki_EMPTY;
        } else { /* invalid value */
          kropki_freegame(game);
          okp_freelist(okp_list);
          return(NULL);
        }
        /* compute the game->field value */
        game->field[KROPKI_GOFFS(game, x+1, y+1)] = fieldval | capturedflag | wallflag;
      }
    }
  }

  /* look for last move */
  okp_chunk = findchunk(okp_list, OKP_LM);
  if (okp_chunk != NULL) {
    game->lastmovex = okp_chunk->data.lm.x;
    game->lastmovey = okp_chunk->data.lm.y;
    game->lastplayer = okp_chunk->data.lm.p + 1;
  }

  /* look for scores */
  okp_chunk = findchunk(okp_list, OKP_SC);
  if (okp_chunk != NULL) {
    game->score[0] = okp_chunk->data.sc.a;
    game->score[1] = okp_chunk->data.sc.b;
  }

  /* fill in nextplayer based on lastmove */
  if (game->lastplayer != 0) {
    game->nextplayer = 3 - game->lastplayer;
  } else {
    /* no move yet: honor optional np chunk, otherwise Player A starts.
     * (kropki_newgame(..., 0) left nextplayer at 0 -- that softlocked AIs.) */
    okp_chunk = findchunk(okp_list, OKP_NP);
    if ((okp_chunk != NULL) && (okp_chunk->data.np.p <= 1)) {
      game->nextplayer = (unsigned char)(okp_chunk->data.np.p + 1);
    } else {
      game->nextplayer = 1;
    }
  }

  /* count playable fields and set playablefieldx/y */
  {
    int xx, yy;
    game->playablefields = 0;
    for (yy = 0; yy < game->field_height; yy++) {
      for (xx = 0; xx < game->field_width; xx++) {
        if (KROPKI_GETPOS(game, xx, yy) != 0) continue;
        if (game->playablefields == 0) {
          game->playablefieldx = xx;
          game->playablefieldy = yy;
        }
        game->playablefields++;
      }
    }
  }

  /* reset nextplayer if game ended */
  if (game->playablefields == 0) game->nextplayer = 0;

  /* handle the 'pa' and 'pb' chunks */
  okp_chunk = findchunk(okp_list, OKP_PA);
  if ((okp_chunk != NULL) && (player1 != NULL) && (playersz > 0)) {
    snprintf(player1, playersz, "%s", okp_chunk->data.pa.name);
  }
  okp_chunk = findchunk(okp_list, OKP_PB);
  if ((okp_chunk != NULL) && (player2 != NULL) && (playersz > 0)) {
    snprintf(player2, playersz, "%s", okp_chunk->data.pb.name);
  }

  /* process walls */
  for (okp_chunk = okp_list; okp_chunk != NULL; okp_chunk = okp_chunk->next) {
    struct kropki_wallelement *we;
    if (okp_chunk->id != OKP_WL) continue; /* skip non-walls */
    we = calloc(sizeof(struct kropki_wallelement), 1);
    if (we == NULL) {
      kropki_freegame(game);
      okp_freelist(okp_list);
      return(NULL);
    }
    we->x1 = okp_chunk->data.wl.x1;
    we->y1 = okp_chunk->data.wl.y1;
    we->x2 = okp_chunk->data.wl.x2;
    we->y2 = okp_chunk->data.wl.y2;
    we->player = kropki_getpos(game, we->x1, we->y1) & 3; /* find out who the wall belongs to */
    /* attach the new wall to game */
    we->next = game->walls;
    game->walls = we;
  }

  okp_freelist(okp_list);

  return(game);
}

/* return an OKP string that represents the kropki game present in struct g */
char *kropki_struct2okp(const struct kropki_game *g, const char *player1, const char *player2) {
  int x, y;
  size_t rsz, i;
  char *r;
  struct kropki_wallelement *we;

  /* alloc the result string */
  rsz = 64 + (size_t)(g->field_height * g->field_width);
  r = calloc(rsz, 1);
  if (r == NULL) return(NULL);

  /* generate the easy parts */
  i = snprintf(r, rsz, "fs%d.%d$sc%d.%d$", g->field_width, g->field_height, g->score[0], g->score[1]);
  if (g->lastplayer > 0) {
    i += snprintf(r + i, rsz - i, "lm%d.%d.%d$", g->lastmovex, g->lastmovey, g->lastplayer - 1); /* lastplayer is 0..1 for OKP, but 1..2 for struct */
  } else if ((g->nextplayer >= 1) && (g->nextplayer <= 2)) {
    /* empty board still has a side to move -- encode it so AIs don't see nextplayer==0 */
    i += snprintf(r + i, rsz - i, "np%d$", g->nextplayer - 1);
  }

  /* add player types */
  if (player1 != NULL) i += snprintf(r + i, rsz - i, "pa%s$", player1);
  if (player2 != NULL) i += snprintf(r + i, rsz - i, "pb%s$", player2);

  /* compute field content */
  i += snprintf(r + i, rsz - i, "fc");
  for (y = 0; y < g->field_height; y++) {
    for (x = 0; x < g->field_width; x++) {
      unsigned char fieldval = kropki_getpos(g, x, y);
      char resultval = 0;
      /* is there a dot? */
      switch (fieldval & 3) {
        case 0:
          resultval = 'c';
          break;
        case 1:
          resultval = 'a';
          break;
        case 2:
          resultval = 'b';
          break;
      }
      /* is this field captured? */
      switch ((fieldval >> 2) & 3) {
        case 0:
          break;
        case 1:
          resultval += 3;
          break;
        case 2:
          resultval += 6;
          break;
      }
      /* is this a wall? */
      if ((fieldval & 0x30) != 0) resultval -= 32; /* 0x30 = bin 00110000 */
      /* add to string */
      i += snprintf(r + i, rsz - i, "%c", resultval);
    }
  }
  i += snprintf(r + i, rsz - i, "$");

  /* add walls */
  for (we = g->walls; we != NULL; we = we->next) {
    /* if there's less than 32 bytes left in r, resize it */
    if (rsz - i < 32) {
      char *n = realloc(r, rsz + 64);
      if (n == NULL) {
        free(r);
        return(NULL);
      }
      r = n;
      rsz += 64;
    }
    /* add wall element */
    i += snprintf(r + i, rsz - i, "wl%d.%d.%d.%d$", we->x1, we->y1, we->x2, we->y2);
  }

  return(r);
}


#ifdef BUILD_OKP_PARSE_TOOL

#include <stdio.h>

int main(int argc, char **argv) {
  struct okp_chunklist *clist, *cnode;

  if (argc != 2) {
    printf("usage: okp_parse OKPSTRING\n");
    return(1);
  }

  clist = okp_parse(argv[1]);
  if (clist == NULL) {
    fprintf(stderr, "error: okp_parse() failed to process the string\n");
    return(2);
  }

  printf("found chunks:\n");

  for (cnode = clist; cnode != NULL; cnode = cnode->next) {
    switch (cnode->id) {
      case OKP_FS:
        printf("FS: field of size %ux%u\n", cnode->data.fs.w, cnode->data.fs.h);
        break;
      case OKP_FC:
        printf("FC: len=%u bytes, playfield='%s'\n", cnode->data.fc.slen, cnode->data.fc.playfield);
        break;
      case OKP_LM:
        printf("LM: last move at [%u,%u]\n", cnode->data.lm.x, cnode->data.lm.y);
        break;
      case OKP_ME:
        printf("ME: opponent is player '%c'\n", cnode->data.me.ab);
        break;
      case OKP_WL:
        printf("WL: wall between coordinates [%u,%u] and [%u,%u]\n", cnode->data.wl.x1, cnode->data.wl.y1, cnode->data.wl.x2, cnode->data.wl.y2);
        break;
      case OKP_HE:
        printf("HE: 'Hello!'\n");
        break;
      case OKP_MV:
        printf("MV: move at [%u,%u]\n", cnode->data.mv.x, cnode->data.mv.y);
        break;
      case OKP_SC:
        printf("SC: scores are %u:%u\n", cnode->data.sc.a, cnode->data.sc.b);
        break;
      case OKP_ER:
        fprintf(stderr, "ERROR: okp_parse() returned an error chunk\n");
        break;
    }
  }

  okp_freelist(clist);

  return(0);
}
#endif
