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

#include "VideoCommon/GraphicsModSystem/Runtime/GraphicsModManager.h"

#include <string>
#include <string_view>
#include <variant>

#include "Common/Logging/Log.h"
#include "Common/StringUtil.h"
#include "Common/VariantUtil.h"

#include "Core/ConfigManager.h"

#include "VideoCommon/Assets/DirectFilesystemAssetLibrary.h"
#include "VideoCommon/GraphicsModSystem/Config/GraphicsMod.h"
#include "VideoCommon/GraphicsModSystem/Config/GraphicsModAsset.h"
#include "VideoCommon/GraphicsModSystem/Config/GraphicsModGroup.h"
#include "VideoCommon/GraphicsModSystem/Runtime/GraphicsModActionFactory.h"
#include "VideoCommon/TextureInfo.h"
#include "VideoCommon/VideoConfig.h"

std::unique_ptr<GraphicsModManager> g_graphics_mod_manager;

class GraphicsModManager::DecoratedAction final : public GraphicsModAction
{
public:
  DecoratedAction(std::unique_ptr<GraphicsModAction> action, GraphicsModConfig mod)
      : m_action_impl(std::move(action)), m_mod(std::move(mod))
  {
  }
  void OnDrawStarted(GraphicsModActionData::DrawStarted* draw_started) override
  {
    if (!m_mod.m_enabled)
      return;
    m_action_impl->OnDrawStarted(draw_started);
  }
  void OnEFB(GraphicsModActionData::EFB* efb) override
  {
    if (!m_mod.m_enabled)
      return;
    m_action_impl->OnEFB(efb);
  }
  void OnProjection(GraphicsModActionData::Projection* projection) override
  {
    if (!m_mod.m_enabled)
      return;
    m_action_impl->OnProjection(projection);
  }
  void OnProjectionAndTexture(GraphicsModActionData::Projection* projection) override
  {
    if (!m_mod.m_enabled)
      return;
    m_action_impl->OnProjectionAndTexture(projection);
  }
  void OnTextureLoad(GraphicsModActionData::TextureLoad* texture_load) override
  {
    if (!m_mod.m_enabled)
      return;
    m_action_impl->OnTextureLoad(texture_load);
  }
  void OnTextureCreate(GraphicsModActionData::TextureCreate* texture_create) override
  {
    if (!m_mod.m_enabled)
      return;
    m_action_impl->OnTextureCreate(texture_create);
  }
  void OnFrameEnd() override
  {
    if (!m_mod.m_enabled)
      return;
    m_action_impl->OnFrameEnd();
  }

private:
  std::unique_ptr<GraphicsModAction> m_action_impl;
  GraphicsModConfig m_mod;
};

bool GraphicsModManager::Initialize()
{
  if (g_ActiveConfig.bGraphicMods)
  {
    // If a config change occurred in a previous session,
    // remember the old change count value.  By setting
    // our current change count to the old value, we
    // avoid loading the stale data when we
    // check for config changes.
    const u32 old_game_mod_changes = g_ActiveConfig.graphics_mod_config ?
                                         g_ActiveConfig.graphics_mod_config->GetChangeCount() :
                                         0;
    g_ActiveConfig.graphics_mod_config = GraphicsModGroupConfig(SConfig::GetInstance().GetGameID());
    g_ActiveConfig.graphics_mod_config->Load();
    g_ActiveConfig.graphics_mod_config->SetChangeCount(old_game_mod_changes);
    g_graphics_mod_manager->Load(*g_ActiveConfig.graphics_mod_config);

    m_end_of_frame_event =
        AfterFrameEvent::Register([this](Core::System&) { EndOfFrame(); }, "ModManager");
  }

  return true;
}

const std::vector<GraphicsModAction*>&
GraphicsModManager::GetProjectionActions(ProjectionType projection_type) const
{
  if (const auto it = m_projection_target_to_actions.find(projection_type);
      it != m_projection_target_to_actions.end())
  {
    return it->second;
  }

  return m_default;
}

const std::vector<GraphicsModAction*>&
GraphicsModManager::GetProjectionTextureActions(ProjectionType projection_type,
                                                const std::string& texture_name) const
{
  const auto lookup = fmt::format("{}_{}", texture_name, static_cast<int>(projection_type));
  if (const auto it = m_projection_texture_target_to_actions.find(lookup);
      it != m_projection_texture_target_to_actions.end())
  {
    return it->second;
  }

  return m_default;
}

const std::vector<GraphicsModAction*>&
GraphicsModManager::GetDrawStartedActions(const std::string& texture_name) const
{
  if (const auto it = m_draw_started_target_to_actions.find(texture_name);
      it != m_draw_started_target_to_actions.end())
  {
    return it->second;
  }

  return m_default;
}

const std::vector<GraphicsModAction*>&
GraphicsModManager::GetTextureLoadActions(const std::string& texture_name) const
{
  if (const auto it = m_load_texture_target_to_actions.find(texture_name);
      it != m_load_texture_target_to_actions.end())
  {
    return it->second;
  }

  return m_default;
}

const std::vector<GraphicsModAction*>&
GraphicsModManager::GetTextureCreateActions(const std::string& texture_name) const
{
  if (const auto it = m_create_texture_target_to_actions.find(texture_name);
      it != m_create_texture_target_to_actions.end())
  {
    return it->second;
  }

  return m_default;
}

const std::vector<GraphicsModAction*>& GraphicsModManager::GetEFBActions(const FBInfo& efb) const
{
  if (const auto it = m_efb_target_to_actions.find(efb); it != m_efb_target_to_actions.end())
  {
    return it->second;
  }

  return m_default;
}

const std::vector<GraphicsModAction*>& GraphicsModManager::GetXFBActions(const FBInfo& xfb) const
{
  if (const auto it = m_efb_target_to_actions.find(xfb); it != m_efb_target_to_actions.end())
  {
    return it->second;
  }

  return m_default;
}

void GraphicsModManager::Load(const GraphicsModGroupConfig& config)
{
  Reset();

  const auto& mods = config.GetMods();

  auto filesystem_library = std::make_shared<VideoCommon::DirectFilesystemAssetLibrary>();

  std::map<std::string, std::vector<GraphicsTargetConfig>> group_to_targets;
  for (const auto& mod : mods)
  {
    for (const GraphicsTargetGroupConfig& group : mod.m_groups)
    {
      if (m_groups.contains(group.m_name))
      {
        WARN_LOG_FMT(
            VIDEO,
            "Specified graphics mod group '{}' for mod '{}' is already specified by another mod.",
            group.m_name, mod.m_title);
      }
      m_groups.insert(group.m_name);

      const auto internal_group = fmt::format("{}.{}", mod.m_title, group.m_name);
      for (const GraphicsTargetConfig& target : group.m_targets)
      {
        group_to_targets[group.m_name].push_back(target);
        group_to_targets[internal_group].push_back(target);
      }
    }

    std::string base_path;
    SplitPath(mod.GetAbsolutePath(), &base_path, nullptr, nullptr);
    for (const GraphicsModAssetConfig& asset : mod.m_assets)
    {
      auto asset_map = asset.m_map;
      for (auto& [k, v] : asset_map)
      {
        if (v.is_absolute())
        {
          WARN_LOG_FMT(VIDEO,
                       "Specified graphics mod asset '{}' for mod '{}' has an absolute path, you "
                       "shouldn't release this to users.",
                       asset.m_asset_id, mod.m_title);
        }
        else
        {
          v = std::filesystem::path{base_path} / v;
        }
      }

      filesystem_library->SetAssetIDMapData(asset.m_asset_id, std::move(asset_map));
    }
  }

  for (const auto& mod : mods)
  {
    for (const GraphicsModFeatureConfig& feature : mod.m_features)
    {
      const auto create_action =
          [filesystem_library](const std::string_view& action_name,
                               const picojson::value& json_data,
                               GraphicsModConfig mod_config) -> std::unique_ptr<GraphicsModAction> {
        auto action =
            GraphicsModActionFactory::Create(action_name, json_data, std::move(filesystem_library));
        if (action == nullptr)
        {
          return nullptr;
        }
        return std::make_unique<DecoratedAction>(std::move(action), std::move(mod_config));
      };

      const auto internal_group = fmt::format("{}.{}", mod.m_title, feature.m_group);

      const auto add_target = [&](const GraphicsTargetConfig& target) {
        std::visit(
            overloaded{
                [&](const DrawStartedTextureTarget& the_target) {
                  m_draw_started_target_to_actions[the_target.m_texture_info_string].push_back(
                      m_actions.back().get());
                },
                [&](const LoadTextureTarget& the_target) {
                  m_load_texture_target_to_actions[the_target.m_texture_info_string].push_back(
                      m_actions.back().get());
                },
                [&](const CreateTextureTarget& the_target) {
                  m_create_texture_target_to_actions[the_target.m_texture_info_string].push_back(
                      m_actions.back().get());
                },
                [&](const EFBTarget& the_target) {
                  FBInfo info;
                  info.m_height = the_target.m_height;
                  info.m_width = the_target.m_width;
                  info.m_texture_format = the_target.m_texture_format;
                  m_efb_target_to_actions[info].push_back(m_actions.back().get());
                },
                [&](const XFBTarget& the_target) {
                  FBInfo info;
                  info.m_height = the_target.m_height;
                  info.m_width = the_target.m_width;
                  info.m_texture_format = the_target.m_texture_format;
                  m_xfb_target_to_actions[info].push_back(m_actions.back().get());
                },
                [&](const ProjectionTarget& the_target) {
                  if (the_target.m_texture_info_string)
                  {
                    const auto lookup = fmt::format("{}_{}", *the_target.m_texture_info_string,
                                                    static_cast<int>(the_target.m_projection_type));
                    m_projection_texture_target_to_actions[lookup].push_back(
                        m_actions.back().get());
                  }
                  else
                  {
                    m_projection_target_to_actions[the_target.m_projection_type].push_back(
                        m_actions.back().get());
                  }
                },
            },
            target);
      };

      const auto add_action = [&](GraphicsModConfig mod_config) -> bool {
        auto action = create_action(feature.m_action, feature.m_action_data, std::move(mod_config));
        if (action == nullptr)
        {
          WARN_LOG_FMT(VIDEO, "Failed to create action '{}' for group '{}'.", feature.m_action,
                       feature.m_group);
          return false;
        }
        m_actions.push_back(std::move(action));
        return true;
      };

      // Prefer groups in the pack over groups from another pack
      if (const auto local_it = group_to_targets.find(internal_group);
          local_it != group_to_targets.end())
      {
        if (add_action(mod))
        {
          for (const GraphicsTargetConfig& target : local_it->second)
          {
            add_target(target);
          }
        }
      }
      else if (const auto global_it = group_to_targets.find(feature.m_group);
               global_it != group_to_targets.end())
      {
        if (add_action(mod))
        {
          for (const GraphicsTargetConfig& target : global_it->second)
          {
            add_target(target);
          }
        }
      }
      else
      {
        WARN_LOG_FMT(VIDEO, "Specified graphics mod group '{}' was not found for mod '{}'",
                     feature.m_group, mod.m_title);
      }
    }
  }
}

void GraphicsModManager::EndOfFrame()
{
  for (auto&& action : m_actions)
  {
    action->OnFrameEnd();
  }
}

void GraphicsModManager::Reset()
{
  m_actions.clear();
  m_groups.clear();
  m_projection_target_to_actions.clear();
  m_projection_texture_target_to_actions.clear();
  m_draw_started_target_to_actions.clear();
  m_load_texture_target_to_actions.clear();
  m_create_texture_target_to_actions.clear();
  m_efb_target_to_actions.clear();
  m_xfb_target_to_actions.clear();
}
