/*  RetroArch - A frontend for libretro.
 *  Copyright (C) 2014-2017 - Jean-André Santoni
 *  Copyright (C) 2015-2018 - Andre Leiradella
 *  Copyright (C) 2018-2020 - natinusala
 *  Copyright (C) 2019-2020 - James Leaver
 *
 *  RetroArch 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 Found-
 *  ation, either version 3 of the License, or (at your option) any later version.
 *
 *  RetroArch 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 RetroArch.
 *  If not, see <http://www.gnu.org/licenses/>.
 */

#include <string/stdstring.h>
#include <file/file_path.h>

#include "../gfx_widgets.h"
#include "../gfx_animation.h"
#include "../gfx_display.h"
#include "../../retroarch.h"
#include "../../core_info.h"
#include "../../playlist.h"
#include "../../paths.h"

#ifdef HAVE_MENU
#include "../../menu/menu_driver.h"
#endif

#define LOAD_CONTENT_ANIMATION_FADE_IN_DURATION   466.0f
#define LOAD_CONTENT_ANIMATION_SLIDE_DURATION     666.0f
#define LOAD_CONTENT_ANIMATION_WAIT_DURATION      1000.0f
#define LOAD_CONTENT_ANIMATION_FADE_OUT_DURATION  433.0f

/* Widget state */

enum gfx_widget_load_content_animation_status
{
   GFX_WIDGET_LOAD_CONTENT_IDLE = 0,
   GFX_WIDGET_LOAD_CONTENT_BEGIN,
   GFX_WIDGET_LOAD_CONTENT_FADE_IN,
   GFX_WIDGET_LOAD_CONTENT_SLIDE,
   GFX_WIDGET_LOAD_CONTENT_WAIT,
   GFX_WIDGET_LOAD_CONTENT_FADE_OUT
};

struct gfx_widget_load_content_animation_state
{
   uintptr_t icon_texture;
   unsigned bg_shadow_height;
   unsigned margin_shadow_width;
   unsigned icon_size;
   unsigned content_name_color;
   unsigned system_name_color;
   unsigned content_name_width;
   unsigned system_name_width;

   unsigned bg_width;
   unsigned bg_height;

   gfx_timer_t timer;      /* float alignment */
   float bg_x;
   float bg_y;
   float alpha;
   float slide_offset;
   float bg_shadow_top_y;
   float bg_shadow_bottom_y;
   float margin_shadow_left_x;
   float margin_shadow_right_x;
   float icon_x_start;
   float icon_x_end;
   float icon_y;

   float text_x_start;
   float text_x_end;
   float content_name_y;
   float system_name_y;

   float bg_alpha;
   float bg_underlay_alpha;
   float bg_shadow_alpha;

   float bg_color[16];
   float bg_underlay_color[16];
   float bg_shadow_top_color[16];
   float bg_shadow_bottom_color[16];
   float margin_shadow_left_color[16];
   float margin_shadow_right_color[16];
   float icon_color[16];

   enum gfx_widget_load_content_animation_status status;

   char content_name[512];
   char system_name[512];
   char icon_directory[PATH_MAX_LENGTH];
   char icon_file[PATH_MAX_LENGTH];

   bool has_icon;
};

typedef struct gfx_widget_load_content_animation_state gfx_widget_load_content_animation_state_t;

static gfx_widget_load_content_animation_state_t p_w_load_content_animation_st = {

   0,                                  /* icon_texture */
   0,                                  /* bg_shadow_height */
   0,                                  /* margin_shadow_width */
   0,                                  /* icon_size */
   0xE0E0E0FF,                         /* content_name_color */
   0xCFCFCFFF,                         /* system_name_color */
   0,                                  /* content_name_width */
   0,                                  /* system_name_width */

   0,                                  /* bg_width */
   0,                                  /* bg_height */

   0.0f,                               /* timer */
   0.0f,                               /* bg_x */
   0.0f,                               /* bg_y */
   0.0f,                               /* alpha */
   0.0f,                               /* slide_offset */
   0.0f,                               /* bg_shadow_top_y */
   0.0f,                               /* bg_shadow_bottom_y */
   0.0f,                               /* margin_shadow_left_x */
   0.0f,                               /* margin_shadow_right_x */
   0.0f,                               /* icon_x_start */
   0.0f,                               /* icon_x_end */
   0.0f,                               /* icon_y */

   0.0f,                               /* text_x_start */
   0.0f,                               /* text_x_end */
   0.0f,                               /* content_name_y */
   0.0f,                               /* system_name_y */

   0.95f,                              /* bg_alpha */
   0.5f,                               /* bg_underlay_alpha */
   0.4f,                               /* bg_shadow_alpha */

   COLOR_HEX_TO_FLOAT(0x000000, 1.0f), /* bg_color */
   COLOR_HEX_TO_FLOAT(0x505050, 1.0f), /* bg_underlay_color */
   COLOR_HEX_TO_FLOAT(0x000000, 0.0f), /* bg_shadow_top_color */
   COLOR_HEX_TO_FLOAT(0x000000, 0.0f), /* bg_shadow_bottom_color */
   COLOR_HEX_TO_FLOAT(0x000000, 0.0f), /* margin_shadow_left_color */
   COLOR_HEX_TO_FLOAT(0x000000, 0.0f), /* margin_shadow_right_color */
   COLOR_HEX_TO_FLOAT(0xE0E0E0, 1.0f), /* icon_color */

   GFX_WIDGET_LOAD_CONTENT_IDLE,       /* status */

   {'\0'},                             /* content_name */
   {'\0'},                             /* system_name */
   {'\0'},                             /* icon_directory */
   {'\0'},                             /* icon_file */

   false                               /* has_icon */
};

gfx_widget_load_content_animation_state_t *gfx_widget_load_content_animation_get_ptr(void)
{
   return &p_w_load_content_animation_st;
}

/* Utilities */

static void gfx_widget_load_content_animation_reset(void)
{
   gfx_widget_load_content_animation_state_t *state = gfx_widget_load_content_animation_get_ptr();
   uintptr_t alpha_tag                              = (uintptr_t)&state->alpha;
   uintptr_t slide_offset_tag                       = (uintptr_t)&state->slide_offset;

   /* Kill any existing timers/animations */
   gfx_timer_kill(&state->timer);
   gfx_animation_kill_by_tag(&alpha_tag);
   gfx_animation_kill_by_tag(&slide_offset_tag);

   /* Reset pertinent state parameters */
   state->status             = GFX_WIDGET_LOAD_CONTENT_IDLE;
   state->alpha              = 0.0f;
   state->slide_offset       = 0.0f;
   state->content_name[0]    = '\0';
   state->system_name[0]     = '\0';
   state->icon_file[0]       = '\0';
   state->has_icon           = false;
   state->content_name_width = 0;
   state->system_name_width  = 0;

   /* Unload any icon texture */
   if (state->icon_texture)
   {
      video_driver_texture_unload(&state->icon_texture);
      state->icon_texture = 0;
   }
}

static void gfx_widget_load_content_animation_load_icon(void)
{
   gfx_widget_load_content_animation_state_t *state = gfx_widget_load_content_animation_get_ptr();

   /* In all cases, unload any existing icon texture */
   if (state->icon_texture)
   {
      video_driver_texture_unload(&state->icon_texture);
      state->icon_texture = 0;
   }

   /* If widget has a valid icon set, load it */
   if (state->has_icon)
      gfx_display_reset_textures_list(
            state->icon_file, state->icon_directory,
            &state->icon_texture,
            TEXTURE_FILTER_MIPMAP_LINEAR, NULL, NULL);
}

/* Callbacks */

static void gfx_widget_load_content_animation_fade_out_cb(void *userdata)
{
   /* Animation is complete - reset widget */
   gfx_widget_load_content_animation_reset();
}

static void gfx_widget_load_content_animation_wait_cb(void *userdata)
{
   gfx_widget_load_content_animation_state_t *state = (gfx_widget_load_content_animation_state_t*)userdata;
   uintptr_t alpha_tag                              = (uintptr_t)&state->alpha;
   gfx_animation_ctx_entry_t animation_entry;

   /* Trigger fade out animation */
   state->alpha                 = 1.0f;

   animation_entry.easing_enum  = EASING_OUT_QUAD;
   animation_entry.tag          = alpha_tag;
   animation_entry.duration     = LOAD_CONTENT_ANIMATION_FADE_OUT_DURATION;
   animation_entry.target_value = 0.0f;
   animation_entry.subject      = &state->alpha;
   animation_entry.cb           = gfx_widget_load_content_animation_fade_out_cb;
   animation_entry.userdata     = NULL;

   gfx_animation_push(&animation_entry);
   state->status = GFX_WIDGET_LOAD_CONTENT_FADE_OUT;
}

static void gfx_widget_load_content_animation_slide_cb(void *userdata)
{
   gfx_widget_load_content_animation_state_t *state = (gfx_widget_load_content_animation_state_t*)userdata;
   gfx_timer_ctx_entry_t timer;

   /* Start wait timer */
   timer.duration = LOAD_CONTENT_ANIMATION_WAIT_DURATION;
   timer.cb       = gfx_widget_load_content_animation_wait_cb;
   timer.userdata = state;

   gfx_timer_start(&state->timer, &timer);
   state->status = GFX_WIDGET_LOAD_CONTENT_WAIT;
}

static void gfx_widget_load_content_animation_fade_in_cb(void *userdata)
{
   gfx_widget_load_content_animation_state_t *state = (gfx_widget_load_content_animation_state_t*)userdata;
   uintptr_t slide_offset_tag                       = (uintptr_t)&state->slide_offset;
   gfx_animation_ctx_entry_t animation_entry;

   /* Trigger slide animation */
   state->slide_offset          = 0.0f;

   animation_entry.easing_enum  = EASING_IN_OUT_QUAD;
   animation_entry.tag          = slide_offset_tag;
   animation_entry.duration     = LOAD_CONTENT_ANIMATION_SLIDE_DURATION;
   animation_entry.target_value = 1.0f;
   animation_entry.subject      = &state->slide_offset;
   animation_entry.cb           = gfx_widget_load_content_animation_slide_cb;
   animation_entry.userdata     = state;

   gfx_animation_push(&animation_entry);
   state->status = GFX_WIDGET_LOAD_CONTENT_SLIDE;
}

/* Widget interface */

bool gfx_widget_start_load_content_animation(void)
{
   gfx_widget_load_content_animation_state_t *state = gfx_widget_load_content_animation_get_ptr();

   const char *content_path                         = path_get(RARCH_PATH_CONTENT);
   const char *core_path                            = path_get(RARCH_PATH_CORE);
   playlist_t *playlist                             = playlist_get_cached();

   bool playlist_entry_found                        = false;
   bool has_content                                 = false;
   bool has_system                                  = false;
   bool has_db_name                                 = false;

   core_info_ctx_find_t core_info_finder;
   char icon_path[PATH_MAX_LENGTH];

   icon_path[0] = '\0';

   /* To ensure we leave the widget in a well defined
    * state, perform a reset before parsing variables */
   gfx_widget_load_content_animation_reset();

   /* Sanity check - we require both content and
    * core path
    * > Note that we would prefer to enable the load
    *   content animation for 'content-less' cores as
    *   well, but allowing no content would mean we
    *   trigger a false positive every time the dummy
    *   core is started (this higher level behaviour is
    *   deeply ingrained in RetroArch, and too difficult
    *   to change...) */
   if (string_is_empty(content_path) ||
       string_is_empty(core_path) ||
       string_is_equal(core_path, "builtin"))
      return false;

   /* Check core validity */
   core_info_finder.inf  = NULL;
   core_info_finder.path = core_path;

   if (!core_info_find(&core_info_finder))
      return false;

   core_path = core_info_finder.inf->path;

   /* Parse content path
    * > If we have a cached playlist, attempt to find
    *   the entry label for the current content */
   if (playlist)
   {
      const struct playlist_entry *entry = NULL;
#ifdef HAVE_MENU
      menu_handle_t *menu                = menu_driver_get_ptr();

      /* If we have an active menu, playlist entry
       * index can be obtained directly */
      if (menu)
      {
         if (playlist_index_is_valid(
               playlist, menu->rpl_entry_selection_ptr,
               content_path, core_path))
            playlist_get_index(
                  playlist, menu->rpl_entry_selection_ptr,
                  &entry);
      }
      else
#endif
      {
         /* No menu - have to search playlist... */
         playlist_get_index_by_path(playlist, content_path,
               &entry);

         if (entry &&
             !string_is_empty(entry->core_path))
         {
            const char *entry_core_file = path_basename(entry->core_path);

            /* Check whether core matches... */
            if (string_is_empty(entry_core_file) ||
                !string_starts_with_size(
                     entry_core_file,
                     core_info_finder.inf->core_file_id.str,
                     core_info_finder.inf->core_file_id.len))
               entry = NULL;
         }
      }

      /* If playlist entry is valid, extract all
       * available information */
      if (entry)
      {
         playlist_entry_found = true;

         /* Get entry label */
         if (!string_is_empty(entry->label))
         {
            strlcpy(state->content_name, entry->label,
                  sizeof(state->content_name));
            has_content = true;
         }

         /* Get entry db_name, */
         if (!string_is_empty(entry->db_name))
         {
            strlcpy(state->system_name, entry->db_name,
                  sizeof(state->system_name));
            path_remove_extension(state->system_name);

            has_system  = true;
            has_db_name = true;
         }
      }

      /* If content was found in playlist but the entry
       * did not have a db_name, use playlist name itself
       * as the system name */
      if (playlist_entry_found && !has_system)
      {
         const char *playlist_path = playlist_get_conf_path(playlist);

         if (!string_is_empty(playlist_path))
         {
            fill_pathname_base_noext(state->system_name, playlist_path,
                  sizeof(state->system_name));

            /* Exclude history and favourites playlists */
            if (string_ends_with_size(state->system_name, "_history",
                     strlen(state->system_name), STRLEN_CONST("_history")) ||
                string_ends_with_size(state->system_name, "_favorites",
                     strlen(state->system_name), STRLEN_CONST("_favorites")))
               state->system_name[0] = '\0';

            /* Check whether a valid system name was found */
            if (!string_is_empty(state->system_name))
            {
               has_system  = true;
               has_db_name = true;
            }
         }
      }
   }

   /* If we haven't yet set the content name,
    * use content file name as a fallback */
   if (!has_content)
      fill_pathname_base_noext(state->content_name, content_path,
            sizeof(state->content_name));

   /* Check whether system name has been set */
   if (!has_system)
   {
      /* Use core display name, if available */
      if (!string_is_empty(core_info_finder.inf->display_name))
         strlcpy(state->system_name, core_info_finder.inf->display_name,
               sizeof(state->system_name));
      /* Otherwise, just use 'RetroArch' as a fallback */
      else
         strlcpy(state->system_name, "RetroArch",
               sizeof(state->system_name));
   }

   /* > Content name has been determined
    * > System name has been determined
    * All that remains is the icon */

   /* Get icon filename
    * > Use db_name, if available */
   if (has_db_name)
   {
      strlcpy(state->icon_file, state->system_name,
            sizeof(state->icon_file));
      strlcat(state->icon_file, ".png",
            sizeof(state->icon_file));

      fill_pathname_join(icon_path,
            state->icon_directory, state->icon_file,
            sizeof(icon_path));

      state->has_icon = path_is_valid(icon_path);
   }

   /* > If db_name is unavailable (or was extracted
    *   from a playlist with non-standard naming),
    *   try to extract a valid system from the core
    *   itself */
   if (!state->has_icon)
   {
      const char *core_db_name           = NULL;
      struct string_list *databases_list =
            core_info_finder.inf->databases_list;

      /* We can only use the core db_name if the
       * core is associated with exactly one database */
      if (databases_list &&
          (databases_list->size == 1))
         core_db_name = databases_list->elems[0].data;

      if (!string_is_empty(core_db_name) &&
          !string_is_equal(core_db_name, state->system_name))
      {
         state->icon_file[0] = '\0';
         icon_path[0]        = '\0';

         strlcpy(state->icon_file, core_db_name,
               sizeof(state->icon_file));
         strlcat(state->icon_file, ".png",
               sizeof(state->icon_file));

         fill_pathname_join(icon_path,
               state->icon_directory, state->icon_file,
               sizeof(icon_path));

         state->has_icon = path_is_valid(icon_path);
      }
   }

   /* > If no system-specific icon is available,
    *   use default 'retroarch' icon as a fallback */
   if (!state->has_icon)
   {
      state->icon_file[0] = '\0';
      icon_path[0]        = '\0';

      strlcpy(state->icon_file, "retroarch.png",
            sizeof(state->icon_file));

      fill_pathname_join(icon_path,
            state->icon_directory, state->icon_file,
            sizeof(icon_path));

      state->has_icon = path_is_valid(icon_path);
   }

   /* All parameters are initialised
    * > Signal that animation should begin */
   state->status = GFX_WIDGET_LOAD_CONTENT_BEGIN;

   return true;
}

/* Widget layout() */

static void gfx_widget_load_content_animation_layout(
      void *data,
      bool is_threaded, const char *dir_assets, char *font_path)
{
   dispgfx_widget_t *p_dispwidget                   = (dispgfx_widget_t*)data;
   gfx_widget_load_content_animation_state_t *state = gfx_widget_load_content_animation_get_ptr();

   unsigned last_video_width                        = gfx_widgets_get_last_video_width(p_dispwidget);
   unsigned last_video_height                       = gfx_widgets_get_last_video_height(p_dispwidget);
   unsigned widget_padding                          = gfx_widgets_get_padding(p_dispwidget);

   gfx_widget_font_data_t *font_regular             = gfx_widgets_get_font_regular(p_dispwidget);
   gfx_widget_font_data_t *font_bold                = gfx_widgets_get_font_bold(p_dispwidget);

   /* Icon layout */
   state->icon_size = (unsigned)((((float)font_regular->line_height +
         (float)font_bold->line_height) * 1.6f) + 0.5f);
   state->icon_x_start = (float)(last_video_width  - state->icon_size) * 0.5f;
   state->icon_y       = (float)(last_video_height - state->icon_size) * 0.5f;
   /* > Note: cannot determine state->icon_x_end
    *   until text strings are set */

   /* Background layout */ 
   state->bg_width  = last_video_width;
   state->bg_height = state->icon_size + (widget_padding * 2);
   state->bg_x      = 0.0f;
   state->bg_y      = (float)(last_video_height - state->bg_height) * 0.5f;

   /* Background shadow layout */
   state->bg_shadow_height   = (unsigned)((float)state->bg_height * 0.3f);
   state->bg_shadow_top_y    = state->bg_y - (float)state->bg_shadow_height;
   state->bg_shadow_bottom_y = state->bg_y + (float)state->bg_height;

   /* Margin shadow layout */
   state->margin_shadow_width   = widget_padding;
   state->margin_shadow_left_x  = 0.0f;
   state->margin_shadow_right_x = (float)(last_video_width - widget_padding);

   /* Text layout */
   state->text_x_start   = state->icon_x_start +
         (float)(state->icon_size + widget_padding);
   state->content_name_y = state->icon_y +
         ((float)state->icon_size * 0.3f) +
         (float)font_bold->line_centre_offset;
   state->system_name_y  = state->icon_y +
         ((float)state->icon_size * 0.7f) +
         (float)font_regular->line_centre_offset;
   /* > Note: cannot determine state->text_x_end
    *   until text strings are set */
}

/* Widget iterate() */

static void gfx_widget_load_content_animation_iterate(void *user_data,
      unsigned width, unsigned height, bool fullscreen,
      const char *dir_assets, char *font_path,
      bool is_threaded)
{
   gfx_widget_load_content_animation_state_t *state = gfx_widget_load_content_animation_get_ptr();

   if (state->status == GFX_WIDGET_LOAD_CONTENT_BEGIN)
   {
      dispgfx_widget_t *p_dispwidget       = (dispgfx_widget_t*)user_data;

      unsigned last_video_width            = gfx_widgets_get_last_video_width(p_dispwidget);
      unsigned widget_padding              = gfx_widgets_get_padding(p_dispwidget);

      gfx_widget_font_data_t *font_regular = gfx_widgets_get_font_regular(p_dispwidget);
      gfx_widget_font_data_t *font_bold    = gfx_widgets_get_font_bold(p_dispwidget);

      uintptr_t alpha_tag                  = (uintptr_t)&state->alpha;

      int content_name_width;
      int system_name_width;
      int text_width;
      gfx_animation_ctx_entry_t animation_entry;

      /* Load icon texture */
      gfx_widget_load_content_animation_load_icon();

      /* Get overall text width */
      content_name_width = font_driver_get_message_width(
            font_bold->font, state->content_name,
            (unsigned)strlen(state->content_name), 1.0f);
      system_name_width = font_driver_get_message_width(
            font_regular->font, state->system_name,
            (unsigned)strlen(state->system_name), 1.0f);

      state->content_name_width = (content_name_width > 0) ?
            (unsigned)content_name_width : 0;
      state->system_name_width  = (system_name_width > 0) ?
            (unsigned)system_name_width : 0;

      text_width = (state->content_name_width > state->system_name_width) ?
            (int)state->content_name_width : (int)state->system_name_width;

      /* Now we have the text width, can determine
       * final icon/text x draw positions */
      state->icon_x_end = ((int)last_video_width - text_width -
            (int)state->icon_size - (3 * (int)widget_padding)) >> 1;
      state->icon_x_end = state->icon_x_end < (int)widget_padding ?
            widget_padding : state->icon_x_end;

      state->text_x_end = state->icon_x_end +
            (float)(state->icon_size + widget_padding);

      /* Trigger fade in animation */
      state->alpha                 = 0.0f;

      animation_entry.easing_enum  = EASING_OUT_QUAD;
      animation_entry.tag          = alpha_tag;
      animation_entry.duration     = LOAD_CONTENT_ANIMATION_FADE_IN_DURATION;
      animation_entry.target_value = 1.0f;
      animation_entry.subject      = &state->alpha;
      animation_entry.cb           = gfx_widget_load_content_animation_fade_in_cb;
      animation_entry.userdata     = state;

      gfx_animation_push(&animation_entry);
      state->status = GFX_WIDGET_LOAD_CONTENT_FADE_IN;
   }
}

/* Widget frame() */

static void gfx_widget_load_content_animation_frame(void *data, void *user_data)
{
   gfx_widget_load_content_animation_state_t *state = gfx_widget_load_content_animation_get_ptr();

   if (state->status != GFX_WIDGET_LOAD_CONTENT_IDLE)
   {
      video_frame_info_t *video_info       = (video_frame_info_t*)data;
      dispgfx_widget_t *p_dispwidget       = (dispgfx_widget_t*)user_data;

      unsigned video_width                 = video_info->width;
      unsigned video_height                = video_info->height;
      void *userdata                       = video_info->userdata;

      gfx_widget_font_data_t *font_regular = gfx_widgets_get_font_regular(p_dispwidget);
      gfx_widget_font_data_t *font_bold    = gfx_widgets_get_font_bold(p_dispwidget);
      size_t msg_queue_size                = gfx_widgets_get_msg_queue_size(p_dispwidget);

      float bg_alpha;
      float icon_alpha;
      float text_alpha;

      float icon_x;
      float text_x;

#ifdef HAVE_MENU
      /* Draw nothing if menu is currently active */
      if (menu_driver_is_alive())
         return;
#endif

      /* Determine status-dependent opacity/position
       * values */
      switch (state->status)
      {
         case GFX_WIDGET_LOAD_CONTENT_FADE_IN:
            bg_alpha   = 1.0f;
            icon_alpha = state->alpha;
            text_alpha = 0.0f;
            icon_x     = state->icon_x_start;
            text_x     = state->text_x_start;
            break;
         case GFX_WIDGET_LOAD_CONTENT_SLIDE:
            bg_alpha   = 1.0f;
            icon_alpha = 1.0f;
            /* Use 'slide_offset' as the alpha value
             * > Saves having to trigger two animations */
            text_alpha = state->slide_offset;
            icon_x     = state->icon_x_start + (state->slide_offset *
                  (state->icon_x_end - state->icon_x_start));
            text_x     = state->text_x_start + (state->slide_offset *
                  (state->text_x_end - state->text_x_start));
            break;
         case GFX_WIDGET_LOAD_CONTENT_WAIT:
            bg_alpha   = 1.0f;
            icon_alpha = 1.0f;
            text_alpha = 1.0f;
            icon_x     = state->icon_x_end;
            text_x     = state->text_x_end;
            break;
         case GFX_WIDGET_LOAD_CONTENT_FADE_OUT:
            bg_alpha   = state->alpha;
            icon_alpha = state->alpha;
            text_alpha = state->alpha;
            icon_x     = state->icon_x_end;
            text_x     = state->text_x_end;
            break;
         case GFX_WIDGET_LOAD_CONTENT_BEGIN:
         default:
            bg_alpha   = 1.0f;
            icon_alpha = 0.0f;
            text_alpha = 0.0f;
            icon_x     = state->icon_x_start;
            text_x     = state->text_x_start;
            break;
      }

      /* Draw background */
      if (bg_alpha > 0.0f)
      {
         /* > Set opacity */
         state->bg_shadow_top_color[3]     = bg_alpha * state->bg_shadow_alpha;
         state->bg_shadow_top_color[7]     = bg_alpha * state->bg_shadow_alpha;
         state->bg_shadow_bottom_color[11] = bg_alpha * state->bg_shadow_alpha;
         state->bg_shadow_bottom_color[15] = bg_alpha * state->bg_shadow_alpha;

         gfx_display_set_alpha(state->bg_color, bg_alpha * state->bg_alpha);
         gfx_display_set_alpha(state->bg_underlay_color,
               bg_alpha * state->bg_underlay_alpha);

         /* > Background underlay */
         gfx_display_draw_quad(
               userdata,
               video_width,
               video_height,
               0,
               0,
               video_width,
               video_height,
               video_width,
               video_height,
               state->bg_underlay_color);

         /* > Background shadow */
         gfx_display_draw_quad(
               userdata,
               video_width,
               video_height,
               state->bg_x,
               state->bg_shadow_top_y,
               state->bg_width,
               state->bg_shadow_height,
               video_width,
               video_height,
               state->bg_shadow_top_color);

         gfx_display_draw_quad(
               userdata,
               video_width,
               video_height,
               state->bg_x,
               state->bg_shadow_bottom_y,
               state->bg_width,
               state->bg_shadow_height,
               video_width,
               video_height,
               state->bg_shadow_bottom_color);

         /* > Background */
         gfx_display_draw_quad(
               userdata,
               video_width,
               video_height,
               state->bg_x,
               state->bg_y,
               state->bg_width,
               state->bg_height,
               video_width,
               video_height,
               state->bg_color);
      }

      /* Draw icon */
      if (icon_alpha > 0.0f)
      {
         gfx_display_set_alpha(state->icon_color, icon_alpha);

         if (state->icon_texture)
         {
            gfx_display_blend_begin(userdata);

            gfx_widgets_draw_icon(
                  userdata,
                  video_width,
                  video_height,
                  state->icon_size,
                  state->icon_size,
                  state->icon_texture,
                  icon_x,
                  state->icon_y,
                  0.0f,
                  1.0f,
                  state->icon_color);

            gfx_display_blend_end(userdata);
         }
         /* If there is no icon, draw a placeholder
          * (otherwise layout will look terrible...) */
         else
            gfx_display_draw_quad(
                  userdata,
                  video_width,
                  video_height,
                  icon_x,
                  state->icon_y,
                  state->icon_size,
                  state->icon_size,
                  video_width,
                  video_height,
                  state->icon_color);
      }

      /* Draw text */
      if (text_alpha > 0.0f)
      {
         unsigned text_alpha_int = (unsigned)(text_alpha * 255.0f);

         /* > Set opacity */
         state->content_name_color = COLOR_TEXT_ALPHA(state->content_name_color,
               text_alpha_int);
         state->system_name_color  = COLOR_TEXT_ALPHA(state->system_name_color,
               text_alpha_int);

         /* > Content name */
         gfx_widgets_draw_text(
               font_bold,
               state->content_name,
               text_x,
               state->content_name_y,
               video_width,
               video_height,
               state->content_name_color,
               TEXT_ALIGN_LEFT,
               true);

         /* > System name */
         gfx_widgets_draw_text(
               font_regular,
               state->system_name,
               text_x,
               state->system_name_y,
               video_width,
               video_height,
               state->system_name_color,
               TEXT_ALIGN_LEFT,
               true);

         /* If the message queue is active, must flush the
          * text here to avoid overlaps */
         if (msg_queue_size > 0)
         {
            gfx_widgets_flush_text(video_width, video_height, font_regular);
            gfx_widgets_flush_text(video_width, video_height, font_bold);
         }
         /* Must also flush text if it overlaps the edge of
          * the screen (otherwise it will bleed through the
          * 'margin' shadows) */
         else
         {
            if (state->system_name_width > video_width -
                  (unsigned)text_x - state->margin_shadow_width)
               gfx_widgets_flush_text(video_width, video_height, font_regular);

            if (state->content_name_width > video_width -
                  (unsigned)text_x - state->margin_shadow_width)
               gfx_widgets_flush_text(video_width, video_height, font_bold);
         }
      }

      /* Draw 'margin' shadows
       * > This ensures rendered text is cleanly
       *   truncated when it exceeds the width of
       *   the screen */
      if (bg_alpha > 0.0f)
      {
         /* > Set opacity */
         state->margin_shadow_left_color[3]   = bg_alpha;
         state->margin_shadow_left_color[11]  = bg_alpha;
         state->margin_shadow_right_color[7]  = bg_alpha;
         state->margin_shadow_right_color[15] = bg_alpha;

         /* > Left */
         gfx_display_draw_quad(
               userdata,
               video_width,
               video_height,
               state->margin_shadow_left_x,
               state->bg_y,
               state->margin_shadow_width,
               state->bg_height,
               video_width,
               video_height,
               state->margin_shadow_left_color);

         /* > Right */
         gfx_display_draw_quad(
               userdata,
               video_width,
               video_height,
               state->margin_shadow_right_x,
               state->bg_y,
               state->margin_shadow_width,
               state->bg_height,
               video_width,
               video_height,
               state->margin_shadow_right_color);
      }
   }
}

/* Widget context_reset() */

static void gfx_widget_load_content_animation_context_reset(
      bool is_threaded,
      unsigned width, unsigned height, bool fullscreen,
      const char *dir_assets, char *font_path,
      char* menu_png_path,
      char* widgets_png_path)
{
   gfx_widget_load_content_animation_state_t *state = gfx_widget_load_content_animation_get_ptr();

   /* Cache icon directory */
   if (string_is_empty(menu_png_path))
      state->icon_directory[0] = '\0';
   else
      strlcpy(state->icon_directory, menu_png_path,
            sizeof(state->icon_directory));

   /* Reload icon texture */
   gfx_widget_load_content_animation_load_icon();
}

/* Widget context_destroy() */

static void gfx_widget_load_content_animation_context_destroy(void)
{
   gfx_widget_load_content_animation_state_t *state = gfx_widget_load_content_animation_get_ptr();

   /* Unload any icon texture */
   if (state->icon_texture)
   {
      video_driver_texture_unload(&state->icon_texture);
      state->icon_texture = 0;
   }
}

/* Widget free() */

static void gfx_widget_load_content_animation_free(void)
{
   gfx_widget_load_content_animation_reset();
}

/* Widget definition */

const gfx_widget_t gfx_widget_load_content_animation = {
   NULL, /* init */
   gfx_widget_load_content_animation_free,
   gfx_widget_load_content_animation_context_reset,
   gfx_widget_load_content_animation_context_destroy,
   gfx_widget_load_content_animation_layout,
   gfx_widget_load_content_animation_iterate,
   gfx_widget_load_content_animation_frame
};