#pragma once

#include <core/types.h>
#include <cstdint>
#include <string>
#include <vector>
#include <unordered_map>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <limits>

class EngineContext;
class ResourceManager;
struct FrameResources;

// Lightweight texture streaming cache.
// - Requests are deduplicated by a hashable TextureKey.
// - Loads happen via ResourceManager (deferred uploads supported).
// - Descriptors registered via watchBinding() are patched in-place
//   when the image becomes Resident, leveraging UPDATE_AFTER_BIND.
// - evictToBudget() rewrites watchers to provided fallbacks.
class TextureCache
{
public:
    struct TextureKey
    {
        enum class SourceKind : uint8_t { FilePath, Bytes };
        enum class ChannelsHint : uint8_t { Auto, R, RG, RGBA };
        SourceKind kind{SourceKind::FilePath};
        std::string path;                 // used when kind==FilePath
        std::vector<uint8_t> bytes;       // used when kind==Bytes
        bool srgb{false};                 // desired sampling format
        bool mipmapped{true};             // generate full mip chain
        ChannelsHint channels{ChannelsHint::Auto}; // prefer narrower formats when possible
        uint32_t mipClampLevels{0};       // 0 = full chain, otherwise limit to N mips
        uint64_t hash{0};                 // stable dedup key
    };

    using TextureHandle = uint32_t;
    static constexpr TextureHandle InvalidHandle = 0xFFFFFFFFu;

    enum class EntryState : uint8_t { Unloaded = 0, Loading = 1, Resident = 2, Evicted = 3 };

    void init(EngineContext *ctx);
    void cleanup();

    // Deduplicated request; returns a stable handle.
    TextureHandle request(const TextureKey &key, VkSampler sampler);

    // Register a descriptor binding to patch when the texture is ready.
    void watchBinding(TextureHandle handle, VkDescriptorSet set, uint32_t binding,
                      VkSampler sampler, VkImageView fallbackView);

    // Remove all watches for a descriptor set (call before destroying the
    // pool that owns the set). Prevents attempts to patch dead sets.
    void unwatchSet(VkDescriptorSet set);

    // Mark a texture as used this frame (for LRU).
    void markUsed(TextureHandle handle, uint32_t frameIndex);
    // Convenience: mark all handles watched by a descriptor set.
    void markSetUsed(VkDescriptorSet set, uint32_t frameIndex);

    // Schedule pending loads and patch descriptors for newly created images.
    void pumpLoads(ResourceManager &rm, FrameResources &frame);

    // Evict least-recently-used entries to fit within a budget in bytes.
    void evictToBudget(size_t budgetBytes);

    // Debug snapshot for UI
    struct DebugRow
    {
        std::string name;
        size_t bytes{0};
        uint32_t lastUsed{0};
        uint8_t state{0}; // cast of EntryState
    };
    struct DebugStats
    {
        size_t residentBytes{0};
        size_t countResident{0};
        size_t countEvicted{0};
        size_t countUnloaded{0};
    };
    void debug_snapshot(std::vector<DebugRow>& outRows, DebugStats& outStats) const;
    // Read-only per-handle state query (main-thread only).
    EntryState state(TextureHandle handle) const;
    size_t resident_bytes() const { return _residentBytes; }
    // CPU-side source bytes currently retained (compressed image payloads kept
    // for potential re-decode). Only applies to entries created with Bytes keys.
    size_t cpu_source_bytes() const { return _cpuSourceBytes; }

    // Runtime controls
    void set_max_loads_per_pump(int n) { _maxLoadsPerPump = (n > 0) ? n : 1; }
    int  max_loads_per_pump() const { return _maxLoadsPerPump; }
    // Limit total bytes admitted for uploads per pump (frame).
    void set_max_bytes_per_pump(size_t bytes) { _maxBytesPerPump = bytes; }
    size_t max_bytes_per_pump() const { return _maxBytesPerPump; }
    // Clamp decoded image dimensions before upload (progressive resolution).
    // 0 disables clamping. When >0, images larger than this dimension on any axis
    // are downscaled by powers of 2 on the decode thread until within limit.
    void set_max_upload_dimension(uint32_t dim) { _maxUploadDimension = dim; }
    uint32_t max_upload_dimension() const { return _maxUploadDimension; }

    // If false (default), compressed source bytes are dropped once an image is
    // uploaded to the GPU and descriptors patched. Set true to retain sources
    // for potential re-decode after eviction.
    void set_keep_source_bytes(bool keep) { _keepSourceBytes = keep; }
    bool keep_source_bytes() const { return _keepSourceBytes; }

    // Set a soft CPU budget (in bytes) for retained compressed sources. After
    // each upload drain, the cache will try to free source bytes for Resident
    // entries until under budget.
    void set_cpu_source_budget(size_t bytes) { _cpuSourceBudget = bytes; }
    size_t cpu_source_budget() const { return _cpuSourceBudget; }

    // Optional GPU residency budget, used to avoid immediate thrashing when
    // accepting new uploads. The engine should refresh this each frame.
    void set_gpu_budget_bytes(size_t bytes) { _gpuBudgetBytes = bytes; }
    size_t gpu_budget_bytes() const { return _gpuBudgetBytes; }

private:
    struct Patch
    {
        VkDescriptorSet set{VK_NULL_HANDLE};
        uint32_t binding{0};
        VkSampler sampler{VK_NULL_HANDLE};
        VkImageView fallbackView{VK_NULL_HANDLE};
    };

    struct Entry
    {
        TextureKey key{};
        VkSampler sampler{VK_NULL_HANDLE};
        EntryState state{EntryState::Unloaded};
        AllocatedImage image{};     // valid when Resident
        size_t sizeBytes{0};        // approximate VRAM cost
        uint32_t lastUsedFrame{0};
        uint32_t lastEvictedFrame{0};
        uint32_t nextAttemptFrame{0}; // gate reload attempts to reduce churn
        std::vector<Patch> patches; // descriptor patches to rewrite

        // Source payload for deferred load
        std::string path;                // for FilePath
        std::vector<uint8_t> bytes;      // for Bytes
    };

    EngineContext *_context{nullptr};
    std::vector<Entry> _entries;
    std::unordered_map<uint64_t, TextureHandle> _lookup; // key.hash -> handle
    std::unordered_map<VkDescriptorSet, std::vector<TextureHandle>> _setToHandles;
    size_t _residentBytes{0};
    size_t _cpuSourceBytes{0};

    // Controls
    int _maxLoadsPerPump{4};
    bool _keepSourceBytes{false};
    size_t _cpuSourceBudget{64ull * 1024ull * 1024ull}; // 64 MiB default
    size_t _gpuBudgetBytes{std::numeric_limits<size_t>::max()}; // unlimited unless set
    uint32_t _reloadCooldownFrames{2};
    size_t _maxBytesPerPump{128ull * 1024ull * 1024ull}; // 128 MiB/frame upload budget
    uint32_t _maxUploadDimension{4096};                   // progressive downscale cap

    void start_load(Entry &e, ResourceManager &rm);
    void patch_ready_entry(const Entry &e);
    void patch_to_fallback(const Entry &e);

    // --- Async decode backend ---
    struct DecodeRequest
    {
        TextureHandle handle{InvalidHandle};
        TextureKey key{};
        std::string path;
        std::vector<uint8_t> bytes;
    };
    struct DecodedResult
    {
        TextureHandle handle{InvalidHandle};
        int width{0};
        int height{0};
        // Prefer heap pointer from stb to avoid an extra memcpy into a vector.
        // If 'heap' is non-null, it must be freed with stbi_image_free() after
        // the upload has copied the data. 'rgba' remains as a fallback path.
        unsigned char *heap{nullptr};
        size_t heapBytes{0};
        std::vector<uint8_t> rgba;
        bool mipmapped{true};
        bool srgb{false};
        TextureKey::ChannelsHint channels{TextureKey::ChannelsHint::Auto};
        uint32_t mipClampLevels{0};

        // Compressed path (KTX2 pre-transcoded BCn). When true, 'rgba/heap'
        // are ignored and the fields below describe the payload.
        bool isKTX2{false};
        VkFormat ktxFormat{VK_FORMAT_UNDEFINED};
        uint32_t ktxMipLevels{0};
        struct KTXPack {
            struct L { uint64_t offset{0}, length{0}; uint32_t width{0}, height{0}; };
            std::vector<uint8_t> bytes;     // full file content
            std::vector<L> levels;           // per-mip region description
        } ktx;
    };

    void worker_loop();
    void enqueue_decode(Entry &e);
    // Returns total resident bytes admitted this pump (after GPU budget gate).
    size_t drain_ready_uploads(ResourceManager &rm, size_t budgetBytes);
    void drop_source_bytes(Entry &e);
    void evictCpuToBudget();

    // Try to free at least 'bytesNeeded' by evicting least-recently-used
    // Resident entries that were not used in the current frame. Returns true
    // if enough space was reclaimed. Does not evict textures used this frame.
    bool try_make_space(size_t bytesNeeded, uint32_t now);

    std::vector<std::thread> _decodeThreads;
    std::mutex _qMutex;
    std::condition_variable _qCV;
    std::deque<DecodeRequest> _queue;
    std::mutex _readyMutex;
    std::deque<DecodedResult> _ready;
    std::atomic<bool> _running{false};
};

// Helpers to build/digest keys
namespace texcache
{
    // 64-bit FNV-1a
    inline uint64_t fnv1a64(std::string_view s)
    {
        const uint64_t FNV_OFFSET = 1469598103934665603ull;
        const uint64_t FNV_PRIME  = 1099511628211ull;
        uint64_t h = FNV_OFFSET;
        for (unsigned char c : s) { h ^= c; h *= FNV_PRIME; }
        return h;
    }
    inline uint64_t fnv1a64(const uint8_t *data, size_t n)
    {
        const uint64_t FNV_OFFSET = 1469598103934665603ull;
        const uint64_t FNV_PRIME  = 1099511628211ull;
        uint64_t h = FNV_OFFSET;
        for (size_t i = 0; i < n; ++i) { h ^= data[i]; h *= FNV_PRIME; }
        return h;
    }
}
