#include "gskcontrolclient.h"
#include "../http/gskhttpclient.h"
#include "../gskstreamclient.h"
#include "../gskmemory.h"
#include "../gskstdio.h"
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

struct _GskControlClient
{
  GskSocketAddress *address;

  char *prompt;
  guint cmd_no;

  guint add_newlines_as_needed : 1;

  GskControlClientErrorFunc error_func;
  gpointer error_func_data;

  int last_argc;
  char **last_argv;
};

static void
abort_on_error (GError *error, gpointer data)
{
  GskControlClient *cc = data;
  g_error ("control-client error: %s: %s",
           gsk_socket_address_to_string (cc->address),
           error->message);
}

/**
 * gsk_control_client_new:
 * @server: the location of the server to contact.
 *
 * Allocates a new control client.
 *
 * This may invoke the main-loop to answer a few questions.
 *
 * returns: the newly allocated control client.
 */
GskControlClient *
gsk_control_client_new (GskSocketAddress *server)
{
  GskControlClient *cc = g_new (GskControlClient, 1);
  cc->address = server ? g_object_ref (server) : NULL;
  cc->prompt = NULL;
  cc->cmd_no = 1;
  cc->add_newlines_as_needed = 1;
  cc->error_func = abort_on_error;
  cc->error_func_data = cc;
  return cc;
}

/**
 * gsk_control_client_set_flag:
 * @cc:
 * @flag:
 * @value:
 *
 * not used yet.
 */
void
gsk_control_client_set_flag (GskControlClient *cc,
                             GskControlClientFlag flag,
                             gboolean             value)
{
  switch (flag)
    {
    case GSK_CONTROL_CLIENT_ADD_NEWLINES_AS_NEEDED:
      cc->add_newlines_as_needed = value ? 1 : 0;
      break;
    default:
      g_return_if_reached ();
    }
}

/**
 * gsk_control_client_get_flag:
 * @cc:
 * @flag:
 * returns:
 *
 * not used yet.
 */
gboolean
gsk_control_client_get_flag (GskControlClient *cc,
                             GskControlClientFlag flag)
{
  switch (flag)
    {
    case GSK_CONTROL_CLIENT_ADD_NEWLINES_AS_NEEDED:
      return cc->add_newlines_as_needed;
    default:
      g_return_val_if_reached (FALSE);
    }
}

/**
 * gsk_control_client_set_prompt:
 * @cc: the control client to affect.
 * @prompt_format: format string.
 *
 * Set the prompt format string.
 */
void
gsk_control_client_set_prompt(GskControlClient *cc,
                              const char       *prompt_format)
{
  char *to_free = cc->prompt;
  cc->prompt = g_strdup (prompt_format);
  g_free (to_free);
}

typedef struct _GetServerFileStatus GetServerFileStatus;
struct _GetServerFileStatus
{
  gboolean done;
  GError *error;
  gpointer *contents_out;
  gsize *length_out;
};

static void
buffer_callback_get_server_file (GskBuffer *buffer, gpointer data)
{
  GetServerFileStatus *fs = data;
  *(fs->length_out) = buffer->size;
  *(fs->contents_out) = g_malloc (buffer->size);
  gsk_buffer_read (buffer, *(fs->contents_out), buffer->size);
  fs->done = TRUE;
}

static void
handle_get_server_file_response (GskHttpRequest  *request,
                                 GskHttpResponse *response,
                                 GskStream       *input,
                                 gpointer         hook_data)
{
  GetServerFileStatus *fs = hook_data;
  GskStream *stream;
  if (response->status_code != GSK_HTTP_STATUS_OK)
    {
      fs->error = g_error_new (GSK_G_ERROR_DOMAIN,
                               GSK_ERROR_HTTP_NOT_FOUND,
                               "got status code %u from server",
                               response->status_code);
      fs->done = TRUE;
      return;
    }
  stream = gsk_memory_buffer_sink_new (buffer_callback_get_server_file, fs, NULL);
  gsk_stream_attach (input, stream, NULL);
  g_object_unref (stream);
}

static gboolean
get_server_file (GskControlClient *cc,
                 const char       *path,
                 gpointer         *contents_out,
                 gsize            *length_out,
                 GError          **error)
{
  GskHttpRequest *request;
  GetServerFileStatus status = { FALSE, NULL, contents_out, length_out };
  GskHttpClient *http_client;
  GskStream *client_stream;
  char *p = g_strdup_printf ("/run.txt?command=cat&arg1=%s", path);
  http_client = gsk_http_client_new ();
  request = gsk_http_request_new (GSK_HTTP_VERB_GET, p);
  g_free (p);
  gsk_http_client_request (http_client, request, NULL,
                           handle_get_server_file_response, &status,
                           NULL);

  client_stream = gsk_stream_new_connecting (cc->address, error);
  if (client_stream == NULL)
    return FALSE;
  if (!gsk_stream_attach_pair (GSK_STREAM (http_client), client_stream, error))
    return FALSE;
  g_object_unref (request);
  g_object_unref (client_stream);
  g_object_unref (http_client);

  while (!status.done)
    gsk_main_loop_run (gsk_main_loop_default (), -1, NULL);
  if (status.error)
    {
      *error = status.error;
      return FALSE;
    }
  return TRUE;
}

/**
 * gsk_control_client_get_prompt:
 * @cc: the control client whose prompt should be obtained.
 * returns: the new prompt string.
 *
 * Get a newly allocated prompt string.
 * This is used by the user-interface program;
 * typically readline() is employed to use this prompt.
 */
char *
gsk_control_client_get_prompt_string(GskControlClient *cc)
{
  GString *rv;
  guint i;
  if (cc->prompt == NULL)
    {
      /* request prompt string from server. */
      gpointer content; gsize len;
      GError *error = NULL;
      if (get_server_file (cc, "/server/client-prompt", &content, &len, &error))
        {
          cc->prompt = g_strndup (content, len);
          g_free (content);
        }
      else
        {
          g_message ("error: %s", error->message);
          g_error_free (error);
          cc->prompt = g_strdup ("");
        }
    }
  rv = g_string_new ("");
  for (i = 0; cc->prompt[i] != 0; i++)
    {
      if (cc->prompt[i] == '%')
        {
          switch (cc->prompt[++i])
            {
            case 'n':
              g_string_append_printf (rv, "%u", cc->cmd_no);
              break;
            case '%':
              g_string_append_c (rv, '%');
              break;
            default:
              /* discard */
              g_warning ("bad prompt string '%s'", cc->prompt);
              break;
            }
        }
      else
        g_string_append_c (rv, cc->prompt[i]);
    }
  return g_string_free (rv, FALSE);
}

static void
set_server_address (GskControlClient *client,
                    const char       *path)
{
  if (client->address)
    {
      g_warning ("already had address");
      return;
    }
  client->address = gsk_socket_address_local_new (path);
}

static void
run_script (GskControlClient *cc,
            const char       *filename)
{
  FILE *fp = fopen (filename, "r");
  guint old_cmd_no;
  char *line;
  if (fp == NULL)
    {
      GError *error = g_error_new (GSK_G_ERROR_DOMAIN,
                                   gsk_error_code_from_errno (errno),
                                   "error opening file: %s",
                                   g_strerror (errno));
      if (cc->error_func)
        cc->error_func (error, cc->error_func_data);
      g_error_free (error);
      return;
    }
  old_cmd_no = cc->cmd_no;
  while ((line=gsk_stdio_readline (fp)) != NULL)
    {
      g_strstrip (line);
      if (line[0] == 0 || line[0] == '#')
        {
          g_free (line);
          continue;
        }
      gsk_control_client_run_command_line (cc, line);
      g_free (line);
    }
  cc->cmd_no = old_cmd_no;
  fclose (fp);
}

#define TEST_OPTION_FLAG(flags, opt) \
  ((flags&GSK_CONTROL_CLIENT_OPTIONS_##opt) == GSK_CONTROL_CLIENT_OPTIONS_##opt)
/**
 * gsk_control_client_parse_command_line_args:
 * @cc: the control client to affect.
 * @argc_inout: a reference to the 'argc' which was passed into main.
 * @argv_inout: a reference to the 'argv' which was passed into main.
 * @flags: bitwise-OR'd flags telling which command-line arguments to parse.
 * returns: whether to parse commands from stdin.
 *
 * Parse standard command-line options.
 *
 * During this parsing, some remote commands may be run.
 * For example -e flags cause instructions to be executed.
 * Therefore, this may reinvoke the mainloop.
 */
gboolean
gsk_control_client_parse_command_line_args (GskControlClient *cc,
                                            int              *argc_inout,
                                            char           ***argv_inout,
                                            GskControlClientOptionFlags flags)
{
  int i;
  gboolean user_specified_interactive = FALSE;
  gboolean ran_commands = FALSE;
#define SWALLOW_ARG(count)              \
    { memmove ((*argv_inout) + i,       \
               (*argv_inout) + (i + count), \
               sizeof(char*) * (*argc_inout + 1 - (i + count))); \
      *argc_inout -= count; }

  for (i = 1; i < *argc_inout; )
    {
      const char *arg = (*argv_inout)[i];
      if (TEST_OPTION_FLAG (flags, RANDOM))
        {
          if (strcmp (arg, "--exact-newlines") == 0)
            {
              SWALLOW_ARG(1);
              cc->add_newlines_as_needed = 0;
              continue;
            }
        }
      if (TEST_OPTION_FLAG (flags, INTERACTIVE))
        {
          if (strcmp (arg, "-i") == 0
           || strcmp (arg, "--interactive") == 0)
            {
              user_specified_interactive = TRUE;
              continue;
            }
        }
      if (TEST_OPTION_FLAG (flags, INLINE_COMMAND))
        {
          if (strcmp (arg, "-e") == 0)
            {
              const char *cmd = (*argv_inout)[i+1];
              SWALLOW_ARG (2);
              gsk_control_client_run_command_line (cc, cmd);
              ran_commands = TRUE;
              continue;
            }
        }
      if (TEST_OPTION_FLAG (flags, SOCKET))
        {
          if (strcmp (arg, "--socket") == 0)
            {
              const char *path = (*argv_inout)[i+1];
              SWALLOW_ARG (2);
              set_server_address (cc, path);
              continue;
            }
          if (g_str_has_prefix (arg, "--socket="))
            {
              const char *path = strchr (arg, '=') + 1;
              SWALLOW_ARG (1);
              set_server_address (cc, path);
              continue;
            }
        }
      if (TEST_OPTION_FLAG (flags, SCRIPTS))
        {
          if (strcmp (arg, "-f") == 0)
            {
              const char *file = (*argv_inout)[i+1];
              SWALLOW_ARG (2);
              run_script (cc, file);
              ran_commands = TRUE;
              continue;
            }
        }

      i++;
    }

#undef SWALLOW_ARG
  return !ran_commands || user_specified_interactive;
}

/**
 * gsk_control_client_print_command_line_usage:
 * @flags: bitwise-OR'd flags telling which command-line arguments to parse.
 *
 * Prints the command-line usage that the control-client class defines.
 */
void
gsk_control_client_print_command_line_usage(GskControlClientOptionFlags flags)
{
  if (TEST_OPTION_FLAG (flags, RANDOM))
    {
      g_print ("  --exact-newlines      Don't add newlines to output.\n");
    }
  if (TEST_OPTION_FLAG (flags, INTERACTIVE))
    {
      g_print ("  -i, --interactive     Force interaction.\n");
    }
  if (TEST_OPTION_FLAG (flags, INLINE_COMMAND))
    {
      g_print ("  -e 'CMD'              Run the command CMD.\n");
    }
  if (TEST_OPTION_FLAG (flags, SCRIPTS))
    {
      g_print ("  -f 'SCRIPT'           Run commands from the file SCRIPT.\n");
    }
  if (TEST_OPTION_FLAG (flags, SOCKET))
    {
      g_print ("  --socket=SOCKET       Connect to the server on the given\n"
               "                        unix-domain socket.\n");
    }
}
#undef TEST_OPTION_FLAG

gboolean gsk_control_client_has_address (GskControlClient *client)
{
  return client->address != NULL;
}

void
gsk_control_client_run_command_line (GskControlClient *cc,
                                     const char       *line)
{
  int argc;
  char **argv;
  GError *error = NULL;
  char *infile = NULL, *outfile = NULL;
  if (!g_shell_parse_argv (line, &argc, &argv, &error))
    {
      g_warning ("error parsing command-line: %s", error->message);
      g_error_free (error);
      return;
    }
  while (argc >= 3)
    {
      if (strcmp (argv[argc - 2], "<") == 0)
        {
          g_free (argv[argc - 2]);
          g_free (infile);
          argv[argc - 2] = NULL;
          infile = argv[argc - 1];
          argc -= 2;
        }
      else if (strcmp (argv[argc - 2], ">") == 0)
        {
          g_free (argv[argc - 2]);
          g_free (outfile);
          argv[argc - 2] = NULL;
          outfile = argv[argc - 1];
          argc -= 2;
        }
      else
        break;
    }
  gsk_control_client_run_command (cc, argv, infile, outfile);
  g_free (infile);
  g_free (outfile);
  g_strfreev (argv);
  return;
}

typedef struct _RequestInfo RequestInfo;
struct _RequestInfo
{
  GskStream *output;
  gboolean output_finalized;
};

static void
handle_output_finalized (RequestInfo *ri)
{
  ri->output_finalized = TRUE;
}

static void
handle_response (GskHttpRequest  *request,
                 GskHttpResponse *response,
                 GskStream       *input,
                 gpointer         hook_data)
{
  RequestInfo *ri = hook_data;
  GskStream *out = GSK_STREAM (ri->output);
#if 0
  if (response->status_code != GSK_HTTP_STATUS_OK)
    g_warning ("ERROR response from server");
#endif

  /* TODO: this doesn't add a newline if needed... */
  gsk_stream_attach (input, out, NULL);

  g_object_weak_ref (G_OBJECT (out), (GWeakNotify) handle_output_finalized, ri);
}

static void
request_info_unref_output_stream (gpointer ri)
{
  RequestInfo *request_info = ri;
  g_object_unref (request_info->output);
}

static void
append_url_quoted (GString *str, const char *t)
{
  while (*t)
    {
      const char *end = t;
      while (('a' <= *end && *end <= 'z')
          || ('A' <= *end && *end <= 'Z')
          || ('0' <= *end && *end <= '9')
          || *end == '-' || *end == '_' || *end == '/')
        end++;
      if (end > t)
        g_string_append_len (str, t, end - t);
      t = end;
      if (*t == 0)
        break;
      g_string_append_printf (str, "%%%02X", (guint8)*t);
      t++;
    }
}


void
gsk_control_client_run_command (GskControlClient *client,
                                char **command_and_args,
                                const char *in_filename,
                                const char *out_filename)
{
  GskStream *client_stream;
  GError *error = NULL;
  GskStream *in_stream, *out_stream;
  GskHttpClient *http_client;
  GString *path;
  guint i;
  GskHttpRequest *request;
  RequestInfo request_info;

  client_stream = gsk_stream_new_connecting (client->address, &error);
  if (client_stream == NULL)
    {
      if (client->error_func)
        (*client->error_func) (error, client->error_func_data);
      g_error_free (error);
      return;
    }
  http_client = gsk_http_client_new ();
  if (!gsk_stream_attach_pair (GSK_STREAM (http_client), client_stream, &error))
    {
      if (client->error_func)
        (*client->error_func) (error, client->error_func_data);
      g_error_free (error);
      return;
    }

  path = g_string_new ("/run.txt?");
  g_string_append (path, "command=");
  append_url_quoted (path, command_and_args[0]);

  for (i = 1; command_and_args[i]; i++)
    {
      char buf[256];
      g_string_append_c (path, '&');
      g_snprintf (buf, sizeof (buf), "arg%u", i);
      g_string_append (path, buf);
      g_string_append_c (path, '=');
      append_url_quoted (path, command_and_args[i]);
    }
  client->cmd_no++;

  request = gsk_http_request_new (in_filename ? GSK_HTTP_VERB_POST : GSK_HTTP_VERB_GET, path->str);
  if (in_filename)
    GSK_HTTP_HEADER (request)->transfer_encoding_type = GSK_HTTP_TRANSFER_ENCODING_CHUNKED;
  else
    GSK_HTTP_HEADER (request)->connection_type = GSK_HTTP_CONNECTION_CLOSE; /* HACK */
  g_string_free (path, TRUE);
  if (in_filename)
    {
      in_stream = gsk_stream_fd_new_read_file (in_filename, &error);
      if (in_stream == NULL)
        {
          if (client->error_func)
            (*client->error_func) (error, client->error_func_data);
          g_error_free (error);
          return;
        }
    }
  else
    in_stream = NULL;
  if (out_filename)
    {
      out_stream = gsk_stream_fd_new_write_file (out_filename, TRUE, TRUE, &error);
      if (out_stream == NULL)
        {
          if (client->error_func)
            (*client->error_func) (error, client->error_func_data);
          g_error_free (error);
          return;
        }
    }
  else
    {
      int fd = dup (STDOUT_FILENO);
      if (fd < 0)
        {
          g_error ("error running dup(1)");
        }
      out_stream = gsk_stream_fd_new_auto (fd);
    }
  request_info.output = out_stream;
  request_info.output_finalized = FALSE;
  gsk_http_client_request (http_client, request, in_stream,
                           handle_response, &request_info,
                           request_info_unref_output_stream);
  gsk_http_client_shutdown_when_done (http_client);
  g_object_unref (http_client);
  g_object_unref (client_stream);
  if (in_stream)
    g_object_unref (in_stream);
  g_object_unref (request);

  while (!request_info.output_finalized)
    {
      gsk_main_loop_run (gsk_main_loop_default (), -1, NULL);
    }
}

void
gsk_control_client_increment_command_number (GskControlClient *cc)
{
  ++(cc->cmd_no);
}

void
gsk_control_client_set_command_number (GskControlClient *cc, guint no)
{
  cc->cmd_no = no;
}


syntax highlighted by Code2HTML, v. 0.9.1