/*
  This file is part of TALER
  Copyright (C) 2019-2025 Taler Systems SA

  TALER is free software; you can redistribute it and/or modify it under the
  terms of the GNU Affero General Public License as published by the Free Software
  Foundation; either version 3, or (at your option) any later version.

  TALER 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 Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License along with
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file mhd2_run.c
 * @brief API for running an MHD daemon with the
 *        GNUnet scheduler
 * @author Christian Grothoff
 */
#include "taler/platform.h"
#include <gnunet/gnunet_util_lib.h>
#include <jansson.h>
#define MHD_APP_SOCKET_CNTX_TYPE struct SocketContext
#include <microhttpd2.h>
#include "taler/taler_mhd2_lib.h"


/**
 * Context to track whatever MHD wants us to wait for.
 */
struct SocketContext
{
  /**
   * Task for this socket.
   */
  struct GNUNET_SCHEDULER_Task *mhd_rtask;

  /**
   * Task for this socket.
   */
  struct GNUNET_SCHEDULER_Task *mhd_wtask;

  /**
   * Internal handle to pass to MHD when ready.
   */
  struct MHD_EventUpdateContext *ecb_cntx;

  /**
   * Socket to watch for.
   */
  struct GNUNET_NETWORK_Handle *fd;

  /**
   * Daemon this socket is about.
   */
  struct DaemonEntry *de;
};


/**
 * Entry in list of HTTP servers we are running.
 */
struct DaemonEntry
{
  /**
   * Kept in a DLL.
   */
  struct DaemonEntry *next;

  /**
   * Kept in a DLL.
   */
  struct DaemonEntry *prev;

  /**
   * The actual daemon.
   */
  struct MHD_Daemon *mhd;

  /**
   * Task running the HTTP server.
   */
  struct GNUNET_SCHEDULER_Task *mhd_task;

  /**
   * Task waiting for timeout on the HTTP server.
   */
  struct GNUNET_SCHEDULER_Task *timeout_task;

  /**
   * Set to true if we should immediately MHD_run() again.
   */
  bool triggered;

};


/**
 * Head of list of HTTP servers.
 */
static struct DaemonEntry *mhd_head;

/**
 * Tail of list of HTTP servers.
 */
static struct DaemonEntry *mhd_tail;


/**
 * Function that queries MHD's select sets and
 * starts the task waiting for them.
 *
 * @param[in,out] de daemon to start tasks for
 */
static void
prepare_daemon (struct DaemonEntry *de);


/**
 * Trigger MHD on timeout.
 *
 * @param cls a `struct DaemonEntry`
 */
static void
handle_timeout (void *cls)
{
  struct DaemonEntry *de = cls;

  de->timeout_task = NULL;
  prepare_daemon (de);
}


static void
prepare_daemon (struct DaemonEntry *de)
{
  uint_fast64_t next_max_wait;
  enum MHD_StatusCode sc;

  sc = MHD_daemon_process_reg_events (de->mhd,
                                      &next_max_wait);
  if (MHD_SC_OK != sc)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "MHD_daemon_process_reg_events failed: %d\n",
                (int) sc);
    return;
  }
  if (MHD_WAIT_INDEFINITELY == next_max_wait)
    return;
  de->timeout_task
    = GNUNET_SCHEDULER_add_delayed (
        GNUNET_TIME_relative_multiply (
          GNUNET_TIME_UNIT_MICROSECONDS,
          next_max_wait),
        &handle_timeout,
        de);
}


/**
 * Call MHD to process pending requests and then go back
 * and schedule the next run.
 *
 * @param cls our `struct DaemonEntry *`
 */
static void
run_daemon (void *cls)
{
  struct DaemonEntry *de = cls;

  de->mhd_task = NULL;
  prepare_daemon (de);
}


/**
 * Called whenever MHD should process read-events on the socket.
 *
 * @param cls a `struct SocketContext`
 */
static void
mhd_rready (void *cls)
{
  struct SocketContext *sc = cls;
  struct DaemonEntry *de = sc->de;

  sc->mhd_rtask = NULL;
  MHD_daemon_event_update (de->mhd,
                           sc->ecb_cntx,
                           MHD_FD_STATE_RECV);
  if (NULL != de->mhd_task)
    GNUNET_SCHEDULER_cancel (de->mhd_task);
  de->mhd_task = GNUNET_SCHEDULER_add_now (&run_daemon,
                                           de);
}


/**
 * Called whenever MHD should process write-events on the socket.
 *
 * @param cls a `struct SocketContext`
 */
static void
mhd_wready (void *cls)
{
  struct SocketContext *sc = cls;
  struct DaemonEntry *de = sc->de;

  sc->mhd_wtask = NULL;
  MHD_daemon_event_update (de->mhd,
                           sc->ecb_cntx,
                           MHD_FD_STATE_SEND);
  if (NULL != de->mhd_task)
    GNUNET_SCHEDULER_cancel (de->mhd_task);
  de->mhd_task = GNUNET_SCHEDULER_add_now (&run_daemon,
                                           de);
}


/**
 * Callback for registration/de-registration of the sockets to watch.
 *
 * @param cls our `struct DaemonEntry`
 * @param fd the socket to watch
 * @param watch_for the states of the @a fd to watch, if set to
 *                  #MHD_FD_STATE_NONE the socket must be de-registred
 * @param app_cntx_old the old application defined context for the socket,
 *                     NULL if @a fd socket was not registered before
 * @param ecb_cntx the context handle to be used
 *                 with #MHD_daemon_event_update()
 * @return NULL if error (to connection will be aborted),
 *         or the new socket context
 */
static MHD_APP_SOCKET_CNTX_TYPE *
socket_registration_update (
  void *cls,
  MHD_Socket fd,
  enum MHD_FdState watch_for,
  MHD_APP_SOCKET_CNTX_TYPE *app_cntx_old,
  struct MHD_EventUpdateContext *ecb_cntx)
{
  struct DaemonEntry *de = cls;

  if (NULL == app_cntx_old)
  {
    app_cntx_old = GNUNET_new (struct SocketContext);
    app_cntx_old->ecb_cntx = ecb_cntx;
    app_cntx_old->fd = GNUNET_NETWORK_socket_box_native (fd);
    app_cntx_old->de = de;
  }
  if (MHD_FD_STATE_NONE == watch_for)
  {
    if (NULL != app_cntx_old->mhd_rtask)
      GNUNET_SCHEDULER_cancel (app_cntx_old->mhd_rtask);
    if (NULL != app_cntx_old->mhd_wtask)
      GNUNET_SCHEDULER_cancel (app_cntx_old->mhd_wtask);
    GNUNET_NETWORK_socket_free_memory_only_ (app_cntx_old->fd);
    GNUNET_free (app_cntx_old);
    return NULL;
  }
  if ( (MHD_FD_STATE_RECV & watch_for) &&
       (NULL == app_cntx_old->mhd_rtask) )
  {
    app_cntx_old->mhd_rtask
      = GNUNET_SCHEDULER_add_read_net (GNUNET_TIME_UNIT_FOREVER_REL,
                                       app_cntx_old->fd,
                                       &mhd_rready,
                                       app_cntx_old);
  }
  if ( (MHD_FD_STATE_SEND & watch_for) &&
       (NULL == app_cntx_old->mhd_wtask) )
  {
    app_cntx_old->mhd_wtask
      = GNUNET_SCHEDULER_add_write_net (GNUNET_TIME_UNIT_FOREVER_REL,
                                        app_cntx_old->fd,
                                        &mhd_wready,
                                        app_cntx_old);
  }
  if ( (0 == (MHD_FD_STATE_RECV & watch_for)) &&
       (NULL != app_cntx_old->mhd_rtask) )
  {
    GNUNET_SCHEDULER_cancel (app_cntx_old->mhd_rtask);
    app_cntx_old->mhd_rtask = NULL;
  }
  if ( (0 == (MHD_FD_STATE_SEND & watch_for)) &&
       (NULL != app_cntx_old->mhd_wtask) )
  {
    GNUNET_SCHEDULER_cancel (app_cntx_old->mhd_wtask);
    app_cntx_old->mhd_wtask = NULL;
  }
  return app_cntx_old;
}


void
TALER_MHD2_daemon_start (struct MHD_Daemon *daemon)
{
  struct DaemonEntry *de;
  enum MHD_StatusCode sc;

  de = GNUNET_new (struct DaemonEntry);
  GNUNET_assert (MHD_SC_OK ==
                 MHD_DAEMON_SET_OPTIONS (
                   daemon,
                   MHD_D_OPTION_REREGISTER_ALL (true),
                   MHD_D_OPTION_WM_EXTERNAL_EVENT_LOOP_CB_LEVEL (
                     &socket_registration_update,
                     de)));
  de->mhd = daemon;
  sc = MHD_daemon_start (de->mhd);
  if (MHD_SC_OK != sc)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "MHD_daemon_start failed: %d\n",
                (int) sc);
    GNUNET_free (de);
    return;
  }
  GNUNET_CONTAINER_DLL_insert (mhd_head,
                               mhd_tail,
                               de);
  prepare_daemon (de);
}


void
TALER_MHD2_daemons_halt (void)
{
  for (struct DaemonEntry *de = mhd_head;
       NULL != de;
       de = de->next)
  {
    if (NULL != de->mhd_task)
    {
      GNUNET_SCHEDULER_cancel (de->mhd_task);
      de->mhd_task = NULL;
    }
    de->triggered = false;
  }
}


void
TALER_MHD2_daemons_quiesce (void)
{
  for (struct DaemonEntry *de = mhd_head;
       NULL != de;
       de = de->next)
  {
#if FIXME_MHD2
    int fd;
#endif

    if (NULL != de->mhd_task)
    {
      GNUNET_SCHEDULER_cancel (de->mhd_task);
      de->mhd_task = NULL;
    }
    de->triggered = false;
#if FIXME_MHD2
    fd = MHD_daemon_quiesce (de->mhd);
    GNUNET_break (0 == close (fd));
#endif
  }
}


void
TALER_MHD2_daemons_destroy (void)
{
  struct DaemonEntry *de;

  while (NULL != (de = mhd_head))
  {
    struct MHD_Daemon *mhd = de->mhd;

    if (NULL != de->mhd_task)
    {
      GNUNET_SCHEDULER_cancel (de->mhd_task);
      de->mhd_task = NULL;
    }
    MHD_daemon_destroy (mhd);
    GNUNET_CONTAINER_DLL_remove (mhd_head,
                                 mhd_tail,
                                 de);
    GNUNET_free (de);
  }
}


void
TALER_MHD2_daemons_trigger (void)
{
  for (struct DaemonEntry *de = mhd_head;
       NULL != de;
       de = de->next)
  {
    if (NULL != de->mhd_task)
    {
      GNUNET_SCHEDULER_cancel (de->mhd_task);
      de->mhd_task = GNUNET_SCHEDULER_add_now (&run_daemon,
                                               de);
    }
    else
    {
      de->triggered = true;
    }
  }
}


/* end of mhd2_run.c */
