/*
 * dex-timeout.c
 *
 * Copyright 2022 Christian Hergert <christian@sourceandstack.com>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This program 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 */

#include "config.h"

#include <gio/gio.h>

#include "dex-compat-private.h"
#include "dex-error.h"
#include "dex-future-private.h"
#include "dex-scheduler.h"
#include "dex-timeout.h"
#include "dex-timeout-private.h"

/**
 * DexTimeout:
 *
 * `DexTimeout` is a [class@Dex.Future] that will reject after the configured
 * period of time.
 */

typedef struct _DexTimeout
{
  DexFuture parent_instance;
  GSource *source;
  DexFuture *future;
} DexTimeout;

typedef struct _DexTimeoutClass
{
  DexFutureClass parent_class;
} DexTimeoutClass;

DEX_DEFINE_FINAL_TYPE (DexTimeout, dex_timeout, DEX_TYPE_FUTURE)

static void
dex_timeout_clear_source (DexTimeout *timeout)
{
  GSource *source = NULL;

  g_assert (DEX_IS_TIMEOUT (timeout));

  dex_object_lock (timeout);
  source = g_steal_pointer (&timeout->source);
  dex_object_unlock (timeout);

  if (source != NULL)
    {
      if (!g_source_is_destroyed (source))
        g_source_destroy (source);

      g_source_unref (source);
    }
}

static void
dex_timeout_discard (DexFuture *future)
{
  DexTimeout *timeout = DEX_TIMEOUT (future);

  dex_timeout_clear_source (timeout);

  if (timeout->future != NULL)
    dex_future_discard (timeout->future, future);
}

static gboolean
dex_timeout_propagate (DexFuture *future,
                       DexFuture *completed)
{
  DexTimeout *timeout = DEX_TIMEOUT (future);

  g_assert (DEX_IS_TIMEOUT (timeout));
  g_assert (DEX_IS_FUTURE (completed));

  if (completed == timeout->future)
    {
      dex_timeout_clear_source (timeout);
      dex_future_complete_from (future, completed);
      return TRUE;
    }

  return FALSE;
}

static void
dex_timeout_finalize (DexObject *object)
{
  DexTimeout *timeout = DEX_TIMEOUT (object);

  if (timeout->source != NULL)
    {
      if (!g_source_is_destroyed (timeout->source))
        {
          g_critical ("%s destroyed while timer was active. "
                      "This is likely a bug as no future is holding a reference to %p",
                      DEX_OBJECT_TYPE_NAME (object), object);
          g_source_destroy (timeout->source);
        }

      g_clear_pointer (&timeout->source, g_source_unref);
    }

  dex_clear (&timeout->future);

  DEX_OBJECT_CLASS (dex_timeout_parent_class)->finalize (object);
}

static void
dex_timeout_class_init (DexTimeoutClass *timeout_class)
{
  DexObjectClass *object_class = DEX_OBJECT_CLASS (timeout_class);
  DexFutureClass *future_class = DEX_FUTURE_CLASS (timeout_class);

  object_class->finalize = dex_timeout_finalize;

  future_class->propagate = dex_timeout_propagate;
  future_class->discard = dex_timeout_discard;
}

static void
dex_timeout_init (DexTimeout *timeout)
{
}

static void
clear_weak_ref (gpointer data)
{
  dex_weak_ref_clear (data);
  g_free (data);
}

static gboolean
dex_timeout_source_func (gpointer data)
{
  DexWeakRef *wr = data;
  DexTimeout *timeout = dex_weak_ref_get (wr);

  g_assert (!timeout || DEX_IS_TIMEOUT (timeout));

  if (timeout != NULL)
    {
      dex_future_complete (DEX_FUTURE (timeout),
                           NULL,
                           g_error_new_literal (DEX_ERROR,
                                                DEX_ERROR_TIMED_OUT,
                                                "Operation timed out"));

      if (timeout->future != NULL)
        dex_future_discard (timeout->future, DEX_FUTURE (timeout));

      dex_object_lock (timeout);
      g_clear_pointer (&timeout->source, g_source_unref);
      dex_object_unlock (timeout);

      dex_unref (timeout);
    }

  return G_SOURCE_REMOVE;
}

/**
 * dex_timeout_new_deadline:
 * @deadline: the deadline in usec in the monotonic clock
 *
 * Creates a new timeout that will reject at a deadline.
 *
 * Returns: (transfer full):
 */
DexFuture *
dex_timeout_new_deadline (gint64 deadline)
{
  static const char *name;
  DexScheduler *scheduler;
  DexTimeout *timeout;
  DexWeakRef *wr;

  if G_UNLIKELY (name == NULL)
    name = g_intern_static_string ("[dex-timeout]");

  timeout = (DexTimeout *)dex_object_create_instance (DEX_TYPE_TIMEOUT);

  wr = g_new0 (DexWeakRef, 1);
  dex_weak_ref_init (wr, timeout);

  timeout->source = g_timeout_source_new (0);
  g_source_set_ready_time (timeout->source, deadline);
  _g_source_set_static_name (timeout->source, name);
  g_source_set_priority (timeout->source, G_PRIORITY_DEFAULT);
  g_source_set_callback (timeout->source,
                         dex_timeout_source_func,
                         wr, clear_weak_ref);

  if (!(scheduler = dex_scheduler_get_thread_default ()))
    scheduler = dex_scheduler_get_default ();

  /* TODO: Delay attaching until timeout is awaited.
   *
   * Currently, this attaches the GSource when the timeout is created. This
   * can be the wrong thing to do when you are creating a bunch of futures and
   * then want them to run on a specific scheduler.
   *
   * In that case, you probably want to delay arming the timeout until something
   * on the thread has "awaited" it.
   *
   * Currently we don't have explicit awaiting though, so this will need to be
   * implemented first before we can do the above work.
   */

  g_source_attach (timeout->source, dex_scheduler_get_main_context (scheduler));

  return DEX_FUTURE (timeout);
}

DexFuture *
_dex_timeout_new_for_future_deadline (DexFuture *future,
                                      gint64     deadline)
{
  DexTimeout *timeout;

  g_return_val_if_fail (DEX_IS_FUTURE (future), NULL);

  timeout = DEX_TIMEOUT (dex_timeout_new_deadline (deadline));
  timeout->future = g_steal_pointer (&future);
  dex_future_chain (timeout->future, DEX_FUTURE (timeout));

  return DEX_FUTURE (timeout);
}

/**
 * dex_timeout_new_seconds:
 * @seconds: number of seconds
 *
 * Create a new timeout that will reject in @seconds seconds
 *
 * Returns: (transfer full):
 */
DexFuture *
dex_timeout_new_seconds (int seconds)
{
  gint64 usec = (gint64) G_USEC_PER_SEC * seconds;
  return dex_timeout_new_deadline (g_get_monotonic_time () + usec);
}

DexFuture *
_dex_timeout_new_for_future_seconds (DexFuture *future,
                                     int        seconds)
{
  gint64 usec = (gint64) G_USEC_PER_SEC * seconds;

  g_return_val_if_fail (DEX_IS_FUTURE (future), NULL);

  return _dex_timeout_new_for_future_deadline (future, g_get_monotonic_time () + usec);
}

/**
 * dex_timeout_new_msec:
 * @msec: number of milliseconds
 *
 * Create a new timeout that will reject in @msec milliseconds
 *
 * Returns: (transfer full):
 */
DexFuture *
dex_timeout_new_msec (int msec)
{
  gint64 usec = (G_USEC_PER_SEC/1000L) * msec;
  return dex_timeout_new_deadline (g_get_monotonic_time () + usec);
}

DexFuture *
_dex_timeout_new_for_future_msec (DexFuture *future,
                                  int        msec)
{
  gint64 usec = (G_USEC_PER_SEC/1000L) * msec;

  g_return_val_if_fail (DEX_IS_FUTURE (future), NULL);

  return _dex_timeout_new_for_future_deadline (future, g_get_monotonic_time () + usec);
}

/**
 * dex_timeout_new_usec:
 * @usec: number of microseconds
 *
 * Create a new timeout that will reject in @usec microseconds
 *
 * Returns: (transfer full):
 */
DexFuture *
dex_timeout_new_usec (gint64 usec)
{
  return dex_timeout_new_deadline (g_get_monotonic_time () + usec);
}

DexFuture *
_dex_timeout_new_for_future (DexFuture *future,
                             gint64     usec)
{
  g_return_val_if_fail (DEX_IS_FUTURE (future), NULL);

  return _dex_timeout_new_for_future_deadline (future, g_get_monotonic_time () + usec);
}

/**
 * dex_timeout_postpone_until:
 * @timeout: a [class@Dex.Timeout]
 * @deadline: a deadline in monotonic clock
 *
 * Postpoone @timeout to complete at @deadline in the monotonic
 * clock. See [func@GLib.get_monotonic_time] for getting the
 * monotonic time in microseconds.
 */
void
dex_timeout_postpone_until (DexTimeout *timeout,
                            gint64      deadline)
{
  g_return_if_fail (DEX_IS_TIMEOUT (timeout));

  dex_object_lock (timeout);
  if (timeout->source != NULL)
    g_source_set_ready_time (timeout->source, deadline);
  dex_object_unlock (timeout);
}
