/*
 *  Copyright (C) 2005-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.
 */

// System headers
#ifdef TARGET_DARWIN
#include <mach-o/dyld.h>
#include <sys/param.h>
#endif

#ifdef TARGET_FREEBSD
#include <sys/param.h>
#include <sys/sysctl.h>
#endif

#ifdef TARGET_POSIX
#include <dirent.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#endif

#ifdef TARGET_ANDROID
#include <androidjni/ApplicationInfo.h>
#endif

// Kodi headers that include system headers
// (come before Util.h to stabilize __stat64 typedefs)
#include "FileItem.h"
#include "ServiceBroker.h"
#include "URL.h"
#include "filesystem/Directory.h"
#include "filesystem/File.h"
#include "filesystem/SpecialProtocol.h"
#include "utils/StringUtils.h"
#include "utils/URIUtils.h"
#include "utils/log.h"

// Class header
#include "Util.h"

// Other platform specific headers
#ifdef TARGET_WINDOWS
#include "WIN32Util.h"
#include "utils/CharsetConverter.h"
#endif

#ifdef TARGET_DARWIN
#include "CompileInfo.h"
#include "platform/darwin/DarwinUtils.h"
#endif

#ifdef TARGET_ANDROID
#include "CompileInfo.h"

#include "platform/android/activity/XBMCApp.h"
#endif

// Remaining headers
#include "addons/VFSEntry.h"
#include "cores/VideoPlayer/DVDSubtitles/DVDSubtitleStream.h"
#include "cores/VideoPlayer/DVDSubtitles/DVDSubtitleTagSami.h"
#include "filesystem/MultiPathDirectory.h"
#include "filesystem/PVRDirectory.h"
#include "filesystem/RSSDirectory.h"
#include "filesystem/StackDirectory.h"
#ifdef HAS_UPNP
#include "filesystem/UPnPDirectory.h"
#endif
#include "guilib/TextureManager.h"
#include "network/Network.h"
#include "network/NetworkFileItemClassify.h"
#include "platform/Environment.h"
#include "playlists/PlayListFileItemClassify.h"
#include "profiles/ProfileManager.h"
#include "resources/LocalizeStrings.h"
#include "resources/ResourcesComponent.h"
#include "settings/AdvancedSettings.h"
#include "settings/MediaSettings.h"
#include "settings/Settings.h"
#include "settings/SettingsComponent.h"
#include "storage/MediaManager.h"
#include "utils/Digest.h"
#include "utils/FileExtensionProvider.h"
#include "utils/RegExp.h"
#include "utils/StringUtils.h"
#include "video/FilenameAttributes.h"
#include "video/VideoDatabase.h"
#include "video/VideoFileItemClassify.h"
#include "windowing/GraphicContext.h"
#include "windowing/WinSystem.h"

#ifdef HAVE_LIBCAP
#include <sys/capability.h>
#endif

#include <algorithm>
#include <array>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <iomanip>
#include <memory>
#include <optional>
#include <random>
#include <ranges>
#include <sstream>
#include <string>
#include <string_view>
#include <unordered_set>
#include <utility>
#include <vector>

#include <fstrcmp.h>

#ifdef HAS_OPTICAL_DRIVE
using namespace MEDIA_DETECT;
#endif

using namespace XFILE;
using KODI::UTILITY::CDigest;
using namespace KODI;

#if !defined(TARGET_WINDOWS)
unsigned int CUtil::s_randomSeed = time(NULL);
#endif

namespace
{
#ifdef TARGET_WINDOWS
bool IsDirectoryValidRoot(std::wstring path)
{
  path += L"\\system\\settings\\settings.xml";
#if defined(TARGET_WINDOWS_STORE)
  auto h = CreateFile2(path.c_str(), GENERIC_READ, 0, OPEN_EXISTING, NULL);
#else
  auto h = CreateFileW(path.c_str(), GENERIC_READ, 0, nullptr,
    OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
#endif
  if (h != INVALID_HANDLE_VALUE)
  {
    CloseHandle(h);
    return true;
  }

  return false;
}

std::string GetHomePath(const std::string& strTarget, std::string strPath)
{
  std::wstring strPathW;

  // Environment variable was set and we have a path
  // Let's make sure it's not relative and test it
  // so it's actually pointing to a directory containing
  // our stuff
  if (strPath.find("..") != std::string::npos)
  {
    //expand potential relative path to full path
    g_charsetConverter.utf8ToW(strPath, strPathW, false);
    CWIN32Util::AddExtraLongPathPrefix(strPathW);
    auto bufSize = GetFullPathNameW(strPathW.c_str(), 0, nullptr, nullptr);
    if (bufSize != 0)
    {
      auto buf = std::make_unique<wchar_t[]>(bufSize);
      if (GetFullPathNameW(strPathW.c_str(), bufSize, buf.get(), nullptr) <= bufSize - 1)
      {
        strPathW = buf.get();
        CWIN32Util::RemoveExtraLongPathPrefix(strPathW);

        if (IsDirectoryValidRoot(strPathW))
        {
          g_charsetConverter.wToUTF8(strPathW, strPath);
          return strPath;
        }
      }
    }
  }

  // Okay se no environment variable is set, let's
  // grab the executable path and check if it's being
  // run from a directory containing our stuff
  strPath = CUtil::ResolveExecutablePath();
  auto last_sep = strPath.find_last_of(PATH_SEPARATOR_CHAR);
  if (last_sep != std::string::npos)
    strPath.resize(last_sep);

  g_charsetConverter.utf8ToW(strPath, strPathW);
  if (IsDirectoryValidRoot(strPathW))
    return strPath;

  // Still nothing, let's check the current working
  // directory and see if it points to a directory
  // with our stuff in it. This bit should never be
  // needed when running on a users system, it's intended
  // to make our dev environment easier.
  auto bufSize = GetCurrentDirectoryW(0, nullptr);
  if (bufSize > 0)
  {
    auto buf = std::make_unique<wchar_t[]>(bufSize);
    if (0 != GetCurrentDirectoryW(bufSize, buf.get()))
    {
      std::string currentDirectory;
      std::wstring currentDirectoryW(buf.get());
      CWIN32Util::RemoveExtraLongPathPrefix(currentDirectoryW);

      if (IsDirectoryValidRoot(currentDirectoryW))
      {
        g_charsetConverter.wToUTF8(currentDirectoryW, currentDirectory);
        return currentDirectory;
      }
    }
  }

  // If we ended up here we're most likely screwed
  // we will crash in a few seconds
  return strPath;
}
#endif
#if defined(TARGET_DARWIN)
#if !defined(TARGET_DARWIN_EMBEDDED)
bool IsDirectoryValidRoot(std::string path)
{
  path += "/system/settings/settings.xml";
  return CFile::Exists(path);
}
#endif

std::string GetHomePath(const std::string& strTarget, std::string strPath)
{
  if (strPath.empty())
  {
    auto strHomePath = CUtil::ResolveExecutablePath();
    int      result = -1;
    char     given_path[2 * MAXPATHLEN];
    size_t path_size = 2 * MAXPATHLEN;

    result = CDarwinUtils::GetExecutablePath(given_path, &path_size);
    if (result == 0)
    {
      // Move backwards to last /.
      for (int n = strlen(given_path) - 1; given_path[n] != '/'; n--)
        given_path[n] = '\0';

#if defined(TARGET_DARWIN_EMBEDDED)
      strcat(given_path, "/AppData/AppHome/");
#else
      // Assume local path inside application bundle.
      strcat(given_path, "../Resources/");
      strcat(given_path, CCompileInfo::GetAppName());
      strcat(given_path, "/");

      // if this path doesn't exist we
      // might not be started from the app bundle
      // but from the debugger/xcode. Lets
      // see if this assumption is valid
      if (!CDirectory::Exists(given_path))
      {
        std::string given_path_stdstr = CUtil::ResolveExecutablePath();
        // try to find the correct folder by going back
        // in the executable path until settings.xml was found
        bool validRoot = false;
        do
        {
          given_path_stdstr = URIUtils::GetParentPath(given_path_stdstr);
          validRoot = IsDirectoryValidRoot(given_path_stdstr);
        }
        while(given_path_stdstr.length() > 0 && !validRoot);
        strncpy(given_path, given_path_stdstr.c_str(), sizeof(given_path)-1);
      }

#endif

      // Convert to real path.
      char real_path[2 * MAXPATHLEN];
      if (realpath(given_path, real_path) != NULL)
      {
        strPath = real_path;
        return strPath;
      }
    }
    size_t last_sep = strHomePath.find_last_of(PATH_SEPARATOR_CHAR);
    if (last_sep != std::string::npos)
      strPath = strHomePath.substr(0, last_sep);
    else
      strPath = strHomePath;

  }
  return strPath;
}
#endif
#if defined(TARGET_POSIX) && !defined(TARGET_DARWIN)
std::string GetHomePath(const std::string& strTarget, std::string strPath)
{
  if (strPath.empty())
  {
    auto strHomePath = CUtil::ResolveExecutablePath();
    size_t last_sep = strHomePath.find_last_of(PATH_SEPARATOR_CHAR);
    if (last_sep != std::string::npos)
      strPath = strHomePath.substr(0, last_sep);
    else
      strPath = strHomePath;
  }
  /* Change strPath accordingly when target is KODI_HOME and when INSTALL_PATH
   * and BIN_INSTALL_PATH differ
   */
  std::string installPath = INSTALL_PATH;
  std::string binInstallPath = BIN_INSTALL_PATH;

  if (strTarget.empty() && installPath.compare(binInstallPath))
  {
    int pos = strPath.length() - binInstallPath.length();
    std::string tmp = strPath;
    tmp.erase(0, pos);
    if (!tmp.compare(binInstallPath))
    {
      strPath.erase(pos, strPath.length());
      strPath.append(installPath);
    }
  }

  return strPath;
}
#endif
}

std::string CUtil::GetTitleFromPath(const std::string& strFileNameAndPath, bool bIsFolder /* = false */)
{
  CURL pathToUrl(strFileNameAndPath);
  return GetTitleFromPath(pathToUrl, bIsFolder);
}

std::string CUtil::GetTitleFromPath(const CURL& url, bool bIsFolder /* = false */)
{
  // use above to get the filename
  std::string path(url.Get());
  URIUtils::RemoveSlashAtEnd(path);
  std::string strFilename = URIUtils::GetFileName(path);

#ifdef HAS_UPNP
  // UPNP
  if (url.IsProtocol("upnp"))
    strFilename = CUPnPDirectory::GetFriendlyName(url);
#endif

  if (url.IsProtocol("rss") || url.IsProtocol("rsss"))
  {
    CRSSDirectory dir;
    CFileItemList items;
    if (dir.GetDirectory(url, items))
    {
      const std::string& title{items.GetTitle()};
      if (!title.empty())
        return title;
    }
  }

  // Shoutcast
  else if (url.IsProtocol("shout"))
  {
    const std::string strFileNameAndPath = url.Get();
    const size_t genre = strFileNameAndPath.find_first_of('=');
    if(genre == std::string::npos)
      strFilename = CServiceBroker::GetResourcesComponent().GetLocalizeStrings().Get(260);
    else
      strFilename = CServiceBroker::GetResourcesComponent().GetLocalizeStrings().Get(260) + " - " +
                    strFileNameAndPath.substr(genre + 1).c_str();
  }

  // Windows SMB Network (SMB)
  else if (url.IsProtocol("smb") && strFilename.empty())
  {
    if (url.GetHostName().empty())
    {
      strFilename = CServiceBroker::GetResourcesComponent().GetLocalizeStrings().Get(20171);
    }
    else
    {
      strFilename = url.GetHostName();
    }
  }

  // Root file views
  else if (url.IsProtocol("sources"))
    strFilename = CServiceBroker::GetResourcesComponent().GetLocalizeStrings().Get(744);

  // Music Playlists
  else if (StringUtils::StartsWith(path, "special://musicplaylists"))
    strFilename = CServiceBroker::GetResourcesComponent().GetLocalizeStrings().Get(136);

  // Video Playlists
  else if (StringUtils::StartsWith(path, "special://videoplaylists"))
    strFilename = CServiceBroker::GetResourcesComponent().GetLocalizeStrings().Get(136);

  else if (URIUtils::HasParentInHostname(url) && strFilename.empty())
    strFilename = URIUtils::GetFileName(url.GetHostName());

  // now remove the extension if needed
  if (!CServiceBroker::GetSettingsComponent()->GetSettings()->GetBool(CSettings::SETTING_FILELISTS_SHOWEXTENSIONS) && !bIsFolder)
  {
    URIUtils::RemoveExtension(strFilename);
    return strFilename;
  }

  // URLDecode since the original path may be an URL
  strFilename = CURL::Decode(strFilename);
  return strFilename;
}

namespace
{
std::string GetPartAndRemoveDiscFromPath(std::string& path,
                                         CUtil::PreserveFileName preserveFileName)
{
  // Only remove file name if it's a disc path and the file name does not represent the movie name
  if (preserveFileName == CUtil::PreserveFileName::KEEP && !URIUtils::IsBDFile(path) &&
      !URIUtils::IsDVDFile(path))
    return {};

  path = URIUtils::GetDirectory(path);
  path = URIUtils::IsDiscPath(path) || URIUtils::IsBlurayPath(path)
             ? URIUtils::GetDiscBasePath(path)
             : path;
  std::string basePath{path};
  URIUtils::RemoveSlashAtEnd(basePath);

  const std::string r{URIUtils::GetTrailingPartNumberRegex()};
  CRegExp regex{true, CRegExp::autoUtf8, r.c_str()};
  if (regex.RegFind(basePath) != -1)
  {
    path = basePath.erase(regex.GetSubStart(0) + 1);
    return regex.GetMatch(1);
  }
  return {};
}
} // unnamed namespace

std::string CUtil::RemoveTrailingPartNumberSegmentFromPath(std::string path,
                                                           PreserveFileName preserveFileName)
{
  GetPartAndRemoveDiscFromPath(path, preserveFileName);
  return path;
}

std::string CUtil::GetPartNumberFromPath(std::string path)
{
  return GetPartAndRemoveDiscFromPath(path, PreserveFileName::REMOVE);
}

void CUtil::CleanString(const std::string& strFileName,
                        std::string& strTitle,
                        std::string& strTitleAndYear,
                        std::string& strYear,
                        bool bRemoveExtension /* = false */,
                        bool bCleanChars /* = true */)
{
  strTitleAndYear = strFileName;

  if (strFileName == "..")
   return;

  KODI::VIDEO::CFilenameAttributes::CleanFilenameAttributePairs(strTitleAndYear, nullptr);

  const std::shared_ptr<CAdvancedSettings> advancedSettings = CServiceBroker::GetSettingsComponent()->GetAdvancedSettings();
  const std::vector<std::string> &regexps = advancedSettings->m_videoCleanStringRegExps;

  CRegExp reTags(true, CRegExp::autoUtf8);
  CRegExp reYear(false, CRegExp::autoUtf8);

  if (!reYear.RegComp(advancedSettings->m_videoCleanDateTimeRegExp))
  {
    CLog::Log(LOGERROR, "{}: Invalid datetime clean RegExp:'{}'", __FUNCTION__,
              advancedSettings->m_videoCleanDateTimeRegExp);
  }
  else
  {
    if (reYear.RegFind(strTitleAndYear.c_str()) >= 0)
    {
      strTitleAndYear = reYear.GetMatch(1);
      strYear = reYear.GetMatch(2);
    }
  }

  URIUtils::RemoveExtension(strTitleAndYear);

  for (const auto &regexp : regexps)
  {
    if (!reTags.RegComp(regexp.c_str()))
    { // invalid regexp - complain in logs
      CLog::Log(LOGERROR, "{}: Invalid string clean RegExp:'{}'", __FUNCTION__, regexp);
      continue;
    }
    int j=0;
    if ((j=reTags.RegFind(strTitleAndYear.c_str())) > 0)
      strTitleAndYear.resize(j);
  }

  // final cleanup - special characters used instead of spaces:
  // all '_' tokens should be replaced by spaces
  // if the file contains no spaces, all '.' tokens should be replaced by
  // spaces - one possibility of a mistake here could be something like:
  // "Dr..StrangeLove" - hopefully no one would have anything like this.
  if (bCleanChars)
  {
    bool initialDots = true;
    bool alreadyContainsSpace = (strTitleAndYear.find(' ') != std::string::npos);

    for (char &c : strTitleAndYear)
    {
      if (c != '.')
        initialDots = false;

      if ((c == '_') || ((!alreadyContainsSpace) && !initialDots && (c == '.')))
      {
        c = ' ';
      }
    }
  }

  StringUtils::Trim(strTitleAndYear);
  strTitle = strTitleAndYear;

  // append year
  if (!strYear.empty())
    strTitleAndYear = strTitle + " (" + strYear + ")";

  // restore extension if needed
  if (!bRemoveExtension)
    strTitleAndYear += URIUtils::GetExtension(strFileName);
}

void CUtil::GetQualifiedFilename(const std::string &strBasePath, std::string &strFilename)
{
  // Check if the filename is a fully qualified URL such as protocol://path/to/file
  CURL plItemUrl(strFilename);
  if (!plItemUrl.GetProtocol().empty())
    return;

  // If the filename starts "x:", "\\" or "/" it's already fully qualified so return
  if (strFilename.size() > 1)
#ifdef TARGET_POSIX
    if ( (strFilename[1] == ':') || (strFilename[0] == '/') )
#else
    if ( strFilename[1] == ':' || (strFilename[0] == '\\' && strFilename[1] == '\\'))
#endif
      return;

  // add to base path and then clean
  strFilename = URIUtils::AddFileToFolder(strBasePath, strFilename);

  // get rid of any /./ or \.\ that happen to be there
  StringUtils::Replace(strFilename, "\\.\\", "\\");
  StringUtils::Replace(strFilename, "/./", "/");

  // now find any "\\..\\" and remove them via GetParentPath
  size_t pos;
  while ((pos = strFilename.find("/../")) != std::string::npos)
  {
    std::string basePath = strFilename.substr(0, pos + 1);
    strFilename.erase(0, pos + 4);
    basePath = URIUtils::GetParentPath(basePath);
    strFilename = URIUtils::AddFileToFolder(basePath, strFilename);
  }
  while ((pos = strFilename.find("\\..\\")) != std::string::npos)
  {
    std::string basePath = strFilename.substr(0, pos + 1);
    strFilename.erase(0, pos + 4);
    basePath = URIUtils::GetParentPath(basePath);
    strFilename = URIUtils::AddFileToFolder(basePath, strFilename);
  }
}

void CUtil::RunShortcut(const char* szShortcutPath)
{
}

std::string CUtil::GetHomePath(const std::string& strTarget)
{
  auto strPath = CEnvironment::getenv(strTarget);

  return ::GetHomePath(strTarget, strPath);
}

bool CUtil::IsPicture(const std::string& strFile)
{
  return URIUtils::HasExtension(strFile,
                  CServiceBroker::GetFileExtensionProvider().GetPictureExtensions()+ "|.tbn|.dds");
}

std::string CUtil::GetSplashPath()
{
  std::array<std::string, 4> candidates {{ "special://home/media/splash.jpg", "special://home/media/splash.png", "special://xbmc/media/splash.jpg", "special://xbmc/media/splash.png" }};
  auto it = std::find_if(candidates.begin(), candidates.end(), [](std::string const& file) { return XFILE::CFile::Exists(file); });
  if (it == candidates.end())
    throw std::runtime_error("No splash image found");
  return CSpecialProtocol::TranslatePathConvertCase(*it);
}

bool CUtil::ExcludeFileOrFolder(const std::string& strFileOrFolder,
                                const std::vector<std::string>& regexps,
                                KODI::REGEXP::RegExpCache* cache)
{
  if (strFileOrFolder.empty())
    return false;

  std::shared_ptr<CRegExp> regExExcludes;

  for (const auto &regexp : regexps)
  {
    if (regExExcludes = KODI::REGEXP::GetRegExp(regexp, cache, true, CRegExp::autoUtf8);
        regExExcludes == nullptr)
    { // invalid regexp - complain in logs
      CLog::Log(LOGERROR, "{}: Invalid exclude RegExp:'{}'", __FUNCTION__, regexp);
      continue;
    }
    if (regExExcludes->RegFind(strFileOrFolder) > -1)
    {
      CLog::LogF(LOGDEBUG, "File '{}' excluded. (Matches exclude rule RegExp: '{}')", CURL::GetRedacted(strFileOrFolder), regexp);
      return true;
    }
  }
  return false;
}

void CUtil::GetFileAndProtocol(const std::string& strURL, std::string& strDir)
{
  strDir = strURL;
  if (!URIUtils::IsRemote(strURL)) return ;
  if (URIUtils::IsDVD(strURL)) return ;

  CURL url(strURL);
  strDir = StringUtils::Format("{}://{}", url.GetProtocol(), url.GetFileName());
}

int CUtil::GetDVDIfoTitle(const std::string& strFile)
{
  std::string strFilename = URIUtils::GetFileName(strFile);
  if (StringUtils::EqualsNoCase(strFilename, "video_ts.ifo")) return 0;
  //VTS_[TITLE]_0.IFO
  return atoi(strFilename.substr(4, 2).c_str());
}

std::string CUtil::GetFileDigest(const std::string& strPath, KODI::UTILITY::CDigest::Type type)
{
  CFile file;
  std::string result;
  if (file.Open(strPath))
  {
    CDigest digest{type};
    char temp[1024];
    while (true)
    {
      ssize_t read = file.Read(temp,1024);
      if (read <= 0)
        break;
      digest.Update(temp,read);
    }
    result = digest.Finalize();
    file.Close();
  }

  return result;
}

bool CUtil::GetDirectoryName(const std::string& strFileName, std::string& strDescription)
{
  std::string strFName = URIUtils::GetFileName(strFileName);
  strDescription = URIUtils::GetDirectory(strFileName);
  URIUtils::RemoveSlashAtEnd(strDescription);

  size_t iPos = strDescription.find_last_of("/\\");
  if (iPos != std::string::npos)
    strDescription = strDescription.substr(iPos + 1);
  else if (strDescription.empty())
    strDescription = strFName;
  return true;
}

void CUtil::GetDVDDriveIcon(const std::string& strPath, std::string& strIcon)
{
  if (!CServiceBroker::GetMediaManager().IsDiscInDrive(strPath))
  {
    strIcon = "DefaultDVDEmpty.png";
    return ;
  }

  CFileItem item = CFileItem(strPath, false);

  if (item.IsBluray())
  {
    strIcon = "DefaultBluray.png";
    return;
  }

  if ( URIUtils::IsDVD(strPath) )
  {
    strIcon = "DefaultDVDFull.png";
    return ;
  }

  if ( URIUtils::IsISO9660(strPath) )
  {
#ifdef HAS_OPTICAL_DRIVE
    CCdInfo* pInfo = CServiceBroker::GetMediaManager().GetCdInfo();
    if ( pInfo != NULL && pInfo->IsVideoCd( 1 ) )
    {
      strIcon = "DefaultVCD.png";
      return ;
    }
#endif
    strIcon = "DefaultDVDRom.png";
    return ;
  }

  if ( URIUtils::IsCDDA(strPath) )
  {
    strIcon = "DefaultCDDA.png";
    return ;
  }
}

void CUtil::RemoveTempFiles()
{
  const std::shared_ptr<CProfileManager> profileManager = CServiceBroker::GetSettingsComponent()->GetProfileManager();

  std::string searchPath = profileManager->GetDatabaseFolder();
  CFileItemList items;
  if (!XFILE::CDirectory::GetDirectory(searchPath, items, ".tmp", DIR_FLAG_NO_FILE_DIRS))
    return;

  for (const auto &item : items)
  {
    if (item->IsFolder())
      continue;
    XFILE::CFile::Delete(item->GetPath());
  }
}

void CUtil::ClearSubtitles()
{
  //delete cached subs
  CFileItemList items;
  CDirectory::GetDirectory("special://temp/",items, "", DIR_FLAG_DEFAULTS);
  for (const auto &item : items)
  {
    if (!item->IsFolder())
    {
      if (item->GetPath().find("subtitle") != std::string::npos ||
          item->GetPath().find("vobsub_queue") != std::string::npos)
      {
        CLog::Log(LOGDEBUG, "{} - Deleting temporary subtitle {}", __FUNCTION__, item->GetPath());
        CFile::Delete(item->GetPath());
      }
    }
  }
}

int64_t CUtil::ToInt64(uint32_t high, uint32_t low)
{
  int64_t n;
  n = high;
  n <<= 32;
  n += low;
  return n;
}

/*!
  \brief Finds next unused filename that matches padded int format identifier provided
  \param[in]  fn_template    filename template consisting of a padded int format identifier (eg screenshot%03d)
  \param[in]  max            maximum number to search for available name
  \return "" on failure, string next available name matching format identifier on success
*/

std::string CUtil::GetNextFilename(const std::string &fn_template, int max)
{
  std::string searchPath = URIUtils::GetDirectory(fn_template);
  std::string mask = URIUtils::GetExtension(fn_template);
  std::string name = StringUtils::Format(fn_template, 0);

  CFileItemList items;
  if (!CDirectory::GetDirectory(searchPath, items, mask, DIR_FLAG_NO_FILE_DIRS))
    return name;

  items.SetFastLookup(true);
  for (int i = 0; i <= max; i++)
  {
    std::string name = StringUtils::Format(fn_template, i);
    if (!items.Get(name))
      return name;
  }
  return "";
}

std::string CUtil::GetNextPathname(const std::string &path_template, int max)
{
  if (path_template.find("%04d") == std::string::npos)
    return "";

  for (int i = 0; i <= max; i++)
  {
    std::string name = StringUtils::Format(path_template, i);
    if (!CFile::Exists(name) && !CDirectory::Exists(name))
      return name;
  }
  return "";
}

void CUtil::StatToStatI64(struct _stati64 *result, struct stat *stat)
{
  result->st_dev = stat->st_dev;
  result->st_ino = stat->st_ino;
  result->st_mode = stat->st_mode;
  result->st_nlink = stat->st_nlink;
  result->st_uid = stat->st_uid;
  result->st_gid = stat->st_gid;
  result->st_rdev = stat->st_rdev;
  result->st_size = (int64_t)stat->st_size;

#ifndef TARGET_POSIX
  result->st_atime = (long)(stat->st_atime & 0xFFFFFFFF);
  result->st_mtime = (long)(stat->st_mtime & 0xFFFFFFFF);
  result->st_ctime = (long)(stat->st_ctime & 0xFFFFFFFF);
#else
  result->_st_atime = (long)(stat->st_atime & 0xFFFFFFFF);
  result->_st_mtime = (long)(stat->st_mtime & 0xFFFFFFFF);
  result->_st_ctime = (long)(stat->st_ctime & 0xFFFFFFFF);
#endif
}

void CUtil::Stat64ToStatI64(struct _stati64 *result, struct __stat64 *stat)
{
  result->st_dev = stat->st_dev;
  result->st_ino = stat->st_ino;
  result->st_mode = stat->st_mode;
  result->st_nlink = stat->st_nlink;
  result->st_uid = stat->st_uid;
  result->st_gid = stat->st_gid;
  result->st_rdev = stat->st_rdev;
  result->st_size = stat->st_size;
#ifndef TARGET_POSIX
  result->st_atime = (long)(stat->st_atime & 0xFFFFFFFF);
  result->st_mtime = (long)(stat->st_mtime & 0xFFFFFFFF);
  result->st_ctime = (long)(stat->st_ctime & 0xFFFFFFFF);
#else
  result->_st_atime = (long)(stat->st_atime & 0xFFFFFFFF);
  result->_st_mtime = (long)(stat->st_mtime & 0xFFFFFFFF);
  result->_st_ctime = (long)(stat->st_ctime & 0xFFFFFFFF);
#endif
}

void CUtil::StatI64ToStat64(struct __stat64 *result, struct _stati64 *stat)
{
  result->st_dev = stat->st_dev;
  result->st_ino = stat->st_ino;
  result->st_mode = stat->st_mode;
  result->st_nlink = stat->st_nlink;
  result->st_uid = stat->st_uid;
  result->st_gid = stat->st_gid;
  result->st_rdev = stat->st_rdev;
  result->st_size = stat->st_size;
#ifndef TARGET_POSIX
  result->st_atime = stat->st_atime;
  result->st_mtime = stat->st_mtime;
  result->st_ctime = stat->st_ctime;
#else
  result->st_atime = stat->_st_atime;
  result->st_mtime = stat->_st_mtime;
  result->st_ctime = stat->_st_ctime;
#endif
}

void CUtil::StatToStat64(struct __stat64 *result, const struct stat *stat)
{
  memset(result, 0, sizeof(*result));
  result->st_dev   = stat->st_dev;
  result->st_ino   = stat->st_ino;
  result->st_mode  = stat->st_mode;
  result->st_nlink = stat->st_nlink;
  result->st_uid   = stat->st_uid;
  result->st_gid   = stat->st_gid;
  result->st_rdev  = stat->st_rdev;
  result->st_size  = stat->st_size;
  result->st_atime = stat->st_atime;
  result->st_mtime = stat->st_mtime;
  result->st_ctime = stat->st_ctime;
}

void CUtil::Stat64ToStat(struct stat *result, struct __stat64 *stat)
{
  result->st_dev = stat->st_dev;
  result->st_ino = stat->st_ino;
  result->st_mode = stat->st_mode;
  result->st_nlink = stat->st_nlink;
  result->st_uid = stat->st_uid;
  result->st_gid = stat->st_gid;
  result->st_rdev = stat->st_rdev;
#ifndef TARGET_POSIX
  if (stat->st_size <= LONG_MAX)
    result->st_size = (_off_t)stat->st_size;
#else
  if (sizeof(stat->st_size) <= sizeof(result->st_size) )
    result->st_size = stat->st_size;
#endif
  else
  {
    result->st_size = 0;
    CLog::Log(LOGWARNING, "WARNING: File is larger than 32bit stat can handle, file size will be reported as 0 bytes");
  }
  result->st_atime = (time_t)(stat->st_atime & 0xFFFFFFFF);
  result->st_mtime = (time_t)(stat->st_mtime & 0xFFFFFFFF);
  result->st_ctime = (time_t)(stat->st_ctime & 0xFFFFFFFF);
}

#ifdef TARGET_WINDOWS
void CUtil::Stat64ToStat64i32(struct _stat64i32 *result, struct __stat64 *stat)
{
  result->st_dev = stat->st_dev;
  result->st_ino = stat->st_ino;
  result->st_mode = stat->st_mode;
  result->st_nlink = stat->st_nlink;
  result->st_uid = stat->st_uid;
  result->st_gid = stat->st_gid;
  result->st_rdev = stat->st_rdev;
#ifndef TARGET_POSIX
  if (stat->st_size <= LONG_MAX)
    result->st_size = (_off_t)stat->st_size;
#else
  if (sizeof(stat->st_size) <= sizeof(result->st_size) )
    result->st_size = stat->st_size;
#endif
  else
  {
    result->st_size = 0;
    CLog::Log(LOGWARNING, "WARNING: File is larger than 32bit stat can handle, file size will be reported as 0 bytes");
  }
#ifndef TARGET_POSIX
  result->st_atime = stat->st_atime;
  result->st_mtime = stat->st_mtime;
  result->st_ctime = stat->st_ctime;
#else
  result->st_atime = stat->_st_atime;
  result->st_mtime = stat->_st_mtime;
  result->st_ctime = stat->_st_ctime;
#endif
}
#endif

bool CUtil::CreateDirectoryEx(const std::string& strPath)
{
  // Function to create all directories at once instead
  // of calling CreateDirectory for every subdir.
  // Creates the directory and subdirectories if needed.

  // return true if directory already exist
  if (CDirectory::Exists(strPath)) return true;

  // we currently only allow HD and smb and nfs paths
  if (!URIUtils::IsHD(strPath) && !URIUtils::IsSmb(strPath) && !URIUtils::IsNfs(strPath))
  {
    CLog::Log(LOGERROR, "{} called with an unsupported path: {}", __FUNCTION__, strPath);
    return false;
  }

  std::vector<std::string> dirs = URIUtils::SplitPath(strPath);
  if (dirs.empty())
    return false;
  std::string dir(dirs.front());
  URIUtils::AddSlashAtEnd(dir);
  for (std::vector<std::string>::const_iterator it = dirs.begin() + 1; it != dirs.end(); ++it)
  {
    dir = URIUtils::AddFileToFolder(dir, *it);
    CDirectory::Create(dir);
  }

  // was the final destination directory successfully created ?
  return CDirectory::Exists(strPath);
}

std::string CUtil::MakeLegalFileName(std::string strFile, LegalPath LegalType)
{
  StringUtils::Replace(strFile, '/', '_');
  StringUtils::Replace(strFile, '\\', '_');
  StringUtils::Replace(strFile, '?', '_');

  if (LegalType == LegalPath::WIN32_COMPAT)
  {
    // just filter out some illegal characters on windows
    StringUtils::Replace(strFile, ':', '_');
    StringUtils::Replace(strFile, '*', '_');
    StringUtils::Replace(strFile, '?', '_');
    StringUtils::Replace(strFile, '\"', '_');
    StringUtils::Replace(strFile, '<', '_');
    StringUtils::Replace(strFile, '>', '_');
    StringUtils::Replace(strFile, '|', '_');
    StringUtils::TrimRight(strFile, ". ");
  }
  return strFile;
}

// legalize entire path
std::string CUtil::MakeLegalPath(const std::string& strPathAndFile, LegalPath LegalType)
{
  if (URIUtils::IsStack(strPathAndFile))
    return MakeLegalPath(CStackDirectory::GetFirstStackedFile(strPathAndFile));
  if (URIUtils::IsMultiPath(strPathAndFile))
    return MakeLegalPath(CMultiPathDirectory::GetFirstPath(strPathAndFile));
  if (!URIUtils::IsHD(strPathAndFile) && !URIUtils::IsSmb(strPathAndFile) && !URIUtils::IsNfs(strPathAndFile))
    return strPathAndFile; // we don't support writing anywhere except HD, SMB and NFS - no need to legalize path

  bool trailingSlash = URIUtils::HasSlashAtEnd(strPathAndFile);
  std::vector<std::string> dirs = URIUtils::SplitPath(strPathAndFile);
  if (dirs.empty())
    return strPathAndFile;
  // we just add first token to path and don't legalize it - possible values:
  // "X:" (local win32), "" (local unix - empty string before '/') or
  // "protocol://domain"
  std::string dir(dirs.front());
  URIUtils::AddSlashAtEnd(dir);
  for (std::vector<std::string>::const_iterator it = dirs.begin() + 1; it != dirs.end(); ++it)
    dir = URIUtils::AddFileToFolder(dir, MakeLegalFileName(*it, LegalType));
  if (trailingSlash) URIUtils::AddSlashAtEnd(dir);
  return dir;
}

std::string CUtil::ValidatePath(std::string path, bool bFixDoubleSlashes /* = false */)
{

  // Don't do any stuff on URLs containing %-characters or protocols that embed
  // filenames. NOTE: Don't use IsInZip or IsInRar here since it will infinitely
  // recurse and crash XBMC
  if (URIUtils::IsURL(path) &&
      (path.find('%') != std::string::npos ||
      StringUtils::StartsWithNoCase(path, "apk:") ||
      StringUtils::StartsWithNoCase(path, "zip:") ||
      StringUtils::StartsWithNoCase(path, "rar:") ||
      StringUtils::StartsWithNoCase(path, "stack:") ||
      StringUtils::StartsWithNoCase(path, "bluray:") ||
      StringUtils::StartsWithNoCase(path, "multipath:") ))
    return path;

    // check the path for incorrect slashes
#ifdef TARGET_WINDOWS
  if (URIUtils::IsDOSPath(path))
  {
    StringUtils::Replace(path, '/', '\\');
    /* The double slash correction should only be used when *absolutely*
       necessary! This applies to certain DLLs or use from Python DLLs/scripts
       that incorrectly generate double (back) slashes.
    */
    if (bFixDoubleSlashes && !path.empty())
    {
      // Fixup for double back slashes (but ignore the \\ of unc-paths)
      for (size_t x = 1; x < path.size() - 1; x++)
      {
        if (path[x] == '\\' && path[x + 1] == '\\')
          path.erase(x, 1);
      }
    }
  }
  else if (path.find("://") != std::string::npos || path.find(":\\\\") != std::string::npos)
#endif
  {
    if (!URIUtils::IsDOSPath(path))
      StringUtils::Replace(path, '\\', '/');
    /* The double slash correction should only be used when *absolutely*
       necessary! This applies to certain DLLs or use from Python DLLs/scripts
       that incorrectly generate double (back) slashes.
    */
    if (bFixDoubleSlashes && !path.empty())
    {
      // Fixup for double forward slashes(/) but don't touch the :// of URLs
      for (size_t x = 2; x < path.size() - 1; x++)
      {
        if (path[x] == '/' && path[x + 1] == '/' &&
            !(path[x - 1] == ':' || (path[x - 1] == '/' && path[x - 2] == ':')))
          path.erase(x, 1);
      }
    }
  }
  return path;
}

void CUtil::SplitParams(const std::string &paramString, std::vector<std::string> &parameters)
{
  bool inQuotes = false;
  bool lastEscaped = false; // only every second character can be escaped
  int inFunction = 0;
  size_t whiteSpacePos = 0;
  std::string parameter;
  parameters.clear();
  for (size_t pos = 0; pos < paramString.size(); pos++)
  {
    char ch = paramString[pos];
    bool escaped = (pos > 0 && paramString[pos - 1] == '\\' && !lastEscaped);
    lastEscaped = escaped;
    if (inQuotes)
    { // if we're in a quote, we accept everything until the closing quote
      if (ch == '"' && !escaped)
      { // finished a quote - no need to add the end quote to our string
        inQuotes = false;
      }
    }
    else
    { // not in a quote, so check if we should be starting one
      if (ch == '"' && !escaped)
      { // start of quote - no need to add the quote to our string
        inQuotes = true;
      }
      if (inFunction && ch == ')')
      { // end of a function
        inFunction--;
      }
      if (ch == '(')
      { // start of function
        inFunction++;
      }
      if (!inFunction && ch == ',')
      { // not in a function, so a comma signifies the end of this parameter
        if (whiteSpacePos)
          parameter.resize(whiteSpacePos);
        // trim off start and end quotes
        if (parameter.length() > 1 && parameter[0] == '"' && parameter[parameter.length() - 1] == '"')
          parameter = parameter.substr(1, parameter.length() - 2);
        else if (parameter.length() > 3 && parameter[parameter.length() - 1] == '"')
        {
          // check name="value" style param.
          size_t quotaPos = parameter.find('"');
          if (quotaPos > 1 && quotaPos < parameter.length() - 1 && parameter[quotaPos - 1] == '=')
          {
            parameter.erase(parameter.length() - 1);
            parameter.erase(quotaPos, 1);
          }
        }
        parameters.push_back(parameter);
        parameter.clear();
        whiteSpacePos = 0;
        continue;
      }
    }
    if ((ch == '"' || ch == '\\') && escaped)
    { // escaped quote or backslash
      parameter[parameter.size()-1] = ch;
      continue;
    }
    // whitespace handling - we skip any whitespace at the left or right of an unquoted parameter
    if (ch == ' ' && !inQuotes)
    {
      if (parameter.empty()) // skip whitespace on left
        continue;
      if (!whiteSpacePos) // make a note of where whitespace starts on the right
        whiteSpacePos = parameter.size();
    }
    else
      whiteSpacePos = 0;
    parameter += ch;
  }
  if (inFunction || inQuotes)
    CLog::Log(LOGWARNING, "{}({}) - end of string while searching for ) or \"", __FUNCTION__,
              paramString);
  if (whiteSpacePos)
    parameter.erase(whiteSpacePos);
  // trim off start and end quotes
  if (parameter.size() > 1 && parameter[0] == '"' && parameter[parameter.size() - 1] == '"')
    parameter = parameter.substr(1,parameter.size() - 2);
  else if (parameter.size() > 3 && parameter[parameter.size() - 1] == '"')
  {
    // check name="value" style param.
    size_t quotaPos = parameter.find('"');
    if (quotaPos > 1 && quotaPos < parameter.length() - 1 && parameter[quotaPos - 1] == '=')
    {
      parameter.erase(parameter.length() - 1);
      parameter.erase(quotaPos, 1);
    }
  }
  if (!parameter.empty() || !parameters.empty())
    parameters.push_back(parameter);
}

int CUtil::GetMatchingSource(const std::string& strPath1,
                             std::vector<CMediaSource>& sources,
                             bool& bIsSourceName)
{
  if (strPath1.empty())
    return -1;

  // copy as we may change strPath
  std::string strPath = strPath1;

  // Check for special protocols
  CURL checkURL(strPath);

  if (StringUtils::StartsWith(strPath, "special://skin/"))
    return 1;

  // do not return early if URL protocol is "plugin"
  // since video- and/or audio-plugins can be configured as mediasource

  // stack://
  if (checkURL.IsProtocol("stack"))
    strPath = CStackDirectory::GetBasePath(checkURL.Get());

  // bluray://
  if (checkURL.IsProtocol("bluray"))
    strPath = URIUtils::GetDiscBase(checkURL.Get()); // get the actual path on disc

  if (checkURL.IsProtocol("shout"))
    strPath = checkURL.GetHostName();

  if (URIUtils::IsArchive(checkURL))
    strPath = checkURL.GetHostName();

  if (checkURL.IsProtocol("multipath"))
    strPath = CMultiPathDirectory::GetFirstPath(strPath);

  bIsSourceName = false;
  int iIndex = -1;

  // we first test the NAME of a source
  for (int i = 0; i < static_cast<int>(sources.size()); ++i)
  {
    const CMediaSource& share = sources[i];
    std::string strName = share.strName;

    // special cases for dvds
    if (URIUtils::IsOnDVD(share.strPath))
    {
      if (URIUtils::IsOnDVD(strPath))
        return i;

      // not a path, so we need to modify the source name
      // since we add the drive status and disc name to the source
      // "Name (Drive Status/Disc Name)"
      size_t iPos = strName.rfind('(');
      if (iPos != std::string::npos && iPos > 1)
        strName.resize(iPos - 1);
    }
    if (StringUtils::EqualsNoCase(strPath, strName))
    {
      bIsSourceName = true;
      return i;
    }
  }

  // now test the paths

  // remove user details, and ensure path only uses forward slashes
  // and ends with a trailing slash so as not to match a substring
  CURL urlDest(strPath);
  urlDest.SetOptions("");
  urlDest.SetProtocolOptions("");
  std::string strDest = urlDest.GetWithoutUserDetails();
  ForceForwardSlashes(strDest);
  if (!URIUtils::HasSlashAtEnd(strDest))
    strDest += "/";

  size_t iLength = 0;
  size_t iLenPath = strDest.size();
  for (int i = 0; i < static_cast<int>(sources.size()); ++i)
  {
    const CMediaSource& share = sources[i];

    // does it match a source name?
    if (share.strPath.substr(0,8) == "shout://")
    {
      CURL url(share.strPath);
      if (strPath == url.GetHostName())
        return i;
    }

    // doesn't match a name, so try the source path
    std::vector<std::string> vecPaths;

    // add any concatenated paths if they exist
    if (!share.vecPaths.empty())
      vecPaths = share.vecPaths;

    // add the actual share path at the front of the vector
    vecPaths.insert(vecPaths.begin(), share.strPath);

    // test each path
    for (const auto &path : vecPaths)
    {
      // remove user details, and ensure path only uses forward slashes
      // and ends with a trailing slash so as not to match a substring
      CURL urlShare(path);
      urlShare.SetOptions("");
      urlShare.SetProtocolOptions("");
      std::string strShare = urlShare.GetWithoutUserDetails();
      ForceForwardSlashes(strShare);
      if (!URIUtils::HasSlashAtEnd(strShare))
        strShare += "/";
      size_t iLenShare = strShare.size();

      if ((iLenPath >= iLenShare) && StringUtils::StartsWithNoCase(strDest, strShare) && (iLenShare > iLength))
      {
        // if exact match, return it immediately
        if (iLenPath == iLenShare)
        {
          // if the path EXACTLY matches an item in a concatenated path
          // set source name to true to load the full virtualpath
          bIsSourceName = false;
          if (vecPaths.size() > 1)
            bIsSourceName = true;
          return i;
        }
        iIndex = i;
        iLength = iLenShare;
      }
    }
  }

  // return the index of the share with the longest match
  if (iIndex == -1)
  {

    // rar:// and zip://
    // if archive wasn't mounted, look for a matching share for the archive instead
    if( StringUtils::StartsWithNoCase(strPath, "rar://") || StringUtils::StartsWithNoCase(strPath, "zip://") )
    {
      // get the hostname portion of the url since it contains the archive file
      strPath = checkURL.GetHostName();

      bIsSourceName = false;
      bool bDummy;
      return GetMatchingSource(strPath, sources, bDummy);
    }

    CLog::Log(LOGDEBUG, "CUtil::GetMatchingSource: no matching source found for [{}]", strPath1);
  }
  return iIndex;
}

std::string CUtil::TranslateSpecialSource(const std::string &strSpecial)
{
  if (!strSpecial.empty() && strSpecial[0] == '$')
  {
    if (StringUtils::StartsWithNoCase(strSpecial, "$home"))
      return URIUtils::AddFileToFolder("special://home/", strSpecial.substr(5));
    else if (StringUtils::StartsWithNoCase(strSpecial, "$subtitles"))
      return URIUtils::AddFileToFolder("special://subtitles/", strSpecial.substr(10));
    else if (StringUtils::StartsWithNoCase(strSpecial, "$userdata"))
      return URIUtils::AddFileToFolder("special://userdata/", strSpecial.substr(9));
    else if (StringUtils::StartsWithNoCase(strSpecial, "$database"))
      return URIUtils::AddFileToFolder("special://database/", strSpecial.substr(9));
    else if (StringUtils::StartsWithNoCase(strSpecial, "$thumbnails"))
      return URIUtils::AddFileToFolder("special://thumbnails/", strSpecial.substr(11));
    else if (StringUtils::StartsWithNoCase(strSpecial, "$recordings"))
      return URIUtils::AddFileToFolder("special://recordings/", strSpecial.substr(11));
    else if (StringUtils::StartsWithNoCase(strSpecial, "$screenshots"))
      return URIUtils::AddFileToFolder("special://screenshots/", strSpecial.substr(12));
    else if (StringUtils::StartsWithNoCase(strSpecial, "$musicplaylists"))
      return URIUtils::AddFileToFolder("special://musicplaylists/", strSpecial.substr(15));
    else if (StringUtils::StartsWithNoCase(strSpecial, "$videoplaylists"))
      return URIUtils::AddFileToFolder("special://videoplaylists/", strSpecial.substr(15));
    else if (StringUtils::StartsWithNoCase(strSpecial, "$cdrips"))
      return URIUtils::AddFileToFolder("special://cdrips/", strSpecial.substr(7));
    // this one will be removed post 2.0
    else if (StringUtils::StartsWithNoCase(strSpecial, "$playlists"))
      return URIUtils::AddFileToFolder(CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH), strSpecial.substr(10));
  }
  return strSpecial;
}

std::string CUtil::MusicPlaylistsLocation()
{
  const std::string path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH);
  std::vector<std::string> vec;
  vec.push_back(URIUtils::AddFileToFolder(path, "music"));
  vec.push_back(URIUtils::AddFileToFolder(path, "mixed"));
  return XFILE::CMultiPathDirectory::ConstructMultiPath(vec);
}

std::string CUtil::VideoPlaylistsLocation()
{
  const std::string path = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SYSTEM_PLAYLISTSPATH);
  std::vector<std::string> vec;
  vec.push_back(URIUtils::AddFileToFolder(path, "video"));
  vec.push_back(URIUtils::AddFileToFolder(path, "mixed"));
  return XFILE::CMultiPathDirectory::ConstructMultiPath(vec);
}

void CUtil::DeleteMusicDatabaseDirectoryCache()
{
  CUtil::DeleteDirectoryCache("mdb-");
  CUtil::DeleteDirectoryCache("sp-"); // overkill as it will delete video smartplaylists, but as we can't differentiate based on URL...
}

void CUtil::DeleteVideoDatabaseDirectoryCache()
{
  CUtil::DeleteDirectoryCache("vdb-");
  CUtil::DeleteDirectoryCache("sp-"); // overkill as it will delete music smartplaylists, but as we can't differentiate based on URL...
}

void CUtil::DeleteDirectoryCache(const std::string &prefix)
{
  std::string searchPath = "special://temp/archive_cache/";
  CFileItemList items;
  if (!XFILE::CDirectory::GetDirectory(searchPath, items, ".fi", DIR_FLAG_NO_FILE_DIRS))
    return;

  for (const auto &item : items)
  {
    if (item->IsFolder())
      continue;
    std::string fileName = URIUtils::GetFileName(item->GetPath());
    if (StringUtils::StartsWith(fileName, prefix))
      XFILE::CFile::Delete(item->GetPath());
  }
}


void CUtil::GetRecursiveListing(const std::string& strPath, CFileItemList& items, const std::string& strMask, unsigned int flags /* = DIR_FLAG_DEFAULTS */)
{
  CFileItemList myItems;
  CDirectory::GetDirectory(strPath,myItems,strMask,flags);
  for (const auto &item : myItems)
  {
    if (item->IsFolder())
      CUtil::GetRecursiveListing(item->GetPath(),items,strMask,flags);
    else
      items.Add(item);
  }
}

void CUtil::GetRecursiveDirsListing(const std::string& strPath, CFileItemList& item, unsigned int flags /* = DIR_FLAG_DEFAULTS */)
{
  CFileItemList myItems;
  CDirectory::GetDirectory(strPath,myItems,"",flags);
  for (const auto &i : myItems)
  {
    if (i->IsFolder() && !i->IsPath(".."))
    {
      item.Add(i);
      CUtil::GetRecursiveDirsListing(i->GetPath(),item,flags);
    }
  }
}

void CUtil::ForceForwardSlashes(std::string& strPath)
{
  size_t iPos = strPath.rfind('\\');
  while (iPos != std::string::npos)
  {
    strPath.at(iPos) = '/';
    iPos = strPath.rfind('\\');
  }
}

double CUtil::AlbumRelevance(const std::string& strAlbumTemp1, const std::string& strAlbum1, const std::string& strArtistTemp1, const std::string& strArtist1)
{
  // case-insensitive fuzzy string comparison on the album and artist for relevance
  // weighting is identical, both album and artist are 50% of the total relevance
  // a missing artist means the maximum relevance can only be 0.50
  std::string strAlbumTemp = strAlbumTemp1;
  StringUtils::ToLower(strAlbumTemp);
  std::string strAlbum = strAlbum1;
  StringUtils::ToLower(strAlbum);
  double fAlbumPercentage = fstrcmp(strAlbumTemp.c_str(), strAlbum.c_str());
  double fArtistPercentage = 0.0;
  if (!strArtist1.empty())
  {
    std::string strArtistTemp = strArtistTemp1;
    StringUtils::ToLower(strArtistTemp);
    std::string strArtist = strArtist1;
    StringUtils::ToLower(strArtist);
    fArtistPercentage = fstrcmp(strArtistTemp.c_str(), strArtist.c_str());
  }
  double fRelevance = fAlbumPercentage * 0.5 + fArtistPercentage * 0.5;
  return fRelevance;
}

bool CUtil::MakeShortenPath(std::string StrInput, std::string& StrOutput, size_t iTextMaxLength)
{
  size_t iStrInputSize = StrInput.size();
  if(iStrInputSize <= 0 || iTextMaxLength >= iStrInputSize)
  {
    StrOutput = StrInput;
    return true;
  }

  char cDelim = '\0';
  size_t nGreaterDelim, nPos;

  nPos = StrInput.find_last_of( '\\' );
  if (nPos != std::string::npos)
    cDelim = '\\';
  else
  {
    nPos = StrInput.find_last_of( '/' );
    if (nPos != std::string::npos)
      cDelim = '/';
  }
  if ( cDelim == '\0' )
    return false;

  if (nPos == StrInput.size() - 1)
  {
    StrInput.erase(StrInput.size() - 1);
    nPos = StrInput.find_last_of(cDelim);
  }
  while( iTextMaxLength < iStrInputSize )
  {
    nPos = StrInput.find_last_of( cDelim, nPos );
    nGreaterDelim = nPos;

    if (nPos == std::string::npos || nPos == 0)
      break;

    nPos = StrInput.find_last_of( cDelim, nPos - 1 );

    if ( nPos == std::string::npos )
      break;
    if ( nGreaterDelim > nPos )
      StrInput.replace( nPos + 1, nGreaterDelim - nPos - 1, ".." );
    iStrInputSize = StrInput.size();
  }
  // replace any additional /../../ with just /../ if necessary
  std::string replaceDots = StringUtils::Format("..{}..", cDelim);
  while (StrInput.size() > (unsigned int)iTextMaxLength)
    if (!StringUtils::Replace(StrInput, replaceDots, ".."))
      break;
  // finally, truncate our string to force inside our max text length,
  // replacing the last 2 characters with ".."

  // eg end up with:
  // "smb://../Playboy Swimsuit Cal.."
  if (iTextMaxLength > 2 && StrInput.size() > (unsigned int)iTextMaxLength)
  {
    StrInput.erase(iTextMaxLength - 2);
    StrInput += "..";
  }
  StrOutput = StrInput;
  return true;
}

bool CUtil::SupportsWriteFileOperations(const std::string& strPath)
{
  // currently only hd, smb, nfs and dav support delete and rename
  if (URIUtils::IsHD(strPath))
    return true;
  if (URIUtils::IsSmb(strPath))
    return true;
  if (URIUtils::IsPVRRecording(strPath))
    return CPVRDirectory::SupportsWriteFileOperations(strPath);
  if (URIUtils::IsNfs(strPath))
    return true;
  if (URIUtils::IsDAV(strPath))
    return true;
  if (URIUtils::IsStack(strPath))
    return SupportsWriteFileOperations(CStackDirectory::GetFirstStackedFile(strPath));
  if (URIUtils::IsMultiPath(strPath))
    return CMultiPathDirectory::SupportsWriteFileOperations(strPath);

  if (CServiceBroker::IsAddonInterfaceUp())
  {
    return std::ranges::any_of(CServiceBroker::GetVFSAddonCache().GetAddonInstances(),
                               [url = CURL(strPath)](const auto& addon)
                               {
                                 const auto& info = addon->GetProtocolInfo();
                                 auto prots = StringUtils::Split(info.type, "|");
                                 return info.supportWrite &&
                                        std::ranges::find(prots, url.GetProtocol()) != prots.end();
                               });
  }

  return false;
}

bool CUtil::SupportsReadFileOperations(const std::string& strPath)
{
  return !URIUtils::IsVideoDb(strPath);
}

std::string CUtil::GetDefaultFolderThumb(const std::string &folderThumb)
{
  if (CServiceBroker::GetGUI()->GetTextureManager().HasTexture(folderThumb))
    return folderThumb;
  return "";
}

void CUtil::GetSkinThemes(std::vector<std::string>& vecTheme)
{
  static const std::string TexturesXbt = "Textures.xbt";

  std::string strPath = URIUtils::AddFileToFolder(CServiceBroker::GetWinSystem()->GetGfxContext().GetMediaDir(), "media");
  CFileItemList items;
  CDirectory::GetDirectory(strPath, items, "", DIR_FLAG_DEFAULTS);
  // Search for Themes in the Current skin!
  for (const auto &pItem : items)
  {
    if (!pItem->IsFolder())
    {
      std::string strExtension = URIUtils::GetExtension(pItem->GetPath());
      std::string strLabel = pItem->GetLabel();
      if ((strExtension == ".xbt" && !StringUtils::EqualsNoCase(strLabel, TexturesXbt)))
        vecTheme.push_back(StringUtils::Left(strLabel, strLabel.size() - strExtension.size()));
    }
    else
    {
      // check if this is an xbt:// VFS path
      CURL itemUrl(pItem->GetPath());
      if (!itemUrl.IsProtocol("xbt") || !itemUrl.GetFileName().empty())
        continue;

      std::string strLabel = URIUtils::GetFileName(itemUrl.GetHostName());
      if (!StringUtils::EqualsNoCase(strLabel, TexturesXbt))
        vecTheme.push_back(StringUtils::Left(strLabel, strLabel.size() - URIUtils::GetExtension(strLabel).size()));
    }
  }
  std::sort(vecTheme.begin(), vecTheme.end(), sortstringbyname());
}

void CUtil::InitRandomSeed()
{
  // Init random seed
  auto now = std::chrono::steady_clock::now();
  auto seed = now.time_since_epoch();

  srand(static_cast<unsigned int>(seed.count()));
}

#if defined(TARGET_POSIX) && !defined(TARGET_DARWIN_TVOS)
bool CUtil::RunCommandLine(const std::string& cmdLine, bool waitExit)
{
  std::vector<std::string> args = StringUtils::Split(cmdLine, ",");

  // Strip quotes and whitespace around the arguments, or exec will fail.
  // This allows the python invocation to be written more naturally with any amount of whitespace around the args.
  // But it's still limited, for example quotes inside the strings are not expanded, etc.
  //! @todo Maybe some python library routine can parse this more properly ?
  for (std::vector<std::string>::iterator it = args.begin(); it != args.end(); ++it)
  {
    size_t pos;
    pos = it->find_first_not_of(" \t\n\"'");
    if (pos != std::string::npos)
    {
      it->erase(0, pos);
    }

    pos = it->find_last_not_of(" \t\n\"'"); // if it returns npos we'll end up with an empty string which is OK
    {
      it->erase(++pos, it->size());
    }
  }

  return Command(args, waitExit);
}

bool CUtil::Command(const std::vector<std::string>& arrArgs, bool waitExit)
{
#ifdef _DEBUG
  printf("Executing: ");
  for (const auto &arg : arrArgs)
    printf("%s ", arg.c_str());
  printf("\n");
#endif

  pid_t child = fork();
  int n = 0;
  if (child == 0)
  {
    if (!waitExit)
    {
      // fork again in order not to leave a zombie process
      child = fork();
      if (child == -1)
        _exit(2);
      else if (child != 0)
        _exit(0);
    }
    close(0);
    close(1);
    close(2);
    if (!arrArgs.empty())
    {
      char **args = (char **)alloca(sizeof(char *) * (arrArgs.size() + 3));
      memset(args, 0, (sizeof(char *) * (arrArgs.size() + 3)));
      for (size_t i=0; i<arrArgs.size(); i++)
        args[i] = const_cast<char *>(arrArgs[i].c_str());
      execvp(args[0], args);
    }
  }
  else
  {
    waitpid(child, &n, 0);
  }

  return (waitExit) ? (WEXITSTATUS(n) == 0) : true;
}
#endif

int CUtil::LookupRomanDigit(char roman_digit)
{
  switch (roman_digit)
  {
    case 'i':
    case 'I':
      return 1;
    case 'v':
    case 'V':
      return 5;
    case 'x':
    case 'X':
      return 10;
    case 'l':
    case 'L':
      return 50;
    case 'c':
    case 'C':
      return 100;
    case 'd':
    case 'D':
      return 500;
    case 'm':
    case 'M':
      return 1000;
    default:
      return 0;
  }
}

int CUtil::TranslateRomanNumeral(const char* roman_numeral)
{

  int decimal = -1;

  if (roman_numeral && roman_numeral[0])
  {
    int temp_sum  = 0,
        last      = 0,
        repeat    = 0,
        trend     = 1;
    decimal = 0;
    while (*roman_numeral)
    {
      int digit = CUtil::LookupRomanDigit(*roman_numeral);
      int test  = last;

      // General sanity checks

      // numeral not in LUT
      if (!digit)
        return -1;

      while (test > 5)
        test /= 10;

      // N = 10^n may not precede (N+1) > 10^(N+1)
      if (test == 1 && digit > last * 10)
        return -1;

      // N = 5*10^n may not precede (N+1) >= N
      if (test == 5 && digit >= last)
        return -1;

      // End general sanity checks

      if (last < digit)
      {
        // smaller numerals may not repeat before a larger one
        if (repeat)
          return -1;

        temp_sum += digit;

        repeat  = 0;
        trend   = 0;
      }
      else if (last == digit)
      {
        temp_sum += digit;
        repeat++;
        trend = 1;
      }
      else
      {
        if (!repeat)
          decimal += 2 * last - temp_sum;
        else
          decimal += temp_sum;

        temp_sum = digit;

        trend   = 1;
        repeat  = 0;
      }
      // Post general sanity checks

      // numerals may not repeat more than thrice
      if (repeat == 3)
        return -1;

      last = digit;
      roman_numeral++;
    }

    if (trend)
      decimal += temp_sum;
    else
      decimal += 2 * last - temp_sum;
  }
  return decimal;
}

std::string CUtil::ResolveExecutablePath()
{
  std::string strExecutablePath;
#ifdef TARGET_WINDOWS
  static const size_t bufSize = MAX_PATH * 2;
  wchar_t* buf = new wchar_t[bufSize];
  buf[0] = 0;
  ::GetModuleFileNameW(0, buf, bufSize);
  buf[bufSize-1] = 0;
  g_charsetConverter.wToUTF8(buf,strExecutablePath);
  delete[] buf;
#elif defined(TARGET_DARWIN)
  char     given_path[2*MAXPATHLEN];
  size_t path_size =2*MAXPATHLEN;

  CDarwinUtils::GetExecutablePath(given_path, &path_size);
  strExecutablePath = given_path;
#elif defined(TARGET_FREEBSD)
  char buf[PATH_MAX];
  size_t buflen;
  int mib[4];

  mib[0] = CTL_KERN;
  mib[1] = KERN_PROC;
  mib[2] = KERN_PROC_PATHNAME;
  mib[3] = getpid();

  buflen = sizeof(buf) - 1;
  if(sysctl(mib, 4, buf, &buflen, NULL, 0) < 0)
    strExecutablePath = "";
  else
    strExecutablePath = buf;
#elif defined(TARGET_ANDROID)
  strExecutablePath = CXBMCApp::getApplicationInfo().nativeLibraryDir;

  std::string appName = CCompileInfo::GetAppName();
  std::string libName = "lib" + appName + ".so";
  StringUtils::ToLower(libName);
  strExecutablePath += "/" + libName;
#else
  /* Get our PID and build the name of the link in /proc */
  pid_t pid = getpid();
  char linkname[64]; /* /proc/<pid>/exe */
  snprintf(linkname, sizeof(linkname), "/proc/%i/exe", pid);

  /* Now read the symbolic link */
  char buf[PATH_MAX + 1];
  buf[0] = 0;

  int ret = readlink(linkname, buf, sizeof(buf) - 1);
  if (ret != -1)
    buf[ret] = 0;

  strExecutablePath = buf;
#endif
  return strExecutablePath;
}

std::string CUtil::GetFrameworksPath(bool forPython)
{
  std::string strFrameworksPath;
#if defined(TARGET_DARWIN)
  strFrameworksPath = CDarwinUtils::GetFrameworkPath(forPython);
#endif
  return strFrameworksPath;
}

// Used to determine external audio and subtitles location and filename
void CUtil::GetVideoBasePathAndFileName(const std::string& videoPath,
                                        std::string& basePath,
                                        std::string& videoFileName)
{
  if (URIUtils::IsOpticalMediaFile(videoPath) || URIUtils::IsBlurayPath(videoPath))
  {
    const std::string path{URIUtils::IsBlurayPath(videoPath) ? URIUtils::GetDiscFile(videoPath)
                                                             : videoPath};
    CFileItem item(path, false);
    videoFileName = item.GetMovieName();
    basePath = item.GetLocalMetadataPath();
  }
  else if (const CURL url{videoPath}; URIUtils::IsArchive(url))
  {
    CFileItem item(videoPath, false);
    videoFileName = item.GetMovieName();
    basePath = item.GetLocalMetadataPath();
  }
  else if (URIUtils::IsStack(videoPath))
  {
    CFileItem item(videoPath, false);
    videoFileName = item.GetMovieName();
    basePath = URIUtils::GetBasePath(videoPath);
  }
  else
  {
    videoFileName = URIUtils::ReplaceExtension(URIUtils::GetFileName(videoPath), "");
    CURL url(videoPath);
    // Strip URL options (e.g., query/fragment) before computing the parent path
    url.SetOptions("");
    basePath = URIUtils::GetParentPath(url.Get());
  }
}

void CUtil::GetItemsToScan(const std::string& videoPath,
                           const std::string& item_exts,
                           const std::vector<std::string>& sub_dirs,
                           CFileItemList& items)
{
  int flags = DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO;

  if (!videoPath.empty())
    CDirectory::GetDirectory(videoPath, items, item_exts, flags);

  std::vector<std::string> additionalPaths;
  for (const auto &item : items)
  {
    for (const auto& subdir : sub_dirs)
    {
      if (StringUtils::EqualsNoCase(item->GetLabel(), subdir))
        additionalPaths.push_back(item->GetPath());
    }
  }

  for (std::vector<std::string>::const_iterator it = additionalPaths.begin(); it != additionalPaths.end(); ++it)
  {
    CFileItemList moreItems;
    CDirectory::GetDirectory(*it, moreItems, item_exts, flags);
    items.Append(moreItems);
  }
}

static constexpr int NOT_ARCHIVE{-1};

void CUtil::ScanPathsForAssociatedItems(const std::string& videoName,
                                        CFileItemList& items,
                                        const std::vector<std::string>& item_exts,
                                        std::vector<std::string>& associatedFiles)
{
  for (int i = items.Size() - 1; i >= 0; --i)
  {
    const auto& item{items[i]};
    if (item->IsFolder())
      continue;

    std::string strCandidate = URIUtils::GetFileName(item->GetPath());

    // skip duplicates
    if (std::ranges::find(associatedFiles, item->GetPath()) != associatedFiles.end())
      continue;

    URIUtils::RemoveExtension(strCandidate);
    // NOTE: We don't know if one of videoName or strCandidate is URL-encoded and the other is not, so try both
    if (StringUtils::StartsWithNoCase(strCandidate, videoName) ||
        (StringUtils::StartsWithNoCase(strCandidate, CURL::Decode(videoName))))
    {
      if (DetermineArchiveAndScanForAssociatedItems(item->GetPath(), "", item_exts,
                                                    associatedFiles) == NOT_ARCHIVE)
      {
        associatedFiles.push_back(item->GetPath());
        CLog::Log(LOGINFO, "{}: found associated file {}", __FUNCTION__,
                  CURL::GetRedacted(item->GetPath()));
      }
      else
      {
        // Remove the archive itself from the associated file list as its contents were scanned in
        //  DetermineArchiveAndScanForAssociatedItems()
        items.Remove(i);
      }
    }
    else
    {
      DetermineArchiveAndScanForAssociatedItems(item->GetPath(), videoName, item_exts,
                                                associatedFiles);
    }
  }
}

int CUtil::DetermineArchiveAndScanForAssociatedItems(const std::string& path,
                                                     const std::string& videoName,
                                                     const std::vector<std::string>& item_exts,
                                                     std::vector<std::string>& associatedFiles,
                                                     int depth /* = 0 */)
{
  std::string archiveExtensions{CServiceBroker::GetFileExtensionProvider().GetArchiveExtensions()};
  if (!archiveExtensions.empty() && archiveExtensions.back() != '|')
    archiveExtensions += '|';
  const std::string extension{URIUtils::GetExtension(path)};

  if (URIUtils::IsRAR(path) || URIUtils::IsZIP(path) ||
      (!extension.empty() && archiveExtensions.find(extension + "|") != std::string::npos))
  {
    return ScanArchiveForAssociatedItems(path, videoName, item_exts, associatedFiles, depth);
  }
  return NOT_ARCHIVE;
}

int CUtil::ScanArchiveForAssociatedItems(const std::string& strArchivePath,
                                         const std::string& videoNameNoExt,
                                         const std::vector<std::string>& item_exts,
                                         std::vector<std::string>& associatedFiles,
                                         int depth)
{
  CLog::LogF(LOGDEBUG, "Scanning archive {} depth {}", CURL::GetRedacted(strArchivePath), depth);

  if (depth > 2)
  {
    CLog::LogF(LOGDEBUG, "Depth limit exceeded");
    return 0;
  }

  int nItemsAdded = 0;
  CFileItemList ItemList;

  CURL pathToUrl(strArchivePath);
  CURL url;
  if (URIUtils::HasExtension(strArchivePath, ".zip"))
    url = URIUtils::CreateArchivePath("zip", pathToUrl, ""); // zip only gets the root dir
  else if (URIUtils::HasExtension(strArchivePath, ".rar"))
    url = URIUtils::CreateArchivePath("rar", pathToUrl, "");
  else
    url = URIUtils::CreateArchivePath("archive", pathToUrl, "");

  if (!CDirectory::GetDirectory(url, ItemList, "", DIR_FLAG_NO_FILE_DIRS))
  {
    CLog::LogF(LOGDEBUG, "Failed to scan archive {}", CURL::GetRedacted(strArchivePath));
    return 0;
  }

  for (const auto& item : ItemList)
  {
    std::string path{item->GetPath()};
    std::string extension{URIUtils::GetExtension(path)};

    if (const int result{DetermineArchiveAndScanForAssociatedItems(path, videoNameNoExt, item_exts,
                                                                   associatedFiles, depth + 1)};
        result != NOT_ARCHIVE)
    {
      nItemsAdded += result;
      continue;
    }

    // check that the found filename matches the movie filename
    // NOTE: We don't know if videoNameNoExt is URL-encoded, so try both
    if (!videoNameNoExt.empty() &&
        !(StringUtils::StartsWithNoCase(URIUtils::GetFileName(path), videoNameNoExt) ||
          StringUtils::StartsWithNoCase(URIUtils::GetFileName(path), CURL::Decode(videoNameNoExt))))
      continue;

    if (std::ranges::any_of(item_exts, [&](std::string_view ext)
                            { return StringUtils::EqualsNoCase(extension, ext); }))
    {
      CLog::Log(LOGINFO, "{}: found associated file {}", __FUNCTION__, CURL::GetRedacted(path));
      associatedFiles.push_back(path);
      ++nItemsAdded;
    }
  }

  return nItemsAdded;
}

void CUtil::ScanForExternalSubtitles(const std::string& strMovie, std::vector<std::string>& vecSubtitles)
{
  auto start = std::chrono::steady_clock::now();

  CFileItem item(strMovie, false);
  if ((NETWORK::IsInternetStream(item) && !URIUtils::IsOnLAN(item.GetDynPath())) ||
      PLAYLIST::IsPlayList(item) || item.IsPVR() || !VIDEO::IsVideo(item))
    return;

  CLog::Log(LOGDEBUG, "{}: Searching for subtitles...", __FUNCTION__);

  std::string strBasePath;
  std::string strSubtitle;

  GetVideoBasePathAndFileName(strMovie, strBasePath, strSubtitle);

  CFileItemList items;
  const std::vector<std::string> common_sub_dirs = { "subs", "subtitles", "vobsubs", "sub", "vobsub", "subtitle" };
  const std::string subtitleExtensions = CServiceBroker::GetFileExtensionProvider().GetSubtitleExtensions();
  GetItemsToScan(strBasePath, subtitleExtensions, common_sub_dirs, items);

  const std::string customPath = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_SUBTITLES_CUSTOMPATH);

  if (!CMediaSettings::GetInstance().GetAdditionalSubtitleDirectoryChecked() && !customPath.empty()) // to avoid checking non-existent directories (network) every time..
  {
    if (!CServiceBroker::GetNetwork().IsAvailable() && !URIUtils::IsHD(customPath))
    {
      CLog::Log(LOGINFO, "CUtil::CacheSubtitles: disabling alternate subtitle directory for this session, it's inaccessible");
      CMediaSettings::GetInstance().SetAdditionalSubtitleDirectoryChecked(-1); // disabled
    }
    else if (!CDirectory::Exists(customPath))
    {
      CLog::Log(LOGINFO, "CUtil::CacheSubtitles: disabling alternate subtitle directory for this session, it's nonexistent");
      CMediaSettings::GetInstance().SetAdditionalSubtitleDirectoryChecked(-1); // disabled
    }

    CMediaSettings::GetInstance().SetAdditionalSubtitleDirectoryChecked(1);
  }

  std::vector<std::string> strLookInPaths;
  // this is last because we dont want to check any common subdirs or cd-dirs in the alternate <subtitles> dir.
  if (CMediaSettings::GetInstance().GetAdditionalSubtitleDirectoryChecked() == 1)
  {
    std::string strPath2 = customPath;
    URIUtils::AddSlashAtEnd(strPath2);
    strLookInPaths.push_back(strPath2);
  }

  int flags = DIR_FLAG_NO_FILE_DIRS | DIR_FLAG_NO_FILE_INFO;
  for (const std::string& path : strLookInPaths)
  {
    CFileItemList moreItems;
    CDirectory::GetDirectory(path, moreItems, subtitleExtensions, flags);
    items.Append(moreItems);
  }

  std::vector<std::string> subtitleExts{StringUtils::Split(subtitleExtensions, '|')};
  const std::string archiveExtensions{
      CServiceBroker::GetFileExtensionProvider().GetArchiveExtensions()};
  std::vector<std::string> archiveExts{StringUtils::Split(archiveExtensions, '|')};
  std::unordered_set<std::string> archiveSet{archiveExts.begin(), archiveExts.end()};
  std::erase_if(subtitleExts, [&](const std::string& ext) { return archiveSet.contains(ext); });

  ScanPathsForAssociatedItems(strSubtitle, items, subtitleExts, vecSubtitles);

  size_t iSize = vecSubtitles.size();
  for (size_t i = 0; i < iSize; i++)
  {
    if (URIUtils::HasExtension(vecSubtitles[i], ".smi"))
    {
      //Cache multi-language sami subtitle
      CDVDSubtitleStream stream;
      if (stream.Open(vecSubtitles[i]))
      {
        CDVDSubtitleTagSami TagConv;
        TagConv.LoadHead(&stream);
        if (TagConv.m_Langclass.size() >= 2)
        {
          for (const auto &lang : TagConv.m_Langclass)
          {
            std::string strDest =
                StringUtils::Format("special://temp/subtitle.{}.{}.smi", lang.Name, i);
            if (CFile::Copy(vecSubtitles[i], strDest))
            {
              CLog::Log(LOGINFO, " cached subtitle {}->{}", CURL::GetRedacted(vecSubtitles[i]),
                        strDest);
              vecSubtitles.push_back(strDest);
            }
          }
        }
      }
    }
  }

  auto end = std::chrono::steady_clock::now();
  auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
  CLog::Log(LOGDEBUG, "{}: END (total time: {} ms)", __FUNCTION__, duration.count());
}

namespace
{
//! Matched case-insensitively against filename tokens
constexpr auto EXTERNAL_STREAM_FLAG_WORDS = std::array{
    std::pair{std::string_view{"none"}, StreamFlags::FLAG_NONE},
    std::pair{std::string_view{"default"}, StreamFlags::FLAG_DEFAULT},
    std::pair{std::string_view{"forced"}, StreamFlags::FLAG_FORCED},
    std::pair{std::string_view{"original"}, StreamFlags::FLAG_ORIGINAL},
    std::pair{std::string_view{"impaired"}, StreamFlags::FLAG_HEARING_IMPAIRED},
};

/*!
 * \brief The stream flag a filename token states.
 * \note FLAG_NONE is one of the words rather than an absence of one, so that a filename
 *       saying "none" has stated a flag rather than part of the stream's name.
 * \param[in] token One token of the filename.
 * \return The flag, or nullopt where the token states none.
 */
std::optional<StreamFlags> ExternalStreamFlagFromToken(std::string_view token)
{
  const auto it = std::ranges::find_if(EXTERNAL_STREAM_FLAG_WORDS, [&token](const auto& word)
                                       { return StringUtils::EqualsNoCase(token, word.first); });
  if (it == EXTERNAL_STREAM_FLAG_WORDS.end())
    return std::nullopt;

  return it->second;
}

/*!
 * \brief The language a filename token states.
 * \param[in] token One token of the filename.
 * \return The language, or nullopt where the token states none.
 */
std::optional<KODI::UTILS::CLanguageTag> ExternalStreamLanguageFromToken(const std::string& token)
{
  // _ stands in for the BCP 47 subtag separator, since - separates the filename's own tokens
  std::string langCode{token};
  std::ranges::replace(langCode, '_', '-');

  return KODI::UTILS::CLanguageTag::TryParse(langCode);
}
} // namespace

ExternalStreamInfo CUtil::GetExternalStreamDetailsFromFilename(const std::string& videoPath, const std::string& associatedFile)
{
  ExternalStreamInfo info;

  std::string videoBaseName = URIUtils::GetFileName(videoPath);
  URIUtils::RemoveExtension(videoBaseName);

  std::string toParse = URIUtils::GetFileName(associatedFile);
  URIUtils::RemoveExtension(toParse);

  // we check left part - if it's same as video base name - strip it
  if (StringUtils::StartsWithNoCase(toParse, videoBaseName))
    toParse = toParse.substr(videoBaseName.length());
  else if (URIUtils::GetExtension(associatedFile) == ".sub" && URIUtils::IsInArchive(associatedFile))
  {
    // exclude parsing of vobsub file names that are embedded in an archive
    CLog::Log(LOGDEBUG, "{} - skipping archived vobsub filename parsing: {}", __FUNCTION__,
              CURL::GetRedacted(associatedFile));
    toParse.clear();
  }

  // trim any non-alphanumeric char in the beginning
  std::string::iterator result = std::find_if(toParse.begin(), toParse.end(), StringUtils::isasciialphanum);

  std::string name;
  if (result != toParse.end()) // if we have anything to parse
  {
    std::string inputString(result, toParse.end());
    std::string delimiters(" .-");
    std::vector<std::string> tokens;
    StringUtils::Tokenize(inputString, tokens, delimiters);

    // The tokens are read from the end of the filename towards the front, so of several languages
    // the one nearest the extension is the stream's and the rest belong to its name
    for (auto it = tokens.rbegin(); it != tokens.rend(); ++it)
    {
      if (const auto flag = ExternalStreamFlagFromToken(*it); flag.has_value())
      {
        info.flag |= *flag;
        continue;
      }

      if (info.language.IsEmpty())
      {
        if (const auto tag = ExternalStreamLanguageFromToken(*it); tag.has_value())
        {
          info.language = *tag;
          continue;
        }
      }

      name = (*it) + " " + name;
    }
  }
  name += " ";
  name += CServiceBroker::GetResourcesComponent().GetLocalizeStrings().Get(21602); // External
  StringUtils::Trim(name);
  info.name = StringUtils::RemoveDuplicatedSpacesAndTabs(name);
  if (info.flag == 0)
    info.flag = StreamFlags::FLAG_NONE;

  CLog::Log(LOGDEBUG, "{} - Language = '{}' / Name = '{}' / Flag = '{}' from {}", __FUNCTION__,
            info.language.AsBcp47(), info.name, info.flag, CURL::GetRedacted(associatedFile));

  return info;
}

/*! \brief in a vector of subtitles finds the corresponding .sub file for a given .idx file
 */
bool CUtil::FindVobSubPair(const std::vector<std::string>& vecSubtitles, const std::string& strIdxPath, std::string& strSubPath)
{
  if (URIUtils::HasExtension(strIdxPath, ".idx"))
  {
    std::string strIdxFile;
    std::string strIdxDirectory;
    URIUtils::Split(strIdxPath, strIdxDirectory, strIdxFile);
    for (const auto &subtitlePath : vecSubtitles)
    {
      std::string strSubFile;
      std::string strSubDirectory;
      URIUtils::Split(subtitlePath, strSubDirectory, strSubFile);
      if (URIUtils::IsInArchive(subtitlePath))
        strSubDirectory = CURL::Decode(strSubDirectory);
      if (URIUtils::HasExtension(strSubFile, ".sub") &&
          (URIUtils::PathEquals(URIUtils::ReplaceExtension(strIdxPath,""),
                                URIUtils::ReplaceExtension(subtitlePath,"")) ||
           (strSubDirectory.size() >= 11 &&
            StringUtils::EqualsNoCase(strSubDirectory.substr(6, strSubDirectory.length()-11), URIUtils::ReplaceExtension(strIdxPath,"")))))
      {
        strSubPath = subtitlePath;
        return true;
      }
    }
  }
  return false;
}

/*! \brief checks if in the vector of subtitles the given .sub file has a corresponding idx and hence is a vobsub file
 */
bool CUtil::IsVobSub(const std::vector<std::string>& vecSubtitles, const std::string& strSubPath)
{
  if (URIUtils::HasExtension(strSubPath, ".sub"))
  {
    std::string strSubFile;
    std::string strSubDirectory;
    URIUtils::Split(strSubPath, strSubDirectory, strSubFile);
    if (URIUtils::IsInArchive(strSubPath))
      strSubDirectory = CURL::Decode(strSubDirectory);
    for (const auto &subtitlePath : vecSubtitles)
    {
      std::string strIdxFile;
      std::string strIdxDirectory;
      URIUtils::Split(subtitlePath, strIdxDirectory, strIdxFile);
      if (URIUtils::HasExtension(strIdxFile, ".idx") &&
          (URIUtils::PathEquals(URIUtils::ReplaceExtension(subtitlePath,""),
                                URIUtils::ReplaceExtension(strSubPath,"")) ||
           (strSubDirectory.size() >= 11 &&
            StringUtils::EqualsNoCase(strSubDirectory.substr(6, strSubDirectory.length()-11), URIUtils::ReplaceExtension(subtitlePath,"")))))
        return true;
    }
  }
  return false;
}

/*! \brief find a plain or archived vobsub .sub file corresponding to an .idx file
 */
std::string CUtil::GetVobSubSubFromIdx(const std::string& vobSubIdx)
{
  std::string vobSub = URIUtils::ReplaceExtension(vobSubIdx, ".sub");

  // check if a .sub file exists in the same directory
  if (CFile::Exists(vobSub))
  {
    return vobSub;
  }

  // look inside a .rar or .zip in the same directory
  const std::string archTypes[] = { "rar", "zip" };
  std::string vobSubFilename = URIUtils::GetFileName(vobSub);
  for (const std::string& archType : archTypes)
  {
    vobSub = URIUtils::CreateArchivePath(archType,
                                         CURL(URIUtils::ReplaceExtension(vobSubIdx, std::string(".") + archType)),
                                         vobSubFilename).Get();
    if (CFile::Exists(vobSub))
      return vobSub;
  }

  return std::string();
}

/*! \brief find a .idx file from a path of a plain or archived vobsub .sub file
 */
std::string CUtil::GetVobSubIdxFromSub(const std::string& vobSub)
{
  std::string vobSubIdx = URIUtils::ReplaceExtension(vobSub, ".idx");

  // check if a .idx file exists in the same directory
  if (CFile::Exists(vobSubIdx))
  {
    return vobSubIdx;
  }

  // look outside archive (usually .rar) if the .sub is inside one
  if (URIUtils::IsInArchive(vobSub))
  {

    std::string archiveFile = URIUtils::GetDirectory(vobSub);
    std::string vobSubIdxDir = URIUtils::GetParentPath(archiveFile);

    if (!vobSubIdxDir.empty())
    {
      std::string vobSubIdxFilename = URIUtils::GetFileName(vobSubIdx);
      std::string vobSubIdx = URIUtils::AddFileToFolder(vobSubIdxDir, vobSubIdxFilename);

      if (CFile::Exists(vobSubIdx))
        return vobSubIdx;
    }
  }

  return std::string();
}

void CUtil::ScanForExternalAudio(const std::string& videoPath, std::vector<std::string>& vecAudio)
{
  CFileItem item(videoPath, false);
  if (NETWORK::IsInternetStream(item) || PLAYLIST::IsPlayList(item) || item.IsLiveTV() ||
      item.IsPVR() || !VIDEO::IsVideo(item))
    return;

  std::string strBasePath;
  std::string strAudio;

  GetVideoBasePathAndFileName(videoPath, strBasePath, strAudio);

  CFileItemList items;
  const std::vector<std::string> common_sub_dirs = { "audio", "tracks"};
  GetItemsToScan(strBasePath, CServiceBroker::GetFileExtensionProvider().GetMusicExtensions(), common_sub_dirs, items);

  std::vector<std::string> exts = StringUtils::Split(CServiceBroker::GetFileExtensionProvider().GetMusicExtensions(), "|");

  CVideoDatabase database;
  database.Open();
  bool useAllExternalAudio = database.GetUseAllExternalAudioForVideo(videoPath);

  if (useAllExternalAudio)
  {
    for (const auto& audioItem : items.GetList())
    {
      vecAudio.push_back(audioItem.get()->GetPath());
    }
  }
  else
    ScanPathsForAssociatedItems(strAudio, items, exts, vecAudio);
}

bool CUtil::CanBindPrivileged()
{
#ifdef TARGET_POSIX

  if (geteuid() == 0)
    return true; //root user can always bind to privileged ports

#ifdef HAVE_LIBCAP

  //check if CAP_NET_BIND_SERVICE is enabled, this allows non-root users to bind to privileged ports
  bool canbind = false;
  cap_t capabilities = cap_get_proc();
  if (capabilities)
  {
    cap_flag_value_t value;
    if (cap_get_flag(capabilities, CAP_NET_BIND_SERVICE, CAP_EFFECTIVE, &value) == 0)
      canbind = value;

    cap_free(capabilities);
  }

  return canbind;

#else //HAVE_LIBCAP

  return false;

#endif //HAVE_LIBCAP

#else //TARGET_POSIX

  return true;

#endif //TARGET_POSIX
}

bool CUtil::ValidatePort(int port)
{
  // check that it's a valid port
#ifdef TARGET_POSIX
  if (!CUtil::CanBindPrivileged() && (port < 1024 || port > 65535))
    return false;
  else
#endif
  if (port <= 0 || port > 65535)
    return false;

  return true;
}

int CUtil::GetRandomNumber()
{
#if !defined(TARGET_WINDOWS)
  return rand_r(&s_randomSeed);
#else
  unsigned int number;
  if (rand_s(&number) == 0)
    return (int)number;

  return rand();
#endif
}

void CUtil::CopyUserDataIfNeeded(const std::string& strPath,
                                 const std::string& file,
                                 const std::string& destname)
{
  std::string destPath;
  if (destname.empty())
    destPath = URIUtils::AddFileToFolder(strPath, file);
  else
    destPath = URIUtils::AddFileToFolder(strPath, destname);

  if (!CFile::Exists(destPath))
  {
    // need to copy it across
    std::string srcPath = URIUtils::AddFileToFolder("special://xbmc/userdata/", file);
    CFile::Copy(srcPath, destPath);
  }
}

std::string CUtil::GetHexString(const std::span<const uint8_t>& buf, int count)
{
  std::stringstream ss;
  ss << std::hex << std::setw(count) << std::setfill('0');
  std::ranges::for_each(buf, [&](auto x) { ss << static_cast<int>(x); });
  return std::move(ss).str();
}

bool CUtil::UseDynPathForAddOrUpdate(const CFileItem& item)
{
  if (item.IsStack() ||
      (URIUtils::IsBlurayPath(item.GetDynPath()) &&
       (item.GetVideoContentType() == VideoDbContentType::MOVIES ||
        item.GetVideoContentType() == VideoDbContentType::EPISODES ||
        item.GetVideoContentType() == VideoDbContentType::UNKNOWN /* Removable bluray */)))
  {
    return true;
  }

  return URIUtils::IsArchive(CURL(item.GetDynPath()));
}
