// Copyright 2018 Dolphin Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "UICommon/DiscordPresence.h"

#include "Core/Config/NetplaySettings.h"
#include "Core/Config/UISettings.h"
#include "Core/ConfigManager.h"

#ifdef USE_DISCORD_PRESENCE

#include <algorithm>
#include <ctime>
#include <set>
#include <string>

#include <discord_rpc.h>
#include <fmt/format.h>

#include "Common/Hash.h"
#include "Common/StringUtil.h"

#endif

namespace Discord
{
#ifdef USE_DISCORD_PRESENCE
namespace
{
Handler* event_handler = nullptr;
const char* username = "";

void HandleDiscordReady(const DiscordUser* user)
{
  username = user->username;
}

void HandleDiscordJoinRequest(const DiscordUser* user)
{
  if (event_handler == nullptr)
    return;

  const std::string discord_tag = fmt::format("{}#{}", user->username, user->discriminator);
  event_handler->DiscordJoinRequest(user->userId, discord_tag, user->avatar);
}

void HandleDiscordJoin(const char* join_secret)
{
  if (event_handler == nullptr)
    return;

  if (Config::Get(Config::NETPLAY_NICKNAME) == Config::NETPLAY_NICKNAME.GetDefaultValue())
    Config::SetCurrent(Config::NETPLAY_NICKNAME, username);

  std::string secret(join_secret);

  std::string type = secret.substr(0, secret.find('\n'));
  size_t offset = type.length() + 1;

  switch (static_cast<SecretType>(std::stol(type)))
  {
  default:
  case SecretType::Empty:
    return;

  case SecretType::IPAddress:
  {
    // SetBaseOrCurrent will save the ip address, which isn't what's wanted in this situation
    Config::SetCurrent(Config::NETPLAY_TRAVERSAL_CHOICE, "direct");

    std::string host = secret.substr(offset, secret.find_last_of(':') - offset);
    Config::SetCurrent(Config::NETPLAY_ADDRESS, host);

    offset += host.length();
    if (secret[offset] == ':')
      Config::SetCurrent(Config::NETPLAY_CONNECT_PORT, std::stoul(secret.substr(offset + 1)));
  }
  break;

  case SecretType::RoomID:
  {
    Config::SetCurrent(Config::NETPLAY_TRAVERSAL_CHOICE, "traversal");

    Config::SetCurrent(Config::NETPLAY_HOST_CODE, secret.substr(offset));
  }
  break;
  }

  event_handler->DiscordJoin();
}

std::string ArtworkForGameId(const std::string& gameid)
{
  static const std::set<std::string> REGISTERED_GAMES{
      "GAF",  // GAFE01: Animal Crossing
      "RUU",  // RUUE01: Animal Crossing: City Folk
      "SF8",  // SF8E01: Donkey Kong Country Returns
      "RDB",  // RDBE70: Dragon Ball Z: Budokai Tenkaichi 2
      "RDS",  // RDSE70: Dragon Ball Z: Budokai Tenkaichi 3
      "GFZ",  // GFZE01: F-Zero GX
      "GFE",  // GFEE01: Fire Emblem: Path of Radiance
      "RFE",  // RFEE01: Fire Emblem: Radiant Dawn
      "S5S",  // S5SJHF: Inazuma Eleven GO: Strikers 2013
      "GKY",  // GKYE01: Kirby Air Ride
      "SUK",  // SUKE01: Kirby's Return to Dream Land
      "GLM",  // GLME01: Luigi's Mansion
      "GFT",  // GFTE01: Mario Golf: Toadstool Tour
      "RMC",  // RMCE01: Mario Kart Wii
      "GM4",  // GM4E01: Mario Kart: Double Dash!!
      "GMP",  // GMPE01: Mario Party 4
      "GP5",  // GP5E01: Mario Party 5
      "GP6",  // GP6E01: Mario Party 6
      "GP7",  // GP7E01: Mario Party 7
      "RM8",  // RM8E01: Mario Party 8
      "SSQ",  // SSQE01: Mario Party 9
      "GOM",  // GOME01: Mario Power Tennis
      "GYQ",  // GYQE01: Mario Superstar Baseball
      "GGS",  // GGSE01: Metal Gear Solid: The Twin Snakes
      "GM8",  // GM8E01: Metroid Prime
      "G2M",  // G2ME01: Metroid Prime 2: Echoes
      "RM3",  // RM3E01: Metroid Prime 3: Corruption
      "R3M",  // R3ME01: Metroid Prime: Trilogy
      "SMN",  // SMNE01: New Super Mario Bros. Wii
      "G8M",  // G8ME01: Paper Mario: The Thousand-Year Door
      "GPI",  // GPIE01: Pikmin (GC)
      "R9I",  // R9IE01: Pikmin (Wii)
      "GPV",  // GPVE01: Pikmin 2 (GC)
      "R92",  // R92E01: Pikmin 2 (Wii)
      "GC6",  // GC6E01: Pokemon Colosseum
      "GXX",  // GXXE01: Pokemon XD: Gale of Darkness
      "GBI",  // GBIE08: Resident Evil
      "GHA",  // GHAE08: Resident Evil 2
      "GLE",  // GLEE08: Resident Evil 3: Nemesis
      "G4B",  // G4BE08: Resident Evil 4
      "GSN",  // GSNE8P: Sonic Adventure 2: Battle
      "GXS",  // GXSE8P: Sonic Adventure DX: Director's Cut
      "SNC",  // SNCE8P: Sonic Colors
      "G9S",  // G9SE8P: Sonic Heroes
      "GRS",  // GRSEAF: SoulCalibur II
      "RSL",  // RSLEAF: SoulCalibur Legends
      "GK2",  // GK2E52: Spider-Man 2
      "GQP",  // GQPE78: SpongeBob SquarePants: Battle for Bikini Bottom
      "SVM",  // SVME01: Super Mario All-Stars: 25th Anniversary Edition
      "RMG",  // RMGE01: Super Mario Galaxy
      "SB4",  // SB4E01: Super Mario Galaxy 2
      "G4Q",  // G4QE01: Super Mario Strikers
      "GMS",  // GMSE01: Super Mario Sunshine
      "GMB",  // GMBE8P: Super Monkey Ball
      "GM2",  // GM2E8P: Super Monkey Ball 2
      "R8P",  // R8PE01: Super Paper Mario
      "RSB",  // RSBE01: Super Smash Bros. Brawl
      "GAL",  // GALE01: Super Smash Bros. Melee
      "PZL",  // PZLE01: The Legend of Zelda: Collector's Edition
      "G4S",  // G4SE01: The Legend of Zelda: Four Swords Adventures
      "D43",  // D43E01: The Legend of Zelda: Ocarina of Time / Master Quest
      "SOU",  // SOUE01: The Legend of Zelda: Skyward Sword
      "GZL",  // GZLE01: The Legend of Zelda: The Wind Waker
      "GZ2",  // GZ2E01: The Legend of Zelda: Twilight Princess (GC)
      "RZD",  // RZDE01: The Legend of Zelda: Twilight Princess (Wii)
      "GHQ",  // GHQE7D: The Simpsons: Hit & Run
      "RSP",  // RSPE01: Wii Sports
      "RZT",  // RZTE01: Wii Sports Resort
      "SX4",  // SX4E01: Xenoblade Chronicles
  };

  std::string region_neutral_gameid = gameid.substr(0, 3);
  if (REGISTERED_GAMES.count(region_neutral_gameid) != 0)
  {
    // Discord asset keys can only be lowercase.
    Common::ToLower(&region_neutral_gameid);
    return "game_" + region_neutral_gameid;
  }
  return "";
}

}  // namespace
#endif

Discord::Handler::~Handler() = default;

void Init()
{
#ifdef USE_DISCORD_PRESENCE
  if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
    return;

  DiscordEventHandlers handlers = {};

  handlers.ready = HandleDiscordReady;
  handlers.joinRequest = HandleDiscordJoinRequest;
  handlers.joinGame = HandleDiscordJoin;
  Discord_Initialize(DEFAULT_CLIENT_ID.c_str(), &handlers, 1, nullptr);
  UpdateDiscordPresence();
#endif
}

void UpdateClientID(const std::string& new_client)
{
#ifdef USE_DISCORD_PRESENCE
  if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
    return;

  s_using_custom_client = new_client.empty() || new_client.compare(DEFAULT_CLIENT_ID) != 0;

  Shutdown();
  if (s_using_custom_client)
    Discord_Initialize(new_client.c_str(), nullptr, 0, nullptr);
  else  // if initialising dolphin's client ID, make sure to restore event handlers
    Init();
#endif
}

void CallPendingCallbacks()
{
#ifdef USE_DISCORD_PRESENCE
  if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
    return;

  Discord_RunCallbacks();

#endif
}

void InitNetPlayFunctionality(Handler& handler)
{
#ifdef USE_DISCORD_PRESENCE
  event_handler = &handler;
#endif
}

bool UpdateDiscordPresenceRaw(const std::string& details, const std::string& state,
                              const std::string& large_image_key,
                              const std::string& large_image_text,
                              const std::string& small_image_key,
                              const std::string& small_image_text, const int64_t start_timestamp,
                              const int64_t end_timestamp, const int party_size,
                              const int party_max)
{
#ifdef USE_DISCORD_PRESENCE
  if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
    return false;

  // only /dev/dolphin sets this, don't let homebrew change official client ID raw presence
  if (!s_using_custom_client)
    return false;

  DiscordRichPresence discord_presence = {};
  discord_presence.details = details.c_str();
  discord_presence.state = state.c_str();
  discord_presence.largeImageKey = large_image_key.c_str();
  discord_presence.largeImageText = large_image_text.c_str();
  discord_presence.smallImageKey = small_image_key.c_str();
  discord_presence.smallImageText = small_image_text.c_str();
  discord_presence.startTimestamp = start_timestamp;
  discord_presence.endTimestamp = end_timestamp;
  discord_presence.partySize = party_size;
  discord_presence.partyMax = party_max;
  Discord_UpdatePresence(&discord_presence);

  return true;
#endif
}

void UpdateDiscordPresence(int party_size, SecretType type, const std::string& secret,
                           const std::string& current_game)
{
#ifdef USE_DISCORD_PRESENCE
  if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
    return;

  // reset the client ID if running homebrew has changed it
  if (s_using_custom_client)
    UpdateClientID(DEFAULT_CLIENT_ID);

  const std::string& title =
      current_game.empty() ? SConfig::GetInstance().GetTitleDescription() : current_game;
  std::string game_artwork = ArtworkForGameId(SConfig::GetInstance().GetGameID());

  DiscordRichPresence discord_presence = {};
  if (game_artwork.empty())
  {
    discord_presence.largeImageKey = "dolphin_logo";
    discord_presence.largeImageText = "Dolphin is an emulator for the GameCube and the Wii.";
  }
  else
  {
    discord_presence.largeImageKey = game_artwork.c_str();
    discord_presence.largeImageText = title.c_str();
    discord_presence.smallImageKey = "dolphin_logo";
    discord_presence.smallImageText = "Dolphin is an emulator for the GameCube and the Wii.";
  }
  discord_presence.details = title.empty() ? "Not in-game" : title.c_str();
  discord_presence.startTimestamp = std::chrono::duration_cast<std::chrono::seconds>(
                                        std::chrono::system_clock::now().time_since_epoch())
                                        .count();

  if (party_size > 0)
  {
    if (party_size < 4)
    {
      discord_presence.state = "In a party";
      discord_presence.partySize = party_size;
      discord_presence.partyMax = 4;
    }
    else
    {
      // others can still join to spectate
      discord_presence.state = "In a full party";
      discord_presence.partySize = party_size;
      // Note: joining still works without partyMax
    }
  }

  std::string party_id;
  std::string secret_final;
  if (type != SecretType::Empty)
  {
    // Declearing party_id or secret_final here will deallocate the variable before passing the
    // values over to Discord_UpdatePresence.

    const size_t secret_length = secret.length();
    party_id = std::to_string(
        Common::HashAdler32(reinterpret_cast<const u8*>(secret.c_str()), secret_length));

    const std::string secret_type = std::to_string(static_cast<int>(type));
    secret_final.reserve(secret_type.length() + 1 + secret_length);
    secret_final += secret_type;
    secret_final += '\n';
    secret_final += secret;
  }
  discord_presence.partyId = party_id.c_str();
  discord_presence.joinSecret = secret_final.c_str();

  Discord_UpdatePresence(&discord_presence);
#endif
}

std::string CreateSecretFromIPAddress(const std::string& ip_address, int port)
{
  const std::string port_string = std::to_string(port);
  std::string secret;
  secret.reserve(ip_address.length() + 1 + port_string.length());
  secret += ip_address;
  secret += ':';
  secret += port_string;
  return secret;
}

void Shutdown()
{
#ifdef USE_DISCORD_PRESENCE
  if (!Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
    return;

  Discord_ClearPresence();
  Discord_Shutdown();
#endif
}

void SetDiscordPresenceEnabled(bool enabled)
{
  if (Config::Get(Config::MAIN_USE_DISCORD_PRESENCE) == enabled)
    return;

  if (Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
    Discord::Shutdown();

  Config::SetBase(Config::MAIN_USE_DISCORD_PRESENCE, enabled);

  if (Config::Get(Config::MAIN_USE_DISCORD_PRESENCE))
    Discord::Init();
}

}  // namespace Discord
