---@class addonTablePlatynator
local addonTable = select(2, ...)

local LSM = LibStub("LibSharedMedia-3.0")

local function Announce()
  if addonTable.Config.Get(addonTable.Config.Options.STYLE):match("^_") then
    addonTable.Config.Set(addonTable.Config.Options.STYLE, addonTable.Constants.CustomName)
  end
  local design = addonTable.CustomiseDialog.GetCurrentDesign()
  local click, stack = addonTable.Utilities.GenerateRects(design)
  if design.regions.click.autoSized then
    design.regions.click = addonTable.Utilities.ConvertRectToWidget(click)
  end
  if design.regions.stack.autoSized then
    design.regions.stack = addonTable.Utilities.ConvertRectToWidget(stack)
  end
  addonTable.CallbackRegistry:TriggerEvent("RefreshStateChange", {[addonTable.Constants.RefreshReason.Design] = true})
end

local GenerateOptions

local pixelStep = 0.5

local function GetSelectorMarker(frame, isHover)
  local texture = frame:CreateTexture()
  texture:SetTexture("Interface/AddOns/Platynator/Assets/selection-outline.png")
  texture:SetVertexColor(78/255, 165/255, 252/255, isHover and 0.45 or 0.8)
  texture:SetTextureSliceMargins(45, 45, 45, 45)
  texture:SetTextureSliceMode(Enum.UITextureSliceMode.Tiled)
  texture:SetScale(0.25)
  texture:SetAllPoints()

  return frame
end

local function RoundPixel(pixel)
  return Round(pixel / pixelStep) * pixelStep
end

local function UpdateWidgetPoints(preview, w, snapping, offsetX, offsetY)
  snapping = snapping or 2
  offsetX = offsetX or 0
  offsetY = offsetY or 0
  local left, bottom, width, height = w:GetRect()
  local widgetRect = {left = left + offsetX, bottom = bottom + offsetY, width = width, height = height}
  left, bottom, width, height = preview:GetRect()
  local previewRect = {left = left, bottom = bottom, width = width, height = height}
  local widgetCenter = {x = widgetRect.left + widgetRect.width / 2, y = widgetRect.bottom + widgetRect.height / 2}
  local previewCenter = {x = previewRect.left + previewRect.width / 2, y = previewRect.bottom + previewRect.height / 2}

  local point, x, y = "", 0, 0

  local snapX, snapY, xLock, yLock = 0, 0, false, false
  if math.abs(widgetCenter.y - previewCenter.y) < snapping then
    snapY = previewCenter.y - widgetCenter.y
    point = point
    yLock = true
  elseif widgetCenter.y < previewCenter.y then
    point = "TOP" .. point
    y = widgetRect.bottom + widgetRect.height - previewCenter.y
  else
    point = "BOTTOM" .. point
    y = widgetRect.bottom - previewCenter.y
  end

  if math.abs(widgetCenter.x - previewCenter.x) < snapping then
    snapX = previewCenter.x - widgetCenter.x
    xLock = true
    point = point
  elseif widgetCenter.x < previewCenter.x then
    point = point .. "LEFT"
    x = widgetRect.left - previewCenter.x
  else
    point = point .. "RIGHT"
    x = widgetRect.left + widgetRect.width - previewCenter.x
  end

  if point == "" then
    w.details.anchor = {}
  elseif x == 0 and y == 0 then
    w.details.anchor = {point}
  else
    w.details.anchor = {point, RoundPixel(x), RoundPixel(y)}
  end

  if x ~= 0 then
    snapX = RoundPixel(x) - x
  end
  if y ~= 0 then
    snapY = RoundPixel(y) - y
  end

  -- snapX, snapY used to offset other widgets to keep them all consistent to each other
  -- xLock, yLock used to prevent a widget shifting because its been centered on an axis
  -- (this prevents infinite loops from the shifts bouncing around)
  return snapX, snapY, xLock, yLock
end

local function GetAutomaticColors(rootParent, lockedElements, addAlpha)
  local selectedValue = ""
  local UpdateSelected

  local container = CreateFrame("Frame", nil, rootParent)
  container:SetAllPoints()

  local colorsListContainer = CreateFrame("Frame", nil, container)

  local inset = CreateFrame("Frame", nil, colorsListContainer, "InsetFrameTemplate")
  inset:SetPoint("TOPLEFT")
  inset:SetPoint("BOTTOMRIGHT", -15, 0)
  --addonTable.Skins.AddFrame("InsetFrame", inset)
  colorsListContainer.ScrollBox = CreateFrame("Frame", nil, colorsListContainer, "WowScrollBoxList")
  colorsListContainer.ScrollBox:SetPoint("TOPLEFT", 1, -3)
  colorsListContainer.ScrollBox:SetPoint("BOTTOMRIGHT", -15, 3)

  local scrollView = CreateScrollBoxListLinearView()
  scrollView:SetElementExtent(40)
  scrollView:SetElementInitializer("Button", function(frame, elementData)
    if not frame.initialized then
      frame.initialized = true
      frame:SetNormalFontObject(GameFontHighlight)
      frame:SetHighlightAtlas("Options_List_Hover")
      frame.selectedTexture = frame:CreateTexture(nil, "ARTWORK")
      frame.selectedTexture:SetAllPoints(true)
      frame.selectedTexture:Hide()
      frame.selectedTexture:SetAtlas("Options_List_Active")
      frame:SetScript("OnClick", function(self, button)
        UpdateSelected(self.value)
      end)
      frame:SetText(" ")
      frame:GetFontString():SetWordWrap(false)
      frame.shiftUp = CreateFrame("Button", nil, frame)
      frame.shiftUp:SetSize(16, 20)
      frame.shiftUp:SetNormalAtlas("bag-arrow")
      frame.shiftUp:GetNormalTexture():SetRotation(- math.pi / 2)
      frame.shiftUp:GetNormalTexture():SetSize(16, 10)
      frame.shiftUp:SetScript("OnEnter", function()
        frame.shiftUp:GetNormalTexture():SetAlpha(0.5)
      end)
      frame.shiftUp:SetScript("OnLeave", function()
        frame.shiftUp:GetNormalTexture():SetAlpha(1)
      end)
      frame.shiftDown = CreateFrame("Button", nil, frame)
      frame.shiftDown:SetSize(16, 20)
      frame.shiftDown:SetNormalAtlas("bag-arrow")
      frame.shiftDown:GetNormalTexture():SetRotation(math.pi / 2)
      frame.shiftDown:GetNormalTexture():SetSize(16, 10)
      frame.shiftDown:SetPoint("RIGHT", -5, 2)
      frame.shiftDown:SetScript("OnEnter", function()
        frame.shiftDown:GetNormalTexture():SetAlpha(0.5)
      end)
      frame.shiftDown:SetScript("OnLeave", function()
        frame.shiftDown:GetNormalTexture():SetAlpha(1)
      end)
      frame.shiftUp:SetPoint("RIGHT", frame.shiftDown, "LEFT", -2, 0)

      frame.shiftUp:SetScript("OnClick", function()
        UpdateSelected(frame.value)
        if frame.index > 1 then
          local old = container.details[frame.index - 1]
          if lockedElements[old.kind] then
            return
          end
          container.details[frame.index - 1] = container.details[frame.index]
          container.details[frame.index] = old
          Announce()
        end
      end)

      frame.shiftDown:SetScript("OnClick", function()
        UpdateSelected(frame.value)
        if frame.index < #container.details then
          local old = container.details[frame.index + 1]
          if lockedElements[old.kind] then
            return
          end
          container.details[frame.index + 1] = container.details[frame.index]
          container.details[frame.index] = old
          Announce()
        end
      end)
    end
    frame.value = elementData.value
    frame.index = elementData.index
    frame.selectedTexture:SetShown(frame.value == selectedValue)
    frame.shiftUp:SetShown(not lockedElements[frame.value] and frame.index > 1 and not lockedElements[container.details[frame.index - 1].kind])
    frame.shiftDown:SetShown(not lockedElements[frame.value] and frame.index < #container.details and not lockedElements[container.details[frame.index + 1].kind])
    frame:SetText(elementData.label)
    frame:GetFontString():SetPoint("RIGHT", -8, 0)
    frame:GetFontString():SetPoint("LEFT", 20, 0)
    frame:GetFontString():SetJustifyH("LEFT")
  end)

  local colorsDropdown = CreateFrame("DropdownButton", nil, colorsListContainer, "WowStyle1DropdownTemplate")
  colorsDropdown:SetWidth(250)
  colorsDropdown:SetPoint("LEFT")
  colorsDropdown:SetPoint("RIGHT")
  colorsDropdown:SetPoint("TOP", colorsListContainer, "BOTTOM", 0, -5)
  colorsDropdown:SetupMenu(function(menu, rootDescription)
    if not container.details then
      return
    end
    local seen = {}
    for _, entry in ipairs(container.details) do
      seen[entry.kind] = true
    end
    for _, kind in ipairs(addonTable.CustomiseDialog.ColorsConfigOrder) do
      if not seen[kind] then
        local details = addonTable.CustomiseDialog.ColorsConfig[kind]
        rootDescription:CreateButton(details.label, function()
          if addAlpha then
            table.insert(container.details, 1, addonTable.CustomiseDialog.AddAlphaToColors(CopyTable(details.default)))
          else
            table.insert(container.details, 1, CopyTable(details.default))
          end
          Announce()
          UpdateSelected(details.default.kind)
        end)
      end
    end
  end)
  colorsDropdown:SetDefaultText(addonTable.Locales.ADD_COLORS)

  colorsListContainer.ScrollBar = CreateFrame("EventFrame", nil, colorsListContainer, "MinimalScrollBar")
  colorsListContainer.ScrollBar:SetPoint("TOPRIGHT")
  colorsListContainer.ScrollBar:SetPoint("BOTTOMRIGHT")
  ScrollUtil.InitScrollBoxListWithScrollBar(colorsListContainer.ScrollBox, colorsListContainer.ScrollBar, scrollView)
  --addonTable.Skins.AddFrame("TrimScrollBar", colorsListContainer.ScrollBar)

  colorsListContainer:SetPoint("TOPLEFT", 20, 0)
  colorsListContainer:SetSize(200, 350)

  local optionsContainer = CreateFrame("Frame", nil, container)
  optionsContainer:SetPoint("TOPLEFT", colorsListContainer, "TOPRIGHT", 10, 0)
  optionsContainer:SetPoint("BOTTOMRIGHT", container)

  local configsByKind = {}
  for kind, colorDetails in pairs(addonTable.CustomiseDialog.ColorsConfig) do
    local yOffset = 0
    local parent = CreateFrame("Frame", nil, optionsContainer)
    parent:SetAllPoints()

    local allFrames = GenerateOptions(parent, yOffset, -40, colorDetails.entries)

    if not lockedElements[kind] then
      local deleteButton = CreateFrame("Button", nil, parent, "UIPanelDynamicResizeButtonTemplate")
      deleteButton:SetText(DELETE)
      DynamicResizeButton_Resize(deleteButton)
      if #allFrames > 0 then
        deleteButton:SetPoint("BOTTOMRIGHT", -15, -20)
      else
        deleteButton:SetPoint("TOP", 0, -15)
      end
      deleteButton:SetScript("OnClick", function()
        table.remove(container.details, tIndexOf(container.details, parent.details))
        parent:Hide()
        Announce()
      end)
    end

    function parent:UpdateOptions(details)
      parent.details = nil
      for _, e in ipairs(details) do
        if e.kind == kind then
          parent.details = e
          break
        end
      end
      if not parent.details then
        parent:Hide()
        return
      end
      for _, f in ipairs(allFrames) do
        f:SetValue(f.Getter())
      end
    end
    parent:Hide()
    configsByKind[kind] = parent
  end

  local function GetListing()
    local listing = {}
    for index, e in ipairs(container.details) do
      table.insert(listing, {
        label = addonTable.CustomiseDialog.ColorsConfig[e.kind].label,
        value = e.kind,
        index = index,
      })
    end

    return listing
  end

  function container:SetValue(details)
    container.details = details
    colorsDropdown:GenerateMenu()

    colorsListContainer.ScrollBox:SetDataProvider(CreateDataProvider(GetListing()))

    for _, c in pairs(configsByKind) do
      if c:IsShown() then
        c:UpdateOptions(container.details)
      end
    end
  end

  UpdateSelected = function(value)
    selectedValue = value
    for kind, c in pairs(configsByKind) do
      if kind == value then
        c:Show()
        c:UpdateOptions(container.details)
      else
        c:Hide()
      end
    end
    colorsListContainer.ScrollBox:SetDataProvider(CreateDataProvider(GetListing()))
  end

  return container
end

local function GetAurasTextPositioning(rootParent, iconID)
  local container = CreateFrame("Frame", nil, rootParent)
  container:SetPoint("LEFT")
  container:SetPoint("RIGHT")
  container:SetHeight(300)
  local previewInset = CreateFrame("Frame", nil, container, "InsetFrameTemplate")
  previewInset:SetSize(160, 120)
  previewInset:SetPoint("TOP")

  local preview = CreateFrame("Frame", nil, previewInset)

  preview:SetPoint("TOP")

  preview:SetAllPoints()
  preview:SetFlattensRenderLayers(true)
  preview:SetScale(3)

  preview:SetSize(40, 40)

  local wrapper = CreateFrame("Frame", nil, preview)
  wrapper:SetSize(20, 20)
  wrapper:SetPoint("CENTER")
  wrapper.Icon = wrapper:CreateTexture(nil, "ARTWORK")
  wrapper.Icon:SetTexture(iconID)
  wrapper.Icon:SetPoint("CENTER")
  wrapper.Icon:SetAllPoints()
  local asset = LSM:Fetch("nineslice", "Platy: 1px")
  assert(asset)
  wrapper.Border = wrapper:CreateTexture(nil, "OVERLAY")
  wrapper.Border:SetAllPoints()
  wrapper.Border:SetScale(asset.scaleModifier)
  wrapper.Border:SetTexture(asset.file)
  wrapper.Border:SetTextureSliceMargins(asset.margins.left, asset.margins.top, asset.margins.right, asset.margins.bottom)
  wrapper.Border:SetVertexColor(0, 0, 0)
  wrapper.Border:SetAllPoints()

  local expectedTexts = {"countdown", "stacks"}

  local widgetOptions = {}
  for _, kind in ipairs(expectedTexts) do
    local optionsContainer = CreateFrame("Frame", nil, container)
    optionsContainer:SetPoint("TOP", preview, "BOTTOM", 0, -30)
    optionsContainer:SetPoint("LEFT")
    optionsContainer:SetPoint("RIGHT")
    optionsContainer:SetHeight(10)
    optionsContainer.allFrames = GenerateOptions(optionsContainer, 0, 0, addonTable.CustomiseDialog.AurasTextsConfig[kind])

    widgetOptions[kind] = optionsContainer
  end

  local titleText = container:CreateFontString(nil, nil, "GameFontHighlightLarge")
  titleText:SetPoint("TOP", previewInset, "BOTTOM", 0, -10)
  titleText:SetJustifyH("RIGHT")
  titleText:SetPoint("RIGHT", -40, 0)
  titleText:SetShadowOffset(1, -1)

  local titleMap = {
    countdown = addonTable.Locales.COUNTDOWN,
    stacks = addonTable.Locales.STACKS,
  }

  local selectedMarker = GetSelectorMarker(CreateFrame("Frame", nil, container), false)
  local hoverMarker = GetSelectorMarker(CreateFrame("Frame", nil, container), true)
  local selection = nil

  local keyboardTrap = CreateFrame("Frame", nil, container)
  keyboardTrap:Hide()

  local function OffsetWidgets(x, y)
    UpdateWidgetPoints(preview, selection, 0.4, x, y)
    Announce()
  end

  keyboardTrap:SetScript("OnKeyDown", function(_, key)
    keyboardTrap:SetPropagateKeyboardInput(false)
    local amount = pixelStep
    if IsShiftKeyDown() then
      amount = amount * 4
    end
    if key == "LEFT" then
      OffsetWidgets(-amount, 0)
    elseif key == "RIGHT" then
      OffsetWidgets(amount, 0)
    elseif key == "UP" then
      OffsetWidgets(0, amount)
    elseif key == "DOWN" then
      OffsetWidgets(0, -amount)
    elseif key == "DELETE" then
      selection.details.visible = false
      Announce()
    else
      keyboardTrap:SetPropagateKeyboardInput(true)
    end
  end)
  keyboardTrap:RegisterEvent("PLAYER_REGEN_ENABLED")
  keyboardTrap:RegisterEvent("PLAYER_REGEN_DISABLED")
  keyboardTrap:SetScript("OnEvent", function(_, event)
    keyboardTrap:SetShown(event == "PLAYER_REGEN_ENABLED" and selection)
  end)

  local function UpdateSelection()
    if selection then
      selectedMarker:Show()
      selectedMarker:SetFrameStrata("HIGH")
      selectedMarker:ClearAllPoints()
      selectedMarker:SetPoint("TOPLEFT", selection, "TOPLEFT", -2, 2)
      selectedMarker:SetPoint("BOTTOMRIGHT", selection, "BOTTOMRIGHT", 2, -2)

      for kind, optionsContainer in pairs(widgetOptions) do
        if kind == selection.kind then
          optionsContainer:Show()
          optionsContainer.details = selection.details
          for _, f in ipairs(optionsContainer.allFrames) do
            if f.getInitData then
              f:Init(f.getInitData(selection.details))
            end
            f:SetValue(f.Getter())
          end
        else
          optionsContainer:Hide()
        end
      end

      titleText:Show()
      titleText:SetText(titleMap[selection.kind])
      keyboardTrap:SetShown(not InCombatLockdown())
    else
      titleText:Hide()
      for _, optionsContainer in pairs(widgetOptions) do
        optionsContainer:Hide()
      end
      selectedMarker:Hide()
      keyboardTrap:Hide()
    end
  end

  local function ToggleSelection(w)
    if selection == w then
      selection = nil
    else
      selection = w
    end
    UpdateSelection()
  end
  local function ForceSelection(w)
    selection = w
    UpdateSelection()
  end

  preview.widgets = {}

  for _, key in ipairs(expectedTexts) do
    local w = CreateFrame("Frame", nil, wrapper)
    w:SetSize(1, 1)
    preview.widgets[key] = w
    w.text = w:CreateFontString(nil, nil, "GameFontNormal")
    w.kind = key
    w:SetMovable(true)
    w:EnableMouse(true)
    w:RegisterForDrag("LeftButton")
    w:SetScript("OnEnter", function()
      hoverMarker:Show()
      hoverMarker:SetFrameStrata("HIGH")
      hoverMarker:ClearAllPoints()
      hoverMarker:SetPoint("TOPLEFT", w, "TOPLEFT", -2, 2)
      hoverMarker:SetPoint("BOTTOMRIGHT", w, "BOTTOMRIGHT", 2, -2)
    end)
    w:SetScript("OnLeave", function()
      hoverMarker:Hide()
    end)
    w:SetScript("OnDragStart", function()
      w:StartMoving()
      ForceSelection(w)
    end)
    w:SetScript("OnDragStop", function()
      w:StopMovingOrSizing()
      UpdateWidgetPoints(preview, w, 1)
      ForceSelection(w)
      Announce()
    end)
    w:SetScript("OnMouseUp", function()
      ToggleSelection(w)
    end)
  end

  preview.widgets.countdown.text:SetText(3)
  preview.widgets.stacks.text:SetText(2)

  function container:SetValue(details)
    container.details = details.texts

    for key, textDetails in pairs(details.texts) do
      local text = preview.widgets[key].text
      text:ClearAllPoints()
      text:SetFontObject(addonTable.CurrentFont)
      if text.SetSmoothScaling then
        text:SetSmoothScaling(addonTable.CurrentFontUsesSmoothing)
      end
      if addonTable.CurrentFontUsesSmoothing then
        text:SetTextScale(1)
        text:SetScale(textDetails.scale)
      else
        text:SetTextScale(textDetails.scale)
        text:SetScale(1)
      end
      text:SetPoint(textDetails.anchor[1] or "CENTER")
      text:SetTextColor(textDetails.color.r, textDetails.color.g, textDetails.color.b)
      if key == "countdown" and addonTable.Constants.IsCooldownFormattingAvailable then
        if textDetails.showFractions then
          text:SetText("2.9")
        else
          text:SetText("3")
        end
      end
      if textDetails.visible then
        preview.widgets[key]:SetAlpha(1)
      else
        preview.widgets[key]:SetAlpha(0.5)
      end
      local w, h = text:GetSize()
      preview.widgets[key]:SetSize(w * text:GetScale(), h * text:GetScale())
      preview.widgets[key].details = textDetails

      addonTable.Display.ApplyAnchor(preview.widgets[key], textDetails.anchor)
    end

    wrapper:SetHeight(20 * details.height)
    wrapper.Icon:SetHeight(20 * details.height)
    wrapper.Border:SetHeight(20 * details.height)
    local texBase = 0.95 * (1 - details.height) / 2
    wrapper.Icon:SetTexCoord(0.05, 0.95, 0.05 + texBase, 0.95 - texBase)

    UpdateSelection()
  end

  return container
end

GenerateOptions = function(parent, yOffset, xOffset, entries)
  local allFrames = {}

  for _, e in ipairs(entries) do
    local frame
    local function Setter(value)
      if not parent.details then
        return
      end
      local oldValue = e.getter(parent.details)
      e.setter(parent.details, value)
      if type(oldValue) == "table" then
        if not tCompare(oldValue, e.getter(parent.details)) then
          Announce()
        end
      elseif oldValue ~= e.getter(parent.details) then
        Announce()
      end
    end
    local function Getter()
      if not parent.details then
        return
      end
      return e.getter(parent.details)
    end
    if e.hide then
      frame = nil
    elseif e.kind == "slider" then
      if e.valuePattern then
        frame = addonTable.CustomiseDialog.Components.GetSlider(parent, e.label, e.min, e.max, function(val) return e.valuePattern:format(val) end, Setter)
      else
        frame = addonTable.CustomiseDialog.Components.GetSlider(parent, e.label, e.min, e.max, e.formatter, Setter)
      end
    elseif e.kind == "dropdown" then
      frame = addonTable.CustomiseDialog.Components.GetBasicDropdown(parent, e.label, function(value)
        if not parent.details then
          return false
        end
        if type(value) == "table" then
          return tCompare(value, e.getter(parent.details))
        else
          return value == e.getter(parent.details)
        end
      end, Setter)
    elseif e.kind == "checkbox" then
      frame = addonTable.CustomiseDialog.Components.GetCheckbox(parent, e.label, 28 + xOffset, Setter)
    elseif e.kind == "colorPicker" then
      frame = addonTable.CustomiseDialog.Components.GetColorPicker(parent, e.label, 28 + xOffset, Setter)
    elseif e.kind == "colorPickerWithCheckbox" then
      frame = addonTable.CustomiseDialog.Components.GetColorPickerWithCheckbox(parent, e.label, 28 + xOffset, Setter)
    elseif e.kind == "autoColors" then
      frame = GetAutomaticColors(parent, e.lockedElements, e.addAlpha)
    elseif e.kind == "auraTextsPositioner" then
      frame = GetAurasTextPositioning(parent, e.icon)
    end

    if frame then
      frame.kind = e.kind
      frame.getInitData = e.getInitData
      frame.Getter = Getter
      if #allFrames == 0 then
        frame:SetPoint("TOP", 0, yOffset)
      else
        frame:SetPoint("TOP", allFrames[#allFrames], "BOTTOM", 0, yOffset)
      end
      table.insert(allFrames, frame)
      yOffset = 0
    elseif e.kind == "spacer" then
      yOffset = -30
    end
  end

  return allFrames
end

function addonTable.CustomiseDialog.GetMainDesigner(parent)
  local container = CreateFrame("Frame", nil, parent)

  local allFrames = {}

  local styleDropdown = addonTable.CustomiseDialog.GetStyleDropdown(container)
  styleDropdown:SetPoint("TOP")
  table.insert(allFrames, styleDropdown)

  local designScale = addonTable.CustomiseDialog.Components.GetSlider(container, addonTable.Locales.STYLE_SCALE, 1, 300, function(val) return ("%d%%"):format(val) end, function(value)
    local design = addonTable.CustomiseDialog.GetCurrentDesign()
    local oldScale = design.scale
    design.scale = value / 100
    if oldScale ~= design.scale then
      Announce()
    end
  end)
  designScale:SetValue(addonTable.CustomiseDialog.GetCurrentDesign().scale * 100)
  designScale.noAuto = true

  designScale:SetPoint("TOP", allFrames[#allFrames], "BOTTOM", 0, 0)
  table.insert(allFrames, designScale)

  local UpdateSelection
  local widgets
  local selectionIndexes = {}
  local hiddenIndexes = {}
  local fociOnDown = {}
  local autoSelectedDetails
  local shouldShowRegions = false
  local UpdateHiding
  local GenerateWidgets

  local titleMap = {}

  local lastHeader
  for _, entry in ipairs(addonTable.CustomiseDialog.DesignWidgets) do
    if entry.special == "header" then
      lastHeader = entry.name
    else
      if not titleMap[entry.kind] then
        titleMap[entry.kind] = {}
      end
      titleMap[entry.kind][entry.default.kind] = lastHeader .. ": " .. entry.name
    end
  end

  local previewInset = CreateFrame("Frame", nil, container, "InsetFrameTemplate")
  previewInset:EnableMouse(true)
  previewInset:SetFrameStrata("HIGH")
  local maximizeButton = CreateFrame("Frame", nil, previewInset, "MaximizeMinimizeButtonFrameTemplate")
  local isMaximized = false

  local preview = CreateFrame("Frame", nil, previewInset)

  preview:SetPoint("TOP", 0, -15)

  preview:SetAllPoints()
  preview:SetFlattensRenderLayers(true)

  local function SetPreviewMaximised(state)
    previewInset:ClearAllPoints()
    isMaximized = state
    if state then
      maximizeButton:SetMinimizedLook()
      previewInset:SetPoint("CENTER", container, 0, 30)
      previewInset:SetSize(math.min(UIParent:GetWidth() * 0.8, container:GetHeight() * 2), container:GetHeight() * 0.95)
      preview:SetScale(4)
      preview:SetPoint("TOP", 0, 0)
      GenerateWidgets()
      UpdateSelection()
    else
      maximizeButton:SetMaximizedLook()
      previewInset:SetPoint("TOP", allFrames[2], "BOTTOM", 0, -15)
      previewInset:SetPoint("LEFT", 20, 0)
      previewInset:SetPoint("RIGHT", -20, 0)
      previewInset:SetHeight(235)
      preview:SetPoint("TOP", 0, -15)
      preview:SetScale(2)
      GenerateWidgets()
      UpdateSelection()
    end
  end

  maximizeButton:SetPoint("BOTTOMRIGHT", previewInset)
  function maximizeButton.maximizedCallback()
    SetPreviewMaximised(true)
  end
  function maximizeButton.minimizedCallback()
    SetPreviewMaximised(false)
  end

  local contextHoverMarker = GetSelectorMarker(CreateFrame("Frame", nil, container), false)

  local function ToggleSelection(rawFoci, rawButton)
    local foci = tFilter(rawFoci, function(w) return w:GetParent() == preview end, true)
    local ApplyIndex
    if IsShiftKeyDown() then
      ApplyIndex = function(index)
        local selectionIndex = tIndexOf(selectionIndexes, index)
        if selectionIndex ~= nil then
          table.remove(selectionIndexes, selectionIndex)
        else
          table.insert(selectionIndexes, index)
        end
        UpdateSelection()
      end
    elseif rawButton == "MiddleButton" then
      ApplyIndex = function(index)
        hiddenIndexes[index] = true
        UpdateHiding()
      end
    else
      ApplyIndex = function(index)
        if #selectionIndexes > 1 or #selectionIndexes == 0 or selectionIndexes[1] ~= index then
          selectionIndexes = {index}
        else
          selectionIndexes = {}
        end
        UpdateSelection()
      end
    end
    if #foci > 1 then
      MenuUtil.CreateContextMenu(foci[1], function(_, rootDescription)
        rootDescription:SetMinimumWidth(1)
        for _, w in ipairs(foci) do
          local button = rootDescription:CreateButton(titleMap[w.kind][w.details.kind], function()
            local index = tIndexOf(widgets, w)
            ApplyIndex(index)
          end)
          button:SetOnEnter(function()
            contextHoverMarker:Show()
            contextHoverMarker:SetFrameStrata("HIGH")
            contextHoverMarker:ClearAllPoints()
            contextHoverMarker:SetPoint("TOPLEFT", w, "TOPLEFT", -2, 2)
            contextHoverMarker:SetPoint("BOTTOMRIGHT", w, "BOTTOMRIGHT", 2, -2)
          end)
          button:SetOnLeave(function()
            contextHoverMarker:Hide()
          end)
        end
        rootDescription:CreateDivider()
        rootDescription:CreateButton(addonTable.Locales.CLEAR_SELECTION, function()
          selectionIndexes = {}
          UpdateSelection()
        end)
      end)
    else
      local index = tIndexOf(widgets, foci[1])
      ApplyIndex(index)
    end
  end
  local function ForceSelection(rawFoci)
    local foci = tFilter(rawFoci, function(w) return w:GetParent() == preview end, true)
    local any = false
    for _, w in ipairs(foci) do
      local index = tIndexOf(widgets, w)
      if tIndexOf(selectionIndexes, index) ~= nil then
        any = true
      end
    end
    if not any then
      local index = tIndexOf(widgets, foci[1])
      selectionIndexes = {index}
      UpdateSelection()
    end
  end

  local function AlignForRelativePoints(offsets, snappingAmount)
    snappingAmount = snappingAmount or 3
    local snapped = true
    local iteration = 0
    -- Shift items around until we reach a happy medium where everything is approximately in the same
    -- relative place to the other items as it was before moving, complex because of allowing widgets to snap to center points
    while snapped and
      -- Capped iterations to a reasonably high number that it isn't expected to reach (unless dozens of items are selected)
      iteration < 200
      do

      iteration = iteration + 1
      snapped = false
      local snapX, snapY, xLock, yLock = 0, 0, nil, nil
      for indexIndex, index in ipairs(selectionIndexes) do
        local w = widgets[index]
        snapX, snapY, xLock, yLock = UpdateWidgetPoints(preview, w, snappingAmount, offsets[indexIndex].x, offsets[indexIndex].y)
        local o = offsets[indexIndex]
        if math.abs(snapX) >= pixelStep/2 and not o.xLock or math.abs(snapY) >= pixelStep/2 and not o.yLock then
          -- See UpdateWidgetPoints for usage of xLock/yLock
          o.xLock = o.xLock or xLock
          o.yLock = o.yLock or yLock
          if o.xLock then
            o.x = o.x + snapX
          end
          if o.yLock then
            o.y = o.y + snapY
          end
          snapped = true
          break
        else
          offsets[indexIndex] = {x = offsets[indexIndex].x + snapX, y = offsets[indexIndex].y + snapY}
        end
      end

      if snapped then
        for indexIndex, index in ipairs(selectionIndexes) do
          local o = offsets[indexIndex]
          if not o.xLock then
            o.x = o.x + snapX
          end
          if not o.yLock then
            o.y = o.y + snapY
          end
        end
      end
    end
  end

  local movingMonitor = CreateFrame("Frame")
  movingMonitor:RegisterEvent("GLOBAL_MOUSE_UP")
  local cursorX, cursorY = GetCursorPosition()
  local function NotifyMouseDown()
    fociOnDown = GetMouseFoci()
    cursorX, cursorY = GetCursorPosition()
    cursorX = cursorX / preview:GetEffectiveScale()
    cursorY = cursorY / preview:GetEffectiveScale()
  end
  local function StartMovingSelection()
    local backupSelectionIndexes = CopyTable(selectionIndexes)

    movingMonitor:SetScript("OnUpdate", function()
      local newCursorX, newCursorY = GetCursorPosition()
      newCursorX = newCursorX / preview:GetEffectiveScale()
      newCursorY = newCursorY / preview:GetEffectiveScale()
      for _, index in ipairs(backupSelectionIndexes) do
        local w = widgets[index]
        w:AdjustPointsOffset(newCursorX - cursorX, newCursorY - cursorY)
      end
      cursorX = newCursorX
      cursorY = newCursorY
    end)
    movingMonitor:SetScript("OnEvent", function()
      movingMonitor:SetScript("OnUpdate", nil)
      movingMonitor:SetScript("OnEvent", nil)
      selectionIndexes = backupSelectionIndexes
      local offsets = {}
      for _, index in ipairs(selectionIndexes) do
        table.insert(offsets, {x = 0, y = 0, xLock = false, yLock = false})
      end
      AlignForRelativePoints(offsets)
      Announce()
    end)
  end

  local function DeleteCurrentWidget()
    table.sort(selectionIndexes, function(a, b) return a > b end)
    for _, index in ipairs(selectionIndexes) do
      local kind = widgets[index].kind
      if kind == "regions" then
        widgets[index].details.autoSized = true
      else
        local details = widgets[index].details
        local design = addonTable.CustomiseDialog.GetCurrentDesign()
        table.remove(design[kind], tIndexOf(design[kind], details))

        -- Update hidden widget indexes
        local keys = GetKeysArray(hiddenIndexes)
        hiddenIndexes = {}
        for _, k in ipairs(keys) do
          if k > index then
            hiddenIndexes[k - 1] = true
          elseif k ~= index then
            hiddenIndexes[k] = true
          end
        end
      end
    end
    selectionIndexes = {}
    Announce()
  end

  local selectorPool = CreateFramePool("Frame", previewInset, nil, nil, false, GetSelectorMarker)
  local keyboardTrap = CreateFrame("Frame", nil, container)
  keyboardTrap:Hide()

  local hoverMarker = GetSelectorMarker(CreateFrame("Frame", nil, previewInset), true)

  local titleText = container:CreateFontString(nil, nil, "GameFontHighlightLarge")
  titleText:SetPoint("TOP", previewInset, "BOTTOM", 0, -15)
  titleText:SetJustifyH("RIGHT")
  titleText:SetPoint("RIGHT", -40, 0)
  titleText:SetShadowOffset(1, -1)

  local suggestionToClick = container:CreateFontString(nil, nil, "GameFontHighlightLarge")
  suggestionToClick:SetText(addonTable.Locales.CLICK_ON_A_WIDGET)
  suggestionToClick:SetPoint("TOP", previewInset, "BOTTOM", 0, -15)

  local function OffsetWidgets(x, y)
    local offsets = {}
    for _ in ipairs(selectionIndexes) do
      table.insert(offsets, {x = x, y = y, xLock = x == 0, yLock = y == 0})
    end
    AlignForRelativePoints(offsets, 0.4)
    Announce()
  end

  keyboardTrap:SetScript("OnKeyDown", function(_, key)
    keyboardTrap:SetPropagateKeyboardInput(false)
    local amount = pixelStep
    if IsShiftKeyDown() then
      amount = amount * 4
    end
    if key == "LEFT" then
      OffsetWidgets(-amount, 0)
    elseif key == "RIGHT" then
      OffsetWidgets(amount, 0)
    elseif key == "UP" then
      OffsetWidgets(0, amount)
    elseif key == "DOWN" then
      OffsetWidgets(0, -amount)
    elseif key == "DELETE" then
      DeleteCurrentWidget()
    else
      keyboardTrap:SetPropagateKeyboardInput(true)
    end
  end)
  keyboardTrap:RegisterEvent("PLAYER_REGEN_ENABLED")
  keyboardTrap:RegisterEvent("PLAYER_REGEN_DISABLED")
  keyboardTrap:SetScript("OnEvent", function(_, event)
    keyboardTrap:SetShown(event == "PLAYER_REGEN_ENABLED" and #selectionIndexes > 0)
  end)

  local addButton = CreateFrame("DropdownButton", nil, previewInset, "UIPanelDynamicResizeButtonTemplate")
  addButton:SetText(addonTable.Locales.ADD)
  DynamicResizeButton_Resize(addButton)
  addButton:SetPoint("TOPLEFT", 1, -1)
  addButton:SetupMenu(function(menu, rootDescription)
    local design = addonTable.CustomiseDialog.GetCurrentDesign()
    for _, details in ipairs(addonTable.CustomiseDialog.DesignWidgets) do
      if details.special == "header" then
        rootDescription:CreateTitle(details.name)
      else
        local skip = false
        if details.noDuplicates then
          if design[details.kind][details.default.kind] then
            skip = true
          end
          for _, entry in ipairs(design[details.kind]) do
            if entry.kind == details.default.kind then
              skip = true
              break
            end
          end
        end
        if not skip then
          rootDescription:CreateButton(details.name, function()
            table.insert(design[details.kind], CopyTable(details.default))
            autoSelectedDetails = design[details.kind][#design[details.kind]]
            Announce()
          end)
        else
          rootDescription:CreateTitle(GRAY_FONT_COLOR:WrapTextInColorCode(details.name))
        end
      end
    end
    rootDescription:SetScrollMode(30 * 20)
  end)

  local deleteButton = CreateFrame("Button", nil, previewInset, "UIPanelDynamicResizeButtonTemplate")
  deleteButton:SetText(addonTable.Locales.DELETE)
  DynamicResizeButton_Resize(deleteButton)
  deleteButton:SetPoint("TOPRIGHT", -1, -1)
  deleteButton:SetScript("OnClick", function()
    DeleteCurrentWidget()
  end)

  local showAllButton = CreateFrame("Button", nil, previewInset, "UIPanelDynamicResizeButtonTemplate")
  showAllButton:SetText(addonTable.Locales.SHOW_ALL)
  DynamicResizeButton_Resize(showAllButton)
  showAllButton:SetPoint("TOP", 0, -1)
  showAllButton:SetScript("OnClick", function()
    hiddenIndexes = {}
    UpdateHiding()
  end)

  local showRegionsButton = CreateFrame("Button", nil, previewInset, "UIPanelDynamicResizeButtonTemplate")
  showRegionsButton:SetText(addonTable.Locales.REGIONS)
  DynamicResizeButton_Resize(showRegionsButton)
  showRegionsButton:SetPoint("BOTTOMLEFT", 1, 1)
  showRegionsButton:SetScript("OnClick", function()
    shouldShowRegions = not shouldShowRegions
    UpdateHiding()
    if not shouldShowRegions then
      addonTable.CallbackRegistry:TriggerEvent("ShowRegion", "click", false)
      addonTable.CallbackRegistry:TriggerEvent("ShowRegion", "stack", false)
    else
      addonTable.CallbackRegistry:TriggerEvent("ShowRegion", "click", addonTable.CustomiseDialog.GetCurrentDesign().regions.click.autoSized)
    end
  end)
  if not addonTable.Constants.IsHitTestPointsAvailable then
    showRegionsButton:Hide()
  end

  UpdateHiding = function()
    for index, w in ipairs(widgets) do
      w:SetShown(hiddenIndexes[index] ~= true and (w.kind ~= "regions" or shouldShowRegions))
    end
    showAllButton:SetShown(next(hiddenIndexes) ~= nil)
    DynamicResizeButton_Resize(showRegionsButton)
    local oldSize = #selectionIndexes
    selectionIndexes = tFilter(selectionIndexes, function(i) return widgets[i] and not hiddenIndexes[i] and (widgets[i].kind ~= "regions" or shouldShowRegions) end, true)
    if #selectionIndexes ~= oldSize then
      UpdateSelection()
    end
  end

  local auraContainers = {
    buffs = CreateFrame("Frame", nil, preview, "PlatynatorPropagateMouseTemplate"),
    debuffs = CreateFrame("Frame", nil, preview, "PlatynatorPropagateMouseTemplate"),
    crowdControl = CreateFrame("Frame", nil, preview, "PlatynatorPropagateMouseTemplate"),
  }
  do
    local textures = {
      buffs = {132117},
      debuffs = {135959},
      crowdControl = {135860},
    }
    local asset = LSM:Fetch("nineslice", "Platy: 1px")
    assert(asset)
    local dispelAsset = LSM:Fetch("nineslice", "Platy: 4px")
    local dispelColor = CreateColorFromHexString("fffc982f")
    assert(dispelAsset and dispelColor)
    for kind, w in pairs(auraContainers) do
      w:SetSize(10, 10)
      w.Wrapper = CreateFrame("Frame", nil, w)
      w.Wrapper:SetSize(10, 20)
      w.Wrapper:SetPoint("BOTTOMLEFT")
      w.count = #textures[kind]
      w.auras = {}
      for index, tex in ipairs(textures[kind]) do
        local buff = CreateFrame("Frame", nil, w.Wrapper, "PlatynatorNameplateBuffButtonTemplate")
        buff.Border = buff:CreateTexture(nil, "OVERLAY")
        buff.Border:SetAllPoints()
        buff.Border:SetScale(asset.scaleModifier)
        buff.Border:SetTexture(asset.file)
        buff.Border:SetTextureSliceMargins(asset.margins.left, asset.margins.top, asset.margins.right, asset.margins.bottom)
        buff.Border:SetVertexColor(0, 0, 0)
        buff.Dispel = CreateFrame("Frame", nil, buff)
        buff.Dispel:SetAllPoints()
        buff.DispelBorder = buff.Dispel:CreateTexture(nil, "OVERLAY")
        buff.DispelBorder:SetAllPoints()
        buff.DispelBorder:SetScale(dispelAsset.scaleModifier)
        buff.DispelBorder:SetTexture(dispelAsset.file)
        buff.DispelBorder:SetTextureSliceMargins(dispelAsset.margins.left, dispelAsset.margins.top, dispelAsset.margins.right, dispelAsset.margins.bottom)
        buff.DispelBorder:SetVertexColor(dispelColor.r, dispelColor.g, dispelColor.b)
        buff:Show()
        buff.Icon:SetTexture(tex)
        buff:SetPoint("LEFT", (index - 1) * 22, 0)
        table.insert(w.auras, buff)
      end
      w.kind = "auras"
      w:SetScript("OnEnter", function()
        if w == GetMouseFoci()[1] then
          hoverMarker:Show()
          hoverMarker:SetFrameStrata("HIGH")
          hoverMarker:ClearAllPoints()
          hoverMarker:SetPoint("TOPLEFT", w, "TOPLEFT", -2, 2)
          hoverMarker:SetPoint("BOTTOMRIGHT", w, "BOTTOMRIGHT", 2, -2)
        end
      end)
      w:SetScript("OnLeave", function()
        local foci = GetMouseFoci()
        if foci[1] and foci[1]:GetParent() == preview then
          foci[1]:GetScript("OnEnter")()
        else
          hoverMarker:Hide()
        end
      end)

      w:SetMovable(true)
      w:EnableMouse(true)
      w:RegisterForDrag("LeftButton")
      w:SetScript("OnMouseDown", function()
        NotifyMouseDown()
      end)
      w:SetScript("OnDragStart", function()
        ForceSelection(fociOnDown)
        StartMovingSelection()
      end)
      w:SetScript("OnMouseUp", function(_, button)
        ToggleSelection(GetMouseFoci(), button)
      end)
    end
  end

  local regionWidgets = {
    click = CreateFrame("Frame", nil, preview, "PlatynatorPropagateMouseTemplate"),
    stack = CreateFrame("Frame", nil, preview, "PlatynatorPropagateMouseTemplate"),
  }
  for _, w in pairs(regionWidgets) do
    w.kind = "regions"
    w.visual = w:CreateTexture()
    w.visual:SetAllPoints()
    w.border = w:CreateTexture()
    w.border:SetAllPoints()
    local borderSliceDetails = LSM:Fetch("nineslice", "Platy: 4px")
    assert(borderSliceDetails)
    w.border:SetTexture(borderSliceDetails.file)
    w.border:SetScale(borderSliceDetails.scaleModifier)
    w.border:SetTextureSliceMargins(borderSliceDetails.margins.left, borderSliceDetails.margins.top, borderSliceDetails.margins.right, borderSliceDetails.margins.bottom)
    w:SetScript("OnEnter", function()
      if w == GetMouseFoci()[1] then
        hoverMarker:Show()
        hoverMarker:SetFrameStrata("HIGH")
        hoverMarker:ClearAllPoints()
        hoverMarker:SetPoint("TOPLEFT", w, "TOPLEFT", -2, 2)
        hoverMarker:SetPoint("BOTTOMRIGHT", w, "BOTTOMRIGHT", 2, -2)
      end
    end)
    w:SetScript("OnLeave", function()
      local foci = GetMouseFoci()
      if foci[1] and foci[1]:GetParent() == preview then
        foci[1]:GetScript("OnEnter")()
      else
        hoverMarker:Hide()
      end
    end)
    w:SetMovable(true)
    w:EnableMouse(true)
    w:RegisterForDrag("LeftButton")
    w:SetScript("OnMouseDown", function()
      NotifyMouseDown()
    end)
    w:SetScript("OnDragStart", function()
      ForceSelection(fociOnDown)
      w.details.autoSized = false
      StartMovingSelection()
    end)
    w:SetScript("OnMouseUp", function(_, button)
      ToggleSelection(GetMouseFoci(), button)
    end)
  end
  regionWidgets.click.visual:SetColorTexture(addonTable.Constants.ClickRegionColor.r, addonTable.Constants.ClickRegionColor.g, addonTable.Constants.ClickRegionColor.b, addonTable.Constants.ClickRegionColor.a)
  regionWidgets.click.border:SetVertexColor(addonTable.Constants.ClickRegionBorderColor.r, addonTable.Constants.ClickRegionBorderColor.g, addonTable.Constants.ClickRegionBorderColor.b)
  regionWidgets.stack.visual:SetColorTexture(addonTable.Constants.StackRegionColor.r, addonTable.Constants.StackRegionColor.g, addonTable.Constants.StackRegionColor.b, addonTable.Constants.StackRegionColor.a)
  regionWidgets.stack.border:SetVertexColor(addonTable.Constants.StackRegionBorderColor.r, addonTable.Constants.StackRegionBorderColor.g, addonTable.Constants.StackRegionBorderColor.b)

  GenerateWidgets = function ()
    if widgets then
      addonTable.Display.ReleaseWidgets(tFilter(widgets, function(w) return w.Strip end, true), true)
    end
    local design = addonTable.CustomiseDialog.GetCurrentDesign()
    widgets = addonTable.Display.GetWidgets(design, preview, true)
    for _, w in ipairs(widgets) do
      w:SetClampedToScreen(true)
      if w.kind == "bars" then
        local defaultColor
        if w.details.kind == "health" then
          for _, s in ipairs(w.details.autoColors) do
            if s.kind == "threat" then
              defaultColor = s.colors.warning
              break
            elseif s.kind == "reaction" then
              defaultColor = s.colors.hostile
              break
            end
          end
        elseif w.details.kind == "cast" then
          defaultColor = w.details.autoColors[#w.details.autoColors].colors.cast
        elseif w.details.kind == "energy" then
          defaultColor = w.details.autoColors[#w.details.autoColors].colors.energy
        end
        w.statusBar:SetMinMaxValues(0, 100)
        w.statusBar:SetValue(70)
        if w.details.kind == "cast" then
          if w.details.interruptMarker.asset ~= "none" then
            w.interruptPositioner:SetMinMaxValues(0, 100)
            w.interruptPositioner:SetValue(0)
            w.interruptMarker:Show()
            w.interruptMarker:SetMinMaxValues(0, 100)
            w.interruptMarker:SetValue(80)
          else
            w.interruptMarker:Hide()
          end
        end
        w.statusBar:GetStatusBarTexture():SetVertexColor(defaultColor.r, defaultColor.g, defaultColor.b)
        if w.details.background.applyColor then
          local mod = w.details.background.color
          w.background:SetVertexColor(defaultColor.r * mod.r, defaultColor.g * mod.g, defaultColor.b * mod.b, mod.a)
        end
        if w.details.kind == "health" then
          w.statusBarAbsorb:SetMinMaxValues(0, 100)
          w.statusBarAbsorb:SetValue(10)
        end
        w.marker:SetVertexColor(defaultColor.r, defaultColor.g, defaultColor.b)
      elseif w.kind == "texts" then
        local display
        if w.details.kind == "health" then
          local types = w.details.displayTypes
          local percentTail = w.details.showPercentSymbol and "%" or ""
          local values = {
            absolute = AbbreviateNumbers(71255),
            percentage = "71" .. percentTail
          }
          if w.details.significantFigures > 0 then
            values.percentage = (w.abbreviateCallback and w.abbreviateCallback(71.255) or w.abbreviateData and AbbreviateNumbers(71.255, w.abbreviateData)) .. percentTail
          end
          if #types == 2 then
            display = string.format(w.details.formatMultiple, values[types[1]], values[types[2]])
          elseif #types == 1 then
            display = string.format("%s", values[types[1]])
          else
            display = addonTable.Locales.NO_VALUE_UPPER
          end
        elseif w.details.kind == "damageAbsorb" then
          w.text:SetText("+" .. AbbreviateNumbers(10290))
        elseif w.details.kind == "creatureName" or w.details.kind == "target" or w.details.kind == "castTarget" or w.details.kind == "castInterrupter" then
          display = "Cheesanator" .. (w.details.kind ~= "creatureName" and "2?" or "")
          if w.details.applyClassColors then
            local c = RAID_CLASS_COLORS["MAGE"]
            w.text:SetTextColor(c.r, c.g, c.b)
          elseif w.details.autoColors then
            for _, s in ipairs(w.details.autoColors) do
              if s.kind == "classColors" then
                local c = RAID_CLASS_COLORS["MAGE"]
                w.text:SetTextColor(c.r, c.g, c.b)
                break
              elseif s.kind == "threat" then
                local c = s.colors.warning
                w.text:SetTextColor(c.r, c.g, c.b)
                break
              elseif s.kind == "reaction" then
                local c = s.colors.hostile
                w.text:SetTextColor(c.r, c.g, c.b)
                break
              end
            end
          end
        elseif w.details.kind == "castTimeLeft" then
          display = "1.2"
        elseif w.details.kind == "guild" then
          display = "Surge of Awesome"
        elseif w.details.kind == "castSpellName" then
          display = addonTable.Locales.ARCANE_FLURRY
        elseif w.details.kind == "level" then
          display = "60"
        elseif w.details.kind == "quest" then
          display = "3/7"
        elseif w.details.kind == "mythicPlusForces" then
          local types = w.details.displayTypes
          local percentTail = w.details.showPercentSymbol and "%" or ""
          local values = {
            absolute = 16,
            percentage = "2.74" .. percentTail
          }
          if #types == 2 then
            display = string.format(w.details.formatMultiple, values[types[1]], values[types[2]])
          elseif #types == 1 then
            display = string.format("%s", values[types[1]])
          else
            display = addonTable.Locales.NO_VALUE_UPPER
          end
        elseif w.details.kind == "energy" then
          display = "50"
          if w.details.showPercentSymbol then
            display = display .. "%"
          end
        end
        if display then
          w.text:SetText(display)
          local width, height = w.text:GetSize()
          w:SetSize(width*w.text:GetScale(), height*w.text:GetScale())
        end

      elseif w.kind == "specialBars" and w.details.kind == "power" then
        local points = {}
        local color = CreateColor(234/255, 61/255, 247/255)
        for i = 1, 6 do
          table.insert(points, {set = i <= 4, color = color})
        end
        w:SetValue(points)
      elseif w.kind == "specialBars" and w.details.kind == "healthFillText" then
        local text = "Cheesanator"
        w.background:SetText(text)
        w.foreground:SetText(text)
        w.absorb:SetText(text)
        w.absorbPlacer:SetText(text)
        w.statusBar:SetMinMaxValues(0, 100)
        w.statusBar:SetValue(70)
        w.statusBarAbsorb:SetMinMaxValues(0, 100)
        w.statusBarAbsorb:SetValue(10)
        local width, height = w.foreground:GetSize()
        w:SetSize(width*w.foreground:GetScale(), height*w.foreground:GetScale())
        local defaultColor
        for _, s in ipairs(w.details.autoColors) do
          if s.kind == "classColors" then
            defaultColor = RAID_CLASS_COLORS["MAGE"]
            break
          elseif s.kind == "threat" then
            defaultColor = s.colors.warning
            break
          elseif s.kind == "reaction" then
            defaultColor = s.colors.hostile
            break
          end
        end
        w.foreground:SetTextColor(defaultColor.r, defaultColor.g, defaultColor.b)
        if w.details.background.applyColor then
          local mod = w.details.background.color
          w.background:SetTextColor(defaultColor.r * mod.r, defaultColor.g * mod.g, defaultColor.b * mod.b, mod.a)
        end
      elseif w.kind == "markers" then
        local asset = addonTable.Assets.Markers[w.details.asset]
        if asset.preview then
          w.marker:SetTexture(asset.preview)
        end
        if w.details.kind == "castIcon" and w.details.square then
          w.background:Show()
        end
      elseif w.kind == "highlights" then
        w:Show()
      end

      w:SetScript("OnEnter", function()
        if w == GetMouseFoci()[1] then
          hoverMarker:Show()
          hoverMarker:SetFrameStrata("HIGH")
          hoverMarker:ClearAllPoints()
          hoverMarker:SetPoint("TOPLEFT", w, "TOPLEFT", -2, 2)
          hoverMarker:SetPoint("BOTTOMRIGHT", w, "BOTTOMRIGHT", 2, -2)
        end
      end)
      w:SetScript("OnLeave", function()
        local foci = GetMouseFoci()
        if foci[1] and foci[1]:GetParent() == preview then
          foci[1]:GetScript("OnEnter")()
        else
          hoverMarker:Hide()
        end
      end)

      w:SetMovable(true)
      w:EnableMouse(true)
      w:RegisterForDrag("LeftButton")
      w:SetScript("OnMouseDown", function()
        NotifyMouseDown()
      end)
      w:SetScript("OnDragStart", function()
        ForceSelection(fociOnDown)
        StartMovingSelection()
      end)
      w:SetScript("OnMouseUp", function(_, button)
        ToggleSelection(GetMouseFoci(), button)
      end)
    end
    for _, container in pairs(auraContainers) do
      container:Hide()
    end
    for _, details in ipairs(design.auras) do
      local container = auraContainers[details.kind]
      container:Show()
      if details.kind == "debuffs" then
        container:SetFrameLevel(addonTable.Constants.LayerFrameLevelStep * details.layer + 450)
      elseif details.kind == "buffs" then
        container:SetFrameLevel(addonTable.Constants.LayerFrameLevelStep * details.layer + 450 + 10)
      else
        container:SetFrameLevel(addonTable.Constants.LayerFrameLevelStep * details.layer + 450 + 20)
      end
      local cdText = container.auras[1].Cooldown:GetRegions()
      cdText:SetFontObject(addonTable.CurrentFont)
      if addonTable.CurrentFontUsesSmoothing then
        cdText:SetTextScale(1)
        cdText:SetScale(details.texts.countdown.scale)
      else
        cdText:SetTextScale(details.texts.countdown.scale)
        cdText:SetScale(1)
      end
      cdText:SetTextColor(details.texts.countdown.color.r, details.texts.countdown.color.g, details.texts.countdown.color.b)
      if cdText.SetSmoothScaling then
        cdText:SetSmoothScaling(addonTable.CurrentFontUsesSmoothing)
      end
      container.auras[1].Cooldown:SetCooldown(GetTime() - 2.1, 5)
      container.auras[1].Cooldown:Pause()
      if container.auras[1].Cooldown.SetCountdownFormatter then
        container.auras[1].Cooldown:SetCountdownFormatter(details.texts.countdown.showFractions and addonTable.Display.Utilities.GetAuraNumericFormatter() or nil)
      end
      container.auras[1].Cooldown:SetHideCountdownNumbers(not details.texts.countdown.visible)
      container.auras[1].Cooldown:SetDrawSwipe(details.showSwipe)
      container.auras[1].Cooldown:SetDrawEdge(details.showSwipe)
      addonTable.Display.ApplyAnchor(cdText, details.texts.countdown.anchor, addonTable.CurrentFontUsesSmoothing and 1/details.texts.countdown.scale or 1)
      container.auras[1].CountFrame.Count:SetText(2)
      container.auras[1].CountFrame.Count:SetFontObject(addonTable.CurrentFont)
      container.auras[1].CountFrame.Count:SetShown(details.texts.stacks.visible)
      if container.auras[1].CountFrame.Count.SetSmoothScaling then
        container.auras[1].CountFrame.Count:SetSmoothScaling(addonTable.CurrentFontUsesSmoothing)
      end
      if addonTable.CurrentFontUsesSmoothing then
        container.auras[1].CountFrame.Count:SetTextScale(1)
        container.auras[1].CountFrame.Count:SetScale(details.texts.stacks.scale)
      else
        container.auras[1].CountFrame.Count:SetTextScale(details.texts.stacks.scale)
        container.auras[1].CountFrame.Count:SetScale(1)
      end
      addonTable.Display.ApplyAnchor(container.auras[1].CountFrame.Count, details.texts.stacks.anchor, addonTable.CurrentFontUsesSmoothing and 1/details.texts.stacks.scale or 1)
      container.auras[1].CountFrame.Count:SetTextColor(details.texts.stacks.color.r, details.texts.stacks.color.g, details.texts.stacks.color.b)
      container.auras[1].DispelBorder:SetShown(details.showType)
      container:SetSize(22 * container.count * details.scale, 20 * details.height * details.scale)
      container.Wrapper:SetHeight(20 * details.height)
      container:SetSize(20 * details.scale, 20 * details.height * details.scale)
      container.Wrapper:SetScale(details.scale)
      local anchor = details.anchor[1] or "CENTER"
      container.Wrapper:SetPoint(anchor)
      container.details = details
      local texBase = 0.95 * (1 - details.height) / 2
      for index, aura in ipairs(container.auras) do
        aura:SetHeight(20 * details.height)
        aura.Icon:SetHeight(20 * details.height)
        aura.Border:SetHeight(20 * details.height)
        aura.Icon:SetTexCoord(0.05, 0.95, 0.05 + texBase, 0.95 - texBase)
        aura:SetPoint(anchor, (index - 1) * 22, 0)
      end
      table.insert(widgets, container)
      container:ClearAllPoints()
      addonTable.Display.ApplyAnchor(container, details.anchor)
    end

    for _, key in pairs({"click", "stack"}) do
      local details = design.regions[key]
      local w = regionWidgets[key]
      w.details = details
      w.details.kind = key
      w:SetSize(details.width * addonTable.Assets.BarBordersSize.width, details.height * addonTable.Assets.BarBordersSize.height)
      w:SetFrameLevel(9000 + 10 * (key == "click" and 1 or 0))
      addonTable.Display.ApplyAnchor(w, details.anchor)
      w:SetShown(shouldShowRegions)
      table.insert(widgets, w)
    end

    UpdateHiding()
  end

  GenerateWidgets()

  local previousDesign = addonTable.Config.Get(addonTable.Config.Options.STYLE)

  addonTable.CallbackRegistry:RegisterCallback("RefreshStateChange", function(_, state)
    if state[addonTable.Constants.RefreshReason.Design] then
      local currentDesign = addonTable.Config.Get(addonTable.Config.Options.STYLE)
      if previousDesign ~= currentDesign and (not previousDesign:match("^_") or currentDesign ~= addonTable.Constants.CustomName) then
        hiddenIndexes = {}
      end
      local design = addonTable.CustomiseDialog.GetCurrentDesign()
      addonTable.CurrentFont = addonTable.Core.GetFontByDesign(design)
      designScale:SetValue(design.scale * 100)
      GenerateWidgets()
      if autoSelectedDetails then
        for index, w in ipairs(widgets) do
          if w.details == autoSelectedDetails then
            selectionIndexes = {index}
            break
          end
        end
        autoSelectedDetails = nil
      end
      UpdateSelection()
    end
  end)

  table.insert(allFrames, previewInset)

  local settingsFrames = {}

  local function Generate()
    local function AddToTab(tab, entries, kind)
      local parent, yOffset = nil, -40
      if kind == "*" then
        parent = tab
      else
        parent = CreateFrame("Frame", nil, tab)
        if tab.lastOption then
          parent:SetPoint("TOP", tab.lastOption, "BOTTOM", 0, -30)
        else
          parent:SetPoint("TOP", 0, yOffset)
        end
        yOffset = 0
        parent:SetPoint("LEFT")
        parent:SetPoint("RIGHT")
        parent:SetHeight(350)
        parent:Hide()
        tab.kindSpecificSettings[kind] = parent
      end

      local allFrames = GenerateOptions(parent, yOffset, 0, entries)

      if parent == tab then
        tab.lastOption = allFrames[#allFrames]
      end

      function parent:UpdateOptions(details)
        parent.details = details
        for _, f in ipairs(allFrames) do
          if f.getInitData then
            f:Init(f.getInitData(details))
          end
          f:SetValue(f.Getter())
        end
      end
    end
    for kind, details in pairs(addonTable.CustomiseDialog.WidgetsConfig) do
      local settingsContainer = CreateFrame("Frame", nil, container)
      settingsContainer:SetPoint("TOP", previewInset, "BOTTOM")
      settingsContainer:SetPoint("LEFT")
      settingsContainer:SetPoint("RIGHT")
      settingsContainer:SetHeight(300)
      settingsContainer:Hide()
      table.insert(settingsFrames, settingsContainer)

      local tabs = {}
      local tabMap = {}
      local function InitTab(tab, tabButton, label)
        tab:SetPoint("LEFT")
        tab:SetPoint("RIGHT")
        tab:SetHeight(300)
        tab.button = tabButton
        tabButton.kind = label
        tabButton.label = label
        tabButton:SetScript("OnClick", function()
          tabButton:GetParent():SetTab(tabButton.label)
        end)
        tab.kindSpecificSettings = {}

        tabMap[label] = tab
        table.insert(tabs, tab)
        function tab:Set(details)
          if tab.UpdateOptions then
            tab:UpdateOptions(details)
          end
          for subKind, nestedContainer in pairs(self.kindSpecificSettings) do
            if subKind == details.kind then
              nestedContainer.details = nil
              nestedContainer:Show()
              nestedContainer:UpdateOptions(details)
            else
              nestedContainer:Hide()
            end
          end
        end
        function tab:IsFor(details)
          if tab.UpdateOptions then
            return true
          end
          for subKind, nestedContainer in pairs(self.kindSpecificSettings) do
            if subKind == details.kind then
              return true
            end
          end
          return false
        end
      end

      function settingsContainer:IsFor(newKind, details)
        return newKind == kind
      end

      settingsContainer.tabIndex = 1
      local tabManager = CreateFrame("Frame", nil, settingsContainer)
      tabManager:SetPoint("TOP", 0, -5)
      tabManager:SetPoint("LEFT")
      tabManager:SetPoint("RIGHT")
      tabManager:SetHeight(30)
      function tabManager:SetTab(label)
        local currentTab
        local visibleTab
        for index, t in ipairs(tabs) do
          if t.button.label ~= label then
            PanelTemplates_DeselectTab(t.button)
            t:Hide()
          else
            currentTab = t
            settingsContainer.tabIndex = index
          end
          if t.button:IsShown() and not visibleTab then
            visibleTab = t
          end
        end
        if not currentTab.button:IsShown() then
          currentTab:Hide()
          PanelTemplates_DeselectTab(currentTab.button)
          currentTab = visibleTab
        end
        PanelTemplates_SelectTab(currentTab.button)
        currentTab.details = nil
        currentTab:Show()
        currentTab:Set(settingsContainer.details)
      end
      function settingsContainer:Set(details)
        settingsContainer.details = details
        local lastTab
        for index, t in ipairs(tabs) do
          local tabButton = t.button
          if t:IsFor(details) then
            tabButton:Show()
            if not lastTab then
              tabButton:SetPoint("TOPLEFT", 20, 0)
            else
              tabButton:SetPoint("TOPLEFT", lastTab, "TOPRIGHT", 5, 0)
            end
            lastTab = t.button
          else
            tabButton:Hide()
          end
        end
        titleText:SetPoint("LEFT", lastTab, "RIGHT", 10, 0)
        tabManager:SetTab(tabs[settingsContainer.tabIndex].button.label)
      end
      if details["*"] then
        for _, tabDetails in ipairs(details["*"]) do
          local tabContainer = CreateFrame("Frame", nil, settingsContainer)
          local tabButton = addonTable.CustomiseDialog.Components.GetTab(tabManager, tabDetails.label)
          InitTab(tabContainer, tabButton, tabDetails.label)
          if #tabDetails.entries > 0 then
            AddToTab(tabContainer, tabDetails.entries, "*")
          end
        end
      end
      for key in pairs(details) do
        if key ~= "*" then
          for _, tabDetails in ipairs(details[key]) do
            if not tabMap[tabDetails.label] then
              local tabContainer = CreateFrame("Frame", nil, settingsContainer)
              local tabButton = addonTable.CustomiseDialog.Components.GetTab(tabManager, tabDetails.label)
              InitTab(tabContainer, tabButton, tabDetails.label)
            end
            AddToTab(tabMap[tabDetails.label], tabDetails.entries, key)
          end
        end
      end

      tabManager.Tabs = {}
      for _, tabContainer in ipairs(tabs) do
        table.insert(tabManager.Tabs, tabContainer.button)
      end
    end
  end

  Generate()

  UpdateSelection = function()
    selectionIndexes = tFilter(selectionIndexes, function(i) return i <= #widgets end, true)

    if shouldShowRegions then
      local stackSelected = tIndexOf(selectionIndexes, tIndexOf(widgets, regionWidgets.stack)) ~= nil
      local clickSelected = tIndexOf(selectionIndexes, tIndexOf(widgets, regionWidgets.click)) ~= nil
      addonTable.CallbackRegistry:TriggerEvent("ShowRegion", "click", regionWidgets.click.details.autoSized and not stackSelected or clickSelected)
      addonTable.CallbackRegistry:TriggerEvent("ShowRegion", "stack", stackSelected)
    end

    if #selectionIndexes == 0 then
      keyboardTrap:Hide()
      deleteButton:Disable()
      for _, frame in ipairs(settingsFrames) do
        frame:Hide()
      end
      selectorPool:ReleaseAll()
      titleText:SetText("")
      suggestionToClick:Show()
      return
    end
    suggestionToClick:Hide()
    deleteButton:Enable()

    if #selectionIndexes > 1 then
      titleText:SetText(addonTable.Locales.MULTIPLE_SELECTED)

      for _, frame in ipairs(settingsFrames) do
        frame:Hide()
      end
    else
      local w = widgets[selectionIndexes[1]]
      titleText:SetText(titleMap[w.kind] and titleMap[w.kind][w.details.kind] or UNKNOWN)

      if not isMaximized then
        for _, frame in ipairs(settingsFrames) do
          if not frame:IsFor(w.kind, w.details) then
            frame:Hide()
          end
        end

        for _, frame in ipairs(settingsFrames) do
          if frame:IsFor(w.kind, w.details) then
            frame.details = nil
            frame:Show()
            frame:Set(w.details)
          end
        end
      else
        for _, frame in ipairs(settingsFrames) do
          frame:Hide()
        end
      end
    end

    selectorPool:ReleaseAll()
    for _, index in ipairs(selectionIndexes) do
      local w = widgets[index]
      local selector = selectorPool:Acquire()
      selector:Show()
      selector:SetFrameStrata("HIGH")
      selector:SetPoint("TOPLEFT", w, "TOPLEFT", -2, 2)
      selector:SetPoint("BOTTOMRIGHT", w, "BOTTOMRIGHT", 2, -2)
    end

    keyboardTrap:SetShown(not InCombatLockdown())
  end

  container:SetScript("OnShow", function()
    for _, f in ipairs(allFrames) do
      if f.SetValue and f.scale then
        f:SetValue(addonTable.Config.Get(f.option) * f.scale)
      elseif f.SetValue and f.option then
        f:SetValue(addonTable.Config.Get(f.option))
      elseif f.SetValue and not f.noAuto then
        f:SetValue()
      end
    end

    designScale:SetValue(addonTable.CustomiseDialog.GetCurrentDesign().scale * 100)

    selectionIndexes = {}
    SetPreviewMaximised(false)
  end)
  container:SetScript("OnHide", function()
    shouldShowRegions = false
    addonTable.CallbackRegistry:TriggerEvent("ShowRegion", "click", false)
    addonTable.CallbackRegistry:TriggerEvent("ShowRegion", "stack", false)
  end)

  return container
end
