#include <ctype.h>
#include <mist.h>
#include <stdio.h>
#include <string.h>

#include <retro_timers.h>
#include <compat/strl.h>
#include <string/stdstring.h>

#include "../input/input_driver.h"
#include "../menu/menu_driver.h"
#include "../menu/menu_entries.h"
#include "../retroarch.h"
#include "../runloop.h"
#include "paths.h"
#include "verbosity.h"

#include "steam.h"

static bool mist_initialized                = false;
static bool mist_showing_osk                = false;
static steam_core_dlc_list_t *mist_dlc_list = NULL;
static enum presence last_presence          = PRESENCE_NONE;

void steam_init(void)
{
   MistResult result = mist_subprocess_init();

   if (MIST_IS_SUCCESS(result))
      mist_initialized = true;
   else
      RARCH_ERR("[Steam]: Failed to initialize mist subprocess (%d-%d)\n", MIST_UNPACK_RESULT(result));
}

void steam_poll(void)
{
   MistCallbackMsg callback;
   steam_core_dlc_list_t *core_dlc_list;
   bool has_callback                     = false;
   settings_t* settings                  = config_get_ptr();
   static bool has_poll_errored          = false;
   static bool has_rich_presence_enabled = false;
   MistResult result                     = mist_poll();
   if (MIST_IS_ERROR(result))
   {
      if (has_poll_errored)
         return;

      RARCH_ERR("[Steam]: Error polling (%d-%d)\n", MIST_UNPACK_RESULT(result));

      has_poll_errored = true;
   }

   result = mist_next_callback(&has_callback, &callback);
   if (MIST_IS_ERROR(result))
      return;

   while (has_callback && MIST_IS_SUCCESS(result))
   {
      switch (callback.callback)
      {
         /* Reload core info and Steam Core DLC mappings */
         case MistCallback_DlcInstalled:
            command_event(CMD_EVENT_CORE_INFO_INIT, NULL);
            steam_get_core_dlcs(&core_dlc_list, false);
            break;
         /* The Steam OSK is dismissed */
         case MistCallback_FloatingGamepadTextInputDismissed:
            /* If we do not poll for input the callback might race condition and
               will dismiss the input even when enter is pressed */
            retro_sleep(50);
            runloop_iterate();
            menu_input_dialog_end();
            break;
      }

      result = mist_next_callback(&has_callback, &callback);
   }

   /* Ensure rich presence state is correct */
   if (settings->bools.steam_rich_presence_enable != has_rich_presence_enabled)
   {
      steam_update_presence(last_presence, true);
      has_rich_presence_enabled = settings->bools.steam_rich_presence_enable;
   }
}

steam_core_dlc_list_t *steam_core_dlc_list_new(size_t count)
{
   steam_core_dlc_list_t *core_dlc_list = (steam_core_dlc_list_t*)
      malloc(sizeof(*core_dlc_list));

   core_dlc_list->list  = (steam_core_dlc_t*)
      malloc(count * sizeof(*core_dlc_list->list));
   core_dlc_list->count = 0; /* This is incremented inside the setup function */

   return core_dlc_list;
}

void steam_core_dlc_list_free(steam_core_dlc_list_t *list)
{
   size_t i;
   if (!list)
      return;

   for (i = 0; list->count > i; i++)
   {
      if (list->list[i].name)
         free(list->list[i].name);
      if (list->list[i].name_lower)
         free(list->list[i].name_lower);
   }

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

steam_core_dlc_t *steam_core_dlc_list_get(steam_core_dlc_list_t *list, size_t i)
{
   if (!list || (i >= list->count))
      return NULL;

   return &list->list[i];
}

/* Sort the dlc cores alphabetically based on their name */
static int dlc_core_qsort_cmp(const void *a_, const void *b_)
{
   const steam_core_dlc_t *a = (const steam_core_dlc_t*)a_;
   const steam_core_dlc_t *b = (const steam_core_dlc_t*)b_;

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

/* Find core info for dlcs
 * TODO: This currently only uses core info for cores that are installed */
core_info_t* steam_find_core_info_for_dlc(const char* name)
{
   int i;
   core_info_list_t *core_info_list = NULL;
   core_info_get_list(&core_info_list);

   if (!core_info_list)
      return NULL;

   for (i = 0; core_info_list->count > i; i++)
   {
      char core_info_name[256];
      core_info_t *core_info   = core_info_get(core_info_list, i);
      /* Find the opening parenthesis for the core name */
      char *start              = strchr(core_info->display_name, '(');
      if (!start)
         continue;

      /* Skip the first parenthesis and copy it to the stack */
      strlcpy(core_info_name, start + 1, sizeof(core_info_name));

      /* Null terminate at the closing parenthesis. */
      char *end = strchr((const char*)&core_info_name, ')');
      if (!end)
            continue;

      *end = '\0';

      /* Make it lowercase */
      string_to_lower((char*)&core_info_name);

      /* Check if it matches */
      if (strcasecmp(core_info_name, name) == 0)
         return core_info;
   }

   return NULL;
}

/* Generate a list with core dlcs
 * Needs to be called after initializion because it uses core info */
MistResult steam_generate_core_dlcs_list(steam_core_dlc_list_t **list)
{
   int count, i;
   steam_core_dlc_list_t *dlc_list = NULL;
   char dlc_name[PATH_MAX_LENGTH]  = { 0 };
   bool available                  = false;
   MistResult result               = mist_steam_apps_get_dlc_count(&count);
   if (MIST_IS_ERROR(result))
      goto error;

   dlc_list = steam_core_dlc_list_new(count);
   for (i = 0; count > i; i++)
   {
      steam_core_dlc_t core_dlc;

      result = mist_steam_apps_get_dlc_data_by_index(i, &core_dlc.app_id, &available, (char*)&dlc_name, PATH_MAX_LENGTH);
      if (MIST_IS_ERROR(result))
         goto error;

      /* Strip away the "RetroArch - " prefix if present */
      if (strncmp(dlc_name, "RetroArch - ", sizeof("RetroArch - ") - 1) == 0)
         core_dlc.name = strdup(dlc_name + sizeof("RetroArch - ") - 1);
      else
         core_dlc.name = strdup(dlc_name);

      /* Make a lower case version */
      core_dlc.name_lower = strdup(core_dlc.name);
      string_to_lower(core_dlc.name_lower);

      core_dlc.core_info = steam_find_core_info_for_dlc(core_dlc.name_lower);

      dlc_list->list[i] = core_dlc;
      dlc_list->count++;
   }

   /* Sort the list */
   qsort(dlc_list->list, dlc_list->count,
         sizeof(steam_core_dlc_t), dlc_core_qsort_cmp);

   *list = dlc_list;
   return MistResult_Success;

error:
   if (dlc_list)
      steam_core_dlc_list_free(dlc_list);

   return result;
}

MistResult steam_get_core_dlcs(steam_core_dlc_list_t **list, bool cached)
{
   MistResult result;
   steam_core_dlc_list_t *new_list = NULL;

   if (cached && mist_dlc_list)
   {
      *list = mist_dlc_list;
      return MistResult_Success;
   }

   result = steam_generate_core_dlcs_list(&new_list);
   if (MIST_IS_ERROR(result))
      return result;

   if (mist_dlc_list)
      steam_core_dlc_list_free(mist_dlc_list);

   mist_dlc_list = new_list;
   *list         = new_list;

   return MistResult_Success;
}

steam_core_dlc_t* steam_get_core_dlc_by_name(
      steam_core_dlc_list_t *list, const char *name)
{
   int i;
   steam_core_dlc_t *core_info;

   for (i = 0; list->count > i; i++)
   {
      core_info = steam_core_dlc_list_get(list, i);
      if (strcasecmp(core_info->name, name) == 0)
         return core_info;
   }

   return NULL;
}

void steam_install_core_dlc(steam_core_dlc_t *core_dlc)
{
   char msg[PATH_MAX_LENGTH] = { 0 };
   bool downloading          = false;
   bool installed            = false;
   uint64_t bytes_downloaded = 0;
   uint64_t bytes_total      = 0;
   /* Check if the core is already being downloaded */
   MistResult result         = mist_steam_apps_get_dlc_download_progress(core_dlc->app_id, &downloading, &bytes_downloaded, &bytes_total);
   if (MIST_IS_ERROR(result))
      goto error;

   /* Check if the core is already installed */
   result = mist_steam_apps_is_dlc_installed(core_dlc->app_id, &installed);
   if (MIST_IS_ERROR(result))
      goto error;

   if (downloading || installed)
   {
      runloop_msg_queue_push(msg_hash_to_str(MSG_CORE_STEAM_CURRENTLY_DOWNLOADING), 1, 180, true, NULL,
         MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR);
      return;
   }

   result = mist_steam_apps_install_dlc(core_dlc->app_id);
   if (MIST_IS_ERROR(result))
      goto error;

   task_push_steam_core_dlc_install(core_dlc->app_id, core_dlc->name);

   return;
error:
   snprintf(msg, sizeof(msg), "%s: (%d-%d)",
         msg_hash_to_str(MSG_ERROR),
         MIST_UNPACK_RESULT(result));

   runloop_msg_queue_push(msg, 1, 180, true, NULL,
         MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR);

   RARCH_ERR("[Steam]: Error installing DLC %d (%d-%d)\n", core_dlc->app_id, MIST_UNPACK_RESULT(result));
}

void steam_uninstall_core_dlc(steam_core_dlc_t *core_dlc)
{
   char msg[PATH_MAX_LENGTH] = { 0 };

   MistResult result = mist_steam_apps_uninstall_dlc(core_dlc->app_id);

   if (MIST_IS_ERROR(result))
      goto error;

   runloop_msg_queue_push(msg_hash_to_str(MSG_CORE_STEAM_UNINSTALLED), 1, 180, true, NULL,
      MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_INFO);
   return;

error:
   snprintf(msg, sizeof(msg), "%s: (%d-%d)",
         msg_hash_to_str(MSG_ERROR),
         MIST_UNPACK_RESULT(result));

   runloop_msg_queue_push(msg, 1, 180, true, NULL,
         MESSAGE_QUEUE_ICON_DEFAULT, MESSAGE_QUEUE_CATEGORY_ERROR);

   RARCH_ERR("[Steam]: Error uninstalling DLC %d (%d-%d)\n", core_dlc->app_id, MIST_UNPACK_RESULT(result));
}

bool steam_open_osk(void)
{
   bool                     shown = false;
   bool                   on_deck = false;
   video_driver_state_t *video_st = video_state_get_ptr();

   /* Only open the Steam OSK if running on a Steam Deck,
      as currently the Big Picture OSK seems to be semi-broken */
   mist_steam_utils_is_steam_running_on_steam_deck(&on_deck);
   if (!on_deck)
      return false;

   mist_steam_utils_show_floating_gamepad_text_input(
      MistFloatingGamepadTextInputMode_SingleLine,
      0,
      0,
      video_st->width,
      video_st->height / 2,
      &shown
   );

   mist_showing_osk = shown;

   return shown;
}

bool steam_has_osk_open(void)
{
   return mist_showing_osk;
}

void steam_update_presence(enum presence presence, bool force)
{
   settings_t* settings = config_get_ptr();

   if (!mist_initialized)
      return;

   /* Avoid spamming steam with presence updates */
   if (presence == last_presence && !force)
      return;
   last_presence = presence;

   /* Ensure rich presence is enabled */
   if (!settings->bools.steam_rich_presence_enable)
   {
      mist_steam_friends_clear_rich_presence();
      return;
   }

   switch (presence)
   {
      case PRESENCE_MENU:
         mist_steam_friends_set_rich_presence("steam_display", "#Status_InMenu");
         break;
      case PRESENCE_GAME_PAUSED:
         mist_steam_friends_set_rich_presence("steam_display", "#Status_Paused");
         break;
      case PRESENCE_GAME:
         {
            size_t _len;
            char content[PATH_MAX_LENGTH];
            const char *label                  = NULL;
            const struct playlist_entry *entry = NULL;
            core_info_t *core_info             = NULL;
            playlist_t *current_playlist       = playlist_get_cached();

            core_info_get_current_core(&core_info);

            if (current_playlist)
            {
               playlist_get_index_by_path(
                     current_playlist,
                     path_get(RARCH_PATH_CONTENT),
                     &entry);

               if (entry && !string_is_empty(entry->label))
                  label = entry->label;
            }

            if (!label)
               label = path_basename(path_get(RARCH_PATH_BASENAME));

            switch(settings->uints.steam_rich_presence_format)
            {
               case STEAM_RICH_PRESENCE_FORMAT_CONTENT:
                  strlcpy(content, label, sizeof(content));
                  break;
               case STEAM_RICH_PRESENCE_FORMAT_CORE:
                  if (core_info)
                     strlcpy(content, core_info->core_name, sizeof(content));
                  else
                  {
                     content[0] = 'N';
                     content[1] = '/';
                     content[2] = 'A';
                     content[3] = '\0';
                  }
                  break;
               case STEAM_RICH_PRESENCE_FORMAT_SYSTEM:
                  if (core_info)
                     strlcpy(content, core_info->systemname, sizeof(content));
                  else
                  {
                     content[0] = 'N';
                     content[1] = '/';
                     content[2] = 'A';
                     content[3] = '\0';
                  }
                  break;
               case STEAM_RICH_PRESENCE_FORMAT_CONTENT_SYSTEM:
                  _len               = strlcpy(content, label, sizeof(content));
                  content[_len  ]    = ' ';
                  content[_len+1]    = '(';
                  content[_len+2]    = '\0';
                  if (core_info)
                  {
                     _len            = strlcat(content, core_info->systemname,
                           sizeof(content));
                     content[_len  ] = ')';
                     content[_len+1] = '\0';
                  }
                  else
                  {
                     content[_len+2] = 'N';
                     content[_len+3] = '/';
                     content[_len+4] = 'A';
                     content[_len+5] = ')';
                     content[_len+6] = '\0';
                  }
                  break;
               case STEAM_RICH_PRESENCE_FORMAT_CONTENT_CORE:
                  _len               = strlcpy(content, label, sizeof(content));
                  content[_len  ]    = ' ';
                  content[_len+1]    = '(';
                  content[_len+2]    = '\0';
                  if (core_info)
                  {
                     _len            = strlcat(content, core_info->core_name,
                           sizeof(content));
                     content[_len  ] = ')';
                     content[_len+1] = '\0';
                  }
                  else
                  {
                     content[_len+2] = 'N';
                     content[_len+3] = '/';
                     content[_len+4] = 'A';
                     content[_len+5] = ')';
                     content[_len+6] = '\0';
                  }
                  break;
               case STEAM_RICH_PRESENCE_FORMAT_CONTENT_SYSTEM_CORE:
                  _len               = strlcpy(content, label, sizeof(content));
                  content[_len  ]    = ' ';
                  content[_len+1]    = '(';
                  content[_len+2]    = '\0';
                  if (core_info)
                  {
                     _len            = strlcat(content, core_info->systemname,
                           sizeof(content));
                     content[_len  ] = ' ';
                     content[_len+1] = '-';
                     content[_len+2] = ' ';
                     _len            = strlcat(content, core_info->core_name,
                           sizeof(content));
                     content[_len  ] = ')';
                     content[_len+1] = '\0';
                  }
                  else
                  {
                     content[_len+2]  = 'N';
                     content[_len+3]  = '/';
                     content[_len+4]  = 'A';
                     content[_len+5]  = ' ';
                     content[_len+6]  = '-';
                     content[_len+7]  = ' ';
                     content[_len+8]  = 'N';
                     content[_len+9]  = '/';
                     content[_len+10] = 'A';
                     content[_len+11] = ')';
                     content[_len+12] = '\0';
                  }
                  break;
               case STEAM_RICH_PRESENCE_FORMAT_NONE:
               default:
                  content[0] = '\0';
                  break;
            }


            mist_steam_friends_set_rich_presence("content", content);
            mist_steam_friends_set_rich_presence("steam_display",
                  settings->uints.steam_rich_presence_format != STEAM_RICH_PRESENCE_FORMAT_NONE
                  ? "#Status_RunningContent" : "#Status_Running" );
         }
         break;
      default:
         break;
   }
}

void steam_deinit(void)
{
   MistResult result = mist_subprocess_deinit();

   /* Free the cached dlc list */
   if (mist_dlc_list)
      steam_core_dlc_list_free(mist_dlc_list);

   if (MIST_IS_SUCCESS(result))
      mist_initialized = false;
   else
      RARCH_ERR("[Steam]: Failed to deinitialize mist subprocess (%d-%d)\n", MIST_UNPACK_RESULT(result));
}