/*
 *  Copyright (C) 2012-2026 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#include "FileExtensionProvider.h"

#include "ServiceBroker.h"
#include "addons/AddonEvents.h"
#include "addons/AddonManager.h"
#include "addons/AudioDecoder.h"
#include "addons/ExtsMimeSupportList.h"
#include "addons/ImageDecoder.h"
#include "addons/addoninfo/AddonInfo.h"
#include "addons/addoninfo/AddonType.h"
#include "settings/AdvancedSettings.h"
#include "settings/SettingsComponent.h"
#include "utils/StringUtils.h"
#include "utils/URIUtils.h"
#include "utils/log.h"

#include <array>
#include <functional>
#include <ranges>
#include <string>
#include <string_view>
#include <vector>

using namespace ADDON;
using namespace KODI::ADDONS;

constexpr std::array ADDON_TYPES{AddonType::VFS, AddonType::IMAGEDECODER, AddonType::AUDIODECODER};

void CFileExtensionProvider::Initialize(ADDON::CAddonMgr& addonManager)
{
  m_addonManager = &addonManager;

  m_advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
  if (!m_advancedSettings)
    m_advancedSettings = std::make_shared<CAdvancedSettings>();

  SetAddonExtensions();

  m_addonManager->Events().Subscribe(this,
                                     [this](const AddonEvent& event)
                                     {
                                       if (typeid(event) == typeid(AddonEvents::Enabled) ||
                                           typeid(event) == typeid(AddonEvents::Disabled) ||
                                           typeid(event) == typeid(AddonEvents::ReInstalled))
                                       {
                                         for (auto& type : ADDON_TYPES)
                                         {
                                           if (m_addonManager->HasType(event.addonId, type))
                                           {
                                             std::lock_guard lock{m_critSection};
                                             SetAddonExtensions(type);
                                             break;
                                           }
                                         }
                                       }
                                       else if (typeid(event) == typeid(AddonEvents::UnInstalled))
                                       {
                                         std::lock_guard lock{m_critSection};
                                         SetAddonExtensions();
                                       }
                                     });

  m_callbackId =
      m_advancedSettings->RegisterSettingsLoadedCallback([this]() { OnAdvancedSettingsLoaded(); });

  m_initialized = true;
}

void CFileExtensionProvider::Deinitialize()
{
  // Both callbacks take m_critSection, so they are detached before it is held here.
  if (m_callbackId.has_value())
  {
    m_advancedSettings->UnregisterSettingsLoadedCallback(m_callbackId.value());
    m_callbackId.reset();
  }

  if (m_addonManager != nullptr)
  {
    m_addonManager->Events().Unsubscribe(this);
    m_addonManager = nullptr;
  }

  // A getter that has already passed the unlocked initialized check builds its list under this
  // lock and checks again once it holds it, so nothing below is dropped while a list is being
  // built from it.
  std::lock_guard lock{m_critSection};

  m_initialized = false;
  m_advancedSettings.reset();
  m_addonExtensions.clear();
  m_addonFileFolderExtensions.clear();

  ReleaseSettingsDerivedLists();

  // Not built from the advanced settings, so a settings reload leaves it alone. Deinitialization
  // is dropping everything, so it goes too.
  std::atomic_store(&m_fileFolderExtensions, {});
}

void CFileExtensionProvider::ReleaseSettingsDerivedLists()
{
  std::atomic_store(&m_discStubExtensions, {});
  std::atomic_store(&m_musicExtensions, {});
  std::atomic_store(&m_pictureExtensions, {});
  std::atomic_store(&m_subtitlesExtensions, {});
  std::atomic_store(&m_videoExtensions, {});
  std::atomic_store(&m_archiveExtensions, {});
  std::atomic_store(&m_compoundArchiveExtensions, {});
}

namespace
{
std::string NotInitialized()
{
  // The provider can be read before the logging service exists and after it has gone.
  if (CServiceBroker::IsLoggingUp())
    CLog::Log(LOGWARNING, "CFileExtensionProvider: extension list requested before initialization "
                          "or after deinitialization");
  return {};
}

std::string GetExtensions(const std::atomic<bool>& initialized,
                          CCriticalSection& mutex,
                          std::shared_ptr<const std::string>& cache,
                          std::function<std::string()> newlist)
{
  if (!initialized)
    return NotInitialized();

  // Double-checked locking - first check
  auto tmp = std::atomic_load_explicit(&cache, std::memory_order_acquire);
  if (tmp == nullptr)
  {
    std::lock_guard lock{mutex};

    // Deinitialize drops the settings the list is built from under this lock, so the check is
    // repeated once it is held: passing it unlocked above proves nothing by now.
    if (!initialized)
      return NotInitialized();

    // Second check for threads that saw nullptr but were held by the lock
    // (another thread performed the update)
    tmp = std::atomic_load_explicit(&cache, std::memory_order_relaxed);
    if (tmp == nullptr)
    {
      tmp = std::make_shared<const std::string>(newlist());
      std::atomic_store_explicit(&cache, tmp, std::memory_order_release);
    }
  }
  return *tmp;
}
} // namespace

std::string CFileExtensionProvider::GetDiscStubExtensions() const
{
  return GetExtensions(m_initialized, m_critSection, m_discStubExtensions,
                       [this]() { return m_advancedSettings->m_discStubExtensions; });
}

std::string CFileExtensionProvider::GetMusicExtensions() const
{
  return GetExtensions(m_initialized, m_critSection, m_musicExtensions,
                       [this]()
                       {
                         return m_advancedSettings->m_musicExtensions + '|' +
                                GetAddonExtensions(AddonType::VFS) + '|' +
                                GetAddonExtensions(AddonType::AUDIODECODER);
                       });
}

std::string CFileExtensionProvider::GetPictureExtensions() const
{
  return GetExtensions(m_initialized, m_critSection, m_pictureExtensions,
                       [this]()
                       {
                         return m_advancedSettings->m_pictureExtensions + '|' +
                                GetAddonExtensions(AddonType::VFS) + '|' +
                                GetAddonExtensions(AddonType::IMAGEDECODER);
                       });
}

std::string CFileExtensionProvider::GetSubtitleExtensions() const
{
  return GetExtensions(m_initialized, m_critSection, m_subtitlesExtensions,
                       [this]()
                       {
                         return m_advancedSettings->m_subtitlesExtensions + '|' +
                                GetAddonExtensions(AddonType::VFS);
                       });
}

std::string CFileExtensionProvider::GetVideoExtensions() const
{
  return GetExtensions(m_initialized, m_critSection, m_videoExtensions,
                       [this]()
                       {
                         std::string extensions(m_advancedSettings->m_videoExtensions);
                         if (!extensions.empty())
                           extensions += '|';
                         extensions += GetAddonExtensions(AddonType::VFS);

                         return extensions;
                       });
}

namespace
{
std::string GetSingleExtensions(const std::string& extensions)
{
  std::string out;
  for (const auto& ext : StringUtils::Split(extensions, "|"))
  {
    if (ext.empty())
      continue;

    if (std::ranges::count(ext, '.') == 1)
      out.append(ext + "|");
  }
  if (!out.empty())
    out.pop_back();
  return out;
}

std::string GetCompoundExtensions(std::string_view extensions)
{
  std::string out;
  for (const auto& ext : StringUtils::Split(extensions, "|"))
  {
    if (ext.empty())
      continue;

    if (std::ranges::count(ext, '.') > 1)
      out.append(ext + "|");
  }
  if (!out.empty())
    out.pop_back();
  return out;
}
} // namespace

std::string CFileExtensionProvider::GetArchiveExtensions() const
{
  return GetExtensions(m_initialized, m_critSection, m_archiveExtensions,
                       [this]()
                       {
                         return m_advancedSettings->m_archiveExtensions + '|' +
                                GetSingleExtensions(GetAddonExtensions(AddonType::VFS));
                       });
}

std::string CFileExtensionProvider::GetCompoundArchiveExtensions() const
{
  return GetExtensions(m_initialized, m_critSection, m_compoundArchiveExtensions,
                       [this]()
                       {
                         return m_advancedSettings->m_compoundArchiveExtensions + '|' +
                                GetCompoundExtensions(GetAddonExtensions(AddonType::VFS));
                       });
}

std::string CFileExtensionProvider::GetFileFolderExtensions() const
{
  return GetExtensions(m_initialized, m_critSection, m_fileFolderExtensions,
                       [this]()
                       {
                         std::string extensions(GetAddonFileFolderExtensions(AddonType::VFS));
                         if (!extensions.empty())
                           extensions += '|';
                         extensions += GetAddonFileFolderExtensions(AddonType::AUDIODECODER);

                         return extensions;
                       });
}

bool CFileExtensionProvider::CanOperateExtension(const std::string& path)
{
  /*!
   * @todo Improve this function to support all cases and not only audio decoder.
   */

  // Get file extensions to find addon related to it.
  std::string strExtension = URIUtils::GetExtension(path);
  StringUtils::ToLower(strExtension);
  if (!strExtension.empty() && CServiceBroker::IsAddonInterfaceUp())
  {
    std::vector<std::unique_ptr<KODI::ADDONS::IAddonSupportCheck>> supportList;

    auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetExtensionSupportedAddonInfos(
        strExtension, CExtsMimeSupportList::FilterSelect::all);
    for (const auto& addonInfo : addonInfos)
    {
      switch (addonInfo.first)
      {
        case AddonType::AUDIODECODER:
          supportList.emplace_back(new CAudioDecoder(addonInfo.second));
          break;
        case AddonType::IMAGEDECODER:
          supportList.emplace_back(new CImageDecoder(addonInfo.second, ""));
          break;
        default:
          break;
      }
    }

    /*!
     * We expect that other addons can support the file, and return true if
     * list empty.
     *
     * @todo Check addons can also be types in conflict with Kodi's
     * supported parts!
     *
     * @warning This part is really big ugly at the moment and as soon as possible
     * add about other addons where works with extensions!!!
     * Due to @ref GetFileFolderExtensions() call from outside place before here, becomes
     * it usable in this way, as there limited to AudioDecoder and VFS addons.
     */
    if (supportList.empty())
    {
      return true;
    }

    /*!
     * Check all found addons about support of asked file.
     */
    for (const auto& addon : supportList)
    {
      if (addon->SupportsFile(path))
        return true;
    }
  }

  /*!
   * If no file extensions present, mark it as not supported.
   */
  return false;
}

std::string CFileExtensionProvider::GetAddonExtensions(AddonType type) const
{
  auto it = m_addonExtensions.find(type);
  if (it != m_addonExtensions.end())
    return it->second;

  return "";
}

std::string CFileExtensionProvider::GetAddonFileFolderExtensions(AddonType type) const
{
  auto it = m_addonFileFolderExtensions.find(type);
  if (it != m_addonFileFolderExtensions.end())
    return it->second;

  return "";
}

void CFileExtensionProvider::SetAddonExtensions()
{
  for (auto const type : ADDON_TYPES)
  {
    SetAddonExtensions(type);
  }
}

void CFileExtensionProvider::SetAddonExtensions(AddonType type)
{
  std::vector<std::string> extensions;
  std::vector<std::string> fileFolderExtensions;

  if (type == AddonType::AUDIODECODER || type == AddonType::IMAGEDECODER)
  {
    auto addonInfos = CServiceBroker::GetExtsMimeSupportList().GetSupportedAddonInfos(
        CExtsMimeSupportList::FilterSelect::all);
    for (const auto& addonInfo : addonInfos)
    {
      if (addonInfo.m_addonType != type)
        continue;

      for (const auto& ext : addonInfo.m_supportedExtensions)
      {
        extensions.push_back(ext.first);
        if (addonInfo.m_hasTracks)
          fileFolderExtensions.push_back(ext.first);
      }
    }
    // Invalidate dependent cached extensions lists
    std::atomic_store(&m_musicExtensions, {});
    std::atomic_store(&m_pictureExtensions, {});
    std::atomic_store(&m_fileFolderExtensions, {});
  }
  else if (type == AddonType::VFS)
  {
    std::vector<AddonInfoPtr> addonInfos;

    if (m_addonManager != nullptr)
      m_addonManager->GetAddonInfos(addonInfos, true, type);

    for (const auto& addonInfo : addonInfos)
    {
      std::string ext = addonInfo->Type(type)->GetValue("@extensions").asString();
      if (!ext.empty())
      {
        extensions.push_back(ext);
        if (addonInfo->Type(type)->GetValue("@filedirectories").asBoolean())
          fileFolderExtensions.push_back(ext);

        if (addonInfo->Type(type)->GetValue("@encodedhostname").asBoolean())
        {
          std::string prot = addonInfo->Type(type)->GetValue("@protocols").asString();
          auto prots = StringUtils::Split(prot, "|");
          for (const std::string& it : prots)
            m_encoded.push_back(it);
        }
      }
    }
    // Invalidate dependent cached extensions lists
    std::atomic_store(&m_musicExtensions, {});
    std::atomic_store(&m_pictureExtensions, {});
    std::atomic_store(&m_subtitlesExtensions, {});
    std::atomic_store(&m_videoExtensions, {});
    std::atomic_store(&m_archiveExtensions, {});
    std::atomic_store(&m_compoundArchiveExtensions, {});
    std::atomic_store(&m_fileFolderExtensions, {});
  }

  m_addonExtensions[type] = StringUtils::Join(extensions, "|");
  m_addonFileFolderExtensions[type] = StringUtils::Join(fileFolderExtensions, "|");
}

void CFileExtensionProvider::RegisterGameExtensions(const std::set<std::string>& extensions)
{
  std::lock_guard lock{m_critSection};
  m_gameExtensions.insert(extensions.begin(), extensions.end());
}

void CFileExtensionProvider::UnregisterGameExtensions(const std::set<std::string>& extensions)
{
  std::lock_guard lock{m_critSection};
  for (const auto& ext : extensions)
    m_gameExtensions.erase(ext);
}

std::string CFileExtensionProvider::GetGameExtensions() const
{
  return StringUtils::Join(m_gameExtensions, "|");
}

bool CFileExtensionProvider::EncodedHostName(const std::string& protocol) const
{
  std::lock_guard lock{m_critSection};

  return std::ranges::find(m_encoded, protocol) != m_encoded.end();
}

void CFileExtensionProvider::OnAdvancedSettingsLoaded()
{
  // m_fileFolderExtensions is deliberately not released here: it is built from the add-ons
  // alone, so a settings reload cannot have changed it.
  ReleaseSettingsDerivedLists();
}
