/*
     This file is part of GNUnet.
     Copyright (C) 2010, 2011 GNUnet e.V.

     GNUnet 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 3, or (at your
     option) any later version.

     GNUnet 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 GNUnet; see the file COPYING.  If not, write to the
     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     Boston, MA 02110-1301, USA.
*/

/**
 * @file src/lib/eventloop.c
 * @brief code for merging GNUnet scheduler and Gtk Main Loop event loops
 * @author Christian Grothoff
 */
#include "gnunet_gtk.h"
#if HAVE_GTK_GTKX_H
#include <gtk/gtkx.h>
#endif

/**
 * Initial size of our poll array cache.
 *
 * TODO: get some statistics, find the maximum number of fds ever
 * polled during normal gnunet-gtk operation, and set this to that number.
 * For non-Windows OSes, that is. For Windows it's always 64, because
 * that's the limit anyway.
 */
#define INITIAL_POLL_ARRAY_SIZE 30

/**
 * Main context for our event loop.
 */
struct GNUNET_GTK_MainLoop
{

  /**
   * Our configuration (includes defaults from gnunet-gtk/config.d/ and gnunet/config.d/)
   */
  struct GNUNET_CONFIGURATION_Handle *cfg;

  /**
   * Name of the glade file for the main window
   */
  const char *main_window_file;

  /**
   * Initial task to run to setup the system.
   */
  GNUNET_SCHEDULER_TaskCallback main_task;

  /**
   * Builder for the main window.
   */
  GtkBuilder *builder;

  /**
   * Gib's Main loop.
   */
  GMainLoop *gml;

  /**
   * GTK's main context.
   */
  GMainContext *gmc;

  /**
   * Read set.
   */
  struct GNUNET_NETWORK_FDSet *rs;

  /**
   * Write set.
   */
  struct GNUNET_NETWORK_FDSet *ws;

  /**
   * Recycled array of polling descriptors.
   */
  GPollFD *cached_poll_array;

  /**
   * Name of the configuration file.
   */
  char *cfgfile;

  /**
   * Size of the 'cached_poll_array'.
   */
  guint cached_poll_array_size;

  /**
   * Task we keep around just to keep the event loop running.
   */
  struct GNUNET_SCHEDULER_Task *dummy_task;

  /**
   * Remaining command-line arguments.
   */
  char *const *argv;

  /**
   * Number of remaining arguments.
   */
  int argc;

};


/**
 * Get the configuration.
 *
 * @param ml handle to the main loop
 * @return handle to the configuration, never NULL
 */
const struct GNUNET_CONFIGURATION_Handle *
GNUNET_GTK_main_loop_get_configuration (struct GNUNET_GTK_MainLoop *ml)
{
  return ml->cfg;
}


/**
 * Trigger shutdown of the GUI and exit the main loop.
 *
 * @param ml handle to the main loop
 */
void
GNUNET_GTK_main_loop_quit (struct GNUNET_GTK_MainLoop *ml)
{
  g_main_loop_quit (ml->gml);
  ml->gml = NULL;
  if (NULL != ml->dummy_task)
  {
    GNUNET_SCHEDULER_cancel (ml->dummy_task);
    ml->dummy_task = NULL;
  }
}


/**
 * Get the builder from the main window.
 *
 * @param ml handle to the main loop
 * @return NULL on error, otherwise the builder
 */
GtkBuilder *
GNUNET_GTK_main_loop_get_builder (struct GNUNET_GTK_MainLoop *ml)
{
  return ml->builder;
}


int
GNUNET_GTK_main_loop_build_window (struct GNUNET_GTK_MainLoop *ml,
                                   gpointer data)
{
  ml->builder = GNUNET_GTK_get_new_builder (ml->main_window_file, data);
  if (ml->builder == NULL)
  {
    GNUNET_GTK_main_loop_quit (ml);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


/**
 * Obtain the name of the configuration file that is being used.
 *
 * @param ml handle to the main loop
 * @return name of configuration file
 */
const char *
GNUNET_GTK_main_loop_get_configuration_file (struct GNUNET_GTK_MainLoop *ml)
{
  return ml->cfgfile;
}


/**
 * Get an object from the main window.
 *
 * @param ml handle to the main loop
 * @param name name of the object
 * @return NULL on error, otherwise the object
 */
GObject *
GNUNET_GTK_main_loop_get_object (struct GNUNET_GTK_MainLoop *ml,
                                 const char *name)
{
  return gtk_builder_get_object (ml->builder, name);
}


/**
 * Get remaining command line arguments.
 *
 * @param ml handle to the main loop
 * @param argc set to argument count
 * @param argv set to argument vector
 */
void
GNUNET_GTK_main_loop_get_args (struct GNUNET_GTK_MainLoop *ml,
                               int *argc,
                               char *const **argv)
{
  *argc = ml->argc;
  *argv = ml->argv;
}


/**
 * Task to run Gtk events (within a GNUnet scheduler task).
 *
 * @param cls the main loop handle
 */
static void
dispatch_gtk_task (void *cls)
{
  struct GNUNET_GTK_MainLoop *ml = cls;

  g_main_context_dispatch (ml->gmc);
}


/**
 * Change the size of the cached poll array to the given value.
 *
 * @param ml main loop context with the cached poll array
 * @param new_size desired size of the cached poll array
 */
static void
resize_cached_poll_array (struct GNUNET_GTK_MainLoop *ml, guint new_size)
{
  if (NULL == ml->cached_poll_array)
    ml->cached_poll_array = g_new (GPollFD, new_size);
  else
    ml->cached_poll_array = g_renew (GPollFD, ml->cached_poll_array, new_size);
  ml->cached_poll_array_size = new_size;
}


/**
 * Dummy task to keep our scheduler running.
 */
static void
keepalive_task (void *cls)
{
  struct GNUNET_GTK_MainLoop *ml = cls;

  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Dummy task was scheduled\n");
  ml->dummy_task = GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_UNIT_FOREVER_REL,
                                                 &keepalive_task,
                                                 ml);
}


#ifndef FD_COPY
#define FD_COPY(s, d) (memcpy ((d), (s), sizeof (fd_set)))
#endif

/**
 * Replacement for the GNUnet scheduler's "select" that integrates the
 * Gtk event loop.  We merge Gtk's events with those from GNUnet's
 * scheduler and then use 'g_poll' on both.  Then we process the Gtk
 * events (by adding a task to do so to the GNUnet scheduler), and, if
 * applicable, return the GNUnet-scheduler events back to GNUnet.
 *
 * @param cls the 'struct GNUNET_GTK_MainLoop'
 * @param rfds set of sockets to be checked for readability
 * @param wfds set of sockets to be checked for writability
 * @param efds set of sockets to be checked for exceptions
 * @param timeout relative value when to return
 * @return number of selected sockets, GNUNET_SYSERR on error
 */
static int
gnunet_gtk_select (void *cls,
                   struct GNUNET_NETWORK_FDSet *rfds,
                   struct GNUNET_NETWORK_FDSet *wfds,
                   struct GNUNET_NETWORK_FDSet *efds,
                   const struct GNUNET_TIME_Relative timeout)
{
  struct GNUNET_GTK_MainLoop *ml = cls;
  int max_nfds;
  gint poll_result;
  gint delay = INT_MAX;
  int i;
  guint ui;
  guint fd_counter;
  guint need_gfds = 0;
  fd_set aread;
  fd_set awrite;
  fd_set aexcept;
  int result = 0;
  gint max_priority;

  if (ml->gml == NULL || TRUE != g_main_loop_is_running (ml->gml))
    return GNUNET_NETWORK_socket_select (rfds, wfds, efds, timeout);
  if (NULL != rfds)
    FD_COPY (&rfds->sds, &aread);
  else
    FD_ZERO (&aread);
  if (NULL != wfds)
    FD_COPY (&wfds->sds, &awrite);
  else
    FD_ZERO (&awrite);
  if (NULL != efds)
    FD_COPY (&efds->sds, &aexcept);
  else
    FD_ZERO (&aexcept);

  max_nfds = -1;
  if (rfds != NULL)
    max_nfds = GNUNET_MAX (max_nfds, rfds->nsds);
  if (wfds != NULL)
    max_nfds = GNUNET_MAX (max_nfds, wfds->nsds);
  if (efds != NULL)
    max_nfds = GNUNET_MAX (max_nfds, efds->nsds);

  if (ml->cached_poll_array_size == 0)
    resize_cached_poll_array (ml, INITIAL_POLL_ARRAY_SIZE);

  fd_counter = 0;
  for (i = 0; i < max_nfds; i++)
  {
    int isset[3];

    isset[0] = (rfds == NULL) ? 0 : FD_ISSET (i, &rfds->sds);
    isset[1] = (wfds == NULL) ? 0 : FD_ISSET (i, &wfds->sds);
    isset[2] = (efds == NULL) ? 0 : FD_ISSET (i, &efds->sds);
    if ((! isset[0]) && (! isset[1]) && (! isset[2]))
      continue;
    if (fd_counter >= ml->cached_poll_array_size)
      resize_cached_poll_array (ml, ml->cached_poll_array_size * 2);
    ml->cached_poll_array[fd_counter].fd = i;
    ml->cached_poll_array[fd_counter].events =
      (isset[0] ? G_IO_IN | G_IO_HUP | G_IO_ERR : 0) |
      (isset[1] ? G_IO_OUT | G_IO_ERR : 0) | (isset[2] ? G_IO_ERR : 0);
    fd_counter++;
  }

  /* combine with Gtk events */
  if (NULL != ml->gmc)
  {
    g_main_context_prepare (ml->gmc, &max_priority);
    while (1)
    {
      need_gfds =
        g_main_context_query (ml->gmc,
                              max_priority,
                              &delay,
                              &ml->cached_poll_array[fd_counter],
                              ml->cached_poll_array_size - fd_counter);
      if (ml->cached_poll_array_size >= need_gfds + fd_counter)
        break;
      resize_cached_poll_array (ml, fd_counter + need_gfds);
    }
  }
  if (timeout.rel_value_us != GNUNET_TIME_UNIT_FOREVER_REL.rel_value_us)
  {
    if (delay >= 0)
      delay = GNUNET_MIN (timeout.rel_value_us /
                            GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us,
                          delay);
    else
      delay = timeout.rel_value_us / GNUNET_TIME_UNIT_MILLISECONDS.rel_value_us;
  }

  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
              "We have %d of our FDs and %d of GMC ones, going to wait %6dms\n",
              fd_counter,
              need_gfds,
              delay);
  poll_result = g_poll (ml->cached_poll_array, fd_counter + need_gfds, delay);
  GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "g_poll returned : %d\n", poll_result);
  if (-1 == poll_result)
    return GNUNET_SYSERR;

  /* Take care of GUI events.
   * Dispatching the events here will eventually crash the scheduler, must do this
   * from within a task (currently we're not in a task, but in a select() call, remember)
   * Startup reason is used to pass the scheduler sanity check.
   */
  if (NULL != ml->gmc)
  {
    if (g_main_context_check (ml->gmc,
                              max_priority,
                              &ml->cached_poll_array[fd_counter],
                              need_gfds))
      GNUNET_SCHEDULER_add_with_reason_and_priority (
        &dispatch_gtk_task,
        ml,
        GNUNET_SCHEDULER_REASON_STARTUP,
        GNUNET_SCHEDULER_PRIORITY_UI);
  }
  /* Now map back GNUnet scheduler events ... */
  if (NULL != rfds)
    GNUNET_NETWORK_fdset_zero (rfds);
  if (NULL != wfds)
    GNUNET_NETWORK_fdset_zero (wfds);
  if (NULL != efds)
    GNUNET_NETWORK_fdset_zero (efds);
  for (ui = 0; ui < fd_counter; ui++)
  {
    int set = 0;

    if ((NULL != rfds) &&
        (set |= (FD_ISSET (ml->cached_poll_array[ui].fd, &aread) &&
                 (0 != (ml->cached_poll_array[ui].revents &
                        (G_IO_IN | G_IO_HUP | G_IO_ERR))))))
      GNUNET_NETWORK_fdset_set_native (rfds, ml->cached_poll_array[ui].fd);
    if ((NULL != wfds) &&
        (set |=
         (FD_ISSET (ml->cached_poll_array[ui].fd, &awrite) &&
          (0 != (ml->cached_poll_array[ui].revents & (G_IO_OUT | G_IO_ERR))))))
      GNUNET_NETWORK_fdset_set_native (wfds, ml->cached_poll_array[ui].fd);
    if ((NULL != efds) &&
        (set |= (FD_ISSET (ml->cached_poll_array[ui].fd, &aexcept) &&
                 (0 != (ml->cached_poll_array[ui].revents & G_IO_ERR)))))
      GNUNET_NETWORK_fdset_set_native (efds, ml->cached_poll_array[ui].fd);
    if (set)
      result++;
  }
  return result;
}


/**
 * Actual main function run right after GNUnet's scheduler
 * is initialized.  Initializes up GTK and Glade and starts the
 * combined event loop.
 *
 * @param cls the `struct GNUNET_GTK_MainLoop`
 * @param args leftover command line arguments (go to gtk)
 * @param cfgfile name of the configuration file
 * @param cfg handle to the configuration
 */
static void
run_main_loop (void *cls,
               char *const *args,
               const char *cfgfile,
               const struct GNUNET_CONFIGURATION_Handle *cfg)
{
  struct GNUNET_GTK_MainLoop *ml = cls;
  struct GNUNET_CONFIGURATION_Handle *gcfg;
  char *baseconfig;
  char *ipath2;
  char *ipath;
  int argc;

  /* command-line processing for Gtk arguments */
  argc = 0;
  while (args[argc] != NULL)
    argc++;
  gtk_init (&argc, (char ***) &args);
  gcfg = GNUNET_CONFIGURATION_create ();

  if (NULL == (ipath = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR)))
  {
    GNUNET_break (0);
    return;
  }
  GNUNET_asprintf (&baseconfig, "%s%s", ipath, "config.d");
  (void) GNUNET_CONFIGURATION_load_from (gcfg, baseconfig);
  GNUNET_free (baseconfig);
  /* FIXME: this was supposed to search the GTK-IPK, so the logic here is badly broken now */
  if (NULL ==
      (ipath2 = GNUNET_OS_installation_get_path (GNUNET_OS_IPK_DATADIR)))
  {
    GNUNET_break (0);
    return;
  }
  if (0 != strcmp (ipath, ipath2))
  {
    GNUNET_asprintf (&baseconfig, "%s%s", ipath2, "config.d");
    if (GNUNET_YES == GNUNET_DISK_directory_test (baseconfig, GNUNET_YES))
      (void) GNUNET_CONFIGURATION_load_from (gcfg, baseconfig);
    GNUNET_free (baseconfig);
  }
  GNUNET_free (ipath2);
  GNUNET_free (ipath);

  if (GNUNET_YES == GNUNET_DISK_file_test (cfgfile))
    (void) GNUNET_CONFIGURATION_parse (gcfg, cfgfile);
  ml->rs = GNUNET_NETWORK_fdset_create ();
  ml->ws = GNUNET_NETWORK_fdset_create ();
  ml->gml = g_main_loop_new (NULL, TRUE);
  ml->gmc = g_main_loop_get_context (ml->gml);
  ml->cfg = gcfg;
  if (NULL != cfgfile)
    ml->cfgfile = GNUNET_strdup (cfgfile);
  ml->argc = argc;
  ml->argv = args;

  /* start the Gtk event loop */
  GNUNET_assert (TRUE == g_main_context_acquire (ml->gmc));
  GNUNET_SCHEDULER_set_select (&gnunet_gtk_select, ml);

  /* keep Gtk event loop running even if there are no GNUnet tasks */
  ml->dummy_task = GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_UNIT_FOREVER_REL,
                                                 &keepalive_task,
                                                 ml);

  /* run main task of the application */
  GNUNET_SCHEDULER_add_with_reason_and_priority (ml->main_task,
                                                 ml,
                                                 GNUNET_SCHEDULER_REASON_STARTUP,
                                                 GNUNET_SCHEDULER_PRIORITY_DEFAULT);
}


/**
 * Initialize the main loop.
 *
 * @param binary_name binary name
 * @param binary_help help text for the binary
 * @param argc number of command line options
 * @param argv command line options
 * @param options allowed command line options
 * @param main_window_file glade file for the main window
 * @param main_task first task to run, closure will be set to the `struct GNUNET_GTK_MainLoop`
 * @return #GNUNET_OK on success, #GNUNET_SYSERR on error (i.e. bad command-line options, etc)
 */
int
GNUNET_GTK_main_loop_start (const char *binary_name,
                            const char *binary_help,
                            int argc,
                            char *const *argv,
                            struct GNUNET_GETOPT_CommandLineOption *options,
                            const char *main_window_file,
                            GNUNET_SCHEDULER_TaskCallback main_task)
{
  struct GNUNET_GTK_MainLoop ml;
  int ret;

  memset (&ml, 0, sizeof (ml));
  ml.main_window_file = main_window_file;
  ml.main_task = main_task;
  ret = GNUNET_PROGRAM_run (argc,
                            argv,
                            binary_name,
                            binary_help,
                            options,
                            &run_main_loop,
                            &ml);
  if (NULL != ml.cached_poll_array)
    g_free (ml.cached_poll_array);
  if (NULL != ml.rs)
    GNUNET_NETWORK_fdset_destroy (ml.rs);
  if (NULL != ml.ws)
    GNUNET_NETWORK_fdset_destroy (ml.ws);
  if (NULL != ml.builder)
    g_object_unref (G_OBJECT (ml.builder));
  if (NULL != ml.gml)
    g_main_loop_unref (ml.gml);
  if (NULL != ml.cfg)
  {
    GNUNET_CONFIGURATION_destroy (ml.cfg);
    ml.cfg = NULL;
  }
  GNUNET_free (ml.cfgfile);
  return ret;
}


/* end of eventloop.c */
