#include <stddef.h>
#include <stdlib.h>
#include <string.h>

#include <boolean.h>

#include "dirty_input.h"
#include "mylist.h"
#include "secondary_core.h"
#include "run_ahead.h"

#include "../core.h"
#include "../dynamic.h"
#include "../audio/audio_driver.h"
#include "../gfx/video_driver.h"

static bool runahead_create(void);
static bool runahead_save_state(void);
static bool runahead_load_state(void);
static bool runahead_load_state_secondary(void);
static bool runahead_run_secondary(void);
static void runahead_suspend_audio(void);
static void runahead_resume_audio(void);
static void runahead_suspend_video(void);
static void runahead_resume_video(void);
static void set_fast_savestate(void);
static void unset_fast_savestate(void);
static void set_hard_disable_audio(void);
static void unset_hard_disable_audio(void);

/* TODO/FIXME - shouldn't this be signed size_t? */
static size_t runahead_save_state_size = -1;

/* Save State List for Run Ahead */
static MyList *runahead_save_state_list;

static void *runahead_save_state_alloc(void)
{
   retro_ctx_serialize_info_t *savestate = (retro_ctx_serialize_info_t*)
      malloc(sizeof(retro_ctx_serialize_info_t));

   if (!savestate)
      return NULL;

   savestate->data          = NULL;
   savestate->data_const    = NULL;
   savestate->size          = 0;

   if (runahead_save_state_size > 0 && runahead_save_state_size != -1)
   {
      savestate->data       = malloc(runahead_save_state_size);
      savestate->data_const = savestate->data;
      savestate->size       = runahead_save_state_size;
   }

   return savestate;
}

static void runahead_save_state_free(void *state)
{
   retro_ctx_serialize_info_t *savestate = (retro_ctx_serialize_info_t*)state;
   if (!savestate)
      return;
   free(savestate->data);
   free(savestate);
}

static void runahead_save_state_list_init(size_t saveStateSize)
{
   runahead_save_state_size = saveStateSize;
   mylist_create(&runahead_save_state_list, 16,
         runahead_save_state_alloc, runahead_save_state_free);
}

static void runahead_save_state_list_destroy(void)
{
   mylist_destroy(&runahead_save_state_list);
}

#if 0
static void runahead_save_state_list_rotate(void)
{
   int i;
   void *element;
   void *firstElement;
   firstElement = runahead_save_state_list->data[0];
   for (i = 1; i < runahead_save_state_list->size; i++)
      runahead_save_state_list->data[i - 1] =
         runahead_save_state_list->data[i];
   runahead_save_state_list->data[runahead_save_state_list->size - 1] =
      firstElement;
}
#endif

/* Hooks - Hooks to cleanup, and add dirty input hooks */

static function_t originalRetroDeinit = NULL;
static function_t originalRetroUnload = NULL;

extern struct retro_core_t current_core;
extern struct retro_callbacks retro_ctx;

static void remove_hooks(void)
{
   if (originalRetroDeinit)
   {
      current_core.retro_deinit = originalRetroDeinit;
      originalRetroDeinit       = NULL;
   }

   if (originalRetroUnload)
   {
      current_core.retro_unload_game = originalRetroUnload;
      originalRetroUnload            = NULL;
   }
   current_core.retro_set_environment(rarch_environment_cb);
   remove_input_state_hook();
}

static void unload_hook(void)
{
   remove_hooks();
   runahead_destroy();
   secondary_core_destroy();
   if (current_core.retro_unload_game)
      current_core.retro_unload_game();
}


static void deinit_hook(void)
{
   remove_hooks();
   runahead_destroy();
   secondary_core_destroy();
   if (current_core.retro_deinit)
      current_core.retro_deinit();
}

static bool env_hook(unsigned cmd, void *data)
{
   bool result = rarch_environment_cb(cmd, data);
   if (cmd == RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE && result)
   {
      bool *bool_p = (bool*)data;
      if (*bool_p == true)
         secondary_core_set_variable_update();
   }
   return result;
}

static void add_hooks(void)
{
   if (!originalRetroDeinit)
   {
      originalRetroDeinit       = current_core.retro_deinit;
      current_core.retro_deinit = deinit_hook;
   }

   if (!originalRetroUnload)
   {
      originalRetroUnload = current_core.retro_unload_game;
      current_core.retro_unload_game = unload_hook;
   }
   current_core.retro_set_environment(env_hook);
   add_input_state_hook();
}


/* Runahead Code */

static bool runahead_video_driver_is_active   = true;
static bool runahead_available                = true;
static bool runahead_secondary_core_available = true;
static bool runahead_force_input_dirty        = true;
static uint64_t runahead_last_frame_count     = 0;

static void runahead_clear_variables(void)
{
   runahead_save_state_size          = -1;
   runahead_video_driver_is_active   = true;
   runahead_available                = true;
   runahead_secondary_core_available = true;
   runahead_force_input_dirty        = true;
   runahead_last_frame_count         = 0;
}

static void runahead_check_for_gui(void)
{
   /* Hack: If we were in the GUI, force a resync. */
   bool is_alive, is_focused = false;
   uint64_t frame_count      = 0;

   video_driver_get_status(&frame_count, &is_alive, &is_focused);

   if (frame_count != runahead_last_frame_count + 1)
      runahead_force_input_dirty = true;

   runahead_last_frame_count = frame_count;
}

void run_ahead(int runahead_count, bool useSecondary)
{
   int frame_number        = 0;
   bool last_frame         = false;
   bool suspended_frame    = false;
#if defined(HAVE_DYNAMIC) || defined(HAVE_DYLIB)
   const bool have_dynamic = true;
#else
   const bool have_dynamic = false;
#endif

   if (runahead_count <= 0 || !runahead_available)
   {
      core_run();
      runahead_force_input_dirty = true;
      return;
   }

   if (runahead_save_state_size == -1)
   {
      if (!runahead_create())
      {
         /* RunAhead has been disabled because the core 
          * does not support savestates. */
         core_run();
         runahead_force_input_dirty = true;
         return;
      }
   }

   runahead_check_for_gui();

   if (!useSecondary || !have_dynamic || !runahead_secondary_core_available)
   {
      /* TODO: multiple savestates for higher performance 
       * when not using secondary core */
      for (frame_number = 0; frame_number <= runahead_count; frame_number++)
      {
         last_frame      = frame_number == runahead_count;
         suspended_frame = !last_frame;

         if (suspended_frame)
         {
            runahead_suspend_audio();
            runahead_suspend_video();
         }

         if (frame_number == 0)
            core_run();
         else
            core_run_no_input_polling();

         if (suspended_frame)
         {
            runahead_resume_video();
            runahead_resume_audio();
         }

         if (frame_number == 0)
         {
            /* RunAhead has been disabled due 
             * to save state failure */
            if (!runahead_save_state())
               return;
         }

         if (last_frame)
         {
            /* RunAhead has been disabled due 
             * to load state failure */
            if (!runahead_load_state())
               return;
         }
      }
   }
   else
   {
#if HAVE_DYNAMIC
      bool okay = false;

      /* run main core with video suspended */
      runahead_suspend_video();
      core_run();
      runahead_resume_video();

      if (input_is_dirty || runahead_force_input_dirty)
      {
         unsigned frame_count;

         input_is_dirty       = false;

         if (!runahead_save_state())
            return;

         /* Could not create a secondary core.
          * RunAhead wll only use the main core now. */
         if (!runahead_load_state_secondary())
            return;

         for (frame_count = 0; frame_count < 
               (unsigned)(runahead_count - 1); frame_count++)
         {
            runahead_suspend_video();
            runahead_suspend_audio();
            set_hard_disable_audio();
            okay = runahead_run_secondary();
            unset_hard_disable_audio();
            runahead_resume_audio();
            runahead_resume_video();

            /* Could not create a secondary core. RunAhead
             * will only use the main core now. */
            if (!okay)
               return;
         }
      }
      runahead_suspend_audio();
      set_hard_disable_audio();
      okay = runahead_run_secondary();
      unset_hard_disable_audio();
      runahead_resume_audio();

      /* Could not create a secondary core. RunAhead
       * will only use the main core now. */
      if (!okay)
         return;
#endif
   }
   runahead_force_input_dirty = false;
}

static void runahead_error(void)
{
   runahead_available = false;
   runahead_save_state_list_destroy();
   remove_hooks();
   runahead_save_state_size = 0;
}

static bool runahead_create(void)
{
   /* get savestate size and allocate buffer */
   retro_ctx_size_info_t info;
   core_serialize_size(&info);

   runahead_save_state_list_init(info.size);
   runahead_video_driver_is_active = video_driver_is_active();

   if (runahead_save_state_size == 0 || runahead_save_state_size == -1)
   {
      runahead_error();
      return false;
   }

   add_hooks();
   runahead_force_input_dirty = true;
   mylist_resize(runahead_save_state_list, 1, true);
   return true;
}

static bool runahead_save_state(void)
{
   bool okay                                  = false;
   retro_ctx_serialize_info_t *serialize_info;
   if (!runahead_save_state_list)
      return false;
   serialize_info =
      (retro_ctx_serialize_info_t*)runahead_save_state_list->data[0];
   set_fast_savestate();
   okay = core_serialize(serialize_info);
   unset_fast_savestate();
   if (!okay)
   {
      runahead_error();
      return false;
   }
   return true;
}

static bool runahead_load_state(void)
{
   bool okay                                  = false;
   retro_ctx_serialize_info_t *serialize_info = (retro_ctx_serialize_info_t*)
      runahead_save_state_list->data[0];
   bool last_dirty                            = input_is_dirty;

   set_fast_savestate();
   /* calling core_unserialize has side effects with 
    * netplay (it triggers transmitting your save state)
      call retro_unserialize directly from the core instead */
   okay = current_core.retro_unserialize(
         serialize_info->data_const, serialize_info->size);
   unset_fast_savestate();
   input_is_dirty = last_dirty;

   if (!okay)
      runahead_error();

   return okay;
}

static bool runahead_load_state_secondary(void)
{
   bool okay                                  = false;
   retro_ctx_serialize_info_t *serialize_info =
      (retro_ctx_serialize_info_t*)runahead_save_state_list->data[0];

   set_fast_savestate();
   okay = secondary_core_deserialize(
         serialize_info->data_const, (int)serialize_info->size);
   unset_fast_savestate();

   if (!okay)
   {
      runahead_secondary_core_available = false;
      return false;
   }

   return true;
}

static bool runahead_run_secondary(void)
{
   if (!secondary_core_run_no_input_polling())
   {
      runahead_secondary_core_available = false;
      return false;
   }
   return true;
}

static void runahead_suspend_audio(void)
{
   audio_driver_suspend();
}

static void runahead_resume_audio(void)
{
   audio_driver_resume();
}

static void runahead_suspend_video(void)
{
   video_driver_unset_active();
}

static void runahead_resume_video(void)
{
   if (runahead_video_driver_is_active)
      video_driver_set_active();
   else
      video_driver_unset_active();
}

void runahead_destroy(void)
{
   runahead_save_state_list_destroy();
   remove_hooks();
   runahead_clear_variables();
}

static bool request_fast_savestate;
static bool hard_disable_audio;


bool want_fast_savestate(void)
{
   return request_fast_savestate;
}

static void set_fast_savestate(void)
{
   request_fast_savestate = true;
}

static void unset_fast_savestate(void)
{
   request_fast_savestate = false;
}

bool get_hard_disable_audio(void)
{
   return hard_disable_audio;
}

static void set_hard_disable_audio(void)
{
   hard_disable_audio = true;
}

static void unset_hard_disable_audio(void)
{
   hard_disable_audio = false;
}
