#include "clutter/clutter.h"
#include "tests/clutter-test-utils.h"

static const float refresh_rate = 60.0;
static const int64_t refresh_interval_us = G_USEC_PER_SEC / refresh_rate;

static ClutterFrameResult
timeline_frame_clock_frame (ClutterFrameClock *frame_clock,
                            ClutterFrame      *frame,
                            gpointer           user_data)
{
  ClutterFrameInfo frame_info;

  frame_info = (ClutterFrameInfo) {
    .refresh_rate = refresh_rate,
    .flags = CLUTTER_FRAME_INFO_FLAG_NONE,
    .sequence = 0,
  };

  if (!clutter_frame_get_target_presentation_time (frame,
                                                   &frame_info.presentation_time))
    frame_info.presentation_time = g_get_monotonic_time ();

  clutter_frame_clock_notify_presented (frame_clock, &frame_info);
  clutter_frame_clock_schedule_update (frame_clock);

  return CLUTTER_FRAME_RESULT_PENDING_PRESENTED;
}

static const ClutterFrameListenerIface timeline_frame_listener_iface = {
  .frame = timeline_frame_clock_frame,
};

static void
on_marker_reached (ClutterTimeline *timeline,
                   const char      *marker_name,
                   unsigned int     frame_number,
                   gboolean        *marker_reached)
{
  *marker_reached = TRUE;
}

static void
on_timeline_new_frame (ClutterTimeline *timeline,
                       int              time_ms,
                       int             *frame_counter)
{
  (*frame_counter)++;
}

static void
on_timeline_completed (ClutterTimeline *timeline,
                       GMainLoop       *main_loop)
{
  g_main_loop_quit (main_loop);
}

static void
frame_clock_timeline_basic (void)
{
  GMainLoop *main_loop;
  ClutterFrameClock *frame_clock;
  ClutterTimeline *timeline;
  gboolean marker1_reached;
  int frame_counter;
  int64_t before_us;
  int64_t after_us;
  int64_t duration_us;
  int64_t lateness_us;

  main_loop = g_main_loop_new (NULL, FALSE);
  frame_clock = clutter_frame_clock_new (refresh_rate,
                                         0,
                                         NULL,
                                         &timeline_frame_listener_iface,
                                         NULL);
  g_object_add_weak_pointer (G_OBJECT (frame_clock), (gpointer *) &frame_clock);

  timeline = g_object_new (CLUTTER_TYPE_TIMELINE,
                           "duration", 1000,
                           "frame-clock", frame_clock,
                           NULL);
  g_object_add_weak_pointer (G_OBJECT (timeline), (gpointer *) &timeline);

  clutter_timeline_add_marker_at_time (timeline, "marker1", 500);

  marker1_reached = FALSE;
  frame_counter = 0;

  g_signal_connect (timeline, "marker-reached::marker1",
                    G_CALLBACK (on_marker_reached),
                    &marker1_reached);
  g_signal_connect (timeline, "new-frame",
                    G_CALLBACK (on_timeline_new_frame),
                    &frame_counter);
  g_signal_connect (timeline, "completed",
                    G_CALLBACK (on_timeline_completed),
                    main_loop);

  clutter_timeline_start (timeline);

  before_us = g_get_monotonic_time ();

  g_main_loop_run (main_loop);

  after_us = g_get_monotonic_time ();
  duration_us = after_us - before_us;
  lateness_us = duration_us - ms2us (clutter_timeline_get_duration (timeline));

  g_assert_cmpint (lateness_us, >, -refresh_interval_us);

  g_assert_true (marker1_reached);

  /* Just check that we got at least a few frames. Require too high and we'll be
   * flaky.
   */
  g_assert_cmpint (frame_counter, >, 20);

  g_main_loop_unref (main_loop);
  g_object_unref (timeline);
  g_assert_null (timeline);
  clutter_frame_clock_destroy (frame_clock);
  g_assert_null (frame_clock);
}

static void
on_switch_reached (ClutterTimeline   *timeline,
                   const char        *marker_name,
                   unsigned int       frame_number,
                   ClutterFrameClock *new_frame_clock)
{
  ClutterFrameClock *old_frame_clock;

  old_frame_clock = clutter_timeline_get_frame_clock (timeline);
  clutter_frame_clock_inhibit (old_frame_clock);

  clutter_timeline_set_frame_clock (timeline, new_frame_clock);
}

static void
frame_clock_timeline_switch (void)
{
  GMainLoop *main_loop;
  ClutterFrameClock *frame_clock2;
  ClutterFrameClock *frame_clock1;
  ClutterTimeline *timeline;
  int frame_counter;
  int64_t before_us;
  int64_t after_us;
  int64_t duration_us;
  int64_t lateness_us;

  main_loop = g_main_loop_new (NULL, FALSE);

  frame_clock1 = clutter_frame_clock_new (refresh_rate,
                                          0,
                                          NULL,
                                          &timeline_frame_listener_iface,
                                          NULL);
  g_object_add_weak_pointer (G_OBJECT (frame_clock1), (gpointer *) &frame_clock1);
  frame_clock2 = clutter_frame_clock_new (refresh_rate,
                                          0,
                                          NULL,
                                          &timeline_frame_listener_iface,
                                          NULL);
  g_object_add_weak_pointer (G_OBJECT (frame_clock2), (gpointer *) &frame_clock2);

  timeline = g_object_new (CLUTTER_TYPE_TIMELINE,
                           "duration", 1000,
                           "frame-clock", frame_clock1,
                           NULL);
  g_object_add_weak_pointer (G_OBJECT (timeline), (gpointer *) &timeline);

  clutter_timeline_add_marker_at_time (timeline, "switch", 500);

  frame_counter = 0;

  g_signal_connect (timeline, "marker-reached::switch",
                    G_CALLBACK (on_switch_reached),
                    frame_clock2);
  g_signal_connect (timeline, "new-frame",
                    G_CALLBACK (on_timeline_new_frame),
                    &frame_counter);
  g_signal_connect (timeline, "completed",
                    G_CALLBACK (on_timeline_completed),
                    main_loop);

  clutter_timeline_start (timeline);

  before_us = g_get_monotonic_time ();

  g_main_loop_run (main_loop);

  after_us = g_get_monotonic_time ();
  duration_us = after_us - before_us;
  lateness_us = duration_us - ms2us (clutter_timeline_get_duration (timeline));

  /* Our threshold is two frames. This is because switching a timeline to a
   * new clock of the same frequency shifts the phase up to one frame. And
   * this timeline has seen two different clocks so its overall duration may
   * be out by almost two frames. But more than two frames is a bug.
   */
  g_assert_cmpint (lateness_us, >, -2 * refresh_interval_us);

  g_assert (clutter_timeline_get_frame_clock (timeline) == frame_clock2);

  /* The duration is 1s, with a 60hz clock, and we switch after 0.5s. To verify
   * we continued to get frames, check that we have a bit more than half of the
   * frames accounted for.
   */
  g_assert_cmpint (frame_counter, >, 35);

  g_main_loop_unref (main_loop);
  g_object_unref (timeline);
  g_assert_null (timeline);
  clutter_frame_clock_destroy (frame_clock1);
  g_assert_null (frame_clock1);
  clutter_frame_clock_destroy (frame_clock2);
  g_assert_null (frame_clock2);
}

CLUTTER_TEST_SUITE (
  CLUTTER_TEST_UNIT ("/frame-clock/timeline/basic", frame_clock_timeline_basic)
  CLUTTER_TEST_UNIT ("/frame-clock/timeline/switch", frame_clock_timeline_switch)
)
