---@class CacheEntry
---@field Name string
---@field Compare fun(): any
---@field WaitTime integer | nil
---@field LastChecked integer|nil
---@field OnChange fun(new:any, old:any)[]
---@field Value any

---@class CacheModule
---@field Caches table<string, CacheEntry>
---@field LoopRunning boolean
---@field Create fun(name:string, compare:fun():any, waitTime:integer | nil): CacheEntry
---@field Get fun(name:string): any
---@field OnChange fun(name:string, onChange:fun(new:any, old:any))
---@field Remove fun(name:string)
Cache = Cache or {} ---@type CacheModule -- <-- we use Cache as a global variable so that if we dont required it more than once

Table = Table or Require('lib/utility/shared/tables.lua')
Id = Id or Require('lib/utility/shared/ids.lua')

local Config = Require("settings/sharedConfig.lua")
local max = 5000
local CreateThread = CreateThread
local Wait = Wait
local GetGameTimer = GetGameTimer

local resourceCallbacks = {} -- Add callbacks from resources
local resourceTracker = {
    caches = {},
    callbacks = {},
    initialized = {}
}

Cache.Caches = Cache.Caches or {}
Cache.LoopRunning = Cache.LoopRunning or false

---@param ... any
local function debugPrint(...)
    if Config.DebugLevel == 0 then return end
    print("^2[Cache]^0", ...)
end

local function HasActiveCaches()
    for _, cache in pairs(Cache.Caches) do
        if cache.WaitTime ~= nil then
            return true
        end
    end
    return false
end

local function processCacheEntry(now, cache)
    cache.LastChecked = cache.LastChecked or now
    cache.WaitTime = tonumber(cache.WaitTime) or max
    local elapsed = now - cache.LastChecked
    local remaining = cache.WaitTime - elapsed

    if remaining <= 0 then
        local oldValue = cache.Value
        cache.Value = cache.Compare()        
        if not Table.Compare(cache.Value, oldValue) and cache.OnChange then
            for i, onChange in pairs(cache.OnChange) do
                onChange(cache.Value, oldValue)
            end
        end
        cache.LastChecked = now
        remaining = cache.WaitTime
    end

    return remaining
end

local function getNextWait(now)
    local minWait = nil
    for name, cache in pairs(Cache.Caches) do
        if cache.Compare and cache.WaitTime ~= nil then
            local remaining = processCacheEntry(now, cache)
            if not minWait or remaining < minWait then
                minWait = remaining
                if minWait <= 0 then break end
            end
        end
    end
    return minWait
end

local function StartLoop()
    if Cache.LoopRunning then return end
    if not HasActiveCaches() then return end
    Cache.LoopRunning = true
    CreateThread(function()
        while Cache.LoopRunning do
            local now = GetGameTimer()
            local minWait = getNextWait(now)
            if minWait then
                Wait(math.max(0, minWait))
            else
                Wait(max)
            end
            if not HasActiveCaches() then
                Cache.LoopRunning = false
            end
        end
    end)
end

local function trackResource(resourceName, type, data)
    if not resourceName then return end

    -- Initialize resource tracking if needed
    if not resourceTracker.initialized[resourceName] then
        resourceTracker.initialized[resourceName] = true
        resourceTracker.caches[resourceName] = resourceTracker.caches[resourceName] or {}
        resourceTracker.callbacks[resourceName] = resourceTracker.callbacks[resourceName] or {}

        AddEventHandler('onResourceStop', function(stoppingResource)
            if stoppingResource ~= resourceName then return end

            -- Clean up caches
            for _, cacheName in ipairs(resourceTracker.caches[resourceName] or {}) do
                if Cache.Caches[cacheName] then
                    Cache.Caches[cacheName] = nil
                    debugPrint(("Removed cache '%s' - resource '%s' stopped"):format(cacheName, resourceName))
                end
            end

            -- Clean up callbacks
            for _, cb in ipairs(resourceTracker.callbacks[resourceName] or {}) do
                local targetCache = Cache.Caches[cb.cacheName]
                if targetCache then
                    targetCache.OnChange[cb.index] = nil
                    debugPrint(("Removed callback '%s' from cache '%s' - resource '%s' stopped"):format(cb.index, cb.cacheName, resourceName))
                end
            end

            -- Clear tracking data
            resourceTracker.caches[resourceName] = nil
            resourceTracker.callbacks[resourceName] = nil
            resourceTracker.initialized[resourceName] = nil
        end)
    end

    -- Track the new item
    if type == "cache" then
        table.insert(resourceTracker.caches[resourceName], data)
    elseif type == "callback" then
        table.insert(resourceTracker.callbacks[resourceName], data)
    end
end

---@param name string
---@param compare fun():any
---@param waitTime integer | nil
---@return CacheEntry | nil
function Cache.Create(name, compare, waitTime)
    assert(name, "Cache name is required.")
    assert(compare, "Comparison function is required.")
    if type(waitTime) ~= "number" then
        waitTime = nil
    end
    local _name = tostring(name)
    local cache = Cache.Caches[_name]
    if cache and cache.Compare == compare then
        debugPrint(_name .. " already exists with the same comparison function.")
        return cache
    end
    local ok, result = pcall(compare)
    if not ok then
        debugPrint("Error creating cache '" .. _name .. "': " .. tostring(result))
        return nil
    end
    ---@type CacheEntry
    local newCache = {
        Name = _name,
        Compare = compare,
        WaitTime = waitTime, -- can be nil
        LastChecked = nil,
        OnChange = {},
        Value = result
    }

    -- Track the cache with its resource
    trackResource(GetInvokingResource(), "cache", _name)

    Cache.Caches[_name] = newCache

    debugPrint(_name .. " created with initial value: " .. tostring(result))
    for _, onChange in pairs(newCache.OnChange) do
        onChange(newCache.Value, 0)
    end
    StartLoop()
    return newCache
end

---@param name string
---@return any
function Cache.Get(name)
    assert(name, "Cache name is required.")
    local _name = tostring(name)
    local cache = Cache.Caches[_name]
    return cache and cache.Value or nil
end

--- This add a callback to the cache entry that will be called when the value changes.
--- The callback will be called with the new value and the old value.
--- you can call the value again to delete the callback.
---@param name string
---@param onChange fun(new:any, old:any)
function Cache.OnChange(name, onChange)
    assert(name, "Cache name is required.")
    local _name = tostring(name)
    local cache = Cache.Caches[_name]
    assert(cache, "Cache with name '" .. _name .. "' does not exist.")

    local id = Id.CreateUniqueId(Cache.Caches[_name]?.OnChange)
    -- Figure out which resource is trying to register this callback
    local invokingResource = GetInvokingResource()
    if not invokingResource then return end
    -- Add the new callback to our list
    local callbackIndex = id
    cache.OnChange[callbackIndex] = onChange

    -- Track the callback with its resource
    trackResource(GetInvokingResource(), "callback", {
        cacheName = _name,
        index = callbackIndex
    })

    debugPrint(("Added new OnChange callback to cache '%s' from resource '%s'"):format(_name, invokingResource))
    return callbackIndex
end

function Cache.RemoveOnChange(name, id)
    assert(name, "Cache name is required.")
    local _name = tostring(name)
    local cache = Cache.Caches[_name]
    assert(cache, "Cache with name '" .. _name .. "' does not exist.")
    if cache.OnChange[id] then
        cache.OnChange[id] = nil
        debugPrint("Removed OnChange callback from cache '" .. _name .. "' with ID: " .. id)
    else
        debugPrint("No OnChange callback found with ID: " .. id .. " in cache '" .. _name .. "'")
    end
end

---@param name string
function Cache.Remove(name)
    assert(name, "Cache name is required.")
    local _name = tostring(name)
    local cache = Cache.Caches[_name]
    if cache then
        Cache.Caches[_name] = nil
        debugPrint(_name .. " removed from cache.")
        if next(Cache.Caches) == nil then
            Cache.LoopRunning = false
        end
    end
end

---@param name string
---@param newValue any
function Cache.Update(name, newValue)
    assert(name, "Cache name is required.")
    local _name = tostring(name)
    local cache = Cache.Caches[_name]
    assert(cache, "Cache with name '" .. _name .. "' does not exist.")
    local oldValue = cache.Value
    if oldValue ~= newValue then
        cache.Value = newValue
        for _, onChange in pairs(cache.OnChange) do
            onChange(newValue, oldValue)
        end
    end
end

return Cache