/* Copyright  (C) 2010-2019 The RetroArch team
 *
 * ---------------------------------------------------------------------------------------
 * The following license statement only applies to this file (runtime_file.c).
 * ---------------------------------------------------------------------------------------
 *
 * Permission is hereby granted, free of charge,
 * to any person obtaining a copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
 * and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <locale.h>

#include <file/file_path.h>
#include <retro_miscellaneous.h>
#include <streams/file_stream.h>
#include <formats/jsonsax_full.h>
#include <string/stdstring.h>
#include <encodings/utf.h>
#include <time/rtime.h>

#include "file_path_special.h"
#include "paths.h"
#include "core_info.h"
#include "verbosity.h"
#include "msg_hash.h"

#if defined(HAVE_MENU)
#include "menu/menu_driver.h"
#endif

#include "runtime_file.h"

#define LOG_FILE_RUNTIME_FORMAT_STR "%u:%02u:%02u"
#define LOG_FILE_LAST_PLAYED_FORMAT_STR "%04u-%02u-%02u %02u:%02u:%02u"

/* JSON Stuff... */

typedef struct
{
   JSON_Parser parser;
   JSON_Writer writer;
   RFILE *file;
   char **current_entry_val;
   char *runtime_string;
   char *last_played_string;
} RtlJSONContext;

static JSON_Parser_HandlerResult RtlJSONObjectMemberHandler(JSON_Parser parser, char *pValue, size_t length, JSON_StringAttributes attributes)
{
   RtlJSONContext *pCtx = (RtlJSONContext*)JSON_Parser_GetUserData(parser);
   (void)attributes; /* unused */

   if (pCtx->current_entry_val)
   {
      /* something went wrong */
      RARCH_ERR("JSON parsing failed at line %d.\n", __LINE__);
      return JSON_Parser_Abort;
   }

   if (length)
   {
      if (string_is_equal(pValue, "runtime"))
         pCtx->current_entry_val = &pCtx->runtime_string;
      else if (string_is_equal(pValue, "last_played"))
         pCtx->current_entry_val = &pCtx->last_played_string;
      /* ignore unknown members */
   }

   return JSON_Parser_Continue;
}

static JSON_Parser_HandlerResult RtlJSONStringHandler(JSON_Parser parser, char *pValue, size_t length, JSON_StringAttributes attributes)
{
   RtlJSONContext *pCtx = (RtlJSONContext*)JSON_Parser_GetUserData(parser);
   (void)attributes; /* unused */

   if (pCtx->current_entry_val && length && !string_is_empty(pValue))
   {
      if (*pCtx->current_entry_val)
         free(*pCtx->current_entry_val);

      *pCtx->current_entry_val = strdup(pValue);
   }
   /* ignore unknown members */

   pCtx->current_entry_val = NULL;

   return JSON_Parser_Continue;
}

static JSON_Writer_HandlerResult RtlJSONOutputHandler(JSON_Writer writer, const char *pBytes, size_t length)
{
   RtlJSONContext *context = (RtlJSONContext*)JSON_Writer_GetUserData(writer);
   (void)writer; /* unused */

   return filestream_write(context->file, pBytes, length) == length ? JSON_Writer_Continue : JSON_Writer_Abort;
}

static void RtlJSONLogError(RtlJSONContext *pCtx)
{
   if (pCtx->parser && JSON_Parser_GetError(pCtx->parser) != JSON_Error_AbortedByHandler)
   {
      JSON_Error error            = JSON_Parser_GetError(pCtx->parser);
      JSON_Location errorLocation = { 0, 0, 0 };

      (void)JSON_Parser_GetErrorLocation(pCtx->parser, &errorLocation);
      RARCH_ERR("Error: Invalid JSON at line %d, column %d (input byte %d) - %s.\n",
            (int)errorLocation.line + 1,
            (int)errorLocation.column + 1,
            (int)errorLocation.byte,
            JSON_ErrorString(error));
   }
   else if (pCtx->writer && JSON_Writer_GetError(pCtx->writer) != JSON_Error_AbortedByHandler)
   {
      RARCH_ERR("Error: could not write output - %s.\n", JSON_ErrorString(JSON_Writer_GetError(pCtx->writer)));
   }
}

/* Initialisation */

/* Parses log file referenced by runtime_log->path.
 * Does nothing if log file does not exist. */
static void runtime_log_read_file(runtime_log_t *runtime_log)
{
   unsigned runtime_hours      = 0;
   unsigned runtime_minutes    = 0;
   unsigned runtime_seconds    = 0;

   unsigned last_played_year   = 0;
   unsigned last_played_month  = 0;
   unsigned last_played_day    = 0;
   unsigned last_played_hour   = 0;
   unsigned last_played_minute = 0;
   unsigned last_played_second = 0;

   RtlJSONContext context      = {0};
   /* Attempt to open log file */
   RFILE *file                 = filestream_open(runtime_log->path,
         RETRO_VFS_FILE_ACCESS_READ, RETRO_VFS_FILE_ACCESS_HINT_NONE);

   if (!file)
   {
      RARCH_ERR("Failed to open runtime log file: %s\n", runtime_log->path);
      return;
   }

   /* Initialise JSON parser */
   context.runtime_string     = NULL;
   context.last_played_string = NULL;
   context.parser             = JSON_Parser_Create(NULL);
   context.file               = file;

   if (!context.parser)
   {
      RARCH_ERR("Failed to create JSON parser.\n");
      goto end;
   }

   /* Configure parser */
   JSON_Parser_SetAllowBOM(context.parser, JSON_True);
   JSON_Parser_SetStringHandler(context.parser, &RtlJSONStringHandler);
   JSON_Parser_SetObjectMemberHandler(context.parser, &RtlJSONObjectMemberHandler);
   JSON_Parser_SetUserData(context.parser, &context);

   /* Read file */
   while (!filestream_eof(file))
   {
      /* Runtime log files are tiny - use small chunk size */
      char chunk[128] = {0};
      int64_t length  = filestream_read(file, chunk, sizeof(chunk));

      /* Error checking... */
      if (!length && !filestream_eof(file))
      {
         RARCH_ERR("Failed to read runtime log file: %s\n", runtime_log->path);
         JSON_Parser_Free(context.parser);
         goto end;
      }

      /* Parse chunk */
      if (!JSON_Parser_Parse(context.parser, chunk, length, JSON_False))
      {
         RARCH_ERR("Error parsing chunk of runtime log file: %s\n---snip---\n%s\n---snip---\n", runtime_log->path, chunk);
         RtlJSONLogError(&context);
         JSON_Parser_Free(context.parser);
         goto end;
      }
   }

   /* Finalise parsing */
   if (!JSON_Parser_Parse(context.parser, NULL, 0, JSON_True))
   {
      RARCH_WARN("Error parsing runtime log file: %s\n", runtime_log->path);
      RtlJSONLogError(&context);
      JSON_Parser_Free(context.parser);
      goto end;
   }

   /* Free parser */
   JSON_Parser_Free(context.parser);

   /* Process string values read from JSON file */

   /* Runtime */
   if (!string_is_empty(context.runtime_string))
   {
      if (sscanf(context.runtime_string, LOG_FILE_RUNTIME_FORMAT_STR,
               &runtime_hours, &runtime_minutes, &runtime_seconds) != 3)
      {
         RARCH_ERR("Runtime log file - invalid 'runtime' entry detected: %s\n", runtime_log->path);
         goto end;
      }
   }

   /* Last played */
   if (!string_is_empty(context.last_played_string))
   {
      if (sscanf(context.last_played_string, LOG_FILE_LAST_PLAYED_FORMAT_STR,
               &last_played_year, &last_played_month, &last_played_day,
               &last_played_hour, &last_played_minute, &last_played_second) != 6)
      {
         RARCH_ERR("Runtime log file - invalid 'last played' entry detected: %s\n", runtime_log->path);
         goto end;
      }
   }

   /* If we reach this point then all is well
    * > Assign values to runtime_log object */
   runtime_log->runtime.hours      = runtime_hours;
   runtime_log->runtime.minutes    = runtime_minutes;
   runtime_log->runtime.seconds    = runtime_seconds;

   runtime_log->last_played.year   = last_played_year;
   runtime_log->last_played.month  = last_played_month;
   runtime_log->last_played.day    = last_played_day;
   runtime_log->last_played.hour   = last_played_hour;
   runtime_log->last_played.minute = last_played_minute;
   runtime_log->last_played.second = last_played_second;

end:

   /* Clean up leftover strings */
   if (context.runtime_string)
      free(context.runtime_string);
   if (context.last_played_string)
      free(context.last_played_string);

   /* Close log file */
   filestream_close(file);
}

/* Initialise runtime log, loading current parameters
 * if log file exists. Returned object must be free()'d.
 * Returns NULL if content_path and/or core_path are invalid */
runtime_log_t *runtime_log_init(
      const char *content_path,
      const char *core_path,
      const char *dir_runtime_log,
      const char *dir_playlist,
      bool log_per_core)
{
   char content_name[PATH_MAX_LENGTH];
   char core_name[PATH_MAX_LENGTH];
   char log_file_dir[PATH_MAX_LENGTH];
   char log_file_path[PATH_MAX_LENGTH];
   char tmp_buf[PATH_MAX_LENGTH];
   core_info_ctx_find_t core_info;
   runtime_log_t *runtime_log = NULL;

   content_name[0]            = '\0';
   core_name[0]               = '\0';
   log_file_dir[0]            = '\0';
   log_file_path[0]           = '\0';
   tmp_buf[0]                 = '\0';

   if (  string_is_empty(dir_runtime_log) &&
         string_is_empty(dir_playlist))
   {
      RARCH_ERR("Runtime log directory is undefined - cannot save"
            " runtime log files.\n");
      return NULL;
   }

   if (  string_is_empty(core_path) ||
         string_is_equal(core_path, "builtin") ||
         string_is_equal(core_path, "DETECT") ||
         string_is_empty(content_path))
      return NULL;

   /* Get core name
    * Note: An annoyance - this is required even when
    * we are performing aggregate (not per core) logging,
    * since content name is sometimes dependent upon core
    * (e.g. see TyrQuake below) */
   core_info.inf  = NULL;
   core_info.path = core_path;

   if (core_info_find(&core_info) &&
       core_info.inf->core_name)
      strlcpy(core_name, core_info.inf->core_name, sizeof(core_name));

   if (string_is_empty(core_name))
      return NULL;

   /* Get runtime log directory */
   if (string_is_empty(dir_runtime_log))
   {
      /* If 'custom' runtime log path is undefined,
       * use default 'playlists/logs' directory... */
      fill_pathname_join(
            tmp_buf,
            dir_playlist,
            "logs",
            sizeof(tmp_buf));
   }
   else
      strlcpy(tmp_buf, dir_runtime_log, sizeof(tmp_buf));

   if (string_is_empty(tmp_buf))
      return NULL;

   if (log_per_core)
      fill_pathname_join(
            log_file_dir,
            tmp_buf,
            core_name,
            sizeof(log_file_dir));
   else
      strlcpy(log_file_dir, tmp_buf, sizeof(log_file_dir));

   if (string_is_empty(log_file_dir))
      return NULL;

   /* Create directory, if required */
   if (!path_is_directory(log_file_dir))
   {
      if (!path_mkdir(log_file_dir))
      {
         RARCH_ERR("[runtime] failed to create directory for"
               " runtime log: %s.\n", log_file_dir);
         return NULL;
      }
   }

   /* Get content name
    * Note: TyrQuake requires a specific hack, since all
    * content has the same name... */
   if (string_is_equal(core_name, "TyrQuake"))
   {
      const char *last_slash = find_last_slash(content_path);
      if (last_slash)
      {
         size_t path_length = last_slash + 1 - content_path;
         if (path_length < PATH_MAX_LENGTH)
         {
            memset(tmp_buf, 0, sizeof(tmp_buf));
            strlcpy(tmp_buf, content_path, path_length * sizeof(char));
            strlcpy(content_name, path_basename(tmp_buf), sizeof(content_name));
         }
      }
   }
   else
   {
      /* path_remove_extension() requires a char * (not const)
       * so have to use a temporary buffer... */
      char *tmp_buf_no_ext = NULL;
      tmp_buf[0]           = '\0';
      strlcpy(tmp_buf, path_basename(content_path), sizeof(tmp_buf));
      tmp_buf_no_ext       = path_remove_extension(tmp_buf);

      if (string_is_empty(tmp_buf_no_ext))
         return NULL;

      strlcpy(content_name, tmp_buf_no_ext, sizeof(content_name));
   }

   if (string_is_empty(content_name))
      return NULL;

   /* Build final log file path */
   fill_pathname_join(log_file_path, log_file_dir, content_name, sizeof(log_file_path));
   strlcat(log_file_path, file_path_str(FILE_PATH_RUNTIME_EXTENSION), sizeof(log_file_path));

   if (string_is_empty(log_file_path))
      return NULL;

   /* Phew... If we get this far then all is well.
    * > Create 'runtime_log' object */
   runtime_log                     = (runtime_log_t*)malloc(sizeof(*runtime_log));
   if (!runtime_log)
      return NULL;

   /* > Populate default values */
   runtime_log->runtime.hours      = 0;
   runtime_log->runtime.minutes    = 0;
   runtime_log->runtime.seconds    = 0;

   runtime_log->last_played.year   = 0;
   runtime_log->last_played.month  = 0;
   runtime_log->last_played.day    = 0;
   runtime_log->last_played.hour   = 0;
   runtime_log->last_played.minute = 0;
   runtime_log->last_played.second = 0;

   runtime_log->path[0]            = '\0';

   strlcpy(runtime_log->path, log_file_path, sizeof(runtime_log->path));

   /* Load existing log file, if it exists */
   if (path_is_valid(runtime_log->path))
      runtime_log_read_file(runtime_log);

   return runtime_log;
}

/* Setters */

/* Set runtime to specified hours, minutes, seconds value */
void runtime_log_set_runtime_hms(runtime_log_t *runtime_log, unsigned hours, unsigned minutes, unsigned seconds)
{
   retro_time_t usec;

   if (!runtime_log)
      return;

   /* Converting to usec and back again may be considered a
    * waste of CPU cycles, but this allows us to handle any
    * kind of broken input without issue - i.e. user can enter
    * minutes and seconds values > 59, and everything still
    * works correctly */
   runtime_log_convert_hms2usec(hours, minutes, seconds, &usec);

   runtime_log_convert_usec2hms(usec,
         &runtime_log->runtime.hours, &runtime_log->runtime.minutes, &runtime_log->runtime.seconds);
}

/* Set runtime to specified microseconds value */
void runtime_log_set_runtime_usec(runtime_log_t *runtime_log, retro_time_t usec)
{
   if (!runtime_log)
      return;

   runtime_log_convert_usec2hms(usec,
         &runtime_log->runtime.hours, &runtime_log->runtime.minutes, &runtime_log->runtime.seconds);
}

/* Adds specified hours, minutes, seconds value to current runtime */
void runtime_log_add_runtime_hms(runtime_log_t *runtime_log, unsigned hours, unsigned minutes, unsigned seconds)
{
   retro_time_t usec_old;
   retro_time_t usec_new;

   if (!runtime_log)
      return;

   runtime_log_convert_hms2usec(
         runtime_log->runtime.hours, runtime_log->runtime.minutes, runtime_log->runtime.seconds,
         &usec_old);

   runtime_log_convert_hms2usec(hours, minutes, seconds, &usec_new);

   runtime_log_convert_usec2hms(usec_old + usec_new,
         &runtime_log->runtime.hours, &runtime_log->runtime.minutes, &runtime_log->runtime.seconds);
}

/* Adds specified microseconds value to current runtime */
void runtime_log_add_runtime_usec(runtime_log_t *runtime_log, retro_time_t usec)
{
   retro_time_t usec_old;

   if (!runtime_log)
      return;

   runtime_log_convert_hms2usec(
         runtime_log->runtime.hours, runtime_log->runtime.minutes, runtime_log->runtime.seconds,
         &usec_old);

   runtime_log_convert_usec2hms(usec_old + usec,
         &runtime_log->runtime.hours, &runtime_log->runtime.minutes, &runtime_log->runtime.seconds);
}

/* Sets last played entry to specified value */
void runtime_log_set_last_played(runtime_log_t *runtime_log,
      unsigned year, unsigned month, unsigned day,
      unsigned hour, unsigned minute, unsigned second)
{
   if (!runtime_log)
      return;

   /* This function should never be needed, so just
    * perform dumb value assignment (i.e. no validation
    * using mktime()) */
   runtime_log->last_played.year   = year;
   runtime_log->last_played.month  = month;
   runtime_log->last_played.day    = day;
   runtime_log->last_played.hour   = hour;
   runtime_log->last_played.minute = minute;
   runtime_log->last_played.second = second;
}

/* Sets last played entry to current date/time */
void runtime_log_set_last_played_now(runtime_log_t *runtime_log)
{
   time_t current_time;
   struct tm time_info;

   if (!runtime_log)
      return;

   /* Get current time */
   time(&current_time);
   rtime_localtime(&current_time, &time_info);

   /* Extract values */
   runtime_log->last_played.year   = (unsigned)time_info.tm_year + 1900;
   runtime_log->last_played.month  = (unsigned)time_info.tm_mon + 1;
   runtime_log->last_played.day    = (unsigned)time_info.tm_mday;
   runtime_log->last_played.hour   = (unsigned)time_info.tm_hour;
   runtime_log->last_played.minute = (unsigned)time_info.tm_min;
   runtime_log->last_played.second = (unsigned)time_info.tm_sec;
}

/* Resets log to default (zero) values */
void runtime_log_reset(runtime_log_t *runtime_log)
{
   if (!runtime_log)
      return;

   runtime_log->runtime.hours      = 0;
   runtime_log->runtime.minutes    = 0;
   runtime_log->runtime.seconds    = 0;

   runtime_log->last_played.year   = 0;
   runtime_log->last_played.month  = 0;
   runtime_log->last_played.day    = 0;
   runtime_log->last_played.hour   = 0;
   runtime_log->last_played.minute = 0;
   runtime_log->last_played.second = 0;
}

/* Getters */

/* Gets runtime in hours, minutes, seconds */
void runtime_log_get_runtime_hms(runtime_log_t *runtime_log,
      unsigned *hours, unsigned *minutes, unsigned *seconds)
{
   if (!runtime_log)
      return;

   *hours   = runtime_log->runtime.hours;
   *minutes = runtime_log->runtime.minutes;
   *seconds = runtime_log->runtime.seconds;
}

/* Gets runtime in microseconds */
void runtime_log_get_runtime_usec(
      runtime_log_t *runtime_log, retro_time_t *usec)
{
   if (runtime_log)
      runtime_log_convert_hms2usec( runtime_log->runtime.hours,
            runtime_log->runtime.minutes, runtime_log->runtime.seconds,
            usec);
}

/* Gets runtime as a pre-formatted string */
void runtime_log_get_runtime_str(runtime_log_t *runtime_log, char *str, size_t len)
{
   int n = 0;

   if (runtime_log)
   {
      n = snprintf(str, len, "%s %02u:%02u:%02u",
            msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_RUNTIME),
            runtime_log->runtime.hours, runtime_log->runtime.minutes, runtime_log->runtime.seconds);
   }
   else
   {
      n = snprintf(str, len, "%s 00:00:00",
            msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_RUNTIME));
   }

   if ((n < 0) || (n >= 64))
      n = 0; /* Silence GCC warnings... */
}

/* Gets last played entry values */
void runtime_log_get_last_played(runtime_log_t *runtime_log,
      unsigned *year, unsigned *month, unsigned *day,
      unsigned *hour, unsigned *minute, unsigned *second)
{
   if (!runtime_log)
      return;

   *year   = runtime_log->last_played.year;
   *month  = runtime_log->last_played.month;
   *day    = runtime_log->last_played.day;
   *hour   = runtime_log->last_played.hour;
   *minute = runtime_log->last_played.minute;
   *second = runtime_log->last_played.second;
}

/* Gets last played entry values as a struct tm 'object'
 * (e.g. for printing with strftime()) */
void runtime_log_get_last_played_time(runtime_log_t *runtime_log, struct tm *time_info)
{
   if (!runtime_log || !time_info)
      return;

   /* Set tm values */
   time_info->tm_year  = (int)runtime_log->last_played.year  - 1900;
   time_info->tm_mon   = (int)runtime_log->last_played.month - 1;
   time_info->tm_mday  = (int)runtime_log->last_played.day;
   time_info->tm_hour  = (int)runtime_log->last_played.hour;
   time_info->tm_min   = (int)runtime_log->last_played.minute;
   time_info->tm_sec   = (int)runtime_log->last_played.second;
   time_info->tm_isdst = -1;

   /* Perform any required range adjustment + populate
    * missing entries */
   mktime(time_info);
}

static void last_played_strftime(runtime_log_t *runtime_log, char *str, size_t len, const char *format)
{
   struct tm time_info;
   char *local = NULL;

   if (!runtime_log)
      return;

   /* Get time */
   runtime_log_get_last_played_time(runtime_log, &time_info);

   /* Ensure correct locale is set */
   setlocale(LC_TIME, "");

   /* Generate string */
#if defined(__linux__) && !defined(ANDROID)
   strftime(str, len, format, &time_info);
#else
   strftime(str, len, format, &time_info);
   local = local_to_utf8_string_alloc(str);

   if (!string_is_empty(local))
      strlcpy(str, local, len);

   if (local)
   {
      free(local);
      local = NULL;
   }
#endif
}

/* Gets last played entry value as a pre-formatted string */
void runtime_log_get_last_played_str(runtime_log_t *runtime_log,
      char *str, size_t len,
      enum playlist_sublabel_last_played_style_type timedate_style,
      enum playlist_sublabel_last_played_date_separator_type date_separator)
{
   bool has_am_pm         = false;
   const char *format_str = "";
   int n                  = 0;
   char tmp[64];

   tmp[0] = '\0';

   if (runtime_log)
   {
      /* Handle 12-hour clock options
       * > These require extra work, due to AM/PM localisation */
      switch (timedate_style)
      {
         case PLAYLIST_LAST_PLAYED_STYLE_YMD_HMS_AMPM:
            has_am_pm = true;
            /* Using switch statements to set the format
             * string is verbose, but has far less performance
             * impact than setting the date separator dynamically
             * (i.e. no snprintf() or character replacement...) */
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = " %Y/%m/%d %I:%M:%S %p";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = " %Y.%m.%d %I:%M:%S %p";
                  break;
               default:
                  format_str = " %Y-%m-%d %I:%M:%S %p";
                  break;
            }
            break;
         case PLAYLIST_LAST_PLAYED_STYLE_YMD_HM_AMPM:
            has_am_pm = true;
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = " %Y/%m/%d %I:%M %p";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = " %Y.%m.%d %I:%M %p";
                  break;
               default:
                  format_str = " %Y-%m-%d %I:%M %p";
                  break;
            }
            break;
         case PLAYLIST_LAST_PLAYED_STYLE_MDYYYY_HMS_AMPM:
            has_am_pm = true;
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = " %m/%d/%Y %I:%M:%S %p";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = " %m.%d.%Y %I:%M:%S %p";
                  break;
               default:
                  format_str = " %m-%d-%Y %I:%M:%S %p";
                  break;
            }
            break;
         case PLAYLIST_LAST_PLAYED_STYLE_MDYYYY_HM_AMPM:
            has_am_pm = true;
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = " %m/%d/%Y %I:%M %p";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = " %m.%d.%Y %I:%M %p";
                  break;
               default:
                  format_str = " %m-%d-%Y %I:%M %p";
                  break;
            }
            break;
         case PLAYLIST_LAST_PLAYED_STYLE_MD_HM_AMPM:
            has_am_pm = true;
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = " %m/%d %I:%M %p";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = " %m.%d %I:%M %p";
                  break;
               default:
                  format_str = " %m-%d %I:%M %p";
                  break;
            }
            break;
         case PLAYLIST_LAST_PLAYED_STYLE_DDMMYYYY_HMS_AMPM:
            has_am_pm = true;
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = " %d/%m/%Y %I:%M:%S %p";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = " %d.%m.%Y %I:%M:%S %p";
                  break;
               default:
                  format_str = " %d-%m-%Y %I:%M:%S %p";
                  break;
            }
            break;
         case PLAYLIST_LAST_PLAYED_STYLE_DDMMYYYY_HM_AMPM:
            has_am_pm = true;
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = " %d/%m/%Y %I:%M %p";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = " %d.%m.%Y %I:%M %p";
                  break;
               default:
                  format_str = " %d-%m-%Y %I:%M %p";
                  break;
            }
            break;
         case PLAYLIST_LAST_PLAYED_STYLE_DDMM_HM_AMPM:
            has_am_pm = true;
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = " %d/%m %I:%M %p";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = " %d.%m %I:%M %p";
                  break;
               default:
                  format_str = " %d-%m %I:%M %p";
                  break;
            }
            break;
         default:
            has_am_pm = false;
            break;
      }

      if (has_am_pm)
      {
         last_played_strftime(runtime_log, tmp, sizeof(tmp), format_str);
         strlcpy(str, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED), len);
         strlcat(str, tmp, len);
         return;
      }

      /* Handle non-12-hour clock options */
      switch (timedate_style)
      {
         case PLAYLIST_LAST_PLAYED_STYLE_YMD_HM:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %04u/%02u/%02u %02u:%02u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %04u.%02u.%02u %02u:%02u";
                  break;
               default:
                  format_str = "%s %04u-%02u-%02u %02u:%02u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.year, runtime_log->last_played.month, runtime_log->last_played.day,
                  runtime_log->last_played.hour, runtime_log->last_played.minute);
            return;
         case PLAYLIST_LAST_PLAYED_STYLE_YMD:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %04u/%02u/%02u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %04u.%02u.%02u";
                  break;
               default:
                  format_str = "%s %04u-%02u-%02u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.year, runtime_log->last_played.month, runtime_log->last_played.day);
            return;
         case PLAYLIST_LAST_PLAYED_STYLE_YM:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %04u/%02u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %04u.%02u";
                  break;
               default:
                  format_str = "%s %04u-%02u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.year, runtime_log->last_played.month);
            return;
         case PLAYLIST_LAST_PLAYED_STYLE_MDYYYY_HMS:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %02u/%02u/%04u %02u:%02u:%02u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %02u.%02u.%04u %02u:%02u:%02u";
                  break;
               default:
                  format_str = "%s %02u-%02u-%04u %02u:%02u:%02u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.month, runtime_log->last_played.day, runtime_log->last_played.year,
                  runtime_log->last_played.hour, runtime_log->last_played.minute, runtime_log->last_played.second);
            return;
         case PLAYLIST_LAST_PLAYED_STYLE_MDYYYY_HM:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %02u/%02u/%04u %02u:%02u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %02u.%02u.%04u %02u:%02u";
                  break;
               default:
                  format_str = "%s %02u-%02u-%04u %02u:%02u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.month, runtime_log->last_played.day, runtime_log->last_played.year,
                  runtime_log->last_played.hour, runtime_log->last_played.minute);
            return;
         case PLAYLIST_LAST_PLAYED_STYLE_MD_HM:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %02u/%02u %02u:%02u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %02u.%02u %02u:%02u";
                  break;
               default:
                  format_str = "%s %02u-%02u %02u:%02u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.month, runtime_log->last_played.day,
                  runtime_log->last_played.hour, runtime_log->last_played.minute);
            return;
         case PLAYLIST_LAST_PLAYED_STYLE_MDYYYY:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %02u/%02u/%04u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %02u.%02u.%04u";
                  break;
               default:
                  format_str = "%s %02u-%02u-%04u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.month, runtime_log->last_played.day, runtime_log->last_played.year);
            return;
         case PLAYLIST_LAST_PLAYED_STYLE_MD:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %02u/%02u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %02u.%02u";
                  break;
               default:
                  format_str = "%s %02u-%02u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.month, runtime_log->last_played.day);
            return;
         case PLAYLIST_LAST_PLAYED_STYLE_DDMMYYYY_HMS:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %02u/%02u/%04u %02u:%02u:%02u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %02u.%02u.%04u %02u:%02u:%02u";
                  break;
               default:
                  format_str = "%s %02u-%02u-%04u %02u:%02u:%02u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.day, runtime_log->last_played.month, runtime_log->last_played.year,
                  runtime_log->last_played.hour, runtime_log->last_played.minute, runtime_log->last_played.second);
            return;
         case PLAYLIST_LAST_PLAYED_STYLE_DDMMYYYY_HM:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %02u/%02u/%04u %02u:%02u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %02u.%02u.%04u %02u:%02u";
                  break;
               default:
                  format_str = "%s %02u-%02u-%04u %02u:%02u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.day, runtime_log->last_played.month, runtime_log->last_played.year,
                  runtime_log->last_played.hour, runtime_log->last_played.minute);
            return;
         case PLAYLIST_LAST_PLAYED_STYLE_DDMM_HM:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %02u/%02u %02u:%02u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %02u.%02u %02u:%02u";
                  break;
               default:
                  format_str = "%s %02u-%02u %02u:%02u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.day, runtime_log->last_played.month,
                  runtime_log->last_played.hour, runtime_log->last_played.minute);
            return;
         case PLAYLIST_LAST_PLAYED_STYLE_DDMMYYYY:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %02u/%02u/%04u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %02u.%02u.%04u";
                  break;
               default:
                  format_str = "%s %02u-%02u-%04u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.day, runtime_log->last_played.month, runtime_log->last_played.year);
            return;
         case PLAYLIST_LAST_PLAYED_STYLE_DDMM:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %02u/%02u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %02u.%02u";
                  break;
               default:
                  format_str = "%s %02u-%02u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.day, runtime_log->last_played.month);
            return;
         case PLAYLIST_LAST_PLAYED_STYLE_YMD_HMS:
         default:
            switch (date_separator)
            {
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_SLASH:
                  format_str = "%s %04u/%02u/%02u %02u:%02u:%02u";
                  break;
               case PLAYLIST_LAST_PLAYED_DATE_SEPARATOR_PERIOD:
                  format_str = "%s %04u.%02u.%02u %02u:%02u:%02u";
                  break;
               default:
                  format_str = "%s %04u-%02u-%02u %02u:%02u:%02u";
                  break;
            }
            n = snprintf(str, len, format_str,
                  msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED),
                  runtime_log->last_played.year, runtime_log->last_played.month, runtime_log->last_played.day,
                  runtime_log->last_played.hour, runtime_log->last_played.minute, runtime_log->last_played.second);
            return;
      }
   }
   else
   {
      n = strlcpy(str, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_SUBLABEL_LAST_PLAYED), len);
      str[n  ]    = ' ';
      str[n+1]    = '\0';
      n = strlcat(str, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PLAYLIST_INLINE_CORE_DISPLAY_NEVER), len);
   }

   if ((n < 0) || (n >= 64))
      n = 0; /* Silence GCC warnings... */
}

/* Status */

/* Returns true if log has a non-zero runtime entry */
bool runtime_log_has_runtime(runtime_log_t *runtime_log)
{
   if (!runtime_log)
      return false;

   return !((runtime_log->runtime.hours   == 0) &&
            (runtime_log->runtime.minutes == 0) &&
            (runtime_log->runtime.seconds == 0));
}

/* Returns true if log has a non-zero last played entry */
bool runtime_log_has_last_played(runtime_log_t *runtime_log)
{
   if (!runtime_log)
      return false;

   return !((runtime_log->last_played.year   == 0) &&
            (runtime_log->last_played.month  == 0) &&
            (runtime_log->last_played.day    == 0) &&
            (runtime_log->last_played.hour   == 0) &&
            (runtime_log->last_played.minute == 0) &&
            (runtime_log->last_played.second == 0));
}

/* Saving */

/* Saves specified runtime log to disk */
void runtime_log_save(runtime_log_t *runtime_log)
{
   int n;
   char value_string[64]; /* 64 characters should be
                             enough for a very long runtime... :) */
   RtlJSONContext context = {0};
   RFILE *file            = NULL;

   if (!runtime_log)
      return;

   RARCH_LOG("Saving runtime log file: %s\n", runtime_log->path);

   /* Attempt to open log file */
   file = filestream_open(runtime_log->path,
         RETRO_VFS_FILE_ACCESS_WRITE, RETRO_VFS_FILE_ACCESS_HINT_NONE);

   if (!file)
   {
      RARCH_ERR("Failed to open runtime log file: %s\n", runtime_log->path);
      return;
   }

   /* Initialise JSON writer */
   context.writer = JSON_Writer_Create(NULL);
   context.file   = file;

   if (!context.writer)
   {
      RARCH_ERR("Failed to create JSON writer.\n");
      goto end;
   }

   /* Configure JSON writer */
   JSON_Writer_SetOutputEncoding(context.writer, JSON_UTF8);
   JSON_Writer_SetOutputHandler(context.writer, &RtlJSONOutputHandler);
   JSON_Writer_SetUserData(context.writer, &context);

   /* Write output file */
   JSON_Writer_WriteStartObject(context.writer);
   JSON_Writer_WriteNewLine(context.writer);

   /* > Version entry */
   JSON_Writer_WriteSpace(context.writer, 2);
   JSON_Writer_WriteString(context.writer, "version",
         STRLEN_CONST("version"), JSON_UTF8);
   JSON_Writer_WriteColon(context.writer);
   JSON_Writer_WriteSpace(context.writer, 1);
   JSON_Writer_WriteString(context.writer, "1.0",
         STRLEN_CONST("1.0"), JSON_UTF8);
   JSON_Writer_WriteComma(context.writer);
   JSON_Writer_WriteNewLine(context.writer);

   /* > Runtime entry */
   value_string[0] = '\0';
   n               = snprintf(value_string,
         sizeof(value_string), LOG_FILE_RUNTIME_FORMAT_STR,
         runtime_log->runtime.hours, runtime_log->runtime.minutes,
         runtime_log->runtime.seconds);
   if ((n < 0) || (n >= 64))
      n = 0; /* Silence GCC warnings... */

   JSON_Writer_WriteSpace(context.writer, 2);
   JSON_Writer_WriteString(context.writer, "runtime",
         STRLEN_CONST("runtime"), JSON_UTF8);
   JSON_Writer_WriteColon(context.writer);
   JSON_Writer_WriteSpace(context.writer, 1);
   JSON_Writer_WriteString(context.writer, value_string,
         strlen(value_string), JSON_UTF8);
   JSON_Writer_WriteComma(context.writer);
   JSON_Writer_WriteNewLine(context.writer);

   /* > Last played entry */
   value_string[0] = '\0';
   n               = snprintf(value_string, sizeof(value_string),
         LOG_FILE_LAST_PLAYED_FORMAT_STR,
         runtime_log->last_played.year, runtime_log->last_played.month,
         runtime_log->last_played.day,
         runtime_log->last_played.hour, runtime_log->last_played.minute,
         runtime_log->last_played.second);
   if ((n < 0) || (n >= 64))
      n = 0; /* Silence GCC warnings... */

   JSON_Writer_WriteSpace(context.writer, 2);
   JSON_Writer_WriteString(context.writer, "last_played",
         STRLEN_CONST("last_played"), JSON_UTF8);
   JSON_Writer_WriteColon(context.writer);
   JSON_Writer_WriteSpace(context.writer, 1);
   JSON_Writer_WriteString(context.writer, value_string,
         strlen(value_string), JSON_UTF8);
   JSON_Writer_WriteNewLine(context.writer);

   /* > Finalise */
   JSON_Writer_WriteEndObject(context.writer);
   JSON_Writer_WriteNewLine(context.writer);

   /* Free JSON writer */
   JSON_Writer_Free(context.writer);

end:
   /* Close log file */
   filestream_close(file);
}

/* Utility functions */

/* Convert from hours, minutes, seconds to microseconds */
void runtime_log_convert_hms2usec(unsigned hours,
      unsigned minutes, unsigned seconds, retro_time_t *usec)
{
   *usec = ((retro_time_t)hours   * 60 * 60 * 1000000) +
           ((retro_time_t)minutes * 60      * 1000000) +
           ((retro_time_t)seconds           * 1000000);
}

/* Convert from microseconds to hours, minutes, seconds */
void runtime_log_convert_usec2hms(retro_time_t usec,
      unsigned *hours, unsigned *minutes, unsigned *seconds)
{
   *seconds  = (unsigned)(usec / 1000000);
   *minutes  = *seconds / 60;
   *hours    = *minutes / 60;

   *seconds -= *minutes * 60;
   *minutes -= *hours * 60;
}

/* Playlist manipulation */

/* Updates specified playlist entry runtime values with
 * contents of associated log file */
void runtime_update_playlist(
      playlist_t *playlist, size_t idx,
      const char *dir_runtime_log,
      const char *dir_playlist,
      bool log_per_core,
      enum playlist_sublabel_last_played_style_type timedate_style,
      enum playlist_sublabel_last_played_date_separator_type date_separator)
{
   char runtime_str[64];
   char last_played_str[64];
   runtime_log_t *runtime_log             = NULL;
   const struct playlist_entry *entry     = NULL;
   struct playlist_entry update_entry     = {0};
#if defined(HAVE_MENU) && (defined(HAVE_OZONE) || defined(HAVE_MATERIALUI))
   const char *menu_ident                 = menu_driver_ident();
#endif

   /* Sanity check */
   if (!playlist)
      return;

   if (idx >= playlist_get_size(playlist))
      return;

   /* Set fallback playlist 'runtime_status'
    * (saves 'if' checks later...) */
   update_entry.runtime_status = PLAYLIST_RUNTIME_MISSING;

   /* 'Attach' runtime/last played strings */
   runtime_str[0]               = '\0';
   last_played_str[0]           = '\0';
   update_entry.runtime_str     = runtime_str;
   update_entry.last_played_str = last_played_str;

   /* Read current playlist entry */
   playlist_get_index(playlist, idx, &entry);

   /* Attempt to open log file */
   runtime_log = runtime_log_init(
         entry->path,
         entry->core_path,
         dir_runtime_log,
         dir_playlist,
         log_per_core);

   if (runtime_log)
   {
      /* Check whether a non-zero runtime has been recorded */
      if (runtime_log_has_runtime(runtime_log))
      {
         /* Read current runtime */
         runtime_log_get_runtime_hms(runtime_log,
               &update_entry.runtime_hours, &update_entry.runtime_minutes, &update_entry.runtime_seconds);

         runtime_log_get_runtime_str(runtime_log, runtime_str, sizeof(runtime_str));

         /* Read last played timestamp */
         runtime_log_get_last_played(runtime_log,
               &update_entry.last_played_year, &update_entry.last_played_month, &update_entry.last_played_day,
               &update_entry.last_played_hour, &update_entry.last_played_minute, &update_entry.last_played_second);

         runtime_log_get_last_played_str(runtime_log,
               last_played_str, sizeof(last_played_str), timedate_style, date_separator);

         /* Playlist entry now contains valid runtime data */
         update_entry.runtime_status = PLAYLIST_RUNTIME_VALID;
      }

      /* Clean up */
      free(runtime_log);
   }

#if defined(HAVE_MENU) && (defined(HAVE_OZONE) || defined(HAVE_MATERIALUI))
   /* Ozone and GLUI require runtime/last played strings
    * to be populated even when no runtime is recorded */
   if (update_entry.runtime_status != PLAYLIST_RUNTIME_VALID)
   {
      if (string_is_equal(menu_ident, "ozone") ||
          string_is_equal(menu_ident, "glui"))
      {
         runtime_log_get_runtime_str(NULL, runtime_str, sizeof(runtime_str));
         runtime_log_get_last_played_str(NULL, last_played_str, sizeof(last_played_str),
               timedate_style, date_separator);

         /* While runtime data does not exist, the playlist
          * entry does now contain valid information... */
         update_entry.runtime_status = PLAYLIST_RUNTIME_VALID;
      }
   }
#endif

   /* Update playlist */
   playlist_update_runtime(playlist, idx, &update_entry, false);
}