if not WeakAuras.IsLibsOK() then return end
local AddonName = ...
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, IsAddOnLoaded, LoadAddOn = CreateFrame, IsAddOnLoaded, LoadAddOn

local AceGUI = LibStub("AceGUI-3.0")

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;
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 = 1,
  whileDead = 1,
  preferredindex = STATICPOPUP_NUMDIALOGS,
}

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
  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
    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)
  OptionsPrivate.TexturePicker(frame):Open(baseObject, paths, properties, textures, SetTextureFunc)
end

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

function OptionsPrivate.OpenModelPicker(baseObject, path)
  if not(IsAddOnLoaded("WeakAurasModelPaths")) then
    local loaded, reason = 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(IsAddOnLoaded("WeakAurasTemplates")) then
    local loaded, reason = 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(GameFontNormalSmall)
      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
