/*****************************************************************************
 * Copyright (c) 2014-2025 OpenRCT2 developers
 *
 * For a complete list of all authors, please refer to contributors.md
 * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
 *
 * OpenRCT2 is licensed under the GNU General Public License version 3.
 *****************************************************************************/

#ifndef DISABLE_OPENGL

    #include "TextureCache.h"

    #include <algorithm>
    #include <openrct2/Diagnostic.h>
    #include <openrct2/core/EnumUtils.hpp>
    #include <openrct2/drawing/Drawing.h>
    #include <openrct2/interface/Colour.h>
    #include <openrct2/world/Location.hpp>
    #include <stdexcept>
    #include <vector>

using namespace OpenRCT2::Ui;
using namespace OpenRCT2::Drawing;

constexpr uint32_t kUnusedIndex = 0xFFFFFFFF;

TextureCache::TextureCache()
{
    std::fill(_indexMap.begin(), _indexMap.end(), kUnusedIndex);
}

TextureCache::~TextureCache()
{
    FreeTextures();
}

void TextureCache::InvalidateImage(ImageIndex image)
{
    unique_lock lock(_mutex);

    uint32_t index = _indexMap[image];
    if (index == kUnusedIndex)
        return;

    AtlasTextureInfo& elem = _textureCache.at(index);

    _atlases[elem.index].Free(elem);
    _indexMap[image] = kUnusedIndex;

    if (index == _textureCache.size() - 1)
    {
        // Last element can be popped back.
        _textureCache.pop_back();
    }
    else
    {
        // Swap last element with element to erase and then pop back.
        AtlasTextureInfo& last = _textureCache.back();

        // Move last to current.
        elem = last;

        // Change index for moved element.
        _indexMap[last.image] = index;

        _textureCache.pop_back();
    }
}

// Note: for performance reasons, this returns a BasicTextureInfo over an AtlasTextureInfo (also to not expose the cache)
BasicTextureInfo TextureCache::GetOrLoadImageTexture(const ImageId imageId)
{
    uint32_t index;

    // Try to read cached texture first.
    {
        shared_lock lock(_mutex);

        index = _indexMap[imageId.GetIndex()];
        if (index != kUnusedIndex)
        {
            const auto& info = _textureCache[index];
            return {
                info.index,
                info.coords,
            };
        }
    }

    // Load new texture.
    unique_lock lock(_mutex);

    index = static_cast<uint32_t>(_textureCache.size());

    AtlasTextureInfo info = LoadImageTexture(imageId);

    _textureCache.push_back(info);
    _indexMap[imageId.GetIndex()] = index;

    return info;
}

BasicTextureInfo TextureCache::GetOrLoadGlyphTexture(const ImageId imageId, const PaletteMap& paletteMap)
{
    GlyphId glyphId{};
    glyphId.Image = imageId.GetIndex();

    // Try to read cached texture first.
    {
        shared_lock lock(_mutex);

        uint8_t glyphMap[8];
        for (uint8_t i = 0; i < 8; i++)
        {
            glyphMap[i] = paletteMap[i];
        }
        std::copy_n(glyphMap, sizeof(glyphId.Palette), reinterpret_cast<uint8_t*>(&glyphId.Palette));

        auto kvp = _glyphTextureMap.find(glyphId);
        if (kvp != _glyphTextureMap.end())
        {
            const auto& info = kvp->second;
            return {
                info.index,
                info.coords,
            };
        }
    }

    // Load new texture.
    unique_lock lock(_mutex);

    auto cacheInfo = LoadGlyphTexture(imageId, paletteMap);
    auto it = _glyphTextureMap.insert(std::make_pair(glyphId, cacheInfo));

    return (*it.first).second;
}

BasicTextureInfo TextureCache::GetOrLoadBitmapTexture(ImageIndex image, const void* pixels, size_t width, size_t height)
{
    uint32_t index;

    // Try to read cached texture first.
    {
        shared_lock lock(_mutex);

        index = _indexMap[image];
        if (index != kUnusedIndex)
        {
            const auto& info = _textureCache[index];
            return {
                info.index,
                info.coords,
            };
        }
    }

    // Load new texture.
    unique_lock lock(_mutex);

    index = uint32_t(_textureCache.size());

    AtlasTextureInfo info = LoadBitmapTexture(image, pixels, width, height);

    _textureCache.push_back(info);
    _indexMap[image] = index;

    return info;
}

void TextureCache::CreateTextures()
{
    if (!_initialized)
    {
        // Determine width and height to use for texture atlases
        glGetIntegerv(GL_MAX_TEXTURE_SIZE, &_atlasesTextureDimensions);
        if (_atlasesTextureDimensions > kTextureCacheMaxAtlasSize)
        {
            _atlasesTextureDimensions = kTextureCacheMaxAtlasSize;
        }

        // Determine maximum number of atlases (minimum of size and array limit)
        glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &_atlasesTextureIndicesLimit);
        if (_atlasesTextureDimensions < _atlasesTextureIndicesLimit)
            _atlasesTextureIndicesLimit = _atlasesTextureDimensions;

        glGenTextures(1, &_atlasesTexture);
        glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasesTexture);
        glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

        glGenTextures(1, &_paletteTexture);
        glBindTexture(GL_TEXTURE_2D, _paletteTexture);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
        GeneratePaletteTexture();

        auto blendArray = GetBlendColourMap();
        if (blendArray != nullptr)
        {
            glGenTextures(1, &_blendPaletteTexture);
            glBindTexture(GL_TEXTURE_2D, _blendPaletteTexture);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
            glTexImage2D(
                GL_TEXTURE_2D, 0, GL_R8UI, kGamePaletteSize, kGamePaletteSize, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, blendArray);
        }

        _initialized = true;
        _atlasesTextureIndices = 0;
        _atlasesTextureCapacity = 0;
    }
}

void TextureCache::GeneratePaletteTexture()
{
    static_assert(kPaletteTotalOffsets + 5 < 256, "Height of palette too large!");
    constexpr int32_t height = 256;
    constexpr int32_t width = height;
    DrawPixelInfo dpi = CreateDPI(width, height);

    // Init no-op palette
    for (int i = 0; i < width; ++i)
    {
        dpi.bits[i] = i;
    }

    for (int i = 0; i < kPaletteTotalOffsets; ++i)
    {
        GLint y = PaletteToY(static_cast<FilterPaletteID>(i));

        auto g1Index = GetPaletteG1Index(i);
        if (g1Index.has_value())
        {
            const auto* element = GfxGetG1Element(g1Index.value());
            if (element != nullptr)
            {
                GfxDrawSpriteSoftware(dpi, ImageId(g1Index.value()), { -element->x_offset, y - element->y_offset });
            }
        }
    }

    glBindTexture(GL_TEXTURE_2D, _paletteTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_R8UI, width, height, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, dpi.bits);
    DeleteDPI(dpi);
}

void TextureCache::EnlargeAtlasesTexture(GLuint newEntries)
{
    CreateTextures();

    GLuint newIndices = _atlasesTextureIndices + newEntries;

    std::vector<char> oldPixels;

    if (newIndices > _atlasesTextureCapacity)
    {
        // Retrieve current array data, growing buffer.
        oldPixels.resize(_atlasesTextureDimensions * _atlasesTextureDimensions * _atlasesTextureCapacity);
        if (!oldPixels.empty())
        {
            glGetTexImage(GL_TEXTURE_2D_ARRAY, 0, GL_RED_INTEGER, GL_UNSIGNED_BYTE, oldPixels.data());
        }

        // Initial capacity will be 12 which covers most cases of a fully visible park.
        _atlasesTextureCapacity = (_atlasesTextureCapacity + 6) << 1uL;

        glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasesTexture);
        glTexImage3D(
            GL_TEXTURE_2D_ARRAY, 0, GL_R8UI, _atlasesTextureDimensions, _atlasesTextureDimensions, _atlasesTextureCapacity, 0,
            GL_RED_INTEGER, GL_UNSIGNED_BYTE, nullptr);

        // Restore old data
        if (!oldPixels.empty())
        {
            glTexSubImage3D(
                GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, _atlasesTextureDimensions, _atlasesTextureDimensions, _atlasesTextureIndices,
                GL_RED_INTEGER, GL_UNSIGNED_BYTE, oldPixels.data());
        }
    }

    _atlasesTextureIndices = newIndices;
}

AtlasTextureInfo TextureCache::LoadImageTexture(const ImageId imageId)
{
    DrawPixelInfo dpi = GetImageAsDPI(ImageId(imageId.GetIndex()));

    auto cacheInfo = AllocateImage(dpi.width, dpi.height);
    cacheInfo.image = imageId.GetIndex();

    glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasesTexture);
    glTexSubImage3D(
        GL_TEXTURE_2D_ARRAY, 0, cacheInfo.bounds.x, cacheInfo.bounds.y, cacheInfo.index, dpi.width, dpi.height, 1,
        GL_RED_INTEGER, GL_UNSIGNED_BYTE, dpi.bits);

    DeleteDPI(dpi);

    return cacheInfo;
}

AtlasTextureInfo TextureCache::LoadGlyphTexture(const ImageId imageId, const PaletteMap& paletteMap)
{
    DrawPixelInfo dpi = GetGlyphAsDPI(imageId, paletteMap);

    auto cacheInfo = AllocateImage(dpi.width, dpi.height);
    cacheInfo.image = imageId.GetIndex();

    glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasesTexture);
    glTexSubImage3D(
        GL_TEXTURE_2D_ARRAY, 0, cacheInfo.bounds.x, cacheInfo.bounds.y, cacheInfo.index, dpi.width, dpi.height, 1,
        GL_RED_INTEGER, GL_UNSIGNED_BYTE, dpi.bits);

    DeleteDPI(dpi);

    return cacheInfo;
}

AtlasTextureInfo TextureCache::LoadBitmapTexture(ImageIndex image, const void* pixels, size_t width, size_t height)
{
    auto cacheInfo = AllocateImage(int32_t(width), int32_t(height));
    cacheInfo.image = image;
    glBindTexture(GL_TEXTURE_2D_ARRAY, _atlasesTexture);
    glTexSubImage3D(
        GL_TEXTURE_2D_ARRAY, 0, cacheInfo.bounds.x, cacheInfo.bounds.y, cacheInfo.index, GLsizei(width), GLsizei(height), 1,
        GL_RED_INTEGER, GL_UNSIGNED_BYTE, reinterpret_cast<const GLvoid*>(pixels));
    return cacheInfo;
}

AtlasTextureInfo TextureCache::AllocateImage(int32_t imageWidth, int32_t imageHeight)
{
    CreateTextures();

    // Find an atlas that fits this image
    for (Atlas& atlas : _atlases)
    {
        if (atlas.GetFreeSlots() > 0 && atlas.IsImageSuitable(imageWidth, imageHeight))
        {
            return atlas.Allocate(imageWidth, imageHeight);
        }
    }

    // If there is no such atlas, then create a new one
    if (static_cast<int32_t>(_atlases.size()) >= _atlasesTextureIndicesLimit)
    {
        throw std::runtime_error("more texture atlases required, but device limit reached!");
    }

    auto atlasIndex = static_cast<int32_t>(_atlases.size());
    int32_t atlasSize = powf(2, static_cast<float>(Atlas::CalculateImageSizeOrder(imageWidth, imageHeight)));

    #ifdef DEBUG
    LOG_VERBOSE("new texture atlas #%d (size %d) allocated", atlasIndex, atlasSize);
    #endif

    _atlases.emplace_back(atlasIndex, atlasSize);
    _atlases.back().Initialise(_atlasesTextureDimensions, _atlasesTextureDimensions);

    // Enlarge texture array to support new atlas
    EnlargeAtlasesTexture(1);

    // And allocate from the new atlas
    return _atlases.back().Allocate(imageWidth, imageHeight);
}

DrawPixelInfo TextureCache::GetImageAsDPI(const ImageId imageId)
{
    auto g1Element = GfxGetG1Element(imageId);
    int32_t width = g1Element->width;
    int32_t height = g1Element->height;

    DrawPixelInfo dpi = CreateDPI(width, height);
    GfxDrawSpriteSoftware(dpi, imageId, { -g1Element->x_offset, -g1Element->y_offset });
    return dpi;
}

DrawPixelInfo TextureCache::GetGlyphAsDPI(const ImageId imageId, const PaletteMap& palette)
{
    auto g1Element = GfxGetG1Element(imageId);
    int32_t width = g1Element->width;
    int32_t height = g1Element->height;

    DrawPixelInfo dpi = CreateDPI(width, height);

    const auto glyphCoords = ScreenCoordsXY{ -g1Element->x_offset, -g1Element->y_offset };
    GfxDrawSpritePaletteSetSoftware(dpi, imageId, glyphCoords, palette);
    return dpi;
}

void TextureCache::FreeTextures()
{
    // Free array texture
    glDeleteTextures(1, &_atlasesTexture);
    _textureCache.clear();
    std::fill(_indexMap.begin(), _indexMap.end(), kUnusedIndex);
}

DrawPixelInfo TextureCache::CreateDPI(int32_t width, int32_t height)
{
    size_t numPixels = width * height;
    auto pixels8 = new uint8_t[numPixels];
    std::fill_n(pixels8, numPixels, 0);

    DrawPixelInfo dpi;
    dpi.bits = pixels8;
    dpi.pitch = 0;
    dpi.x = 0;
    dpi.y = 0;
    dpi.width = width;
    dpi.height = height;
    dpi.zoom_level = ZoomLevel{ 0 };
    return dpi;
}

void TextureCache::DeleteDPI(DrawPixelInfo dpi)
{
    delete[] dpi.bits;
}

GLuint TextureCache::GetAtlasesTexture()
{
    return _atlasesTexture;
}

GLuint TextureCache::GetPaletteTexture()
{
    return _paletteTexture;
}

GLuint TextureCache::GetBlendPaletteTexture()
{
    return _blendPaletteTexture;
}

GLint TextureCache::PaletteToY(FilterPaletteID palette)
{
    return palette > FilterPaletteID::PaletteWater ? EnumValue(palette) + 5 : EnumValue(palette) + 1;
}

#endif /* DISABLE_OPENGL */
