/* Quake-style Command Processor */
/* Malloc-less environment. */

#include "cmdproc.h"



#if 0
Integrating the command processor into another program:

 * Create an instance of cmdproc_t (global variable or struct member).
 * Make regular frequent calls (e.g. once very 0.1 seconds) to:
    cmdproc_cycle(cmdproc_t *)
 * primary methods:
    cmdproc_append(cmdproc_t *, const char *);   /* To append command lines. */
    cmdproc_insert(cmdproc_t *, const char *);   /* To prepend command lines. */
    cmdproc_execute(cmdproc_t *, const char *);  /* bypass buffer, direct exection (one line's worth). */
 * extension methods:
    int cmdproc_register (cmdproc_t *, const char *, cmd_f *);  /* Bind command name to function.  Error (0) if command name already exists. */
    cmdproc_deregister (cmdproc_t *, const char *);  /* Remove command binding. */


For extension commands:
 * Properties for command arguments:
    const char * cmdproc_argv(cmdproc_t *, int);  /* Get nth argument (0 == command) */
    int cmdproc_argc(cmdproc_t *);  /* Number of arguments. */
    const char * cmdproc_args(cmdproc_t *);  /* Copy of command line (unparsed) starting from first parameter (argv[1]). */

#endif /* 0 */



int
cmdproc_strlen (const char *s)
{
  int i;

  for (i = 0; *s; i++, s++);
  return i;
}

int
cmdproc_tolower (int ch)
{
  /* Assumes ASCII. */
  if (('A' <= ch) && (ch <= 'Z'))
    return ('a' + (ch - 'A'));
  return ch;
}

int
cmdproc_toupper (int ch)
{
  /* Assumes ASCII. */
  if (('a' <= ch) && (ch <= 'z'))
    return ('A' + (ch - 'a'));
  return ch;
}

char *
cmdproc_strcpy (char *dst, const char *src)
{
  if (!dst) return NULL;
  if (!src) return dst;
  do  /* do..while lets us copy the nul term also. */
    {
      *dst = *src;
      dst++; src++;
    }
  while (*src);
  return dst;
}

char *
cmdproc_strncpyz (char *dst, const char *src, int len)
{
  int i;

  if (!dst) return NULL;
  if (!src) return dst;
   /* one less, to guarantee space for NUL. */
  for (i = 1; *src && (i < len); i++)
    {
      *dst = *src;
      dst++; src++;
    }
  *dst++ = 0;
  return dst;
}

int
cmdproc_strcasecmp (const char *s1, const char *s2)
{
  int a, b, d;

  if ((!s1) || (!s2))
    return s1 - s2;
  while (*s1 && *s2)
    {
      a = cmdproc_tolower(*s1);
      b = cmdproc_tolower(*s2);
      d = b - a;
      if (d != 0)
        return d;
      s1++; s2++;
    }
  return *s1 - *s2;
}

int
cmdproc_strncasecmp (const char *s1, const char *s2, int len)
{
  int a, b, d;

  if ((!s1) || (!s2))
    return s1 - s2;
  while (*s1 && *s2 && (len > 0))
    {
      a = cmdproc_tolower(*s1);
      b = cmdproc_tolower(*s2);
      d = b - a;
      if (d != 0)
        return d;
      s1++; s2++; len--;
    }
  return *s1 - *s2;
}





/*
  Unformatted output.
*/
int
cmdproc_putstr (cmdproc_t *self, const char *buf)
{
  return printf("%s", buf);
//  return puts(buf);
}

/*
  Formatted output.
*/
int
cmdproc_printf (cmdproc_t *self, const char *fmt, ...)
{
  va_list vp;
  int retval;

  va_start(vp, fmt);
  retval = vprintf(fmt, vp);
  va_end(vp);
  return retval;
}


int
cmdproc_dump (cmdproc_t *self)
{
  int buflen;
  int i, c;

  buflen = (self->cbuf.tail - self->cbuf.head + self->cbuf.max) % self->cbuf.max;
  if (buflen < 0) buflen = self->cbuf.max - buflen - 1;
  printf("Command list:\n");
  for (i = 0; i < self->maxlu; i++)
    {
      if (self->lu[i].used)
        printf(" '%s': %p\n", self->lu[i].cmdname, self->lu[i].cmdfunc);
    }
  printf("Command buffer size: %d\n", buflen);
  printf("Command buffer content:");
  for (i = 0; i < buflen; i++)
    {
      c = self->cbuf.buf[(self->cbuf.head + i) % self->cbuf.max];
      if (c < 32)
        printf("%%%02X", c);
      else
        printf("%c", c);
    }
  printf("\n");
  return 0;
}


/*
  Append `text' to the end of command buffer.
  Returns how many characters in `text' were appended.
*/
int
cmdproc_append (cmdproc_t *self, const char *text)
{
  const char *p;
  int n, t, lastch;

  p = text;
  n = 0;
/*  crumb("cmdproc_append: appending '%s'\n", text);
*/
  while (*p)
    {
      t = (self->cbuf.tail + 1) % self->cbuf.max;  /* New tail spot. */
      if (t == self->cbuf.head)
        {
          /* Ring buffer full. */
          crumb("cmdproc_append: ring buffer full\n");
          break;
        }
      self->cbuf.buf[self->cbuf.tail] = *p;
/*      crumb("cmdproc_append: append char (%d)%c\n", *p, *p);
*/
      self->cbuf.tail = t;
      lastch = *p;
      n++;
      p++;
    }
  self->cbuf.buf[self->cbuf.tail] = 0;  /* null-terminate. */
/*
  if ((lastch != ';') && (lastch != '\n'))
    self->cbuf.tail = (self->cbuf.tail + 1) % self->cbuf.max;
*/
  return n;
}


/*
  Prepend (Insert) `text' into command buffer.
  Returns number of characters in `text' (from its end) inserted.
*/
int
cmdproc_insert (cmdproc_t *self, const char *text)
{
  const char *p;
  int copylen, n, h, lastch;

  if (!(text && *text))
    return 0;
  p = text;
  copylen = 0;
  for (; *p; copylen++, p++);
  n = 0;
  h = 0;
  p--;
  while (n < copylen)
    {
      h = (self->cbuf.head - 1 + self->cbuf.max) % self->cbuf.max;  /* New head spot. */
      if (h == self->cbuf.tail)
        {
          /* Barging in on its tail. */
          break;
        }
      self->cbuf.buf[h] = *p;
      self->cbuf.head = h;
      lastch = *p;
      p--;
      n++;
    }
  return n;
}


/*
  Add new command to commands lookup table.
*/
int
cmdproc_register (cmdproc_t *self, const char *cmdname, cmd_f cmdfunc)
{
  cmdlookup_t *c;
  int i, end;

  end = self->maxlu;
  c = self->lu;
  i = 0;
  while (i < end)
    {
      c = self->lu + i;
      if (c->used)
        {
          /* slot already used. */
//          if (0 == strncasecmp(cmdname, c->cmdname, CMDLEN))
          if (0 == cmdproc_strncasecmp(cmdname, c->cmdname, CMDLEN))
            {
              return 0;  /* Can't register same name. */
            }
        }
      else
        {
          c->used = 1;
//          strncpy(c->cmdname, cmdname, CMDLEN);
          cmdproc_strncpyz(c->cmdname, cmdname, CMDLEN);
//          c->cmdname[CMDLEN-1] = 0;
          c->cmdfunc = cmdfunc;
          return 1;  /* Successfully registered. */
        }
      i++;
    }
  return 0;  /* Failed to register -- out of space. */
}


/*
  Remove command from commands lookup table.
*/
int
cmdproc_deregister (cmdproc_t *self, const char *cmdname)
{
  cmdlookup_t *c;
  int i, end;

  end = self->maxlu;
  c = self->lu;
  i = 0;
  while (i < end)
    {
      c = self->lu + i;
      if (c->used)
        {
          /* slot already used. */
          if (0 == cmdproc_strncasecmp(cmdname, c->cmdname, CMDLEN))
            {
              c->cmdfunc = NULL;
              c->cmdname[0] = 0;
              c->used = 0;
              return 1;  /* Successfully deregistered. */
            }
        }
      i++;
    }
  return 0;  /* Couldn't deregister -- name not found. */
}


/*
  Extract one line from command buffer.
  Returns pointer to extracted line.
*/
const char *
cmdproc_extract (cmdproc_t *self)
{
  int i, c, e;
  parse_t parse;

  e = 0;
  self->extract[e] = 0;
  parse = START;
#if 0
  for (i = self->cbuf.head; i != self->cbuf.tail; i = (i + 1) % self->cbuf.max)
    { 
      c = self->cbuf.buf[i];
      if ((c == 0) || (c == '\n') || ((parse != QUOTE) && (c == ';')))
          break;
      self->extract[e++] = c;
      if (c == '"')
        parse = (parse == QUOTE) ? START : QUOTE;
    }
#else
  for (i = self->cbuf.head; parse != STOP; i = (i + 1) % self->cbuf.max)
    {
      c = self->cbuf.buf[i];
      if (i == self->cbuf.tail)
          parse = STOP;
      switch (parse)
        {
          case START:
#undef EXTRASMARTEXTRACT
          case TOKEN:
            if ((c == 0) || (c == '\n') || (c == ';'))
              parse = STOP;
#if EXTRASMARTEXTRACT
            else if (c == '/')
              parse = SEMICOMMENT;
#endif /* EXTRASMARTEXTRACT */
            else if (c == '"')
              parse = QUOTE;
            break;
          case QUOTE:
            if ((c == 0) || (c == '\n'))
              parse = STOP;
            else if (c == '"')
              parse = START;
            break;
#ifdef EXTRASMARTEXTRACT
          case SEMICOMMENT:
            if ((c == 0) || (c == '\n') || (c == ';'))
              parse = STOP;
            else if (c == '/')
              parse = COMMENT;
            else
              parse = TOKEN;
            break;
          case COMMENT:
            if ((c == 0) || (c == '\n'))
              parse = STOP;
            break;
#endif
          case STOP:
            break;
        }
      if (parse == STOP)
          i--;
      else
          self->extract[e++] = c;
    }
#endif /* 0 */
  self->extract[e] = 0;
  self->cbuf.head = (i + 1) % self->cbuf.max;
//  crumb("Extracted: '%s'\n", self->extract);
  return self->extract;
}


/*
  Parse command line to args fields in self->args.
  Returns number of tokens parsed.
*/
int
cmdproc_parse (cmdproc_t *self, const char *line)
{
  int c, i, slen;
  int start, stop, copylen;
  parse_t parse;
  char *dst;

  memset(&(self->cmdarg), 0, sizeof(self->cmdarg));
  dst = self->cmdarg.arguments;  /* Start of arguments list. */
  parse = START;
  start = stop = 0;
  i = 0;
//  c = line[i];
  slen = cmdproc_strlen(line);  /* for failsafe. */
  while (parse != STOP)
    {
      c = line[i];
      switch (parse)
        {
          case START:
//            crumb("parsing start '%c'\n", c);
            if ((c == 0) || (c == '\n') || (c == ';'))
              {
                parse = STOP;
              }
            else if (c == '/')
              {
                start = i;
                parse = SEMICOMMENT;
              }
            else if (c == '"')
              {
                start = i + 1;
                parse = QUOTE;
              }
            else if (c > 32)
              {
                start = i;
                parse = TOKEN;
              }
            /* otherwise keep on going. */
            break;
          case TOKEN:
//            crumb("parsing token '%c'\n", c);
            if ((c == 0) || (c == '\n') || (c == ';'))
              {
                stop = i;
                parse = STOP;
              }
            else if (c == '/')
              {
                /* Maybe a comment.  Could just be a slash in mid-token. */
                parse = TOKENSEMICOMMENT;
              }
            else if (c == '"')
              {
                /* token sep and token start.  Backstep one. */
                stop = i;
                parse = START;
                i--;
              }
            else if (c <= 32)
              {
                /* token sep. */
                stop = i;
                parse = START;
                i--;
              }
            /* otherwise, still in token.  Keep on counting. */
            break;
          case QUOTE:
//            crumb("parsing quoted '%c'\n", c);
            if ((c == 0) || (c == '\n'))
              {
                stop = i;
                parse = STOP;
              }
            else if (c == '"')
              {
                stop = i;
                parse = START;
              }
            break;
          case SEMICOMMENT:
            if (c == '/')
              {
                parse = COMMENT;
              }
            else
              {
                /* Token starting with '/'. */
                i--;
                parse = TOKEN;
              }
            break;
          case TOKENSEMICOMMENT:
            if (c == '/')
              {
                stop = i-1;
                parse = COMMENT;
              }
            else
              {
                parse = TOKEN;
              }
          case COMMENT:
            if ((c == 0) || (c == '\n') || (c == ';'))
              {
                /* no token. */
                start = stop = i;
                parse = STOP;
              }
            /* else keep on going. */
            break;
          default:
            /* Unknown parser state. */
            break;
        }
      if (stop > start)
        {
          if (self->cmdarg.count == 1)
              self->cmdarg.argstr = line + start;
          copylen = stop - start + 1;
//          strncpy(dst, line + start, copylen);
          cmdproc_strncpyz(dst, line + start, copylen);
          self->cmdarg.vector[self->cmdarg.count] = dst;
          self->cmdarg.count++;
          dst += copylen;  /* Shift over dst to new stop. */
          stop = 0;
        }
      i++;
      if (i > slen)   /* failsafe. (or is it?) */
        parse = STOP;
    }
  return self->cmdarg.count;
}


/* Report unknown command. */
int
cmdproc_unknown (cmdproc_t *self)
{
  self->printf(self, "Unknown command: %s\n", self->cmdarg.vector[0]);
  return 0;
}


/*
  Dispatch command based on contents of args fields.
  Returns command return value.
*/
int
cmdproc_dispatch (cmdproc_t *self)
{
  cmdlookup_t *mapping;
  const char *cmd;
  cmd_f dispatchee;
  int i;

  mapping = self->lu;
  if (self->cmdarg.count < 1)
    /* No command. */
    return 0;
  cmd = self->cmdarg.vector[0];
  dispatchee = NULL;
  i = 0;
  while (i < self->maxlu)
    {
      if ((mapping->used) && (0 == cmdproc_strncasecmp(mapping->cmdname, cmd, CMDLEN)))
        {
          dispatchee = mapping->cmdfunc;
          break;
        }
      mapping++;
      i++;
    }
  if (dispatchee)
    {
      return dispatchee(self);
    }
  else
    {
      return cmdproc_unknown(self);
    }
}


/*
  Execute `text' as a command.  Parse, then dispatch.
*/
int
cmdproc_execute (cmdproc_t *self, const char *text)
{
  cmdproc_parse(self, text);
  return cmdproc_dispatch(self);
}


/*
  One cycle of command processor.
*/
int
cmdproc_cycle (cmdproc_t *self)
{
  const char *line;

  if (self->suspend)
    {
      self->suspend--;
      crumb("Waiting %d more cycles\n", self->suspend);
      return 0;
    }
  line = cmdproc_extract(self);
//  crumb("Extracted line: %s\n", line);
#if 0
  cmdproc_parse(self, line);
  {
    int i;
    crumb("Args dump:\n");
    for (i = 0; i < self->cmdarg.count; i++)
      {
        crumb(" arg[%d] = \"%s\"\n", i, self->cmdarg.vector[i]);
      }
  }
  return cmdproc_dispatch(self);
#else
  return cmdproc_execute(self, line);
#endif /* 0 */
}



int
cmdproc_argc (cmdproc_t *self)
{
  return self->cmdarg.count;
}

const char *
cmdproc_args (cmdproc_t *self)
{
  const char *s;

  s = self->cmdarg.argstr;
  if (!s)
    s = "";
  return s;
}

const char *
cmdproc_argv (cmdproc_t *self, int n)
{
  return self->cmdarg.vector[n];
}



int
cmd_crash (cmdproc_t *self)
{
  self->putstr(self, "*** Enforced crash (because I can't be sure 'quit' works as expected).\n");
  exit(0);
  return 0;  /* Though I fail to see how exit() could fail. */
}

int
cmd_cmdlist (cmdproc_t *self)
{
  cmdlookup_t *cmdbind;
  int i, n;

  i = 0;
  n = 0;
  cmdbind = self->lu;
  while (i < self->maxlu)
    {
      if (cmdbind->used)
        {
          self->printf(self, "%s\n", cmdbind->cmdname);
          n++;
        }
      cmdbind++;
      i++;
    }
  self->printf(self, "%d commands.\n", n);
  return n;
}

int
cmd_echo (cmdproc_t *self)
{
  int i;
  for (i = 1; i < cmdproc_argc(self); i++)
    {
      if (i != 1)
        self->putstr(self, " ");
      self->putstr(self, cmdproc_argv(self, i));
    }
  self->putstr(self, "\n");
  return 0;
}

int
cmd_exec (cmdproc_t *self)
{
  FILE *execfile;
  const char *filename;
  char buf[CBUFSIZE];

  filename = cmdproc_argv(self, 1);
  execfile = fopen(filename, "rt");
  if (!execfile)
    {
      self->printf(self, "Could not exec %s\n", filename);
      return 1;
    }
  self->printf(self, "Execing %s\n", filename);
  fread(buf, sizeof(char), sizeof(buf), execfile);
  buf[sizeof(buf)] = 0;  /* Force nul-terminate. */
//  cmdproc_execute(self, buf);
  cmdproc_append(self, buf);
  fclose(execfile);
  return 0;
}

int
cmd_wait (cmdproc_t *self)
{
  int i;

  if (cmdproc_argc(self) > 1)
    {
      i = atoi(cmdproc_argv(self, 1));
      self->suspend += i;
    }
  else
    {
      self->suspend++;
    }
  return 0;
}



cmdproc_t *
cmdproc_init (cmdproc_t *self)
{
  memset(self, 0, sizeof(*self));
  self->cbuf.max = sizeof(self->cbuf.buf);
  self->maxlu = sizeof(self->lu) / sizeof(*self->lu);
//  fflush(stdout);
  /* Methods. */
  self->printf = cmdproc_printf;
  self->putstr = cmdproc_putstr;
  /* Built-in commands. */
  cmdproc_register(self, "crash", cmd_crash);
  cmdproc_register(self, "cmdlist", cmd_cmdlist);
  cmdproc_register(self, "echo", cmd_echo);
  cmdproc_register(self, "exec", cmd_exec);
  cmdproc_register(self, "wait", cmd_wait);
  return self;
}





int
main (int argc, char **argv)
{
  cmdproc_t _CP, *CP = &_CP;
  char inbuf[1024];


  cmdproc_init(CP);
#if 0
  cmdproc_append(CP, "echo hi\n");
  cmdproc_append(CP, "echo lali  ho\n");
  cmdproc_append(CP, "yo\n");
  cmdproc_append(CP, "quit\n");
  cmdproc_insert(CP, "hrm.;");
  cmdproc_append(CP, "echo \"hello;  world\".\n");
  cmdproc_dump(CP);
  while (CP->cbuf.head != CP->cbuf.tail)
    cmdproc_cycle(CP);
//  cmd_crash(CP);
#endif /* 0 */

  printf("Command Processor struct memory footprint: %d B\n", sizeof(*CP));
  while (!feof(stdin))
    {
      printf("] "); fflush(stdout);
      *inbuf = 0;
      fgets(inbuf, sizeof(inbuf), stdin);
      cmdproc_append(CP, inbuf);
      do {
        cmdproc_cycle(CP);
      } while (CP->cbuf.head != CP->cbuf.tail);
    }
  CP->putstr(CP, "\n");

  return 0;
}
