/* egg-frame-source.c
 *
 * Copyright (C) 2010-2015 Christian Hergert <christian@hergert.me>
 *
 * 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 3 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 "egg-frame-source.h"

typedef struct
{
   GSource parent;
   guint   fps;
   guint   frame_count;
   gint64  start_time;
} EggFrameSource;

static gboolean
egg_frame_source_prepare (GSource *source,
                          gint    *timeout_)
{
   EggFrameSource *fsource = (EggFrameSource *)(gpointer)source;
   gint64 current_time;
   guint elapsed_time;
   guint new_frame_num;
   guint frame_time;

   current_time = g_source_get_time(source) / 1000;
   elapsed_time = current_time - fsource->start_time;
   new_frame_num = elapsed_time * fsource->fps / 1000;

   /* If time has gone backwards or the time since the last frame is
    * greater than the two frames worth then reset the time and do a
    * frame now */
   if (new_frame_num < fsource->frame_count ||
       new_frame_num - fsource->frame_count > 2) {
      /* Get the frame time rounded up to the nearest ms */
      frame_time = (1000 + fsource->fps - 1) / fsource->fps;

      /* Reset the start time */
      fsource->start_time = current_time;

      /* Move the start time as if one whole frame has elapsed */
      fsource->start_time -= frame_time;
      fsource->frame_count = 0;
      *timeout_ = 0;
      return TRUE;
   } else if (new_frame_num > fsource->frame_count) {
      *timeout_ = 0;
      return TRUE;
   } else {
      *timeout_ = (fsource->frame_count + 1) * 1000 / fsource->fps - elapsed_time;
      return FALSE;
   }
}

static gboolean
egg_frame_source_check (GSource *source)
{
   gint timeout_;
   return egg_frame_source_prepare(source, &timeout_);
}

static gboolean
egg_frame_source_dispatch (GSource     *source,
                           GSourceFunc  source_func,
                           gpointer     user_data)
{
   EggFrameSource *fsource = (EggFrameSource *)(gpointer)source;
   gboolean ret;

   if ((ret = source_func(user_data)))
      fsource->frame_count++;
   return ret;
}

static GSourceFuncs source_funcs = {
   egg_frame_source_prepare,
   egg_frame_source_check,
   egg_frame_source_dispatch,
};

/**
 * egg_frame_source_add:
 * @frames_per_sec: (in): Target frames per second.
 * @callback: (in) (scope notified): A #GSourceFunc to execute.
 * @user_data: (in): User data for @callback.
 *
 * Creates a new frame source that will execute when the timeout interval
 * for the source has elapsed. The timing will try to synchronize based
 * on the end time of the animation.
 *
 * Returns: A source id that can be removed with g_source_remove().
 */
guint
egg_frame_source_add (guint       frames_per_sec,
                      GSourceFunc callback,
                      gpointer    user_data)
{
   EggFrameSource *fsource;
   GSource *source;
   guint ret;

   g_return_val_if_fail (frames_per_sec > 0, 0);
   g_return_val_if_fail (frames_per_sec <= 120, 0);

   source = g_source_new(&source_funcs, sizeof(EggFrameSource));
   fsource = (EggFrameSource *)(gpointer)source;
   fsource->fps = frames_per_sec;
   fsource->frame_count = 0;
   fsource->start_time = g_get_monotonic_time() / 1000;
   g_source_set_callback(source, callback, user_data, NULL);
   g_source_set_name(source, "EggFrameSource");

   ret = g_source_attach(source, NULL);
   g_source_unref(source);

   return ret;
}
