local S, L, O, U, D, E = unpack((select(2, ...)));

-- Lua API
local pairs, ipairs, select, type, unpack, tostring, tonumber, string_format, string_find, string_len, string_sub, string_gsub, string_byte, math_floor = pairs, ipairs, select, type, unpack, tostring, tonumber, string.format, string.find, string.len, string.sub, string.gsub, string.byte, math.floor;

-- WoW/Lua API
local strsplit = strsplit;

-- WoW API
local UnitClassBase, UnitDetailedThreatSituation, UnitThreatPercentageOfLead, UnitIsTapDenied, UnitIsDeadOrGhost, UnitIsConnected, UnitIsPlayer, UnitPlayerControlled, UnitSelectionColor, GetPlayerInfoByGUID =
      UnitClassBase, UnitDetailedThreatSituation, UnitThreatPercentageOfLead, UnitIsTapDenied, UnitIsDeadOrGhost, UnitIsConnected, UnitIsPlayer, UnitPlayerControlled, UnitSelectionColor, GetPlayerInfoByGUID;
local UnitLevel, UnitEffectiveLevel, UnitGUID, UnitAffectingCombat, UnitClassification, UnitTreatAsPlayerForDisplay =
      UnitLevel, UnitEffectiveLevel, UnitGUID, UnitAffectingCombat, UnitClassification, UnitTreatAsPlayerForDisplay;
local UnitGroupRolesAssigned, GetSpecialization, GetSpecializationRole, UnitCastingInfo, UnitChannelInfo =
      UnitGroupRolesAssigned, GetSpecialization, GetSpecializationRole, UnitCastingInfo, UnitChannelInfo;
local GetQuestDifficultyColor = GetQuestDifficultyColor;
local IsInGuild, GetGuildInfo = IsInGuild, GetGuildInfo;
local IsActiveBattlefieldArena, GetZonePVPInfo,       IsInInstance, UnitInBattleground, C_Map_GetBestMapForUnit =
      IsActiveBattlefieldArena, C_PvP.GetZonePVPInfo, IsInInstance, UnitInBattleground, C_Map.GetBestMapForUnit
local C_Spell_GetSpellInfo, C_Spell_GetSpellName, IsSpellKnown, IsSpellKnownOrOverridesKnown, IsPlayerSpell = C_Spell.GetSpellInfo, C_Spell.GetSpellName, IsSpellKnown, IsSpellKnownOrOverridesKnown, IsPlayerSpell;
local CreateColor = CreateColor;

-- WoW C API
local C_MythicPlus_GetCurrentAffixes, C_ChallengeMode_GetActiveKeystoneInfo = C_MythicPlus.GetCurrentAffixes, C_ChallengeMode.GetActiveKeystoneInfo;

U.Print = function(message, debug)
    if not message or message == '' then
        return;
    end

    if debug and S.Debug then
        print(string_format('%s | DEBUG | %s', S.Media.GRADIENT_NAME, message));
    else
        print(string_format('%s | %s', S.Media.GRADIENT_NAME, message));
    end
end

U.CanAccessObject = function(obj)
    return issecure() or not obj:IsForbidden();
end

U.PlayerInCombat = function()
    return (InCombatLockdown() or (UnitAffectingCombat('player') or UnitAffectingCombat('pet')));
end

U.UnitIsTapped = function(unit)
    return not UnitPlayerControlled(unit) and UnitIsTapDenied(unit);
end

U.UnitIsOnThreatListWithPlayer = function(unit)
    local _, threatStatus = UnitDetailedThreatSituation('player', unit);
    return threatStatus ~= nil;
end

U.IsPlayer = function(unit)
    return UnitIsPlayer(unit) or UnitTreatAsPlayerForDisplay(unit);
end

U.IsRealPlayer = function(unit)
    return UnitIsPlayer(unit);
end

U.UnitIsPet = function(unit)
    return (not UnitIsPlayer(unit) and UnitPlayerControlled(unit));
end

U.UnitIsPetByGUID = function(guid)
    local unitType = strsplit('-', guid);
    return unitType == 'Pet';
end

U.PlayerInGuild = function()
    return IsInGuild() and GetGuildInfo('player');
end

U.UnitInGuild = function(unit)
    return GetGuildInfo(unit);
end

U.UnitIsCasting = function(unit)
    if not unit then
        return;
    end

    local _, _, _, _, _, _, _, notInterruptible, spellId = UnitCastingInfo(unit);

    if not spellId then
        _, _, _, _, _, _, notInterruptible, spellId = UnitChannelInfo(unit);
    end

    return spellId, notInterruptible;
end

U.IsPlayerEffectivelyTank = function()
    local assignedRole = UnitGroupRolesAssigned('player');

    if assignedRole == 'NONE' then
        local spec = GetSpecialization();
        return spec and GetSpecializationRole(spec) == 'TANK';
    end

    return assignedRole == 'TANK';
end

U.GetPlayerRole = function()
    local assignedRole = UnitGroupRolesAssigned('player');

    if assignedRole == 'NONE' then
        local spec = GetSpecialization();
        return spec and GetSpecializationRole(spec);
    end

    return assignedRole;
end

U.IsInArena = function()
    if IsActiveBattlefieldArena() or GetZonePVPInfo() == 'arena' or select(2, IsInInstance()) == 'arena' or (UnitInBattleground('player') and (C_Map_GetBestMapForUnit('player') and C_Map_GetBestMapForUnit('player') < 0)) then
        return true;
    else
        return false;
    end
end

U.GetUnitThreatSituationStatus = function(unit)
    if not unit then
        return;
    end

    local isTanking, status, threatpct = UnitDetailedThreatSituation('player', unit);
    local display = threatpct;

    if isTanking then
        local lead = UnitThreatPercentageOfLead('player', unit);
        display = lead == 0 and 100 or lead;
    end

    return display, status, isTanking;
end

do
    local NAMES_CACHE = {};
    local SUBLABELS_CACHE = {};
    local UNIT_CREATURE_LINK = 'unit:Creature-0-0-0-0-%d';
    local UNKNOWN = UNKNOWN;

    U.GetNpcNameByID = function(id)
        if not id or id == 0 then
            return UNKNOWN;
        end

        if NAMES_CACHE[id] then
            return NAMES_CACHE[id];
        end

        local name = UNKNOWN;

        local tooltipData = C_TooltipInfo.GetHyperlink(string_format(UNIT_CREATURE_LINK, id));

        if tooltipData then
            local line = tooltipData.lines and tooltipData.lines[1];

            if line then
                name = line.leftText or UNKNOWN;
            end

            if name ~= '' and name ~= UNKNOWN then
                NAMES_CACHE[id] = name;
            end
        end

        return name;
    end

    U.GetNpcSubLabelByID = function(id)
        if not id or id == 0 then
            return;
        end

        if SUBLABELS_CACHE[id] then
            return SUBLABELS_CACHE[id];
        end

        local sublabel;

        local tooltipData = C_TooltipInfo.GetHyperlink(string_format(UNIT_CREATURE_LINK, id));

        if tooltipData then
            local line = tooltipData.lines and tooltipData.lines[2];

            if line then
                sublabel = line and line.leftText;
            end
        end

        if not sublabel or string_find(sublabel or '', '??', 1, true) then
            SUBLABELS_CACHE[id] = nil;
        else
            SUBLABELS_CACHE[id] = sublabel;
        end

        return SUBLABELS_CACHE[id];
    end

end

U.GetNpcID = function(unit)
    return tonumber((select(6, strsplit('-', UnitGUID(unit) or ''))));
end

U.GetNpcIDByGUID = function(guid)
    return tonumber((select(6, strsplit('-', guid or ''))));
end

do
    local CACHE = {};

    U.CreateColor = function(r, g, b, a)
        a = a or 1;

        local cacheKey = string_format('%s%s%s%s', r, g, b, a);

        if CACHE[cacheKey] then
            return CACHE[cacheKey];
        end

        local color = CreateColor(r, g, b, a);

        CACHE[cacheKey] = color;

        return color;
    end
end

do
    local LCN = {};

    for k, v in pairs(LOCALIZED_CLASS_NAMES_MALE) do
        LCN[v] = k;
    end

    for k, v in pairs(LOCALIZED_CLASS_NAMES_FEMALE) do
        LCN[v] = k;
    end

    local defaultClassColor = U.CreateColor(0.8, 0.8, 0.8);

    U.GetClassColor = function(class, str)
        if not class then
            class = UnitClassBase('player');
        elseif not RAID_CLASS_COLORS[class] then
            if LCN[class] then
                class = LCN[class];
            else
                local _;
                _, class = UnitClass(class);
            end
        end

        if O.db.health_bar_color_class_use then
            class = O.db['health_bar_color_class_' .. (class or '')];
        else
            class = (CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS)[class];
        end

        if not class then
            class = defaultClassColor;
        end

        if str == 2 then
            if class[1] then
                return class[1], class[2], class[3];
            else
                return class.r, class.g, class.b;
            end
        elseif str then
            if class[1] then
                return string_format('%02x%02x%02x', class[1] * 255, class[2] * 255, class[3] * 255);
            else
                return string_format('%02x%02x%02x', class.r * 255, class.g * 255, class.b * 255);
            end
        else
            return class;
        end
    end
end

do
    local reusableUnitColorTable = {
        r = 1,
        g = 1,
        b = 1,
    };

    U.GetUnitColor = function(unit, str)
        local r, g, b;

        if UnitIsTapDenied(unit) or UnitIsDeadOrGhost(unit) or not UnitIsConnected(unit) then
            r, g, b = 0.5, 0.5, 0.5;
        else
            if UnitIsPlayer(unit) or U.UnitIsPet(unit) then
                return U.GetClassColor(unit, str);
            else
                r, g, b = UnitSelectionColor(unit);
            end
        end

        if str == 2 then
            return r, g, b;
        elseif str then
            return string_format('%02x%02x%02x', r * 255, g * 255, b * 255);
        else
            reusableUnitColorTable.r = r;
            reusableUnitColorTable.g = b;
            reusableUnitColorTable.b = b;

            return reusableUnitColorTable;
        end
    end
end

U.GetClassColorByGUID = function(guid, str)
    return U.GetClassColor(select(2, GetPlayerInfoByGUID(guid)), str);
end

U.GetUnitNameByGUID = function(guid, withRealm, realmDelimiter)
    if withRealm then
       local name, realm = select(6, GetPlayerInfoByGUID(guid));

       if realm and realm ~= '' then
           return name .. (realmDelimiter or '-') .. realm;
       end

       return name .. (realmDelimiter or '-') .. D.Player.RealmNormalized;
    end

    return (select(6, GetPlayerInfoByGUID(guid)));
end

do
    local ClassificationTable = {
        elite     = { '+',  'elite'      },
        rare      = { 'r',  'rare'       },
        rareelite = { 'r+', 'rare elite' },
        worldboss = { 'b',  'boss'       },
    };

    U.GetUnitLevel = function(unit, long, real)
        if not unit then
            return '??', '', GetQuestDifficultyColor(1);
        end

        local level = real and UnitLevel(unit) or UnitEffectiveLevel(unit);
        local classification = UnitClassification(unit);
        local diff = GetQuestDifficultyColor(level <= 0 and 999 or level);

        if ClassificationTable[classification] then
            classification = long and ClassificationTable[classification][2] or ClassificationTable[classification][1];
        else
            classification = '';
        end

        if level == -1 then
            level = '??';
        end

        return level, classification, diff;
    end
end

U.GetUnitArenaId = function(unit)
    for i = 1, GetNumArenaOpponents() do
        if UnitIsUnit(unit, 'arena' .. i) or UnitIsUnit(unit, 'arenapet' .. i) then
            return i;
        end
    end
end

U.IsAffixCurrent = function(affixID)
    local currentAffixes = C_MythicPlus_GetCurrentAffixes();

    if currentAffixes then
        for _, affix in ipairs(currentAffixes) do
            if affix.id == affixID then
                return true;
            end
        end
    end

    return false;
end

U.IsAffixActive = function(affixID)
    local _, affixes = C_ChallengeMode_GetActiveKeystoneInfo();

    if affixes then
        for i = 1, #affixes do
            if affixes[i] == affixID then
                return true;
            end
        end
    end

    return false;
end

U.UnitHasAura = function(unit, spellIdOrTable, checkFunction)
    local Cache = S:GetModule('Auras_Cache');

    if not Cache then
        return;
    end

    if checkFunction then
        local allAuras = Cache:GetAll(unit);

        if allAuras then
            for _, aura in pairs(allAuras) do
                if checkFunction(aura) then
                    return aura;
                end
            end
        end
    else
        local isTable = type(spellIdOrTable) == 'table';

        if isTable then
            local allAuras = Cache:GetAll(unit);

            if allAuras then
                for _, aura in pairs(allAuras) do
                    if spellIdOrTable[aura.spellId] then
                        return aura;
                    end
                end
            end
        else
            local aura = Cache:Get(unit, spellIdOrTable);

            return aura;
        end
    end
end

U.IsSpellKnown = function(spellId)
    return IsSpellKnown(spellId) or IsSpellKnownOrOverridesKnown(spellId) or IsPlayerSpell(spellId);
end

--[[
    IsPlayerSpell(spellId), IsSpellKnown(spellId), IsSpellKnownOrOverridesKnown(spellId)
    Incorrectly returned false for some spells
    FindSpellOverrideByID, FindBaseSpellByID -- to not forget
]]

U.GetTrulySpellId = function(spellId)
    -- here we extract the spell name and then get needed spellId by spell name
    local spellName = C_Spell_GetSpellName(spellId);
    local spellInfo = spellName and C_Spell_GetSpellInfo(spellName);

    return spellInfo.spellID;
end

U.GetInterruptSpellId = function()
    if D.KickByClassId[D.Player.ClassId] then
        return D.KickByClassId[D.Player.ClassId][D.Player.SpecIndex];
    end

    -- Warlock logic
    local spellId;

    if D.Player.SpecIndex == 1 or D.Player.SpecIndex == 3 then -- Aff & Destro
        if IsSpellKnown(19647, true) then -- Felhunter
            spellId = 119910;
        elseif IsPlayerSpell(108503) then
            spellId = 132409;
        end
    elseif D.Player.SpecIndex == 2 then -- Demo
        if IsSpellKnown(89766, true) then -- Felguard
            spellId = 119914;
        elseif IsSpellKnown(19647, true) then -- Felhunter
            spellId = 119910;
        end
    end

    return spellId;
end

do
    local bestIcon = {};

    U.GetIconFromSpellCache = function(name)
        if not name then
            return;
        end

        if bestIcon[name] then
            return bestIcon[name];
        end

        local icons = StripesSpellDB and StripesSpellDB.data and StripesSpellDB.data[name];
        local bestMatch = nil;

        if icons and icons.spells then
            for spell, icon in icons.spells:gmatch('(%d+)=(%d+)') do
                local spellId = tonumber(spell);

                if not bestMatch or (spellId and IsSpellKnown(spellId)) then
                    bestMatch = tonumber(icon);
                end
            end
        end

        bestIcon[name] = bestMatch;

        return bestIcon[name];
    end
end

U.IsInInstance = function()
    local inInstance, instanceType = IsInInstance();

    if inInstance or not (instanceType == 'none') then
        return true;
    end

    return false;
end

-- Colors
U.RGB2HEX = function(r, g, b)
    if type(r) == 'table' then
        if r.r then
            r, g, b = r.r, r.g, r.b;
        else
            r, g, b = unpack(r);
        end
    end

    if not r or not g or not b then
        r, g, b = 1, 1, 1;
    end

    return string_format('%02x%02x%02x', r * 255, g * 255, b * 255);
end

U.RGB2CFFHEX = function(r, g, b)
    if type(r) == 'table' then
        if r.r then
            r, g, b = r.r, r.g, r.b;
        else
            r, g, b = unpack(r);
        end
    end

    if not r or not g or not b then
        r, g, b = 1, 1, 1;
    end

    return string_format('|cff%02x%02x%02x', r * 255, g * 255, b * 255);
end

U.HEX2RGB = function(hex)
    hex = string_gsub(hex, '#', '');

    if string_len(hex) == 3 then
        return (tonumber('0x' .. string_sub(hex, 1, 1)) * 17) / 255, (tonumber('0x' .. string_sub(hex, 2, 2)) * 17) / 255, (tonumber('0x' .. string_sub(hex, 3, 3)) * 17) / 255;
    else
        return tonumber('0x' .. string_sub(hex, 1, 2)) / 255, tonumber('0x' .. string_sub(hex, 3, 4)) / 255, tonumber('0x' .. string_sub(hex, 5, 6)) / 255;
    end
end

-- Math
U.ShortValue = function(num)
    num = tonumber(num);

    if not num then
        return;
    end

    if num < 1000 then
        return math_floor(num);
    elseif num >= 1000000000000 then
        return string_format('%.3ft', num/1000000000000);
    elseif num >= 1000000000 then
        return string_format('%.3fb', num/1000000000);
    elseif num >= 1000000 then
        return string_format('%.2fm', num/1000000);
    elseif num >= 1000 then
        return string_format('%.1fk', num/1000);
    end
end

U.NUMBER_SEPARATOR = ',';

do
    local NUMBER_SEPARATOR = U.NUMBER_SEPARATOR;

    U.LargeNumberFormat = function(amount, separator)
        amount = tostring(amount);
        separator = separator or NUMBER_SEPARATOR;

        local newDisplay = '';
        local strlen = string_len(amount);

        for i = 4, strlen, 3 do
            newDisplay = separator .. string_sub(amount, -(i - 1), -(i - 3)) .. newDisplay;
        end

        return string_sub(amount, 1, (strlen % 3 == 0) and 3 or (strlen % 3)) .. newDisplay;
    end
end

-- String
do
    local function chsize(char)
        if not char then
            return 0;
        elseif char > 240 then
            return 4;
        elseif char > 225 then
            return 3;
        elseif char > 192 then
            return 2;
        else
            return 1;
        end
    end

    U.UTF8SUB = function(str, startChar, numChars)
        numChars = numChars or #str;

        local startIndex = 1;

        while startChar > 1 do
            local char = string_byte(str, startIndex);

            startIndex = startIndex + chsize(char);
            startChar = startChar - 1;
        end

        local currentIndex = startIndex;

        while numChars > 0 and currentIndex <= #str do
            local char = string_byte(str, currentIndex);

            currentIndex = currentIndex + chsize(char);
            numChars = numChars - 1;
        end

        return string_sub(str, startIndex, currentIndex - 1);
    end
end

if S.ClientLocale == 'ruRU' then
    U.FirstToUpper = function(str)
        return str:gsub('^[%z\1-\127\194-\244][\128-\191]', string.upper);
    end

    U.FirstToLower = function(str)
        return str:gsub('^[%z\1-\127\194-\244][\128-\191]', string.lower);
    end
else
    U.FirstToUpper = function(str)
        return (str:gsub('^%l', string.upper));
    end

    U.FirstToLower = function(str)
        return (str:gsub('^%u', string.lower));
    end
end

-- Table
U.TableCount = function(tbl)
    local count = 0;

    for _, _ in pairs(tbl) do
        count = count + 1;
    end

    return count;
end

U.DeepCopy = function(orig)
    local orig_type = type(orig);
    local copy;

    if orig_type == 'table' then
        copy = {};

        for orig_key, orig_value in next, orig, nil do
            copy[U.DeepCopy(orig_key)] = U.DeepCopy(orig_value);
        end

        setmetatable(copy, U.DeepCopy(getmetatable(orig)));
    else -- number, string, boolean, etc
        copy = orig;
    end

    return copy;
end

U.Merge = function(src, dst)
    if type(src) ~= 'table' then
        return {};
    end

    if type(dst) ~= 'table' then
        dst = {};
    end

    for k, v in pairs(src) do
        if type(v) == 'table' then
            dst[k] = U.Merge(v, dst[k]);
        elseif type(v) ~= type(dst[k]) then
            dst[k] = v;
        end
    end

    return dst;
end

local LCG = S.Libraries.LCG;
U.GlowStart = function(frame, glowType, glowColor, glowKey)
    if glowType == 1 then
        LCG.PixelGlow_Start(frame, glowColor, nil, nil, nil, nil, nil, nil, nil, glowKey);
    elseif glowType == 2 then
        LCG.AutoCastGlow_Start(frame, glowColor, nil, nil, nil, nil, nil, glowKey);
    elseif glowType == 3 then
        LCG.ButtonGlow_Start(frame, glowColor);
    end
end

U.GlowStopType = function(frame, glowType, glowKey)
    if glowType == 1 then
        LCG.PixelGlow_Stop(frame, glowKey);
    elseif glowType == 2 then
        LCG.AutoCastGlow_Stop(frame, glowKey);
    elseif glowType == 3 then
        LCG.ButtonGlow_Stop(frame);
    end
end

U.GlowStopAll = function(frame, glowKey)
    LCG.PixelGlow_Stop(frame, glowKey);
    LCG.AutoCastGlow_Stop(frame, glowKey);
    LCG.ButtonGlow_Stop(frame);
end

U.AutoAdjustFontSize = function(fontString, fontSizeStep, fontSizeMinLimit)
    if not fontString or fontString.autoFontSize then
        return;
    end

    fontSizeStep = fontSizeStep or 0.25;
    fontSizeMinLimit = math.max(3, (fontSizeMinLimit or 3));

    hooksecurefunc(fontString, 'SetText', function(self, text)
        local fontValue, fontSize, fontOutline = self:GetFont();

        if fontSize <= fontSizeMinLimit then
            return;
        end

        if self:IsTruncated() then
            self:SetFont(fontValue, math.max(fontSizeMinLimit, fontSize - fontSizeStep), fontOutline);
            self:SetText(text);
        end
    end);

    fontString.autoFontSize = true;
end