--[[
    Collection/State.lua

    The layer that answers "does this player have it?".

    Deliberately separate from Data/Registry.lua: the static database describes
    what exists in the game, this module describes what happened to one player.
    Nothing writes collection flags into creature or appearance records.

        DATABASE (static)  +  COLLECTION STATE (per player)  ->  UI

    Storage split, by intent:
      * collected / pending  -> per character (taming happens per character)
      * favorite / ignored   -> account wide (a wishlist spans alts)

    Every mutation raises "COLLECTION_CHANGED" so the UI can refresh without
    polling.
]]

local _, ns = ...

local State = ns.NewModule("CollectionState")
ns.Collection = ns.Collection or {}
ns.Collection.State = State

--- Marks used by the browser. Kept as an enum so list rendering never has to
--- branch on several booleans.
State.status = {
    COLLECTED = "COLLECTED",
    SEEN = "SEEN",
    MISSING = "MISSING",
    PENDING = "PENDING",
    IGNORED = "IGNORED",
}

local function CharacterTable(name)
    local character = ns.Database:GetCharacterData()
    return character and character[name]
end

local function AccountTable(name)
    local account = ns.Database:GetAccountData()
    return account and account[name]
end

local function SetFlag(tbl, appearanceID, enabled)
    if not tbl or appearanceID == nil then
        return false
    end

    local current = tbl[appearanceID] and true or false
    if current == enabled then
        return false
    end

    tbl[appearanceID] = enabled or nil
    return true
end

--=============================================================================
-- Collected
--=============================================================================

function State:IsCollected(appearanceID)
    local collected = CharacterTable("collected")
    return collected ~= nil and collected[appearanceID] == true
end

--- @param appearanceID number
-- @param collected boolean
function State:SetCollected(appearanceID, collected)
    if SetFlag(CharacterTable("collected"), appearanceID, collected) then
        ns.Debug:Log("STATE", "collected[%s] = %s", tostring(appearanceID), tostring(collected))
        ns.Events:Send("COLLECTION_CHANGED", appearanceID)
    end
end

--=============================================================================
-- Seen
--
-- Encountered in the world, tamed or not. A pet you walked past at level 20
-- and could not tame still belongs in the journal.
--=============================================================================

function State:IsSeen(appearanceID)
    local seen = CharacterTable("seen")
    return seen ~= nil and seen[appearanceID] == true
end

--- Records an encounter. Additive only: nothing ever un-sees a pet.
function State:MarkSeen(appearanceID)
    if SetFlag(CharacterTable("seen"), appearanceID, true) then
        ns.Debug:Log("STATE", "seen[%s]", tostring(appearanceID))
        ns.Events:Send("COLLECTION_CHANGED", appearanceID)
    end
end

--=============================================================================
-- Favourites and ignore list
--=============================================================================

function State:IsFavorite(appearanceID)
    local favorites = AccountTable("favorites")
    return favorites ~= nil and favorites[appearanceID] == true
end

function State:SetFavorite(appearanceID, favorite)
    if SetFlag(AccountTable("favorites"), appearanceID, favorite) then
        ns.Debug:Log("STATE", "favorite[%s] = %s", tostring(appearanceID), tostring(favorite))
        ns.Events:Send("COLLECTION_CHANGED", appearanceID)
    end
end

--- @return boolean The new favourite state.
function State:ToggleFavorite(appearanceID)
    local favorite = not self:IsFavorite(appearanceID)
    self:SetFavorite(appearanceID, favorite)
    return favorite
end

function State:IsIgnored(appearanceID)
    local ignored = AccountTable("ignored")
    return ignored ~= nil and ignored[appearanceID] == true
end

function State:SetIgnored(appearanceID, ignored)
    if SetFlag(AccountTable("ignored"), appearanceID, ignored) then
        ns.Events:Send("COLLECTION_CHANGED", appearanceID)
    end
end

function State:IsPending(appearanceID)
    local pending = CharacterTable("pending")
    return pending ~= nil and pending[appearanceID] == true
end

function State:SetPending(appearanceID, pending)
    if SetFlag(CharacterTable("pending"), appearanceID, pending) then
        ns.Events:Send("COLLECTION_CHANGED", appearanceID)
    end
end

--=============================================================================
-- Ownership, derived from the stable
--
-- Deliberately not the same thing as "collected". Ownership is whatever the
-- last stable read saw; collected is the player's journal, which the scanner
-- may add to but never clears. Keeping them apart is what stops a partial or
-- stale read from looking like a lost pet.
--=============================================================================

--- @param appearanceID number
-- @return boolean Whether the last stable read saw this appearance.
function State:IsOwned(appearanceID)
    local owned = CharacterTable("owned")
    return owned ~= nil and owned[appearanceID] == true
end

--- Stores what the stable said. Only ever called after a read that actually
--- returned pets, so a failed read leaves the previous snapshot intact.
-- @param pets table { { slot, displayID, familyName, name }, ... } verbatim
--        from the client.
-- @param appearanceIDs table Set keyed by appearanceID, derived from `pets`.
function State:SetStableSnapshot(pets, appearanceIDs)
    local character = ns.Database:GetCharacterData()
    if not character then
        return
    end

    character.stablePets = pets
    character.owned = appearanceIDs
    character.stableScannedAt = time()

    ns.Debug:Log("STATE", "stable snapshot stored: %d pets, %d recognised",
        #pets, ns.Util.CountKeys(appearanceIDs))

    ns.Events:Send("COLLECTION_CHANGED")
end

--- The pets seen by the last successful read. Survives logout, so the addon
--- can describe the player's stable away from a stable master.
-- @return table Possibly empty; never nil.
function State:GetStableSnapshot()
    local character = ns.Database:GetCharacterData()
    return character and character.stablePets or {}
end

--- @return number Server time of the last successful stable read, or 0.
function State:GetLastStableScan()
    local character = ns.Database:GetCharacterData()
    return character and character.stableScannedAt or 0
end

--=============================================================================
-- Aggregates
--=============================================================================

--- @param appearanceID number
-- @return string One of State.status.
function State:GetStatus(appearanceID)
    if self:IsCollected(appearanceID) then
        return self.status.COLLECTED
    elseif self:IsIgnored(appearanceID) then
        return self.status.IGNORED
    elseif self:IsPending(appearanceID) then
        return self.status.PENDING
    elseif self:IsSeen(appearanceID) then
        return self.status.SEEN
    end

    return self.status.MISSING
end

--- Overall progress for the window header.
-- @param appearanceSet table|nil A set keyed by appearanceID; defaults to the
--        whole database.
-- @return number collected, number total
function State:GetProgress(appearanceSet)
    appearanceSet = appearanceSet or ns.Data.API:GetAllAppearances()

    local collected, total = 0, 0

    for appearanceID in pairs(appearanceSet) do
        total = total + 1
        if self:IsCollected(appearanceID) then
            collected = collected + 1
        end
    end

    return collected, total
end
