/*
 * OKP parsing library
 *
 * Copyright (C) 2020 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 <stdlib.h>
#include <string.h>

#include "okp_parse.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_ME: return(OKP_ME);
    case OKP_WL: return(OKP_WL);
    case OKP_HE: return(OKP_HE);
    case OKP_MV: return(OKP_MV);
    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] = 0;
        }
        clist->data.fc.slen = l - 1;
        memcpy(clist->data.fc.playfield, okps, l);
        clist->data.fc.playfield[l - 1] = 0;
        break;
      case OKP_LM: /* last move */
        l = readnums(nums, 2, okps);
        if (l == 0) goto STRING_ERROR;
        clist->data.lm.x = nums[0];
        clist->data.lm.y = nums[1];
        break;
      case OKP_ME: /* */
        if ((*okps != 'a') && (*okps != 'b')) goto STRING_ERROR;
        if (okps[1] != '$') goto STRING_ERROR;
        clist->data.me.ab = *okps;
        l = 2;
        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_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;
  }
}


#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_ER:
        fprintf(stderr, "ERROR: okp_parse() returned an error chunk\n");
        break;
    }
  }

  okp_freelist(clist);

  return(0);
}
#endif
