/* -*- mode:C; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/*
 * Copyright (C) 2006 OpenedHand Ltd.
 *
 * Author: Tomas Frydrych <tf@o-hand.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 * Readline interface code for interactive mode
 */

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pwd.h>

#include <readline/readline.h>
#include <readline/history.h>

#include "exmap.h"

int rows = 24;
int columns = 80;

enum
  {
    EMSG_UNKNOWN_COMMAND = 0,
    EMSG_INVALID_PARAMETER,
    EMSG_PID_NOT_SELECTED,
    EMSG_NO_PROCESS_SELECTED,
    EMSG_FOPEN_ERROR,
    EMSG_UNKNOWN_ERROR,

    /* MUST BE LAST, HAVE NO CORRESPONDING STRING */
    EMSG_ERROR_NO_MESSAGE, /* for errors for which msg was already printed */
    EMSG_SUCCESS
  };

static char * emsg [] =
  {
    "  Unknown command '%s'\n",
    "  Invalid parameter '%s'\n",
    "  Process %d is not selected\n",
    "  No process selected\n",
    "  Could not open file [%s]\n",
    "  Unknown error occured\n"
  };

static gchar *
get_hist_file ()
{
  struct passwd *pws = getpwuid(getuid());
  if (pws && pws->pw_name)
    {
      gchar * s = g_strconcat ("/home/", pws->pw_name,
                               "/.exmap-console-history", NULL);

      return s;
    }

  return NULL;
}

void
interactive_init (data_t * d)
{
  rl_initialize();
  rl_get_screen_size (&rows, &columns);

  using_history ();

  gchar * s = get_hist_file ();
  read_history (s);
  g_free (s);

  DETAIL_UNSET (d->args.detail, DETAIL_FILE_INFO);
  DETAIL_UNSET (d->args.detail, DETAIL_ELF_INFO);

  g_print ("\nWelcome to exmap-console " VERSION "\n");
}

static GList *
cmd_to_pid (data_t * d, const char * cmd)
{
  GList * result = NULL;
  GList * l = d->mem_list;
  while (l)
    {
      memory_t * m = l->data;
      if (m->cmd && *m->cmd)
        {
          gchar * c = m->cmd;
          gchar * name = m->cmd;

          while ((c = strchr (c, '/')))
            {
              c++;
              name = c;
            }

          if (!strncmp (name, cmd, strlen (cmd)))
            {
              result = g_list_prepend (result, GINT_TO_POINTER (m->pid));
            }
        }

      l = l->next;
    }

  return result;
}

/* converts a GList of pid_t entries to an array; if the old_pids is not NULL
 * the GList entries get appended to the end of the old arrary
 * count is the number of entries in the array when called / on return
 */
static pid_t *
pid_list_to_array (GList * l, pid_t * old_pids, int * count)
{
  int i;
  guint len = g_list_length (l);

  pid_t * pids;

  if (old_pids)
    pids = g_realloc (old_pids, (*count + len + 1) * sizeof(pid_t));
  else
    {
      pids = g_malloc ((len + 1) * sizeof (pid_t));
      *count = 0;
    }

  for (i = 0; i < len; ++i)
    {
      pid_t pid = GPOINTER_TO_INT (l->data);
      pids[*count + i] = pid;
      l = l->next;
    }

  *count += len;
  pids[*count] = 0;

  g_list_free (l);

  return pids;
}

static guint
interactive_print (data_t * d, const char * p)
{
  if (p && *p)
    {
      GList  * l;
      pid_t    pid;

      if (!strcmp (p, "all"))
        {
          d->args.all_pids = TRUE;
          main_print (d);
          d->args.all_pids = FALSE;
          return EMSG_SUCCESS;
        }
      else
        {
          pid_t  * old_pids  = d->args.pids_arg;
          guint    old_count = d->args.pids_count;

          if ((pid = atoi (p)))
            {
              d->args.pids_arg = g_malloc0(2 * sizeof(pid_t));
              d->args.pids_arg[0] = pid;
              d->args.pids_arg[1] = 0;
              d->args.pids_count = 1;
            }
          else if ((l = cmd_to_pid (d, p)))
            {
              d->args.pids_count = 0;
              d->args.pids_arg = pid_list_to_array (l, NULL,
                                                    &d->args.pids_count);
            }
          else
            {
              return EMSG_INVALID_PARAMETER;
            }

          main_print (d);

          g_free (d->args.pids_arg);
          d->args.pids_arg = old_pids;
          d->args.pids_count = old_count;

          return EMSG_SUCCESS;
        }
    }
  else
    main_print (d);

  return EMSG_SUCCESS;
}

static guint
interactive_add (data_t *d, const char * p)
{
  GList * l;
  pid_t   pid;

  if (!strcmp (p, "all"))
    {
      d->args.all_pids = TRUE;
    }
  else if (*p && (pid = atoi (p)))
    {
      d->args.pids_arg = g_realloc(d->args.pids_arg,
                                   (d->args.pids_count + 2)*sizeof(pid_t));

      d->args.pids_arg[d->args.pids_count] = pid;
      d->args.pids_count++;
      d->args.pids_arg[d->args.pids_count] = 0;
    }
  else if (*p && (l = cmd_to_pid (d, p)))
    {
      d->args.pids_arg = pid_list_to_array (l, d->args.pids_arg,
                                            &d->args.pids_count);
    }
  else
    return EMSG_INVALID_PARAMETER;

  return EMSG_SUCCESS;
}

static guint
interactive_rm (data_t * d, const char * p)
{
  pid_t pid;

  if (!strcmp (p, "all"))
    d->args.all_pids = FALSE;
  else if (*p && (pid = atoi (p)))
    {
      pid_t * pp = d->args.pids_arg;
      gboolean found = FALSE;

      while (*pp)
        {
          if (*pp == pid)
            {
              found = TRUE;
              break;
            }

          pp++;
        }

      if (found)
        {
          while (*pp)
            {
              *pp = *(pp + 1);
              pp++;
            }

          d->args.pids_count--;
        }
      else
        {
          g_print (emsg[EMSG_PID_NOT_SELECTED], pid);
          return EMSG_ERROR_NO_MESSAGE;
        }
    }
  else
    return EMSG_INVALID_PARAMETER;

  return EMSG_SUCCESS;
}

static guint
interactive_ls (data_t *d)
{
  if (d->args.all_pids)
    g_print ("'all'\n");
  else
    {
      pid_t * p = d->args.pids_arg;
      if (p && *p)
        {
          while (*p)
            {
              g_print ("%d ", *p);
              p++;
            }

          g_print ("\n");
        }
      else
        return EMSG_NO_PROCESS_SELECTED;
    }

  return EMSG_SUCCESS;
}

static guint
interactive_save (data_t * d, const char * file)
{
  FILE  * old_of = d->of;
  FILE  * of = fopen (file, "w");

  if (!of)
    {
      g_print (emsg[EMSG_FOPEN_ERROR], file);
      return EMSG_ERROR_NO_MESSAGE;
    }

  /* step out of interactive mode ... */
  d->of = of;
  d->args.interactive = FALSE;

  main_print (d);

  d->of = old_of;
  d->args.interactive = TRUE;

  fclose (of);

  return EMSG_SUCCESS;
}

static guint
interactive_help ()
{
  g_print (
      "Available commands:\n"
      "  add 'all'|pid|command       : select process for examination\n"
      "  clear                       : deselect all selected processes\n"
      "  detail 'none'|'file'|'elf'  : set level of detail to show\n"
      "  ls                          : list selected processes\n"
      "  print ['all'|pid|command]   : print data for selected/specified processes\n"
      "  ps [options]                : run system ps command\n"
      "  reload                      : recreate system snapshot\n"
      "  rm pid|'all'                : deselect process with pid, or clear 'all' flag\n"
      "  save file                   : save data for selected process to file\n"
      "  shell commands              : run shell commands\n"
      "  sort asc|desc|vm|sole|heap|stack|mapped|emapped\n"
      "                              : sort results by\n"
      "  threshold                   : set minimum VM for processes printed\n"
      "  ?, help                     : help\n"
      "  quit                        : quit\n");

  return EMSG_SUCCESS;
}

static guint
interactive_spawn (data_t *d, const char * cmd, const char * params)
{
  GError * error = NULL;
  gchar  * out = NULL;
  gchar  * err = NULL;
  gint     status;
  guint    retval = EMSG_SUCCESS;
  gchar  * cmdline = NULL;

  cmdline = g_strconcat (cmd, " ", params, NULL);

  d->global_lines = 0;

  if (g_spawn_command_line_sync (cmdline, &out, &err, &status, &error))
    {
      if (out)
        {
          print_paged (d, out);
          g_free (out);
        }

      if (WEXITSTATUS (status))
        {
          if (err)
            {
              print_paged (d, err);
              g_free (err);
            }

          retval = EMSG_UNKNOWN_ERROR;
        }
    }
  else
    {
      print_paged (d, error->message);
      g_error_free (error);
      retval = EMSG_UNKNOWN_ERROR;
    }

  g_free (cmdline);

  return EMSG_SUCCESS;
}

static gboolean
interactive_parse_line (data_t *d, char * line)
{
  char       c;
  char     * p;
  gboolean   history = TRUE;
  guint      retval = EMSG_SUCCESS;
  gchar    * cmd;
  size_t     cmd_len;

  if (!line)
    return FALSE;

  cmd = g_strdup (line);

  while (isspace (*cmd))
    cmd++;

  c = *cmd;
  p = cmd;

  while (*p && !isspace (*p))
    p++;

  while (*p && isspace (*p))
    {
      *p = 0;
      p++;
    }

  cmd_len = strlen (cmd);

  switch (c)
    {
    case 'a':
      if (!strncmp (cmd, "add", cmd_len))
        retval = interactive_add (d, p);
      else
        retval = EMSG_UNKNOWN_COMMAND;
      break;

    case 'c':
      if (!strncmp (cmd, "clear", cmd_len))
        {
          d->args.all_pids = FALSE;
          g_free (d->args.pids_arg);
          d->args.pids_arg = NULL;
          d->args.pids_count = 0;
        }
      else
        retval = EMSG_UNKNOWN_COMMAND;
      break;

    case 'd':
      if (!strncmp (cmd, "detail", cmd_len))
        {
          if (!strcmp (p, "none"))
            d->args.detail = 0;
          else if (!strcmp (p, "file"))
            DETAIL_SET (d->args.detail, DETAIL_FILE_INFO);
          else if (!strcmp (p, "elf"))
            DETAIL_SET (d->args.detail, DETAIL_ELF_INFO);
          else
            {
              g_print (emsg[EMSG_INVALID_PARAMETER], p);
              retval = EMSG_ERROR_NO_MESSAGE;
            }
        }
      else
        retval = EMSG_UNKNOWN_COMMAND;
      break;

    case 'h':
      if (!strncmp (cmd, "help", cmd_len))
        retval = interactive_help ();
      else
        retval = interactive_help ();
      break;

    case '?':
      retval = interactive_help ();
      break;

    case 'l':
      if (!strncmp (cmd, "ls", cmd_len))
        retval = interactive_ls (d);
      else
        retval = EMSG_UNKNOWN_COMMAND;
      break;

    case 'p':
      if (!strcmp (cmd, "ps"))
        retval = interactive_spawn (d, "/bin/ps", p);
      else if (!strncmp (cmd, "print", cmd_len))
        retval = interactive_print (d, p);
      else
        retval = EMSG_UNKNOWN_COMMAND;
      break;

    case 'q':
      if (!strncmp (cmd, "quit", cmd_len))
        return TRUE;
      else
        retval = EMSG_UNKNOWN_COMMAND;

    case 'r':
      if (!strcmp (cmd, "rm"))
        retval = interactive_rm (d, p);
      else if (!strncmp (cmd, "reload", cmd_len))
        {
          main_reset (d);
          main_load (d);
        }
      else
        retval = EMSG_UNKNOWN_COMMAND;
      break;

    case 's':
      if (!strncmp (cmd, "sort", cmd_len))
        {
          if (!strcmp (p, "asc"))
            SORT_TYPE_SET (d->args.sort, SORT_TYPE_ASC);
          else if (!strcmp (p, "desc"))
            SORT_TYPE_SET (d->args.sort, SORT_TYPE_DESC);
          else if (!strcmp (p, "vm"))
            SORT_BY_SET (d->args.sort, SORT_BY_VM);
          else if (!strcmp (p, "sole"))
            SORT_BY_SET (d->args.sort, SORT_BY_SOLE);
          else if (!strcmp (p, "heap"))
            SORT_BY_SET (d->args.sort, SORT_BY_HEAP);
          else if (!strcmp (p, "stack"))
            SORT_BY_SET (d->args.sort, SORT_BY_STACK);
          else if (!strcmp (p, "mapped"))
            SORT_BY_SET (d->args.sort, SORT_BY_MAPPED);
          else if (!strcmp (p, "emapped"))
            SORT_BY_SET (d->args.sort, SORT_BY_EMAPPED);

          main_sort (d);
        }
      else if (!strncmp (cmd, "shell", cmd_len))
        {
          gchar * s = g_strconcat ("\"", p, "\"", NULL);
          retval = interactive_spawn (d, "/bin/sh -r -c", s);
          g_free (s);
        }
      else if (!strncmp (cmd, "save", cmd_len))
        retval = interactive_save (d, p);
      else
        retval = EMSG_UNKNOWN_COMMAND;
      break;

    case 't':
      if (!strncmp (cmd, "threshold", cmd_len))
        {
          if (*p)
            d->args.threshold = atoi (p);
          else
            retval = EMSG_INVALID_PARAMETER;
        }
    }

  if (retval != EMSG_SUCCESS)
    {
      switch (retval)
        {
        case EMSG_INVALID_PARAMETER:
          g_print (emsg[retval], p);
          break;

        case EMSG_NO_PROCESS_SELECTED:
          g_print (emsg[retval]);
          break;

        case EMSG_UNKNOWN_COMMAND:
          g_print (emsg[retval], cmd);
          break;

        case EMSG_UNKNOWN_ERROR:
        case EMSG_ERROR_NO_MESSAGE:
          break;

        default:
          g_warning ("Unhandled error value %d", retval);

        }

      return FALSE;
    }

  if (history)
    {
      /* if the line already exists in the history, we remove it
       * first, so that we have no duplicates and the most recent
       * one is always on the top
       */
      if (-1 < history_search_prefix (line, -1))
        {
          int indx = where_history ();
          HIST_ENTRY * h = remove_history (indx);
          if (h)
            {
#if RL_VERSION_MAJOR < 5
              free (h->line);
              free (h);
#else
              free_history_entry (h);
#endif
            }
        }

      add_history (line);
    }

  return FALSE;
}

void
interactive_loop (data_t * d)
{
  char * line;

  while ((line = readline (": ")))
    {
      if (interactive_parse_line (d, line))
        break;

      free (line);
    }

  gchar * s = get_hist_file ();
  write_history (s);
  g_free (s);
}
