#include "staprun.h"
#include <termios.h>
#include <unistd.h>

#ifdef HAVE_MONITOR_LIBS

#include <json-c/json.h>
#include <panel.h>
#include <curses.h>
#include <time.h>
#include <string.h>

#define COMP_FNS 7
#define MAX_INDEX_LEN 5
#define MAX_COLS 256
#define MAX_DATA 262144 /* XXX: pass procfs.read().maxsize(NNN) from stap */
#define MAX_HISTORY 8192

#define MIN(X,Y) (((X) < (Y)) ? (X) : (Y))
#define MAX(X,Y) (((X) > (Y)) ? (X) : (Y))

#define HIGHLIGHT(NAME,IDX,CUR) (((IDX == CUR)) ? (NAME "*") : (NAME))

typedef struct History_Queue
{
  char *lines[MAX_HISTORY]; /* each malloc/strdup'd(). */
  int oldest;
  int newest;
  int count;
} History_Queue;

enum probe_attributes
{
  p_index,
  p_state,
  p_hits,
  p_min,
  p_avg,
  p_max,
  p_name,
  num_attributes
};

static enum state
{
  normal,
  exited,
  insert,
} monitor_state;

static History_Queue h_queue;
static pthread_mutex_t mutex;
static char probe[MAX_INDEX_LEN];
static WINDOW *status = NULL;
static WINDOW *output = NULL;
static WINDOW *help = NULL;
static WINDOW *status_border = NULL;
static WINDOW *help_border = NULL;
static PANEL *status_panel = NULL;
static PANEL *output_panel = NULL;
static PANEL *help_panel = NULL;
static PANEL *status_border_panel = NULL;
static PANEL *help_border_panel = NULL;
static int comp_fn_index = 0;
static int num_probes = 0;
static int probe_scroll = 0;
static int output_scroll = 0;
static time_t elapsed_time = 0;
static time_t start_time = 0;
static int resized = 0;
static int input = 0; /* fresh input received in monitor_input() */
static int rendered = 0;
static int status_hidden = 0;
static int active_window = 1; /* 0 for status window, 1 for output window */
static int disable_curses = 0; /* 0 if curses should be enabled, 1 otherwise */
/* Forward declarations */
static int comp_index(const void *p1, const void *p2);
static int comp_state(const void *p1, const void *p2);
static int comp_hits(const void *p1, const void *p2);
static int comp_min(const void *p1, const void *p2);
static int comp_avg(const void *p1, const void *p2);
static int comp_max(const void *p1, const void *p2);
static int comp_name(const void *p1, const void *p2);

static int (*comp_fn[COMP_FNS])(const void *p1, const void *p2) =
{
  comp_index,
  comp_state,
  comp_hits,
  comp_min,
  comp_avg,
  comp_max,
  comp_name
};

static int comp_index(const void *p1, const void *p2)
{
  json_object *j1 = *(json_object**)p1;
  json_object *j2 = *(json_object**)p2;
  json_object *index1, *index2;
  json_object_object_get_ex(j1, "index", &index1);
  json_object_object_get_ex(j2, "index", &index2);
  return json_object_get_int(index1) - json_object_get_int(index2);
}

static int comp_state(const void *p1, const void *p2)
{
  json_object *j1 = *(json_object**)p1;
  json_object *j2 = *(json_object**)p2;
  json_object *state1, *state2;
  json_object_object_get_ex(j1, "state", &state1);
  json_object_object_get_ex(j2, "state", &state2);
  return strcmp(json_object_get_string(state1), json_object_get_string(state2));
}

static int comp_hits(const void *p1, const void *p2)
{
  json_object *j1 = *(json_object**)p1;
  json_object *j2 = *(json_object**)p2;
  json_object *hits1, *hits2;
  json_object_object_get_ex(j1, "hits", &hits1);
  json_object_object_get_ex(j2, "hits", &hits2);
  return json_object_get_int(hits2) - json_object_get_int(hits1);
}

static int comp_min(const void *p1, const void *p2)
{
  json_object *j1 = *(json_object**)p1;
  json_object *j2 = *(json_object**)p2;
  json_object *min1, *min2;
  json_object_object_get_ex(j1, "min", &min1);
  json_object_object_get_ex(j2, "min", &min2);
  return json_object_get_int(min2) - json_object_get_int(min1);
}

static int comp_avg(const void *p1, const void *p2)
{
  json_object *j1 = *(json_object**)p1;
  json_object *j2 = *(json_object**)p2;
  json_object *avg1, *avg2;
  json_object_object_get_ex(j1, "avg", &avg1);
  json_object_object_get_ex(j2, "avg", &avg2);
  return json_object_get_int(avg2) - json_object_get_int(avg1);
}

static int comp_max(const void *p1, const void *p2)
{
  json_object *j1 = *(json_object**)p1;
  json_object *j2 = *(json_object**)p2;
  json_object *max1, *max2;
  json_object_object_get_ex(j1, "max", &max1);
  json_object_object_get_ex(j2, "max", &max2);
  return json_object_get_int(max2) - json_object_get_int(max1);
}

static int comp_name(const void *p1, const void *p2)
{
  json_object *j1 = *(json_object**)p1;
  json_object *j2 = *(json_object**)p2;
  json_object *name1, *name2;
  json_object_object_get_ex(j1, "name", &name1);
  json_object_object_get_ex(j2, "name", &name2);
  return strcmp(json_object_get_string(name1), json_object_get_string(name2));
}

static void write_command(const char *msg)
{
  char path[PATH_MAX];
  FILE* fp;
  size_t len = strlen(msg);

  if (sprintf_chk(path, "/proc/systemtap/%s/monitor_control", modname))
    return;
  if (!(fp = fopen(path, "w")))
    return;
  if (fwrite(msg, 1, len, fp) != len)
    {
      fclose(fp);
      return;
    }
  fclose(fp);
}

static void clear_screen()
{
  if (status)
    {
      del_panel(status_panel);
      del_panel(status_border_panel);
      delwin(status);
      delwin(status_border);
      status = NULL;
    }
  if (output)
    {
      del_panel(output_panel);
      delwin(output);
      output = NULL;
    }
  if (help)
    {
      del_panel(help_border_panel);
      del_panel(help_panel);
      delwin(help_border);
      delwin(help);
      help = NULL;
    }
}

static void setup_status_window()
{
  /* Status window not needed when hidden */
  if (!status_hidden)
    {
      status_border = newwin(LINES/2, COLS, 0, 0);
      status_border_panel = new_panel(status_border);
      status = newwin(LINES/2-2, COLS-2, 1, 1);
      status_panel = new_panel(status);
      box(status_border, 0 ,0);
    }
}

static void setup_output_window()
{
  /* Use the entire screen if not displaying status */
  if (status_hidden)
    output = newwin(LINES,COLS,0,0);
  else
    output = newwin(LINES/2,COLS,LINES/2,0);
  output_panel = new_panel(output);
  scrollok(output, TRUE);
}

static void setup_help_window()
{
  help_border = newwin((3.0/4)*LINES+2, (3.0/4)*COLS+2, LINES/8-1, COLS/8-1);
  help = newwin((3.0/4)*LINES, (3.0/4)*COLS, LINES/8, COLS/8);
  help_border_panel = new_panel(help_border);
  help_panel = new_panel(help);
  hide_panel(help_border_panel);
  hide_panel(help_panel);
  box(help_border, 0 ,0);
  wattron(help, A_BOLD);
  wprintw(help, "MONITOR MODE COMMANDS\n");
  wattroff(help, A_BOLD);
  wprintw(help, "h - Show/hide help page\n");
  wprintw(help, "c - Reset all global variables to their initial states\n");
  wprintw(help, "s - Rotate sort column for probes\n");
  wprintw(help, "t - Open a prompt to enter the index of a probe to toggle\n");
  wprintw(help, "p/r - Pause/resume script by toggling off/on all probes\n");
  wprintw(help, "x - Hide/show the status window\n");
  wprintw(help, "q - Quit script\n");
  wprintw(help, "j,k/DownArrow,UpArrow - Scroll down/up by one entry\n");
  wprintw(help, "PgDown/PgUp - Scroll down/up by one page\n");
  wprintw(help, "Home/End - Scroll to beginning/end\n");
  wprintw(help, "Tab - Toggle scroll window\n");
}

static void handle_resize()
{
  usleep (500*1000); /* prevent too many allocations */
  endwin();
  refresh();
  clear_screen();
  setup_status_window();
  setup_output_window();
  setup_help_window();
  resized = 0;
}

void monitor_winch(__attribute__((unused)) int signum)
{
  resized = 1;
}

void monitor_setup(void)
{
  pthread_mutex_init(&mutex, NULL);
  probe[0] = '\0';
  start_time = time(NULL);
  // If we have a non tty output, no need to render with curses
  disable_curses = !isatty(STDOUT_FILENO);
  if(disable_curses) return;
  initscr();
  curs_set(0);
  cbreak();
  noecho();
  keypad(stdscr, TRUE);
  nodelay(stdscr, TRUE);
  setup_status_window();
  setup_output_window();
  setup_help_window();
}

void monitor_cleanup(void)
{
  pthread_mutex_destroy(&mutex);
  monitor_end = 1;
  if(disable_curses) return;
  endwin();
}

void monitor_render(void)
{
  if(disable_curses) return;
  FILE *monitor_fp;
  char path[PATH_MAX];
  time_t current_time = time(NULL);
  int monitor_x, monitor_y, max_cols, max_rows, cur_y, cur_x;
  int i;
  int discard;

  if (resized)
    handle_resize();

  /* Bound scrolling by window height and entry count */
  getmaxyx(output, max_rows, max_cols);
  output_scroll = MIN(MAX(0, h_queue.count-max_rows+1), output_scroll);

  /* Render previously recorded output */
  wclear(output);
  pthread_mutex_lock(&mutex);
  for (i = 0; i < h_queue.count-output_scroll; i++)
    wprintw(output, "%s", h_queue.lines[(h_queue.oldest+i) % MAX_HISTORY]);
  pthread_mutex_unlock(&mutex);
  update_panels();
  doupdate();

  if (status_hidden)
    return;

  if (!input && rendered && (elapsed_time = current_time - start_time) < monitor_interval)
    return;

  start_time = current_time;
  input = 0;

  getmaxyx(status, monitor_y, monitor_x);
  max_rows = monitor_y;
  max_cols = MIN(MAX_COLS, monitor_x);

  static json_object *jso = NULL; /* survives across refresh calls */
  char json[MAX_DATA];
  size_t bytes = 0;

  /* Render monitor mode statistics */
  if (sprintf_chk(path, "/proc/systemtap/%s/monitor_status", modname))
    return;
  monitor_fp = fopen(path, "r");
  if (monitor_fp)
    {
      bytes = fread(json, sizeof(char), sizeof(json), monitor_fp);
      fclose(monitor_fp);
    }
  if (bytes >= 1)
    {
      /* Free allocated memory */
      if (jso)
        json_object_put(jso);
      jso = json_tokener_parse(json);
    }

  wclear(status);

  if (jso)
    {
      char monitor_str[MAX_COLS];
      char monitor_out[MAX_COLS];
      int printed;
      int col;
      int i;
      int width[num_attributes] = {0};
      json_object *jso_uptime, *jso_uid, *jso_mem,
                  *jso_name, *jso_globals, *jso_probe_list;
      struct json_object_iterator it, it_end;

      rendered = 1;

      json_object_object_get_ex(jso, "uptime", &jso_uptime);
      json_object_object_get_ex(jso, "uid", &jso_uid);
      json_object_object_get_ex(jso, "memory", &jso_mem);
      json_object_object_get_ex(jso, "module_name", &jso_name);
      json_object_object_get_ex(jso, "globals", &jso_globals);
      json_object_object_get_ex(jso, "probe_list", &jso_probe_list);
      num_probes = json_object_array_length(jso_probe_list);

      wmove(status, 0, 0);

      wprintw(status, "uptime: %s uid: %s memory: %s\n",
              json_object_get_string(jso_uptime),
              json_object_get_string(jso_uid),
              json_object_get_string(jso_mem));

      wprintw(status, "module_name: %s probes: %d \n",
              json_object_get_string(jso_name),
              num_probes);

      col = 0;
      col += snprintf(monitor_out, max_cols, "globals: ");
      it = json_object_iter_begin(jso_globals);
      it_end = json_object_iter_end(jso_globals);
      while (!json_object_iter_equal(&it, &it_end))
        {
          printed = snprintf(monitor_str, max_cols, "%s: %s ",
                             json_object_iter_peek_name(&it),
                             json_object_get_string(json_object_iter_peek_value(&it))) + 1;

          /* Prevent line folding for globals */
          if ((max_cols - (col + printed)) >= 0)
            {
              col += snprintf(monitor_out+col, printed, "%s", monitor_str);
              json_object_iter_next(&it);
            }
          else if (printed > max_cols)
            {
              /* Skip globals that do not fit on one line */
              json_object_iter_next(&it);
            }
          else
            {
              wprintw(status, "%s\n", monitor_out);
              col = 0;
            }
        }
      if (col != 0)
        wprintw(status, "%s\n", monitor_out);


      /* Find max width of each field for alignment uses */
      for (i = 0; i < num_probes; i++)
        {
          json_object *probe, *field;
          probe = json_object_array_get_idx(jso_probe_list, i);

          json_object_object_get_ex(probe, "index", &field);
          width[p_index] = MAX(width[p_index], (int) strlen(json_object_get_string(field)));
          json_object_object_get_ex(probe, "state", &field);
          width[p_state] = MAX(width[p_state], (int) strlen(json_object_get_string(field)));
          json_object_object_get_ex(probe, "hits", &field);
          width[p_hits] = MAX(width[p_hits], (int) strlen(json_object_get_string(field)));
          json_object_object_get_ex(probe, "min", &field);
          width[p_min] = MAX(width[p_min], (int) strlen(json_object_get_string(field)));
          json_object_object_get_ex(probe, "avg", &field);
          width[p_avg] = MAX(width[p_avg], (int) strlen(json_object_get_string(field)));
          json_object_object_get_ex(probe, "max", &field);
          width[p_max] = MAX(width[p_max], (int) strlen(json_object_get_string(field)));
          json_object_object_get_ex(probe, "name", &field);
          width[p_name] = MAX(width[p_name], (int) strlen(json_object_get_string(field)));
        }

      json_object_array_sort(jso_probe_list, comp_fn[comp_fn_index]);

      if (active_window == 0)
        wattron(status, A_BOLD);
      wprintw(status, "\n%*s\t%*s\t%*s\t%*s\t%*s\t%*s\t%s\n",
              (int)width[p_index], HIGHLIGHT("index", p_index, comp_fn_index),
              (int)width[p_state], HIGHLIGHT("state", p_state, comp_fn_index),
              (int)width[p_hits], HIGHLIGHT("hits", p_hits, comp_fn_index),
              (int)width[p_min], HIGHLIGHT("min", p_min, comp_fn_index),
              (int)width[p_avg], HIGHLIGHT("avg", p_avg, comp_fn_index),
              (int)width[p_max], HIGHLIGHT("max", p_max, comp_fn_index),
              HIGHLIGHT("name", p_name, comp_fn_index));
      if (active_window == 0)
        wattroff(status, A_BOLD);

      getyx(status, cur_y, discard);
      if (probe_scroll >= num_probes)
        probe_scroll = num_probes-1;
      for (i = probe_scroll; i < MIN(num_probes, probe_scroll+max_rows-cur_y); i++)
        {
          json_object *probe, *field;
          probe = json_object_array_get_idx(jso_probe_list, i);
          json_object_object_get_ex(probe, "index", &field);
          wprintw(status, "%*s\t", (int)width[p_index], json_object_get_string(field));
          json_object_object_get_ex(probe, "state", &field);
          wprintw(status, "%*s\t", (int)width[p_state], json_object_get_string(field));
          json_object_object_get_ex(probe, "hits", &field);
          wprintw(status, "%*s\t", (int)width[p_hits], json_object_get_string(field));
          json_object_object_get_ex(probe, "min", &field);
          wprintw(status, "%*s\t", (int)width[p_min], json_object_get_string(field));
          json_object_object_get_ex(probe, "avg", &field);
          wprintw(status, "%*s\t", (int)width[p_avg], json_object_get_string(field));
          json_object_object_get_ex(probe, "max", &field);
          wprintw(status, "%*s\t", (int)width[p_max], json_object_get_string(field));
          getyx(status, discard, cur_x);
          json_object_object_get_ex(probe, "name", &field);
          wprintw(status, "%.*s", max_cols-cur_x-1, json_object_get_string(field));
          wprintw(status, "\n");
        }
      mvwprintw(status, max_rows-1, 0,
                "press h for help\n");
    }
  else /* ! jso */
    {
      wmove(status, 0, 0);
      wprintw(status, "(data not available)");
    }

  if (monitor_state == insert)
    mvwprintw(status, max_rows-1, 0, "enter probe index: %s\n", probe);
  else if (monitor_state == exited)
    mvwprintw(status, max_rows-1, 0,
              "EXITED: press q again to quit\n");

  update_panels();
  doupdate();
  (void) discard; /* Unused */
}

/* Called in staprun/relay.c */
void monitor_remember_output_line(const char* buf, const size_t bytes)
{
  if(disable_curses && monitor_state != exited){
    // Just output normally
    printf("%.*s", (int)bytes, buf);
    return;
  }
  pthread_mutex_lock(&mutex);
  free (h_queue.lines[h_queue.newest]);
  h_queue.lines[h_queue.newest] = strndup(buf, bytes);
  h_queue.newest = (h_queue.newest+1) % MAX_HISTORY;

  if (h_queue.count < MAX_HISTORY)
    h_queue.count++; /* and h_queue.oldest stays at 0 */
  else
    h_queue.oldest = (h_queue.oldest+1) % MAX_HISTORY;
  pthread_mutex_unlock(&mutex);
}

void monitor_exited(void)
{
  // We don't have interactive curses running, so just cleanup fully.
  // No need for the 2 stage shutdown
  if(disable_curses){
    cleanup_and_exit(0, 0 /* error_detected unavailable here */ );
  }

  monitor_state = exited;
  input = 1;
}

void monitor_input(void)
{
  if(disable_curses) return;
  static int i = 0;
  int ch;
  int max_rows;
  int cur_y;
  int discard;

  getmaxyx(output, max_rows, discard);
  getyx(status, cur_y, discard);

  switch (monitor_state)
    {
      case normal:
      case exited:
        ch = getch();
        switch (ch)
          {
            case '\t':
              active_window ^= 1;
              break;
            case 'j':
            case KEY_DOWN:
              if (active_window == 0)
                probe_scroll++;
              else
                output_scroll--;
              break;
            case 'k':
            case KEY_UP:
              if (active_window == 0)
                probe_scroll--;
              else
                output_scroll++;
              break;
            case KEY_NPAGE:
              if (active_window == 0)
                probe_scroll += max_rows-cur_y;
              else
                output_scroll -= max_rows-1;
              break;
            case KEY_PPAGE:
              if (active_window == 0)
                probe_scroll -= max_rows-cur_y;
              else
                output_scroll += max_rows-1;
              break;
            case KEY_HOME:
              if (active_window == 0)
                probe_scroll = 0;
              else
                output_scroll = h_queue.count-max_rows+1;
              break;
            case KEY_END:
              if (active_window == 0)
                probe_scroll = num_probes-1;
              else
                output_scroll = 0;
              break;
            case 's':
              comp_fn_index++;
              if (comp_fn_index == COMP_FNS)
                comp_fn_index = 0;
              break;
            case 'c':
              write_command("clear");
              break;
            case 'r':
              write_command("resume");
              break;
            case 'p':
              write_command("pause");
              break;
            case 'q':
              if (monitor_state == exited)
	        {
		  cleanup_and_exit(0, 0 /* error_detected unavailable here */ );
		  /* NOTREACHED */
		}
              else
                write_command("quit");
              break;
            case 't':
              monitor_state = insert;
              break;
            case 'h':
              if (panel_hidden(help_panel))
                {
                  show_panel(help_border_panel);
                  show_panel(help_panel);
                }
              else
                {
                  hide_panel(help_border_panel);
                  hide_panel(help_panel);
                }
              break;
            case 'x':
              status_hidden ^= 1;
              handle_resize();
              break;
          }
        probe_scroll = MAX(0, probe_scroll);
        output_scroll = MAX(0, output_scroll);
        if (ch != ERR)
          input = 1;
        break;
      case insert:
        ch = getch();
        if (ch == '\n' || i == MAX_INDEX_LEN)
          {
            write_command(probe);
            monitor_state = normal;
            i = 0;
            probe[0] = '\0';
            rendered = 0;
          }
        else if (ch == KEY_BACKSPACE && i > 0)
          {
            probe[--i] = '\0';
            input = 1;
          }
        else if (ch != ERR && ch != KEY_BACKSPACE)
          {
            probe[i++] = ch;
            probe[i] = '\0';
            input = 1;
          }
        break;
    }
  (void) discard; /* Unused */
}

#else /* ! HAVE_MONITOR_LIBS */

void monitor_winch(int signum) { (void) signum; }
void monitor_setup(void) {}
void monitor_cleanup(void) {}
void monitor_render(void) {}
void monitor_input(void) {}
void monitor_exited(void) {}
void monitor_remember_output_line(const char* buf, const size_t bytes) { (void)buf; (void) bytes; }

#endif

void *redirect_stdin(void *arg)
{
        char path[PATH_MAX];
        int c;
        int fd;

        snprintf(path, PATH_MAX - 25, "/proc/systemtap/%s/__stdin", modname);
        /* destination file may not exist yet */
        while ((fd = open(path, O_WRONLY)) == -1) {
                if (errno != ENOENT) {
                        _perr("Unexpected failure during file open.\n");
                        exit(1);
                }
                usleep(2000);
        }
        while ((c = getchar()) != EOF) {
	        char ch = (char)c;
                if (! write(fd, &ch, sizeof(ch)) && errno != ENOENT) {
                        _perr("Unexpected failure during write.\n");
                        exit(1);
                }
        }
        read_stdin_cleanup();
        close(fd); /* if it pleases m'lud coverity */
        return arg;
}

void read_stdin_setup(void)
{
        pthread_t t;

        if (isatty(STDIN_FILENO)) {
                /* enter non-canonical mode */
                struct termios oldterm, newterm;
                if (tcgetattr(STDIN_FILENO, &oldterm) != 0) {
                        _perr("Failed to reconfigure terminal.\n");
                        exit(1);
                }
                newterm = oldterm;
                newterm.c_lflag &= ~ICANON & ~ECHO;
                if (tcsetattr(STDIN_FILENO, TCSANOW, &newterm) != 0) {
                        _perr("Failed to reconfigure terminal.\n");
                        exit(1);
                }
        }
        if (setvbuf(stdin, NULL, _IONBF, 0) != 0) {
                _perr("Failed to reconfigure input stream.\n");
                exit(1);
        }
        if (pthread_create(&t, NULL, redirect_stdin, NULL) != 0) {
                _perr("Failed to create thread.\n");
                exit(1);
        }
        if (pthread_detach(t) != 0) {
                _perr("Failed to detach thread.\n");
                exit(1);
        }
        return;
}

void read_stdin_cleanup(void)
{
        if (isatty(STDIN_FILENO)) {
            struct termios newterm;

            if (tcgetattr(STDIN_FILENO, &newterm) != 0) {
                    _perr("Failed to reconfigure terminal.\n");
                    return;
            }
            newterm.c_lflag |= ICANON | ECHO;
            if (tcsetattr(STDIN_FILENO, TCSANOW, &newterm) != 0) {
                   _perr("Failed to reconfigure terminal.\n");
            }
        }
        return;
}

/* vim: set sw=2 ts=8 cino=>4,n-2,{2,^-2,t0,(0,u0,w1,M1 : */
