/*
 * Copyright (C) 2001 Havoc Pennington
 * Copyright (C) 2005 Elijah Newren
 * Copyright (C) 2020 Red Hat Inc.
 *
 * This program 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 2 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
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "compositor/meta-later-private.h"

#include "cogl/cogl.h"
#include "compositor/compositor-private.h"
#include "core/display-private.h"
#include "meta/meta-later.h"

typedef struct _MetaLater
{
  unsigned int id;
  unsigned int ref_count;
  MetaLaterType when;

  GSourceFunc func;
  gpointer user_data;
  GDestroyNotify destroy_notify;

  guint source_id;
  gboolean run_once;
} MetaLater;

#define META_LATER_N_TYPES (META_LATER_IDLE + 1)

struct _MetaLaters
{
  MetaCompositor *compositor;

  unsigned int last_later_id;

  GSList *laters[META_LATER_N_TYPES];

  gulong before_update_handler_id;
};

static MetaLater *
meta_later_ref (MetaLater *later)
{
  later->ref_count++;
  return later;
}

static void
meta_later_unref (MetaLater *later)
{
  if (--later->ref_count == 0)
    {
      if (later->destroy_notify)
        {
          later->destroy_notify (later->user_data);
          later->destroy_notify = NULL;
        }

      g_free (later);
    }
}

static void
meta_later_destroy (MetaLater *later)
{
  g_clear_handle_id (&later->source_id, g_source_remove);
  later->func = NULL;
  meta_later_unref (later);
}

#ifdef COGL_HAS_TRACING
static const char *
later_type_to_string (MetaLaterType when)
{
  switch (when)
    {
    case META_LATER_RESIZE:
      return "Later (resize)";
    case META_LATER_CALC_SHOWING:
      return "Later (calc-showing)";
    case META_LATER_CHECK_FULLSCREEN:
      return "Later (check-fullscreen)";
    case META_LATER_SYNC_STACK:
      return "Later (sync-stack)";
    case META_LATER_BEFORE_REDRAW:
      return "Later (before-redraw)";
    case META_LATER_IDLE:
      return "Later (idle)";
    }

  return "unknown";
}
#endif

static gboolean
meta_later_invoke (MetaLater *later)
{
  COGL_TRACE_BEGIN_SCOPED (later, later_type_to_string (later->when));
  return later->func (later->user_data);
}

static gboolean
remove_later_from_list (unsigned int   later_id,
                        GSList       **laters_list)
{
  GSList *l;

  for (l = *laters_list; l; l = l->next)
    {
      MetaLater *later = l->data;

      if (later->id == later_id)
        {
          *laters_list = g_slist_delete_link (*laters_list, l);
          meta_later_destroy (later);
          return TRUE;
        }
    }

  return FALSE;
}

static void
run_repaint_laters (GSList **laters_list)
{
  g_autoptr (GSList) laters_copy = NULL;
  GSList *l;

  for (l = *laters_list; l; l = l->next)
    {
      MetaLater *later = l->data;

      if (!later->source_id ||
          (later->when <= META_LATER_BEFORE_REDRAW && !later->run_once))
        laters_copy = g_slist_prepend (laters_copy, meta_later_ref (later));
    }
  laters_copy = g_slist_reverse (laters_copy);

  for (l = laters_copy; l; l = l->next)
    {
      MetaLater *later = l->data;

      if (!later->func)
        remove_later_from_list (later->id, laters_list);
      else if (!meta_later_invoke (later))
        remove_later_from_list (later->id, laters_list);

      meta_later_unref (later);
    }
}

static void
on_before_update (ClutterStage     *stage,
                  ClutterStageView *stage_view,
                  MetaLaters       *laters)
{
  unsigned int i;
  GSList *l;
  gboolean needs_schedule_update = FALSE;

  for (i = 0; i < G_N_ELEMENTS (laters->laters); i++)
    run_repaint_laters (&laters->laters[i]);

  for (i = 0; i < G_N_ELEMENTS (laters->laters); i++)
    {
      for (l = laters->laters[i]; l; l = l->next)
        {
          MetaLater *later = l->data;

          if (!later->source_id)
            needs_schedule_update = TRUE;
        }
    }

  if (needs_schedule_update)
    clutter_stage_schedule_update (stage);
}

static gboolean
invoke_later_idle (gpointer data)
{
  MetaLater *later = data;

  if (!later->func (later->user_data))
    {
      meta_later_remove (later->id);
      return FALSE;
    }
  else
    {
      later->run_once = TRUE;
      return TRUE;
    }
}

/**
 * meta_laters_add:
 * @laters: a #MetaLaters
 * @when: enumeration value determining the phase at which to run the callback
 * @func: callback to run later
 * @user_data: data to pass to the callback
 * @notify: function to call to destroy @data when it is no longer in use, or %NULL
 *
 * Sets up a callback  to be called at some later time. @when determines the
 * particular later occasion at which it is called. This is much like g_idle_add(),
 * except that the functions interact properly with clutter event handling.
 * If a "later" function is added from a clutter event handler, and is supposed
 * to be run before the stage is redrawn, it will be run before that redraw
 * of the stage, not the next one.
 *
 * Return value: an integer ID (guaranteed to be non-zero) that can be used
 *  to cancel the callback and prevent it from being run.
 */
unsigned int
meta_laters_add (MetaLaters     *laters,
                 MetaLaterType   when,
                 GSourceFunc     func,
                 gpointer        user_data,
                 GDestroyNotify  notify)
{
  ClutterStage *stage = meta_compositor_get_stage (laters->compositor);
  MetaLater *later = g_new0 (MetaLater, 1);

  later->id = ++laters->last_later_id;
  later->ref_count = 1;
  later->when = when;
  later->func = func;
  later->user_data = user_data;
  later->destroy_notify = notify;

  laters->laters[when] = g_slist_prepend (laters->laters[when], later);

  switch (when)
    {
    case META_LATER_RESIZE:
      later->source_id = g_idle_add_full (META_PRIORITY_RESIZE,
                                          invoke_later_idle,
                                          later, NULL);
      g_source_set_name_by_id (later->source_id, "[mutter] invoke_later_idle");
      clutter_stage_schedule_update (stage);
      break;
    case META_LATER_CALC_SHOWING:
    case META_LATER_CHECK_FULLSCREEN:
    case META_LATER_SYNC_STACK:
    case META_LATER_BEFORE_REDRAW:
      clutter_stage_schedule_update (stage);
      break;
    case META_LATER_IDLE:
      later->source_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
                                          invoke_later_idle,
                                          later, NULL);
      g_source_set_name_by_id (later->source_id, "[mutter] invoke_later_idle");
      break;
    }

  return later->id;
}

/**
 * meta_later_add:
 * @when: enumeration value determining the phase at which to run the callback
 * @func: callback to run later
 * @data: data to pass to the callback
 * @notify: function to call to destroy @data when it is no longer in use, or %NULL
 *
 * Sets up a callback  to be called at some later time. @when determines the
 * particular later occasion at which it is called. This is much like g_idle_add(),
 * except that the functions interact properly with clutter event handling.
 * If a "later" function is added from a clutter event handler, and is supposed
 * to be run before the stage is redrawn, it will be run before that redraw
 * of the stage, not the next one.
 *
 * Return value: an integer ID (guaranteed to be non-zero) that can be used
 *  to cancel the callback and prevent it from being run.
 */
unsigned int
meta_later_add (MetaLaterType  when,
                GSourceFunc    func,
                gpointer       data,
                GDestroyNotify notify)
{
  MetaDisplay *display = meta_get_display ();
  MetaCompositor *compositor;

  g_return_val_if_fail (display, 0);
  g_return_val_if_fail (display->compositor, 0);

  compositor = display->compositor;
  return meta_laters_add (meta_compositor_get_laters (compositor),
                          when, func, data, notify);
}

/**
 * meta_laters_remove:
 * @laters: a #MetaLaters
 * @later_id: the integer ID returned from meta_later_add()
 *
 * Removes a callback added with meta_later_add()
 */
void
meta_laters_remove (MetaLaters   *laters,
                    unsigned int  later_id)
{
  unsigned int i;

  for (i = 0; i < G_N_ELEMENTS (laters->laters); i++)
    {
      if (remove_later_from_list (later_id, &laters->laters[i]))
        return;
    }
}

/**
 * meta_later_remove:
 * @later_id: the integer ID returned from meta_later_add()
 *
 * Removes a callback added with meta_later_add()
 */
void
meta_later_remove (unsigned int later_id)
{
  MetaDisplay *display = meta_get_display ();
  MetaCompositor *compositor;

  g_return_if_fail (display);

  compositor = display->compositor;
  if (!compositor)
    return;

  meta_laters_remove (meta_compositor_get_laters (compositor), later_id);
}

MetaLaters *
meta_laters_new (MetaCompositor *compositor)
{
  ClutterStage *stage = meta_compositor_get_stage (compositor);
  MetaLaters *laters;

  laters = g_new0 (MetaLaters, 1);
  laters->compositor = compositor;

  laters->before_update_handler_id =
    g_signal_connect (stage, "before-update",
                      G_CALLBACK (on_before_update),
                      laters);

  return laters;
}

void
meta_laters_free (MetaLaters *laters)
{
  ClutterStage *stage = meta_compositor_get_stage (laters->compositor);
  unsigned int i;

  for (i = 0; i < G_N_ELEMENTS (laters->laters); i++)
    g_slist_free_full (laters->laters[i], (GDestroyNotify) meta_later_unref);

  g_clear_signal_handler (&laters->before_update_handler_id, stage);
  g_free (laters);
}
