local TXUI, F, E, I, V, P, G = unpack(select(2, ...))
local A = TXUI:NewModule("Armory", "AceHook-3.0", "AceEvent-3.0", "AceTimer-3.0")

-- Globals
local _G = _G
local BreakUpLargeNumbers = BreakUpLargeNumbers
local C_PaperDollInfo = _G.C_PaperDollInfo
local CreateFrame = CreateFrame
local EFFECTIVE_LEVEL_FORMAT = _G.EFFECTIVE_LEVEL_FORMAT
local format = string.format
local GetAchievementInfo = GetAchievementInfo
local GetAverageItemLevel = GetAverageItemLevel
local GetCurrentTitle = GetCurrentTitle
local GetInventoryItemID = GetInventoryItemID
local GetItemInfo = GetItemInfo
local GetMeleeHaste = GetMeleeHaste
local GetSpecialization = GetSpecialization
local GetSpecializationInfo = GetSpecializationInfo
local GetSpecializationRole = GetSpecializationRole
local GetTitleName = GetTitleName
local InCombatLockdown = InCombatLockdown
local ipairs = ipairs
local LE_ITEM_CLASS_WEAPON = _G.LE_ITEM_CLASS_WEAPON
local LE_UNIT_STAT_AGILITY = _G.LE_UNIT_STAT_AGILITY
local LE_UNIT_STAT_INTELLECT = _G.LE_UNIT_STAT_INTELLECT
local LE_UNIT_STAT_STRENGTH = _G.LE_UNIT_STAT_STRENGTH
local LOCALIZED_CLASS_NAMES_FEMALE = _G.LOCALIZED_CLASS_NAMES_FEMALE
local LOCALIZED_CLASS_NAMES_MALE = _G.LOCALIZED_CLASS_NAMES_MALE
local max = math.max
local MIN_PLAYER_LEVEL_FOR_ITEM_LEVEL_DISPLAY = _G.MIN_PLAYER_LEVEL_FOR_ITEM_LEVEL_DISPLAY
local next = next
local pairs = pairs
local select = select
local UnitAttackSpeed = UnitAttackSpeed
local UnitEffectiveLevel = UnitEffectiveLevel
local UnitLevel = UnitLevel
local UnitSex = UnitSex
local unpack = unpack
local wipe = wipe

-- Vars
A.enumDirection = F.Enum { "LEFT", "RIGHT", "BOTTOM" }
A.characterSlots = {
    ["HeadSlot"] = {
        id = 1,
        needsEnchant = false,
        direction = A.enumDirection.LEFT,
    },
    ["NeckSlot"] = {
        id = 2,
        needsEnchant = false,
        direction = A.enumDirection.LEFT,
    },
    ["ShoulderSlot"] = {
        id = 3,
        needsEnchant = false,
        direction = A.enumDirection.LEFT,
    },
    ["BackSlot"] = {
        id = 15,
        needsEnchant = true,
        direction = A.enumDirection.LEFT,
    },
    ["ChestSlot"] = {
        id = 5,
        needsEnchant = true,
        direction = A.enumDirection.LEFT,
    },
    ["ShirtSlot"] = {
        id = 4,
        needsEnchant = false,
        direction = A.enumDirection.LEFT,
    },
    ["TabardSlot"] = {
        id = 18,
        needsEnchant = false,
        direction = A.enumDirection.LEFT,
    },
    ["WristSlot"] = {
        id = 9,
        needsEnchant = true,
        enchantCondition = {
            level = 60,
            primary = LE_UNIT_STAT_INTELLECT,
        },
        direction = A.enumDirection.LEFT,
    },
    ["HandsSlot"] = {
        id = 10,
        needsEnchant = true,
        enchantCondition = {
            level = 60,
            primary = LE_UNIT_STAT_STRENGTH,
        },
        direction = A.enumDirection.RIGHT,
    },
    ["WaistSlot"] = {
        id = 6,
        needsEnchant = false,
        direction = A.enumDirection.RIGHT,
    },
    ["LegsSlot"] = {
        id = 7,
        needsEnchant = false,
        direction = A.enumDirection.RIGHT,
    },
    ["FeetSlot"] = {
        id = 8,
        needsEnchant = true,
        enchantCondition = {
            level = 60,
            primary = LE_UNIT_STAT_AGILITY,
        },
        direction = A.enumDirection.RIGHT,
    },
    ["Finger0Slot"] = {
        id = 11,
        needsEnchant = true,
        direction = A.enumDirection.RIGHT,
    },
    ["Finger1Slot"] = {
        id = 12,
        needsEnchant = true,
        direction = A.enumDirection.RIGHT,
    },
    ["Trinket0Slot"] = {
        id = 13,
        needsEnchant = false,
        direction = A.enumDirection.RIGHT,
    },
    ["Trinket1Slot"] = {
        id = 14,
        needsEnchant = false,
        direction = A.enumDirection.RIGHT,
    },
    ["MainHandSlot"] = {
        id = 16,
        needsEnchant = true,
        direction = A.enumDirection.RIGHT,
    },
    ["SecondaryHandSlot"] = {
        id = 17,
        needsEnchant = true,
        enchantCondition = {
            itemType = LE_ITEM_CLASS_WEAPON,
        },
        direction = A.enumDirection.LEFT,
    },
}

function A:UseFontGradient(db, prefix)
    local dbEntry = db[prefix .. "FontColor"]
    return (dbEntry == "GRADIENT")
end

function A:GetSlotNameByID(slotId)
    for slot, options in pairs(self.characterSlots) do
        if options.id == slotId then return slot end
    end
end

function A:CheckEnchantCondition(slotOptions)
    local conditions = slotOptions.enchantCondition
    local enchantNeeded = true

    -- Level Condition
    if enchantNeeded and conditions.level then enchantNeeded = (conditions.level == UnitLevel("player")) end

    -- Primary Stat Condition
    if enchantNeeded and conditions.primary then
        enchantNeeded = false
        local spec = GetSpecialization()
        if spec then
            local primaryStat = select(6, GetSpecializationInfo(spec, nil, nil, nil, UnitSex("player")))
            enchantNeeded = (conditions.primary == primaryStat)
        end
    end

    -- ItemType and ItemSubtype check
    if enchantNeeded and conditions.itemType then
        local itemType = select(12, GetItemInfo(GetInventoryItemID("player", slotOptions.id)))
        enchantNeeded = (itemType == conditions.itemType)
    end

    return enchantNeeded
end

function A:AnimationsAllowed()
    return (not InCombatLockdown()) and self.db.animations
end

function A:ClearAnimations(stats)
    if stats then
        self.statsCount = 1
        wipe(self.statsObjects)
    else
        wipe(self.animationObjects)
    end
end

function A:GetAnimationSlot(stats)
    if stats then
        local count = self.statsCount
        self.statsCount = self.statsCount + 1
        return count
    end
end

function A:AddAnimation(anim, stats, slot)
    if stats then
        if not slot then self.statsCount = self.statsCount + 1 end
        self.statsObjects[anim] = true
    else
        self.animationObjects[anim] = true
    end
end

function A:PlayAnimations()
    for anim, _ in pairs(self.statsObjects) do
        if anim:IsPlaying() then anim:Stop() end
        if self:AnimationsAllowed() then anim:Play() end
    end

    for anim, _ in pairs(self.animationObjects) do
        if anim:IsPlaying() then anim:Stop() end
        if self:AnimationsAllowed() then anim:Play() end
    end
end

function A:SetupGrowAnimation(obj, hold)
    local holdDuration, fadeDuration, growDuration = 0.02, 0.15, 1

    if obj.GrowIn then
        if hold and hold > 0 then
            obj.GrowIn.Hold:SetDuration(((hold * holdDuration) * self.db.animationsMult) + ((fadeDuration * 0.3) * self.db.animationsMult))
        else
            obj.GrowIn.Hold:SetDuration(0)
        end

        obj.GrowIn.Grow:SetDuration(growDuration * self.db.animationsMult)
        return
    end

    obj.GrowIn = TXUI:CreateAnimationGroup(obj)

    obj.GrowIn.ResetGrow = obj.GrowIn:CreateAnimation("Width")
    obj.GrowIn.ResetGrow:SetDuration(0)
    obj.GrowIn.ResetGrow:SetChange(0)
    obj.GrowIn.ResetGrow:SetOrder(1)

    obj.GrowIn.Hold = obj.GrowIn:CreateAnimation("Sleep")
    obj.GrowIn.Hold:SetOrder(2)

    obj.GrowIn.Grow = obj.GrowIn:CreateAnimation("Width")
    obj.GrowIn.Grow:SetEasing("out-quintic")
    obj.GrowIn.Grow:SetDuration(growDuration * self.db.animationsMult)
    obj.GrowIn.Grow:SetOrder(3)

    if hold and hold > 0 then
        obj.GrowIn.Hold:SetDuration(((hold * holdDuration) * self.db.animationsMult) + ((fadeDuration * 0.3) * self.db.animationsMult))
    else
        obj.GrowIn.Hold:SetDuration(0)
    end

    self:AddAnimation(obj.GrowIn)
end

function A:SetupFadeAnimation(obj, slot)
    local holdDuration, fadeDuration = 0.02, 0.15

    if obj.FadeIn then
        obj.FadeIn.Hold:SetDuration(((slot or self.statsCount) * holdDuration) * self.db.animationsMult)
        obj.FadeIn.Fade:SetDuration(fadeDuration * self.db.animationsMult)
        self:AddAnimation(obj.FadeIn, true, slot)
        return
    end

    obj.FadeIn = TXUI:CreateAnimationGroup(obj)

    obj.FadeIn.ResetFade = obj.FadeIn:CreateAnimation("Fade")
    obj.FadeIn.ResetFade:SetDuration(0)
    obj.FadeIn.ResetFade:SetChange(0)
    obj.FadeIn.ResetFade:SetOrder(1)

    obj.FadeIn.Hold = obj.FadeIn:CreateAnimation("Sleep")
    obj.FadeIn.Hold:SetDuration(((slot or self.statsCount) * holdDuration) * self.db.animationsMult)
    obj.FadeIn.Hold:SetOrder(2)

    obj.FadeIn.Fade = obj.FadeIn:CreateAnimation("Fade")
    obj.FadeIn.Fade:SetDuration(fadeDuration * self.db.animationsMult)
    obj.FadeIn.Fade:SetEasing("out-quintic")
    obj.FadeIn.Fade:SetChange(1)
    obj.FadeIn.Fade:SetOrder(3)

    self:AddAnimation(obj.FadeIn, true, slot)
end

function A:UpdateItemLevel()
    if not self.frame:IsShown() then return end

    F.SetFontFromDB(self.db.stats, "itemLevel", self.frame.ItemLevelText)

    local itemLevelText

    local avgItemLevel, avgItemLevelEquipped = GetAverageItemLevel()
    local minItemLevel = C_PaperDollInfo.GetMinItemLevel()
    local displayItemLevel = max(minItemLevel or 0, avgItemLevelEquipped)

    if self.db.stats.showAvgItemLevel then
        itemLevelText = format(format("%s / %s", self.db.stats.itemLevelFormat, self.db.stats.itemLevelFormat), displayItemLevel, avgItemLevel)
    else
        itemLevelText = format(self.db.stats.itemLevelFormat, displayItemLevel)
    end

    if self:UseFontGradient(self.db.stats, "itemLevel") then
        local epicComplete = select(13, GetAchievementInfo(14797))

        if epicComplete then
            self.frame.ItemLevelText:SetText(F.String.FastGradient(itemLevelText, 0.78, 0.13, 0.57, 0.42, 0.08, 0.82))
        else
            local rareComplete = select(13, GetAchievementInfo(14796))

            if rareComplete then
                self.frame.ItemLevelText:SetText(F.String.FastGradient(itemLevelText, 0.01, 0.78, 0.98, 0, 0.38, 0.90))
            else
                self.frame.ItemLevelText:SetText(F.String.FastGradient(itemLevelText, 0.07, 0.90, 0.15, 0, 0.69, 0.11))
            end
        end
    else
        self.frame.ItemLevelText:SetText(itemLevelText)
        F.SetFontColorFromDB(self.db.stats, "itemLevel", self.frame.ItemLevelText)
    end
end

function A:UpdateTitle()
    F.SetFontFromDB(self.db, "nameText", self.nameText, false)
    F.SetFontFromDB(self.db, "titleText", self.titleText, false)
    F.SetFontFromDB(self.db, "levelTitleText", self.levelTitleText)
    F.SetFontFromDB(self.db, "levelText", self.levelText)
    F.SetFontFromDB(self.db, "classText", self.classText, false)
    F.SetFontFromDB(self.db, "specIcon", self.specIcon, false)

    local titleId = GetCurrentTitle()
    local titleName = GetTitleName(titleId) or ""
    local classNames = LOCALIZED_CLASS_NAMES_MALE
    local playerLevel = UnitLevel("player")
    local playerEffectiveLevel = UnitEffectiveLevel("player")

    if playerEffectiveLevel ~= playerLevel then playerLevel = EFFECTIVE_LEVEL_FORMAT:format(playerEffectiveLevel, playerLevel) end

    local currentClass = E.myclass
    if UnitSex("player") == 3 then classNames = LOCALIZED_CLASS_NAMES_FEMALE end

    local primaryTalentTreeIdx = 0
    local primaryTalentTree = GetSpecialization()
    if primaryTalentTree then primaryTalentTreeIdx = GetSpecializationInfo(primaryTalentTree) or 0 end

    -- Those cannot be empty
    if not currentClass or not playerLevel then return end

    local classColorNormal = E.db.TXUI.themes.gradientMode.classColorMap[I.Enum.GradientMode.Color.NORMAL][currentClass]
    local classColorShift = E.db.TXUI.themes.gradientMode.classColorMap[I.Enum.GradientMode.Color.SHIFT][currentClass]

    if self:UseFontGradient(self.db, "nameText") then
        self.nameText:SetText(F.String.FastGradient(E.myname, 0, 0.6, 1, 0, 0.9, 1))
    else
        self.nameText:SetText(E.myname)
        F.SetFontColorFromDB(self.db, "nameText", self.nameText)
    end

    if self:UseFontGradient(self.db, "titleText") then
        self.titleText:SetText(F.String.FastGradient(titleName, 0, 0.9, 1, 0, 0.6, 1))
    else
        self.titleText:SetText(titleName)
        F.SetFontColorFromDB(self.db, "titleText", self.titleText)
    end

    self.levelTitleText:SetText("Level")
    self.levelText:SetText(playerLevel)

    local fontIcon = P.wunderbar.subModules.SpecSwitch.icons[primaryTalentTreeIdx] or P.wunderbar.subModules.SpecSwitch.icons[0]

    if self:UseFontGradient(self.db, "specIcon") then
        self.specIcon:SetText(F.String.RGB(fontIcon, classColorNormal))
    else
        self.specIcon:SetText(fontIcon)
        F.SetFontColorFromDB(self.db, "specIcon", self.specIcon)
    end

    if self:UseFontGradient(self.db, "classText") then
        self.classText:SetText(
            F.String.FastGradient(classNames[currentClass], classColorNormal.r, classColorNormal.g, classColorNormal.b, classColorShift.r, classColorShift.g, classColorShift.b)
        )
    else
        self.classText:SetText(classNames[currentClass])
        F.SetFontColorFromDB(self.db, "classText", self.classText)
    end

    self.nameText:ClearAllPoints()
    self.nameText:SetPoint("TOP", self.frameModel, self.db.nameTextOffsetX, 59 + self.db.nameTextOffsetY)
    self.nameText:SetJustifyH("CENTER")
    self.nameText:SetJustifyV("BOTTOM")

    self.titleText:ClearAllPoints()
    self.titleText:SetPoint("LEFT", self.nameText, "RIGHT", self.db.titleTextOffsetX, self.db.titleTextOffsetY)
    self.titleText:SetJustifyH("LEFT")
    self.titleText:SetJustifyV("BOTTOM")

    local iconPadding = 10
    local textPadding = 2

    local leftWidth = self.levelText:GetStringWidth() + self.levelTitleText:GetStringWidth() + textPadding
    local rightWidth = self.classText:GetStringWidth()
    local iconWidth = self.specIcon:GetStringWidth() + (iconPadding * 2)
    local totalWidth = leftWidth + rightWidth + iconWidth
    local anchorWidth = totalWidth - (leftWidth + (iconWidth / 2))
    local centerOffset = (totalWidth / 2) - anchorWidth

    self.specIcon:ClearAllPoints()
    self.specIcon:SetPoint("TOP", self.frameModel, centerOffset, 30)
    self.specIcon:SetJustifyH("CENTER")
    self.specIcon:SetJustifyV("BOTTOM")

    self.levelText:ClearAllPoints()
    self.levelText:SetPoint("RIGHT", self.specIcon, "LEFT", (-iconPadding + self.db.levelTextOffsetX), self.db.levelTextOffsetY)
    self.levelText:SetJustifyH("LEFT")
    self.levelText:SetJustifyV("BOTTOM")

    self.levelTitleText:ClearAllPoints()
    self.levelTitleText:SetPoint("RIGHT", self.levelText, "LEFT", (-textPadding + self.db.levelTitleTextOffsetX), self.db.levelTitleTextOffsetY)
    self.levelTitleText:SetJustifyH("LEFT")
    self.levelTitleText:SetJustifyV("BOTTOM")

    self.classText:ClearAllPoints()
    self.classText:SetPoint("LEFT", self.specIcon, "RIGHT", (iconPadding + self.db.classTextOffsetX), self.db.classTextOffsetY)
    self.classText:SetJustifyH("RIGHT")
    self.classText:SetJustifyV("BOTTOM")
end

function A:EnchantAbbreviate(str)
    local abbrevs = {
        [_G["STAT_VERSATILITY"]] = "Vers.",
        [_G["SPELL_STAT" .. _G.LE_UNIT_STAT_STRENGTH .. "_NAME"]] = "Str.",
        [_G["SPELL_STAT" .. _G.LE_UNIT_STAT_AGILITY .. "_NAME"]] = "Agi.",
        [_G["SPELL_STAT" .. _G.LE_UNIT_STAT_INTELLECT .. "_NAME"]] = "Int.",
        [_G["SPELL_STAT" .. _G.LE_UNIT_STAT_STAMINA .. "_NAME"]] = "Stam.",
    }

    local short = F.String.Abbreviate(str)
    for stat, abbrev in pairs(abbrevs) do
        short = short:gsub(stat, abbrev)
    end

    return short
end

function A:UpdatePageStrings(_, slotId, _, slotItem, slotInfo, which)
    if which ~= "Character" then return end
    if not slotItem.enchantText or not slotItem.iLvlText then return end

    local slotName = self:GetSlotNameByID(slotId)
    if not slotName then return end

    local slotOptions = self.characterSlots[slotName]
    if not slotOptions then return end

    -- Enchant Text Handling
    if self.db.pageInfo.enchantTextEnabled and slotInfo.itemLevelColors and next(slotInfo.itemLevelColors) then
        if slotInfo.enchantColors and next(slotInfo.enchantColors) then
            if slotInfo.enchantText and (slotInfo.enchantText ~= "") then
                local text = slotInfo.enchantTextShort
                if self.db.pageInfo.abbreviateEnchantText then text = self:EnchantAbbreviate(slotInfo.enchantText) end
                if slotOptions.direction == self.enumDirection.LEFT then
                    slotItem.enchantText:SetText(F.String.FastGradient(text, 0, 0.69, 0.11, 0.07, 0.90, 0.15))
                elseif slotOptions.direction == self.enumDirection.RIGHT then
                    slotItem.enchantText:SetText(F.String.FastGradient(text, 0.07, 0.90, 0.15, 0, 0.69, 0.11))
                end
            end
        elseif self.db.pageInfo.missingEnchantText and slotOptions.needsEnchant then
            if not slotOptions.enchantCondition or (self:CheckEnchantCondition(slotOptions)) then
                slotItem.enchantText:SetText(F.String.Error("Missing"))
            else
                slotItem.enchantText:SetText("")
            end
        else
            slotItem.enchantText:SetText("")
        end
    else
        slotItem.enchantText:SetText("")
    end

    -- Hide Gradient
    if slotItem.TXGradient then slotItem.TXGradient:Hide() end

    -- If we got an item color, show gradient and set color
    if slotInfo.itemLevelColors and next(slotInfo.itemLevelColors) then
        local r, g, b = unpack(slotInfo.itemLevelColors)

        -- Create Gradient if it doesen't exist
        if not slotItem.TXGradient then
            slotItem.TXGradient = CreateFrame("Frame", nil, slotItem)
            slotItem.TXGradient:SetFrameLevel(self.frameModel:GetFrameLevel() - 1)

            slotItem.TXGradient.Texture = slotItem.TXGradient:CreateTexture(nil, "OVERLAY")
            slotItem.TXGradient.Texture:SetInside()
            slotItem.TXGradient.Texture:SetTexture(E.media.blankTex)
            slotItem.TXGradient.Texture:SetVertexColor(1, 1, 1, 1)

            if slotOptions.direction == self.enumDirection.LEFT then
                slotItem.TXGradient:Point("BOTTOMLEFT", slotItem, "BOTTOMRIGHT", -1, -1)
            elseif slotOptions.direction == self.enumDirection.RIGHT then
                slotItem.TXGradient:Point("BOTTOMRIGHT", slotItem, "BOTTOMLEFT", 1, -1)
            end
        end

        -- Setup Animations
        self:SetupGrowAnimation(slotItem.TXGradient)

        -- Update Size
        slotItem.TXGradient.GrowIn.Grow:SetChange(E:Scale(self.db.pageInfo.itemQualityGradientWidth))
        slotItem.TXGradient:Size(self.db.pageInfo.itemQualityGradientWidth, self.db.pageInfo.itemQualityGradientHeight)

        -- Update Colors
        if slotOptions.direction == self.enumDirection.LEFT then
            slotItem.TXGradient.Texture:SetGradientAlpha(
                "HORIZONTAL",
                r,
                g,
                b,
                self.db.pageInfo.itemQualityGradientStartAlpha,
                r,
                g,
                b,
                self.db.pageInfo.itemQualityGradientEndAlpha
            )
        elseif slotOptions.direction == self.enumDirection.RIGHT then
            slotItem.TXGradient.Texture:SetGradientAlpha(
                "HORIZONTAL",
                r,
                g,
                b,
                self.db.pageInfo.itemQualityGradientEndAlpha,
                r,
                g,
                b,
                self.db.pageInfo.itemQualityGradientStartAlpha
            )
        end

        slotItem.TXGradient:Show()
    end

    -- iLvL Text Handling
    if not self.db.pageInfo.itemLevelTextEnabled then slotItem.iLvlText:SetText("") end

    -- Icons Handling
    if not self.db.pageInfo.itemLevelTextEnabled or not self.db.pageInfo.iconsEnabled then
        for x = 1, 10 do
            local essenceType = slotItem["textureSlotEssenceType" .. x]
            if essenceType then essenceType:Hide() end
            slotItem["textureSlot" .. x]:SetTexture()
            slotItem["textureSlotBackdrop" .. x]:Hide()
        end
    end
end

function A:UpdatePageInfo(_, _, which)
    if (which ~= nil) and (which ~= "Character") then return end

    for slot, options in pairs(self.characterSlots) do
        if (options.id ~= 4) and (options.id ~= 18) then
            local slotFrame = _G["Character" .. slot]

            -- ItemLevel Slot Text
            if slotFrame.iLvlText then F.SetFontFromDB(self.db.pageInfo, "iLvL", slotFrame.iLvlText, false) end

            -- Enchant Slot Text
            if slotFrame.enchantText then F.SetFontFromDB(self.db.pageInfo, "enchant", slotFrame.enchantText, false) end
        end
    end

    self:UpdateItemLevel()
end

function A:UpdateCategoryHeader(frame, animationSlot)
    if frame.StripTextures then frame:StripTextures() end
    if frame.backdrop then frame.backdrop:Kill() end
    if frame.Background then frame.Background:Kill() end

    -- Set cutom font
    F.SetFontFromDB(self.db.stats, "header", frame.Title, false)

    local useHeaderGradient = self:UseFontGradient(self.db.stats, "header")

    -- Set color gradient
    if useHeaderGradient then
        frame.Title:SetText(F.String.FastGradient(F.String.StripColor(frame.Title:GetText()), 0, 0.9, 1, 0, 0.6, 1))
    else
        frame.Title:SetText(F.String.StripColor(frame.Title:GetText()))
        F.SetFontColorFromDB(self.db.stats, "header", frame.Title)
    end

    -- Create left divider
    local leftDivider = frame.Title.TXLeftDivider or frame:CreateTexture(nil, "ARTWORK")
    leftDivider:Height(2)
    leftDivider:SetTexture(E.media.blankTex)
    leftDivider:SetVertexColor(1, 1, 1, 1)

    if useHeaderGradient then
        leftDivider:SetGradientAlpha("HORIZONTAL", 0, 0.6, 1, 0, 0, 0.9, 1, 1)
    else
        local fontColor = F.GetFontColorFromDB(self.db.stats, "header")
        leftDivider:SetGradientAlpha("HORIZONTAL", fontColor.r, fontColor.g, fontColor.b, 0, fontColor.r, fontColor.g, fontColor.b, fontColor.a)
    end

    -- Create right divider
    local rightDivider = frame.Title.TXRightDivider or frame:CreateTexture(nil, "ARTWORK")
    rightDivider:Height(2)
    rightDivider:SetTexture(E.media.blankTex)
    rightDivider:SetVertexColor(1, 1, 1, 1)
    rightDivider:SetGradientAlpha("HORIZONTAL", 0, 0.9, 1, 1, 0, 0.6, 1, 0)

    if useHeaderGradient then
        rightDivider:SetGradientAlpha("HORIZONTAL", 0, 0.9, 1, 1, 0, 0.6, 1, 0)
    else
        local fontColor = F.GetFontColorFromDB(self.db.stats, "header")
        rightDivider:SetGradientAlpha("HORIZONTAL", fontColor.r, fontColor.g, fontColor.b, fontColor.a, fontColor.r, fontColor.g, fontColor.b, 0)
    end

    -- Setup Animations
    self:SetupGrowAnimation(leftDivider, animationSlot)
    self:SetupGrowAnimation(rightDivider, animationSlot)

    -- Anchor to calculate size
    leftDivider:ClearAllPoints()
    leftDivider:Point("LEFT", frame, "LEFT", 3, 0)
    leftDivider:Point("RIGHT", frame.Title, "LEFT", -3, 0)
    rightDivider:ClearAllPoints()
    rightDivider:Point("RIGHT", frame, "RIGHT", -3, 0)
    rightDivider:Point("LEFT", frame.Title, "RIGHT", 3, 0)

    -- Vars
    local leftDividerWidth = leftDivider:GetWidth()
    local rightDividerWidth = leftDivider:GetWidth()

    -- Update Animations
    leftDivider.GrowIn.Grow:SetChange(leftDividerWidth)
    rightDivider.GrowIn.Grow:SetChange(rightDividerWidth)

    -- Set Final Anchor
    leftDivider:ClearAllPoints()
    leftDivider:Point("RIGHT", frame.Title, "LEFT", -3, 0)

    rightDivider:ClearAllPoints()
    rightDivider:Point("LEFT", frame.Title, "RIGHT", 3, 0)

    -- Set refs
    frame.Title.TXLeftDivider = leftDivider
    frame.Title.TXRightDivider = rightDivider
end

function A:CleanupCharacterStat(frame)
    -- Kill Blizzard and ElvUI Stuff
    if frame.Background then frame.Background:Kill() end
    if frame.leftGrad then frame.leftGrad:Kill() end
    if frame.rightGrad then frame.rightGrad:Kill() end
end

function A:UpdateCharacterStat(frame, showGradient)
    -- Kill Blizzard and ElvUI Stuff
    A:CleanupCharacterStat(frame)

    -- Set custom font gradient for label
    if frame.Label then
        F.SetFontFromDB(self.db.stats, "label", frame.Label, false)

        if self:UseFontGradient(self.db.stats, "label") then
            frame.Label:SetText(F.String.FastGradient(F.String.StripColor(frame.Label:GetText()), 0, 0.6, 1, 0, 0.9, 1))
        else
            frame.Label:SetText(F.String.StripColor(frame.Label:GetText()))
            F.SetFontColorFromDB(self.db.stats, "label", frame.Label)
        end
    end

    -- Set custom for value
    if frame.Value then F.SetFontFromDB(self.db.stats, "value", frame.Value) end

    -- Set custom background gradient
    if frame.TXGradient then frame.TXGradient:Hide() end
    if showGradient and self.db.stats.alternatingBackgroundEnabled then
        frame.TXGradient = frame.TXGradient or frame:CreateTexture(nil, "ARTWORK")
        frame.TXGradient:Point("LEFT", frame, "CENTER")
        frame.TXGradient:Size(90, frame:GetHeight())
        frame.TXGradient:SetTexture(E.media.blankTex)

        if self:UseFontGradient(self.db.stats, "label") then
            frame.TXGradient:SetGradientAlpha("HORIZONTAL", 0, 0.6, 1, 0, 0, 0.9, 1, self.db.stats.alternatingBackgroundAlpha)
        else
            local fontColor = F.GetFontColorFromDB(self.db.stats, "label")
            frame.TXGradient:SetGradientAlpha(
                "HORIZONTAL",
                fontColor.r,
                fontColor.g,
                fontColor.b,
                0,
                fontColor.r,
                fontColor.g,
                fontColor.b,
                self.db.stats.alternatingBackgroundAlpha
            )
        end

        frame.TXGradient:Show()
    end
end

function A:UpdateCharacterStats()
    if not self.frame:IsShown() then return end

    local characterStatsPane = _G.CharacterStatsPane
    if not characterStatsPane then return end

    local spec = GetSpecialization()
    local level = UnitLevel("player")
    local categoryYOffset = 0
    local statYOffset = 0
    local lastAnchor, role

    self:ClearAnimations(true)

    if spec then role = GetSpecializationRole(spec) end

    if level >= MIN_PLAYER_LEVEL_FOR_ITEM_LEVEL_DISPLAY then
        self:CleanupCharacterStat(characterStatsPane.ItemLevelFrame)

        local animationSlot = self:GetAnimationSlot(true)
        self:SetupFadeAnimation(characterStatsPane.ItemLevelFrame)
        self:SetupFadeAnimation(characterStatsPane.ItemLevelCategory, animationSlot)
        self:UpdateCategoryHeader(characterStatsPane.ItemLevelCategory, animationSlot)

        characterStatsPane.ItemLevelCategory:Show()
        characterStatsPane.ItemLevelFrame:Show()
        characterStatsPane.AttributesCategory:ClearAllPoints()
        characterStatsPane.AttributesCategory:SetPoint("TOP", characterStatsPane.ItemLevelFrame, "BOTTOM", 0, 0)
    else
        characterStatsPane.ItemLevelCategory:Hide()
        characterStatsPane.ItemLevelFrame:Hide()
        characterStatsPane.AttributesCategory:ClearAllPoints()
        characterStatsPane.AttributesCategory:SetPoint("TOP", characterStatsPane, "TOP", 0, -2)
        categoryYOffset = -11
        statYOffset = -5
    end

    characterStatsPane.statsFramePool:ReleaseAll()
    local statFrame = characterStatsPane.statsFramePool:Acquire()
    local categories = _G.PAPERDOLL_STATCATEGORIES

    for catIndex = 1, #categories do
        local catFrame = characterStatsPane[categories[catIndex].categoryFrame]
        local animationSlot = self:GetAnimationSlot(true)
        local numStatInCat = 0
        for statIndex = 1, #categories[catIndex].stats do
            local stat = categories[catIndex].stats[statIndex]
            local hideAt = stat.hideAt
            local showStat = true
            local statMode = 1

            if self.db.stats.mode[stat.stat] ~= nil then
                statMode = self.db.stats.mode[stat.stat].mode
            else
                showStat = false
            end

            -- Mode 0 - Always Hide
            if showStat and (statMode == 0) then showStat = false end

            -- Mode 1 - Smart
            if showStat and (statMode == 1) then
                if showStat and (stat.primary and spec) then
                    local primaryStat = select(6, GetSpecializationInfo(spec, nil, nil, nil, UnitSex("player")))
                    if stat.primary ~= primaryStat then showStat = false end
                end

                if showStat and (stat.primaries and spec) then
                    local primaryStat = select(6, GetSpecializationInfo(spec, nil, nil, nil, UnitSex("player")))
                    local foundPrimary = false

                    for _, primary in pairs(stat.primaries) do
                        if primaryStat == primary then
                            foundPrimary = true
                            break
                        end
                    end

                    showStat = foundPrimary
                end

                if showStat and stat.roles then
                    local foundRole = false

                    for _, statRole in pairs(stat.roles) do
                        if role == statRole then
                            foundRole = true
                            break
                        end
                    end

                    showStat = foundRole
                end

                if showStat and stat.classes then
                    local foundClass = false

                    for _, statClass in pairs(stat.classes) do
                        if E.myclass == statClass then
                            foundClass = true
                            break
                        end
                    end

                    showStat = foundClass
                end

                if showStat and stat.showFunc then showStat = stat.showFunc() end
            end

            -- Mode 2 - Always Show if not empty
            if showStat and (statMode == 2) and (hideAt == nil) then hideAt = 0 end

            -- Mode 3 - Always Show
            -- This is not needed here, just added here to make the logic more clearer
            if showStat and (statMode == 3) then showStat = true end

            if showStat then
                statFrame.onEnterFunc = nil
                statFrame.UpdateTooltip = nil

                _G.PAPERDOLL_STATINFO[stat.stat].updateFunc(statFrame, "player")

                -- Mode 1/2 - Validate hideAt value in Smart Mode/Always Show if not empty mode
                if (hideAt ~= nil) and ((statMode == 1) or (statMode == 2)) then showStat = (stat.hideAt ~= statFrame.numericValue) end

                if showStat then
                    if numStatInCat == 0 then
                        if lastAnchor then catFrame:SetPoint("TOP", lastAnchor, "BOTTOM", 0, categoryYOffset) end

                        statFrame:SetPoint("TOP", catFrame, "BOTTOM", 0, -2)
                    else
                        statFrame:SetPoint("TOP", lastAnchor, "BOTTOM", 0, statYOffset)
                    end

                    numStatInCat = numStatInCat + 1
                    self:UpdateCharacterStat(statFrame, (numStatInCat % 2) == 0)
                    self:SetupFadeAnimation(statFrame)

                    lastAnchor = statFrame
                    statFrame = characterStatsPane.statsFramePool:Acquire()
                end
            end
        end

        if numStatInCat > 0 then
            catFrame:Show()
            self:SetupFadeAnimation(catFrame, animationSlot)
            self:UpdateCategoryHeader(catFrame, animationSlot)
        else
            catFrame:Hide()
        end
    end

    characterStatsPane.statsFramePool:Release(statFrame)
end

function A:UpdateAttackSpeed(statFrame, unit)
    local meleeHaste = GetMeleeHaste()
    local speed, offhandSpeed = UnitAttackSpeed(unit)

    local displaySpeed = format("%.2f", speed)
    if offhandSpeed then offhandSpeed = format("%.2f", offhandSpeed) end
    if offhandSpeed then
        displaySpeed = BreakUpLargeNumbers(displaySpeed) .. " / " .. offhandSpeed
    else
        displaySpeed = BreakUpLargeNumbers(displaySpeed)
    end

    _G.PaperDollFrame_SetLabelAndText(statFrame, _G.WEAPON_SPEED, displaySpeed, false, speed)

    statFrame.tooltip = _G.HIGHLIGHT_FONT_COLOR_CODE .. format(_G.PAPERDOLLFRAME_TOOLTIP_FORMAT, _G.ATTACK_SPEED) .. " " .. displaySpeed .. _G.FONT_COLOR_CODE_CLOSE
    statFrame.tooltip2 = format(_G.STAT_ATTACK_SPEED_BASE_TOOLTIP, BreakUpLargeNumbers(meleeHaste))

    statFrame:Show()
end

function A:ApplyCustomStatCategories()
    _G.PAPERDOLL_STATCATEGORIES = {
        [1] = {
            categoryFrame = "AttributesCategory",
            stats = {
                [1] = {
                    stat = "STRENGTH",
                    primary = LE_UNIT_STAT_STRENGTH,
                },
                [2] = {
                    stat = "AGILITY",
                    primary = LE_UNIT_STAT_AGILITY,
                },
                [3] = {
                    stat = "INTELLECT",
                    primary = LE_UNIT_STAT_INTELLECT,
                },
                [4] = {
                    stat = "STAMINA",
                },
                [5] = {
                    stat = "HEALTH",
                    roles = { "TANK" },
                }, -- Added
                [6] = {
                    stat = "POWER",
                    roles = { "HEALER" },
                }, -- Added
                [7] = {
                    stat = "ARMOR",
                    roles = { "TANK" },
                }, -- Modified Smart
                [8] = {
                    stat = "STAGGER",
                    hideAt = 0,
                    roles = { "TANK" },
                    classes = { "MONK" },
                }, -- Modified Smart
                [9] = {
                    stat = "MANAREGEN",
                    roles = { "HEALER" },
                },
                [10] = {
                    stat = "ENERGY_REGEN",
                    hideAt = 0,
                    roles = { "TANK", "DAMAGER" },
                    classes = { "ROUGE", "DRUID", "MONK" },
                }, -- Added
                [11] = {
                    stat = "RUNE_REGEN",
                    hideAt = 0,
                    classes = { "DEATHKNIGHT" },
                }, -- Added
                [12] = {
                    stat = "FOCUS_REGEN",
                    hideAt = 0,
                    classes = { "HUNTER" },
                }, -- Added
                [13] = {
                    stat = "MOVESPEED",
                    hideAt = 0,
                }, -- Added
            },
        },
        [2] = {
            categoryFrame = "EnhancementsCategory",
            stats = {
                {
                    stat = "ATTACK_DAMAGE",
                    hideAt = 0,
                    primaries = { LE_UNIT_STAT_STRENGTH, LE_UNIT_STAT_AGILITY },
                    roles = { "DAMAGER" },
                }, -- Added
                {
                    stat = "ATTACK_AP",
                    hideAt = 0,
                    primaries = { LE_UNIT_STAT_STRENGTH, LE_UNIT_STAT_AGILITY },
                    roles = { "DAMAGER" },
                }, -- Added
                {
                    stat = "ATTACK_ATTACKSPEED",
                    hideAt = 0,
                    primaries = { LE_UNIT_STAT_STRENGTH, LE_UNIT_STAT_AGILITY },
                    roles = { "DAMAGER" },
                }, -- Added
                {
                    stat = "SPELLPOWER",
                    hideAt = 0,
                    primary = LE_UNIT_STAT_INTELLECT,
                    roles = { "HEALER", "DAMAGER" },
                }, -- Added
                {
                    stat = "CRITCHANCE",
                    hideAt = 0,
                }, -- 1
                {
                    stat = "HASTE",
                    hideAt = 0,
                }, -- 2
                {
                    stat = "MASTERY",
                    hideAt = 0,
                }, -- 3
                {
                    stat = "VERSATILITY",
                    hideAt = 0,
                }, -- 4
                {
                    stat = "LIFESTEAL",
                    hideAt = 0,
                }, -- 5
                {
                    stat = "AVOIDANCE",
                    hideAt = 0,
                }, -- 6
                {
                    stat = "SPEED",
                    hideAt = 0,
                }, -- 7
                {
                    stat = "DODGE",
                    roles = { "TANK" },
                }, -- 8
                {
                    stat = "PARRY",
                    hideAt = 0,
                    roles = { "TANK" },
                }, -- 9
                {
                    stat = "BLOCK",
                    hideAt = 0,
                    showFunc = C_PaperDollInfo.OffhandHasShield,
                }, -- 10
            },
        },
    }
end

function A:UpdateBackground()
    if self.db.background.enabled then
        self.frame.TXBackground.Texture:SetTexture(I.Media.Armory["BG" .. self.db.background.style])
        self.frame.TXBackground.Texture:SetVertexColor(1, 1, 1, self.db.background.alpha)
    else
        self.frame.TXBackground.Texture:SetTexture(nil)
        self.frame.TXBackground.Texture:SetVertexColor(0, 0, 0, 0)
    end
end

function A:KillBlizzard()
    local killList = { "CharacterLevelText", "CharacterFrameTitleText", "CharacterModelFrameBackgroundOverlay" }
    for _, frame in ipairs(killList) do
        if _G[frame] then _G[frame]:Kill() end
    end

    if self.frameModel.backdrop then self.frameModel.backdrop:Kill() end

    for _, corner in pairs { "TopLeft", "TopRight", "BotLeft", "BotRight" } do
        local bg = _G["CharacterModelFrameBackground" .. corner]
        if bg then bg:Kill() end
    end

    self.frameModel:DisableDrawLayer("BACKGROUND")
    self.frameModel:DisableDrawLayer("BORDER")
    self.frameModel:DisableDrawLayer("OVERLAY")
end

function A:UpdateCharacterArmory()
    if not self.db or not self.db.enabled then return end

    self:KillBlizzard()
    self:UpdateBackground()
    self:UpdateTitle()
    self:UpdatePageInfo() -- Also does :UpdateItemLevel
    self:UpdateCharacterStats()

    -- Update ElvUI Item Info instantly if frame is currently open
    if self.frame:IsShown() then E:GetModule("Misc"):UpdateCharacterInfo() end
end

function A:OpenCharacterArmory()
    self:UpdateCharacterArmory()
    self:PlayAnimations(false)
    self:PlayAnimations(true)
end

function A:CreateElements()
    if self.frame then return end

    self.frame = _G.CharacterFrame
    self.frameModel = _G.CharacterModelFrame
    self.frameName = self.frame:GetName()
    self.frameHolder = CreateFrame("FRAME", nil, self.frameModel)

    self.animationObjects = {}
    self.statsObjects = {}
    self.statsCount = 1

    local background = CreateFrame("Frame", nil, self.frameHolder)
    local nameText = self.frameHolder:CreateFontString(nil, "OVERLAY")
    local titleText = self.frameHolder:CreateFontString(nil, "OVERLAY")
    local levelTitleText = self.frameHolder:CreateFontString(nil, "OVERLAY")
    local levelText = self.frameHolder:CreateFontString(nil, "OVERLAY")
    local specIcon = self.frameHolder:CreateFontString(nil, "OVERLAY")
    local classText = self.frameHolder:CreateFontString(nil, "OVERLAY")

    local frameHeight, frameWidth = self.frame:GetSize()
    local cutOffPercentage = (1 - (frameHeight / frameWidth))

    background:SetInside(self.frame)
    background:SetFrameLevel(self.frameModel:GetFrameLevel() - 1)
    background.Texture = background:CreateTexture(nil, "BACKGROUND")
    background.Texture:SetInside()
    background.Texture:SetTexCoord(0, 1, cutOffPercentage, 1)

    self.frame.TXBackground = background

    self.nameText = nameText
    self.titleText = titleText
    self.levelTitleText = levelTitleText
    self.levelText = levelText
    self.specIcon = specIcon
    self.classText = classText
end

function A:ElvOptionsCheck()
    if not E.db.general.itemLevel.displayCharacterInfo then
        E.db.general.itemLevel.displayCharacterInfo = true
        E:GetModule("Misc"):ToggleItemLevelInfo(false)
        self:UpdateItemLevel()
    end
end

function A:HandleEvent(event, unit)
    if not self.frame:IsShown() then return end

    if event == "UNIT_NAME_UPDATE" then
        if unit == "player" then self:UpdateTitle() end
    elseif (event == "PLAYER_PVP_RANK_CHANGED") or (event == "PLAYER_TALENT_UPDATE") then
        self:UpdateTitle()
    elseif event == "PLAYER_AVG_ITEM_LEVEL_UPDATE" then
        self:UpdateItemLevel()
    end
end

function A:Disable()
    if not self.Initialized then return end
    if not self:IsHooked(_G, "PaperDollFrame_UpdateStats") then return end

    self:CancelAllTimers()
    self:UnhookAll()

    F.Event.UnregisterFrameEventAndCallback("UNIT_NAME_UPDATE", self)
    F.Event.UnregisterFrameEventAndCallback("PLAYER_PVP_RANK_CHANGED", self)
    F.Event.UnregisterFrameEventAndCallback("PLAYER_AVG_ITEM_LEVEL_UPDATE", self)
    F.Event.UnregisterFrameEventAndCallback("PLAYER_TALENT_UPDATE", self)
end

function A:Enable()
    if not self.Initialized then return end
    if self:IsHooked(_G, "PaperDollFrame_UpdateStats") then return end

    self:CreateElements()

    -- Hook ElvUI Overrides
    local m = E:GetModule("Misc")
    self:SecureHook(m, "UpdateCharacterItemLevel", F.Event.GenerateClosure(self.UpdateItemLevel, self))
    self:SecureHook(m, "UpdateAverageString", F.Event.GenerateClosure(self.UpdateItemLevel, self))
    self:SecureHook(m, "UpdatePageStrings", F.Event.GenerateClosure(self.UpdatePageStrings, self))
    self:SecureHook(m, "CreateSlotStrings", F.Event.GenerateClosure(self.UpdatePageInfo, self))
    self:SecureHook(m, "UpdateInspectPageFonts", F.Event.GenerateClosure(self.UpdatePageInfo, self))
    self:SecureHook(m, "ToggleItemLevelInfo", F.Event.GenerateClosure(self.ElvOptionsCheck, self))
    self:SecureHook(_G, "PaperDollFrame_UpdateStats", F.Event.GenerateClosure(self.UpdateCharacterStats, self))

    -- Register Events
    F.Event.RegisterFrameEventAndCallback("UNIT_NAME_UPDATE", self.HandleEvent, self, "UNIT_NAME_UPDATE")
    F.Event.RegisterFrameEventAndCallback("PLAYER_PVP_RANK_CHANGED", self.HandleEvent, self, "PLAYER_PVP_RANK_CHANGED")
    F.Event.RegisterFrameEventAndCallback("PLAYER_AVG_ITEM_LEVEL_UPDATE", self.HandleEvent, self, "PLAYER_AVG_ITEM_LEVEL_UPDATE")
    F.Event.RegisterFrameEventAndCallback("PLAYER_TALENT_UPDATE", self.HandleEvent, self, "PLAYER_TALENT_UPDATE")

    -- Hook Blizzard OnShow
    self:SecureHookScript(self.frame, "OnShow", "OpenCharacterArmory")

    -- Hook broken blizzard function
    self:RawHook(_G, "PaperDollFrame_SetAttackSpeed", "UpdateAttackSpeed", true)

    -- Apply our custom stat categories
    self:ApplyCustomStatCategories()

    -- Check ElvUI Options
    self:ElvOptionsCheck()

    -- Update instantly if frame is currently open
    if self.frame:IsShown() then self:UpdateCharacterArmory() end
end

function A:DatabaseUpdate()
    -- Set db
    self.db = F.GetDBFromPath("TXUI.armory")

    -- Enable/Disable only out of combat
    F.Event.ContinueOutOfCombat(function()
        -- Disable
        self:Disable()

        -- Enable
        if TXUI:HasRequirements(I.Requirements.Armory) and (self.db and self.db.enabled) then self:Enable() end
    end)
end

function A:Initialize()
    -- Don't init second time
    if self.Initialized then return end

    -- Register for updates
    F.Event.RegisterOnceCallback("TXUI.InitializedSafe", F.Event.GenerateClosure(self.DatabaseUpdate, self))
    F.Event.RegisterCallback("TXUI.DatabaseUpdate", self.DatabaseUpdate, self)
    F.Event.RegisterCallback("Armory.DatabaseUpdate", self.DatabaseUpdate, self)
    F.Event.RegisterCallback("Armory.SettingsUpdate", self.UpdateCharacterArmory, self)

    -- We are done, hooray!
    self.Initialized = true
end

if TXUI.IsRetail then TXUI:RegisterModule(A:GetName()) end
