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

#include <string/stdstring.h>
#include <lists/string_list.h>
#include <file/file_path.h>
#include <streams/interface_stream.h>
#include <streams/file_stream.h>
#include <lists/dir_list.h>
#include <time/rtime.h>
#include <retro_miscellaneous.h>

#include "frontend/frontend_driver.h"
#include "file_path_special.h"
#include "verbosity.h"

#include "core_backup.h"

/* Holds all entries in a core backup list */
struct core_backup_list
{
   core_backup_list_entry_t *entries;
   size_t size;
   size_t capacity;
};

/*********************/
/* Utility Functions */
/*********************/

/* Generates backup directory path for specified core.
 * Returns false if 'core_path' and/or 'dir_core_assets'
 * are invalid, or a filesystem error occurs */
static bool core_backup_get_backup_dir(
      const char *dir_libretro, const char *dir_core_assets,
      const char *core_filename,
      char *backup_dir, size_t len)
{
   char *last_underscore = NULL;
   char core_file_id[PATH_MAX_LENGTH];
   char tmp[PATH_MAX_LENGTH];

   core_file_id[0] = '\0';
   tmp[0]          = '\0';

   /* Extract core file 'ID' (name without extension + suffix)
    * from core path */
   if (string_is_empty(dir_libretro) ||
       string_is_empty(core_filename) ||
       (len < 1))
      return false;

   strlcpy(core_file_id, core_filename, sizeof(core_file_id));

   /* > Remove file extension */
   path_remove_extension(core_file_id);

   if (string_is_empty(core_file_id))
      return false;

   /* > Remove platform-specific file name suffix,
    *   if required */
   last_underscore = strrchr(core_file_id, '_');

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

   if (string_is_empty(core_file_id))
      return false;

   /* Get core backup directory
    * > If no assets directory is defined, use
    *   core directory as a base */
   fill_pathname_join(tmp, string_is_empty(dir_core_assets) ?
         dir_libretro : dir_core_assets,
               "core_backups", sizeof(tmp));

   fill_pathname_join(backup_dir, tmp,
         core_file_id, len);

   if (string_is_empty(backup_dir))
      return false;

   /* > Create directory, if required */
   if (!path_is_directory(backup_dir))
   {
      if (!path_mkdir(backup_dir))
      {
         RARCH_ERR("[core backup] Failed to create backup directory: %s.\n", backup_dir);
         return false;
      }
   }

   return true;
}

/* Generates a timestamped core backup file path from
 * the specified core path. Returns true if successful */
bool core_backup_get_backup_path(
      const char *core_path, uint32_t crc, enum core_backup_mode backup_mode,
      const char *dir_core_assets, char *backup_path, size_t len)
{
   int n;
   time_t current_time;
   struct tm time_info;
   const char *core_filename = NULL;
   char core_dir[PATH_MAX_LENGTH];
   char backup_dir[PATH_MAX_LENGTH];
   char backup_filename[PATH_MAX_LENGTH];

   core_dir[0]        = '\0'; 
   backup_dir[0]      = '\0';
   backup_filename[0] = '\0';

   /* Get core filename and parent directory */
   if (string_is_empty(core_path))
      return false;

   core_filename = path_basename(core_path);

   if (string_is_empty(core_filename))
      return false;

   fill_pathname_parent_dir(core_dir, core_path, sizeof(core_dir));

   if (string_is_empty(core_dir))
      return false;

   /* Get backup directory */
   if (!core_backup_get_backup_dir(core_dir, dir_core_assets, core_filename,
         backup_dir, sizeof(backup_dir)))
      return false;

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

   /* Generate backup filename */
   n = snprintf(backup_filename, sizeof(backup_filename),
         "%s.%04u%02u%02uT%02u%02u%02u.%08x.%u%s",
         core_filename,
         (unsigned)time_info.tm_year + 1900,
         (unsigned)time_info.tm_mon + 1,
         (unsigned)time_info.tm_mday,
         (unsigned)time_info.tm_hour,
         (unsigned)time_info.tm_min,
         (unsigned)time_info.tm_sec,
         crc,
         (unsigned)backup_mode,
         FILE_PATH_CORE_BACKUP_EXTENSION);

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

   /* Build final path */
   fill_pathname_join(backup_path, backup_dir,
         backup_filename, len);

   return true;
}

/* Returns detected type of specified core backup file */
enum core_backup_type core_backup_get_backup_type(const char *backup_path)
{
   const char *backup_ext            = NULL;
   struct string_list *metadata_list = NULL;
   char core_ext[255];

   core_ext[0] = '\0';

   if (string_is_empty(backup_path) || !path_is_valid(backup_path))
      goto error;

   /* Get backup file extension */
   backup_ext = path_get_extension(backup_path);

   if (string_is_empty(backup_ext))
      goto error;

   /* Get platform-specific dynamic library extension */
   if (!frontend_driver_get_core_extension(core_ext, sizeof(core_ext)))
      goto error;

   /* Check if this is an archived backup */
   if (string_is_equal_noncase(backup_ext,
         FILE_PATH_CORE_BACKUP_EXTENSION_NO_DOT))
   {
      const char *backup_filename = NULL;
      const char *src_ext         = NULL;

      /* Split the backup filename into its various
       * metadata components */
      backup_filename = path_basename(backup_path);

      if (string_is_empty(backup_filename))
         goto error;

      metadata_list = string_split(backup_filename, ".");

      if (!metadata_list || (metadata_list->size != 6))
         goto error;

      /* Get extension of source core file */
      src_ext = metadata_list->elems[1].data;

      if (string_is_empty(src_ext))
         goto error;

      /* Check whether extension is valid */
      if (!string_is_equal_noncase(src_ext, core_ext))
         goto error;

      string_list_free(metadata_list);
      metadata_list = NULL;
   
      return CORE_BACKUP_TYPE_ARCHIVE;
   }

   /* Check if this is a plain dynamic library file */
   if (string_is_equal_noncase(backup_ext, core_ext))
      return CORE_BACKUP_TYPE_LIB;

error:
   if (metadata_list)
   {
      string_list_free(metadata_list);
      metadata_list = NULL;
   }

   return CORE_BACKUP_TYPE_INVALID;
}

/* Fetches crc value of specified core backup file.
 * Returns true if successful */
bool core_backup_get_backup_crc(char *backup_path, uint32_t *crc)
{
   struct string_list *metadata_list = NULL;
   enum core_backup_type backup_type;

   if (string_is_empty(backup_path) || !crc)
      goto error;

   /* Get backup type */
   backup_type = core_backup_get_backup_type(backup_path);

   switch (backup_type)
   {
      case CORE_BACKUP_TYPE_ARCHIVE:
         {
            const char *backup_filename = NULL;
            const char *crc_str         = NULL;

            /* Split the backup filename into its various
             * metadata components */
            backup_filename = path_basename(backup_path);

            if (string_is_empty(backup_filename))
               goto error;

            metadata_list = string_split(backup_filename, ".");

            if (!metadata_list || (metadata_list->size != 6))
               goto error;

            /* Get crc string */
            crc_str = metadata_list->elems[3].data;

            if (string_is_empty(crc_str))
               goto error;

            /* Convert to an integer */
            *crc = (uint32_t)string_hex_to_unsigned(crc_str);

            if (*crc == 0)
               goto error;

            string_list_free(metadata_list);
            metadata_list = NULL;

         }
         return true;
      case CORE_BACKUP_TYPE_LIB:
         {
            intfstream_t *backup_file = NULL;

            /* This is a plain dynamic library file,
             * have to read file data to determine crc */

            /* Open backup file */
            backup_file = intfstream_open_file(
                  backup_path, RETRO_VFS_FILE_ACCESS_READ,
                  RETRO_VFS_FILE_ACCESS_HINT_NONE);

            if (backup_file)
            {
               bool success;

               /* Get crc value */
               success = intfstream_get_crc(backup_file, crc);

               /* Close backup file */
               intfstream_close(backup_file);
               free(backup_file);
               backup_file = NULL;

               return success;
            }
         }
         break;
      default:
         /* Backup is invalid */
         break;
   }

error:
   if (metadata_list)
   {
      string_list_free(metadata_list);
      metadata_list = NULL;
   }

   return false;
}

/* Fetches core path associated with specified core
 * backup file. Returns detected type of backup
 * file - CORE_BACKUP_TYPE_INVALID indicates that
 * backup file cannot be restored/installed, or
 * arguments are otherwise invalid */
enum core_backup_type core_backup_get_core_path(
      const char *backup_path, const char *dir_libretro,
      char *core_path, size_t len)
{
   const char *backup_filename       = NULL;
   char *core_filename               = NULL;
   enum core_backup_type backup_type = CORE_BACKUP_TYPE_INVALID;

   if (string_is_empty(backup_path) || string_is_empty(dir_libretro))
      return backup_type;

   backup_filename = path_basename(backup_path);

   if (string_is_empty(backup_filename))
      return backup_type;

   /* Check backup type */
   switch (core_backup_get_backup_type(backup_path))
   {
      case CORE_BACKUP_TYPE_ARCHIVE:
         {
            char *period  = NULL;

            /* This is an archived backup with timestamp/crc
             * metadata in the filename */
            core_filename = strdup(backup_filename);

            /* Find the location of the second period */
            period = strchr(core_filename, '.');
            if (!period || (*(++period) == '\0'))
               break;

            period = strchr(period, '.');
            if (!period)
               break;

            /* Trim everything after (and including) the
             * second period */
            *period = '\0';

            if (string_is_empty(core_filename))
               break;

            /* All good - build core path */
            fill_pathname_join(core_path, dir_libretro,
                  core_filename, len);

            backup_type = CORE_BACKUP_TYPE_ARCHIVE;
         }
         break;
      case CORE_BACKUP_TYPE_LIB:
         /* This is a plain dynamic library file */
         fill_pathname_join(core_path, dir_libretro,
               backup_filename, len);
         backup_type = CORE_BACKUP_TYPE_LIB;
         break;
      default:
         /* Backup is invalid */
         break;
   }

   if (core_filename)
   {
      free(core_filename);
      core_filename = NULL;
   }

   return backup_type;
}

/*************************/
/* Backup List Functions */
/*************************/

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

/* Parses backup file name and adds to backup list, if valid */
static bool core_backup_add_entry(core_backup_list_t *backup_list,
      const char *core_filename, const char *backup_path)
{
   char *backup_filename           = NULL;
   core_backup_list_entry_t *entry = NULL;
   unsigned backup_mode            = 0;

   if (!backup_list ||
       string_is_empty(core_filename) ||
       string_is_empty(backup_path) ||
       (backup_list->size >= backup_list->capacity))
      goto error;

   backup_filename = strdup(path_basename(backup_path));

   if (string_is_empty(backup_filename))
      goto error;

   /* Ensure base backup filename matches core */
   if (!string_starts_with(backup_filename, core_filename))
      goto error;

   /* Remove backup file extension */
   path_remove_extension(backup_filename);

   /* Parse backup filename metadata
    * - <core_filename>.<timestamp>.<crc>.<backup_mode>
    * - timestamp: YYYYMMDDTHHMMSS */
   entry = &backup_list->entries[backup_list->size];

   if (sscanf(backup_filename + strlen(core_filename),
       ".%04u%02u%02uT%02u%02u%02u.%08x.%u",
       &entry->date.year, &entry->date.month, &entry->date.day,
       &entry->date.hour, &entry->date.minute, &entry->date.second,
       &entry->crc, &backup_mode) != 8)
      goto error;

   entry->backup_mode = (enum core_backup_mode)backup_mode;

   /* Cache backup path */
   entry->backup_path = strdup(backup_path);
   backup_list->size++;

   free(backup_filename);

   return true;

error:
   if (backup_filename)
      free(backup_filename);

   return false;
}

/* Creates a new core backup list containing entries
 * for all existing backup files.
 * Returns a handle to a new core_backup_list_t object
 * on success, otherwise returns NULL. */
core_backup_list_t *core_backup_list_init(
      const char *core_path, const char *dir_core_assets)
{
   size_t i;
   const char *core_filename         = NULL;
   struct string_list *dir_list      = NULL;
   core_backup_list_t *backup_list   = NULL;
   core_backup_list_entry_t *entries = NULL;
   char core_dir[PATH_MAX_LENGTH];
   char backup_dir[PATH_MAX_LENGTH];

   core_dir[0]   = '\0'; 
   backup_dir[0] = '\0';

   /* Get core filename and parent directory */
   if (string_is_empty(core_path))
      goto error;

   core_filename = path_basename(core_path);

   if (string_is_empty(core_filename))
      goto error;

   fill_pathname_parent_dir(core_dir, core_path, sizeof(core_dir));

   if (string_is_empty(core_dir))
      goto error;

   /* Get backup directory */
   if (!core_backup_get_backup_dir(core_dir, dir_core_assets, core_filename,
         backup_dir, sizeof(backup_dir)))
      goto error;

   /* Get backup file list */
   dir_list = dir_list_new(
         backup_dir,
         FILE_PATH_CORE_BACKUP_EXTENSION,
         false, /* include_dirs */
         false, /* include_hidden */
         false, /* include_compressed */
         false  /* recursive */
   );

   /* Sanity check */
   if (!dir_list)
      goto error;

   if (dir_list->size < 1)
      goto error;

   /* Ensure list is sorted in alphabetical order
    * > This corresponds to 'timestamp' order */
   dir_list_sort(dir_list, true);

   /* Create core backup list */
   backup_list = (core_backup_list_t*)malloc(sizeof(*backup_list));

   if (!backup_list)
      goto error;

   backup_list->entries  = NULL;
   backup_list->capacity = 0;
   backup_list->size     = 0;

   /* Create entries array
    * (Note: Set this to the full size of the directory
    * list - this may be larger than we need, but saves
    * many inefficiencies later)   */
   entries               = (core_backup_list_entry_t*)
      calloc(dir_list->size, sizeof(*entries));

   if (!entries)
      goto error;

   backup_list->entries  = entries;
   backup_list->capacity = dir_list->size;

   /* Loop over backup files and parse file names */
   for (i = 0; i < dir_list->size; i++)
   {
      const char *backup_path = dir_list->elems[i].data;
      core_backup_add_entry(backup_list, core_filename, backup_path);
   }

   if (backup_list->size == 0)
      goto error;

   string_list_free(dir_list);

   return backup_list;

error:
   if (dir_list)
      string_list_free(dir_list);

   if (backup_list)
      core_backup_list_free(backup_list);

   return NULL;
}

/* Frees specified core backup list */
void core_backup_list_free(core_backup_list_t *backup_list)
{
   size_t i;

   if (!backup_list)
      return;

   if (backup_list->entries)
   {
      for (i = 0; i < backup_list->size; i++)
      {
         core_backup_list_entry_t *entry = &backup_list->entries[i];

         if (!entry)
            continue;

         if (entry->backup_path)
         {
            free(entry->backup_path);
            entry->backup_path = NULL;
         }
      }

      free(backup_list->entries);
      backup_list->entries = NULL;
   }

   free(backup_list);
}

/***********/
/* Getters */
/***********/

/* Returns number of entries in core backup list */
size_t core_backup_list_size(core_backup_list_t *backup_list)
{
   if (!backup_list)
      return 0;

   return backup_list->size;
}

/* Returns number of entries of specified 'backup mode'
 * (manual or automatic) in core backup list */
size_t core_backup_list_get_num_backups(
      core_backup_list_t *backup_list,
      enum core_backup_mode backup_mode)
{
   size_t i;
   size_t num_backups = 0;

   if (!backup_list || !backup_list->entries)
      return 0;

   for (i = 0; i < backup_list->size; i++)
   {
      core_backup_list_entry_t *current_entry = &backup_list->entries[i];

      if (current_entry &&
          (current_entry->backup_mode == backup_mode))
         num_backups++;
   }

   return num_backups;
}

/* Fetches core backup list entry corresponding
 * to the specified entry index.
 * Returns false if index is invalid. */
bool core_backup_list_get_index(
      core_backup_list_t *backup_list,
      size_t idx,
      const core_backup_list_entry_t **entry)
{
   if (!backup_list || !backup_list->entries || !entry)
      return false;

   if (idx >= backup_list->size)
      return false;

   *entry = &backup_list->entries[idx];

   if (*entry)
      return true;

   return false;
}

/* Fetches core backup list entry corresponding
 * to the specified core crc checksum value.
 * Note that 'manual' and 'auto' backups are
 * considered independent - we only compare
 * crc values for the specified backup_mode.
 * Returns false if entry is not found. */
bool core_backup_list_get_crc(
      core_backup_list_t *backup_list,
      uint32_t crc, enum core_backup_mode backup_mode,
      const core_backup_list_entry_t **entry)
{
   size_t i;

   if (!backup_list || !backup_list->entries || !entry)
      return false;

   for (i = 0; i < backup_list->size; i++)
   {
      core_backup_list_entry_t *current_entry = &backup_list->entries[i];

      if (current_entry &&
          (current_entry->crc == crc) &&
          (current_entry->backup_mode == backup_mode))
      {
         *entry = current_entry;
         return true;
      }
   }

   return false;
}

/* Fetches a string representation of a backup
 * list entry timestamp.
 * Returns false in the event of an error */
bool core_backup_list_get_entry_timestamp_str(
      const core_backup_list_entry_t *entry,
      enum core_backup_date_separator_type date_separator,
      char *timestamp, size_t len)
{
   int n;
   const char *format_str = "";

   if (!entry || (len < 20))
      return false;

   /* Get time format string */
   switch (date_separator)
   {
      case CORE_BACKUP_DATE_SEPARATOR_SLASH:
         format_str = "%04u/%02u/%02u %02u:%02u:%02u";
         break;
      case CORE_BACKUP_DATE_SEPARATOR_PERIOD:
         format_str = "%04u.%02u.%02u %02u:%02u:%02u";
         break;
      default:
         format_str = "%04u-%02u-%02u %02u:%02u:%02u";
         break;
   }

   n = snprintf(timestamp, len,
         format_str,
         entry->date.year,
         entry->date.month,
         entry->date.day,
         entry->date.hour,
         entry->date.minute,
         entry->date.second);

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

   return true;
}

/* Fetches a string representation of a backup
 * list entry crc value.
 * Returns false in the event of an error */
bool core_backup_list_get_entry_crc_str(
      const core_backup_list_entry_t *entry,
      char *crc, size_t len)
{
   int n;

   if (!entry || (len < 9))
      return false;

   n = snprintf(crc, len, "%08x", entry->crc);

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

   return true;
}