/* GDK - The GIMP Drawing Kit
 * Copyright (C) 2012 Red Hat, Inc.
 *
 * 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/>.
 */

#include "config.h"

#include <string.h>

#include "gdkframetimingsprivate.h"

#include "gdkenumtypes.h"

/**
 * GdkFrameTimings:
 *
 * Holds timing information for a single frame of the application’s displays.
 *
 * To retrieve `GdkFrameTimings` objects, use [method@Gdk.FrameClock.get_timings]
 * or [method@Gdk.FrameClock.get_current_timings]. The information in
 * `GdkFrameTimings` is useful for precise synchronization of video with
 * the event or audio streams, and for measuring quality metrics for the
 * application’s display, such as latency and jitter.
 */

G_DEFINE_BOXED_TYPE (GdkFrameTimings, gdk_frame_timings,
                     gdk_frame_timings_ref,
                     gdk_frame_timings_unref)

GdkFrameTimings *
_gdk_frame_timings_new (gint64 frame_counter)
{
  GdkFrameTimings *timings;

  timings = g_new0 (GdkFrameTimings, 1);
  timings->ref_count = 1;
  timings->frame_counter = frame_counter;
  timings->result = GDK_FRAME_PREPARING;

  return timings;
}

gboolean
_gdk_frame_timings_steal (GdkFrameTimings *timings,
                          gint64           frame_counter)
{
  if (timings->ref_count == 1)
    {
      memset (timings, 0, sizeof *timings);
      timings->ref_count = 1;
      timings->frame_counter = frame_counter;
      timings->result = GDK_FRAME_PREPARING;
      return TRUE;
    }

  return FALSE;
}

/**
 * gdk_frame_timings_setup:
 * @self: the timings
 * @frame_time: the frame time
 * @predicted_presentation_time: The predicted presentation time
 * @frame_start_time: The time this frame started the
 *   `GDK_FRAME_STAGE_FLUSH_EVENTS` stage
 * @stage_start_time: The time this frame started the
 *   `GDK_FRAME_STAGE_BEFORE_PAINT` stage
 *
 * Initializes the required values for a `GdkFrameTimings`.
 *
 * All provided timestamps are in nanoseconds.
 **/
void
gdk_frame_timings_setup (GdkFrameTimings *self,
                         uint64_t         frame_time,
                         uint64_t         predicted_presentation_time,
                         uint64_t         frame_start_time,
                         uint64_t         stage_start_time)
{
  self->frame_time = frame_time;
  self->predicted_presentation_time = predicted_presentation_time;
  self->stage_end_time[GDK_FRAME_STAGE_NONE] = frame_start_time;
  self->stage_end_time[GDK_FRAME_STAGE_FLUSH_EVENTS] = stage_start_time;
}

/**
 * gdk_frame_timings_ref:
 * @timings: a `GdkFrameTimings`
 *
 * Increases the reference count of @timings.
 *
 * Returns: @timings
 */
GdkFrameTimings *
gdk_frame_timings_ref (GdkFrameTimings *timings)
{
  g_return_val_if_fail (timings != NULL, NULL);

  timings->ref_count++;

  return timings;
}

/**
 * gdk_frame_timings_unref:
 * @timings: a `GdkFrameTimings`
 *
 * Decreases the reference count of @timings.
 *
 * If @timings is no longer referenced, it will be freed.
 */
void
gdk_frame_timings_unref (GdkFrameTimings *timings)
{
  g_return_if_fail (timings != NULL);
  g_return_if_fail (timings->ref_count > 0);

  timings->ref_count--;
  if (timings->ref_count == 0)
    g_free (timings);
}

/**
 * gdk_frame_timings_get_frame_counter:
 * @timings: a `GdkFrameTimings`
 *
 * Gets the frame counter value of the `GdkFrameClock` when
 * this frame was drawn.
 *
 * Returns: the frame counter value for this frame
 */
gint64
gdk_frame_timings_get_frame_counter (GdkFrameTimings *timings)
{
  return timings->frame_counter;
}

/**
 * gdk_frame_timings_get_complete:
 * @timings: a `GdkFrameTimings`
 *
 * Returns whether @timings are complete.
 *
 * The timing information in a `GdkFrameTimings` is filled in
 * incrementally as the frame as drawn and passed off to the
 * window system for processing and display to the user. The
 * accessor functions for `GdkFrameTimings` can return 0 to
 * indicate an unavailable value for two reasons: either because
 * the information is not yet available, or because it isn't
 * available at all.
 *
 * Once this function returns %TRUE for a frame, you can be
 * certain that no further values will become available and be
 * stored in the `GdkFrameTimings`.
 *
 * Returns: %TRUE if all information that will be available
 *   for the frame has been filled in.
 */
gboolean
gdk_frame_timings_get_complete (GdkFrameTimings *timings)
{
  g_return_val_if_fail (timings != NULL, FALSE);

  if (timings->throttling_hint == 0)
    return FALSE;

  switch (timings->result)
  {
    case GDK_FRAME_PREPARING:
    case GDK_FRAME_OUTSTANDING:
      return FALSE;

    case GDK_FRAME_SKIPPED:
    case GDK_FRAME_EMPTY:
    case GDK_FRAME_SUBMITTED:
    case GDK_FRAME_DISCARDED:
    case GDK_FRAME_PRESENTED:
      return TRUE;

    default:
      g_return_val_if_reached (TRUE);
  }
}

/**
 * gdk_frame_timings_get_result:
 * @timings: a `GdkFrameTimings`
 *
 * Gets the result of the frame cycle that recorded these timings. 
 *
 * The timing information in a `GdkFrameTimings` is filled in
 * incrementally as the frame as drawn and passed off to the
 * window system for processing and display to the user. The
 * accessor functions for `GdkFrameTimings` can return 0 to
 * indicate an unavailable value for two reasons: either because
 * the information is not yet available, or because it isn't
 * available at all. Looking at the result of the timings gives
 * an explanation for why a value is not available.
 *
 * Returns: The result of the frame these timings have been recorded for.
 *
 * Since: 4.24
 **/
GdkFrameResult
gdk_frame_timings_get_result (GdkFrameTimings *timings)
{
  g_return_val_if_fail (timings != NULL, GDK_FRAME_EMPTY);

  return timings->result;
}

/**
 * gdk_frame_timings_get_frame_time:
 * @timings: A `GdkFrameTimings`
 *
 * Returns the frame time for the frame.
 *
 * This is the time value that is typically used to time
 * animations for the frame. See [method@Gdk.FrameClock.get_frame_time].
 *
 * Returns: the frame time for the frame, in the timescale
 *  of g_get_monotonic_time()
 */
gint64
gdk_frame_timings_get_frame_time (GdkFrameTimings *timings)
{
  g_return_val_if_fail (timings != NULL, 0);

  return timings->frame_time / 1000;
}

uint64_t
gdk_frame_timings_get_frame_time_ns (GdkFrameTimings *timings)
{
  g_return_val_if_fail (timings != NULL, 0);

  return timings->frame_time;
}

/**
 * gdk_frame_timings_get_presentation_time:
 * @timings: a `GdkFrameTimings`
 *
 * Reurns the presentation time.
 *
 * This is the time at which the frame became visible to the user.
 *
 * Returns: the time the frame was displayed to the user, in the
 *   timescale of g_get_monotonic_time(), or 0 if no presentation
 *   time is available. See [method@Gdk.FrameTimings.get_complete]
 */
gint64
gdk_frame_timings_get_presentation_time (GdkFrameTimings *timings)
{
  g_return_val_if_fail (timings != NULL, 0);

  return timings->presentation_time / 1000;
}

uint64_t
gdk_frame_timings_get_presentation_time_ns (GdkFrameTimings *timings)
{
  g_return_val_if_fail (timings != NULL, 0);

  return timings->presentation_time;
}

/**
 * gdk_frame_timings_get_predicted_presentation_time:
 * @timings: a `GdkFrameTimings`
 *
 * Gets the predicted time at which this frame will be displayed.
 *
 * Although no predicted time may be available, if one is available,
 * it will be available while the frame is being generated, in contrast
 * to [method@Gdk.FrameTimings.get_presentation_time], which is only
 * available after the frame has been presented.
 *
 * In general, if you are simply animating, you should use
 * [method@Gdk.FrameClock.get_frame_time] rather than this function,
 * but this function is useful for applications that want exact control
 * over latency. For example, a movie player may want this information
 * for Audio/Video synchronization.
 *
 * Returns: The predicted time at which the frame will be presented,
 *   in the timescale of g_get_monotonic_time(), or 0 if no predicted
 *   presentation time is available.
 */
gint64
gdk_frame_timings_get_predicted_presentation_time (GdkFrameTimings *timings)
{
  g_return_val_if_fail (timings != NULL, 0);

  return timings->predicted_presentation_time / 1000;
}

uint64_t
gdk_frame_timings_get_predicted_presentation_time_ns (GdkFrameTimings *timings)
{
  g_return_val_if_fail (timings != NULL, 0);

  return timings->predicted_presentation_time;
}

/**
 * gdk_frame_timings_get_refresh_interval:
 * @timings: a `GdkFrameTimings`
 *
 * Gets the natural interval between presentation times for
 * the display that this frame was displayed on.
 *
 * Frame presentation usually happens during the “vertical
 * blanking interval”.
 *
 * Returns: the refresh interval of the display, in microseconds,
 *   or 0 if the refresh interval is not available.
 *   See [method@Gdk.FrameTimings.get_complete].
 */
gint64
gdk_frame_timings_get_refresh_interval (GdkFrameTimings *timings)
{
  g_return_val_if_fail (timings != NULL, 0);

  return (timings->refresh_interval + 500) / 1000;
}

uint64_t
gdk_frame_timings_get_refresh_interval_ns (GdkFrameTimings *timings)
{
  g_return_val_if_fail (timings != NULL, 0);

  return timings->refresh_interval;
}

/*<private>
 * gdk_frame_timings_get_start_time:
 * @self: the timings
 * @stage: the stage to query the start time for
 *
 * Gets the timestamp of when the given stage started processing.
 * 
 * If the stage has not started processing yet - usually because this
 * is the current frame's timings and the stage has not been reached yet,
 * then the timestamp of the last stage that has started will be
 * returned instead.
 *
 * Returns: the timestamp in nanoseconds
 **/
uint64_t
gdk_frame_timings_get_start_time (GdkFrameTimings *self,
                                  GdkFrameStage    stage)
{
  if (stage > 0)
    stage--;

  while (self->stage_end_time[stage] == 0 && stage > 0)
    stage--;

  return self->stage_end_time[stage];
}

/*<private>
 * gdk_frame_timings_get_end_time:
 * @self: the timings
 * @stage: the stage to query the end time for
 *
 * Gets the timestamp of when the given stage was finished processing.
 * 
 * If the stage has not finished processing yet - either because it is
 * in-process or because it has not yet started processing, then the
 * timestamp of the last stage that has finished will be returned instead.
 *
 * Returns: the timestamp in nanoseconds
 **/
uint64_t
gdk_frame_timings_get_end_time (GdkFrameTimings *self,
                                GdkFrameStage    stage)
{
  while (self->stage_end_time[stage] == 0 && stage > 0)
    stage--;

  return self->stage_end_time[stage];
}

/*<private>
 * gdk_frame_timings_get_throttling_hint:
 * @self: the timings
 *
 * Gets the timestamp of when the compositor suggested to resume rendering.
 *
 * Many backends implement a hint to allow clients to throttle painting. This
 * is modeled after 
 * [wl_surface::frame](https://wayland.app/protocols/wayland#wl_surface:request:frame)
 * or [AChoreographer::postFrameCallback](https://developer.android.com/ndk/reference/group/choreographer#achoreographer_framecallback)
 * and is the timestamp of when GTK's callback was called, not any timestamp
 * carried by the event.
 *
 * If the frame has not finished throttling yet - either because
 * it is still in-process or because the compositor has not
 * indicated to stop throttling yet - then 0 will be returned.
 *
 * If the compositor did not do any throttling for this frame, either because
 * GTK didn't request it or because the compositor does not support throttling
 * hints, then this time will be less than or equal to the end of the frame
 * timings queried via `gdk_frame_timings_get_end_time(self, GDK_FRAME_STAGE_RESUME_EVENTS)`.
 *
 * Returns: the timestamp in nanoseconds
 **/
uint64_t
gdk_frame_timings_get_throttling_hint (GdkFrameTimings *self)
{
  return self->throttling_hint;
}

void
gdk_frame_timings_outstanding (GdkFrameTimings *self)
{
  /* frames can only be completed in AFTER_PAINT, so we must still be in progress.
   * We might however be OUTSTANDING already because of a different surface submitting
   * a buffer.
   */
  g_warn_if_fail (self->result == GDK_FRAME_PREPARING || self->result == GDK_FRAME_OUTSTANDING);

  self->result = GDK_FRAME_OUTSTANDING;
}

void
gdk_frame_timings_throttling_hint (GdkFrameTimings *self,
                                   uint64_t         timestamp)
{
  g_warn_if_fail (self->result != GDK_FRAME_PREPARING);
  g_warn_if_fail (self->throttling_hint == 0);

  self->throttling_hint = timestamp;
}

void
gdk_frame_timings_submitted (GdkFrameTimings *self,
                             uint64_t         refresh)
{
  switch (self->result)
    {
      case GDK_FRAME_PREPARING:
        self->result = GDK_FRAME_SKIPPED;
        break;

      case GDK_FRAME_OUTSTANDING:
        self->result = GDK_FRAME_SUBMITTED;
        break;

      case GDK_FRAME_SKIPPED:
      case GDK_FRAME_PRESENTED:
        /* duplicate calls are allowed, but must have the same values */
        if (self->refresh_interval != refresh)
          {
            g_warning_once ("Duplicate call to gdk_frame_timings_submitted() with different values.");
          }
        return;

      case GDK_FRAME_EMPTY:
      case GDK_FRAME_SUBMITTED:
      case GDK_FRAME_DISCARDED:
        g_warning_once ("gdk_frame_timings_submitted() called on %s frame.",
                        g_enum_get_value (g_type_class_get (GDK_TYPE_FRAME_RESULT), self->result)->value_nick);
        return;

      default:
        g_assert_not_reached ();
    }

  if (refresh != 0)
    self->refresh_interval = refresh;
}

void
gdk_frame_timings_discarded (GdkFrameTimings *self)
{
  switch (self->result)
    {
      case GDK_FRAME_PREPARING:
        self->result = GDK_FRAME_SKIPPED;
        break;

      case GDK_FRAME_OUTSTANDING:
        self->result = GDK_FRAME_DISCARDED;
        break;

      case GDK_FRAME_SKIPPED:
      case GDK_FRAME_DISCARDED:
        /* duplicate calls are allowed */
        return;

      case GDK_FRAME_EMPTY:
      case GDK_FRAME_SUBMITTED:
      case GDK_FRAME_PRESENTED:
        g_warning_once ("gdk_frame_timings_discarded() called on already %s frame.",
                        g_enum_get_value (g_type_class_get (GDK_TYPE_FRAME_RESULT), self->result)->value_nick);
        return;

      default:
        g_assert_not_reached ();
        return;
    }
}

void
gdk_frame_timings_presented (GdkFrameTimings *self,
                             uint64_t         presentation_time,
                             uint64_t         refresh)
{
  switch (self->result)
    {
      case GDK_FRAME_PREPARING:
        self->result = GDK_FRAME_EMPTY;
        break;

      case GDK_FRAME_OUTSTANDING:
        self->result = GDK_FRAME_PRESENTED;
        break;

      case GDK_FRAME_EMPTY:
      case GDK_FRAME_PRESENTED:
        /* duplicate calls are allowed, but must have the same values */
        if (self->presentation_time != presentation_time ||
            self->refresh_interval != refresh)
          {
            int64_t time_diff = (int64_t) (presentation_time - self->presentation_time);
            g_warning_once ("Duplicate call to gdk_frame_timings_presented() with different values: "
                            "presentation time is %" PRId64 ".%" PRId64 "ms off from expected",
                            time_diff / (1000 * 1000), (time_diff / 1000) % 1000);
          }
        return;

      case GDK_FRAME_SKIPPED:
      case GDK_FRAME_SUBMITTED:
      case GDK_FRAME_DISCARDED:
        g_warning_once ("gdk_frame_timings_presented() called on %s frame.",
                        g_enum_get_value (g_type_class_get (GDK_TYPE_FRAME_RESULT), self->result)->value_nick);
        return;

      default:
        g_assert_not_reached ();
    }

  self->presentation_time = presentation_time;
  if (refresh != 0)
    self->refresh_interval = refresh;
}

guint64
gdk_frame_timings_get_serial (GdkFrameTimings *self)
{
  return self->serial;
}

/*<private>
 * gdk_frame_timings_set_serial:
 * @self: the timings
 * @serial: the serial
 *
 * Allows backends to set a serial to map the frame timings
 * to OS-specific IDs.
 * Timings can then later be queried by backends using
 * `gdk_frame_clock_find_timings()`.
 **/
void
gdk_frame_timings_set_serial (GdkFrameTimings *self,
                              guint64          serial)
{
  self->serial = serial;
}

