if not WeakAuras.IsLibsOK() then return end
---@type string
local AddonName = ...
---@class OptionsPrivate
local OptionsPrivate = select(2, ...)

-- Lua APIs
local tinsert, tremove, wipe = table.insert, table.remove, wipe
local pairs, type = pairs, type
local error = error
local coroutine = coroutine
local _G = _G

-- WoW APIs
local InCombatLockdown = InCombatLockdown
local CreateFrame = CreateFrame

local AceGUI = LibStub("AceGUI-3.0")

---@class WeakAuras
local WeakAuras = WeakAuras
local L = WeakAuras.L
local ADDON_NAME = "WeakAurasOptions";

local displayButtons = {};
OptionsPrivate.displayButtons = displayButtons;

local spellCache = WeakAuras.spellCache;
local savedVars = {};
OptionsPrivate.savedVars = savedVars;

OptionsPrivate.expanderAnchors = {}
OptionsPrivate.expanderButtons = {}

local collapsedOptions = {}
local collapsed = {} -- magic value

local tempGroup = {
  id = {"tempGroup"},
  regionType = "group",
  controlledChildren = {},
  load = {},
  triggers = {{}},
  config = {},
  authorOptions = {},
  anchorPoint = "CENTER",
  anchorFrameType = "SCREEN",
  xOffset = 0,
  yOffset = 0
};
OptionsPrivate.tempGroup = tempGroup;

-- Does not duplicate child auras.
function OptionsPrivate.DuplicateAura(data, newParent, massEdit, targetIndex)
  local base_id = data.id .. " "
  local num = 2

  -- if the old id ends with a number increment the number
  local matchName, matchNumber = string.match(data.id, "^(.-)(%d*)$")
  matchNumber = tonumber(matchNumber)
  if (matchName ~= "" and matchNumber ~= nil) then
    base_id = matchName
    num = matchNumber + 1
  end

  local new_id = base_id .. num
  while(WeakAuras.GetData(new_id)) do
    new_id = base_id .. num
    num = num + 1
  end

  local newData = CopyTable(data)
  newData.id = new_id
  newData.parent = nil
  newData.uid = WeakAuras.GenerateUniqueID()
  if newData.controlledChildren then
    newData.controlledChildren = {}
  end
  WeakAuras.Add(newData)
  WeakAuras.NewDisplayButton(newData, massEdit)
  if(newParent or data.parent) then
    local parentId = newParent or data.parent
    local parentData = WeakAuras.GetData(parentId)
    local index
    if targetIndex then
      index = targetIndex
    elseif newParent then
      index = #parentData.controlledChildren + 1
    else
      index = tIndexOf(parentData.controlledChildren, data.id) + 1
    end
    if(index) then
      tinsert(parentData.controlledChildren, index, newData.id)
      newData.parent = parentId
      WeakAuras.Add(newData)
      WeakAuras.Add(parentData)
      OptionsPrivate.Private.AddParents(parentData)

      for index, id in pairs(parentData.controlledChildren) do
        local childButton = OptionsPrivate.GetDisplayButton(id)
        childButton:SetGroup(parentData.id, parentData.regionType == "dynamicgroup")
        childButton:SetGroupOrder(index, #parentData.controlledChildren)
      end

      if not massEdit then
        local button = OptionsPrivate.GetDisplayButton(parentData.id)
        button.callbacks.UpdateExpandButton()
        button:UpdateParentWarning()
      end
      OptionsPrivate.ClearOptions(parentData.id)
    end
  end
  return newData
end

AceGUI:RegisterLayout("AbsoluteList", function(content, children)
  local yOffset = 0;
  for i = 1, #children do
    local child = children[i]

    local frame = child.frame;
    frame:ClearAllPoints();
    frame:Show();

    frame:SetPoint("LEFT", content);
    frame:SetPoint("RIGHT", content);
    frame:SetPoint("TOP", content, "TOP", 0, yOffset)

    if child.DoLayout then
      child:DoLayout()
    end

    yOffset = yOffset - ((frame.height or frame:GetHeight() or 0) + 2);
  end
  if(content.obj.LayoutFinished) then
    content.obj:LayoutFinished(nil, yOffset * -1);
  end
end);

AceGUI:RegisterLayout("ButtonsScrollLayout", function(content, children, skipLayoutFinished)
  local yOffset = 0
  local scrollTop, scrollBottom = content.obj:GetScrollPos()
  for i = 1, #children do
    local child = children[i]
    local frame = child.frame;

    if not child.dragging then
      local frameHeight = (frame.height or frame:GetHeight() or 0);
      frame:ClearAllPoints();
      if (-yOffset + frameHeight > scrollTop and -yOffset - frameHeight < scrollBottom) then
        frame:Show();
        frame:SetPoint("LEFT", content);
        frame:SetPoint("RIGHT", content);
        frame:SetPoint("TOP", content, "TOP", 0, yOffset)
      else
        frame:Hide();
        frame.yOffset = yOffset
      end
      yOffset = yOffset - (frameHeight + 2);
    end

    if child.DoLayout then
      child:DoLayout()
    end

  end
  if(content.obj.LayoutFinished and not skipLayoutFinished) then
    content.obj:LayoutFinished(nil, yOffset * -1)
  end
end)

function OptionsPrivate.MultipleDisplayTooltipDesc()
  local desc = {{L["Multiple Displays"], L["Temporary Group"]}};
  for index, id in pairs(tempGroup.controlledChildren) do
    desc[index + 1] = {" ", id};
  end
  desc[2][1] = L["Children:"]
  tinsert(desc, " ");
  tinsert(desc, {" ", "|cFF00FFFF"..L["Right-click for more options"]});
  tinsert(desc, {" ", "|cFF00FFFF"..L["Drag to move"]});
  return desc;
end

local frame;
local db;
local odb;
--- @type boolean?
local reopenAfterCombat = false;
local loadedFrame = CreateFrame("Frame");
loadedFrame:RegisterEvent("ADDON_LOADED");
loadedFrame:RegisterEvent("PLAYER_REGEN_ENABLED");
loadedFrame:RegisterEvent("PLAYER_REGEN_DISABLED");
loadedFrame:SetScript("OnEvent", function(self, event, addon)
  if (event == "ADDON_LOADED") then
    if(addon == ADDON_NAME) then
      db = WeakAurasSaved;
      WeakAurasOptionsSaved = WeakAurasOptionsSaved or {};

      odb = WeakAurasOptionsSaved;

      -- Remove icon and id cache (replaced with spellCache)
      if (odb.iconCache) then
        odb.iconCache = nil;
      end
      if (odb.idCache) then
        odb.idCache = nil;
      end
      odb.spellCache = odb.spellCache or {};
      spellCache.Load(odb);

      if odb.magnetAlign == nil then
        odb.magnetAlign = true
      end

      if db.import_disabled then
        db.import_disabled = nil
      end

      savedVars.db = db;
      savedVars.odb = odb;
    end
  elseif (event == "PLAYER_REGEN_DISABLED") then
    if(frame and frame:IsVisible()) then
      reopenAfterCombat = true;
      WeakAuras.HideOptions();
    end
  elseif (event == "PLAYER_REGEN_ENABLED") then
    if (reopenAfterCombat) then
      reopenAfterCombat = nil;
      WeakAuras.ShowOptions()
    end
  end
end);

local function addParents(hash, data)
  local parent = data.parent
  if parent then
    hash[parent] = true
    local parentData = WeakAuras.GetData(parent)
    if parentData then
      addParents(hash, parentData)
    end
  end
end

local function commonParent(controlledChildren)
  local allSame = true
  local parent = nil
  local targetIndex = math.huge
  for index, id in ipairs(controlledChildren) do
    local childData = WeakAuras.GetData(id);
    local childButton = OptionsPrivate.GetDisplayButton(id)
    targetIndex = min(targetIndex, childButton:GetGroupOrder() or math.huge)

    if (parent == nil) then
      parent = childData.parent
    elseif not childData.parent then
      allSame = false
    elseif childData.parent ~= parent then
      allSame = false
    end
  end
  if allSame then
    return parent, targetIndex
  end
end

local function CreateNewGroupFromSelection(regionType, resetChildPositions)
  local data = {
    id = OptionsPrivate.Private.FindUnusedId(tempGroup.controlledChildren[1].." Group"),
    regionType = regionType,
  };

  WeakAuras.DeepMixin(data, OptionsPrivate.Private.data_stub)
  data.internalVersion = WeakAuras.InternalVersion()
  OptionsPrivate.Private.validate(data, OptionsPrivate.Private.regionTypes[regionType].default);

  local parent, targetIndex = commonParent(tempGroup.controlledChildren)

  if (parent) then
    local parentData = WeakAuras.GetData(parent)
    tinsert(parentData.controlledChildren, targetIndex, data.id)
    data.parent = parent
    WeakAuras.Add(data);
    WeakAuras.Add(parentData);
    OptionsPrivate.Private.AddParents(parentData)
    WeakAuras.NewDisplayButton(data);
    WeakAuras.UpdateGroupOrders(parentData);
    OptionsPrivate.ClearOptions(parentData.id);

    local parentButton = OptionsPrivate.GetDisplayButton(parent)
    parentButton.callbacks.UpdateExpandButton();
    parentButton:Expand();
    parentButton:ReloadTooltip();
    parentButton:UpdateParentWarning();
  else
    WeakAuras.Add(data);
    WeakAuras.NewDisplayButton(data);
  end

  for index, childId in pairs(tempGroup.controlledChildren) do
    local childData = WeakAuras.GetData(childId);
    local childButton = OptionsPrivate.GetDisplayButton(childId)
    local oldParent = childData.parent
    local oldParentData = WeakAuras.GetData(oldParent)
    if (oldParent) then
      local oldIndex = childButton:GetGroupOrder()

      tremove(oldParentData.controlledChildren, oldIndex)
      WeakAuras.Add(oldParentData)
      OptionsPrivate.Private.AddParents(oldParentData)
      WeakAuras.UpdateGroupOrders(oldParentData);
      WeakAuras.ClearAndUpdateOptions(oldParent);
      local oldParentButton = OptionsPrivate.GetDisplayButton(oldParent)
      oldParentButton.callbacks.UpdateExpandButton();
      oldParentButton:ReloadTooltip()
      oldParentButton:UpdateParentWarning()
    end

    tinsert(data.controlledChildren, childId);
    childData.parent = data.id;
    if resetChildPositions then
      childData.xOffset = 0;
        childData.yOffset = 0;
    end
    WeakAuras.Add(data);
    WeakAuras.Add(childData);
    OptionsPrivate.ClearOptions(childData.id)

    childButton:SetGroup(data.id, data.regionType == "dynamicgroup");
    childButton:SetGroupOrder(index, #data.controlledChildren);
  end

  local button = OptionsPrivate.GetDisplayButton(data.id);
  button.callbacks.UpdateExpandButton();
  button:UpdateParentWarning()
  OptionsPrivate.SortDisplayButtons();
  button:Expand();

  if data.parent then
    OptionsPrivate.Private.AddParents(data)
  end
end

function OptionsPrivate.MultipleDisplayTooltipMenu()
  local frame = frame;
  local menu = {
    {
      text = L["Add to new Group"],
      notCheckable = 1,
      func = function()
        CreateNewGroupFromSelection("group")
      end
    },
    {
      text = L["Add to new Dynamic Group"],
      notCheckable = 1,
      func = function()
        CreateNewGroupFromSelection("dynamicgroup", true)
      end
    },
    {
      text = L["Duplicate All"],
      notCheckable = 1,
      func = function()
        local duplicated = {};
        for child in OptionsPrivate.Private.TraverseAllChildren(tempGroup) do
          local newData = OptionsPrivate.DuplicateAura(child)
          tinsert(duplicated, newData.id);
        end

        OptionsPrivate.ClearPicks();
        frame:PickDisplayBatch(duplicated);
      end
    },
    {
      text = " ",
      notCheckable = 1,
      notClickable = 1
    },
    {
      text = L["Delete all"],
      notCheckable = 1,
      func = function()
        local toDelete = {};
        local parents = {};
        for child in OptionsPrivate.Private.TraverseAllChildren(tempGroup) do
          tinsert(toDelete, child)
          addParents(parents, child)
        end
        OptionsPrivate.ConfirmDelete(toDelete, parents)
      end
    },
    {
      text = " ",
      notClickable = 1,
      notCheckable = 1,
    },
    {
      text = L["Close"],
      notCheckable = 1,
      func = function() WeakAuras_DropDownMenu:Hide() end
    }
  };

  local anyGroup = false;
  local allSameParent = true
  local commonParent = nil
  local first = true
  for _, id in pairs(tempGroup.controlledChildren) do
    local childData = WeakAuras.GetData(id);
    if(childData and childData.controlledChildren) then
      anyGroup = true;
    end

    if (first) then
      commonParent = childData.parent
      first = false
    elseif childData.parent ~= commonParent then
      allSameParent = false
    end
  end

  if(anyGroup) then
    -- Disable "Add to New Dynamic Group"
    menu[2].notClickable = 1;
    menu[2].text = "|cFF777777"..menu[2].text;
  end

  -- Also disable Add to New Dynamic Group/Group if that would create
  -- a group inside a dynamic group
  if (allSameParent and commonParent) then
    local parentData = WeakAuras.GetData(commonParent);
    if (parentData and parentData.regionType == "dynamicgroup") then
      menu[1].notClickable = 1;
      menu[1].text = "|cFF777777"..menu[1].text;
      menu[2].notClickable = 1;
      menu[2].text = "|cFF777777"..menu[1].text;
    end
  end

  return menu;
end

StaticPopupDialogs["WEAKAURAS_CONFIRM_DELETE"] = {
  text = "",
  button1 = L["Delete"],
  button2 = L["Cancel"],
  OnAccept = function(self)
    if self.data then
      OptionsPrivate.DeleteAuras(self.data.toDelete, self.data.parents)
    end
  end,
  OnCancel = function(self)
    self.data = nil
  end,
  showAlert = true,
  whileDead = true,
  preferredindex = 4,
}

function OptionsPrivate.IsWagoUpdateIgnored(auraId)
    local auraData = WeakAuras.GetData(auraId)
      if auraData then
        for child in OptionsPrivate.Private.TraverseAll(auraData) do
          if child.ignoreWagoUpdate then
            return true
          end
        end
      end
    return false
end

function OptionsPrivate.HasWagoUrl(auraId)
  local auraData = WeakAuras.GetData(auraId)
    if auraData then
      for child in OptionsPrivate.Private.TraverseAll(auraData) do
        if child.url and child.url ~= "" then
          return true
        end
      end
    end
  return false
end

function OptionsPrivate.ConfirmDelete(toDelete, parents)
  if toDelete then
    local warningForm = L["You are about to delete %d aura(s). |cFFFF0000This cannot be undone!|r Would you like to continue?"]
    StaticPopupDialogs["WEAKAURAS_CONFIRM_DELETE"].text = warningForm:format(#toDelete)
    StaticPopup_Show("WEAKAURAS_CONFIRM_DELETE", "", "", {toDelete = toDelete, parents = parents})
  end
end

local function AfterScanForLoads()
  if(frame) then
    if (frame:IsVisible()) then
      OptionsPrivate.SortDisplayButtons(nil, true);
    else
      frame.needsSort = true;
    end
  end
end

local function OnAboutToDelete(event, uid, id, parentUid, parentId)
  local data = OptionsPrivate.Private.GetDataByUID(uid)
  if(data.controlledChildren) then
    for index, childId in pairs(data.controlledChildren) do
      local childButton = displayButtons[childId];
      if(childButton) then
        childButton:SetGroup();
      end
      local childData = db.displays[childId];
      if(childData) then
        childData.parent = nil;
      end
    end
  end

  OptionsPrivate.Private.CollapseAllClones(id);
  OptionsPrivate.ClearOptions(id)

  frame:ClearPicks();

  if(displayButtons[id])then
    frame.buttonsScroll:DeleteChild(displayButtons[id]);
    displayButtons[id] = nil;
  end

  collapsedOptions[id] = nil
end

local function OnRename(event, uid, oldid, newid)
  local data = OptionsPrivate.Private.GetDataByUID(uid)

  OptionsPrivate.displayButtons[newid] = OptionsPrivate.displayButtons[oldid];
  OptionsPrivate.displayButtons[newid]:SetData(data)
  OptionsPrivate.displayButtons[oldid] = nil;
  OptionsPrivate.ClearOptions(oldid)

  OptionsPrivate.displayButtons[newid]:SetTitle(newid);

  collapsedOptions[newid] = collapsedOptions[oldid]
  collapsedOptions[oldid] = nil

  if(data.controlledChildren) then
    for _, childId in pairs(data.controlledChildren) do
      OptionsPrivate.displayButtons[childId]:SetGroup(newid)
    end
  end

  OptionsPrivate.StopGrouping()
  OptionsPrivate.SortDisplayButtons(nil, true)

  frame:OnRename(uid, oldid, newid)

  WeakAuras.PickDisplay(newid)

  local parent = data.parent
  while parent do
    OptionsPrivate.ClearOptions(parent)
    local parentData = WeakAuras.GetData(parent)
    parent = parentData.parent
  end
end

local function OptionsFrame()
  if(frame) then
    return frame
  else
    return nil
  end
end

if not WeakAuras.ToggleOptions then
  ---@type fun(msg: string, Private: Private)
  function WeakAuras.ToggleOptions(msg, Private)
    if not Private then
      return
    end
    if not OptionsPrivate.Private then
      OptionsPrivate.Private = Private
      Private.OptionsFrame = OptionsFrame
      for _, fn in ipairs(OptionsPrivate.registerRegions) do
        fn()
      end
      OptionsPrivate.Private.callbacks:RegisterCallback("AuraWarningsUpdated", function(event, uid)
        local id = OptionsPrivate.Private.UIDtoID(uid)
        if displayButtons[id] then
          -- The button does not yet exists if a new aura is created
          displayButtons[id]:UpdateWarning()
        end
        local data = Private.GetDataByUID(uid)
        if data and data.parent then
          local button = OptionsPrivate.GetDisplayButton(data.parent);
          if button then
            button:UpdateParentWarning()
          end
        end
      end)

      OptionsPrivate.Private.callbacks:RegisterCallback("ScanForLoads", AfterScanForLoads)
      OptionsPrivate.Private.callbacks:RegisterCallback("AboutToDelete", OnAboutToDelete)
      OptionsPrivate.Private.callbacks:RegisterCallback("Rename", OnRename)
      OptionsPrivate.Private.OpenUpdate = OptionsPrivate.OpenUpdate
    end

    if(frame and frame:IsVisible()) then
      WeakAuras.HideOptions();
    elseif (InCombatLockdown()) then
      WeakAuras.prettyPrint(L["Options will open after combat ends."])
      reopenAfterCombat = true;
    else
      WeakAuras.ShowOptions(msg);
    end
  end
end

function WeakAuras.HideOptions()
  if(frame) then
    frame:Hide()
  end
end

function WeakAuras.IsOptionsOpen()
  if(frame and frame:IsVisible()) then
    return true;
  else
    return false;
  end
end

local function EnsureDisplayButton(data)
  local id = data.id;
  if not(displayButtons[id]) then
    displayButtons[id] = AceGUI:Create("WeakAurasDisplayButton");
    if(displayButtons[id]) then
      displayButtons[id]:SetData(data);
      displayButtons[id]:Initialize();
      displayButtons[id]:UpdateWarning()
    else
      print("|cFF8800FFWeakAuras|r: Error creating button for", id);
    end
  end
end

local function GetSortedOptionsLists()
  local loadedSorted, unloadedSorted = {}, {};
  local to_sort = {};
  for id, data in pairs(db.displays) do
    if(data.parent) then
    -- Do nothing; children will be added later
  elseif(OptionsPrivate.Private.loaded[id]) then
      tinsert(to_sort, id);
    end
  end
  table.sort(to_sort, function(a, b) return a:lower() < b:lower() end)
  for _, id in ipairs(to_sort) do
    local data = WeakAuras.GetData(id);
    for child in OptionsPrivate.Private.TraverseAll(data) do
      tinsert(loadedSorted, child.id)
    end
  end

  wipe(to_sort);
  for id, data in pairs(db.displays) do
    if(data.parent) then
    -- Do nothing; children will be added later
    elseif not(OptionsPrivate.Private.loaded[id]) then
      tinsert(to_sort, id);
    end
  end
  table.sort(to_sort, function(a, b) return a:lower() < b:lower() end)
  for _, id in ipairs(to_sort) do
    local data = WeakAuras.GetData(id);
    for child in OptionsPrivate.Private.TraverseAll(data) do
      tinsert(unloadedSorted, child.id)
    end
  end

  return loadedSorted, unloadedSorted;
end

local function LayoutDisplayButtons(msg)
  local total = 0;
  for _,_ in pairs(db.displays) do
    total = total + 1;
  end

  local loadedSorted, unloadedSorted = GetSortedOptionsLists();

  frame:SetLoadProgressVisible(true)
  if OptionsPrivate.Private.CompanionData.slugs then
    frame.buttonsScroll:AddChild(frame.pendingInstallButton);
    frame.buttonsScroll:AddChild(frame.pendingUpdateButton);
  end
  frame.buttonsScroll:AddChild(frame.loadedButton);
  frame.buttonsScroll:AddChild(frame.unloadedButton);

  local func2 = function()
    local num = frame.loadProgressNum or 0;
    for _, id in pairs(unloadedSorted) do
      local data = WeakAuras.GetData(id);
      if(data) then
        EnsureDisplayButton(data);
        WeakAuras.UpdateThumbnail(data);

        frame.buttonsScroll:AddChild(displayButtons[data.id]);

        if (num % 50 == 0) then
          frame.buttonsScroll:ResumeLayout()
          frame.buttonsScroll:PerformLayout()
          frame.buttonsScroll:PauseLayout()
        end

        num = num + 1;
      end
      frame.loadProgress:SetText(L["Creating buttons: "]..num.."/"..total);
      frame.loadProgressNum = num;
      coroutine.yield();
    end

    frame.buttonsScroll:ResumeLayout()
    frame.buttonsScroll:PerformLayout()
    OptionsPrivate.SortDisplayButtons(msg);

    local suspended = OptionsPrivate.Private.PauseAllDynamicGroups()
    if (WeakAuras.IsOptionsOpen()) then
      for id, button in pairs(displayButtons) do
        if OptionsPrivate.Private.loaded[id] then
          button:PriorityShow(1);
          coroutine.yield()
        end
      end
      OptionsPrivate.Private.OptionsFrame().loadedButton:RecheckVisibility()
      coroutine.yield()
    end
    OptionsPrivate.Private.ResumeAllDynamicGroups(suspended)

    frame:SetLoadProgressVisible(false)
  end

  local func1 = function()
    local num = frame.loadProgressNum or 0;
    frame.buttonsScroll:PauseLayout()
    for _, id in pairs(loadedSorted) do
      local data = WeakAuras.GetData(id);
      if(data) then
        EnsureDisplayButton(data);
        WeakAuras.UpdateThumbnail(data);

        local button = displayButtons[data.id]
        frame.buttonsScroll:AddChild(button);
        num = num + 1;
      end

      if (num % 50 == 0) then
        frame.buttonsScroll:ResumeLayout()
        frame.buttonsScroll:PerformLayout()
        frame.buttonsScroll:PauseLayout()
      end

      frame.loadProgress:SetText(L["Creating buttons: "]..num.."/"..total);
      frame.loadProgressNum = num;
      coroutine.yield();
    end

    local co2 = coroutine.create(func2);
    OptionsPrivate.Private.Threads:Add("LayoutDisplayButtons2", co2);
  end

  local co1 = coroutine.create(func1);
  OptionsPrivate.Private.Threads:Add("LayoutDisplayButtons1", co1);
end

function OptionsPrivate.DeleteAuras(auras, parents)
  local func1 = function()
    frame:SetLoadProgressVisible(true)
    local num = 0
    local total = 0
    for _, auraData in pairs(auras) do
      total = total +1
    end

    frame.loadProgress:SetText(L["Deleting auras: "]..num.."/"..total)

    local suspended = OptionsPrivate.Private.PauseAllDynamicGroups()
    OptionsPrivate.massDelete = true
    for _, auraData in pairs(auras) do
      WeakAuras.Delete(auraData)
      num = num +1
      frame.loadProgress:SetText(L["Deleting auras: "]..num.."/"..total)
      coroutine.yield()
    end
    OptionsPrivate.massDelete = false

    if parents then
      for id in pairs(parents) do
        local parentData = WeakAuras.GetData(id)
        local parentButton = OptionsPrivate.GetDisplayButton(id)
        WeakAuras.UpdateGroupOrders(parentData)
        if(#parentData.controlledChildren == 0) then
          parentButton:DisableExpand()
        else
          parentButton:EnableExpand()
        end
        parentButton:SetNormalTooltip()
        WeakAuras.Add(parentData)
        WeakAuras.ClearAndUpdateOptions(parentData.id)
        parentButton:UpdateParentWarning()
        frame.loadProgress:SetText(L["Finishing..."])
        coroutine.yield()
      end
    end
    OptionsPrivate.Private.ResumeAllDynamicGroups(suspended)
    OptionsPrivate.SortDisplayButtons(nil, true)

    frame:SetLoadProgressVisible(false)
  end

  local co1 = coroutine.create(func1)
  OptionsPrivate.Private.Threads:Add("Deleting Auras", co1)
end

function WeakAuras.ShowOptions(msg)
  local firstLoad = not(frame);
  OptionsPrivate.Private.Pause();
  OptionsPrivate.Private.SetFakeStates()

  WeakAuras.spellCache.Build()

  if (firstLoad) then
    frame = OptionsPrivate.CreateFrame();
    frame.buttonsScroll.frame:Show();

    LayoutDisplayButtons(msg);
  end

  if (frame:GetWidth() > GetScreenWidth()) then
    frame:SetWidth(GetScreenWidth())
  end

  if (frame:GetHeight() > GetScreenHeight() - 50) then
    frame:SetHeight(GetScreenHeight() - 50)
  end

  frame.buttonsScroll.frame:Show();

  if (frame.needsSort) then
    OptionsPrivate.SortDisplayButtons();
    frame.needsSort = nil;
  end

  frame:Show();

  if (OptionsPrivate.Private.mouseFrame) then
    OptionsPrivate.Private.mouseFrame:OptionsOpened();
  end

  if (OptionsPrivate.Private.personalRessourceDisplayFrame) then
    OptionsPrivate.Private.personalRessourceDisplayFrame:OptionsOpened();
  end

  if frame.moversizer then
    frame.moversizer:OptionsOpened()
  end

  if not(firstLoad) then
    -- Show what was last shown
    local suspended = OptionsPrivate.Private.PauseAllDynamicGroups()
    for id, button in pairs(displayButtons) do
      button:SyncVisibility()
    end
    OptionsPrivate.Private.ResumeAllDynamicGroups(suspended)
  end

  if (frame.pickedDisplay) then
    if (OptionsPrivate.IsPickedMultiple()) then
      local children = {}
      for k,v in pairs(tempGroup.controlledChildren) do
        children[k] = v
      end
      frame:PickDisplayBatch(children);
    else
      WeakAuras.PickDisplay(frame.pickedDisplay);
    end
  else
    frame:NewAura();
  end

  if (frame.window == "codereview") then
    local codereview = OptionsPrivate.CodeReview(frame, true)
    if codereview then
      codereview:Close();
    end
  end

  if firstLoad then
    frame:ShowTip()
  end

end

function OptionsPrivate.UpdateOptions()
  frame:UpdateOptions()
end

function WeakAuras.ClearAndUpdateOptions(id, clearChildren)
  frame:ClearAndUpdateOptions(id, clearChildren)
end

function OptionsPrivate.ClearOptions(id)
  frame:ClearOptions(id)
end

function WeakAuras.FillOptions()
  frame:FillOptions()
end

function OptionsPrivate.EnsureOptions(data, subOption)
  return frame:EnsureOptions(data, subOption)
end

function OptionsPrivate.GetPickedDisplay()
  return frame:GetPickedDisplay()
end

function OptionsPrivate.OpenTextEditor(...)
  OptionsPrivate.TextEditor(frame):Open(...);
end

function OptionsPrivate.ExportToString(id)
  OptionsPrivate.ImportExport(frame):Open("export", id);
end

function OptionsPrivate.ExportToTable(id)
  OptionsPrivate.ImportExport(frame):Open("table", id);
end

function OptionsPrivate.ImportFromString()
  OptionsPrivate.ImportExport(frame):Open("import");
end

function OptionsPrivate.OpenDebugLog(text)
  OptionsPrivate.DebugLog(frame):Open(text)
end

function OptionsPrivate.OpenUpdate(data, children, target, linkedAuras, sender, callbackFunc)
  return OptionsPrivate.UpdateFrame(frame):Open(data, children, target, linkedAuras, sender, callbackFunc)
end

function OptionsPrivate.ConvertDisplay(data, newType)
  local id = data.id;
  local visibility = displayButtons[id]:GetVisibility();
  displayButtons[id]:PriorityHide(2);

  if OptionsPrivate.Private.regions[id] and OptionsPrivate.Private.regions[id].region then
    OptionsPrivate.Private.regions[id].region:Collapse()
  end
  OptionsPrivate.Private.CollapseAllClones(id);

  OptionsPrivate.Private.Convert(data, newType);
  displayButtons[id]:Initialize();
  displayButtons[id]:PriorityShow(visibility);
  frame:ClearOptions(id)
  frame:FillOptions();
  WeakAuras.UpdateThumbnail(data);
  WeakAuras.SetMoverSizer(id)
  OptionsPrivate.ResetMoverSizer();
  OptionsPrivate.SortDisplayButtons()
end

function WeakAuras.NewDisplayButton(data, massEdit)
  local id = data.id;
  OptionsPrivate.Private.ScanForLoads({[id] = true});
  EnsureDisplayButton(db.displays[id]);
  WeakAuras.UpdateThumbnail(db.displays[id]);
  frame.buttonsScroll:AddChild(displayButtons[id]);
  if not massEdit then
    OptionsPrivate.SortDisplayButtons()
  end
end

function WeakAuras.UpdateGroupOrders(data)
  if(data.controlledChildren) then
    local total = #data.controlledChildren;
    for index, id in pairs(data.controlledChildren) do
      local button = OptionsPrivate.GetDisplayButton(id);
      button:SetGroupOrder(index, total);
    end
  end
end

function OptionsPrivate.UpdateButtonsScroll()
  if OptionsPrivate.Private.IsOptionsProcessingPaused() then return end
  frame.buttonsScroll:DoLayout()
end

local function addButton(button, aurasMatchingFilter, visible)
  button.frame:Show();
  if button.AcquireThumbnail then
    button:AcquireThumbnail()
  end
  tinsert(frame.buttonsScroll.children, button);
  visible[button] = true

  if button.data.controlledChildren and button:GetExpanded() then
    for _, childId in ipairs(button.data.controlledChildren) do
      if aurasMatchingFilter[childId] then
        addButton(displayButtons[childId], aurasMatchingFilter, visible)
      end
    end
  end
end

local previousFilter;
local pendingUpdateButtons = {}
local pendingInstallButtons = {}
function OptionsPrivate.SortDisplayButtons(filter, overrideReset, id)
  if (OptionsPrivate.Private.IsOptionsProcessingPaused()) then
    return;
  end

  local recenter = false;
  filter = filter or (overrideReset and previousFilter or "");
  if(frame.filterInput:GetText() ~= filter) then
    frame.filterInput:SetText(filter);
  end
  if(previousFilter and previousFilter ~= "" and (filter == "" or not filter)) then
    recenter = true;
  end
  previousFilter = filter;
  filter = filter:lower();

  wipe(frame.buttonsScroll.children);

  local pendingInstallButtonShown = false
  if OptionsPrivate.Private.CompanionData.stash then
    for id, companionData in pairs(OptionsPrivate.Private.CompanionData.stash) do
      if not pendingInstallButtonShown then
        tinsert(frame.buttonsScroll.children, frame.pendingInstallButton)
        pendingInstallButtonShown = true
      end
      local child = pendingInstallButtons[id]
      if frame.pendingInstallButton:GetExpanded() then
        if not child then
          child = AceGUI:Create("WeakAurasPendingInstallButton")
          pendingInstallButtons[id] = child
          child:Initialize(id, companionData)
          if companionData.logo then
            child:SetLogo(companionData.logo)
          end
          if companionData.refreshLogo then
            child:SetRefreshLogo(companionData.refreshLogo)
          end
          child.frame:Show()
          child:AcquireThumbnail()
          frame.buttonsScroll:AddChild(child)
        else
          if not child.frame:IsShown() then
            child.frame:Show()
            child:AcquireThumbnail()
          end
          tinsert(frame.buttonsScroll.children, child)
        end
      elseif child then
        child.frame:Hide()
        if child.ReleaseThumbnail then
          child:ReleaseThumbnail()
        end
      end
    end
  end
  if not pendingInstallButtonShown and frame.pendingInstallButton then
    frame.pendingInstallButton.frame:Hide()
  end

  local pendingUpdateButtonShown = false
  if OptionsPrivate.Private.CompanionData.slugs then
    local buttonsShown = {}
    for _, button in pairs(pendingUpdateButtons) do
      button:ResetLinkedAuras()
    end
    for id, aura in pairs(WeakAurasSaved.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 auraData = OptionsPrivate.Private.CompanionData.slugs[slug]
          if auraData and auraData.wagoVersion then
            if tonumber(auraData.wagoVersion) > tonumber(version) then
              -- there is an update for this aura
              if not pendingUpdateButtonShown then
                tinsert(frame.buttonsScroll.children, frame.pendingUpdateButton)
                pendingUpdateButtonShown = true
              end
              if frame.pendingUpdateButton:GetExpanded() then
                local child = pendingUpdateButtons[slug]
                if not child then
                  child = AceGUI:Create("WeakAurasPendingUpdateButton")
                  pendingUpdateButtons[slug] = child
                  child:Initialize(slug, auraData)
                  if auraData.logo then
                    child:SetLogo(auraData.logo)
                  end
                  if auraData.refreshLogo then
                    child:SetRefreshLogo(auraData.refreshLogo)
                  end
                  child.frame:Show()
                  child:AcquireThumbnail()
                  frame.buttonsScroll:AddChild(child)
                  buttonsShown[slug] = true
                end
                if not child.frame:IsShown() then
                  child.frame:Show()
                  child:AcquireThumbnail()
                end
                if not buttonsShown[slug] then
                  tinsert(frame.buttonsScroll.children, child)
                  buttonsShown[slug] = true
                end
                child:MarkLinkedAura(id)
                for childData in OptionsPrivate.Private.TraverseAllChildren(aura) do
                  child:MarkLinkedChildren(childData.id)
                end
              end
            end
          end
        end
      end
    end
    -- hide all buttons not marked as shown
    for slug, button in pairs(pendingUpdateButtons) do
      if not buttonsShown[slug] then
        if button and button.frame:IsShown() then
          button.frame:Hide()
          if button.ReleaseThumbnail then
            button:ReleaseThumbnail()
          end
        end
      end
    end
  end
  if not pendingUpdateButtonShown and frame.pendingUpdateButton then
    frame.pendingUpdateButton.frame:Hide()
  end

  tinsert(frame.buttonsScroll.children, frame.loadedButton);

  local aurasMatchingFilter = {}
  local useTextFilter = filter ~= ""
  local filterTable = OptionsPrivate.Private.splitAtOr(filter)
  local topLevelLoadedAuras = {}
  local topLevelUnloadedAuras = {}
  local visible = {}

  for id, child in pairs(displayButtons) do
    if child.data.controlledChildren then
      local hasLoaded, hasStandBy, hasNotLoaded = 0, 0, 0
      for leaf in OptionsPrivate.Private.TraverseLeafs(child.data) do
        local id = leaf.id
        if OptionsPrivate.Private.loaded[id] == true then
          hasLoaded = hasLoaded + 1
        elseif OptionsPrivate.Private.loaded[id] == false then
          hasStandBy = hasStandBy + 1
        else
          hasNotLoaded = hasNotLoaded + 1
        end
      end
      if hasLoaded > 0 then
        child:SetLoaded(1, "loaded", L["Loaded"], L["%d displays loaded"]:format(hasLoaded))
      elseif hasStandBy > 0 then
        child:SetLoaded(2, "standby", L["Standby"], L["%d displays on standby"]:format(hasStandBy))
      elseif hasNotLoaded > 0 then
        child:SetLoaded(3, "unloaded", L["Not Loaded"], L["%d displays not loaded"]:format(hasNotLoaded))
      else
        child:ClearLoaded()
      end
    else
      if OptionsPrivate.Private.loaded[id] == true then
        child:SetLoaded(1, "loaded", L["Loaded"], L["This display is currently loaded"])
      elseif OptionsPrivate.Private.loaded[id] == false then
        child:SetLoaded(2, "standby", L["Standby"], L["This display is on standby, it will be loaded when needed."])
      else
        child:SetLoaded(3, "unloaded", L["Not Loaded"], L["This display is not currently loaded"])
      end
    end

    if useTextFilter then
      for _, word in ipairs(filterTable) do
        if(id:lower():find(word, 1, true)) then
          aurasMatchingFilter[id] = true
          for parent in OptionsPrivate.Private.TraverseParents(child.data) do
            aurasMatchingFilter[parent.id] = true
          end
        end
      end
    else
      aurasMatchingFilter[id] = true
    end

    if not child:GetGroup() then
      -- Top Level aura
      if OptionsPrivate.Private.loaded[id] ~= nil then
        tinsert(topLevelLoadedAuras, id)
      else
        tinsert(topLevelUnloadedAuras, id)
      end
    end
  end

  wipe(frame.loadedButton.childButtons)
  if frame.loadedButton:GetExpanded() then
    table.sort(topLevelLoadedAuras, function(a, b) return a:lower() < b:lower() end)
    for _, id in ipairs(topLevelLoadedAuras) do
      if aurasMatchingFilter[id] then
        addButton(displayButtons[id], aurasMatchingFilter, visible)
      end
    end
  end

  for _, id in ipairs(topLevelLoadedAuras) do
    for child in OptionsPrivate.Private.TraverseLeafsOrAura(WeakAuras.GetData(id)) do
      tinsert(frame.loadedButton.childButtons, displayButtons[child.id])
    end
  end

  tinsert(frame.buttonsScroll.children, frame.unloadedButton);

  wipe(frame.unloadedButton.childButtons)
  if frame.unloadedButton:GetExpanded() then
    table.sort(topLevelUnloadedAuras, function(a, b) return a:lower() < b:lower() end)
    for _, id in ipairs(topLevelUnloadedAuras) do
      if aurasMatchingFilter[id] then
        addButton(displayButtons[id], aurasMatchingFilter, visible)
      end
    end
  end

  for _, id in ipairs(topLevelUnloadedAuras) do
    for child in OptionsPrivate.Private.TraverseLeafsOrAura(WeakAuras.GetData(id)) do
      tinsert(frame.unloadedButton.childButtons, displayButtons[child.id])
    end
  end

  for _, child in pairs(displayButtons) do
    if(not visible[child]) then
      child.frame:Hide();
      if child.ReleaseThumbnail then
        child:ReleaseThumbnail()
      end
    end
  end

  frame.buttonsScroll:DoLayout();
  if(recenter) then
    frame:CenterOnPicked();
  end
end


function OptionsPrivate.IsPickedMultiple()
  if(frame.pickedDisplay == tempGroup) then
    return true;
  else
    return false;
  end
end

function OptionsPrivate.IsDisplayPicked(id)
  if(frame.pickedDisplay == tempGroup) then
    for child in OptionsPrivate.Private.TraverseLeafs(tempGroup) do
      if(id == child.id) then
        return true;
      end
    end
    return false;
  else
    return frame.pickedDisplay == id;
  end
end

function WeakAuras.PickDisplay(id, tab, noHide)
  frame:PickDisplay(id, tab, noHide)
  OptionsPrivate.UpdateButtonsScroll()
end

function OptionsPrivate.PickAndEditDisplay(id)
  frame:PickDisplay(id);
  OptionsPrivate.UpdateButtonsScroll()
  displayButtons[id].callbacks.OnRenameClick();
end

function OptionsPrivate.ClearPick(id)
  frame:ClearPick(id);
end

function OptionsPrivate.ClearPicks()
  frame:ClearPicks();
end

function OptionsPrivate.PickDisplayMultiple(id)
  frame:PickDisplayMultiple(id);
end

function OptionsPrivate.PickDisplayMultipleShift(target)
  if (frame.pickedDisplay) then
    -- get first aura selected
    local first;
    if (OptionsPrivate.IsPickedMultiple()) then
      first = tempGroup.controlledChildren[#tempGroup.controlledChildren];
    else
      first = frame.pickedDisplay;
    end
    if (first and first ~= target) then
      -- check if target and first are in same group and are not a group
      local firstData = WeakAuras.GetData(first);
      local targetData = WeakAuras.GetData(target);
      if (firstData.parent == targetData.parent and not targetData.controlledChildren and not firstData.controlledChildren) then
        local batchSelection = {};
        -- in a group
        if (firstData.parent) then
          local group = WeakAuras.GetData(targetData.parent);
          for index, child in ipairs(group.controlledChildren) do
            -- 1st button
            if (child == target or child == first) then
              table.insert(batchSelection, child);
              for i = index + 1, #group.controlledChildren do
                local current = group.controlledChildren[i];
                if (WeakAuras.GetData(current).controlledChildren) then
                  -- Skip sub groups
                else
                  table.insert(batchSelection, current);
                end
                -- last button: stop selection
                if (current == target or current == first) then
                  break;
                end
              end
              break;
            end
          end
        elseif (firstData.parent == nil and targetData.parent == nil) then
          -- top-level
          for index, button in ipairs(frame.buttonsScroll.children) do
            if button.type == "WeakAurasDisplayButton" then
              local data = button.data;
              -- 1st button
              if (data and (data.id == target or data.id == first)) then
                table.insert(batchSelection, data.id);
                for i = index + 1, #frame.buttonsScroll.children do
                  local current = frame.buttonsScroll.children[i];
                  local currentData = current.data;
                  if currentData and not currentData.parent and not currentData.controlledChildren then
                    table.insert(batchSelection, currentData.id);
                    -- last button: stop selection
                    if (currentData.id == target or currentData.id == first) then
                      break;
                    end
                  end
                end
                break;
              end
            end
          end
        end
        if #batchSelection > 0 then
          frame:PickDisplayBatch(batchSelection);
        end
      end
    end
  else
    WeakAuras.PickDisplay(target);
  end
end

function OptionsPrivate.GetDisplayButton(id)
  if(id and displayButtons[id]) then
    return displayButtons[id];
  end
end

function OptionsPrivate.AddDisplayButton(data)
  EnsureDisplayButton(data);
  WeakAuras.UpdateThumbnail(data);
  frame.buttonsScroll:AddChild(displayButtons[data.id]);
end

function OptionsPrivate.StartGrouping(data)
  if not data then
    return
  end

  if not OptionsPrivate.IsDisplayPicked(data) then
    WeakAuras.PickDisplay(data.id)
  end

  if (frame.pickedDisplay == tempGroup and #tempGroup.controlledChildren > 0) then
    local children = {};
    -- start grouping for selected buttons
    for index, childId in ipairs(tempGroup.controlledChildren) do
      local button = OptionsPrivate.GetDisplayButton(childId);
      button:StartGrouping(tempGroup.controlledChildren, true);
      children[childId] = true;
    end
    -- set grouping for non selected buttons
    for _, button in pairs(displayButtons) do
      if not children[button.data.id] then
        button:StartGrouping(tempGroup.controlledChildren, false);
      end
    end
  else
    local children = {};
    for child in OptionsPrivate.Private.TraverseAllChildren(data) do
      children[child.id] = true
    end

    for id, button in pairs(displayButtons) do
      button:StartGrouping({data.id},
                           data.id == id,
                           data.regionType == "dynamicgroup" or data.regionType == "group",
                           children[id]);
    end
  end
end

function OptionsPrivate.StopGrouping(data)
  for id, button in pairs(displayButtons) do
    button:StopGrouping();
  end
end

function OptionsPrivate.Ungroup(data)
  if not OptionsPrivate.IsDisplayPicked(data.id) then
    WeakAuras.PickDisplay(data.id)
  end

  if (frame.pickedDisplay == tempGroup and #tempGroup.controlledChildren > 0) then
    for index, childId in ipairs(tempGroup.controlledChildren) do
      local button = OptionsPrivate.GetDisplayButton(childId);
      button:Ungroup(data);
    end
  else
    local button = OptionsPrivate.GetDisplayButton(data.id);
    button:Ungroup(data);
  end
  WeakAuras.FillOptions()
end

function OptionsPrivate.DragReset()
  for _, button in pairs(displayButtons) do
    button:DragReset();
  end
  OptionsPrivate.UpdateButtonsScroll()
end

local function CompareButtonOrder(a, b)
  if (a.data.parent == b.data.parent) then
    if (a.data.parent) then
      return a:GetGroupOrder() < b:GetGroupOrder()
    else
      return a.data.id < b.data.id
    end
  end

  -- Different parents, so find common parent by first
  -- going up a's hierarchy

  local parents = {}

  local aNode = a.data.id
  local lastAParent = aNode

  while(aNode) do
    local parent = WeakAuras.GetData(aNode).parent
    if (parent) then
      parents[parent] = aNode
      lastAParent = parent
    end
    aNode = parent
  end

  local bNode = b.data.id
  local lastBParent = bNode

  while(bNode) do
    local parent = WeakAuras.GetData(bNode).parent
    if parent then
      if (parents[parent]) then
        -- We have found the common parent, the last node in the chain is
        -- Compare the previous nodes GroupOrder
        local aButton = OptionsPrivate.GetDisplayButton(parents[parent])
        local bButton = OptionsPrivate.GetDisplayButton(bNode)
        return aButton:GetGroupOrder() < bButton:GetGroupOrder()
      end
      lastBParent = parent
    end
    bNode = parent
  end

  -- If we are here there was no common parent
  local aButton = OptionsPrivate.GetDisplayButton(lastAParent)
  local bButton = OptionsPrivate.GetDisplayButton(lastBParent)

  return aButton.data.id < bButton.data.id
end

local function CompareButtonOrderReverse(a, b)
  return CompareButtonOrder(b, a)
end

function OptionsPrivate.Drop(mainAura, target, action, area)
  WeakAuras_DropDownMenu:Hide()

  local func1 = function()
    frame:SetLoadProgressVisible(true)

    local total = 0
    local num = 0
    for id, button in pairs(displayButtons) do
      if button:IsDragging() then
        total = total + 1
      end
    end
    frame.loadProgress:SetText(L["Moving auras: "]..num.."/"..total)

    local mode = ""
    if (frame.pickedDisplay == tempGroup and #tempGroup.controlledChildren > 0) then
      mode = "MULTI"
    elseif mainAura.controlledChildren then
      mode = "GROUP"
    else
      mode = "SINGLE"
    end

    local buttonsToSort = {}

    for id, button in pairs(displayButtons) do
      if button:IsDragging() then
        tinsert(buttonsToSort, button)
        num = num + 1
        frame.loadProgress:SetText(L["Preparing auras: "]..num.."/"..total)
      else
        button:Drop(mode, mainAura, target, action);
      end
      coroutine.yield()
    end

    num = 0
    frame.loadProgress:SetText(L["Moving auras: "]..num.."/"..total)
    if mode == "MULTI" then
      -- If we are dragging and dropping multiple auras at once, the order in which we drop is important
      -- We want to preserve the top-down order
      -- Depending on how exactly we find the insert position, we need to use the right order of insertions
      if area == "GROUP" then
        table.sort(buttonsToSort, CompareButtonOrderReverse)
      elseif area == "BEFORE" then
        table.sort(buttonsToSort, CompareButtonOrder)
      else -- After
        table.sort(buttonsToSort, CompareButtonOrderReverse)
      end
    end

    for _, button in ipairs(buttonsToSort) do
      button:Drop(mode, mainAura, target, action)
      num = num + 1
      frame.loadProgress:SetText(L["Moving auras: "]..num.."/"..total)
      coroutine.yield()
    end

    -- Update offset, this is a bit wasteful to do for every aura
    -- But we also need to update the offset if a parent was dragged
    for _, button in pairs(displayButtons) do
      button:UpdateOffset();
    end
    coroutine.yield()
    frame:SetLoadProgressVisible(false)
    OptionsPrivate.SortDisplayButtons()
    OptionsPrivate.UpdateButtonsScroll()
    WeakAuras.FillOptions()
  end

  local co1 = coroutine.create(func1)
  OptionsPrivate.Private.Threads:Add("Dropping Auras", co1)
end

function OptionsPrivate.StartDrag(mainAura)
  WeakAuras_DropDownMenu:Hide()

  if (frame.pickedDisplay == tempGroup and #tempGroup.controlledChildren > 0) then
    -- Multi selection
    local children = {};
    local size = #tempGroup.controlledChildren;
    -- set dragging for selected buttons in reverse for ordering

    for child in OptionsPrivate.Private.TraverseAllChildren(tempGroup) do
      local button = OptionsPrivate.GetDisplayButton(child.id);
      button:DragStart("MULTI", true, mainAura, size)
      children[child.id] = true
    end
    -- set dragging for non selected buttons
    for id, button in pairs(displayButtons) do
      if not children[button.data.id] then
        button:DragStart("MULTI", false, mainAura);
      end
    end
  else
    if mainAura.controlledChildren then
      -- Group aura
      local mode = "GROUP"
      local children = {};
      for child in OptionsPrivate.Private.TraverseAll(mainAura) do
        local button = OptionsPrivate.GetDisplayButton(child.id);
        button:DragStart(mode, true, mainAura)
        children[child.id] = true
      end
      -- set dragging for non selected buttons
      for _, button in pairs(displayButtons) do
        if not children[button.data.id] then
          button:DragStart(mode, false, mainAura);
        end
      end
    else
      for id, button in pairs(displayButtons) do
        button:DragStart("SINGLE", id == mainAura.id, mainAura);
      end
    end
  end
  OptionsPrivate.UpdateButtonsScroll()
end

function OptionsPrivate.DropIndicator()
  local indicator = frame.dropIndicator
  if not indicator then
    ---@class Frame
    indicator = CreateFrame("Frame", "WeakAuras_DropIndicator")
    indicator:SetHeight(4)
    indicator:SetFrameStrata("FULLSCREEN")

    local groupTexture = indicator:CreateTexture(nil, "ARTWORK")
    groupTexture:SetBlendMode("ADD")
    groupTexture:SetTexture("Interface\\AddOns\\WeakAuras\\Media\\Textures\\Square_FullWhite")

    local lineTexture = indicator:CreateTexture(nil, "ARTWORK")
    lineTexture:SetBlendMode("ADD")
    lineTexture:SetAllPoints(indicator)
    lineTexture:SetTexture("Interface\\PaperDollInfoFrame\\UI-Character-Tab-Highlight")

    indicator.lineTexture = lineTexture
    indicator.groupTexture = groupTexture
    frame.dropIndicator = indicator
    indicator:Hide()

    function indicator:ShowAction(target, action)
      self:Show()
      self:ClearAllPoints()
      if action == "GROUP" then
        self.groupTexture:ClearAllPoints()
        self.groupTexture:SetVertexColor(0.4, 0.7, 1, 0.7)
        self.groupTexture:Show()
        self.groupTexture:SetPoint("TOPLEFT", target.icon, "TOPRIGHT", 2, -1)
        self.groupTexture:SetPoint("BOTTOMRIGHT", target.frame, "BOTTOMRIGHT", 0, 1)
      else
        self.groupTexture:Hide()
      end

      -- Position line texture, if needed
      if action == "BEFORE" then
        self.lineTexture:Show()
        self:SetPoint("BOTTOMLEFT", target.frame, "TOPLEFT", 0, -1)
        self:SetPoint("BOTTOMRIGHT", target.frame, "TOPRIGHT", 0, -1)
        self:SetHeight(4)
      elseif action == "AFTER" then
        self.lineTexture:Show()
        self:SetPoint("TOPLEFT", target.frame, "BOTTOMLEFT", 0, 1)
        self:SetPoint("TOPRIGHT", target.frame, "BOTTOMRIGHT", 0, 1)
        self:SetHeight(4)
      else
        self.lineTexture:Hide()
      end
    end

  end
  return indicator
end

function WeakAuras.UpdateThumbnail(data)
  local id = data.id
  local button = displayButtons[id]
  if (not button) then
    return
  end
  button:UpdateThumbnail()
end

function OptionsPrivate.OpenTexturePicker(baseObject, paths, properties, textures, SetTextureFunc, adjustSize)
   OptionsPrivate.TexturePicker(frame):Open(baseObject, paths, properties, textures, SetTextureFunc, adjustSize)
end

function OptionsPrivate.OpenIconPicker(baseObject, paths, groupIcon)
  OptionsPrivate.IconPicker(frame):Open(baseObject, paths, groupIcon)
end

function OptionsPrivate.OpenModelPicker(baseObject, path)
  if not(C_AddOns.IsAddOnLoaded("WeakAurasModelPaths")) then
    local loaded, reason = C_AddOns.LoadAddOn("WeakAurasModelPaths");
    if not(loaded) then
      reason = string.lower("|cffff2020" .. _G["ADDON_" .. reason] .. "|r.")
      WeakAuras.prettyPrint(string.format(L["ModelPaths could not be loaded, the addon is %s"], reason));
      WeakAuras.ModelPaths = {};
    end
    OptionsPrivate.ModelPicker(frame).modelTree:SetTree(WeakAuras.ModelPaths)
  end
  OptionsPrivate.ModelPicker(frame):Open(baseObject, path);
end

function OptionsPrivate.OpenCodeReview(data)
  OptionsPrivate.CodeReview(frame):Open(data);
end

function OptionsPrivate.OpenTriggerTemplate(data, targetId)
  if not(C_AddOns.IsAddOnLoaded("WeakAurasTemplates")) then
    local loaded, reason = C_AddOns.LoadAddOn("WeakAurasTemplates");
    if not(loaded) then
      reason = string.lower("|cffff2020" .. _G["ADDON_" .. reason] .. "|r.")
      WeakAuras.prettyPrint(string.format(L["Templates could not be loaded, the addon is %s"], reason));
      return;
    end
    frame.newView = WeakAuras.CreateTemplateView(OptionsPrivate.Private, frame);
  end
  -- This is called multiple times if a group is selected
  if frame.window ~= "newView" then
    frame.newView:Open(data, targetId);
  end
end

OptionsPrivate.currentDynamicTextInput = false;

local BaseDynamicTextCodes = {
  trigger = {
    {type = "mini", name = "p", desc = L["Progress - The remaining time of a timer, or a non-timer value"]},
    {type = "mini", name = "t", desc = L["Total - The maximum duration of a timer, or a maximum non-timer value"]},
    {type = "mini", name = "n", desc = L["Name - The name of the display (usually an aura name), or the display's ID if there is no dynamic name"]},
    {type = "mini", name = "i", desc = L["Icon - The icon associated with the display"]},
    {type = "mini", name = "s", desc = L["Stacks - The number of stacks of an aura (usually)"]},
  },
  global = {
    {type = "mini", name = "c", desc = L["Custom - Allows you to define a custom Lua function that returns a list of string values. %c1 will be replaced by the first value returned, %c2 by the second, etc."]},
    {type = "mini", name = "%", desc = L["% - To show a percent sign"]},
  }
}

function OptionsPrivate.UpdateTextReplacements(frame, data)
  frame.scrollList:ReleaseChildren()

  local props = OptionsPrivate.Private.GetAdditionalProperties(data)
  local sortedProps = {}

  -- Add global header and markers
  table.insert(sortedProps, {type = "header", triggerNum = 0, name = "Global Properties"})
  for index, icon in ipairs(ICON_LIST) do
    table.insert(sortedProps, {type = "marker", triggerNum = 0, name = "{rt"..index.."}", desc = icon..":0|t", widthFraction = #ICON_LIST})
  end

  -- Add base dynamic text codes
  local globalProps = {}
  tAppendAll(globalProps, CopyTable(BaseDynamicTextCodes.trigger))
  tAppendAll(globalProps, CopyTable(BaseDynamicTextCodes.global))
  for _, prop in ipairs(globalProps) do
    prop.widthFraction = #globalProps
    prop.triggerNum = 0
    table.insert(sortedProps, prop)
  end

  -- Process each trigger's properties
  for triggerNum, triggerProps in pairs(props) do
    if next(triggerProps) then
      -- Create a temporary table for this trigger's properties
      local tempProps = {}

      -- Add the properties to the temporary table
      for name, data in pairs(triggerProps) do
        table.insert(tempProps, {triggerNum = triggerNum, name = name, desc = data.display})
      end

      -- Sort the temporary table by name
      table.sort(tempProps, function(a, b)
        return a.name < b.name
      end)

      -- Add a header for the trigger
      table.insert(sortedProps, {type = "header", triggerNum = triggerNum, name = OptionsPrivate.GetTriggerTitle(data, triggerNum)})

      -- Add the base properties for the trigger
      for _, v in ipairs(BaseDynamicTextCodes.trigger) do
        local prop = CopyTable(v)
        prop.widthFraction = #BaseDynamicTextCodes.trigger
        prop.triggerNum = triggerNum
        table.insert(sortedProps, prop)
      end

      -- Add the sorted properties to the sortedProps table
      for _, prop in ipairs(tempProps) do
        table.insert(sortedProps, prop)
      end
    end
  end

  -- Create a modified WeakAurasSnippetButton for each property and add it to ScrollList
  local lastType, miniGroup
  for i, prop in ipairs(sortedProps) do
    if prop.type == "header" then
      local heading = AceGUI:Create("Heading")
      heading:SetText(prop.name)
      heading:SetRelativeWidth(1)
      heading.label:SetFontObject(GameFontNormalSmall2)
      frame.scrollList:AddChild(heading)
    else
      if ((prop.type == "mini" or prop.type == "marker") and prop.type ~= lastType)
      then
        miniGroup = AceGUI:Create("SimpleGroup")
        miniGroup:SetLayout("Flow")
        miniGroup:SetAutoAdjustHeight(true)
        miniGroup:SetRelativeWidth(1)
        frame.scrollList:AddChild(miniGroup)
      end
      local button = AceGUI:Create("WeakAurasSnippetButton")
      local propIndex = prop.triggerNum > 0 and ("%s"):format(prop.triggerNum) or ""
      local propPrefix = prop.triggerNum > 0 and ("%%%s."):format(propIndex) or "%"
      if prop.type == "marker" then
        button:SetTitle(prop.desc)
      else
        button:SetTitle(string.format("|cFFFFCC00%s|r%s", propPrefix, prop.name))
      end
      if prop.type == "mini" or prop.type == "marker" then
        button:SetRelativeWidth((1/prop.widthFraction) - 1e-10)
      else
        button:SetRelativeWidth(1)
      end
      button.title:SetFontObject(GameFontNormal)
      button.frame:SetHeight(28)
      button:SetDynamicTextStyle()

      -- Set Tooltip
      if prop.type ~= "marker" then
        button.frame:SetScript("OnEnter", function(frame)
          local tooltip = GameTooltip
          tooltip:SetWidth(300)
          tooltip:SetOwner(frame, "ANCHOR_RIGHT")
          tooltip:ClearLines()
          tooltip:AddLine(("%s%s"):format(propPrefix, prop.name))
          tooltip:AddLine(prop.desc, 1, 1, 1, true)
          if prop.name ~= "c" and prop.name ~= "%" then
            tooltip:AddLine("\n")
            tooltip:AddLine(
              prop.triggerNum > 0
              and L["The trigger number is optional. When no trigger number is specified, the trigger selected via dynamic information will be used."]
              or L["By default this shows the information from the trigger selected via dynamic information. The information from a specific trigger can be shown via e.g. %2.p."],
              0.8, 0.8, 0.8,
              true)
          end
          tooltip:Show()
          frame.obj:Fire("OnEnter")
        end)
      else
        button.frame:SetScript("OnEnter", nil)
      end

      -- Insert dynamic text property on click
      button:SetCallback("OnClick", function()
        local insertProp
        if prop.type == "marker" then
          insertProp = prop.name
        else
          if IsShiftKeyDown() then
            insertProp = prop.name == "%" and "%%" or ("%%{%s}"):format(prop.name)
            if prop.triggerNum > 0 then
              insertProp = string.format("%%{%d.%s}", propIndex, prop.name)
            end
          else
            insertProp = prop.name == "%" and "%%" or ("%%%s"):format(prop.name)
            if prop.triggerNum > 0 then
              insertProp = string.format("%%%d.%s", propIndex, prop.name)
            end
          end
        end

        OptionsPrivate.currentDynamicTextInput.editbox:Insert(insertProp)
        OptionsPrivate.currentDynamicTextInput.editbox:SetFocus()
      end)

      if prop.type == "mini" or prop.type == "marker" then
        miniGroup:AddChild(button)
      else
        frame.scrollList:AddChild(button)
      end
    end
    lastType = prop.type
  end
end

function OptionsPrivate.ResetMoverSizer()
  if(frame and frame.mover and frame.moversizer and frame.mover.moving.region and frame.mover.moving.data) then
    frame.moversizer:SetToRegion(frame.mover.moving.region, frame.mover.moving.data);
  end
end

function WeakAuras.SetMoverSizer(id)
  OptionsPrivate.Private.EnsureRegion(id)
  if OptionsPrivate.Private.regions[id].region.toShow then
    frame.moversizer:SetToRegion(OptionsPrivate.Private.regions[id].region, db.displays[id])
  else
    if OptionsPrivate.Private.clones[id] then
      local _, clone = next(OptionsPrivate.Private.clones[id])
      if clone then
        frame.moversizer:SetToRegion(clone, db.displays[id])
      end
    end
  end
end

function WeakAuras.GetMoverSizerId()
  return frame.moversizer:GetCurrentId()
end

local function AddDefaultSubRegions(data)
  data.subRegions = data.subRegions or {}
  for type, subRegionData in pairs(OptionsPrivate.Private.subRegionTypes) do
    if subRegionData.addDefaultsForNewAura then
      subRegionData.addDefaultsForNewAura(data)
    end
  end
end

function WeakAuras.NewAura(sourceData, regionType, targetId)
  local function ensure(t, k, v)
    return t and k and v and t[k] == v
  end
  local new_id = OptionsPrivate.Private.FindUnusedId("New")
  local data = {id = new_id, regionType = regionType, uid = WeakAuras.GenerateUniqueID()}
  WeakAuras.DeepMixin(data, OptionsPrivate.Private.data_stub);
  if (sourceData) then
    WeakAuras.DeepMixin(data, sourceData);
  end
  data.internalVersion = WeakAuras.InternalVersion();
  OptionsPrivate.Private.validate(data, OptionsPrivate.Private.regionTypes[regionType].default);

  AddDefaultSubRegions(data)

  if targetId then
    local target = OptionsPrivate.GetDisplayButton(targetId);
    local group
    if (target) then
      if (target:IsGroup()) then
        group = target;
      else
        group = OptionsPrivate.GetDisplayButton(target.data.parent);
      end
      if (group) then
        -- Sanity check so that we don't create a group/dynamic group in a group
        if (regionType == "group" or regionType == "dynamicgroup") and group.data.regionType == "dynamicgroup" then
          return
        end

        local children = group.data.controlledChildren;
        local index = target:GetGroupOrder();
        if (ensure(children, index, target.data.id)) then
          -- account for insert position
          index = index + 1;
          tinsert(children, index, data.id);
        else
          -- move source into group as the first child
          tinsert(children, 1, data.id);
        end
        data.parent = group.data.id;
        WeakAuras.Add(data);
        WeakAuras.Add(group.data);
        OptionsPrivate.Private.AddParents(group.data)
        WeakAuras.NewDisplayButton(data);
        WeakAuras.UpdateGroupOrders(group.data);
        OptionsPrivate.ClearOptions(group.data.id);
        group.callbacks.UpdateExpandButton();
        group:UpdateParentWarning();
        group:Expand();
        group:ReloadTooltip();
        OptionsPrivate.PickAndEditDisplay(data.id);
      else
        -- move source into the top-level list
        WeakAuras.Add(data);
        WeakAuras.NewDisplayButton(data);
        OptionsPrivate.PickAndEditDisplay(data.id);
      end
    else
      error(string.format("Calling 'WeakAuras.NewAura' with invalid groupId %s. Reload your UI to fix the display list.", targetId))
    end
  else
    -- move source into the top-level list
    WeakAuras.Add(data);
    WeakAuras.NewDisplayButton(data);
    OptionsPrivate.PickAndEditDisplay(data.id);
  end
end


function OptionsPrivate.ResetCollapsed(id, namespace)
  if id then
    if namespace and collapsedOptions[id] then
      collapsedOptions[id][namespace] = nil
    else
      collapsedOptions[id] = nil
    end
  end
end

function OptionsPrivate.IsCollapsed(id, namespace, path, default)
  local tmp = collapsedOptions[id]
  if tmp == nil then return default end

  tmp = tmp[namespace]
  if tmp == nil then return default end

  if type(path) ~= "table" then
    tmp = tmp[path]
  else
    for _, key in ipairs(path) do
      tmp = tmp[key]
      if tmp == nil or tmp[collapsed] then
        break
      end
    end
  end
  if tmp == nil or tmp[collapsed] == nil then
    return default
  else
    return tmp[collapsed]
  end
end

function OptionsPrivate.SetCollapsed(id, namespace, path, v)
  collapsedOptions[id] = collapsedOptions[id] or {}
  collapsedOptions[id][namespace] = collapsedOptions[id][namespace] or {}
  if type(path) ~= "table" then
    collapsedOptions[id][namespace][path] = collapsedOptions[id][namespace][path] or {}
    collapsedOptions[id][namespace][path][collapsed] = v
  else
    local tmp = collapsedOptions[id][namespace] or {}
    for _, key in ipairs(path) do
      tmp[key] = tmp[key] or {}
      tmp = tmp[key]
    end
    tmp[collapsed] = v
  end
end

function OptionsPrivate.MoveCollapseDataUp(id, namespace, path)
  collapsedOptions[id] = collapsedOptions[id] or {}
  collapsedOptions[id][namespace] = collapsedOptions[id][namespace] or {}
  if type(path) ~= "table" then
    collapsedOptions[id][namespace][path], collapsedOptions[id][namespace][path - 1]
      = collapsedOptions[id][namespace][path - 1], collapsedOptions[id][namespace][path]
  else
    local tmp = collapsedOptions[id][namespace]
    local lastKey = tremove(path)
    for _, key in ipairs(path) do
      tmp[key] = tmp[key] or {}
      tmp = tmp[key]
    end
    tmp[lastKey], tmp[lastKey - 1] = tmp[lastKey - 1], tmp[lastKey]
  end
end

function OptionsPrivate.MoveCollapseDataDown(id, namespace, path)
  collapsedOptions[id] = collapsedOptions[id] or {}
  collapsedOptions[id][namespace] = collapsedOptions[id][namespace] or {}
  if type(path) ~= "table" then
    collapsedOptions[id][namespace][path], collapsedOptions[id][namespace][path + 1]
      = collapsedOptions[id][namespace][path + 1], collapsedOptions[id][namespace][path]
  else
    local tmp = collapsedOptions[id][namespace]
    local lastKey = tremove(path)
    for _, key in ipairs(path) do
      tmp[key] = tmp[key] or {}
      tmp = tmp[key]
    end
    tmp[lastKey], tmp[lastKey + 1] = tmp[lastKey + 1], tmp[lastKey]
  end
end

function OptionsPrivate.RemoveCollapsed(id, namespace, path)
  local data = collapsedOptions[id] and collapsedOptions[id][namespace]
  if not data then
    return
  end
  local index
  local maxIndex = 0
  if type(path) ~= "table" then
    index = path
  else
    index = path[#path]
    for i = 1, #path - 1 do
      data = data[path[i]]
      if not data then
        return
      end
    end
  end
  for k in pairs(data) do
    if k ~= collapsed then
      maxIndex = max(maxIndex, k)
    end
  end
  while index <= maxIndex do
    data[index] = data[index + 1]
    index = index + 1
  end
end

function OptionsPrivate.InsertCollapsed(id, namespace, path, value)
  local data = collapsedOptions[id] and collapsedOptions[id][namespace]
  if not data then
    return
  end
  local insertPoint
  local maxIndex
  if type(path) ~= "table" then
    insertPoint = path
  else
    insertPoint = path[#path]
    for i = 1, #path - 1 do
      data = data[path[i]]
      if not data then
        return
      end
    end
  end
  for k in pairs(data) do
    if k ~= collapsed and k >= insertPoint then
      if not maxIndex or k > maxIndex then
        maxIndex = k
      end
    end
  end
  if maxIndex then -- may be nil if insertPoint is greater than the max of anything else
    for i = maxIndex, insertPoint, -1 do
      data[i + 1] = data[i]
    end
  end
  data[insertPoint] = {[collapsed] = value}
end

function OptionsPrivate.DuplicateCollapseData(id, namespace, path)
  collapsedOptions[id] = collapsedOptions[id] or {}
  collapsedOptions[id][namespace] = collapsedOptions[id][namespace] or {}
  if type(path) ~= "table" then
    if (collapsedOptions[id][namespace][path]) then
      tinsert(collapsedOptions[id][namespace], path + 1, CopyTable(collapsedOptions[id][namespace][path]))
    end
  else
    local tmp = collapsedOptions[id][namespace]
    local lastKey = tremove(path)
    for _, key in ipairs(path) do
      tmp[key] = tmp[key] or {}
      tmp = tmp[key]
    end

    if (tmp[lastKey]) then
      tinsert(tmp, lastKey + 1, CopyTable(tmp[lastKey]))
    end
  end
end

function OptionsPrivate.AddTextFormatOption(input, withHeader, get, addOption, hidden, setHidden,
                                            withoutColor, index, total)
  local headerOption
  if withHeader and (not index or index == 1) then
    headerOption =  {
      type = "execute",
      control = "WeakAurasExpandSmall",
      name = L["|cffffcc00Format Options|r"],
      width = WeakAuras.doubleWidth,
      func = function(info, button)
        setHidden(not hidden())
      end,
      image = function()
        return hidden() and "collapsed" or "expanded"
      end,
      imageWidth = 15,
      imageHeight = 15,
      arg = {
        expanderName = tostring(addOption)
      }
    }
    addOption("header", headerOption)
  else
    hidden = false
  end


  local seenSymbols = {}

  local parseFn = function(symbol)
    if not seenSymbols[symbol] then
      local _, sym = string.match(symbol, "(.+)%.(.+)")
      sym = sym or symbol

      if sym == "i" then
        -- No special options for these
      else
        addOption(symbol .. "desc", {
          type = "description",
          name = L["Format for %s"]:format("%" .. symbol),
          width = WeakAuras.normalWidth,
          hidden = hidden
        })
        addOption(symbol .. "_format", {
          type = "select",
          name = L["Format"],
          width = WeakAuras.normalWidth,
          values = OptionsPrivate.Private.format_types_display,
          hidden = hidden,
          reloadOptions = true
        })

        local selectedFormat = get(symbol .. "_format")
        if (OptionsPrivate.Private.format_types[selectedFormat]) then
          OptionsPrivate.Private.format_types[selectedFormat].AddOptions(symbol, hidden, addOption, get, withoutColor)
        end
        seenSymbols[symbol] = true
      end
    end
  end

  if type(input) == "table" then
    for _, txt in ipairs(input) do
      OptionsPrivate.Private.ParseTextStr(txt, parseFn)
    end
  else
    OptionsPrivate.Private.ParseTextStr(input, parseFn)
  end

  if withHeader and (not index or index == total) then
    addOption("header_anchor",
    {
      type = "description",
      name = "",
      control = "WeakAurasExpandAnchor",
      arg = {
        expanderName = tostring(addOption)
      }
    }

  )
  end

  if not next(seenSymbols) and headerOption and not index then
    headerOption.hidden = true
  end

  return next(seenSymbols) ~= nil
end
