--[[
============================================================================
Enhanced Mount Tracker  -  Core/MountAPI.lua
============================================================================
Single source of truth for "which mounts does the player own", "what is this
mount called", and "can the player use it".

DETECTION  -------------------------------------------------------------------
MoP Classic 5.4.8 ships the Mount Journal API (verified: MountJournalEnhanced
loads on Interface 50503 and uses C_MountJournal throughout). We use it as the
primary path because it is authoritative — it enumerates every mount in the
client and reports `isCollected` directly:

    C_MountJournal.GetMountIDs()             -> { mountID, ... }
    C_MountJournal.GetMountInfoByID(mountID) -> name, spellID, icon, active,
        isUsable, sourceType, isFavorite, isFactionSpecific, faction,
        shouldHideOnChar, isCollected, mountID, ...

We index it by spellID (return #2) — the same key ATT's mnt(spellID,...) uses,
so the journal joins directly against ns.EMT.MountData.

NOTE: on Classic the journal's `sourceType` is nil for every mount, so source
categorisation still comes from our ATT-derived data — the journal is used
only for the live collected / name / icon / usable facts.

If C_MountJournal is somehow unavailable we fall back to the older
GetCompanionInfo("MOUNT") path (works since Wrath).
============================================================================
]]--

local ADDON_NAME, NS = ...
local EMT = NS.EMT
if not EMT then return end

EMT.MountAPI = EMT.MountAPI or {}
local API = EMT.MountAPI

-- ----------------------------------------------------------------------------
-- STATE
-- ----------------------------------------------------------------------------

-- _journal[spellID] = { mountID, name, icon, collected, usable, companion }
API._journal        = {}
API._scanned        = false
API._collectedCount = 0
API._usingJournal   = false   -- true if the C_MountJournal path was used

API.playerClassID = nil
API.playerRaceID  = nil
API.playerFaction = nil       -- 1 = Horde, 2 = Alliance  (matches ATT 'r')

-- UnitRace in 5.4.8 returns only (raceName, raceFileName) — no numeric ID —
-- so we map the file name to ATT's race IDs ourselves.
local RACE_FILE_TO_ID = {
    Human = 1, Orc = 2, Dwarf = 3, NightElf = 4, Scourge = 5, Undead = 5,
    Tauren = 6, Gnome = 7, Troll = 8, Goblin = 9, BloodElf = 10,
    Draenei = 11, Worgen = 22, Pandaren = 24,
}

-- ----------------------------------------------------------------------------
-- COLLECTION SCAN
-- ----------------------------------------------------------------------------

local function hasMountJournal()
    return C_MountJournal
        and C_MountJournal.GetMountIDs
        and C_MountJournal.GetMountInfoByID
end

function API:Refresh()
    wipe(self._journal)
    self._collectedCount = 0

    if hasMountJournal() then
        self._usingJournal = true
        local ids = C_MountJournal.GetMountIDs()
        for _, mountID in ipairs(ids) do
            local name, spellID, icon, _, isUsable, _, _, _, _, _, isCollected =
                C_MountJournal.GetMountInfoByID(mountID)
            if spellID then
                self._journal[spellID] = {
                    mountID   = mountID,
                    name      = name,
                    icon      = icon,
                    collected = isCollected and true or false,
                    usable    = isUsable and true or false,
                }
                if isCollected then
                    self._collectedCount = self._collectedCount + 1
                end
            end
        end
    elseif GetNumCompanions then
        -- Fallback: the learned-companion list (collected mounts only).
        self._usingJournal = false
        local n = GetNumCompanions("MOUNT") or 0
        for i = 1, n do
            local _, _, spellID, icon = GetCompanionInfo("MOUNT", i)
            if spellID then
                self._journal[spellID] = {
                    mountID   = nil,
                    companion = i,        -- index for CallCompanion fallback
                    name      = (GetSpellInfo(spellID)),
                    icon      = icon,
                    collected = true,
                    usable    = true,
                }
                self._collectedCount = self._collectedCount + 1
            end
        end
    end

    self._scanned = true
    EMT:Debug("MountAPI", string.format("scanned via %s: %d collected",
        self._usingJournal and "C_MountJournal" or "GetCompanionInfo",
        self._collectedCount))
    EMT:Emit("EMT_COLLECTION_CHANGED")
end

local function ensureScanned()
    if not API._scanned then API:Refresh() end
end

function API:IsCollected(spellID)
    ensureScanned()
    local j = self._journal[spellID]
    return j ~= nil and j.collected == true
end

-- Mount Journal ID for a spell (lets callers query the journal's own
-- authoritative source text via C_MountJournal.GetMountInfoExtraByID).
function API:GetJournalMountID(spellID)
    ensureScanned()
    local j = self._journal[spellID]
    return j and j.mountID or nil
end

-- True if the spell is a mount the Mount Journal enumerates (a permanent,
-- usable-indefinitely mount). On the collected-only fallback (no
-- C_MountJournal) we can't see uncollected mounts, so we don't exclude any.
function API:IsJournalMount(spellID)
    ensureScanned()
    if not self._usingJournal then return true end
    return self._journal[spellID] ~= nil
end

-- Owned + total over the shipped data set.
function API:GetProgress()
    ensureScanned()
    local data = EMT.MountData
    if not data then return 0, 0 end
    local owned, total = 0, 0
    for spellID in pairs(data) do
        total = total + 1
        if self:IsCollected(spellID) then owned = owned + 1 end
    end
    return owned, total
end

-- Every mount the player owns, including ones outside our data set.
function API:GetOwnedCount()
    ensureScanned()
    return self._collectedCount
end

-- Summon an owned mount by spellID. Prefers the journal's SummonByID; falls
-- back to CallCompanion. Neither is a protected function.
function API:Summon(spellID)
    ensureScanned()
    local j = self._journal[spellID]
    if not j then return false end
    if j.mountID and C_MountJournal and C_MountJournal.SummonByID then
        C_MountJournal.SummonByID(j.mountID)
        return true
    elseif j.companion and CallCompanion then
        CallCompanion("MOUNT", j.companion)
        return true
    end
    return false
end

-- ----------------------------------------------------------------------------
-- DATA-vs-CLIENT AUDIT
-- ----------------------------------------------------------------------------
-- Compares our shipped MountData against the live Mount Journal so the player
-- (and we) can see exactly where the data set is incomplete or stale.
-- Returns: inBoth, dataOnly (spellIDs in our file but not the journal),
--          journalOnly ({spellID,name} in the journal but not our file),
--          usingJournal (false => journalOnly can't be computed).

function API:Audit()
    ensureScanned()
    local data = EMT.MountData or {}
    local inBoth, dataOnly, journalOnly = 0, {}, {}

    for spellID in pairs(data) do
        if self._journal[spellID] then
            inBoth = inBoth + 1
        else
            dataOnly[#dataOnly + 1] = spellID
        end
    end
    if self._usingJournal then
        for spellID, j in pairs(self._journal) do
            if not data[spellID] then
                journalOnly[#journalOnly + 1] = { spellID = spellID, name = j.name }
            end
        end
    end
    return inBoth, dataOnly, journalOnly, self._usingJournal
end

-- ----------------------------------------------------------------------------
-- NAME / ICON RESOLUTION
-- ----------------------------------------------------------------------------
-- Prefer the journal's name/icon (instant + authoritative). Fall back to
-- GetSpellInfo / GetItemInfo for anything the journal doesn't know.

function API:GetName(spellID, itemID)
    local j = self._journal[spellID]
    if j and type(j.name) == "string" and j.name ~= "" then return j.name end
    local name = GetSpellInfo(spellID)
    if type(name) == "string" and name ~= "" then return name end
    if itemID then
        local iname = GetItemInfo(itemID)
        if type(iname) == "string" and iname ~= "" then return iname end
    end
    return "Mount #" .. tostring(spellID)
end

function API:GetIcon(spellID, itemID)
    local j = self._journal[spellID]
    if j and j.icon then return j.icon end
    local _, _, icon = GetSpellInfo(spellID)
    if icon then return icon end
    if itemID then
        local tex = select(10, GetItemInfo(itemID))
        if tex then return tex end
    end
    return "Interface\\Icons\\Ability_Mount_RidingHorse"
end

local QUALITY_HEX = {
    [1] = "ffffffff", [2] = "ff1eff00", [3] = "ff0070dd",
    [4] = "ffa335ee", [5] = "ffff8000", [6] = "ffe6cc80",
}
function API:QualityColor(q)
    return "|c" .. (QUALITY_HEX[q or 1] or QUALITY_HEX[1])
end

-- ----------------------------------------------------------------------------
-- PLAYER USABILITY
-- ----------------------------------------------------------------------------

function API:_resolvePlayer()
    local _, _, classID = UnitClass("player")
    self.playerClassID = classID

    local _, raceFile = UnitRace("player")
    self.playerRaceID = raceFile and RACE_FILE_TO_ID[raceFile] or nil

    local fac = UnitFactionGroup("player")
    self.playerFaction = (fac == "Horde" and 1) or (fac == "Alliance" and 2) or nil
end

-- Returns usable(bool), reason(string|nil)
function API:CanUse(rec)
    if not rec then return true, nil end

    if rec.r and self.playerFaction and rec.r ~= self.playerFaction then
        return false, "Wrong faction"
    end

    if rec.cl and self.playerClassID then
        local ok = false
        for _, cid in ipairs(rec.cl) do
            if cid == self.playerClassID then ok = true break end
        end
        if not ok then return false, "Wrong class" end
    end

    if rec.rc and self.playerRaceID then
        local ok = false
        for _, rid in ipairs(rec.rc) do
            if rid == self.playerRaceID
               or (self.playerRaceID == 24 and (rid == 25 or rid == 26)) then
                ok = true break
            end
        end
        if not ok then return false, "Wrong race" end
    end

    return true, nil
end

function API:IsObtainable(rec)
    return rec and rec.u == nil
end

-- ----------------------------------------------------------------------------
-- INIT  +  EVENT WIRING
-- ----------------------------------------------------------------------------

EMT:RegisterInit(function()
    API:_resolvePlayer()
    API:Refresh()

    local f = CreateFrame("Frame")
    -- Companion events cover the GetCompanionInfo fallback path; the journal
    -- events cover the C_MountJournal path. Registering an event the client
    -- doesn't fire is harmless, so we register both sets defensively.
    f:RegisterEvent("COMPANION_LEARNED")
    f:RegisterEvent("COMPANION_UNLEARNED")
    f:RegisterEvent("COMPANION_UPDATE")
    pcall(f.RegisterEvent, f, "NEW_MOUNT_ADDED")
    pcall(f.RegisterEvent, f, "MOUNT_JOURNAL_USABILITY_CHANGED")

    local pending = false
    f:SetScript("OnEvent", function()
        if pending then return end       -- coalesce event bursts into one scan
        pending = true
        C_Timer.After(0.2, function()
            pending = false
            API:Refresh()
        end)
    end)
    API._eventFrame = f
end)
