-- DopingControl scan/gear.lua
-- Equipment + enchant reading for one unit: GetInventoryItemLink for the 16
-- equipment-tab slots (DC.SLOTS_EQUIPMENT), enchant id parsed from the item
-- link ("item:ID:ENCHANT:..."), item details via GetItemInfo with the
-- MEASURED Turtle 1.18.1 return order
-- : 1 name, 2 link, 3 QUALITY, 4 reqLevel, 5 itemType,
-- 6 ITEMSUBTYPE, 7 maxStack, 8 equipSlot, 9 ICONPATH, 10 vendorPrice.
-- Reading position 4 as itemLevel or 10 as texture breaks SILENTLY with
-- plausible-looking values -- do not "fix" this back to the vanilla order.
--
-- Readability rule (binding):
--   ANY slot returned a link  => gearRead = true; slots with nil => "EMPTY"
--   ALL 13 slots returned nil => gearRead = false (failed read, NOT naked)
-- A link that came back but does not parse leaves its slot nil while
-- gearRead stays true -- core/model.lua shows that cell as UNKNOWN
-- ("no data"), never as MISSING (resolution: a parse anomaly must not be
-- claimed as "no item").
--
-- Pure Lua 5.0, dofile-loadable offline: ParseLink / ParseLinkName /
-- ParseWeaponEnchantLine / Assemble are pure (offline-tested);
-- ReadUnit, ItemInfo and ReadSelfWeaponEnchantName
-- touch the WoW API and run in-game only, pcall-wrapped.

DopingControl = DopingControl or {}
local DC = DopingControl

DC_Gear = DC_Gear or {}
local G = DC_Gear

-- Fallback inventory-slot list (= invSlot column of DC.SLOTS_EQUIPMENT,
-- data/enchants.lua) so this file works standalone in tests.
local FALLBACK_INV_SLOTS = { 1, 2, 3, 15, 5, 6, 9, 10, 7, 8, 16, 17, 18, 11, 12, 13, 14 }

-- InvSlots() -> array of the 17 inventory slot numbers, in column order.
-- Prefers DC.SLOTS_EQUIPMENT (single source of truth once data/ is loaded).
function G.InvSlots()
    if DC.SLOTS_EQUIPMENT then
        local out = {}
        local n = table.getn(DC.SLOTS_EQUIPMENT)
        for i = 1, n do
            table.insert(out, DC.SLOTS_EQUIPMENT[i].invSlot)
        end
        return out
    end
    return FALLBACK_INV_SLOTS
end

-- ==================================================================
-- Pure part
-- ==================================================================

-- ParseLink(link) -> itemId, enchantId (numbers) | nil, nil on malformed.
-- Enchant id is the 2nd field of the link payload (standard 1.12 link
-- pattern, widened to also capture the item id).
function G.ParseLink(link)
    if type(link) ~= "string" then
        return nil, nil
    end
    local _, _, idStr, enchStr = string.find(link, "item:(%d+):(%d+)")
    if not idStr then
        return nil, nil
    end
    return tonumber(idStr), tonumber(enchStr)
end

-- ParseLinkName(link) -> "[bracket] name" | nil (fallback when GetItemInfo
-- has no cached data yet; standard 1.12 pattern).
function G.ParseLinkName(link)
    if type(link) ~= "string" then
        return nil
    end
    local _, _, name = string.find(link, "%[(.+)%]")
    return name
end

-- ParseWeaponEnchantLine(text) -> name|nil (pure).
-- The main-hand tooltip's temporary-enchant line reads "<Name> (N min)" on
-- the English client (e.g. "Dense Sharpening Stone (28 min)") -- strip the
-- "(N min)" part, keep the name. Any other line (item name, "Speed 2.70",
-- "+9 Damage", a bare "(28 min)") and any non-string input -> nil.
function G.ParseWeaponEnchantLine(text)
    if type(text) ~= "string" then
        return nil
    end
    local _, _, name = string.find(text, "^(.-)%s*%((%d+) min%)%s*$")
    if name and name ~= "" then
        return name
    end
    return nil
end

-- Assemble(rawLinks, slots, itemInfo) -> gear, gearRead
--   rawLinks : { [invSlot] = link string | nil }
--   slots    : array of invSlot numbers (default G.InvSlots())
--   itemInfo : nil, or function(itemId) -> name, quality, subType, texture
--              (already de-shifted; in-game wrapper is G.ItemInfo, tests
--              pass a stub)
-- Returns the store shape:
--   gearRead = false => gear = nil
--   gearRead = true  => gear[invSlot] = { id, enchant, quality, subType,
--                        texture, name } | "EMPTY" (no link) | nil
--                        (link present but unparseable -> UNKNOWN cell)
-- The subType field feeds DC.ENCH_EXEMPT in core/model.lua (wand/holdable
-- exemptions); the model explicitly expects it.
function G.Assemble(rawLinks, slots, itemInfo)
    rawLinks = rawLinks or {}
    slots = slots or G.InvSlots()
    local n = table.getn(slots)

    local gear = {}
    local any = false
    for i = 1, n do
        local inv = slots[i]
        local link = rawLinks[inv]
        if link ~= nil then
            any = true
            local id, ench = G.ParseLink(link)
            if id then
                local entry = { id = id, enchant = ench }
                if itemInfo then
                    local name, quality, subType, texture = itemInfo(id)
                    entry.name = name
                    entry.quality = quality
                    entry.subType = subType
                    entry.texture = texture
                end
                if not entry.name then
                    entry.name = G.ParseLinkName(link)
                end
                gear[inv] = entry
            end
            -- unparseable link: slot stays nil (UNKNOWN), see header
        end
    end

    if not any then
        return nil, false
    end
    for i = 1, n do
        local inv = slots[i]
        if rawLinks[inv] == nil then
            gear[inv] = "EMPTY"
        end
    end
    return gear, true
end

-- ==================================================================
-- WoW part (runtime only; every foreign-unit call pcall-wrapped)
-- ==================================================================

-- ItemInfo(itemId) -> name, quality, subType, texture | nil (uncached/err).
-- De-shifts the Turtle 1.18.1 GetItemInfo order (see header).
function G.ItemInfo(itemId)
    if not GetItemInfo then
        return nil
    end
    local ok, name, link, quality, reqLevel, itemType, subType,
        maxStack, equipSlot, iconPath = pcall(GetItemInfo, itemId)
    if not ok or not name then
        return nil
    end
    return name, quality, subType, iconPath
end

-- ReadSelfWeaponEnchantName() -> name|nil (WoW runtime only).
-- Self path for store.weaponMainName: GetWeaponEnchantInfo() carries
-- no NAME for the own unit, but the equipped main hand's tooltip lists the
-- temp enchant as its own "<Name> (N min)" line (English client). Scans
-- the shared hidden tooltip (scan/aura.lua accessor -- never a second
-- frame under the same global name) over SetInventoryItem("player", 16)
-- and returns the first line that parses; nil-safe when no such line.
function G.ReadSelfWeaponEnchantName()
    local tip = DC_Aura and DC_Aura.GetScanTip and DC_Aura.GetScanTip()
    if not tip then
        return nil
    end
    local ok = pcall(function()
        tip:ClearLines()
        tip:SetInventoryItem("player", 16)
    end)
    if not ok then
        return nil
    end
    local n = 30
    if tip.NumLines then
        local ok2, v = pcall(tip.NumLines, tip)
        if ok2 and type(v) == "number" then
            n = v
        end
    end
    for i = 1, n do
        local obj = getglobal("DopingControlScanTipTextLeft" .. i)
        if obj then
            local name = G.ParseWeaponEnchantLine(obj:GetText())
            if name then
                return name
            end
        end
    end
    return nil
end

-- ReadUnit(unit) -> rawLinks table for G.Assemble.
-- Vanilla serves GetInventoryItemLink only for the own/inspected unit;
-- SuperWoW serves it for all friendly units (unverified beyond sight,
-- probe questions 4/5) -- either way nil slots are handled by the
-- EMPTY-vs-gearRead rule above, so no capability branching here.
function G.ReadUnit(unit)
    local raw = {}
    if GetInventoryItemLink then
        local slots = G.InvSlots()
        local n = table.getn(slots)
        for i = 1, n do
            local ok, link = pcall(GetInventoryItemLink, unit, slots[i])
            if ok and link then
                raw[slots[i]] = link
            end
        end
    end
    return raw
end
