--[[
    Collection/StableScanner.lua

    Reads the hunter's own stable and turns it into collection state.

    ---------------------------------------------------------------------------
    What the client gives us
    ---------------------------------------------------------------------------

    `C_StableInfo.GetStablePetInfo(slotID)` returns a table carrying, among
    other fields, `name`, `icon`, `familyName` and -- the one that matters --
    `displayID`. A display id is exact and locale independent, so it is the only
    thing we match on. Family names are localized and pet names are whatever the
    player called them; neither identifies an appearance.

    ---------------------------------------------------------------------------
    The availability limitation
    ---------------------------------------------------------------------------

    The full stable appears to be readable only while the stable UI is open.
    See docs/wow-api.md for the evidence. This module therefore does not
    pretend to sync on login: it reads when the game tells us the stable is
    there, and `/shiny stable` reports plainly whether a read was possible.

    That is also why `/shiny stable` is worth running twice -- once in the open
    world, once at a stable master. The difference is the answer to the open
    question, and the command is written to make it visible rather than to hide
    a failure behind an empty list.

    ---------------------------------------------------------------------------
    Two rules that protect the player's data
    ---------------------------------------------------------------------------

      1. Ownership is replaced only by a read that returned something. An empty
         result means "could not read", never "you own nothing".

      2. The scanner only ever ADDS to `collected`. A pet missing from a read
         may mean the read was partial; concluding the player released it would
         be inventing information, and it would silently erase a journal mark
         they can never get back.
]]

local _, ns = ...

local Scanner = ns.NewModule("StableScanner")
ns.Collection = ns.Collection or {}
ns.Collection.StableScanner = Scanner

-- Slot range. 1-5 are the active pets and 7-205 the stabled ones; slot 6 is
-- read too rather than assumed to be special, since reading it costs nothing
-- and skipping it would be a guess.
local FIRST_SLOT, LAST_SLOT = 1, 205

-- PET_STABLE_UPDATE can fire several times as the frame populates. Coalesce.
local SYNC_DELAY = 0.25

local stableIsOpen = false
local pendingSync

--=============================================================================
-- Reading
--=============================================================================

--- Reads every slot the client will answer for.
-- @return table pets A list of { slot, displayID, familyName, name }.
-- @return boolean apiAvailable Whether C_StableInfo is present at all.
function Scanner:Read()
    if not C_StableInfo or not C_StableInfo.GetStablePetInfo then
        return {}, false
    end

    local pets = {}

    for slot = FIRST_SLOT, LAST_SLOT do
        -- Guarded per slot: an out-of-range or unpopulated slot must not take
        -- the whole scan down.
        local ok, info = pcall(C_StableInfo.GetStablePetInfo, slot)

        if ok and type(info) == "table" and info.displayID then
            pets[#pets + 1] = {
                slot = slot,
                displayID = info.displayID,
                familyName = info.familyName,
                name = info.name,
            }
        end
    end

    return pets, true
end

--=============================================================================
-- Matching
--=============================================================================

--- Turns a read into appearance ids, and reports what could not be matched.
-- @param pets table The result of Scanner:Read(), or a stored snapshot.
-- @return table appearanceIDs Set keyed by appearanceID.
-- @return table unknown List of pets whose display id no appearance claims.
function Scanner:Match(pets)
    local API = ns.Data.API
    local appearanceIDs, unknown = {}, {}
    local seenUnknown = {}

    for _, pet in ipairs(pets) do
        local appearanceID = API:GetAppearanceIDByDisplayID(pet.displayID)

        if appearanceID then
            appearanceIDs[appearanceID] = true
        elseif not seenUnknown[pet.displayID] then
            seenUnknown[pet.displayID] = true
            unknown[#unknown + 1] = pet
        end
    end

    return appearanceIDs, unknown
end

--=============================================================================
-- Syncing
--=============================================================================

--- Reads the stable and applies it to the collection state.
-- @return table|nil result { pets, appearanceIDs, unknown, marked } or nil if
--         nothing could be read.
function Scanner:Sync()
    local pets, apiAvailable = self:Read()

    if not apiAvailable then
        ns.Debug:Warn("STABLE", "C_StableInfo is unavailable on this client")
        return nil
    end

    if #pets == 0 then
        -- Not an error, and emphatically not "the player owns nothing": the
        -- stable is simply not readable right now. Existing state is left
        -- exactly as it was.
        ns.Debug:Log("STABLE", "no pets readable (stable open: %s)", tostring(stableIsOpen))
        return nil
    end

    local appearanceIDs, unknown = self:Match(pets)
    local State = ns.Collection.State

    -- Persisted, so the addon still knows this hunter's pets after logging out
    -- and away from any stable master.
    State:SetStableSnapshot(pets, appearanceIDs)
    local rareMarked = ns.Collection.RareState:ReconcileStable(pets)

    -- Additive only. See the rules at the top of this file.
    local marked = 0
    for appearanceID in pairs(appearanceIDs) do
        if not State:IsCollected(appearanceID) then
            State:SetCollected(appearanceID, true)
            marked = marked + 1
        end
    end

    ns.Debug:Log("STABLE", "%d pets read, %d matched, %d unknown display ids, %d appearances and %d named rares newly marked",
        #pets, ns.Util.CountKeys(appearanceIDs), #unknown, marked, rareMarked)

    if marked > 0 then
        ns.Print(ns.Plural("STABLE_AUTO_MARKED", marked):format(marked))
    end

    return {
        pets = pets,
        appearanceIDs = appearanceIDs,
        unknown = unknown,
        marked = marked,
        rareMarked = rareMarked,
    }
end

local function ScheduleSync()
    if pendingSync then
        pendingSync:Cancel()
    end

    pendingSync = C_Timer.NewTimer(SYNC_DELAY, function()
        pendingSync = nil
        Scanner:Sync()
    end)
end

--=============================================================================
-- Diagnostics
--=============================================================================

--- Prints what the client will tell us right now, falling back to the stored
--- snapshot when there is no live read.
--
-- Run it once away from a stable master and once at one: the difference is the
-- answer to whether the stable is readable in the open world.
function Scanner:PrintReport()
    local L = ns.L
    local State = ns.Collection.State
    local pets, apiAvailable = self:Read()

    ns.Print(L["STABLE_REPORT_TITLE"])
    ns.Print(L["STABLE_REPORT_FRAME"]:format(
        stableIsOpen and L["STABLE_STATE_OPEN"] or L["STABLE_STATE_CLOSED"]))

    if not apiAvailable then
        ns.Print(L["STABLE_REPORT_NO_API"])
        return
    end

    if #pets == 0 then
        -- No live read. Show what was stored rather than an empty list, which
        -- would read as "you have no pets".
        pets = State:GetStableSnapshot()

        if #pets == 0 then
            ns.Print(L["STABLE_REPORT_EMPTY"])
            return
        end

        ns.Print(L["STABLE_REPORT_SAVED"]:format(
            date("%d/%m/%Y %H:%M", State:GetLastStableScan())))
    end

    local appearanceIDs, unknown = self:Match(pets)

    ns.Print(L["STABLE_REPORT_SUMMARY"]:format(
        #pets, ns.Util.CountKeys(appearanceIDs), #unknown))

    -- The unknown display ids are the useful output: they are verified values
    -- from the player's own client, and they are what the database is missing.
    for _, pet in ipairs(unknown) do
        ns.Print(("|cffffd100%d|r  %s  (%s)"):format(
            pet.displayID,
            tostring(pet.familyName or "?"),
            tostring(pet.name or "?")))
    end

    if #unknown > 0 then
        ns.Print(L["STABLE_REPORT_UNKNOWN_HINT"])
    end
end

--=============================================================================
-- Lifecycle
--=============================================================================

function Scanner:OnEnable()
    ns.Events:RegisterEvent("PET_STABLE_SHOW", self, function()
        stableIsOpen = true
        ScheduleSync()
    end)

    ns.Events:RegisterEvent("PET_STABLE_UPDATE", self, function()
        if stableIsOpen then
            ScheduleSync()
        end
    end)

    ns.Events:RegisterEvent("PET_STABLE_CLOSED", self, function()
        stableIsOpen = false

        if pendingSync then
            pendingSync:Cancel()
            pendingSync = nil
        end
    end)

    ns.Debug:Log("STABLE", "scanner listening for stable events")
end

--- @return boolean Whether the stable UI is currently open.
function Scanner:IsStableOpen()
    return stableIsOpen
end
