--[[
    Collection/ClientFacts.lua

    Learns facts about a creature from the player's own client: its Blizzard
    display id, and its name in the language the player actually plays in.

    ---------------------------------------------------------------------------
    Why this exists
    ---------------------------------------------------------------------------

    39 of the 604 catalogued rares ship without a display id, six of them in
    Eversong Woods, and the newest rares ship with English names on every
    locale. Neither gap is carelessness: the published client-data export does
    not carry those creatures yet, and asking it answers
    `{"error":"Creature not found!"}`.

    The project rule is absolute -- a display id is never guessed, because a
    guessed one shows the wrong beast, which is worse than showing none. A
    translation is never invented either.

    But neither fact is unavailable. Both are sitting in the player's client
    every time they look at the creature. Harvesting them there is the same
    principle the sighting log already uses: first-party data, gathered by
    playing, with no external source and no licence question attached.

    ---------------------------------------------------------------------------
    Sources
    ---------------------------------------------------------------------------

      live unit    UnitName gives the localized name; a model frame pointed at
                   the unit gives the display id
      stable       a tamed pet reports its own display id, and a catalogue name
                   that is unique identifies which rare it came from

    ---------------------------------------------------------------------------
    Unverified API
    ---------------------------------------------------------------------------

    Reading a display id back out of a model frame is not observed in use by
    any addon on this client, so it ships capability-checked. If the method is
    absent, names and the stable source still work and nothing else is
    affected; the start-up log says which it was. See docs/wow-api.md.

    Learned facts never enter the shipped database. They live in the player's
    saved variables, and shipped data always wins over them.
]]

local _, ns = ...

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

local IsSecretValue = issecretvalue

local function IsSecret(value)
    return IsSecretValue and IsSecretValue(value)
end

-- One offscreen frame, built on first use and reused. It is the only thing
-- here that costs anything, and most sessions never need it.
local probe
local probeUsable

-- Creatures whose probe attempt has already been reported, so a beast that
-- stays on screen does not repeat the same diagnosis every nameplate update.
local probeReported = {}

--- @return table|nil A model frame able to report a display id, if any.
local function AcquireProbe()
    if probeUsable ~= nil then
        return probeUsable and probe or nil
    end

    probe = CreateFrame("PlayerModel", nil, UIParent)

    -- Shown, not hidden. A hidden model frame has nothing to draw, so the
    -- client has no reason to stream the creature in -- and an unloaded model
    -- cannot report which display it is. It is parked far outside the screen
    -- and fully transparent instead, which the player never sees and the
    -- renderer still treats as live.
    probe:SetSize(32, 32)
    probe:SetPoint("TOPLEFT", UIParent, "TOPLEFT", -4000, 4000)
    probe:SetAlpha(0)
    probe:Show()

    probeUsable = type(probe.SetUnit) == "function"
        and type(probe.GetDisplayInfo) == "function"

    if not probeUsable then
        ns.Debug:Warn("FACTS",
            "this client does not report display ids from a model frame")
        probe = nil
    end

    return probeUsable and probe or nil
end

--=============================================================================
-- Storage
--=============================================================================

local function Store(field)
    local account = ns.Database:GetAccountData()
    if not account then return nil end
    account[field] = account[field] or {}
    return account[field]
end

--- @return number|nil The display id learned from this player's client.
function ClientFacts:GetDisplayID(creatureID)
    local learned = creatureID and Store("learnedDisplayIDs")
    return learned and learned[creatureID] or nil
end

--- @return table Every display id this client taught us, keyed by creature.
-- Returned rather than counted because a player sharing field notes is handing
-- back the portraits that brand new entries are missing.
function ClientFacts:GetLearnedDisplayIDs()
    return Store("learnedDisplayIDs") or {}
end

--- @return string|nil The creature name as this client spells it.
function ClientFacts:GetName(creatureID)
    local learned = creatureID and Store("learnedNames")
    return learned and learned[creatureID] or nil
end

--- Records a display id observed on this client.
-- @return boolean Whether this was new.
function ClientFacts:LearnDisplayID(creatureID, displayID, source)
    if not creatureID or not displayID or displayID == 0 then return false end
    if IsSecret(creatureID) or IsSecret(displayID) then return false end

    local learned = Store("learnedDisplayIDs")
    if not learned or learned[creatureID] == displayID then return false end

    learned[creatureID] = displayID
    ns.Debug:Log("FACTS", "learned display id %d for creature %d (%s)",
        displayID, creatureID, source)
    ns.Events:Send("CLIENT_FACT_LEARNED", creatureID)
    return true
end

--- Records the name this client gives a creature.
-- Only stored when it differs from the shipped one, so a locale that already
-- matches the catalogue costs nothing.
-- @return boolean Whether this was new.
function ClientFacts:LearnName(creatureID, name, shippedName)
    if not creatureID or type(name) ~= "string" or name == "" then return false end
    if IsSecret(name) or name == shippedName then return false end

    local learned = Store("learnedNames")
    if not learned or learned[creatureID] == name then return false end

    learned[creatureID] = name
    ns.Debug:Log("FACTS", "learned name %q for creature %d", name, creatureID)
    ns.Events:Send("CLIENT_FACT_LEARNED", creatureID)
    return true
end

--=============================================================================
-- Sources
--=============================================================================

--- Reads what this client knows about a unit standing in front of the player.
-- Called from the sighting path for catalogued rares only, and each fact is
-- read at most once per creature, so ordinary play costs nothing.
-- @param unit string A unit token, e.g. "target" or "nameplate3".
-- @param record table The RareCatalog record for that creature.
function ClientFacts:InspectUnit(unit, record)
    if not record or not UnitExists(unit) then return end

    local creatureID = record.npcID

    if not self:GetName(creatureID) then
        local name = UnitName(unit)
        if not IsSecret(name) then
            self:LearnName(creatureID, name, record.name)
        end
    end

    local shipped = record.displayIDs and record.displayIDs[1]
    if shipped or self:GetDisplayID(creatureID) then
        return
    end

    local frame = AcquireProbe()
    if not frame then return end

    -- Every outcome below is reported once per creature. Reading a display id
    -- from a model frame is unverified on this client, and "nothing happened"
    -- is the one answer that cannot be acted on: it could mean the method is
    -- refused, that it answers only after the model loads, or that this code
    -- never ran at all. Each of those needs a different fix, so each says so.
    if probeReported[creatureID] then return end
    probeReported[creatureID] = true

    -- A failure here must never take the caller down: this runs inside the
    -- sighting path, which is on the critical route for the journal.
    local posed, poseError = pcall(frame.SetUnit, frame, unit)
    if not posed then
        ns.Debug:Warn("FACTS", "SetUnit refused for creature %d: %s",
            creatureID, tostring(poseError))
        return
    end

    local read, displayID = pcall(frame.GetDisplayInfo, frame)
    if not read then
        ns.Debug:Warn("FACTS", "GetDisplayInfo refused for creature %d: %s",
            creatureID, tostring(displayID))
        return
    end

    if not displayID or displayID == 0 then
        -- The model frame may still be streaming the creature in. Ask again on
        -- the next frame before concluding the client will not tell us.
        C_Timer.After(0.5, function()
            local retried, retryID = pcall(frame.GetDisplayInfo, frame)
            if retried and retryID and retryID ~= 0 then
                ClientFacts:LearnDisplayID(creatureID, retryID, "live unit, delayed")
            else
                ns.Debug:Warn("FACTS",
                    "no display id for creature %d (got %s, then %s)",
                    creatureID, tostring(displayID), tostring(retryID))
            end
        end)
        return
    end

    self:LearnDisplayID(creatureID, displayID, "live unit")
end

--- Records display ids reported by the player's own stable.
-- A pet only identifies a rare when its catalogue name is unique, the same
-- rule the capture reconciler uses.
-- @param pets table Array of { name, displayID, ... } from StableScanner.
function ClientFacts:ReconcileStable(pets)
    local learned = 0

    for _, pet in ipairs(pets or {}) do
        local name = pet.name
        if pet.displayID and type(name) == "string" and not IsSecret(name) then
            local rare = ns.Data.RareCatalog:GetUniqueByName(name)
            if rare and self:LearnDisplayID(rare.npcID, pet.displayID, "stable") then
                learned = learned + 1
            end
        end
    end

    return learned
end

--=============================================================================
-- What the interface asks for
--=============================================================================

--- The display id to show for a catalogue record. Shipped ids are verified
--- Blizzard data and always win over a learned one.
-- @return number|nil displayID, boolean wasLearned
function ClientFacts:ResolveDisplayID(record)
    if not record then return nil, false end

    local shipped = record.displayIDs and record.displayIDs[1]
    if shipped then return shipped, false end

    local displayID = self:GetDisplayID(record.npcID)
    return displayID, displayID ~= nil
end

--- The name to show for a catalogue record. The client's own spelling wins
--- here, because it is what the player reads on the creature itself.
-- @return string The best available name.
function ClientFacts:ResolveName(record)
    if not record then return "" end
    return self:GetName(record.npcID) or record.name
end

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

function ClientFacts:OnEnable()
    ns.Debug:Log("FACTS", "learned so far: %d display ids, %d names",
        ns.Util.CountKeys(Store("learnedDisplayIDs") or {}),
        ns.Util.CountKeys(Store("learnedNames") or {}))
end
