---@type string
local AddonName = ...
---@class Private
local Private = select(2, ...)

local internalVersion = 77

-- Lua APIs
local insert = table.insert

-- WoW APIs
local GetTalentInfo, InCombatLockdown = GetTalentInfo, InCombatLockdown
local UnitName, GetRealmName, UnitRace, UnitFactionGroup, IsInRaid
  = UnitName, GetRealmName, UnitRace, UnitFactionGroup, IsInRaid
local UnitClass, UnitExists, UnitGUID, UnitAffectingCombat, GetInstanceInfo, IsInInstance
  = UnitClass, UnitExists, UnitGUID, UnitAffectingCombat, GetInstanceInfo, IsInInstance
local UnitIsUnit, GetRaidRosterInfo, GetSpecialization, UnitInVehicle, UnitHasVehicleUI
  = UnitIsUnit, GetRaidRosterInfo, GetSpecialization, UnitInVehicle, UnitHasVehicleUI
local SendChatMessage, UnitInBattleground, UnitInRaid, UnitInParty, GetTime
  = SendChatMessage, UnitInBattleground, UnitInRaid, UnitInParty, GetTime
local CreateFrame, IsShiftKeyDown, GetScreenWidth, GetScreenHeight, GetCursorPosition, UpdateAddOnCPUUsage, GetFrameCPUUsage, debugprofilestop
  = CreateFrame, IsShiftKeyDown, GetScreenWidth, GetScreenHeight, GetCursorPosition, UpdateAddOnCPUUsage, GetFrameCPUUsage, debugprofilestop
local debugstack = debugstack
local GetNumTalentTabs, GetNumTalents = GetNumTalentTabs, GetNumTalents
local MAX_NUM_TALENTS = MAX_NUM_TALENTS or 20

local ADDON_NAME = "WeakAuras"
---@class WeakAuras
local WeakAuras = WeakAuras
local L = WeakAuras.L
local versionString = WeakAuras.versionString
local prettyPrint = WeakAuras.prettyPrint

WeakAurasTimers = setmetatable({}, {__tostring=function() return "WeakAuras" end})
LibStub("AceTimer-3.0"):Embed(WeakAurasTimers)

Private.maxTimerDuration = 604800; -- A week, in seconds
local maxUpTime = 4294967; -- 2^32 / 1000

Private.watched_trigger_events = {}

-- The worlds simplest callback system.
-- That supports 1:N, but no de-registration and breaks if registering in a callback
--- @class callbacks
--- @field events table
--- @field RegisterCallback fun(self: callbacks, event: string, handler: function)
--- @field Fire fun(self: callbacks, event: string, ... : any)
Private.callbacks = {}
Private.callbacks.events = {}

function Private.callbacks:RegisterCallback(event, handler)
  self.events[event] = self.events[event] or {}
  tinsert(self.events[event], handler)
end

function Private.callbacks:Fire(event, ...)
  if self.events[event] then
    for index, f in ipairs(self.events[event]) do
      f(event, ...)
    end
  end
end

function WeakAurasTimers:ScheduleTimerFixed(func, delay, ...)
  if (delay < Private.maxTimerDuration) then
    if delay + GetTime() > maxUpTime then
      WeakAuras.prettyPrint(WeakAuras.L["Can't schedule timer with %i, due to a World of Warcraft bug with high computer uptime. (Uptime: %i). Please restart your computer."]:format(delay, GetTime()))
      return
    end
    return self:ScheduleTimer(func, delay, ...)
  end
end

local LDB = LibStub("LibDataBroker-1.1")
local LDBIcon = LibStub("LibDBIcon-1.0")
local LCG = LibStub("LibCustomGlow-1.0")
local LGF = LibStub("LibGetFrame-1.0")

local CustomNames = C_AddOns.IsAddOnLoaded("CustomNames") and LibStub("CustomNames") -- optional addon
if CustomNames then
  WeakAuras.GetName = CustomNames.Get
  WeakAuras.UnitName = CustomNames.UnitName
  WeakAuras.GetUnitName = CustomNames.GetUnitName
  WeakAuras.UnitFullName = CustomNames.UnitFullName
else
  WeakAuras.GetName = function(name) return name end
  WeakAuras.UnitName = UnitName
  WeakAuras.GetUnitName = GetUnitName
  WeakAuras.UnitFullName = UnitFullName
end

local timer = WeakAurasTimers
WeakAuras.timer = timer

local loginQueue = {}
local queueshowooc

function WeakAuras.InternalVersion()
  return internalVersion;
end

do
  local currentErrorHandlerId
  local currentErrorHandlerUid
  local currentErrorHandlerContext
  local function waErrorHandler(errorMessage)
    local juicedMessage = {}
    local data
    if currentErrorHandlerId then
      data = WeakAuras.GetData(currentErrorHandlerId)
    elseif currentErrorHandlerUid then
      data = Private.GetDataByUID(currentErrorHandlerUid)
    end
    if data then
      Private.AuraWarnings.UpdateWarning(data.uid, "LuaError", "error",
        L["This aura has caused a Lua error."] .. "\n" .. L["Install the addons BugSack and BugGrabber for detailed error logs."], true)
      table.insert(juicedMessage, L["Lua error in aura '%s': %s"]:format(data.id, currentErrorHandlerContext or L["unknown location"]))
    else
      table.insert(juicedMessage, L["Lua error"])
    end
    table.insert(juicedMessage, L["WeakAuras Version: %s"]:format(WeakAuras.versionString))
    local version = data and (data.semver or data.version)
    if version then
      table.insert(juicedMessage, L["Aura Version: %s"]:format(version))
    end
    table.insert(juicedMessage, L["Stack trace:"])
    table.insert(juicedMessage, errorMessage)
    geterrorhandler()(table.concat(juicedMessage, "\n"))
  end

  function Private.GetErrorHandlerId(id, context)
    currentErrorHandlerUid = nil
    currentErrorHandlerId = id
    currentErrorHandlerContext = context
    return waErrorHandler
  end
  function Private.GetErrorHandlerUid(uid, context)
    currentErrorHandlerUid = uid
    currentErrorHandlerId = nil
    currentErrorHandlerContext = context
    return waErrorHandler
  end
end

function Private.LoadOptions(msg)
  if not(C_AddOns.IsAddOnLoaded("WeakAurasOptions")) then
    if not WeakAuras.IsLoginFinished() then
      prettyPrint(Private.LoginMessage())
      loginQueue[#loginQueue + 1] = WeakAuras.OpenOptions
    elseif InCombatLockdown() then
      -- inform the user and queue ooc
      prettyPrint(L["Options will finish loading after combat ends."])
      queueshowooc = msg or "";
      Private.frames["Addon Initialization Handler"]:RegisterEvent("PLAYER_REGEN_ENABLED")
      return false;
    else
      local loaded, reason = C_AddOns.LoadAddOn("WeakAurasOptions");
      if not(loaded) then
        reason = string.lower("|cffff2020" .. _G["ADDON_" .. reason] .. "|r.")
        WeakAuras.prettyPrint(string.format(L["Options could not be loaded, the addon is %s"], reason));
        return false;
      end
    end
  end
  return true;
end

function WeakAuras.OpenOptions(msg)
  if Private.NeedToRepairDatabase() then
    StaticPopup_Show("WEAKAURAS_CONFIRM_REPAIR", nil, nil, {reason = "downgrade"})
  elseif (WeakAuras.IsLoginFinished() and Private.LoadOptions(msg)) then
    WeakAuras.ToggleOptions(msg, Private);
  end
end

function Private.PrintHelp()
  print(L["Usage:"])
  print(L["/wa help - Show this message"])
  print(L["/wa minimap - Toggle the minimap icon"])
  print(L["/wa pstart - Start profiling. Optionally include a duration in seconds after which profiling automatically stops. To profile the next combat/encounter, pass a \"combat\" or \"encounter\" argument."])
  print(L["/wa pstop - Finish profiling"])
  print(L["/wa pprint - Show the results from the most recent profiling"])
  print(L["/wa repair - Repair tool"])
  print(L["If you require additional assistance, please open a ticket on GitHub or visit our Discord at https://discord.gg/weakauras!"])
end

SLASH_WEAKAURAS1, SLASH_WEAKAURAS2 = "/weakauras", "/wa";
function SlashCmdList.WEAKAURAS(input)
  local args, msg = {}, nil

  for v in string.gmatch(input, "%S+") do
    if not msg then
      msg = v:lower()
    else
      insert(args, v:lower())
    end
  end

  if msg == "pstart" then
    WeakAuras.StartProfile(args[1]);
  elseif msg == "pstop" then
    WeakAuras.StopProfile();
  elseif msg == "pprint" then
    WeakAuras.PrintProfile();
  elseif msg == "pcancel" then
    WeakAuras.CancelScheduledProfile()
  elseif msg == "minimap" then
    WeakAuras.ToggleMinimap();
  elseif msg == "help" then
    Private.PrintHelp();
  elseif msg == "repair" then
    StaticPopup_Show("WEAKAURAS_CONFIRM_REPAIR", nil, nil, {reason = "user"})
  elseif msg == "ff" or msg == "feat" or msg == "feature" then
    if #args < 2 then
      local features = Private.Features:ListFeatures()
      local summary = {}
      for _, feature in ipairs(features) do
        table.insert(summary, ("|c%s%s|r"):format(feature.enabled and "ff00ff00" or "ffff0000", feature.id))
      end
      prettyPrint(L["Syntax /wa feature <toggle|on|enable|disable|off> <feature>"])
      prettyPrint(L["Available features: %s"]:format(table.concat(summary, ", ")))
    else
      local action = ({
        toggle = "toggle",
        on = "enable",
        enable = "enable",
        disable = "disable",
        off = "disable"
      })[args[1]]
      if not action then
        prettyPrint(L["Unknown action %q"]:format(args[1]))
      else
        local feature = args[2]
        if not Private.Features:Exists(feature) then
          prettyPrint(L["Unknown feature %q"]:format(feature))
        elseif not Private.Features:Enabled(feature) then
          if action ~= "disable" then
            Private.Features:Enable(feature)
            prettyPrint(L["Enabled feature %q"]:format(feature))
          else
            prettyPrint(L["Feature %q is already disabled"]:format(feature))
          end
        elseif Private.Features:Enabled(feature) then
          if action ~= "enable" then
            Private.Features:Disable(feature)
            prettyPrint(L["Disabled feature %q"]:format(feature))
          else
            prettyPrint(L["Feature %q is already enabled"]:format(feature))
          end
        end
      end
    end
  else
    WeakAuras.OpenOptions(msg);
  end
end

if not WeakAuras.IsLibsOK() then return end

function WeakAuras.ToggleMinimap()
  WeakAurasSaved.minimap.hide = not WeakAurasSaved.minimap.hide
  if WeakAurasSaved.minimap.hide then
    LDBIcon:Hide("WeakAuras");
    prettyPrint(L["Use /wa minimap to show the minimap icon again."])
  else
    LDBIcon:Show("WeakAuras");
  end
end

BINDING_HEADER_WEAKAURAS = ADDON_NAME
BINDING_NAME_WEAKAURASTOGGLE = L["Toggle Options Window"]
BINDING_NAME_WEAKAURASPROFILINGTOGGLE = L["Toggle Performance Profiling Window"]
BINDING_NAME_WEAKAURASPRINTPROFILING = L["Print Profiling Results"]

-- An alias for WeakAurasSaved, the SavedVariables
-- Noteable properties:
--  debug: If set to true, WeakAura.debug() outputs messages to the chat frame
--  displays: All aura settings, keyed on their id

---@class WeakAurasSaved
local db;

-- While true no events are handled. E.g. WeakAuras is paused while the Options dialog is open
local paused = true;
local importing = false;

-- squelches actions and sounds from auras. is used e.g. to prevent lots of actions/sounds from triggering
-- on login or after closing the options dialog
local squelch_actions = true;
local in_loading_screen = false;

-- Load functions, keyed on id
local loadFuncs = {};
-- Load functions for the Options window that ignore various load options
local loadFuncsForOptions = {};
-- Mapping of events to ids, contains true if a aura should be checked for a certain event
local loadEvents = {}

-- All regions keyed on id, has properties: region, regionType, also see clones
Private.regions = {};

-- keyed on id, contains bool indicating whether the aura is loaded
Private.loaded = {};
local loaded = Private.loaded;

-- contains regions for clones
Private.clones = {};
local clones = Private.clones;

-- Unused regions that are kept around for clones
local clonePool = {}

-- One table per regionType, see RegisterRegionType, notable properties: create, modify and default
Private.regionTypes = {};
local regionTypes = Private.regionTypes;

Private.subRegionTypes = {}
local subRegionTypes = Private.subRegionTypes

-- One table per regionType, see RegisterRegionOptions
Private.regionOptions = {};
local regionOptions = Private.regionOptions;

Private.subRegionOptions = {}
local subRegionOptions = Private.subRegionOptions

-- Maps from trigger type to trigger system
Private.triggerTypes = {};
local triggerTypes = Private.triggerTypes;

-- Maps from trigger type to a function that can create options for the trigger
Private.triggerTypesOptions = {};

-- Trigger State, updated by trigger systems, then applied to regions by UpdatedTriggerState
-- keyed on id, triggernum, cloneid
-- cloneid can be a empty string

-- Noteable properties:
--  changed: Whether this trigger state was recently changed and its properties
--           need to be applied to a region. The glue code resets this
--           after syncing the region to the trigger state
--  show: Whether the region for this trigger state should be shown
--  progressType: Either "timed", "static"
--    duration: The duration if the progressType is timed
--    expirationTime: The expirationTime if the progressType is timed
--    autoHide: If the aura should be hidden on expiring
--    value: The value if the progressType is static
--    total: The total if the progressType is static
--    inverse: The static values should be interpreted inversely
--  name: The name information
--  icon: The icon information
--  texture: The texture information
--  stacks: The stacks information
--  index: The index of the buff/debuff for the buff trigger system, used to set the tooltip
--  spellId: spellId of the buff/debuff, used to set the tooltip

local triggerState = {}

-- Fallback states
local fallbacksStates = {};

-- List of all trigger systems, contains each system once
local triggerSystems = {}

local timers = {}; -- Timers for autohiding, keyed on id, triggernum, cloneid

WeakAuras.raidUnits = {};
WeakAuras.raidpetUnits = {};
WeakAuras.partyUnits = {};
WeakAuras.partypetUnits = {};
WeakAuras.petUnitToUnit = {
  pet = "player"
}
WeakAuras.unitToPetUnit = {
  player = "pet"
}
do
  for i=1,40 do
    WeakAuras.raidUnits[i] = "raid"..i
    WeakAuras.raidpetUnits[i] = "raidpet"..i
    WeakAuras.petUnitToUnit["raidpet"..i] = "raid"..i
    WeakAuras.unitToPetUnit["raid"..i] = "raidpet"..i
  end
  for i=1,4 do
    WeakAuras.partyUnits[i] = "party"..i
    WeakAuras.partypetUnits[i] = "partypet"..i
    WeakAuras.petUnitToUnit["partypet"..i] = "party"..i
    WeakAuras.unitToPetUnit["party"..i] = "partypet"..i
  end
end

---@param unit UnitToken
---@return boolean isPet
WeakAuras.UnitIsPet = function(unit)
  return WeakAuras.petUnitToUnit[unit] ~= nil
end

local playerLevel = UnitLevel("player");
local currentInstanceType = "none"

-- Custom Action Functions, keyed on id, "init" / "start" / "finish"
Private.customActionsFunctions = {};

-- Custom Functions used in conditions, keyed on id, condition number, "changes", property number
Private.ExecEnv.customConditionsFunctions = {};
-- Text format functions for chat messages, keyed on id, condition number, changes, property number
Private.ExecEnv.conditionTextFormatters = {}

-- Helpers for conditions, that is custom run functions and preamble objects for built in checks
-- keyed on UID not on id!
Private.ExecEnv.conditionHelpers = {}

local load_prototype = Private.load_prototype;

function Private.validate(input, default)
  for field, defaultValue in pairs(default) do
    if(type(defaultValue) == "table" and type(input[field]) ~= "table") then
      input[field] = {};
    elseif(input[field] == nil) or (type(input[field]) ~= type(defaultValue)) then
      input[field] = defaultValue;
    end
    if(type(input[field]) == "table") then
      Private.validate(input[field], defaultValue);
    end
  end
end

---@diagnostic disable-next-line: duplicate-set-field
function Private.RegisterRegionType(name, createFunction, modifyFunction, default, properties, validate)
  if not(name) then
    error("Improper arguments to Private.RegisterRegionType - name is not defined", 2);
  elseif(type(name) ~= "string") then
    error("Improper arguments to Private.RegisterRegionType - name is not a string", 2);
  elseif not(createFunction) then
    error("Improper arguments to Private.RegisterRegionType - creation function is not defined", 2);
  elseif(type(createFunction) ~= "function") then
    error("Improper arguments to Private.RegisterRegionType - creation function is not a function", 2);
  elseif not(modifyFunction) then
    error("Improper arguments to Private.RegisterRegionType - modification function is not defined", 2);
  elseif(type(modifyFunction) ~= "function") then
    error("Improper arguments to Private.RegisterRegionType - modification function is not a function", 2)
  elseif not(default) then
    error("Improper arguments to Private.RegisterRegionType - default options are not defined", 2);
  elseif(type(default) ~= "table") then
    error("Improper arguments to Private.RegisterRegionType - default options are not a table", 2);
  elseif(type(default) ~= "table" and type(default) ~= "nil") then
    error("Improper arguments to Private.RegisterRegionType - properties options are not a table", 2);
  elseif(regionTypes[name]) then
    error("Improper arguments to Private.RegisterRegionType - region type \""..name.."\" already defined", 2);
  else
    regionTypes[name] = {
      create = createFunction,
      modify = modifyFunction,
      default = default,
      validate = validate,
      properties = properties,
    };
  end
end

---@private
---@param name string
---@param displayName string
---@param supportFunction function
---@param createFunction function
---@param modifyFunction function
---@param onAcquire function
---@param onRelease function
---@param default table
---@param addDefaultsForNewAura function
---@param properties table
---@param supportsAdd? boolean
function WeakAuras.RegisterSubRegionType(name, displayName, supportFunction, createFunction, modifyFunction, onAcquire, onRelease, default, addDefaultsForNewAura, properties, supportsAdd)
  if not(name) then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - name is not defined", 2);
  elseif(type(name) ~= "string") then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - name is not a string", 2);
  elseif not(displayName) then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - display name is not defined".." "..name, 2);
  elseif(type(displayName) ~= "string") then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - display name is not a string", 2);
  elseif not(supportFunction) then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - support function is not defined", 2);
  elseif(type(supportFunction) ~= "function") then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - support function is not a function", 2);
  elseif not(createFunction) then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - creation function is not defined", 2);
  elseif(type(createFunction) ~= "function") then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - creation function is not a function", 2);
  elseif not(modifyFunction) then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - modification function is not defined", 2);
  elseif(type(modifyFunction) ~= "function") then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - modification function is not a function", 2)
  elseif not(onAcquire) then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - onAcquire function is not defined", 2);
  elseif(type(onAcquire) ~= "function") then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - onAcquire function is not a function", 2)
  elseif not(onRelease) then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - onRelease function is not defined", 2);
  elseif(type(onRelease) ~= "function") then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - onRelease function is not a function", 2)
  elseif not(default) then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - default options are not defined", 2);
  elseif(type(default) ~= "table" and type(default) ~= "function") then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - default options are not a table or a function", 2);
  elseif(addDefaultsForNewAura and type(addDefaultsForNewAura) ~= "function") then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - addDefaultsForNewAura function is not nil or a function", 2)
  elseif(subRegionTypes[name]) then
    error("Improper arguments to WeakAuras.RegisterSubRegionType - region type \""..name.."\" already defined", 2);
  else
    local pool = CreateObjectPool(createFunction)

    subRegionTypes[name] = {
      displayName = displayName,
      supports = supportFunction,
      modify = modifyFunction,
      default = default,
      addDefaultsForNewAura = addDefaultsForNewAura,
      properties = properties,
      supportsAdd = supportsAdd == nil or supportsAdd,
      acquire = function()
        local subRegion = pool:Acquire()
        onAcquire(subRegion)
        subRegion.type = name
        return subRegion
      end,
      release = function(subRegion)
        onRelease(subRegion)
        pool:Release(subRegion)
      end
    };
  end
end

---@diagnostic disable-next-line: duplicate-set-field
function Private.RegisterRegionOptions(name, createFunction, icon, displayName, createThumbnail, modifyThumbnail, description, templates, getAnchors)
  if not(name) then
    error("Improper arguments to Private.RegisterRegionOptions - name is not defined", 2);
  elseif(type(name) ~= "string") then
    error("Improper arguments to Private.RegisterRegionOptions - name is not a string", 2);
  elseif not(createFunction) then
    error("Improper arguments to Private.RegisterRegionOptions - creation function is not defined", 2);
  elseif(type(createFunction) ~= "function") then
    error("Improper arguments to Private.RegisterRegionOptions - creation function is not a function", 2);
  elseif not(icon) then
    error("Improper arguments to Private.RegisterRegionOptions - icon is not defined", 2);
  elseif not(type(icon) == "string" or type(icon) == "function") then
    error("Improper arguments to Private.RegisterRegionOptions - icon is not a string or a function", 2)
  elseif not(displayName) then
    error("Improper arguments to Private.RegisterRegionOptions - display name is not defined".." "..name, 2);
  elseif(type(displayName) ~= "string") then
    error("Improper arguments to Private.RegisterRegionOptions - display name is not a string", 2);
  elseif (getAnchors and type(getAnchors) ~= "function") then
    error("Improper arguments to Private.RegisterRegionOptions - anchors is not a function", 2);
  elseif(regionOptions[name]) then
    error("Improper arguments to Private.RegisterRegionOptions - region type \""..name.."\" already defined", 2);
  else
    local templateIcon
    if (type(icon) == "function") then
      -- We only want to create two icons and reparent it as needed
      templateIcon = icon()
      templateIcon:Hide()
      icon = icon()
      icon:Hide()
    else
      templateIcon = icon
    end

    local acquireThumbnail, releaseThumbnail
    if createThumbnail and modifyThumbnail then
      local thumbnailPool = CreateObjectPool(createThumbnail)
      acquireThumbnail = function(parent, data)
        local thumbnail, newObject = thumbnailPool:Acquire()
        thumbnail:Show()
        modifyThumbnail(parent, thumbnail, data)
        return thumbnail
      end
      releaseThumbnail = function(thumbnail)
        thumbnail:Hide()
        thumbnailPool:Release(thumbnail)
      end
    end
    regionOptions[name] = {
      create = createFunction,
      icon = icon,
      templateIcon = templateIcon,
      displayName = displayName,
      createThumbnail = createThumbnail,
      modifyThumbnail = modifyThumbnail,
      acquireThumbnail = acquireThumbnail,
      releaseThumbnail = releaseThumbnail,
      description = description,
      templates = templates,
      getAnchors = getAnchors
    };
  end
end

---@private
---@param name string
---@param createFunction function
---@param description string
function WeakAuras.RegisterSubRegionOptions(name, createFunction, description)
  if not(name) then
    error("Improper arguments to WeakAuras.RegisterSubRegionOptions - name is not defined", 2);
  elseif(type(name) ~= "string") then
    error("Improper arguments to WeakAuras.RegisterSubRegionOptions - name is not a string", 2);
  elseif not(createFunction) then
    error("Improper arguments to WeakAuras.RegisterSubRegionOptions - creation function is not defined", 2);
  elseif(type(createFunction) ~= "function") then
    error("Improper arguments to WeakAuras.RegisterSubRegionOptions - creation function is not a function", 2);
  elseif(subRegionOptions[name]) then
    error("Improper arguments to WeakAuras.RegisterSubRegionOptions - region type \""..name.."\" already defined", 2);
  else
    subRegionOptions[name] = {
      create = createFunction,
      description = description,
    };
  end
end

---@diagnostic disable-next-line: duplicate-set-field (it's replaced in WeakAurasOptions.lua)
function WeakAuras.IsOptionsOpen()
  return false;
end

function Private.ParseNumber(numString)
  if not(numString and type(numString) == "string") then
    if(type(numString) == "number") then
      return numString, "notastring";
    else
      return nil;
    end
  elseif(numString:sub(-1) == "%") then
    local percent = tonumber(numString:sub(1, -2));
    if(percent) then
      return percent / 100, "percent";
    else
      return nil;
    end
  else
    -- Matches any string with two integers separated by a forward slash
    -- Captures the two integers
    local _, _, numerator, denominator = numString:find("(%d+)%s*/%s*(%d+)");
    numerator, denominator = tonumber(numerator), tonumber(denominator);
    if(numerator and denominator) then
      if(denominator == 0) then
        return nil;
      else
        return numerator / denominator, "fraction";
      end
    else
      local num = tonumber(numString)
      if(num) then
        if(math.floor(num) ~= num) then
          return num, "decimal";
        else
          return num, "whole";
        end
      else
        return nil;
      end
    end
  end
end

local function EvalBooleanArg(arg, trigger, default)
  if(type(arg) == "function") then
    return arg(trigger);
  elseif type(arg) == "boolean" then
    return arg
  elseif type(arg) == "nil" then
    return default
  end
end

local function singleTest(arg, trigger, use, name, value, operator, use_exact, caseInsensitive)
  local number = value and tonumber(value) or nil
  if(arg.type == "tristate") then
    if(use == false) then
      return "(not "..name..")";
    elseif(use) then
      if(arg.test) then
        return "("..arg.test:format(value)..")";
      else
        return name;
      end
    end
  elseif(arg.type == "tristatestring") then
    if(use == false) then
      return "("..name.. "~=".. (number or string.format("%s", Private.QuotedString(value or ""))) .. ")"
    elseif(use) then
      return "("..name.. "==".. (number or string.format("%s", Private.QuotedString(value or ""))) .. ")"
    end
  elseif(arg.type == "multiselect") then
    if arg.multiNoSingle then
      -- convert single to multi
      -- this is a lazy migration because multiNoSingle is not set for all game versions
      if use == true then
        trigger["use_"..name] = false
        trigger[name] = trigger[name] or {}
        trigger[name].multi = {};
        if trigger[name].single ~= nil then
          trigger[name].multi[trigger[name].single] = true;
          trigger[name].single = nil
        end
      end
    end
    if(use == false) then -- multi selection
      local any = false;
      if (value and value.multi) then
        local test = "(";
        for value, positive in pairs(value.multi) do
          local arg1 = tonumber(value) or ("[["..value.."]]")
          local arg2
          if arg.extraOption then
            arg2 = trigger[name .. "_extraOption"] or 0
          elseif arg.multiTristate then
            arg2 = positive and 4 or 5
          end
          local testEnabled = true
          if type(arg.enableTest) == "function" then
            testEnabled = arg.enableTest(trigger, arg1, arg2)
          end
          if testEnabled then
            local check
            if not arg.test then
              check = name.."=="..arg1
            else
              check = arg.test:format(arg1, arg2)
            end
            if arg.multiAll then
              test = test..check.." and "
            else
              test = test..check.." or  "
            end
            any = true;
          end
        end
        if(any) then
          test = test:sub(1, -6);
        else
          test = "(false";
        end
        test = test..")"
        if arg.inverse then
          if type(arg.inverse) == "boolean" then
            test = "not " .. test
          elseif type(arg.inverse) == "function" then
            if arg.inverse(trigger) then
              test = "not " .. test
            end
          end
        end
        return test
      end
    elseif(use) then -- single selection
      local value = value and value.single or nil;
      if not arg.test then
        return value and "("..name.."=="..(tonumber(value) or ("[["..value.."]]"))..")";
      else
        return value and "("..arg.test:format(tonumber(value) or ("[["..value.."]]"))..")";
      end
    end
  elseif(arg.type == "toggle") then
    if(use) then
      if(arg.test) then
        return "("..arg.test:format(value)..")";
      else
        return name;
      end
    end
  elseif (arg.type == "spell") then
    if arg.showExactOption then
      return "("..arg.test:format(value, tostring(use_exact) or "false") ..")";
    else
      return "("..arg.test:format(value)..")";
    end
  elseif(arg.test) then
    return "("..arg.test:format(value)..")";
  elseif(arg.type == "longstring" and operator) then
    if(operator == "==") then
      if caseInsensitive then
        return ("(%s and %s:lower() == [[%s]]:lower())"):format(name, name, value)
      else
        return "("..name.."==[["..value.."]])";
      end
    else
      if caseInsensitive then
        local op = operator:format(value:lower())
        return ("(%s:lower():%s)"):format(name, op)
      else
        return "("..name..":"..operator:format(value)..")";
      end
    end
  elseif(arg.type == "number") then
    if number then
      return "("..name..(operator or "==").. number ..")";
    end
  else
    if(type(value) == "table") then
      value = "error";
    end
    return "("..name..(operator or "==")..(number or ("[["..(value or "").."]]"))..")";
  end
end

-- Used for the load function, could be simplified a bit
-- It used to be also used for the generic trigger system
local function ConstructFunction(prototype, trigger, skipOptional)
  local input = {"event"};
  local required = {};
  local tests = {};
  local debug = {};
  local events = {}
  local init;
  local preambles = ""
  local orConjunctionGroups = {}
  if(prototype.init) then
    init = prototype.init(trigger);
  else
    init = "";
  end
  for index, arg in pairs(prototype.args) do
    local enable = EvalBooleanArg(arg.enable, trigger, true)
    local init = arg.init
    local name = arg.name;
    if(arg.init == "arg") then
      tinsert(input, name);
    end

    if(enable) then
      if (arg.optional and skipOptional) then
      -- Do nothing
      elseif arg.type == "tristate"
        or arg.type == "toggle"
        or arg.type == "tristatestring"
        or (arg.type == "multiselect" and trigger["use_"..name] ~= nil)
        or ((trigger["use_"..name] or arg.required) and trigger[name])
      then
        local test;

        if arg.multiEntry then
          if type(trigger[name]) == "table" and #trigger[name] > 0 then
            test = ""
            for i, value in ipairs(trigger[name]) do
              local operator = name and type(trigger[name.."_operator"]) == "table" and trigger[name.."_operator"][i]
              local caseInsensitive = name and arg.canBeCaseInsensitive and type(trigger[name.."_caseInsensitive"]) == "table" and trigger[name.."_caseInsensitive"][i]
              local use_exact = name and type(trigger["use_exact_" .. name]) == "table" and trigger["use_exact_" .. name][i]
              local use = name and trigger["use_"..name]
              local single = singleTest(arg, trigger, use, name, value, operator, use_exact, caseInsensitive)
              if single then
                if test ~= "" then
                  test = test .. arg.multiEntry.operator
                end
                test = test .. single
              end
            end
            if test == "" then
              test = nil
            else
              test = "(" .. test .. ")"
            end
          end
        else
          local value = trigger[name]
          local operator = name and trigger[name.."_operator"]
          local caseInsensitive = name and trigger[name.."_caseInsensitive"]
          local use_exact = name and trigger["use_exact_" .. name]
          local use = name and trigger["use_"..name]
          test = singleTest(arg, trigger, use, name, value, operator, use_exact, caseInsensitive)
        end

        if (arg.preamble) then
          preambles = preambles .. arg.preamble:format(trigger[name]) .. "\n"
        end

        if test ~= "(test)" then
          if(arg.required) then
            tinsert(required, test);
          elseif test ~= nil then
            if arg.orConjunctionGroup then
              orConjunctionGroups[arg.orConjunctionGroup ] = orConjunctionGroups[arg.orConjunctionGroup ] or {}
              tinsert(orConjunctionGroups[arg.orConjunctionGroup ], test)
            else
              tinsert(tests, test);
            end
          end
        end

        if test and arg.events then
          for index, event in ipairs(arg.events) do
            events[event] = true
          end
        end

        if(arg.debug) then
          tinsert(debug, arg.debug:format(trigger[name]));
        end
      end
    end
  end

  for _, orConjunctionGroup  in pairs(orConjunctionGroups) do
    tinsert(tests, "("..table.concat(orConjunctionGroup , " or ")..")")
  end
  local ret = {preambles .. "return function("..table.concat(input, ", ")..")\n"};
  table.insert(ret, (init or ""));
  table.insert(ret, (#debug > 0 and table.concat(debug, "\n") or ""));
  table.insert(ret, "if(");
  table.insert(ret, ((#required > 0) and table.concat(required, " and ").." and " or ""));
  table.insert(ret, (#tests > 0 and table.concat(tests, " and ") or "true"));
  table.insert(ret, ") then\n");
  if(#debug > 0) then
    table.insert(ret, "print('ret: true');\n");
  end
  table.insert(ret, "return true else return false end end");

  return table.concat(ret), events;
end

function WeakAuras.GetActiveConditions(id, cloneId)
  triggerState[id].activatedConditions[cloneId] = triggerState[id].activatedConditions[cloneId] or {};
  return triggerState[id].activatedConditions[cloneId];
end

local function LoadCustomActionFunctions(data)
  local id = data.id;
  Private.customActionsFunctions[id] = {};

  if (data.actions) then
    if (data.actions.init and data.actions.init.do_custom and data.actions.init.custom) then
      local func = WeakAuras.LoadFunction("return function() "..(data.actions.init.custom).."\n end");
      Private.customActionsFunctions[id]["init"] = func;
    end

    if (data.actions.start) then
      if (data.actions.start.do_custom and data.actions.start.custom) then
        local func = WeakAuras.LoadFunction("return function() "..(data.actions.start.custom).."\n end");
        Private.customActionsFunctions[id]["start"] = func;
      end

      if (data.actions.start.do_message and data.actions.start.message_custom) then
        local func = WeakAuras.LoadFunction("return "..(data.actions.start.message_custom));
        Private.customActionsFunctions[id]["start_message"] = func;
      end
    end

    if (data.actions.finish) then
      if (data.actions.finish.do_custom and data.actions.finish.custom) then
        local func = WeakAuras.LoadFunction("return function() "..(data.actions.finish.custom).."\n end");
        Private.customActionsFunctions[id]["finish"] = func;
      end

      if (data.actions.finish.do_message and data.actions.finish.message_custom) then
        local func = WeakAuras.LoadFunction("return "..(data.actions.finish.message_custom));
        Private.customActionsFunctions[id]["finish_message"] = func;
      end
    end
  end
end

Private.talent_types_specific = {}
Private.pvp_talent_types_specific = {}
local function CreateTalentCache()
  local _, player_class = UnitClass("player")

  Private.talent_types_specific[player_class] = Private.talent_types_specific[player_class] or {};

  if WeakAuras.IsClassicOrCata() then
    for tab = 1, GetNumTalentTabs() do
      for num_talent = 1, GetNumTalents(tab) do
        local talentName, talentIcon = GetTalentInfo(tab, num_talent);
        local talentId = (tab - 1) * MAX_NUM_TALENTS + num_talent
        if (talentName and talentIcon) then
          Private.talent_types_specific[player_class][talentId] = "|T"..talentIcon..":0|t "..talentName
        end
      end
    end
  else
    local spec = GetSpecialization()
    Private.talent_types_specific[player_class][spec] = Private.talent_types_specific[player_class][spec] or {};

    for tier = 1, MAX_TALENT_TIERS do
      for column = 1, NUM_TALENT_COLUMNS do
        -- Get name and icon info for the current talent of the current class and save it
        local _, talentName, talentIcon = GetTalentInfo(tier, column, 1)
        local talentId = (tier-1)*3+column
        -- Get the icon and name from the talent cache and record it in the table that will be used by WeakAurasOptions
        if (talentName and talentIcon) then
          Private.talent_types_specific[player_class][spec][talentId] = "|T"..talentIcon..":0|t "..talentName
        end
      end
    end
  end
end

local function CreatePvPTalentCache()
  local _, player_class = UnitClass("player")
  local spec = GetSpecialization()

  if (not player_class or not spec) then
    return;
  end

  Private.pvp_talent_types_specific[player_class] = Private.pvp_talent_types_specific[player_class] or {};
  Private.pvp_talent_types_specific[player_class][spec] = Private.pvp_talent_types_specific[player_class][spec] or {};

  --- @type fun(talentId: number): number, string
  local function formatTalent(talentId)
    local _, name, icon, _, _, spellId = GetPvpTalentInfoByID(talentId);
    return spellId, "|T"..icon..":0|t "..name
  end

  local slotInfo = C_SpecializationInfo.GetPvpTalentSlotInfo(1);
  if (slotInfo) then
    Private.pvp_talent_types_specific[player_class][spec] = {};

    local pvpSpecTalents = slotInfo.availableTalentIDs;
    for _, talentId in ipairs(pvpSpecTalents) do
      local index, displayText = formatTalent(talentId)
      Private.pvp_talent_types_specific[player_class][spec][index] = displayText
    end
  end
end

Private.CompanionData = {}
-- use this function to not overwrite data from other companion compatible addons
-- when using this function, do not name your global data table "WeakAurasCompanion"
function WeakAuras.AddCompanionData(data)
  WeakAuras.DeepMixin(Private.CompanionData, data)
end

-- add data from versions of companion compatible addon that does not use WeakAuras.AddCompanionData yet
local function AddLegacyCompanionData()
  local CompanionData = WeakAurasCompanion and WeakAurasCompanion.WeakAuras or WeakAurasCompanion
  if CompanionData then
    WeakAuras.AddCompanionData(CompanionData)
  end
end

function Private.PostAddCompanion()
  -- add data from older version of companion addons
  AddLegacyCompanionData()
  -- nag if updates
  local count = Private.CountWagoUpdates()
  if count and count > 0 then
    WeakAuras.prettyPrint(L["There are %i updates to your auras ready to be installed!"]:format(count))
  end
  -- nag if new installs
  if Private.CompanionData.stash and next(Private.CompanionData.stash) then
    WeakAuras.prettyPrint(L["You have new auras ready to be installed!"])
  end
end

function Private.CountWagoUpdates()
  if not (Private.CompanionData.slugs) then
    return 0
  end
  local updatedSlugs, updatedSlugsCount = {}, 0
  for id, aura in pairs(db.displays) do
    if not aura.ignoreWagoUpdate and aura.url and aura.url ~= "" then
      local slug, version = aura.url:match("wago.io/([^/]+)/([0-9]+)")
      if not slug and not version then
        slug = aura.url:match("wago.io/([^/]+)$")
        version = 1
      end
      if slug and version then
        local wago = Private.CompanionData.slugs[slug]
        if wago and wago.wagoVersion and tonumber(wago.wagoVersion) > tonumber(version) then
          if not updatedSlugs[slug] then
            updatedSlugs[slug] = true
            updatedSlugsCount = updatedSlugsCount + 1
          end
        end
      end
    end
  end

  return updatedSlugsCount
end

local function tooltip_draw(isAddonCompartment, blizzardTooltip)
  local tooltip
  if isAddonCompartment then
    tooltip = blizzardTooltip
  else
    tooltip = GameTooltip
  end
  tooltip:ClearLines()
  tooltip:AddDoubleLine("WeakAuras", versionString)
  if Private.CompanionData.slugs then
    local count = Private.CountWagoUpdates()
    if count > 0 then
      tooltip:AddLine(" ");
      tooltip:AddLine((L["There are %i updates to your auras ready to be installed!"]):format(count));
    end
  end
  tooltip:AddLine(" ");
  tooltip:AddLine(L["|cffeda55fLeft-Click|r to toggle showing the main window."], 0.2, 1, 0.2);
  if not WeakAuras.IsOptionsOpen() then
    if paused then
      tooltip:AddLine("|cFFFF0000"..L["Paused"].." - "..L["Shift-Click to resume addon execution."], 0.2, 1, 0.2);
    else
      tooltip:AddLine(L["|cffeda55fShift-Click|r to pause addon execution."], 0.2, 1, 0.2);
    end
  end
  tooltip:AddLine(L["|cffeda55fRight-Click|r to toggle performance profiling window."], 0.2, 1, 0.2);
  if not isAddonCompartment then
    tooltip:AddLine(L["|cffeda55fMiddle-Click|r to toggle the minimap icon on or off."], 0.2, 1, 0.2);
  end
  tooltip:Show();
end

WeakAuras.GenerateTooltip = tooltip_draw;

local colorFrame = CreateFrame("Frame");
Private.frames["LDB Icon Recoloring"] = colorFrame;

local colorElapsed = 0;
local colorDelay = 2;
local r, g, b = 0.8, 0, 1;
local r2, g2, b2 = random(2)-1, random(2)-1, random(2)-1;

local tooltip_update_frame = CreateFrame("Frame");
Private.frames["LDB Tooltip Updater"] = tooltip_update_frame;

-- function copied from LibDBIcon-1.0.lua
local function getAnchors(frame)
	local x, y = frame:GetCenter()
	if not x or not y then return "CENTER" end
	local hHalf = (x > UIParent:GetWidth()*2/3) and "RIGHT" or (x < UIParent:GetWidth()/3) and "LEFT" or ""
	local vHalf = (y > UIParent:GetHeight()/2) and "TOP" or "BOTTOM"
	return vHalf..hHalf, frame, (vHalf == "TOP" and "BOTTOM" or "TOP")..hHalf
end

local Broker_WeakAuras;
Broker_WeakAuras = LDB:NewDataObject("WeakAuras", {
  type = "launcher",
  text = "WeakAuras",
  icon = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\icon.blp",
  OnClick = function(self, button)
    if button == 'LeftButton' then
      if(IsShiftKeyDown()) then
        if not(WeakAuras.IsOptionsOpen()) then
          WeakAuras.Toggle();
        end
      else
        WeakAuras.OpenOptions();
      end
    elseif(button == 'MiddleButton') then
      WeakAuras.ToggleMinimap();
    else
      WeakAurasProfilingFrame:Toggle()
    end
    tooltip_draw()
  end,
  OnEnter = function(self)
    colorFrame:SetScript("OnUpdate", function(self, elaps)
      colorElapsed = colorElapsed + elaps;
      if(colorElapsed > colorDelay) then
        colorElapsed = colorElapsed - colorDelay;
        r, g, b = r2, g2, b2;
        r2, g2, b2 = random(2)-1, random(2)-1, random(2)-1;
      end
      Broker_WeakAuras.iconR = r + (r2 - r) * colorElapsed / colorDelay;
      Broker_WeakAuras.iconG = g + (g2 - g) * colorElapsed / colorDelay;
      Broker_WeakAuras.iconB = b + (b2 - b) * colorElapsed / colorDelay;
    end);
    local elapsed = 0;
    local delay = 1;
    tooltip_update_frame:SetScript("OnUpdate", function(self, elap)
      elapsed = elapsed + elap;
      if(elapsed > delay) then
        elapsed = 0;
        tooltip_draw();
      end
    end);
    GameTooltip:SetOwner(self, "ANCHOR_NONE");
    GameTooltip:SetPoint(getAnchors(self))
    tooltip_draw();
  end,
  OnLeave = function(self)
    colorFrame:SetScript("OnUpdate", nil);
    tooltip_update_frame:SetScript("OnUpdate", nil);
    GameTooltip:Hide();
  end,
  iconR = 0.6,
  iconG = 0,
  iconB = 1
});

do -- Archive stuff
  local Archivist = select(2, ...).Archivist
  local function OpenArchive()
    if Archivist:IsInitialized() then
      return Archivist
    else
      if not C_AddOns.IsAddOnLoaded("WeakAurasArchive") then
        local ok, reason = C_AddOns.LoadAddOn("WeakAurasArchive")
        if not ok then
          reason = string.lower("|cffff2020" .. _G["ADDON_" .. reason] .. "|r.")
          error(string.format(L["Could not load WeakAuras Archive, the addon is %s"], reason))
        end
      end
      if type(WeakAurasArchive) ~= "table" then
        WeakAurasArchive = {}
      end
      Archivist:Initialize(WeakAurasArchive)
    end
    return Archivist
  end

  function WeakAuras.LoadFromArchive(storeType, storeID)
    local Archive = OpenArchive()
    return Archive:Load(storeType, storeID)
  end
end

local loginFinished, loginMessage = false, L["Options will open after the login process has completed."]

function WeakAuras.IsLoginFinished()
  return loginFinished
end

function Private.LoginMessage()
  return loginMessage
end

local function CheckForPreviousEncounter()
  if (UnitAffectingCombat ("player") or InCombatLockdown()) then
    for i = 1, 10 do
      if (UnitExists ("boss" .. i)) then
        local guid = UnitGUID ("boss" .. i)
        if (guid and db.CurrentEncounter.boss_guids [guid]) then
          -- we are in the same encounter
          WeakAuras.CurrentEncounter = db.CurrentEncounter
          return true
        end
      end
    end
    db.CurrentEncounter = nil
  else
    db.CurrentEncounter = nil
  end
end

function Private.Login(takeNewSnapshots)
  local loginThread = coroutine.create(function()
    Private.Pause();
    coroutine.yield(100)
    if db.history then
      local histRepo = WeakAuras.LoadFromArchive("Repository", "history")
      local migrationRepo = WeakAuras.LoadFromArchive("Repository", "migration")
      for uid, hist in pairs(db.history) do
        local histStore = histRepo:Set(uid, hist.data)
        local migrationStore = migrationRepo:Set(uid, hist.migration)
        coroutine.yield(1000, "login move old history")
      end
      -- history is now in archive so we can shrink WeakAurasSaved
      db.history = nil
    end


    Private.Features:Hydrate()
    coroutine.yield(3000, "login check uid corruption")

    local toAdd = {};
    loginFinished = false
    loginMessage = L["Options will open after the login process has completed."]
    for id, data in pairs(db.displays) do
      if(id ~= data.id) then
        print("|cFF8800FFWeakAuras|r detected a corrupt entry in WeakAuras saved displays - '"..tostring(id).."' vs '"..tostring(data.id).."'" );
        data.id = id;
      end

      tinsert(toAdd, data);
    end
    coroutine.yield(8000);

    Private.AddMany(toAdd, takeNewSnapshots);
    coroutine.yield(1000);

    -- check in case of a disconnect during an encounter.
    if (db.CurrentEncounter) then
      CheckForPreviousEncounter()
    end
    coroutine.yield(1000);
    Private.RegisterLoadEvents();
    coroutine.yield(10000);
    Private.Resume();
    coroutine.yield(100);

    local nextCallback = loginQueue[1];
    while nextCallback do
      tremove(loginQueue, 1);
      if type(nextCallback) == 'table' then
        nextCallback[1](unpack(nextCallback[2]))
      else
        nextCallback()
      end
      coroutine.yield(1000, "login post login callbacks");
      nextCallback = loginQueue[1];
    end

    loginFinished = true
    -- Tell Dynamic Groups that we are done with login
    for _, region in pairs(Private.regions) do
      if (region.region and region.region.RunDelayedActions) then
        region.region:RunDelayedActions();
        coroutine.yield(500, "login delayed region actions");
      end
    end
  end)

  Private.Threads:Immediate('login', loginThread, 15000, 1000)
end

local WeakAurasFrame = CreateFrame("Frame", "WeakAurasFrame", UIParent);
Private.frames["WeakAuras Main Frame"] = WeakAurasFrame;
WeakAurasFrame:SetAllPoints(UIParent);

local loadedFrame = CreateFrame("Frame");
Private.frames["Addon Initialization Handler"] = loadedFrame;
loadedFrame:RegisterEvent("ADDON_LOADED");
loadedFrame:RegisterEvent("PLAYER_LOGIN");
loadedFrame:RegisterEvent("PLAYER_LOGOUT")
loadedFrame:RegisterEvent("PLAYER_ENTERING_WORLD");
loadedFrame:RegisterEvent("LOADING_SCREEN_ENABLED");
loadedFrame:RegisterEvent("LOADING_SCREEN_DISABLED");
if WeakAuras.IsRetail() then
  loadedFrame:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED");
  loadedFrame:RegisterEvent("PLAYER_PVP_TALENT_UPDATE");
else
  loadedFrame:RegisterEvent("CHARACTER_POINTS_CHANGED");
  loadedFrame:RegisterEvent("SPELLS_CHANGED");
end
loadedFrame:SetScript("OnEvent", function(self, event, addon)
  if(event == "ADDON_LOADED") then
    if(addon == ADDON_NAME) then
      ---@type WeakAurasSaved
      WeakAurasSaved = WeakAurasSaved or {};
      db = WeakAurasSaved;
      Private.db = db
      -- Defines the action squelch period after login
      -- Stored in SavedVariables so it can be changed by the user if they find it necessary
      db.login_squelch_time = db.login_squelch_time or 10;

      -- Deprecated fields with *lots* of data, clear them out
      db.iconCache = nil;
      db.iconHash = nil;
      db.tempIconCache = nil;
      db.dynamicIconCache = db.dynamicIconCache or {};

      db.displays = db.displays or {};
      db.registered = db.registered or {};
      db.features = db.features or {}
      db.migrationCutoff = db.migrationCutoff or 730
      db.historyCutoff = db.historyCutoff or 730

      Private.UpdateCurrentInstanceType();
      Private.SyncParentChildRelationships();
      local isFirstUIDValidation = db.dbVersion == nil or db.dbVersion < 26;
      Private.ValidateUniqueDataIds(isFirstUIDValidation);

      if db.lastArchiveClear == nil then
        db.lastArchiveClear = time();
      elseif db.lastArchiveClear < time() - 86400 then
        Private.CleanArchive(db.historyCutoff, db.migrationCutoff);
      end
      db.minimap = db.minimap or { hide = false };
      LDBIcon:Register("WeakAuras", Broker_WeakAuras, db.minimap);
    end
  elseif(event == "PLAYER_LOGIN") then
    local dbIsValid, takeNewSnapshots
    if not db.dbVersion or db.dbVersion < internalVersion then
      -- db is out of date, will run any necessary migrations in AddMany
      db.dbVersion = internalVersion
      db.lastUpgrade = time()
      dbIsValid = true
      takeNewSnapshots = true
    elseif db.dbVersion > internalVersion then
      -- user has downgraded past a forwards-incompatible migration
      dbIsValid = false
    else
      -- db has same version as code, can commit to login
      dbIsValid = true
    end
    if dbIsValid then
      Private.Login(takeNewSnapshots)
    else
      -- db isn't valid. Request permission to run repair tool before logging in
      StaticPopup_Show("WEAKAURAS_CONFIRM_REPAIR", nil, nil, {reason = "downgrade"})
    end
  elseif event == "PLAYER_LOGOUT" then
    for id in pairs(db.displays) do
      Private.ClearAuraEnvironment(id)
    end
  elseif(event == "LOADING_SCREEN_ENABLED") then
    in_loading_screen = true;
  elseif(event == "LOADING_SCREEN_DISABLED") then
    in_loading_screen = false;
  else
    local callback
    if(event == "PLAYER_ENTERING_WORLD") then
      -- Schedule events that need to be handled some time after login
      local now = GetTime()
      callback = function()
        local elapsed = GetTime() - now
        local remainingSquelch = db.login_squelch_time - elapsed
        if remainingSquelch > 0 then
          timer:ScheduleTimer(function() squelch_actions = false; end, remainingSquelch); -- No sounds while loading
        end
        CreateTalentCache() -- It seems that GetTalentInfo might give info about whatever class was previously being played, until PLAYER_ENTERING_WORLD
        Private.UpdateCurrentInstanceType();
        Private.InitializeEncounterAndZoneLists()
      end
      Private.PostAddCompanion()
    elseif(event == "PLAYER_PVP_TALENT_UPDATE") then
      callback = CreatePvPTalentCache;
    elseif(event == "ACTIVE_TALENT_GROUP_CHANGED" or event == "CHARACTER_POINTS_CHANGED" or event == "SPELLS_CHANGED") then
      callback = CreateTalentCache;
    elseif(event == "PLAYER_REGEN_ENABLED") then
      callback = function()
        if (queueshowooc) then
          WeakAuras.OpenOptions(queueshowooc)
          queueshowooc = nil
          Private.frames["Addon Initialization Handler"]:UnregisterEvent("PLAYER_REGEN_ENABLED")
        end
      end
    end
    if WeakAuras.IsLoginFinished() then
      callback()
    else
      loginQueue[#loginQueue + 1] = callback
    end
  end
end)

function Private.SetImporting(b)
  importing = b;
end

function WeakAuras.IsImporting()
  return importing;
end

function WeakAuras.IsPaused()
  return paused;
end

function Private.Pause()
  for id, states in pairs(triggerState) do
    local changed
    for triggernum in ipairs(states) do
      changed = Private.SetAllStatesHidden(id, triggernum) or changed
    end
    if changed then
      Private.UpdatedTriggerState(id)
    end
  end

  paused = true;
end

function WeakAuras.Toggle()
  if(paused) then
    Private.Resume();
  else
    Private.Pause();
  end
end

function Private.SquelchingActions()
  return squelch_actions;
end

function WeakAuras.InLoadingScreen()
  return in_loading_screen;
end

function Private.PauseAllDynamicGroups()
  local suspended = {}
  for id, region in pairs(Private.regions) do
    if (region.region and region.region.Suspend) then
      region.region:Suspend();
      tinsert(suspended, id)
    end
  end
  return suspended
end

function Private.ResumeAllDynamicGroups(suspended)
  for _, id in ipairs(suspended) do
    local region = WeakAuras.GetRegion(id)
    if (region and region.Resume) then
      region:Resume();
    end
  end
end

-- Encounter stuff
local function StoreBossGUIDs()
  Private.StartProfileSystem("boss_guids")
  if (WeakAuras.CurrentEncounter and WeakAuras.CurrentEncounter.boss_guids) then
    for i = 1, 10 do
      if (UnitExists ("boss" .. i)) then
        local guid = UnitGUID ("boss" .. i)
        if (guid) then
          WeakAuras.CurrentEncounter.boss_guids [guid] = true
        end
      end
    end
    db.CurrentEncounter = WeakAuras.CurrentEncounter
  end
  Private.StopProfileSystem("boss_guids")
end

local function DestroyEncounterTable()
  if (WeakAuras.CurrentEncounter) then
    wipe(WeakAuras.CurrentEncounter)
  end
  WeakAuras.CurrentEncounter = nil
  db.CurrentEncounter = nil
end

local function CreateEncounterTable(encounter_id)
  local _, _, _, _, _, _, _, instanceId = GetInstanceInfo()
  ---@class CurrentEncounter
  ---@field encounterId number
  ---@field zone_id number
  ---@field boss_guids number[]
  WeakAuras.CurrentEncounter = {
    id = encounter_id,
    zone_id = instanceId,
    boss_guids = {},
  }
  timer:ScheduleTimer(StoreBossGUIDs, 2)

  return WeakAuras.CurrentEncounter
end

local encounterScriptsDeferred = {}
local function LoadEncounterInitScriptsImpl(id)
  if (currentInstanceType ~= "raid") then
    return
  end
  if (id) then
    local data = db.displays[id]
    if (data and data.load.use_encounterid and not Private.IsEnvironmentInitialized(id) and data.actions.init and data.actions.init.do_custom) then
      Private.ActivateAuraEnvironment(id)
      Private.ActivateAuraEnvironment(nil)
    end
    encounterScriptsDeferred[id] = nil
  else
    for id, data in pairs(db.displays) do
      if (data.load.use_encounterid and not Private.IsEnvironmentInitialized(id) and data.actions.init and data.actions.init.do_custom) then
        Private.ActivateAuraEnvironment(id)
        Private.ActivateAuraEnvironment(nil)
      end
    end
  end
end

local function LoadEncounterInitScripts(id)
  if not WeakAuras.IsLoginFinished() then
    if encounterScriptsDeferred[id] then
      return
    end
    loginQueue[#loginQueue + 1] = {LoadEncounterInitScriptsImpl, {id}}
    encounterScriptsDeferred[id] = true
    return
  end
  LoadEncounterInitScriptsImpl(id)
end

function Private.UpdateCurrentInstanceType(instanceType)
  if (not IsInInstance()) then
    currentInstanceType = "none"
  else
    currentInstanceType = instanceType or select (2, GetInstanceInfo())
  end
end

local pausedOptionsProcessing = false;
function Private.pauseOptionsProcessing(enable)
  pausedOptionsProcessing = enable;
end

function Private.IsOptionsProcessingPaused()
  return pausedOptionsProcessing;
end

function Private.ExecEnv.GroupType()
  if (IsInRaid()) then
    return "raid";
  end
  if (IsInGroup()) then
    return "group";
  end
  return "solo";
end

local function GetInstanceTypeAndSize()
  local size, difficulty
  local inInstance, Type = IsInInstance()
  local _, instanceType, difficultyIndex, _, _, _, _, instanceId = GetInstanceInfo()
  if (inInstance) then
    size = Type
    local difficultyInfo = Private.difficulty_info[difficultyIndex]
    if difficultyInfo then
      size, difficulty = difficultyInfo.size, difficultyInfo.difficulty
    else
      if WeakAuras.IsRetail() then
        if size == "arena" then
          if C_PvP.IsRatedArena() and not IsArenaSkirmish() then
            size = "ratedarena"
          end
        elseif size == "pvp" then
          if C_PvP.IsRatedBattleground() then
            size = "ratedpvp"
          end
        end
      end
    end
    return size, difficulty, instanceType, instanceId, difficultyIndex
  end
  return "none", "none", nil, nil, nil
end

---@return string instanceType
function WeakAuras.InstanceType()
  return (GetInstanceTypeAndSize())
end

---@return string difficulty
function WeakAuras.InstanceDifficulty()
  return select(2, GetInstanceTypeAndSize())
end

---@return number? difficultyID
function WeakAuras.InstanceTypeRaw()
  return select(5, GetInstanceTypeAndSize())
end

local toLoad = {}
local toUnload = {};
local function scanForLoadsImpl(toCheck, event, arg1, ...)
  if (Private.IsOptionsProcessingPaused()) then
    return;
  end

  toCheck = toCheck or loadEvents[event or "SCAN_ALL"]

  -- PET_BATTLE_CLOSE fires twice at the end of a pet battle. IsInBattle evaluates to TRUE during the
  -- first firing, and FALSE during the second. I am not sure if this check is necessary, but the
  -- following IF statement limits the impact of the PET_BATTLE_CLOSE event to the second one.
  if (event == "PET_BATTLE_CLOSE" and C_PetBattles.IsInBattle()) then return end

  if (event == "PLAYER_LEVEL_UP") then
    playerLevel = arg1;
  end

  -- encounter id stuff, we are holding the current combat id to further load checks.
  -- there is three ways to unload: encounter_end / zone changed (hearthstone used) / reload or disconnect
  -- regen_enabled isn't good due to combat drop abilities such invisibility, vanish, fake death, etc.
  local encounter_id = WeakAuras.CurrentEncounter and WeakAuras.CurrentEncounter.id or 0

  if (event == "ENCOUNTER_START") then
    encounter_id = tonumber(arg1)
    CreateEncounterTable(encounter_id)
  elseif (event == "ENCOUNTER_END") then
    encounter_id = 0
    DestroyEncounterTable()
  end

  if toCheck == nil or next(toCheck) == nil then
    return
  end

  local player, realm, zone = UnitName("player"), GetRealmName(), GetRealZoneText()
  --- @type boolean|number|nil, boolean|string|nil, boolean|string|nil, boolean|string
  local specId, role, position, raidRole = false, false, false, false
  --- @type boolean, boolean, boolean
  local inPetBattle, vehicle, vehicleUi = false, false, false
  --- @type boolean
  local dragonriding
  local zoneId = C_Map.GetBestMapForUnit("player")
  local zonegroupId = zoneId and C_Map.GetMapGroupID(zoneId)
  local minimapText = GetMinimapZoneText()
  local _, race = UnitRace("player")
  local faction = UnitFactionGroup("player")
  local _, class = UnitClass("player")
  local inCombat = UnitAffectingCombat("player")
  --- @type boolean
  local inEncounter = encounter_id ~= 0;
  local alive = not UnitIsDeadOrGhost('player')
  local raidMemberType = 0

  if UnitIsGroupLeader("player") then
    raidMemberType = 1
  end

  if UnitIsGroupAssistant("player") then
    raidMemberType = raidMemberType + 2
  end

  if WeakAuras.IsClassicOrCata() then
    local raidID = UnitInRaid("player")
    if raidID then
      raidRole = select(10, GetRaidRosterInfo(raidID))
    end
    role = "none"
    if WeakAuras.IsCataClassic() then
      vehicle = UnitInVehicle('player') or UnitOnTaxi('player') or false
      vehicleUi = UnitHasVehicleUI('player') or HasOverrideActionBar() or HasVehicleActionBar() or false
    else
      vehicle = UnitOnTaxi('player')
    end
  else
    dragonriding = Private.IsDragonriding()
    inPetBattle = C_PetBattles.IsInBattle()
    vehicle = UnitInVehicle('player') or UnitOnTaxi('player') or false
    vehicleUi = UnitHasVehicleUI('player') or HasOverrideActionBar() or HasVehicleActionBar() or false
  end

  if WeakAuras.IsCataOrRetail() then
    specId, role, position = Private.LibSpecWrapper.SpecRolePositionForUnit("player")
  end

  local size, difficulty, instanceType, instanceId, difficultyIndex= GetInstanceTypeAndSize()
  Private.UpdateCurrentInstanceType(instanceType)

  if (WeakAuras.CurrentEncounter) then
    if (instanceId ~= WeakAuras.CurrentEncounter.zone_id and not inCombat) then
      encounter_id = 0
      DestroyEncounterTable()
    end
  end

  if (event == "ZONE_CHANGED_NEW_AREA") then
    LoadEncounterInitScripts();
  end

  local group = Private.ExecEnv.GroupType()
  local groupSize = GetNumGroupMembers()

  local affixes, warmodeActive, effectiveLevel = 0, false, 0
  if WeakAuras.IsRetail() then
    effectiveLevel = UnitEffectiveLevel("player")
    affixes = C_ChallengeMode.IsChallengeModeActive() and select(2, C_ChallengeMode.GetActiveKeystoneInfo())
    warmodeActive = C_PvP.IsWarModeDesired();
  end

  local changed = 0;
  local shouldBeLoaded, couldBeLoaded;
  local parentsToCheck = {}
  wipe(toLoad);
  wipe(toUnload);

  for id in pairs(toCheck) do
    local data = WeakAuras.GetData(id)
    if (data and not data.controlledChildren) then
      local loadFunc = loadFuncs[id];
      local loadOpt = loadFuncsForOptions[id];
      if WeakAuras.IsClassicEra() then
        shouldBeLoaded = loadFunc and loadFunc("ScanForLoads_Auras", inCombat, alive, inEncounter, vehicle, class, player, realm, race, faction, playerLevel, raidRole, group, groupSize, raidMemberType, zone, zoneId, zonegroupId, instanceId, minimapText, encounter_id, size)
        couldBeLoaded =  loadOpt and loadOpt("ScanForLoads_Auras",   inCombat, alive, inEncounter, vehicle, class, player, realm, race, faction, playerLevel, raidRole, group, groupSize, raidMemberType, zone, zoneId, zonegroupId, instanceId, minimapText, encounter_id, size)
      elseif WeakAuras.IsCataClassic() then
        shouldBeLoaded = loadFunc and loadFunc("ScanForLoads_Auras", inCombat, alive, inEncounter, vehicle, vehicleUi, class, specId, player, realm, race, faction, playerLevel, role, position, raidRole, group, groupSize, raidMemberType, zone, zoneId, zonegroupId, instanceId, minimapText, encounter_id, size, difficulty, difficultyIndex)
        couldBeLoaded =  loadOpt and loadOpt("ScanForLoads_Auras",   inCombat, alive, inEncounter, vehicle, vehicleUi, class, specId, player, realm, race, faction, playerLevel, role, position, raidRole, group, groupSize, raidMemberType, zone, zoneId, zonegroupId, instanceId, minimapText, encounter_id, size, difficulty, difficultyIndex)
      elseif WeakAuras.IsRetail() then
        shouldBeLoaded = loadFunc and loadFunc("ScanForLoads_Auras", inCombat, alive, inEncounter, warmodeActive, inPetBattle, vehicle, vehicleUi, dragonriding, specId, player, realm, race, faction, playerLevel, effectiveLevel, role, position, group, groupSize, raidMemberType, zone, zoneId, zonegroupId, instanceId, minimapText, encounter_id, size, difficulty, difficultyIndex, affixes)
        couldBeLoaded =  loadOpt and loadOpt("ScanForLoads_Auras",   inCombat, alive, inEncounter, warmodeActive, inPetBattle, vehicle, vehicleUi, dragonriding, specId, player, realm, race, faction, playerLevel, effectiveLevel, role, position, group, groupSize, raidMemberType, zone, zoneId, zonegroupId, instanceId, minimapText, encounter_id, size, difficulty, difficultyIndex, affixes)
      end

      if(shouldBeLoaded and not loaded[id]) then
        changed = changed + 1;
        toLoad[id] = true;
        Private.EnsureRegion(id)
        for parent in Private.TraverseParents(data) do
          parentsToCheck[parent.id] = true
        end
      end

      if(loaded[id] and not shouldBeLoaded) then
        toUnload[id] = true;
        changed = changed + 1;
        for parent in Private.TraverseParents(data) do
          parentsToCheck[parent.id] = true
        end
      end
      if(shouldBeLoaded) then
        loaded[id] = true;
      elseif(couldBeLoaded) then
        loaded[id] = false;
      else
        loaded[id] = nil;
      end
    end
  end

  if(changed > 0 and not paused) then
    Private.LoadDisplays(toLoad, event, arg1, ...);
    Private.UnloadDisplays(toUnload, event, arg1, ...);
    Private.FinishLoadUnload();
  end

  Private.ScanForLoadsGroup(parentsToCheck)
  Private.callbacks:Fire("ScanForLoads")

  wipe(toLoad);
  wipe(toUnload)
end

function Private.ScanForLoadsGroup(toCheck)
  for id in pairs(toCheck) do
    local data = WeakAuras.GetData(id)
    if(data.controlledChildren) then
      if(#data.controlledChildren > 0) then
        ---@type boolean?
        local any_loaded = false;
        for child in Private.TraverseLeafs(data) do
          if(loaded[child.id] ~= nil) then
            any_loaded = true;
            break;
          else
            any_loaded = nil
          end
        end
        if any_loaded then
          Private.EnsureRegion(id)
        end
        loaded[id] = any_loaded;
      else
        Private.EnsureRegion(id)
        loaded[id] = true;
      end
    end
  end
end

function Private.ScanForLoads(toCheck, event, arg1, ...)
  if not WeakAuras.IsLoginFinished() then
    return
  end
  scanForLoadsImpl(toCheck, event, arg1, ...)
end

local loadFrame = CreateFrame("Frame");
Private.frames["Display Load Handling"] = loadFrame;

loadFrame:RegisterEvent("ENCOUNTER_START");
loadFrame:RegisterEvent("ENCOUNTER_END");

if WeakAuras.IsRetail() then
  loadFrame:RegisterEvent("PLAYER_TALENT_UPDATE");
  loadFrame:RegisterEvent("PLAYER_PVP_TALENT_UPDATE");
  loadFrame:RegisterEvent("PLAYER_DIFFICULTY_CHANGED");
  loadFrame:RegisterEvent("PET_BATTLE_OPENING_START");
  loadFrame:RegisterEvent("PET_BATTLE_CLOSE");
  loadFrame:RegisterEvent("VEHICLE_UPDATE");
  loadFrame:RegisterEvent("UPDATE_VEHICLE_ACTIONBAR")
  loadFrame:RegisterEvent("UPDATE_OVERRIDE_ACTIONBAR");
  loadFrame:RegisterEvent("CHALLENGE_MODE_COMPLETED")
  loadFrame:RegisterEvent("CHALLENGE_MODE_START")
  loadFrame:RegisterEvent("TRAIT_CONFIG_CREATED")
  loadFrame:RegisterEvent("TRAIT_CONFIG_UPDATED")
else
  loadFrame:RegisterEvent("CHARACTER_POINTS_CHANGED")
  loadFrame:RegisterEvent("PLAYER_TALENT_UPDATE");
  loadFrame:RegisterEvent("ACTIVE_TALENT_GROUP_CHANGED");
end

if WeakAuras.IsCataClassic() then
  loadFrame:RegisterEvent("VEHICLE_UPDATE");
  loadFrame:RegisterEvent("UPDATE_VEHICLE_ACTIONBAR")
  loadFrame:RegisterEvent("UPDATE_OVERRIDE_ACTIONBAR");
end
loadFrame:RegisterEvent("GROUP_ROSTER_UPDATE");
loadFrame:RegisterEvent("ZONE_CHANGED");
loadFrame:RegisterEvent("ZONE_CHANGED_INDOORS");
loadFrame:RegisterEvent("ZONE_CHANGED_NEW_AREA");
loadFrame:RegisterEvent("PLAYER_LEVEL_UP");
loadFrame:RegisterEvent("PLAYER_REGEN_DISABLED");
loadFrame:RegisterEvent("PLAYER_REGEN_ENABLED");
loadFrame:RegisterEvent("PLAYER_ROLES_ASSIGNED");
loadFrame:RegisterEvent("SPELLS_CHANGED");
loadFrame:RegisterUnitEvent("UNIT_INVENTORY_CHANGED", "player")
loadFrame:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
loadFrame:RegisterEvent("PLAYER_DEAD")
loadFrame:RegisterEvent("PLAYER_ALIVE")
loadFrame:RegisterEvent("PLAYER_UNGHOST")
loadFrame:RegisterEvent("PARTY_LEADER_CHANGED")

if WeakAuras.IsRetail() then
  Private.callbacks:RegisterCallback("WA_DRAGONRIDING_UPDATE", function ()
    Private.StartProfileSystem("load");
    Private.ScanForLoads(nil, "WA_DRAGONRIDING_UPDATE")
    Private.StopProfileSystem("load");
  end)
end

local unitLoadFrame = CreateFrame("Frame");
Private.frames["Display Load Handling 2"] = unitLoadFrame;

unitLoadFrame:RegisterUnitEvent("UNIT_FLAGS", "player");
if WeakAuras.IsCataOrRetail() then
  unitLoadFrame:RegisterUnitEvent("UNIT_ENTERED_VEHICLE", "player");
  unitLoadFrame:RegisterUnitEvent("UNIT_EXITED_VEHICLE", "player");
  unitLoadFrame:RegisterUnitEvent("PLAYER_FLAGS_CHANGED", "player");
end

function Private.RegisterLoadEvents()
  loadFrame:SetScript("OnEvent", function(frame, ...)
    Private.StartProfileSystem("load");
    Private.ScanForLoads(nil, ...)
    Private.StopProfileSystem("load");
  end);

  C_Timer.NewTicker(0.5, function()
    Private.StartProfileSystem("load");
    local zoneId = C_Map.GetBestMapForUnit("player");
    if loadFrame.zoneId ~= zoneId then
      Private.ScanForLoads(nil, "ZONE_CHANGED")
      loadFrame.zoneId = zoneId;
    end
    Private.StopProfileSystem("load");
  end)

  unitLoadFrame:SetScript("OnEvent", function(frame, e, arg1, ...)
    Private.StartProfileSystem("load");
    if (arg1 == "player") then
      Private.ScanForLoads(nil, e, arg1, ...)
    end
    Private.StopProfileSystem("load");
  end);
end

local function UnloadAll()
  -- Even though auras are collapsed, their finish animation can be running
  for id in pairs(loaded) do
    if Private.regions[id] and Private.regions[id].region then
      Private.CancelAnimation(Private.regions[id].region, true, true, true, true, true, true)
    end
    if clones[id] then
      for cloneId, region in pairs(clones[id]) do
        Private.CancelAnimation(region, true, true, true, true, true, true)
      end
    end
  end

  for _, v in pairs(triggerState) do
    for i = 1, v.numTriggers do
      if (v[i]) then
        wipe(v[i]);
      end
    end
  end

  for _, aura in pairs(timers) do
    for _, trigger in pairs(aura) do
      for _, record in pairs(trigger) do
        if (record.handle) then
          timer:CancelTimer(record.handle);
        end
      end
    end
  end
  wipe(timers);

  Private.UnloadAllConditions()

  for _, triggerSystem in pairs(triggerSystems) do
    triggerSystem.UnloadAll();
  end
  wipe(loaded);
end

function Private.Resume()
  paused = false;

  local suspended = Private.PauseAllDynamicGroups()

  for id, region in pairs(Private.regions) do
    if region.region then
      region.region:Collapse();
    end
  end

  for id, cloneList in pairs(clones) do
    for cloneId, clone in pairs(cloneList) do
      clone:Collapse();
    end
  end


  UnloadAll();
  scanForLoadsImpl();
  if loadEvents["GROUP"] then
    Private.ScanForLoadsGroup(loadEvents["GROUP"])
  end

  Private.ResumeAllDynamicGroups(suspended)
end

function Private.LoadDisplays(toLoad, ...)
  for id in pairs(toLoad) do
    local uid = WeakAuras.GetData(id).uid
    Private.RegisterForGlobalConditions(uid);
    triggerState[id].triggers = {};
    triggerState[id].triggerCount = 0;
    triggerState[id].show = false;
    triggerState[id].activeTrigger = nil;
    triggerState[id].activatedConditions = {};
    if Private.DebugLog.IsEnabled(uid) then
      WeakAuras.prettyPrint(L["Debug Logging enabled for '%s'"]:format(id))
      Private.DebugLog.Print(uid, L["Aura loaded"])
    end
  end
  for _, triggerSystem in pairs(triggerSystems) do
    triggerSystem.LoadDisplays(toLoad, ...);
  end
end

function Private.UnloadDisplays(toUnload, ...)
  for _, triggerSystem in pairs(triggerSystems) do
    triggerSystem.UnloadDisplays(toUnload, ...);
  end

  for id in pairs(toUnload) do

    for i = 1, triggerState[id].numTriggers do
      if (triggerState[id][i]) then
        wipe(triggerState[id][i]);
      end
    end
    triggerState[id].show = nil;
    triggerState[id].activeTrigger = nil;

    if (timers[id]) then
      for _, trigger in pairs(timers[id]) do
        for _, record in pairs(trigger) do
          if (record.handle) then
            timer:CancelTimer(record.handle);
          end
        end
      end
      timers[id] = nil;
    end

    local uid = WeakAuras.GetData(id).uid
    Private.UnloadConditions(uid)

    Private.regions[id].region:Collapse();
    Private.CollapseAllClones(id);

    -- Even though auras are collapsed, their finish animation can be running
    Private.CancelAnimation(Private.regions[id].region, true, true, true, true, true, true)
    if clones[id] then
      for _, region in pairs(clones[id]) do
        Private.CancelAnimation(region, true, true, true, true, true, true)
      end
    end
  end
end

function Private.FinishLoadUnload()
  for _, triggerSystem in pairs(triggerSystems) do
    triggerSystem.FinishLoadUnload();
  end
end

-- transient cache of uid => id
-- eventually, the database will be migrated to index by uid
-- and this mapping will become redundant
-- this cache is loaded lazily via pAdd()
local UIDtoID = {}

function Private.GetDataByUID(uid)
  return WeakAuras.GetData(UIDtoID[uid])
end

function Private.UIDtoID(uid)
  return UIDtoID[uid]
end

---@private
function WeakAuras.Delete(data)
  local id = data.id;
  local uid = data.uid
  local parentId = data.parent
  local parentUid = data.parent and db.displays[data.parent].uid

  Private.callbacks:Fire("AboutToDelete", uid, id, parentUid, parentId)

  if(data.parent) then
    local parentData = db.displays[data.parent];
    if(parentData and parentData.controlledChildren) then
      for index, childId in pairs(parentData.controlledChildren) do
        if(childId == id) then
          tremove(parentData.controlledChildren, index);
        end
      end
      if parentData.sortHybridTable then
        parentData.sortHybridTable[id] = nil
      end
      for parent in Private.TraverseParents(data) do
        Private.ClearAuraEnvironment(parent.id);
      end
    end
  end

  UIDtoID[data.uid] = nil
  if(data.controlledChildren) then
    for _, childId in pairs(data.controlledChildren) do
      local childData = db.displays[childId];
      if(childData) then
        childData.parent = nil;
        WeakAuras.Add(childData);
      end
    end
  end

  if Private.regions[id] and Private.regions[id].region then
    Private.regions[id].region:Collapse()
    Private.CancelAnimation(Private.regions[id].region, true, true, true, true, true, true)

    -- Groups have a empty Collapse method so, we need to hide them here
    Private.regions[id].region:Hide();

    Private.regions[id].region = nil
    Private.regions[id] = nil
  end

  if clones[id] then
    for _, region in pairs(clones[id]) do
      region:Collapse();
      Private.CancelAnimation(region, true, true, true, true, true, true)
    end
    clones[id] = nil
  end

  db.registered[id] = nil;

  for _, triggerSystem in pairs(triggerSystems) do
    triggerSystem.Delete(id);
  end


  loaded[id] = nil;
  loadFuncs[id] = nil;
  loadFuncsForOptions[id] = nil;
  for event, eventData in pairs(loadEvents) do
    eventData[id] = nil
  end

  db.displays[id] = nil;

  Private.DeleteAuraEnvironment(id)
  triggerState[id] = nil;

  if (Private.personalRessourceDisplayFrame) then
    Private.personalRessourceDisplayFrame:delete(id);
  end

  if (Private.mouseFrame) then
    Private.mouseFrame:delete(id);
  end

  Private.customActionsFunctions[id] = nil;
  Private.ExecEnv.customConditionsFunctions[id] = nil;
  Private.ExecEnv.conditionTextFormatters[id] = nil
  Private.frameLevels[id] = nil;
  Private.ExecEnv.conditionHelpers[data.uid] = nil

  Private.RemoveHistory(data.uid)

  Private.AddParents(data)
  Private.callbacks:Fire("Delete", uid, id, parentUid, parentId)
end

function WeakAuras.Rename(data, newid)
  local oldid = data.id
  if(data.parent) then
    local parentData = db.displays[data.parent];
    if(parentData.controlledChildren) then
      for index, childId in pairs(parentData.controlledChildren) do
        if(childId == data.id) then
          parentData.controlledChildren[index] = newid;
        end
      end
      if parentData.sortHybridTable and parentData.sortHybridTable[oldid] then
        parentData.sortHybridTable[newid] = true
        parentData.sortHybridTable[oldid] = nil
      end
    end
    local parentRegion = WeakAuras.GetRegion(data.parent)
    if parentRegion and parentRegion.ReloadControlledChildren then
      parentRegion:ReloadControlledChildren()
    end
  end

  UIDtoID[data.uid] = newid
  Private.regions[newid] = Private.regions[oldid];
  Private.regions[oldid] = nil;
  if Private.regions[newid] and Private.regions[newid].region then
    Private.regions[newid].region.id = newid
  end

  if(clones[oldid]) then
    clones[newid] = clones[oldid]
    clones[oldid] = nil
    for cloneid, clone in pairs(clones[newid]) do
      clone.id = newid
    end
  end

  for _, triggerSystem in pairs(triggerSystems) do
    triggerSystem.Rename(oldid, newid);
  end

  loaded[newid] = loaded[oldid];
  loaded[oldid] = nil;
  loadFuncs[newid] = loadFuncs[oldid];
  loadFuncs[oldid] = nil;

  loadFuncsForOptions[newid] = loadFuncsForOptions[oldid]
  loadFuncsForOptions[oldid] = nil;

  for event, eventData in pairs(loadEvents) do
    eventData[newid] = eventData[oldid]
    eventData[oldid] = nil
  end

  timers[newid] = timers[oldid];
  timers[oldid] = nil;

  triggerState[newid] = triggerState[oldid];
  triggerState[oldid] = nil;

  Private.RenameAuraEnvironment(oldid, newid)

  db.displays[newid] = db.displays[oldid];
  db.displays[oldid] = nil;
  db.displays[newid].id = newid;

  if(data.controlledChildren) then
    for index, childId in pairs(data.controlledChildren) do
      local childData = db.displays[childId];
      if(childData) then
        childData.parent = data.id;
      end
    end
  end

  if (Private.personalRessourceDisplayFrame) then
    Private.personalRessourceDisplayFrame:rename(oldid, newid);
  end

  if (Private.mouseFrame) then
    Private.mouseFrame:rename(oldid, newid);
  end

  Private.customActionsFunctions[newid] = Private.customActionsFunctions[oldid];
  Private.customActionsFunctions[oldid] = nil;

  Private.ExecEnv.customConditionsFunctions[newid] = Private.ExecEnv.customConditionsFunctions[oldid];
  Private.ExecEnv.customConditionsFunctions[oldid] = nil;

  Private.ExecEnv.conditionTextFormatters[newid] = Private.ExecEnv.conditionTextFormatters[oldid]
  Private.ExecEnv.conditionTextFormatters[oldid] = nil

  Private.frameLevels[newid] = Private.frameLevels[oldid];
  Private.frameLevels[oldid] = nil;

  Private.ProfileRenameAura(oldid, newid);

  -- TODO: This should not be necessary
  WeakAuras.Add(data)

  Private.callbacks:Fire("Rename", data.uid, oldid, newid)
end

function Private.Convert(data, newType)
  local id = data.id;
  Private.FakeStatesFor(id, false)

  if Private.regions[id] then
    Private.regions[id].region = nil
    Private.regions[id] = nil
  end

  data.regionType = newType;

  -- Clean up sub regions
  if data.subRegions then
    for index, subRegionData in ipairs_reverse(data.subRegions) do
      local subType = subRegionData.type
      local removeSubRegion = true
      if subType and Private.subRegionTypes[subType] then
        if Private.subRegionTypes[subType].supports(data.regionType) then
          removeSubRegion = false
        end
      end
      if removeSubRegion then
        tremove(data.subRegions, index)
        -- Adjust conditions!
        if data.conditions then
          for _, condition in ipairs(data.conditions) do
            if type(condition.changes) == "table" then
              for _, change in ipairs(condition.changes) do
                if change.property then
                  local subRegionIndex, property = change.property:match("^sub%.(%d+)%.(.*)")
                  subRegionIndex = tonumber(subRegionIndex)
                  if subRegionIndex and property then
                    if subRegionIndex == index then
                      change.property = nil
                    elseif subRegionIndex > index then
                      change.property = "sub." .. subRegionIndex -1 .. "." .. property
                    end
                  end
                end
              end
            end
          end
        end
      end
    end
  end


  WeakAuras.Add(data);

  Private.FakeStatesFor(id, true)

  local parentRegion = WeakAuras.GetRegion(data.parent)
  if parentRegion and parentRegion.ReloadControlledChildren then
    parentRegion:ReloadControlledChildren()
  end
end

-- The default mixin doesn't recurse, this does
function WeakAuras.DeepMixin(dest, source)
  local function recurse(source, dest)
    for i,v in pairs(source) do
      if(type(v) == "table") then
        dest[i] = type(dest[i]) == "table" and dest[i] or {};
        recurse(v, dest[i]);
      else
        dest[i] = v;
      end
    end
  end
  recurse(source, dest);
end

local function LastUpgrade()
  return db.lastUpgrade and date(nil, db.lastUpgrade) or "unknown"
end

function Private.NeedToRepairDatabase()
  return db.dbVersion and db.dbVersion > WeakAuras.InternalVersion()
end

local function RepairDatabase()
  local coro = coroutine.create(function()
    Private.SetImporting(true)
    -- set db version to current code version
    db.dbVersion = WeakAuras.InternalVersion()
    -- reinstall snapshots from history
    local newDB = Mixin({}, db.displays)
    coroutine.yield(1000)
    for id, data in pairs(db.displays) do
      local snapshot = Private.GetMigrationSnapshot(data.uid)
      if snapshot then
        newDB[id] = nil
        newDB[snapshot.id] = snapshot
        coroutine.yield(1000, "repair get snapshot")
      end
    end
    db.displays = newDB
    Private.SetImporting(false)
    -- finally, login
    Private.Login()
  end)
  Private.Threads:Add("repair", coro, 'urgent')
end

StaticPopupDialogs["WEAKAURAS_CONFIRM_REPAIR"] = {
  text = "",
  button1 = L["Repair"],
  button2 = L["Cancel"],
  OnAccept = function(self)
     RepairDatabase()
  end,
  OnShow = function(self)
    local AutomaticRepairText = L["WeakAuras has detected that it has been downgraded.\nYour saved auras may no longer work properly.\nWould you like to run the |cffff0000EXPERIMENTAL|r repair tool? This will overwrite any changes you have made since the last database upgrade.\nLast upgrade: %s\n\n|cffff0000You should BACKUP your WTF folder BEFORE pressing this button.|r"]
    local ManualRepairText = L["Are you sure you want to run the |cffff0000EXPERIMENTAL|r repair tool?\nThis will overwrite any changes you have made since the last database upgrade.\nLast upgrade: %s"]

    if self.data.reason == "user" then
      self.text:SetText(ManualRepairText:format(LastUpgrade()))
    else
      self.text:SetText(AutomaticRepairText:format(LastUpgrade()))
    end
  end,
  OnCancel = function(self)
    if self.data.reason ~= "user" then
      Private.Login()
    end
  end,
  whileDead = true,
  showAlert = true,
  timeout = 0,
  preferredindex = STATICPOPUP_NUMDIALOGS
}

function Private.ValidateUniqueDataIds(silent)
  -- ensure that there are no duplicated uids anywhere in the database
  local seenUIDs = {}
  for _, data in pairs(db.displays) do
    if type(data.uid) == "string" then
      if seenUIDs[data.uid] then
        if not silent then
          prettyPrint("Duplicate uid \""..data.uid.."\" detected in saved variables between \""..data.id.."\" and \""..seenUIDs[data.uid].id.."\".")
        end
        data.uid = WeakAuras.GenerateUniqueID()
        seenUIDs[data.uid] = data
      else
        seenUIDs[data.uid] = data
      end
    elseif data.uid ~= nil then
      if not silent then
        prettyPrint("Invalid uid detected in saved variables for \""..data.id.."\"")
      end
      data.uid = WeakAuras.GenerateUniqueID()
      seenUIDs[data.uid] = data
    end
  end
  for uid, data in pairs(seenUIDs) do
    UIDtoID[uid] = data.id
  end
end

function Private.SyncParentChildRelationships(silent)
  -- 1. Find all auras where data.parent ~= nil or data.controlledChildren ~= nil
  --    If an aura has data.parent which doesn't exist, then remove data.parent
  --    If an aura has data.parent which doesn't have data.controlledChildren, then remove data.parent
  -- 2. For each aura with data.controlledChildren, iterate through the list of children and remove entries where:
  --    The child doesn't exist in the database
  --    The child ID is duplicated in data.controlledChildren (only the first will be kept)
  --    The child's data.parent points to a different parent
  --    The parent is a dynamic group and the child is a group/dynamic group
  --    Otherwise, mark the child as having a valid parent relationship
  -- 3. For each aura with data.parent, remove data.parent if it was not marked to have a valid relationship in 2.
  local parents = {}
  local children = {}
  local childHasParent = {}
  for id, data in pairs(db.displays) do
    if data.parent then
      if not db.displays[data.parent] then
        if not(silent) then
          prettyPrint("Detected corruption in saved variables: "..id.." has a nonexistent parent.")
        end
        data.parent = nil
      elseif not db.displays[data.parent].controlledChildren then
        if not silent then
          prettyPrint("Detected corruption in saved variables: "..id.." thinks "..data.parent..
                      " controls it, but "..data.parent.." is not a group.")
        end
        data.parent = nil
      else
        children[id] = data
      end
    end
    if data.controlledChildren then
      parents[id] = data
    end
  end

  for id, data in pairs(parents) do
    local groupChildren = {}
    local childrenToRemove = {}
    local dynamicGroup = data.regionType == "dynamicgroup"
    for index, childID in ipairs(data.controlledChildren) do
      local child = children[childID]
      if not child then
        if not silent then
          prettyPrint("Detected corruption in saved variables: "..id.." thinks it controls "..childID.." which doesn't exist.")
        end
        childrenToRemove[index] = true
      elseif child.parent ~= id then
        if not silent then
          prettyPrint("Detected corruption in saved variables: "..id.." thinks it controls "..childID.." which it does not.")
        end
        childrenToRemove[index] = true
      elseif dynamicGroup and child.controlledChildren then
        if not silent then
          prettyPrint("Detected corruption in saved variables: "..id.." is a dynamic group and controls "..childID.." which is a group/dynamicgroup.")
        end
        child.parent = nil
        children[child.id] = nil
        childrenToRemove[index] = true
      elseif groupChildren[childID] then
        if not silent then
          prettyPrint("Detected corruption in saved variables: "..id.." has "..childID.." as a child in multiple positions.")
        end
        childrenToRemove[index] = true
      else
        groupChildren[childID] = index
        childHasParent[childID] = true
      end
    end
    if next(childrenToRemove) ~= nil then
      for i = #data.controlledChildren, 1, -1 do
        if childrenToRemove[i] then
          tremove(data.controlledChildren, i)
        end
      end
    end
  end

  for id, data in pairs(children) do
    if not childHasParent[id] then
      if not silent then
        prettyPrint("Detected corruption in saved variables: "..id.." should be controlled by "..data.parent.." but isn't.")
      end
      local parent = parents[data.parent]
      tinsert(parent.controlledChildren, id)
    end
  end
end

local function loadOrder(tbl, idtable)
  local order = {}

  local loaded = {};
  local function load(id, depends)
    local data = idtable[id];
    if(data.parent) then
      if(idtable[data.parent]) then
        if depends[data.parent] then
          error("Circular dependency in Private.AddMany between "..table.concat(depends, ", "));
        else
          if not(loaded[data.parent]) then
            local dependsOut = CopyTable(depends)
            dependsOut[data.parent] = true
            coroutine.yield(100, "sort deps")
            load(data.parent, dependsOut)
            coroutine.yield(100, "sort deps")
          end
        end
      else
        data.parent = nil;
      end
    end
    if not(loaded[id]) then
      coroutine.yield(100, "sort deps");
      loaded[id] = true;
      tinsert(order, idtable[id])
    end
  end

  for id, data in pairs(idtable) do
    load(id, {});
    coroutine.yield(100, "sort deps")
  end

  return order
end

---@type fun(data: auraData)
local pAdd

---@param tbl auraData[]
---@param takeSnapshots boolean
function Private.AddMany(tbl, takeSnapshots)
  --- @type table<auraId, auraData>
  local idtable = {};
  --- @type table<auraId, auraId> The anchoring targets of other auras, key is the anchor, value is the aura that is anchoring
  local anchorTargets = {}
  for _, data in ipairs(tbl) do
    -- There was an unfortunate bug in update.lua in 2022 that resulted
    -- in auras having a circular dependencies
    -- Fix one of the two known cases here
    if data.id == data.parent then
      data.parent = nil
      tDeleteItem(data.controlledChildren, data.id)
    end
    idtable[data.id] = data;
    if data.anchorFrameType == "SELECTFRAME" and data.anchorFrameFrame and data.anchorFrameFrame:sub(1, 10) == "WeakAuras:" then
      anchorTargets[data.anchorFrameFrame:sub(11)] = data.id
    end
  end

  -- Now fix up anchors, see #3971, where aura p was anchored to aura c and where c was a child of p, thus c was anchored to p
  -- The game used to detect such anchoring circles. We can't detect all of them, but at least detect the one from the ticket.
  for target, source in pairs(anchorTargets) do
    -- We walk up the parent's of target, to check for source
    local parent = target
    if idtable[target] then
      while(parent) do
        if parent == source then
          WeakAuras.prettyPrint(L["Warning: Anchoring to your own child '%s' in aura '%s' is imposssible."]:format(target, source))
          idtable[source].anchorFrameType = "SCREEN"
        end
        parent = idtable[parent].parent
      end
    end
  end

  local order = loadOrder(tbl, idtable)
  coroutine.yield(5000)

  local oldSnapshots = {}
  local copies = {}
  if takeSnapshots then
    for _, data in ipairs(order) do
      if Private.ModernizeNeedsOldSnapshot(data) then
        oldSnapshots[data.uid] = Private.GetMigrationSnapshot(data.uid)
      end
      copies[data.uid] = CopyTable(data)
      coroutine.yield(200, "addmany prepare snapshot")
    end
    Private.Threads:Add("snapshot", coroutine.create(function()
      prettyPrint(L["WeakAuras is creating a rollback snapshot of your auras. This snapshot will allow you to revert to the current state of your auras if something goes wrong. This process may cause your framerate to drop until it is complete."])
      for uid, data in pairs(copies) do
        Private.SetMigrationSnapshot(uid, data)
        coroutine.yield(200, "snapshot")
      end
      prettyPrint(L["Rollback snapshot is complete. Thank you for your patience!"])
    end), 'normal')
  end

  local groups = {}
  local bads = {}
  for _, data in ipairs(order) do
    if data.parent and bads[data.parent] then
      bads[data.id] = true
    else
      local oldSnapshot = oldSnapshots[data.uid] or nil
      local ok = xpcall(WeakAuras.PreAdd, Private.GetErrorHandlerUid(data.uid, "PreAdd"), data, oldSnapshot)
      if not ok then
        prettyPrint(L["Unable to modernize aura '%s'. This is probably due to corrupt data or a bad migration, please report this to the WeakAuras team."]:format(data.id))
        if data.regionType == "dynamicgroup" or data.regionType == "group" then
          prettyPrint(L["All children of this aura will also not be loaded, to minimize the chance of further corruption."])
        end
        bads[data.id] = true
      elseif data.regionType == "dynamicgroup" or data.regionType == "group" then
        groups[data] = true
      end
      coroutine.yield(1000, "addmany modernize")
    end
  end

  for _, data in ipairs(order) do
    if not bads[data.id] then
      if data.parent and bads[data.parent] then
        bads[data.id] = true
      else
        local ok = xpcall(pAdd, Private.GetErrorHandlerUid(data.uid, "pAdd"), data)
        if not ok then
          bads[data.id] = true
        end
      end
    end
    coroutine.yield(2000, "addmany add")
  end

  for id in pairs(anchorTargets) do
    local data = idtable[id]
    if data and not bads[data.id] and (data.parent == nil or idtable[data.parent].regionType ~= "dynamicgroup") then
      Private.EnsureRegion(id)
      coroutine.yield(100, "addmany ensure anchor")
    end
  end

  for data in pairs(groups) do
    if not bads[data.id] then
      if data.type == "dynamicgroup" then
        if Private.regions[data.id] and Private.regions[data.id].region then
          Private.regions[data.id].region:ReloadControlledChildren()
        end
      else
        WeakAuras.Add(data)
      end
    end
    coroutine.yield(1000, "addmany reload dynamic group");
  end
end

local function customOptionIsValid(option)
  if not option.type then
    return false
  elseif Private.author_option_classes[option.type] == "simple" then
    if not option.key
    or not option.name
    or not option.default == nil then
      return false
    end
  elseif Private.author_option_classes[option.type] == "group" then
    if not option.key
    or not option.name
    or not option.default == nil
    or not option.subOptions then
      return false
    end
  end
  return true
end

local function validateUserConfig(data, options, config)
  local authorOptionKeys, corruptOptions = {}, {}
  for index, option in ipairs(options) do
    if not customOptionIsValid(option) or authorOptionKeys[option.key] then
      prettyPrint(data.id .. " Custom Option #" .. index .. " in " .. data.id .. " has been detected as corrupt, and has been deleted.")
      corruptOptions[index] = true
    else
      local optionClass = Private.author_option_classes[option.type]
      if option.key then
        authorOptionKeys[option.key] = index
      end
      if optionClass == "simple" then
        if not option.key then
          option.key = WeakAuras.GenerateUniqueID()
        end
        if config[option.key] == nil then
          if type(option.default) ~= "table" then
            config[option.key] = option.default
          else
            config[option.key] = CopyTable(option.default)
          end
        end
      elseif optionClass == "group" then
        local subOptions = option.subOptions
        if type(config[option.key]) ~= "table" then
          config[option.key] = {}
        end
        local subConfig = config[option.key]
        if option.groupType == "array" then
          for k, v in pairs(subConfig) do
            if type(k) ~= "number" or type(v) ~= "table" then
              -- if k was not a number, then this was a simple group before
              -- if v is not a table, then this was likely a color option
              wipe(subConfig) -- second iteration will fill table with defaults
              break
            end
          end
          if option.limitType == "fixed" then
            for i = #subConfig + 1, option.size do
              -- add missing entries
              subConfig[i] = {}
            end
          end
          if option.limitType ~= "none" then
            for i = option.size + 1, #subConfig do
              -- remove excess entries
              subConfig[i] = nil
            end
          end
          for _, toValidate in pairs(subConfig) do
            validateUserConfig(data, subOptions, toValidate)
          end
        else
          if type(next(subConfig)) ~= "string" then
            -- either there are no sub options, in which case this is a noop
            -- or this group was previously an array, in which case we need to wipe
            wipe(subConfig)
          end
          validateUserConfig(data, subOptions, subConfig)
        end
      end
    end
  end
  for key, value in pairs(config) do
    if not authorOptionKeys[key] then
      config[key] = nil
    else
      local option = options[authorOptionKeys[key]]
      local optionClass = Private.author_option_classes[option.type]
      if optionClass ~= "group" then
        local option = options[authorOptionKeys[key]]
        if option.type == "media" then
          -- sounds can be number or string, other kinds of media can only be string
          if type(value) ~= "string" and (type(value) ~= "number" or option.mediaType ~= "sound") then
            config[key] = option.default
          end
        elseif type(value) ~= type(option.default) then
          -- if type mismatch then we know that it can't be right
          if type(option.default) ~= "table" then
            config[key] = option.default
          else
            config[key] = CopyTable(option.default)
          end
        elseif option.type == "input" and option.useLength then
          config[key] = config[key]:sub(1, option.length)
        elseif option.type == "number" or option.type == "range" then
          if (option.max and option.max < value) or (option.min and option.min > value) then
            config[key] = option.default
          else
            if option.type == "number" and option.step then
              local min = option.min or 0
              config[key] = option.step * Round((value - min)/option.step) + min
            end
          end
        elseif option.type == "select" then
          if value < 1 or value > #option.values then
            config[key] = option.default
          end
        elseif option.type == "multiselect" then
          local multiselect = config[key]
          for i, v in ipairs(multiselect) do
            if option.default[i] ~= nil then
              if type(v) ~= "boolean" then
                multiselect[i] = option.default[i]
              end
            else
              multiselect[i] = nil
            end
          end
          for i, v in ipairs(option.default) do
            if type(multiselect[i]) ~= "boolean" then
              multiselect[i] = v
            end
          end
        elseif option.type == "color" then
          for i = 1, 4 do
            local c = config[key][i]
            if type(c) ~= "number" or c < 0 or c > 1 then
              config[key] = option.default
              break
            end
          end
        end
      end
    end
  end
  for i = #options, 1, -1 do
    if corruptOptions[i] then
      tremove(options, i)
    end
  end
end


local oldDataStub = {
  -- note: this is the minimal data stub which prevents false positives in diff upon reimporting an aura.
  -- pending a refactor of other code which adds unnecessary fields, it is possible to shrink it
  trigger = {
    type = "aura",
    names = {},
    event = "Health",
    subeventPrefix = "SPELL",
    subeventSuffix = "_CAST_START",
    spellIds = {},
    unit = "player",
    debuffType = "HELPFUL",
  },
  numTriggers = 1,
  untrigger = {},
  load = {
    size = {
      multi = {},
    },
    spec = {
      multi = {},
    },
    class = {
      multi = {},
    },
  },
  actions = {
    init = {},
    start = {},
    finish = {},
  },
  animation = {
    start = {
      type = "none",
      duration_type = "seconds",
    },
    main = {
      type = "none",
      duration_type = "seconds",
    },
    finish = {
      type = "none",
      duration_type = "seconds",
    },
  },
  conditions = {},
}

local oldDataStub2 = {
  -- note: this is the minimal data stub which prevents false positives in diff upon reimporting an aura.
  -- pending a refactor of other code which adds unnecessary fields, it is possible to shrink it
  triggers = {
    {
      trigger = {
        type = "aura",
        names = {},
        event = "Health",
        subeventPrefix = "SPELL",
        subeventSuffix = "_CAST_START",
        spellIds = {},
        unit = "player",
        debuffType = "HELPFUL",
      },
      untrigger = {},
    },
  },
  load = {
    size = {
      multi = {},
    },
    spec = {
      multi = {},
    },
    class = {
      multi = {},
    },
  },
  actions = {
    init = {},
    start = {},
    finish = {},
  },
  animation = {
    start = {
      type = "none",
      duration_type = "seconds",
    },
    main = {
      type = "none",
      duration_type = "seconds",
    },
    finish = {
      type = "none",
      duration_type = "seconds",
    },
  },
  conditions = {},
}

function Private.UpdateSoundIcon(data)
  local function testConditions()
    local sound, tts
    if data.conditions then
      for _, condition in ipairs(data.conditions) do
        for changeIndex, change in ipairs(condition.changes) do
          if change.property == "sound" then
            sound = true
          end
          if change.property == "chat" and change.value and change.value.message_type == "TTS" then
            tts = true
          end
          if sound and tts then break end
        end
      end
    end
    return sound, tts
  end

  local soundCondition, ttsCondition = testConditions()

  -- sound
  if data.actions.start.do_sound or data.actions.finish.do_sound then
    Private.AuraWarnings.UpdateWarning(data.uid, "sound_action", "sound", L["This aura plays a sound via an action."])
  else
    Private.AuraWarnings.UpdateWarning(data.uid, "sound_action")
  end

  if soundCondition then
    Private.AuraWarnings.UpdateWarning(data.uid, "sound_condition", "sound", L["This aura plays a sound via a condition."])
  else
    Private.AuraWarnings.UpdateWarning(data.uid, "sound_condition")
  end

  -- tts
  if (data.actions.start.do_message and data.actions.start.message_type == "TTS")
  or (data.actions.finish.do_message and data.actions.finish.message_type == "TTS")
  then
    Private.AuraWarnings.UpdateWarning(data.uid, "tts_action", "tts", L["This aura plays a Text To Speech via an action."])
  else
    Private.AuraWarnings.UpdateWarning(data.uid, "tts_action")
  end

  if ttsCondition then
    Private.AuraWarnings.UpdateWarning(data.uid, "tts_condition", "tts", L["This aura plays a Text To Speech via a condition."])
  else
    Private.AuraWarnings.UpdateWarning(data.uid, "tts_condition")
  end
end

function WeakAuras.PreAdd(data, snapshot)
  if not data then return end
  -- Readd what Compress removed before version 8
  if (not data.internalVersion or data.internalVersion < 7) then
    Private.validate(data, oldDataStub)
  elseif (data.internalVersion < 8) then
    Private.validate(data, oldDataStub2)
  end

  xpcall(Private.Modernize, Private.GetErrorHandlerId(data.id, L["Modernize"]), data, snapshot)

  local default = data.regionType and Private.regionTypes[data.regionType] and Private.regionTypes[data.regionType].default
  if default then
    Private.validate(data, default)
  end

  local regionValidate = data.regionType and Private.regionTypes[data.regionType] and Private.regionTypes[data.regionType].validate
  if regionValidate then
    regionValidate(data)
  end

  Private.validate(data, Private.data_stub);
  if data.subRegions then
    for _, subRegionData in ipairs(data.subRegions) do
      local subType = subRegionData.type
      if subType and Private.subRegionTypes[subType] then
        if Private.subRegionTypes[subType].supports(data.regionType) then
          local default = Private.subRegionTypes[subType].default
          if type(default) == "function" then
            default = default(data.regionType)
          end
          if default then
            Private.validate(subRegionData, default)
          end
        else
          WeakAuras.prettyPrint(L["ERROR in '%s' unknown or incompatible sub element type '%s'"]:format(data.id, subType))
        end
      end
    end
  end
  validateUserConfig(data, data.authorOptions, data.config)
  data.init_started = nil
  data.init_completed = nil
  data.expanded = nil
end

function pAdd(data, simpleChange)
  local id = data.id;
  if not(id) then
    error("Improper arguments to WeakAuras.Add - id not defined");
    return;
  end

  data.uid = data.uid or WeakAuras.GenerateUniqueID()
  if db.displays[id] and db.displays[id].uid ~= data.uid then
    print("Improper? arguments to WeakAuras.Add - id", id, "is assigned to a different uid.", data.uid, db.displays[id].uid)
  end
  if UIDtoID[data.uid] and UIDtoID[data.uid] ~= id then
    print("Improper? arguments to WeakAuras.Add - uid is assigned to a id. Uid:", data.uid, "assigned too:", UIDtoID[data.uid], "assigning now to", data.id)
  end

  local otherID = UIDtoID[data.uid]
  if not otherID then
    UIDtoID[data.uid] = id
  elseif otherID ~= id then
    -- duplicate uid
    data.uid = WeakAuras.GenerateUniqueID()
    UIDtoID[data.uid] = id
  end

  if simpleChange then
    db.displays[id] = data
    if WeakAuras.GetRegion(data.id) then
      Private.SetRegion(data)
    end
    if clones[id] then
      for cloneId, region in pairs(clones[id]) do
        Private.SetRegion(data, cloneId)
      end
    end
    Private.UpdatedTriggerState(id)
    Private.callbacks:Fire("Add", data.uid, data.id, data, simpleChange)
  else
    Private.DebugLog.SetEnabled(data.uid, data.information.debugLog)

    if Private.IsGroupType(data) then
      Private.ClearAuraEnvironment(id);
      for parent in Private.TraverseParents(data) do
        Private.ClearAuraEnvironment(parent.id);
      end
      db.displays[id] = data;
      if WeakAuras.GetRegion(data.id) then
        Private.SetRegion(data)
      end
      Private.ScanForLoadsGroup({[id] = true});
      loadEvents["GROUP"] = loadEvents["GROUP"] or {}
      loadEvents["GROUP"][id] = true
    else -- Non group aura
      -- Make sure that we don't have a controlledChildren member.
      data.controlledChildren = nil
      local visible
      if (WeakAuras.IsOptionsOpen()) then
        visible = Private.FakeStatesFor(id, false)
      else
        if (Private.regions[id] and Private.regions[id].region) then
          Private.regions[id].region:Collapse()
        else
          Private.CollapseAllClones(id)
        end
      end

      -- If the aura has a onHide animation we need to cancel it to ensure it's truly hidden now
      if Private.regions[id] then
        Private.CancelAnimation(Private.regions[id].region, true, true, true, true, true, true)
      end
      if clones[id] then
        for _, region in pairs(clones[id]) do
          Private.CancelAnimation(region, true, true, true, true, true, true)
        end
      end

      Private.ClearAuraEnvironment(id);
      for parent in Private.TraverseParents(data) do
        Private.ClearAuraEnvironment(parent.id);
      end

      db.displays[id] = data;

      if (not data.triggers.activeTriggerMode or data.triggers.activeTriggerMode > #data.triggers) then
        data.triggers.activeTriggerMode = Private.trigger_modes.first_active;
      end

      for _, triggerSystem in pairs(triggerSystems) do
        triggerSystem.Add(data);
      end

      local loadFuncStr, events = ConstructFunction(load_prototype, data.load);
      for event, eventData in pairs(loadEvents) do
        eventData[id] = nil
      end
      for event in pairs(events) do
        loadEvents[event] = loadEvents[event] or {}
        loadEvents[event][id] = true
      end
      loadEvents["SCAN_ALL"] = loadEvents["SCAN_ALL"] or {}
      loadEvents["SCAN_ALL"][id] = true

      local loadForOptionsFuncStr = ConstructFunction(load_prototype, data.load, true);
      local loadFunc = Private.LoadFunction(loadFuncStr);
      local loadForOptionsFunc = Private.LoadFunction(loadForOptionsFuncStr);
      local triggerLogicFunc;
      if data.triggers.disjunctive == "custom" then
        triggerLogicFunc = WeakAuras.LoadFunction("return "..(data.triggers.customTriggerLogic or ""));
      end

      LoadCustomActionFunctions(data);
      Private.LoadConditionPropertyFunctions(data);
      Private.LoadConditionFunction(data)

      loadFuncs[id] = loadFunc;
      loadFuncsForOptions[id] = loadForOptionsFunc;
      clones[id] = clones[id] or {};

      if (timers[id]) then
        for _, trigger in pairs(timers[id]) do
          for _, record in pairs(trigger) do
            if (record.handle) then
              timer:CancelTimer(record.handle);
            end
          end
        end
        timers[id] = nil;
      end

      if WeakAuras.GetRegion(data.id) then
        Private.SetRegion(data)
      end

      triggerState[id] = {
        disjunctive = data.triggers.disjunctive or "all",
        numTriggers = #data.triggers,
        activeTriggerMode = data.triggers.activeTriggerMode or Private.trigger_modes.first_active,
        triggerLogicFunc = triggerLogicFunc,
        triggers = {},
        triggerCount = 0,
        activatedConditions = {},
      };

      LoadEncounterInitScripts(id);

      if (WeakAuras.IsOptionsOpen()) then
        Private.FakeStatesFor(id, visible)
      end

      if not(paused) then
        Private.ScanForLoads({[id] = true});
      end
    end

    Private.UpdateSoundIcon(data)
    Private.callbacks:Fire("Add", data.uid, data.id, data, simpleChange)
  end
end

---@private
function WeakAuras.Add(data, simpleChange)
  local oldSnapshot
  if Private.ModernizeNeedsOldSnapshot(data) then
    oldSnapshot = Private.GetMigrationSnapshot(data.uid)
  end
  if (data.internalVersion or 0) < internalVersion then
    Private.SetMigrationSnapshot(data.uid, data)
  end
  local ok = xpcall(WeakAuras.PreAdd, Private.GetErrorHandlerUid(data.uid, "PreAdd"), data, oldSnapshot)
  if ok then
    pAdd(data, simpleChange)
  end
end

function Private.AddParents(data)
  local parent = data.parent
  if (parent) then
    local parentData = WeakAuras.GetData(parent)
    WeakAuras.Add(parentData)
    Private.AddParents(parentData)
  end
end

function Private.SetRegion(data, cloneId)
  local regionType = data.regionType;
  if not(regionType) then
    error("Improper arguments to Private.SetRegion - regionType not defined in ".. data.id)
  else
    if(not regionTypes[regionType]) then
      regionType = "fallback";
      print("Improper arguments to WeakAuras.CreateRegion - regionType \""..data.regionType.."\" is not supported in ".. data.id)
    end

    local id = data.id;
    if not(id) then
      error("Improper arguments to Private.SetRegion - id not defined");
    else
      local region;
      if(cloneId) then
        region = clones[id][cloneId];
        if (not region or region.regionType ~= data.regionType) then
          if (region) then
            clonePool[region.regionType] = clonePool[region.regionType] or {};
            tinsert(clonePool[region.regionType], region);
            region:Hide();
          end
          if(clonePool[data.regionType] and clonePool[data.regionType][1]) then
            clones[id][cloneId] = tremove(clonePool[data.regionType]);
          else
            local clone = regionTypes[data.regionType].create(WeakAurasFrame, data);
            clone.regionType = data.regionType;
            clone:Hide();
            clones[id][cloneId] = clone;
          end
          region = clones[id][cloneId];
        end
      else
        if((not Private.regions[id]) or (not Private.regions[id].region) or Private.regions[id].regionType ~= regionType) then
          region = regionTypes[regionType].create(WeakAurasFrame, data);
          Private.regions[id] = {
            regionType = regionType,
            region = region
          };
          if regionType ~= "dynamicgroup" and regionType ~= "group" then
            region.toShow = false
            region:Hide()
          else
            region.toShow = true
          end
        else
          region = Private.regions[id].region
        end
      end
      region.id = id;
      region.cloneId = cloneId or "";
      Private.validate(data, regionTypes[regionType].default);

      local parent = WeakAurasFrame;
      if(data.parent) then
        if WeakAuras.GetData(data.parent) then
          parent = Private.EnsureRegion(data.parent)
        else
          data.parent = nil;
        end
      end
      local loginFinished = WeakAuras.IsLoginFinished();
      local anim_cancelled = loginFinished and Private.CancelAnimation(region, true, true, true, true, true, true);

      regionTypes[regionType].modify(parent, region, data);
      Private.regionPrototype.AddSetDurationInfo(region, data.uid)
      Private.regionPrototype.AddExpandFunction(data, region, cloneId, parent, parent.regionType)

      data.animation = data.animation or {};
      data.animation.start = data.animation.start or {type = "none"};
      data.animation.main = data.animation.main or {type = "none"};
      data.animation.finish = data.animation.finish or {type = "none"};
      data.animation.start.duration_type = data.animation.start.duration_type or "seconds"
      data.animation.main.duration_type = data.animation.main.duration_type or "seconds"
      data.animation.finish.duration_type = data.animation.finish.duration_type or "seconds"

      if(cloneId) then
        clonePool[regionType] = clonePool[regionType] or {};
      end
      if(anim_cancelled) then
        Private.Animate("display", data.uid, "main", data.animation.main, region, false, nil, true, cloneId);
      end
      return region;
    end
  end
end

--- Ensures that a clone exists
---@param id auraId
---@param cloneId string
---@return table
local function EnsureClone(id, cloneId)
  clones[id] = clones[id] or {}
  if not(clones[id][cloneId]) then
    local data = WeakAuras.GetData(id)
    Private.SetRegion(data, cloneId)
  end
  return clones[id][cloneId]
end

local creatingRegions = false

function Private.CreatingRegions()
  return creatingRegions
end

--- Ensures that a region exists
---@param id auraId
---@return table
local function EnsureRegion(id)
  if not Private.regions[id] or not Private.regions[id].region then
    Private.regions[id] = Private.regions[id] or {}

    -- The region doesn't yet exist
    -- But we must also ensure that our parents exists

    -- So we go up the list of parents and collect auras that must be created
    -- If we find a parent already exists, we can stop
    --- @type auraId[]
    local aurasToCreate = {}

    while(id) do
      local data = WeakAuras.GetData(id)
      tinsert(aurasToCreate, data.id)
      id = data.parent
    end

    for _, toCreateId in ipairs_reverse(aurasToCreate) do
      local data = WeakAuras.GetData(toCreateId)
      Private.SetRegion(data)
    end
  end
  return Private.regions[id] and Private.regions[id].region
end

--- Ensures that a region/clone exists and returns it
function Private.EnsureRegion(id, cloneId)
  -- Even if we are asked to only create a clone, we create the default region
  -- too.
  EnsureRegion(id)
  if(cloneId and cloneId ~= "") then
    return EnsureClone(id, cloneId);
  end
  return WeakAuras.GetRegion(id)
end

---returns the region, if it exists
---@param id auraId
---@param cloneId string|nil
---@return table|nil
function WeakAuras.GetRegion(id, cloneId)
  if(cloneId and cloneId ~= "") then
    return clones[id] and clones[id][cloneId]
  end
  return Private.regions[id] and Private.regions[id].region
end

-- Note, does not create a clone!
function Private.GetRegionByUID(uid, cloneId)
  local id = Private.UIDtoID(uid)
  if(cloneId and cloneId ~= "") then
    return id and clones[id] and clones[id][cloneId];
  end
  return id and Private.regions[id] and Private.regions[id].region
end

function Private.CollapseAllClones(id, triggernum)
  if(clones[id]) then
    for i,v in pairs(clones[id]) do
      v:Collapse();
    end
  end
end

function Private.SetAllStatesHidden(id, triggernum)
  local triggerState = WeakAuras.GetTriggerStateForTrigger(id, triggernum);
  local changed = false
  for _, state in pairs(triggerState) do
    changed = changed or state.show
    state.show = false;
    state.changed = true;
  end
  return changed
end

function Private.SetAllStatesHiddenExcept(id, triggernum, list)
  local triggerState = WeakAuras.GetTriggerStateForTrigger(id, triggernum);
  for cloneId, state in  pairs(triggerState) do
    if (not (list[cloneId])) then
      state.show = false;
      state.changed = true;
    end
  end
end

function Private.ReleaseClone(id, cloneId, regionType)
  if (not clones[id]) then
    return;
  end
  local region = clones[id][cloneId];
  clones[id][cloneId] = nil;
  if region:IsProtected() then
    WeakAuras.prettyPrint(L["Error '%s' created a secure clone. We advise deleting the aura. For more information:\nhttps://github.com/WeakAuras/WeakAuras2/wiki/Protected-Frames"]:format(id))
  else
    clonePool[regionType][#clonePool[regionType] + 1] = region;
  end
end

function Private.HandleChatAction(message_type, message, message_dest, message_dest_isunit, message_channel, r, g, b, region, customFunc, when, formatters, voice)
  local useHiddenStates = when == "finish"
  if (message:find('%%')) then
    message = Private.ReplacePlaceHolders(message, region, customFunc, useHiddenStates, formatters);
  end
  if(message_type == "PRINT") then
    DEFAULT_CHAT_FRAME:AddMessage(message, r or 1, g or 1, b or 1);
  elseif message_type == "TTS" then
    local validVoice = voice and Private.tts_voices[voice]
    if not Private.SquelchingActions() then
      pcall(function()
        C_VoiceChat.SpeakText(
          validVoice and voice or next(Private.tts_voices) or 0,
          message,
          1,
          C_TTSSettings and C_TTSSettings.GetSpeechRate() or 0,
          C_TTSSettings and C_TTSSettings.GetSpeechVolume() or 100
        );
      end)
    end
  elseif message_type == "ERROR" then
    UIErrorsFrame:AddMessage(message, r or 1, g or 1, b or 1)
  elseif(message_type == "COMBAT") then
    if(CombatText_AddMessage) then
      CombatText_AddMessage(message, COMBAT_TEXT_SCROLL_FUNCTION, r or 1, g or 1, b or 1);
    end
  elseif(message_type == "WHISPER") then
    if(message_dest) then
      if (message_dest:find('%%')) then
        message_dest = Private.ReplacePlaceHolders(message_dest, region, customFunc, useHiddenStates, formatters);
      end
      if message_dest_isunit == true then
        message_dest = GetUnitName(message_dest, true)
      end
      pcall(function() SendChatMessage(message, "WHISPER", nil, message_dest) end);
    end
  elseif(message_type == "SMARTRAID") then
    local isInstanceGroup = IsInGroup(LE_PARTY_CATEGORY_INSTANCE)
    if UnitInBattleground("player") then
      pcall(function() SendChatMessage(message, "INSTANCE_CHAT") end)
    elseif UnitInRaid("player") then
      pcall(function() SendChatMessage(message, "RAID") end)
    elseif UnitInParty("player") then
      if isInstanceGroup then
        pcall(function() SendChatMessage(message, "INSTANCE_CHAT") end)
      else
        pcall(function() SendChatMessage(message, "PARTY") end)
      end
    else
      if IsInInstance() then
        pcall(function() SendChatMessage(message, "SAY") end)
      end
    end
  elseif(message_type == "SAY" or message_type == "YELL") then
    if IsInInstance() then
      pcall(function() SendChatMessage(message, message_type, nil, nil) end)
    end
  else
    pcall(function() SendChatMessage(message, message_type, nil, nil) end);
  end
end

local function actionGlowStop(actions, frame, id)
  if not frame.__WAGlowFrame then return end
  if actions.glow_type == "buttonOverlay" then
    LCG.ButtonGlow_Stop(frame.__WAGlowFrame)
  elseif actions.glow_type == "Pixel" then
    LCG.PixelGlow_Stop(frame.__WAGlowFrame, id)
  elseif actions.glow_type == "ACShine" then
    LCG.AutoCastGlow_Stop(frame.__WAGlowFrame, id)
  elseif actions.glow_type == "Proc" then
    LCG.ProcGlow_Stop(frame.__WAGlowFrame, id)
  end
end

local function actionGlowStart(actions, frame, id)
  if not frame.__WAGlowFrame then
    frame.__WAGlowFrame = CreateFrame("Frame", nil, frame)
    frame.__WAGlowFrame:SetAllPoints(frame)
    frame.__WAGlowFrame:SetSize(frame:GetSize())
  end
  local glow_frame = frame.__WAGlowFrame
  if glow_frame:GetWidth() < 1 or glow_frame:GetHeight() < 1 then
    actionGlowStop(actions, frame)
    return
  end
  local color = actions.use_glow_color and actions.glow_color or nil
  if actions.glow_type == "buttonOverlay" then
    LCG.ButtonGlow_Start(glow_frame, color)
  elseif actions.glow_type == "Pixel" then
    LCG.PixelGlow_Start(
      glow_frame,
      color,
      actions.glow_lines,
      actions.glow_frequency,
      actions.glow_length,
      actions.glow_thickness,
      actions.glow_XOffset,
      actions.glow_YOffset,
      actions.glow_border and true or false,
      id
    )
  elseif actions.glow_type == "ACShine" then
    LCG.AutoCastGlow_Start(
      glow_frame,
      color,
      actions.glow_lines,
      actions.glow_frequency,
      actions.glow_scale,
      actions.glow_XOffset,
      actions.glow_YOffset,
      id
    )
  elseif actions.glow_type == "Proc" then
    LCG.ProcGlow_Start(glow_frame, {
      color = color,
      startAnim = actions.glow_startAnim and true or false,
      xOffset = actions.glow_XOffset,
      yOffset = actions.glow_YOffset,
      duration = actions.glow_duration or 1,
      key = id
  })
  end
end

local glow_frame_monitor
local anchor_unitframe_monitor
Private.dyngroup_unitframe_monitor = {}
do
  local function frame_monitor_callback(event, frame, unit, previousUnit)
    local new_frame
    local FRAME_UNIT_UPDATE = event == "FRAME_UNIT_UPDATE"
    local FRAME_UNIT_ADDED = event == "FRAME_UNIT_ADDED"
    local FRAME_UNIT_REMOVED = event == "FRAME_UNIT_REMOVED"

    local dynamicGroupsToUpdate = {}

    if type(glow_frame_monitor) == "table" then
      for region, data in pairs(glow_frame_monitor) do
        if region.state and type(region.state.unit) == "string" and UnitIsUnit(region.state.unit, unit)
        and ((data.frame ~= frame) and (FRAME_UNIT_ADDED or FRAME_UNIT_UPDATE))
        or ((data.frame == frame) and FRAME_UNIT_REMOVED)
        then
          if not new_frame then
            new_frame = WeakAuras.GetUnitFrame(unit)
          end
          if new_frame ~= data.frame then
            local id = region.id .. (region.cloneId or "")
            -- remove previous glow
            if data.frame then
              actionGlowStop(data.actions, data.frame, id)
            end
            data.frame = new_frame
            if new_frame then
              -- apply the glow to new_frame
              actionGlowStart(data.actions, data.frame, id)
              -- update hidefunc
              local region = region
              region.active_glows_hidefunc = region.active_glows_hidefunc or {}
              region.active_glows_hidefunc[data.frame] = function()
                actionGlowStop(data.actions, data.frame, id)
                glow_frame_monitor[region] = nil
              end
            end
          end
        end
      end
    end
    if type(anchor_unitframe_monitor) == "table" then
      for region, data in pairs(anchor_unitframe_monitor) do
        if region.state and type(region.state.unit) == "string" and UnitIsUnit(region.state.unit, unit)
        and ((data.frame ~= frame) and (FRAME_UNIT_ADDED or FRAME_UNIT_UPDATE))
        or ((data.frame == frame) and FRAME_UNIT_REMOVED)
        then
          if not new_frame then
            new_frame = WeakAuras.GetUnitFrame(unit) or WeakAuras.HiddenFrames
          end
          if new_frame ~= data.frame then
            Private.AnchorFrame(data.data, region, data.parent)
          end
        end
      end
    end
    for regionData, data_frame in pairs(Private.dyngroup_unitframe_monitor) do
      if regionData.region.state and type(regionData.region.state.unit) == "string" and UnitIsUnit(regionData.region.state.unit, unit)
      and ((data_frame ~= frame) and (FRAME_UNIT_ADDED or FRAME_UNIT_UPDATE))
      or ((data_frame == frame) and FRAME_UNIT_REMOVED)
      then
        if not new_frame then
          new_frame = WeakAuras.GetUnitFrame(unit) or WeakAuras.HiddenFrames
        end
        if new_frame and new_frame ~= data_frame then
          dynamicGroupsToUpdate[regionData.parent] = true
        end
      end
    end

    for frame in pairs(dynamicGroupsToUpdate) do
      frame:DoPositionChildren()
    end
  end

  LGF.RegisterCallback("WeakAuras", "FRAME_UNIT_UPDATE", frame_monitor_callback)
  LGF.RegisterCallback("WeakAuras", "FRAME_UNIT_ADDED", frame_monitor_callback)
  LGF.RegisterCallback("WeakAuras", "FRAME_UNIT_REMOVED", frame_monitor_callback)
end

function Private.HandleGlowAction(actions, region)
  if actions.glow_action
  and (
    (
      (actions.glow_frame_type == "UNITFRAME" or actions.glow_frame_type == "NAMEPLATE")
      and region.state.unit
    )
    or (actions.glow_frame_type == "FRAMESELECTOR" and actions.glow_frame)
    or (actions.glow_frame_type == "PARENTFRAME" and region:GetParent())
  )
  then
    local glow_frame, should_glow_frame
    if actions.glow_frame_type == "FRAMESELECTOR" then
      if actions.glow_frame:sub(1, 10) == "WeakAuras:" then
        local frame_name = actions.glow_frame:sub(11)
        if WeakAuras.GetData(frame_name) then
          Private.EnsureRegion(frame_name)
        end
        if Private.regions[frame_name] and Private.regions[frame_name].region then
          glow_frame = Private.regions[frame_name].region
          should_glow_frame = true
        end
      else
        glow_frame = Private.GetSanitizedGlobal(actions.glow_frame)
        should_glow_frame = true
      end
    elseif actions.glow_frame_type == "UNITFRAME" and region.state.unit then
      glow_frame = WeakAuras.GetUnitFrame(region.state.unit)
      should_glow_frame = true
    elseif actions.glow_frame_type == "NAMEPLATE" and region.state.unit then
      glow_frame = WeakAuras.GetUnitNameplate(region.state.unit)
      should_glow_frame = true
    elseif actions.glow_frame_type == "PARENTFRAME" then
      glow_frame = region:GetParent()
      should_glow_frame = true
    end

    if should_glow_frame then
      local id = region.id .. (region.cloneId or "")
      if actions.glow_action == "show" then
        -- remove previous glow
        if glow_frame then
          if region.active_glows_hidefunc
          and region.active_glows_hidefunc[glow_frame]
          then
            region.active_glows_hidefunc[glow_frame]()
          end
          -- start glow
          actionGlowStart(actions, glow_frame, id)
          -- make unglow function & monitor unitframe changes
          region.active_glows_hidefunc = region.active_glows_hidefunc or {}
          if actions.glow_frame_type == "UNITFRAME" then
            region.active_glows_hidefunc[glow_frame] = function()
              actionGlowStop(actions, glow_frame, id)
              glow_frame_monitor[region] = nil
            end
          else
            region.active_glows_hidefunc[glow_frame] = function()
              actionGlowStop(actions, glow_frame, id)
            end
          end
        end
        if actions.glow_frame_type == "UNITFRAME" then
          glow_frame_monitor = glow_frame_monitor or {}
          glow_frame_monitor[region] = {
            actions = actions,
            frame = glow_frame
          }
        end
      elseif actions.glow_action == "hide"
      and glow_frame
      and region.active_glows_hidefunc
      and region.active_glows_hidefunc[glow_frame]
      then
        region.active_glows_hidefunc[glow_frame]()
        region.active_glows_hidefunc[glow_frame] = nil
      end
    end
  end
end

function Private.PerformActions(data, when, region)
  if (paused or WeakAuras.IsOptionsOpen()) then
    return;
  end;
  local actions;
  local formatters
  if(when == "start") then
    actions = data.actions.start;
    formatters = region.startFormatters
  elseif(when == "finish") then
    actions = data.actions.finish;
    formatters = region.finishFormatters
  else
    return;
  end

  if(actions.do_message and actions.message_type and actions.message) then
    local customFunc = Private.customActionsFunctions[data.id][when .. "_message"];
    Private.HandleChatAction(actions.message_type, actions.message, actions.message_dest, actions.message_dest_isunit, actions.message_channel, actions.r, actions.g, actions.b, region, customFunc, when, formatters, actions.message_tts_voice);
  end

  if (actions.stop_sound) then
    if (region.SoundStop) then
      local fadeoutTime = actions.do_sound_fade and actions.stop_sound_fade and actions.stop_sound_fade * 1000 or 0
      region:SoundStop(fadeoutTime);
    end
  end

  if(actions.do_sound and actions.sound) then
    if (region.SoundPlay) then
      region:SoundPlay(actions);
    end
  end

  if(actions.do_custom and actions.custom) then
    local func = Private.customActionsFunctions[data.id][when]
    if func then
      Private.ActivateAuraEnvironment(region.id, region.cloneId, region.state, region.states);
      xpcall(func, Private.GetErrorHandlerId(data.id, L["Custom Action"]));
      Private.ActivateAuraEnvironment(nil);
    end
  end

  -- Apply start glow actions even if squelch_actions is true, but don't apply finish glow actions
  if actions.do_glow then
    Private.HandleGlowAction(actions, region)
  end

  -- remove all glows on finish
  if when == "finish" and actions.hide_all_glows and region.active_glows_hidefunc then
    for _, hideFunc in pairs(region.active_glows_hidefunc) do
      hideFunc()
    end
    wipe(region.active_glows_hidefunc)
  end
  if when == "finish" and type(anchor_unitframe_monitor) == "table" then
    anchor_unitframe_monitor[region] = nil
  end
end

--- @type fun(id: auraId): auraData?
function WeakAuras.GetData(id)
  return id and db.displays[id];
end

local function GetTriggerSystem(data, triggernum)
  local triggerType = data.triggers[triggernum] and data.triggers[triggernum].trigger.type
  return triggerType and triggerTypes[triggerType]
end

local function wrapTriggerSystemFunction(functionName, mode)
  local func;
  func = function(data, triggernum, ...)
    if (not triggernum) then
      return func(data, data.triggers.activeTriggerMode or -1, ...);
    elseif (triggernum < 0) then
      local result;
      if (mode == "or") then
        result = false;
        for i = 1, #data.triggers do
          result = result or func(data, i);
        end
      elseif (mode == "and") then
        result = true;
        for i = 1, #data.triggers do
          result = result and func(data, i);
        end
      elseif (mode == "table") then
        result = {};
        for i = 1, #data.triggers do
          local tmp = func(data, i);
          if (tmp) then
            for k, v in pairs(tmp) do
              result[k] = v;
            end
          end
        end
      elseif (mode == "call") then
        for i = 1, #data.triggers do
          func(data, i, ...);
        end
      elseif (mode == "firstValue") then
        result = nil;
        for i = 1, #data.triggers do
          local tmp = func(data, i);
          if (tmp) then
            result = tmp;
            break;
          end
        end
      elseif (mode == "nameAndIcon") then
        for i = 1, #data.triggers do
          local tmp1, tmp2 = func(data, i);
          if (tmp1) then
            return tmp1, tmp2;
          end
        end
      end
      return result;
    else -- triggernum >= 1
      local triggerSystem = GetTriggerSystem(data, triggernum);
      if (not triggerSystem) then
        return false
      end
      return triggerSystem[functionName](data, triggernum, ...);
    end
  end
  return func;
end

Private.CanHaveTooltip = wrapTriggerSystemFunction("CanHaveTooltip", "or");
-- This has to be in WeakAuras for now, because GetNameAndIcon can be called from the options
-- before the Options has access to Private
WeakAuras.GetNameAndIcon = wrapTriggerSystemFunction("GetNameAndIcon", "nameAndIcon");
Private.GetTriggerDescription = wrapTriggerSystemFunction("GetTriggerDescription", "call");

local wrappedGetOverlayInfo = wrapTriggerSystemFunction("GetOverlayInfo", "table");

Private.GetAdditionalProperties = function(data)
  local props = {}
  for child in Private.TraverseLeafsOrAura(data) do
    for i, trigger in ipairs(child.triggers) do
      local triggerSystem = GetTriggerSystem(child, i)
      if triggerSystem then
        local triggerProps = triggerSystem.GetAdditionalProperties(child, i)
        if triggerProps and props[i] then
          MergeTable(props[i], triggerProps)
        elseif triggerProps then
          props[i] = triggerProps
        end
      end
    end
  end
  return props
end

Private.GetProgressSources = function(data)
  local values = {}
  if Private.IsGroupType(data) then
    return values
  end
  for i = 1, #data.triggers do
    local triggerSystem = GetTriggerSystem(data, i);
    if (triggerSystem) then
      triggerSystem.GetProgressSources(data, i, values)
    end
  end
  return values
end

Private.GetProgressSourceFor = function(data, trigger, property)
  local values = {}
  local triggerSystem = GetTriggerSystem(data, trigger);
  if (triggerSystem) then
    triggerSystem.GetProgressSources(data, trigger, values)
    for _, v in ipairs(values) do
      if v.property == property then
        return {trigger, v.type, v.property, v.total, v.modRate, v.inverse, v.paused, v.remaining}
      end
    end
  end
  return nil
end

-- In the aura data we only store trigger + property
-- But for the region we don't want to gather necessary meta data all the time
-- So we collect that in region:modify + on creation of the conditions function
Private.AddProgressSourceMetaData = function(data, progressSource)
  if not progressSource then
    return {}
  end
  local trigger = progressSource[1]
  local property = progressSource[2]
  if trigger == -2 then
    return {-2, "auto", ""}
  elseif trigger == -1 then
    return {-1, "auto", ""}
  elseif trigger == 0 then
    return {0, "manual", progressSource[3], progressSource[4]}
  else
    return Private.GetProgressSourceFor(data, trigger, property)
  end
end

-- ProgressSource values
-- For AceOptions to work correctly progress sources need to be comparable
-- via ==. We use a constants table so that identical tables use the same table
-- Additional while data.progressSource does contain additional data e.g. for manual progress
-- This is only for the progress source combobox, which only cares about the first or first two values
-- The greatness of the hacks knows no bounds
-- The constants table has weak keys
do
  local function CompareProgressValueTables(a, b)
    -- For auto/manual progress, only compare a[] with b[1]
    if a[1] == -1 or a[1] == 0 then
      return a[1] == b[1]
    end
    -- Only care about trigger + property
    return a[1] == b[1] and a[2] == b[2]
  end

  local progressValueConstants = {}
  setmetatable(progressValueConstants, {_mode = "v"})

  function Private.GetProgressValueConstant(v)
    if v == nil then
      return v
    end

    -- This uses pairs because there could be empty slots
    for _, constant in pairs(progressValueConstants) do
      if CompareProgressValueTables(v, constant) then
        return constant
      end
    end
    -- And this inserts into the first empty slot for the array
    tinsert(progressValueConstants, v)
    return v
  end
end

function Private.GetProgressSourcesForUi(data, subelement)
  local values

  if subelement then
    -- Sub elements Automatic means to use the main auras' progress
    values = {
      [{-2, ""}] = L["Automatic"]
    }
  else
    values = {
      [{-1, ""}] = L["Automatic"],
      [{0, ""}] = L["Manual"],
    }
  end

  local triggerValues = Private.GetProgressSources(data)
  for _, e in ipairs(triggerValues) do
    if e.trigger and e.property then
      values[{e.trigger, e.property}] = {L["Trigger %s"]:format(e.trigger), e.display}
    end
  end

  local result = {}
  for k, v in pairs(values) do
    result[Private.GetProgressValueConstant(k)] = v
  end

  return result
end


function Private.GetOverlayInfo(data, triggernum)
  local overlayInfo;
  if (data.controlledChildren) then
    overlayInfo = {};
    for child in Private.TraverseLeafs(data) do
      local tmp = wrappedGetOverlayInfo(child, triggernum);
      if (tmp) then
        for k, v in pairs(tmp) do
          overlayInfo[k] = v;
        end
      end
    end
  else
    overlayInfo = wrappedGetOverlayInfo(data, triggernum);
  end
  return overlayInfo;
end

function Private.GetTriggerConditions(data)
  local conditions = {};
  for i = 1, #data.triggers do
    local triggerSystem = GetTriggerSystem(data, i);
    if (triggerSystem) then
      conditions[i] = triggerSystem.GetTriggerConditions(data, i);
      conditions[i] = conditions[i] or {};
      conditions[i].show = {
        display = L["Active"],
        type = "bool",
        test = function(state, needle)
          return (state and state.id and triggerState[state.id].triggers[i] or false) == (needle == 1);
        end
      }
    end
  end
  return conditions;
end

local function CreateFallbackState(id, triggernum)
  fallbacksStates[id] = fallbacksStates[id] or {};
  fallbacksStates[id][triggernum] = fallbacksStates[id][triggernum] or {};

  local states = fallbacksStates[id][triggernum];
  states[""] = states[""] or {};
  local state = states[""];

  local data = db.displays[id];
  local triggerSystem = GetTriggerSystem(data, triggernum);
  if (triggerSystem) then
    triggerSystem.CreateFallbackState(data, triggernum, state)
    state.trigger = data.triggers[triggernum].trigger
    state.triggernum = triggernum
  else
    state.show = true;
    state.changed = true;
    state.progressType = "timed";
    state.duration = 0;
    state.expirationTime = math.huge;
  end

  state.id = id

  return states;
end

local currentTooltipRegion;
local currentTooltipOwner;
local function UpdateMouseoverTooltip(region)
  if(region == currentTooltipRegion) then
    Private.ShowMouseoverTooltip(currentTooltipRegion, currentTooltipOwner);
  end
end

function Private.ShowMouseoverTooltip(region, owner)
  currentTooltipRegion = region;
  currentTooltipOwner = owner;

  GameTooltip:SetOwner(owner, "ANCHOR_NONE");
  GameTooltip:SetPoint("LEFT", owner, "RIGHT");
  GameTooltip:ClearLines();

  local triggerType;
  if (region.state) then
    triggerType = region.state.trigger.type;
  end

  local triggerSystem = triggerType and triggerTypes[triggerType];
  if (not triggerSystem) then
    GameTooltip:Hide();
    return;
  end

  if (triggerSystem.SetToolTip(region.state.trigger, region.state)) then
    GameTooltip:Show();
  else
    GameTooltip:Hide();
  end
end

function Private.HideTooltip()
  currentTooltipRegion = nil;
  currentTooltipOwner = nil;
  -- If a tooltip was shown for a "restricted" frame, that is e.g. for a aura
  -- anchored to a nameplate, then that frame is no longer clamped to the screen,
  -- because restricted frames can't be clamped. So dance to make the tooltip
  -- unrestricted and then clamp it again.
  GameTooltip:ClearAllPoints()
  GameTooltip:SetPoint("RIGHT", UIParent, "LEFT");
  GameTooltip:SetClampedToScreen(true)

  GameTooltip:Hide()
end

do
  local hiddenTooltip;
  function WeakAuras.GetHiddenTooltip()
    if not(hiddenTooltip) then
      hiddenTooltip = CreateFrame("GameTooltip", "WeakAurasTooltip", nil, "GameTooltipTemplate");
      hiddenTooltip:SetOwner(WorldFrame, "ANCHOR_NONE");
      hiddenTooltip:AddFontStrings(
        hiddenTooltip:CreateFontString("$parentTextLeft1", nil, "GameTooltipText"),
        hiddenTooltip:CreateFontString("$parentTextRight1", nil, "GameTooltipText")
      );
    end
    return hiddenTooltip;
  end
end

function WeakAuras.GetAuraInstanceTooltipInfo(unit, auraInstanceId, filter)
  if WeakAuras.IsRetail() then
    local tooltipText = ""
    local tooltipData
    if filter == "HELPFUL" then
      tooltipData = C_TooltipInfo.GetUnitBuffByAuraInstanceID(unit, auraInstanceId, filter)
    else
      tooltipData = C_TooltipInfo.GetUnitDebuffByAuraInstanceID(unit, auraInstanceId, filter)
    end
    if not tooltipData then
      return nil, "", "none", 0
    end
    local secondLine = tooltipData.lines[2] -- This is the line we want
    if secondLine and secondLine.leftText then
      tooltipText = secondLine.leftText
    end
    return tooltipData.dataInstanceID, Private.ParseTooltipText(tooltipText)
  end
end

function Private.ParseTooltipText(tooltipText)
  local debuffType = "none";
  local tooltipSize = {};
  if(tooltipText) then
    for t in tooltipText:gmatch("(-?%d[%d%.,]*)") do
      if (LARGE_NUMBER_SEPERATOR == ",") then
        t = t:gsub(",", "");
      else
        t = t:gsub("%.", "");
        t = t:gsub(",", ".");
      end
      tinsert(tooltipSize, tonumber(t));
    end
  end

  if (#tooltipSize) then
    return tooltipText, debuffType, unpack(tooltipSize);
  else
    return tooltipText, debuffType, 0;
  end
end

function WeakAuras.GetAuraTooltipInfo(unit, index, filter)
  local tooltipText = ""
  if WeakAuras.IsRetail() then
    local tooltipData = C_TooltipInfo.GetUnitAura(unit, index, filter)
    local secondLine = tooltipData and tooltipData.lines[2] -- This is the line we want
    if secondLine and secondLine.leftText then
      tooltipText = secondLine.leftText
    end
  else
    local tooltip = WeakAuras.GetHiddenTooltip();
    tooltip:ClearLines();
    tooltip:SetUnitAura(unit, index, filter);
    local tooltipTextLine = select(5, tooltip:GetRegions())
    tooltipText = tooltipTextLine and tooltipTextLine:GetObjectType() == "FontString" and tooltipTextLine:GetText() or "";
  end

  return Private.ParseTooltipText(tooltipText)
end

local FrameTimes = {};
function WeakAuras.ProfileFrames(all)
  UpdateAddOnCPUUsage();
  for name, frame in pairs(Private.frames) do
    local FrameTime = GetFrameCPUUsage(frame);
    FrameTimes[name] = FrameTimes[name] or 0;
    if(all or FrameTime > FrameTimes[name]) then
      print("|cFFFF0000"..name.."|r -", FrameTime, "-", FrameTime - FrameTimes[name]);
    end
    FrameTimes[name] = FrameTime;
  end
end

local DisplayTimes = {};
function WeakAuras.ProfileDisplays(all)
  UpdateAddOnCPUUsage();
  for id, regionData in pairs(Private.regions) do
    if regionData.region then
      local DisplayTime = GetFrameCPUUsage(regionData.region, true);
      DisplayTimes[id] = DisplayTimes[id] or 0;
      if(all or DisplayTime > DisplayTimes[id]) then
        print("|cFFFF0000"..id.."|r -", DisplayTime, "-", DisplayTime - DisplayTimes[id]);
      end
      DisplayTimes[id] = DisplayTime;
    end
  end
end

function Private.ValueFromPath(data, path)
  if not data then
    return nil
  end
  if (#path == 0) then
    return data
  elseif(#path == 1) then
    return data[path[1]];
  else
    local reducedPath = {};
    for i=2,#path do
      reducedPath[i-1] = path[i];
    end
    return Private.ValueFromPath(data[path[1]], reducedPath);
  end
end

function Private.ValueToPath(data, path, value)
  if not data then
    return
  end
  if(#path == 1) then
    data[path[1]] = value;
  else
    local reducedPath = {};
    for i=2,#path do
      reducedPath[i-1] = path[i];
    end
    Private.ValueToPath(data[path[1]], reducedPath, value);
  end
end

Private.frameLevels = {};
local function SetFrameLevel(id, frameLevel)
  if (Private.frameLevels[id] == frameLevel) then
    return;
  end
  if (Private.regions[id] and Private.regions[id].region) then
    Private.ApplyFrameLevel(Private.regions[id].region, frameLevel)
  end
  if (clones[id]) then
    for i,v in pairs(clones[id]) do
      Private.ApplyFrameLevel(v, frameLevel)
    end
  end
  Private.frameLevels[id] = frameLevel;
end

local function FixGroupChildrenOrderImpl(data, frameLevel)
  SetFrameLevel(data.id, frameLevel)
  local offset
  if data.sharedFrameLevel then
    offset = 0
  else
    offset = 4
  end
  for _, childId in ipairs(data.controlledChildren) do
    local childData = WeakAuras.GetData(childId)
    if childData.regionType ~= "group" and childData.regionType ~= "dynamicgroup" then
      frameLevel = frameLevel + offset
      SetFrameLevel(childId, frameLevel)
    else
      frameLevel = frameLevel + offset
      local endFrameLevel = FixGroupChildrenOrderImpl(childData, frameLevel)
      if not data.sharedFrameLevel then
        frameLevel = endFrameLevel
      end
    end
  end
  return frameLevel
end

function Private.FixGroupChildrenOrderForGroup(data)
  if data.parent then
    return
  end
  FixGroupChildrenOrderImpl(data, 0)
end

local function GetFrameLevelFor(id)
  return Private.frameLevels[id] or 5;
end

function Private.ApplyFrameLevel(region, frameLevel)
  frameLevel = frameLevel or GetFrameLevelFor(region.id)

  local setBackgroundFrameLevel = false
  if region.subRegions then
    for index, subRegion in pairs(region.subRegions) do
      if subRegion.type == "subbackground" then
        subRegion:SetFrameLevel(frameLevel + index)
        setBackgroundFrameLevel = true
      end
    end

    if not setBackgroundFrameLevel then
      region:SetFrameLevel(frameLevel)
    end

    for index, subRegion in pairs(region.subRegions) do
      if subRegion.type ~= "subbackground" then
        subRegion:SetFrameLevel(frameLevel + index)
      end
    end
  else
    region:SetFrameLevel(frameLevel)
  end
end

function WeakAuras.EnsureString(input)
  if (input == nil) then
    return "";
  end
  return tostring(input);
end

-- Handle coroutines
---@alias threadPriority 'urgent' | 'normal' | 'background' | 'instant'
---@alias threadPool table<string, threadData>
---@class threadData
---@field thread thread
---@field sequence table<string, number> to help debug problems in threads
---@class Threads
---@field pools table<threadPriority, threadPool>
local threads = {
  frame = CreateFrame("Frame"),
  size = 0,
  ---@type table<string, threadPriority>
  prios = {},
  pools = {
    urgent = {},
    normal = {},
    background = {},
    instant = {},
  },
};
do

  ---@type table<threadPriority, true>
  local validPriorities = {
    urgent = true,
    normal = true,
    background = true,
    instant = true,
  }

  -- Add an action to be resumed via OnUpdate
  ---@param name string
  ---@param thread thread | function
  ---@param prio threadPriority?
  function threads:Add(name, thread, prio)
    if not prio or not validPriorities[prio] then
      prio = "normal"
    end
    if type(thread) == "function" then
      thread = coroutine.create(thread)
    end
    if not self.prios[name] then
      self.prios[name] = prio
      self.pools[prio][name] = {
        thread = thread,
        sequence = {}
      }
      self.size = self.size + 1
      self.frame:Show()
    end
  end

  ---@param name string
  ---@param prio threadPriority
  function threads:SetPriority(name, prio)
    local oldPrio = self.prios[name]
    if oldPrio and oldPrio ~= prio then
      self.pools[prio][name] = self.pools[oldPrio][name]
      self.pools[oldPrio][name] = nil
      self.prios[name] = prio
    end
  end

  -- Remove an action from OnUpdate
  ---@param name string
  function threads:Remove(name)
    local prio = self.prios[name]
    if prio then
      local pool = self.pools[prio]
      pool[name] = nil
      self.prios[name] = nil
      self.size = self.size - 1
      if self.size == 0 then
        self.frame:Hide()
      end
    end
  end


  ---@param pool threadPool
  ---@param finish number
  ---@param defaultEstimate number
  local function runThreadPool(pool, finish, defaultEstimate)
    local start = debugprofilestop()
    if finish <= start then return end
    local estimates = {}
    local ok, val1, val2
    local continue = false
    repeat
      continue = false
      for name, threadData in pairs(pool) do
        local estimate = estimates[name] or defaultEstimate
        if debugprofilestop() + estimate > finish then
          break
        else
          continue = true
          ok, val1, val2 = coroutine.resume(threadData.thread)
          if not ok then
            geterrorhandler()(val1 .. '\n' .. debugstack(threadData.thread))
          elseif coroutine.status(threadData.thread) ~= "dead" then
            estimates[name] = type(val1) == "number" and val1 or defaultEstimate
            local sequence = val2 or "" --[[@as string]]
            threadData.sequence[sequence] = (threadData.sequence[sequence] or 0) + 1
          else
            threads:Remove(name)
          end
        end
      end
    until not continue
  end


  ---@param name string
  ---@param func thread
  ---@param limit number
  ---@param defaultEstimate number?
  function threads:Immediate(name, func, limit, defaultEstimate)
    self:Add(name, func, "instant")
    runThreadPool(self.pools.instant, debugprofilestop() + limit, defaultEstimate or 1000)
    if coroutine.status(func) ~= "dead" then
      self:SetPriority(name, "urgent")
    else
      self:Remove(name)
    end
  end

  -- Setup frame
  threads.frame:Hide();
  threads.frame:SetScript("OnUpdate", function()
    local start = debugprofilestop();
    runThreadPool(threads.pools.urgent, start + 15000, 1000)
    runThreadPool(threads.pools.normal, start + 20, 1)
    runThreadPool(threads.pools.background, start + 2, 0.5)
  end);
  threads.frame:RegisterEvent("PLAYER_REGEN_ENABLED")
  threads.frame:RegisterEvent("PLAYER_REGEN_DISABLED")
  threads.frame:SetScript("OnEvent", function(self, event)
    if event == "PLAYER_REGEN_ENABLED" and self:IsShown() then
      self:Hide()
    elseif event == "PLAYER_REGEN_DISABLED" and not self:IsShown() and threads.size > 0 then
      self:Show()
    end
  end)
end

Private.Threads = threads;

function WeakAuras.RegisterTriggerSystem(types, triggerSystem)
  for _, v in ipairs(types) do
    triggerTypes[v] = triggerSystem;
  end
  tinsert(triggerSystems, triggerSystem);
end

function WeakAuras.RegisterTriggerSystemOptions(types, func)
  for _, v in ipairs(types) do
    Private.triggerTypesOptions[v] = func;
  end
end

function WeakAuras.GetTriggerStateForTrigger(id, triggernum)
  if (triggernum == -1) then
    return Private.GetGlobalConditionState();
  end
  triggerState[id][triggernum] = triggerState[id][triggernum] or {}
  return triggerState[id][triggernum];
end

function WeakAuras.GetActiveStates(id)
  return triggerState[id].activeStates
end

function WeakAuras.GetActiveTriggers(id)
  return triggerState[id].triggers
end

do
  --- @type table<auraId, boolean>
  local visibleFakeStates = {}

  --- @type fun(_: any, uid: uid, id: auraId)
  local function OnDelete(_, uid, id)
    visibleFakeStates[id] = nil
  end

  --- @type fun(_: any, uid: uid, oldId: auraId, newId: auraId)
  local function OnRename(_, uid, oldId, newId)
    visibleFakeStates[newId] = visibleFakeStates[oldId]
    visibleFakeStates[oldId] = nil
  end

  Private.callbacks:RegisterCallback("Delete", OnDelete)
  Private.callbacks:RegisterCallback("Rename", OnRename)

  local UpdateFakeTimesHandle

  local function UpdateFakeTimers()
    local suspended = Private.PauseAllDynamicGroups()
    local t = GetTime()
    for id, triggers in pairs(triggerState) do
      local changed = false
      for triggernum, triggerData in ipairs(triggers) do
        for id, state in pairs(triggerData) do
          if state.progressType == "timed" and state.expirationTime and state.expirationTime < t and state.duration and state.duration > 0 then
            state.expirationTime = t + state.duration
            state.changed = true
            changed = true
          end
        end
      end
      if changed then
        Private.UpdatedTriggerState(id)
      end
    end
    Private.ResumeAllDynamicGroups(suspended)
  end

  function Private.SetFakeStates()
    if UpdateFakeTimesHandle then
      return
    end

    for id, states in pairs(triggerState) do
      local changed
      for triggernum in ipairs(states) do
        changed = Private.SetAllStatesHidden(id, triggernum) or changed
      end
      if changed then
        Private.UpdatedTriggerState(id)
      end
    end
    UpdateFakeTimesHandle = timer:ScheduleRepeatingTimer(UpdateFakeTimers, 1)
  end

  function Private.ClearFakeStates()
    timer:CancelTimer(UpdateFakeTimesHandle)
    for id in pairs(triggerState) do
      Private.FakeStatesFor(id, false)
    end
  end

  function Private.FakeStatesFor(id, visible)
    if visibleFakeStates[id] == visible then
      return visibleFakeStates[id]
    end
    if visible then
      visibleFakeStates[id] = true
      Private.UpdateFakeStatesFor(id)
    else
      visibleFakeStates[id] = false
      if triggerState[id] then
        local changed = false
        for triggernum in ipairs(triggerState[id]) do
          changed = Private.SetAllStatesHidden(id, triggernum) or changed
        end
        if changed then
          Private.UpdatedTriggerState(id)
        end
      end
    end
    return not visibleFakeStates[id]
  end

  function Private.UpdateFakeStatesFor(id)
    if (WeakAuras.IsOptionsOpen() and visibleFakeStates[id]) then
      local data = WeakAuras.GetData(id)
      if (data) then
        for triggernum in ipairs(data.triggers) do
          Private.SetAllStatesHidden(id, triggernum)
          local triggerSystem = GetTriggerSystem(data, triggernum)
          if triggerSystem and triggerSystem.CreateFakeStates then
            triggerSystem.CreateFakeStates(id, triggernum)
          end
        end
        Private.UpdatedTriggerState(id)
        if WeakAuras.GetMoverSizerId() == id then
          WeakAuras.SetMoverSizer(id)
        end
      end
    end
  end
end

--- @type fun(id: auraId, triggernum: integer, cloneId: string)
local function stopAutoHideTimer(id, triggernum, cloneId)
  if(timers[id] and timers[id][triggernum] and timers[id][triggernum][cloneId]) then
    local record = timers[id][triggernum][cloneId];
    if (record.handle) then
      timer:CancelTimer(record.handle);
    end
    record.handle = nil;
    record.expirationTime = nil;
    record.state = nil
  end
end

--- @type fun(id: auraId, triggernum: integer, cloneId: string, state: state)
local function startStopTimers(id, cloneId, triggernum, state)
  if not state.show or not state.autoHide then
    stopAutoHideTimer(id, triggernum, cloneId)
    return
  end

  -- state.autoHide can be a timer, or a boolean
  -- if it's a bool, for backwards compability we look at paused
  local expirationTime
  if type(state.autoHide) == "boolean" then
    if state.paused then
      stopAutoHideTimer(id, triggernum, cloneId)
      return
    else
      if state.expirationTime == nil and type(state.duration) == "number" then
        -- Set the expiration time, because users rely on that, even though it's wrong to do
        state.expirationTime = GetTime() + state.duration
      end
      expirationTime = state.expirationTime
    end
  elseif type(state.autoHide) == "number" then
    expirationTime = state.autoHide
  end

  timers[id] = timers[id] or {};
  timers[id][triggernum] = timers[id][triggernum] or {};
  timers[id][triggernum][cloneId] = timers[id][triggernum][cloneId] or {};
  local record = timers[id][triggernum][cloneId];
  if (record.expirationTime ~= expirationTime or record.state ~= state) then
    if (record.handle ~= nil) then
      timer:CancelTimer(record.handle);
    end

    if expirationTime then
      record.handle = timer:ScheduleTimerFixed(
        function()
          if (state.show ~= false and state.show ~= nil) then
            state.show = false;
            state.changed = true;

            -- if the trigger has updated then check to see if it is flagged for WatchedTrigger and send to queue if it is
            if Private.watched_trigger_events[id] and Private.watched_trigger_events[id][triggernum] then
              Private.AddToWatchedTriggerDelay(id, triggernum)
            end

            Private.UpdatedTriggerState(id);
          end
        end,
        expirationTime - GetTime());
      record.expirationTime = expirationTime;
      record.state = state
    end
  end
end

local function ApplyStateToRegion(id, cloneId, region, parent)
  -- Force custom text function to be run again
  region.values.lastCustomTextUpdate = nil
  region:Update();

  region.subRegionEvents:Notify("Update", region.state, region.states)

  UpdateMouseoverTooltip(region);
  region:Expand();
  if parent and parent.ActivateChild then
    parent:ActivateChild(id, cloneId)
  end
end

-- Fallbacks if the states are empty
local emptyState = {};
emptyState[""] = {};

local function applyToTriggerStateTriggers(stateShown, id, triggernum)
  if (stateShown and not triggerState[id].triggers[triggernum]) then
    triggerState[id].triggers[triggernum] = true;
    triggerState[id].triggerCount = triggerState[id].triggerCount + 1;

    return true;
  elseif (not stateShown and triggerState[id].triggers[triggernum]) then
    triggerState[id].triggers[triggernum] = false;
    triggerState[id].triggerCount = triggerState[id].triggerCount - 1;

    return true;
  end

  return false;
end

local function evaluateTriggerStateTriggers(id)
  local result = false;

  if WeakAuras.IsOptionsOpen() then
    -- While the options are open ignore the combination function
    return triggerState[id].triggerCount > 0
  end

  if (triggerState[id].disjunctive == "any" and triggerState[id].triggerCount > 0) then
    result = true;
  elseif(triggerState[id].disjunctive == "all" and triggerState[id].triggerCount == triggerState[id].numTriggers) then
    result = true;
  else
    if (triggerState[id].disjunctive == "custom" and triggerState[id].triggerLogicFunc) then
      Private.ActivateAuraEnvironment(id)
      local ok, returnValue = xpcall(triggerState[id].triggerLogicFunc, Private.GetErrorHandlerId(id, L["Custom Trigger Combination"]), triggerState[id].triggers);
      Private.ActivateAuraEnvironment()
      result = ok and returnValue;
    end
  end

  return result;
end

local function ApplyStatesToRegions(id, activeTrigger, states)
  -- Show new clones
  local data = WeakAuras.GetData(id)
  local parent
  if data and data.parent then
    parent = Private.EnsureRegion(data.parent)
  end
  if parent and parent.Suspend then
    parent:Suspend()
  end
  for cloneId, state in pairs(states) do
    if (state.show) then
      local region = Private.EnsureRegion(id, cloneId);
      local applyChanges = not region.toShow or state.changed or region.state ~= state
      region.state = state
      region.states = region.states or {}
      for triggernum = -1, triggerState[id].numTriggers do
        local triggerState
        if triggernum == activeTrigger then
          triggerState = state
        else
          local triggerStates = WeakAuras.GetTriggerStateForTrigger(id, triggernum)
          triggerState = triggerStates[cloneId] or triggerStates[""] or {}
        end
        if triggernum > 0 then
          applyChanges = applyChanges or region.states[triggernum] ~= triggerState or (triggerState and triggerState.changed)
                       or region.states[triggernum] ~= triggerState
                       or (triggerState and triggerState.changed)
        end

        region.states[triggernum] = triggerState
      end

      if (applyChanges) then
        ApplyStateToRegion(id, cloneId, region, parent);
        Private.RunConditions(region, data.uid, not state.show)
      end
    end
  end
  if parent and parent.Resume then
    parent:Resume()
  end
end

-- handle trigger updates that have been requested to be sent into custom
-- we need the id and triggernum that's changing, but can't send the ScanEvents to the custom trigger until after UpdatedTriggerState has fired
local delayed_watched_trigger = {}
function Private.AddToWatchedTriggerDelay(id, triggernum)
  delayed_watched_trigger[id] = delayed_watched_trigger[id] or {}
  tinsert(delayed_watched_trigger[id], triggernum)
end

Private.callbacks:RegisterCallback("Delete", function(_, uid, id)
  delayed_watched_trigger[id] = nil
end)

Private.callbacks:RegisterCallback("Rename", function(_, uid, oldId, newId)
  delayed_watched_trigger[newId] = delayed_watched_trigger[oldId]
  delayed_watched_trigger[oldId] = nil
end)

function Private.SendDelayedWatchedTriggers()
  for id in pairs(delayed_watched_trigger) do
    local watched = delayed_watched_trigger[id]
    -- Since the observers are themselves observable, we set the list of observers to
    -- empty here.
    delayed_watched_trigger[id] = {}
    Private.ScanEventsWatchedTrigger(id, watched)
  end
end

function Private.UpdatedTriggerState(id)
  if (not triggerState[id]) then
    return;
  end

  local changed = false;
  for triggernum = 1, triggerState[id].numTriggers do
    triggerState[id][triggernum] = triggerState[id][triggernum] or {};

    local anyStateShown = false;

    for cloneId, state in pairs(triggerState[id][triggernum]) do
      state.trigger = db.displays[id].triggers[triggernum] and db.displays[id].triggers[triggernum].trigger;
      state.triggernum = triggernum;
      state.id = id;

      if (state.changed) then
        startStopTimers(id, cloneId, triggernum, state);
      end
      anyStateShown = anyStateShown or state.show;
    end
    -- Update triggerState.triggers
    changed = applyToTriggerStateTriggers(anyStateShown, id, triggernum) or changed;
  end

  -- Figure out whether we should be shown or not
  local show = triggerState[id].show;

  if (changed or show == nil) then
    show = evaluateTriggerStateTriggers(id);
  end

  -- Figure out which subtrigger is active, and if it changed
  local newActiveTrigger = triggerState[id].activeTriggerMode;
  if (newActiveTrigger == Private.trigger_modes.first_active) then
    -- Mode is first active trigger, so find a active trigger
    for i = 1, triggerState[id].numTriggers do
      if (triggerState[id].triggers[i]) then
        newActiveTrigger = i;
        break;
      end
    end
  end

  local oldShow = triggerState[id].show;
  triggerState[id].activeTrigger = newActiveTrigger;
  triggerState[id].show = show;
  triggerState[id].fallbackStates = nil

  local activeTriggerState = WeakAuras.GetTriggerStateForTrigger(id, newActiveTrigger);
  if (not next(activeTriggerState)) then
    if (show) then
      activeTriggerState = CreateFallbackState(id, newActiveTrigger)
    else
      activeTriggerState = emptyState;
    end
  elseif (show) then
    local needsFallback = true;
    for _, state in pairs(activeTriggerState) do
      if (state.show) then
        needsFallback = false;
        break;
      end
    end
    if (needsFallback) then
      activeTriggerState = CreateFallbackState(id, newActiveTrigger)
    end
  end
  triggerState[id].activeStates = activeTriggerState

  local region;
  -- Now apply
  if (show and not oldShow) then -- Hide => Show
    ApplyStatesToRegions(id, newActiveTrigger, activeTriggerState);
  elseif (not show and oldShow) then -- Show => Hide
    for _, clone in pairs(clones[id]) do
      clone:Collapse()
    end
    if Private.regions[id] and Private.regions[id].region then
      Private.regions[id].region:Collapse()
    end
  elseif (show and oldShow) then -- Already shown, update regions
    -- Hide old clones
    for cloneId, clone in pairs(clones[id]) do
      if (not activeTriggerState[cloneId] or not activeTriggerState[cloneId].show) then
        clone:Collapse()
      end
    end
    if (not activeTriggerState[""] or not activeTriggerState[""].show) then
      if Private.regions[id] and Private.regions[id].region then
        Private.regions[id].region:Collapse()
      end
    end
    -- Show new states
    ApplyStatesToRegions(id, newActiveTrigger, activeTriggerState);
  end

  for triggernum = 1, triggerState[id].numTriggers do
    triggerState[id][triggernum] = triggerState[id][triggernum] or {};
    for cloneId, state in pairs(triggerState[id][triggernum]) do
      if (not state.show) then
        triggerState[id][triggernum][cloneId] = nil;
      end
      state.changed = false;
    end
  end
  -- once updatedTriggerStates is complete, and empty states removed, etc., then check for queued watched triggers update
  Private.SendDelayedWatchedTriggers()
end

function Private.RunCustomTextFunc(region, customFunc)

  if not customFunc then
    return nil
  end

  local state = region.state

  Private.ActivateAuraEnvironment(region.id, region.cloneId, region.state, region.states);

  local progress = Private.dynamic_texts.p.func(Private.dynamic_texts.p.get(state), state, 1)
  local dur = Private.dynamic_texts.t.func(Private.dynamic_texts.t.get(state), state, 1)
  local name = Private.dynamic_texts.n.func(Private.dynamic_texts.n.get(state))
  local icon = Private.dynamic_texts.i.func(Private.dynamic_texts.i.get(state))
  local stacks = Private.dynamic_texts.s.func(Private.dynamic_texts.s.get(state))
  local expirationTime
  local duration

  if state then
    if state.progressType == "timed" then
      expirationTime = state.expirationTime
      duration = state.duration
    else
      expirationTime = state.total
      duration = state.value
    end
  end

  local custom = {select(2, xpcall(customFunc, Private.GetErrorHandlerId(region.id, L["Custom Text Function"]), expirationTime or math.huge, duration or 0, progress, dur, name, icon, stacks))}
  Private.ActivateAuraEnvironment(nil)

  return custom
end

local function ReplaceValuePlaceHolders(textStr, region, customFunc, state, formatter, trigger)
  local value;
  if string.sub(textStr, 1, 1) == "c" then
    local custom
    if customFunc then
      custom = Private.RunCustomTextFunc(region, customFunc)
    else
      custom = region.values.custom
    end

    local index = tonumber(textStr:match("^c(%d+)$") or 1)

    if custom then
      value = custom[index]
    end

    if value == nil then value = "" end

    if formatter then
      value = formatter(value, state)
    end

    if custom then
      value = WeakAuras.EnsureString(value)
    end
  else
    local variable = Private.dynamic_texts[textStr];
    if (not variable) then
      return nil;
    end
    value = variable.get(state)
    if formatter then
      value = formatter(value, state, trigger)
    elseif variable.func then
      value = variable.func(value)
    end
  end

  return type(value) ~= "table" and value or ""
end

-- States:
-- 0 Normal state, text is just appended to result. Can transition to percent start state 1 via %
-- 1 Percent start state, entered via %. Can transition to via { to braced, via % to normal, AZaz09 to percent rest state
-- 2 Percent rest state, stay in it via AZaz09, transition to normal on anything else
-- 3 Braced state, } transitions to normal, everything else stay in braced state
local function nextState(char, state)
  if state == 0 then -- Normal State
    if char == 37 then -- % sign
      return 1 -- Enter Percent state
    end
    return 0
  elseif state == 1 then -- Percent Start State
    if char == 37 then -- % sign
      return 0 -- Return to normal state
    elseif char == 123 then -- { sign
      return 3 -- Enter Braced state
    elseif (char >= 48 and char <= 57) or (char >= 65 and char <= 90) or (char >= 97 and char <= 122) or char == 46 then
        -- 0-9a-zA-Z or dot character
      return 2 -- Enter Percent rest state
    end
    return 0 -- % followed by non alpha-numeric. Back to normal state
  elseif state == 2 then
    if (char >= 48 and char <= 57) or (char >= 65 and char <= 90) or (char >= 97 and char <= 122) or char == 46 then
      return 2 -- Continue in same state
    end
    if char == 37 then
      return 1 -- End of %, but also start of new %
    end
    return 0 -- Back to normal
  elseif state == 3 then
    if char == 125 then -- } closing brace
      return 0 -- Back to normal
    end
    return 3
  end
  -- Shouldn't happen
  return state
end

local function ContainsPlaceHolders(textStr, symbolFunc, checkDoublePercent)
  if not textStr then
    return false
  end

  local endPos = textStr:len();
  local state = 0
  local currentPos = 1
  local start = 1
  local containsDoublePercent = false
  while currentPos <= endPos do
    local char = string.byte(textStr, currentPos);
    local nextState = nextState(char, state)

    if state == 1 then -- Last char was a %
      if char == 123 then -- {
        start = currentPos + 1
      elseif char == 37 then -- %
        containsDoublePercent = true
        start = currentPos
      else
        start = currentPos
      end
    elseif state == 2 or state == 3 then
      if nextState == 0 or nextState == 1 then
        local symbol = string.sub(textStr, start, currentPos - 1)
        if symbolFunc(symbol) then
          return true
        end
      end
    end

    state = nextState
    currentPos = currentPos + 1
  end

  if state == 2 then
    local symbol = string.sub(textStr, start, currentPos - 1)
    if symbolFunc(symbol) then
      return true
    end
  end

  if checkDoublePercent then
    return containsDoublePercent
  end
  return false
end

function Private.ContainsCustomPlaceHolder(textStr)
  return ContainsPlaceHolders(textStr, function(symbol)
    return string.match(symbol, "^c%d*$")
  end)
end

function Private.ContainsPlaceHolders(textStr, toCheck)
  return ContainsPlaceHolders(textStr, function(symbol)
    if symbol:len() == 1 and toCheck:find(symbol, 1, true) then
     return true
    end

   local _, last = symbol:find("^%d+%.")
   if not last then
     return false
   end

   symbol = symbol:sub(last + 1)
   if symbol:len() == 1 and toCheck:find(symbol, 1, true) then
     return true
   end
  end)
end

function Private.ContainsAnyPlaceHolders(textStr)
  return ContainsPlaceHolders(textStr, function(symbol) return true end, true)
end

Private.ContainsPlaceHoldersPredicate = ContainsPlaceHolders

local function ValueForSymbol(symbol, region, customFunc, regionState, regionStates, useHiddenStates, formatters)
  local triggerNum, sym = string.match(symbol, "(.+)%.(.+)")
  triggerNum = triggerNum and tonumber(triggerNum)
  if triggerNum and sym then
    if regionStates[triggerNum] then
      if (useHiddenStates or regionStates[triggerNum].show) then
        if regionStates[triggerNum][sym] then
          local value = regionStates[triggerNum][sym]
          if formatters[symbol] then
            return tostring(formatters[symbol](value, regionStates[triggerNum], triggerNum) or "") or ""
          else
            return tostring(value) or ""
          end
        else
          local value = ReplaceValuePlaceHolders(sym, region, customFunc, regionStates[triggerNum], formatters[symbol], triggerNum);
          return value or ""
        end
      end
    end
    return ""
  elseif regionState[symbol] then
    if(useHiddenStates or regionState.show) then
      local value = regionState[symbol]
      if formatters[symbol] then
        return tostring(formatters[symbol](value, regionState, triggerState[regionState.id].activeTrigger) or "") or ""
      else
        return tostring(value) or ""
      end
    end
    return ""
  else
    local activeTrigger = triggerState[regionState.id].activeTrigger
    local value = (useHiddenStates or regionState.show)
                  and ReplaceValuePlaceHolders(symbol, region, customFunc, regionState, formatters[symbol], activeTrigger)
    return value or ""
  end
end

function Private.ReplacePlaceHolders(textStr, region, customFunc, useHiddenStates, formatters)
  local regionValues = region.values;
  local regionState = region.state or {};
  local regionStates = region.states or {};
  if (not regionState and not regionValues) then
    return;
  end
  local endPos = textStr:len();
  if (endPos < 2) then
    textStr = textStr:gsub("\\n", "\n");
    return textStr;
  end

  if (endPos == 2) then
    if string.byte(textStr, 1) == 37 then
      local symbol = string.sub(textStr, 2)
      if symbol == "%" then
        return "%" -- Double % input
      end
      local value = ValueForSymbol(symbol, region, customFunc, regionState, regionStates, useHiddenStates, formatters);
      if (value) then
        textStr = tostring(value);
      end
    end
    textStr = textStr:gsub("\\n", "\n");
    return textStr;
  end

  local result = ""
  local currentPos = 1 -- Position of the "cursor"
  local state = 0
  local start = 1 -- Start of whatever "word" we are currently considering, doesn't include % or {} symbols

  while currentPos <= endPos do
    local char = string.byte(textStr, currentPos);
    if state == 0 then -- Normal State
      if char == 37 then -- % sign
        if currentPos > start then
          result = result .. string.sub(textStr, start, currentPos - 1)
        end
      end
    elseif state == 1 then -- Percent Start State
      if char == 123 then -- { sign
        start = currentPos + 1
      else
        start = currentPos
      end
    elseif state == 2 then -- Percent Rest State
      if (char >= 48 and char <= 57) or (char >= 65 and char <= 90) or (char >= 97 and char <= 122) or char == 46 then
        -- 0-9a-zA-Z or dot character
      else -- End of variable
        local symbol = string.sub(textStr, start, currentPos - 1)
        result = result .. ValueForSymbol(symbol, region, customFunc, regionState, regionStates, useHiddenStates, formatters)

        if char == 37 then
          -- Do nothing
        else
          start = currentPos
        end
      end
    elseif state == 3 then
      if char == 125 then -- } closing brace
        local symbol = string.sub(textStr, start, currentPos - 1)
        result = result .. ValueForSymbol(symbol, region, customFunc, regionState, regionStates, useHiddenStates, formatters)
        start = currentPos + 1
      end
    end
    state = nextState(char, state)
    currentPos = currentPos + 1
  end

  if state == 0 and currentPos > start then
    result = result .. string.sub(textStr, start, currentPos - 1)
  elseif state == 2 and currentPos > start then
    local symbol = string.sub(textStr, start, currentPos - 1)
    result = result .. ValueForSymbol(symbol, region, customFunc, regionState, regionStates, useHiddenStates, formatters)
  elseif state == 1 then
    result = result .. "%"
  end

  textStr = result:gsub("\\n", "\n");
  return textStr;
end

function Private.ParseTextStr(textStr, symbolCallback)
  if not textStr then
    return
  end
  local endPos = textStr:len();
  local currentPos = 1 -- Position of the "cursor"
  local state = 0
  local start = 1 -- Start of whatever "word" we are currently considering, doesn't include % or {} symbols

  while currentPos <= endPos do
    local char = string.byte(textStr, currentPos);
    if state == 0 then -- Normal State
    elseif state == 1 then -- Percent Start State
      if char == 123 then
        start = currentPos + 1
      else
        start = currentPos
      end
    elseif state == 2 then -- Percent Rest State
      if (char >= 48 and char <= 57) or (char >= 65 and char <= 90) or (char >= 97 and char <= 122) or char == 46 then
        -- 0-9a-zA-Z or dot character
      else -- End of variable
        local symbol = string.sub(textStr, start, currentPos - 1)
        symbolCallback(symbol)
        if char == 37 then
          -- Do nothing
        else
          start = currentPos
        end
      end
    elseif state == 3 then
      if char == 125 then -- } closing brace
        local symbol = string.sub(textStr, start, currentPos - 1)
        symbolCallback(symbol)
        start = currentPos + 1
      end
    end
    state = nextState(char, state)
    currentPos = currentPos + 1
  end

  if state == 2 and currentPos > start then
    local symbol = string.sub(textStr, start, currentPos - 1)
    symbolCallback(symbol)
  end
end

function Private.CreateFormatters(input, getter, withoutColor, data)
  local seenSymbols = {}
  local formatters = {}
  local everyFrameFormatters = {}

  local parseFn = function(symbol)
    if not seenSymbols[symbol] then
      local _, sym = string.match(symbol, "(.+)%.(.+)")
      sym = sym or symbol
      if sym == "i" then
        -- Do nothing
      else
        local default = (sym == "p" or sym == "t") and "timed" or "none"
        local selectedFormat = getter(symbol ..  "_format", default)
        if (Private.format_types[selectedFormat]) then
          formatters[symbol], everyFrameFormatters[symbol] = Private.format_types[selectedFormat].CreateFormatter(symbol, getter, withoutColor, data)
        end
      end
    end
    seenSymbols[symbol] = true
  end

  if type(input) == "string" then
    Private.ParseTextStr(input, parseFn)
  elseif type(input) == "table" then
    for _, v in ipairs(input) do
      Private.ParseTextStr(v, parseFn)
    end
  end

  return formatters, everyFrameFormatters
end

function Private.AnyEveryFrameFormatters(textStr, everyFrameFormatters)
  if next(everyFrameFormatters) then
    local function predicate(symbol)
      if everyFrameFormatters[symbol] then
        return true
      end
    end
    return Private.ContainsPlaceHoldersPredicate(textStr, predicate)
  end
end

function Private.IsAuraActive(uid)
  local id = Private.UIDtoID(uid)
  local active = triggerState[id];

  return active and active.show;
end

function WeakAuras.IsAuraActive(id)
  local active = triggerState[id]

  return active and active.show
end

function Private.ActiveTrigger(uid)
  local id = Private.UIDtoID(uid)

  return triggerState[id] and triggerState[id].activeTrigger
end

-- Attach to Cursor/Frames code
-- Very simple function to convert a hsv angle to a color with
-- value hardcoded to 1 and saturation hardcoded to 0.75
local function colorWheel(angle)
  local hh = angle / 60;
  local i = floor(hh);
  local ff = hh - i;
  local p = 0;
  local q = 0.75 * (1.0 - ff);
  local t = 0.75 * ff;
  if (i == 0) then
    return 0.75, t, p;
  elseif (i == 1) then
    return q, 0.75, p;
  elseif (i == 2) then
    return p, 0.75, t;
  elseif (i == 3) then
    return p, q, 0.75;
  elseif (i == 4) then
    return t, p, 0.75;
  else
    return 0.75, p, q;
  end
end

local function xPositionNextToOptions()
  local xOffset;
  local optionsFrame = Private.OptionsFrame();
  local centerX = (optionsFrame:GetLeft() + optionsFrame:GetRight()) / 2;
  if (centerX > GetScreenWidth() / 2) then
    if (optionsFrame:GetLeft() > 400) then
      xOffset = optionsFrame:GetLeft() - 200;
    else
      xOffset = optionsFrame:GetLeft() / 2;
    end
  else
    if (GetScreenWidth() - optionsFrame:GetRight() > 400 ) then
      xOffset = optionsFrame:GetRight() + 200;
    else
      xOffset = (GetScreenWidth() + optionsFrame:GetRight()) / 2;
    end
  end

  return xOffset;
end

local mouseFrame;
local function ensureMouseFrame()
  if (mouseFrame) then
    return;
  end
  ---@class Frame
  mouseFrame = CreateFrame("Frame", "WeakAurasAttachToMouseFrame", UIParent);
  mouseFrame.attachedVisibleFrames = {};
  mouseFrame:SetWidth(1);
  mouseFrame:SetHeight(1);

  local moverFrame = CreateFrame("Frame", "WeakAurasMousePointerFrame", mouseFrame);
  mouseFrame.moverFrame = moverFrame;
  moverFrame:SetPoint("TOPLEFT", mouseFrame, "CENTER");
  moverFrame:SetWidth(32);
  moverFrame:SetHeight(32);
  moverFrame:SetFrameStrata("FULLSCREEN"); -- above settings dialog

  moverFrame:EnableMouse(true)
  moverFrame:SetScript("OnMouseDown", function()
    mouseFrame:SetMovable(true);
    mouseFrame:StartMoving()
  end);
  moverFrame:SetScript("OnMouseUp", function()
    mouseFrame:StopMovingOrSizing();
    mouseFrame:SetMovable(false);
    local xOffset = mouseFrame:GetRight() - GetScreenWidth();
    local yOffset = mouseFrame:GetTop() - GetScreenHeight();
    db.mousePointerFrame = db.mousePointerFrame or {};
    db.mousePointerFrame.xOffset = xOffset;
    db.mousePointerFrame.yOffset = yOffset;
  end);
  moverFrame.colorWheelAnimation = function()
    local angle = ((GetTime() - moverFrame.startTime) % 5) / 5 * 360;
    moverFrame.texture:SetVertexColor(colorWheel(angle));
  end;
  local texture = moverFrame:CreateTexture(nil, "BACKGROUND");
  moverFrame.texture = texture;
  texture:SetAllPoints(moverFrame);
  texture:SetTexture("Interface\\Cursor\\Point");

  local label = moverFrame:CreateFontString(nil, "BACKGROUND", "GameFontHighlightSmall")
  label:SetJustifyH("LEFT")
  label:SetJustifyV("TOP")
  label:SetPoint("TOPLEFT", moverFrame, "BOTTOMLEFT");
  label:SetText("WeakAuras Anchor");

  moverFrame:Hide();

  mouseFrame.OptionsOpened = function()
    if (db.mousePointerFrame) then
      -- Restore from settings
      mouseFrame:ClearAllPoints();
      mouseFrame:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", db.mousePointerFrame.xOffset, db.mousePointerFrame.yOffset);
    else
      -- Fnd a suitable position
      local optionsFrame = Private.OptionsFrame();
      local yOffset = (optionsFrame:GetTop() + optionsFrame:GetBottom()) / 2;
      local xOffset = xPositionNextToOptions();
      -- We use the top right, because the main frame uses the top right as the reference too
      mouseFrame:ClearAllPoints();
      mouseFrame:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", xOffset - GetScreenWidth(), yOffset - GetScreenHeight());
    end
    -- Change the color of the mouse cursor
    moverFrame.startTime = GetTime();
    moverFrame:SetScript("OnUpdate", moverFrame.colorWheelAnimation);
    mouseFrame:SetScript("OnUpdate", nil);
  end

  mouseFrame.moveWithMouse = function()
    local scale = 1 / UIParent:GetEffectiveScale();
    local x, y =  GetCursorPosition();
    mouseFrame:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x * scale, y * scale);
  end

  mouseFrame.OptionsClosed = function()
    moverFrame:Hide();
    mouseFrame:ClearAllPoints();
    mouseFrame:SetScript("OnUpdate", mouseFrame.moveWithMouse);
    moverFrame:SetScript("OnUpdate", nil);
    wipe(mouseFrame.attachedVisibleFrames);
  end

  mouseFrame.expand = function(self, id)
    local data = WeakAuras.GetData(id);
    if (data.anchorFrameType == "MOUSE") then
      self.attachedVisibleFrames[id] = true;
      self:updateVisible();
    end
  end

  mouseFrame.collapse = function(self, id)
    self.attachedVisibleFrames[id] = nil;
    self:updateVisible();
  end

  mouseFrame.rename = function(self, oldid, newid)
    self.attachedVisibleFrames[newid] = self.attachedVisibleFrames[oldid];
    self.attachedVisibleFrames[oldid] = nil;
    self:updateVisible();
  end

  mouseFrame.delete = function(self, id)
    self.attachedVisibleFrames[id] = nil;
    self:updateVisible();
  end

  mouseFrame.anchorFrame = function(self, id, anchorFrameType)
    if (anchorFrameType == "MOUSE") then
      self.attachedVisibleFrames[id] = true;
    else
      self.attachedVisibleFrames[id] = nil;
    end
    self:updateVisible();
  end

  mouseFrame.updateVisible = function(self)
    if (not WeakAuras.IsOptionsOpen()) then
      return;
    end

    if (next(self.attachedVisibleFrames)) then
      mouseFrame.moverFrame:Show();
    else
      mouseFrame.moverFrame:Hide();
    end
  end

  if (WeakAuras.IsOptionsOpen()) then
    mouseFrame:OptionsOpened();
  else
    mouseFrame:OptionsClosed();
  end

  Private.mouseFrame = mouseFrame;
end

local personalRessourceDisplayFrame;
function Private.ensurePRDFrame()
  if (personalRessourceDisplayFrame) then
    return;
  end
  personalRessourceDisplayFrame = CreateFrame("Frame", "WeakAurasAttachToPRD", UIParent);
  personalRessourceDisplayFrame:Hide();
  personalRessourceDisplayFrame.attachedVisibleFrames = {};
  -- force an early frame draw; otherwise this frame won't be drawn until the next frame,
  -- and any attached auras won't have a valid rect
  personalRessourceDisplayFrame:SetPoint("CENTER", UIParent, "CENTER");
  personalRessourceDisplayFrame:SetSize(16, 16)
  personalRessourceDisplayFrame:GetSize()
  Private.personalRessourceDisplayFrame = personalRessourceDisplayFrame;

  local moverFrame = CreateFrame("Frame", "WeakAurasPRDMoverFrame", personalRessourceDisplayFrame);
  personalRessourceDisplayFrame.moverFrame = moverFrame;
  moverFrame:SetPoint("TOPLEFT", personalRessourceDisplayFrame, "TOPLEFT", -2, 2);
  moverFrame:SetPoint("BOTTOMRIGHT", personalRessourceDisplayFrame, "BOTTOMRIGHT", 2, -2);
  moverFrame:SetFrameStrata("FULLSCREEN"); -- above settings dialog

  moverFrame:EnableMouse(true)
  moverFrame:SetScript("OnMouseDown", function()
    personalRessourceDisplayFrame:SetMovable(true);
    personalRessourceDisplayFrame:StartMoving()
  end);
  moverFrame:SetScript("OnMouseUp", function()
    personalRessourceDisplayFrame:StopMovingOrSizing();
    personalRessourceDisplayFrame:SetMovable(false);
    local xOffset = personalRessourceDisplayFrame:GetRight();
    local yOffset = personalRessourceDisplayFrame:GetTop();

    db.personalRessourceDisplayFrame = db.personalRessourceDisplayFrame or {};
    local scale = UIParent:GetEffectiveScale() / personalRessourceDisplayFrame:GetEffectiveScale();
    db.personalRessourceDisplayFrame.xOffset = xOffset / scale - GetScreenWidth();
    db.personalRessourceDisplayFrame.yOffset = yOffset / scale - GetScreenHeight();
  end);
  moverFrame:Hide();

  local texture = moverFrame:CreateTexture(nil, "BACKGROUND");
  personalRessourceDisplayFrame.texture = texture;
  texture:SetAllPoints(moverFrame);
  texture:SetTexture("Interface\\AddOns\\WeakAuras\\Media\\Textures\\PRDFrame");

  local label = moverFrame:CreateFontString(nil, "BACKGROUND", "GameFontHighlight")
  label:SetPoint("CENTER", moverFrame, "CENTER");
  label:SetText("WeakAuras Anchor");

  personalRessourceDisplayFrame:RegisterEvent('NAME_PLATE_UNIT_ADDED');
  personalRessourceDisplayFrame:RegisterEvent('NAME_PLATE_UNIT_REMOVED');

  personalRessourceDisplayFrame.Attach = function(self, frame, frameTL, frameBR)
    self:SetParent(frame);
    self:ClearAllPoints();
    self:SetPoint("TOPLEFT", frameTL, "TOPLEFT");
    self:SetPoint("BOTTOMRIGHT", frameBR, "BOTTOMRIGHT");
    self:Show()
  end

  personalRessourceDisplayFrame.Detach = function(self, frame)
    self:ClearAllPoints();
    self:Hide()
    self:SetParent(UIParent)
  end

  personalRessourceDisplayFrame.OptionsOpened = function()
    personalRessourceDisplayFrame:Detach();
    personalRessourceDisplayFrame:SetScript("OnEvent", nil);
    personalRessourceDisplayFrame:ClearAllPoints();
    personalRessourceDisplayFrame:Show()
    local xOffset, yOffset;
    if (db.personalRessourceDisplayFrame) then
      xOffset = db.personalRessourceDisplayFrame.xOffset;
      yOffset = db.personalRessourceDisplayFrame.yOffset;
    end

    -- Calculate size of self nameplate
    local prdWidth;
    local prdHeight;

    if (KuiNameplatesCore and KuiNameplatesCore.profile) then
      prdWidth = KuiNameplatesCore.profile.frame_width_personal;
      prdHeight = KuiNameplatesCore.profile.frame_height_personal;
      if (KuiNameplatesCore.profile.ignore_uiscale) then
        local _, screenWidth = GetPhysicalScreenSize();
        local uiScale = 1;
        if (screenWidth) then
          uiScale = 768 / screenWidth;
        end
        personalRessourceDisplayFrame:SetScale(uiScale / UIParent:GetEffectiveScale());
      else
        personalRessourceDisplayFrame:SetScale(1);
      end
      personalRessourceDisplayFrame.texture:SetTexture("Interface\\AddOns\\WeakAuras\\Media\\Textures\\PRDFrameKui");
    else
      local namePlateVerticalScale = tonumber(GetCVar("NamePlateVerticalScale"));
      local zeroBasedScale = namePlateVerticalScale - 1.0;
      local clampedZeroBasedScale = Saturate(zeroBasedScale);
      local horizontalScale = tonumber(GetCVar("NamePlateHorizontalScale"));
      local baseNamePlateWidth = NamePlateDriverFrame.baseNamePlateWidth;
      prdWidth = baseNamePlateWidth * horizontalScale * Lerp(1.1, 1.0, clampedZeroBasedScale) - 24;
      prdHeight = 4 * namePlateVerticalScale * Lerp(1.2, 1.0, clampedZeroBasedScale) * 2  + 1;
      personalRessourceDisplayFrame:SetScale(1 / UIParent:GetEffectiveScale());
      personalRessourceDisplayFrame.texture:SetTexture("Interface\\AddOns\\WeakAuras\\Media\\Textures\\PRDFrame");
    end

    local scale = UIParent:GetEffectiveScale() / personalRessourceDisplayFrame:GetEffectiveScale();
    if (not xOffset or not yOffset) then
      local optionsFrame = Private.OptionsFrame();
      yOffset = optionsFrame:GetBottom() + prdHeight / scale - GetScreenHeight();
      xOffset = xPositionNextToOptions() + prdWidth / 2 / scale - GetScreenWidth();
    end

    xOffset = xOffset * scale;
    yOffset = yOffset * scale;

    personalRessourceDisplayFrame:SetPoint("TOPRIGHT", UIParent, "TOPRIGHT", xOffset, yOffset);
    personalRessourceDisplayFrame:SetPoint("BOTTOMLEFT", UIParent, "TOPRIGHT", xOffset - prdWidth, yOffset - prdHeight);
  end

  personalRessourceDisplayFrame.OptionsClosed = function()
    personalRessourceDisplayFrame:SetScale(1);
    local frame = C_NamePlate.GetNamePlateForUnit("player");
    if (frame) then
      if (Plater and frame.unitFrame.PlaterOnScreen) then
        personalRessourceDisplayFrame:Attach(frame, frame.unitFrame.healthBar, frame.unitFrame.powerBar);
      elseif (frame.kui and frame.kui.bg and frame.kui:IsShown()) then
        personalRessourceDisplayFrame:Attach(frame.kui, frame.kui.bg, frame.kui.bg);
      elseif (ElvUIPlayerNamePlateAnchor) then
        personalRessourceDisplayFrame:Attach(ElvUIPlayerNamePlateAnchor, ElvUIPlayerNamePlateAnchor, ElvUIPlayerNamePlateAnchor);
      else
        personalRessourceDisplayFrame:Attach(frame, frame.UnitFrame.healthBar, NamePlateDriverFrame.classNamePlatePowerBar);
      end
    else
      personalRessourceDisplayFrame:Detach();
      personalRessourceDisplayFrame:Hide();
    end

    personalRessourceDisplayFrame:SetScript("OnEvent", personalRessourceDisplayFrame.eventHandler);
    personalRessourceDisplayFrame.texture:Hide();
    personalRessourceDisplayFrame.moverFrame:Hide();
    wipe(personalRessourceDisplayFrame.attachedVisibleFrames);
  end

  personalRessourceDisplayFrame.eventHandler = function(self, event, nameplate)
    Private.StartProfileSystem("prd");
    if (event == "NAME_PLATE_UNIT_ADDED") then
      if (UnitIsUnit(nameplate, "player")) then
        local frame = C_NamePlate.GetNamePlateForUnit("player");
        if (frame) then
          if (Plater and frame.unitFrame.PlaterOnScreen) then
            personalRessourceDisplayFrame:Attach(frame, frame.unitFrame.healthBar, frame.unitFrame.powerBar);
          elseif (frame.kui and frame.kui.bg and frame.kui:IsShown()) then
            personalRessourceDisplayFrame:Attach(frame.kui, KuiNameplatesPlayerAnchor, KuiNameplatesPlayerAnchor);
          elseif (ElvUIPlayerNamePlateAnchor) then
            personalRessourceDisplayFrame:Attach(ElvUIPlayerNamePlateAnchor, ElvUIPlayerNamePlateAnchor, ElvUIPlayerNamePlateAnchor);
          else
            personalRessourceDisplayFrame:Attach(frame, frame.UnitFrame.healthBar, NamePlateDriverFrame.classNamePlatePowerBar);
          end
          personalRessourceDisplayFrame:Show();
          db.personalRessourceDisplayFrame = db.personalRessourceDisplayFrame or {};
        else
          personalRessourceDisplayFrame:Detach();
          personalRessourceDisplayFrame:Hide();
        end
      end
    elseif (event == "NAME_PLATE_UNIT_REMOVED") then
      if (UnitIsUnit(nameplate, "player")) then
        personalRessourceDisplayFrame:Detach();
        personalRessourceDisplayFrame:Hide();
      end
    end
    Private.StopProfileSystem("prd");
  end

  personalRessourceDisplayFrame.expand = function(self, id)
    local data = WeakAuras.GetData(id);
    if (data.anchorFrameType == "PRD") then
      self.attachedVisibleFrames[id] = true;
      self:updateVisible();
    end
  end

  personalRessourceDisplayFrame.collapse = function(self, id)
    self.attachedVisibleFrames[id] = nil;
    self:updateVisible();
  end

  personalRessourceDisplayFrame.rename = function(self, oldid, newid)
    self.attachedVisibleFrames[newid] = self.attachedVisibleFrames[oldid];
    self.attachedVisibleFrames[oldid] = nil;
    self:updateVisible();
  end

  personalRessourceDisplayFrame.delete = function(self, id)
    self.attachedVisibleFrames[id] = nil;
    self:updateVisible();
  end

  personalRessourceDisplayFrame.anchorFrame = function(self, id, anchorFrameType)
    if (anchorFrameType == "PRD" or anchorFrameType == "NAMEPLATE") then
      self.attachedVisibleFrames[id] = true;
    else
      self.attachedVisibleFrames[id] = nil;
    end
    self:updateVisible();
  end

  personalRessourceDisplayFrame.updateVisible = function(self)
    if (not WeakAuras.IsOptionsOpen()) then
      return;
    end

    if (next(self.attachedVisibleFrames)) then
      personalRessourceDisplayFrame.texture:Show();
      personalRessourceDisplayFrame.moverFrame:Show();
      personalRessourceDisplayFrame:Show();
    else
      personalRessourceDisplayFrame.texture:Hide();
      personalRessourceDisplayFrame.moverFrame:Hide();
      personalRessourceDisplayFrame:Hide();
    end
  end

  if (WeakAuras.IsOptionsOpen()) then
    personalRessourceDisplayFrame.OptionsOpened();
  else
    personalRessourceDisplayFrame.OptionsClosed();
  end
  Private.personalRessourceDisplayFrame = personalRessourceDisplayFrame
end

local postPonedAnchors = {};
local anchorTimer

local function tryAnchorAgain()
  local delayed = postPonedAnchors;
  postPonedAnchors = {};
  anchorTimer = nil;

  for id, _ in pairs(delayed) do
    local data = WeakAuras.GetData(id);
    local region = WeakAuras.GetRegion(id);
    if (data and region) then
      local parent = WeakAurasFrame;
      local parentData
      if data.parent then
        parentData = WeakAuras.GetData(data.parent)
        if parentData and Private.EnsureRegion(data.parent) then
          parent = Private.regions[data.parent].region
        end
      end
      if not parentData or parentData.regionType ~= "dynamicgroup" then
        Private.AnchorFrame(data, region, parent)
      end
    end
  end
end

local function postponeAnchor(id)
  postPonedAnchors[id] = true;
  if (not anchorTimer) then
    anchorTimer = timer:ScheduleTimer(tryAnchorAgain, 1);
  end
end

local HiddenFrames = CreateFrame("Frame", "WeakAurasHiddenFrames")
HiddenFrames:Hide()
WeakAuras.HiddenFrames = HiddenFrames

local function GetAnchorFrame(data, region, parent)
  local id = region.id
  local anchorFrameType = data.anchorFrameType
  local anchorFrameFrame = data.anchorFrameFrame
  if not id then return end
  if (personalRessourceDisplayFrame) then
    personalRessourceDisplayFrame:anchorFrame(id, anchorFrameType);
  end

  if (mouseFrame) then
    mouseFrame:anchorFrame(id, anchorFrameType);
  end

  if (anchorFrameType == "SCREEN") then
    return parent;
  end

  if (anchorFrameType == "UIPARENT") then
    return UIParent;
  end

  if (anchorFrameType == "PRD") then
    Private.ensurePRDFrame();
    personalRessourceDisplayFrame:anchorFrame(id, anchorFrameType);
    return personalRessourceDisplayFrame;
  end

  if (anchorFrameType == "MOUSE") then
    ensureMouseFrame();
    mouseFrame:anchorFrame(id, anchorFrameType);
    return mouseFrame;
  end

  if (anchorFrameType == "NAMEPLATE") then
    local unit = region.state and region.state.unit
    if unit then
      local frame = unit and WeakAuras.GetUnitNameplate(unit)
      if frame then return frame end
    end
    if WeakAuras.IsOptionsOpen() then
      Private.ensurePRDFrame()
      personalRessourceDisplayFrame:anchorFrame(id, anchorFrameType)
      return personalRessourceDisplayFrame
    end
  end

  if (anchorFrameType == "UNITFRAME") then
    local unit = region.state and region.state.unit
    if unit then
      local frame = WeakAuras.GetUnitFrame(unit) or WeakAuras.HiddenFrames
      if frame then
        anchor_unitframe_monitor = anchor_unitframe_monitor or {}
        anchor_unitframe_monitor[region] = {
          data = data,
          parent = parent,
          frame = frame
        }
        return frame
      end
    end
  end

  if (anchorFrameType == "SELECTFRAME" and anchorFrameFrame) then
    if(anchorFrameFrame:sub(1, 10) == "WeakAuras:") then
      local frame_name = anchorFrameFrame:sub(11);
      if (frame_name == id) then
        return parent;
      end

      local targetData = WeakAuras.GetData(frame_name)
      if targetData then
        for parentData in Private.TraverseParents(targetData) do
          if parentData.id == data.id then
            WeakAuras.prettyPrint(L["Warning: Anchoring to your own child '%s' in aura '%s' is imposssible."]:format(frame_name, data.id))
            return parent
          end
        end
      end

      if Private.regions[frame_name] and Private.regions[frame_name].region then
        return Private.regions[frame_name].region;
      end
      postponeAnchor(id);
    else
      if (Private.GetSanitizedGlobal(anchorFrameFrame)) then
        return Private.GetSanitizedGlobal(anchorFrameFrame);
      end
      postponeAnchor(id);
      return parent;
    end
  end

  if (anchorFrameType == "CUSTOM" and region.customAnchorFunc) then
    Private.StartProfileSystem("custom region anchor")
    Private.StartProfileAura(region.id)
    Private.ActivateAuraEnvironment(region.id, region.cloneId, region.state)
    local ok, frame = xpcall(region.customAnchorFunc, Private.GetErrorHandlerId(region.id, L["Custom Anchor"]))
    Private.ActivateAuraEnvironment()
    Private.StopProfileSystem("custom region anchor")
    Private.StopProfileAura(region.id)
    if ok and frame then
      return frame
    elseif WeakAuras.IsOptionsOpen() then
      return parent
    else
      return HiddenFrames
    end
  end
  -- Fallback
  return parent;
end

local anchorFrameDeferred = {}

function Private.AnchorFrame(data, region, parent, force)
  if data.anchorFrameType == "CUSTOM"
  and (data.regionType == "group" or data.regionType == "dynamicgroup")
  and not WeakAuras.IsLoginFinished()
  and not force
  then
    if not anchorFrameDeferred[data.id] then
      loginQueue[#loginQueue + 1] = {Private.AnchorFrame, {data, region, parent, true}}
      anchorFrameDeferred[data.id] = true
    end
  else
    local anchorParent = GetAnchorFrame(data, region, parent);
    if not anchorParent then return end
    if (data.anchorFrameParent or data.anchorFrameParent == nil
        or data.anchorFrameType == "SCREEN" or data.anchorFrameType == "UIPARENT" or data.anchorFrameType == "MOUSE") then
      xpcall(region.SetParent, Private.GetErrorHandlerId(data.id, L["Anchoring"]), region, anchorParent);
    else
      region:SetParent(parent or WeakAurasFrame);
    end

    local anchorPoint = data.anchorPoint
    if data.parent then
      if data.anchorFrameType == "SCREEN" or data.anchorFrameType == "MOUSE" then
        anchorPoint = "CENTER"
      end
    else
      if data.anchorFrameType == "MOUSE" then
        anchorPoint = "CENTER"
      end
    end

    region:SetAnchor(data.selfPoint, anchorParent, anchorPoint);

    if(data.frameStrata == 1) then
      region:SetFrameStrata(region:GetParent():GetFrameStrata());
    else
      region:SetFrameStrata(Private.frame_strata_types[data.frameStrata]);
    end
    Private.ApplyFrameLevel(region)
    anchorFrameDeferred[data.id] = nil
  end
end

function Private.FindUnusedId(prefix)
  prefix = prefix or "New"
  local num = 2;
  local id = prefix
  while(db.displays[id]) do
    id = prefix .. " " .. num;
    num = num + 1;
  end
  return id
end

function WeakAuras.SetModel(frame, unused, model_fileId, isUnit, isDisplayInfo)
  if isDisplayInfo then
    pcall(frame.SetDisplayInfo, frame, tonumber(model_fileId))
  elseif isUnit then
    pcall(frame.SetUnit, frame, model_fileId)
  else
    pcall(frame.SetModel, frame, tonumber(model_fileId))
  end
end

function Private.IsCLEUSubevent(subevent)
  if Private.subevent_prefix_types[subevent] then
     return true
  else
    for prefix in pairs(Private.subevent_prefix_types) do
      if subevent:match(prefix) then
        local suffix = subevent:sub(#prefix + 1)
        if Private.subevent_suffix_types[suffix] then
          return true
        end
      end
    end
  end
  return false
end

--- SafeToNumber converts a string to number, but only if it fits into a unsigned 32bit integer
--- The C api often takes only 32bit values, and complains if passed a value outside
---@param input any
---@return number|nil number
function WeakAuras.SafeToNumber(input)
  local nr = tonumber(input)
  return nr and (nr < 2147483648 and nr > -2147483649) and nr or nil
end

local textSymbols = {
  ["{rt1}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_1:0|t",
  ["{rt2}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_2:0|t",
  ["{rt3}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_3:0|t",
  ["{rt4}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_4:0|t",
  ["{rt5}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_5:0|t",
  ["{rt6}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_6:0|t",
  ["{rt7}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_7:0|t",
  ["{rt8}"] = "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_8:0|t"
}

---@param txt string
---@return string result
function WeakAuras.ReplaceRaidMarkerSymbols(txt)
  local start = 1

  while true do
    local firstChar = txt:find("{", start, true)
    if not firstChar then
      return txt
    end
    local lastChar = txt:find("}", firstChar, true)
    if not lastChar then
      return txt
    end
    local replace = textSymbols[txt:sub(firstChar, lastChar)]
    if replace then
      txt = txt:sub(1, firstChar - 1) .. replace .. txt:sub(lastChar + 1)
      start = firstChar + #replace
    else
      start = lastChar
    end
  end
end

function Private.ReplaceLocalizedRaidMarkers(txt)
  local start = 1

  while true do
    local firstChar = txt:find("{", start, true)
    if not firstChar then
      return txt
    end
    local lastChar = txt:find("}", firstChar, true)
    if not lastChar then
      return txt
    end

    local symbol = strlower(txt:sub(firstChar + 1, lastChar - 1))
    if ICON_TAG_LIST[symbol] then
      local replace = "rt" .. ICON_TAG_LIST[symbol]
      if replace then
        txt = txt:sub(1, firstChar) .. replace .. txt:sub(lastChar)
        start = firstChar + #replace
      else
        start = lastChar
      end
    else
      start  = lastChar
    end
  end
end

-- WORKAROUND
-- UnitPlayerControlled doesn't work if the target is "too" far away
--- @return boolean?
function Private.UnitPlayerControlledFixed(unit)
  local guid = UnitGUID(unit)
  return guid and guid:sub(1, 6) == "Player"
end

do
  local trackableUnits = {}
  trackableUnits["player"] = true
  trackableUnits["target"] = true
  trackableUnits["focus"] = true
  trackableUnits["pet"] = true
  trackableUnits["vehicle"] = true
  trackableUnits["softenemy"] = true
  trackableUnits["softfriend"] = true
  for i = 1, 5 do
    trackableUnits["arena" .. i] = true
    trackableUnits["arenapet" .. i] = true
  end

  for i = 1, 4 do
    trackableUnits["party" .. i] = true
    trackableUnits["partypet" .. i] = true
  end

  for i = 1, 10 do
    trackableUnits["boss" .. i] = true
  end

  for i = 1, 40 do
    trackableUnits["raid" .. i] = true
    trackableUnits["raidpet" .. i] = true
    trackableUnits["nameplate" .. i] = true
  end

  ---@param unit UnitToken
  ---@return boolean? result
  function WeakAuras.IsUntrackableSoftTarget(unit)
    if not Private.soft_target_cvars[unit] then return end
    -- technically this is incorrect if user doesn't have KBM and sets CVar to "2" (KBM only)
    -- but, there doesn't seem to be a way to detect 'user lacks KBM'
    -- anyways, the intersection of people who know how to set cvars and also don't have KBM for WoW is probably nil
    -- that might change if WoW ever ends up on playstation & friends, but also hell might freeze over so who knows
    local threshold = C_GamePad.IsEnabled() and 1 or 2
    return (tonumber(C_CVar.GetCVar(Private.soft_target_cvars[unit])) or 0) < threshold
  end

  ---@param unit UnitToken
  ---@return boolean result
  function WeakAuras.UntrackableUnit(unit)
    return not trackableUnits[unit]
  end
end

do
  local ownRealm = select(2, UnitFullName("player"))
  ---@param unit UnitToken
  ---@return string name
  ---@return string realm
  function WeakAuras.UnitNameWithRealm(unit)
    ownRealm = ownRealm or select(2, UnitFullName("player"))
    local name, realm = UnitFullName(unit)
    return name or "", realm or ownRealm or ""
  end

  function WeakAuras.UnitNameWithRealmCustomName(unit)
    ownRealm = ownRealm or select(2, UnitFullName("player"))
    local name, realm =  WeakAuras.UnitFullName(unit)
    return name or "", realm or ownRealm or ""
  end
end

function Private.ExecEnv.ParseNameCheck(name)
  local matches = {
    name = {},
    realm = {},
    full = {},
    AddMatch = function(self, input, start, last)
      local match = strtrim(input:sub(start, last))

      -- state: 1: In name
      -- state: 2: In Realm
      -- state: -1: Escape Name
      -- state: -2: In Escape Realm
      local state = 1
      local name = ""
      local realm = ""


      for index = 1, #match do
        local c = match:sub(index, index)

        if state == -1 then
          name = name .. c
          state = 1
        elseif state == -2 then
          realm = realm .. c
          state = 2
        elseif state == 1 then
          if c == "\\" then
            state = -1
          elseif c == "-" then
            state = 2
          else
            name = name .. c
          end
        elseif state == 2 then
          if c == "\\" then
            state = -2
          else
            realm = realm .. c
          end
        end
      end

      if name == "" then
        if realm == "" then
          -- Do nothing
        else
          self.realm[realm] = true
        end
      else
        if realm == "" then
          self.name[name] = true
        else
          self.full[name .. "-" .. realm] = true
        end
      end
    end,
    Check = function(self, name, realm)
      if not name or not realm then
        return false
      end
      return self.name[name] or self.realm[realm] or self.full[name .. "-" .. realm]
    end
  }

  if not name then return end
  local start = 1
  local last = name:find(',', start, true)

  while (last) do
    matches:AddMatch(name, start, last - 1)
    start = last + 1
    last = name:find(',', start, true)
  end

  last = #name
  matches:AddMatch(name, start, last)

  return matches
end

function Private.ExecEnv.ParseZoneCheck(input)
  if not input then return end

  local matcher = {
    Check = function(self)
      return false
    end,
    CheckBoth = function(self, zoneId, zonegroupId, instanceId, minimapZoneText)
      return self:CheckPositive(zoneId, zonegroupId, instanceId, minimapZoneText)
             and self:CheckNegative(zoneId, zonegroupId, instanceId, minimapZoneText)
    end,
    CheckPositive = function(self, zoneId, zonegroupId, instanceId, minimapZoneText)
      return self.zoneIds[zoneId] or self.zoneGroupIds[zonegroupId] or (instanceId and self.instanceIds[instanceId]) or self.areaNames[minimapZoneText]
    end,
    CheckNegative = function(self, zoneId, zonegroupId, instanceId, minimapZoneText)
      return not (self.negZoneIds[zoneId]
                  or self.negZoneGroupIds[zonegroupId]
                  or (instanceId and self.negInstanceIds[instanceId])
                  or self.negAreaNames[minimapZoneText])
    end,
    AddId = function(self, input, start, last)
      local id = tonumber(strtrim(input:sub(start, last)))
      if id then
        local prevChar = input:sub(start - 1, start - 1)
        local prevPrevchar = input:sub(start - 2, start - 2)
        if prevChar == 'g' or prevChar == 'G' then
          if prevPrevchar == "-" then
            self.negZoneGroupIds[id] = true
          else
            self.zoneGroupIds[id] = true
          end
        elseif prevChar == 'c' or prevChar == 'C' then
          local addTo = self.zoneIds
          if prevPrevchar == "-" then
            addTo = self.negZoneIds
          end
          addTo[id] = true
          local info = C_Map.GetMapChildrenInfo(id, nil, true)
          if info then
            for _,childInfo in pairs(info) do
              addTo[childInfo.mapID] = true
            end
          end
        elseif prevChar == 'a' or prevChar == 'A' then
          local areaName = C_Map.GetAreaInfo(id)
          if areaName then
            if prevPrevchar == "-" then
              self.negAreaNames[areaName] = true
            else
              self.areaNames[areaName] = true
            end
          end
        elseif prevChar == 'i' or prevChar == 'I' then
          if prevPrevchar == "-" then
            self.negInstanceIds[id] = true
          else
            self.instanceIds[id] = true
          end
        else
          if prevChar == "-" then
            self.negZoneIds[id] = true
          else
            self.zoneIds[id] = true
          end
        end
      end
    end,
    zoneIds = {},
    zoneGroupIds = {},
    instanceIds = {},
    areaNames = {},
    negZoneIds = {},
    negZoneGroupIds = {},
    negInstanceIds = {},
    negAreaNames = {},
  }

  local start = input:find('%d', 1)
  if start then
    local last = input:find('%D', start)
    while (last) do
      matcher:AddId(input, start, last - 1)
      start = input:find('%d', last + 1) or #input + 1
      last = input:find('%D', start)
    end

    last = #input
    matcher:AddId(input, start, last)
  end
  local hasPositive = next(matcher.zoneIds) or next(matcher.zoneGroupIds) or next(matcher.instanceIds) or next(matcher.areaNames)
  local hasNegative = next(matcher.negZoneIds) or next(matcher.negZoneGroupIds) or next(matcher.negInstanceIds) or next(matcher.negAreaNames)
  if hasPositive and hasNegative then
    matcher.Check = matcher.CheckBoth
  elseif hasPositive then
    matcher.Check = matcher.CheckPositive
  elseif hasNegative then
    matcher.Check = matcher.CheckNegative
  end
  return matcher
end

function WeakAuras.IsAuraLoaded(id)
  return Private.loaded[id]
end

function Private.ExecEnv.CreateSpellChecker()
  local matcher = {
    names = {},
    spellIds = {},
    AddName = function(self, name)
      local spellId = tonumber(name)
      if spellId then
        name = Private.ExecEnv.GetSpellName(spellId)
        if name then
          self.names[name] = true
        end
      else
        self.names[name] = true
      end
    end,
    AddExact = function(self, spellId)
      spellId = tonumber(spellId)
      self.spellIds[spellId] = true
    end,
    Check = function(self, spellId)
      if spellId then
        return self.spellIds[spellId] or self.names[Private.ExecEnv.GetSpellName(spellId)]
      end
    end,
    CheckName = function(self, name)
      return self.names[name]
    end
  }
  return matcher
end

function Private.IconSources(data)
  local values = {
    [-1] = L["Automatic"],
    [0] = L["Manual Icon"],
  }

  for i = 1, #data.triggers do
    values[i] = string.format(L["Trigger %i"], i)
  end
  return values
end

-- This should be used instead of string.format("...%q...", input)
-- e.g. string.format("...%s...", Private.QuotedString(input))
-- If the string is passed to loadstring.
-- It escapes --, which loadstring would otherwise interpret as comment starts
function Private.QuotedString(input)
  local str = string.format("%q", input)
  return (str:gsub("%-%-", "-\\-"))
end

-- Helper function to make the templates not care, how the generic triggers
-- are categorized
---@private
function WeakAuras.GetTriggerCategoryFor(triggerType)
  local prototype = Private.event_prototypes[triggerType]
  return prototype and prototype.type
end

function Private.SortOrderForValues(values)
  local sortOrder = {}
  for key, value in pairs(values) do
    tinsert(sortOrder, key)
  end
  table.sort(sortOrder, function(aKey, bKey)
    local aValue = values[aKey]
    local bValue = values[bKey]

    if aValue:sub(1, #WeakAuras.newFeatureString) == WeakAuras.newFeatureString then
      aValue = aValue:sub(#WeakAuras.newFeatureString + 1)
    end

    if bValue:sub(1, #WeakAuras.newFeatureString) == WeakAuras.newFeatureString then
      bValue = bValue:sub(#WeakAuras.newFeatureString + 1)
    end

    return aValue < bValue
  end)
  return sortOrder
end

do
  local function shouldInclude(data, includeGroups, includeLeafs)
    if data.controlledChildren then
      return includeGroups
    else
      return includeLeafs
    end
  end

  local function Traverse(data, includeSelf, includeGroups, includeLeafs)
    if includeSelf and shouldInclude(data, includeGroups, includeLeafs) then
      coroutine.yield(data)
    end

    if data.controlledChildren then
      for _, child in ipairs(data.controlledChildren) do
        Traverse(WeakAuras.GetData(child), true, includeGroups, includeLeafs)
      end
    end
  end

  local function TraverseLeafs(data)
    return Traverse(data, false, false, true)
  end

  local function TraverseLeafsOrAura(data)
    return Traverse(data, true, false, true)
  end

  local function TraverseGroups(data)
    return Traverse(data, true, true, false)
  end

  local function TraverseSubGroups(data)
    return Traverse(data, false, true, false)
  end

  local function TraverseAllChildren(data)
    return Traverse(data, false, true, true)
  end

  local function TraverseAll(data)
    return Traverse(data, true, true, true)
  end

  local function TraverseParents(data)
    while data.parent do
      local parentData = WeakAuras.GetData(data.parent)
      coroutine.yield(parentData)
      data = parentData
    end
  end

  -- Only non-group auras, not include self
  function Private.TraverseLeafs(data)
    return coroutine.wrap(TraverseLeafs), data
  end

  -- The root if it is a non-group, otherwise non-group children
  function Private.TraverseLeafsOrAura(data)
    return coroutine.wrap(TraverseLeafsOrAura), data
  end

  -- All groups, includes self
  function Private.TraverseGroups(data)
    return coroutine.wrap(TraverseGroups), data
  end

  -- All groups, excludes self
  function Private.TraverseSubGroups(data)
    return coroutine.wrap(TraverseSubGroups), data
  end

  -- All Children, excludes self
  function Private.TraverseAllChildren(data)
    return coroutine.wrap(TraverseAllChildren), data
  end

  -- All Children and self
  function Private.TraverseAll(data)
    return coroutine.wrap(TraverseAll), data
  end

  function Private.TraverseParents(data)
    return coroutine.wrap(TraverseParents), data
  end

  --- Returns whether the data is a group or dynamicgroup
  ---@param data auraData
  ---@return boolean
  function Private.IsGroupType(data)
    return data.regionType == "group" or data.regionType == "dynamicgroup"
  end
end

