/*
** cmdmisc.c - mruby debugger miscellaneous command functions
**
*/

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

#include "apilist.h"
#include "apistring.h"
#include <mruby/compile.h>

typedef struct help_msg {
  const char *cmd1;
  const char *cmd2;
  const char *short_msg;
  const char *long_msg;
} help_msg;

static help_msg help_msg_list[] = {
  {
    "b[reak]", NULL, "Set breakpoint",
    "Usage: break [file:]line\n"
    "       break [class:]method\n"
    "\n"
    "Set breakpoint at specified line or method.\n"
    "If \'[file:]line\' is specified, break at start of code for that line (in a file).\n"
    "If \'[class:]method\' is specified, break at start of code for that method (of the class).\n"
  },
  {
    "c[ontinue]", NULL, "Continue program being debugged",
    "Usage: continue [N]\n"
    "\n"
    "Continue program stopped by a breakpoint.\n"
    "If N, which is non negative value, is passed,\n"
    "proceed program until the N-th breakpoint is coming.\n"
    "If N is not passed, N is assumed 1.\n"
  },
  {
    "d[elete]", NULL, "Delete some breakpoints",
    "Usage: delete [bpno1 [bpno2 [... [bpnoN]]]]\n"
    "\n"
    "Delete some breakpoints.\n"
    "Arguments are breakpoint numbers with spaces in between.\n"
    "To delete all breakpoints, give no argument.\n"
  },
  {
    "dis[able]", NULL, "Disable some breakpoints",
    "Usage: disable [bpno1 [bpno2 [... [bpnoN]]]]\n"
    "\n"
    "Disable some breakpoints.\n"
    "Arguments are breakpoint numbers with spaces in between.\n"
    "To disable all breakpoints, give no argument.\n"
  },
  {
    "en[able]", NULL, "Enable some breakpoints",
    "Usage: enable [bpno1 [bpno2 [... [bpnoN]]]]\n"
    "\n"
    "Enable some breakpoints.\n"
    "Arguments are breakpoint numbers with spaces in between.\n"
    "To enable all breakpoints, give no argument.\n"
  },
  {
    "ev[al]", NULL, "Evaluate expression",
    "Usage: eval expr\n"
    "\n"
    "It evaluates and prints the value of the mruby expression.\n"
    "This is equivalent to the \'print\' command.\n"
  },
  {
    "h[elp]", NULL, "Print this help",
    "Usage: help [command]\n"
    "\n"
    "With no arguments, help displays a short list of commands.\n"
    "With a command name as help argument, help displays how to use that command.\n"
  },
  {
    "i[nfo]", "b[reakpoints]", "Status of breakpoints",
    "Usage: info breakpoints [bpno1 [bpno2 [... [bpnoN]]]]\n"
    "\n"
    "Status of specified breakpoints (all user-settable breakpoints if no argument).\n"
    "Arguments are breakpoint numbers with spaces in between.\n"
  },
  {
    "i[nfo]", "l[ocals]", "Print name of local variables",
    "Usage: info locals\n"
    "\n"
    "Print name of local variables.\n"
  },
  {
    "l[ist]", NULL, "List specified line",
    "Usage: list\n"
    "       list first[,last]\n"
    "       list filename:first[,last]\n"
    "\n"
    "Print lines from a source file.\n"
    "\n"
    "With first and last, list prints lines from first to last.\n"
    "When last is empty, it stands for ten lines away from first.\n"
    "With filename, list prints lines in the specified source file.\n"
  },
  {
    "p[rint]", NULL, "Print value of expression",
    "Usage: print expr\n"
    "\n"
    "It evaluates and prints the value of the mruby expression.\n"
    "This is equivalent to the \'eval\' command.\n"
  },
  {
    "q[uit]", NULL, "Exit mrdb",
    "Usage: quit\n"
    "\n"
    "Exit mrdb.\n"
  },
  {
    "r[un]", NULL, "Start debugged program",
    "Usage: run\n"
    "\n"
    "Start debugged program.\n"
  },
  {
    "s[tep]", NULL, "Step program until it reaches a different source line",
    "Usage: step\n"
    "\n"
    "Step program until it reaches a different source line.\n"
  },
  { NULL, NULL, NULL, NULL }
};

typedef struct listcmd_parser_state {
  mrb_bool parse_error;
  mrb_bool has_line_min;
  mrb_bool has_line_max;
  char *filename;
  uint16_t line_min;
  uint16_t line_max;
} listcmd_parser_state;

static listcmd_parser_state*
listcmd_parser_state_new(mrb_state *mrb)
{
  listcmd_parser_state *st = (listcmd_parser_state*)mrb_malloc(mrb, sizeof(listcmd_parser_state));
  static const listcmd_parser_state st_zero = {0};
  *st = st_zero;
  return st;
}

static void
listcmd_parser_state_free(mrb_state *mrb, listcmd_parser_state *st)
{
  if (st != NULL) {
    if (st->filename != NULL) {
      mrb_free(mrb, st->filename);
    }
    mrb_free(mrb, st);
  }
}

static mrb_bool
parse_uint(char **sp, uint16_t *n)
{
  char *p;
  int i;

  if (*sp == NULL || **sp == '\0') {
    return FALSE;
  }

  for (p = *sp; *p != '\0' && ISDIGIT(*p); p++)
    ;

  if (p != *sp && (i = atoi(*sp)) >= 0) {
    *n = (uint16_t)i;
    *sp = p;
    return TRUE;
  }
  return FALSE;
}

static mrb_bool
skip_char(char **sp, char c)
{
  if (*sp != NULL && **sp == c) {
    ++*sp;
    return TRUE;
  }
  return FALSE;
}

static mrb_bool
parse_lineno(mrb_state *mrb, char **sp, listcmd_parser_state *st)
{
  if (*sp == NULL || **sp == '\0') {
    return FALSE;
  }

  st->has_line_min = FALSE;
  st->has_line_max = FALSE;

  if (parse_uint(sp, &st->line_min)) {
    st->has_line_min = TRUE;
  }
  else {
    return FALSE;
  }

  if (skip_char(sp, ',')) {
    if (parse_uint(sp, &st->line_max)) {
      st->has_line_max = TRUE;
    }
    else {
      st->parse_error = TRUE;
      return FALSE;
    }
  }
  return TRUE;
}

static mrb_bool
parse_filename(mrb_state *mrb, char **sp, listcmd_parser_state *st)
{
  char *p;
  int len;

  if (st->filename != NULL) {
    mrb_free(mrb, st->filename);
    st->filename = NULL;
  }

  if ((p = strchr(*sp, ':')) != NULL) {
    len = p - *sp;
  }
  else {
    len = strlen(*sp);
  }

  if (len > 0 && (st->filename = mrdb_strndup(mrb, *sp, len)) != NULL) {
    *sp += len;
    return TRUE;
  }
  else {
    return FALSE;
  }
}

char*
replace_ext(mrb_state *mrb, const char *filename, const char *ext)
{
  size_t len;
  const char *p;
  char *s;

  if (filename == NULL) {
    return NULL;
  }

  if ((p = strrchr(filename, '.')) != NULL && strchr(p, '/') == NULL) {
    len = p - filename;
  }
  else {
    len = strlen(filename);
  }

  s = (char*)mrb_malloc(mrb, len + strlen(ext) + 1);
  memset(s, '\0', len + strlen(ext) + 1);
  strncpy(s, filename, len);
  strcat(s, ext);

  return s;
}

/* parse: <lineno> | <filename>:<lineno> | <filename> */
static void
parse_file_line_spec(mrb_state *mrb, char *arg, listcmd_parser_state *st)
{
  char *p = arg;

  if (parse_lineno(mrb, &p, st)) {
    /* matched <lineno> or <lineno>,<lineno> */
  }
  else if (parse_filename(mrb, &p, st)) {
    if (skip_char(&p, ':') && !parse_lineno(mrb, &p, st)) {
      st->parse_error = TRUE;
    }
  }
  else {
    st->parse_error = TRUE;
  }
  if (*p != '\0') {
    st->parse_error = TRUE;
  }
}

static mrb_bool
parse_listcmd_args(mrb_state *mrb, mrdb_state *mrdb, listcmd_parser_state *st)
{
  switch (mrdb->wcnt) {
  case 2:
    parse_file_line_spec(mrb, mrdb->words[1], st);
    break;
  case 1:
  case 0:
    break;
  default:
    st->parse_error = TRUE;
    printf("too many arguments\n");
    break;
  }

  if (!st->parse_error) {
    if (!st->has_line_min) {
      st->line_min = (!st->filename && mrdb->dbg->prvline > 0) ? mrdb->dbg->prvline : 1;
    }

    if (!st->has_line_max) {
      st->line_max = st->line_min + 9;
    }

    if (st->filename == NULL) {
      if (mrdb->dbg->prvfile && strcmp(mrdb->dbg->prvfile, "-")) {
        st->filename = replace_ext(mrb, mrdb->dbg->prvfile, ".rb");
      }
    }
  }

  if (st->parse_error || st->filename == NULL) {
    return FALSE;
  }

  return TRUE;
}

static mrb_bool
check_cmd_pattern(const char *pattern, const char *cmd)
{
  const char *lbracket, *rbracket, *p, *q;

  if (pattern == NULL && cmd == NULL) {
    return TRUE;
  }
  if (pattern == NULL || cmd == NULL) {
    return FALSE;
  }
  if ((lbracket = strchr(pattern, '[')) == NULL) {
    return !strcmp(pattern, cmd);
  }
  if ((rbracket = strchr(pattern, ']')) == NULL) {
    return FALSE;
  }
  if (strncmp(pattern, cmd, lbracket - pattern)) {
    return FALSE;
  }

  p = lbracket + 1;
  q = (char*)cmd + (lbracket - pattern);

  for (; p < rbracket && *q != '\0'; p++, q++) {
    if (*p != *q) {
      break;
    }
  }
  return *q == '\0';
}

static help_msg*
get_help_msg(char *cmd1, char *cmd2)
{
  help_msg *p;

  if (cmd1 == NULL) {
    return NULL;
  }
  for (p = help_msg_list; p->cmd1 != NULL; p++) {
    if (check_cmd_pattern(p->cmd1, cmd1) && check_cmd_pattern(p->cmd2, cmd2)) {
      return p;
    }
  }
  return NULL;
}

static mrb_bool
show_short_help(void)
{
  help_msg *p;

  printf("Commands\n");

  for (p = help_msg_list; p->cmd1 != NULL; p++) {
    if (p->cmd2 == NULL) {
      printf("  %s -- %s\n", p->cmd1, p->short_msg);
    }
    else {
      printf("  %s %s -- %s\n", p->cmd1, p->cmd2, p->short_msg);
    }
  }
  return TRUE;
}

static mrb_bool
show_long_help(char *cmd1, char *cmd2)
{
  help_msg *help;

  if ((help = get_help_msg(cmd1, cmd2)) == NULL) {
    return FALSE;
  }
  printf("%s", help->long_msg);
  return TRUE;
}

dbgcmd_state
dbgcmd_list(mrb_state *mrb, mrdb_state *mrdb)
{
  char *filename;
  listcmd_parser_state *st = listcmd_parser_state_new(mrb);

  if (parse_listcmd_args(mrb, mrdb, st)) {
    if ((filename = mrb_debug_get_source(mrb, mrdb, mrdb->srcpath, st->filename)) == NULL) {
      filename = st->filename;
    }
    mrb_debug_list(mrb, mrdb->dbg, filename, st->line_min, st->line_max);

    if (filename != NULL && filename != st->filename) {
      mrb_free(mrb, filename);
    }
    listcmd_parser_state_free(mrb, st);
  }

  return DBGST_PROMPT;
}

dbgcmd_state
dbgcmd_help(mrb_state *mrb, mrdb_state *mrdb)
{
  mrb_bool is_valid;
  int i;

  switch (mrdb->wcnt) {
  case 0:
  case 1:
    is_valid = show_short_help();
    break;
  case 2:
    is_valid = show_long_help(mrdb->words[1], NULL);
    break;
  case 3:
    is_valid = show_long_help(mrdb->words[1], mrdb->words[2]);
    break;
  default:
    is_valid = FALSE;
    break;
  }

  if (!is_valid) {
    printf("Invalid command \"");
    for (i = 1; i < mrdb->wcnt; i++) {
      printf("%s%s", i == 1 ? "" : " ", mrdb->words[i]);
    }
    printf("\". Try \"help\".\n");
  }

  return DBGST_PROMPT;
}

dbgcmd_state
dbgcmd_quit(mrb_state *mrb, mrdb_state *mrdb)
{
  switch (mrdb->dbg->xm) {
  case DBG_RUN:
  case DBG_STEP:
  case DBG_NEXT:
    while (1) {
      char c;
      int buf;

      printf("The program is running.  Exit anyway? (y or n) ");
      fflush(stdout);

      if ((buf = getchar()) == EOF) {
        mrdb->dbg->xm = DBG_QUIT;
        break;
      }
      c = buf;
      while (buf != '\n' && (buf = getchar()) != EOF)
        ;

      if (c == 'y' || c == 'Y') {
        mrdb->dbg->xm = DBG_QUIT;
        break;
      }
      else if (c == 'n' || c == 'N') {
        break;
      }
      else {
        printf("Please answer y or n.\n");
      }
    }
    break;
  default:
    mrdb->dbg->xm = DBG_QUIT;
    break;
  }

  if (mrdb->dbg->xm == DBG_QUIT) {
    raise_debugger_exception(mrb, "DebuggerExit", "Exit mrdb");
  }
  return DBGST_PROMPT;
}
