/*
     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 "anastasis_gtk_util.h"

/**
 * 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 ANASTASIS_GTK_MainLoop
{

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

  /**
   * GNUnet configuration (includes defaults from gnunet/config.d/)
   */
  struct GNUNET_CONFIGURATION_Handle *gnunet_cfg;

  /**
   * Name of the GtkBuilder UI 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 for gnunet-gtk.
   */
  char *gnunet_gtk_cfgfile;

  /**
   * Name of the configuration file for GNUnet (core).
   */
  char *gnunet_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;

};


const struct GNUNET_CONFIGURATION_Handle *
ANASTASIS_GTK_main_loop_get_gnunet_configuration (
  struct ANASTASIS_GTK_MainLoop *ml)
{
  return ml->gnunet_cfg;
}


const struct GNUNET_CONFIGURATION_Handle *
ANASTASIS_GTK_main_loop_get_gtk_configuration (
  struct ANASTASIS_GTK_MainLoop *ml)
{
  return ml->gnunet_gtk_cfg;
}


void
ANASTASIS_GTK_main_loop_quit (struct ANASTASIS_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;
  }
}


GtkBuilder *
ANASTASIS_GTK_main_loop_get_builder (struct ANASTASIS_GTK_MainLoop *ml)
{
  return ml->builder;
}


int
ANASTASIS_GTK_main_loop_build_window (
  const struct GNUNET_OS_ProjectData *pd,
  struct ANASTASIS_GTK_MainLoop *ml,
  GObject *data)
{
  ANASTASIS_GTK_set_icon_search_path (pd);
  ANASTASIS_GTK_load_css (pd);
  ml->builder = ANASTASIS_GTK_get_new_builder (pd,
                                               ml->main_window_file,
                                               data);
  if (NULL == ml->builder)
  {
    ANASTASIS_GTK_main_loop_quit (ml);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


const char *
ANASTASIS_GTK_main_loop_get_gnunet_configuration_file (
  struct ANASTASIS_GTK_MainLoop *ml)
{
  return ml->gnunet_cfgfile;
}


const char *
ANASTASIS_GTK_main_loop_get_gtk_configuration_file (
  struct ANASTASIS_GTK_MainLoop *ml)
{
  return ml->gnunet_gtk_cfgfile;
}


GObject *
ANASTASIS_GTK_main_loop_get_object (struct ANASTASIS_GTK_MainLoop *ml,
                                 const char *name)
{
  return gtk_builder_get_object (ml->builder,
                                 name);
}


void
ANASTASIS_GTK_main_loop_get_args (
  struct ANASTASIS_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 ANASTASIS_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 ANASTASIS_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 ANASTASIS_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 ANASTASIS_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 ANASTASIS_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 GTK and starts the
 * combined event loop.
 *
 * @param cls the `struct ANASTASIS_GTK_MainLoop`
 * @param args leftover command line arguments (go to gtk)
 * @param gnunet_gtk_cfgfile name of the gnunet-gtk configuration file
 * @param gnunet_gtk_cfg handle to the configuration
 */
static void
run_main_loop (void *cls,
               char *const *args,
               const char *gnunet_gtk_cfgfile,
               const struct GNUNET_CONFIGURATION_Handle *gnunet_gtk_cfg)
{
  struct ANASTASIS_GTK_MainLoop *ml = cls;
  const struct GNUNET_OS_ProjectData *gnunet_pd
    = GNUNET_OS_project_data_gnunet ();
  int argc;

  /* GTK4 no longer consumes command-line arguments of its own, so whatever
     GNUnet's option parser left over is ours to interpret */
  argc = 0;
  while (NULL != args[argc])
    argc++;
  gtk_init ();
  ml->argc = argc;
  ml->argv = args;

  if (NULL != gnunet_gtk_cfgfile)
    ml->gnunet_gtk_cfgfile = GNUNET_strdup (gnunet_gtk_cfgfile);
  ml->gnunet_gtk_cfg = gnunet_gtk_cfg;
  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->gnunet_cfg
    = GNUNET_CONFIGURATION_create (gnunet_pd);
  if (GNUNET_OK !=
      GNUNET_CONFIGURATION_load (ml->gnunet_cfg,
                                 ml->gnunet_cfgfile))
  {
    GNUNET_break (0);
    GNUNET_SCHEDULER_shutdown ();
    return;
  }

  /* start the Gtk event loop */
  GNUNET_assert (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);
}


int
ANASTASIS_GTK_main_loop_start (
  const struct GNUNET_OS_ProjectData *pd,
  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 ANASTASIS_GTK_MainLoop ml;
  int ret;
  unsigned int olen = 0;

  memset (&ml, 0, sizeof (ml));
  ml.main_window_file = main_window_file;
  ml.main_task = main_task;
  while (NULL != options[olen].name)
    olen++;
  {
    struct GNUNET_GETOPT_CommandLineOption ox[] = {
      GNUNET_GETOPT_option_string ('C',
                                   "gnunet-config",
                                   "FILENAME",
                                   "configuration file used by GNUnet core",
                                   &ml.gnunet_cfgfile)
    };
    struct GNUNET_GETOPT_CommandLineOption *o2;

    o2 = GNUNET_new_array (olen + 2,
                           struct GNUNET_GETOPT_CommandLineOption);
    memcpy (&o2[0],
            &ox,
            sizeof (struct GNUNET_GETOPT_CommandLineOption));
    memcpy (&o2[1],
            options,
            sizeof (struct GNUNET_GETOPT_CommandLineOption) * olen);
    ret = GNUNET_PROGRAM_run (pd,
                              argc,
                              argv,
                              binary_name,
                              binary_help,
                              o2,
                              &run_main_loop,
                              &ml);
    GNUNET_free (o2);
  }


  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.gnunet_cfg)
  {
    GNUNET_CONFIGURATION_destroy (ml.gnunet_cfg);
    ml.gnunet_cfg = NULL;
  }
  ml.gnunet_gtk_cfg = NULL;
  GNUNET_free (ml.gnunet_cfgfile);
  GNUNET_free (ml.gnunet_gtk_cfgfile);
  return ret;
}


/* end of eventloop.c */
