#include <math.h>
#include <gtk/gtk.h>

#include "variable.h"

typedef struct {
  double angle;
  gint64 stream_time;
  gint64 clock_time;
  gint64 frame_counter;
} FrameData;

static FrameData *displayed_frame;
static GtkWidget *window;
static GList *past_frames;
static Variable latency_error = VARIABLE_INIT;
static Variable time_factor_stats = VARIABLE_INIT;
static int dropped_frames = 0;
static int n_frames = 0;

static gboolean pll;
static int fps = 24;

/* Thread-safe frame queue */

#define MAX_QUEUE_LENGTH 5

static GQueue *frame_queue;
static GMutex frame_mutex;
static GCond frame_cond;

static void
queue_frame (FrameData *frame_data)
{
  g_mutex_lock (&frame_mutex);

  while (frame_queue->length == MAX_QUEUE_LENGTH)
    g_cond_wait (&frame_cond, &frame_mutex);

  g_queue_push_tail (frame_queue, frame_data);

  g_mutex_unlock (&frame_mutex);
}

static FrameData *
unqueue_frame (void)
{
  FrameData *frame_data;

  g_mutex_lock (&frame_mutex);

  if (frame_queue->length > 0)
    {
      frame_data = g_queue_pop_head (frame_queue);
      g_cond_signal (&frame_cond);
    }
  else
    {
      frame_data = NULL;
    }

  g_mutex_unlock (&frame_mutex);

  return frame_data;
}

static FrameData *
peek_pending_frame (void)
{
  FrameData *frame_data;

  g_mutex_lock (&frame_mutex);

  if (frame_queue->head)
    frame_data = frame_queue->head->data;
  else
    frame_data = NULL;

  g_mutex_unlock (&frame_mutex);

  return frame_data;
}

static FrameData *
peek_next_frame (void)
{
  FrameData *frame_data;

  g_mutex_lock (&frame_mutex);

  if (frame_queue->head && frame_queue->head->next)
    frame_data = frame_queue->head->next->data;
  else
    frame_data = NULL;

  g_mutex_unlock (&frame_mutex);

  return frame_data;
}

/* Frame producer thread */

static gpointer
create_frames_thread (gpointer data)
{
  int frame_count = 0;

  while (TRUE)
    {
      FrameData *frame_data = g_slice_new0 (FrameData);
      frame_data->angle = 2 * M_PI * (frame_count % fps) / (double)fps;
      frame_data->stream_time = (G_GINT64_CONSTANT (1000000) * frame_count) / fps;

      queue_frame (frame_data);
      frame_count++;
    }

  return NULL;
}

/* Clock management:
 *
 * The logic here, which is activated by the --pll argument
 * demonstrates adjusting the playback rate so that the frames exactly match
 * when they are displayed both frequency and phase. If there was an
 * accompanying audio track, you would need to resample the audio to match
 * the clock.
 *
 * The algorithm isn't exactly a PLL - I wrote it first that way, but
 * it oscillicated before coming into sync and this approach was easier than
 * fine-tuning the PLL filter.
 *
 * A more complicated algorithm could also establish sync when the playback
 * rate isn't exactly an integral divisor of the VBlank rate, such as 24fps
 * video on a 60fps display.
 */
#define PRE_BUFFER_TIME 500000

static gint64 stream_time_base;
static gint64 clock_time_base;
static double time_factor = 1.0;
static double frequency_time_factor = 1.0;
static double phase_time_factor = 1.0;

static gint64
stream_time_to_clock_time (gint64 stream_time)
{
  return clock_time_base + (stream_time - stream_time_base) * time_factor;
}

static void
adjust_clock_for_phase (gint64 frame_clock_time,
                        gint64 presentation_time)
{
  static int count = 0;
  static gint64 previous_frame_clock_time;
  static gint64 previous_presentation_time;
  gint64 phase = presentation_time - frame_clock_time;

  count++;
  if (count >= fps) /* Give a second of warmup */
    {
      gint64 time_delta = frame_clock_time - previous_frame_clock_time;
      gint64 previous_phase = previous_presentation_time - previous_frame_clock_time;

      double expected_phase_delta;

      stream_time_base += (frame_clock_time - clock_time_base) / time_factor;
      clock_time_base = frame_clock_time;

      expected_phase_delta = time_delta * (1 - phase_time_factor);

      /* If the phase is increasing that means the computed clock times are
       * increasing too slowly. We increase the frequency time factor to compensate,
       * but decrease the compensation so that it takes effect over 1 second to
       * avoid jitter */
      frequency_time_factor += (phase - previous_phase - expected_phase_delta) / (double)time_delta / fps;

      /* We also want to increase or decrease the frequency to bring the phase
       * into sync. We do that again so that the phase should sync up over 1 seconds
       */
      phase_time_factor = 1 + phase / 2000000.;

      time_factor = frequency_time_factor * phase_time_factor;
    }

  previous_frame_clock_time = frame_clock_time;
  previous_presentation_time = presentation_time;
}

/* Drawing */

static void
on_draw (GtkDrawingArea *da,
         cairo_t        *cr,
         int             width,
         int             height,
         gpointer        data)
{
  double cx, cy, r;

  cairo_set_source_rgb (cr, 1., 1., 1.);
  cairo_paint (cr);

  cairo_set_source_rgb (cr, 0., 0., 0.);

  cx = width / 2.;
  cy = height / 2.;
  r = MIN (width, height) / 2.;

  cairo_arc (cr, cx, cy, r,
             0, 2 * M_PI);
  cairo_stroke (cr);
  if (displayed_frame)
    {
      cairo_move_to (cr, cx, cy);
      cairo_line_to (cr,
                     cx + r * cos(displayed_frame->angle - M_PI / 2),
                     cy + r * sin(displayed_frame->angle - M_PI / 2));
      cairo_stroke (cr);

      if (displayed_frame->frame_counter == 0)
        {
          GdkFrameClock *frame_clock = gtk_widget_get_frame_clock (window);
          displayed_frame->frame_counter = gdk_frame_clock_get_frame_counter (frame_clock);
        }
    }
}

static void
collect_old_frames (void)
{
  GdkFrameClock *frame_clock = gtk_widget_get_frame_clock (window);
  GList *l, *l_next;

  for (l = past_frames; l; l = l_next)
    {
      FrameData *frame_data = l->data;
      gboolean remove = FALSE;
      l_next = l->next;

      GdkFrameTimings *timings = gdk_frame_clock_get_timings (frame_clock,
                                                              frame_data->frame_counter);
      if (timings == NULL)
        {
          remove = TRUE;
        }
      else if (gdk_frame_timings_get_complete (timings))
        {
          gint64 presentation_time = gdk_frame_timings_get_predicted_presentation_time (timings);
          gint64 refresh_interval = gdk_frame_timings_get_refresh_interval (timings);

          if (pll &&
              presentation_time && refresh_interval &&
              presentation_time > frame_data->clock_time - refresh_interval / 2 &&
              presentation_time < frame_data->clock_time + refresh_interval / 2)
            adjust_clock_for_phase (frame_data->clock_time, presentation_time);

          if (presentation_time)
            variable_add (&latency_error,
                          presentation_time - frame_data->clock_time);

          remove = TRUE;
        }

      if (remove)
        {
          past_frames = g_list_delete_link (past_frames, l);
          g_slice_free (FrameData, frame_data);
        }
    }
}

static void
print_statistics (void)
{
  gint64 now = g_get_monotonic_time ();
  static gint64 last_print_time = 0;

  if (last_print_time == 0)
    last_print_time = now;
  else if (now -last_print_time > 5000000)
    {
      g_print ("dropped_frames: %d/%d\n",
               dropped_frames, n_frames);
      g_print ("collected_frames: %g/%d\n",
               latency_error.weight, n_frames);
      g_print ("latency_error: %g +/- %g\n",
               variable_mean (&latency_error),
               variable_standard_deviation (&latency_error));
      if (pll)
        g_print ("playback rate adjustment: %g +/- %g %%\n",
                 (variable_mean (&time_factor_stats) - 1) * 100,
                 variable_standard_deviation (&time_factor_stats) * 100);
      variable_init (&latency_error);
      variable_init (&time_factor_stats);
      dropped_frames = 0;
      n_frames = 0;
      last_print_time = now;
    }
}

static void
on_update (GdkFrameClock *frame_clock,
           gpointer       data)
{
  GdkFrameTimings *timings = gdk_frame_clock_get_current_timings (frame_clock);
  gint64 frame_time = gdk_frame_timings_get_frame_time (timings);
  gint64 predicted_presentation_time = gdk_frame_timings_get_predicted_presentation_time (timings);
  gint64 refresh_interval;
  FrameData *pending_frame;

  if (clock_time_base == 0)
    clock_time_base = frame_time + PRE_BUFFER_TIME;

  gdk_frame_clock_get_refresh_info (frame_clock, frame_time,
                                    &refresh_interval, NULL);

  pending_frame = peek_pending_frame ();
  g_assert (pending_frame);

  if (stream_time_to_clock_time (pending_frame->stream_time)
      < predicted_presentation_time + refresh_interval / 2)
    {
      while (TRUE)
        {
          FrameData *next_frame = peek_next_frame ();
          if (next_frame &&
              stream_time_to_clock_time (next_frame->stream_time)
              < predicted_presentation_time + refresh_interval / 2)
            {
              g_slice_free (FrameData, unqueue_frame ());
              n_frames++;
              dropped_frames++;
              pending_frame = next_frame;
            }
          else
            break;
        }

      if (displayed_frame)
        past_frames = g_list_prepend (past_frames, displayed_frame);

      n_frames++;
      displayed_frame = unqueue_frame ();
      g_assert (displayed_frame);
      displayed_frame->clock_time = stream_time_to_clock_time (displayed_frame->stream_time);

      displayed_frame->frame_counter = gdk_frame_timings_get_frame_counter (timings);
      variable_add (&time_factor_stats, time_factor);

      collect_old_frames ();
      print_statistics ();

      gtk_widget_queue_draw (window);
    }
}

static GOptionEntry options[] = {
  { "pll", 'p', 0, G_OPTION_ARG_NONE, &pll, "Sync frame rate to refresh", NULL },
  { "fps", 'f', 0, G_OPTION_ARG_INT, &fps, "Frame rate", "FPS" },
  { NULL }
};

static void
quit_cb (GtkWidget *widget,
         gpointer   data)
{
  gboolean *done = data;

  *done = TRUE;

  g_main_context_wakeup (NULL);
}

int
main(int argc, char **argv)
{
  GtkWidget *da;
  GError *error = NULL;
  GdkFrameClock *frame_clock;
  GOptionContext *context;
  gboolean done = FALSE;

  context = g_option_context_new ("");
  g_option_context_add_main_entries (context, options, NULL);

  if (!g_option_context_parse (context, &argc, &argv, &error))
    {
      g_printerr ("Option parsing failed: %s\n", error->message);
      return 1;
    }

  g_option_context_free (context);

  gtk_init ();

  window = gtk_window_new ();
  gtk_window_set_default_size (GTK_WINDOW (window), 300, 300);
  g_signal_connect (window, "destroy",
                    G_CALLBACK (quit_cb), &done);

  da = gtk_drawing_area_new ();
  gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (da), on_draw, NULL, NULL);
  gtk_window_set_child (GTK_WINDOW (window), da);

  gtk_widget_show (window);

  frame_queue = g_queue_new ();
  g_mutex_init (&frame_mutex);
  g_cond_init (&frame_cond);

  g_thread_new ("Create Frames", create_frames_thread, NULL);

  frame_clock = gtk_widget_get_frame_clock (window);
  g_signal_connect (frame_clock, "update",
                    G_CALLBACK (on_update), NULL);
  gdk_frame_clock_begin_updating (frame_clock);

  while (!done)
    g_main_context_iteration (NULL, TRUE);

  return 0;
}
