--[[
Name: DRList-1.0
Description: Diminishing returns categorization. Fork of outdated DRData-1.0.
Website: https://github.com/wardz/DRList-1.0/
Documentation: https://wardz.github.io/DRList-1.0/
Dependencies: LibStub
License: MIT
]]

--- DRList-1.0
-- @module DRList-1.0
local MAJOR, MINOR = "DRList-1.0", 69 -- Don't forget to change this in Spells.lua aswell!
local Lib = assert(LibStub, MAJOR .. " requires LibStub."):NewLibrary(MAJOR, MINOR)
if not Lib then return end -- already loaded

local GetSpellName = C_Spell and C_Spell.GetSpellName or GetSpellInfo

Lib.L = {}

-------------------------------------------------------------------------------
-- Please see Curseforge localization page if you'd like to help translate:
-- https://www.curseforge.com/wow/addons/drlist-1-0/localization
local L = Lib.L
L["DISARMS"] = "Disarms"
L["DISORIENTS"] = "Disorients"
L["INCAPACITATES"] = "Incapacitates"
L["KNOCKBACKS"] = "Knockbacks"
L["ROOTS"] = "Roots"
L["SILENCES"] = "Silences"
L["STUNS"] = "Stuns"
L["TAUNTS"] = "Taunts"
L["FEARS"] = "Fears"
L["RANDOM_ROOTS"] = "Random roots"
L["RANDOM_STUNS"] = "Random stuns"
L["OPENER_STUN"] = "Opener stuns"
L["HORROR"] = "Horrors"
L["SCATTERS"] = "Scatters"
L["DEEP_FREEZE_ROF"] = "DF/RoF Shared"
L["MIND_CONTROL"] = GetSpellName(605) or "Mind Control"
L["FROST_SHOCK"] = GetSpellName(15089) or "Frost Shock"
L["KIDNEY_SHOT"] = GetSpellName(408) or "Kidney Shot"
L["DEATH_COIL"] = GetSpellName(28412) or "Death Coil"
L["UNSTABLE_AFFLICTION"] = GetSpellName(31117) or "Unstable Affliction"
L["CHASTISE"] = GetSpellName(44041) or "Chastise"
L["COUNTERATTACK"] = GetSpellName(19306) or "Counterattack"
L["BIND_ELEMENTAL"] = GetSpellName(76780) or "Bind Elemental"
L["CYCLONE"] = GetSpellName(33786) or "Cyclone"
L["CHARGE"] = GetSpellName(100) or "Charge"

-- luacheck: push ignore 542
local locale = GetLocale()
if locale == "deDE" then
    L["FEARS"] = "Furchteffekte"
    L["KNOCKBACKS"] = "Rückstoßeffekte"
    L["ROOTS"] = "Bewegungsunfähigkeitseffekte"
    L["SILENCES"] = "Stilleeffekte"
    L["STUNS"] = "Betäubungseffekte"
    L["TAUNTS"] = "Spotteffekte"
elseif locale == "frFR" then
    L["FEARS"] = "Peurs"
    L["KNOCKBACKS"] = "Projections"
    L["ROOTS"] = "Immobilisations"
    L["SILENCES"] = "Silences"
    L["STUNS"] = "Etourdissements"
    L["TAUNTS"] = "Provocations"
elseif locale == "koKR" then
    L["DISORIENTS"] = "방향 감각 상실"
    L["INCAPACITATES"] = "행동 불가"
    L["KNOCKBACKS"] = "밀쳐내기"
    L["ROOTS"] = "이동 불가"
    L["SILENCES"] = "침묵"
    L["STUNS"] = "기절"
elseif locale == "ruRU" then
    L["DISARMS"] = "Разоружение"
    L["DISORIENTS"] = "Дезориентация"
    L["FEARS"] = "Опасения"
    L["INCAPACITATES"] = "Паралич"
    L["KNOCKBACKS"] = "Отбрасывание"
    L["RANDOM_ROOTS"] = "Случайные корни"
    L["RANDOM_STUNS"] = "Случайные оглушения"
    L["ROOTS"] = "Сковывание"
    L["SILENCES"] = "Немота"
    L["STUNS"] = "Оглушение"
    L["TAUNTS"] = "Насмешки"
elseif locale == "esES" or locale == "esMX" then
    L["DISARMS"] = "Desarmar"
    L["DISORIENTS"] = "Desorientar"
    L["FEARS"] = "Miedos"
    L["INCAPACITATES"] = "Incapacitar"
    L["KNOCKBACKS"] = "Derribos"
    L["RANDOM_ROOTS"] = "Raíces aleatorias"
    L["RANDOM_STUNS"] = "Aturdir aleatorio"
    L["ROOTS"] = "Raíces"
    L["SILENCES"] = "Silencios"
    L["STUNS"] = "Aturdimientos"
    L["TAUNTS"] = "Provocaciones"
elseif locale == "zhCN" then
    L["DISARMS"] = "缴械"
    L["DISORIENTS"] = "迷惑"
    L["FEARS"] = "恐惧"
    L["INCAPACITATES"] = "瘫痪"
    L["KNOCKBACKS"] = "击退"
    L["RANDOM_ROOTS"] = "随机定身"
    L["RANDOM_STUNS"] = "随机眩晕"
    L["ROOTS"] = "定身"
    L["SILENCES"] = "沉默"
    L["STUNS"] = "昏迷"
    L["TAUNTS"] = "嘲讽"
elseif locale == "zhTW" then
    L["DISARMS"] = "繳械"
    L["DISORIENTS"] = "迷惑"
    L["FEARS"] = "恐懼"
    L["INCAPACITATES"] = "癱瘓"
    L["KNOCKBACKS"] = "擊退"
    L["RANDOM_ROOTS"] = "隨機定身"
    L["RANDOM_STUNS"] = "隨機昏迷"
    L["ROOTS"] = "定身"
    L["SILENCES"] = "沉默"
    L["STUNS"] = "昏迷"
    L["TAUNTS"] = "嘲諷"
end
-- luacheck: pop
-------------------------------------------------------------------------------

-- Check what game version we're running
Lib.gameExpansion = ({
    [WOW_PROJECT_MAINLINE] = "retail",
    [WOW_PROJECT_CLASSIC] = "classic",
    [WOW_PROJECT_BURNING_CRUSADE_CLASSIC or 5] = "tbc",
    [WOW_PROJECT_WRATH_CLASSIC or 11] = "wotlk",
    [WOW_PROJECT_CATACLYSM_CLASSIC or 14] = "cata",
})[WOW_PROJECT_ID] or "cata" -- Fallback to cata when unknown ID (most likely a new classic expansion build)

-- How long it takes for a DR to expire, in seconds.
Lib.resetTimes = {
    retail = {
        ["default"] = 18.5, -- static 18 sec + 0.5 latency
        ["npc"] = 20, -- Against mobs it seems to still be dynamic
        ["knockback"] = 10.5, -- Knockbacks are immediately immune and only DRs for 10s
    },

    classic = {
        ["default"] = 20, -- dynamic between 15 and 20s
        ["npc"] = 20,
    },

    tbc = {
        ["default"] = 20,
        ["npc"] = 20,
    },

    wotlk = {
        ["default"] = 20,
        ["npc"] = 20,
    },

    cata = {
        ["default"] = 20,
        ["npc"] = 20,
    },
}

-- List of all DR categories, english -> localized.
Lib.categoryNames = {
    --- @table categoryNames.retail
    retail = {
        ["disorient"] = L.DISORIENTS,
        ["incapacitate"] = L.INCAPACITATES,
        ["silence"] = L.SILENCES,
        ["stun"] = L.STUNS,
        ["root"] = L.ROOTS,
        ["disarm"] = L.DISARMS,
        ["taunt"] = L.TAUNTS,
        ["knockback"] = L.KNOCKBACKS,
    },

    --- @table categoryNames.classic
    classic = {
        ["incapacitate"] = L.INCAPACITATES,
        ["stun"] = L.STUNS,
        ["root"] = L.ROOTS,
        ["random_stun"] = L.RANDOM_STUNS,
        ["random_root"] = L.RANDOM_ROOTS,
        ["fear"] = L.FEARS,
        ["mind_control"] = L.MIND_CONTROL,
        ["frost_shock"] = L.FROST_SHOCK,
        ["kidney_shot"] = L.KIDNEY_SHOT,
    },

    --- @table categoryNames.tbc
    tbc = {
        ["disorient"] = L.DISORIENTS,
        ["incapacitate"] = L.INCAPACITATES,
        ["stun"] = L.STUNS,
        ["random_stun"] = L.RANDOM_STUNS,
        ["random_root"] = L.RANDOM_ROOTS,
        ["root"] = L.ROOTS,
        ["disarm"] = L.DISARMS,
        ["fear"] = L.FEARS,
        ["scatter"] = L.SCATTERS,
        ["mind_control"] = L.MIND_CONTROL,
        ["kidney_shot"] = L.KIDNEY_SHOT,
        ["death_coil"] = L.DEATH_COIL,
        ["unstable_affliction"] = L.UNSTABLE_AFFLICTION,
        ["chastise"] = L.CHASTISE,
        ["counterattack"] = L.COUNTERATTACK,
    },

    --- @table categoryNames.wotlk
    wotlk = {
        ["incapacitate"] = L.INCAPACITATES,
        ["stun"] = L.STUNS,
        ["random_stun"] = L.RANDOM_STUNS,
        ["random_root"] = L.RANDOM_ROOTS,
        ["root"] = L.ROOTS,
        ["disarm"] = L.DISARMS,
        ["fear"] = L.FEARS,
        ["scatter"] = L.SCATTERS,
        ["silence"] = L.SILENCES,
        ["horror"] = L.HORROR,
        ["mind_control"] = L.MIND_CONTROL,
        ["cyclone"] = L.CYCLONE,
        ["charge"] = L.CHARGE,
        ["opener_stun"] = L.OPENER_STUN,
        ["counterattack"] = L.COUNTERATTACK,
        ["taunt"] = L.TAUNTS,
    },

    --- @table categoryNames.cata
    cata = {
        ["incapacitate"] = L.INCAPACITATES,
        ["stun"] = L.STUNS,
        ["random_stun"] = L.RANDOM_STUNS,
        ["random_root"] = L.RANDOM_ROOTS,
        ["root"] = L.ROOTS,
        ["disarm"] = L.DISARMS,
        ["fear"] = L.FEARS,
        ["scatter"] = L.SCATTERS,
        ["silence"] = L.SILENCES,
        ["horror"] = L.HORROR,
        ["mind_control"] = L.MIND_CONTROL,
        ["cyclone"] = L.CYCLONE,
        ["counterattack"] = L.COUNTERATTACK,
        ["bind_elemental"] = L.BIND_ELEMENTAL,
        ["deep_freeze_rof"] = L.DEEP_FREEZE_ROF,
        ["taunt"] = L.TAUNTS,
    },
}

-- Categories that have DR against normal mobs.
Lib.categoriesPvE = {
    retail = {
        ["taunt"] = L.TAUNTS,
        ["stun"] = L.STUNS,
    },

    classic = {
        ["stun"] = L.STUNS,
        ["kidney_shot"] = L.KIDNEY_SHOT,
    },

    tbc = {
        ["stun"] = L.STUNS,
        ["random_stun"] = L.RANDOM_STUNS,
        ["kidney_shot"] = L.KIDNEY_SHOT,
    },

    wotlk = {
        ["taunt"] = L.TAUNTS,
        ["stun"] = L.STUNS,
        ["random_stun"] = L.RANDOM_STUNS,
        ["opener_stun"] = L.OPENER_STUN,
    },

    cata = {
        ["taunt"] = L.TAUNTS,
        ["stun"] = L.STUNS,
        ["random_stun"] = L.RANDOM_STUNS,
        ["cyclone"] = L.CYCLONE, -- TODO: check me cata+wrath
    },
}

-- Successives diminished durations
Lib.diminishedDurations = {
    retail = {
        -- Decreases by 50%, immune at the 4th application
        ["default"] = { 0.50, 0.25 },
        -- Decreases by 35%, immune at the 5th application
        ["taunt"] = { 0.65, 0.42, 0.27 },
        -- Immediately immune
        ["knockback"] = {},
    },

    classic = {
        ["default"] = { 0.50, 0.25 },
    },

    tbc = {
        ["default"] = { 0.50, 0.25 },
    },

    wotlk = {
        ["default"] = { 0.50, 0.25 },
        ["taunt"] = { 0.65, 0.42, 0.27 },
    },

    cata = {
        ["default"] = { 0.50, 0.25 },
        ["taunt"] = { 0.65, 0.42, 0.27 },
    },
}

-------------------------------------------------------------------------------
-- Public API
-------------------------------------------------------------------------------

--- Get table of all spells that DRs.
-- Key is the spellID, and value is the unlocalized DR category string.
-- Value is instead a table of strings for spells that have shared DRs.
-- @see GetCategoryBySpellID
-- @treturn table {number=string|table}
function Lib:GetSpells()
    return Lib.spellList
end

--- Get table of all DR categories.
-- Key is unlocalized name used for API functions, value is localized name used for UI.
-- Note: the 'taunt' category should be ignored for players, but not player pets.
-- @treturn table {string=string}
function Lib:GetCategories()
    return Lib.categoryNames[Lib.gameExpansion]
end

--- Get table of all categories that DRs in PvE.
-- Key is unlocalized name used for API functions, value is localized name used for UI.
-- Note that for retail some special mobs have DR on all categories, you need to check for this yourself;
-- see UnitClassification() and UnitIsQuestBoss(). Player pets have DR on all categories.
-- Tip: you can combine :GetPvECategories() and :IterateSpellsByCategory() to get spellIDs only for PvE aswell.
-- @treturn table {string=string}
function Lib:GetPvECategories()
    return Lib.categoriesPvE[Lib.gameExpansion]
end

--- Get constant for how long a DR lasts for a given category.
-- @tparam[opt="default"] string category Unlocalized category name, or "npc" for PvE timer.
-- @treturn number Seconds before DR resets.
function Lib:GetResetTime(category)
    return Lib.resetTimes[Lib.gameExpansion][category or "default"] or Lib.resetTimes[Lib.gameExpansion].default
end

local type = _G.type -- GetCategoryBySpellID() is ran frequently from the CLEU so might aswell upvalue this

--- Get unlocalized DR category by spell ID.
-- This is the main checker for if a spell has a DR.
-- See https://github.com/wardz/DRList-1.0/wiki/Example-Usage for full example usage.
-- Note: the 'taunt' category should be ignored for players, but not player pets.
-- @tparam number spellID
-- @treturn ?string The category name.
-- @treturn ?{string,...} Read-only array with multiple categories if spellID has any shared DR categories. (Note: array includes main category too)
function Lib:GetCategoryBySpellID(spellID)
    local category = Lib.spellList[spellID]
    if category and type(category) == "table" then
        return category[1], category
    end

    return category
end

--- Get localized category from unlocalized category name, case sensitive.
-- @tparam string category Unlocalized category name
-- @treturn ?string|nil The localized category name.
function Lib:GetCategoryLocalization(category)
    return Lib.categoryNames[Lib.gameExpansion][category]
end

--- Check if a category has DR against mobs.
-- Note that for retail some special mobs have DR on all categories, you need to check for this yourself;
-- see UnitClassification() and UnitIsQuestBoss(). Player pets have DR on all categories.
-- @tparam string category Unlocalized category name
-- @treturn bool
function Lib:IsPvECategory(category)
    return Lib.categoriesPvE[Lib.gameExpansion][category] and true or false -- make sure bool is always returned here
end

--- Get a specific diminished duration value.
-- @tparam number diminished How many times the DR has been applied so far
-- @tparam[opt="default"] string category Unlocalized category name
-- @usage local duration = DRList:GetNextDR(1) -- 0.50 (half aura duration)
-- @usage local duration = DRList:GetNextDR(2) -- 0.25 (quarter aura duration)
-- @usage local duration = DRList:GetNextDR(3) -- 0.00 (zero aura duration / immune)
-- @usage local duration = DRList:GetNextDR(1, "knockback") -- 0.00 (immediately immune)
-- @treturn number Diminished duration value or 0 for invalid arguments
function Lib:GetNextDR(diminished, category)
    local durations = Lib.diminishedDurations[Lib.gameExpansion][category or "default"]
    if not durations and Lib.categoryNames[Lib.gameExpansion][category] then
        -- Redirect to default when "stun", "root" etc is passed
        durations = Lib.diminishedDurations[Lib.gameExpansion]["default"]
    end

    return durations and durations[diminished] or 0
end

--- Get the next successive diminished duration value.
-- @tparam number diminished The current diminished duration value
-- @tparam[opt="default"] string category Unlocalized category name
-- @usage
-- local diminished = 1.0 -- initial full aura duration
-- diminished = DRList:NextDR(diminished, "stun") -- 0.50 (half aura duration)
-- diminished = DRList:NextDR(diminished, "stun") -- 0.25 (quarter aura duration)
-- diminished = DRList:NextDR(diminished, "stun") -- 0.00 (zero aura duration / immune)
-- @treturn number Diminished duration value
function Lib:NextDR(diminished, category)
    local durations = Lib.diminishedDurations[Lib.gameExpansion][category or "default"] or Lib.diminishedDurations[Lib.gameExpansion].default

    for i = 1, #durations do
        if diminished > durations[i] then
            return durations[i]
        end
    end

    return 0
end

do
    local next = _G.next

    local function CategoryIterator(category, index)
        local spellList, newCategory = Lib.spellList
        repeat
            index, newCategory = next(spellList, index)
            if index then
                if newCategory == category then
                    return index, category
                elseif type(newCategory) == "table" then
                    for i = 1, #newCategory do
                        if newCategory[i] == category then
                            return index, category
                        end
                    end
                end
            end
        until not index
    end

    --- Iterate through the spells of a given category.
    -- Pass "nil" to iterate through all spells instead.
    -- Note: a spell might be returned multiple times here due to spell ranks in classic, check its name to avoid duplicates.
    -- @tparam string|nil category Unlocalized category name
    -- @usage for spellID in DRList:IterateSpellsByCategory("root") do print(spellID) end
    -- @return Iterator function
    function Lib:IterateSpellsByCategory(category)
        if category then
            return CategoryIterator, category
        else
            return next, Lib.spellList
        end
    end
end

-- keep same API as DRData-1.0 for easier transitions
Lib.GetCategoryName = Lib.GetCategoryLocalization
Lib.IsPVE = Lib.IsPvECategory
Lib.GetSpellCategory = Lib.GetCategoryBySpellID
Lib.IterateSpells = Lib.IterateSpellsByCategory
--Lib.IterateProviders = Lib.IterateSpellsByCategory -- OBSOLETE
--Lib.GetProviders = Lib.GetSpells() -- OBSOLETE
Lib.RESET_TIME = Lib.resetTimes[Lib.gameExpansion].default
Lib.pveDR = Lib.categoriesPvE
