/*
  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 "platform.h"
#include <gnunet/gnunet_util_lib.h>
#include <jansson.h>
#define MHD_APP_SOCKET_CNTX_TYPE struct SocketContext
#include <microhttpd2.h>
#include "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;
};


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

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

/**
 * The MHD daemon we are running.
 */
static struct MHD_Daemon *mhd;


/**
 * Function that queries MHD's select sets and
 * starts the task waiting for them.
 */
static struct GNUNET_SCHEDULER_Task *
prepare_daemon (void);


/**
 * Trigger MHD on timeout.
 *
 * @param cls not used
 */
static void
handle_timeout (void *cls)
{
  (void) cls;
  mhd_task = prepare_daemon ();
}


/**
 * Function that queries MHD's select sets and
 * starts the task waiting for them.
 */
static struct GNUNET_SCHEDULER_Task *
prepare_daemon (void)
{
  uint_fast64_t next_max_wait;
  enum MHD_StatusCode sc;

  sc = MHD_daemon_process_reg_events (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 NULL;
  }
  if (MHD_WAIT_INDEFINITELY == next_max_wait)
    return NULL;
  return GNUNET_SCHEDULER_add_delayed (
    GNUNET_TIME_relative_multiply (
      GNUNET_TIME_UNIT_MICROSECONDS,
      next_max_wait),
    &handle_timeout,
    NULL);
}


/**
 * Call MHD to process pending requests and then go back
 * and schedule the next run.
 *
 * @param cls NULL
 */
static void
run_daemon (void *cls)
{
  (void) cls;
  mhd_task = prepare_daemon ();
}


/**
 * 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;

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


/**
 * 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;

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


/**
 * Callback for registration/de-registration of the sockets to watch.
 *
 * @param cls the closure
 * @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)
{
  (void) 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);
  }
  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)
{
  enum MHD_StatusCode sc;

  GNUNET_assert (NULL == mhd);
  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,
                     NULL)));
  mhd = daemon;
  sc = MHD_daemon_start (mhd);
  if (MHD_SC_OK != sc)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "MHD_daemon_start failed: %d\n",
                (int) sc);
    return;
  }
  mhd_task = prepare_daemon ();
}


struct MHD_Daemon *
TALER_MHD2_daemon_stop (void)
{
  struct MHD_Daemon *ret;

  if (NULL != mhd_task)
  {
    GNUNET_SCHEDULER_cancel (mhd_task);
    mhd_task = NULL;
  }
  ret = mhd;
  mhd = NULL;
  return ret;
}


void
TALER_MHD2_daemon_trigger (void)
{
  if (NULL != mhd_task)
  {
    GNUNET_SCHEDULER_cancel (mhd_task);
    mhd_task = GNUNET_SCHEDULER_add_now (&run_daemon,
                                         NULL);
  }
  else
  {
    triggered = true;
  }
}


/* end of mhd2_run.c */
