-- Events.lua
-- June 2024

local addon, ns = ...
local Hekili = _G[ addon ]

local class = Hekili.Class
local state = Hekili.State
local PTR = ns.PTR
local TTD = ns.TTD

local formatKey = ns.formatKey

local abs = math.abs
local lower = string.lower
local insert, remove, sort, wipe = table.insert, table.remove, table.sort, table.wipe

local CGetItemInfo = C_Item.GetItemInfo
local IsEquippedItem = C_Item.IsEquippedItem
local GetDetailedItemLevelInfo = C_Item.GetDetailedItemLevelInfo
local UA_GetPlayerAuraBySpellID = C_UnitAuras.GetPlayerAuraBySpellID
local IsUsableItem = C_Item.IsUsableItem
local GetItemSpell = C_Item.GetItemSpell
local GetTalentInfo = C_SpecializationInfo.GetTalentInfo
local GetSpecialization= C_SpecializationInfo.GetSpecialization
local GetSpecializationInfo = C_SpecializationInfo.GetSpecializationInfo
local GetSpellCooldown = function(spellID)
    local spellCooldownInfo = C_Spell.GetSpellCooldown(spellID);
    if spellCooldownInfo then
        return spellCooldownInfo.startTime, spellCooldownInfo.duration, spellCooldownInfo.isEnabled, spellCooldownInfo.modRate;
    end
end

local GetSpellInfo = ns.GetUnpackedSpellInfo

local FindStringInInventoryItemTooltip = ns.FindStringInInventoryItemTooltip
local ResetDisabledGearAndSpells = ns.ResetDisabledGearAndSpells
local WipeCovenantCache = ns.WipeCovenantCache

local RC = LibStub( "LibRangeCheck-3.0" )

-- Abandoning AceEvent in favor of darkend's solution from:
-- http://andydote.co.uk/2014/11/23/good-design-in-warcraft-addons.html
-- This should be a bit friendlier for our modules.

local events = CreateFrame( "Frame" )
Hekili:ProfileFrame( "GeneralEvents", events )
local handlers = {}
local unitHandlers = {}

local itemCallbacks = {}
local spellCallbacks = {}
local activeDisplays = {}


function Hekili:GetActiveDisplays()
    return activeDisplays
end


local handlerCount = {}
Hekili.ECount = handlerCount
Hekili.IC = itemCallbacks

local eventData = {}
Hekili.EData = eventData

local function GenericOnEvent( self, event, ... )
    local eventHandlers = handlers[ event ]

    if not eventHandlers then return end

    for i, handler in ipairs( eventHandlers ) do
        local key = event .. "_" .. i
        local start = debugprofilestop()
        handler( event, ... )
        local finish = debugprofilestop()

        handlerCount[ key ] = ( handlerCount[ key ] or 0 ) + 1

        eventData[ key ] = eventData[ key ] or {}
        eventData[ key ].max = max( eventData[ key ].max or 0, finish - start )
        eventData[ key ].total = ( eventData[ key ].total or 0 ) + ( finish - start )
    end
end

local function UnitSpecificOnEvent( self, event, unit, ... )
    local unitFrame = unitHandlers[ unit ]

    if unitFrame then
        local eventHandlers = unitFrame.events[ event ]

        if not eventHandlers then return end

        for i, handler in ipairs( eventHandlers ) do
            local key = event .. "_" .. unit .. "_" .. i
            local start = debugprofilestop()
            handler( event, unit, ... )
            local finish = debugprofilestop()

            handlerCount[ key ] = ( handlerCount[ key ] or 0 ) + 1

            eventData[ key ] = eventData[ key ] or {}
            eventData[ key ].max = max( eventData[ key ].max or 0, finish - start )
            eventData[ key ].total = ( eventData[ key ].total or 0 ) + ( finish - start )
        end
    end
end

function ns.StartEventHandler()
    events:SetScript( "OnEvent", GenericOnEvent )

    for unit, unitFrame in pairs( unitHandlers ) do
        unitFrame:SetScript( "OnEvent", UnitSpecificOnEvent )
    end

    events:SetScript( "OnUpdate", function( self, elapsed )
        if Hekili.PendingSpecializationChange then
            Hekili:SpecializationChanged()
            Hekili.PendingSpecializationChange = false
            -- Spec updates are expensive; exit and do other work in the next frame.
            return
        end

        if handlers.FRAME_UPDATE then
            for i, handler in pairs( handlers.FRAME_UPDATE ) do
                local key = "FRAME_UPDATE_" .. i
                local start = debugprofilestop()
                handler( event, elapsed )
                local finish = debugprofilestop()

                handlerCount[ key ] = ( handlerCount[ key ] or 0 ) + 1

                eventData[ key ] = eventData[ key ] or {}
                eventData[ key ].max = max( eventData[ key ].max or 0, finish - start )
                eventData[ key ].total = ( eventData[ key ].total or 0 ) + ( finish - start )
            end
        end
    end )

    Hekili:RunSpellCallbacks()
end


function ns.StopEventHandler()
    events:SetScript( "OnEvent", nil )

    for unit, unitFrame in pairs( unitHandlers ) do
        unitFrame:SetScript( "OnEvent", nil )
    end

    events:SetScript( "OnUpdate", nil )
end


Hekili.EventSources = {}

ns.RegisterEvent = function( event, handler )

    handlers[ event ] = handlers[ event ] or {}
    insert( handlers[ event ], handler )

    if event ~= "FRAME_UPDATE" then events:RegisterEvent( event ) end

    local key = event .. "_" .. #handlers[event]
    Hekili:ProfileCPU( key, handler )

    local stack = debugstack(2)
    local file, line = stack:match([[Hekili/(.-)"%]:(%d+)]])
    Hekili.EventSources[ key ] = file and ( file .. ":" .. ( line or 0 ) ) or stack:match( "^(.*)\n" )
end
local RegisterEvent = ns.RegisterEvent


ns.UnregisterEvent = function( event, handler )
    local hands = handlers[ event ]

    if not hands then return end

    for i = #hands, 1, -1 do
        if hands[i] == handler then
            remove( hands, i )
        end
    end

    if #hands == 0 then events:UnregisterEvent( event ) end
end
local UnregisterEvent = ns.UnregisterEvent


-- For our purposes, all UnitEvents are player/target oriented.
ns.RegisterUnitEvent = function( event, unit1, unit2, handler )
    if not unit1 then unit1 = "player" end

    if not unitHandlers[ unit1 ] then
        unitHandlers[ unit1 ] = CreateFrame( "Frame" )
        Hekili:ProfileFrame( "UnitEvents:" .. unit1, unitHandlers[ unit1 ] )

        unitHandlers[ unit1 ].events = {}
    end

    local unitFrame = unitHandlers[ unit1 ]

    unitFrame.events[ event ] = unitFrame.events[ event ] or {}
    insert( unitFrame.events[ event ], handler )

    local stack = debugstack(2)
    local file, line = stack:match([[Hekili/(.-)"%]:(%d+)]])
    Hekili.EventSources[ event .. "_" .. unit1 .. "_" .. #unitFrame.events[ event ] ] = file and ( file .. ":" .. ( line or 0 ) ) or stack:match( "^(.*)\n" )

    unitFrame:RegisterUnitEvent( event, unit1 )
    Hekili:ProfileCPU( event .. "_" .. unit1 .. "_" .. #unitFrame.events[ event ], handler )

    if unit2 then
        if not unitHandlers[ unit2 ] then
            unitHandlers[ unit2 ] = CreateFrame( "Frame" )
            Hekili:ProfileFrame( "UnitEvents:" .. unit2, unitHandlers[ unit2 ] )

            unitHandlers[ unit2 ].events = {}
        end

        unitFrame = unitHandlers[ unit2 ]

        unitFrame.events[ event ] = unitFrame.events[ event ] or {}
        insert( unitFrame.events[ event ], handler )

        Hekili.EventSources[ event .. "_" .. unit2 .. "_" .. #unitFrame.events[ event ] ] = file and ( file .. ":" .. ( line or 0 ) ) or stack:match( "^(.*)\n" )

        unitFrame:RegisterUnitEvent( event, unit2 )
        Hekili:ProfileCPU( event .. "_" .. unit2 .. "_" .. #unitFrame.events[ event ], handler )
    end
end
local RegisterUnitEvent = ns.RegisterUnitEvent


function ns.UnregisterUnitEvent( event, handler )
    local hands = unitHandlers[ event ]

    if not hands then return end

    for i = #hands, 1, -1 do
        if hands[i] == handler then
            remove( hands, i )
        end
    end
end


ns.FeignEvent = function( event, ... )
    local eventHandlers = handlers[ event ]

    if not eventHandlers then return end

    for i, handler in pairs( eventHandlers ) do
        handler( event, ... )
    end
end
Hekili.FeignEvent = ns.FeignEvent


do
    local isUnregistered = false
    local next = _G.next

    local requeued = {}

    local HandleSpellData = function( event, spellID, success )
        local callbacks = spellCallbacks[ spellID ]

        if callbacks then
            for i = #callbacks, 1, -1 do
                callbacks[i]( event, spellID, success )
                remove( callbacks, i )
            end

            if #callbacks == 0 then
                spellCallbacks[ spellID ] = nil
            end
        end

        if spellCallbacks == nil or next( spellCallbacks ) == nil then
            UnregisterEvent( "SPELL_DATA_LOAD_RESULT", HandleSpellData )
            isUnregistered = true
        end
    end

    function Hekili:ContinueOnSpellLoad( spellID, func )
        if C_Spell.IsSpellDataCached( spellID ) then
            func( true )
            return
        end

        local callbacks = spellCallbacks[ spellID ] or {}
        insert( callbacks, func )
        spellCallbacks[ spellID ] = callbacks

        if isUnregistered then
            RegisterEvent( "SPELL_DATA_LOAD_RESULT", HandleSpellData )
            isUnregistered = false
        end

        C_Spell.RequestLoadSpellData( spellID )
    end

    function Hekili:RunSpellCallbacks()
        for spell, callbacks in pairs( spellCallbacks ) do
            for i = #callbacks, 1, -1 do
                if not callbacks[ i ]( true ) == false then remove( callbacks, i ) end
            end

            if #callbacks == 0 then
                spellCallbacks[ spell ] = nil
            end
        end
    end
end


RegisterEvent( "DISPLAY_SIZE_CHANGED", function()
    Hekili:BuildUI()
end )



RegisterEvent( "PLAYER_ENTERING_WORLD", function( event, login, reload )
    if not Hekili.PLAYER_ENTERING_WORLD and ( login or reload ) then
        Hekili.PLAYER_ENTERING_WORLD = true
        Hekili:SpecializationChanged()
        Hekili:RestoreDefaults()

        ns.checkImports()
        ns.updateGear()

        if state.combat == 0 and InCombatLockdown() then
            state.combat = GetTime() - 0.01
        end

        local _, zone, _, _, _, _, _, instanceID = GetInstanceInfo()
        state.bg = zone == "pvp"
        state.arena = zone == "arena"
        state.torghast = IsInJailersTower()
        state.instance_id = instanceID or -1

        Hekili:BuildUI()
    end
end )


do
    if Hekili.IsWrath() then
        RegisterEvent( "ACTIVE_TALENT_GROUP_CHANGED", function()
            Hekili:SpecializationChanged()
        end )
    else
        local specializationEvents = {
            ACTIVE_PLAYER_SPECIALIZATION_CHANGED = 1,
            ACTIVE_TALENT_GROUP_CHANGED = 1,
            CONFIRM_TALENT_WIPE = 1,
            PLAYER_TALENT_UPDATE = 1,
            SPEC_INVOLUNTARILY_CHANGED = 1,
            TALENTS_INVOLUNTARILY_RESET = 1
        }

        local function CheckForTalentUpdate( event )
            local specialization = GetSpecialization()
            local specID = specialization and GetSpecializationInfo( specialization )

            if specID and specID ~= state.spec.id then
                Hekili.PendingSpecializationChange = true
            end
        end

        RegisterEvent( "ACTIVE_PLAYER_SPECIALIZATION_CHANGED", CheckForTalentUpdate )

        for event in pairs( specializationEvents ) do
            RegisterEvent( event, CheckForTalentUpdate )
        end
    end
end


do
    local function UpdateZoneInfo()
        local _, zone, _, _, _, _, _, instanceID = GetInstanceInfo()
        state.bg = zone == "pvp"
        state.arena = zone == "arena"
        state.torghast = IsInJailersTower()
        state.instance_id = instanceID or -1
    end

    RegisterEvent( "ZONE_CHANGED", UpdateZoneInfo )
    RegisterEvent( "ARENA_PREP_OPPONENT_SPECIALIZATIONS", UpdateZoneInfo )
end


-- Hide when going into the barbershop.
RegisterEvent( "BARBER_SHOP_OPEN", function ()
    Hekili.Barber = true
end )

RegisterEvent( "BARBER_SHOP_CLOSE", function ()
    Hekili.Barber = false
end )


-- Update visibility when getting on/off a taxi.
RegisterEvent( "PLAYER_CONTROL_LOST", function ()
    Hekili:After( 0.1, Hekili.UpdateDisplayVisibility, Hekili )
end )

RegisterEvent( "PLAYER_CONTROL_GAINED", function ()
    Hekili:After( 0.1, Hekili.UpdateDisplayVisibility, Hekili )
end )


function ns.updateTalents()
    for k, _ in pairs( state.talent ) do
        state.talent[ k ].enabled = false
    end

    -- local specGroup = GetSpecialization()

    for k, v in pairs( class.talents ) do
        local _, name, _, enabled, _, sID, _, _, _, _, known = GetTalentInfoByID( v, 1 )

        if not name then
            -- We probably used a spellID.
            enabled = IsPlayerSpell( v )
        end

        enabled = enabled or known

        if rawget( state.talent, k ) then
            state.talent[ k ].enabled = enabled
        else
            state.talent[ k ] = { enabled = enabled }
        end
    end

    for k, _ in pairs( state.pvptalent ) do
        state.pvptalent[ k ]._enabled = false
    end

    for k, v in pairs( class.pvptalents ) do
        local _, name, _, enabled, _, sID, _, _, _, known = GetPvpTalentInfoByID( v, 1 )

        if not name then
            enabled = IsPlayerSpell( v )
        end

        enabled = enabled or known

        if rawget( state.pvptalent, k ) then
            state.pvptalent[ k ]._enabled = enabled
        else
            state.pvptalent[ k ] = {
                _enabled = enabled
            }
        end
    end

    WipeCovenantCache()
    ResetDisabledGearAndSpells()
end


-- TBD:  Consider making `boss' a check to see whether the current unit is a boss# unit instead.
RegisterEvent( "ENCOUNTER_START", function ( _, id, name, difficulty, groupSize )
    state.encounterID = id
    state.encounterName = name
    state.encounterDifficulty = difficulty
    state.encounterSize = groupSize
end )

RegisterEvent( "ENCOUNTER_END", function ()
    state.encounterID = 0
    state.encounterName = "None"
    state.encounterDifficulty = 0
    state.encounterSize = 0
end )


do
    local loc = ItemLocation:CreateEmpty()

    local GetAllTierInfo = C_AzeriteEmpoweredItem.GetAllTierInfo
    local GetPowerInfo = C_AzeriteEmpoweredItem.GetPowerInfo
    local IsAzeriteEmpoweredItemByID = C_AzeriteEmpoweredItem.IsAzeriteEmpoweredItemByID
    local IsPowerSelected = C_AzeriteEmpoweredItem.IsPowerSelected

    local MAX_INV_SLOTS = 19

    function ns.updatePowers()
        local p = state.azerite

        for k, v in pairs( p ) do
            v.__rank = 0
        end

        if next( class.powers ) == nil then
            C_Timer.After( 3, ns.updatePowers )
            return
        end

        for slot = 1, MAX_INV_SLOTS do
            local id = GetInventoryItemID( "player", slot )

            if id and IsAzeriteEmpoweredItemByID( id ) then
                loc:SetEquipmentSlot( slot )
                local tiers = GetAllTierInfo( loc )

                for tier, tierInfo in ipairs( tiers ) do
                    for _, power in ipairs( tierInfo.azeritePowerIDs ) do
                        local pInfo = GetPowerInfo( power )

                        if IsPowerSelected( loc, power ) then
                            local name = class.powers[ pInfo.spellID ]
                            if not name then
                                Hekili:Error( "Missing Azerite Power info for #" .. pInfo.spellID .. ": " .. GetSpellInfo( pInfo.spellID ) .. "." )
                            else
                                p[ name ] = rawget( p, name ) or { __rank = 0 }
                                p[ name ].__rank = p[ name ].__rank + 1
                            end
                        end
                    end
                end
            end
        end

        loc:Clear()
    end

    Hekili:ProfileCPU( "updatePowers", ns.updatePowers )


    -- Essences
    local AE = C_AzeriteEssence
    local GetMilestoneEssence, GetEssenceInfo = AE.GetMilestoneEssence, AE.GetEssenceInfo
    local milestones = { 115, 116, 117, 119 }

    local essenceKeys = {
        [2]  = "azeroths_undying_gift",
        [3]  = "sphere_of_suppression",
        [4]  = "worldvein_resonance",
        [5]  = "essence_of_the_focusing_iris",
        [6]  = "purification_protocol",
        [7]  = "anima_of_life_and_death",
        [12] = "the_crucible_of_flame",
        [13] = "nullification_dynamo",
        [14] = "condensed_lifeforce",
        [15] = "ripple_in_space",
        [16] = "unwavering_ward",
        [17] = "everrising_tide",
        [18] = "artifice_of_time",
        [19] = "well_of_existence",
        [20] = "lifebinders_invocation",
        [21] = "vitality_conduit",
        [22] = "vision_of_perfection",
        [23] = "blood_of_the_enemy",
        [24] = "spirit_of_preservation",
        [25] = "aegis_of_the_deep",
        [27] = "memory_of_lucid_dreams",
        [28] = "the_unbound_force",
        [32] = "conflict_and_strife",
        [33] = "touch_of_the_everlasting",
        [34] = "strength_of_the_warden",
        [35] = "breath_of_the_dying",
        [36] = "spark_of_inspiration",
        [37] = "the_formless_void"
    }

    local essenceMajors = {
        -- everrising_tide = "",
        -- lifebinders_invocation = "",
        -- touch_of_the_everlasting = "",
        -- vision_of_perfection = "",
        -- vitality_conduit = "",
        -- well_of_existence = "",
        -- conflict_and_strife = "",
        aegis_of_the_deep = "aegis_of_the_deep",
        anima_of_life_and_death = "anima_of_death",
        artifice_of_time = "standstill",
        azeroths_undying_gift = "azeroths_undying_gift",
        blood_of_the_enemy = "blood_of_the_enemy",
        breath_of_the_dying = "reaping_flames",
        condensed_lifeforce = "guardian_of_azeroth",
        essence_of_the_focusing_iris = "focused_azerite_beam",
        memory_of_lucid_dreams = "memory_of_lucid_dreams",
        nullification_dynamo = "empowered_null_barrier",
        purification_protocol = "purifying_blast",
        ripple_in_space = "ripple_in_space",
        spark_of_inspiration = "moment_of_glory",
        sphere_of_suppression = "suppressing_pulse",
        spirit_of_preservation = "spirit_of_preservation",
        strength_of_the_warden = "vigilant_protector",
        the_crucible_of_flame = "concentrated_flame",
        the_formless_void = "replica_of_knowledge",
        the_unbound_force = "the_unbound_force",
        unwavering_ward = "guardian_shell",
        worldvein_resonance = "worldvein_resonance",
    }

    for _, key in pairs( essenceKeys ) do
        state.essence[ key ] = { __rank = 0, __major = false }
    end


    function ns.updateEssences()
        local e = state.bfa_essence

        for k, v in pairs( e ) do
            v.__rank = 0
        end

        class.active_essence = nil

        if state.equipped[ 158075 ] then
            for i, ms in ipairs( milestones ) do
                local essence = GetMilestoneEssence( ms )

                if essence then
                    local info = GetEssenceInfo( essence )

                    if info then
                        local key = essenceKeys[ info.ID ]

                        e[ key ].__rank = info.rank
                        e[ key ].__minor = true

                        if i == 1 then
                            e[ key ].__major = true
                            class.active_essence = essenceMajors[ key ]
                        end
                    end
                end
            end
        end
    end

    ns.updateEssences()
end


do
    local function itemSorter( a, b )
        local action1, action2 = class.abilities[ a.action ].cooldown, class.abilities[ b.action ].cooldown
        return action1 > action2
    end

    local function buildUseItemsList()
        local itemList = class.itemPack.lists.items
        wipe( itemList )

        if #state.items > 0 then
            for i, item in ipairs( state.items ) do
                if not Hekili:IsItemScripted( item ) then
                    insert( itemList, {
                        action = item,
                        enabled = true,
                        criteria = "( ! settings.boss || boss ) & " ..
                            "( settings.targetMin = 0 || active_enemies >= settings.targetMin ) & " ..
                            "( settings.targetMax = 0 || active_enemies <= settings.targetMax )"
                    } )
                end
            end
        end

        sort( itemList, itemSorter )

        class.essence_unscripted = ( class.active_essence and not Hekili:IsEssenceScripted( class.active_essence ) ) or false

        Hekili:LoadItemScripts()
    end

    function Hekili:UpdateUseItems()
        if not Hekili.PLAYER_ENTERING_WORLD then
            C_Timer.After( 1, buildUseItemsList )
            return
        end

        buildUseItemsList()
    end


    local GearHooks = {}

    -- This is a simple way to separate expansion-based gear into separate systems.
    function Hekili:RegisterGearHook( r, u )
        insert( GearHooks, {
            reset = r,
            update = u
        } )
    end

    local wasWearing = {}
    local maxItemSlot = Hekili.IsWrath() and INVSLOT_LAST_EQUIPPED or Enum.ItemSlotFilterTypeMeta.MaxValue

    local timer

    local function Update()
        ns.updateGear()
    end

    local function QueueUpdate()
        if timer and not timer:IsCancelled() then timer:Cancel() end
        timer = C_Timer.NewTimer( 1, Update )
    end

    function ns.updateGear()
        if not Hekili.PLAYER_ENTERING_WORLD then
            QueueUpdate()
            return
        end

        wipe( state.set_bonus )

        for _, hook in ipairs( GearHooks ) do
            if hook.reset then hook.reset() end
        end

        wipe( wasWearing )

        for i, item in ipairs( state.items ) do
            wasWearing[ i ] = item
        end

        wipe( state.items )

        for set, items in pairs( class.gear ) do
            state.set_bonus[ set ] = 0
            for item, _ in pairs( items ) do
                if item > maxItemSlot and IsEquippedItem( item ) then
                    state.set_bonus[ set ] = state.set_bonus[ set ] + 1
                end
            end
        end

        for bonus, aura in pairs( class.setBonuses ) do
            if UA_GetPlayerAuraBySpellID( aura ) then
                state.set_bonus[ bonus ] = 1
            end
        end

        -- Trinkets
        -- We want to know:
        -- 1. Which trinket?
        -- 2. Does it have a spell?  (GetItemSpell)
        -- 3. Does it have an on-use?  (IsItemUsable)
        -- 4. ???

        local T1 = GetInventoryItemID( "player", 13 )

        state.trinket.t1.__id = 0
        state.trinket.t1.ilvl = 0
        state.trinket.t1.__ability = "null_cooldown"
        state.trinket.t1.__usable = false
        state.trinket.t1.__has_use_buff = false
        state.trinket.t1.__has_use_damage = false
        state.trinket.t1.__use_buff_duration = 0.01
        state.trinket.t1.__proc = false

        if T1 then
            state.trinket.t1.__id = T1
            -- So this isn't *truly* accurate, but it's accurate relatively speaking.
            state.trinket.t1.ilvl = GetDetailedItemLevelInfo( T1 )

            local isUsable = IsUsableItem( T1 )
            local name, spellID = GetItemSpell( T1 )
            local tSpell = class.itemMap[ T1 ]

            if tSpell then
                class.abilities.trinket1 = class.abilities[ tSpell ]
                class.specs[ 0 ].abilities.trinket2 = class.abilities[ tSpell ]

                state.trinket.t1.__usable = isUsable
                state.trinket.t1.__ability = tSpell

                local ability = class.abilities[ tSpell ]

                state.trinket.t1.__has_use_damage = ability and ability.proc == "damage"

                local aura = ability and class.auras[ ability.self_buff or spellID ]

                if spellID and SpellIsSelfBuff( spellID ) and aura then
                    state.trinket.t1.__has_use_buff = not aura.ignore_buff and not ( ability and ability.proc and ( ability.proc == "damage" or ability.proc == "healing" or ability.proc == "mana" or ability.proc == "absorb" or ability.proc == "speed" ) )
                    state.trinket.t1.__use_buff_duration = aura.duration > 0 and aura.duration or 0.01
                elseif ability.self_buff then
                    state.trinket.t1.__has_use_buff = not aura.ignore_buff and not ( ability and ability.proc and ( ability.proc == "damage" or ability.proc == "healing" or ability.proc == "mana" or ability.proc == "absorb" or ability.proc == "speed" ) )
                    state.trinket.t1.__use_buff_duration = aura and aura.duration > 0 and aura.duration or 0.01
                end

                if not isUsable then
                    state.trinket.t1.cooldown = state.cooldown.null_cooldown
                else
                    state.cooldown.trinket1 = state.cooldown[ ability.key ]
                    state.trinket.t1.cooldown = state.cooldown.trinket1
                end
            else
                class.abilities.trinket1 = class.abilities.actual_trinket1
                class.specs[ 0 ].abilities.trinket1 = class.abilities.actual_trinket1
                state.trinket.t1.cooldown = state.cooldown.null_cooldown
            end

            if not isUsable then state.trinket.t1.__proc = FindStringInInventoryItemTooltip( "^" .. ITEM_SPELL_TRIGGER_ONEQUIP, 13, true, true ) end
        end

        local T2 = GetInventoryItemID( "player", 14 )

        state.trinket.t2.__id = 0
        state.trinket.t2.__ability = "null_cooldown"
        state.trinket.t2.__usable = false
        state.trinket.t2.__has_use_buff = false
        state.trinket.t2.__has_use_damage = false
        state.trinket.t2.__use_buff_duration = 0.01
        state.trinket.t2.__proc = false
        state.trinket.t2.ilvl = 0

        if T2 then
            state.trinket.t2.__id = T2
             -- So this isn't *truly* accurate, but it's accurate relatively speaking.
             state.trinket.t2.ilvl = GetDetailedItemLevelInfo( T2 )

            local isUsable = IsUsableItem( T2 )
            local name, spellID = GetItemSpell( T2 )
            local tSpell = class.itemMap[ T2 ]

            if tSpell then
                class.abilities.trinket2 = class.abilities[ tSpell ]
                class.specs[ 0 ].abilities.trinket2 = class.abilities[ tSpell ]

                state.trinket.t2.__usable = isUsable
                state.trinket.t2.__ability = tSpell

                local ability = class.abilities[ tSpell ]

                state.trinket.t2.__has_use_damage = ability and ability.proc == "damage"

                local aura = class.auras[ ability.self_buff or spellID ]

                if spellID and SpellIsSelfBuff( spellID ) and aura then
                    state.trinket.t2.__has_use_buff = not aura.ignore_buff and not ( ability and ability.proc and ( ability.proc == "damage" or ability.proc == "healing" or ability.proc == "mana" or ability.proc == "absorb" or ability.proc == "speed" ) )
                    state.trinket.t2.__use_buff_duration = aura.duration > 0 and aura.duration or 0.01
                elseif ability.self_buff then
                    state.trinket.t2.__has_use_buff = true
                    state.trinket.t2.__use_buff_duration = aura and aura.duration > 0 and aura.duration or 0.01
                end

                if not isUsable then
                    state.trinket.t2.cooldown = state.cooldown.null_cooldown
                else
                    state.cooldown.trinket2 = state.cooldown[ ability.key ]
                    state.trinket.t2.cooldown = state.cooldown.trinket2
                end
            else
                class.abilities.trinket2 = class.abilities.actual_trinket2
                class.specs[ 0 ].abilities.trinket2 = class.abilities.actual_trinket2
                state.trinket.t2.cooldown = state.cooldown.null_cooldown
            end

            if not isUsable then state.trinket.t2.__proc = FindStringInInventoryItemTooltip( "^" .. ITEM_SPELL_TRIGGER_ONEQUIP, 14, true, true ) end
        end

        state.main_hand.size = 0
        state.off_hand.size = 0

        local MH = GetInventoryItemID( "player", 16 )

        state.trinket.main_hand.__id = 0
        state.trinket.main_hand.__ability = "null_cooldown"
        state.trinket.main_hand.__usable = false
        state.trinket.main_hand.__has_use_buff = false
        state.trinket.main_hand.__has_use_damage = false
        state.trinket.main_hand.__use_buff_duration = 0.01
        state.trinket.main_hand.__proc = false
        state.trinket.main_hand.ilvl = 0

        class.abilities.main_hand = class.abilities.actual_main_hand

        if MH then
            local isUsable = IsUsableItem( MH )
            local name, spellID = GetItemSpell( MH )
            local tSpell = class.itemMap[ MH ]
            local ability = class.abilities[ tSpell ]

            if ability and tSpell then
                class.abilities.main_hand = class.abilities[ tSpell ]
                class.specs[ 0 ].abilities.main_hand = class.abilities[ tSpell ]

                local aura = ability and class.auras[ ability.self_buff or spellID ]

                if spellID and SpellIsSelfBuff( spellID ) and aura then
                    state.trinket.main_hand.__has_use_buff = not aura.ignore_buff and not ( ability and ability.proc and ( ability.proc == "damage" or ability.proc == "healing" or ability.proc == "mana" or ability.proc == "absorb" or ability.proc == "speed" ) )
                    state.trinket.main_hand.__use_buff_duration = aura.duration > 0 and aura.duration or 0.01
                elseif ability.self_buff then
                    state.trinket.main_hand.__has_use_buff = true
                    state.trinket.main_hand.__use_buff_duration = aura and aura.duration > 0 and aura.duration or 0.01
                end

                if not isUsable then
                    state.trinket.main_hand.cooldown = state.cooldown.null_cooldown
                else
                    state.cooldown.main_hand = state.cooldown[ ability.key ]
                    state.trinket.main_hand.cooldown = state.cooldown.main_hand
                end
            else
                class.abilities.main_hand = class.abilities.actual_main_hand
                class.specs[ 0 ].abilities.main_hand = class.abilities.actual_main_hand
                state.trinket.main_hand.cooldown = state.cooldown.null_cooldown
            end

            if not isUsable then state.trinket.t2.__proc = FindStringInInventoryItemTooltip( "^" .. ITEM_SPELL_TRIGGER_ONEQUIP, 16, true, true ) end
        end

        for i = 1, 19 do
            local item = GetInventoryItemID( "player", i )

            if item then
                state.set_bonus[ item ] = 1
                local key, _, _, _, _, _, _, _, equipLoc = CGetItemInfo( item )
                if key then
                    key = formatKey( key )
                    state.set_bonus[ key ] = 1
                end

                if i == 16 then
                    if equipLoc == "INVTYPE_2HWEAPON" then
                        state.main_hand.size = 2
                    elseif equipLoc == "INVTYPE_WEAPON" or equipLoc == "INVTYPE_WEAPONMAINHAND" then
                        state.main_hand.size = 1
                    elseif equipLoc == "INVTYPE_RANGED" or equipLoc == "INVTYPE_RANGEDRIGHT" then
                        state.set_bonus.ranged = 1
                    end
                elseif i == 17 then
                    if equipLoc == "INVTYPE_2HWEAPON" then
                        state.off_hand.size = 2
                    elseif equipLoc == "INVTYPE_WEAPON" or equipLoc == "INVTYPE_WEAPONOFFHAND" then
                        state.off_hand.size = 1
                    elseif equipLoc == "INVTYPE_RANGED" or equipLoc == "INVTYPE_RANGEDRIGHT" then
                        state.set_bonus.ranged = 1
                    elseif equipLoc == "INVTYPE_SHIELD" then
                        state.set_bonus.shield = 1
                    end
                end



                -- Fire any/all GearHooks (may be expansion-driven).
                for _, hook in ipairs( GearHooks ) do
                    if hook.update then hook.update( i, item ) end
                end

                local usable = class.itemMap[ item ]
                if usable then insert( state.items, usable ) end
            end
        end

        -- Improve Pocket-Sized Computronic Device.
        if state.equipped.pocketsized_computation_device then
            local tName = CGetItemInfo( 167555 )
            local redName, redLink = C_Item.GetItemGem( tName, 1 )

            if redName and redLink then
                local redID = tonumber( redLink:match("item:(%d+)") )
                local action = class.itemMap[ redID ]

                if action and class.abilities[ action ] and redID then
                    state.set_bonus[ action ] = 1
                    state.set_bonus[ redID ] = 1
                    class.abilities.pocketsized_computation_device = class.abilities[ action ]
                    class.abilities[ tName ] = class.abilities[ action ]
                    insert( state.items, action )
                end
            else
                if class.abilities.inactive_red_punchcard then
                    class.abilities.pocketsized_computation_device = class.abilities.inactive_red_punchcard
                    class.abilities[ tName ] = class.abilities.inactive_red_punchcard
                end
            end
        end

        ns.updatePowers()
        ns.updateTalents()
        ns.updateEssences()

        local sameItems = #wasWearing == #state.items

        if sameItems then
            for i = 1, #state.items do
                if wasWearing[i] ~= state.items[i] then
                    sameItems = false
                    break
                end
            end
        end

        Hekili:UpdateUseItems()
        state.swings.mh_speed, state.swings.oh_speed = UnitAttackSpeed( "player" )
    end

    RegisterEvent( "PLAYER_EQUIPMENT_CHANGED", QueueUpdate )
end


do
    local timer

    local function Update()
        ns.updateTalents()
    end

    local function QueueUpdate()
        if timer and not timer:IsCancelled() then timer:Cancel() end
        timer = C_Timer.NewTimer( 0.5, Update )
    end

    local talentEvents = {
        "TRAIT_CONFIG_CREATED",
        "ACTIVE_COMBAT_CONFIG_CHANGED",
        "STARTER_BUILD_ACTIVATION_FAILED",
        "TRAIT_CONFIG_DELETED",
        "TRAIT_CONFIG_UPDATED",
        "CONFIG_COMMIT_FAILED",
        "PLAYER_ALIVE",
        "PLAYER_UNGHOST"
    }

    for _, event in pairs( talentEvents ) do
        RegisterEvent( event, QueueUpdate )
    end
end


-- Update Azerite Essence Data.
do
    local azeriteEvents = {
        "AZERITE_ESSENCE_UPDATE",
        "AZERITE_ESSENCE_MILESTONE_UNLOCKED",
        "AZERITE_ESSENCE_FORGE_CLOSE",
        "AZERITE_ESSENCE_CHANGED",
        "AZERITE_ESSENCE_ACTIVATED",
        "AZERITE_ESSENCE_ACTIVATION_FAILED"
    }

    local function UpdateEssences()
        local lastEssence = class.active_essence
        ns.updateEssences()

        if class.active_essence ~= lastEssence then
            Hekili:UpdateUseItems()
        end
    end

    --[[ for i, event in pairs( azeriteEvents ) do
        RegisterEvent( event, UpdateEssences )
    end ]]
end


local last_combat, combat_ended = 0, 0
local COMBAT_RESUME_TIME = 5

RegisterEvent( "PLAYER_REGEN_DISABLED", function( event )
    local t = GetTime()

    if t - combat_ended <= COMBAT_RESUME_TIME then
        state.combat = last_combat
    else
        state.combat = GetTime() - 0.01
    end

    if Hekili.Config and not LibStub( "AceConfigDialog-3.0" ).OpenFrames[ "Hekili" ] then
        ns.StopConfiguration()
        Hekili:UpdateDisplayVisibility()
    end

    -- Hekili:ExpireTTDs( true )
    Hekili:ForceUpdate( event ) -- Force update on entering combat since OOC refresh can be very slow (0.5s).
end )


RegisterEvent( "PLAYER_REGEN_ENABLED", function ()
    last_combat = state.combat
    combat_ended = GetTime()

    state.combat = 0

    state.swings.mh_actual = 0
    state.swings.oh_actual = 0

    C_Timer.After( 5, function ()
        if not InCombatLockdown() then
            ns.Audit( "combatExit" )
        end
    end )

    Hekili:ReleaseHolds( true )
    Hekili:UpdateDisplayVisibility()
end )


local dynamic_keys = setmetatable( {}, {
    __index = function( t, k )
        local name = GetSpellInfo( k )
        local key = name and formatKey( name ) or k
        t[k] = key
        return t[k]
    end
} )


ns.castsOff = { 'no_action', 'no_action', 'no_action', 'no_action', 'no_action' }
ns.castsOn = { 'no_action', 'no_action', 'no_action', 'no_action', 'no_action' }
ns.castsAll = { 'no_action', 'no_action', 'no_action', 'no_action', 'no_action' }

local castsOn, castsOff, castsAll = ns.castsOn, ns.castsOff, ns.castsAll


function state:AddToHistory( spellID, destGUID, rank )
    local ability = class.abilities[ spellID ]
    local key = ability and ability.key or dynamic_keys[ spellID ]

    local now = GetTime()
    local player = self.player

    player.lastcast = key
    player.casttime = now

    if ability then
        local history = self.prev.history
        insert( history, 1, key )
        history[6] = nil

        if ability.gcd ~= "off" then
            history = self.prev_gcd.history
            player.lastgcd = key
            player.lastgcdtime = now
        else
            history = self.prev_off_gcd.history
            player.lastoffgcd = key
            player.lastoffgcdtime = now
        end
        insert( history, 1, key )
        history[6] = nil

        ability.realCast = now
        ability.realUnit = destGUID
        ability.realRank = rank
    end
end


local SpellQueueWindow = 0.4

local function UpdateSpellQueueWindow( event, variable, value )
    if variable == "SpellQueueWindow" then
        SpellQueueWindow = ( tonumber( value ) or 400 ) / 1000
    end
end

RegisterEvent( "CVAR_UPDATE", UpdateSpellQueueWindow )
RegisterEvent( "VARIABLES_LOADED", UpdateSpellQueueWindow )

C_Timer.After( 60, UpdateSpellQueueWindow )


do
    local box, text
    local info = {}

    hooksecurefunc( "ChatEdit_SendText", function( b )
        if box and box == b and text then
            local action, target = SecureCmdOptionParse( text )
            local ability = action and class.abilities[ action ]

            if ability and ability.key then
                local m = info[ ability.key ] or {}

                m.target = UnitGUID( target or "target" )
                m.time   = GetTime()

                info[ ability.key ] = m
            end

            text = nil
            return
        end

        if not box and b ~= DEFAULT_CHAT_FRAME.editBox then
            box = b

            box:HookScript( "OnTextSet", function( self )
                local t = self:GetText()
                if t and t ~= "" then text = t end
            end )
        end
    end )

    function Hekili:GetMacroCastTarget( spell, castTime, source )
        local ability = class.abilities[ spell ]
        local buffer = 0.1 + SpellQueueWindow

        if ability and ability.key then
            local m = info[ ability.key ]

            if m and abs( castTime - m.time ) < buffer then
                return m.target -- This is a GUID.
            end
        end
    end
end


local lowLevelWarned = false
local noClassWarned = false
-- Change here every expansion to automatically warn low-level users.
local minimumLevel = 71
local expansionName = "The War Within"

-- Need to make caching system.
RegisterUnitEvent( "UNIT_SPELLCAST_SUCCEEDED", "player", "target", function( event, unit, _, spellID )
    if not noClassWarned and not class.initialized then
        Hekili:Notify( UnitClass( "player" ) .. " does not have any Hekili modules loaded (yet).\nWatch for updates.", 10 )
        noClassWarned = true
    elseif not lowLevelWarned and UnitLevel( "player" ) < minimumLevel then
        Hekili:Notify( "Hekili is designed for use in " .. expansionName .. " content.\nIt may not work as expected below level " .. minimumLevel .. ".", 10 )
        lowLevelWarned = true
    end

    if unit == "player" then
        local ability = class.abilities[ spellID ]

        if ability then
            Hekili:ForceUpdate( event )
            if state.holds[ ability.key ] then Hekili:RemoveHold( ability.key, true ) end
        end
    end
end )


RegisterUnitEvent( "UNIT_SPELLCAST_START", "player", "target", function( event, unit, cast, spellID )
    if unit == "player" then
        local ability = class.abilities[ spellID ]
        if ability then
            Hekili:ForceUpdate( event )
            if state.holds[ ability.key ] then Hekili:RemoveHold( ability.key, true ) end
        end

        Hekili:ForceUpdate( event, true )
    end
end )


do
    local empowerment = state.empowerment
    local stages = empowerment.stages

    RegisterUnitEvent( "UNIT_SPELLCAST_EMPOWER_START", "player", nil, function( event, unit, cast, spellID )
        local ability = class.abilities[ spellID ]
        if not ability then return end

        wipe( stages )
        local start = GetTime()

        empowerment.spell = ability.key
        empowerment.start = start
        empowerment.active = true

        for i = 1, 4 do
            local n = GetUnitEmpowerStageDuration( "player", i - 1 )
            if n == 0 then break end

            if i == 1 then insert( stages, start + n * 0.001 )
            else insert( stages, stages[ i - 1 ] + n * 0.001 ) end
        end

        empowerment.finish = stages[ #stages ]
        empowerment.hold = empowerment.finish + GetUnitEmpowerHoldAtMaxTime( "player" ) * 0.001

        Hekili:ForceUpdate( event, true )
    end )

    RegisterUnitEvent( "UNIT_SPELLCAST_EMPOWER_STOP", "player", nil, function( event, unit, cast, spellID )
        empowerment.spell = "none"

        empowerment.active = false
        empowerment.start = 0
        empowerment.finish = 0
        empowerment.hold = 0

        Hekili:ForceUpdate( event )
    end )
end


RegisterUnitEvent( "UNIT_SPELLCAST_CHANNEL_START", "player", nil, function( event, unit, cast, spellID )
    local ability = class.abilities[ spellID ]
    if not ability then return end

    if state.holds[ ability.key ] then Hekili:RemoveHold( ability.key, true ) end
    Hekili:ForceUpdate( event )
end )


RegisterUnitEvent( "UNIT_SPELLCAST_CHANNEL_STOP", "player", nil, function( event, unit, cast, spellID )
    Hekili:ForceUpdate( event )

end )


RegisterUnitEvent( "UNIT_SPELLCAST_CHANNEL_UPDATE", "player", nil, function( event, unit, _, spellID )
    Hekili:ForceUpdate( event )
end )


RegisterUnitEvent( "UNIT_SPELLCAST_STOP", "player", nil, function( event, unit, cast, spellID )
    local ability = class.abilities[ spellID ]
    if not ability then return end

    if state.holds[ ability.key ] then Hekili:RemoveHold( ability.key, true ) end
    Hekili:ForceUpdate( event )
end )


RegisterUnitEvent( "UNIT_SPELLCAST_DELAYED", "player", nil, function( event, unit, _, spellID )
    local ability = class.abilities[ spellID ]

    if ability then
        local action = ability.key
        local _, _, _, start, finish = UnitCastingInfo( "player" )
        local target = select( 5, state:GetEventInfo( action, nil, nil, "CAST_FINISH", nil, true ) )

        state:RemoveSpellEvent( action, true, "CAST_FINISH" )
        state:RemoveSpellEvent( action, true, "PROJECTILE_IMPACT", true )

        if start and finish then
            if not target then target = Hekili:GetMacroCastTarget( action, start / 1000, "DELAYED" ) end
            state:QueueEvent( action, start / 1000, finish / 1000, "CAST_FINISH", target, true )

            if ability.isProjectile then
                local travel

                if ability.flightTime then
                    travel = ability.flightTime

                elseif target then
                    local u = Hekili:GetUnitByGUID( target ) or Hekili:GetNameplateUnitForGUID( target ) or "target"

                    if u then
                        local _, maxR = RC:GetRange( u )
                        maxR = maxR or select( 6, GetSpellInfo( ability.id ) ) or 40
                        travel = maxR / ability.velocity
                    end
                end

                if not travel then
                    travel = ( select( 6, GetSpellInfo( ability.id ) ) or 40 ) / ability.velocity
                end

                state:QueueEvent( ability.impactSpell or ability.key, finish / 1000, 0.05 + travel, "PROJECTILE_IMPACT", target, true )
            end
        end

        Hekili:ForceUpdate( event )
    end
end )


-- TODO:  This should be changed to stash this information and then commit it on next UNIT_SPELLCAST_START or UNIT_SPELLCAST_SUCCEEDED.
RegisterEvent( "UNIT_SPELLCAST_SENT", function ( event, unit, target_name, castID, spellID )
    Hekili:ForceUpdate( event )

    if target_name and UnitGUID( target_name ) then
        state.cast_target = UnitGUID( target_name )
        return
    end

    local gubn = Hekili:GetUnitByName( target_name )
    if gubn and UnitGUID( gubn ) then
        state.cast_target = UnitGUID( gubn )
        return
    end

    if UnitName( "target" ) == target_name then
        state.cast_target = UnitGUID( "target" )
        return
    end

    state.cast_target = nil
end )


--[[ This event is too spammy.
RegisterEvent( "CURRENT_SPELL_CAST_CHANGED", function( event, cancelled )
    Hekili:ForceUpdate( event, true )
end ) ]]


--[[ Update due to player totems.
RegisterEvent( "PLAYER_TOTEM_UPDATE", function( event )
    Hekili:ForceUpdate( event )
end ) -- TODO:  Re-evaluate whether this is necessary to force a faster update. ]]


local power_tick_data = {
    focus_avg = 0.10,
    focus_ticks = 1,

    energy_avg = 0.10,
    energy_ticks = 1,
}


local spell_names = setmetatable( {}, {
    __index = function( t, k )
        t[ k ] = GetSpellInfo( k )
        return t[ k ]
    end
} )


local lastPower = {}

local function UNIT_POWER_FREQUENT( event, unit, power )

    if power == "FOCUS" and rawget( state, "focus" ) then
        local now = GetTime()
        local elapsed = now - ( state.focus.last_tick or 0 )

        elapsed = elapsed > power_tick_data.focus_avg * 1.5 and power_tick_data.focus_avg or elapsed

        if elapsed > 0.075 then
            power_tick_data.focus_avg = ( elapsed + ( power_tick_data.focus_avg * power_tick_data.focus_ticks ) ) / ( power_tick_data.focus_ticks + 1 )
            power_tick_data.focus_ticks = power_tick_data.focus_ticks + 1
            state.focus.last_tick = now
        end

    elseif power == "ENERGY" and rawget( state, "energy" ) then
        local now = GetTime()
        local elapsed = min( 0.12, now - ( state.energy.last_tick or 0 ) )

        elapsed = elapsed > power_tick_data.energy_avg * 1.5 and power_tick_data.energy_avg or elapsed

        if elapsed > 0.075 then
            power_tick_data.energy_avg = ( elapsed + ( power_tick_data.energy_avg * power_tick_data.energy_ticks ) ) / ( power_tick_data.energy_ticks + 1 )
            power_tick_data.energy_ticks = power_tick_data.energy_ticks + 1
            state.energy.last_tick = now
        end
    end

    local newPower = UnitPower( "player", Enum.PowerType[ power ] )

    if lastPower[ power ] and newPower < lastPower[ power ] then
        Hekili:ForceUpdate( event, true )
    end

    lastPower[ power ] = newPower
end
Hekili:ProfileCPU( "UNIT_POWER_FREQUENT", UNIT_POWER_FREQUENT )

RegisterUnitEvent( "UNIT_POWER_FREQUENT", "player", nil, UNIT_POWER_FREQUENT )


local autoAuraKey = setmetatable( {}, {
    __index = function( t, k )
        local name = GetSpellInfo( k )

        if not name then return end

        local key = formatKey( name )

        if class.auras[ key ] then
            local i = 1

            while ( true ) do
                local new = key .. '_' .. i

                if not class.auras[ new ] then
                    key = new
                    break
                end

                i = i + 1
            end
        end

        -- Store the aura and save the key if we can.
        if ns.addAura then
            ns.addAura( key, k, 'name', name )
            t[k] = key
        end

        return t[k]
    end
} )


do
    local playerInstances = {}
    local targetInstances = {}

    local instanceDB

    local GetAuraDataByAuraInstanceID = C_UnitAuras.GetAuraDataByAuraInstanceID
    local ForEachAura = AuraUtil.ForEachAura

    local function StoreInstanceInfo( aura )
        local id = aura.spellId
        local model = class.auras[ id ]

        instanceDB[ aura.auraInstanceID ] = aura.isBossAura or aura.isStealable or model and ( model.shared or model.used and aura.isFromPlayerOrPlayerPet )
    end

    RegisterUnitEvent( "UNIT_AURA", "player", "target", function( event, unit, data )
        local isPlayer = ( unit == "player" )
        instanceDB = isPlayer and playerInstances or targetInstances

        if data.isFullUpdate then
            wipe( instanceDB )

            ForEachAura( unit, "HELPFUL", nil, StoreInstanceInfo, true )
            ForEachAura( unit, "HARMFUL", nil, StoreInstanceInfo, true )

            state[ unit ].updated = true
            Hekili:ForceUpdate( "UNIT_AURA_FULL", true )
            return
        end

        local forceUpdateNeeded = false

        if data.addedAuras and #data.addedAuras > 0 then
            for _, aura in ipairs( data.addedAuras ) do
                local id = aura.spellId
                local model = class.auras[ id ]

                local ofConcern = aura.isBossAura or aura.isStealable or model and ( model.shared or model.used and aura.isFromPlayerOrPlayerPet )
                instanceDB[ aura.auraInstanceID ] = ofConcern

                if ofConcern then
                    forceUpdateNeeded = true
                end
            end
        end

        if data.updatedAuraInstanceIDs and #data.updatedAuraInstanceIDs > 0 then
            for _, instance in ipairs( data.updatedAuraInstanceIDs ) do
                local aura = GetAuraDataByAuraInstanceID( unit, instance )
                local ofConcern = false

                if aura then
                    local id = aura.spellId
                    local model = class.auras[ id ]

                    ofConcern = aura.isBossAura or aura.isStealable or model and ( model.shared or model.used and aura.isFromPlayerOrPlayerPet )
                    instanceDB[ instance ] = ofConcern
                end

                if ofConcern then
                    forceUpdateNeeded = true
                end
            end
        end

        if data.removedAuraInstanceIDs and #data.removedAuraInstanceIDs > 0 then
            for _, instance in ipairs( data.removedAuraInstanceIDs ) do
                if instanceDB[ instance ] then forceUpdateNeeded = true end
                instanceDB[ instance ] = nil
            end
        end

        state[ unit ].updated = true

        if forceUpdateNeeded then Hekili:ForceUpdate( "UNIT_AURA_ITER" ) end
    end )

    RegisterEvent( "PLAYER_TARGET_CHANGED", function( event )
        instanceDB = targetInstances
        wipe( instanceDB )

        if UnitExists( "target" ) then
            ForEachAura( "target", "HELPFUL", nil, StoreInstanceInfo, true )
            ForEachAura( "target", "HARMFUL", nil, StoreInstanceInfo, true )
        end

        state.target.updated = true
        Hekili:ForceUpdate( event, true )
    end )
end



do
    local MOVEMENT_ICD = 0.5

    local lastStart = 0
    local lastEnd = 0

    RegisterEvent( "PLAYER_STARTED_MOVING", function( event )
        local now = GetTime()

        if now - lastStart > MOVEMENT_ICD then
            lastStart = now
            Hekili:ForceUpdate( event )
        end
    end )


    RegisterEvent( "PLAYER_STOPPED_MOVING", function( event )
        local now = GetTime()

        if now - lastEnd > MOVEMENT_ICD then
            lastEnd = now
            Hekili:ForceUpdate( event )
        end
    end )
end


local cast_events = {
    SPELL_CAST_START        = true,
    SPELL_CAST_FAILED       = true,
    SPELL_CAST_SUCCESS      = true,
    SPELL_DAMAGE            = true,
    SPELL_AURA_REMOVED      = true,
    SPELL_EMPOWER_START     = true,
    SPELL_EMPOWER_INTERRUPT = true,
    SPELL_EMPOWER_END       = true
}


local aura_events = {
    SPELL_AURA_APPLIED      = true,
    SPELL_AURA_APPLIED_DOSE = true,
    SPELL_AURA_REFRESH      = true,
    SPELL_AURA_REMOVED      = true,
    SPELL_AURA_REMOVED_DOSE = true,
    SPELL_AURA_BROKEN       = true,
    SPELL_AURA_BROKEN_SPELL = true,
    SPELL_CAST_SUCCESS      = true -- it appears you can refresh stacking buffs w/o a SPELL_AURA_x event.
}


local dmg_events = {
    SPELL_DAMAGE            = true,
    SPELL_MISSED            = true,
    SPELL_PERIODIC_DAMAGE   = true,
    SPELL_PERIODIC_MISSED   = true,
    SWING_DAMAGE            = true,
    SWING_MISSED            = true,
    RANGE_DAMAGE            = true,
    RANGE_MISSED            = true,
    ENVIRONMENTAL_DAMAGE    = true,
    ENVIRONMENTAL_MISSED    = true
}


local direct_dmg_events = {
    SPELL_DAMAGE            = true,
    SPELL_MISSED            = true,
    SWING_DAMAGE            = true,
    SWING_MISSED            = true,
    RANGE_DAMAGE            = true,
    RANGE_MISSED            = true,
    ENVIRONMENTAL_DAMAGE    = true,
    ENVIRONMENTAL_MISSED    = true
}


local death_events = {
    UNIT_DIED               = true,
    UNIT_DESTROYED          = true,
    UNIT_DISSIPATES         = true,
    PARTY_KILL              = true,
    SPELL_INSTAKILL         = true,
}

local dmg_filtered = {
    [280705] = true, -- Laser Matrix.
    [450412] = true, -- Sentinel.
    [462952] = true, -- Squall Sailor's Citrine
}


local function IsActuallyFriend( unit )
    if not IsInGroup() then return false end
    if not UnitIsPlayer( unit ) then return false end
    if UnitInRaid( unit ) or UnitInParty( unit ) then return true end
    return false
end


local countDamage = false
local countDots = false
local countPets = false


function Hekili:UpdateDamageDetectionForCLEU()
    local profile = self.DB.profile
    local spec = rawget( profile.specs, state.spec.id )

    countDamage = spec and spec.damage or false
    countDots = spec and spec.damageDots or false
    countPets = spec and spec.damagePets or false
end


-- Use dots/debuffs to count active targets.
-- Track dot power (until 6.0) for snapshotting.
-- Note that this was ported from an unreleased version of Hekili, and is currently only counting damaged enemies.
local function CLEU_HANDLER( event, timestamp, subtype, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, spellID, spellName, school, amount, interrupt, a, b, c, d, offhand, multistrike, ... )
    -- This is used by both RegisterCombatLogEvent( x ) and RegisterHook( "COMBAT_LOG_EVENT_UNFILTERED", x ).
    ns.callHook( "COMBAT_LOG_EVENT_UNFILTERED", timestamp, subtype, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, spellID, spellName, school, amount, interrupt, a, b, c, d, offhand, multistrike, ... )

    if death_events[ subtype ] then
        if ns.isTarget( destGUID ) then
            ns.eliminateUnit( destGUID, true )
            -- Hekili:ForceUpdate( subtype )

        elseif ns.isMinion( destGUID ) then
            local npcid = destGUID:match("(%d+)-%x-$")
            npcid = npcid and tonumber( npcid )

            if npcid == state.pet.guardian_of_azeroth.id then
                state.pet.guardian_of_azeroth.summonTime = 0
            end

            ns.updateMinion( destGUID )
        end
        return
    end

    local time = GetTime()

    local amSource  = ( sourceGUID == state.GUID )
    local petSource = ( UnitExists( "pet" ) and sourceGUID == UnitGUID( "pet" ) )
    local amTarget  = ( destGUID   == state.GUID )
    local isSensePower = ( class.auras.sense_power_active and spellID == 361022 )

    if not InCombatLockdown() and not ( amSource or petSource or amTarget ) then return end

    if subtype == 'SPELL_SUMMON' and amSource then
        -- Guardian of Azeroth check.
        -- ID is 152396.
        local npcid = destGUID:match("(%d+)-%x-$")
        npcid = npcid and tonumber( npcid )

        if npcid == state.pet.guardian_of_azeroth.id then
            state.pet.guardian_of_azeroth.summonTime = time
        end

        ns.updateMinion( destGUID, time )
        return
    end

    local hostile = ( bit.band( destFlags, COMBATLOG_OBJECT_REACTION_FRIENDLY ) == 0 ) and not IsActuallyFriend( destName )

    if dmg_events[ subtype ] and not ( amSource or petSource ) and amTarget then
        local damage, damageType

        if subtype:sub( 1, 13 ) == "ENVIRONMENTAL" then
            damageType = 1

            if subtype:sub(-7) == "_DAMAGE" then
                damage = spellName

            elseif spellName == "ABSORB" then
                damage = amount

            end

        elseif subtype:sub( 1, 5 ) == "SWING" then
            damageType = 1

            if subtype == "SWING_DAMAGE" then
                damage = spellID

            else
                if spellID == "ABSORB" then
                    damage = interrupt
                end

            end

        else -- SPELL_x
            if subtype:find( "_MISSED" ) then
                if amount == "ABSORB" then
                    damage = a
                    damageType = school or 1
                end

            else
                damage = amount
                damageType = school

            end

        end

        if damage and damage > 0 then
            ns.storeDamage( time, damage, bit.band( damageType, 0x1 ) == 1 )
        end
    end

    local minion = ns.isMinion( sourceGUID )

    if not ( amSource or petSource or isSensePower ) and not ( state.role.tank and destGUID == state.GUID ) and ( not minion or not countPets ) then
        return
    end

    if amSource then
        if cast_events[ subtype ] then
            local ability = class.abilities[ spellID ]

            if ability then
                if subtype == "SPELL_CAST_START" then
                    local _, _, _, start, finish = UnitCastingInfo( "player" )

                    if destGUID:len() == 0 then
                        destGUID = Hekili:GetMacroCastTarget( ability.key, GetTime(), "START" ) or UnitGUID( "target" )
                    end

                    if start then
                        state:QueueEvent( ability.key, start / 1000, finish / 1000, "CAST_FINISH", destGUID, true )

                        if ability.isProjectile then
                            local travel

                            if ability.flightTime then
                                travel = ability.flightTime

                            elseif destGUID then
                                local unit = Hekili:GetUnitByGUID( destGUID ) or Hekili:GetNameplateUnitForGUID( destGUID ) or "target"

                                if unit then
                                    local _, maxR = RC:GetRange( unit )
                                    maxR = maxR or select( 6, GetSpellInfo( ability.id ) ) or 40
                                    travel = maxR / ability.velocity
                                end
                            end

                            if not travel then travel = ( select( 6, GetSpellInfo( ability.id ) ) or 40 ) / ability.velocity end

                            state:QueueEvent( ability.impactSpell or ability.key, finish / 1000, travel, "PROJECTILE_IMPACT", destGUID, true )
                        end
                    end

                elseif subtype == "SPELL_CAST_FAILED" then
                    if state:RemoveSpellEvent( ability.key, true, "CAST_FINISH" ) then -- remove next cast finish.
                        if ability.isProjectile then state:RemoveSpellEvent( ability.key, true, "PROJECTILE_IMPACT", true ) end -- remove last impact.
                    end
                    -- Hekili:ForceUpdate( "SPELL_CAST_FAILED" ) ]]

                elseif subtype == "SPELL_AURA_REMOVED" and ability.channeled then
                    state:RemoveSpellEvents( ability.key, true ) -- remove ticks, finish, impacts.
                    -- Hekili:ForceUpdate( "SPELL_AURA_REMOVED_CHANNEL" )

                elseif subtype == "SPELL_CAST_SUCCESS" then
                    state:RemoveSpellEvent( ability.key, true, "CAST_FINISH" ) -- remove next cast finish.

                    if ability.channeled then
                        local _, _, _, start, finish = UnitChannelInfo( "player" )

                        if destGUID:len() == 0 then
                            destGUID = Hekili:GetMacroCastTarget( ability.key, GetTime(), "START" ) or UnitGUID( "target" )
                        end

                        if start then
                            start = start / 1000
                            finish = finish / 1000

                            state:QueueEvent( ability.key, start, finish, "CHANNEL_FINISH", destGUID, true )

                            local tick_time = ability.tick_time or ( ability.aura and class.auras[ ability.aura ].tick_time )

                            if tick_time and tick_time > 0 then
                                local tick = tick_time

                                while ( start + tick < finish ) do
                                    state:QueueEvent( ability.key, start, start + tick, "CHANNEL_TICK", destGUID, true )
                                    tick = tick + tick_time
                                end
                            end
                        end
                    end

                    if ability.isProjectile and not state:IsInFlight( ability.key, true ) then
                        local travel

                        if ability.flightTime then
                            travel = ability.flightTime

                        elseif destGUID then
                            local unit = Hekili:GetUnitByGUID( destGUID ) or Hekili:GetNameplateUnitForGUID( destGUID ) or "target"

                            if unit then
                                local _, maxR = RC:GetRange( unit )
                                maxR = maxR or select( 6, GetSpellInfo( ability.id ) ) or 40
                                travel = maxR / ability.velocity
                            end
                        end

                        if not travel then travel = state.target.maxR / ability.velocity end

                        state:QueueEvent( ability.impactSpell or ability.key, time, travel, "PROJECTILE_IMPACT", destGUID, true )
                    end

                    state:AddToHistory( ability.key, destGUID )

                elseif subtype == "SPELL_EMPOWER_END" then
                    state:AddToHistory( ability.key, destGUID, amount )

                elseif subtype == "SPELL_DAMAGE" then
                    -- Could be an impact.
                    if state:RemoveSpellEvent( ability.key, true, "PROJECTILE_IMPACT" ) then
                        Hekili:ForceUpdate( "PROJECTILE_IMPACT" )
                    end
                end
            end

            state.gcd.lastStart = max( state.gcd.lastStart, ( GetSpellCooldown( 61304 ) ) )
            -- if subtype ~= "SPELL_DAMAGE" then Hekili:ForceUpdate( subtype, true ) end
        end
    end

    if state.role.tank and state.GUID == destGUID and subtype:sub(1,5) == 'SWING' and not IsActuallyFriend( sourceName ) then
        ns.updateTarget( sourceGUID, time, true )

    elseif subtype:sub( 1, 5 ) == 'SWING' and not multistrike then
        if subtype == 'SWING_MISSED' then offhand = spellName end

        local sw = state.swings

        if offhand and time > sw.oh_actual and sw.oh_speed then
            sw.oh_actual = time
            sw.oh_speed = select( 2, UnitAttackSpeed( 'player' ) ) or sw.oh_speed
            sw.oh_projected = sw.oh_actual + sw.oh_speed

        elseif not offhand and time > sw.mh_actual then
            sw.mh_actual = time
            sw.mh_speed = UnitAttackSpeed( 'player' ) or sw.mh_speed
            sw.mh_projected = sw.mh_actual + sw.mh_speed

        end

    -- Player/Minion Event
    elseif ( amSource or petSource or isSensePower ) or ( countPets and minion ) or ( sourceGUID == destGUID and sourceGUID == UnitGUID( 'target' ) ) then
        --[[ if aura_events[ subtype ] then
            if subtype == "SPELL_CAST_SUCCESS" or state.GUID == destGUID then
                if class.abilities[ spellID ] or class.auras[ spellID ] then
                    Hekili:ForceUpdate( subtype, true )
                end
            end

            if UnitGUID( 'target' ) == destGUID then
                if class.auras[ spellID ] then Hekili:ForceUpdate( subtype ) end
            end
        end ]]

        local aura = class.auras and class.auras[ spellID ]

        if aura then
            if hostile and sourceGUID ~= destGUID and not aura.friendly then
                -- Aura Tracking
                if subtype == 'SPELL_AURA_APPLIED' or subtype == 'SPELL_AURA_REFRESH' or subtype == 'SPELL_AURA_APPLIED_DOSE' then
                    ns.trackDebuff( spellID, destGUID, time, true )
                    if ( not minion or countPets ) and countDots then ns.updateTarget( destGUID, time, amSource, spellID ) end

                    --[[ if spellID == 48108 or spellID == 48107 then
                        Hekili:ForceUpdate( "SPELL_AURA_SUPER", true )
                    end ]]

                elseif subtype == 'SPELL_PERIODIC_DAMAGE' or subtype == 'SPELL_PERIODIC_MISSED' then
                    ns.trackDebuff( spellID, destGUID, time )
                    if countDots and ( not minion or countPets ) then
                        ns.updateTarget( destGUID, time, amSource )
                    end

                elseif destGUID and subtype == 'SPELL_AURA_REMOVED' or subtype == 'SPELL_AURA_BROKEN' or subtype == 'SPELL_AURA_BROKEN_SPELL' then
                    ns.trackDebuff( spellID, destGUID )

                end

            elseif ( amSource or petSource or isSensePower ) and aura.friendly then -- friendly effects
                if subtype == 'SPELL_AURA_APPLIED'  or subtype == 'SPELL_AURA_REFRESH' or subtype == 'SPELL_AURA_APPLIED_DOSE' then
                    ns.trackDebuff( spellID, destGUID, time, true )

                elseif subtype == 'SPELL_PERIODIC_HEAL' or subtype == 'SPELL_PERIODIC_MISSED' then
                    ns.trackDebuff( spellID, destGUID, time )

                elseif destGUID and subtype == 'SPELL_AURA_REMOVED' or subtype == 'SPELL_AURA_BROKEN' or subtype == 'SPELL_AURA_BROKEN_SPELL' then
                    ns.trackDebuff( spellID, destGUID )

                end

            end

        end

        if hostile and ( countDots and dmg_events[ subtype ] or direct_dmg_events[ subtype ] ) and not dmg_filtered[ spellID ] then
            -- Don't wipe overkill targets in rested areas (it is likely a dummy).
            -- Interrupt is actually overkill.
            if not IsResting() and ( ( ( subtype == "SPELL_DAMAGE" or subtype == "SPELL_PERIODIC_DAMAGE" ) and interrupt > 0 ) or ( subtype == "SWING_DAMAGE" and spellName > 0 ) ) and ns.isTarget( destGUID ) then
                ns.eliminateUnit( destGUID, true )
                -- Hekili:ForceUpdate( "SPELL_DAMAGE_OVERKILL" )
            elseif not ( subtype == "SPELL_MISSED" and amount == "IMMUNE" ) then
                ns.updateTarget( destGUID, time, amSource, spellID )
            end
        end
    end
end
Hekili:ProfileCPU( "CLEU_HANDLER", CLEU_HANDLER )
RegisterEvent( "COMBAT_LOG_EVENT_UNFILTERED", function ( event ) CLEU_HANDLER( event, CombatLogGetCurrentEventInfo() ) end )


do
    local function UNIT_COMBAT( event, unit, action, _, amount )
        if amount > 0 and action == 'HEAL' then
            ns.storeHealing( GetTime(), amount )
        end
    end
    Hekili:ProfileCPU( "UNIT_COMBAT", UNIT_COMBAT )
    RegisterUnitEvent( "UNIT_COMBAT", "player", nil, UNIT_COMBAT )
end


local keys = ns.hotkeys
Hekili.KeybindInfo = keys
local updatedKeys = {}

local bindingSubs = {
    { "CTRL%-", "C" },
    { "ALT%-", "A" },
    { "SHIFT%-", "S" },
    { "STRG%-", "ST" },
    { "%s+", "" },
    { "NUMPAD", "N" },
    { "PLUS", "+" },
    { "MINUS", "-" },
    { "MULTIPLY", "*" },
    { "DIVIDE", "/" },
    { "BUTTON", "M" },
    { "MOUSEWHEELUP", "MwU" },
    { "MOUSEWHEELDOWN", "MwD" },
    { "MOUSEWHEEL", "Mw" },
    { "DOWN", "Dn" },
    { "UP", "Up" },
    { "PAGE", "Pg" },
    { "BACKSPACE", "BkSp" },
    { "DECIMAL", "." },
    { "CAPSLOCK", "CAPS" },
}

local function improvedGetBindingText( binding )
    if not binding then return "" end

    for i, rep in ipairs( bindingSubs ) do
        binding = binding:gsub( rep[1], rep[2] )
    end

    return binding
end


local itemToAbility = {
    [5512]   = "healthstone",
    [177278] = "phial_of_serenity"
}


local function StoreKeybindInfo( page, key, aType, id, console )

    if not key or not aType or not id then return end

    local action, ability

    if aType == "item" then
        local item, link = CGetItemInfo( id )
        ability = item and ( class.abilities[ item ] or class.abilities[ link ] )
        action = ability and ability.key

        if not action then
            if itemToAbility[ id ] then
                action = itemToAbility[ id ]
            else
                for k, v in pairs( class.potions ) do
                    if v.item == id then
                        action = "potion"
                        break
                    end
                end
            end
        end
    else
        ability = class.abilities[ id ]
        action = ability and ability.key
    end

    if action then
        keys[ action ] = keys[ action ] or {
            lower = {},
            upper = {},
            console = {}
        }

        if console == "cPort" then
            local newKey = key:gsub( ":%d+:%d+:0:0", ":0:0:0:0" )
            keys[ action ].console[ page ] = newKey
        else
            keys[ action ].upper[ page ] = improvedGetBindingText( key )
            keys[ action ].lower[ page ] = lower( keys[ action ].upper[ page ] )
        end
        updatedKeys[ action ] = true

        local bind = ability and ability.bind

        if bind then
            if type( bind ) == 'table' then
                for _, b in ipairs( bind ) do
                    keys[ b ] = keys[ b ] or {
                        lower = {},
                        upper = {},
                        console = {}
                    }

                    keys[ b ].lower[ page ] = keys[ action ].lower[ page ]
                    keys[ b ].upper[ page ] = keys[ action ].upper[ page ]
                    keys[ b ].console[ page ] = keys[ action ].console[ page ]

                    updatedKeys[ b ] = true
                end
            else
                keys[ bind ] = keys[ bind ] or {
                    lower = {},
                    upper = {},
                    console = {}
                }

                keys[ bind ].lower[ page ] = keys[ action ].lower[ page ]
                keys[ bind ].upper[ page ] = keys[ action ].upper[ page ]
                keys[ bind ].console[ page ] = keys[ action ].console[ page ]

                updatedKeys[ bind ] = true
            end
        end
    end
end


local defaultBarMap = {
    WARRIOR = {
        { bonus = 1, bar = 7 },
        { bonus = 2, bar = 8 },
    },
    ROGUE = {
        { bonus = 1, bar = 7 },
        { bonus = 2, bar = 7 },
        { bonus = 3, bar = 7 },
    },
    DRUID = {
        { bonus = 1, stealth = false, bar = 7 },
        { bonus = 1, stealth = true,  bar = 8 },
        { bonus = 2, bar = 8 },
        { bonus = 3, bar = 9 },
        { bonus = 4, bar = 10 },
    },
    MONK = {
        { bonus = 1, bar = 7 },
        { bonus = 2, bar = 8 },
        { bonus = 3, bar = 9 },
    },
    PRIEST = {
        { bonus = 1, bar = 7 },
    },
}


local slotsUsed = {}

local function ReadKeybindings( event )
        if not Hekili:IsValidSpec() then return end

        for k, v in pairs( keys ) do
            wipe( v.console )
            wipe( v.upper )
            wipe( v.lower )
        end

        -- Bartender4 support; if BT4 bindings are set, use them, otherwise fall back on default UI bindings below.
        -- This will still get viewed as misleading...
        if _G["Bartender4"] then
            table.wipe( slotsUsed )

            for i = 1, 180 do
                local keybind = "CLICK BT4Button" .. i .. ":Keybind"
                local bar = ceil( i / 12 )

                if GetBindingKey( keybind ) then
                    StoreKeybindInfo( bar, GetBindingKey( keybind ), GetActionInfo( i ) )
                    slotsUsed[ i ] = true
                end
            end

        -- Dominos support
        elseif C_AddOns.IsAddOnLoaded("Dominos") then
            table.wipe( slotsUsed )

            for i = 1, 14 do
                local bar = _G["DominosFrame" .. i]
                for b = 1, 12 do
                    local btn = bar.buttons[b]

                    if btn.action then
                        local keybind
                        local action = btn.action
                        if action <= 0 then
                            keybind = "CLICK " .. btn:GetName() .. ":HOTKEY"
                        elseif action <= 12 then
                            keybind = "ACTIONBUTTON" .. action
                        elseif action <= 24 then
                            keybind = "CLICK " .. btn:GetName() .. ":HOTKEY"
                        elseif action <= 36 then
                            keybind = "MULTIACTIONBAR3BUTTON" .. (action - 24)
                        elseif action <= 48 then
                            keybind = "MULTIACTIONBAR4BUTTON" .. (action - 36)
                        elseif action <= 60 then
                            keybind = "MULTIACTIONBAR2BUTTON" .. (action - 48)
                        elseif action <= 72 then
                            keybind = "MULTIACTIONBAR1BUTTON" .. (action - 60)
                        elseif action <= 132 then
                            keybind = "CLICK " .. btn:GetName() .. ":HOTKEY"
                        elseif action <= 144 then
                            keybind = "MULTIACTIONBAR5BUTTON" .. (action - 132)
                        elseif action <= 156 then
                            keybind = "MULTIACTIONBAR6BUTTON" .. (action - 144)
                        elseif action <= 168 then
                            keybind = "MULTIACTIONBAR7BUTTON" .. (action - 156)
                        end

                        if keybind and GetBindingKey( keybind ) then
                            StoreKeybindInfo( i, GetBindingKey( keybind ), GetActionInfo( btn.action ) )
                            slotsUsed[ btn.action ] = true
                        end
                    end
                end
            end

        -- Use ElvUI's actionbars only if they are actually enabled.
        elseif _G["ElvUI"] and _G[ "ElvUI_Bar1Button1" ] then
            table.wipe( slotsUsed )

            for i = 1, 15 do
                for b = 1, 12 do
                    local btn = _G["ElvUI_Bar" .. i .. "Button" .. b]

                    if btn then
                        local binding = btn.bindstring or btn.keyBoundTarget or ( "CLICK " .. btn:GetName() .. ":LeftButton" )

                        if i > 6 then
                            -- Checking whether bar is active.
                            local bar = _G["ElvUI_Bar" .. i]

                            if not bar or not bar.db.enabled then
                                binding = "ACTIONBUTTON" .. b
                            end
                        end

                        local action, aType = btn._state_action, "spell"

                        if action and type( action ) == "number" then
                            slotsUsed[ action ] = true

                            binding = GetBindingKey( binding )
                            action, aType = GetActionInfo( action )
                            if binding then StoreKeybindInfo( i, binding, action, aType ) end
                        end
                    end
                end
            end
        end

            for i = 1, 12 do
                if not slotsUsed[ i ] then
                    StoreKeybindInfo( 1, GetBindingKey( "ACTIONBUTTON" .. i ), GetActionInfo( i ) )
                end
            end

            for i = 13, 24 do
                if not slotsUsed[ i ] then
                    StoreKeybindInfo( 2, GetBindingKey( "ACTIONBUTTON" .. i - 12 ), GetActionInfo( i ) )
                end
            end

            for i = 25, 36 do
                if not slotsUsed[ i ] then
                    StoreKeybindInfo( 3, GetBindingKey( "MULTIACTIONBAR3BUTTON" .. i - 24 ), GetActionInfo( i ) )
                end
            end

            for i = 37, 48 do
                if not slotsUsed[ i ] then
                    StoreKeybindInfo( 4, GetBindingKey( "MULTIACTIONBAR4BUTTON" .. i - 36 ), GetActionInfo( i ) )
                end
            end

            for i = 49, 60 do
                if not slotsUsed[ i ] then
                    StoreKeybindInfo( 5, GetBindingKey( "MULTIACTIONBAR2BUTTON" .. i - 48 ), GetActionInfo( i ) )
                end
            end

            for i = 61, 72 do
                if not slotsUsed[ i ] then
                    StoreKeybindInfo( 6, GetBindingKey( "MULTIACTIONBAR1BUTTON" .. i - 60 ), GetActionInfo( i ) )
                end
            end

            for i = 72, 143 do
                if not slotsUsed[ i ] then
                    StoreKeybindInfo( 7 + floor( ( i - 72 ) / 12 ), GetBindingKey( "ACTIONBUTTON" .. 1 + ( i - 72 ) % 12 ), GetActionInfo( i + 1 ) )
                end
            end

            for i = 145, 156 do
                if not slotsUsed[ i ] then
                    StoreKeybindInfo( 13, GetBindingKey( "MULTIACTIONBAR5BUTTON" .. i - 144 ), GetActionInfo( i ) )
                end
            end

            for i = 157, 168 do
                if not slotsUsed[ i ] then
                    StoreKeybindInfo( 14, GetBindingKey( "MULTIACTIONBAR6BUTTON" .. i - 156 ), GetActionInfo( i ) )
                end
            end

            for i = 169, 180 do
                if not slotsUsed[ i ] then
                    StoreKeybindInfo( 15, GetBindingKey( "MULTIACTIONBAR7BUTTON" .. i - 168 ), GetActionInfo( i ) )
                end
            end

        if _G.ConsolePort then
            for i = 1, 180 do
                local action, id = GetActionInfo( i )

                if action and id then
                    local bind = ConsolePort:GetActionBinding( i )
                    local key, mod = ConsolePort:GetCurrentBindingOwner( bind )

                    if key then
                        StoreKeybindInfo( math.ceil( i / 12 ), ConsolePort:GetFormattedButtonCombination( key, mod ), action, id, "cPort" )
                    end
                end
            end
        end

        for k, v in pairs( keys ) do
            local ability = class.abilities[ k ]

            if ability and ability.bind then
                if type( ability.bind ) == 'table' then
                    for _, b in ipairs( ability.bind ) do
                        for page, value in pairs( v.lower ) do
                            keys[ b ] = keys[ b ] or {
                                lower = {},
                                upper = {},
                                console = {}
                            }
                            keys[ b ].lower[ page ] = value
                            keys[ b ].upper[ page ] = v.upper[ page ]
                            keys[ b ].console[ page ] = v.console[ page ]
                        end
                    end
                else
                    for page, value in pairs( v.lower ) do
                        keys[ ability.bind ] = keys[ ability.bind ] or {
                            lower = {},
                            upper = {},
                            console = {}
                        }
                        keys[ ability.bind ].lower[ page ] = value
                        keys[ ability.bind ].upper[ page ] = v.upper[ page ]
                        keys[ ability.bind ].console[ page ] = v.console[ page ]
                    end
                end
            end
        end

        -- This is also the right time to update pet-based target detection.
        Hekili:SetupPetBasedTargetDetection()
end
ns.ReadKeybindings = ReadKeybindings

local function ReadOneKeybinding( event, slot )
    if not Hekili:IsValidSpec() then return end
    if not slot or slot == 0 then return end

    local actionBarNumber = ceil( slot / 12 )
    local keyNumber = slot - ( 12 * ( actionBarNumber - 1 ) )

    local ability
    local completed = false

    -- Bartender4 support; if BT4 bindings are set, use them, otherwise fall back on default UI bindings below.
    -- This will still get viewed as misleading...
    if _G["Bartender4"] then
        local keybind = "CLICK BT4Button" .. slot .. ":Keybind"

        if GetBindingKey( keybind ) then
            StoreKeybindInfo( actionBarNumber, GetBindingKey( keybind ), GetActionInfo( slot ) )
            completed = true
        end

    -- Dominos support
    elseif C_AddOns.IsAddOnLoaded("Dominos") then
        local bar = _G["DominosFrame" .. actionBarNumber]
        local button = bar.buttons[keyNumber]

        if button.action then
            local keybind
            local action = button.action

            if action <= 0 then
                keybind = "CLICK " .. button:GetName() .. ":HOTKEY"
            elseif action <= 12 then
                keybind = "ACTIONBUTTON" .. action
            elseif action <= 24 then
                keybind = "CLICK " .. button:GetName() .. ":HOTKEY"
            elseif action <= 36 then
                keybind = "MULTIACTIONBAR3BUTTON" .. (action - 24)
            elseif action <= 48 then
                keybind = "MULTIACTIONBAR4BUTTON" .. (action - 36)
            elseif action <= 60 then
                keybind = "MULTIACTIONBAR2BUTTON" .. (action - 48)
            elseif action <= 72 then
                keybind = "MULTIACTIONBAR1BUTTON" .. (action - 60)
            elseif action <= 132 then
                keybind = "CLICK " .. button:GetName() .. ":HOTKEY"
            elseif action <= 144 then
                keybind = "MULTIACTIONBAR5BUTTON" .. (action - 132)
            elseif action <= 156 then
                keybind = "MULTIACTIONBAR6BUTTON" .. (action - 144)
            elseif action <= 168 then
                keybind = "MULTIACTIONBAR7BUTTON" .. (action - 156)
            end

            if keybind and GetBindingKey( keybind ) then
                StoreKeybindInfo( actionBarNumber, GetBindingKey( keybind ), GetActionInfo( slot ) )
                completed = true
            end
        end

    elseif _G["ElvUI"] and _G["ElvUI_Bar1Button1"] then
        local btn = _G[ "ElvUI_Bar" .. actionBarNumber .. "Button" .. keyNumber ]

        if btn then
            local binding = btn.bindstring or btn.keyBoundTarget or ( " CLICK " .. btn:GetName() .. ":LeftButton" )

            if actionBarNumber > 6 then
                -- Checking whether bar is active.
                local bar = _G[ "ElvUI_Bar" .. actionBarNumber ]

                if not bar or not bar.db.enabled then
                    binding = "ACTIONBUTTON" .. keyNumber
                end
            end

            local action, aType = btn._state_action, "spell"

            if action and type( action ) == "number" then
                binding = GetBindingKey( binding )
                action, aType = GetActionInfo( action )
                if binding then StoreKeybindInfo( actionBarNumber, binding, action, aType ) end
            end
        end
    end

    if not completed then
        if actionBarNumber == 1 or actionBarNumber == 2 or ( actionBarNumber > 6  and actionBarNumber < 13 ) then
            ability = StoreKeybindInfo( keyNumber, GetBindingKey( "ACTIONBUTTON" .. keyNumber ), GetActionInfo( slot ) )

        elseif actionBarNumber > 2 and actionBarNumber < 5 then
            ability = StoreKeybindInfo( actionBarNumber, GetBindingKey( "MULTIACTIONBAR" .. actionBarNumber .. "BUTTON" .. keyNumber ), GetActionInfo( slot ) )

        elseif actionBarNumber == 5 then
            ability = StoreKeybindInfo( actionBarNumber, GetBindingKey( "MULTIACTIONBAR2BUTTON" .. keyNumber ), GetActionInfo( slot ) )

        elseif actionBarNumber == 6 then
            ability = StoreKeybindInfo( actionBarNumber, GetBindingKey( "MULTIACTIONBAR1BUTTON" .. keyNumber ), GetActionInfo( slot ) )

        elseif actionBarNumber == 13 then
            ability = StoreKeybindInfo( actionBarNumber, GetBindingKey( "MULTIACTIONBAR5BUTTON" .. keyNumber ), GetActionInfo( slot ) )

        elseif actionBarNumber == 14 then
            ability = StoreKeybindInfo( actionBarNumber, GetBindingKey( "MULTIACTIONBAR6BUTTON" .. keyNumber ), GetActionInfo( slot ) )

        elseif actionBarNumber == 15 then
            ability = StoreKeybindInfo( actionBarNumber, GetBindingKey( "MULTIACTIONBAR7BUTTON" .. keyNumber ), GetActionInfo( slot ) )

        end
    end

    if _G.ConsolePort then
        local action, id = GetActionInfo( slot )

        if action and id then
            local bind = ConsolePort:GetActionBinding( slot )
            local key, mod = ConsolePort:GetCurrentBindingOwner( bind )

            if key then
                ability = StoreKeybindInfo( actionBarNumber, ConsolePort:GetFormattedButtonCombination( key, mod ), action, id, "cPort" )
            end
        end
    end

    ability = ability and class.abilities[ ability ]

    if ability and ability.bind then
        if type( ability.bind ) == 'table' then
            for _, b in ipairs( ability.bind ) do
                for page, value in pairs( v.lower ) do
                    keys[ b ] = keys[ b ] or {
                        lower = {},
                        upper = {},
                        console = {}
                    }
                    keys[ b ].lower[ page ] = value
                    keys[ b ].upper[ page ] = v.upper[ page ]
                    keys[ b ].console[ page ] = v.console[ page ]
                end
            end
        else
            for page, value in pairs( v.lower ) do
                keys[ ability.bind ] = keys[ ability.bind ] or {
                    lower = {},
                    upper = {},
                    console = {}
                }
                keys[ ability.bind ].lower[ page ] = value
                keys[ ability.bind ].upper[ page ] = v.upper[ page ]
                keys[ ability.bind ].console[ page ] = v.console[ page ]
            end
        end
    end

    -- This is also the right time to update pet-based target detection.
    Hekili:SetupPetBasedTargetDetection()
end


local allTimer

local function DelayedUpdateKeybindings( event )
    if allTimer and not allTimer:IsCancelled() then allTimer:Cancel() end
    allTimer = C_Timer.After( 0.2, function()
        ReadKeybindings( event )
    end )
end

--[[ local function DelayedUpdateOneKeybinding( event, slot )
    if oneTimer and not oneTimer:IsCancelled() then oneTimer:Cancel() end
    oneTimer = C_Timer.After( 0.2, function() ReadOneKeybinding( event, slot ) end )
end ]]

RegisterEvent( "UPDATE_BINDINGS", DelayedUpdateKeybindings )
RegisterEvent( "SPELLS_CHANGED", DelayedUpdateKeybindings )
RegisterEvent( "ACTIONBAR_SHOWGRID", DelayedUpdateKeybindings )
RegisterEvent( "ACTIONBAR_HIDEGRID", DelayedUpdateKeybindings )
-- RegisterEvent( "ACTIONBAR_PAGE_CHANGED", DelayedUpdateKeybindings )
-- RegisterEvent( "UPDATE_SHAPESHIFT_FORM", DelayedUpdateKeybindings )

if Hekili.IsWrath() then
    RegisterEvent( "ACTIVE_TALENT_GROUP_CHANGED", DelayedUpdateKeybindings )
else
    RegisterEvent( "ACTIVE_PLAYER_SPECIALIZATION_CHANGED", DelayedUpdateKeybindings )
    RegisterEvent( "TRAIT_CONFIG_UPDATED", DelayedUpdateKeybindings )
end



if select( 2, UnitClass( "player" ) ) == "DRUID" then
    local prowlOrder = { 8, 7, 1, 2, 3, 4, 5, 6, 10, 9, 13, 14, 15 }
    local catOrder = { 7, 8, 1, 2, 3, 4, 5, 6, 10, 9, 13, 14, 15 }
    local bearOrder = { 9, 1, 2, 3, 4, 5, 6, 7, 8, 10, 13, 14, 15, 1 }
    local owlOrder = { 10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 13, 14, 15 }
    local defaultOrder = { 1, 2, 3, 4, 5, 6, 10, 7, 8, 9, 13, 14, 15 }

    function Hekili:GetBindingForAction( key, display, i )
        if not key then return "" end

        local ability = class.abilities[ key ]
        key = ability and ability.key or key

        local override = state.spec.id
        local overrideType = ability and ability.item and "items" or "abilities"

        override = override and rawget( self.DB.profile.specs, override )
        override = override and override[ overrideType ][ key ]
        override = override and override.keybind

        if override and override ~= "" then
            return override
        end

        if not keys[ key ] then return "" end

        local caps, console = true, false

        local queued = ( i or 1 ) > 1 and display.keybindings.separateQueueStyle

        if display then
            caps = not ( queued and display.keybindings.queuedLowercase or display.keybindings.lowercase )
            console = ConsolePort ~= nil and display.keybindings.cPortOverride
        end

        local db = console and keys[ key ].console or ( caps and keys[ key ].upper or keys[ key ].lower )

        local output, source

        local order = defaultOrder
        -- TODO: These checks should use actual aura data rather than potential stale/manipulated virtual state data.
        if class.file == "DRUID" then
            order = ( state.prowling and prowlOrder ) or ( state.buff.cat_form.up and catOrder ) or ( state.buff.bear_form.up and bearOrder ) or ( state.buff.moonkin_form.up and owlOrder ) or order
        end

        if order then
            for _, n in ipairs( order ) do
                output = db[ n ]

                if output then
                    source = n
                    break
                end
            end
        end

        output = output or ""
        source = source or -1

        if output ~= "" and console then
            local size = output:match( "Icons(%d%d)" )
            size = tonumber(size)

            if size then
                local margin = floor( size * display.keybindings.cPortZoom * 0.5 )
                output = output:gsub( ":0|t", ":0:" .. size .. ":" .. size .. ":" .. margin .. ":" .. ( size - margin ) .. ":" .. margin .. ":" .. ( size - margin ) .. "|t" )
            end
        end

        return output
    end

elseif select( 2, UnitClass( "player" ) ) == "ROGUE" then
    local stealthedOrder = { 7, 8, 1, 2, 3, 4, 5, 6, 9, 10, 13, 14, 15 }
    local defaultOrder = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 14, 15 }

    function Hekili:GetBindingForAction( key, display, i )
        if not key then return "" end

        local ability = class.abilities[ key ]
        key = ability and ability.key or key

        local override = state.spec.id
        local overrideType = ability and ability.item and "items" or "abilities"

        override = override and rawget( self.DB.profile.specs, override )
        override = override and override[ overrideType ][ key ]
        override = override and override.keybind

        if override and override ~= "" then
            return override
        end

        if not keys[ key ] then
            return ""
        end

        local queued = ( i or 1 ) > 1 and display.keybindings.separateQueueStyle

        local caps, console = true, false
        if display then
            caps = not ( queued and display.keybindings.queuedLowercase or display.keybindings.lowercase )
            console = ConsolePort ~= nil and display.keybindings.cPortOverride
        end

        local db = console and keys[ key ].console or ( caps and keys[ key ].upper or keys[ key ].lower )

        local output, source
        local order = state.stealthed.all and stealthedOrder or defaultOrder

        for _, n in ipairs( order ) do
            output = db[ n ]

            if output then
                source = n
                break
            end
        end

        output = output or ""
        source = source or -1

        if output ~= "" and console then
            local size = output:match( "Icons(%d%d)" )
            size = tonumber(size)

            if size then
                local margin = floor( size * display.keybindings.cPortZoom * 0.5 )
                output = output:gsub( ":0|t", ":0:" .. size .. ":" .. size .. ":" .. margin .. ":" .. ( size - margin ) .. ":" .. margin .. ":" .. ( size - margin ) .. "|t" )
            end
        end

        return output, source
    end

else
    function Hekili:GetBindingForAction( key, display, i )
        local ability = class.abilities[ key ]
        key = ability and ability.key or key

        local override = state.spec.id
        local overrideType = ability and ability.item and "items" or "abilities"

        override = override and rawget( self.DB.profile.specs, override )
        override = override and override[ overrideType ][ key ]
        override = override and override.keybind

        if override and override ~= "" then
            return override
        end

        if not keys[ key ] then return "" end

        local queued = ( i or 1 ) > 1 and display.keybindings.separateQueueStyle

        local caps, console = true, false
        if display then
            caps = not ( queued and display.keybindings.queuedLowercase or display.keybindings.lowercase )
            console = ConsolePort ~= nil and display.keybindings.cPortOverride
        end

        local db = console and keys[ key ].console or ( caps and keys[ key ].upper or keys[ key ].lower )

        local output, source

        for _, n in ipairs( { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 13, 14, 15 } ) do
            output = db[ n ]

            if output then
                source = n
                break
            end
        end

        output = output or ""
        source = source or -1

        if output ~= "" and console then
            local size = output:match( "Icons(%d%d)" )
            size = tonumber(size)

            if size then
                local margin = floor( size * display.keybindings.cPortZoom * 0.5 )
                output = output:gsub( ":0:0:0:0|t", ":0:0:0:0:" .. size .. ":" .. size .. ":" .. margin .. ":" .. ( size - margin ) .. ":" .. margin .. ":" .. ( size - margin ) .. "|t" )
            end
        end

        return output, source
    end
end
