-- State.lua
-- January 2025

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

local auras = ns.auras

local formatKey = ns.formatKey
local ResourceRegenerates = ns.ResourceRegenerates

local Error = ns.Error

local orderedPairs = ns.orderedPairs
local round, roundUp, roundDown = ns.round, ns.roundUp, ns.roundDown
local safeMin, safeMax = ns.safeMin, ns.safeMax

local GetPlayerAuraBySpellID = C_UnitAuras.GetPlayerAuraBySpellID
local GetItemSpell = C_Item.GetItemSpell
local GetItemCooldown = C_Item.GetItemCooldown
local IsUsableItem = C_Item.IsUsableItem
local GetSpellInfo, GetSpellCharges, GetSpellLossOfControlCooldown = ns.GetUnpackedSpellInfo, C_Spell.GetSpellCharges, C_Spell.GetSpellLossOfControlCooldown
local UnitBuff, UnitDebuff = ns.UnitBuff, ns.UnitDebuff

local GetBuffDataByIndex, GetDebuffDataByIndex = C_UnitAuras.GetBuffDataByIndex, C_UnitAuras.GetDebuffDataByIndex
local UnpackAuraData = AuraUtil.UnpackAuraData

local GetSpellCharges = function(spellID)
    local spellChargeInfo = GetSpellCharges(spellID);
    if spellChargeInfo then
        return spellChargeInfo.currentCharges, spellChargeInfo.maxCharges, spellChargeInfo.cooldownStartTime, spellChargeInfo.cooldownDuration, spellChargeInfo.chargeModRate;
    end
end
local FindPlayerAuraByID, IsAbilityDisabled, IsDisabledCovenantSpell = ns.FindPlayerAuraByID, ns.IsAbilityDisabled, ns.IsDisabledCovenantSpell

local GetSpecialization = C_SpecializationInfo.GetSpecialization

-- Clean up table_x later.
---@diagnostic disable-next-line: deprecated
local insert, remove, sort, tcopy, unpack, wipe = table.insert, table.remove, table.sort, ns.tableCopy, table.unpack, table.wipe
local format = string.format

local Mark, SuperMark, ClearMarks = ns.Mark, ns.SuperMark, ns.ClearMarks

local RC = LibStub( "LibRangeCheck-3.0" )
local LSR = LibStub( "SpellRange-1.0" )

local class = Hekili.Class
local scripts = Hekili.Scripts

local unknown_buff, unknown_debuff


-- This will be our environment table for local functions.
local state = Hekili.State

local PTR = ns.PTR
state.PTR = PTR
state.ptr = PTR and 1 or 0

state.now = 0
state.offset = 0
state.modified = false
state.resetType = "heavy"

state.encounterID = 0
state.encounterName = "None"
state.encounterDifficulty = 0

state.aggro = false
state.tanking = false

state.delay = 0
state.delayMin = 0
state.delayMax = 15

state.false_start = 0
state.latency = 0

state.filter = "none"
state.cast_target = "nobody"

state.arena = false
state.bg = false

state.mainhand_speed = 0
state.offhand_speed = 0

state.min_targets = 0
state.max_targets = 0

state.action = {}
state.active_dot = {}
state.args = {}
state.azerite = {}
state.essence = {}
state.aura = {}
state.auras = auras
state.buff = {}
state.consumable = {}
state.cooldown = {}

state.empowerment = {
    id = 0,
    active = false,
    spell = "none",
    start = 0,
    finish = 0,
    hold = 0,
    stages = {}
}
state.max_empower = 3
state.empowering = {}

state.health = {
    current = 1,
    max = 1,
    percent = 100,
    timeTo = function( amount )
        if state.health.current >= amount then return 0 end
        return 3600
    end,

    initialized = false
}
state.legendary = {}
state.runeforge = state.legendary -- Different APLs use runeforge.X.equipped vs. legendary.X.enabled.
state.debuff = {}
state.dot = {}
state.equipped = {}
state.main_hand = {
    size = 0
}
state.off_hand = {
    size = 0
}

state.gcd = {}

state.hero_tree = setmetatable( {
    current = "none"
}, {
    __index = function( t, k )
        return k == t.current
    end
} )


state.history = {
    casts = {},
    units = {}
}

state.holds = {}

state.items = {}
state.pet = {
    fake_pet = {
        name = "Mary-Kate Olsen",
        expires = 0,
        permanent = false,
    }
}
state.player = {
    lastcast = "none",
    lastgcd = "none",
    lastoffgcd = "none",
    casttime = 0,
    updated = true,
    channeling = false,
    channel_start = 0,
    channel_end = 0,
    channel_spell = nil
}
state.prev = {
    meta = "castsAll",
    history = { "no_action", "no_action", "no_action", "no_action", "no_action", "no_action", "no_action", "no_action", "no_action", "no_action" }
}
state.prev_gcd = {
    meta = "castsOn",
    history = { "no_action", "no_action", "no_action", "no_action", "no_action", "no_action", "no_action", "no_action", "no_action", "no_action" }
}
state.prev_off_gcd = {
    meta = "castsOff",
    history = { "no_action", "no_action", "no_action", "no_action", "no_action", "no_action", "no_action", "no_action", "no_action", "no_action" }
}
state.predictions = {}
state.predictionsOff = {}
state.predictionsOn = {}
state.purge = {}
state.pvptalent = {}
state.race = {}
state.script = {}
state.set_bonus = {}
state.settings = {}
state.sim = {}
state.spec = {}
state.stance = {}
state.stat = {}
state.swings = {
    mh_actual = 0,
    mh_speed = UnitAttackSpeed( "player" ) > 0 and UnitAttackSpeed( "player" ) or 2.6,
    mh_projected = 2.6,
    oh_actual = 0,
    oh_speed = select( 2, UnitAttackSpeed( "player" ) ) or 2.6,
    oh_projected = 3.9
}
state.system = {}
state.table = table
state.talent = {}
state.target = {
    debuff = state.debuff,
    dot = state.dot,
    health = {},
    updated = true
}

state.movement = {}

setmetatable( state.movement, {
    __index = function( t, k )
        if k == "distance" then
            if state.buff.movement.up then return state.target.maxR end
            return 0
        end

        return state.target[ k ]
    end
} )

state.sim.target = state.target
state.toggle = {}
state.totem = {}


state.trinket = {

    none = {
        slot = "none",
        __id = 0,
        __ability = "null_cooldown",
        __usable = false,
        __has_use_buff = false,
        __has_use_damage = false,
        __use_buff_duration = 0.01,
        __proc = false,
        ilvl = 0,

        --[[ has_cooldown = {
            slot = "none",
        }, ]]

        stacking_stat = {
            slot = "none"
        },
        has_stacking_stat = {
            slot = "none"
        },

        stat = {
            slot = "none"
        },
        has_stat = {
            slot = "none",
        },

        is = {
            slot = "none",
        },
    },

    t1 = {
        slot = "t1",
        __id = 0,
        __ability = "null_cooldown",
        __usable = false,
        __has_use_buff = false,
        __has_use_damage = false,
        __use_buff_duration = 0.01,
        __proc = false,
        ilvl = 0,

        --[[ has_cooldown = {
            slot = "t1"
        }, ]]

        stacking_stat = {
            slot = "t1"
        },
        has_stacking_stat = {
            slot = "t1"
        },

        stat = {
            slot = "t1"
        },
        has_stat = {
            slot = "t1"
        },

        is = {
            slot = "t1"
        },
    },

    t2 = {
        slot = "t2",
        __id = 0,
        __ability = "null_cooldown",
        __usable = false,
        __has_use_buff = false,
        __has_use_damage = false,
        __use_buff_duration = 0.01,
        __proc = false,
        ilvl = 0,

        --[[ has_cooldown = {
            slot = "t2",
        }, ]]

        stacking_stat = {
            slot = "t2"
        },
        has_stacking_stat = {
            slot = "t2"
        },

        stat = {
            slot = "t2"
        },
        has_stat = {
            slot = "t2",
        },

        is = {
            slot = "t2",
        },
    },

    main_hand = {
        slot = "main_hand",
        __id = 0,
        __ability = "null_cooldown",
        __usable = false,
        __has_use_buff = false,
        __has_use_damage = false,
        __use_buff_duration = 0.01,
        __proc = false,
        ilvl = 0,

        --[[ has_cooldown = {
            slot = "main_hand",
        }, ]]

        stacking_stat = {
            slot = "main_hand"
        },
        has_stacking_stat = {
            slot = "main_hand"
        },

        stat = {
            slot = "main_hand"
        },
        has_stat = {
            slot = "main_hand",
        },

        is = {
            slot = "main_hand",
        },
    },
    any = {},

    cooldown = {
    },
    has_cooldown = {
    },

    stacking_stat = {
    },
    has_stacking_stat = {
    },

    stacking_proc = {
    },
    has_stacking_proc = {
    },

    stat = {
    },
    has_stat = {
    },
}
state.trinket.proc = state.trinket.stat
state.trinket[1] = state.trinket.t1
state.trinket[2] = state.trinket.t2

state.using_apl = setmetatable( {}, {
    __index = function( t, k )
        return false
    end
} )


state.role = setmetatable( {}, {
    __index = function( t, k )
        return false
    end
} )

local mt_no_trinket_cooldown = {
}

local mt_no_trinket_stacking_stat = {
}

local mt_no_trinket_stat = {
}


local mt_no_trinket = {
    __index = function( t, k )
        if k:sub(1,4) == "has_" then
            return false
        elseif k == "down" then
            return true
        end

        return false
    end
}

local no_trinket = state.trinket.none
--[[
local no_trinket = setmetatable( {
    slot = "none",
    __id = 0,
    __ability = "null_cooldown",
    __usable = false,
    __has_use_buff = false,
    __has_use_damage = false,
    __use_buff_duration = nil,
    __proc = false,
    ilvl = 0,

    stacking_stat = {
        slot = "none"
    },
    has_stacking_stat = {
        slot = "none"
    },

    stat = {
        slot = "none"
    },
    has_stat = {
        slot = "none",
    },

    is = {
        slot = "none",
    }
}, mt_no_trinket ) ]]

setmetatable( state.trinket, {
    __index = function( t, k )
        if t.t1.is[ k ] then
            return t.t1
        elseif t.t2.is[ k ] then
            return t.t2
        else
            return no_trinket
        end
    end
} )

state.trinket.stat.any = state.trinket.any


local mt_trinket_any = {
    __index = function( t, k )
        return state.trinket.t1[ k ] or state.trinket.t2[ k ]
    end
}

setmetatable( state.trinket.any, mt_trinket_any )

local mt_trinket_any_stacking_stat = {
    __index = function( t, k )
        if state.trinket.t1.has_stacking_stat[k] then return state.trinket.t1
            elseif state.trinket.t2.has_stacking_stat[k] then return state.trinket.t2 end
        return no_trinket
    end
}

setmetatable( state.trinket.stacking_stat, mt_trinket_any_stacking_stat )
setmetatable( state.trinket.stacking_proc, mt_trinket_any_stacking_stat )

local mt_trinket_any_stat = {
    __index = function( t, k )
        --[[ if k == "any" then
        return ( state.trinket.has_stat[
    end ]]

        if state.trinket.t1.has_stat[k] then return state.trinket.t1
        elseif state.trinket.t2.has_stat[k] then return state.trinket.t2 end
        return no_trinket
    end
}

setmetatable( state.trinket.stat, mt_trinket_any_stat )


local mt_trinket = {
    __index = function( t, k )
        local isEnabled = ( not rawget( t, "__usable" ) ) or ( rawget( t, "__ability" ) and not state:IsDisabled( t.__ability ) or false )

        if k == "id" then
            return isEnabled and t.__id or 0
        elseif k == "ability" then
            return rawget( t, "__ability" ) or "null_cooldown"
        elseif k == "usable" or k == "has_use" then
            return rawget( t, "__usable" ) or false
        elseif k == "has_use_buff" or k == "use_buff" then
            return isEnabled and t.__has_use_buff or false
        elseif k == "has_use_damage" then
            return isEnabled and t.__has_use_damage or false
        elseif k == "use_buff_duration" or k == "buff_duration" then
            return isEnabled and t.__has_use_buff and t.__use_buff_duration or 0.01
        elseif k == "has_proc" or k == "proc" then
            return isEnabled and t.__proc or false
        elseif k == "proc_duration" then
            return t.__proc and t.__use_buff_duration or 0
        elseif k == "duration" then
            return t.__use_buff_duration or 0
        end

        if k == "up" or k == "ticking" or k == "active" then
            return isEnabled and class.trinkets[ t.id ].buff and state.buff[ class.trinkets[ t.id ].buff ].up or false
        elseif k == "react" or k == "stack" or k == "stacks" then
            return isEnabled and class.trinkets[ t.id ].buff and state.buff[ class.trinkets[ t.id ].buff ][ k ] or 0
        elseif k == "remains" then
            return isEnabled and class.trinkets[ t.id ].buff and state.buff[ class.trinkets[ t.id ].buff ].remains or 0
        elseif k == "has_cooldown" then
            return isEnabled and ( GetItemSpell( t.id ) ~= nil ) or false
        elseif k == "ready_cooldown" then
            if isEnabled and t.usable and t.ability then
                return t.cooldown.ready
            end
            return true
        elseif k == "cooldown" then
            if t.usable and t.ability and state.cooldown[ t.ability ] then
                return state.cooldown[ t.ability ]
            end
            return state.cooldown.null_cooldown
        elseif k == "cooldown_remains" then
            if t.usable and t.ability and state.cooldown[ t.ability ] then return state.cooldown[ t.ability ].remains end
            return state.cooldown.null_cooldown.remains

        elseif k == "cast_time" or k == "cast_time" then
            return t.usable and t.ability and class.abilities[ t.ability ] and class.abilities[ t.ability ].cast or 0
        end

        Hekili:Error( "Unknown trinket expression '%s' in %s.", k, state.this_id )
        return k
    end
}

setmetatable( state.trinket.none, mt_trinket )
setmetatable( state.trinket.t1, mt_trinket )
setmetatable( state.trinket.t2, mt_trinket )
setmetatable( state.trinket.main_hand, mt_trinket )


local mt_trinket_is = {
    __index = function( t, k )
        local item = state.trinket[ t.slot ]

        if item.usable and item.ability == k then return true end
        if item.__id and class.gear[ k ] and class.gear[ k ][ 1 ] == item.__id then return true end

        return false
    end,
}

setmetatable( state.trinket.none.is, mt_trinket_is )
setmetatable( state.trinket.t1.is, mt_trinket_is )
setmetatable( state.trinket.t2.is, mt_trinket_is )
setmetatable( state.trinket.main_hand.is, mt_trinket_is )


--[[ local mt_trinket_cooldown = {
    __index = function(t, k)
        if k == "duration" or k == "expires" then
            -- Refresh the ID in case we changed specs and ability is spec dependent.
            local start, duration = GetItemCooldown( state.trinket[ t.slot ].id )

            t.duration = duration or 0
            t.expires = start and ( start + duration ) or 0

            return t[k]

        elseif k == "remains" then
            return max( 0, t.expires - ( state.query_time ) )

        elseif k == "up" then
            return t.remains == 0

        elseif k == "down" then
            return t.remains > 0

        end

        -- return Error( "UNK: " .. k )

    end
}

setmetatable( state.trinket.t1.cooldown, mt_trinket_cooldown )
setmetatable( state.trinket.t2.cooldown, mt_trinket_cooldown ) ]]

local mt_trinket_has_stacking_stat = {
    __index = function( t, k )
        local trinket = state.trinket[ t.slot ]
        trinket = trinket and trinket.__ability
        trinket = trinket and class.abilities[ trinket ]

        if not trinket then return false end

        local buff = trinket.self_buff
        buff = buff and class.auras[ buff ]

        if not buff or buff.max_stack == 1 then return false end

        if k == "any" then return true end

        local proc = trinket.proc or "none"
        if not proc then return false end

        if k == "any_dps" then return not ( proc == "damage" or proc == "healing" or proc == "health" or proc == "absorb" or proc == "mana" or proc == "speed" or proc == "leech" or proc == "avoidance" ) end
        return proc == k
    end
}

setmetatable( state.trinket.none.has_stacking_stat, mt_trinket_has_stacking_stat )
setmetatable( state.trinket.t1.has_stacking_stat, mt_trinket_has_stacking_stat )
setmetatable( state.trinket.t2.has_stacking_stat, mt_trinket_has_stacking_stat )
setmetatable( state.trinket.main_hand.has_stacking_stat, mt_trinket_has_stacking_stat )


local mt_trinket_has_stat = {
    __index = function( t, k )
        local trinket = state.trinket[ t.slot ]
        trinket = trinket and trinket.__ability
        trinket = trinket and class.abilities[ trinket ]

        if not trinket then
            return false
        end

        local buff = trinket.self_buff
        buff = buff and class.auras[ buff ]

        if not buff then
            return false
        end

        if k == "any" then return true end

        local proc = trinket.proc or "none"
        if not proc then
            return false
        end

        if k == "any_dps" then return not ( proc == "damage" or proc == "healing" or proc == "absorb" or proc == "mana" or proc == "speed" or proc == "leech" or proc == "avoidance" ) end
        return proc == k
    end
}

setmetatable( state.trinket.none.has_stat, mt_trinket_has_stat )
setmetatable( state.trinket.t1.has_stat, mt_trinket_has_stat )
setmetatable( state.trinket.t2.has_stat, mt_trinket_has_stat )
setmetatable( state.trinket.main_hand.has_stat, mt_trinket_has_stat )


local mt_trinket_with_stat = {
    __index = function( t, k )
        local trinket = state.trinket[ t.slot ]
        return trinket and trinket.has_stat[ k ] and trinket or no_trinket
    end
}

setmetatable( state.trinket.none.stat, mt_trinket_with_stat )
setmetatable( state.trinket.t1.stat, mt_trinket_with_stat )
setmetatable( state.trinket.t2.stat, mt_trinket_with_stat )
setmetatable( state.trinket.main_hand.stat, mt_trinket_with_stat )

state.trinket.none.proc = state.trinket.none.stat
state.trinket.t1.proc = state.trinket.t1.stat
state.trinket.t2.proc = state.trinket.t2.stat
state.trinket.main_hand.proc = state.trinket.main_hand.stat


local mt_trinkets_has_stat = {
    __index = function( t, k )
        return state.trinket.t1.has_stat[ k ] or state.trinket.t2.has_stat[ k ]
    end
}

setmetatable( state.trinket.has_stat, mt_trinkets_has_stat )


local mt_trinkets_has_stacking_stat = {
    __index = function( t, k )
        return state.trinket.t1.has_stacking_stat[ k ] or state.trinket.t2.has_stacking_stat[ k ]
    end
}

setmetatable( state.trinket.has_stacking_stat, mt_trinkets_has_stacking_stat )


state.max = safeMax
state.min = safeMin
state.abs = safeAbs

if Hekili.Version:match( "^Dev" ) then
    state.print = print
else
    state.print = function() end
end

state.Enum = Enum
state.FindPlayerAuraByID = ns.FindPlayerAuraByID
state.FindUnitBuffByID = ns.FindUnitBuffByID
state.FindUnitDebuffByID = ns.FindUnitDebuffByID
state.FindRaidBuffByID = ns.FindRaidBuffByID
state.FindRaidBuffLowestRemainsByID = ns.FindRaidBuffLowestRemainsByID
state.FindLowHpPlayerWithoutBuffByID = ns.FindLowHpPlayerWithoutBuffByID
state.GetActiveLossOfControlData = C_LossOfControl.GetActiveLossOfControlData
state.GetActiveLossOfControlDataCount = C_LossOfControl.GetActiveLossOfControlDataCount
state.GetNumGroupMembers = GetNumGroupMembers
-- state.GetItemCooldown = GetItemCooldown
state.GetItemCount = C_Item.GetItemCount
state.GetItemGem = GetItemGem
state.GetItemInfo = GetItemInfo
state.GetPlayerAuraBySpellID = GetPlayerAuraBySpellID
state.GetShapeshiftForm = GetShapeshiftForm
state.GetShapeshiftFormInfo = GetShapeshiftFormInfo
state.GetSpellCount = C_Spell.GetSpellCastCount
state.GetSpellInfo = ns.GetUnpackedSpellInfo
state.GetSpellLink = GetSpellLink
state.GetSpellTexture = C_Spell.GetSpellTexture
state.GetStablePetInfo = GetStablePetInfo
state.GetTime = GetTime
state.GetTotemInfo = GetTotemInfo
state.InCombatLockdown = InCombatLockdown
state.IsActiveSpell = ns.IsActiveSpell
state.IsPlayerSpell = C_SpellBook.IsSpellKnown
state.IsSpellKnown = C_SpellBook.IsSpellInSpellBook
state.IsSpellKnownOrOverridesKnown = C_SpellBook.IsSpellInSpellBook
state.IsUsableItem = C_Item.IsUsableItem
state.IsUsableSpell = C_Spell.IsSpellUsable
state.UnitAura = UnitAura
state.UnitAuraSlots = C_UnitAuras.GetAuraSlots
state.UnitBuff = UnitBuff
state.UnitCanAttack = UnitCanAttack
state.UnitCastingInfo = UnitCastingInfo
state.UnitChannelInfo = UnitChannelInfo
state.UnitClassification = UnitClassification
state.UnitDebuff = UnitDebuff
state.UnitExists = UnitExists
state.UnitGetTotalAbsorbs = UnitGetTotalAbsorbs
state.UnitGUID = UnitGUID
state.UnitHealth = UnitHealth
state.UnitHealthMax = UnitHealthMax
state.UnitName = UnitName
state.UnitIsFriend = UnitIsFriend

local UnitIsUnit = _G.UnitIsUnit

state.UnitIsUnit = function( a, b )
    return a == b or UnitIsUnit( a, b )
end

state.UnitIsPlayer = UnitIsPlayer
state.UnitLevel = UnitLevel
state.UnitPower = UnitPower
state.UnitPartialPower = UnitPartialPower
state.UnitPowerMax = UnitPowerMax
state.abs = math.abs
state.ceil = math.ceil
state.floor = math.floor
state.format = string.format
state.ipairs = ipairs
state.pairs = pairs
state.rawget = rawget
state.rawset = rawset
state.select = select
state.tinsert = table.insert
state.insert = table.insert
state.remove = table.remove
state.tonumber = tonumber
state.tostring = tostring
state.type = type
state.unpack = unpack

state.safenum = function( val )
    if type( val ) == "number" then return val end
    return val == true and 1 or 0
end

state.safebool = function( val )
    if type( val ) == "boolean" then return val end
    if val == nil then return false end
    if val == 0 then return false end
    return true
end

state.combat = 0
state.faction = UnitFactionGroup( "player" )
state.race[ formatKey( UnitRace("player") ) ] = true

state.class = Hekili.Class
state.targets = ns.targets

state._G = 0


-- Place an ability on cooldown in the simulated game state.
local function setCooldown( action, duration )
    local cd = state.cooldown[ action ] or {}
    cd.duration = duration > 0 and duration or cd.duration
    cd.expires = state.query_time + duration

    cd.charge = 0
    cd.recharge_began = state.query_time

    state.cooldown[ action ] = cd
end
state.setCooldown = setCooldown


local function spendCharges( action, charges )
    local ability = class.abilities[ action ]

    if not ability.charges or ability.charges == 1 then
        setCooldown( action, ability.cooldown )
        return
    end

    if not state.cooldown[ action ] then state.cooldown[ action ] = {} end
    local cd = state.cooldown[ action ]

    if cd.charges == ability.charges then
        cd.charge = ability.charges
        cd.recharge_began = 0
    elseif cd.charges > cd.charge then
        local addl = cd.charges_fractional - cd.charges
        cd.charge = cd.charges
        cd.recharge_began = state.query_time - ( addl * ( ability.recharge or ability.cooldown ) )
    end

    cd.charge = max( 0, cd.charge - charges )
    if cd.recharge_began == 0 then cd.recharge_began = state.query_time end

    local dur = ability.recharge or ability.cooldown
    cd.duration = dur > 0 and dur or cd.duration
    cd.expires = cd.charge == 0 and ( cd.recharge_began + dur ) or 0
end
state.spendCharges = spendCharges


local function gainCharges( action, charges )

    if class.abilities[ action ].charges then
        state.cooldown[ action ].charge = min( class.abilities[ action ].charges, state.cooldown[ action ].charge + charges )

        -- resolve cooldown state.
        if state.cooldown[ action ].charge > 0 then
            -- state.cooldown[ action ].duration = 0
            state.cooldown[ action ].expires = 0
        end

        if state.cooldown[ action ].charge == class.abilities[ action ].charges then
            state.cooldown[ action ].recharge_began = 0
        end

    else
        -- Error-proof gaining charges for abilities without charges.
        if charges >= 1 then
            setCooldown( action, 0 )
        end
    end

end
state.gainCharges = gainCharges


function state.gainChargeTime( action, time, debug )
    local ability = class.abilities[ action ]
    if not ability then return end

    local cooldown = state.cooldown[ action ]

    if not ability.charges then
        -- Error-proof gaining charge time on chargeless abilities.
        cooldown.expires = cooldown.expires - time
        return
    end

    if cooldown.charge == ability.charges then return end
    cooldown.recharge_began = cooldown.recharge_began - time

    if cooldown.expires > 0 then cooldown.expires = max( 0, cooldown.expires - time ) end
end


function state.reduceCooldown( action, time )
    local ability = class.abilities[ action ]
    if not ability then return end

    if ability.charges then
        state.gainChargeTime( action, time )
        return
    end

    state.cooldown[ action ].expires = max( 0, state.cooldown[ action ].expires - time )
end


-- Cycling System...
do
    local cycle = {}
    local debug = function( ... ) if Hekili.ActiveDebug then Hekili:Debug( ... ) end end

    function state.SetupCycle( ability, quiet )
        wipe( cycle )

        if not ability and not quiet then
            debug( " - no ability provided to SetupCycle." )
            return
        end

        local aura = ability.cycle or ability.aura or ability.key

        if not aura and not quiet then
            debug( " - no aura identified for target-cycling and no aura matching " .. ability.key .. " found in ability / spec module; target cycling disabled." )
            return
        end

        local cDebuff = class.auras[ aura ] and state.debuff[ aura ]

        if not cDebuff then
            debug( " - the debuff '%s' was not found in our database.", aura )
            return
        end

        if cDebuff.up and not ability.cycle_to then
            -- We want to target enemies with this debuff.
            cycle.expires = cDebuff.expires
            cycle.minTTD  = max( state.settings.cycle_min, ability.min_ttd or 0, cDebuff.duration / 2 )
            cycle.maxTTD  = ability.max_ttd

            cycle.aura = aura

            if state.active_dot[ aura ] >= state.cycle_enemies then
                if not quiet then debug( " - we do not have another valid target for " .. aura .. ": " .. state.active_dot[ aura ] .. " vs " .. state.cycle_enemies .. "." ) end
                state.ClearCycle()
                return
            end

            if not quiet then
                debug( " - we will use the ability on a different target, if available, until %s expires at %.2f [+%.2f].", cycle.aura, cycle.expires, cycle.expires - state.query_time )
            end
        elseif cDebuff.down and ability.cycle_to and state.active_dot[ aura ] > 0 and state.query_time < state.now + ( 3 * state.gcd.max ) then
            cycle.expires = state.query_time + ( 2 * state.gcd.max ) -- Assume the aura is available for 3 GCDs (don't forecast a slow target swap).
            cycle.minTTD  = max( state.settings.cycle_min, ability.min_ttd or 0, cDebuff.duration / 2 )
            cycle.maxTTD  = ability.max_ttd

            cycle.aura = aura

            if not quiet then
                debug( " - we will use the ability on a target with %s, if available, .", cycle.aura )
            end
        else
            if not quiet then debug( " - cycle aura appears to be down, so we're sticking with our current target." ) end
        end

        --[[ Possible future version:
            local debuffIsUp = cDebuff.up

            cycle.expires = cDebuff.duration + state.query_time

            cycle.minTTD  = max( state.settings.cycle_min, ability.min_ttd or 0, cDebuff.duration / 2 )
            cycle.maxTTD  = ability.max_ttd
            cycle.aura = aura

            if debuffIsUp and not ability.cycle_to and state.active_dot[ aura ] >= state.cycle_enemies then
                debug( " - we will not use the ability on a different target, as we have enough targets with %s.", cycle.aura )
                state.ClearCycle()
                return
            end

            if not debuffIsUp and not ability.cycle_to then
                debug( " - we will not use the ability on a different target, as current target does not have %s.", cycle.aura )
                state.ClearCycle()
                return
            end

            if not debuffIsUp and ability.cycle_to and active_dot[ aura ] == 0 then
                debug( " - we will not use the ability on a different target, as no other target has %s applied.", cycle.aura)
                state.ClearCycle()
                return
            end
        ]]
    end

    function state.GetCycleInfo()
        return cycle.expires, cycle.minTTD, cycle.maxTTD, cycle.aura
    end

    function state.SetCycleInfo( expires, minTTD, maxTTD, aura )
        cycle.expires = expires
        cycle.minTTD  = minTTD
        cycle.maxTTD  = maxTTD
        cycle.aura    = aura
    end

    function state.HasCyclingDebuff( aura )
        if not cycle.aura then return false end
        if aura and aura ~= cycle.aura then return false end
        return true
    end

    function state.IsCycling( aura, quiet )
        if not cycle.aura then
            return false, "cycle.aura is nil"
        end
        if aura and cycle.aura ~= aura then
            if not quiet then debug( "cycle.aura ~= '%s'", aura ) end
            return false, format( "cycle aura (%s) is not '%s'", cycle.aura or "none", aura )
        end
        if state.cycle_enemies == 1 then
            return false, "cycle_enemies == 1"
        end
        if cycle.expires < state.query_time then
            return false, format( "cycle aura (%s) expires before current time", cycle.aura )
        end
        if state.active_dot[ cycle.aura ] >= state.cycle_enemies then
            return false, format( "active_dot[%d] >= cycle_enemies[%d]", state.active_dot[ cycle.aura ], state.cycle_enemies )
        end

        return true
    end

    function state.ClearCycle()
        if cycle.aura then wipe( cycle ) end
        state.cycle = nil
    end

    state.cycleInfo = cycle
end


-- Apply a buff to the current game state.
local function applyBuff( aura, duration, stacks, value, v2, v3, applied )
    if not aura then
        Error( "Attempted to apply/remove a nameless aura '%s'.\n\n%s", aura or "nil", debugstack() )
        return
    end

    local model = class.auras[ aura ]

    if not model then
        Error( "Warning: attempted to apply unknown buff '%s'.", aura )
        local spec = class.specs[ state.spec.id ]
        if spec then
            spec:RegisterAura( aura, { ["duration"] = duration } )
            class.auras[ aura ] = spec.auras[ aura ]
        end

        model = class.auras[ aura ]
        if not model then return end
    end

    if model.alias then
        aura = model.alias[1]
    end

    if state.cycle then
        if duration == 0 then state.active_dot[ aura ] = max( 0, state.active_dot[ aura ] - 1 )
        else state.active_dot[ aura ] = max( state.active_enemies, state.active_dot[ aura ] + 1 ) end
        return
    end

    local b = state.buff[ aura ]
    if not b then return end

    duration = duration or model.duration or 15

    if duration == 0 then
        b.last_expiry = b.expires or 0
        b.expires = 0

        b.lastCount = b.count
        b.count = 0

        b.lastApplied = b.applied
        b.last_application = b.applied or 0

        b.v1 = value or 0
        b.v2 = 0
        b.v3 = 0
        b.applied = 0
        b.caster = "unknown"

        state.active_dot[ aura ] = max( 0, state.active_dot[ aura ] - 1 )

        if model.funcs.onRemove then model.funcs.onRemove() end

    else
        if not b.up or model.friendly then state.active_dot[ aura ] = min( state.group_members, state.active_dot[ aura ] + 1 ) end

        b.lastCount = b.count
        b.lastApplied = b.applied

        b.applied = applied or state.query_time
        b.last_application = b.applied or 0

        -- b.duration = duration

        b.expires = b.applied + duration
        b.last_expiry = b.expires

        b.count = max( 0, min( class.auras[ aura ].max_stack or 1, stacks or 1 ) )
        b.v1 = value or 0
        if v2 == nil then b.v2 = 0
        else b.v2 = v2 end
        if v3 == nil then b.v3 = 0
        else b.v3 = v3 end
        b.caster = "player"
    end

    for resource, auras in pairs( class.resourceAuras ) do
        if auras[ aura ] then
            state.forecastResources( resource )
        end
    end

    if aura ~= "potion" and class.auras.potion and class.auras[ aura ].id == class.auras.potion.id then
        applyBuff( "potion", duration, stacks, value )
    end
end
state.applyBuff = applyBuff


local function removeBuff( aura )
    local model = class.auras[ aura ]
    if model and model.alias then
        for _, child in ipairs( model.alias ) do
            applyBuff( child, 0 )
        end
    else
        applyBuff( aura, 0 )
    end
end
state.removeBuff = removeBuff


-- Apply stacks of a buff to the current game state.
-- Wraps around applyBuff() to check for an existing buff.
local function addStack( aura, duration, stacks, value )
    local a = class.auras[ aura ]

    duration = duration or ( a and a.duration or 15 )
    stacks = stacks or 1

    local max_stack = a and a.max_stack or 1

    local b = state.buff[ aura ]

    if b.remains > 0 then
        applyBuff( aura, duration, min( max_stack, b.count + stacks ), value )
    else
        applyBuff( aura, duration, min( max_stack, stacks ), value )
    end

end
state.addStack = addStack


local function removeStack( aura, stacks )
    stacks = stacks or 1

    local b = state.buff[ aura ]

    if b.count > stacks then
        b.lastCount = b.count
        b.count = max( 1, b.count - stacks )
    else
        state.removeBuff( aura )
    end
end
state.removeStack = removeStack


-- Add a debuff to the simulated game state.
-- Needs to actually use "unit" !
local function applyDebuff( unit, aura, duration, stacks, value, noPandemic )
    -- Off by 1.
    if not aura or type( aura ) == "number" then
        noPandemic = value
        value = stacks
        stacks = duration
        duration = aura
        aura = unit
        unit = "target"
    end

    local model = class.auras[ aura ]
    if not model then
        Error( "Warning: attempted to apply unknown debuff '%s'.", aura )
        local spec = class.specs[ state.spec.id ]
        if spec then
            spec:RegisterAura( aura, { ["duration"] = duration } )
            class.auras[ aura ] = spec.auras[ aura ]
        end

        model = class.auras[ aura ]
        if not model then return end
    end

    if state.cycle then
        if duration == 0 then
            if Hekili.ActiveDebug then Hekili:Debug( "Removed an application of '%s' while target-cycling.", aura ) end
            state.active_dot[ aura ] = state.active_dot[ aura ] - 1
            if model.key ~= aura then state.active_dot[ model.key ] = state.active_dot[ aura ] end
        else
            if Hekili.ActiveDebug then Hekili:Debug( "Added an application of '%s' while target-cycling.", aura ) end
            state.active_dot[ aura ] = state.active_dot[ aura ] + 1
            if model.key ~= aura then state.active_dot[ model.key ] = state.active_dot[ aura ] end
        end
        return
    end

    local d = state.debuff[ aura ]
    duration = duration or model.duration or 15

    if duration == 0 then
        d.expires = 0

        d.lastCount = d.count
        d.lastApplied = d.lastApplied

        d.count = 0
        d.value = 0
        d.applied = 0
        d.unit = unit

        state.active_dot[ aura ] = max( 0, state.active_dot[ aura ] - 1 )
        if model.key ~= aura then state.active_dot[ model.key ] = state.active_dot[ aura ] end
    else
        if d.down or state.active_dot[ aura ] == 0 then
            state.active_dot[ aura ] = state.active_dot[ aura ] + 1
            if model.key ~= aura then state.active_dot[ model.key ] = state.active_dot[ aura ] end
            -- TODO: Aura scraping utility may want to populate active_dot table when it sees an aura that wasn't tracked.
        end

        -- state.debuff[ aura ] = state.debuff[ aura ] or {}
        if not noPandemic then duration = min( 1.3 * duration, d.remains + duration ) end

        -- d.duration = duration
        d.expires = state.query_time + duration

        d.lastCount = d.count or 0
        d.lastApplied = d.applied or 0

        d.count = min( model.max_stack or 1, stacks or 1 )
        d.value = value or 0
        d.applied = state.query_time
        d.unit = unit or "target"
    end

end
state.applyDebuff = applyDebuff


local function removeDebuff( unit, aura )
    applyDebuff( unit, aura, 0 )
end
state.removeDebuff = removeDebuff


local function removeDebuffStack( unit, aura, stacks )
    stacks = stacks or 1
    local d = state.debuff[ aura ]
    if not d then return 0 end

    local removed = min( stacks, d.count )

    if d.count > stacks then
        d.lastCount = d.count
        d.count = max( 0, d.count - stacks )
    else
        removeDebuff( unit, aura )
    end

    return removed
end

state.removeDebuffStack = removeDebuffStack


local function setStance( stance )
    for k in pairs( state.stance ) do
        state.stance[ k ] = false
    end
    state.stance[ stance ] = true
end
state.setStance = setStance


local function interrupt()
    state.removeDebuff( "target", "casting" )
end
state.interrupt = interrupt


-- Use this for readyTime in an interrupt action; will interrupt casts at end of cast and channels ASAP.
local function timeToInterrupt()
    local casting = state.debuff.casting
    if casting.down or casting.v2 == 1 then return 3600 end
    if casting.v3 == 1 then return 0 end
    return max( 0, casting.remains - ( Hekili.DB.profile.toggles.interrupts.castRemainingThreshold or 0.25 ) )
end
state.timeToInterrupt = timeToInterrupt


-- Pet stuff.
local function summonPet( name, duration, spec )
    state.pet[ name ] = rawget( state.pet, name ) or {}
    state.pet[ name ].name = name
    state.pet[ name ].expires = state.query_time + ( duration or 3600 )

    local model = class.pets[ name ]

    if model then
        state.pet[ name ].model = model  -- access id dynamically via metatable
        if ( type( model.duration ) == "function" and model.duration() or model.duration ) == 3600 then
            state.pet.alive = true
        end
    end

    if spec then
        state.pet[ name ].spec = spec
        state.pet[ spec ] = state.pet[ name ]
    end
end
state.summonPet = summonPet


local function dismissPet( name )
    local pet = rawget( state.pet, name ) or {}
    pet.name = name
    pet.expires = 0
    if pet.spec then
        rawset( state.pet, pet.spec, nil )
    end
    state.pet[ name ] = pet
end
state.dismissPet = dismissPet


local function summonTotem( name, elem, duration )
    if elem then
        state.totem[ elem ] = rawget( state.totem, elem ) or {}
        state.totem[ elem ].name = name
        state.totem[ elem ].expires = state.query_time + duration
        summonPet( elem, duration )
    end

    summonPet( name, duration )
end
state.summonTotem = summonTotem


-- Useful for things like leap/charge/etc.
local function setDistance( minimum, maximum )
    state.target.minR = minimum or 5
    state.target.maxR = maximum or minimum or 5
    state.target.distance = ( state.target.minR + state.target.maxR ) / 2
end
state.setDistance = setDistance


-- For tracking if we are currently channeling.
function state.channelSpell( name, start, duration, id )
    if name then
        local ability = class.abilities[ name ]

        start = start or state.query_time

        if ability then
            duration = duration or ability.cast
        end

        if not duration or duration == 0 then return end

        applyBuff( "casting", duration, nil, id or ( ability and ability.id ) or 0, nil, 1, start )
    end
end

function state.stopChanneling( reset, action )
    if not reset then
        local spell = state.channel
        local ability = spell and class.abilities[ spell ]

        if spell then
            if Hekili.ActiveDebug then Hekili:Debug( "Breaking channel of %s.", spell ) end
            if ability and ability.breakchannel then ability.breakchannel() end
            state:RemoveSpellEvents( spell )
            state.removeBuff( "casting" )
        end
    end

    -- This will lock in gains from channeling before the channel ends.
    for resource, auras in pairs( class.resourceAuras ) do
        if auras.casting then state[ resource ].actual = state[ resource ].current end
    end

    removeBuff( "casting" )
end
-- See mt_state for 'isChanneling'.


-- Spell Targets, so I don't have to convert it in APLs any more.
-- This will also factor in target caps and TTD restrictions.
state.spell_targets = setmetatable( {}, {
    __index = function( t, k )
        if state.active_enemies == 1 then return 1 end
        if k == "any" then return state.active_enemies end

        local ability = class.abilities[ k ]
        if not ability then return state.active_enemies end

        local n = state.active_enemies
        if ability.max_ttd then n = min( n, Hekili:GetNumTTDsBefore( ability.max_ttd + state.offset + state.delay ) ) end
        if ability.min_ttd then n = min( n, Hekili:GetNumTTDsAfter( ability.min_ttd + state.offset + state.delay ) ) end
        if ability.max_targets then n = min( n, ability.max_targets ) end

        return n
    end
} )


local raid_event_filter = {
    ["in"] = 3600,
    amount = 0,
    duration = 0,
    remains = 0,
    cooldown = 0,
    exists = false,
    distance = 0,
    max_distance = 0,
    min_distance = 0,
    to_pct = 0,
    up = false,
    down = true
}

state.raid_event = setmetatable( {}, {
    __index = function( t, k )
        return raid_event_filter[ k ] or raid_event_filter
    end
} )


-- We'll pretend we're in an active raid_event.adds when there are multiple targets.
state.raid_event.adds = setmetatable( {
    ["in"] = 3600, -- raid_event.adds.in appears to return time to the next add event, so we can just always say it's waaaay in the future.
}, {
    __index = function( t, k )
        if k == "up" or k == "exists" then
            return state.active_enemies > 1
        elseif k == "down" then
            return state.active_enemies <= 1
        elseif k == "count" then
            return max( 0, state.active_enemies - 1 )
        elseif k == "in" then
            return state.active_enemies > 1 and 0 or 3600
        elseif k == "duration" or k == "remains" then
            return state.active_enemies > 1 and state.fight_remains or 0
        elseif raid_event_filter[k] ~= nil then return raid_event_filter[k] end

        return 0
    end
} )

-- Resource Modeling!
local forecastResources

do
    local events = {}
    local remains = {}

    local function resourceModelSort( a, b )
        return b == nil or ( a.next < b.next )
    end

    -- Increase max forecast duration because Assassination is pooling hard this tier.
    local FORECAST_DURATION = 10.01

    forecastResources = function( resource )
        if not resource then return end

        -- Initialize or wipe tables
        wipe( events )
        wipe( remains )

        local now = state.now + state.offset
        local timeout = FORECAST_DURATION * state.haste

        if state.class.file == "DEATHKNIGHT" and state.runes then
            -- Adjust timeout based on rune cooldowns and regen models for Frost DK
            timeout = max( timeout, 0.01 + state.runes.expiry[ 6 ] - state.query_time )
        elseif state.spec.assassination then
            timeout = max( timeout, 0.01 + state.energy.max / max( 0.001, state.energy.regen_combined ) )
        end

        timeout = timeout + state.gcd.remains
        local r = state[ resource ]

        -- Initialize forecast tables
        remains[ resource ] = timeout

        wipe( r.times )
        wipe( r.values )

        r.forecast[ 1 ] = r.forecast[ 1 ] or {}
        r.forecast[ 1 ].t = now
        r.forecast[ 1 ].v = r.actual
        r.forecast[ 1 ].e = "actual"
        r.fcount = 1

        local models = r.regenModel

        if models then
            for k, v in pairs( models ) do
                if ( not v.resource or v.resource == resource ) and
                   ( not v.spec or state.spec[ v.spec ] ) and
                   ( not v.equip or state.equipped[ v.equip ] ) and
                   ( not v.talent or state.talent[ v.talent ].enabled ) and
                   ( not v.pvptalent or state.pvptalent[ v.pvptalent ].enabled ) and
                   ( not v.aura or state[ v.debuff and "debuff" or "buff" ][ v.aura ].remains > 0 ) and
                   ( not v.set_bonus or state.set_bonus[ v.set_bonus ] > 0 ) and
                   ( not v.setting or state.settings[ v.setting ] ) and
                   ( not v.swing or state.swings[ v.swing .. "_speed" ] and state.swings[ v.swing .. "_speed" ] > 0 ) and
                   ( not v.channel or state.buff.casting.up and state.buff.casting.v3 == 1 and state.buff.casting.v1 == class.abilities[ v.channel ].id ) then

                    local l = v.last()
                    local i = type( v.interval ) == "number" and v.interval or
                              ( type( v.interval ) == "function" and v.interval( now, r.actual ) or
                              ( type( v.interval ) == "string" and state[ v.interval ] or 0 ) )

                    v.next = l + i
                    v.name = k

                    if i > 0 and v.next >= 0 then
                        table.insert( events, v )
                    end
                end
            end
        end

        sort( events, resourceModelSort )

        local finish = now + timeout
        local prev = now
        local iter = 0

        local regen = r.regen_combined and r.regen_combined or r.regen or 0
        regen = regen > 0.001 and regen or 0

        while ( #events > 0 and now <= finish and iter < 30 ) do
            local e = events[ 1 ]
            iter = iter + 1

            if e.next > finish or not r or not r.actual then
                table.remove( events, 1 )
            else
                now = e.next

                local bonus = regen * ( now - prev )
                local stop = e.stop and e.stop( r.forecast[ r.fcount ].v )
                local aura = e.aura and state[ e.debuff and "debuff" or "buff" ][ e.aura ].expires < now
                local channel = ( e.channel and state.buff.casting.expires < now )

                if stop or aura or channel then
                    table.remove( events, 1 )
                    local v = max( 0, min( r.max, r.forecast[ r.fcount ].v + bonus ) )
                    local idx

                    if r.forecast[ r.fcount ].t == now then
                        idx = r.fcount
                    else
                        idx = r.fcount + 1
                    end

                    r.forecast[ idx ] = r.forecast[ idx ] or {}
                    r.forecast[ idx ].t = now
                    r.forecast[ idx ].v = v
                    r.forecast[ idx ].e = ( e.name or "none" ) .. ( stop and "-stop" or aura and "-aura" or channel and "-channel" or "-unknown" )
                    r.fcount = idx
                else
                    prev = now

                    local val = r.fcount > 0 and r.forecast[ r.fcount ].v or r.actual
                    local v = max( 0, min( r.max, val + bonus ) )
                    v = max( 0, min( r.max, v + ( type( e.value ) == "number" and e.value or e.value( now ) ) ) )
                    local idx

                    if r.forecast[ r.fcount ].t == now then
                        idx = r.fcount
                    else
                        idx = r.fcount + 1
                    end

                    r.forecast[ idx ] = r.forecast[ idx ] or {}
                    r.forecast[ idx ].t = now
                    r.forecast[ idx ].v = v
                    r.forecast[ idx ].e = e.name or "none"
                    r.fcount = idx

                    local step = roundDown( type( e.interval ) == "number" and e.interval or
                                           ( type( e.interval ) == "function" and e.interval( now, v ) or
                                           ( type( e.interval ) == "string" and state[ e.interval ] or 0 ) ), 3 )

                    remains[ e.resource ] = finish - e.next
                    e.next = e.next + step

                    if e.next > finish or step < 0 or
                       ( e.aura and state[ e.debuff and "debuff" or "buff" ][ e.aura ].expires < e.next ) or
                       ( e.channel and state.buff.casting.expires < e.next ) then
                        table.remove( events, 1 )
                    end
                end

                if #events > 1 then sort( events, resourceModelSort ) end
            end
        end

        if regen > 0 and r.forecast[ r.fcount ].v < r.max then
            for k, v in pairs( remains ) do
                local val = r.fcount > 0 and r.forecast[ r.fcount ].v or r.actual
                local idx = r.fcount + 1

                r.forecast[ idx ] = r.forecast[ idx ] or {}
                r.forecast[ idx ].t = finish
                r.forecast[ idx ].v = min( r.max, val + ( v * regen ) )
                r.forecast[ idx ].e = "to_max"
                r.fcount = idx
            end
        end
    end

    ns.forecastResources = forecastResources
    state.forecastResources = forecastResources
end


function state:ForecastSwingbasedResources()
    for k, v in pairs( class.resources ) do
        if v and v.state and v.state.swingGen then
            forecastResources( k )
        end
    end
end


local resourceChange = function( amount, resource, overcap )
    if amount == 0 then return false end

    local r = state[ resource ]
    local pre = r.current

    if amount < 0 and r.spend then r.spend( -amount, resource, overcap )
    elseif amount > 0 and r.gain then r.gain( amount, resource, overcap )
    else
        r.actual = max( 0, r.current + amount )
        if not overcap then r.actual = min( r.max, r.actual ) end
    end

    return true
end


-- Noteworthy hooks for gain/spend:
-- pregain - the hook is expected to return modified values for the resource (i.e., special cost reduction or refunds).
-- gain    - the hook can do whatever it wants, but if it changes the same resource again it will cause another forecast.

local gain = function( amount, resource, overcap, noforecast )
    amount, resource, overcap = ns.callHook( "pregain", amount, resource, overcap )
    resourceChange( amount, resource, overcap )
    if not noforecast and resource ~= "health" then forecastResources( resource ) end
    ns.callHook( "gain", amount, resource, overcap )
end

local rawGain = function( amount, resource, overcap )
    resourceChange( amount, resource, overcap )
    forecastResources( resource )
end


local spend = function( amount, resource, noforecast )
    amount, resource = ns.callHook( "prespend", amount, resource )
    resourceChange( -amount, resource, overcap )
    if not noforecast and resource ~= "health" then forecastResources( resource ) end
    ns.callHook( "spend", amount, resource, overcap, true )
end

local rawSpend = function( amount, resource )
    resourceChange( -amount, resource, overcap )
    forecastResources( resource )
end


state.gain = gain
state.rawGain = rawGain

state.spend = spend
state.rawSpend = rawSpend


do
    -- Rechecking System
    -- Setup on a per-ability basis, this gives the prediction engine a head's up that the ability may become ready in a short time.

    local workTable = {}
    state.recheckTimes = {}

    local function recheckHelper( t, ... )
        local n = select( "#", ... )

        for i = 1, n do
            local x = select( i, ... )
            if type( x ) == "number" then
                if x > 0 and x >= state.delayMin and x <= state.delayMax then
                    t[ x ] = true
                -- elseif Hekili.ActiveDebug and x < 60 then
                --    Hekili:Debug( "Excluded %.2f recheck time as it is outside our constraints ( %.2f - %.2f ).", x, state.delayMin or -1, state.delayMax or -1 )
                end
            end
        end
    end

    local recurseRechecks
    recurseRechecks = function( script, seen )
        if not seen then seen = {}
        else seen[ script.ID ] = script.Recheck or true end

        if script.Variables then
            for _, var in ipairs( script.Variables ) do
                local varIDs = state:GetVariableIDs( var )
                if varIDs then
                    for _, entry in ipairs( varIDs ) do
                        if not seen[ entry.id ] then
                            local subscript = scripts.DB[ entry.id ]
                            if subscript then recurseRechecks( subscript, seen ) end
                        end
                    end
                end
            end
        end

        return seen
    end

    function state.recheck( ability, script, stack, block )
        local times = state.recheckTimes
        wipe( workTable )

        -- local debug = Hekili.ActiveDebug
        -- local steps = {}

        if script then
            if script.Recheck then
                recheckHelper( workTable, script.Recheck() )
            end

            if script.Variables then
                if not script.AllRechecks then
                    script.AllRechecks = recurseRechecks( script )

                    for id, func in pairs( script.AllRechecks ) do
                        if type( func ) ~= "function" then script.AllRechecks[ id ] = nil end
                    end
                end

                for id, func in pairs( script.AllRechecks ) do
                    recheckHelper( workTable, func() )
                end
            end
        end

        -- if Hekili.ActiveDebug then table.insert( steps, debugprofilestop() ) end

        local data = class.abilities[ ability ]
        if data and data.aura then
            local a = state.buff[ data.aura ]
            if a and a.up then
                recheckHelper( workTable, a.remains )
            end

            a = state.debuff[ data.aura ]
            if a and a.up then
                recheckHelper( workTable, a.remains )
            end
        end

        -- if Hekili.ActiveDebug then table.insert( steps, debugprofilestop() ) end

        if stack and #stack > 0 then
            for i, caller in ipairs( stack ) do
                local callScript = caller.script
                callScript = callScript and scripts:GetScript( callScript )

                if callScript and callScript.Recheck then
                    recheckHelper( workTable, callScript.Recheck() )

                    if callScript.Variables then
                        if not callScript.AllRechecks then
                            callScript.AllRechecks = recurseRechecks( callScript )

                            for id, func in pairs( callScript.AllRechecks ) do
                                if type( func ) ~= "function" then callScript.AllRechecks[ id ] = nil end
                            end
                        end

                        for id, func in pairs( callScript.AllRechecks ) do
                            recheckHelper( workTable, func() )
                        end
                    end
                end
            end
        end

        if block and #block > 0 then
            for i, caller in ipairs( block ) do
                local callScript = caller.script
                callScript = callScript and scripts:GetScript( callScript )

                if callScript and callScript.Recheck then
                    recheckHelper( workTable, callScript.Recheck() )
                end

                if callScript and callScript.Recheck then
                    recheckHelper( workTable, callScript.Recheck() )

                    if callScript.Variables then
                        if not callScript.AllRechecks then
                            callScript.AllRechecks = recurseRechecks( callScript )

                            for id, func in pairs( callScript.AllRechecks ) do
                                if type( func ) ~= "function" then callScript.AllRechecks[ id ] = nil end
                            end
                        end

                        for id, func in pairs( callScript.AllRechecks ) do
                            recheckHelper( workTable, func() )
                        end
                    end
                end
            end
        end

        wipe( times )

        for k, v in pairs( workTable ) do
            -- if Hekili.ActiveDebug then Hekili:Debug( "%s - %s", tostring( k ), tostring( v ) ) end
            times[ #times + 1 ] = k
        end

        ns.callHook( "recheck", times )

        sort( times )
    end
end



--------------------------------------
-- UGLY METATABLES BELOW THIS POINT --
--------------------------------------
ns.metatables = {}


-- Returns false instead of nil when a key is not found.
local mt_false = {
    __index = function(t, k)
        return false
    end
}
ns.metatables.mt_false = mt_false


do
    local a = class.knownAuraAttributes

    -- Populate table of known aura attributes so we know if we should bother looking in buffs/debuffs for this information.

    a.applied = true
    a.caster = true
    a.cooldown_remains = true
    a.count = true
    a.down = true
    a.duration = true
    a.expires = true
    a.i_up = true
    a.id = true
    a.key = true
    a.lastApplied = true
    a.lastCount = true
    a.last_application = true
    a.last_expiry = true
    a.max_stack = true
    a.max_stacks = true
    a.mine = true
    a.name = true
    a.rank = true
    a.react = true
    a.refreshable = true
    a.remains = true
    a.stack = true
    a.stack_pct = true
    a.stacks = true
    a.tick_time_remains = true
    a.ticking = true
    a.ticks = true
    a.ticks_remain = true
    a.time_to_refresh = true
    a.timeMod = true
    a.unit = true
    a.up = true
    a.v1 = true
    a.v2 = true
    a.v3 = true
end




-- Gives calculated values for some state options in order to emulate SimC syntax.
local mt_state
do
    local logged_state_errors = {}

    local autoReset = setmetatable( {
        -- Internal processing stuff.
        display = 1,
        latency = 1,
        resetting = 1,
        scriptID = 1,
        whitelist = 1,

        -- Timings.
        delay = 1,
        expected_combat_length = 1,
        false_start = 1,
        fight_remains = 1,
            interpolated_fight_remains = 1,
            time_to_die = 1,
        index = 1,
        longest_ttd = 1,
        now = 1,
        offset = 1,
        query_time = 1,
        shortest_ttd = 1,
        time = 1,

        -- Current ability in question, or selected ability when testing the next.
        current_action = 1,
        modified = 1,
        selected_action = 1,
        selection = 1,
        selection_time = 1,
        this_action = 1,
        this_list = 1,
        this_id = 1,

        -- Calculated from event data.
        aggro = 1,
        boss = 1,
        encounter = 1,
        group = 1,
        group_members = 1,
        active_allies = 1,
        level = 1,
        mounted = 1,
            is_mounted = 1,
        moving = 1,
        raid = 1,
        solo = 1,
        tanking = 1,
        group_health = 1,
        at_risk = 1,
        raid_health = 1,

        -- Number of enemies.
        active_enemies = 1,
        cycle_enemies = 1,
        desired_targets = 1,
        max_targets = 1,
        min_targets = 1,
        my_enemies = 1,
        stationary_enemies = 1,
        true_active_enemies = 1,
            true_stationary_enemies = 1,
        true_my_enemies = 1,

        -- Stats (that really come from state.stat )
        crit = 1,
            attack_crit = 1,
            spell_crit = 1,
        haste = 1,
            spell_haste = 1,
        melee_haste = 1,
            attack_haste = 1,
        mastery_value = 1,

        -- ???
        -- Information may be "real" and durable, so we want to leave it be.
        -- active = 1,
        -- hardcast = 1,
        -- miss_react = 1,
        -- ranged = 1,
        -- wait_for_gcd = 1,

        -- Spec State Expressions (probably redundant here).
        effective_combo_points = 1,
        prowling = 1,

        -- Real incoming damage/healing information (handled by autoReset metatable).
        -- incoming_damage = 1,
        -- incoming_heal = 1,
        -- incoming_magic = 1,
        -- incoming_physical = 1,
        -- time_to_pct = 1,

        -- Channels.
        channel = 1,
        channel_breakable = 1,
        channel_remains = 1,
        channeling = 1,

        -- Abilities/Cooldowns.
        action_cooldown = 1,
        cast_delay = 1,
        cast_regen = 1,
        cast_time = 1,
        charges = 1,
        charges_fractional = 1,
        charges_max = 1,
            max_charges = 1,
        cooldown_react = 1,
            cooldown_up = 1,
        cost = 1,
        -- These two belong with ability data because individual abilities have modeled crit modifiers.
        crit_pct_current = 1,
            crit_percent_current = 1,
        execute_remains = 1,
        execute_time = 1,
        executing = 1,
        full_recharge_time = 1,
            time_to_max_charges = 1,
        hardcast = 1,
        in_flight = 1,
        in_flight_remains = 1,
        in_range = 1,
        recharge = 1,
        recharge_time = 1,
        travel_time = 1,

        -- Auras.
        down = 1,
        duration = 1,
        refreshable = 1,
        remains = 1,
        tick_time = 1,
        tick_time_remains = 1,
        ticking = 1,
            up = 1,
        ticks = 1,
        ticks_remain = 1,
        time_to_refresh = 1,

        --[[ Macro conditionals.
        advflyable = 1,
        canexitvehicle = 1,
        -- channeling = 1, -- already exists.
        -- combat = 1, -- already exists.
        flyable = 1,
        flying = 1,
        -- group = 1, -- already exists.
        indoors = 1,
        outdoors = 1,
        -- mounted = 1, -- already exists.
        petbattle = 1,
        pvpcombat = 1,
        resting = 1,
        bonusbar = 1,
        cursor = 1,
        extrabar = 1,
        mod = 1,
        modifier = 1,
        overridebar = 1,
        possessbar = 1,
        shapeshift = 1,
        vehicleui = 1, ]]
    }, {
        __index = function ( t, k )
            -- Make sure to get anything that should be dynamic, in case it was set (via newindex).
            if class.stateExprs[ k ] then return 1 end
            if class.knownAuraAttributes[ k ] then return 1 end
            if k:match( "^time_to_pct" ) then return 1 end
            if k:match( "^incoming_damage" ) then return 1 end
            if k:match( "^incoming_physical" ) then return 1 end
            if k:match( "^incoming_magic" ) then return 1 end
            if k:match( "^incoming_heal" ) then return 1 end

            -- I should reorder this file to avoid this.
            local x = rawget( t, "GetVariableIDs" )
            if x and x( t, k ) ~= nil then return 1 end

            x = rawget( t, "settings" )
            if x and x[ k ] ~= nil then return 1 end

            x = rawget( t, "toggle" )
            if x and x[ k ] ~= nil then return 1 end
        end
    } )

    mt_state = {
        __index = function( t, k )
            -- Simple rules for a resettable table.
            -- If the information won't change (unless explicitly set), assign the value so it's more efficient when used multiple times.
            -- If the information will change and is time-sensitive, just return the calculated value.
            -- If something externally will set the value and it needs to persist, do not include in the reset table.
            -- If it's possible that something will set a value during recommendations generation, the key should also be in the reset table so it'll get wiped for each new set of recommendations.
            if class.stateExprs[ k ] then return class.stateExprs[ k ]()

            -- Internal processing stuff.
            elseif k == "display" then t[k] = "Primary"
            elseif k == "latency" then t[k] = select( 4, GetNetStats() ) / 1000
            elseif k == "resetting" then t[k] = false
            elseif k == "scriptID" then t[k] = "NilScriptID"
            elseif k == "whitelist" then return nil
            elseif k == "cycle" then t[k] = false

            -- Timings.
            elseif k == "delay" then t[k] = 0
            elseif k == "expected_combat_length" then
                if not t.boss then t[k] = 3600 end
                t[k] = t.longest_ttd + t.time -- + t.offset + t.delay
            elseif k == "false_start" then return 0
            elseif k == "fight_remains" or k == "interpolated_fight_remains" or k == "time_to_die" then
                local n = t.longest_ttd
                if not n then Hekili:Error( "longest_ttd was nil, GetGreatestTTD is " .. ( Hekili:GetGreatestTTD() or "nil" ) .. "." ) end
                return max( 1, t.longest_ttd - ( t.offset + t.delay ) )
            elseif k == "index" then t[k] = 0
            elseif k == "longest_ttd" then t[k] = Hekili:GetGreatestTTD()
            elseif k == "now" then t[k] = GetTime()
            elseif k == "offset" then t[k] = 0
            elseif k == "query_time" then  return t.now + t.offset + t.delay
            elseif k == "shortest_ttd" then t[k] = Hekili:GetLowestTTD()
            elseif k == "time" then
                -- Calculate time in combat.
                local c, fs = t.combat, t.false_start
                if c == 0 and fs == 0 then return 0 end
                return t.query_time - max( c, fs )
            elseif type(k) == "string" and k:sub(1, 12) == "time_to_pct_" then
                local percent = tonumber( k:sub( 13 ) ) or 0
                return Hekili:GetGreatestTimeToPct( percent ) - ( t.offset + t.delay )

            -- Current ability in question, or selected ability to compare to ability in question.
            elseif k == "current_action" then return t.this_action
            elseif k == "modified" then t[k] = false
            elseif k == "selected_action" then return
            elseif k == "selection" then return t.selection_time < 60
            elseif k == "selection_time" then t[k] = 60
            elseif k == "this_action" then t[k] = "wait"
            elseif k == "this_list" then t[k] = "default"
            elseif k == "this_id" then t[k] = "Not Set:default:1"

            -- Calculated from real event data.
            elseif k == "aggro" then t[k] = ( UnitThreatSituation( "player" ) or 0 ) > 1
            elseif k == "boss" then
                t[k] = t.encounterID > 0 or UnitCanAttack( "player", "target" ) and ( UnitClassification( "target" ) == "worldboss" or UnitLevel( "target" ) == -1 )
            elseif k == "encounter" then t[k] = t.encounterID > 0
            elseif k == "group" then t[k] = t.group_members > 1
            elseif k == "group_members" then t[k] = max( 1, GetNumGroupMembers() )
            elseif k == "active_allies" then
                local n = GetNumGroupMembers()
                local maxUnits = IsInRaid() and 40 or 5
                local token = IsInRaid() and "raid" or "party"
                local count = 0

                for i = 1, maxUnits do
                    local unit = token .. i
                    if UnitExists( unit ) and not UnitIsDead( unit ) and UnitIsConnected( unit ) and UnitInRange( unit ) then
                        count = count + 1
                        if n == 1 then break end
                        n = n - 1
                    end
                end

                t[k] = max( 1, count )

            elseif k == "level" then t[k] = UnitEffectiveLevel("player") or MAX_PLAYER_LEVEL
            elseif k == "mounted" or k == "is_mounted" then t[k] = IsMounted()
            elseif k == "moving" then t[k] = ( GetUnitSpeed("player") > 0 )
            elseif k == "raid" then t[k] = IsInRaid() and t.group_members > 5
            elseif k == "solo" then t[k] = t.group_members == 1
            elseif k == "tanking" then t[k] = t.role.tank and t.aggro
            elseif k == "group_health" then
                local health, maxHealth, absorbs = UnitHealth( "player" ), UnitHealthMax( "player" ), UnitGetTotalAbsorbs( "player" )
                local count = 1

                for i = 2, 5 do
                    local token = "party" .. i
                    if UnitExists( token ) and not UnitIsDeadOrGhost( token )then
                        count = count + 1

                        health = health + UnitHealth( token )
                        maxHealth = maxHealth + UnitHealthMax( token )
                        absorbs = absorbs + UnitGetTotalAbsorbs( token )
                    end
                end

                local effHealthPct = 100 * ( health + absorbs ) / maxHealth
                t[k] = effHealthPct

            elseif k == "at_risk" then
                local health, maxHealth, absorbs = UnitHealth( "player" ), UnitHealthMax( "player" ), UnitGetTotalAbsorbs( "player" )
                local count = ( 100 * ( health + absorbs ) / maxHealth ) < 70 and 1 or 0

                for i = 2, 5 do
                    local token = "party" .. i
                    if UnitExists( token ) and not UnitIsDeadOrGhost( token )then
                        health = health + UnitHealth( token )
                        maxHealth = maxHealth + UnitHealthMax( token )
                        absorbs = absorbs + UnitGetTotalAbsorbs( token )

                        if ( 100 * ( health + absorbs ) / maxHealth ) < 70 and 1 or 0 then count = count + 1 end
                    end
                end

                t[k] = count

            -- Enemy counting.
            elseif k == "active_enemies" then
                local n = t.true_active_enemies
                if t.min_targets > 0 then n = max( t.min_targets, n ) end
                if t.max_targets > 0 then n = min( t.max_targets, n ) end
                t[k] = max( 1, n or 1 )

            elseif k == "cycle_enemies" then
                if not t.settings.cycle or t.active_enemies == 1 then return 1 end

                local targets = t.true_active_enemies
                local timeframe = t.delay + t.offset

                local minTTD = timeframe + min( t.cycleInfo.minTTD or 10, t.settings.cycle_min )
                local maxTTD = t.cycleInfo.maxTTD

                if not t.HasCyclingDebuff() and t.settings.cycleDebuff then
                    -- See if the specialization has a default aura to use for cycling (i.e., Unholy using Festering Wound).
                    minTTD = max( minTTD, t.debuff[ t.settings.cycleDebuff ].duration / 2 )
                end

                targets = targets - Hekili:GetNumTTDsBefore( minTTD )

                if maxTTD then
                    targets = targets - Hekili:GetNumTTDsAfter( maxTTD )
                end

                -- So the reason we're stuck here is that we may need "cycle_enemies" when we *aren't* cycling targets.
                -- I.e., we would cycle Festering Strike (festering_wound) but if we've already dotted our valid adds, we'd hit Death and Decay.

                -- testing: don't force minimum targets for cycling purposes, since they may objectively not exist.
                -- if t.min_targets > 0 then targets = max( t.min_targets, targets ) end

                -- cap cycle_targets if forced into single-target model.
                if t.max_targets > 0 then targets = min( t.max_targets, targets ) end

                -- if Hekili.ActiveDebug then Hekili:Debug( "cycle min:%.2f, max:%.2f, ae:%d, before:%d, after:%d, cycle_enemies:%d", minTTD or 0, maxTTD or 0, t.active_enemies, minTTD and Hekili:GetNumTTDsBefore( minTTD ) or 0, maxTTD and Hekili:GetNumTTDsAfter( maxTTD ) or 0, max( 1, targets ) ) end

                return max( 1, targets )

            elseif k == "desired_targets" then t[k] = 1
            elseif k == "max_targets" then t[k] = 0
            elseif k == "min_targets" then t[k] = 0
            elseif k == "my_enemies" then
                local n = t.true_my_enemies
                if t.min_targets > 0 then n = max( t.min_targets, n ) end
                if t.max_targets > 0 then n = min( t.max_targets, n ) end
                t[k] = max( 1, n )

            elseif k == "stationary_enemies" then
                local n = t.true_stationary_enemies
                if t.min_targets > 0 then n = max( t.min_targets, n ) end
                if t.max_targets > 0 then n = min( t.max_targets, n ) end
                t[k] = max( 1, n )

            elseif k == "true_active_enemies" or k == "true_stationary_enemies" then
                local n, s = ns.getNumberTargets()
                t.true_active_enemies = max( 1, n or 1 )
                t.true_stationary_enemies = max( 1, s or 1 )

            elseif k == "true_my_enemies" then t[k] = max( 1, ns.numTargets() )

            -- Stats (that refer to state.stat, generally)
            elseif k == "crit" or k == "spell_crit" or k == "attack_crit" then return ( t.stat.crit / 100 )
            elseif k == "haste" or k == "spell_haste" then return ( 1 / ( 1 + t.stat.spell_haste ) )
            elseif k == "raw_haste_pct" then return t.stat.haste
            elseif k == "melee_haste" or k == "attack_haste" then return ( 1 / ( 1 + t.stat.melee_haste ) )
            elseif k == "mastery_value" then return t.stat.mastery_value

            -- ???; assume it's "durable" and don't expect it to get reset.
            elseif k == "active" then return false
            elseif k == "cast_target" then return "nobody"
            elseif k == "miss_react" then return false
            elseif k == "ranged" then return false
            elseif k == "wait_for_gcd" then return false

            -- Specialization State Expressions
            elseif k == "effective_combo_points" then return 0
            elseif k == "prowling" then return t.buff.prowl.up or ( t.buff.cat_form.up and t.buff.shadowform.up )

            -- Durable stuff; should get manually set if/when needed.
            -- Don't reset.

            -- Channels.
            elseif k == "channel" then
                if t.buff.casting.down or t.buff.casting.v3 ~= 1 then return nil end
                local chan = class.abilities[ t.buff.casting.v1 ]

                if chan then return chan.key end
                return tostring( t.buff.casting.v1 )

            elseif k == "channel_breakable" then t[k] = false
            elseif k == "channel_remains" then return t.buff.casting.up and t.buff.casting.v3 == 1 and t.buff.casting.remains or 0
            elseif k == "channeling" then return t.buff.casting.up and t.buff.casting.v3 == 1

            -- Workaround for Evoker resource (Essence)
            elseif k == "essence" then return t.bfa_essence

            -- Real incoming damage/healing information.
            elseif type(k) == "string" and k:sub(1, 15) == "incoming_damage" then
                local remains = k:sub(17)
                local time = remains:match("^(%d+)[m]?s")

                if not time then
                    return 0
                    -- Error("ERR: " .. remains )
                end

                time = tonumber( time )

                if time > 100 then
                    t[k] = ns.damageInLast( time / 1000 )
                else
                    t[k] = ns.damageInLast( min( 15, time ) )
                end

                return t[ k ]

            elseif type(k) == "string" and k:sub(1, 17) == "incoming_physical" then
                local remains = k:sub(18, 24) == "_damage" and k:sub(26) or k:sub(19)
                local time = remains:match("^(%d+)[m]?s")

                if not time then
                    return 0
                    -- Error("ERR: " .. remains )
                end

                time = tonumber( time )

                if time > 100 then
                    t[k] = ns.damageInLast( time / 1000, true )
                else
                    t[k] = ns.damageInLast( min( 15, time ), true )
                end

                return t[ k ]

            elseif type(k) == "string" and k:sub(1, 14) == "incoming_magic" then
                local remains = k:sub(15, 21) == "_damage" and k:sub(23) or k:sub(16)
                local time = remains:match("^(%d+)[m]?s")

                if not time then
                    return 0
                    -- Error("ERR: " .. remains )
                end

                time = tonumber( time )

                if time > 100 then
                    t[k] = ns.damageInLast( time / 1000, false )
                else
                    t[k] = ns.damageInLast( min( 15, time ), false )
                end

                return t[ k ]

            elseif type(k) == "string" and k:sub(1, 13) == "incoming_heal" then
                local remains = k:sub(15)
                local time = remains:match("^(%d+)[m]?s")

                if not time then
                    return 0
                    -- Error("ERR: " .. remains)
                end

                time = tonumber( time )

                if time > 100 then
                    t[ k ] = ns.healingInLast( time / 1000 )
                else
                    t[ k ] = ns.healingInLast( min( 15, time ) )
                end

                return t[ k ]

            end

            -- If we successfully calculated during the above, return it.
            local value = rawget( t, k )
            if value ~= nil then return value end

            -- The next block are values that reference an ability.
            local action = t.this_action
            local model = t.action[ action ]
            local ability = class.abilities[ action ]
            local cooldown = t.cooldown[ action ]

            -- Empowerment oddity.
            if k == "duration" and action and model and model.empowered then return max( t.gcd.max, model.cast_time ) end -- Still ugh.

            if k == "action_cooldown" then return ability and ability.cooldown or 0
            elseif k == "cast_delay" then return 0
            elseif k == "cast_regen" then
                local resType = class.primaryResource
                local amount, resource

                if ability then
                    amount, resource = ability.spend

                    if not resource and ability.spendType then
                        resource = ability.spendType
                    end
                end

                resType = resource or resType

                local regen = t[ resType ].regen
                if regen == 0.001 then regen = 0 end

                if not amount then return t.gcd.execute * regen end
                return ( max( t.gcd.execute, ability.cast or 0 ) * regen ) - ( ability.spend or 0 )

            elseif k == "cast_time" then return ability and ability.cast or 0
            elseif k == "charges" then return cooldown.charges
            elseif k == "charges_fractional" then return cooldown.charges_fractional
            elseif k == "charges_max" or k == "max_charges" then return ability and ability.charges or 1
            elseif k == "cooldown_duration" then return cooldown.duration
            elseif k == "cooldown_react" or k == "cooldown_up" then return cooldown.remains == 0
            elseif k == "cooldown_remains" then return cooldown.remains
            elseif k == "cost" then
                if not ability then return 0 end

                local c = ability.cost
                if c then return c end

                c = ability.spend
                if c and c > 0 and c < 1 then
                    c = c * t[ ability.spendType or class.primaryResource ].modmax
                end
                return c or 0

            elseif k == "crit_pct_current" or k == "crit_percent_current" then return ability and ability.critical or t.stat.crit

            elseif k == "execute_remains" then
                -- TODO:  Check out if this is functioning as expected.
                -- Should buff.casting already suffice for a cast?  A queued cast should already trigger a casting buff.
                return ( t:IsCasting( action ) and max( t:QueuedCastRemains( action ), t.gcd.remains ) ) or ( t.prev[1][ action ] and t.gcd.remains ) or 0

            elseif k == "execute_time" then return max( t.gcd.execute, ability and ability.cast or 0 )
            elseif k == "executing" then return t:IsCasting( action ) or ( t.prev[ 1 ][ action ] and t.gcd.remains > 0 )
            elseif k == "full_recharge_time" or k == "time_to_max_charges" then return cooldown.full_recharge_time
            elseif k == "hardcast" then return false -- will set to true if/when a spell is hardcast.
            elseif k == "in_flight" or k == "in_flight_to_target" then return model and model.in_flight or false
            elseif k == "in_flight_remains" then return model and model.in_flight_remains or 0
            elseif k == "in_range" then return model.in_range
            elseif k == "recharge" then return cooldown.recharge
            elseif k == "recharge_time" then return cooldown.recharge_time
            elseif k == "travel_time" then
                local f, v = ability.flightTime or 0, ability.velocity or 0
                if f > 0 then return f end
                if v == 0 then return 0 end
                return t.target.maxR / v

            end

            --[[ None of the Abilities/Actions/Cooldowns block currently sets a value, so there's no need for this.
            value = rawget( t, k )
            if value ~= nil then return value end ]]

            local aura_name = ability and ability.aura or t.this_action
            local aura = aura_name and class.auras[ aura_name ]
            local app = aura and ( ( t.buff[ aura_name ].up and t.buff[ aura_name ] ) or ( t.debuff[ aura_name ].up and t.debuff[ aura_name ] ) or t.buff[ aura_name ] )

            if not app then
                if ability and ability.startsCombat then
                    app = unknown_debuff
                else
                    app = unknown_buff
                end
            end

            if aura and class.knownAuraAttributes[ k ] then
                -- Buffs, debuffs...

                value = app and app[ k ]
                if value ~= nil then return value end

                -- This uses the default aura duration (if available) to keep pandemic windows accurate.
                -- local duration = aura and aura.duration or 15

                -- This allows for overridden tick times on a particular application of an aura (i.e., Exsanguinate).
                -- local tick_time = app and app.tick_time or ( aura and aura.tick_time ) or ( 3 * t.haste )

                if k == "up" or k == "ticking" then return false
                elseif k == "down" then return true
                elseif k == "duration" then return ( aura.duration or 15 )
                elseif k == "refreshable" then return true
                    -- When cycling targets, we want to consider that there may be a valid other target.
                    -- if t.isCyclingTargets( action, aura_name ) then return true end
                    -- if app then return app.remains < 0.3 * ( aura.duration or 15 ) end
                    -- return true

                elseif k == "remains" then return 0
                    -- if app then return app.remains end
                    -- return 0
                elseif k == "tick_time" then return aura.tick_time or ( 3 * state.haste )
                elseif k == "tick_time_remains" then return 0
                elseif k == "ticks" then return ( aura.duration or 15 ) / ( aura.tick_time or ( 3 * haste ) )
                elseif k == "ticks_remain" then return 0
                elseif k == "time_to_refresh" then return 0 end
            end

            -- Fallback to action cast time; found in Augmentation APL.
            if k == "duration" then return ability and ability.cast or 0 end

            -- Check if this is a resource table pre-init.
            for key in pairs( class.resources ) do
                if key == k then
                    return nil
                end
            end

            if t:GetVariableIDs( k ) then return t.variable[ k ] end
            if t.settings[ k ] ~= nil then return t.settings[ k ] end
            if t.toggle[ k ]   ~= nil then return t.toggle[ k ] end

            if k ~= "scriptID" and not ( logged_state_errors[ t.scriptID ] and logged_state_errors[ t.scriptID ][ k ] ) then
                Hekili:Error( "Unknown key '" .. k .. "' in emulated environment for [ " .. t.scriptID .. " : " .. t.this_action .. " ].\n\n" .. debugstack() )
                logged_state_errors[ t.script ] = logged_state_errors[ t.script ] or {}
                logged_state_errors[ t.script ][ k ] = true
            end
        end,
        __newindex = function( t, k, v )
            if v ~= nil and autoReset[ k ] then
                Mark( t, k )
            end
            rawset( t, k, v )
        end
    }

    SuperMark( state, autoReset )
end
ns.metatables.mt_state = mt_state


local mt_spec = {
    __index = function(t, k)
        return false
    end
}
ns.metatables.mt_spec = mt_spec


local mt_stat = {
    __index = function(t, k)
        if k == "strength" then
            t[k] = UnitStat( "player", 1 )

        elseif k == "agility" then
            t[k] = UnitStat( "player", 2 )

        elseif k == "stamina" then
            t[k] = UnitStat( "player", 3 )

        elseif k == "intellect" then
            t[k] = UnitStat( "player", 4 )

        elseif k == "spirit" then
            t[k] = UnitStat( "player", 5 )

        elseif k == "health" then
            return state.health and state.health.current or 50000

        elseif k == "maximum_health" then
            return state.health and state.health.max or 50000

        elseif k == "health_pct" then
            return state.health and state.health.pct or 100

        elseif k == "mana" then
            return state.mana and state.mana.current or 0

        elseif k == "maximum_mana" then
            return state.mana and state.mana.max or 0

        elseif k == "rage" then
            return state.rage and state.rage.current or 0

        elseif k == "maximum_rage" then
            return state.rage and state.rage.max or 0

        elseif k == "energy" then
            return state.energy and state.energy.current or 0

        elseif k == "maximum_energy" then
            return state.energy and state.energy.max or 0

        elseif k == "focus" then
            return state.focus and state.focus.current or 0

        elseif k == "maximum_focus" then
            return state.focus and state.focus.max or 0

        elseif k == "runic" or k == "runic_power" then
            return state.runic_power and state.runic_power.current or 0

        elseif k == "maximum_runic" or k == "maximum_runic_power" then
            return state.runic_power and state.runic_power.max or 0

        elseif k == "spell_power" then
            t[k] = GetSpellBonusDamage(7)

        elseif k == "mp5" then
            t[k] = state.mana and state.mana.regen or 0

        elseif k == "attack_power" then
            t[k] = UnitAttackPower("player") + UnitWeaponAttackPower("player")

        elseif k == "crit_rating" then
            t[k] = GetCombatRating(CR_CRIT_MELEE)

        elseif k == "haste_rating" then
            t[k] = GetCombatRating(CR_HASTE_MELEE)

        elseif k == "weapon_dps" or k == "weapon_offhand_dps" then
            local low, high, offlow, offhigh = UnitDamage( "player" )
            t.weapon_dps = 0.5 * ( low + high )
            t.weapon_offhand_dps = 0.5 * ( low + high )

        elseif k == "weapon_speed" or k == "weapon_offhand_speed" then
            local main, off = UnitAttackSpeed( "player" )
            t.weapon_speed = main or 0
            t.weapon_offhand_speed = off or 0

        elseif k == "armor" or k == "bonus_armor" then
            local _, eff, _, bonus = UnitArmor( "player" )
            t.armor = eff or 0
            t.bonus_armor = bonus or 0

        elseif k == "resilience_rating" then
            t[k] = GetCombatRating(CR_CRIT_TAKEN_SPELL)

        elseif k == "mastery_rating" then
            t[k] = GetCombatRating(CR_MASTERY)

        elseif k == "mastery_value" then
            t[k] = GetMasteryEffect() / 100

        elseif k == "versatility_atk_rating" then
            t[k] = GetCombatRating(CR_VERSATILITY_DAMAGE_DONE)

        elseif k == "versatility_atk_mod" then
            t[k] = GetCombatRatingBonus(CR_VERSATILITY_DAMAGE_DONE) / 100

        elseif k == "versatility_def_rating" then
            t[k] = GetCombatRating(CR_VERSATILITY_DAMAGE_TAKEN)

        elseif k == "versatility_def_mod" then
            t[k] = GetCombatRatingBonus(CR_VERSATILITY_DAMAGE_TAKEN) / 100

        elseif k == "mod_haste_pct" then
            t[k] = 0

        elseif k == "spell_haste" then
            t[k] = ( UnitSpellHaste( "player" ) + ( t.mod_haste_pct or 0 ) ) / 100

        elseif k == "melee_haste" then
            t[k] = ( GetMeleeHaste() + ( t.mod_haste_pct or 0 ) ) / 100

        elseif k == "haste" then
            t[k] = t.spell_haste or t.melee_haste

        elseif k == "mod_crit_pct" then
            t[k] = 0

        elseif k == "crit" then
            t[k] = ( max( GetCritChance(), GetSpellCritChance( "player" ), GetRangedCritChance() ) + ( t.mod_crit_pct or 0 ) )

        end

        return rawget( t, k )
    end,
    __newindex = function( t, k, v )
        if v == nil then return end
        Mark( t, k )
        rawset( t, k, v )
    end
}
ns.metatables.mt_stat = mt_stat

-- Table of pet data.
local mt_default_pet, mt_pets
do
    local autoReset = {
        expires = 1,
        summonTime = 1,
        id = 1,
        spec = 1,
    }

    -- Table of default handlers for specific pets/totems.
    mt_default_pet = {
        __index = function( t, k )
            if k == "expires" then
                local totemIcon = rawget( t, "icon" )

                if totemIcon then
                    -- This is actually a totem; check them.
                    for i = 1, 5 do
                        local present, name, start, duration, icon, _, spellID = GetTotemInfo( i )
                        if duration == 0 then duration = 3600 end

                        local ability = class.abilities[ spellID ]
                        if present and ( icon == totemIcon or abilitty and t.key == ability.key ) then
                            t.expires = start + duration
                            return t.expires
                        end
                    end

                    t.expires = 0
                    return t.expires
                end

                local petSpell = rawget( t, "spell" )
                petSpell = petSpell and state.action[ petSpell ]

                if petSpell then
                    -- We have to track by time since cast.
                    local lastCast = petSpell.lastCast
                    local duration = t.duration

                    if type( duration ) == 'function' then duration = duration() end
                    local expires = lastCast + duration

                    if expires > state.query_time then
                        t.expires = expires
                        return expires
                    end
                end

                local petGUID = UnitGUID( "pet" )
                petGUID = petGUID and tonumber( petGUID:match( "%-(%d+)%-[0-9A-F]+$" ) )

                local id = t.id
                if petGUID and id == petGUID and UnitHealth( "pet" ) > 0 then
                    t.expires = state.query_time + 3600
                else
                    t.expires = 0
                end
                return t.expires

            elseif k == "remains" then
                return max( 0, t.expires - ( state.query_time ) )

            elseif k == "up" or k == "active" or k == "alive" or k == "exists" then
                return ( t.expires >= ( state.query_time ) )

            elseif k == "down" then
                return ( t.expires < ( state.query_time ) )

            elseif k == "id" then
                local id = t.model and t.model.id
                if type( id ) == "function" then id = id() end
                return id

            elseif k == "spec" then
                return t.exists and GetSpecialization( false, true )

            elseif k == "key" then
                for pet, v in pairs( state.pet ) do
                    if type( v ) == "table" and t == v then
                        t.key = pet
                    end
                    return pet
                end

            end
        end,
        __newindex = function( t, k, v )
            if v == nil then return end
            if autoReset[ k ] then Mark( t, k ) end
            rawset( t, k, v )
        end
    }
    ns.metatables.mt_default_pet = mt_default_pet

    local petAutoReset = setmetatable( {
        real_pet = 1,
        up = 1,
        exists = 1,
        active = 1,
        alive = 1,
        dead = 1,
        health_pct = 1,
        health_percent = 1,
    }, {
        __index = function( t, k, v )
            if v == nil then return end
            if type( v ) == "boolean" then Mark( t, k ) end
            rawset( t, k, v )
        end
    } )

    mt_pets = {
        __index = function( t, k )
            local guid = UnitGUID( "pet" )

            if rawget( t, "__last_pet_guid" ) ~= guid then
                rawset( t, "__last_pet_guid", guid )
                rawset( t, "real_pet", nil )

                for alias, petData in pairs( class.pets ) do
                    local token = petData.token or alias
                    local entry = rawget( t, token )
                    if entry and type( entry ) == "table" then
                        t.expires = 0
                    end
                end
            end

            if not rawget( t, "real_pet" ) then
                local key
                local petID = UnitGUID( "pet" )

                if petID then
                    petID = tonumber( petID:match( "%-(%d+)%-[0-9A-F]+$" ) )
                    local model, token
                    -- Try direct alias match first
                    model = class.pets[ petID ]
                    if model then
                        token = model.token or petID
                    else
                        -- Fall back to checking .id from each registered pet
                        for k, v in pairs( class.pets ) do
                            local id = type( v.id ) == "function" and v.id() or v.id
                            if id == petID then
                                model = v
                                token = v.token or k
                                break
                            end
                        end
                    end

                    if model then
                        key = token
                        local spell = model.spell
                        local ability = spell and class.abilities[ spell ]
                        local lastCast = ability and ability.lastCast or 0
                        local duration = model.duration and ( type( model.duration ) == "function" and model.duration() or model.duration ) or 3600

                        if lastCast > 0 and duration < 3600 then
                            summonPet( key, lastCast + duration - state.now )
                        else
                            summonPet( key )
                        end
                    end

                end

                t.real_pet = key or rawget( t, "real_pet" ) or "fake_pet"
                if key == nil then
                    if Hekili.ActiveDebug then
                        Hekili:Debug( "Failed to resolve pet ID %s to any registered pet.", tostring( petID ) )
                    end
                end
            end

            if k == "up" or k == "exists" or k == "active" then
                for k, v in pairs( t ) do
                    if type(v) == "table" then
                        if v.expires > state.query_time then return true end
                    end
                end
                return UnitExists( "pet" ) and ( not UnitIsDead( "pet" ) )

            elseif k == "alive" then
                return UnitExists( "pet" ) and not UnitIsDead( "pet" ) and UnitHealth( "pet" ) > 0

            elseif k == "dead" then
                return UnitExists( "pet" ) and UnitIsDead( "pet" )

            elseif k == "health_pct" or k == "health_percent" then
                if t.alive then return 100 * UnitHealth( "pet" ) / UnitHealthMax( "pet" ) end
                return 100

            end

            local model = class.pets[ k ]

            if model then
                t[ k ] = {
                    name = k,
                    spell = model.spell,
                    duration = model.duration,
                    expires = nil,
                    spec = model.spec,
                    model = model
                }

                if model.spec then
                    t[ model.spec ] = t[ k ]
                end

                return t[ k ]
            end

            local ttm = class.totems[ k ]

            if ttm then
                t[ k ] = {
                    key = k,
                    icon = ttm
                }

                return t[ k ]
            end

            return t.fake_pet
        end,

        __newindex = function(t, k, v)
            if type(v) == "table" then
                if not v.key then v.key = k end
                rawset( t, k, setmetatable( v, mt_default_pet ) )
                return
            end
            if v == nil then return end
            if petAutoReset[ k ] then Mark( t, k ) end
            rawset( t, k, v )
        end

    }
    ns.metatables.mt_pets = mt_pets
end


-- TODO: This may require revision, since other code might change buffs w/o changing stance.
local mt_stances = {
    __index = function( t, k )
        if not class.stances[ k ] or not GetShapeshiftForm() then return false
        elseif GetShapeshiftForm() < 1 then return false
        elseif not select( 5, GetShapeshiftFormInfo( GetShapeshiftForm() ) ) == class.stances[k] then return false end
        rawset(t, k, select( 5, GetShapeshiftFormInfo( GetShapeshiftForm() ) ) == class.stances[k] )
        return t [ k]
    end,
    __newindex = function( t, k, v )
        if v == nil then return end
        Mark( t, k )
        rawset( t, k, v )
    end,
}
ns.metatables.mt_stances = mt_stances

-- Table of supported toggles (via keybinding).
-- Need to add a commandline interface for these, but for some reason, I keep neglecting that.
local mt_toggle = {
    __index = function( t, k )
        if not k then return end

        local db = Hekili.DB
        if not db then return end

        local toggle = db.profile.toggles[ k ]

        if k == "cooldowns" and ( toggle.override and state.buff.bloodlust.up or toggle.infusion and state.buff.power_infusion.up ) then return true end
        if k == "essences" and toggle.override and state.toggle.cooldowns then return true end
        if k == "potions" and toggle.override and state.toggle.cooldowns then return true end

        if toggle then return toggle.value end
    end
}
ns.metatables.mt_toggle = mt_toggle


local mt_settings = {
    __index = function( t, k )
        local ability = state.this_action and class.abilities[ state.this_action ]

        if rawget( t, "spec" ) then
            if t.spec.settings[ k ] ~= nil then return t.spec.settings[ k ] end
            if t.spec[ k ] ~= nil then return t.spec[ k ] end

            if ability then
                if ability.item and t.spec.items[ state.this_action ] ~= nil then return t.spec.items[ state.this_action ][ k ]
                elseif not ability.item and t.spec.abilities[ state.this_action ] ~= nil then return t.spec.abilities[ state.this_action ][ k ] end
            end
        end

        return
    end
}
ns.metatables.mt_settings = mt_settings


-- Table of target attributes. Needs to be expanded.
-- Needs review.
local mt_target
do
    local autoReset = setmetatable( {
        distance = 1,
        in_range = 1,
        maxR = 1,
        minR = 1,
        outside = 1,
        range = 1,
        within = 1,

        adds = 1,
        casting = 1,
        class = 1,
        exists = 1,
        health_current = 1,
        health_max = 1,
        health_pct = 1,
        is_add = 1,
        is_boss = 1,
        is_dead = 1,
        is_demon = 1,
        is_friendly = 1,
        is_in_party = 1,
        is_in_raid = 1,
        is_player = 1,
        is_dummy = 1,
        is_undead = 1,
        level = 1,
        moving = 1,
        real_ttd = 1,
        time_to_die = 1,
        unit = 1,
        npcid = 1,
        token = 1
    }, {
        __index = function( t, k )
            local expr, value = k:match( "^(.+)_?(%d+)$" )
            if expr and value then
                if expr == "time_to_pct" then return 1 end
                if expr == "within" then return 1 end
            end

            local value2
            expr, value, value2 = k:match( "^(.+)(%d+)to(%d+)$" )
            if expr and value and value2 then
                if expr == "range" then return 1 end
            end
        end
    } )

    local PvpDummies = ns.PvpDummies
    local TargetDummies = ns.TargetDummies

    mt_target = {
        __index = function( t, k )
            if k == "distance" then t[k] = UnitCanAttack( "player", "target" ) and ( ( t.minR + t.maxR ) / 2 ) or 7.5
            elseif k == "in_range" then return t.distance <= 8
            elseif k == "minR" or k == "maxR" then
                local minR, maxR = RC:GetRange( "target" )
                t.minR = minR or 5
                t.maxR = maxR or 10

            elseif k:sub(1, 7) == "outside" then
                local minR = k:match( "^outside([0-9.]+)$" )
                if not minR then return false end
                return ( t.minR > tonumber( minR ) )

            elseif k:sub(1, 6) == "within" then
                local maxR = k:match( "^within([0-9.]+)$" )
                if not maxR then return false end
                return ( t.maxR <= tonumber( maxR ) )

            elseif k:sub(1, 5) == "range" then
                local minR, maxR = k:match( "^range([0-9.]+)to([0-9.]+)$" )
                if not minR or not maxR then return false end
                return ( t.minR >= tonumber( minR ) and t.maxR <= tonumber( maxR ) )

            elseif k == "adds" then t[k] = state.active_enemies - 1
            elseif k == "casting" then return state.debuff.casting.up and not state.debuff.casting.v2
            elseif k == "class" then
                if not t.exists then t[k] = "virtual"
                elseif not t.is_player then t[k] = "npc"
                else
                    local c = UnitClassBase( "target" )
                    if c then t[k] = strlower( c )
                    else t[k] = "unknown" end
                end
            elseif k == "exists" then t[k] = UnitExists( "target" )
            elseif k == "health_current" then return t.health.current
            elseif k == "health_max" then return t.health.max
            elseif k == "health_pct" or k == "health_percent" then return t.health.percent
            elseif k == "has_vehicle_ui" then t[k] = UnitInVehicle( "target" )
            elseif k == "is_add" then t[k] = not t.is_boss
            elseif k == "is_boss" then
                if UnitExists( "boss1" ) and UnitIsUnit( "target", "boss1" ) or
                    UnitExists( "boss2" ) and UnitIsUnit( "target", "boss2" ) or
                    UnitExists( "boss3" ) and UnitIsUnit( "target", "boss3" ) or
                    UnitExists( "boss4" ) and UnitIsUnit( "target", "boss4" ) or
                    UnitExists( "boss5" ) and UnitIsUnit( "target", "boss5" ) then t[k] = true
                else
                    t[k] = ( UnitCanAttack( "player", "target" ) and ( UnitClassification( "target" ) == "worldboss" or UnitLevel( "target" ) == -1 ) )
                end
            elseif k == "is_dead" then t[k] = UnitIsDeadOrGhost("target")
            elseif k == "is_demon" then t[k] = UnitCreatureType( "target" ) == PET_TYPE_DEMON
            elseif k == "is_friendly" then t[k] = UnitCanAssist( "player", "target" )
            elseif k == "is_in_party" then t[k] = UnitInParty( "target" )
            elseif k == "is_in_raid" then t[k] = UnitInRaid( "target" ) ~= nil
            elseif k == "is_player" then
                local isPlayer = UnitIsPlayer( "target" )
                if not isPlayer then isPlayer = PvpDummies[ t.npcid ] end
                t[k] = isPlayer or false -- Enables proper treatment of Absolute Corruption and similar modified-in-PvP effects.

            elseif k == "is_dummy" then t[k] = ( TargetDummies[ t.npcid ] ~= nil )
            elseif k == "is_undead" then t[k] = UnitCreatureType( "target" ) == BATTLE_PET_NAME_4

            elseif k == "level" then t[k] = UnitLevel( "target" ) or UnitLevel( "player" ) or MAX_PLAYER_LEVEL
            elseif k == "moving" then t[k] = GetUnitSpeed( "target" ) > 0
            elseif k == "real_ttd" or k == "true_ttd" then
                t[k] = Hekili:GetTTD( "target" )

            elseif k == "time_to_die" then
                if state.IsCycling() then return state.raid_event.adds.remains end

                local ttd = t.real_ttd
                if ttd == 3600 then t[k] = ttd
                else return max( 1, t.real_ttd - ( state.offset + state.delay ) ) end

            elseif k:sub(1, 12) == "time_to_pct_" then
                local percent = tonumber( k:sub( 13 ) ) or 0
                return Hekili:GetTimeToPct( "target", percent ) - ( state.offset + state.delay )

            elseif k == "unit" then
                if state.args.cycle_target == 1 then return UnitGUID( "target" ) .. "c" or "cycle"
                elseif state.args.target then return ( UnitGUID( "target" ) .. '+' .. state.args.target ) or "unknown" end
                return UnitGUID( "target" ) or "unknown"

            elseif k == "npcid" then
                if UnitExists( "target" ) then
                    local id = UnitGUID( "target" )
                    id = id and id:match( "(%d+)-%x-$" )
                    id = id and tonumber( id )

                    return id or -1
                end

                return -1

            elseif k == "token" then
                for _, token in ipairs( { "focus", "mouseover", "target", "targettarget" } ) do
                    if UnitExists( token ) and
                        not UnitIsDeadOrGhost( token ) and
                        UnitCanAttack( "player", token ) then
                        t[k] = token
                        break
                    end
                end
            end

            return rawget( t, k )
        end,
        __newindex = function( t, k, v )
            if v == nil then return end
            if autoReset[ k ] then Mark( t, k ) end
            rawset( t, k, v )
        end
    }
    ns.metatables.mt_target = mt_target
end


local mt_target_health
do
    local autoReset = {
        actual = 1,
        current = 1,
        max = 1,
        pct = 1,
        percent = 1,
    }

    mt_target_health = {
        __index = function(t, k)
            if k == "current" or k == "actual" then
                return UnitCanAttack("player", "target") and not UnitIsDead( "target" ) and UnitHealth("target") or 100000

            elseif k == "max" then
                return UnitCanAttack("player", "target") and not UnitIsDead( "target" ) and UnitHealthMax("target") or 100000

            elseif k == "pct" or k == "percent" then
                return t.max ~= 0 and ( 100 * t.current / t.max ) or 100
            end
        end,
        __newindex = function( t, k, v )
            if v == nil then return end
            if autoReset[ k ] then Mark( t, k ) end
            rawset( t, k, v )
        end
    }
    ns.metatables.mt_target_health = mt_target_health
end



local mt_consumable = {
    __index = function( t, k )
        return class.potion == k
    end
}
setmetatable( state.consumable, mt_consumable )


local mt_default_cooldown
local mt_cooldowns

do
    local autoReset = {
        charge = 1,
        duration = 1,
        expires = 1,
        remains = 1,
        next_charge = 1,
        recharge = 1,
        recharge_began = 1,
        true_duration = 1,
        true_expires = 1,
        true_remains = 1,
    }

    local function GetUnpackedSpellCooldown( spellID )
        local spellCooldownInfo = C_Spell.GetSpellCooldown( spellID );
        if spellCooldownInfo then
            return spellCooldownInfo.startTime, spellCooldownInfo.duration, spellCooldownInfo.isEnabled, spellCooldownInfo.modRate;
        end
    end

    -- Table of default handlers for specific ability cooldowns.
    mt_default_cooldown = {
        __index = function( t, k )
            local ability = rawget( t, "key" ) and class.abilities[ t.key ] or class.abilities.null_cooldown

            local GetCooldown = GetUnpackedSpellCooldown
            local profile = Hekili.DB.profile
            local id = ability.id

            if ability then
                if rawget( ability, "meta" ) and ability.meta[ k ] then
                    return ability.meta[ k ]( t )
                end

                if ability.funcs.cooldown_special then
                    GetCooldown = ability.funcs.cooldown_special
                    id = 999999
                elseif ability.item then
                    GetCooldown = GetItemCooldown
                    id = ability.itemCd or ability.item
                end
            end

            -- We don't cache IDs in cooldown tables to avoid issues with ID and specialization changes.
            if k == "id" then
                return id
            end

            local noFeignCD = rawget( profile.specs, state.spec.id )
            noFeignCD = noFeignCD and noFeignCD.noFeignedCooldown

            local raw = ( state.display ~= "Primary" and state.display ~= "AOE" ) or ( profile.toggles.cooldowns.value and profile.toggles.cooldowns.separate and noFeignCD )

            if k:sub(1, 5) == "true_" then
                k = k:sub(6)
                raw = true
            end

            if k == "duration" or k == "expires" or k == "charge" or k == "recharge_began" then
                local start, duration = 0, 0

                if id > 0 then
                    local _, modRate = nil, 1
                    start, duration, _, modRate = GetCooldown( id )

                    local lossStart, lossDuration = GetSpellLossOfControlCooldown( id )
                    if lossStart and lossDuration and lossStart + lossDuration > start + duration then
                        start = lossStart
                        duration = lossDuration
                    end

                    --[[
                        Void Emissary: Voidbinding
                        If Voidbinding has 10s remaining, and the affected spell shows 15s remaining on its cooldown, then
                        when 10s passes, the spell CD will jump from 5s to 6.5s.
                    ]]--

                    if state.debuff.voidbinding.up and modRate and modRate ~= 1 then
                        local extraTime = start + duration - state.query_time - state.debuff.voidbinding.remains
                        if extraTime > 0 then
                            if Hekili.ActiveDebug then Hekili:Debug( "Extending '%s' remaining cooldown by %.2f because the cooldown exceeds Voidbinding's remaining time by %.2f.", ability.key, ( extraTime * 0.3 ), extraTime ) end
                            duration = duration + ( extraTime * 0.3 )
                        end
                    end
                end

                if t.key ~= "global_cooldown" then
                    local gcd = state.cooldown.global_cooldown
                    local gcdStart, gcdDuration = gcd.expires - gcd.duration, gcd.duration
                    if gcdStart == start and gcdDuration == duration then start, duration = 0, 0 end
                end

                local true_duration = duration

                if t.key == "ascendance" and state.buff.ascendance.up then
                    start = state.buff.ascendance.expires - class.auras.ascendance.duration
                    duration = class.abilities[ "ascendance" ].cooldown

                elseif t.key == "potion" then
                    local potion = class.abilities.potion.item
                    if state.toggle.potions and potion then
                        start, duration = GetItemCooldown( potion )

                    else
                        start = state.now
                        duration = 0

                    end

                end

                t.duration = max( duration or 0, ability.cooldown or 0, ability.recharge or 0 )
                t.expires = start > 0 and ( start + duration ) or 0
                t.true_duration = true_duration
                t.true_expires = start > 0 and ( start + true_duration ) or 0

                if ability.charges and ability.charges > 1 then
                    local charges, _
                    charges, _, start, duration = GetSpellCharges( id )

                    -- TODO: Determine if any charged abilities matter enough to solve for Voidbinding CDR.

                    if not duration then duration = max( ability.recharge or 0, ability.cooldown or 0 ) end

                    t.true_duration = duration
                    duration = max( duration, ability.recharge or 0 )

                    t.charge = charges or 1
                    t.duration = duration
                    t.recharge = duration

                    t.recharge_began = start or t.expires - t.duration

                else
                    t.charge = t.expires < state.query_time and 1 or 0
                    t.recharge_began = t.expires - t.duration
                end

                return t[ k ]

            elseif k == "charges" then
                if not raw then
                    if ( state:IsDisabled( t.key ) or ability.disabled ) then return 0 end
                    if not state:IsKnown( t.key ) then return ability.charges or 1 end
                end

                if t.charge == ( ability.charges or 1 ) then return t.charge end
                return floor( t[ raw and "true_charges_fractional" or "charges_fractional" ] )

            elseif k == "charges_max" or k == "max_charges" then
                return ability.charges or 1

            elseif k == "recharge" then
                return ability.recharge or ability.cooldown or 0

            elseif k == "time_to_max_charges" or k == "full_recharge_time" then
                if not raw then
                    -- if state.buff.empowering.up and state.buff.empowering.spell ~= t.key then return 0 end
                    if ( state:IsDisabled( t.key ) or ability.disabled ) then return ( ability.charges or 1 ) * t.duration end
                    if not state:IsKnown( t.key ) then return 0 end
                end

                return ( ( ability.charges or 1 ) - t.true_charges_fractional ) * max( ability.recharge or ability.cooldown, t.true_duration )

            elseif k == "remains" then
                if t.key == "global_cooldown" then
                    return max( 0, t.expires - state.query_time )
                end

                -- If the ability is toggled off in the profile, we may want to fake its CD.
                -- Revisit this if I add base_cooldown to the ability tables.
                if not raw then
                    if ( state:IsDisabled( t.key ) or ability.disabled ) then return ability.cooldown end
                    if not state:IsKnown( t.key ) then return 0 end
                end

                local bonus_cdr = 0
                bonus_cdr = ns.callHook( "cooldown_recovery", bonus_cdr ) or bonus_cdr

                return max( 0, t.expires - state.query_time - bonus_cdr )

            elseif k == "charges_fractional" then
                if not raw then
                    if state:IsDisabled( t.key ) or ability.disabled then return 0 end
                    if not state:IsKnown( t.key ) then return ability.charges or 1 end
                end

                if not ability.charges or t.charge == ability.charges then return t.charge or 1 end
                return min( ability.charges, t.charge + max( 0, ( state.query_time - t.recharge_began ) / ( ability.recharge or ability.cooldown ) ) )

            elseif k == "recharge_time" then
                if not ability.charges then return t.duration or 0 end
                return t.recharge

            elseif k == "ready" or k == "up" then
                return ( ability.cooldown_ready == nil or ability.cooldown_ready ) and t.remains == 0

            -- Hunters
            elseif k == "remains_guess" or k == "remains_expected" then
                local remains, duration = t.remains, t.duration
                if remains == 0 or remains == duration then return remains end

                local lastCast = state.action[ t.key ].lastCast or 0
                if lastCast == 0 then return remains end

                local reduction = ( state.query_time - lastCast ) / ( duration - remains )
                return remains * reduction

            elseif k == "duration_guess" or k == "duration_expected" then
                local expected = class.abilities[ t.key ].cooldown_estimate
                if expected then return expected end

                local remains, duration = t.remains, t.duration
                if remains == 0 or remains == duration then return duration end

                -- not actually the same as simc here, which tracks when CDs charge.
                local lastCast = state.action[ t.key ].lastCast or 0
                if lastCast == 0 then return duration end

                local reduction = ( state.query_time - lastCast ) / ( duration - remains )
                return duration * reduction

            end

            Error( "UNK: cooldown." .. t.key .. "." .. k )
            return

        end,
        __newindex = function( t, k, v )
            if v == nil then return end
            if autoReset[ k ] then Mark( t, k ) end
            rawset( t, k, v )
        end
    }
    ns.metatables.mt_default_cooldown = mt_default_cooldown


    -- Table for gathering cooldown information. Some abilities with odd behavior are getting embedded here.
    -- Probably need a better system that I can keep in the class modules.
    -- Needs review.
    mt_cooldowns = {
        -- The action doesn't exist in our table so check the real game state, -- and copy it so we don't have to use the API next time.
        __index = function( t, k )
            local entry = class.abilities[ k ]

            if not entry then
                -- Check if this is one of SimC's lovely itemname_spellid type tokens.
                local shortkey = k:match( "^([a-z0-9_])_%d+$" )

                if shortkey and class.abilities[ shortkey ] then
                    class.abilities[ k ] = class.abilities[ shortkey ]
                    entry = class.abilities[ k ]
                else
                    if not rawget( t, "null_cooldown" ) then t.null_cooldown = { key = "null_cooldown", duration = 1 } end
                    return t.null_cooldown
                end
            end

            if k ~= entry.key then
                t[ k ] = t[ entry.key ]
                return t[ k ]
            end

            t[ k ] = { key = k }
            return t[ k ]
        end,
        __newindex = function(t, k, v)
            rawset( t, k, setmetatable( v, mt_default_cooldown ) )
        end
    }
    ns.metatables.mt_cooldowns = mt_cooldowns
end

local mt_dot = {
    __index = function( t, k )
        local a = class.auras[ k ]
        local dotType = a and a.dot
        if not dotType then return state.debuff[ k ] end

        if dotType == "both" then
            if state.buff[ k ].up then return state.buff[ k ] end
            return state.debuff[ k ]
        end

        return state[ dotType ][ k ]
    end,
}
ns.metatables.mt_dot = mt_dot


local one_sec_gcd = {
    DEMONHUNTER = true,
    MONK = true,
    ROGUE = true,
}

local mt_gcd = {
    __index = function( t, k )
        if k == "execute" then
            local ability = state.this_action and class.abilities[ state.this_action ]

            -- We can specify this for any ability, if we want.
            if ability and ability.gcdTime then return ability.gcdTime end

            local gcd = ( state.this_action == "wait" and "spell" ) or ( ability and ability.gcd or "spell" )

            if gcd == "off" then return 0 end
            if gcd == "totem" then return 1 end

            if one_sec_gcd[ class.file ] or class.file == "DRUID" and UnitPowerType( "player" ) == Enum.PowerType.Energy then
                return state.buff.adrenaline_rush.up and max( 0.75, state.haste ) or 1
            end

            return max( 1.5 * state.haste, state.buff.voidform.up and 0.67 or 0.75 )

        elseif k == "remains" then
            return state.cooldown.global_cooldown.remains

        elseif k == "expires" then
            return state.cooldown.global_cooldown.expires

        elseif k == "max" or k == "duration" then
            if one_sec_gcd[ class.file ] or class.file == "DRUID" and UnitPowerType( "player" ) == Enum.PowerType.Energy then
                return state.buff.adrenaline_rush.up and max( 0.75, state.haste ) or 1
            end

            return max( 1.5 * state.haste, state.buff.voidform.up and 0.67 or 0.75 )

        elseif k == "lastStart" then
            return 0

        end

        return
    end
}
ns.metatables.mt_gcd = mt_gcd
setmetatable( state.gcd, mt_gcd )


local mt_prev_lookup = {
    __index = function( t, k )
        local idx = t.index
        local preds, prev

        if     t.meta == "castsAll" then preds, prev = state.predictions   , state.prev
        elseif t.meta == "castsOn"  then preds, prev = state.predictionsOn , state.prev_gcd
        elseif t.meta == "castsOff" then preds, prev = state.predictionsOff, state.prev_off_gcd end

        if k == "spell" then
            -- Return the actual spell for the slot, for lookups.
            if preds[ idx ] then return preds[ idx ] end

            if state.player.queued_ability then
                if idx == #preds + 1 then return state.player.queued_ability end
                return prev.history[ idx - #preds + 1 ]
            end

            if idx == 1 and prev.override then
                return prev.override
            end

            return prev.history[ idx - #preds ]
        end

        if preds[ idx ] then return preds[ idx ] == k end

        if state.player.queued_ability then
            if idx == #preds + 1 then
                return state.player.queued_ability == k
            end
            return prev.history[ idx - #preds + 1 ] == k
        end

        if idx == 1 and prev.override then
            return prev.override == k
        end

        return prev.history[ idx - #preds ] == k
    end,
}

local prev_lookup = setmetatable( {
    index = 1,
    meta = 'castsAll'
}, mt_prev_lookup )


local mt_prev

do
    local autoReset = {
        last = 1,
        override = 1,
    }

    mt_prev = {
        __index = function( t, k )
            if type( k ) == "number" then
                prev_lookup.meta = t.meta -- Which data to use? castsAll, castsOn (GCD), castsOff (offGCD)?
                prev_lookup.index = k
                return prev_lookup
            elseif k == "last" then
                if t.meta == "castAll" then t.last = state.player.lastcast
                elseif t.meta == "castsOn" then t.last = state.player.lastgcd
                elseif t.meta == "castsOff" then t.last = state.player.lastoffgcd end
                return rawget( t, "last" ) or "none"
            end

            if k == t.last then
                return true
            end

            return false
        end,
        __newindex = function( t, k, v )
            if v == nil then return end
            if autoReset[ k ] then Mark( t, k ) end
            rawset( t, k, v )
        end
    }
    ns.metatables.mt_prev = mt_prev
end


local resource_meta_functions = {}

function state:AddResourceMetaFunction( name, f )
    resource_meta_functions[ name ] = f
end


function state:CombinedResourceRegen( t )
    local regen = t.regen
    if regen == 0.001 then regen = 0 end

    local model = t.regenModel
    if not model then return regen end

    for _, source in pairs( model ) do
        local value = type( source.value ) == "function" and source.value() or source.value
        local interval = type( source.interval ) == "function" and source.interval() or source.interval

        local aura = source.aura

        if aura then
            aura = source.debuff and state.debuff[ aura ] or state.buff[ aura ]

            if aura.up then
                regen = regen + ( value / interval )
            end
        end
    end

    return regen
end

function state:TimeToResource( t, amount )
    -- Handle invalid or overflow cases.
    if not amount or amount > t.max then return 3600 end
    if t.current >= amount then return 0 end

    local regen, lastTick, pad = t.regen, nil, 0
    local queryTime = state.query_time
    local deficit = amount - t.current

    --[[ Handle rune-specific logic.
    if t.resource == "runes" then
        local totalTime = 0

        -- Check `expiry` to calculate when the required runes will regenerate.
        for i = 1, deficit do
            if i > #t.expiry then break end
            local nextRune = t.expiry[ i ] or 0
            if nextRune > queryTime then
                totalTime = max( totalTime, nextRune - queryTime )
            end
        end
        return totalTime
    end ]]

    -- Handle tick-based resources (e.g., energy, focus).
    if t.resource == "energy" or t.resource == "focus" then
        -- Round any result requiring ticks to the next tick.
        lastTick = t.last_tick
    end

    if regen == 0.001 then regen = 0 end

    -- Handle forecasted resources with slices (e.g., energy/focus models).
    if t.forecast and t.fcount > 0 then
        if t.times[ amount ] then return t.times[ amount ] - queryTime end

        if regen == 0 then
            for i = 1, t.fcount do
                local v = t.forecast[ i ]
                if v.v >= amount then
                    t.times[ amount ] = v.t
                    return max( 0, t.times[ amount ] - queryTime )
                end
            end
            t.times[ amount ] = queryTime + 3600
            return max( 0, t.times[ amount ] - queryTime )
        end

        for i = 1, t.fcount do
            local slice = t.forecast[ i ]
            local after = t.forecast[ i + 1 ]

            if slice.v >= amount then
                t.times[ amount ] = slice.t

                if lastTick then
                    pad = ( slice.t - lastTick ) % 0.1
                    pad = 0.1 - pad
                end

                return max( 0, pad + t.times[ amount ] - queryTime )

            elseif after and after.v >= amount then
                -- Check if regeneration fills the deficit before the next slice.
                local time_diff = after.t - slice.t
                local deficit = amount - slice.v
                local regen_time = deficit / regen

                if lastTick then
                    pad = ( slice.t - lastTick ) % 0.1
                    pad = 0.1 - pad
                end

                if regen_time < time_diff then
                    t.times[ amount ] = ( pad + slice.t + regen_time )
                else
                    t.times[ amount ] = after.t
                end
                return max( 0, t.times[ amount ] - queryTime )
            end
        end

        t.times[ amount ] = queryTime + 3600
        return max( 0, t.times[ amount ] - queryTime )
    end

    -- Fallback for non-modeled resources.
    if lastTick and regen > 0 then
        pad = ( queryTime - lastTick ) % 0.1
        pad = 0.1 - pad
    end

    if regen <= 0 then return 3600 end
    return max( 0, pad + ( ( amount - t.current ) / regen ) )
end


local mt_resource = {
    __index = function( t, k )

        local meta = t.meta[ k ]
        if meta ~= nil then
            local result = meta( t )

            if result ~= nil then
                return result
            end
        end

        if k == "pct" or k == "percent" then
            return 100 * ( t.current / t.max )

        elseif k == "deficit_pct" or k == "deficit_percent" then
            return 100 - t.pct

        elseif k == "current" then
            local regen = t.regen
            if regen == 0.001 then regen = 0 end

            -- If this is a modeled resource, use our lookup system.
            if t.forecast and t.fcount > 0 then
                local q = state.query_time
                local index, slice

                if t.values[ q ] then return t.values[ q ] end

                for i = 1, t.fcount do
                    local v = t.forecast[ i ]
                    if v.t <= q and v.v ~= nil then
                        index = i
                        slice = v
                    else
                        break
                    end
                end

                -- We have a slice.
                if index and slice and slice.v then
                    t.values[ q ] = max( 0, min( t.max, slice.v + ( ( state.query_time - slice.t ) * regen ) ) )
                    return t.values[ q ]
                end
            end

            -- No forecast.
            if regen ~= 0 then
                return max( 0, min( t.max, t.actual + ( regen * state.delay ) ) )
            end

            return t.actual

        elseif k == "deficit" or k == "base_deficit" then
            return t.max - t.current

        elseif k == "max_nonproc" then
            return t.max -- need to accommodate buffs that increase mana, etc.

        elseif k == "time_to_max" or k == "base_time_to_max" then
            return state:TimeToResource( t, t.max )

        elseif k == "time_to_max_combined" then
            if not state.spec.assassination then return t.time_to_max end

            -- Assassination, April 2021
            -- Using the same as time_to_max because our time_to_max uses modeled regen events...
            return state:TimeToResource( t, t.max )

        elseif k:sub(1, 16) == "time_to_deficit_" then
            local amount = k:sub(17)
            amount = tonumber(amount)

            if not amount then return 0 end

            return state:TimeToResource( t, t.max - amount )

        elseif k:sub(1, 8) == "time_to_" then
            local amount = k:sub(9)
            amount = tonumber(amount)

            if not amount then return 3600 end

            return state:TimeToResource( t, amount )

        elseif k == "regen" then
            return ( state.time > 0 and t.active_regen or t.inactive_regen ) or 0

        elseif k == "regen_combined" then
            local regen = t.regen
            if regen == 0.001 then regen = 0 end
            return max( regen, state:CombinedResourceRegen( t ) )

        elseif k == "modmax" then
            return t.max

        elseif k == "model" then
            return

        elseif k == 'onAdvance' then
            return

        end

    end
}
ns.metatables.mt_resource = mt_resource


local default_buff_values = {
    name = "no_name",
    count = 0,
    lastCount = 0,
    lastApplied = 0,
    expires = 0,
    applied = 0,
    -- duration = 15,
    caster = "nobody",
    timeMod = 1,
    v1 = 0,
    v2 = 0,
    v3 = 0,

    last_application = 0,
    last_expiry = 0,

    unit = "player"
}


function state:AddBuffMetaFunction( aura, key, func )
    local a = class.auras[ aura ]
    if not a then return end

    if not a.meta then a.meta = {} end
    a.meta[ key ] = setfenv( func, self )
end



local requiresLookup = {
    name = true,
    count = true,
    lastCount = true,
    lastApplied = true,
    expires = true,
    applied = true,
    caster = true,
    id = true,
    timeMod = true,
    v1 = true,
    v2 = true,
    v3 = true,

    last_application = true,
    last_expiry = true,

    unit = true
}


-- Table of default handlers for auras (buffs, debuffs).
-- Aliases let a single buff name refer to any of multiple buffs.
local mt_alias_buff
local mt_default_buff
local mt_buffs

do
    local autoReset = setmetatable( {
        applied = 1,
        caster = 1,
        count = 1,
        -- duration = 1,
        expires = 1,
        last_application = 1,
        last_expiry = 1,
        lastApplied = 1,
        lastCount = 1,
        name = 1,
        timeMod = 1,
        unit = 1,
        v1 = 1,
        v2 = 1,
        v3 = 1,
    }, {
        __index = function( t, k )
            if class.knownAuraAttributes[ k ] ~= nil then return 1 end
        end,
    } )

    -- Developed mainly for RtB; it will also report "stack" or "count" as the sum of stacks of multiple buffs.
    mt_alias_buff = {
        __index = function( t, k )
            local aura = class.auras[ t.key ]
            local type = aura.aliasType or "buff"

            if aura.meta and aura.meta[ k ] then return aura.meta[ k ]() end

            if k == "max_stack" then return aura.max_stack or 1
            elseif k == "count" or k == "stack" or k == "stacks" then
                local n = 0

                if type == "any" then
                    for i, child in ipairs( aura.alias ) do
                        if state.buff[ child ].up then n = n + max( 1, state.buff[ child ].stack ) end
                        if state.debuff[ child ].up then n = n + max( 1, state.debuff[ child ].stack ) end
                    end
                else
                    for i, child in ipairs( aura.alias ) do
                        if state[ type ][ child ].up then n = n + max( 1, state[ type ][ child ].stack ) end
                    end
                end

                return n

            end

            local alias
            local mode = aura.aliasMode or "first"

            for i, v in ipairs( aura.alias ) do
                local child

                if type == "any" then
                    child = state.debuff[ v ].up and state.debuff[ v ] or state.buff[ v ]
                else
                    child = state[ type ][ v ]
                end

                if not alias and mode == "first" and child.up then return child[ k ] end

                if child.up then
                    if mode == "shortest" and ( not alias or child.remains < alias.remains ) then alias = child
                    elseif mode == "longest" and ( not alias or child.remains > alias.remains ) then alias = child end
                end
            end

            if type == "any" then type = "buff" end

            if alias then return alias[ k ]
            else return state[ type ][ aura.alias[1] ][ k ] end
        end,
        __newindex = function( t, k, v )
            if v == nil then return end
            class.knownAuraAttributes[ k ] = true
            if autoReset[ k ] then Mark( t, k ) end
            rawset( t, k, v )
        end
    }
    ns.metatables.mt_alias_buff = mt_alias_buff

    mt_default_buff = {
        mtID = "default_buff",

        __index = function( t, k )
            local aura = class.auras[ t.key ]

            if aura and aura.hidden then
                -- Hidden auras might be detectable with FindPlayerAuraByID.
                local name, _, count, _, duration, expires, caster, _, _, spellID, _, _, _, _, timeMod, v1, v2, v3 = FindPlayerAuraByID( aura.id )

                if name then
                    local buff = auras.player.buff[ t.key ] or {}

                    buff.key = t.key
                    buff.id = spellID
                    buff.name = name
                    buff.count = count > 0 and count or 1
                    -- buff.duration = duration
                    buff.expires = expires
                    buff.caster = caster
                    buff.applied = expires - duration
                    buff.caster = caster
                    buff.timeMod = timeMod
                    buff.v1 = v1
                    buff.v2 = v2
                    buff.v3 = v3

                    buff.last_application = buff.last_application or 0
                    buff.last_expiry      = buff.last_expiry or 0

                    buff.unit = "player"

                    auras.player.buff[ t.key ] = buff
                end
            end

            if aura and rawget( aura, "meta" ) and aura.meta[ k ] then
                if not t.metastack[ k ] then
                    t.metastack[ k ] = true
                    local value = aura.meta[ k ]( t, "buff" )
                    t.metastack[ k ] = nil
                    if value ~= nil then return value end
                end
            end

            if requiresLookup[ k ] then
                if aura and aura.generate then
                    for attr, a_val in pairs( default_buff_values ) do
                        t[ attr ] = rawget( t, attr ) or rawget( aura, attr ) or a_val
                    end

                    aura.generate( t, "buff" )
                    t.id = aura and aura.id or t.key

                    return rawget( t, k )
                end

                local real = auras.player.buff[ t.key ] or auras.target.buff[ t.key ]

                if real then
                    t.name = real.name
                    t.count = real.count
                    t.lastCount = real.lastCount or 0
                    t.lastApplied = real.lastApplied or 0
                    -- t.duration = real.duration
                    t.expires = real.expires
                    t.applied = real.applied
                    t.caster = real.caster
                    t.id = real.id or class.auras[ t.key ].id
                    t.timeMod = real.timeMod
                    t.v1 = real.v1
                    t.v2 = real.v2
                    t.v3 = real.v3

                    t.last_application = real.last_application or 0
                    t.last_expiry = real.last_expiry or 0

                    t.unit = real.unit
                else
                    local meta = aura and rawget( aura, "meta" )

                    for attr, a_val in pairs( default_buff_values ) do
                        if not meta or not meta[ attr ] then
                            t[ attr ] = aura and aura[ attr ] or a_val
                        end
                    end

                    t.id = rawget( t, id ) or ( aura and aura.id ) or t.key
                end

                return rawget( t, k )

            elseif k == "up" or k == "ticking" then
                return t.remains > 0

            elseif k == "down" then
                return t.remains == 0

            elseif k == "remains" then
                -- if state.IsCycling( t.key ) then return 0 end
                return t.applied <= state.query_time and max( 0, t.expires - state.query_time ) or 0

            elseif k == "duration" then
                return aura.duration or ( t.remains > 0 and t.expires - t.applied or 0 ) or 15

            elseif k == "refreshable" then
                local tr = t.remains
                return tr == 0 or tr < ( 0.3 * ( aura.duration or 30 ) )

            elseif k == "time_to_refresh" then
                local remains = t.remains
                if remains == 0 then return 0 end
                return max( 0, 0.01 + remains - ( 0.3 * ( aura.duration or 30 ) ) )

            elseif k == "cooldown_remains" then
                return state.cooldown[ t.key ] and state.cooldown[ t.key ].remains or 0

            elseif k == "react" or k == "stack" or k == "stacks" then
                if t.remains == 0 then return 0 end
                return t.count

            elseif k == "max_stack" or k == "max_stacks" then
                return max( t.stacks, aura and aura.max_stack or 1 )

            elseif k == "mine" then
                return t.caster == "player"

            elseif k == "v1" then
                return 0

            elseif k == "v2" then
                return 0

            elseif k == "v3" then
                return 0

            elseif k == "value" then
                if t.remains == 0 then return 0 end
                return t.v1 or 0

            elseif k == "stack_value" then
                if t.remains == 0 then return 0 end
                return t.value * t.stack

            elseif k == "stack_pct" then
                if t.remains == 0 then return 0 end
                return ( 100 * t.stack / t.max_stack )

            elseif k == "at_max_stacks" then
                return t.stack_pct >= 100

            elseif k == "ticks" then
                if t.remains == 0 then return 0 end
                -- if t.applied <= state.query_time and state.query_time < t.expires then return t.duration / t.tick_time - t.ticks_remain end
                -- if t.up then return 1 + ( ( class.auras[ t.key ].duration or ( 30 * state.haste ) ) / ( class.auras[ t.key ].tick_time or ( 3 * t.haste ) ) ) - t.ticks_remain end
                return t.duration / t.tick_time - t.ticks_remain

            elseif k == "tick_time" then
                if t.remains == 0 then return 0 end
                return aura and aura.tick_time or ( 3 * state.haste ) -- Default tick time will be 3 because why not?

            elseif k == "ticks_remain" then
                if t.remains == 0 then return 0 end
                return t.remains / t.tick_time

            elseif k == "tick_time_remains" then
                -- Match SimC behavior:  They return the max of a 64 bit integer ... 3600 is sufficient.
                if t.remains == 0 then return 3600 end
                if t.applied <= state.query_time and state.query_time < t.expires then
                    if not aura.tick_time then return t.remains end
                    return aura.tick_time - ( ( state.query_time - t.applied ) % aura.tick_time )
                end
                return 0

            elseif k == "last_trigger" then
                if state.combat > 0 then return max( 0, t.last_application - state.combat ) end
                return 0

            elseif k == "last_expire" then
                if state.combat > 0 and t.last_expiry < state.query_time then return max( 0, t.last_expiry - state.combat ) end
                return 0

            else
                if class.auras[ t.key ] and class.auras[ t.key ][ k ] ~= nil then
                    return class.auras[ t.key ][ k ]
                end
            end

            Error( "UNK: buff." .. t.key .. "." .. k .. "\n\n" .. debugstack() )

        end,

        __newindex = function( t, k, v )
            if v == nil then return end

            class.knownAuraAttributes[ k ] = true
            -- Prevent a fixed value from being entered if it is calculated by a meta function.
            -- 20220828:  This was bugged, and fixing it might cause new bugs.  Watch carefully.
            --[[ if t.meta and t.meta[ k ] then
                return
            end ]]

            if autoReset[ k ] then Mark( t, k ) end
            rawset( t, k, v )
        end
    }
    ns.metatables.mt_default_buff = mt_default_buff

    unknown_buff = setmetatable( {
        key = "unknown_buff",
        name = "No Name",
        count = 0,
        lastCount = 0,
        lastApplied = 0,
        -- duration = 30,
        expires = 0,
        applied = 0,
        caster = "nobody",
        timeMod = 1,
        v1 = 0,
        v2 = 0,
        v3 = 0,
        unit = "player"
    }, mt_default_buff )




    -- This will currently accept any key and make an honest effort to find the buff on the player.
    -- Unfortunately, that means a buff.dog_farts.up check will actually get a return value.

    local buffs_warned = {}

    -- Fullscan definitely needs revamping, but it works for now.
    mt_buffs = {
        -- The aura doesn't exist in our table so check the real game state, -- and copy it so we don't have to use the API next time.
        __index = function( t, k )
            if k == "__scanned" then
                return false
            end

            local aura = class.auras[ k ]

            if not aura then
                if Hekili.PLAYER_ENTERING_WORLD and not buffs_warned[ k ] then
                    Hekili:Error( "Unknown buff in [" .. state.scriptID .. "]: " .. k .. "\n\n" .. debugstack() )
                    buffs_warned[ k ] = true
                end
                return unknown_buff
            end

            if k ~= aura.key then
                t[ aura.key ] = rawget( t, aura.key ) or {
                    key = aura.key,
                    name = aura.name
                }
                t[ k ] = t[ aura.key ]
            else
                t[k] = {
                    key = aura.key,
                    name = aura.name
                }
            end

            if aura.generate then
                for attr, a_val in pairs( default_buff_values ) do
                    t[ k ][ attr ] = rawget( t[ k ], attr ) or a_val
                end
                aura.generate( t[ k ], "buff" )
                return t[ k ]
            end

            local real = auras.player.buff[ k ] or auras.target.buff[ k ]

            local buff = t[k]

            if real then
                buff.name = real.name
                buff.count = real.count
                buff.lastCount = real.lastCount or 0
                buff.lastApplied = real.lastApplied or 0
                -- buff.duration = real.duration
                buff.expires = real.expires
                buff.applied = real.applied
                buff.caster = real.caster
                buff.id = real.id
                buff.timeMod = real.timeMod
                buff.v1 = real.v1
                buff.v2 = real.v2
                buff.v3 = real.v3

                buff.unit = real.unit
            end

            return t[ k ]

        end,

        __newindex = function( t, k, v )
            local aura = class.auras[ k ]

            if aura then
                aura.used = true
                if aura.meta then rawset( v, "metastack", {} ) end
                if aura.alias then
                    rawset( t, k, setmetatable( v, mt_alias_buff ) )
                    return
                end
            end

            rawset( t, k, setmetatable( v, mt_default_buff ) )
        end
    }
    ns.metatables.mt_buffs = mt_buffs
end


local mt_default_talent = {
    __index = function( t, k )
        if k == "i_enabled" or k == "rank" then return t.enabled and 1 or 0 end
        return k
    end,
}
ns.metatables.mt_default_talent = mt_default_talent


local null_talent = setmetatable( {
    enabled = false,
}, mt_default_talent )
ns.metatables.null_talent = null_talent

local logged_talent_errors = {}

local mt_talents = {
    __index = function( t, k )
        if class.talents[ k ] == nil and not logged_talent_errors[ k ] and #class.specs > 1 then
            Hekili:Error( "Unknown talent in [ " .. state.scriptID .. " ]: " .. k .. "\n\n" .. debugstack() )
            logged_talent_errors[ k ] = true
        end
        return ( null_talent )
    end,

    __newindex = function( t, k, v )
        if type( v ) == "table" then
            rawset( t, k, setmetatable( v, mt_default_talent ) )
            return
        end
        rawset( t, k, v )
    end,
}
ns.metatables.mt_talents = mt_talents


local function IslandPvP()
    local _, instanceType, difficulty = GetInstanceInfo()
    return instanceType == "scenario" and difficulty == 45
end

local mt_default_pvptalent = {
    __index = function( t, k )
        local enlisted = state.bg or state.arena or state.buff.enlisted.up or IslandPvP()

        if k == "enabled" then return enlisted and rawget( t, "_enabled" ) or false
        elseif k == "_enabled" then return false
        elseif k == "i_enabled" or k == "rank" then return enlisted and rawget( t, "_enabled" ) and 1 or 0 end

        return k
    end,
}


local null_pvptalent = setmetatable( {
    _enabled = false
}, mt_default_pvptalent )


local mt_pvptalents = {
    __index = function( t, k )
        return null_pvptalent
    end,

    __newindex = function( t, k, v )
        rawset( t, k, setmetatable( v, mt_default_pvptalent ) )
    end,
}


do
    local mt_default_gen_trait = {
        __index = function( t, k )
            if k == "enabled" or k == "minor" or k == "equipped" then
                return t.rank and t.rank > 0
            elseif k == "time_value" then
                local mod = t.mod

                if mod >= 1000 or mod <= -1000 then
                    return mod / 1000
                end

                return mod
            elseif k == "disabled" then
                return not t.rank or t.rank == 0
            end
        end
    }
    ns.metatables.mt_default_gen_trait = mt_default_gen_trait

    local mt_generic_traits = {
        __index = function( t, k )
            return t.no_trait
        end,

        __newindex = function( t, k, v )
            rawset( t, k, setmetatable( v, mt_default_gen_trait ) )
            return t[ k ]
        end
    }
    ns.metatables.mt_generic_traits = mt_generic_traits

    setmetatable( state.legendary, mt_generic_traits )
    state.legendary.no_trait = { rank = 0 }



    -- Azerite and Essences.
    local mt_default_trait = {
        __index = function( t, k )
            if not state.azerite.active then
                if k == "enabled" or k == "equipped" or k == "major" or k == "minor" then
                    return false
                elseif k == "disabled" then
                    return true
                end
                return 0
            end

            if k == "enabled" or k == "equipped" then
                return t.__rank and t.__rank > 0
            elseif k == "disabled" then
                return not t.__rank or t.__rank == 0
            elseif k == "rank" then
                return t.__rank or 0
            elseif k == "major" then
                return t.__major or false
            elseif k == "minor" then
                return t.__minor or false
            end
        end
    }


    local HEART_OF_AZEROTH_ITEM_ID = 158075

    local mt_artifact_traits = {
        __index = function( t, k )
            if k == "active" then
                local neck = GetInventoryItemID( "player", INVSLOT_NECK )
                if not neck or neck ~= HEART_OF_AZEROTH_ITEM_ID then
                    rawset( t, "active", false )
                    return false
                end

                local item = C_AzeriteItem.FindActiveAzeriteItem()
                rawset( t, "active", item and item:IsEquipmentSlot() and C_AzeriteItem.IsAzeriteItemEnabled( item ) )
                return t.active
            end

            return t.no_trait
        end,

        __newindex = function( t, k, v )
            if v ~= nil then rawset( t, k, setmetatable( v, mt_default_trait ) ) end
        end
    }

    ns.metatables.mt_artifact_traits = mt_artifact_traits

    setmetatable( state.azerite, mt_artifact_traits )
    state.azerite.no_trait = { rank = 0 }
    state.artifact = state.azerite

    setmetatable( state.essence, ns.metatables.mt_artifact_traits )
    state.essence.no_trait = { rank = 0, major = false, minor = false }

    -- DF: essence.X will be used for Evoker resources.
    -- The state metatable will redirect essence -> bfa_essence for non-Evokers.
    state.bfa_essence = state.essence
    rawset( state, "essence", nil )
end


do
    local db = scripts.DB

    -- Args table, make it nicer.
    setmetatable( state.args, {
        __index = function( t, k )
            -- No script selected.
            if not state.scriptID then return end

            local script = db[ state.scriptID ]

            -- No script by that name.
            if not script then return end

            -- Script has no modifiers.
            if not script.Modifiers then return end

            local mod = script.Modifiers[ k ]

            if mod then
                local s, val = pcall( mod )
                if s then return val end
            end
        end,
    } )
end


-- Table for counting active dots.
local mt_active_dot = {
    __index = function(t, k)
        local aura = class.auras[ k ]

        if aura then
            if rawget( t, aura.key ) then return t[ aura.key ] end
            local id = aura.id
            local count = ns.numDebuffs( id )

            if aura.copy then
                if type( aura.copy ) == "table" then
                    for _, v in ipairs( aura.copy ) do
                        if type(v) == "number" and v > 0 and v ~= id then

                            count = count + ns.numDebuffs( v )
                        end
                    end
                elseif type( aura.copy ) == "number" and aura.copy > 0 and aura.copy ~= id then
                    count = count + ns.numDebuffs( aura.copy )
                end
            end

            t[ k ] = count
            return t[ k ]
        else
            return 0

        end
    end,
    __newindex = function( t, k, v )
        if v == nil then return end
        Mark( t, k )
        rawset( t, k, v )
    end
}
ns.metatables.mt_active_dot = mt_active_dot


-- Table of default handlers for a totem. Under-implemented at the moment.
-- Needs review.
local mt_default_totem = {
    __index = function( t, k )
        if k == "expires" then
            local _, name, start, duration, icon, modRate, spellID = GetTotemInfo( t.totem )

            t.name = name
            t.expires = ( start or 0 ) + ( duration or 0 )

            return t[ k ]

        elseif k == "up" or k == "active" or k == "alive" then
            return ( t.expires > ( state.query_time ) )

        elseif k == "remains" then
            if t.expires > ( state.query_time ) then
                return ( t.expires - ( state.query_time ) )
            else
                return 0
            end

        end

        Error( "UNK: totem." .. name or "no_name" .. "." .. k )
    end,
    __newindex = function( t, k, v )
        if v == nil then return end
        Mark( t, k )
        rawset( t, k, v )
    end
}
Hekili.mt_default_totem = mt_default_totem


-- Table of totems. Currently Shaman-centric.
-- Needs review.
local mt_totem = {
    __index = function(t, k)
        if k == "fire" then
            local _, name, start, duration = GetTotemInfo(1)

            t[k] = {
            key = k, totem = 1, name = name, expires = (start + duration) or 0, }
            return t[k]

        elseif k == "earth" then
            local _, name, start, duration = GetTotemInfo(2)

            t[k] = {
            key = k, totem = 2, name = name, expires = (start + duration) or 0, }
            return t[k]

        elseif k == "water" then
            local _, name, start, duration = GetTotemInfo(3)

            t[k] = {
            key = k, totem = 3, name = name, expires = (start + duration) or 0, }
            return t[k]

        elseif k == "air" then
            local _, name, start, duration = GetTotemInfo(4)

            t[k] = {
            key = k, totem = 4, name = name, expires = (start + duration) or 0, }
            return t[k]
        end

        if state.pet[ k ] ~= nil then return state.pet[ k ] end

        Error( "UNK: totem." .. k )
    end,
    __newindex = function( t, k, v )
        rawset( t, k, setmetatable( v, mt_default_totem ) )
    end
}
ns.metatables.mt_totem = mt_totem


do
    local db = {}
    local cache = {}
    local pathState = {}

    state.varDB = db
    -- state.varCache = cache
    state.varPaths = pathState


    local entryPool = {}

    function state:RegisterVariable( key, scriptID, list, preconditions )
        db[ key ] = db[ key ] or {}
        local data = db[ key ]

        cache[ key ] = cache[ key ] or {}

        local fullPath = scriptID

        local entry = remove( entryPool ) or {
            mustPass = {},
            -- mustFail = {}
        }

        entry.id = scriptID
        entry.list = list

        if preconditions then
            for i, prereq in ipairs( preconditions ) do
                local script = prereq.script
                if script ~= 0 then
                    insert( entry.mustPass, script )
                    fullPath = fullPath .. "+" .. script
                end
            end
        end

        --[[ if preclusions then
            for i, block in ipairs( preclusions ) do
                local script = block.script
                if script ~= 0 then
                    insert( entry.mustFail, script )
                    fullPath = fullPath .. "-" .. script
                end
            end
        end ]]

        entry.fullPath = fullPath
        insert( data, entry )
    end


    function state:PurgeListVariables( list )
        for variable, data in pairs( db ) do
            for i = #data, 1, -1 do
                if data[ i ].list == list then
                    local item = remove( data, i )
                    wipe( item.mustPass )
                    insert( entryPool, item )
                    wipe( cache[ variable ] )
                end
            end
        end
    end


    function state:ResetVariables()
        for k, v in pairs( db ) do
            for i = #v, 1, -1 do
                local x = remove( v, i )
                wipe( x.mustPass )
                -- wipe( x.mustFail )
                insert( entryPool, x )
            end
            wipe( cache[ k ] )
            wipe( self.variable )
        end

        wipe( pathState )
    end


    function state:GetVariableIDs( key )
        return db[ key ]
    end


    local defaultValue = 0

    function state:SetDefaultVariable( value )
        if value == nil then value = 0 end
        defaultValue = value
    end


    state.variable = setmetatable( {
    }, {
        __index = function( t, var )
            local debug = Hekili.ActiveDebug

            if class.variables[ var ] then
                return class.variables[ var ]()
            end

            local now = state.query_time

            if Hekili.LoadingScripts then
                return defaultValue
            end

            if not db[ var ] then
                if debug then Hekili:Debug( "No such variable '%s'.", var ) end
                Hekili:Error( "Variable '%s' referenced in %s but is undefined.", var, state.scriptID )
                return defaultValue
            end

            local value

            local data = db[ var ]
            local parent = state.scriptID
            local which_mod = "value"

            -- If we're checking variable with no script loaded, don't bother.
            if not parent or parent == "NilScriptID" then return 0 end

            for i, entry in ipairs( data ) do
                local scriptID = entry.id
                local currPath = entry.fullPath .. ":" .. now

                value = rawget( state.variable, var )
                if value == nil then
                    state.variable[ var ] = defaultValue
                    value = defaultValue
                end

                -- Check the requirements/exclusions in the APL stack.
                if pathState[ currPath ] == nil then
                    pathState[ currPath ] = true

                    for r, prereq in ipairs( entry.mustPass ) do
                        state.scriptID = prereq
                        if not scripts:CheckScript( prereq ) then
                            pathState[ currPath ] = false
                            break
                        end
                    end

                    --[[ if pathState[ currPath ] then
                        for e, excl in ipairs( entry.mustFail ) do
                            state.scriptID = excl
                            if scripts:CheckScript( excl ) then
                                pathState[ currPath ] = false
                                break
                            end
                        end
                    end ]]
                end

                if pathState[ currPath ] then
                    local pathKey = currPath .. "-" .. i

                    if cache[ var ][ pathKey ] ~= nil then
                        value = cache[ var ][ pathKey ]
                        state.variable[ var ] = value

                    else
                        state.scriptID = scriptID
                        local op = state.args.op or "set"

                        local passed = scripts:CheckScript( scriptID )

                        local conditions = "(none)"
                        local valueString = "(none)"

                        --[[    add = "Add Value",
                                ceil
                                x default = "Set Default Value",
                                div = "Divide Value",
                                floor
                                max = "Maximum Value",
                                min = "Minimum Value",
                                mod = "Modulo Value",
                                mul = "Multiply Value",
                                pow = "Raise Value to X Power",
                                x reset = "Reset to Default",
                                x set = "Set Value",
                                x setif = "Set Value If...",
                                sub = "Subtract Value" ]]

                        if op == "set" or op == "setif" then
                            if passed then
                                local v1 = state.args.value
                                if v1 ~= nil then value = v1
                                else value = state.args.default end
                            else
                                local v2 = state.args.value_else
                                if v2 ~= nil then
                                    value = v2
                                    which_mod = "value_else"
                                end
                            end

                        elseif op == "reset" then
                            if passed then
                                local v = state.args.value
                                if v == nil then v = state.args.default end
                                if v == nil then v = 0 end
                                value = v
                            end

                        elseif passed then
                            -- Math Ops.
                            local currType = type( value )

                            if currType == "number" then
                                -- Operations on existing value.
                                if op == "floor" then
                                    value = floor( value )
                                elseif op == "ceil" then
                                    value = ceil( value )
                                else
                                    -- Operations with two values.
                                    local newVal = state.args.value
                                    local valType = type( newVal )

                                    if valType == "number" then
                                        if op == "add" then
                                            value = value + newVal
                                        elseif op == "div" then
                                            if newVal == 0 then value = 0
                                            else value = value / newVal end
                                        elseif op == "max" then
                                            value = max( value, newVal )
                                        elseif op == "min" then
                                            value = min( value, newVal )
                                        elseif op == "mod" then
                                            if newVal == 0 then value = 0
                                            else value = value % newVal end
                                        elseif op == "mul" then
                                            value = value * newVal
                                        elseif op == "pow" then
                                            value = value ^ newVal
                                        elseif op == "sub" then
                                            value = value - newVal
                                        end
                                    end
                                end
                            end
                        end

                        -- Cache the value in case it is an intermediate value (i.e., multiple calculation steps).
                        if debug then
                            conditions = format( "%s: %s", passed and "PASS" or "FAIL", scripts:GetConditionsAndValues( scriptID ) )
                            valueString = format( "%s: %s", value ~= nil and tostring( value ) or "nil", scripts:GetModifierValues( "value", scriptID ) )

                            Hekili:Debug( var .. " #" .. i .. " [" .. scriptID .. "]; conditions = " .. conditions .. "\n - value = " .. valueString )
                        end
                        state.variable[ var ] = value
                        cache[ var ][ pathKey ] = value
                    end
                end
            end

            -- Clear cache and clear the flag that we are checking this variable already.
            state.variable[ var ] = nil

            --[[ if debug then
                Hekili:Debug( "%s Result = %s.", var, tostring( value ) )
            end ]]

            state.scriptID = parent

            return value
        end
    } )
end

local tierSetAliasMap = {
    -- For specs with APLs that don't use the normal tier/season identifier that the majority uses
    thewarwithin_season_2 = "tww2",
    thewarwithin_season_3 = "tww3",
}

-- Table of set bonuses. Some string manipulation to honor the SimC syntax.
local mt_set_bonuses = {
    __index = function( t, k )
        if type( k ) == "number" then return 0 end

        -- Match specific set bonus effect checks, 2pc/4pc
          -- standard (tww2_2pc)
          -- hero tree (tww3_rider_of_the_apocalypse_2pc)
        local prefix, pieces = k:match( "^(.+)_([24])pc$" )
        if prefix and pieces then
            pieces = tonumber( pieces )

            -- Try as hero tree first (contains additional underscore for hero tree name)
            local heroSet = prefix:match( "^([%w_]+)_" .. state.hero_tree.current .. "$" )
            if heroSet then
                heroSet = tierSetAliasMap[ heroSet ] or heroSet
                return ( rawget( t, heroSet ) or 0 ) >= pieces and 1 or 0
            end

            -- Try as standard set bonus (no additional hero tree part)
            local standardSet = tierSetAliasMap[ prefix ] or prefix
            return ( rawget( t, standardSet ) or 0 ) >= pieces and 1 or 0
        end

        -- Check if this is a basic set name that should be aliased first
        if tierSetAliasMap[ k ] then return rawget( t,  tierSetAliasMap[ k ] ) or 0 end

        -- Match hero tree set name (tww3_rider_of_the_apocalypse)
        local heroSet = k:match( "^([%w_]-)_" .. state.hero_tree.current .. "$" )
        if heroSet then
            -- Hero tree set name
            heroSet = tierSetAliasMap[ heroSet ] or heroSet
            return rawget( t, heroSet ) or 0
        end

        -- t[ k ] is nil or this metafunction would not have fired.
        return 0
    end
}
ns.metatables.mt_set_bonuses = mt_set_bonuses


local mt_equipped = {
    __index = function(t, k)
        -- if not class.artifacts[ k ] and ( state.bg or state.arena ) then return false end
        return state.set_bonus[k] > 0 or state.legendary[k].rank > 0
    end
}
ns.metatables.mt_equipped = mt_equipped


-- Aliases let a single buff name refer to any of multiple buffs.
-- Developed mainly for RtB; it will also report "stack" or "count" as the sum of stacks of multiple buffs.
local mt_alias_debuff

do
    local autoReset = setmetatable( {
        applied = 1,
        caster = 1,
        count = 1,
        -- duration = 1,
        expires = 1,
        last_application = 1,
        last_expiry = 1,
        lastApplied = 1,
        lastCount = 1,
        name = 1,
        timeMod = 1,
        unit = 1,
        v1 = 1,
        v2 = 1,
        v3 = 1,
    }, {
        __index = function( t, k )
            if class.knownAuraAttributes[ k ] ~= nil then return 1 end
        end,
    } )

    mt_alias_debuff = {
        __index = function( t, k )
            local aura = class.auras[ t.key ]
            local type = aura.aliasType or "debuff"

            if aura.meta and aura.meta[ k ] then return aura.meta[ k ]() end

            if k == "count" or k == "stack" or k == "stacks" then
                local n = 0

                if type == "any" then
                    for i, child in ipairs( aura.alias ) do
                        if state.buff[ child ].up then n = n + max( 1, state.buff[ child ].stack ) end
                        if state.debuff[ child ].up then n = n + max( 1, state.debuff[ child ].stack ) end
                    end
                else
                    for i, child in ipairs( aura.alias ) do
                        if state[ type ][ child ].up then n = n + max( 1, state[ type ][ child ].stack ) end
                    end
                end

                return n
            end

            local alias
            local mode = aura.aliasMode or "first"

            for i, v in ipairs( aura.alias ) do
                local child

                if type == "any" then
                    child = state.buff[ v ].up and state.buff[ v ] or state.debuff[ v ]
                else
                    child = state.debuff[ v ]
                end

                if not alias and mode == "first" and child.up then return child[ k ] end

                if child.up then
                    if mode == "shortest" and ( not alias or child.remains < alias.remains ) then alias = child
                    elseif mode == "longest" and ( not alias or child.remains > alias.remains ) then alias = child end
                end
            end

            if type == "any" then type = "debuff" end

            if alias then return alias[ k ]
            else return state[ type ][ aura.alias[1] ][ k ] end
        end,
        __newindex = function( t, k, v )
            if v == nil then return end
            class.knownAuraAttributes[ k ] = true
            if autoReset[ k ] then Mark( t, k ) end
            rawset( t, k, v )
        end
    }
end


local default_debuff_values = {
    name = "no_name",
    count = 0,
    lastCount = 0,
    lastApplied = 0,
    expires = 0,
    applied = 0,
    -- duration = 15,
    caster = "nobody",
    timeMod = 1,
    v1 = 0,
    v2 = 0,
    v3 = 0,
    unit = "target"
}


local cycle_debuff = {
    name = "cycle",
    count = 0,
    lastCount = 0,
    lastApplied = 0,
    expires = 0,
    applied = 0,
    -- duration = 0,
    caster = "nobody",
    timeMod = 1,
    v1 = 0,
    v2 = 0,
    v3 = 0,
    unit = "target",

    down = true,
    i_up = 0,
    rank = 0,
    react = 0,
    refreshable = true,
    remains = 0,
    stack = 0,
    stack_pct = 0,
    tick_time_remains = 0,
    ticking = false,
    ticks = 0,
    ticks_remain = 0,
    time_to_refresh = 0,
    up = false,
}


-- Table of default handlers for debuffs.
-- Needs review.
local mt_default_debuff, mt_debuffs
local flagged_debuffs = {}

do
    local autoReset = {
        applied = 1,
        caster = 1,
        count = 1,
        -- duration = 1,
        expires = 1,
        lastApplied = 1,
        lastCount = 1,
        name = 1,
        timeMod = 1,
        unit = 1,
        v1 = 1,
        v2 = 1,
        v3 = 1,
        pmultiplier = 1,

        haste = 1,
        last_tick = 1,
        next_tick = 1
    }

    mt_default_debuff = {
        mtID = "default_debuff",

        ticks_before = function( t, span )
            if span <= 0 then return 0 end

            local remains = t.remains
            if remains == 0 then return 0 end

            local ticks_remain = t.ticks_remain
            span = min( span, remains )

            local real_delay = state.delay
            state.delay = state.delay + span

            local ticks_before = max( 0, ticks_remain - t.ticks_remain )
            state.delay = real_delay

            return ticks_before
        end,

        __index = function( t, k )
            local aura = class.auras[ t.key ]

            if state.IsCycling() and state.active_dot[ t.key ] < state.cycle_enemies and cycle_debuff[ k ] ~= nil then
                return cycle_debuff[ k ]
            end

            local meta = aura and rawget( aura, "meta" )

            if meta and meta[ k ]  then
                if not t.metastack[ k ] then
                    t.metastack[ k ] = true
                    local value = meta[ k ]( t, "debuff" )
                    t.metastack[ k ] = nil
                    if value ~= nil then return value end
                end

            elseif requiresLookup[ k ] then
                if aura and aura.generate then
                    for attr, a_val in pairs( default_debuff_values ) do
                        t[ attr ] = rawget( t, attr ) or rawget( aura, attr ) or  a_val
                    end

                    aura.generate( t, "debuff" )
                    t.id = aura and aura.id or t.key

                    return rawget( t, k )
                end

                local real = auras.target.debuff[ t.key ] or auras.player.debuff[ t.key ]
                if aura and aura.shared and auras[ aura.shared ] then
                    real = auras.player.debuff[ t.key ]
                end

                if real then
                    t.name = real.name or t.key
                    t.count = real.count
                    t.lastCount = real.lastCount or 0
                    t.lastApplied = real.lastApplied or 0
                    -- t.duration = real.duration
                    t.expires = real.expires or 0
                    t.applied = real.applied or 0
                    t.caster = real.caster
                    t.id = real.id
                    t.timeMod = real.timeMod
                    t.v1 = real.v1
                    t.v2 = real.v2
                    t.v3 = real.v3

                    t.unit = real.unit
                else
                    for attr, a_val in pairs( default_debuff_values ) do
                        t[ attr ] = aura and aura[ attr ] or a_val
                    end

                    t.id = aura and aura.id or t.id
                end

                return rawget( t, k )
            end

            -- 20250412: Revamped to avoid having this metatable call this metatable call this metatable...
            -- This change means that any references to t.X should only occur where X is a real value and not a metatable lookup.

            local moment = state.query_time
            local applied = meta and meta.applied and meta.applied( t, "debuff" ) or t.applied
            local expires = meta and meta.expires and meta.expires( t, "debuff" ) or t.expires
            local remains = meta and meta.remains and meta.remains( t, "debuff" ) or applied ~= 0 and applied <= moment and expires > moment and expires - moment or 0

            if k == "remains" then return remains end

            if k == "up" or
                k == "ticking" then return remains > 0 end

            if k == "down" then return remains == 0 end

            -- These should be long since deprecated.
            if k == "i_up" or
                k == "rank" then return remains > 0 and 1 or 0 end

            if k == "stack" or
                k == "stacks" or
                k == "react" then return remains > 0 and t.count or 0 end

            if k == "max_stack" or
               k == "max_stacks" then return max( t.count, aura.max_stack or 1 ) end

            if k == "at_max_stacks" then return remains > 0 and t.count >= ( aura.max_stack or 1 ) end
            if k == "stack_pct" then return remains > 0 and ( 100 * t.count / ( aura.max_stack or 1 ) ) or 0 end
            if k == "value" then return remains > 0 and t.v1 or 0 end
            if k == "stack_value" then return remains > 0 and t.v1 * t.count or 0 end

            local duration = remains > 0 and ( expires - applied ) or aura.duration or 30
            local apply_duration = aura.duration or duration

            if k == "duration" then return duration end
            if k == "apply_duration" then return apply_duration end
            if k == "refreshable" then return remains < 0.3 * apply_duration end
            if k == "time_to_refresh" then return remains > 0 and max( 0, 0.01 + remains - ( 0.3 * apply_duration ) ) or 0 end

            if k == "pmultiplier" then
                t.pmultiplier = remains > 0 and ns.getModifier( aura.id, state.target.unit ) or 0
                return t.pmultiplier
            end

            local tick_time = aura.tick_time or ( 3 * state.haste )
            if k == "tick_time" then return tick_time end

            local last_tick = remains > 0 and max( ns.GetDebuffNextTick( t.key, state.target.unit ), applied ) or 0
            if last_tick > 0 and moment - last_tick > tick_time then
                last_tick = last_tick + floor( ( moment - last_tick ) / tick_time ) * tick_time
            end

            if k == "last_tick" then return last_tick end
            if k == "ticks" then return remains > 0 and floor( ( last_tick - applied ) / tick_time ) or 0 end

            local next_tick = remains > 0 and max( ns.GetDebuffNextTick( t.key, state.target.unit ), last_tick + tick_time ) or 0
            if next_tick > 0 and next_tick < moment then
                next_tick = next_tick + ceil( ( moment - last_tick ) / tick_time ) * tick_time
            end

            if k == "next_tick" then return next_tick end
            if k == "tick_time_remains" then return max( 0, next_tick - moment ) end
            if k == "ticks_remain" then return expires > moment and max( 0, 1 + floor( ( expires - next_tick ) / tick_time ) ) or 0 end

            if k == "cast_time" then
                local ability = class.abilities[ t.key ]
                return ability and state.action[ t.key ].cast_time or 0
            end

            if k == "execute_time" then
                local ability = class.abilities[ t.key ]
                return ability and state.action[ t.key ].execute_time or gcd.max
            end

            local attr = aura[ k ]
            if attr ~= nil then return attr end

            if not flagged_debuffs[ t.key ] then
                Error( "UNK: debuff." .. t.key .. "." .. k )
                flagged_debuffs[ t.key ] = true
            end
        end,
        __newindex = function( t, k, v )
            if v ~= nil and autoReset[ k ] then Mark( t, k ) end
            rawset( t, k, v )
        end
    }
    ns.metatables.mt_default_debuff = mt_default_debuff


    unknown_debuff = setmetatable( {
        key = "unknown_debuff",
        name = "No Name",
        count = 0,
        lastCount = 0,
        lastApplied = 0,
        -- duration = 30,
        expires = 0,
        applied = 0,
        caster = "nobody",
        timeMod = 1,
        v1 = 0,
        v2 = 0,
        v3 = 0,
        unit = "player"
    }, mt_default_debuff )


    -- Table of debuffs applied to the target by the player.
    local debuffs_warned = {}
    mt_debuffs = {
        -- The debuff/ doesn't exist in our table so check the real game state,
        -- and copy it so we don't have to use the API next time.

        __index = function( t, k )
            local aura = class.auras[ k ]

            if aura then
                if k ~= aura.key then
                    t[ aura.key ] = rawget( t, aura.key ) or {
                        key = aura.key,
                        name = aura.name
                    }
                    t[ k ] = t[ aura.key ]
                else
                    t[ k ] = {
                        key = aura.key,
                        name = aura.name
                    }
                end

                if aura.generate then
                    for attr, a_val in pairs( default_debuff_values ) do
                        t[ k ][ attr ] = rawget( t[ k ], attr ) or a_val
                    end
                    aura.generate( t[ k ], "debuff" )
                    return t[ k ]
                end

            else
                if Hekili.PLAYER_ENTERING_WORLD and not debuffs_warned[ k ] then
                    Hekili:Error( "WARNING: Unknown debuff in [" .. ( state.scriptID or "unknown" ) .. "]: " .. k .. "\n\n" .. debugstack() )
                    debuffs_warned[ k ] = true
                end

                t[ k ] = {
                    key = k,
                    name = k,
                    id = k
                }
            end

            local real = auras.player.debuff[ k ] or auras.target.debuff[ k ]
            local debuff = t[k]

            if real then
                debuff.name = real.name
                debuff.count = real.count
                debuff.lastCount = real.lastCount or 0
                debuff.lastApplied = real.lastApplied or 0
                -- debuff.duration = real.duration
                debuff.expires = real.expires
                debuff.applied = real.applied
                debuff.caster = real.caster
                debuff.id = real.id
                debuff.timeMod = real.timeMod
                debuff.v1 = real.v1
                debuff.v2 = real.v2
                debuff.v3 = real.v3

                debuff.unit = real.unit

            else
                debuff.name = aura and aura.name or "No Name"
                debuff.count = 0
                debuff.lastCount = 0
                debuff.lastApplied = 0
                -- debuff.duration = aura and aura.duration or 30
                debuff.expires = 0
                debuff.applied = 0
                debuff.caster = "nobody"
                -- debuff.id = nil
                debuff.timeMod = 1
                debuff.v1 = 0
                debuff.v2 = 0
                debuff.v3 = 0

                debuff.unit = aura and aura.unit or "player"
            end

            t[k] = debuff
            return t[ k ]
        end,

        __newindex = function( t, k, v )
            local aura = class.auras[ k ]

            if aura then
                aura.used = true
                if aura.meta then rawset( v, "metastack", {} ) end
                if aura.alias then
                    rawset( t, k, setmetatable( v, mt_alias_debuff ) )
                    return
                end
            end

            rawset( t, k, setmetatable( v, mt_default_debuff ) )
        end
    }
    ns.metatables.mt_debuffs = mt_debuffs
end


-- Table of default handlers for actions.
-- Needs review.
local mt_default_action = {
    __index = function( t, k )
        local ability = t.action and class.abilities[ t.action ]
        local aura = ability and ability.aura or t.action

        if k == "enabled" or k == "known" then
            return state:IsKnown( t.action )

        elseif k == "disabled" then
            return state:IsDisabled( t.action )

        elseif k == "gcd" then
            local queued_action = state.this_action
            state.this_action = t.action

            local value = state.gcd.execute
            state.this_action = queued_action

            return value

        elseif k == "execute_time" then
            local queued_action = state.this_action
            state.this_action = t.action

            local value = state.gcd.execute
            state.this_action = queued_action

            return max( value, t.cast_time )

        elseif k == "charges" then
            return state.cooldown[ t.action ].charges

        elseif k == "charges_fractional" then
            return state.cooldown[ t.action ].charges_fractional

        elseif k == "recharge_time" then
            return state.cooldown[ t.action ].recharge_time

        elseif k == "max_charges" then
            return ability.charges or 0

        elseif k == "time_to_max_charges" or k == "full_recharge_time" then
            return state.cooldown[ t.action ].full_recharge_time

        elseif k == "ready_time" then
            return state:IsUsable( t.action ) and state:TimeToReady( t.action ) or 999

        elseif k == "ready" then
            return state:IsUsable( t.action ) and state:IsReady( t.action )

        elseif k == "usable_in" then
            return state.cooldown[ t.action ].remains

        elseif k == "cast_time" then
            return ability.cast

        elseif k == "cooldown" then
            return ability.cooldown

        elseif k == "damage" then
            return ability.damage or 1

        elseif k == "crit_pct_current" then
            return ability.critical or state.stat.crit

        elseif k == "ticking" then
            return ( state.dot[ aura ].ticking )

        elseif k == "ticks" then
            return 1 + ( state.dot[ aura ].duration or ( 30 * state.haste ) / class.auras[ aura ].tick_time or ( 3 * state.haste ) ) - t.ticks_remain

        elseif k == "ticks_remain" then
            return state.dot[ aura ].remains / ( class.auras[ aura ].tick_time or ( 3 * state.haste ) )

        elseif k == "remains" then
            return ( state.dot[ aura ].remains )

        elseif k == "tick_time" then
            return class.auras[ aura ].tick_time or ( 3 * state.haste )

        elseif k == "travel_time" then
            -- NYI: maybe capture the last travel time for the spell and use that?
            local f, v = ability.flightTime, ability.velocity
            if f then return f end
            if v and v > 0 then return state.target.maxR / v end
            return 0

        elseif k == "miss_react" then
            return false

        elseif k == "cooldown_react" then
            return state.cooldown[ t.action ].remains == 0

        elseif k == "cast_delay" then
            return 0

        elseif k == "cast_regen" then
            local regen = t.regen
            if regen == 0.001 then regen = 0 end

            return floor( max( state.gcd.execute, t.cast_time ) * regen ) - t.cost

        elseif k == "cost" then
            if ability then
                local c = ability.cost

                if c then return c end

                c = ability.spend

                if c and c > 0 and c < 1 then
                    c = c * state[ ability.spendType or class.primaryResource ].modmax
                end

                return c or 0
            end

            return 0

        elseif k == "cost_type" then
            local a, _ = ability.spendType
            if type( a ) == "string" then return a end

            a = ability.spend
            if type( a ) == "function" then _, a = a() end
            if type( a ) == "string" then return a end
            return class.primaryResource

        elseif k == "in_flight" or k == "in_flight_to_target" then
            if ability.flightTime then
                return ability.lastCast + max( ability.flightTime, 0.25 ) > state.query_time
            end

            return state:IsInFlight( t.action ) or ability.isProjectile and ability.lastCast + 0.25 > state.query_time

        elseif k == "in_flight_remains" then
            if ability.flightTime then

                return max( 0, ability.lastCast + max( ability.flightTime, 0.25 ) - state.query_time )
            end
            return max( state:InFlightRemains( t.action ), ability.isProjectile and ability.lastCast + 0.25 - state.query_time or 0 )

        elseif k == "channeling" then
            return state:IsChanneling( t.action )

        elseif k == "channel_remains" then
            return state:IsChanneling( t.action ) and state:QueuedCastRemains( t.action ) or 0

        elseif k == "executing" then
            return state:IsCasting( t.action ) or ( state.prev[ 1 ][ t.action ] and state.gcd.remains > 0 )

        elseif k == "execute_remains" then
            return ( state:IsCasting( t.action ) and max( state:QueuedCastRemains( t.action ), state.gcd.remains ) ) or ( state.prev[1][ t.action ] and state.gcd.remains ) or 0

        elseif k == "last_used" then
            return state.combat > 0 and max( 0, ability.lastCast - state.combat ) or 0

        elseif k == "time_since" then
            return min( 3600, max( 0, state.query_time - ability.lastCast ) )

        elseif k == "in_range" then
            if UnitExists( "target" ) and UnitCanAttack( "player", "target" ) and LSR.IsSpellInRange( ability.rangeSpell or ability.id, "target" ) == 0 then
                return false
            end

            return true

        elseif k == "cycle" then
            return ability.cycle == "cycle"

        else
            local val = ability[ k ]

            if val ~= nil then
                if type( val ) == "function" then return val() end
                return val
            end
        end

        return 0
    end
}
ns.metatables.mt_default_action = mt_default_action


-- mt_actions: provides action information for display/priority queue/action criteria.
-- NYI.
local mt_actions = {
    __index = function(t, k)
        local action = class.abilities[ k ]

        -- Need a null_action table.
        if not action then return nil end

        t[k] = {
            action = k,
            name = action.name,
            gcdType = action.gcd
        }

        local h = state.haste
        state.haste = 0
        t[k].base_cast = action.cast
        state.haste = h

        return ( t[k] )
        end, __newindex = function(t, k, v)
        rawset( t, k, setmetatable( v, mt_default_action ) )
    end
}
ns.metatables.mt_actions = mt_actions



-- mt_swings: used for projecting weapon swing-based resource gains.
local mt_swings = {
    __index = function( t, k )
        if k == "mainhand" then
            return t.mh_pseudo or t.mh_actual

        elseif k == "offhand" then
            return t.oh_pseudo or t.oh_actual

        elseif k == "mainhand_speed" then
            return t.mh_pseudo_speed or t.mh_speed or 0

        elseif k == "offhand_speed" then
            return t.oh_pseudo_speed or t.oh_speed or 0

        end
    end
}


state.swing = {}

local mt_swing_timer = {
    __index = function( t, k )
        local speed = state.swings[ t.type .. "_speed" ]
        if speed == 0 then return 999 end

        local swing = state.time == 0 and state.now or state.swings.mainhand
        if swing == 0 then return speed end

        -- Technically, we didn't even check if this were "remains" but there are no other symbols.
        local t = state.query_time
        return swing + ( ceil( ( t - swing ) / speed ) * speed ) - t
    end,
}

state.swing.mh = setmetatable( { type = "mainhand" }, mt_swing_timer )
state.swing.mainhand = state.swing.mh
state.swing.main_hand = state.swing.mh

state.swing.oh = setmetatable( { type = "offhand" }, mt_swing_timer )
state.swing.offhand = state.swing.oh
state.swing.off_hand = state.swing.oh


local mt_weapon_type = {
    __index = function( t, k )
        local size = t.size

        if k == "two_handed" or k == "2h" or k == "two_hand" then
            return size == 2
        elseif k == "one_handed" or k == "1h" or k == "one_hand" then
            return size == 1
        end

        return false
    end,
}


local mt_aura = {
    __index = function( t, k )
        return rawget( state.buff, k ) or rawget( state.debuff, k )
    end
}


local mt_empowering = {
    __index = function( t, k )
        if state.buff.empowering.down then return false end
        return state.buff.empowering.spell == k
    end
}


setmetatable( state, mt_state )
setmetatable( state.action, mt_actions )
setmetatable( state.active_dot, mt_active_dot )
setmetatable( state.aura, mt_aura )
setmetatable( state.buff, mt_buffs )
setmetatable( state.cooldown, mt_cooldowns )
setmetatable( state.debuff, mt_debuffs )
setmetatable( state.dot, mt_dot )
setmetatable( state.empowering, mt_empowering )
setmetatable( state.equipped, mt_equipped )
setmetatable( state.main_hand, mt_weapon_type )
setmetatable( state.off_hand, mt_weapon_type )
-- setmetatable( state.health, mt_resource )
setmetatable( state.pet, mt_pets )
setmetatable( state.pet.fake_pet, mt_default_pet )
setmetatable( state.prev, mt_prev )
setmetatable( state.prev_gcd, mt_prev )
setmetatable( state.prev_off_gcd, mt_prev )
setmetatable( state.pvptalent, mt_pvptalents )
setmetatable( state.race, mt_false )
setmetatable( state.set_bonus, mt_set_bonuses )
setmetatable( state.settings, mt_settings )
setmetatable( state.spec, mt_spec )
setmetatable( state.stance, mt_stances )
setmetatable( state.stat, mt_stat )
setmetatable( state.swings, mt_swings )
setmetatable( state.talent, mt_talents )
setmetatable( state.target, mt_target )
setmetatable( state.target.health, mt_target_health )
setmetatable( state.toggle, mt_toggle )
setmetatable( state.totem, mt_totem )



local all = class.specs[ 0 ]

-- 04072017: Let's go ahead and cache aura information to reduce overhead.
local autoAuraKey = setmetatable( {}, {
    __index = function( t, k )
        local aura_name = GetSpellInfo( k )

        if not aura_name then return end

        local name

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

            while( true ) do
                local new = aura_name .. ' ' .. i

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

                i = i + 1
            end
        end
        name = name or aura_name

        local key = formatKey( aura_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 not all then all = class.specs[ 0 ] end
        if all then
            all:RegisterAura( key, {
                id = k,
                name = name
            } )
        end
        t[k] = key

        return t[k]
    end
} )



do
    local UnitAuraBySlot = UnitAuraBySlot

    function state.StoreMatchingAuras( unit, auras, filter, ... )
        local n = auras.count
        auras.count = nil

        local db = ns.auras[ unit ][ filter == "HELPFUL" and "buff" or "debuff" ]

        for k, v in pairs( auras ) do
            local aura = class.auras[ v ]
            local key = aura.key

            local a = db[ key ] or {}

            a.key              = key
            a.name             = nil
            a.lastCount        = a.count or 0
            a.lastApplied      = a.applied or 0
            a.last_application = max( 0, a.applied or 0, a.last_application or 0 )
            a.last_expiry      = max( 0, a.expires or 0, a.last_expiry or 0 )
            a.count            = 0
            a.expires          = 0
            a.applied          = 0
            -- a.duration         = aura.duration or a.duration
            a.caster           = "nobody"
            a.timeMod          = 1
            a.v1               = 0
            a.v2               = 0
            a.v3               = 0
            a.unit             = unit

            db[ key ] = a
        end

        for i = select( "#", ... ), 1, -1 do
            local slot = select( i, ... )
            local name, _, count, _, duration, expires, caster, _, _, spellID, _, _, _, _, timeMod, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10 = UnitAuraBySlot( unit, slot )

            local key = auras[ spellID ]

            if key and name and ( unit == "player" or caster and ( UnitIsUnit( caster, "player" ) or UnitIsUnit( caster, "pet" ) ) ) then
                local a = db[ key ]

                if expires == 0 then
                    expires = GetTime() + 3600
                    duration = 7200
                end

                a.name     = name
                a.count    = count > 0 and count or 1
                -- a.duration = duration
                a.expires  = expires
                a.applied  = expires - duration
                a.caster   = caster
                a.timeMod  = timeMod
                a.v1       = v1
                a.v2       = v2
                a.v3       = v3
                a.v4       = v4
                a.v5       = v5
                a.v6       = v6
                a.v7       = v7
                a.v8       = v8
                a.v9       = v9
                a.v10      = v10

                n = n - 1
                if n == 0 then break end
            end
        end
    end
    Hekili.StoreMatchingAuras = state.StoreMatchingAuras


    function state.ScrapeUnitAuras( unit, newTarget, why )
        local db = ns.auras[ unit ]

        for k,v in pairs( db.buff ) do
            v.name = nil
            v.lastCount = newTarget and 0 or v.count
            v.lastApplied = newTarget and 0 or v.applied

            v.last_application = max( 0, v.applied, v.last_application or 0 )
            v.last_expiry  = max( 0, v.expires, v.last_expiry or 0 )

            v.count = 0
            v.expires = 0
            v.applied = 0
            -- v.duration = class.auras[ k ] and class.auras[ k ].duration or v.duration
            v.caster = "nobody"
            v.timeMod = 1
            v.v1 = 0
            v.v2 = 0
            v.v3 = 0
            v.unit = unit
        end

        for k,v in pairs( db.debuff ) do
            v.name = nil
            v.lastCount = newTarget and 0 or v.count
            v.lastApplied = newTarget and 0 or v.applied
            v.count = 0
            v.expires = 0
            v.applied = 0
            -- v.duration = class.auras[ k ] and class.auras[ k ].duration or v.duration
            v.caster = "nobody"
            v.timeMod = 1
            v.v1 = 0
            v.v2 = 0
            v.v3 = 0
            v.unit = unit
        end

        state[ unit ].updated = false
        if not UnitExists( unit ) then return end

        local i = 1
        while ( true ) do
            local buffData = GetBuffDataByIndex( unit, i )
            if not buffData then break end

            local name, _, count, _, duration, expires, caster, _, _, spellID, _, _, _, _, timeMod, v1, v2, v3 = UnpackAuraData( buffData )
            if not name then break end

            local aura = class.auras[ spellID ]
            local shared = aura and aura.shared
            local key = aura and aura.key or autoAuraKey[ spellID ]

            if aura and key and ( shared or caster and ( UnitIsUnit( caster, "player" ) or UnitIsUnit( caster, "pet" ) ) ) then
                db.buff[ key ] = db.buff[ key ] or {}
                local buff = db.buff[ key ]

                if expires == 0 then
                    expires = GetTime() + 3600
                    duration = 7200
                end

                buff.key = key
                buff.id = spellID
                buff.name = name
                if buff.count and buff.count > 0 then
                    buff.count = buff.count + ( count > 0 and count or 1 )
                    buff.expires = max( buff.expires, expires )
                else
                    buff.count = count > 0 and count or 1
                    buff.expires = expires
                end
                buff.applied = expires - duration
                -- buff.duration = duration
                buff.caster = caster
                buff.timeMod = timeMod
                buff.v1 = v1
                buff.v2 = v2
                buff.v3 = v3

                buff.last_application = buff.last_application or 0
                buff.last_expiry      = buff.last_expiry or 0

                buff.unit = unit
            end

            i = i + 1
        end

        i = 1
        while ( true ) do
            local debuffData = GetDebuffDataByIndex( unit, i )
            if not debuffData then break end

            local name, _, count, _, duration, expires, caster, _, _, spellID, _, _, _, _, timeMod, v1, v2, v3 = UnpackAuraData( debuffData )
            if not name then break end

            local aura = class.auras[ spellID ]
            local shared = aura and aura.shared
            local key = aura and aura.key or autoAuraKey[ spellID ]

            if aura and key and ( shared or caster and ( UnitIsUnit( caster, "player" ) or UnitIsUnit( caster, "pet" ) ) ) then
                db.debuff[ key ] = db.debuff[ key ] or {}
                local debuff = db.debuff[ key ]

                if expires == 0 then
                    expires = GetTime() + 3600
                    duration = 7200
                end

                debuff.key = key
                debuff.id = spellID
                debuff.name = name
                debuff.count = count > 0 and count or 1
                debuff.expires = expires
                -- debuff.duration = duration
                debuff.applied = expires - duration
                debuff.caster = caster
                debuff.timeMod = timeMod
                debuff.v1 = v1
                debuff.v2 = v2
                debuff.v3 = v3

                debuff.unit = unit
            end

            i = i + 1
        end

        if UnitIsUnit( unit, "player" ) and IsInJailersTower() then
            i = 1
            while ( true ) do
                local name, _, count, _, duration, expires, caster, _, _, spellID, _, _, _, _, timeMod, v1, v2, v3 = UnitBuff( unit, i, "MAW" )
                if not name then break end

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

                if not key then key = autoAuraKey[ spellID ] end

                if key then
                    db.buff[ key ] = db.buff[ key ] or {}
                    local buff = db.buff[ key ]

                    if expires == 0 then
                        expires = GetTime() + 3600
                        duration = 7200
                    end

                    buff.key = key
                    buff.id = spellID
                    buff.name = name
                    buff.count = count > 0 and count or 1
                    buff.expires = expires
                    -- buff.duration = duration
                    buff.applied = expires - duration
                    buff.caster = caster
                    buff.timeMod = timeMod
                    buff.v1 = v1
                    buff.v2 = v2
                    buff.v3 = v3

                    if aura and buff.count > aura.max_stack then aura.max_stack = buff.count end

                    buff.last_application = buff.last_application or 0
                    buff.last_expiry      = buff.last_expiry or 0

                    buff.unit = unit
                end

                i = i + 1
            end

            i = 1
            while ( true ) do
                local name, _, count, _, duration, expires, caster, _, _, spellID, _, _, _, _, timeMod, v1, v2, v3 = UnitDebuff( unit, i, "MAW" )
                if not name then break end

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

                if not key then key = autoAuraKey[ spellID ] end

                if key then
                    db.debuff[ key ] = db.debuff[ key ] or {}
                    local debuff = db.debuff[ key ]

                    if expires == 0 then
                        expires = GetTime() + 3600
                        duration = 7200
                    end

                    debuff.key = key
                    debuff.id = spellID
                    debuff.name = name
                    debuff.count = count > 0 and count or 1
                    debuff.expires = expires
                    -- debuff.duration = duration
                    debuff.applied = expires - duration
                    debuff.caster = caster
                    debuff.timeMod = timeMod
                    debuff.v1 = v1
                    debuff.v2 = v2
                    debuff.v3 = v3

                    if aura and debuff.count > aura.max_stack then aura.max_stack = debuff.count end

                    debuff.unit = unit
                end

                i = i + 1
            end
        end
    end
    Hekili.ScrapeUnitAuras = state.ScrapeUnitAuras

    Hekili:ProfileCPU( "ScrapeUnitAuras", state.ScrapeUnitAuras )

    Hekili.AuraDB = ns.auras
end
local ScrapeUnitAuras = state.ScrapeUnitAuras


-- Helper functions to query the real aura data that has been scraped.
-- Used for snapshotting projectile data to be handled when a spell impacts.
function state.PlayerBuffUp( buff )
    local aura = state.auras.player.buff[ buff ]
    return aura and aura.expires > GetTime()
end

function state.PlayerDebuffUp( debuff )
    local aura = state.auras.player.debuff[ debuff ]
    return aura and aura.expires > GetTime()
end

function state.PlayerDebuffRemains( debuff )
    local aura = state.auras.player.debuff[ debuff ]
    return aura and aura.expires > GetTime() and aura.expires - GetTime() or 0
end

function state.TargetBuffUp( buff )
    local aura = state.auras.target.buff[ buff ]
    return aura and aura.expires > GetTime()
end

function state.TargetDebuffUp( debuff )
    local aura = state.auras.target.debuff[ debuff ]
    return aura and aura.expires > GetTime()
end


function state.putTrinketsOnCD( val )
    val = val or 10

    for i, item in ipairs( state.items ) do
        if not class.abilities[ item ].essence and not class.abilities[ item ].no_icd and state.cooldown[ item ].remains < val then
            state.setCooldown( item, val )
        end
    end
end


do
    -- Simpler Queue System
    local realQueue = {}
    state.realQueue = realQueue

    local virtualQueue = {}
    state.queue = virtualQueue

    local byTime = function( a, b )
        return a.time < b.time
    end


    local eventPool = {}

    local function NewEvent()
        if #eventPool > 0 then
            return remove( eventPool, 1 )
        end

        return {}
    end

    local function RecycleEvent( queue, i )
        local e = queue[ i ]

        if e then
            e.action = nil
            e.start  = nil
            e.time   = nil
            e.type   = nil
            e.target = nil
            e.real   = nil
            e.func   = nil

            insert( eventPool, e )
            remove( queue, i )
        end
    end

    function state:QueueEvent( action, start, time, type, target, real )
        local queue = real and realQueue or virtualQueue
        local e = NewEvent()

        if not time then
            local ability = class.abilities[ action ]

            if ability then
                if type == "PROJECTILE_IMPACT" then
                    if ability.flightTime then time = start + 0.05 + ability.flightTime
                    else time = start + 0.05 + ( state.target.maxR / ability.velocity ) end

                elseif type == "CHANNEL_START" then
                    time = start

                elseif not time and ( type == "CHANNEL_FINISH" or type == "CAST_FINISH" ) then
                    time = start + ability.cast

                end
            end
        end

        if action and start and time and type then
            if time < start then time = start + time end

            e.action = action
            e.start  = start
            e.time   = time
            e.type   = type
            e.target = target
            e.real   = real
            e.func   = nil

            insert( queue, e )
            sort( queue, byTime )

            if real then
                queue[ action ] = ( queue[ action ] or 0 ) + 1
            end

            if Hekili.ActiveDebug and not real then Hekili:Debug( "Queued %s from %.2f to %.2f (%s).", action, start, time, type ) end
        end
    end

    Hekili:ProfileCPU( "QueueEvent", state.QueueEvent )

    function state:QueueAuraEvent( action, func, time, eType, data )
        local queue = virtualQueue
        local e = NewEvent()

        if not time then return end

        e.action = action
        e.func   = func
        e.start  = self.query_time
        e.time   = time
        e.type   = eType
        e.target = "nobody"
        e.real   = false
        e.data   = data

        insert( queue, e )
        sort( queue, byTime )

        if Hekili.ActiveDebug then Hekili:Debug( "Queued %s %s at +%.2f.", action, eType, time - state.query_time ) end
    end

    function state:RemoveAuraEvent( action, eType )
        local queue = virtualQueue
        eType = eType or "AURA_EXPIRATION"

        Hekili:Debug( "Trying to remove %s %s from queue.", action, eType )

        for i = 1, #queue do
            local e = queue[ i ]

            if e.action == action and e.type == eType then
                RecycleEvent( queue, i )
                Hekili:Debug( "Removed #%d from queue.", i )
                break
            end
        end
    end

    function state:QueueAuraExpiration( action, func, time, data )
        self:QueueAuraEvent( action, func, time, "AURA_EXPIRATION", data )
    end

    function state:RemoveAuraExpiration( action )
        self:RemoveAuraEvent( action, "AURA_EXPIRATION" )
    end

    function state:RemoveEvent( e, real )
        local queue = real and realQueue or virtualQueue

        Hekili:Debug( "Trying to remove %s %s from queue.", ( e.action or "NO_ACTION" ), ( e.type or "NO_TYPE" ) )

        for i = #queue, 1, -1 do
            if queue[ i ] == e then
                Hekili:Debug( "Removing %d from queue.", i )
                RecycleEvent( queue, i )

                if real then
                    queue[ action ] = max( 0, ( queue[ action ] or 0 ) - 1 )
                end
                break
            end
        end
    end

    Hekili:ProfileCPU( "RemoveEvent", state.RemoveEvent )

    function state:GetEventInfo( action, start, time, type, target, real )
        local queue = real and realQueue or virtualQueue

        if real and ( queue[ action ] or 0 ) == 0 then return end

        -- Find the first event that matches the provided criteria and return all the data.
        for i, event in ipairs( queue ) do
            if ( not action or event.action == action ) and
               ( not start  or event.start  == start  ) and
               ( not time   or event.time   == time   ) and
               ( not type   or event.type   == type   ) and
               ( not target or event.target == target ) then

               return event.action, event.start, event.time, event.type, event.target, event.real
            end
        end
    end

    function state:RemoveSpellEvent( action, real, eType, reverse )
        local queue = real and realQueue or virtualQueue

        local success = false

        if reverse then
            for i = #queue, 1, -1 do
                local e = queue[ i ]

                if e.action == action and ( eType == nil or e.type == eType ) then
                    RecycleEvent( queue, i )
                    if real then
                        queue[ action ] = ( queue[ action ] or 1 ) - 1
                    end
                    return true
                end
            end
        else
            for i = 1, #queue do
                local e = queue[ i ]

                if e.action == action and ( eType == nil or e.type == eType ) then
                    RecycleEvent( queue, i )
                    if real then
                        queue[ action ] = ( queue[ action ] or 0 ) - 1
                    end
                    return true
                end
            end
        end

        return false
    end

    function state:RemoveSpellEvents( action, real, eType )
        local queue = real and realQueue or virtualQueue

        local success = false
        local impactSpells = class.abilities[ action ] and class.abilities[ action ].impactSpells

        for i = #queue, 1, -1 do
            local e = queue[ i ]

            if ( e.action == action or impactSpells and impactSpells[ action ] ) and ( eType == nil or e.type == eType ) then
                RecycleEvent( queue, i )
                if real then
                    queue[ action ] = ( queue[ action ] or 1 ) - 1
                end
                success = true
            end
        end

        if success then
            for k in pairs( class.resources ) do
                forecastResources( k )
            end
        end

        return success
    end


    local EVENT_EXPIRE_MARGIN = 0.2

    function state:ResetQueues()
        for i = #virtualQueue, 1, -1 do
            RecycleEvent( virtualQueue, i )
        end

        local now = GetTime()

        for i = #realQueue, 1, -1 do
            local e = realQueue[ i ]

            if e.time + EVENT_EXPIRE_MARGIN < now then
                RecycleEvent( realQueue, i )
                if e.action then
                    realQueue[ e.action ] = max( 0, ( realQueue[ e.action ] or 1 ) - 1 )
                end
            end
        end

        for i, r in ipairs( realQueue ) do
            local e = NewEvent()

            e.action = r.action
            e.start  = r.start
            e.time   = r.time
            e.type   = r.type
            e.target = r.target

            virtualQueue[ i ] = e
        end
    end


    local times = {}

    function state:GetQueueTimes( queue )
        wipe( times )

        for i, v in ipairs( queue ) do
            times[ i ] = v.time
        end

        return unpack( times )
    end


    function state:GetQueue( real )
        if real then return realQueue end
        return virtualQueue
    end


    function state:HandleEvent( e )
        if not e then return end

        local action = e.action
        local ability
        local curr_action = self.this_action

        if e.type ~= "AURA_EXPIRATION" and e.type ~= "AURA_PERIODIC" then
            ability = class.abilities[ e.action ]

            if not ability then
                state:RemoveEvent( e )
                return
            end

            self.this_action = action
        end

        if Hekili.ActiveDebug then Hekili:Debug( "\nHandling %s at %.2f (%s).", action, e.time, e.type ) end

        if e.type == "CAST_FINISH" then
            self.hardcast = true
            local cooldown = ability.cooldown

            -- Put the action on cooldown. (It's slightly premature, but addresses CD resets like Echo of the Elements.)
            -- if ability.charges and ability.charges > 1 and ability.recharge > 0 then
            if ability.charges and ( ability.recharge or ability.cooldown ) > 0 then
                self.spendCharges( action, 1 )

            elseif action ~= "global_cooldown" then
                self.setCooldown( action, cooldown )
            end

            -- Spend resources.
            ns.spendResources( action )

            local wasCycling = self.IsCycling( nil, true )
            local expires, minTTD, maxTTD, aura

            if wasCycling then
                expires, minTTD, maxTTD, aura = self.GetCycleInfo()
            end

            if e.target and e.target ~= self.target.unit then
                if Hekili.ActiveDebug then Hekili:Debug( "Using ability on a different target." ) end
                self.SetupCycle( ability )
            end

            -- Perform the action.
            self:RunHandler( action )
            self.hardcast = nil
            self.whitelist = nil
            self.removeBuff( "casting" ) -- TODO: Revisit for Casting while Casting scenarios; check Fire Mage.

            if wasCycling then
                self.SetCycleInfo( expires, minTTD, maxTTD, aura )
            else
                self.ClearCycle()
            end

            self:SetWhitelist( nil )

            if ability.item and not ( ability.essence or ability.no_icd ) then
                self.putTrinketsOnCD( cooldown / 6 )
            end

        elseif e.type == "CHANNEL_TICK" then
            if ability.tick then ability.tick() end

        elseif e.type == "CHANNEL_FINISH" then
            if ability.finish then ability.finish() end
            self.whitelist = nil
            self.removeBuff( "casting" )

        elseif e.type == "PROJECTILE_IMPACT" then
            local wasCycling = self.IsCycling( nil, true )
            local expires, minTTD, maxTTD, aura

            if wasCycling then
                expires, minTTD, maxTTD, aura = self.GetCycleInfo()
            end

            if e.target and e.target ~= self.target.unit then
                if Hekili.ActiveDebug then Hekili:Debug( "Using ability on a different target." ) end
                self.SetupCycle( ability )
            end

            if ability.impact then ability.impact( e.real ) end
            self:StartCombat()

            if wasCycling then
                self.SetCycleInfo( expires, minTTD, maxTTD, aura )
            else
                self.ClearCycle()
            end
        end

        if e.func then e.func( e.data ) end

        state.this_action = curr_action
        state:RemoveEvent( e )
    end

    Hekili:ProfileCPU( "HandleEvent", state.HandleEvent )


    function state:IsQueued( action, real )
        if real and ( realQueue[ action ] or 0 ) == 0 then return false end

        local queue = real and realQueue or virtualQueue

        for i, entry in ipairs( queue ) do
            if entry.action == action then return true end
        end

        return false
    end


    function state:IsInFlight( action, real )
        if real and ( realQueue[ action ] or 0 ) == 0 then return false end

        local queue = real and realQueue or virtualQueue

        for i, entry in ipairs( queue ) do
            if entry.action == action and entry.type == "PROJECTILE_IMPACT" and entry.start <= self.query_time then return true end
        end

        return false
    end


    function state:InFlightRemains( action, real )
        if real and ( realQueue[ action ] or 0 ) == 0 then return 0 end

        local queue = real and realQueue or virtualQueue

        for i, entry in ipairs( queue ) do
            if entry.action == action and entry.type == "PROJECTILE_IMPACT" and entry.start <= self.query_time then return max( 0, entry.time - self.query_time ) end
        end

        return 0
    end


    local cast_events = {
        CAST_FINISH = true,
        CHANNEL_FINISH = true
    }

    function state:IsCasting( action, real )
        if real and ( realQueue[ action ] or 0 ) == 0 then return false end
        local queue = real and realQueue or virtualQueue

        for i, entry in ipairs( queue ) do
            if entry.type == "CAST_FINISH" and ( action == nil or entry.action == action ) and entry.start <= self.query_time then return true end
        end

        return false
    end


    function state:QueuedCastRemains( action, real )
        if real and ( realQueue[ action ] or 0 ) == 0 then return 0 end

        local queue = real and realQueue or virtualQueue

        for i, entry in ipairs( queue ) do
            if cast_events[ entry.type ] and ( action == nil or entry.action == action ) and entry.start <= self.query_time then return max( 0, entry.time - self.query_time ) end
        end

        return 0
    end


    function state:IsChanneling( action, real )
        local queue = real and realQueue or virtualQueue

        for i, entry in ipairs( queue ) do
            if entry.type == "CHANNEL_FINISH" and ( action == nil or entry.action == action ) and entry.start <= self.query_time then return true end
        end

        return false
    end


    function state:ApplyCastingAuraFromQueue( action, real )
        local queue = real and realQueue or virtualQueue

        for i, entry in ipairs( queue ) do
            if cast_events[ entry.type ] and ( action == nil or entry.action == action ) and entry.start <= self.query_time then
                local casting = self.buff.casting

                casting.applied = entry.start

                if entry.time > entry.start then
                    casting.expires = entry.time
                else
                    casting.expires = entry.start + entry.time
                end

                casting.duration = casting.expires - casting.applied

                casting.v3 = entry.type == "CHANNEL_FINISH" and 1 or 0

                if entry.action then
                    local spell = class.abilities[ entry.action ]
                    if spell and spell.id then
                        casting.v1 = spell.id
                    else
                        casting.v1 = 0
                    end
                else
                    casting.v1 = 0
                end

                return
            end
        end
    end
end


function state:RunHandler( key, noStart )
    local ability = class.abilities[ key ]

    if not ability then
        -- ns.Error( "runHandler() attempting to run handler for non-existant ability '" .. key .. "'." )
        return
    end

    --[[ if self.channeling and not ability.dual_cast then
        self.stopChanneling( false, ability.key )
    end ]]

    -- Any ability handler is likely to modify the game state enough to force a reset later.
    if not state.resetting then state.modified = true end

    if ability.channeled then
        if ability.start then ability.start() end
        self.channelSpell( key, self.query_time, ability.cast, ability.id )
    elseif ability.handler then ability.handler() end

    if self.buff.empowering.up then removeBuff( "empowering" ) end

    self.prev.last = key
    self[ ability.gcd == "off" and "prev_off_gcd" or "prev_gcd" ].last = key

    table.insert( self.predictions, 1, key )
    table.insert( self[ ability.gcd == "off" and 'predictionsOff' or 'predictionsOn' ], 1, key )

    self.history.casts[ key ] = self.query_time

    self.predictions[11] = nil
    self.predictionsOn[11] = nil
    self.predictionsOff[11] = nil

    self.prev.override = nil
    self.prev_gcd.override = nil
    self.prev_off_gcd.override = nil

    if Hekili.ActiveDebug then
        Hekili:Debug( "Running %s at %.2f.", key, self.query_time )
    end

    if self.time == 0 and ability.startsCombat and not noStart then -- and not ability.isProjectile
        -- Assume MH swing at combat start and OH swing half a swing later?
        self:StartCombat()
        ns.callHook( "runHandler_startCombat", key )
    end

    -- state.cast_start = 0
    ns.callHook( "runHandler", key )
end

function state.runHandler( key, noStart )
    state:RunHandler( key, noStart )
end


do
    local firstTime = true

    function state.reset( dispName, full )
        local started = debugprofilestop()

        dispName = dispName or "Primary"
        local displayFrame = _G[ "HekiliDisplay" .. dispName ]

        full = dispName == "Primary" or state.offset > 0

        ClearMarks( firstTime )
        firstTime = nil

        state.ClearCycle()
        state:ResetVariables()
        -- This will be our comprehensive resetter.
        state:ResetQueues()

        -- TODO:  Review.  How many of these are necessary?
        state.resetting = true

        ns.callHook( "reset_preauras" )

        if state.target.updated then
            ScrapeUnitAuras( "target" )
            state.target.updated = false
        end

        if state.player.updated then
            ScrapeUnitAuras( "player" )
            state.player.updated = false
        end

        local p = Hekili.DB.profile

        local display = dispName and p.displays[ dispName ]
        local spec = state.spec.id and p.specs[ state.spec.id ]
        local mode = p.toggles.mode.value

        state.display = dispName
        state.filter = "none"
        state.rangefilter = false

        if display then
            if dispName == 'Primary' then
                if mode == "single" or mode == "dual" or mode == "reactive" then state.max_targets = 1
                elseif mode == "aoe" then state.min_targets = spec and spec.aoe or 3 end
                state.filter = "none"
            elseif dispName == 'AOE' then
                state.min_targets = spec and spec.aoe or 3
                state.filter = "none"
            elseif dispName == 'Cooldowns' then state.filter = "cooldowns"
            elseif dispName == 'Interrupts' then state.filter = "interrupts"
            elseif dispName == 'Defensives' then state.filter = "defensives"
            end

            state.rangefilter = display.range.enabled and display.range.type == "xclude"
        end

        -- Trying again to have partial resets for the low-impact (single icon displays).
        if not full then
            state.resetting = false

            displayFrame.resetShortLast = debugprofilestop() - started

            if displayFrame.resetShortN then
                displayFrame.resetShortMean = ( displayFrame.resetShortMean * displayFrame.resetShortN + displayFrame.resetShortLast ) / ( displayFrame.resetShortN + 1 )
                displayFrame.resetShortN = displayFrame.resetShortN + 1
            else
                displayFrame.resetShortMean = displayFrame.resetShortLast
                displayFrame.resetShortN = 1
            end

            displayFrame.resetTypeLast = "short"
            return
        end

        -- TODO: Determine if we can Mark/Purge these tables instead of having their own resets.
        for k in pairs( class.stateTables ) do
            if rawget( state[ k ], "onReset" ) then state[ k ].onReset( state[ k ] ) end
        end

        -- Hekili:Yield( "Reset Post-States" )

        for i = 1, 5 do
            local _, _, start, duration, icon = GetTotemInfo( i )
            if icon and class.totems[ icon ] then
                summonPet( class.totems[ icon ], start + duration - state.now )
            end
        end

        -- TODO: These could be cleaner but also it doesn't matter.
        wipe( state.predictions )
        wipe( state.predictionsOn )
        wipe( state.predictionsOff )
        wipe( state.history.casts )
        wipe( state.history.units )

        if state.time == 0 and InCombatLockdown() then
            local a = state.player.lastcast and class.abilities[ state.player.lastcast ]
            if a and a.startsCombat and state.now - a.lastCast < 1 then
                state.false_start = a.lastCast - 0.01
                if Hekili.ActiveDebug then Hekili:Debug( format( "Starting combat based on %s cast; time is now: %.2f.", state.player.lastcast, state.time ) ) end
            end
        end

        local foundResource = false

        -- Hekili:Yield( "Reset Pre-Powers" )

        for k, power in pairs( class.resources ) do
            local res = rawget( state, k )

            if res then
                res.actual = UnitPower( "player", power.type )
                res.max = UnitPowerMax( "player", power.type )

                if res.max > 0 then foundResource = true end

                if k == "mana" and state.spec.arcane then
                    res.modmax = res.max / ( 1 + state.mastery_value )
                end

                res.last_tick = rawget( res, "last_tick" ) or 0
                res.tick_rate = rawget( res, "tick_rate" ) or 0.1

                if power.type == Enum.PowerType.Mana then
                    local inactive, active = GetManaRegen()

                    res.active_regen = active or 0
                    res.inactive_regen = inactive or 0
                    res.regen = nil
                else
                    if ResourceRegenerates( k ) then
                        local inactive, active = GetPowerRegenForPowerType( power.type )
                        res.active_regen = active or 0.001
                        res.inactive_regen = inactive or 0.001
                        res.regen = nil
                    else
                        res.regen = 0.001
                    end
                end

                if res.reset then res.reset() end
                forecastResources( k )
            end
        end

        if not foundResource then
            state.resetting = false
            displayFrame.resetAbortLast = debugprofilestop() - started

            if displayFrame.resetAbortN then
                displayFrame.resetAbortMean = ( displayFrame.resetAbortMean * displayFrame.resetAbortN + displayFrame.resetAbortLast ) / ( displayFrame.resetAbortN + 1 )
                displayFrame.resetAbortN = displayFrame.resetAbortN + 1
            else
                displayFrame.resetAbortMean = displayFrame.resetAbortLast
                displayFrame.resetAbortN = 1
            end

            displayFrame.resetTypeLast = "abort"
            return false, "no available resources"
        end

        -- Hekili:Yield( "Reset Post-Powers" )

        -- Setting this here because the metatable would pull from UnitPower.
        if not state.health.initialized then
            state.health.resource = "health"
            state.health.meta = {}
            state.health.percent = nil
            setmetatable( state.health, mt_resource )
            state.health.initialized = true
        end
        state.health.current = nil
        state.health.actual = UnitHealth( "player" ) or 10000
        state.health.max = max( 1, UnitHealthMax( "player" ) or 10000 )
        state.health.regen = 0.001

        -- TODO: All of this stuff for swings is terrible.
        state.swings.mh_speed, state.swings.oh_speed = UnitAttackSpeed( "player" )
        state.swings.mh_speed = state.swings.mh_speed or 0
        state.swings.oh_speed = state.swings.oh_speed or 0

        state.mainhand_speed = state.swings.mh_speed or 0
        state.offhand_speed = state.swings.oh_speed or 0

        state.nextMH = ( state.combat > 0 and state.swings.mh_actual > state.combat and state.swings.mh_actual + state.mainhand_speed ) or 0
        state.nextOH = ( state.combat > 0 and state.swings.oh_actual > state.combat and state.swings.oh_actual + state.offhand_speed ) or 0

        state.swings.mh_pseudo = nil
        state.swings.oh_pseudo = nil

        -- Special case spells that suck.
        if class.abilities[ "ascendance" ] and state.buff.ascendance.up then
            state.setCooldown( "ascendance", state.buff.ascendance.remains + 165 )
        end

        --[[ Trinkets that need special handling.
        if state.buff.stormeaters_boon.up and state.debuff.rooted.down then
            state.applyDebuff( "player", "rooted", state.buff.stormeaters_boon.remains )
        end

        -- BUGS: Windscar Whetstone invisibly keeps trinkets on CD for 6 additional seconds.
        local ww_cd_remains = state.action.windscar_whetstone.lastCast + 26 - state.now
        if ww_cd_remains > 0 then
            state.putTrinketsOnCD( ww_cd_remains )
        end

        -- TODO: Move this all to those aura generator functions.
        if state.set_bonus.cache_of_acquired_treasures > 0 then
            -- This required changing how buffs are tracked (that applied time is greater than the query time, which was always just expected to be true before).
            -- If this remains problematic, use QueueAuraExpiration instead.
            if state.buff.acquired_sword.up then
                state.applyBuff( "acquired_axe" )
                state.buff.acquired_axe.expires = state.buff.acquired_sword.expires + 12
                state.buff.acquired_axe.applied = state.buff.acquired_sword.expires
                state.applyBuff( "acquired_wand" )
                state.buff.acquired_wand.expires = state.buff.acquired_axe.expires + 12
                state.buff.acquired_wand.applied = state.buff.acquired_axe.expires
            elseif state.buff.acquired_axe.up then
                state.applyBuff( "acquired_wand" )
                state.buff.acquired_wand.expires = state.buff.acquired_axe.expires + 12
                state.buff.acquired_wand.applied = state.buff.acquired_axe.expires
                state.applyBuff( "acquired_sword" )
                state.buff.acquired_sword.expires = state.buff.acquired_wand.expires + 12
                state.buff.acquired_sword.applied = state.buff.acquired_wand.expires
            elseif state.buff.acquired_wand.up then
                state.applyBuff( "acquired_sword" )
                state.buff.acquired_sword.expires = state.buff.acquired_wand.expires + 12
                state.buff.acquired_sword.applied = state.buff.acquired_wand.expires
                state.applyBuff( "acquired_axe" )
                state.buff.acquired_axe.expires = state.buff.acquired_sword.expires + 12
                state.buff.acquired_axe.applied = state.buff.acquired_sword.expires
            end
        end ]]

        -- Hekili:Yield( "Reset Pre-Cast Hook" )
        ns.callHook( "reset_precast" )
        -- Hekili:Yield( "Reset Pre-Casting" )

        -- TODO: All of this cast-queuing seems like it should be simpler, but that's for another time.
        local cast_time, casting, ability = 0, nil, nil
        state.buff.casting.generate( state.buff.casting, "buff" )

        -- print( state.display, GetTime(), "Casting: ", state.buff.casting.name, state.buff.casting.applied, state.buff.casting.expires, state.buff.casting.remains )

        if state.buff.casting.up then
            cast_time = state.buff.casting.remains

            local castID = state.buff.casting.v1
            ability = class.abilities[ castID ] or class.abilities[ state.buff.casting.name ]

            if ability then
                casting = ability.key

                if castID == class.abilities.cyclotronic_blast.id then
                    -- Set up Pocket-Sized Computation Device.
                    if state.buff.casting.v3 == 1 then
                        -- We are in the channeled part of the cast.
                        setCooldown( "pocketsized_computation_device", state.buff.casting.applied + 120 - state.now )
                        setCooldown( "global_cooldown", cast_time )
                    else
                        -- This is the casting portion.
                        casting = class.abilities.pocketsized_computation_device.key
                        state.buff.casting.v1 = class.abilities.pocketsized_computation_device.id
                    end
                end
            end
        end

        -- Okay, two paths here.
        -- 1.  We can cast while casting (i.e., Fire Blast for Fire Mage), so we want to hand off the current cast to the event system, and then let the recommendation engine sort it out.
        -- 2.  We cannot cast anything while casting (typical), so we want to advance the clock, complete the cast, and then generate recommendations.

        if casting and cast_time > 0 then
            local channeled, destGUID = state.buff.casting.v3 == 1

            if ability then
                channeled = channeled or ability.channeled
                destGUID  = Hekili:GetMacroCastTarget( ability.key, state.buff.casting.applied, "RESET" ) or state.target.unit
            end

            if not state:IsCasting() and not channeled then
                state:QueueEvent( casting, state.buff.casting.applied, state.buff.casting.expires, "CAST_FINISH", destGUID )

                -- Projectile spells have two handlers, effectively.  An onCast handler, and then an onImpact handler.
                if ability and ability.isProjectile then
                    state:QueueEvent( ability.key, state.buff.casting.expires, nil, "PROJECTILE_IMPACT", destGUID )
                    -- state:QueueEvent( action, "projectile", true )
                end

            elseif not state:IsChanneling() and channeled then
                state:QueueEvent( casting, state.buff.casting.applied, state.buff.casting.expires, "CHANNEL_FINISH", destGUID )

                if ability then
                    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 next_tick = state.buff.casting.applied + tick_time
                        local expires = state.buff.cast.expires

                        while( next_tick < expires ) do
                            state:QueueEvent( casting, state.buff.casting.applied, next_tick, "CHANNEL_TICK", destGUID )
                            next_tick = next_tick + tick_time
                        end
                    end

                        -- Projectile spells have two handlers, effectively.  An onCast handler, and then an onImpact handler.
                    if ability.isProjectile then
                        state:QueueEvent( ability.key, state.buff.casting.expires, nil, "PROJECTILE_IMPACT", destGUID )
                        -- state:QueueEvent( action, "projectile", true )
                    end
                end
            end

            -- Delay to end of GCD.
            if dispName == "Primary" or dispName == "AOE" then
                local delay = 0

                if not state.spec.can_dual_cast and state.buff.casting.up and state.buff.casting.v3 ~= 1 then -- v3=1 means it's channeled.
                    delay = max( delay, state.buff.casting.remains )
                end

                delay = ns.callHook( "reset_postcast", delay )

                if delay > 0 then
                    if Hekili.ActiveDebug then Hekili:Debug( "Advancing by %.2f per GCD or cast or channel or reset_postcast value.", delay ) end
                    state.advance( delay )
                end
            end

            state.resetType = "none"
        end

        -- Hekili:Yield( "Reset Post-Casting" )

        state.resetting = false

        displayFrame.resetFullLast = debugprofilestop() - started

        if displayFrame.resetFullN then
            displayFrame.resetFullMean = ( displayFrame.resetFullMean * displayFrame.resetFullN + displayFrame.resetFullLast ) / ( displayFrame.resetFullN + 1 )
            displayFrame.resetFullN = displayFrame.resetFullN + 1
        else
            displayFrame.resetFullMean = displayFrame.resetFullLast
            displayFrame.resetFullN = 1
        end

        displayFrame.resetTypeLast = "full"
        return true
    end
end
Hekili:ProfileCPU( "state.reset", state.reset )


function state:SetConstraint( min, max )
    if min then state.delayMin = min end
    if max then state.delayMax = max end
end


function state:SetWhitelist( t )
    state.whitelist = t
end


function state:StartCombat()
    if self.time == 0 then
        self.false_start = self.query_time - 0.01
        if Hekili.ActiveDebug then Hekili:Debug( format( "Starting combat at %.2f -- time is %.2f.", self.false_start, self.time ) ) end
    end

    local swing = false

    if self.swings.mainhand == 0 and self.swings.mainhand_speed > 0 then
        self.swings.mh_pseudo = self.query_time
        swing = true
    end

    if self.swings.offhand == 0 and self.swings.offhand_speed > 0 then
        self.swings.oh_pseudo = self.query_time + ( self.swings.offhand_speed / 2 )
        swing = true
    end

    if swing then self:ForecastSwingbasedResources() end
end


function state.advance( time )
    if not state.resetting and not state.modified then
        state.modified = true
    end

    time = ns.callHook( "advance", time ) or time
    if not state.resetting then time = roundUp( time, 3 ) end

    state.delay = 0

    local realOffset = state.offset

    if state.player.queued_ability then
        local lands = max( state.now + 0.01, state.player.queued_lands )

        if lands > state.query_time and lands <= state.query_time + time then
            state.offset = lands - state.query_time
            state:RunHandler( state.player.queued_ability, true )

            state.offset = realOffset
        end
    end

    local events = state:GetQueue()
    local event = events[ 1 ]

    local eCount = 0

    while( event ) do
        if event.time <= state.query_time + time then
            state.offset = event.time - state.now

            if Hekili.ActiveDebug then Hekili:Debug( "While advancing by %.2f to %.2f, %s %s occurred at %.2f.", time, realOffset + time, event.action, event.type, state.offset ) end

            state:HandleEvent( event )

            event = events[ 1 ]
            state.offset = realOffset
        else
            break
        end

        eCount = eCount + 1
        if eCount == 10 then break end
    end

    if time <= 0 then
        return
    end

    for k in pairs( class.resources ) do
        local resource = state[ k ]

        if not resource.regenModel then
            local override = ns.callHook( "advance_resource_regen", false, k, time )

            local regen = resource.regen
            if regen == 0.001 then regen = 0 end

            if not override and resource.regen and regen ~= 0 then
                resource.actual = min( resource.max, max( 0, resource.actual + ( regen * time ) ) )
            end
        else
            -- revisit this, may want to forecastResources( k ) instead.
            state.delay = time
            resource.actual = resource.current
            state.delay = 0
        end
    end

    state.offset = state.offset + time
    time = ns.callHook( "advance_end", time ) or time
    return time
end


function state.GetResourceType( ability )
    local action = class.abilities[ ability ]

    if not action then return end

    if action.spend ~= nil then
        if type( action.spend ) == "number" then
            return action.spendType or class.primaryResource

        elseif type( action.spend ) == "function" then
            return select( 2, action.spend() ) or action.spendType or class.primaryResource

        end
    end

    return nil
end


ns.spendResources = function( ability )
    local action = class.abilities[ ability ]

    if not action then return end

    -- First, spend resources.
    if action.spend ~= nil then
        local cost, resource

        if type( action.spend ) == "number" then
            cost = action.spend
            resource = action.spendType or class.primaryResource
        elseif type( action.spend ) == "function" then
            cost, resource = action.spend()
            resource = resource or action.spendType or class.primaryResource
        else
            cost = cost or 0
            resource = resource or "health"
        end

        if cost > 0 and cost < 1 then
            cost = ( cost * state[ resource ].modmax )
        end

        if cost ~= 0 then
            state.spend( cost, resource )
        end
    end
end
state.SpendResources = ns.spendResources


do
    local HOLD_PERMANENT = 1
    local HOLD_COMBAT    = 2

    function Hekili:PlaceHold( action, combat, verbose )
        if not action then return end

        action = action:trim()
        local ability = class.abilities[ action ]

        if not ability then
            action = action:lower()
            -- Try to auto-complete.
            for k, v in orderedPairs( class.abilities ) do
                if type(k) == "string" and k:sub( 1, action:len() ):lower() == action then
                    action = v.key
                    ability = class.abilities[ action ]
                    break
                end
            end
        end

        if ability then
            state.holds[ ability.key ] = combat and HOLD_COMBAT or HOLD_PERMANENT
            if verbose then Hekili:Print( class.abilities[ ability.key ].name .. " placed on hold" .. ( combat and " until end of combat." or "." ) ) end
            Hekili:ForceUpdate( "HEKILI_HOLD_APPLIED" )
        end
    end

    function Hekili:RemoveHold( action, verbose )
        if not action then return end

        action = action:trim()
        local ability = class.abilities[ action ]

        if not ability then
            action = action:lower()
            -- Try to auto-complete.
            for k, v in orderedPairs( class.abilities ) do
                if type(k) == "string" and k:sub( 1, action:len() ):lower() == action then
                    action = v.key
                    ability = class.abilities[ action ]
                    break
                end
            end
        end

        if ability and state.holds[ ability.key ] then
            state.holds[ ability.key ] = nil
            if verbose then Hekili:Print( class.abilities[ ability.key ].name .. " hold removed." ) end
            Hekili:ForceUpdate( "HEKILI_HOLD_REMOVED" )
        end
    end

    function Hekili:ToggleHold( action, combat, verbose )
        if self:IsHeld( action ) then
            self:RemoveHold( action, verbose )
            return
        end

        self:PlaceHold( action, combat, verbose )
    end

    function Hekili:IsHeld( action )
        action = action and action:trim()
        local ability = class.abilities[ action ]

        if not ability then
            action = action:lower()
            -- Try to auto-complete.
            for k, v in orderedPairs( class.abilities ) do
                if type(k) == "string" and k:sub( 1, action:len() ):lower() == action then
                    action = v.key
                    ability = class.abilities[ action ]
                    break
                end
            end
        end

        if ability and state.holds[ ability.key ] then
            return true, state.holds[ ability.key ]
        end

        return false
    end

    function Hekili:ReleaseHolds( combat )
        local holdRemoved = false

        for k, v in pairs( state.holds ) do
            if not combat or v == HOLD_COMBAT then
                state.holds[ k ] = nil
                holdRemoved = true
            end
        end

        if holdRemoved then Hekili:ForceUpdate( "HEKILI_COMBAT_HOLD_REMOVED" ) end
    end
end


function state:IsKnown( sID )
    local original = sID
    if type(sID) ~= "number" then sID = class.abilities[ sID ] and class.abilities[ sID ].id or nil end

    if not sID then
        return false, "could not find valid ID" -- no ability
    end

    local ability = class.abilities[ sID ]

    if not ability then
        Error( "IsKnown() - " .. tostring( sID ) .. " / " .. original .. " not found in abilities table.\n\n" .. debugstack() )
        return false, format( "%s / %s not found in abilities table", tostring( original ), tostring( sID ) )
    end

    if IsAbilityDisabled( ability ) then return false, "not usable here" end

    local z -- scratch variable

    if sID < 0 then
        if ability.known ~= nil then
            if type( ability.known ) == "number" then
                return IsUsableItem( ability.known ), "IsUsableItem known"
            end
            return ability.known
        end

        if ability.item and ability.key ~= "potion" then
            return IsUsableItem( ability.item ), "IsUsableItem item " .. ability.item .. " and " .. ( tostring( ability.known ) or "nil" )
        end

        return true
    end

    if IsDisabledCovenantSpell( sID ) then return false, "covenant spells are disabled" end

    if ability.spec and not state.spec[ ability.spec ] then
        return false, "wrong specialization"
    end

    if ability.nospec and state.spec[ ability.nospec ] then
        return false, "spec [ " .. ability.nospec .. " ] disallowed"
    end

    z = ability.talent
    if type( z ) == "table" then
        for _, v in ipairs( z ) do
            if not state.talent[ v ].enabled then
                return false, "talent [ " .. v .. " ] missing"
            end
        end
    elseif z and not state.talent[ z ].enabled then
        return false, "talent [ " .. z .. " ] missing"
    end

    z = ability.notalent
    if type( z ) == "table" then
        for _, v in ipairs( z ) do
            if state.talent[ v ].enabled then
                return false, "talent [ " .. v .. " ] disallowed"
            end
        end
    elseif z and state.talent[ z ].enabled then
        return false, "talent [ " .. z .. " ] disallowed"
    end

    z = ability.pvptalent
    if type( z ) == "table" then
        for _, v in ipairs( z ) do
            if not state.pvptalent[ v ].enabled then
                return false, "PvP talent [ " .. v .. " ] missing"
            end
        end
    elseif z and not state.pvptalent[ z ].enabled then
        return false, "PvP talent [ " .. z .. " ] missing"
    end

    z = ability.nopvptalent
    if type( z ) == "table" then
        for _, v in ipairs( z ) do
            if state.pvptalent[ v ].enabled then
                return false, "PvP talent [ " .. v .. " ] disallowed"
            end
        end
    elseif z and state.pvptalent[ z ].enabled then
        return false, "PvP talent [ " .. z .. " ] disallowed"
    end

    if ability.trait and not state.artifact[ ability.trait ].enabled then
        return false, "trait [ " .. ability.trait .. " ] missing"
    end

    if ability.equipped and not state.equipped[ ability.equipped ] then
        return false, "equipment [ " .. ability.equipped .. " ] missing"
    end

    if ability.item and not ability.bagItem and not state.equipped[ ability.item ] then
        return false, "item [ " .. ability.item .. " ] missing"
    end

    if ability.noOverride and IsSpellKnownOrOverridesKnown( ability.noOverride ) then
        return false, "override [ " .. ability.noOverride .. " ] disallowed"
    end

    if ability.known ~= nil then
        if type( ability.known ) == "number" then
            return IsPlayerSpell( ability.known ) or IsSpellKnownOrOverridesKnown( ability.known ) or IsSpellKnown( ability.known, true )
        end
        return ability.known
    end

    return IsPlayerSpell( sID ) or IsSpellKnownOrOverridesKnown( sID ) or IsSpellKnown( sID, true )

end



do
    local toggleSpells = {
        potion = true,
        cancel_buff = true,
        phial_of_serenity = true,
    }

    -- If an ability has been manually disabled, don't consider it.
    function state:IsDisabled( spell, strict )
        spell = spell or self.this_action

        local ability = class.abilities[ spell ]
        if not ability then return false end

        if ability.id > -100 and ability.id < 0 then
            return false, "internal function"
        end

        spell = ability.key

        local profile = Hekili.DB.profile
        local spec = rawget( profile.specs, state.spec.id )
        if not spec then return true end

        local option = ability.item and spec.items[ spell ] or spec.abilities[ spell ]

        if ability.item and ability.toggle ~= "potions" then
            local sp = rawget( profile.specs, state.spec.id )
            if sp and sp.disable_items then
                return true, "preference - spec disables gear/items"
            end
        end

        if not strict then
            local toggle = option.toggle
            if not toggle or toggle == "default" then toggle = ability.toggle end

            if ( toggle == "potion" or toggle == "essences" ) and profile.toggles[ toggle ].separate and not profile.toggles[ toggle ].value then toggle = "cooldowns" end

            if toggle and toggle ~= "none" and ( not self.toggle[ toggle ] or ( profile.toggles[ toggle ].separate and state.filter ~= toggle ) ) then return true, format( "toggle %s", toggle ) end

            if ability.id < -100 or ability.id > 0 or toggleSpells[ spell ] then
                if state.filter ~= "none" and state.filter ~= toggle and not ability[ state.filter ] then return true, "display"
                elseif ability.item and not ability.bagItem and not state.equipped[ ability.item ] then return false end
            end
        end

        if ability.disabled then return true, "disabled per ability function" end
        if self.holds[ spell ] then return true, "on hold" end
        if option.disabled then return true, "preference" end
        if option.boss and not state.boss then return true, "boss-only" end
        if option.targetMin > 0 and self.active_enemies < option.targetMin then
            return true, "active_enemies[" .. self.active_enemies .. "] is less than ability's minimum targets [" .. option.targetMin .. "]"
        elseif option.targetMax > 0 and self.active_enemies > option.targetMax then
            return true, "active_enemies[" .. self.active_enemies .. "] is more than ability's maximum targets [" .. option.targetMax .. "]"
        end

        -- New: DoT Cap
        local dotCap = option.dotCap
        if dotCap and dotCap > 0 and class.auras[ spell ] then
            local activeDot = state.active_dot[ spell ]
            local aura = state.dot[ spell ]
            if activeDot and activeDot >= dotCap then
                if aura and not aura.refreshable then
                    return true, "applications[" .. activeDot .. "] meet/exceed the ability's maximum targets [" .. dotCap .. "]"
                end
            end

            if Hekili.ActiveDebug then
                if activeDot < dotCap then
                    Hekili:Debug( "    %s: current applications fewer than maximum applications [%d/%d].", spell, activeDot, dotCap )
                elseif aura and aura.refreshable then
                    Hekili:Debug( "    %s: target refreshable despite maximum applications [%d/%d]", spell, activeDot, dotCap )
                end
            end
        end

        return false
    end

    -- TODO:  Finish this, need to support toggles that knock spells to their own display vs. toggles that disable an ability entirely.
    function state:IsFiltered( spell )
        spell = spell or self.this_action

        local ability = class.abilities[ spell ]
        if not ability then return false end

        if ability.id > -100 and ability.id < 0 then
            return false, "internal function"
        end

        spell = ability.key

        if state.filter == "none" then return false end

        local profile = Hekili.DB.profile
        local spec = rawget( profile.specs, state.spec.id )
        if not spec then return true end

        local option = ability.item and spec.items[ spell ] or spec.abilities[ spell ]
        local toggle = option.toggle
        if not toggle or toggle == "default" then toggle = ability.toggle end

        if ( toggle == "potion" or toggle == "essences" ) and profile.toggles[ toggle ].separate and not profile.toggles[ toggle ].value then toggle = "cooldowns" end

        if state.buff.empowering.up and state.buff.empowering.spell ~= spell then return true, "empowerment" end
        if state.filter ~= "none" and state.filter ~= toggle and not ability[ state.filter ] then return true, "display" end
        if ability.item and not ability.bagItem and not state.equipped[ ability.item ] then return false, "not equipped" end
        if toggleSpell[ spell ] and toggle and toggle ~= "none" then
            if not self.toggle[ toggle ] or ( profile.toggles[ toggle ].separate and state.filter ~= toggle and not spec.noFeignedCooldown ) then return true, format( "%s filtered", toggle ) end
        end

        return false
    end


    -- Filter out non-resource driven issues with abilities.
    -- Unusable abilities are treated as on CD unless overridden.
    function state:IsUsable( spell )
        spell = spell or self.this_action

        local ability = class.abilities[ spell ]
        if not ability then return true end

        local hook, reason = ns.callHook( "IsUsable", spell )
        if hook == false then
            return false, reason
        end

        if ability.funcs.usable then
            local usable, reason = ability.funcs.usable( self, ability )
            if usable == false then -- Have allowed nil return values for usable to be treated as usable before.
                return false, reason
            end
        else
            local usable = ability.usable
            if type( usable ) == "number" and not IsUsableSpell( usable ) then
                return false, "IsUsableSpell(" .. usable .. ") was false"
            elseif type( usable ) == "boolean" and not usable then
                return false, "ability.usable was false"
            end
        end

        local profile = Hekili.DB.profile
        local z -- scratch variable

        if self.rangefilter and UnitExists( "target" ) then
            if LSR.IsSpellInRange( ability.rangeSpell or ability.id, "target" ) == 0 then
                return false, "filtered out of range"
            end

            if ability.range then
                local _, dist = RC:GetRange( "target" )

                if dist and dist > ability.range then
                    return false, "not within ability-specified range (" .. ability.range .. ")"
                end
            end
        end

        if ability.item then
            if not ability.bagItem and not self.equipped[ ability.item ] then
                return false, "item not equipped"
            end
        end

        if ability.disabled then
            return false, "ability.disabled returned true"
        end

        if ability.empowered and self.args.empower_to and self.args.empower_to > self.max_empower then
            return false, "empowerment level " .. self.args.empower_to .. " not available"
        end

        if self.args.only_cwc and ( not self.buff.casting.up or self.buff.casting.v3 ~= 1 or not ability.dual_cast ) then
            return false, "only castable while channeling"
        end

        if ability.nomounted and IsMounted() then
            return false, "not recommended while mounted"
        end

        if ability.nocombat and self.time > 0 then
            return false, "not usable in combat"
        end

        if ability.form and not state.buff[ ability.form ].up then
            return false, "required form (" .. ability.form .. ") not active"
        end

        if ability.noform and state.buff[ ability.noform ].up then
            return false, "not usable in current form (" .. ability.noform .. ")"
        end

        z = ability.buff
        if type( z ) == "table" then
            for _, v in ipairs( z ) do
                if not state.buff[ v ].up then
                    return false, "required buff (" .. v .. ") not active"
                end
            end
        elseif z and not state.buff[ z ].up then
            return false, "required buff (" .. z .. ") not active"
        end

        z = ability.debuff
        if type( z ) == "table" then
            for _, v in ipairs( z ) do
                if not state.debuff[ v ].up then
                    return false, "required debuff (" .. v .. ") not active"
                end
            end
        elseif z and not state.debuff[ z ].up then
            return false, "required debuff (" .. z .. ") not active"
        end

        if ability.channeling then
            local c = class.abilities[ ability.channeling ] and class.abilities[ ability.channeling ].id

            if not c or state.buff.casting.remains < 0.1 or state.buff.casting.v3 ~= 1 or state.buff.casting.v1 ~= c then
                return false, "required channel (" .. c .. " / " .. ability.channeling .. ") not active or too short [ " .. state.buff.casting.remains .. " / " .. state.buff.casting.applied .. " / " .. state.buff.casting.expires .. " / " .. state.query_time .. " / " .. tostring( state.buff.casting.v3 ) .. " / " .. state.buff.casting.v1 .. " ]"
            end
        end

        if self.args.moving == 1 and state.buff.movement.down then
            return false, "entry requires movement and player is not moving"
        end

        if self.args.moving == 0 and state.buff.movement.up then
            return false, "entry requires no movement and player is moving"
        end

        -- Moved this into TimeToReady; we can see when the buff falls off.
        --[[ if ability.nobuff and state.buff[ ability.nobuff ].up then
            return false
        end ]]

        return true
    end

end

ns.hasRequiredResources = function( ability )
    local action = class.abilities[ ability ]

    if not action then return end

    -- First, spend resources.
    if action.spend and action.spend ~= 0 then
        local spend, resource

        if type( action.spend ) == "number" then
            spend = action.spend
            resource = action.spendType or class.primaryResource
        elseif type( action.spend ) == "function" then
            spend, resource = action.spend()
        end

        if resource == "focus" or resource == "energy" then
            -- Thought: We'll already delay CD based on time to get energy/focus.
            -- So let's leave it alone.
            return true
        end

        if spend > 0 and spend < 1 then
            spend = ( spend * state[ resource ].modmax )
        end

        if spend > 0 then
            return ( state[ resource ].current >= spend )
        end
    end

    return true
end
function state:HasRequiredResources( action )
    return ns.hasRequiredResources( action )
end


local power_tick_rate = 0.115


--[[ Really dumb timing tool to find what was bottlenecking TTR.
local longestTTR = 0

local lastTime, lastName = 0, "nothing"
local longTime, longName = 0, "nothing"

local function profilestop( name, reset )
    local newTime = debugprofilestop()

    if reset then
        lastTime = newTime
        lastName = name

        longTime = 0
        longName = name

        return lastTime
    end

    if newTime - lastTime > longTime then
        longTime = newTime - lastTime
        longName = name
    end

    lastTime = newTime
    return newTime
end ]]


function state:TimeToReady( action, pool )
    local now = self.now + self.offset
    action = action or self.this_action
    local ability = class.abilities[ action ]
    if not ability then return 3600, "not modeled" end

    if ability.id > -100 and ability.id < 0 then
        return 0, "internal function"
    end

    local delay = state.delay
    state.delay = 0

    -- Need to ignore the wait for this part.
    local wait = self.cooldown[ action ].remains

    -- Early exit for timeToReadyOverride. This is to fix the fact that some spells show as unavailable in game even though
    -- the addon knows that it will become available on the next GCD due to a guaranteed buff/proc
    --[[ Commenting out for now. After 11.1 review is finished we can revisit
    if ability.timeToReadyOverride then
        local override = ability.timeToReadyOverride
        if override ~= nil then
            override = max( override, self.cooldown.global_cooldown.remains )
            if Hekili.ActiveDebug then
                Hekili:Debug( "%s has a timeToReadyOverride. Returning %.2f.", action, override )
            end
            return max( override, self.delayMin )
        end
    end
    --]]

    -- Working variable.
    local z = ability.id

    -- Don't use before the GCD expires, unless:
    -- 1. The "use_off_gcd" flag is set in the priority.
    -- 2. The ability is flagged as an interrupt or defensive.
    local requires = ability.toggle
    if requires ~= "interrupts" and requires ~= "defensives" and not self.safebool( self.args.use_off_gcd ) then
        wait = max( wait, self.cooldown.global_cooldown.remains )
    end

    if not state.channel_breakable and not ability.dual_cast then
        z = self.buff.casting.remains
        if z > wait then
            wait = z
        end
    end

    if self.buff.empowering.up then
        if self.buff.empowering.spell == ability.key then return ability.cast end
        wait = max( wait, ability.cast )
    end

    local spend, resource = ability.spend
    if spend then
        if type( spend ) == "number" then
            resource = ability.spendType or class.primaryResource
        elseif type( spend ) == "function" then
            spend, resource = spend()
            resource = resource or ability.spendType or class.primaryResource
        end

        spend = spend or 0
    end

    if spend and resource and spend > 0 and spend < 1 then
        spend = spend * self[ resource ].modmax
    end

    if not pool then
        z = ability.readyTime
        if z and z > wait then
            wait = z
        end

        z = ability.readySpend
        if z then
            spend = z
        end
    end

    -- Okay, so we don't have enough of the resource.
    z = resource and self[ resource ]
    z = z and z[ "time_to_" .. spend ]
    if spend and z and z > wait then
        wait = max( wait, ceil( z * 100 ) / 100 )
    end

    z = ability.nobuff
    if type( z ) == "table" then
        for _, v in ipairs( z ) do
            z = self.buff[ v ].remains
            if z and z > wait then
                wait = z
            end
        end
    else
        z = z and self.buff[ z ].remains
        if z and z > wait then
            wait = z
        end
    end

    z = ability.nodebuff
    if type( z ) == "table" then
        for _, v in ipairs( z ) do
            z = self.debuff[ v ].remains
            if z and z > wait then
                wait = z
            end
        end
    else
        z = z and self.debuff[ z ].remains
        if z and z > wait then
            wait = z
        end
    end

    --[[ Need to house this in an encounter module, really.
    z = self.debuff.repeat_performance.remains
    if z and z > 0 and self.prev[1][ action ] then
        wait = max( wait, z )
    end
    profilestop( "post-repeat" ) ]]


    -- If ready is a function, it returns time.
    -- Ignore this if we are just checking pool_resources.

    --[[ if state.spec.fire and state.buff.casting.up and ( ability.id > 0 or ability.id < -99 ) and ability.gcd ~= "off" and not ability.dual_cast then
        wait = max( wait, state.buff.casting.remains )
    end
    profilestop( "post-casting" ) ]]

    z = ability.timeToReady
    if z and z > wait then
        wait = z
    end

    local lastCast = ability.lastCast

    z = ability.icd
    z = z and z + lastCast - now
    if z and z > wait then
        if Hekili.ActiveDebug then Hekili:Debug( "ICD is " .. z .. ", last cast was " .. lastCast .. ", remaining CD: " .. max( 0, lastCast + z - now ) ) end
        wait = z
    end

    local line_cd = self.args.line_cd
    if self.time > 0 and lastCast > max( self.combat, self.false_start ) and line_cd and type( line_cd ) == "number" then
        if Hekili.ActiveDebug then Hekili:Debug( "Line CD is " .. line_cd .. ", last cast was " .. lastCast .. ", remaining CD: " .. max( 0, lastCast + line_cd - now ) ) end
        wait = max( wait, lastCast + line_cd - now )
    end

    local sync = state.args.sync
    local synced = sync and class.abilities[ sync ]

    if synced and sync ~= action and state:IsKnown( sync ) then
        wait = max( wait, state:TimeToReady( sync ) )
    end

    wait = ns.callHook( "TimeToReady", wait, action )

    state.delay = delay
    return max( wait, self.delayMin )
end


function state:IsReady( action )
    action = action or self.this_action
    local ability = action and class.abilities[ action ]

    if not ability then
        Hekili:Error( "Failed state:IsReady( " .. ( action or "BLANK" ) .. " )." )
        return false
    end

    if ability.spend then
        local spend, resource

        if type( ability.spend ) == "number" then
            spend = ability.spend
            resource = ability.spendType or class.primaryResource
        elseif type( ability.spend ) == "function" then
            spend, resource = ability.spend()
        end

        if resource == "focus" or resource == "energy" or state.script.entry then
            local ttr = self:TimeToReady( action )

            if ttr < self.delayMin or ttr > self.delayMax then return false, format( "not ready within  time contraint ( %.2f - %.2f )", self.delayMin, self.delayMax )
            elseif ttr >= self.selection_time then return false, format( "not ready [%.2f] before selected action %s [%.2f]", ttr, self.selected_action or "no_action", self.selection_time ) end

            return true
        end

    end

    return self:HasRequiredResources( action ) and self.cooldown[ action ].remains <= self.delay
end


function state:IsReadyNow( action )
    action = action or self.this_action
    local a = class.abilities[ action ]

    if not a then return false end

    action = a.key
    local profile = Hekili.DB.profile
    local spec = rawget( profile.specs, state.spec.id )
    if not spec then return false end

    local option = spec.abilities[ action ]
    local clash = option.clash or 0

    if self.cooldown[ action ].remains - clash > 0 then return false end
    local wait = ns.callHook( "TimeToReady", 0, action )
    if wait and wait > 0 then return false end

    if a.ready and type( a.ready ) == "function" and a.ready() > 0 then return false end

    if a.spend and a.spend ~= 0 then
        local spend, resource

        if type( a.spend ) == "number" then
            spend = a.spend
            resource = a.spendType or class.primaryResource

        elseif type( a.spend ) == "function" then
            spend, resource = a.spend()

        end

        if a.ready and type( a.ready ) == "number" then
            spend = a.ready
        end

        if spend > 0 and spend < 1 then
            spend = ( spend * state[ resource ].modmax )
        end

        if spend > 0 then
            return state[ resource ].current >= spend
        end
    end

    return true
end

function state:ClashOffset( action )
    local a = class.abilities[ action ]
    if not a then return 0 end
    action = a.key

    local spec = rawget( Hekili.DB.profile.specs, state.spec.id )
    if not spec then return 0 end

    local option = spec.abilities[ action ]

    return ns.callHook( "clash", option.clash, action )
end

for k, v in pairs( state ) do
    ns.commitKey( k )
end

ns.attr = { "serenity", "active", "active_enemies", "my_enemies", "active_flame_shock", "adds", "agility", "air", "armor", "attack_power", "bonus_armor", "cast_delay", "cast_time", "casting", "cooldown_react", "cooldown_remains", "cooldown_up", "crit_rating", "deficit", "distance", "down", "duration", "earth", "enabled", "energy", "execute_time", "fire", "five", "focus", "four", "gcd", "hardcasts", "haste", "haste_rating", "health", "health_max", "health_pct", "intellect", "level", "mana", "mastery_rating", "mastery_value", "max_nonproc", "max_stack", "maximum_energy", "maximum_focus", "maximum_health", "maximum_mana", "maximum_rage", "maximum_runic", "melee_haste", "miss_react", "moving", "mp5", "multistrike_pct", "multistrike_rating", "one", "pct", "rage", "react", "regen", "remains", "resilience_rating", "runic", "seal", "spell_haste", "spell_power", "spirit", "stack", "stack_pct", "stacks", "stamina", "strength", "this_action", "three", "tick_damage", "tick_dmg", "tick_time", "ticking", "ticks", "ticks_remain", "time", "time_to_die", "time_to_max", "travel_time", "two", "up", "water", "weapon_dps", "weapon_offhand_dps", "weapon_offhand_speed", "weapon_speed", "single", "aoe", "cleave", "percent", "last_judgment_target", "unit", "ready", "refreshable", "pvptalent", "conduit", "legendary", "runeforge", "covenant", "soulbind", "enabled", "full_recharge_time", "time_to_max_charges", "remains_guess", "execute", "actual", "current", "cast_regen", "boss", "exists", "disabled", "fight_remains", "last_used", "time_since", "max" }
