/* gdkdisplaylinksource.c
 *
 * Copyright (C) 2015 Christian Hergert <christian@hergert.me>
 *
 * This library 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 of the License, or (at your option) any later version.
 *
 * This library 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/>.
 *
 * Authors:
 *   Christian Hergert <christian@hergert.me>
 */

#include "config.h"

#include <AppKit/AppKit.h>
#include <mach/mach_time.h>

#include "gdkdisplaylinksource.h"

#include "gdkdebug.h"
#include "gdkmacoseventsource-private.h"
#include "gdkmacosmonitor-private.h"
#include "gdk-private.h"

static gint64 host_to_frame_clock_time (gint64 val);

static gboolean
gdk_display_link_source_prepare (GSource *source,
                                 int     *timeout_)
{
  GdkDisplayLinkSource *impl = (GdkDisplayLinkSource *)source;
  gint64 now;

  now = g_source_get_time (source);

  if (now < impl->presentation_time)
    *timeout_ = (impl->presentation_time - now) / 1000L;
  else
    *timeout_ = -1;

  return impl->needs_dispatch;
}

static gboolean
gdk_display_link_source_check (GSource *source)
{
  GdkDisplayLinkSource *impl = (GdkDisplayLinkSource *)source;
  return impl->needs_dispatch;
}

static gboolean
gdk_display_link_source_dispatch (GSource     *source,
                                  GSourceFunc  callback,
                                  gpointer     user_data)
{
  GdkDisplayLinkSource *impl = (GdkDisplayLinkSource *)source;
  gboolean ret = G_SOURCE_CONTINUE;

  impl->needs_dispatch = FALSE;

  if (!impl->paused && callback != NULL)
    ret = callback (user_data);

  return ret;
}

static void
gdk_display_link_source_finalize (GSource *source)
{
  GdkDisplayLinkSource *impl = (GdkDisplayLinkSource *)source;

  if (!impl->paused)
    CVDisplayLinkStop (impl->display_link);

  CVDisplayLinkRelease (impl->display_link);
}

static GSourceFuncs gdk_display_link_source_funcs = {
  gdk_display_link_source_prepare,
  gdk_display_link_source_check,
  gdk_display_link_source_dispatch,
  gdk_display_link_source_finalize
};

void
gdk_display_link_source_pause (GdkDisplayLinkSource *source)
{
  g_return_if_fail (source->paused == FALSE);

  source->paused = TRUE;
  CVDisplayLinkStop (source->display_link);
}

void
gdk_display_link_source_unpause (GdkDisplayLinkSource *source)
{
  g_return_if_fail (source->paused == TRUE);

  source->paused = FALSE;
  CVDisplayLinkStart (source->display_link);
}

static CVReturn
gdk_display_link_source_frame_cb (CVDisplayLinkRef   display_link,
                                  const CVTimeStamp *inNow,
                                  const CVTimeStamp *inOutputTime,
                                  CVOptionFlags      flagsIn,
                                  CVOptionFlags     *flagsOut,
                                  void              *user_data)
{
  GdkDisplayLinkSource *impl = user_data;
  gint64 presentation_time;
  gboolean needs_wakeup;

  needs_wakeup = !g_atomic_int_get (&impl->needs_dispatch);

  presentation_time = host_to_frame_clock_time (inOutputTime->hostTime);

  impl->presentation_time = presentation_time;
  impl->needs_dispatch = TRUE;

  if (needs_wakeup)
    {
      NSEvent *event;

      /* Post a message so we'll break out of the message loop.
       *
       * We don't use g_main_context_wakeup() here because that
       * would result in sending a message to the pipe(2) fd in
       * the select thread which would then send this message as
       * well. Lots of extra work.
       */
      event = [NSEvent otherEventWithType: NSEventTypeApplicationDefined
                                 location: NSZeroPoint
                            modifierFlags: 0
                                timestamp: 0
                             windowNumber: 0
                                  context: nil
                                  subtype: GDK_MACOS_EVENT_SUBTYPE_EVENTLOOP
                                    data1: 0
                                    data2: 0];

      [NSApp postEvent:event atStart:YES];
    }

  return kCVReturnSuccess;
}

/**
 * gdk_display_link_source_new:
 * @display_id: the identifier of the monitor
 *
 * Creates a new `GSource` that will activate the dispatch function upon
 * notification from a CVDisplayLink that a new frame should be drawn.
 *
 * Effort is made to keep the transition from the high-priority
 * CVDisplayLink thread into this GSource lightweight. However, this is
 * somewhat non-ideal since the best case would be to do the drawing
 * from the high-priority thread.
 *
 * Returns: (transfer full): A newly created `GSource`
 */
GSource *
gdk_display_link_source_new (CGDirectDisplayID display_id,
                             CGDisplayModeRef  mode)
{
  GdkDisplayLinkSource *impl;
  GSource *source;
  char *name;

  source = g_source_new (&gdk_display_link_source_funcs, sizeof *impl);
  impl = (GdkDisplayLinkSource *)source;
  impl->display_id = display_id;
  impl->paused = TRUE;

  /* Create DisplayLink for timing information for the display in
   * question so that we can produce graphics for that display at whatever
   * rate it can provide.
   */
  if (CVDisplayLinkCreateWithCGDisplay (display_id, &impl->display_link) != kCVReturnSuccess)
    {
      g_warning ("Failed to initialize CVDisplayLink!");
      goto failure;
    }

  impl->refresh_rate = CGDisplayModeGetRefreshRate (mode) * 1000.0;

  if (impl->refresh_rate == 0)
    {
      const CVTime time = CVDisplayLinkGetNominalOutputVideoRefreshPeriod (impl->display_link);
      if (!(time.flags & kCVTimeIsIndefinite))
        impl->refresh_rate = (double)time.timeScale / (double)time.timeValue * 1000.0;
    }

  if (impl->refresh_rate != 0)
    {
      impl->refresh_interval = 1000000.0 / (double)impl->refresh_rate * 1000.0;
    }
  else
    {
      double period = CVDisplayLinkGetActualOutputVideoRefreshPeriod (impl->display_link);

      if (period == 0.0)
        period = 1.0 / 60.0;

      impl->refresh_rate = 1.0 / period * 1000L;
      impl->refresh_interval = period * 1000000L;
    }

  name = _gdk_macos_monitor_get_connector_name (display_id);
  GDK_NOTE (MISC,
            g_message ("Monitor \"%s\" discovered with Refresh Rate %d and Interval %"G_GINT64_FORMAT,
                       name ? name : "unknown",
                       impl->refresh_rate,
                       impl->refresh_interval));
  g_free (name);

  /* Wire up our callback to be executed within the high-priority thread. */
  CVDisplayLinkSetOutputCallback (impl->display_link,
                                  gdk_display_link_source_frame_cb,
                                  source);

  g_source_set_static_name (source, "[gdk] quartz frame clock");

  return source;

failure:
  g_source_unref (source);
  return NULL;
}

static gint64
host_to_frame_clock_time (gint64 val)
{
  /* NOTE: Code adapted from GLib's g_get_monotonic_time(). */

  mach_timebase_info_data_t timebase_info;

  /* we get nanoseconds from mach_absolute_time() using timebase_info */
  mach_timebase_info (&timebase_info);

  if (timebase_info.numer != timebase_info.denom)
    {
#ifdef HAVE_UINT128_T
      val = ((__uint128_t) val * (__uint128_t) timebase_info.numer) / timebase_info.denom / 1000;
#else
      guint64 t_high, t_low;
      guint64 result_high, result_low;

      /* 64 bit x 32 bit / 32 bit with 96-bit intermediate
       * algorithm lifted from qemu */
      t_low = (val & 0xffffffffLL) * (guint64) timebase_info.numer;
      t_high = (val >> 32) * (guint64) timebase_info.numer;
      t_high += (t_low >> 32);
      result_high = t_high / (guint64) timebase_info.denom;
      result_low = (((t_high % (guint64) timebase_info.denom) << 32) +
                    (t_low & 0xffffffff)) /
                   (guint64) timebase_info.denom;
      val = ((result_high << 32) | result_low) / 1000;
#endif
    }
  else
    {
      /* nanoseconds to microseconds */
      val = val / 1000;
    }

  return val;
}
