/*  RetroArch - A frontend for libretro.
 *  Copyright (C) 2010-2014 - Hans-Kristian Arntzen
 *  Copyright (C) 2011-2017 - Daniel De Matteis
 *
 *  RetroArch is free software: you can redistribute it and/or modify it under the terms
 *  of the GNU General Public License as published by the Free Software Found-
 *  ation, either version 3 of the License, or (at your option) any later version.
 *
 *  RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 *  without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 *  PURPOSE.  See the GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with RetroArch.
 *  If not, see <http://www.gnu.org/licenses/>.
 */

#include <string/stdstring.h>

#ifdef HAVE_CHEEVOS
#include "cheevos/cheevos.h"
#endif

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

#include "core_option_manager.h"
#include "msg_hash.h"

/*********************/
/* Option Conversion */
/*********************/

/**
 * core_option_manager_convert_v1:
 *
 * @options_v1 : an array of retro_core_option_definition
 *               structs
 *
 * Converts an array of core option v1 definitions into
 * a v2 core options struct. Returned pointer must be
 * freed using core_option_manager_free_converted().
 *
 * Returns: Valid pointer to a new v2 core options struct
 * if successful, otherwise NULL.
 **/
struct retro_core_options_v2 *core_option_manager_convert_v1(
      const struct retro_core_option_definition *options_v1)
{
   size_t i;
   size_t num_options                                     = 0;
   struct retro_core_options_v2 *options_v2               = NULL;
   struct retro_core_option_v2_definition *option_v2_defs = NULL;

   if (!options_v1)
      return NULL;

   /* Determine number of options */
   for (;;)
   {
      if (string_is_empty(options_v1[num_options].key))
         break;
      num_options++;
   }

   if (num_options < 1)
      return NULL;

   /* Allocate output retro_core_options_v2 struct */
   if (!(options_v2 = (struct retro_core_options_v2 *)
         malloc(sizeof(*options_v2))))
      return NULL;

   /* Note: v1 options have no concept of
    * categories, so this field will be left
    * as NULL */
   options_v2->categories  = NULL;
   options_v2->definitions = NULL;

   /* Allocate output option_v2_defs array
    * > One extra entry required for terminating NULL entry
    * > Note that calloc() sets terminating NULL entry and
    *   correctly 'nullifies' each values array */
   if (!(option_v2_defs = (struct retro_core_option_v2_definition *)
         calloc(num_options + 1, sizeof(*option_v2_defs))))
   {
      free(options_v2);
      return NULL;
   }

   options_v2->definitions = option_v2_defs;

   /* Loop through options... */
   for (i = 0; i < num_options; i++)
   {
      size_t j;
      size_t num_values = 0;

      /* Set key */
      option_v2_defs[i].key           = options_v1[i].key;

      /* Set default value */
      option_v2_defs[i].default_value = options_v1[i].default_value;

      /* Set desc and info strings */
      option_v2_defs[i].desc = options_v1[i].desc;
      option_v2_defs[i].info = options_v1[i].info;

      /* v1 options have no concept of categories
       * (Note: These are already nullified by
       * the preceding calloc(), but we do it
       * explicitly here for code clarity) */
      option_v2_defs[i].desc_categorized = NULL;
      option_v2_defs[i].info_categorized = NULL;
      option_v2_defs[i].category_key     = NULL;

      /* Determine number of values */
      for (;;)
      {
         if (string_is_empty(options_v1[i].values[num_values].value))
            break;
         num_values++;
      }

      /* Copy values */
      for (j = 0; j < num_values; j++)
      {
         /* Set value string */
         option_v2_defs[i].values[j].value = options_v1[i].values[j].value;

         /* Set value label string */
         option_v2_defs[i].values[j].label = options_v1[i].values[j].label;
      }
   }

   return options_v2;
}

/**
 * core_option_manager_convert_v1_intl:
 *
 * @options_v1_intl : pointer to a retro_core_options_intl
 *                    struct
 *
 * Converts a v1 'international' core options definition
 * struct into a v2 core options struct. Returned pointer
 * must be freed using core_option_manager_free_converted().
 *
 * Returns: Valid pointer to a new v2 core options struct
 * if successful, otherwise NULL.
 **/
struct retro_core_options_v2 *core_option_manager_convert_v1_intl(
      const struct retro_core_options_intl *options_v1_intl)
{
   size_t i;
   size_t num_options                                     = 0;
   struct retro_core_option_definition *option_defs_us    = NULL;
   struct retro_core_option_definition *option_defs_local = NULL;
   struct retro_core_options_v2 *options_v2               = NULL;
   struct retro_core_option_v2_definition *option_v2_defs = NULL;

   if (!options_v1_intl)
      return NULL;

   option_defs_us    = options_v1_intl->us;
   option_defs_local = options_v1_intl->local;

   if (!option_defs_us)
      return NULL;

   /* Determine number of options */
   for (;;)
   {
      if (string_is_empty(option_defs_us[num_options].key))
         break;
      num_options++;
   }

   if (num_options < 1)
      return NULL;

   /* Allocate output retro_core_options_v2 struct */
   if (!(options_v2 = (struct retro_core_options_v2 *)
         malloc(sizeof(*options_v2))))
      return NULL;

   /* Note: v1 options have no concept of
    * categories, so this field will be left
    * as NULL */
   options_v2->categories  = NULL;
   options_v2->definitions = NULL;

   /* Allocate output option_v2_defs array
    * > One extra entry required for terminating NULL entry
    * > Note that calloc() sets terminating NULL entry and
    *   correctly 'nullifies' each values array */
   if (!(option_v2_defs = (struct retro_core_option_v2_definition *)
         calloc(num_options + 1, sizeof(*option_v2_defs))))
   {
      core_option_manager_free_converted(options_v2);
      return NULL;
   }

   options_v2->definitions = option_v2_defs;

   /* Loop through options... */
   for (i = 0; i < num_options; i++)
   {
      size_t j;
      size_t num_values                            = 0;
      const char *key                              = option_defs_us[i].key;
      const char *local_desc                       = NULL;
      const char *local_info                       = NULL;
      struct retro_core_option_value *local_values = NULL;

      /* Key is always taken from us english defs */
      option_v2_defs[i].key = key;

      /* Default value is always taken from us english defs */
      option_v2_defs[i].default_value = option_defs_us[i].default_value;

      /* Try to find corresponding entry in local defs array */
      if (option_defs_local)
      {
         size_t index = 0;

         for (;;)
         {
            const char *local_key = option_defs_local[index].key;

            if (string_is_empty(local_key))
               break;

            if (string_is_equal(key, local_key))
            {
               local_desc   = option_defs_local[index].desc;
               local_info   = option_defs_local[index].info;
               local_values = option_defs_local[index].values;
               break;
            }

            index++;
         }
      }

      /* Set desc and info strings */
      option_v2_defs[i].desc = string_is_empty(local_desc) ?
            option_defs_us[i].desc : local_desc;
      option_v2_defs[i].info = string_is_empty(local_info) ?
            option_defs_us[i].info : local_info;

      /* v1 options have no concept of categories
       * (Note: These are already nullified by
       * the preceding calloc(), but we do it
       * explicitly here for code clarity) */
      option_v2_defs[i].desc_categorized = NULL;
      option_v2_defs[i].info_categorized = NULL;
      option_v2_defs[i].category_key     = NULL;

      /* Determine number of values
       * (always taken from us english defs) */
      for (;;)
      {
         if (string_is_empty(option_defs_us[i].values[num_values].value))
            break;
         num_values++;
      }

      /* Copy values */
      for (j = 0; j < num_values; j++)
      {
         const char *value       = option_defs_us[i].values[j].value;
         const char *local_label = NULL;

         /* Value string is always taken from us english defs */
         option_v2_defs[i].values[j].value = value;

         /* Try to find corresponding entry in local defs values array */
         if (local_values)
         {
            size_t value_index = 0;

            for (;;)
            {
               const char *local_value = local_values[value_index].value;

               if (string_is_empty(local_value))
                  break;

               if (string_is_equal(value, local_value))
               {
                  local_label = local_values[value_index].label;
                  break;
               }

               value_index++;
            }
         }

         /* Set value label string */
         option_v2_defs[i].values[j].label = string_is_empty(local_label) ?
               option_defs_us[i].values[j].label : local_label;
      }
   }

   return options_v2;
}

/**
 * core_option_manager_convert_v2_intl:
 *
 * @options_v2_intl : pointer to a retro_core_options_v2_intl
 *                    struct
 *
 * Converts a v2 'international' core options struct
 * into a regular v2 core options struct. Returned pointer
 * must be freed using core_option_manager_free_converted().
 *
 * Returns: Valid pointer to a new v2 core options struct
 * if successful, otherwise NULL.
 **/
struct retro_core_options_v2 *core_option_manager_convert_v2_intl(
      const struct retro_core_options_v2_intl *options_v2_intl)
{
   size_t i;
   size_t num_categories                                  = 0;
   size_t num_options                                     = 0;
   struct retro_core_options_v2 *options_v2_us            = NULL;
   struct retro_core_options_v2 *options_v2_local         = NULL;
   struct retro_core_options_v2 *options_v2               = NULL;
   struct retro_core_option_v2_category *option_v2_cats   = NULL;
   struct retro_core_option_v2_definition *option_v2_defs = NULL;

   if (!options_v2_intl)
      return NULL;

   options_v2_us    = options_v2_intl->us;
   options_v2_local = options_v2_intl->local;

   if (!options_v2_us ||
       !options_v2_us->definitions)
      return NULL;

   /* Determine number of categories
    * (Note: zero categories are permitted) */
   if (options_v2_us->categories)
   {
      for (;;)
      {
         if (string_is_empty(options_v2_us->categories[num_categories].key))
            break;
         num_categories++;
      }
   }

   /* Determine number of options */
   for (;;)
   {
      if (string_is_empty(options_v2_us->definitions[num_options].key))
         break;
      num_options++;
   }

   if (num_options < 1)
      return NULL;

   /* Allocate output retro_core_options_v2 struct */
   if (!(options_v2 = (struct retro_core_options_v2 *)
         malloc(sizeof(*options_v2))))
      return NULL;

   options_v2->categories  = NULL;
   options_v2->definitions = NULL;

   /* Allocate output option_v2_cats array
    * > One extra entry required for terminating NULL entry
    * > Note that calloc() sets terminating NULL entry */
   if (num_categories > 0)
   {
      if (!(option_v2_cats = (struct retro_core_option_v2_category *)
            calloc(num_categories + 1, sizeof(*option_v2_cats))))
      {
         core_option_manager_free_converted(options_v2);
         return NULL;
      }
   }

   options_v2->categories = option_v2_cats;

   /* Allocate output option_v2_defs array
    * > One extra entry required for terminating NULL entry
    * > Note that calloc() sets terminating NULL entry and
    *   correctly 'nullifies' each values array */
   if (!(option_v2_defs = (struct retro_core_option_v2_definition *)
         calloc(num_options + 1, sizeof(*option_v2_defs))))
   {
      core_option_manager_free_converted(options_v2);
      return NULL;
   }

   options_v2->definitions = option_v2_defs;

   /* Loop through categories...
    * (Note: This loop will not execute if
    * options_v2_us->categories is NULL) */
   for (i = 0; i < num_categories; i++)
   {
      const char *key        = options_v2_us->categories[i].key;
      const char *local_desc = NULL;
      const char *local_info = NULL;

      /* Key is always taken from us english
       * categories */
      option_v2_cats[i].key = key;

      /* Try to find corresponding entry in local
       * categories array */
      if (options_v2_local &&
          options_v2_local->categories)
      {
         size_t index = 0;

         for (;;)
         {
            const char *local_key = options_v2_local->categories[index].key;

            if (string_is_empty(local_key))
               break;

            if (string_is_equal(key, local_key))
            {
               local_desc = options_v2_local->categories[index].desc;
               local_info = options_v2_local->categories[index].info;
               break;
            }

            index++;
         }
      }

      /* Set desc and info strings */
      option_v2_cats[i].desc = string_is_empty(local_desc) ?
            options_v2_us->categories[i].desc : local_desc;
      option_v2_cats[i].info = string_is_empty(local_info) ?
            options_v2_us->categories[i].info : local_info;
      
   }

   /* Loop through options... */
   for (i = 0; i < num_options; i++)
   {
      size_t j;
      size_t num_values                            = 0;
      const char *key                              = options_v2_us->definitions[i].key;
      const char *local_desc                       = NULL;
      const char *local_desc_categorized           = NULL;
      const char *local_info                       = NULL;
      const char *local_info_categorized           = NULL;
      struct retro_core_option_value *local_values = NULL;

      /* Key is always taken from us english defs */
      option_v2_defs[i].key = key;

      /* Default value is always taken from us english defs */
      option_v2_defs[i].default_value = options_v2_us->definitions[i].default_value;

      /* Try to find corresponding entry in local defs array */
      if (options_v2_local &&
          options_v2_local->definitions)
      {
         size_t index = 0;

         for (;;)
         {
            const char *local_key = options_v2_local->definitions[index].key;

            if (string_is_empty(local_key))
               break;

            if (string_is_equal(key, local_key))
            {
               local_desc             = options_v2_local->definitions[index].desc;
               local_desc_categorized = options_v2_local->definitions[index].desc_categorized;
               local_info             = options_v2_local->definitions[index].info;
               local_info_categorized = options_v2_local->definitions[index].info_categorized;
               local_values           = options_v2_local->definitions[index].values;
               break;
            }

            index++;
         }
      }

      /* Set desc and info strings */
      option_v2_defs[i].desc             = string_is_empty(local_desc) ?
            options_v2_us->definitions[i].desc : local_desc;
      option_v2_defs[i].desc_categorized = string_is_empty(local_desc_categorized) ?
            options_v2_us->definitions[i].desc_categorized : local_desc_categorized;
      option_v2_defs[i].info             = string_is_empty(local_info) ?
            options_v2_us->definitions[i].info : local_info;
      option_v2_defs[i].info_categorized = string_is_empty(local_info_categorized) ?
            options_v2_us->definitions[i].info_categorized : local_info_categorized;

      /* Category key is always taken from us english defs */
      option_v2_defs[i].category_key = options_v2_us->definitions[i].category_key;

      /* Determine number of values
       * (always taken from us english defs) */
      for (;;)
      {
         if (string_is_empty(
               options_v2_us->definitions[i].values[num_values].value))
            break;
         num_values++;
      }

      /* Copy values */
      for (j = 0; j < num_values; j++)
      {
         const char *value       = options_v2_us->definitions[i].values[j].value;
         const char *local_label = NULL;

         /* Value string is always taken from us english defs */
         option_v2_defs[i].values[j].value = value;

         /* Try to find corresponding entry in local defs values array */
         if (local_values)
         {
            size_t value_index = 0;

            for (;;)
            {
               const char *local_value = local_values[value_index].value;

               if (string_is_empty(local_value))
                  break;

               if (string_is_equal(value, local_value))
               {
                  local_label = local_values[value_index].label;
                  break;
               }

               value_index++;
            }
         }

         /* Set value label string */
         option_v2_defs[i].values[j].label = string_is_empty(local_label) ?
               options_v2_us->definitions[i].values[j].label : local_label;
      }
   }

   return options_v2;
}

/**
 * core_option_manager_convert_v2_intl:
 *
 * @options_v2 : pointer to a retro_core_options_v2
 *               struct
 *
 * Frees the pointer returned by any
 * core_option_manager_convert_*() function.
 **/
void core_option_manager_free_converted(
      struct retro_core_options_v2 *options_v2)
{
   if (!options_v2)
      return;

   if (options_v2->categories)
   {
      free(options_v2->categories);
      options_v2->categories = NULL;
   }

   if (options_v2->definitions)
   {
      free(options_v2->definitions);
      options_v2->definitions = NULL;
   }

   free(options_v2);
}

/**************************************/
/* Initialisation / De-Initialisation */
/**************************************/

/* Generates a hash key for the specified string */
static uint32_t core_option_manager_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);
}

/* Sanitises a core option value label, handling the case
 * where an explicit label is not provided and performing
 * conversion of various true/false identifiers to
 * ON/OFF strings */
static const char *core_option_manager_parse_value_label(
      const char *value, const char *value_label)
{
   /* 'value_label' may be NULL */
   const char *label = string_is_empty(value_label) ?
         value : value_label;

   if (string_is_empty(label))
      return NULL;

   /* Any label starting with a digit (or +/-)
    * cannot be a boolean string, and requires
    * no further processing */
   if (ISDIGIT((unsigned char)*label) ||
       (*label == '+') ||
       (*label == '-'))
      return label;

   /* Core devs have a habit of using arbitrary
    * strings to label boolean values (i.e. enabled,
    * Enabled, on, On, ON, true, True, TRUE, disabled,
    * Disabled, off, Off, OFF, false, False, FALSE).
    * These should all be converted to standard ON/OFF
    * strings
    * > Note: We require some duplication here
    *   (e.g. MENU_ENUM_LABEL_ENABLED *and*
    *    MENU_ENUM_LABEL_VALUE_ENABLED) in order
    *   to match both localised and non-localised
    *   strings. This function is not performance
    *   critical, so these extra comparisons do
    *   no harm */
   if (string_is_equal_noncase(label, msg_hash_to_str(MENU_ENUM_LABEL_ENABLED)) ||
       string_is_equal_noncase(label, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ENABLED)) ||
       string_is_equal_noncase(label, "enable") ||
       string_is_equal_noncase(label, "on") ||
       string_is_equal_noncase(label, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ON)) ||
       string_is_equal_noncase(label, "true") ||
       string_is_equal_noncase(label, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_TRUE)))
      label = msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ON);
   else if (string_is_equal_noncase(label, msg_hash_to_str(MENU_ENUM_LABEL_DISABLED)) ||
            string_is_equal_noncase(label, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_DISABLED)) ||
            string_is_equal_noncase(label, "disable") ||
            string_is_equal_noncase(label, "off") ||
            string_is_equal_noncase(label, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_OFF)) ||
            string_is_equal_noncase(label, "false") ||
            string_is_equal_noncase(label, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_FALSE)))
      label = msg_hash_to_str(MENU_ENUM_LABEL_VALUE_OFF);

   return label;
}

/* Parses a single legacy core options interface
 * variable, extracting all present core_option
 * information */
static bool core_option_manager_parse_variable(
      core_option_manager_t *opt, size_t idx,
      const struct retro_variable *var,
      config_file_t *config_src)
{
   size_t i;
   union string_list_elem_attr attr;
   const char *val_start      = NULL;
   char *value                = NULL;
   char *desc_end             = NULL;
   struct core_option *option = (struct core_option*)&opt->opts[idx];
   struct config_entry_list
         *entry               = NULL;

   /* Record option index (required to facilitate
    * option map handling) */
   option->opt_idx            = idx;

   /* All options are visible by default */
   option->visible            = true;

   if (!string_is_empty(var->key))
   {
      option->key             = strdup(var->key);
      option->key_hash        = core_option_manager_hash_string(var->key);
   }

   if (!string_is_empty(var->value))
      value                   = strdup(var->value);

   if (!string_is_empty(value))
      desc_end                = strstr(value, "; ");

   if (!desc_end)
      goto error;

   *desc_end    = '\0';

   if (!string_is_empty(value))
      option->desc    = strdup(value);

   val_start          = desc_end + 2;
   option->vals       = string_split(val_start, "|");

   if (!option->vals)
      goto error;

   /* Legacy core option interface has no concept
    * of value labels
    * > Use actual values for display purposes */
   attr.i             = 0;
   option->val_labels = string_list_new();

   if (!option->val_labels)
      goto error;

   /* > Loop over values and:
    *   - Set value hashes
    *   - 'Extract' labels */
   for (i = 0; i < option->vals->size; i++)
   {
      const char *value       = option->vals->elems[i].data;
      uint32_t *value_hash    = (uint32_t *)malloc(sizeof(uint32_t));
      const char *value_label = core_option_manager_parse_value_label(
            value, NULL);

      /* Set value hash */
      *value_hash                     = core_option_manager_hash_string(value);
      option->vals->elems[i].userdata = (void*)value_hash;

      /* Redundant safely check... */
      if (string_is_empty(value_label))
         value_label = value;

      /* Append value label string */
      string_list_append(option->val_labels, value_label, attr);
   }

   /* Legacy core option interface always uses first
    * defined value as the default */
   option->default_index = 0;
   option->index         = 0;

   if (config_src)
      entry              = config_get_entry(config_src, option->key);
   else
      entry              = config_get_entry(opt->conf,  option->key);

   /* Set current config value */
   if (entry && !string_is_empty(entry->value))
   {
      uint32_t entry_value_hash = core_option_manager_hash_string(entry->value);

      for (i = 0; i < option->vals->size; i++)
      {
         const char *value   = option->vals->elems[i].data;
         uint32_t value_hash = *((uint32_t*)option->vals->elems[i].userdata);

         if ((value_hash == entry_value_hash) &&
             string_is_equal(value, entry->value))
         {
            option->index = i;
            break;
         }
      }
   }

   /* Legacy core option interface has no concept
    * of categories */
   option->desc_categorized = NULL;
   option->info_categorized = NULL;
   option->category_key     = NULL;

   free(value);
   return true;

error:
   free(value);
   return false;
}

/**
 * core_option_manager_new_vars:
 *
 * @conf_path     : Filesystem path to write core option
 *                  config file to
 * @src_conf_path : Filesystem path from which to load
 *                  initial config settings.
 * @vars          : Pointer to core option variable array
 *                  handle
 *
 * Legacy version of core_option_manager_new().
 * Creates and initializes a core manager handle.
 *
 * Returns: handle to new core manager handle if successful,
 * otherwise NULL.
 **/
core_option_manager_t *core_option_manager_new_vars(
      const char *conf_path, const char *src_conf_path,
      const struct retro_variable *vars)
{
   const struct retro_variable *var = NULL;
   size_t size                      = 0;
   config_file_t *config_src        = NULL;
   core_option_manager_t *opt       = NULL;

   if (!vars)
      return NULL;

   if (!(opt = (core_option_manager_t*)malloc(sizeof(*opt))))
      return NULL;

   opt->conf                        = NULL;
   opt->conf_path[0]                = '\0';
   /* Legacy core option interface has no concept
    * of categories, so leave opt->cats as NULL
    * and opt->cats_size as zero */
   opt->cats                        = NULL;
   opt->cats_size                   = 0;
   opt->opts                        = NULL;
   opt->size                        = 0;
   opt->option_map                  = nested_list_init();
   opt->updated                     = false;

   if (!opt->option_map)
      goto error;

   /* Open 'output' config file */
   if (!string_is_empty(conf_path))
      if (!(opt->conf = config_file_new_from_path_to_string(conf_path)))
         if (!(opt->conf = config_file_new_alloc()))
            goto error;

   strlcpy(opt->conf_path, conf_path, sizeof(opt->conf_path));

   /* Load source config file, if required */
   if (!string_is_empty(src_conf_path))
      config_src = config_file_new_from_path_to_string(src_conf_path);

   /* Get number of variables */
   for (var = vars; var->key && var->value; var++)
      size++;

   if (size == 0)
      goto error;

   /* Create options array */
   if (!(opt->opts = (struct core_option*)calloc(size, sizeof(*opt->opts))))
      goto error;

   opt->size = size;
   size      = 0;

   /* Parse each variable */
   for (var = vars; var->key && var->value; size++, var++)
   {
      if (core_option_manager_parse_variable(opt, size, var, config_src))
      {
         /* If variable is read correctly, add it to
          * the map */
         char address[256];

         address[0] = '#';
         address[1] = '\0';

         /* Address string is normally:
          *    <category_key><delim><tag><option_key>
          * ...where <tag> is prepended to the option
          * key in order to avoid category/option key
          * collisions. Legacy options have no categories,
          * so we could just set the address to
          * <option_key> - but for consistency with
          * 'modern' options, we apply the tag regardless */
         strlcat(address, var->key, sizeof(address));

         if (!nested_list_add_item(opt->option_map,
               address, NULL, (const void*)&opt->opts[size]))
            goto error;
      }
      else
         goto error;
   }

   if (config_src)
      config_file_free(config_src);

   return opt;

error:
   if (config_src)
      config_file_free(config_src);
   core_option_manager_free(opt);
   return NULL;
}

/* Parses a single v2 core options interface
 * option, extracting all present core_option
 * information */
static bool core_option_manager_parse_option(
      core_option_manager_t *opt, size_t idx,
      const struct retro_core_option_v2_definition *option_def,
      config_file_t *config_src)
{
   size_t i;
   union string_list_elem_attr attr;
   struct config_entry_list
      *entry                  = NULL;
   size_t num_vals            = 0;
   struct core_option *option = (struct core_option*)&opt->opts[idx];
   const char *key            = option_def->key;
   const char *category_key   = option_def->category_key;
   const struct retro_core_option_value
         *values              = option_def->values;

   /* Record option index (required to facilitate
    * option map handling) */
   option->opt_idx            = idx;

   /* All options are visible by default */
   option->visible            = true;

   if (!string_is_empty(option_def->desc))
      option->desc            = strdup(option_def->desc);

   if (!string_is_empty(option_def->info))
      option->info            = strdup(option_def->info);

   /* Set category-related parameters
    * > Ignore if specified category key does not
    *   match an entry in the categories array
    * > Category key cannot contain a map delimiter
    *   character */
   if (opt->cats &&
       !string_is_empty(category_key) &&
       !strstr(category_key, ":"))
   {
      for (i = 0; i < opt->cats_size; i++)
      {
         const char *search_key = opt->cats[i].key;

         if (string_is_empty(search_key))
            break;

         if (string_is_equal(search_key, category_key))
         {
            option->category_key        = strdup(category_key);

            if (!string_is_empty(option_def->desc_categorized))
               option->desc_categorized = strdup(option_def->desc_categorized);

            if (!string_is_empty(option_def->info_categorized))
               option->info_categorized = strdup(option_def->info_categorized);

            break;
         }
      }
   }

   /* Have to set key *after* checking for option
    * categories */
   if (!string_is_empty(option_def->key))
   {
      /* If option has a category, option key
       * cannot contain a map delimiter character */
      if (!string_is_empty(option->category_key) &&
          strstr(key, ":"))
         return false;

      option->key      = strdup(key);
      option->key_hash = core_option_manager_hash_string(key);
   }

   /* Get number of values */
   for (;;)
   {
      if (string_is_empty(values[num_vals].value))
         break;
      num_vals++;
   }

   if (num_vals < 1)
      return false;

   /* Initialise string lists */
   attr.i             = 0;
   option->vals       = string_list_new();
   option->val_labels = string_list_new();

   if (!option->vals || !option->val_labels)
      return false;

   /* Initialise default value */
   option->default_index = 0;
   option->index         = 0;

   /* Extract value/label pairs */
   for (i = 0; i < num_vals; i++)
   {
      const char *value       = values[i].value;
      uint32_t *value_hash    = (uint32_t *)malloc(sizeof(uint32_t));
      const char *value_label = values[i].label;

      /* Append value string
       * > We know that 'value' is always valid */
      string_list_append(option->vals, value, attr);

      /* > Set value hash */
      *value_hash = core_option_manager_hash_string(value);
      option->vals->elems[option->vals->size - 1].userdata = (void*)value_hash;

      /* Value label requires additional processing */
      value_label = core_option_manager_parse_value_label(
            value, value_label);

      /* > Redundant safely check... */
      if (string_is_empty(value_label))
         value_label = value;

      /* Append value label string */
      string_list_append(option->val_labels, value_label, attr);

      /* Check whether this value is the default setting */
      if (!string_is_empty(option_def->default_value))
      {
         if (string_is_equal(option_def->default_value, value))
         {
            option->default_index = i;
            option->index         = i;
         }
      }
   }

   if (config_src)
      entry              = config_get_entry(config_src, option->key);
   else
      entry              = config_get_entry(opt->conf,  option->key);

   /* Set current config value */
   if (entry && !string_is_empty(entry->value))
   {
      uint32_t entry_value_hash = core_option_manager_hash_string(entry->value);

      for (i = 0; i < option->vals->size; i++)
      {
         const char *value   = option->vals->elems[i].data;
         uint32_t value_hash = *((uint32_t*)option->vals->elems[i].userdata);

         if ((value_hash == entry_value_hash) &&
             string_is_equal(value, entry->value))
         {
            option->index = i;
            break;
         }
      }
   }

   return true;
}

/**
 * core_option_manager_new:
 *
 * @conf_path     : Filesystem path to write core option
 *                  config file to
 * @src_conf_path : Filesystem path from which to load
 *                  initial config settings.
 * @options_v2    : Pointer to retro_core_options_v2 struct
 * @categorized   : Flag specifying whether core option
 *                  category information should be read
 *                  from @options_v2
 *
 * Creates and initializes a core manager handle. Parses
 * information from a retro_core_options_v2 struct.
 * If @categorized is false, all option category
 * assignments will be ignored.
 *
 * Returns: handle to new core manager handle if successful,
 * otherwise NULL.
 **/
core_option_manager_t *core_option_manager_new(
      const char *conf_path, const char *src_conf_path,
      const struct retro_core_options_v2 *options_v2,
      bool categorized)
{
   const struct retro_core_option_v2_category *option_cat   = NULL;
   const struct retro_core_option_v2_definition *option_def = NULL;
   struct retro_core_option_v2_category *option_cats        = NULL;
   struct retro_core_option_v2_definition *option_defs      = NULL;
   size_t cats_size                                         = 0;
   size_t size                                              = 0;
   config_file_t *config_src                                = NULL;
   core_option_manager_t *opt                               = NULL;

   if (!options_v2 ||
       !options_v2->definitions)
      return NULL;

   option_cats = options_v2->categories;
   option_defs = options_v2->definitions;

   if (!(opt = (core_option_manager_t*)malloc(sizeof(*opt))))
      return NULL;

   opt->conf                         = NULL;
   opt->conf_path[0]                 = '\0';
   opt->cats                         = NULL;
   opt->cats_size                    = 0;
   opt->opts                         = NULL;
   opt->size                         = 0;
   opt->option_map                   = nested_list_init();
   opt->updated                      = false;

   if (!opt->option_map)
      goto error;

   /* Open 'output' config file */
   if (!string_is_empty(conf_path))
      if (!(opt->conf = config_file_new_from_path_to_string(conf_path)))
         if (!(opt->conf = config_file_new_alloc()))
            goto error;

   strlcpy(opt->conf_path, conf_path, sizeof(opt->conf_path));

   /* Load source config file, if required */
   if (!string_is_empty(src_conf_path))
      config_src = config_file_new_from_path_to_string(src_conf_path);

   /* Get number of categories, if required
    * > Note: 'option_cat->info == NULL' is valid */
   if (categorized && option_cats)
   {
      for (option_cat = option_cats;
           !string_is_empty(option_cat->key) &&
                  !string_is_empty(option_cat->desc);
           option_cat++)
         cats_size++;
   }

   /* Get number of options
    * > Note: 'option_def->info == NULL' is valid */
   for (option_def = option_defs;
        option_def->key && option_def->desc && option_def->values[0].value;
        option_def++)
      size++;

   if (size == 0)
      goto error;

   /* Create categories array */
   if (cats_size > 0)
   {
      if (!(opt->cats = (struct core_category*)calloc(size,
                  sizeof(*opt->cats))))
         goto error;

      opt->cats_size = cats_size;
      cats_size      = 0;

      /* Parse each category
       * > Note: 'option_cat->info == NULL' is valid */
      for (option_cat = option_cats;
           !string_is_empty(option_cat->key) &&
                  !string_is_empty(option_cat->desc);
           cats_size++, option_cat++)
      {
         opt->cats[cats_size].key      = strdup(option_cat->key);
         opt->cats[cats_size].key_hash = core_option_manager_hash_string(option_cat->key);
         opt->cats[cats_size].desc     = strdup(option_cat->desc);

         if (!string_is_empty(option_cat->info))
            opt->cats[cats_size].info  = strdup(option_cat->info);
      }
   }

   /* Create options array */
   if (!(opt->opts = (struct core_option*)calloc(size, sizeof(*opt->opts))))
      goto error;

   opt->size = size;
   size      = 0;

   /* Parse each option
    * > Note: 'option_def->info == NULL' is valid */
   for (option_def = option_defs;
        option_def->key && option_def->desc && option_def->values[0].value;
        size++, option_def++)
   {
      if (core_option_manager_parse_option(opt, size, option_def, config_src))
      {
         /* If option is read correctly, add it to
          * the map */
         const char *category_key = opt->opts[size].category_key;
         char address[256];


         /* Address string is nominally:
          *    <category_key><delim><tag><option_key>
          * ...where <tag> is prepended to the option
          * key in order to avoid category/option key
          * collisions */
         if (string_is_empty(category_key))
         {
            address[0] = '#';
            address[1] = '\0';
            strlcat(address, option_def->key, sizeof(address));
         }
         else
         {
            size_t _len      = strlcpy(address, category_key, sizeof(address));
            address[_len  ]  = ':';
            address[_len+1]  = '#';
            address[_len+2]  = '\0';
            strlcat(address, option_def->key, sizeof(address));
         }

         if (!nested_list_add_item(opt->option_map,
               address, ":",
               (const void*)&opt->opts[size]))
            goto error;
      }
      else
         goto error;
   }

   if (config_src)
      config_file_free(config_src);

   return opt;

error:
   if (config_src)
      config_file_free(config_src);
   core_option_manager_free(opt);
   return NULL;
}

/**
 * core_option_manager_free:
 *
 * @opt : options manager handle
 *
 * Frees specified core options manager handle.
 **/
void core_option_manager_free(core_option_manager_t *opt)
{
   size_t i;

   if (!opt)
      return;

   for (i = 0; i < opt->cats_size; i++)
   {
      if (opt->cats[i].key)
         free(opt->cats[i].key);
      if (opt->cats[i].desc)
         free(opt->cats[i].desc);
      if (opt->cats[i].info)
         free(opt->cats[i].info);

      opt->cats[i].key  = NULL;
      opt->cats[i].desc = NULL;
      opt->cats[i].info = NULL;
   }

   for (i = 0; i < opt->size; i++)
   {
      if (opt->opts[i].desc)
         free(opt->opts[i].desc);
      if (opt->opts[i].desc_categorized)
         free(opt->opts[i].desc_categorized);
      if (opt->opts[i].info)
         free(opt->opts[i].info);
      if (opt->opts[i].info_categorized)
         free(opt->opts[i].info_categorized);
      if (opt->opts[i].key)
         free(opt->opts[i].key);
      if (opt->opts[i].category_key)
         free(opt->opts[i].category_key);

      if (opt->opts[i].vals)
         string_list_free(opt->opts[i].vals);
      if (opt->opts[i].val_labels)
         string_list_free(opt->opts[i].val_labels);

      opt->opts[i].desc             = NULL;
      opt->opts[i].desc_categorized = NULL;
      opt->opts[i].info             = NULL;
      opt->opts[i].info_categorized = NULL;
      opt->opts[i].key              = NULL;
      opt->opts[i].category_key     = NULL;
      opt->opts[i].vals             = NULL;
   }

   if (opt->option_map)
      nested_list_free(opt->option_map);

   if (opt->conf)
      config_file_free(opt->conf);

   free(opt->cats);
   free(opt->opts);
   free(opt);
}

/********************/
/* Category Getters */
/********************/

/**
 * core_option_manager_get_category_desc:
 *
 * @opt : options manager handle
 * @key : core option category id string
 *
 * Fetches the 'description' text of the core option
 * category identified by @key (used as the
 * category label in the menu).
 *
 * Returns: description string (menu label) of the
 * specified option category if successful,
 * otherwise NULL.
 **/
const char *core_option_manager_get_category_desc(core_option_manager_t *opt,
      const char *key)
{
   uint32_t key_hash;
   size_t i;

   if (  !opt
       || string_is_empty(key))
      return NULL;

   key_hash = core_option_manager_hash_string(key);

   for (i = 0; i < opt->cats_size; i++)
   {
      struct core_category *category = &opt->cats[i];

      if (   (key_hash == category->key_hash)
          && !string_is_empty(category->key)
          &&  string_is_equal(key, category->key))
         return category->desc;
   }

   return NULL;
}

/**
 * core_option_manager_get_category_info:
 *
 * @opt : options manager handle
 * @key : core option category id string
 *
 * Fetches the 'info' text of the core option
 * category identified by @key (used as the category
 * sublabel in the menu).
 *
 * Returns: information string (menu sublabel) of
 * the specified option category if successful,
 * otherwise NULL.
 **/
const char *core_option_manager_get_category_info(core_option_manager_t *opt,
      const char *key)
{
   uint32_t key_hash;
   size_t i;

   if (!opt ||
       string_is_empty(key))
      return NULL;

   key_hash = core_option_manager_hash_string(key);

   for (i = 0; i < opt->cats_size; i++)
   {
      struct core_category *category = &opt->cats[i];

      if (   (key_hash == category->key_hash)
          && !string_is_empty(category->key)
          &&  string_is_equal(key, category->key))
         return category->info;
   }

   return NULL;
}

/**
 * core_option_manager_get_category_visible:
 *
 * @opt : options manager handle
 * @key : core option category id string
 *
 * Queries whether the core option category
 * identified by @key should be displayed in
 * the frontend menu. (A category is deemed to
 * be visible if at least one of the options
 * in the category is visible)
 *
 * @return true if option category should be
 * displayed by the frontend, otherwise false.
 **/
bool core_option_manager_get_category_visible(core_option_manager_t *opt,
      const char *key)
{
   size_t i;
   nested_list_item_t *category_item = NULL;
   nested_list_t *option_list        = NULL;

   if (  !opt 
       || string_is_empty(key))
      return false;

   /* Fetch category item from map */
   if (!(category_item = nested_list_get_item(opt->option_map,
         key, NULL)))
      return false;

   /* Get child options of specified category */
   if (!(option_list = nested_list_item_get_children(category_item)))
      return false;

   /* Loop over child options */
   for (i = 0; i < nested_list_get_size(option_list); i++)
   {
      nested_list_item_t *option_item  = nested_list_get_item_idx(option_list, i);
      const struct core_option *option = (const struct core_option *)
            nested_list_item_get_value(option_item);
      /* Check if current option is visible */
      if (option && option->visible)
         return true;
   }

   return false;
}

/******************/
/* Option Getters */
/******************/

/**
 * core_option_manager_get_idx:
 *
 * @opt : options manager handle
 * @key : core option key string (variable to query
 *        in RETRO_ENVIRONMENT_GET_VARIABLE)
 * @idx : index of core option corresponding
 *        to @key
 *
 * Fetches the index of the core option identified
 * by the specified @key.
 *
 * @return true if option matching the specified
 * key was found, otherwise false.
 **/
bool core_option_manager_get_idx(core_option_manager_t *opt,
      const char *key, size_t *idx)
{
   uint32_t key_hash;
   size_t i;

   if (  !opt
       || string_is_empty(key)
       || !idx)
      return false;

   key_hash = core_option_manager_hash_string(key);

   for (i = 0; i < opt->size; i++)
   {
      struct core_option *option = &opt->opts[i];

      if (   (key_hash == option->key_hash)
          && !string_is_empty(option->key)
          &&  string_is_equal(key, option->key))
      {
         *idx = i;
         return true;
      }
   }

   return false;
}

/**
 * core_option_manager_get_val_idx:
 *
 * @opt     : options manager handle
 * @idx     : core option index
 * @val     : string representation of the
 *            core option value
 * @val_idx : index of core option value
 *            corresponding to @val
 *
 * Fetches the index of the core option value
 * identified by the specified core option @idx
 * and @val string.
 *
 * Returns: true if option value matching the
 * specified option index and value string
 * was found, otherwise false.
 **/
bool core_option_manager_get_val_idx(core_option_manager_t *opt,
      size_t idx, const char *val, size_t *val_idx)
{
   struct core_option *option = NULL;
   uint32_t val_hash;
   size_t i;

   if (   !opt
       || (idx >= opt->size)
       || string_is_empty(val)
       || !val_idx)
      return false;

   val_hash = core_option_manager_hash_string(val);
   option   = (struct core_option*)&opt->opts[idx];

   for (i = 0; i < option->vals->size; i++)
   {
      const char *option_val   = option->vals->elems[i].data;
      uint32_t option_val_hash = *((uint32_t*)option->vals->elems[i].userdata);

      if ((val_hash == option_val_hash) &&
          !string_is_empty(option_val) &&
          string_is_equal(val, option_val))
      {
         *val_idx = i;
         return true;
      }
   }

   return false;
}

/**
 * core_option_manager_get_desc:
 *
 * @opt         : options manager handle
 * @idx         : core option index
 * @categorized : flag specifying whether to
 *                fetch the categorised description
 *                or the legacy fallback
 *
 * Fetches the 'description' of the core option at
 * index @idx (used as the option label in the menu).
 * If menu has option category support, @categorized
 * should be true. (At present, only the Qt interface
 * requires @categorized to be false)
 *
 * Returns: description string (menu label) of the
 * specified option if successful, otherwise NULL.
 **/
const char *core_option_manager_get_desc(core_option_manager_t *opt,
      size_t idx, bool categorized)
{
   const char *desc = NULL;

   if (   !opt
       || (idx >= opt->size))
      return NULL;
   /* Try categorised description first,
    * if requested */
   if (categorized)
      desc = opt->opts[idx].desc_categorized;
   /* Fall back to legacy description, if
    * required */
   if (string_is_empty(desc))
      return opt->opts[idx].desc;
   return desc;
}

/**
 * core_option_manager_get_info:
 *
 * @opt         : options manager handle
 * @idx         : core option index
 * @categorized : flag specifying whether to
 *                fetch the categorised information
 *                or the legacy fallback
 *
 * Fetches the 'info' text of the core option at
 * index @idx (used as the option sublabel in the
 * menu). If menu has option category support,
 * @categorized should be true. (At present, only
 * the Qt interface requires @categorized to be false)
 *
 * Returns: information string (menu sublabel) of the
 * specified option if successful, otherwise NULL.
 **/
const char *core_option_manager_get_info(core_option_manager_t *opt,
      size_t idx, bool categorized)
{
   const char *info = NULL;

   if (   !opt
       || (idx >= opt->size))
      return NULL;

   /* Try categorised information first,
    * if requested */
   if (categorized)
      info = opt->opts[idx].info_categorized;
   /* Fall back to legacy information, if
    * required */
   if (string_is_empty(info))
      return opt->opts[idx].info;
   return info;
}

/**
 * core_option_manager_get_val:
 *
 * @opt : options manager handle
 * @idx : core option index
 *
 * Fetches the string representation of the current
 * value of the core option at index @idx.
 *
 * Returns: core option value string if successful,
 * otherwise NULL.
 **/
const char *core_option_manager_get_val(core_option_manager_t *opt,
      size_t idx)
{
   struct core_option *option = NULL;

   if (   !opt
       || (idx >= opt->size))
      return NULL;

   option = (struct core_option*)&opt->opts[idx];

   return option->vals->elems[option->index].data;
}

/**
 * core_option_manager_get_val_label:
 *
 * @opt : options manager handle
 * @idx : core option index
 *
 * Fetches the 'label' text (used for display purposes
 * in the menu) for the current value of the core
 * option at index @idx.
 *
 * Returns: core option value label string if
 * successful, otherwise NULL.
 **/
const char *core_option_manager_get_val_label(core_option_manager_t *opt,
      size_t idx)
{
   struct core_option *option = NULL;

   if (   !opt 
       || (idx >= opt->size))
      return NULL;

   option = (struct core_option*)&opt->opts[idx];

   return option->val_labels->elems[option->index].data;
}

/**
 * core_option_manager_get_visible:
 *
 * @opt : options manager handle
 * @idx : core option index
 *
 * Queries whether the core option at index @idx
 * should be displayed in the frontend menu.
 *
 * Returns: true if option should be displayed by
 * the frontend, otherwise false.
 **/
bool core_option_manager_get_visible(core_option_manager_t *opt,
      size_t idx)
{
   if (    !opt
       || (idx >= opt->size))
      return false;

   return opt->opts[idx].visible;
}

/******************/
/* Option Setters */
/******************/

/**
 * core_option_manager_set_val:
 *
 * @opt          : options manager handle
 * @idx          : core option index
 * @val_idx      : index of the value to set
 * @refresh_menu : flag specifying whether menu
 *                 should be refreshed if changes
 *                 to option visibility are detected
 *
 * Sets the core option at index @idx to the
 * option value corresponding to @val_idx.
 * After setting the option value, a request
 * will be made for the core to update the
 * in-menu visibility of all options; if
 * visibility changes are detected and
 * @refresh_menu is true, the menu will be
 * redrawn.
 **/
void core_option_manager_set_val(core_option_manager_t *opt,
      size_t idx, size_t val_idx, bool refresh_menu)
{
   struct core_option *option = NULL;

   if (!opt ||
       (idx >= opt->size))
      return;

   option        = (struct core_option*)&opt->opts[idx];
   option->index = val_idx % option->vals->size;
   opt->updated  = true;

#ifdef HAVE_CHEEVOS
   rcheevos_validate_config_settings();
#endif

#ifdef HAVE_MENU
   /* Refresh menu (if required) if core option
    * visibility has changed as a result of modifying
    * the current option value */
   if (retroarch_ctl(RARCH_CTL_CORE_OPTION_UPDATE_DISPLAY, NULL) &&
       refresh_menu)
   {
      struct menu_state *menu_st = menu_state_get_ptr();
      menu_st->flags            |=  MENU_ST_FLAG_ENTRIES_NEED_REFRESH
                                 |  MENU_ST_FLAG_PREVENT_POPULATE;
   }
#endif
}

/**
 * core_option_manager_adjust_val:
 *
 * @opt          : options manager handle
 * @idx          : core option index
 * @adjustment   : offset to apply from current
 *                 value index
 * @refresh_menu : flag specifying whether menu
 *                 should be refreshed if changes
 *                 to option visibility are detected
 *
 * Modifies the value of the core option at
 * index @idx by incrementing the current option
 * value index by @adjustment.
 * After setting the option value, a request
 * will be made for the core to update the
 * in-menu visibility of all options; if
 * visibility changes are detected and
 * @refresh_menu is true, the menu will be
 * redrawn.
 **/
void core_option_manager_adjust_val(core_option_manager_t* opt,
      size_t idx, int adjustment, bool refresh_menu)
{
   struct core_option* option = NULL;

   if (   !opt
       || (idx >= opt->size))
      return;

   option        = (struct core_option*)&opt->opts[idx];
   option->index = (option->index + option->vals->size + adjustment) % option->vals->size;
   opt->updated  = true;

#ifdef HAVE_CHEEVOS
   rcheevos_validate_config_settings();
#endif

#ifdef HAVE_MENU
   /* Refresh menu (if required) if core option
    * visibility has changed as a result of modifying
    * the current option value */
   if (retroarch_ctl(RARCH_CTL_CORE_OPTION_UPDATE_DISPLAY, NULL) &&
       refresh_menu)
   {
      struct menu_state *menu_st = menu_state_get_ptr();
      menu_st->flags            |=  MENU_ST_FLAG_ENTRIES_NEED_REFRESH
                                 |  MENU_ST_FLAG_PREVENT_POPULATE;
   }
#endif
}

/**
 * core_option_manager_set_default:
 *
 * @opt          : options manager handle
 * @idx          : core option index
 * @refresh_menu : flag specifying whether menu
 *                 should be refreshed if changes
 *                 to option visibility are detected
 *
 * Resets the core option at index @idx to
 * its default value.
 * After setting the option value, a request
 * will be made for the core to update the
 * in-menu visibility of all options; if
 * visibility changes are detected and
 * @refresh_menu is true, the menu will be
 * redrawn.
 **/
void core_option_manager_set_default(core_option_manager_t *opt,
      size_t idx, bool refresh_menu)
{
   if (   !opt
       || (idx >= opt->size))
      return;

   opt->opts[idx].index = opt->opts[idx].default_index;
   opt->updated         = true;

#ifdef HAVE_CHEEVOS
   rcheevos_validate_config_settings();
#endif

#ifdef HAVE_MENU
   /* Refresh menu (if required) if core option
    * visibility has changed as a result of modifying
    * the current option value */
   if (retroarch_ctl(RARCH_CTL_CORE_OPTION_UPDATE_DISPLAY, NULL) &&
       refresh_menu)
   {
      struct menu_state *menu_st = menu_state_get_ptr();
      menu_st->flags            |=  MENU_ST_FLAG_ENTRIES_NEED_REFRESH
                                 |  MENU_ST_FLAG_PREVENT_POPULATE;
   }
#endif
}

/**
 * core_option_manager_set_visible:
 *
 * @opt     : options manager handle
 * @key     : core option key string (variable to query
 *            in RETRO_ENVIRONMENT_GET_VARIABLE)
 * @visible : flag specifying whether option should
 *            be shown in the menu
 *
 * Sets the in-menu visibility of the core option
 * identified by the specified @key.
 **/
void core_option_manager_set_visible(core_option_manager_t *opt,
      const char *key, bool visible)
{
   uint32_t key_hash;
   size_t i;

   if (!opt || string_is_empty(key))
      return;

   key_hash = core_option_manager_hash_string(key);

   for (i = 0; i < opt->size; i++)
   {
      struct core_option *option = &opt->opts[i];

      if ((key_hash == option->key_hash) &&
          !string_is_empty(option->key) &&
          string_is_equal(key, option->key))
      {
         option->visible = visible;
         return;
      }
   }
}

/**********************/
/* Configuration File */
/**********************/

/**
 * core_option_manager_flush:
 *
 * @opt  : options manager handle
 * @conf : configuration file handle
 *
 * Writes all core option key-pair values from the
 * specified core option manager handle to the
 * specified configuration file struct.
 **/
void core_option_manager_flush(core_option_manager_t *opt,
      config_file_t *conf)
{
   size_t i;

   for (i = 0; i < opt->size; i++)
   {
      struct core_option *option = (struct core_option*)&opt->opts[i];

      if (option)
         config_set_string(conf, option->key,
               opt->opts[i].vals->elems[opt->opts[i].index].data);
   }
}
