local pluginName, pluginShared = ...
-- The route: the raids, dungeons and single items the player wants next. They sit under Routing at
-- the top of the Raid Lockouts panel, nearest first, and the guide walks them one after another. A
-- raid stays on the route through weekly resets until the player takes it off or every starred item
-- in it is collected; a single item leaves the route when it is collected.
local RouteList = {}
pluginShared.RouteList = RouteList

RouteList.ICON = "Interface\\AddOns\\AppearanceMatic\\Media\\Textures\\add-to-route.tga"
RouteList.ICON_DIM = "Interface\\AddOns\\AppearanceMatic\\Media\\Textures\\add-to-route-dim.tga"

function RouteList:EnsureDB()
    AppearanceMaticDB = AppearanceMaticDB or {}
    local db = AppearanceMaticDB.routeList
    if type(db) ~= "table" then
        db = { seq = 0, appearances = {}, mounts = {}, raids = {} }
        AppearanceMaticDB.routeList = db
    end
    db.seq = tonumber(db.seq) or 0
    db.appearances = db.appearances or {}
    db.mounts = db.mounts or {}
    db.raids = db.raids or {}
    return db
end

local function bucket(db, isMount)
    return isMount and db.mounts or db.appearances
end

-- The panel asks for a route position once per starred row, several hundred times per rebuild. The
-- order only changes when the route itself changes, so it is built once and kept until then.
local orderCache, positionCache, orderCacheFor

local function forgetOrder()
    orderCache, positionCache, orderCacheFor = nil, nil, nil
end

local function positionKey(isMount, id)
    return (isMount and "m:" or "a:") .. tostring(id)
end

local function notify()
    forgetOrder()
    local favorites = pluginShared.Favorites
    if favorites and favorites.Notify then
        favorites:Notify()
    end
end

-- isMount: true = mountID, false = appearance visualID
function RouteList:Has(isMount, id)
    return id ~= nil and bucket(self:EnsureDB(), isMount)[id] ~= nil
end

-- Adding also stars the item: the route is a subset of what the panel tracks.
function RouteList:Add(isMount, id)
    if not id then
        return false
    end
    local db = self:EnsureDB()
    local set = bucket(db, isMount)
    if set[id] then
        return false
    end
    local favorites = pluginShared.Favorites
    if favorites and favorites.AlreadyCollected and favorites:AlreadyCollected(isMount, id) then
        if favorites.SayCollected then
            favorites:SayCollected()
        end
        return false
    end
    db.seq = db.seq + 1
    set[id] = db.seq
    if favorites then
        if isMount then
            if favorites.MountIsFavorite and favorites.MountSet and not favorites:MountIsFavorite(id) then
                favorites:MountSet(id, true)
            end
        elseif favorites.IsFavorite and favorites.Set and not favorites:IsFavorite(id) then
            favorites:Set(id, true)
        end
    end
    notify()
    return true
end

function RouteList:Remove(isMount, id)
    if not id then
        return false
    end
    local set = bucket(self:EnsureDB(), isMount)
    if not set[id] then
        return false
    end
    set[id] = nil
    notify()
    return true
end

-- true when the item is now on the route
function RouteList:Toggle(isMount, id)
    if self:Has(isMount, id) then
        self:Remove(isMount, id)
        return false
    end
    -- Add refuses things already collected: say so with its answer, never a false success
    return self:Add(isMount, id)
end

-- Raids and dungeons go on the route by the name the panel lists them under.
function RouteList:HasRaid(raidName)
    return type(raidName) == "string" and self:EnsureDB().raids[raidName] ~= nil
end

function RouteList:AddRaid(raidName)
    if type(raidName) ~= "string" or raidName == "" then
        return false
    end
    local db = self:EnsureDB()
    if db.raids[raidName] then
        return false
    end
    db.seq = db.seq + 1
    db.raids[raidName] = db.seq
    notify()
    return true
end

function RouteList:RemoveRaid(raidName)
    local db = self:EnsureDB()
    if type(raidName) ~= "string" or not db.raids[raidName] then
        return false
    end
    db.raids[raidName] = nil
    notify()
    return true
end

-- true when the raid is now on the route
function RouteList:ToggleRaid(raidName)
    if self:HasRaid(raidName) then
        self:RemoveRaid(raidName)
        return false
    end
    return self:AddRaid(raidName)
end

-- Everything off the route at once
function RouteList:Clear()
    local db = self:EnsureDB()
    local had = next(db.appearances) ~= nil or next(db.mounts) ~= nil or next(db.raids) ~= nil
    db.appearances, db.mounts, db.raids = {}, {}, {}
    if had then
        notify()
    end
    return had
end

-- { { isMount = bool, id = n, seq = n } | { raid = name, seq = n }, ... } oldest first
function RouteList:Ordered()
    local db = self:EnsureDB()
    if orderCache and orderCacheFor == db then
        return orderCache
    end
    local out = {}
    for id, seq in pairs(db.appearances) do
        out[#out + 1] = { isMount = false, id = id, seq = seq }
    end
    for id, seq in pairs(db.mounts) do
        out[#out + 1] = { isMount = true, id = id, seq = seq }
    end
    for raidName, seq in pairs(db.raids) do
        out[#out + 1] = { raid = raidName, seq = seq }
    end
    table.sort(out, function(a, b) return a.seq < b.seq end)
    local places = {}
    for index, entry in ipairs(out) do
        if entry.raid ~= nil then
            places["r:" .. tostring(entry.raid)] = index
        else
            places[positionKey(entry.isMount, entry.id)] = index
        end
    end
    orderCache, positionCache, orderCacheFor = out, places, db
    return out
end

function RouteList:Count()
    return #self:Ordered()
end

function RouteList:First()
    return self:Ordered()[1]
end

-- Position of an item on the route by the order it was added (1 = oldest), nil when it is not on it
function RouteList:Position(isMount, id)
    if id == nil then
        return nil
    end
    self:Ordered()
    return positionCache and positionCache[positionKey(isMount and true or false, id)] or nil
end

function RouteList:RaidPosition(raidName)
    if raidName == nil then
        return nil
    end
    self:Ordered()
    return positionCache and positionCache["r:" .. tostring(raidName)] or nil
end
