/*  RetroArch - A frontend for libretro.
 *  Copyright (C) 2011-2021 - Daniel De Matteis
 *  Copyright (C) 2019-2021 - 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 <stdlib.h>
#include <boolean.h>

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

#include "../verbosity.h"

#if defined(HAVE_CONFIG_H)
#include "../config.h"
#endif

#include "menu_screensaver.h"

#ifndef PI
#define PI 3.14159265359f
#endif

/* Font path defines */
#define MENU_SS_PKG_DIR "pkg"
#define MENU_SS_FONT_FILE "osd-font.ttf"

/* Determines whether current platform has
 * Unicode character support */
#if defined(HAVE_FREETYPE) || (defined(__APPLE__) && defined(HAVE_CORETEXT)) || (defined(HAVE_STB_FONT) && (defined(VITA) || defined(WIIU) || defined(ANDROID) || (defined(_WIN32) && !defined(_XBOX) && !defined(_MSC_VER) && _MSC_VER >= 1400) || (defined(_WIN32) && !defined(_XBOX) && defined(_MSC_VER)) || defined(HAVE_LIBNX) || defined(__linux__) || defined (HAVE_EMSCRIPTEN) || defined(__APPLE__) || defined(HAVE_ODROIDGO2) || defined(__PS3__)))
#define MENU_SS_UNICODE_ENABLED true
#else
#define MENU_SS_UNICODE_ENABLED false
#endif

/* 256 on-screen particles provides a good
 * balance between effect density and draw
 * performance */
#define MENU_SS_NUM_PARTICLES 256

/* Particle effect animations update at a base rate
 * of 60Hz (-> 16.666 ms update period) */
#define MENU_SS_EFFECT_PERIOD ((1.0f / 60.0f) * 1000.0f)

/* To produce a sharp image, font glyphs must
 * be oversized and scaled down. Requested font
 * size is:
 *   (smallest screen dimension) * MENU_SS_FONT_SIZE_FACTOR */
#define MENU_SS_FONT_SIZE_FACTOR 0.1f

/* On a 240p display, base particle size should
 * be 4 pixels. To achieve this, we apply a global
 * scaling factor of:
 *   ((smallest screen dimension) * MENU_SS_PARTICLE_SIZE_FACTOR) / (font size) */
#define MENU_SS_PARTICLE_SIZE_FACTOR (4.0f / 240.0f)

/* Produces a colour value in RGBA32 format
 * from the specified 'tint' adjusted by the
 * specified 'luminosity'
 * > lum must not exceed 1.0f */
#define MENU_SS_PARTICLE_COLOR(tint_r, tint_g, tint_b, lum) (((uint32_t)(tint_r * lum) << 24) | ((uint32_t)(tint_g * lum) << 16) | ((uint32_t)(tint_b * lum) << 8) | 0xFF)

/* Definition of screensaver 'particle':
 * - symbol:          string representation of a font glyph
 * - x:               centre x-coordinate of draw position
 * - y:               centre y-coordinate of draw position
 * - size:            glyph scale factor when drawn
 *                    (1.0 == default size)
 * - a,b,c,d:         general purpose effect-specific variables
 *                    (e.g. velocity, radius, theta, ...)
 * - color:           particle colour in RGBA32 format */
typedef struct
{
   uint32_t color;
   float x;
   float y;
   float size;
   float a;
   float b;
   float c;
   float d;
   const char *symbol;
} menu_ss_particle_t;

/* Holds all objects + metadata corresponding
 * to a font */
typedef struct
{
   font_data_t *font;
   video_font_raster_block_t raster_block; /* ptr alignment */
   float y_centre_offset;
} menu_ss_font_data_t;

/* Holds values used to colourise a particle */
typedef struct
{
   uint32_t color;
   uint32_t r;
   uint32_t g;
   uint32_t b;
} menu_ss_particle_tint_t;

struct menu_ss_handle
{
   float bg_color[16];
   menu_ss_font_data_t font_data;
   unsigned last_width;
   unsigned last_height;
   float font_size;
   float particle_scale;
   menu_ss_particle_tint_t particle_tint;
   menu_ss_particle_t *particles;
   enum menu_screensaver_effect effect;
   bool font_enabled;
   bool unicode_enabled;
};

/* UTF-8 Symbols */
static const char * const menu_ss_fallback_symbol = "*";

#define MENU_SS_NUM_SNOW_SYMBOLS 3
static const char * const menu_ss_snow_symbols[] = {
   "\xE2\x9D\x84", /* Snowflake, U+2744 */
   "\xE2\x9D\x85", /* Tight Trifoliate Snowflake, U+2745 */
   "\xE2\x9D\x86"  /* Heavy Chevron Snowflake, U+2746 */
};

#define MENU_SS_NUM_STARFIELD_SYMBOLS 8
static const char * const menu_ss_starfield_symbols[] = {
   "\xE2\x98\x85", /* Black Star, U+2605 */
   "\xE2\x9C\xA6", /* Black Four Pointed Star, U+2726 */
   "\xE2\x9C\xB4", /* Eight Pointed Black Star, U+2734 */
   "\xE2\x9C\xB6", /* Six Pointed Black Star, U+2736 */
   "\xE2\x9C\xB7", /* Eight Pointed Rectilinear Black Star, U+2737 */
   "\xE2\x9C\xB8", /* Heavy Eight Pointed Rectilinear Black Star, U+2738 */
   "\xE2\x9C\xB9", /* Twelve Pointed Black Star, U+2739 */
   "\xE2\x97\x8F"  /* Black Circle, U+25CF */
};

#define MENU_SS_NUM_VORTEX_SYMBOLS 6
static const char * const menu_ss_vortex_symbols[] = {
   "\xE2\x9D\x8B", /* Heavy Eight Teardrop-Spoked Propeller Asterisk, U+274B */
   "\xE2\x9C\xBD", /* Heavy Teardrop-Spoked Asterisk, U+273D */
   "\xE2\x9C\xBB", /* Teardrop-Spoked Asterisk, U+273B */
   "\xE2\x97\x89", /* Fisheye, U+25C9 */
   "\xE2\x97\x8F", /* Black Circle, U+25CF */
   "\xE2\x97\x86"  /* Black Diamond, U+25C6 */
};

/******************/
/* Initialisation */
/******************/

/* Creates and initialises a new screensaver object.
 * Returned object must be freed using
 * menu_screensaver_free().
 * Returns NULL in the event of an error. */
menu_screensaver_t *menu_screensaver_init(void)
{
   menu_screensaver_t *screensaver = NULL;
   /* Screensaver background must be pure black,
    * 100% opacity */
   float bg_color[16]              = COLOR_HEX_TO_FLOAT(0x000000, 1.0f);

   /* Create menu_screensaver_t object */
   screensaver = (menu_screensaver_t*)malloc(sizeof(*screensaver));

   if (!screensaver)
      return NULL;

   /* Initial effect is always 'blank' */
   screensaver->effect = MENU_SCREENSAVER_BLANK;

   /* > Fonts must be enabled for the screensaver to
    *   function. 'font_enabled' flag exists purely
    *   to prevent re-initialisation spam in the event
    *   that font creation fails
    * > Initialise 'Unicode enabled' state based on
    *   compiler flags */
   screensaver->font_enabled    = true;
   screensaver->unicode_enabled = MENU_SS_UNICODE_ENABLED;

   /* Set background colour */
   memcpy(screensaver->bg_color, bg_color, sizeof(screensaver->bg_color));

   /* Font is loaded on-demand
    * (Don't waste memory unless we are actually
    *  drawing an effect) */
   memset(&screensaver->font_data, 0, sizeof(screensaver->font_data));

   /* Particle array is created on-demand
    * (Don't waste memory unless we are actually
    *  drawing an effect) */
   screensaver->particles           = NULL;
   screensaver->particle_tint.color = 0;
   screensaver->particle_tint.r     = 0;
   screensaver->particle_tint.g     = 0;
   screensaver->particle_tint.b     = 0;

   /* Initial dimensions are zeroed out - will be set
    * on first call of menu_screensaver_iterate() */
   screensaver->last_width     = 0;
   screensaver->last_height    = 0;
   screensaver->font_size      = 0.0f;
   screensaver->particle_scale = 0.0f;

   return screensaver;
}

/* Frees specified screensaver object */
void menu_screensaver_free(menu_screensaver_t *screensaver)
{
   if (!screensaver)
      return;

   /* Free font */
   if (screensaver->font_data.font)
   {
      gfx_display_font_free(screensaver->font_data.font);
      video_coord_array_free(&screensaver->font_data.raster_block.carr);
      screensaver->font_data.font = NULL;

      font_driver_bind_block(NULL, NULL);
   }

   /* Free particle array */
   if (screensaver->particles)
      free(screensaver->particles);
   screensaver->particles = NULL;

   free(screensaver);
}

/*********************/
/* Context functions */
/*********************/

/* Called when the graphics context is destroyed
 * or reset (a dedicated 'reset' function is
 * unnecessary) */
void menu_screensaver_context_destroy(menu_screensaver_t *screensaver)
{
   if (!screensaver)
      return;

   /* Free any existing font
    * (will be recreated, if required, on the next
    * call of menu_screensaver_iterate()) */
   if (screensaver->font_data.font)
   {
      gfx_display_font_free(screensaver->font_data.font);
      video_coord_array_free(&screensaver->font_data.raster_block.carr);
      screensaver->font_data.font = NULL;
   }
}

/**********************/
/* Run loop functions */
/**********************/

/* qsort() helpers */
static int menu_ss_starfield_qsort_func(const menu_ss_particle_t *a,
      const menu_ss_particle_t *b)
{
   return a->c > b->c ? -1 : 1;
}

static int menu_ss_vortex_qsort_func(const menu_ss_particle_t *a,
      const menu_ss_particle_t *b)
{
   return a->a < b->a ? -1 : 1;
}

/* Calculates base font size and particle
 * scale based on current screen dimensions */
static INLINE void menu_screensaver_set_dimensions(
      menu_screensaver_t *screensaver,
      unsigned width, unsigned height)
{
   float screen_size           = (float)((width < height) ? width : height);
   screensaver->font_size      = (screen_size * MENU_SS_FONT_SIZE_FACTOR) + 0.5f;
   screensaver->particle_scale = (screen_size * MENU_SS_PARTICLE_SIZE_FACTOR) / screensaver->font_size;
   screensaver->last_width     = width;
   screensaver->last_height    = height;
}

static bool menu_screensaver_init_effect(menu_screensaver_t *screensaver)
{
   unsigned width;
   unsigned height;
   size_t i;

   /* Create particle array, if required */
   if (!screensaver->particles)
   {
      screensaver->particles = (menu_ss_particle_t*)
            calloc(MENU_SS_NUM_PARTICLES, sizeof(*screensaver->particles));

      if (!screensaver->particles)
         return false;
   }
   
   width  = screensaver->last_width;
   height = screensaver->last_height;

   /* Initialise array */
   switch (screensaver->effect)
   {
      case MENU_SCREENSAVER_SNOW:
         {
            for (i = 0; i < MENU_SS_NUM_PARTICLES; i++)
            {
               menu_ss_particle_t *particle = &screensaver->particles[i];
               float size_factor;

               particle->x    = (float)(rand() % width);
               particle->y    = (float)(rand() % height);
               particle->a    = (float)(rand() % 64 - 16) * 0.1f;
               particle->b    = (float)(rand() % 64 - 48) * 0.1f;

               /* Get particle size */
               size_factor    = (float)i / (float)MENU_SS_NUM_PARTICLES;
               size_factor    = size_factor * size_factor;
               particle->size = 1.0f + (size_factor * 2.0f);

               /* If Unicode is supported, select a random
                * snowflake symbol */
               particle->symbol    = menu_ss_fallback_symbol;
               if (screensaver->unicode_enabled)
                  particle->symbol = menu_ss_snow_symbols[(unsigned)(rand() % MENU_SS_NUM_SNOW_SYMBOLS)];
            }
         }
         break;
      case MENU_SCREENSAVER_STARFIELD:
         {
            float max_depth            = (float)(width > height ? width : height);
            float initial_speed_factor = 0.02f * max_depth / 240.0f;

            for (i = 0; i < MENU_SS_NUM_PARTICLES; i++)
            {
               menu_ss_particle_t *particle = &screensaver->particles[i];

               /* x pos ('physical' space) */
               particle->a = (float)(rand() % width);
               /* y pos ('physical' space) */
               particle->b = (float)(rand() % height);
               /* depth */
               particle->c = max_depth;
               /* speed */
               particle->d = 1.0f + ((float)(rand() % 20) * initial_speed_factor);

               /* If Unicode is supported, select a random
                * star symbol */
               particle->symbol    = menu_ss_fallback_symbol;
               if (screensaver->unicode_enabled)
                  particle->symbol = menu_ss_starfield_symbols[(unsigned)(rand() % MENU_SS_NUM_STARFIELD_SYMBOLS)];
            }
         }
         break;
      case MENU_SCREENSAVER_VORTEX:
         {
            float min_screen_dimension = (float)(width < height ? width : height);
            float max_radius           = (float)sqrt((double)((width * width) + (height * height))) / 2.0f;
            float radial_speed_factor  = 0.001f * min_screen_dimension / 240.0f;

            for (i = 0; i < MENU_SS_NUM_PARTICLES; i++)
            {
               menu_ss_particle_t *particle = &screensaver->particles[i];

               /* radius */
               particle->a = 1.0f + (((float)rand() / (float)RAND_MAX) * max_radius);
               /* theta */
               particle->b = ((float)rand() / (float)RAND_MAX) * 2.0f * PI;
               /* radial speed */
               particle->c = (float)((rand() % 100) + 1) * radial_speed_factor;
               /* rotational speed */
               particle->d = (((float)((rand() % 50) + 1) * 0.005f) + 0.1f) * (PI / 360.0f);

               /* If Unicode is supported, select a random
                * star symbol */
               particle->symbol    = menu_ss_fallback_symbol;
               if (screensaver->unicode_enabled)
                  particle->symbol = menu_ss_vortex_symbols[(unsigned)(rand() % MENU_SS_NUM_VORTEX_SYMBOLS)];
            }
         }
         break;
      default:
         /* Error condition - do nothing */
         return false;
   }

   return true;
}

/* Checks for and applies any pending screensaver
 * state changes */
static bool menu_screensaver_update_state(
      menu_screensaver_t *screensaver, gfx_display_t *p_disp,
      enum menu_screensaver_effect effect, uint32_t particle_tint,
      unsigned width, unsigned height, const char *dir_assets)
{
   bool init_effect = false;

#if defined(_3DS)
   /* 3DS has an 'incomplete' font driver,
    * and cannot render screensaver effects */
   effect = MENU_SCREENSAVER_BLANK;
#endif

   /* Check if dimensions have changed */
   if ((screensaver->last_width  != width) ||
       (screensaver->last_height != height))
   {
      menu_screensaver_set_dimensions(screensaver, width, height);

      /* Free any existing font */
      if (screensaver->font_data.font)
      {
         gfx_display_font_free(screensaver->font_data.font);
         video_coord_array_free(&screensaver->font_data.raster_block.carr);
         screensaver->font_data.font = NULL;
      }

      init_effect = true;
   }

   /* Check if effect has changed */
   if (screensaver->effect != effect)
   {
      screensaver->effect = effect;
      init_effect         = true;
   }

   /* Check if particle tint has changed */
   if (screensaver->particle_tint.color != particle_tint)
   {
      screensaver->particle_tint.color = particle_tint;
      screensaver->particle_tint.r     = (particle_tint >> 16) & 0xFF;
      screensaver->particle_tint.g     = (particle_tint >>  8) & 0xFF;
      screensaver->particle_tint.b     = (particle_tint      ) & 0xFF;
   }

   /* Create font, if required */
   if ((screensaver->effect != MENU_SCREENSAVER_BLANK) &&
       !screensaver->font_data.font &&
       screensaver->font_enabled)
   {
#if defined(HAVE_FREETYPE) || (defined(__APPLE__) && defined(HAVE_CORETEXT)) || defined(HAVE_STB_FONT)
      char font_file[PATH_MAX_LENGTH];
      char pkg_path[PATH_MAX_LENGTH];

      font_file[0] = '\0';
      pkg_path[0]  = '\0';

      /* Get font file path */
      if (!string_is_empty(dir_assets))
         fill_pathname_join(pkg_path, dir_assets, MENU_SS_PKG_DIR, sizeof(pkg_path));
      else
         strlcpy(pkg_path, MENU_SS_PKG_DIR, sizeof(pkg_path));

      fill_pathname_join(font_file, pkg_path, MENU_SS_FONT_FILE,
            sizeof(font_file));

      /* Warn if font file is missing */
      if (!path_is_valid(font_file))
      {
         RARCH_WARN("[Menu Screensaver] Font asset missing: %s\n", font_file);
         screensaver->unicode_enabled = false;
      }
#else
      /* On platforms without TTF support, there is
       * no need to generate a font path (a bitmap
       * font will be created automatically) */
      char font_file[PATH_MAX_LENGTH];
      font_file[0] = '\0';
#endif

      /* Create font */
      screensaver->font_data.font = gfx_display_font_file(p_disp,
            font_file, screensaver->font_size,
            video_driver_is_threaded());

      /* If font was created successfully, fetch metadata */
      if (screensaver->font_data.font)
         screensaver->font_data.y_centre_offset =
               (float)font_driver_get_line_centre_offset(
                     screensaver->font_data.font, 1.0f);
      /* In case of error, warn and disable
       * further attempts to create fonts */
      else
      {
         RARCH_WARN("[Menu Screensaver] Failed to initialise font - animation disabled\n");
         screensaver->font_enabled = false;
         return false;
      }
   }

   /* Initialise animation effect, if required */
   if (init_effect)
      return menu_screensaver_init_effect(screensaver);

   return true;
}

/* Processes screensaver animation logic
 * Called every frame on the main thread */
void menu_screensaver_iterate(
      menu_screensaver_t *screensaver,
      gfx_display_t *p_disp, gfx_animation_t *p_anim,
      enum menu_screensaver_effect effect, float effect_speed,
      uint32_t particle_tint, unsigned width, unsigned height,
      const char *dir_assets)
{
   float base_particle_size;
   uint32_t tint_r;
   uint32_t tint_g;
   uint32_t tint_b;
   float global_speed_factor;
   size_t i;

   if (!screensaver)
      return;

   /* Apply pending state changes */
   if (!menu_screensaver_update_state(
         screensaver, p_disp,
         effect, particle_tint,
         width, height, dir_assets) ||
       (screensaver->effect == MENU_SCREENSAVER_BLANK) ||
       !screensaver->particles)
      return;

   base_particle_size = screensaver->particle_scale * screensaver->font_size;
   tint_r             = screensaver->particle_tint.r;
   tint_g             = screensaver->particle_tint.g;
   tint_b             = screensaver->particle_tint.b;

   /* Set global animation speed */
   global_speed_factor = p_anim->delta_time / MENU_SS_EFFECT_PERIOD;
   if (effect_speed > 0.0001f) 
      global_speed_factor *= effect_speed;

   /* Update particle array */
   switch (screensaver->effect)
   {
      case MENU_SCREENSAVER_SNOW:
         for (i = 0; i < MENU_SS_NUM_PARTICLES; i++)
         {
            menu_ss_particle_t *particle = &screensaver->particles[i];
            float particle_size_px       = particle->size * base_particle_size;
            bool update_symbol           = false;
            float luminosity;

            /* Update particle 'speed' */
            particle->a = particle->a + (float)(rand() % 16 - 9) * 0.01f;
            particle->b = particle->b + (float)(rand() % 16 - 7) * 0.01f;

            particle->a = (particle->a < -0.4f) ? -0.4f : particle->a;
            particle->a = (particle->a >  0.1f) ?  0.1f : particle->a;

            particle->b = (particle->b < -0.1f) ? -0.1f : particle->b;
            particle->b = (particle->b >  0.4f) ?  0.4f : particle->b;

            /* Update particle location */
            particle->x = particle->x + (global_speed_factor * particle->size * particle->a);
            particle->y = particle->y + (global_speed_factor * particle->size * particle->b);

            /* Get particle colour */
            luminosity      = 0.5f + (particle->size / 6.0f);
            particle->color = MENU_SS_PARTICLE_COLOR(tint_r, tint_g, tint_b, luminosity);

            /* Reset particle if it has fallen off screen */
            if (particle->x < -particle_size_px)
            {
               particle->x   = (float)width + particle_size_px;
               update_symbol = true;
            }

            if (particle->y > (float)height + particle_size_px)
            {
               particle->y   = -particle_size_px;
               update_symbol = true;
            }

            if (update_symbol && screensaver->unicode_enabled)
               particle->symbol = menu_ss_snow_symbols[(unsigned)(rand() % MENU_SS_NUM_SNOW_SYMBOLS)];
         }
         break;
      case MENU_SCREENSAVER_STARFIELD:
         {
            float max_depth            = (float)(width > height ? width : height);
            float initial_speed_factor = 0.02f * max_depth / 240.0f;
            float focal_length         = max_depth * 2.0f;
            float x_centre             = (float)(width >> 1);
            float y_centre             = (float)(height >> 1);
            float particle_size_px;
            float luminosity;

            /* Based on an example found here:
             * https://codepen.io/nodws/pen/pejBNb */
            for (i = 0; i < MENU_SS_NUM_PARTICLES; i++)
            {
               menu_ss_particle_t *particle = &screensaver->particles[i];

               /* Get particle size */
               particle->size   = focal_length / (2.0f * particle->c);
               particle_size_px = particle->size * base_particle_size;

               /* Update depth */
               particle->c -= particle->d * global_speed_factor;

               /* Reset particle if it has:
                * - Dropped off the edge of the screen
                * - Reached the screen depth */
               if ((particle->x < -particle_size_px) ||
                   (particle->x > (float)width + particle_size_px) ||
                   (particle->y < -particle_size_px) ||
                   (particle->y > (float)height + particle_size_px) ||
                   (particle->c <= 0.0f))
               {
                  /* x pos ('physical' space) */
                  particle->a = (float)(rand() % width);
                  /* y pos ('physical' space) */
                  particle->b = (float)(rand() % height);
                  /* depth */
                  particle->c = max_depth;
                  /* speed */
                  particle->d = 1.0f + ((float)(rand() % 20) * initial_speed_factor);

                  /* Reset size */
                  particle->size = 1.0f;

                  /* If Unicode is supported, select a random
                   * star symbol */
                  if (screensaver->unicode_enabled)
                     particle->symbol = menu_ss_starfield_symbols[(unsigned)(rand() % MENU_SS_NUM_STARFIELD_SYMBOLS)];
               }

               /* Get particle location */
               particle->x = (particle->a - x_centre) * (focal_length / particle->c);
               particle->x += x_centre;

               particle->y = (particle->b - y_centre) * (focal_length / particle->c);
               particle->y += y_centre;

               /* Get particle colour */
               luminosity      = 0.25f + (0.75f - (particle->c / max_depth) * 0.75f);
               particle->color = MENU_SS_PARTICLE_COLOR(tint_r, tint_g, tint_b, luminosity);
            }

            /* Particles must be drawn in order of depth,
             * from furthest away to nearest */
            qsort(screensaver->particles,
                  MENU_SS_NUM_PARTICLES, sizeof(menu_ss_particle_t),
                  (int (*)(const void *, const void *))menu_ss_starfield_qsort_func);
         }
         break;
      case MENU_SCREENSAVER_VORTEX:
         {
            float min_screen_dimension = (float)(width < height ? width : height);
            float max_radius           = (float)sqrt((double)((width * width) + (height * height))) / 2.0f;
            float radial_speed_factor  = 0.001f * min_screen_dimension / 240.0f;
            float x_centre             = (float)(width >> 1);
            float y_centre             = (float)(height >> 1);
            float r_speed;
            float theta_speed;
            float size_factor;
            float luminosity;

            for (i = 0; i < MENU_SS_NUM_PARTICLES; i++)
            {
               menu_ss_particle_t *particle = &screensaver->particles[i];

               /* Update particle speed */
               r_speed     = particle->c * global_speed_factor;
               theta_speed = particle->d * global_speed_factor;
               if ((particle->a > 0.0f) && (particle->a < min_screen_dimension))
               {
                  float base_scale_factor = (min_screen_dimension - particle->a) / min_screen_dimension;
                  r_speed     *= 1.0f + (base_scale_factor * 8.0f);
                  theta_speed *= 1.0f + (base_scale_factor * base_scale_factor * 6.0f);
               }
               particle->a -= r_speed;
               particle->b += theta_speed;

               /* Reset particle if it has reached the centre of the screen */
               if (particle->a < 0.0f)
               {
                  /* radius
                   * Note: In theory, this should be:
                   * > particle->a = max_radius;
                   * ...but it turns out that spawning new particles at random
                   * locations produces a more visually appealing result... */
                  particle->a = 1.0f + (((float)rand() / (float)RAND_MAX) * max_radius);
                  /* theta */
                  particle->b = ((float)rand() / (float)RAND_MAX) * 2.0f * PI;
                  /* radial speed */
                  particle->c = (float)((rand() % 100) + 1) * radial_speed_factor;
                  /* rotational speed */
                  particle->d = (((float)((rand() % 50) + 1) * 0.005f) + 0.1f) * (PI / 360.0f);

                  /* If Unicode is supported, select a random
                   * star symbol */
                  if (screensaver->unicode_enabled)
                     particle->symbol = menu_ss_vortex_symbols[(unsigned)(rand() % MENU_SS_NUM_VORTEX_SYMBOLS)];
               }

               /* Get particle location */
               particle->x = (particle->a * cos(particle->b)) + x_centre;
               particle->y = (particle->a * sin(particle->b)) + y_centre;

               /* Get particle size */
               size_factor    = 1.0f - ((max_radius - particle->a) / max_radius);
               particle->size = sqrt(size_factor) * 2.5f;

               /* Get particle colour */
               luminosity      = 0.2f + (0.8f - size_factor * 0.8f);
               particle->color = MENU_SS_PARTICLE_COLOR(tint_r, tint_g, tint_b, luminosity);
            }

            /* Particles must be drawn in order of radius;
             * particles closest to the centre are further away
             * from the screen, and must be drawn first */
            qsort(screensaver->particles,
                  MENU_SS_NUM_PARTICLES, sizeof(menu_ss_particle_t),
                  (int (*)(const void *, const void *))menu_ss_vortex_qsort_func);
         }
         break;
      default:
         /* Error condition - do nothing */
         break;
   }
}

/* Draws screensaver
 * Called every frame (on the video thread,
 * if threaded video is on) */
void menu_screensaver_frame(menu_screensaver_t *screensaver,
      video_frame_info_t *video_info, gfx_display_t *p_disp)
{
   void *userdata = NULL;
   unsigned video_width;
   unsigned video_height;

   if (!screensaver)
      return;

   video_width  = video_info->width;
   video_height = video_info->height;
   userdata     = video_info->userdata;

   /* Set viewport */
   video_driver_set_viewport(video_width, video_height, true, false);

   /* Draw background */
   gfx_display_draw_quad(
         p_disp,
         userdata,
         video_width,
         video_height,
         0, 0,
         screensaver->last_width, screensaver->last_height,
         screensaver->last_width, screensaver->last_height,
         screensaver->bg_color,
         NULL);

   /* Draw particle effect, if required */
   if ((screensaver->effect != MENU_SCREENSAVER_BLANK) &&
       screensaver->font_data.font &&
       screensaver->particles)
   {
      font_data_t *font     = screensaver->font_data.font;
      float y_centre_offset = screensaver->font_data.y_centre_offset;
      float particle_scale  = screensaver->particle_scale;
      size_t i;

      /* Bind font */
      font_driver_bind_block(font, &screensaver->font_data.raster_block);
      screensaver->font_data.raster_block.carr.coords.vertices = 0;

      /* Render text */
      for (i = 0; i < MENU_SS_NUM_PARTICLES; i++)
      {
         menu_ss_particle_t *particle = &screensaver->particles[i];

         gfx_display_draw_text(
               font,
               particle->symbol,
               particle->x,
               particle->y + y_centre_offset,
               video_width, video_height,
               particle->color,
               TEXT_ALIGN_CENTER,
               particle->size * particle_scale,
               false, 0.0f, true);
      }

      /* Flush text and unbind font */
      if (screensaver->font_data.raster_block.carr.coords.vertices != 0)
      {
         font_driver_flush(video_width, video_height, font);
         screensaver->font_data.raster_block.carr.coords.vertices = 0;
      }
      font_driver_bind_block(font, NULL);
   }

   /* Unset viewport */
   video_driver_set_viewport(video_width, video_height, false, true);
}
