/*  RetroArch - A frontend for libretro.
 *  Copyright (C) 2010-2014 - Hans-Kristian Arntzen
 *  Copyright (C) 2011-2017 - Daniel De Matteis
 *  Copyright (C) 2016-2019 - Brad Parker
 *
 *  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 <retro_assert.h>
#include <compat/strl.h>
#include <string/stdstring.h>
#include <file/config_file.h>
#include <file/file_path.h>
#include <streams/file_stream.h>
#include <streams/interface_stream.h>
#include <formats/rjson.h>
#include <lists/dir_list.h>
#include <file/archive_file.h>

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "retroarch.h"
#include "verbosity.h"

#include "core_info.h"
#include "file_path_special.h"

#if defined(__WINRT__) || defined(WINAPI_FAMILY) && WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP
#include "uwp/uwp_func.h"
#endif

#if defined(ANDROID)
#include "play_feature_delivery/play_feature_delivery.h"
#endif

/*************************/
/* Core Info Cache START */
/*************************/

#define CORE_INFO_CACHE_DEFAULT_CAPACITY 8

/* TODO/FIXME: Apparently rzip compression is an issue on UWP */
#if defined(HAVE_ZLIB) && !(defined(__WINRT__) || defined(WINAPI_FAMILY) && WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP)
#define CORE_INFO_CACHE_COMPRESS
#endif

typedef struct
{
   core_info_t *items;
   size_t length;
   size_t capacity;
   bool refresh;
} core_info_cache_list_t;

typedef struct
{
   core_info_t *core_info;
   core_info_cache_list_t *core_info_cache_list;
   char **current_string_val;
   struct string_list **current_string_list_val;
   uint32_t *current_entry_uint_val;
   bool *current_entry_bool_val;
   unsigned array_depth;
   unsigned object_depth;
   bool to_core_file_id;
   bool to_firmware;
} CCJSONContext;

/* Forward declarations */
static void core_info_free(core_info_t* info);
static uint32_t core_info_hash_string(const char *str);
static core_info_cache_list_t *core_info_cache_list_new(void);
static void core_info_cache_add(core_info_cache_list_t *list, core_info_t *info,
      bool transfer);

/* JSON Handlers START */

static bool CCJSONObjectMemberHandler(void *context, const char *pValue, size_t length)
{
   CCJSONContext *pCtx = (CCJSONContext *)context;

   if ((pCtx->object_depth == 2) && (pCtx->array_depth == 1) && length)
   {
      pCtx->current_string_val      = NULL;
      pCtx->current_string_list_val = NULL;
      pCtx->current_entry_uint_val  = NULL;
      pCtx->current_entry_bool_val  = NULL;
      pCtx->to_core_file_id         = false;
      pCtx->to_firmware             = false;

      switch (pValue[0])
      {
         case 'a':
            if (string_is_equal(pValue,      "authors"))
            {
               pCtx->current_string_val      = &pCtx->core_info->authors;
               pCtx->current_string_list_val = &pCtx->core_info->authors_list;
            }
            break;
         case 'c':
            if (string_is_equal(pValue,      "categories"))
            {
               pCtx->current_string_val      = &pCtx->core_info->categories;
               pCtx->current_string_list_val = &pCtx->core_info->categories_list;
            }
            else if (string_is_equal(pValue, "core_name"))
               pCtx->current_string_val      = &pCtx->core_info->core_name;
            else if (string_is_equal(pValue, "core_file_id"))
               pCtx->to_core_file_id         = true;
            break;
         case 'd':
            if (string_is_equal(pValue,      "display_name"))
               pCtx->current_string_val      = &pCtx->core_info->display_name;
            else if (string_is_equal(pValue, "display_version"))
               pCtx->current_string_val      = &pCtx->core_info->display_version;
            else if (string_is_equal(pValue, "databases"))
            {
               pCtx->current_string_val      = &pCtx->core_info->databases;
               pCtx->current_string_list_val = &pCtx->core_info->databases_list;
            }
            else if (string_is_equal(pValue, "description"))
               pCtx->current_string_val      = &pCtx->core_info->description;
            else if (string_is_equal(pValue, "database_match_archive_member"))
               pCtx->current_entry_bool_val  = &pCtx->core_info->database_match_archive_member;
            break;
         case 'f':
            if (string_is_equal(pValue,      "firmware"))
               pCtx->to_firmware             = true;
            break;
         case 'h':
            if (string_is_equal(pValue,      "has_info"))
               pCtx->current_entry_bool_val  = &pCtx->core_info->has_info;
            break;
         case 'l':
            if (string_is_equal(pValue,      "licenses"))
            {
               pCtx->current_string_val      = &pCtx->core_info->licenses;
               pCtx->current_string_list_val = &pCtx->core_info->licenses_list;
            }
            else if (string_is_equal(pValue, "is_experimental"))
               pCtx->current_entry_bool_val  = &pCtx->core_info->is_experimental;
            break;
         case 'n':
            if (string_is_equal(pValue,      "notes"))
            {
               pCtx->current_string_val      = &pCtx->core_info->notes;
               pCtx->current_string_list_val = &pCtx->core_info->note_list;
            }
            break;
         case 'p':
            if (string_is_equal(pValue,      "path"))
               pCtx->current_string_val      = &pCtx->core_info->path;
            else if (string_is_equal(pValue, "permissions"))
            {
               pCtx->current_string_val      = &pCtx->core_info->permissions;
               pCtx->current_string_list_val = &pCtx->core_info->permissions_list;
            }
            break;
         case 'r':
            if (string_is_equal(pValue,      "required_hw_api"))
            {
               pCtx->current_string_val      = &pCtx->core_info->required_hw_api;
               pCtx->current_string_list_val = &pCtx->core_info->required_hw_api_list;
            }
            break;
         case 's':
            if (string_is_equal(pValue,      "system_manufacturer"))
               pCtx->current_string_val      = &pCtx->core_info->system_manufacturer;
            else if (string_is_equal(pValue, "systemname"))
               pCtx->current_string_val      = &pCtx->core_info->systemname;
            else if (string_is_equal(pValue, "system_id"))
               pCtx->current_string_val      = &pCtx->core_info->system_id;
            else if (string_is_equal(pValue, "supported_extensions"))
            {
               pCtx->current_string_val      = &pCtx->core_info->supported_extensions;
               pCtx->current_string_list_val = &pCtx->core_info->supported_extensions_list;
            }
            else if (string_is_equal(pValue, "supports_no_game"))
               pCtx->current_entry_bool_val  = &pCtx->core_info->supports_no_game;
            break;
      }
   }
   else if ((pCtx->object_depth == 3) && (pCtx->array_depth == 1) && length)
   {
      pCtx->current_string_val      = NULL;
      pCtx->current_entry_uint_val  = NULL;

      if (pCtx->to_core_file_id)
      {
         if (string_is_equal(pValue,         "str"))
            pCtx->current_string_val         = &pCtx->core_info->core_file_id.str;
         else if (string_is_equal(pValue,    "hash"))
            pCtx->current_entry_uint_val     = &pCtx->core_info->core_file_id.hash;
      }
   }
   else if ((pCtx->object_depth == 3) && (pCtx->array_depth == 2) && length)
   {
      pCtx->current_string_val      = NULL;
      pCtx->current_entry_bool_val  = NULL;

      if (pCtx->to_firmware && (pCtx->core_info->firmware_count > 0))
      {
         size_t firmware_idx = pCtx->core_info->firmware_count - 1;

         if (string_is_equal(pValue,         "path"))
            pCtx->current_string_val         = &pCtx->core_info->firmware[firmware_idx].path;
         else if (string_is_equal(pValue,    "desc"))
            pCtx->current_string_val         = &pCtx->core_info->firmware[firmware_idx].desc;
         else if (string_is_equal(pValue,    "optional"))
            pCtx->current_entry_bool_val     = &pCtx->core_info->firmware[firmware_idx].optional;
      }
   }

   return true;
}

static bool CCJSONStringHandler(void *context, const char *pValue, size_t length)
{
   CCJSONContext *pCtx = (CCJSONContext*)context;

   if (pCtx->current_string_val && length && !string_is_empty(pValue))
   {
      if (*pCtx->current_string_val)
         free(*pCtx->current_string_val);
      *pCtx->current_string_val = strdup(pValue);

      if (pCtx->current_string_list_val)
      {
         if (*pCtx->current_string_list_val)
            string_list_free(*pCtx->current_string_list_val);
         *pCtx->current_string_list_val = string_split(*pCtx->current_string_val, "|");
      }
   }

   pCtx->current_string_val      = NULL;
   pCtx->current_string_list_val = NULL;

   return true;
}

static bool CCJSONNumberHandler(void *context, const char *pValue, size_t length)
{
   CCJSONContext *pCtx = (CCJSONContext*)context;

   if (pCtx->current_entry_uint_val)
      *pCtx->current_entry_uint_val = string_to_unsigned(pValue);

   pCtx->current_entry_uint_val = NULL;

   return true;
}

static bool CCJSONBoolHandler(void *context, bool value)
{
   CCJSONContext *pCtx = (CCJSONContext *)context;

   if (pCtx->current_entry_bool_val)
      *pCtx->current_entry_bool_val = value;

   pCtx->current_entry_bool_val = NULL;

   return true;
}

static bool CCJSONStartObjectHandler(void *context)
{
   CCJSONContext *pCtx = (CCJSONContext*)context;

   pCtx->object_depth++;

   if ((pCtx->object_depth == 1) && (pCtx->array_depth == 0))
   {
      if (pCtx->core_info_cache_list)
         return false;

      pCtx->core_info_cache_list = core_info_cache_list_new();
      if (!pCtx->core_info_cache_list)
         return false;
   }
   else if ((pCtx->object_depth == 2) && (pCtx->array_depth == 1))
   {
      if (pCtx->core_info)
      {
         core_info_free(pCtx->core_info);
         free(pCtx->core_info);
         pCtx->core_info = NULL;
      }

      pCtx->core_info = (core_info_t*)calloc(1, sizeof(core_info_t));
      if (!pCtx->core_info)
         return false;
   }
   else if ((pCtx->object_depth == 3) && (pCtx->array_depth == 2))
   {
      if (pCtx->to_firmware)
      {
         size_t new_idx                     = pCtx->core_info->firmware_count;
         core_info_firmware_t *firmware_tmp = (core_info_firmware_t*)
               realloc(pCtx->core_info->firmware,
                     (pCtx->core_info->firmware_count + 1) * sizeof(core_info_firmware_t));

         if (!firmware_tmp)
            return false;

         firmware_tmp[new_idx].path     = NULL;
         firmware_tmp[new_idx].desc     = NULL;
         firmware_tmp[new_idx].missing  = false;
         firmware_tmp[new_idx].optional = false;

         pCtx->core_info->firmware      = firmware_tmp;
         pCtx->core_info->firmware_count++;
      }
   }

   return true;
}

static bool CCJSONEndObjectHandler(void *context)
{
   CCJSONContext *pCtx = (CCJSONContext*)context;

   if ((pCtx->object_depth == 2) && (pCtx->array_depth == 1) && pCtx->core_info)
   {
      core_info_cache_add(pCtx->core_info_cache_list, pCtx->core_info, true);
      free(pCtx->core_info);
      pCtx->core_info = NULL;
   }
   else if ((pCtx->object_depth == 3) && (pCtx->array_depth == 1))
      pCtx->to_core_file_id = false;

   retro_assert(pCtx->object_depth > 0);
   pCtx->object_depth--;

   return true;
}

static bool CCJSONStartArrayHandler(void *context)
{
   CCJSONContext *pCtx = (CCJSONContext*)context;
   pCtx->array_depth++;
   return true;
}

static bool CCJSONEndArrayHandler(void *context)
{
   CCJSONContext *pCtx = (CCJSONContext*)context;

   if ((pCtx->object_depth == 2) && (pCtx->array_depth == 2))
      pCtx->to_firmware = false;

   retro_assert(pCtx->array_depth > 0);
   pCtx->array_depth--;

   return true;
}

/* JSON Handlers END */

/* Note: 'dst' must be zero initialised, or memory
 * leaks will occur */
static void core_info_copy(core_info_t *src, core_info_t *dst)
{
   dst->path                      = src->path                 ? strdup(src->path)                 : NULL;
   dst->display_name              = src->display_name         ? strdup(src->display_name)         : NULL;
   dst->display_version           = src->display_version      ? strdup(src->display_version)      : NULL;
   dst->core_name                 = src->core_name            ? strdup(src->core_name)            : NULL;
   dst->system_manufacturer       = src->system_manufacturer  ? strdup(src->system_manufacturer)  : NULL;
   dst->systemname                = src->systemname           ? strdup(src->systemname)           : NULL;
   dst->system_id                 = src->system_id            ? strdup(src->system_id)            : NULL;
   dst->supported_extensions      = src->supported_extensions ? strdup(src->supported_extensions) : NULL;
   dst->authors                   = src->authors              ? strdup(src->authors)              : NULL;
   dst->permissions               = src->permissions          ? strdup(src->permissions)          : NULL;
   dst->licenses                  = src->licenses             ? strdup(src->licenses)             : NULL;
   dst->categories                = src->categories           ? strdup(src->categories)           : NULL;
   dst->databases                 = src->databases            ? strdup(src->databases)            : NULL;
   dst->notes                     = src->notes                ? strdup(src->notes)                : NULL;
   dst->required_hw_api           = src->required_hw_api      ? strdup(src->required_hw_api)      : NULL;
   dst->description               = src->description          ? strdup(src->description)          : NULL;

   dst->categories_list           = src->categories_list           ? string_list_clone(src->categories_list)           : NULL;
   dst->databases_list            = src->databases_list            ? string_list_clone(src->databases_list)            : NULL;
   dst->note_list                 = src->note_list                 ? string_list_clone(src->note_list)                 : NULL;
   dst->supported_extensions_list = src->supported_extensions_list ? string_list_clone(src->supported_extensions_list) : NULL;
   dst->authors_list              = src->authors_list              ? string_list_clone(src->authors_list)              : NULL;
   dst->permissions_list          = src->permissions_list          ? string_list_clone(src->permissions_list)          : NULL;
   dst->licenses_list             = src->licenses_list             ? string_list_clone(src->licenses_list)             : NULL;
   dst->required_hw_api_list      = src->required_hw_api_list      ? string_list_clone(src->required_hw_api_list)      : NULL;

   if (src->firmware_count > 0)
   {
      dst->firmware = (core_info_firmware_t*)calloc(src->firmware_count,
            sizeof(core_info_firmware_t));

      if (dst->firmware)
      {
         size_t i;

         dst->firmware_count = src->firmware_count;

         for (i = 0; i < src->firmware_count; i++)
         {
            dst->firmware[i].path     = src->firmware[i].path ? strdup(src->firmware[i].path) : NULL;
            dst->firmware[i].desc     = src->firmware[i].desc ? strdup(src->firmware[i].desc) : NULL;
            dst->firmware[i].missing  = src->firmware[i].missing;
            dst->firmware[i].optional = src->firmware[i].optional;
         }
      }
      else
         dst->firmware_count = 0;
   }

   dst->core_file_id.str  = src->core_file_id.str ? strdup(src->core_file_id.str) : NULL;
   dst->core_file_id.hash = src->core_file_id.hash;

   dst->has_info                      = src->has_info;
   dst->supports_no_game              = src->supports_no_game;
   dst->database_match_archive_member = src->database_match_archive_member;
   dst->is_experimental               = src->is_experimental;
   dst->is_locked                     = src->is_locked;
   dst->is_installed                  = src->is_installed;
}

/* Like core_info_copy, but transfers 'ownership'
 * of internal objects/data structures from 'src'
 * to 'dst' */
static void core_info_transfer(core_info_t *src, core_info_t *dst)
{
   dst->path                      = src->path;
   src->path                      = NULL;

   dst->display_name              = src->display_name;
   src->display_name              = NULL;

   dst->display_version           = src->display_version;
   src->display_version           = NULL;

   dst->core_name                 = src->core_name;
   src->core_name                 = NULL;

   dst->system_manufacturer       = src->system_manufacturer;
   src->system_manufacturer       = NULL;

   dst->systemname                = src->systemname;
   src->systemname                = NULL;

   dst->system_id                 = src->system_id;
   src->system_id                 = NULL;

   dst->supported_extensions      = src->supported_extensions;
   src->supported_extensions      = NULL;

   dst->authors                   = src->authors;
   src->authors                   = NULL;

   dst->permissions               = src->permissions;
   src->permissions               = NULL;

   dst->licenses                  = src->licenses;
   src->licenses                  = NULL;

   dst->categories                = src->categories;
   src->categories                = NULL;

   dst->databases                 = src->databases;
   src->databases                 = NULL;

   dst->notes                     = src->notes;
   src->notes                     = NULL;

   dst->required_hw_api           = src->required_hw_api;
   src->required_hw_api           = NULL;

   dst->description               = src->description;
   src->description               = NULL;

   dst->categories_list           = src->categories_list;
   src->categories_list           = NULL;

   dst->databases_list            = src->databases_list;
   src->databases_list            = NULL;

   dst->note_list                 = src->note_list;
   src->note_list                 = NULL;

   dst->supported_extensions_list = src->supported_extensions_list;
   src->supported_extensions_list = NULL;

   dst->authors_list              = src->authors_list;
   src->authors_list              = NULL;

   dst->permissions_list          = src->permissions_list;
   src->permissions_list          = NULL;

   dst->licenses_list             = src->licenses_list;
   src->licenses_list             = NULL;

   dst->required_hw_api_list      = src->required_hw_api_list;
   src->required_hw_api_list      = NULL;

   dst->firmware                  = src->firmware;
   dst->firmware_count            = src->firmware_count;
   src->firmware                  = NULL;
   src->firmware_count            = 0;

   dst->core_file_id.str          = src->core_file_id.str;
   src->core_file_id.str          = NULL;
   dst->core_file_id.hash         = src->core_file_id.hash;

   dst->has_info                      = src->has_info;
   dst->supports_no_game              = src->supports_no_game;
   dst->database_match_archive_member = src->database_match_archive_member;
   dst->is_experimental               = src->is_experimental;
   dst->is_locked                     = src->is_locked;
   dst->is_installed                  = src->is_installed;
}

static void core_info_cache_list_free(core_info_cache_list_t *core_info_cache_list)
{
   size_t i;

   if (!core_info_cache_list)
      return;

   for (i = 0; i < core_info_cache_list->length; i++)
   {
      core_info_t* info = (core_info_t*)&core_info_cache_list->items[i];
      core_info_free(info);
   }

   free(core_info_cache_list->items);
   free(core_info_cache_list);
}

static core_info_cache_list_t *core_info_cache_list_new(void)
{
   core_info_cache_list_t *core_info_cache_list = NULL;

   core_info_cache_list = (core_info_cache_list_t *)malloc(sizeof(*core_info_cache_list));
   if (!core_info_cache_list)
      return NULL;

   core_info_cache_list->length = 0;
   core_info_cache_list->items  = (core_info_t *)calloc(CORE_INFO_CACHE_DEFAULT_CAPACITY,
         sizeof(core_info_t));

   if (!core_info_cache_list->items)
   {
      core_info_cache_list_free(core_info_cache_list);
      return NULL;
   }

   core_info_cache_list->capacity = CORE_INFO_CACHE_DEFAULT_CAPACITY;
   core_info_cache_list->refresh  = false;

   return core_info_cache_list;
}

static core_info_t *core_info_cache_find(core_info_cache_list_t *list, char *core_file_id)
{
   uint32_t hash;
   size_t i;

   if (!list ||
       string_is_empty(core_file_id))
      return NULL;

   hash = core_info_hash_string(core_file_id);

   for (i = 0; i < list->length; i++)
   {
      core_info_t *info = (core_info_t*)&list->items[i];

      if (!info)
         continue;

      if ((info->core_file_id.hash == hash) &&
          string_is_equal(info->core_file_id.str, core_file_id))
      {
         info->is_installed = true;
         return info;
      }
   }

   return NULL;
}

static void core_info_cache_add(core_info_cache_list_t *list, core_info_t *info,
      bool transfer)
{
   core_info_t *info_cache = NULL;

   if (!list ||
       !info ||
       (info->core_file_id.hash == 0) ||
       string_is_empty(info->core_file_id.str))
      return;

   if (list->length >= list->capacity)
   {
      size_t prev_capacity   = list->capacity;
      core_info_t *items_tmp = (core_info_t*)realloc(list->items,
            (list->capacity << 1) * sizeof(core_info_t));

      if (!items_tmp)
         return;

      list->capacity = list->capacity << 1;
      list->items    = items_tmp;

      memset(&list->items[prev_capacity], 0,
            (list->capacity - prev_capacity) * sizeof(core_info_t));
   }

   info_cache = (core_info_t*)&list->items[list->length];

   if (transfer)
      core_info_transfer(info, info_cache);
   else
      core_info_copy(info, info_cache);

   list->length++;
}

static core_info_cache_list_t *core_info_cache_read(const char *info_dir)
{
   intfstream_t *file                           = NULL;
   rjson_t *parser                              = NULL;
   CCJSONContext context                        = {0};
   core_info_cache_list_t *core_info_cache_list = NULL;
   char file_path[PATH_MAX_LENGTH];

   /* Check whether a 'force refresh' file
    * is present */
   file_path[0] = '\0';

   if (string_is_empty(info_dir))
      strlcpy(file_path, FILE_PATH_CORE_INFO_CACHE_REFRESH, sizeof(file_path));
   else
      fill_pathname_join(file_path, info_dir, FILE_PATH_CORE_INFO_CACHE_REFRESH,
            sizeof(file_path));

   if (path_is_valid(file_path))
      return core_info_cache_list_new();

   /* Open info cache file */
   file_path[0] = '\0';

   if (string_is_empty(info_dir))
      strlcpy(file_path, FILE_PATH_CORE_INFO_CACHE, sizeof(file_path));
   else
      fill_pathname_join(file_path, info_dir, FILE_PATH_CORE_INFO_CACHE,
            sizeof(file_path));

#if defined(HAVE_ZLIB)
   file = intfstream_open_rzip_file(file_path,
         RETRO_VFS_FILE_ACCESS_READ);
#else
   file = intfstream_open_file(file_path,
         RETRO_VFS_FILE_ACCESS_READ,
         RETRO_VFS_FILE_ACCESS_HINT_NONE);
#endif

   if (!file)
      return core_info_cache_list_new();

   /* Parse info cache file */
   parser = rjson_open_stream(file);
   if (!parser)
   {
      RARCH_ERR("[Core Info] Failed to create JSON parser\n");
      goto end;
   }

   rjson_set_options(parser,
           RJSON_OPTION_ALLOW_UTF8BOM
         | RJSON_OPTION_ALLOW_COMMENTS
         | RJSON_OPTION_ALLOW_UNESCAPED_CONTROL_CHARACTERS
         | RJSON_OPTION_REPLACE_INVALID_ENCODING);

   if (rjson_parse(parser, &context,
         CCJSONObjectMemberHandler,
         CCJSONStringHandler,
         CCJSONNumberHandler,
         CCJSONStartObjectHandler,
         CCJSONEndObjectHandler,
         CCJSONStartArrayHandler,
         CCJSONEndArrayHandler,
         CCJSONBoolHandler,
         NULL) /* Unused null handler */
         != RJSON_DONE)
   {
      RARCH_WARN("[Core Info] Error parsing chunk:\n---snip---\n%.*s\n---snip---\n",
            rjson_get_source_context_len(parser),
            rjson_get_source_context_buf(parser));
      RARCH_WARN("[Core Info] Error: Invalid JSON at line %d, column %d - %s.\n",
            (int)rjson_get_source_line(parser),
            (int)rjson_get_source_column(parser),
            (*rjson_get_error(parser) ? rjson_get_error(parser) : "format error"));

      /* Info cache is corrupt - discard it */
      core_info_cache_list_free(context.core_info_cache_list);
      core_info_cache_list = core_info_cache_list_new();
   }
   else
      core_info_cache_list = context.core_info_cache_list;

   rjson_free(parser);

   /* Clean up leftovers in the event of
    * a parsing error */
   if (context.core_info)
   {
      core_info_free(context.core_info);
      free(context.core_info);
   }

end:
   intfstream_close(file);
   free(file);

   return core_info_cache_list;
}

static void core_info_cache_write(core_info_cache_list_t *list, const char *info_dir)
{
   intfstream_t *file    = NULL;
   rjsonwriter_t *writer = NULL;
   char file_path[PATH_MAX_LENGTH];
   size_t i, j;

   file_path[0] = '\0';

   if (!list)
      return;

   /* Open info cache file */
   if (string_is_empty(info_dir))
      strlcpy(file_path, FILE_PATH_CORE_INFO_CACHE, sizeof(file_path));
   else
      fill_pathname_join(file_path, info_dir, FILE_PATH_CORE_INFO_CACHE,
            sizeof(file_path));

#if defined(CORE_INFO_CACHE_COMPRESS)
   file = intfstream_open_rzip_file(file_path,
         RETRO_VFS_FILE_ACCESS_WRITE);
#else
   file = intfstream_open_file(file_path,
         RETRO_VFS_FILE_ACCESS_WRITE,
         RETRO_VFS_FILE_ACCESS_HINT_NONE);
#endif

   if (!file)
   {
      RARCH_ERR("[Core Info] Failed to write to core info cache file: %s\n", file_path);
      return;
   }

   /* Write info cache */
   writer = rjsonwriter_open_stream(file);
   if (!writer)
   {
      RARCH_ERR("[Core Info] Failed to create JSON writer\n");
      goto end;
   }

#if defined(CORE_INFO_CACHE_COMPRESS)
   /* When compressing info cache, human readability
    * is not a factor - can skip all indentation
    * and new line characters */
   rjsonwriter_set_options(writer, RJSONWRITER_OPTION_SKIP_WHITESPACE);
#endif

   rjsonwriter_add_start_object(writer);
   rjsonwriter_add_newline(writer);
   rjsonwriter_add_spaces(writer, 2);
   rjsonwriter_add_string(writer, "version");
   rjsonwriter_add_colon(writer);
   rjsonwriter_add_space(writer);
   rjsonwriter_add_string(writer, "1.0");
   rjsonwriter_add_comma(writer);
   rjsonwriter_add_newline(writer);
   rjsonwriter_add_spaces(writer, 2);
   rjsonwriter_add_string(writer, "items");
   rjsonwriter_add_colon(writer);
   rjsonwriter_add_space(writer);
   rjsonwriter_add_start_array(writer);
   rjsonwriter_add_newline(writer);

   for (i = 0; i < list->length; i++)
   {
      core_info_t* info = &list->items[i];

      if (!info || !info->is_installed)
         continue;

      if (i > 0)
      {
         rjsonwriter_add_comma(writer);
         rjsonwriter_add_newline(writer);
      }

      rjsonwriter_add_spaces(writer, 4);
      rjsonwriter_add_start_object(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "path");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->path);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "display_name");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->display_name);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "display_version");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->display_version);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "core_name");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->core_name);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "system_manufacturer");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->system_manufacturer);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "systemname");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->systemname);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "system_id");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->system_id);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "supported_extensions");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->supported_extensions);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "authors");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->authors);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "permissions");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->permissions);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "licenses");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->licenses);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "categories");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->categories);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "databases");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->databases);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "notes");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->notes);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "required_hw_api");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->required_hw_api);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "description");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->description);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      if (info->firmware_count > 0)
      {
         rjsonwriter_add_spaces(writer, 6);
         rjsonwriter_add_string(writer, "firmware");
         rjsonwriter_add_colon(writer);
         rjsonwriter_add_space(writer);
         rjsonwriter_add_start_array(writer);
         rjsonwriter_add_newline(writer);

         for (j = 0; j < info->firmware_count; j++)
         {
            rjsonwriter_add_spaces(writer, 8);
            rjsonwriter_add_start_object(writer);
            rjsonwriter_add_newline(writer);
            rjsonwriter_add_spaces(writer, 10);
            rjsonwriter_add_string(writer, "path");
            rjsonwriter_add_colon(writer);
            rjsonwriter_add_space(writer);
            rjsonwriter_add_string(writer, info->firmware[j].path);
            rjsonwriter_add_comma(writer);
            rjsonwriter_add_newline(writer);
            rjsonwriter_add_spaces(writer, 10);
            rjsonwriter_add_string(writer, "desc");
            rjsonwriter_add_colon(writer);
            rjsonwriter_add_space(writer);
            rjsonwriter_add_string(writer, info->firmware[j].desc);
            rjsonwriter_add_comma(writer);
            rjsonwriter_add_newline(writer);
            rjsonwriter_add_spaces(writer, 10);
            rjsonwriter_add_string(writer, "optional");
            rjsonwriter_add_colon(writer);
            rjsonwriter_add_space(writer);
            rjsonwriter_add_bool(writer, info->firmware[j].optional);
            rjsonwriter_add_newline(writer);
            rjsonwriter_add_spaces(writer, 8);
            rjsonwriter_add_end_object(writer);

            if (j < info->firmware_count - 1)
               rjsonwriter_add_comma(writer);

            rjsonwriter_add_newline(writer);
         }

         rjsonwriter_add_spaces(writer, 6);
         rjsonwriter_add_end_array(writer);
         rjsonwriter_add_comma(writer);
         rjsonwriter_add_newline(writer);
      }

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "core_file_id");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_newline(writer);
      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_start_object(writer);
      rjsonwriter_add_newline(writer);
      rjsonwriter_add_spaces(writer, 8);
      rjsonwriter_add_string(writer, "str");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_string(writer, info->core_file_id.str);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);
      rjsonwriter_add_spaces(writer, 8);
      rjsonwriter_add_string(writer, "hash");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_unsigned(writer, info->core_file_id.hash);
      rjsonwriter_add_newline(writer);
      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_end_object(writer);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "firmware_count");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_unsigned(writer, info->firmware_count);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "has_info");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_bool(writer, info->has_info);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "supports_no_game");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_bool(writer, info->supports_no_game);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "database_match_archive_member");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_bool(writer, info->database_match_archive_member);
      rjsonwriter_add_comma(writer);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 6);
      rjsonwriter_add_string(writer, "is_experimental");
      rjsonwriter_add_colon(writer);
      rjsonwriter_add_space(writer);
      rjsonwriter_add_bool(writer, info->is_experimental);
      rjsonwriter_add_newline(writer);

      rjsonwriter_add_spaces(writer, 4);
      rjsonwriter_add_end_object(writer);
   }

   rjsonwriter_add_newline(writer);
   rjsonwriter_add_spaces(writer, 2);
   rjsonwriter_add_end_array(writer);
   rjsonwriter_add_newline(writer);
   rjsonwriter_add_end_object(writer);
   rjsonwriter_add_newline(writer);
   rjsonwriter_free(writer);

   RARCH_LOG("[Core Info] Wrote to cache file: %s\n", file_path);

   /* Remove 'force refresh' file, if required */
   file_path[0] = '\0';

   if (string_is_empty(info_dir))
      strlcpy(file_path, FILE_PATH_CORE_INFO_CACHE_REFRESH, sizeof(file_path));
   else
      fill_pathname_join(file_path, info_dir, FILE_PATH_CORE_INFO_CACHE_REFRESH,
            sizeof(file_path));

   if (path_is_valid(file_path))
      filestream_delete(file_path);

end:
   intfstream_close(file);
   free(file);

   list->refresh = false;
}

static void core_info_check_uninstalled(core_info_cache_list_t *list)
{
   size_t i;

   if (!list)
      return;

   for (i = 0; i < list->length; i++)
   {
      core_info_t *info = (core_info_t *)&list->items[i];

      if (!info)
         continue;

      if (!info->is_installed)
      {
         list->refresh = true;
         return;
      }
   }
}

/* When called, generates a temporary file
 * that will force an info cache refresh the
 * next time that core info is initialised with
 * caching enabled */
bool core_info_cache_force_refresh(const char *path_info)
{
   char file_path[PATH_MAX_LENGTH];

   file_path[0] = '\0';

   /* Get 'force refresh' file path */
   if (string_is_empty(path_info))
      strlcpy(file_path, FILE_PATH_CORE_INFO_CACHE_REFRESH, sizeof(file_path));
   else
      fill_pathname_join(file_path, path_info, FILE_PATH_CORE_INFO_CACHE_REFRESH,
            sizeof(file_path));

   /* Generate a new, empty 'force refresh' file,
    * if required */
   if (!path_is_valid(file_path))
   {
      RFILE *refresh_file = filestream_open(
            file_path,
            RETRO_VFS_FILE_ACCESS_WRITE,
            RETRO_VFS_FILE_ACCESS_HINT_NONE);

      if (!refresh_file)
         return false;

      /* We have to write something - just output
       * a single character */
      if (filestream_putc(refresh_file, 0) != 0)
      {
         filestream_close(refresh_file);
         return false;
      }

      filestream_close(refresh_file);
   }

   return true;
}

/***********************/
/* Core Info Cache END */
/***********************/

enum compare_op
{
   COMPARE_OP_EQUAL = 0,
   COMPARE_OP_NOT_EQUAL,
   COMPARE_OP_LESS,
   COMPARE_OP_LESS_EQUAL,
   COMPARE_OP_GREATER,
   COMPARE_OP_GREATER_EQUAL
};

typedef struct
{
   const char *path;
   const char *filename;
} core_file_path_t;

typedef struct
{
   core_file_path_t *list;
   size_t size;
} core_file_path_list_t;

typedef struct
{
   const char *filename;
   uint32_t hash;
} core_lock_file_path_t;

typedef struct
{
   core_lock_file_path_t *list;
   size_t size;
} core_lock_file_path_list_t;

typedef struct
{
   struct string_list *dir_list;
   core_file_path_list_t *core_list;
   core_lock_file_path_list_t *lock_list;
} core_path_list_t;

static uint32_t core_info_hash_string(const char *str)
{
   unsigned char c;
   uint32_t hash = (uint32_t)0x811c9dc5;
   while ((c = (unsigned char)*(str++)) != '\0')
      hash = ((hash * (uint32_t)0x01000193) ^ (uint32_t)c);
   return (hash ? hash : 1);
}

static void core_info_path_list_free(core_path_list_t *path_list)
{
   if (!path_list)
      return;

   if (path_list->core_list)
   {
      if (path_list->core_list->list)
         free(path_list->core_list->list);
      free(path_list->core_list);
   }

   if (path_list->lock_list)
   {
      if (path_list->lock_list->list)
         free(path_list->lock_list->list);
      free(path_list->lock_list);
   }

   if (path_list->dir_list)
      string_list_free(path_list->dir_list);

   free(path_list);
}

static core_path_list_t *core_info_path_list_new(const char *core_dir,
      const char *core_exts, bool show_hidden_files)
{
   core_path_list_t *path_list       = (core_path_list_t*)
         calloc(1, sizeof(*path_list));
   struct string_list *core_ext_list = NULL;
   bool dir_list_ok                  = false;
   char exts[32];
   size_t i;

   exts[0] = '\0';

   if (string_is_empty(core_exts) ||
       !path_list)
      goto error;

   core_ext_list = string_split(core_exts, "|");
   if (!core_ext_list)
      goto error;

   /* Allocate list containers */
   path_list->dir_list  = string_list_new();
   path_list->core_list = (core_file_path_list_t*)calloc(1,
         sizeof(*path_list->core_list));
   path_list->lock_list = (core_lock_file_path_list_t*)calloc(1,
         sizeof(*path_list->lock_list));

   if (!path_list->dir_list ||
       !path_list->core_list ||
       !path_list->lock_list)
      goto error;

   /* Get list of file extensions to include
    * (core + lock file) */
   fill_pathname_join_delim(exts, core_exts, FILE_PATH_LOCK_EXTENSION_NO_DOT,
         '|', sizeof(exts));

   /* Fetch core directory listing */
   dir_list_ok = dir_list_append(path_list->dir_list,
         core_dir, exts, false, show_hidden_files,
               false, false);

#if defined(__WINRT__) || defined(WINAPI_FAMILY) && WINAPI_FAMILY == WINAPI_FAMILY_PHONE_APP
   {
      /* UWP: browse the optional packages for additional cores */
      struct string_list core_packages = {0};

      if (string_list_initialize(&core_packages))
      {
         uwp_fill_installed_core_packages(&core_packages);
         for (i = 0; i < core_packages.size; i++)
            dir_list_append(path_list->dir_list,
                  core_packages.elems[i].data, exts, false,
                        show_hidden_files, false, false);
         string_list_deinitialize(&core_packages);
      }
   }
#else
   /* Keep the old 'directory not found' behaviour */
   if (!dir_list_ok)
      goto error;
#endif

   /* Allocate sub lists */
   path_list->core_list->list = (core_file_path_t*)
         malloc(path_list->dir_list->size *
               sizeof(*path_list->core_list->list));
   path_list->lock_list->list = (core_lock_file_path_t*)
         malloc(path_list->dir_list->size *
               sizeof(*path_list->lock_list->list));

   if (!path_list->core_list->list ||
       !path_list->lock_list->list)
      goto error;

   /* Parse directory listing */
   for (i = 0; i < path_list->dir_list->size; i++)
   {
      const char *file_path = path_list->dir_list->elems[i].data;
      const char *filename  = NULL;
      const char *file_ext  = NULL;

      if (string_is_empty(file_path) ||
          !(filename = path_basename_nocompression(file_path)) ||
          !(file_ext = path_get_extension(filename)))
         continue;

      /* Check whether this is a core or lock file */
      if (string_list_find_elem(core_ext_list, file_ext))
      {
         path_list->core_list->list[
               path_list->core_list->size].path     = file_path;
         path_list->core_list->list[
               path_list->core_list->size].filename = filename;
         path_list->core_list->size++;
      }
      else if (string_is_equal(file_ext, FILE_PATH_LOCK_EXTENSION_NO_DOT))
      {
         path_list->lock_list->list[
               path_list->lock_list->size].filename = filename;
         path_list->lock_list->list[
               path_list->lock_list->size].hash     = core_info_hash_string(filename);
         path_list->lock_list->size++;
      }
   }

   string_list_free(core_ext_list);
   return path_list;

error:
   string_list_free(core_ext_list);
   core_info_path_list_free(path_list);
   return NULL;
}

static bool core_info_path_is_locked(core_lock_file_path_list_t *lock_list,
      const char *core_file_name)
{
   size_t i;
   uint32_t hash;
   char lock_filename[256];

   if (lock_list->size < 1)
      return false;

   snprintf(lock_filename, sizeof(lock_filename),
         "%s" FILE_PATH_LOCK_EXTENSION, core_file_name);

   hash = core_info_hash_string(lock_filename);

   for (i = 0; i < lock_list->size; i++)
   {
      core_lock_file_path_t *lock_file = &lock_list->list[i];

      if ((lock_file->hash == hash) &&
          string_is_equal(lock_file->filename, lock_filename))
         return true;
   }

   return false;
}

static bool core_info_get_file_id(const char *core_filename,
      char *core_file_id, size_t len)
{
   char *last_underscore = NULL;

   if (string_is_empty(core_filename))
      return false;

   /* Core file 'id' is filename without extension
    * or platform-specific suffix */

   /* > Remove extension */
   strlcpy(core_file_id, core_filename, len);
   path_remove_extension(core_file_id);

   /* > Remove suffix */
   last_underscore = (char*)strrchr(core_file_id, '_');

   if (!string_is_empty(last_underscore) &&
       !string_is_equal(last_underscore, "_libretro"))
      *last_underscore = '\0';

   return !string_is_empty(core_file_id);
}

static core_info_t *core_info_find_internal(
      core_info_list_t *list,
      const char *core_path)
{
   char core_file_id[256];
   uint32_t hash;
   size_t i;

   core_file_id[0] = '\0';

   if (!list ||
       string_is_empty(core_path) ||
       !core_info_get_file_id(path_basename_nocompression(core_path),
            core_file_id, sizeof(core_file_id)))
      return NULL;

   hash = core_info_hash_string(core_file_id);

   for (i = 0; i < list->count; i++)
   {
      core_info_t *info = &list->list[i];

      if ((info->core_file_id.hash == hash) &&
          string_is_equal(info->core_file_id.str, core_file_id))
         return info;
   }

   return NULL;
}

static void core_info_resolve_firmware(
      core_info_t *info, config_file_t *conf)
{
   unsigned i;
   unsigned firmware_count        = 0;
   core_info_firmware_t *firmware = NULL;

   if (!config_get_uint(conf, "firmware_count", &firmware_count))
      return;

   firmware = (core_info_firmware_t*)calloc(firmware_count, sizeof(*firmware));

   if (!firmware)
      return;

   for (i = 0; i < firmware_count; i++)
   {
      char path_key[64];
      char desc_key[64];
      char opt_key[64];
      struct config_entry_list *entry = NULL;
      bool tmp_bool                   = false;

      path_key[0] = '\0';
      desc_key[0] = '\0';
      opt_key[0]  = '\0';

      snprintf(path_key, sizeof(path_key), "firmware%u_path", i);
      snprintf(desc_key, sizeof(desc_key), "firmware%u_desc", i);
      snprintf(opt_key,  sizeof(opt_key),  "firmware%u_opt",  i);

      entry = config_get_entry(conf, path_key);

      if (entry && !string_is_empty(entry->value))
      {
         firmware[i].path = entry->value;
         entry->value     = NULL;
      }

      entry = config_get_entry(conf, desc_key);

      if (entry && !string_is_empty(entry->value))
      {
         firmware[i].desc = entry->value;
         entry->value     = NULL;
      }

      if (config_get_bool(conf, opt_key , &tmp_bool))
         firmware[i].optional = tmp_bool;
   }

   info->firmware_count = firmware_count;
   info->firmware       = firmware;
}

static config_file_t *core_info_get_config_file(
      const char *core_file_id,
      const char *info_dir)
{
   char info_path[PATH_MAX_LENGTH];

   if (string_is_empty(info_dir))
      snprintf(info_path, sizeof(info_path),
            "%s" ".info", core_file_id);
   else
   {
      info_path[0] = '\0';
      fill_pathname_join(info_path, info_dir, core_file_id,
            sizeof(info_path));
      strlcat(info_path, ".info", sizeof(info_path));
   }

   return config_file_new_from_path_to_string(info_path);
}

static void core_info_parse_config_file(
      core_info_list_t *list, core_info_t *info,
      config_file_t *conf)
{
   struct config_entry_list *entry = NULL;
   bool tmp_bool                   = false;

   entry = config_get_entry(conf, "display_name");

   if (entry && !string_is_empty(entry->value))
   {
      info->display_name = entry->value;
      entry->value       = NULL;
   }

   entry = config_get_entry(conf, "display_version");

   if (entry && !string_is_empty(entry->value))
   {
      info->display_version = entry->value;
      entry->value          = NULL;
   }

   entry = config_get_entry(conf, "corename");

   if (entry && !string_is_empty(entry->value))
   {
      info->core_name = entry->value;
      entry->value    = NULL;
   }

   entry = config_get_entry(conf, "systemname");

   if (entry && !string_is_empty(entry->value))
   {
      info->systemname = entry->value;
      entry->value     = NULL;
   }

   entry = config_get_entry(conf, "systemid");

   if (entry && !string_is_empty(entry->value))
   {
      info->system_id = entry->value;
      entry->value    = NULL;
   }

   entry = config_get_entry(conf, "manufacturer");

   if (entry && !string_is_empty(entry->value))
   {
      info->system_manufacturer = entry->value;
      entry->value              = NULL;
   }

   entry = config_get_entry(conf, "supported_extensions");

   if (entry && !string_is_empty(entry->value))
   {
      info->supported_extensions      = entry->value;
      entry->value                    = NULL;

      info->supported_extensions_list =
            string_split(info->supported_extensions, "|");
   }

   entry = config_get_entry(conf, "authors");

   if (entry && !string_is_empty(entry->value))
   {
      info->authors      = entry->value;
      entry->value       = NULL;

      info->authors_list =
            string_split(info->authors, "|");
   }

   entry = config_get_entry(conf, "permissions");

   if (entry && !string_is_empty(entry->value))
   {
      info->permissions      = entry->value;
      entry->value           = NULL;

      info->permissions_list =
            string_split(info->permissions, "|");
   }

   entry = config_get_entry(conf, "license");

   if (entry && !string_is_empty(entry->value))
   {
      info->licenses      = entry->value;
      entry->value        = NULL;

      info->licenses_list =
            string_split(info->licenses, "|");
   }

   entry = config_get_entry(conf, "categories");

   if (entry && !string_is_empty(entry->value))
   {
      info->categories      = entry->value;
      entry->value          = NULL;

      info->categories_list =
            string_split(info->categories, "|");
   }

   entry = config_get_entry(conf, "database");

   if (entry && !string_is_empty(entry->value))
   {
      info->databases      = entry->value;
      entry->value         = NULL;

      info->databases_list =
            string_split(info->databases, "|");
   }

   entry = config_get_entry(conf, "notes");

   if (entry && !string_is_empty(entry->value))
   {
      info->notes     = entry->value;
      entry->value    = NULL;

      info->note_list =
            string_split(info->notes, "|");
   }

   entry = config_get_entry(conf, "required_hw_api");

   if (entry && !string_is_empty(entry->value))
   {
      info->required_hw_api      = entry->value;
      entry->value               = NULL;

      info->required_hw_api_list =
            string_split(info->required_hw_api, "|");
   }

   entry = config_get_entry(conf, "description");

   if (entry && !string_is_empty(entry->value))
   {
      info->description = entry->value;
      entry->value      = NULL;
   }

   if (config_get_bool(conf, "supports_no_game",
            &tmp_bool))
      info->supports_no_game = tmp_bool;

   if (config_get_bool(conf, "database_match_archive_member",
            &tmp_bool))
      info->database_match_archive_member = tmp_bool;

   if (config_get_bool(conf, "is_experimental",
            &tmp_bool))
      info->is_experimental = tmp_bool;

   core_info_resolve_firmware(info, conf);

   info->has_info = true;
   list->info_count++;
}

static void core_info_list_resolve_all_extensions(
      core_info_list_t *core_info_list)
{
   size_t i              = 0;
   size_t all_ext_len    = 0;
   char *all_ext         = NULL;

   for (i = 0; i < core_info_list->count; i++)
   {
      if (core_info_list->list[i].supported_extensions)
         all_ext_len +=
            (strlen(core_info_list->list[i].supported_extensions) + 2);
   }

   all_ext_len += STRLEN_CONST("7z|") + STRLEN_CONST("zip|");

   all_ext      = (char*)calloc(1, all_ext_len);

   if (!all_ext)
      return;

   core_info_list->all_ext = all_ext;

   for (i = 0; i < core_info_list->count; i++)
   {
      if (!core_info_list->list[i].supported_extensions)
         continue;

      strlcat(core_info_list->all_ext,
            core_info_list->list[i].supported_extensions, all_ext_len);
      strlcat(core_info_list->all_ext, "|", all_ext_len);
   }
#ifdef HAVE_7ZIP
   strlcat(core_info_list->all_ext, "7z|", all_ext_len);
#endif
#ifdef HAVE_ZLIB
   strlcat(core_info_list->all_ext, "zip|", all_ext_len);
#endif
}

static void core_info_free(core_info_t* info)
{
   size_t i;

   free(info->path);
   free(info->core_name);
   free(info->systemname);
   free(info->system_id);
   free(info->system_manufacturer);
   free(info->display_name);
   free(info->display_version);
   free(info->supported_extensions);
   free(info->authors);
   free(info->permissions);
   free(info->licenses);
   free(info->categories);
   free(info->databases);
   free(info->notes);
   free(info->required_hw_api);
   free(info->description);
   string_list_free(info->supported_extensions_list);
   string_list_free(info->authors_list);
   string_list_free(info->note_list);
   string_list_free(info->permissions_list);
   string_list_free(info->licenses_list);
   string_list_free(info->categories_list);
   string_list_free(info->databases_list);
   string_list_free(info->required_hw_api_list);

   for (i = 0; i < info->firmware_count; i++)
   {
      free(info->firmware[i].path);
      free(info->firmware[i].desc);
   }
   free(info->firmware);

   free(info->core_file_id.str);
}

static void core_info_list_free(core_info_list_t *core_info_list)
{
   size_t i;

   if (!core_info_list)
      return;

   for (i = 0; i < core_info_list->count; i++)
   {
      core_info_t *info = (core_info_t*)&core_info_list->list[i];
      core_info_free(info);
   }

   free(core_info_list->all_ext);
   free(core_info_list->list);
   free(core_info_list);
}

static core_info_list_t *core_info_list_new(const char *path,
      const char *libretro_info_dir,
      const char *exts,
      bool dir_show_hidden_files,
      bool enable_cache)
{
   size_t i;
   core_path_list_t *path_list                  = NULL;
   core_info_t *core_info                       = NULL;
   core_info_list_t *core_info_list             = NULL;
   core_info_cache_list_t *core_info_cache_list = NULL;
   const char *info_dir                         = libretro_info_dir;

   path_list = core_info_path_list_new(path, exts,
         dir_show_hidden_files);
   if (!path_list)
      goto error;

   core_info_list = (core_info_list_t*)malloc(sizeof(*core_info_list));
   if (!core_info_list)
      goto error;

   core_info_list->list       = NULL;
   core_info_list->count      = 0;
   core_info_list->info_count = 0;
   core_info_list->all_ext    = NULL;

   core_info = (core_info_t*)calloc(path_list->core_list->size,
         sizeof(*core_info));

   if (!core_info)
   {
      core_info_list_free(core_info_list);
      goto error;
   }

   core_info_list->list  = core_info;
   core_info_list->count = path_list->core_list->size;

#if !defined(IOS)
   /* Read core info cache, if enabled
    * > This functionality is hard disabled on iOS/tvOS,
    *   where core path changes on every install
    *   (invalidating any cached parameters) */
   if (enable_cache)
   {
      core_info_cache_list = core_info_cache_read(info_dir);
      if (!core_info_cache_list)
         goto error;
   }
#endif

   for (i = 0; i < path_list->core_list->size; i++)
   {
      core_info_t *info           = &core_info[i];
      core_file_path_t *core_file = &path_list->core_list->list[i];
      const char *base_path       = core_file->path;
      const char *core_filename   = core_file->filename;
      config_file_t *conf         = NULL;
      char core_file_id[256];

      core_file_id[0] = '\0';

      if (!core_info_get_file_id(core_filename, core_file_id,
               sizeof(core_file_id)))
         continue;

      /* If info cache is available, search for
       * current core */
      if (core_info_cache_list)
      {
         core_info_t *info_cache = core_info_cache_find(core_info_cache_list,
               core_file_id);

         if (info_cache)
         {
            core_info_copy(info_cache, info);
            /* Core lock status is 'dynamic', and
             * cannot be cached */
            info->is_locked = core_info_path_is_locked(path_list->lock_list,
                  core_filename);
            /* 'info_count' is normally incremented inside
             * core_info_parse_config_file(). If core entry
             * is cached, must instead increment the value
             * here */
            if (info->has_info)
               core_info_list->info_count++;
            continue;
         }
      }

      /* Cache core path */
      info->path = strdup(base_path);

      /* Get core lock status */
      info->is_locked = core_info_path_is_locked(path_list->lock_list,
            core_filename);

      /* Cache core file 'id' */
      info->core_file_id.str  = strdup(core_file_id);
      info->core_file_id.hash = core_info_hash_string(core_file_id);

      /* Parse core info file */
      conf = core_info_get_config_file(core_file_id, info_dir);

      if (conf)
      {
         core_info_parse_config_file(core_info_list, info, conf);
         config_file_free(conf);
      }

      /* Get fallback display name, if required */
      if (!info->display_name)
         info->display_name = strdup(core_filename);

      info->is_installed = true;

      /* If info cache is enabled and we reach this
       * point, current core is uncached
       * > Add it to the list, and trigger a cache
       *   refresh */
      if (core_info_cache_list)
      {
         core_info_cache_add(core_info_cache_list, info, false);
         core_info_cache_list->refresh = true;
      }
   }

   core_info_list_resolve_all_extensions(core_info_list);

   /* If info cache is enabled
    * > Check whether any cached cores have been
    *   uninstalled since the last run (triggers
    *   a refresh)
    * > Write new cache to disk if updates are
    *   required */
   if (core_info_cache_list)
   {
      core_info_check_uninstalled(core_info_cache_list);

      if (core_info_cache_list->refresh)
         core_info_cache_write(core_info_cache_list, info_dir);

      core_info_cache_list_free(core_info_cache_list);
   }

   core_info_path_list_free(path_list);
   return core_info_list;

error:
   core_info_path_list_free(path_list);
   return NULL;
}

/* Shallow-copies internal state.
 *
 * Data in *info is invalidated when the
 * core_info_list is freed. */
bool core_info_list_get_info(core_info_list_t *core_info_list,
      core_info_t *out_info, const char *core_path)
{
   core_info_t *info = core_info_find_internal(
         core_info_list, core_path);

   if (!out_info)
      return false;

   memset(out_info, 0, sizeof(*out_info));

   if (info)
   {
      *out_info = *info;
      return true;
   }

   return false;
}

#ifdef HAVE_COMPRESSION
static bool core_info_does_support_any_file(const core_info_t *core,
      const struct string_list *list)
{
   size_t i;
   if (!list || !core || !core->supported_extensions_list)
      return false;

   for (i = 0; i < list->size; i++)
      if (string_list_find_elem_prefix(core->supported_extensions_list,
               ".", path_get_extension(list->elems[i].data)))
         return true;
   return false;
}
#endif

static bool core_info_does_support_file(
      const core_info_t *core, const char *path)
{
   if (!core || !core->supported_extensions_list)
      return false;
   if (string_is_empty(path))
      return false;

   return string_list_find_elem_prefix(
         core->supported_extensions_list, ".", path_get_extension(path));
}

/* qsort_r() is not in standard C, sadly. */

static int core_info_qsort_cmp(const void *a_, const void *b_)
{
   core_info_state_t *p_coreinfo = coreinfo_get_ptr();
   const core_info_t          *a = (const core_info_t*)a_;
   const core_info_t          *b = (const core_info_t*)b_;
   int support_a                 = core_info_does_support_file(a,
         p_coreinfo->tmp_path);
   int support_b                 = core_info_does_support_file(b,
         p_coreinfo->tmp_path);
#ifdef HAVE_COMPRESSION
   support_a            = support_a ||
      core_info_does_support_any_file(a, p_coreinfo->tmp_list);
   support_b            = support_b ||
      core_info_does_support_any_file(b, p_coreinfo->tmp_list);
#endif

   if (support_a != support_b)
      return support_b - support_a;
   return strcasecmp(a->display_name, b->display_name);
}

static bool core_info_list_update_missing_firmware_internal(
      core_info_list_t *core_info_list,
      const char *core_path,
      const char *systemdir,
      bool *set_missing_bios)
{
   size_t i;
   char path[PATH_MAX_LENGTH];
   core_info_t      *info = NULL;

   if (!core_info_list)
      return false;

   info                   = core_info_find_internal(core_info_list, core_path);

   if (!info)
      return false;

   path[0]                = '\0';

   for (i = 0; i < info->firmware_count; i++)
   {
      if (string_is_empty(info->firmware[i].path))
         continue;

      fill_pathname_join(path, systemdir,
            info->firmware[i].path, sizeof(path));
      info->firmware[i].missing = !path_is_valid(path);
      if (info->firmware[i].missing && !info->firmware[i].optional)
         *set_missing_bios = true;
   }

   return true;
}

void core_info_free_current_core(core_info_state_t *p_coreinfo)
{
   if (p_coreinfo->current)
      free(p_coreinfo->current);
   p_coreinfo->current = NULL;
}

bool core_info_init_current_core(void)
{
   core_info_state_t *p_coreinfo          = coreinfo_get_ptr();
   core_info_t *current                   = (core_info_t*)
      malloc(sizeof(*current));
   if (!current)
      return false;
   current->has_info                      = false;
   current->supports_no_game              = false;
   current->database_match_archive_member = false;
   current->is_experimental               = false;
   current->is_locked                     = false;
   current->firmware_count                = 0;
   current->path                          = NULL;
   current->display_name                  = NULL;
   current->display_version               = NULL;
   current->core_name                     = NULL;
   current->system_manufacturer           = NULL;
   current->systemname                    = NULL;
   current->system_id                     = NULL;
   current->supported_extensions          = NULL;
   current->authors                       = NULL;
   current->permissions                   = NULL;
   current->licenses                      = NULL;
   current->categories                    = NULL;
   current->databases                     = NULL;
   current->notes                         = NULL;
   current->required_hw_api               = NULL;
   current->description                   = NULL;
   current->categories_list               = NULL;
   current->databases_list                = NULL;
   current->note_list                     = NULL;
   current->supported_extensions_list     = NULL;
   current->authors_list                  = NULL;
   current->permissions_list              = NULL;
   current->licenses_list                 = NULL;
   current->required_hw_api_list          = NULL;
   current->firmware                      = NULL;
   current->core_file_id.str              = NULL;
   current->core_file_id.hash             = 0;

   p_coreinfo->current                    = current;
   return true;
}

bool core_info_get_current_core(core_info_t **core)
{
   core_info_state_t *p_coreinfo = coreinfo_get_ptr();
   if (!core)
      return false;
   *core = p_coreinfo->current;
   return true;
}

void core_info_deinit_list(void)
{
   core_info_state_t *p_coreinfo = coreinfo_get_ptr();
   if (p_coreinfo->curr_list)
      core_info_list_free(p_coreinfo->curr_list);
   p_coreinfo->curr_list = NULL;
}

bool core_info_init_list(const char *path_info, const char *dir_cores,
      const char *exts, bool dir_show_hidden_files, bool enable_cache)
{
   core_info_state_t *p_coreinfo = coreinfo_get_ptr();
   if (!(p_coreinfo->curr_list = core_info_list_new(dir_cores,
               !string_is_empty(path_info) ? path_info : dir_cores,
               exts,
               dir_show_hidden_files,
               enable_cache)))
      return false;
   return true;
}

bool core_info_get_list(core_info_list_t **core)
{
   core_info_state_t *p_coreinfo = coreinfo_get_ptr();
   if (!core)
      return false;
   *core = p_coreinfo->curr_list;
   return true;
}

/* Returns number of installed cores */
size_t core_info_count(void)
{
   core_info_state_t *p_coreinfo = coreinfo_get_ptr();
   if (!p_coreinfo || !p_coreinfo->curr_list)
      return 0;
   return p_coreinfo->curr_list->count;
}

bool core_info_list_update_missing_firmware(core_info_ctx_firmware_t *info,
      bool *set_missing_bios)
{
   core_info_state_t *p_coreinfo = coreinfo_get_ptr();
   if (!info)
      return false;
   return core_info_list_update_missing_firmware_internal(
         p_coreinfo->curr_list,
         info->path, info->directory.system,
         set_missing_bios);
}

bool core_info_load(const char *core_path,
      core_info_state_t *p_coreinfo)
{
   core_info_t    *core_info     = NULL;

   if (!p_coreinfo->current)
      core_info_init_current_core();

   core_info_get_current_core(&core_info);

   if (!p_coreinfo->curr_list)
      return false;

   if (!core_info_list_get_info(p_coreinfo->curr_list,
            core_info, core_path))
      return false;

   return true;
}

bool core_info_find(const char *core_path,
      core_info_t **core_info)
{
   core_info_state_t *p_coreinfo = coreinfo_get_ptr();
   core_info_t *info             = NULL;

   if (!core_info || !p_coreinfo->curr_list)
      return false;

   info = core_info_find_internal(p_coreinfo->curr_list, core_path);

   if (!info)
      return false;

   *core_info = info;
   return true;
}

core_info_t *core_info_get(core_info_list_t *list, size_t i)
{
   core_info_t *info = NULL;

   if (!list || (i >= list->count))
      return NULL;
   info = (core_info_t*)&list->list[i];
   if (!info || !info->path)
      return NULL;

   return info;
}

void core_info_list_get_supported_cores(core_info_list_t *core_info_list,
      const char *path, const core_info_t **infos, size_t *num_infos)
{
   size_t i;
   size_t supported              = 0;
#ifdef HAVE_COMPRESSION
   struct string_list *list      = NULL;
#endif
   core_info_state_t *p_coreinfo = coreinfo_get_ptr();

   if (!core_info_list)
      return;

   p_coreinfo->tmp_path          = path;

#ifdef HAVE_COMPRESSION
   if (path_is_compressed_file(path))
      list = file_archive_get_file_list(path, NULL);
   p_coreinfo->tmp_list = list;
#endif

   /* Let supported core come first in list so we can return
    * a pointer to them. */
   qsort(core_info_list->list, core_info_list->count,
         sizeof(core_info_t), core_info_qsort_cmp);

   for (i = 0; i < core_info_list->count; i++, supported++)
   {
      const core_info_t *core = &core_info_list->list[i];

      if (core_info_does_support_file(core, path))
         continue;

#ifdef HAVE_COMPRESSION
      if (core_info_does_support_any_file(core, list))
         continue;
#endif

      break;
   }

#ifdef HAVE_COMPRESSION
   if (list)
      string_list_free(list);
#endif

   *infos     = core_info_list->list;
   *num_infos = supported;
}

/*
 * Matches core A and B file IDs
 *
 * e.g.:
 *   snes9x_libretro.dll and snes9x_libretro_android.so are matched
 *   snes9x__2005_libretro.dll and snes9x_libretro_android.so are 
 *   NOT matched
 */
bool core_info_core_file_id_is_equal(const char *core_path_a,
      const char *core_path_b)
{
   char core_file_id_a[256];
   char core_file_id_b[256];

   core_file_id_a[0] = '\0';
   core_file_id_b[0] = '\0';

   if (string_is_empty(core_path_a) ||
       string_is_empty(core_path_b) ||
       !core_info_get_file_id(path_basename_nocompression(core_path_a),
            core_file_id_a, sizeof(core_file_id_a)) ||
       !core_info_get_file_id(path_basename_nocompression(core_path_b),
            core_file_id_b, sizeof(core_file_id_b)))
      return false;

   return string_is_equal(core_file_id_a, core_file_id_b);
}

bool core_info_database_match_archive_member(const char *database_path)
{
   char      *database           = NULL;
   const char      *new_path     = path_basename_nocompression(database_path);
   core_info_state_t *p_coreinfo = NULL;

   if (string_is_empty(new_path))
      return false;
   if (!(database = strdup(new_path)))
      return false;

   path_remove_extension(database);

   p_coreinfo               = coreinfo_get_ptr();

   if (p_coreinfo->curr_list)
   {
      size_t i;

      for (i = 0; i < p_coreinfo->curr_list->count; i++)
      {
         const core_info_t *info = &p_coreinfo->curr_list->list[i];

         if (!info->database_match_archive_member)
             continue;

         if (!string_list_find_elem(info->databases_list, database))
             continue;

         free(database);
         return true;
      }
   }

   free(database);
   return false;
}

bool core_info_database_supports_content_path(
      const char *database_path, const char *path)
{
   char      *database           = NULL;
   const char      *new_path     = path_basename(database_path);
   core_info_state_t *p_coreinfo = NULL;

   if (string_is_empty(new_path))
      return false;
   if (!(database = strdup(new_path)))
      return false;

   path_remove_extension(database);

   p_coreinfo                    = coreinfo_get_ptr();

   if (p_coreinfo->curr_list)
   {
      size_t i;

      for (i = 0; i < p_coreinfo->curr_list->count; i++)
      {
         const core_info_t *info = &p_coreinfo->curr_list->list[i];

         if (!string_list_find_elem(info->supported_extensions_list,
                  path_get_extension(path)))
            continue;

         if (!string_list_find_elem(info->databases_list, database))
            continue;

         free(database);
         return true;
      }
   }

   free(database);
   return false;
}

bool core_info_list_get_display_name(core_info_list_t *core_info_list,
      const char *core_path, char *s, size_t len)
{
   core_info_t *info = core_info_find_internal(
         core_info_list, core_path);

   if (s &&
       info &&
       !string_is_empty(info->display_name))
   {
      strlcpy(s, info->display_name, len);
      return true;
   }

   return false;
}

/* Returns core_info parameters required for
 * core updater tasks, read from specified file.
 * Returned core_updater_info_t object must be
 * freed using core_info_free_core_updater_info().
 * Returns NULL if 'path' is invalid. */
core_updater_info_t *core_info_get_core_updater_info(const char *info_path)
{
   struct config_entry_list 
      *entry                 = NULL;
   bool tmp_bool             = false;
   core_updater_info_t *info = NULL;
   config_file_t *conf       = NULL;

   if (string_is_empty(info_path))
      return NULL;

   /* Read config file */
   conf = config_file_new_from_path_to_string(info_path);

   if (!conf)
      return NULL;

   /* Create info struct */
   info                      = (core_updater_info_t*)malloc(sizeof(*info));

   if (!info)
      return NULL;

   info->is_experimental     = false;
   info->display_name        = NULL;
   info->description         = NULL;
   info->licenses            = NULL;

   /* Fetch required parameters */

   /* > is_experimental */
   if (config_get_bool(conf, "is_experimental", &tmp_bool))
      info->is_experimental  = tmp_bool;

   /* > display_name */
   entry                     = config_get_entry(conf, "display_name");

   if (entry && !string_is_empty(entry->value))
   {
      info->display_name     = entry->value;
      entry->value           = NULL;
   }

   /* > description */
   entry                     = config_get_entry(conf, "description");

   if (entry && !string_is_empty(entry->value))
   {
      info->description      = entry->value;
      entry->value           = NULL;
   }

   /* > licenses */
   entry                     = config_get_entry(conf, "license");

   if (entry && !string_is_empty(entry->value))
   {
      info->licenses         = entry->value;
      entry->value           = NULL;
   }

   /* Clean up */
   config_file_free(conf);

   return info;
}

void core_info_free_core_updater_info(core_updater_info_t *info)
{
   if (!info)
      return;

   if (info->display_name)
      free(info->display_name);

   if (info->description)
      free(info->description);

   if (info->licenses)
      free(info->licenses);

   free(info);
   info = NULL;
}

static int core_info_qsort_func_path(const core_info_t *a,
      const core_info_t *b)
{
   if (!a || !b)
      return 0;

   if (string_is_empty(a->path) || string_is_empty(b->path))
      return 0;

   return strcasecmp(a->path, b->path);
}

static int core_info_qsort_func_display_name(const core_info_t *a,
      const core_info_t *b)
{
   if (!a || !b)
      return 0;

   if (string_is_empty(a->display_name) || string_is_empty(b->display_name))
      return 0;

   return strcasecmp(a->display_name, b->display_name);
}

static int core_info_qsort_func_core_name(const core_info_t *a,
      const core_info_t *b)
{
   if (!a || !b)
      return 0;

   if (string_is_empty(a->core_name) || string_is_empty(b->core_name))
      return 0;

   return strcasecmp(a->core_name, b->core_name);
}

static int core_info_qsort_func_system_name(const core_info_t *a,
      const core_info_t *b)
{
   if (!a || !b)
      return 0;

   if (string_is_empty(a->systemname) || string_is_empty(b->systemname))
      return 0;

   return strcasecmp(a->systemname, b->systemname);
}

void core_info_qsort(core_info_list_t *core_info_list,
      enum core_info_list_qsort_type qsort_type)
{
   if (!core_info_list)
      return;

   if (core_info_list->count < 2)
      return;

   switch (qsort_type)
   {
      case CORE_INFO_LIST_SORT_PATH:
         qsort(core_info_list->list,
               core_info_list->count,
               sizeof(core_info_t),
               (int (*)(const void *, const void *))
               core_info_qsort_func_path);
         break;
      case CORE_INFO_LIST_SORT_DISPLAY_NAME:
         qsort(core_info_list->list,
               core_info_list->count,
               sizeof(core_info_t),
               (int (*)(const void *, const void *))
               core_info_qsort_func_display_name);
         break;
      case CORE_INFO_LIST_SORT_CORE_NAME:
         qsort(core_info_list->list,
               core_info_list->count,
               sizeof(core_info_t),
               (int (*)(const void *, const void *))
               core_info_qsort_func_core_name);
         break;
      case CORE_INFO_LIST_SORT_SYSTEM_NAME:
         qsort(core_info_list->list,
               core_info_list->count,
               sizeof(core_info_t),
               (int (*)(const void *, const void *))
               core_info_qsort_func_system_name);
         break;
      default:
         return;
   }
}

static bool core_info_compare_api_version(int sys_major, int sys_minor, int major, int minor, enum compare_op op)
{
   switch (op)
   {
      case COMPARE_OP_EQUAL:
         if (sys_major == major && sys_minor == minor)
            return true;
         break;
      case COMPARE_OP_NOT_EQUAL:
         if (!(sys_major == major && sys_minor == minor))
            return true;
         break;
      case COMPARE_OP_LESS:
         if (sys_major < major || (sys_major == major && sys_minor < minor))
            return true;
         break;
      case COMPARE_OP_LESS_EQUAL:
         if (sys_major < major || (sys_major == major && sys_minor <= minor))
            return true;
         break;
      case COMPARE_OP_GREATER:
         if (sys_major > major || (sys_major == major && sys_minor > minor))
            return true;
         break;
      case COMPARE_OP_GREATER_EQUAL:
         if (sys_major > major || (sys_major == major && sys_minor >= minor))
            return true;
         break;
      default:
         break;
   }

   return false;
}

bool core_info_hw_api_supported(core_info_t *info)
{
#ifdef RARCH_INTERNAL
   unsigned i;
   enum gfx_ctx_api sys_api;
   int sys_api_version_major       = 0;
   int sys_api_version_minor       = 0;
   const char *sys_api_version_str = video_driver_get_gpu_api_version_string();
   gfx_ctx_flags_t sys_flags       = video_driver_get_flags_wrapper();

   enum api_parse_state
   {
      STATE_API_NAME,
      STATE_API_COMPARE_OP,
      STATE_API_VERSION
   };

   if (!info || !info->required_hw_api_list || info->required_hw_api_list->size == 0)
      return true;

   sys_api = video_context_driver_get_api();

   for (i = 0; i < info->required_hw_api_list->size; i++)
   {
      char api_str[32]           = {0};
      char version[16]           = {0};
      char major_str[16]         = {0};
      char minor_str[16]         = {0};
      const char *cur_api        = info->required_hw_api_list->elems[i].data;
      int api_pos                = 0;
      int major_str_pos          = 0;
      int minor_str_pos          = 0;
      int major                  = 0;
      int minor                  = 0;
      unsigned cur_api_len       = 0;
      unsigned j                 = 0;
      bool found_major           = false;
      bool found_minor           = false;
      enum compare_op op         = COMPARE_OP_GREATER_EQUAL;
      enum api_parse_state state = STATE_API_NAME;

      if (string_is_empty(cur_api))
         continue;

      cur_api_len                = (int)strlen(cur_api);

      for (j = 0; j < cur_api_len; j++)
      {
         if (cur_api[j] == ' ')
            continue;

         switch (state)
         {
            case STATE_API_NAME:
            {
               if (  ISUPPER((unsigned char)cur_api[j]) || 
                     ISLOWER((unsigned char)cur_api[j]))
                  api_str[api_pos++] = cur_api[j];
               else
               {
                  j--;
                  state = STATE_API_COMPARE_OP;
                  break;
               }

               break;
            }
            case STATE_API_COMPARE_OP:
            {
               if (j < cur_api_len - 1 && !(cur_api[j] >= '0' && cur_api[j] <= '9'))
               {
                  if (cur_api[j] == '=' && cur_api[j + 1] == '=')
                  {
                     op = COMPARE_OP_EQUAL;
                     j++;
                  }
                  else if (cur_api[j] == '=')
                     op = COMPARE_OP_EQUAL;
                  else if (cur_api[j] == '!' && cur_api[j + 1] == '=')
                  {
                     op = COMPARE_OP_NOT_EQUAL;
                     j++;
                  }
                  else if (cur_api[j] == '<' && cur_api[j + 1] == '=')
                  {
                     op = COMPARE_OP_LESS_EQUAL;
                     j++;
                  }
                  else if (cur_api[j] == '>' && cur_api[j + 1] == '=')
                  {
                     op = COMPARE_OP_GREATER_EQUAL;
                     j++;
                  }
                  else if (cur_api[j] == '<')
                     op = COMPARE_OP_LESS;
                  else if (cur_api[j] == '>')
                     op = COMPARE_OP_GREATER;
               }

               state = STATE_API_VERSION;

               break;
            }
            case STATE_API_VERSION:
            {
               if (    !found_minor 
                     && cur_api[j] >= '0'
                     && cur_api[j] <= '9'
                     && cur_api[j] != '.')
               {
                  found_major = true;

                  if (major_str_pos < sizeof(major_str) - 1)
                     major_str[major_str_pos++] = cur_api[j];
               }
               else if (
                        found_major 
                     && found_minor
                     && cur_api[j] >= '0'
                     && cur_api[j] <= '9')
               {
                  if (minor_str_pos < sizeof(minor_str) - 1)
                     minor_str[minor_str_pos++] = cur_api[j];
               }
               else if (cur_api[j] == '.')
                  found_minor = true;
               break;
            }
            default:
               break;
         }
      }

      sscanf(major_str, "%d", &major);
      sscanf(minor_str, "%d", &minor);
      snprintf(version, sizeof(version), "%d.%d", major, minor);
#if 0
      printf("Major: %d\n", major);
      printf("Minor: %d\n", minor);
      printf("API: %s\n", api_str);
      printf("Version: %s\n", version);
      fflush(stdout);
#endif

      if ((string_is_equal_noncase(api_str, "opengl") && sys_api == GFX_CTX_OPENGL_API) ||
            (string_is_equal_noncase(api_str, "openglcompat") && sys_api == GFX_CTX_OPENGL_API) ||
            (string_is_equal_noncase(api_str, "openglcompatibility") && sys_api == GFX_CTX_OPENGL_API))
      {
         /* system is running a core context while compat is requested */
         if (sys_flags.flags & (1 << GFX_CTX_FLAGS_GL_CORE_CONTEXT))   
            return false;

         sscanf(sys_api_version_str, "%d.%d", &sys_api_version_major, &sys_api_version_minor);

         if (core_info_compare_api_version(sys_api_version_major, sys_api_version_minor, major, minor, op))
            return true;
      }
      else if (string_is_equal_noncase(api_str, "openglcore") && sys_api == GFX_CTX_OPENGL_API)
      {
         sscanf(sys_api_version_str, "%d.%d", &sys_api_version_major, &sys_api_version_minor);

         if (core_info_compare_api_version(sys_api_version_major, sys_api_version_minor, major, minor, op))
            return true;
      }
      else if (string_is_equal_noncase(api_str, "opengles") && sys_api == GFX_CTX_OPENGL_ES_API)
      {
         sscanf(sys_api_version_str, "OpenGL ES %d.%d", &sys_api_version_major, &sys_api_version_minor);

         if (core_info_compare_api_version(sys_api_version_major, sys_api_version_minor, major, minor, op))
            return true;
      }
      else if (string_is_equal_noncase(api_str, "direct3d8") && sys_api == GFX_CTX_DIRECT3D8_API)
      {
         sys_api_version_major = 8;
         sys_api_version_minor = 0;

         if (core_info_compare_api_version(sys_api_version_major, sys_api_version_minor, major, minor, op))
            return true;
      }
      else if (string_is_equal_noncase(api_str, "direct3d9") && sys_api == GFX_CTX_DIRECT3D9_API)
      {
         sys_api_version_major = 9;
         sys_api_version_minor = 0;

         if (core_info_compare_api_version(sys_api_version_major, sys_api_version_minor, major, minor, op))
            return true;
      }
      else if (string_is_equal_noncase(api_str, "direct3d10") && sys_api == GFX_CTX_DIRECT3D10_API)
      {
         sys_api_version_major = 10;
         sys_api_version_minor = 0;

         if (core_info_compare_api_version(sys_api_version_major, sys_api_version_minor, major, minor, op))
            return true;
      }
      else if (string_is_equal_noncase(api_str, "direct3d11") && sys_api == GFX_CTX_DIRECT3D11_API)
      {
         sys_api_version_major = 11;
         sys_api_version_minor = 0;

         if (core_info_compare_api_version(sys_api_version_major, sys_api_version_minor, major, minor, op))
            return true;
      }
      else if (string_is_equal_noncase(api_str, "direct3d12") && sys_api == GFX_CTX_DIRECT3D12_API)
      {
         sys_api_version_major = 12;
         sys_api_version_minor = 0;

         if (core_info_compare_api_version(sys_api_version_major, sys_api_version_minor, major, minor, op))
            return true;
      }
      else if (string_is_equal_noncase(api_str, "vulkan") && sys_api == GFX_CTX_VULKAN_API)
      {
         sscanf(sys_api_version_str, "%d.%d", &sys_api_version_major, &sys_api_version_minor);

         if (core_info_compare_api_version(sys_api_version_major, sys_api_version_minor, major, minor, op))
            return true;
      }
      else if (string_is_equal_noncase(api_str, "metal") && sys_api == GFX_CTX_METAL_API)
      {
         sscanf(sys_api_version_str, "%d.%d", &sys_api_version_major, &sys_api_version_minor);

         if (core_info_compare_api_version(sys_api_version_major, sys_api_version_minor, major, minor, op))
            return true;
      }
   }

   return false;
#else
   return true;
#endif
}

/* Sets 'locked' status of specified core
 * > Returns true if successful
 * > Like all functions that access the cached
 *   core info list this is *not* thread safe */
bool core_info_set_core_lock(const char *core_path, bool lock)
{
   core_info_t *core_info = NULL;
   bool lock_file_exists  = false;
   char lock_file_path[PATH_MAX_LENGTH];

#if defined(ANDROID)
   /* Play Store builds do not support
    * core locking */
   if (play_feature_delivery_enabled())
      return false;
#endif

   if (string_is_empty(core_path))
      return false;

   /* Search for specified core */
   if (!core_info_find(core_path, &core_info))
      return false;

   if (string_is_empty(core_info->path))
      return false;

   /* Get lock file path */
   snprintf(lock_file_path, sizeof(lock_file_path),
         "%s" FILE_PATH_LOCK_EXTENSION, core_info->path);

   /* Check whether lock file exists */
   lock_file_exists = path_is_valid(lock_file_path);

   /* Create or delete lock file, as required */
   if (lock && !lock_file_exists)
   {
      RFILE *lock_file = filestream_open(
            lock_file_path,
            RETRO_VFS_FILE_ACCESS_WRITE,
            RETRO_VFS_FILE_ACCESS_HINT_NONE);

      if (!lock_file)
         return false;

      /* We have to write something - just output
       * a single character */
      if (filestream_putc(lock_file, 0) != 0)
      {
         filestream_close(lock_file);
         return false;
      }

      filestream_close(lock_file);
   }
   else if (!lock && lock_file_exists)
      if (filestream_delete(lock_file_path) != 0)
         return false;

   /* File operations were successful - update
    * core info entry */
   core_info->is_locked = lock;

   return true;
}

/* Fetches 'locked' status of specified core
 * > If 'validate_path' is 'true', will search
 *   cached core info list for a corresponding
 *   'sanitised' core file path. This is *not*
 *   thread safe
 * > If 'validate_path' is 'false', performs a
 *   direct filesystem check. This *is* thread
 *   safe, but validity of specified core path
 *   must be checked externally */
bool core_info_get_core_lock(const char *core_path, bool validate_path)
{
   core_info_t *core_info     = NULL;
   const char *core_file_path = NULL;
   bool is_locked             = false;
   char lock_file_path[PATH_MAX_LENGTH];

#if defined(ANDROID)
   /* Play Store builds do not support
    * core locking */
   if (play_feature_delivery_enabled())
      return false;
#endif

   if (string_is_empty(core_path))
      return false;

   /* Check whether core path is to be validated */
   if (validate_path)
   {
      if (core_info_find(core_path, &core_info))
         core_file_path = core_info->path;
   }
   else
      core_file_path = core_path;

   /* A core cannot be locked if it does not exist... */
   if (string_is_empty(core_file_path) ||
       !path_is_valid(core_file_path))
      return false;

   /* Get lock file path */
   snprintf(lock_file_path, sizeof(lock_file_path),
         "%s" FILE_PATH_LOCK_EXTENSION, core_file_path);

   /* Check whether lock file exists */
   is_locked = path_is_valid(lock_file_path);

   /* If core path has been validated (and a
    * core info object is available), ensure
    * that core info 'is_locked' field is
    * up to date */
   if (validate_path && core_info)
      core_info->is_locked = is_locked;

   return is_locked;
}