#include "resourcehelpers.hpp"

#include <algorithm>
#include <sstream>
#include <string_view>

#include <components/esm/common.hpp>
#include <components/esm/refid.hpp>

#include <components/misc/pathhelpers.hpp>
#include <components/misc/strings/algorithm.hpp>
#include <components/misc/strings/lower.hpp>

#include <components/vfs/manager.hpp>
#include <components/vfs/pathutil.hpp>

namespace
{

    struct MatchPathSeparator
    {
        bool operator()(char ch) const { return ch == '\\' || ch == '/'; }
    };

    std::string getBasename(std::string const& pathname)
    {
        return std::string(
            std::find_if(pathname.rbegin(), pathname.rend(), MatchPathSeparator()).base(), pathname.end());
    }

}

bool changeExtension(std::string& path, std::string_view ext)
{
    std::string::size_type pos = path.rfind('.');
    if (pos != std::string::npos && path.compare(pos, path.length() - pos, ext) != 0)
    {
        path.replace(pos, path.length(), ext);
        return true;
    }
    return false;
}

bool Misc::ResourceHelpers::changeExtensionToDds(std::string& path)
{
    return changeExtension(path, ".dds");
}

std::string Misc::ResourceHelpers::correctResourcePath(
    std::string_view topLevelDirectory, std::string_view resPath, const VFS::Manager* vfs)
{
    /* Bethesda at some point converted all their BSA
     * textures from tga to dds for increased load speed, but all
     * texture file name references were kept as .tga.
     */

    std::string correctedPath = Misc::StringUtils::lowerCase(resPath);

    // Flatten slashes
    std::replace(correctedPath.begin(), correctedPath.end(), '/', '\\');
    auto bothSeparators = [](char a, char b) { return a == '\\' && b == '\\'; };
    correctedPath.erase(std::unique(correctedPath.begin(), correctedPath.end(), bothSeparators), correctedPath.end());

    // Remove leading separator
    if (!correctedPath.empty() && correctedPath[0] == '\\')
        correctedPath.erase(0, 1);

    // Handle top level directory
    if (!correctedPath.starts_with(topLevelDirectory) || correctedPath.size() <= topLevelDirectory.size()
        || correctedPath[topLevelDirectory.size()] != '\\')
    {
        std::string topLevelPrefix = std::string{ topLevelDirectory } + '\\';
        size_t topLevelPos = correctedPath.find('\\' + topLevelPrefix);
        if (topLevelPos == std::string::npos)
            correctedPath = topLevelPrefix + correctedPath;
        else
            correctedPath.erase(0, topLevelPos + 1);
    }

    std::string origExt = correctedPath;

    // since we know all (GOTY edition or less) textures end
    // in .dds, we change the extension
    bool changedToDds = changeExtensionToDds(correctedPath);
    if (vfs->exists(correctedPath))
        return correctedPath;
    // if it turns out that the above wasn't true in all cases (not for vanilla, but maybe mods)
    // verify, and revert if false (this call succeeds quickly, but fails slowly)
    if (changedToDds && vfs->exists(origExt))
        return origExt;

    // fall back to a resource in the top level directory if it exists
    std::string fallback{ topLevelDirectory };
    fallback += '\\';
    fallback += getBasename(correctedPath);
    if (vfs->exists(fallback))
        return fallback;

    if (changedToDds)
    {
        fallback = topLevelDirectory;
        fallback += '\\';
        fallback += getBasename(origExt);
        if (vfs->exists(fallback))
            return fallback;
    }

    return correctedPath;
}

std::string Misc::ResourceHelpers::correctTexturePath(std::string_view resPath, const VFS::Manager* vfs)
{
    return correctResourcePath("textures", resPath, vfs);
}

std::string Misc::ResourceHelpers::correctIconPath(std::string_view resPath, const VFS::Manager* vfs)
{
    return correctResourcePath("icons", resPath, vfs);
}

std::string Misc::ResourceHelpers::correctBookartPath(std::string_view resPath, const VFS::Manager* vfs)
{
    return correctResourcePath("bookart", resPath, vfs);
}

std::string Misc::ResourceHelpers::correctBookartPath(
    std::string_view resPath, int width, int height, const VFS::Manager* vfs)
{
    std::string image = correctBookartPath(resPath, vfs);

    // Apparently a bug with some morrowind versions, they reference the image without the size suffix.
    // So if the image isn't found, try appending the size.
    if (!vfs->exists(image))
    {
        std::stringstream str;
        str << image.substr(0, image.rfind('.')) << "_" << width << "_" << height << image.substr(image.rfind('.'));
        image = Misc::ResourceHelpers::correctBookartPath(str.str(), vfs);
    }

    return image;
}

std::string Misc::ResourceHelpers::correctActorModelPath(const std::string& resPath, const VFS::Manager* vfs)
{
    std::string mdlname = resPath;
    std::string::size_type p = mdlname.find_last_of("/\\");
    if (p != std::string::npos)
        mdlname.insert(mdlname.begin() + p + 1, 'x');
    else
        mdlname.insert(mdlname.begin(), 'x');
    std::string kfname = mdlname;
    if (Misc::StringUtils::ciEndsWith(kfname, ".nif"))
        kfname.replace(kfname.size() - 4, 4, ".kf");

    if (!vfs->exists(kfname))
    {
        return resPath;
    }
    return mdlname;
}

std::string Misc::ResourceHelpers::correctMeshPath(std::string_view resPath)
{
    std::string res = "meshes\\";
    res.append(resPath);
    return res;
}

std::string Misc::ResourceHelpers::correctSoundPath(const std::string& resPath)
{
    return "sound\\" + resPath;
}

std::string Misc::ResourceHelpers::correctMusicPath(const std::string& resPath)
{
    return "music\\" + resPath;
}

std::string_view Misc::ResourceHelpers::meshPathForESM3(std::string_view resPath)
{
    constexpr std::string_view prefix = "meshes";
    if (resPath.length() < prefix.size() + 1 || !Misc::StringUtils::ciStartsWith(resPath, prefix)
        || (resPath[prefix.size()] != '/' && resPath[prefix.size()] != '\\'))
    {
        throw std::runtime_error("Path should start with 'meshes\\'");
    }
    return resPath.substr(prefix.size() + 1);
}

std::string Misc::ResourceHelpers::correctSoundPath(std::string_view resPath, const VFS::Manager* vfs)
{
    // Workaround: Bethesda at some point converted some of the files to mp3, but the references were kept as .wav.
    if (!vfs->exists(resPath))
    {
        std::string sound{ resPath };
        changeExtension(sound, ".mp3");
        VFS::Path::normalizeFilenameInPlace(sound);
        return sound;
    }
    return VFS::Path::normalizeFilename(resPath);
}

bool Misc::ResourceHelpers::isHiddenMarker(const ESM::RefId& id)
{
    return id == "prisonmarker" || id == "divinemarker" || id == "templemarker" || id == "northmarker";
}

namespace
{
    std::string getLODMeshNameImpl(std::string resPath, const VFS::Manager* vfs, std::string_view pattern)
    {
        if (auto w = Misc::findExtension(resPath); w != std::string::npos)
            resPath.insert(w, pattern);
        return VFS::Path::normalizeFilename(resPath);
    }

    std::string getBestLODMeshName(std::string const& resPath, const VFS::Manager* vfs, std::string_view pattern)
    {
        if (const auto& result = getLODMeshNameImpl(resPath, vfs, pattern); vfs->exists(result))
            return result;
        return resPath;
    }
}

std::string Misc::ResourceHelpers::getLODMeshName(
    int esmVersion, std::string resPath, const VFS::Manager* vfs, unsigned char lod)
{
    const std::string distantMeshPattern = [&esmVersion] {
        switch (esmVersion)
        {
            case ESM::VER_120:
            case ESM::VER_130:
                return "_dist";
            case ESM::VER_080:
            case ESM::VER_100:
                return "_far";
            case ESM::VER_094:
            case ESM::VER_170:
                return "_lod";
            default:
                return "";
        }
    }();
    for (int l = lod; l >= 0; --l)
    {
        std::stringstream patern;
        patern << distantMeshPattern << "_" << l;
        std::string const meshName = getBestLODMeshName(resPath, vfs, patern.str());
        if (meshName != resPath)
            return meshName;
    }
    return getBestLODMeshName(resPath, vfs, distantMeshPattern);
}