--[[
  writes to options field of aura data, which is then read to construct the user config panel

  Values in aura data:
  authorOptions -> array of options.
  config -> key/value hash table.
    key, and format of value are defined by author, and the precise value is defined by the user.
    This table gets copied into the aura's script environment via aura_env.config.
  authorMode -> bool, used to determine if author or user mode is displayed in Custom Options tab.

  option -> table with fields:
    type (required) -> string such as "toggle", "slider", "string", "number", "color", etc.
    key (required) -> string which custom scripts can use to read the selected option
    default (required) -> default value of the option
    name (required) -> displayed name in the user config panel
    width (required) -> number between 0.1 and 2 (softMin of 0.5). Determines the width of the option.
    useDesc (optional) -> bool. If false, then the tooltip will not be used.
    desc (optional) -> string to be displayed in the option tooltip
  When options are merged together (i.e. when the user multiselects and then opens the custom options tab), there is one additional field:
    references -> childID <=> optionID map, used to dereference to the proper option table in setters
  Supported option types, and additional fields that each type supports/requires:
    group -> represents a group of options.
      useCollapse (optional) -> if true, then group will have a collapsible header in user mode.
      collapse (optional) -> whether or not the collapsible header begins collapsed when the user begins a session.
      subOptions (required) -> array of options
      groupType (required) -> type of group:
        simple -> group is for organizational purposes only.
                  config value is a sub config
        array -> group represents an array of entries from the user, with similar information between them:
                 config value is arranged as an array of sub configs, one for each entry in the array.
          limitType (required) -> Specifies if user can add or remove entries from the array freely
          size (optional) -> required if the limitType is not "none".
    description -> dummy option which can be used to display some text. Not interactive, and so key/default/name are not set or required.
      text (required) -> text displayed on the panel
      fontSize (optional) -> fontSize. Default is medium.
    space -> dummy option which acts as a spacer. Not interactive, and so key/default/name are not set or required.
      useWidth (required) -> bool. If false, then the space is given full width in AceConfig. Else, option.width is used.
      useHeight (required) -> bool. If false, then the space covers only the line it renders on. Else, it covers the number of lines specified.
      height (optional) -> number. Height of space, in lines.
    separator -> AceConfig header widget.
      useName (required) -> bool. If true, then option.text is used as the name.
      test (optional) -> string. Text to be shown on the header widget.
    input -> text field which the user can input a string.
      length (optional) -> allowed length of the string. If set, then input longer than the allowed length will be trimmed
    number -> text field which the user can type in a number. Input is converted to a number value.
      max (optional) -> maximum allowed value. If set, then input greater than the maximum will be clamped
      min (optional) -> minimum allowed value. If set, then input lesser than the minimum will be clamped
      step (optional) -> stepsize
    select -> dropdown menu with author-specified strings to select.
      values (required) -> array of strings to select. config value will be the index corresponding to the string.
    toggle -> checkbutton which can be in a state of true or false, corresponding to checked and unchecked
    color -> color selector. Color is delivered as an {r, g, b, a} array.
    range -> slider element which allows the user to select a number value.
      max (optional) -> maximum allowed value. If set, then input greater than the maximum will be clamped
      min (optional) -> minimum allowed value. If set, then input lesser than the minimum will be clamped
      softmax (optional) -> Like max, but the manual entry will accept values up to the softmax.
      softmin (optional) -> Like min, but the manual entry will accept values down to the softmin.
      bigStep (optional) -> step size of the slider. Defaults to 0.05
      step (optional) -> like bigStep, but applies to number input as well
]]
if not WeakAuras.IsLibsOK() then return end
---@type string
local AddonName = ...
---@class OptionsPrivate
local OptionsPrivate = select(2, ...)

---@class WeakAuras
local WeakAuras = WeakAuras
local L = WeakAuras.L

local tinsert, tremove, tconcat = table.insert, table.remove, table.concat
local conflictBlue = "|cFF4080FF"
local conflict = {} -- magic value

local function atLeastOneSet(references, key)
  for _, optionData in pairs(references) do
    local childOption = optionData.options[optionData.index]
    if childOption[key] ~= nil then
      return true
    end
  end
end

local function neq(a, b)
  if type(a) == "table" and type(b) == "table" then
    for k, v in pairs(a) do
      if neq(v, b[k]) then
        return true
      end
    end
    for k, v in pairs(b) do
      if neq(v, a[k]) then
        return true
      end
    end
  else
    return a ~= b
  end
end

-- blues the name if there are conflicts between the references for this value
local function name(option, key, name, phrase)
  local header = name or phrase
  if option[key] ~= nil or not atLeastOneSet(option.references, key) then
    return header
  else
    return conflictBlue .. header
  end
end

-- blue if at least one member of the group does not have an option in the references
local function nameHead(data, option, phrase)
  if not data.controlledChildren then
    return phrase
  else
    for child in OptionsPrivate.Private.TraverseLeafs(data) do
      if not option.references[child.id] then
        return conflictBlue .. phrase
      end
    end
  end
  return phrase
end

local function nameUser(option)
  local firstValue
  for id, optionData in pairs(option.references) do
    local childConfig = optionData.config
    if not childConfig then
      return option.name
    elseif firstValue == nil then
      firstValue = childConfig[option.key]
    elseif neq(firstValue, childConfig[option.key]) then
      return conflictBlue .. option.name
    end
  end
  return option.name
end

local function nameUserDesc(option)
  if option.text then
    return option.text
  else
    local text = {}
    for id, optionData in pairs(option.references) do
      local childOption = optionData.options[optionData.index]
      if childOption.text and childOption.text ~= nil then
        tinsert(text, childOption.text)
      end
    end
    return conflictBlue .. tconcat(text, "\n")
  end
end

local function nameArray(option, array, index, phrase)
  local value
  for id, optionData in pairs(option.references) do
    local childOption = optionData.options[optionData.index]
    if not childOption[array] then
      return conflictBlue .. phrase
    elseif childOption[array][index] == nil then
      return conflictBlue .. phrase
    elseif value == nil then
      value = childOption[array][index]
    elseif value ~= childOption[array][index] then
      return conflictBlue .. phrase
    end
  end
  return phrase
end

-- provides a tooltip showing all the conflicting values if there are any
local function desc(option, key, phrase)
  if option[key] or not atLeastOneSet(option.references, key) then
    return phrase
  else
    local desc = {}
    if phrase then
      desc[1] = phrase
    end
    tinsert(desc, L["Values:"])
    for id, optionData in pairs(option.references) do
      local childOption = optionData.options[optionData.index]
      local childData = optionData.data
      if childOption[key] ~= nil then
        tinsert(
          desc,
          ("%s #%i: %s"):format(childData.id, optionData.path[#optionData.path], tostring(childOption[key]))
        )
      end
    end
    return tconcat(desc, "\n")
  end
end

local function descType(option)
  local desc = {
    L["This setting controls what widget is generated in user mode."],
    L["Used in Auras:"]
  }
  for id, optionData in pairs(option.references) do
    tinsert(desc, ("%s - Option %i"):format(id, optionData.path[#optionData.path]))
  end
  return tconcat(desc, "\n")
end

local function descSelect(option, key)
  if option.values then
    return ""
  else
    local desc = {L["Values:"]}
    for id, optionData in pairs(option.references) do
      local childOption = optionData.options[optionData.index]
      if childOption.values[key] ~= nil then
        tinsert(
          desc,
          ("%s %i: - %s"):format(id, optionData.path[#optionData.path], tostring(childOption.values[key]))
        )
      end
    end
    return tconcat(desc, "\n")
  end
end

local function descColor(option, key)
  if option[key] or not atLeastOneSet(option.references, key) then
    return L["Values are in normalized rgba format."]
  else
    local desc = {
      L["Values are in normalized rgba format."],
      L["Values:"]
    }
    for id, optionData in pairs(option.references) do
      local childOption = optionData.options[optionData.index]
      if childOption[key] ~= nil then
        tinsert(
          desc,
          ("%s #%i: %.2f %.2f %.2f %.2f"):format(
            id,
            childOption.path[#childOption.path],
            unpack(childOption[key])
          )
        )
      end
    end
    return tconcat(desc, "\n")
  end
end

local function descUser(option)
  if option.useDesc ~= nil and option.desc ~= nil then
    return option.useDesc and option.desc or nil
  else
    local desc = {}
    for id, optionData in pairs(option.references) do
      local childOption = optionData.options[optionData.index]
      if childOption.useDesc and childOption.desc and childOption.desc ~= "" then
        tinsert(desc, ("%s - %s"):format(id, childOption.desc))
      end
    end
    return tconcat(desc, "\n")
  end
end

local function descArray(option, array, index, phrase)
  local desc, values, isConflict = {phrase}, {}, false
  local initialValue = nil
  for id, optionData in pairs(option.references) do
    local childOption = optionData.options[optionData.index]
    values[id] = tostring(childOption[array][index])
    if initialValue == nil then
      initialValue = values[id]
    elseif values[id] ~= initialValue then
      isConflict = true
    end
  end
  if isConflict then
    for id, value in pairs(values) do
      tinsert(desc, ("%s - %s"):format(id,  value))
    end
  end
  return tconcat(desc, "\n")
end

-- getters for AceConfig
local function get(option, key)
  return function()
    return option[key]
  end
end

local function getUser(option)
  return function()
    local value
    for _, optionData in pairs(option.references) do
      if not optionData.config then
        return
      elseif value == nil then
        value = optionData.config[option.key]
      elseif neq(value, optionData.config[option.key]) then
        return
      end
    end
    return value
  end
end

local function getStr(option, key)
  return function()
    local str = option[key] or ""
    return str:gsub("|", "||")
  end
end

local function getNumAsString(option, key)
  return function()
    if option[key] ~= nil then
      return tostring(option[key])
    end
  end
end

local function getUserNumAsString(option)
  return function()
    local value
    for id, optionData in pairs(option.references) do
      if value == nil then
        value = optionData.config[option.key]
      elseif neq(value, optionData.config[option.key]) then
        return ""
      end
    end
    if value ~= nil then
      return tostring(value)
    end
  end
end

local function getValues(option)
  local values = {}
  local firstChild = true
  for _, optionData in pairs(option.references) do
    local childOption = optionData.options[optionData.index]
    local childValues = childOption.values
    local i = 1
    while i <= #values or i <= #childValues do
      if firstChild then
        values[i] = childValues[i]
      elseif values[i] ~= childValues[i] then
        values[i] = conflict
      end
      i = i + 1
    end
    firstChild = false
  end
  return values
end

local function getUserValues(option)
  local values = getValues(option)
  for i, v in ipairs(values) do
    if v == conflict then
      values[i] = conflictBlue .. L["Value %i"]:format(i)
    end
  end
  return values
end

local function getColor(option, key)
  return function()
    if option[key] then
      return unpack(option[key])
    end
  end
end

local function getUserColor(option)
  return function()
    local firstValue
    for id, optionData in pairs(option.references) do
      local childConfig = optionData.config
      if firstValue == nil then
        firstValue = childConfig[option.key]
      elseif neq(firstValue, childConfig[option.key]) then
        return
      end
      if firstValue then
        return unpack(firstValue)
      end
    end
  end
end

local function getArrayStr(option, array, index)
  return function()
    if option[array][index] then
      return option[array][index]:gsub("|","||")
    else
      return ""
    end
  end
end

-- setters for AceConfig
local function set(data, option, key)
  return function(_, value)
    for _, optionData in pairs(option.references) do
      local childOption = optionData.options[optionData.index]
      local childData = optionData.data
      childOption[key] = value
      WeakAuras.Add(childData)
    end
    WeakAuras.ClearAndUpdateOptions(data.id, true)
  end
end

local function setUser(data, option)
  return function(_, value)
    for _, optionData in pairs(option.references) do
      local childData = optionData.data
      local childConfig = optionData.config
      childConfig[option.key] = value
      WeakAuras.Add(childData)
    end
    WeakAuras.ClearAndUpdateOptions(data.id, true)
  end
end

local function setStr(data, option, key)
  return function(_, value)
    value = value:gsub("||", "|")
    for id, optionData in pairs(option.references) do
      local childOption = optionData.options[optionData.index]
      local childData = optionData.data
      childOption[key] = value
      WeakAuras.Add(childData)
    end
    WeakAuras.ClearAndUpdateOptions(data.id, true)
  end
end

local function setNum(data, option, key, required)
  return function(_, value)
    if value ~= "" then
      local num = tonumber(value)
      if not num or math.abs(num) == math.huge or tostring(num) == "nan" then
        return
      end
      for id, optionData in pairs(option.references) do
        local childOption = optionData.options[optionData.index]
        local childData = optionData.data
        childOption[key] = num
        WeakAuras.Add(childData)
      end
    elseif not required then
      for id, optionData in pairs(option.references) do
        local childOption = optionData.options[optionData.index]
        local childData = optionData.data
        childOption[key] = nil
        WeakAuras.Add(childData)
      end
    end
    WeakAuras.ClearAndUpdateOptions(data.id, true)
  end
end

local function setUserNum(data, option)
  return function(_, value)
    if value ~= "" then
      local num = tonumber(value)
      if not num or math.abs(num) == math.huge or tostring(num) == "nan" then return end
      for _, optionData in pairs(option.references) do
        local childData = optionData.data
        local childConfig = optionData.config
        childConfig[option.key] = num
        WeakAuras.Add(childData)
      end
      WeakAuras.ClearAndUpdateOptions(data.id, true)
    end
  end
end

local function setColor(data, option, key)
  return function(_, r, g, b, a)
    local color = {r, g, b, a}
    for id, optionData in pairs(option.references) do
      local childOption = optionData.options[optionData.index]
      local childData = optionData.data
      childOption[key] = color
      WeakAuras.Add(childData)
    end
    WeakAuras.ClearAndUpdateOptions(data.id, true)
  end
end

local function setUserColor(data, option)
  return function(_, r, g, b, a)
    local color = {r, g, b, a}
    for id, optionData in pairs(option.references) do
      local childData = optionData.data
      local childConfig = optionData.config
      childConfig[option.key] = color
      WeakAuras.Add(childData)
    end
    WeakAuras.ClearAndUpdateOptions(data.id, true)
  end
end

local function setSelectDefault(data, option, key)
  return function(_, value)
    for id, optionData in pairs(option.references) do
      local childOption = optionData.options[optionData.index]
      local childData = optionData.data
      childOption.default = min(value, #childOption.values)
      WeakAuras.Add(childData)
    end
    WeakAuras.ClearAndUpdateOptions(data.id, true)
  end
end

local function setArrayStr(data, option, array, index)
  return function(_, value)
    value = value:gsub("||","|")
    for id, optionData in pairs(option.references) do
      local childOption = optionData.options[optionData.index]
      local childData = optionData.data
      childOption[array][index] = value
      WeakAuras.Add(childData)
    end
    WeakAuras.ClearAndUpdateOptions(data.id, true)
  end
end

local function ensureUniqueKey(candidate, suffix, options, index)
  index = index or 1
  local goodKey = true
  local key = candidate
  local existingKeys = {}
  for _, option in ipairs(options) do
    if option.key then
      if option.key == key then
        goodKey = false
      end
      existingKeys[option.key] = true
    end
  end
  if not goodKey then
    local prefix = candidate .. suffix
    while not goodKey do
      key = prefix .. index
      goodKey = not existingKeys[key]
      index = index + 1
    end
  end
  return key
end

local function generateKey(prefix, options, index)
  return ensureUniqueKey(prefix, "", options, index)
end

local typeControlAdders, addAuthorModeOption
typeControlAdders = {
  toggle = function(options, args, data, order, prefix, i)
    local option = options[i]
    args[prefix .. "default"] = {
      type = "select",
      width = WeakAuras.normalWidth,
      name = name(option, "default", L["Default"]),
      desc = desc(option, "default"),
      order = order(),
      values = OptionsPrivate.Private.bool_types,
      get = function()
        if option.default == nil then
          return
        end
        return option.default and 1 or 0
      end,
      set = function(_, value)
        local val = value == 1
        for id, optionData in pairs(option.references) do
          local childOption = optionData.options[optionData.index]
          local childData = optionData.data
          childOption.default = val
          WeakAuras.Add(childData)
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end
    }
  end,
  input = function(options, args, data, order, prefix, i)
    local option = options[i]
    args[prefix .. "default"] = {
      type = "input",
      width = WeakAuras.normalWidth,
      name = name(option, "default", L["Default"]),
      desc = desc(option, "default"),
      order = order(),
      get = get(option, "default"),
      set = set(data, option, "default")
    }
    args[prefix .. "useLength"] = {
      type = "toggle",
      width = WeakAuras.normalWidth,
      name = name(option, "useLength", L["Max Length"]),
      desc = desc(option, "useLength"),
      order = order(),
      get = get(option, "useLength"),
      set = set(data, option, "useLength")
    }
    args[prefix .. "length"] = {
      type = "range",
      control = "WeakAurasSpinBox",
      width = WeakAuras.normalWidth,
      name = name(option, "length", L["Length"]),
      desc = desc(option, "length"),
      order = order(),
      min = 1,
      step = 1,
      softMax = 20,
      get = get(option, "length"),
      set = set(data, option, "length"),
      disabled = function()
        return not option.useLength
      end
    }
    args[prefix .. "multiline"] = {
      type = "toggle",
      width = WeakAuras.doubleWidth,
      name = name(option, "multiline", L["Large Input"]),
      desc = desc(option, "multiline", L["If checked, then the user will see a multi line edit box. This is useful for inputting large amounts of text."]),
      order = order(),
      get = get(option, "multiline"),
      set = set(data, option, "multiline"),
    }
  end,
  number = function(options, args, data, order, prefix, i)
    local option = options[i]
    args[prefix .. "default"] = {
      type = "input",
      width = WeakAuras.normalWidth,
      name = name(option, "default", L["Default"]),
      desc = desc(option, "default"),
      order = order(),
      get = getNumAsString(option, "default"),
      set = setNum(data, option, "default", true)
    }
    args[prefix .. "min"] = {
      type = "input",
      width = WeakAuras.normalWidth * 2 / 3,
      name = name(option, "min", L["Min"]),
      desc = desc(option, "min"),
      order = order(),
      get = getNumAsString(option, "min"),
      set = setNum(data, option, "min")
    }
    args[prefix .. "max"] = {
      type = "input",
      width = WeakAuras.normalWidth * 2 / 3,
      name = name(option, "max", L["Max"]),
      desc = desc(option, "min"),
      order = order(),
      get = getNumAsString(option, "max"),
      set = setNum(data, option, "max")
    }
    args[prefix .. "step"] = {
      type = "input",
      width = WeakAuras.normalWidth * 2 / 3,
      name = name(option, "step", L["Step Size"]),
      desc = desc(option, "step"),
      order = order(),
      get = getNumAsString(option, "step"),
      set = setNum(data, option, "step")
    }
  end,
  range = function(options, args, data, order, prefix, i)
    local option = options[i]
    local min, max, softMin, softMax, step, bigStep
    softMax = option.softMax
    softMin = option.softMin
    bigStep = option.bigStep
    min = option.min
    max = option.max
    local effectiveMin = softMin or min or 0
    local effectiveMax = softMax or max or 100
    if (effectiveMin > effectiveMax) then
      -- This will cause a error inside the slider
      -- Fix up either softMax or max, depending on which one is the effective one
      if softMax then
        softMax = effectiveMin
      elseif max then
        max = effectiveMin
      else
        softMax = effectiveMin
      end
    end
    step = option.step
    args[prefix .. "default"] = {
      type = "range",
      control = "WeakAurasSpinBox",
      width = WeakAuras.normalWidth,
      name = name(option, "default", L["Default"]),
      desc = desc(option, "default"),
      order = order(),
      get = get(option, "default"),
      set = set(data, option, "default"),
      min = min,
      max = max,
      step = step,
      softMin = softMin,
      softMax = softMax,
      bigStep = bigStep,
    }

    args[prefix .. "min"] = {
      type = "input",
      width = WeakAuras.normalWidth * 2 / 3,
      name = name(option, "min", L["Min"]),
      desc = desc(option, "min"),
      order = order(),
      get = getNumAsString(option, "min"),
      set = setNum(data, option, "min")
    }

    args[prefix .. "max"] = {
      type = "input",
      width = WeakAuras.normalWidth * 2 / 3,
      name = name(option, "max", L["Max"]),
      desc = desc(option, "max"),
      order = order(),
      get = getNumAsString(option, "max"),
      set = setNum(data, option, "max")
    }

    args[prefix .. "step"] = {
      type = "input",
      width = WeakAuras.normalWidth * 2 / 3,
      name = name(option, "step", L["Step Size"]),
      desc = desc(option, "step"),
      order = order(),
      get = getNumAsString(option, "step"),
      set = setNum(data, option, "step")
    }

    args[prefix .. "softmin"] = {
      type = "input",
      width = WeakAuras.normalWidth * 2 / 3,
      name = name(option, "softMin", L["Soft Min"]),
      desc = desc(option, "softMin"),
      order = order(),
      get = getNumAsString(option, "softMin"),
      set = setNum(data, option, "softMin")
    }

    args[prefix .. "softmax"] = {
      type = "input",
      width = WeakAuras.normalWidth * 2 / 3,
      name = name(option, "softMax", L["Soft Max"]),
      desc = desc(option, "softMax"),
      order = order(),
      get = getNumAsString(option, "softMax"),
      set = setNum(data, option, "softMax")
    }

    args[prefix .. "bigstep"] = {
      type = "input",
      width = WeakAuras.normalWidth * 2 / 3,
      name = name(option, "bigStep", L["Slider Step Size"]),
      desc = desc(option, "bigStep"),
      order = order(),
      get = getNumAsString(option, "bigStep"),
      set = setNum(data, option, "bigStep")
    }
  end,
  description = function(options, args, data, order, prefix, i)
    local option = options[i]
    args[prefix .. "key"] = nil
    args[prefix .. "name"] = nil
    args[prefix .. "fontsize"] = {
      type = "select",
      width = WeakAuras.normalWidth,
      name = name(option, "fontSize", L["Font Size"]),
      desc = desc(option, "fontSize"),
      order = order(),
      values = OptionsPrivate.Private.font_sizes,
      get = get(option, "fontSize"),
      set = set(data, option, "fontSize")
    }
    args[prefix .. "descinput"] = {
      type = "input",
      width = WeakAuras.doubleWidth,
      name = name(option, "text", L["Description Text"]),
      desc = desc(option, "text"),
      order = order(),
      multiline = true,
      get = getStr(option, "text"),
      set = setStr(data, option, "text")
    }
  end,
  color = function(options, args, data, order, prefix, i)
    local option = options[i]
    args[prefix .. "default"] = {
      type = "color",
      width = WeakAuras.normalWidth,
      hasAlpha = true,
      name = name(option, "default", L["Default"]),
      desc = descColor(option, "default"),
      order = order(),
      get = getColor(option, "default"),
      set = setColor(data, option, "default")
    }
  end,
  select = function(options, args, data, order, prefix, i)
    local option = options[i]
    local values = getValues(option)
    local defaultValues = {}
    for i, v in ipairs(values) do
      if v == conflict then
        defaultValues[i] = conflictBlue .. L["Value %i"]:format(i)
      else
        defaultValues[i] = v
      end
    end
    args[prefix .. "default"] = {
      type = "select",
      width = WeakAuras.normalWidth,
      name = name(option, "default", L["Default"]),
      desc = desc(option, "default"),
      order = order(),
      values = defaultValues,
      get = get(option, "default"),
      set = setSelectDefault(data, option)
    }
    for j, value in ipairs(values) do
      args[prefix .. "space" .. j] = {
        type = "toggle",
        width = WeakAuras.normalWidth,
        name = L["Value %i"]:format(j),
        order = order(),
        disabled = function()
          return true
        end,
        get = function()
          return true
        end,
        set = function()
        end
      }
      args[prefix .. "value" .. j] = {
        type = "input",
        width = WeakAuras.normalWidth - 0.15,
        name = (value == conflict and conflictBlue or "") .. L["Value %i"]:format(j),
        desc = descSelect(option, j),
        order = order(),
        get = function()
          if value ~= conflict then
            return value:gsub("|", "||")
          end
        end,
        set = function(_, value)
          value = value:gsub("||", "|")
          for id, optionData in pairs(option.references) do
            local childOption = optionData.options[optionData.index]
            local childData = optionData.data
            local insertPoint = math.min(j, #childOption.values + 1)
            if value == "" then
              tremove(childOption.values, insertPoint)
            else
              childOption.values[insertPoint] = value
            end
            WeakAuras.Add(childData)
          end
          WeakAuras.ClearAndUpdateOptions(data.id, true)
        end
      }
      args[prefix .. "valdelete" .. j] = {
        type = "execute",
        width = 0.15,
        name = L["Delete"],
        order = order(),
        func = function()
          for id, optionData in pairs(option.references) do
            local childOption = optionData.options[optionData.index]
            local childData = optionData.data
            tremove(childOption.values, j)
            WeakAuras.Add(childData)
          end
          WeakAuras.ClearAndUpdateOptions(data.id, true)
        end,
        image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\delete",
        imageWidth = 24,
        imageHeight = 24,
        control = "WeakAurasIcon"
      }
    end
    args[prefix .. "newvaluespace"] = {
      type = "toggle",
      width = WeakAuras.normalWidth,
      name = L["New Value"],
      order = order(),
      disabled = function()
        return true
      end,
      get = function()
        return true
      end,
      set = function()
      end
    }
    args[prefix .. "newvalue"] = {
      type = "input",
      width = WeakAuras.normalWidth,
      name = L["New Value"],
      order = order(),
      get = function()
        return ""
      end,
      set = function(_, value)
        value = value:gsub("||", "|")
        for id, optionData in pairs(option.references) do
          local childOption = optionData.options[optionData.index]
          local childData = optionData.data
          childOption.values[#childOption.values + 1] = value
          WeakAuras.Add(childData)
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end
    }
  end,
  space = function(options, args, data, order, prefix, i)
    local option = options[i]
    -- this option should be just useWidth but no need to do a migration in the data just for that.
    args[prefix .. "variableWidth"] = {
      type = "toggle",
      width = WeakAuras.normalWidth,
      order = order(),
      name = name(option, "variableWidth", L["Width"]),
      desc = desc(
        option,
        "variableWidth",
        L["If unchecked, then this space will fill the entire line it is on in User Mode."]
      ),
      get = get(option, "variableWidth"),
      set = set(data, option, "variableWidth")
    }
    args[prefix .. "widthSpace"] = nil

    local widthOption = args[prefix .. "width"]
    widthOption.name = name(option, "width", L["Width"])
    widthOption.disabled = function()
      return not option.variableWidth
    end
    widthOption.order = order()

    args[prefix .. "useHeight"] = {
      type = "toggle",
      width = WeakAuras.normalWidth,
      order = order(),
      name = name(option, "useHeight", L["Height"]),
      desc = desc(option, "useHeight", L["If checked, then this space will span across multiple lines."]),
      get = get(option, "useHeight"),
      set = set(data, option, "useHeight")
    }

    args[prefix .. "height"] = {
      type = "range",
      control = "WeakAurasSpinBox",
      width = WeakAuras.normalWidth,
      order = order(),
      name = name(option, "height", L["Height"]),
      desc = desc(option, "height"),
      get = get(option, "height"),
      set = set(data, option, "height"),
      disabled = function()
        return not option.useHeight
      end,
      min = 1,
      softMax = 10,
      step = 1
    }
  end,
  media = function(options, args, data, order, prefix, i)
    local option = options[i]
    args[prefix .. "mediaType"] = {
      type = "select",
      width = WeakAuras.normalWidth,
      name = name(option, "mediaType", L["Media Type"]),
      desc = desc(option, "mediaType"),
      values = OptionsPrivate.Private.shared_media_types,
      order = order(),
      get = get(option, "mediaType"),
      set = function(_, value)
        for _, optionData in pairs(option.references) do
          local childOption = optionData.options[optionData.index]
          local childData = optionData.data
          childOption.mediaType = value
          childOption.default = OptionsPrivate.Private.author_option_media_defaults[value]
          WeakAuras.Add(childData)
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end
    }
    args[prefix .. "default"] = {
      type = "select",
      width = WeakAuras.doubleWidth,
      name = name(option, "default", L["Default"]),
      desc = desc(option, "default"),
      values = function()
        if option.mediaType == "sound" then
          return OptionsPrivate.Private.sound_file_types
        else
          return AceGUIWidgetLSMlists[option.mediaType]
        end
      end,
      sorting = function()
        if option.mediaType == "sound" then
          return OptionsPrivate.Private.SortOrderForValues(OptionsPrivate.Private.sound_file_types)
        else
          return nil
        end
      end,
      dialogControl = OptionsPrivate.Private.author_option_media_controls[option.mediaType],
      itemControl = OptionsPrivate.Private.author_option_media_itemControls[option.mediaType],
      order = order(),
      get = get(option, "default"),
      set = function(_, value)
        if option.mediaType == "sound" then
          -- do this outside the deref loop, so we don't play the sound a million times
          PlaySoundFile(value, "Master")
        end
        for _, optionData in pairs(option.references) do
          local childOption = optionData.options[optionData.index]
          local childData = optionData.data
          childOption.default = value
          WeakAuras.Add(childData)
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end
    }
  end,
  multiselect = function(options, args, data, order, prefix, i)
    local option = options[i]
    local values = getValues(option)
    local defaultValues = {}
    for i, v in ipairs(values) do
      if v == conflict then
        defaultValues[i] = conflictBlue .. L["Value %i"]:format(i)
      else
        defaultValues[i] = v
      end
    end
    args[prefix .. "default"] = {
      type = "multiselect",
      width = WeakAuras.normalWidth * 0.9,
      name = L["Default"],
      order = order(),
      values = defaultValues,
      get = function(_, k)
        return option.default and option.default[k]
      end,
      set = function(_, k, v)
        for id, optionData in pairs(option.references) do
          local childOption = optionData.options[optionData.index]
          local childData = optionData.data
          childOption.default[k] = v
          WeakAuras.Add(childData)
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end
    }
    for j, value in ipairs(values) do
      args[prefix .. "space" .. j] = {
        type = "toggle",
        width = WeakAuras.normalWidth,
        name = L["Value %i"]:format(j),
        order = order(),
        disabled = function()
          return true
        end,
        get = function()
          return true
        end,
        set = function()
        end
      }
      args[prefix .. "value" .. j] = {
        type = "input",
        width = WeakAuras.normalWidth - 0.15,
        name = (value == conflict and conflictBlue or "") .. L["Value %i"]:format(j),
        desc = descSelect(option, j),
        order = order(),
        get = function()
          if value ~= conflict then
            return value:gsub("|", "||")
          end
        end,
        set = function(_, value)
          value = value:gsub("||", "|")
          for id, optionData in pairs(option.references) do
            local childOption = optionData.options[optionData.index]
            local childData = optionData.data
            local insertPoint = math.min(j, #childOption.values + 1)
            if value == "" then
              tremove(childOption.values, insertPoint)
              tremove(childOption.default, insertPoint)
            else
              childOption.values[insertPoint] = value
            end
            WeakAuras.Add(childData)
          end
          WeakAuras.ClearAndUpdateOptions(data.id, true)
        end
      }
      args[prefix .. "valdelete" .. j] = {
        type = "execute",
        width = 0.15,
        name = "",
        order = order(),
        func = function()
          for id, optionData in pairs(option.references) do
            local childOption = optionData.options[optionData.index]
            local childData = optionData.data
            tremove(childOption.values, j)
            tremove(childOption.default, j)
            WeakAuras.Add(childData)
          end
          WeakAuras.ClearAndUpdateOptions(data.id, true)
        end,
        image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\delete",
        imageWidth = 24,
        imageHeight = 24
      }
    end
    args[prefix .. "newvaluespace"] = {
      type = "toggle",
      width = WeakAuras.normalWidth,
      name = L["New Value"],
      order = order(),
      disabled = function()
        return true
      end,
      get = function()
        return true
      end,
      set = function()
      end
    }
    args[prefix .. "newvalue"] = {
      type = "input",
      width = WeakAuras.normalWidth,
      name = L["New Value"],
      order = order(),
      get = function()
        return ""
      end,
      set = function(_, value)
        value = value:gsub("||", "|")
        for id, optionData in pairs(option.references) do
          local childOption = optionData.options[optionData.index]
          local childData = optionData.data
          childOption.values[#childOption.values + 1] = value
          childOption.default[#childOption.default + 1] = false
          WeakAuras.Add(childData)
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end
    }
  end,
  header = function(options, args, data, order, prefix, i)
    local option = options[i]
    args[prefix .. "width"] = nil
    args[prefix .. "useName"] = {
      type = "toggle",
      name = name(option, "useName", L["Separator text"]),
      desc = desc(
        option,
        "useName",
        L["If checked, then this separator will include text. Otherwise, it will be just a horizontal line."]
      ),
      order = order(),
      width = WeakAuras.normalWidth,
      get = get(option, "useName"),
      set = set(data, option, "useName")
    }
    args[prefix .. "text"] = {
      type = "input",
      name = name(option, "text", L["Separator Text"]),
      desc = desc(option, "text"),
      order = order(),
      width = WeakAuras.normalWidth,
      get = getStr(option, "text"),
      set = setStr(data, option, "text"),
      disabled = function()
        return not option.useName
      end
    }
  end,
  group = function(options, args, data, order, prefix, i)
    local option = options[i]
    args[prefix .. "width"] = nil
    args[prefix .. "groupType"] = {
      type = "select",
      name = name(option, "groupType", L["Group Type"]),
      order = order(),
      width = WeakAuras.doubleWidth,
      values = OptionsPrivate.Private.group_option_types,
      get = get(option, "groupType"),
      set = function(_, value)
        for id, optionData in pairs(option.references) do
          local childOption = optionData.options[optionData.index]
          local childData = optionData.data
          childOption.groupType = value
          WeakAuras.Add(childData)
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end
    }
    args[prefix .. "useCollapse"] = {
      type = "toggle",
      name = name(option, "useCollapse", L["Collapsible Group"]),
      desc = desc(option, "useCollapse", L["If checked, then this option group can be temporarily collapsed by the user."]),
      order = order(),
      width = WeakAuras.normalWidth,
      get = get(option, "useCollapse"),
      set = set(data, option, "useCollapse"),
    }
    args[prefix .. "collapseDefault"] = {
      type = "toggle",
      name = name(option, "collapse", L["Start Collapsed"]),
      desc = desc(option, "collapse", L["If checked, then this option group will start collapsed."]),
      order = order(),
      width = WeakAuras.normalWidth,
      get = get(option, "collapse"),
      set = function(_, value)
        for id, optionData in pairs(option.references) do
          local childOption = optionData.options[optionData.index]
          local childData = optionData.data
          childOption.collapse = value
          OptionsPrivate.SetCollapsed(id, "config", optionData.path, value)
          WeakAuras.Add(childData)
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end,
      disabled = function() return not option.useCollapse end
    }
    args[prefix .. "noMerge"] = {
      type = "toggle",
      name = WeakAuras.newFeatureString .. name(option, "noMerge", L["Prevent Merging"]),
      desc = desc(option, "noMerge", L["If checked, then this group will not merge with other group when selecting multiple auras."]),
      order = order(),
      width = option.groupType =="simple" and WeakAuras.doubleWidth or WeakAuras.normalWidth,
      get = get(option, "noMerge"),
      set = set(data, option, "noMerge"),
    }
    if option.groupType ~="simple" then
      args[prefix .. "sortAlphabetically"] = {
        type = "toggle",
        name = WeakAuras.newFeatureString .. name(option, "sortAlphabetically", L["Sort"]),
        desc = desc(option, "sortAlphabetically", L["If checked, then the combo box in the User settings will be sorted."]),
        order = order(),
        width = WeakAuras.normalWidth,
        get = get(option, "sortAlphabetically"),
        set = set(data, option, "sortAlphabetically"),
      }
    end
    if option.groupType ~="simple" then
      args[prefix .. "limitType"] = {
        type = "select",
        name = name(option, "limitType", L["Number of Entries"]),
        desc = desc(option, "limitType", L["Determines how many entries can be in the table."]),
        order = order(),
        width = WeakAuras.normalWidth,
        values = OptionsPrivate.Private.group_limit_types,
        get = get(option, "limitType"),
        set = function(_, value)
          for id, optionData in pairs(option.references) do
            local childOption = optionData.options[optionData.index]
            local childData = optionData.data
            if childOption.limitType == "fixed" and childOption.nameSource == -1 and value ~= "fixed" then
              childOption.entryNames = nil
              childOption.nameSource = 0
            end
            childOption.limitType = value
            WeakAuras.Add(childData)
          end
          WeakAuras.ClearAndUpdateOptions(data.id, true)
        end,
      }
      args[prefix .. "size"] = {
        type = "range",
        control = "WeakAurasSpinBox",
        name = name(option, "limitType", option.limitType == "max" and L["Entry limit"] or L["Number of Entries"]),
        desc = desc(option, "limitType"),
        order = order(),
        width = WeakAuras.normalWidth,
        min = 1, -- no point in a table with no entries
        softMax = 20, -- 20 people in a mythic raid group
        step = 1,
        get = get(option, "size"),
        set = function(_, value)
          for id, optionData in pairs(option.references) do
            local childOption = optionData.options[optionData.index]
            local childData = optionData.data
            if childOption.nameSource == -1 then
              if value < childOption.size then
                for i = value + 1, childOption.size do
                  childOption.entryNames[i] = nil
                end
              else
                for i = childOption.size + 1, value do
                  childOption.entryNames[i] = L["Entry %i"]:format(i)
                end
              end
            end
            childOption.size = value
            WeakAuras.Add(childData)
          end
          WeakAuras.ClearAndUpdateOptions(data.id, true)
        end,
        disabled = function() return option.limitType == "none" end,
      }
      args[prefix .. "hideReorder"] = {
        type = "toggle",
        name = name(option, "hideReorder", L["Disallow Entry Reordering"]),
        desc = desc(option, "hideReorder"),
        order = order(),
        width = WeakAuras.normalWidth,
        get = function()
          return option.hideReorder or option.nameSource == -1
        end,
        set = set(data, option, "hideReorder"),
        disabled = function()
          return option.nameSource == -1
        end,
      }
      local nameSources = CopyTable(OptionsPrivate.Private.array_entry_name_types)
      local validNameSourceTypes = OptionsPrivate.Private.name_source_option_types
      if option.limitType ~= "fixed" then
        nameSources[-1] = nil
      end
      for subIndex, subOption in ipairs(option.subOptions) do
        if validNameSourceTypes[subOption.type] then
          local allShareThisOption = true
          for id in pairs(option.references) do
            if not subOption.references[id] then
              allShareThisOption = false
              break
            end
          end
          if allShareThisOption then
            nameSources[subIndex] = subOption.key
          end
        end
      end
      args[prefix .. "nameSource"] = {
        type = "select",
        name = name(option, "nameSource", L["Entry Name Source"]),
        desc = desc(option, "nameSource"),
        order = order(),
        values = nameSources,
        width = WeakAuras.doubleWidth,
        get = function()
          return option.nameSource or 0
        end,
        set = function(_, value)
          for id, optionData in pairs(option.references) do
            local childOption = optionData.options[optionData.index]
            local childData = optionData.data
            if (value == -1) ~= (childOption.nameSource == -1) then
              if value == -1 then
                local entryNames = {}
                for i = 1, childOption.size do
                  entryNames[i] = L["Entry %i"]:format(i)
                end
                childOption.entryNames = entryNames
              else
                childOption.entryNames = nil
              end
            end
            if value > 0 then
              childOption.nameSource = option.subOptions[value].references[id].index
            else
              childOption.nameSource = value
            end
            WeakAuras.Add(childData)
          end
          WeakAuras.ClearAndUpdateOptions(data.id, true)
        end,
      }
      if option.nameSource == -1 then
        for i = 1, option.size do
          args[prefix .. "entry" .. i .. "name"] = {
            type = "input",
            name = nameArray(option, "entryNames", i, L["Entry %i"]:format(i)),
            desc = descArray(option, "entryNames", i),
            order = order(),
            width = WeakAuras.doubleWidth,
            get = getArrayStr(option, "entryNames", i),
            set = setArrayStr(data, option, "entryNames", i),
          }
        end
      end
    end
    args[prefix .. "groupStart"] = {
      type = "header",
      name = L["Start of %s"]:format(option.name),
      order = order()
    }
    local subPrefix = prefix .. "option"
    for subIndex, subOption in ipairs(option.subOptions) do
      local addControlsForType = typeControlAdders[subOption.type]
      if addControlsForType then
        addAuthorModeOption(option.subOptions, args, data, order, subPrefix .. subIndex, subIndex)
      end
    end
    args[prefix .. "addSubOption"] = {
      type = "execute",
      name = L["Add Sub Option"],
      order = order(),
      width = WeakAuras.normalWidth,
      func = function()
        for id, optionData in pairs(option.references) do
          local childOption = optionData.options[optionData.index]
          local childData = optionData.data
          local path = optionData.path
          local j = #childOption.subOptions + 1
          path[#path + 1] = j
          childOption.subOptions[j] = {
            type = "toggle",
            key = generateKey("subOption", childOption.subOptions, j),
            name = L["Sub Option %i"]:format(j),
            default = false,
            width = 1,
            useDesc = false,
          }
          OptionsPrivate.SetCollapsed(id, "author", path, false)
          WeakAuras.Add(childData)
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end
    }
    args[prefix .. "groupEnd"] = {
      type = "header",
      name = L["End of %s"]:format(option.name),
      order = order()
    }
  end
}

local function up(data, options, index)
  local option = options[index]
  return function()
    for id, optionData in pairs(option.references) do
      if optionData.path[#optionData.path] <= 1 then
        return true
      end
    end
  end, function()
    for id, optionData in pairs(option.references) do
      -- move the option up in the subOptions
      local path = optionData.path
      local optionID = optionData.index
      local childData = optionData.data
      local childOptions = optionData.options
      local parent = optionData.parent
      if parent and parent.groupType == "array" then
        local dereferencedParent = parent.references[id].options[parent.references[id].index]
        if dereferencedParent.nameSource == optionID then
          dereferencedParent.nameSource = optionID - 1
        elseif dereferencedParent.nameSource == optionID - 1 then
          dereferencedParent.nameSource = optionID
        end
      end
      OptionsPrivate.MoveCollapseDataUp(id, "author", path)
      childOptions[optionID], childOptions[optionID - 1] = childOptions[optionID - 1], childOptions[optionID]
      WeakAuras.Add(childData)
    end
    WeakAuras.ClearAndUpdateOptions(data.id, true)
  end
end

local function down(data, options, index)
  local option = options[index]
  return function()
    for id, optionData in pairs(option.references) do
      if optionData.path[#optionData.path] >= #optionData.options then
        return true
      end
    end
  end, function()
    for id, optionData in pairs(option.references) do
      -- move the option down in the subOptions
      local path = optionData.path
      local optionID = optionData.index
      local childData = optionData.data
      local parent = optionData.parent
      if parent and parent.groupType == "array" then
        local dereferencedParent = parent.references[id].options[parent.references[id].index]
        if dereferencedParent.nameSource == optionID then
          dereferencedParent.nameSource = optionID + 1
        elseif dereferencedParent.nameSource == optionID + 1 then
          dereferencedParent.nameSource = optionID
        end
      end
      local childOptions = optionData.options
      OptionsPrivate.MoveCollapseDataDown(id, "author", path)
      childOptions[optionID], childOptions[optionID + 1] = childOptions[optionID + 1], childOptions[optionID]
      WeakAuras.Add(childData)
    end
    WeakAuras.ClearAndUpdateOptions(data.id, true)
  end
end

local function duplicate(data, options, index)
  local option = options[index]
  return function()
    for id, optionData in pairs(option.references) do
      local optionID = optionData.index
      local childOptions = optionData.options
      local childData = optionData.data
      local path = optionData.path
      path[#path] = path[#path] + 1 -- this data is being regenerated very soon
      OptionsPrivate.InsertCollapsed(id, "author", optionData.path, false)
      local newOption = CopyTable(childOptions[optionID])
      if newOption.key then
        local existingKeys = {}
        for _, option in ipairs(childOptions) do
          if option.key then
            existingKeys[option.key] = true
          end
        end
        while existingKeys[newOption.key] do
          newOption.key = generateKey(newOption.key .. "copy", childOptions, 1)
        end
      end
      if newOption.name then
        newOption.name = newOption.name .. " - " .. L["Copy"]
      end
      tinsert(childOptions, optionID + 1, newOption)
      WeakAuras.Add(childData)
    end
    WeakAuras.ClearAndUpdateOptions(data.id, true)
  end
end

local function validateNonDuplicateKey(option)
  -- note: this has some unintuitive behavior
  -- e.g. if aura A has option keys "foo", "bar"
  -- and aura B has option keys "foo", "baz",
  -- then you still cannot change the merged option with key "foo" to "bar"
  -- unless you unselect aura A, even though aura B would be fine with that.
  return function(_, newKey)
    for id, optionData in pairs(option.references) do
      for index, otherOption in ipairs(optionData.options) do
        if index ~= optionData.index and otherOption.key == newKey then
          return L["%s - Option #%i has the key %s. Please choose a different option key."]:format(id, index, newKey)
        end
      end
    end
    return true
  end
end

function addAuthorModeOption(options, args, data, order, prefix, i)
  -- add header controls
  local option = options[i]

  local collapsed = false
  for id, optionData in pairs(option.references) do
    if OptionsPrivate.IsCollapsed(id, "author", optionData.path, true) then
      collapsed = true
      break
    end
  end

  local _, optionData = next(option.references)
  local isInGroup = optionData.parent ~= nil
  local buttonWidth = 0.6
  if isInGroup then
    buttonWidth = buttonWidth + 0.3
  end

  local optionBelow = options[i + 1]
  local isAboveGroup = optionBelow and OptionsPrivate.Private.author_option_classes[optionBelow.type] == "group"
  if isAboveGroup then
    buttonWidth = buttonWidth + 0.15
  end

  local optionAbove = options[i - 1]
  local isBelowGroup = optionAbove and OptionsPrivate.Private.author_option_classes[optionAbove.type] == "group"
  if isBelowGroup then
    buttonWidth = buttonWidth + 0.15
  end
  local optionClass = OptionsPrivate.Private.author_option_classes[option.type]
  local optionName = optionClass == "noninteractive" and OptionsPrivate.Private.author_option_types[option.type]
                     or option.name

  args[prefix .. "collapse"] = {
    type = "execute",
    name = nameHead(data, option, optionName),
    order = order(),
    width = WeakAuras.doubleWidth - buttonWidth,
    func = function()
      for id, optionData in pairs(option.references) do
        OptionsPrivate.SetCollapsed(id, "author", optionData.path, not collapsed)
      end
      WeakAuras.ClearAndUpdateOptions(data.id, true)
    end,
    image = collapsed and "Interface\\AddOns\\WeakAuras\\Media\\Textures\\expand" or
      "Interface\\AddOns\\WeakAuras\\Media\\Textures\\collapse",
    imageWidth = 18,
    imageHeight = 18,
    control = "WeakAurasExpand"
  }

  args[prefix .. "upAndIn"] = {
    type = "execute",
    width = 0.15,
    name = L["Move Into Above Group"],
    order = order(),
    hidden = function() return not isBelowGroup end,
    func = function()
      for id, optionData in pairs(option.references) do
        local groupData = optionAbove.references[id]
        if groupData then
          local childGroup = groupData.options[groupData.index]
          local childCollapsed = OptionsPrivate.IsCollapsed(id, "author", optionData.path, true)
          OptionsPrivate.RemoveCollapsed(id, "author", optionData.path)
          local newPath = groupData.path
          tinsert(newPath, #childGroup.subOptions + 1)
          OptionsPrivate.InsertCollapsed(id, "author", newPath, childCollapsed)
          local childOption = tremove(optionData.options, optionData.index)
          childOption.key = ensureUniqueKey(childOption.key, "In", childGroup.subOptions)
          local childData = optionData.data
          tinsert(childGroup.subOptions, childOption)
          WeakAuras.Add(childData)
        end
      end
      WeakAuras.ClearAndUpdateOptions(data.id, true)
    end,
    image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\upright",
    imageWidth = 24,
    imageHeight = 24,
    control = "WeakAurasIcon"
  }
  args[prefix .. "downAndIn"] = {
    type = "execute",
    width = 0.15,
    name = L["Move Into Below Group"],
    order = order(),
    hidden = function() return not isAboveGroup end,
    func = function()
      for id, optionData in pairs(option.references) do
        local groupData = optionBelow.references[id]
        if groupData then
          local childGroup = groupData.options[groupData.index]
          local childCollapsed = OptionsPrivate.IsCollapsed(id, "author", optionData.path, true)
          OptionsPrivate.RemoveCollapsed(id, "author", optionData.path)
          local newPath = groupData.path
          tinsert(newPath, 1)
          OptionsPrivate.InsertCollapsed(id, "author", newPath, childCollapsed)
          local childOption = tremove(optionData.options, optionData.index)
          childOption.key = ensureUniqueKey(childOption.key, "In", childGroup.subOptions)
          local childData = optionData.data
          tinsert(childGroup.subOptions, 1, childOption)
          WeakAuras.Add(childData)
        end
      end
      WeakAuras.ClearAndUpdateOptions(data.id, true)
    end,
    image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\downright",
    imageWidth = 24,
    imageHeight = 24,
    control = "WeakAurasIcon"
  }

  args[prefix .. "upAndOut"] = {
    type = "execute",
    width = 0.15,
    name = L["Move Above Group"],
    order = order(),
    hidden = function() return not isInGroup end,
    func = function()
      for id, optionData in pairs(option.references) do
        local path = optionData.path
        local parent = optionData.parent
        local parentOptions = parent and parent.references[id].options or optionData.data.authorOptions
        local childOption = tremove(optionData.options, optionData.index)
        if parent and parent.groupType == "array" then
          local dereferencedParent = parent.references[id].options[parent.references[id].index]
          if dereferencedParent.nameSource == optionData.index then
            dereferencedParent.nameSource = 0
          elseif dereferencedParent.nameSource > optionData.index then
            dereferencedParent.nameSource = dereferencedParent.nameSource - 1
          end
        end
        OptionsPrivate.RemoveCollapsed(id, "author", optionData.path)
        childOption.key = ensureUniqueKey(childOption.key, "Out", parentOptions)
        tinsert(parentOptions, path[#path - 1], childOption)
        path[#path] = nil
        OptionsPrivate.InsertCollapsed(id, "author", path)
        WeakAuras.Add(optionData.data)
      end
      WeakAuras.ClearAndUpdateOptions(data.id, true)
    end,
    image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\upleft",
    imageWidth = 24,
    imageHeight = 24,
    control = "WeakAurasIcon"
  }
  args[prefix .. "downAndOut"] = {
    type = "execute",
    width = 0.15,
    name = L["Move Below Group"],
    order = order(),
    hidden = function() return not isInGroup end,
    func = function()
      for id, optionData in pairs(option.references) do
        local path = optionData.path
        local parent = optionData.parent
        local parentOptions = parent and parent.references[id].options or optionData.data.authorOptions
        local childOption = tremove(optionData.options, optionData.index)
        if parent and parent.groupType == "array" then
          local dereferencedParent = parent.references[id].options[parent.references[id].index]
          if dereferencedParent.nameSource == optionData.index then
            dereferencedParent.nameSource = 0
          elseif dereferencedParent.nameSource > optionData.index then
            dereferencedParent.nameSource = dereferencedParent.nameSource - 1
          end
        end
        OptionsPrivate.RemoveCollapsed(id, "author", optionData.path)
        childOption.key = ensureUniqueKey(childOption.key, "Out", parentOptions)
        tinsert(parentOptions, path[#path - 1] + 1, childOption)
        path[#path] = nil
        path[#path] = path[#path] + 1
        OptionsPrivate.InsertCollapsed(id, "author", path)
        WeakAuras.Add(optionData.data)
      end
      WeakAuras.ClearAndUpdateOptions(data.id, true)
    end,
    image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\downleft",
    imageWidth = 24,
    imageHeight = 24,
    control = "WeakAurasIcon"
  }
  local upDisable, upFunc = up(data, options, i)
  args[prefix .. "up"] = {
    type = "execute",
    width = 0.15,
    name = L["Move Up"],
    order = order(),
    disabled = upDisable,
    func = upFunc,
    image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\moveup",
    imageWidth = 24,
    imageHeight = 24,
    control = "WeakAurasIcon"
  }

  local downDisable, downFunc = down(data, options, i)
  args[prefix .. "down"] = {
    type = "execute",
    width = 0.15,
    name = L["Move Down"],
    order = order(),
    disabled = downDisable,
    func = downFunc,
    image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\movedown",
    imageWidth = 24,
    imageHeight = 24,
    control = "WeakAurasIcon"
  }

  args[prefix .. "duplicate"] = {
    type = "execute",
    width = 0.15,
    name = L["Duplicate"],
    order = order(),
    func = duplicate(data, options, i),
    image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\duplicate",
    imageWidth = 24,
    imageHeight = 24,
    control = "WeakAurasIcon"
  }

  args[prefix .. "delete"] = {
    type = "execute",
    width = 0.15,
    name = L["Delete"],
    order = order(),
    func = function()
      for id, optionData in pairs(option.references) do
        local childOptions = optionData.options
        local optionIndex = optionData.index
        local childData = optionData.data
        local parent = optionData.parent
        OptionsPrivate.RemoveCollapsed(id, "author", optionData.path)
        tremove(childOptions, optionIndex)
        if parent and parent.groupType == "array" then
          local dereferencedParent = parent.references[id].options[parent.references[id].index]
          if dereferencedParent.nameSource == optionData.index then
            dereferencedParent.nameSource = 0
          elseif dereferencedParent.nameSource > optionData.index then
            dereferencedParent.nameSource = dereferencedParent.nameSource - 1
          end
        end
        WeakAuras.Add(childData)
      end
      WeakAuras.ClearAndUpdateOptions(data.id, true)
    end,
    image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\delete",
    imageWidth = 24,
    imageHeight = 24,
    control = "WeakAurasIcon"
  }

  if collapsed then return end

  args[prefix .. "type"] = {
    type = "select",
    width = WeakAuras.doubleWidth,
    name = L["Option Type"],
    desc = descType(option),
    order = order(),
    values = OptionsPrivate.Private.author_option_types,
    get = get(option, "type"),
    set = function(_, value)
      if value == option.type then
        return
      end
      local author_option_fields = OptionsPrivate.Private.author_option_fields
      local commonFields, newFields = author_option_fields.common, author_option_fields[value]
      local newClass = OptionsPrivate.Private.author_option_classes[value]
      for id, optionData in pairs(option.references) do
        local childOption = optionData.options[optionData.index]
        local childData = optionData.data
        local parentOption = optionData.parent
        for k in pairs(childOption) do
          if not commonFields[k] then
            childOption[k] = nil
          end
        end
        for k, v in pairs(newFields) do
          if type(v) == "table" then
            childOption[k] = CopyTable(v)
          else
            childOption[k] = v
          end
        end
        childOption.type = value
        if newClass == "noninteractive" then
          childOption.name = nil
          childOption.desc = nil
          childOption.key = nil
          childOption.useDesc = nil
          childOption.default = nil
        else
          -- don't use the option index here if switching from a noninteractive type
          -- mostly because it would have a very non-intuitive effect
          -- the names and keys would likely not match anymore, and so
          -- the merged display would basically explode into a bunch of separate options
          childOption.name = childOption.name or (L["Option %i"]):format(i)
          if not childOption.key then
            local newKey = "option" .. i
            local existingKeys = {}
            for index, option in pairs(optionData.options) do
              if index ~= optionData.index and option.key then
                existingKeys[option.key] = true
              end
            end
            while existingKeys[newKey] do
              newKey = newKey .. "copy"
            end
            childOption.key = newKey
          end
        end
        if parentOption and parentOption.groupType == "array" and not OptionsPrivate.Private.array_entry_name_types[value] then
          local dereferencedParent = parentOption.references[id]
          if dereferencedParent.nameSource == optionData.index then
            dereferencedParent.nameSource = 0
          end
        end
        WeakAuras.Add(childData)
      end
      WeakAuras.ClearAndUpdateOptions(data.id, true)
    end
  }

  if optionClass ~= "noninteractive" then
    args[prefix .. "name"] = {
      type = "input",
      width = WeakAuras.normalWidth,
      name = name(option, "name", L["Display Name"]),
      desc = desc(option, "name"),
      order = order(),
      get = getStr(option, "name"),
      set = setStr(data, option, "name")
    }

    args[prefix .. "key"] = {
      type = "input",
      width = WeakAuras.normalWidth,
      name = name(option, "key", optionClass == "group" and L["Group key"] or L["Option key"]),
      order = order(),
      validate = validateNonDuplicateKey(option),
      get = get(option, "key"),
      set = set(data, option, "key")
    }
  end

  if optionClass == "simple" then
    args[prefix .. "tooltipSpace"] = {
      type = "description",
      width = WeakAuras.doubleWidth,
      name = "",
      order = order
    }
    args[prefix .. "usetooltip"] = {
      type = "toggle",
      name = name(option, "useDesc", L["Tooltip"]),
      order = order(),
      width = WeakAuras.halfWidth,
      get = get(option, "useDesc"),
      set = set(data, option, "useDesc")
    }
    args[prefix .. "tooltip"] = {
      type = "input",
      name = name(option, "desc", L["Tooltip Text"]),
      desc = desc(option, "desc"),
      order = order(),
      width = WeakAuras.normalWidth * 1.5,
      get = getStr(option, "desc"),
      set = setStr(data, option, "desc"),
      disabled = function()
        return not option.useDesc
      end
    }
  end

  args[prefix .. "width"] = {
    type = "range",
    control = "WeakAurasSpinBox",
    width = WeakAuras.normalWidth,
    name = name(option, "width", L["Width"]),
    desc = desc(option, "width"),
    order = order(),
    min = 0.1,
    max = 2,
    step = 0.05,
    get = get(option, "width"),
    set = set(data, option, "width")
  }

  local addControlsForType = typeControlAdders[option.type]
  if addControlsForType then
    addControlsForType(options, args, data, order, prefix, i)
  end
end

local groupPages = {}

local function getPage(id, path, max)
  max = max or math.huge
  groupPages[id] = groupPages[id] or {}
  local base = groupPages[id]
  for _, index in ipairs(path) do
    if not base[index] then
      base[index] = {}
    end
    base = base[index]
  end
  if not base.page or (max and base.page > max) then
    base.page = 1
  end
  return base.page
end

local function setPage(id, path, page)
  groupPages[id] = groupPages[id] or {}
  local base = groupPages[id]
  for _, index in ipairs(path) do
    if not base[index] then
      base[index] = {}
    end
    base = base[index]
  end
  base.page = page
end

local function addUserModeOption(options, args, data, order, prefix, i)
  local option = options[i]
  local optionType = option.type
  local optionClass = OptionsPrivate.Private.author_option_classes[optionType]
  local userOption

  if optionClass == "simple" then
    userOption = {
      type = optionType,
      name = nameUser(option),
      desc = descUser(option),
      width = (option.width or 1) * WeakAuras.normalWidth,
      order = order(),
      get = getUser(option),
      set = setUser(data, option)
    }
  elseif optionClass == "noninteractive" then
    userOption = {
      type = "description",
      order = order(),
      name = "",
      width = (option.width or 1) * WeakAuras.normalWidth
    }
  elseif optionClass == "group" then
    local collapsed = false
    if option.useCollapse then
      local defaultCollapsed = true
      if option.collapse ~= nil then
        defaultCollapsed = option.collapse
      end
      for id, optionData in pairs(option.references) do
        if OptionsPrivate.IsCollapsed(id, "config", optionData.path, defaultCollapsed) then
          collapsed = true
          break
        end
      end
      args[prefix .. "collapse"] = {
        type = "execute",
        name = option.name,
        order = order(),
        width = WeakAuras.doubleWidth,
        func = function()
          for id, optionData in pairs(option.references) do
            OptionsPrivate.SetCollapsed(id, "config", optionData.path, not collapsed)
          end
          WeakAuras.ClearAndUpdateOptions(data.id, true)
        end,
        image = collapsed and "Interface\\AddOns\\WeakAuras\\Media\\Textures\\expand" or
          "Interface\\AddOns\\WeakAuras\\Media\\Textures\\collapse",
        imageWidth = 18,
        imageHeight = 18,
        control = "WeakAurasExpand"
      }
    end
    if not collapsed then
      local skipSubOptions = false
      if option.groupType == "array" then
        local values, firstChild = {}, true
        local nameSource = option.nameSource or 0
        if nameSource > 0 then
          nameSource = option.subOptions[option.nameSource].key
        end
        if nameSource == -1 then
          for id, optionData in pairs(option.references) do
            local i = 1
            local childOption = optionData.options[optionData.index]
            local entryNames = childOption.entryNames
            while i <= #values or i <= childOption.size do
              if firstChild then
                values[i] = entryNames[i]
              elseif values[i] ~= entryNames[i] then
                values[i] = conflictBlue .. L["Entry %i"]:format(i)
              end
              i = i + 1
            end
            firstChild = false
          end
        elseif nameSource == 0 then
          for id, optionData in pairs(option.references) do
            local i = 1
            local childOption = optionData.options[optionData.index]
            local childValues = optionData.config[childOption.key]
            while i <= #values or i <= #childValues do
              if firstChild then
                values[i] = L["Entry %i"]:format(i)
              elseif values[i] == nil or childValues[i] == nil then
                values[i] = conflictBlue .. L["Entry %i"]:format(i)
              end
              i = i + 1
            end
            firstChild = false
          end
        else
          for id, optionData in pairs(option.references) do
            local i = 1
            local childOption = optionData.options[optionData.index]
            local childValues = optionData.config[option.key]
            while i <= #values or i <= #childValues do
              if firstChild then
                values[i] = childValues[i][nameSource] or conflictBlue .. L["Entry %i"]:format(i)
              elseif not childValues[i] or childValues[i][nameSource] ~= values[i] then
                values[i] = conflictBlue .. L["Entry %i"]:format(i)
              end
              i = i + 1
            end
            firstChild = false
          end
        end
        skipSubOptions = #values == 0
        local buttonWidth = 0.75
        if option.limitType == "fixed" then
          buttonWidth = buttonWidth - 0.30
        end
        if option.hideReorder or option.nameSource == -1 then
          buttonWidth = buttonWidth - 0.30
        end
        args[prefix .. "entryChoice"] = {
          type = "select",
          name = nameUser(option),
          order = order(),
          width = WeakAuras.doubleWidth - buttonWidth,
          values = values,
          get = function()
            if skipSubOptions then
              return 1 -- show the "create" prompt, which is at index 1
            end
            local value
            for id, optionData in pairs(option.references) do
              local childOption = optionData.options[optionData.index]
              local childConfigList = optionData.config[childOption.key]
              if value == nil then
                value = getPage(id, optionData.path, #childConfigList)
              elseif value ~= getPage(id, optionData.path, #childConfigList) then
                return ""
              end
            end
            return value
          end,
          set = function(_, value)
            for id, optionData in pairs(option.references) do
              setPage(id, optionData.path, value) -- XXX: mergeOptions will reset this to the maximum value if it's too big
            end
            WeakAuras.ClearAndUpdateOptions(data.id, true)
          end,
          sorting = option.sortAlphabetically and OptionsPrivate.Private.SortOrderForValues(values) or nil
        }
        args[prefix .. "resetEntry"] = {
          type = "execute",
          name = L["Reset Entry"],
          order = order(),
          func = function()
            for id, optionData in pairs(option.references) do
              local childOption = optionData.options[optionData.index]
              local childData = optionData.data
              local childPage = getPage(id, optionData.path)
              local childConfigList = optionData.config[childOption.key]
              childConfigList[childPage] = {}
              WeakAuras.Add(childData)
            end
            WeakAuras.ClearAndUpdateOptions(data.id, true)
          end,
          width = 0.15,
          image = "Interface\\Addons\\WeakAuras\\Media\\Textures\\reset",
          imageWidth = 18,
          imageHeight = 18,
          control = "WeakAurasIcon"
        }
        if option.limitType ~= "fixed" then
          args[prefix .. "createEntry"] = {
            type = "execute",
            name = L["Add Entry"],
            order = order(),
            func = function()
              for id, optionData in pairs(option.references) do
                local childOption = optionData.options[optionData.index]
                local childConfigList = optionData.config[childOption.key]
                local childData = optionData.data
                if childOption.limitType == "none" or #childConfigList < childOption.size then
                  tinsert(childConfigList, {})
                  setPage(id, optionData.path, #childConfigList)
                  -- we do need to Add here, so that the new entry can get its default values
                  WeakAuras.Add(childData)
                end
              end
              WeakAuras.ClearAndUpdateOptions(data.id, true)
            end,
            disabled = function()
              if option.limitType == "none" then
                return false
              else
                for id, optionData in pairs(option.references) do
                  local childOption = optionData.options[optionData.index]
                  local childConfigList = optionData.config[childOption.key]
                  if #childConfigList >= childOption.size then
                    return true
                  end
                end
              end
            end,
            width = 0.15,
            image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\add",
            imageWidth = 18,
            imageHeight = 18,
            control = "WeakAurasIcon"
          }
          args[prefix .. "deleteEntry"] = {
            type = "execute",
            name = L["Delete Entry"],
            order = order(),
            confirm = true,
            func = function()
              for id, optionData in pairs(option.references) do
                local childOption = optionData.options[optionData.index]
                local childConfigList = optionData.config[childOption.key]
                local childData = optionData.data
                local page = getPage(id, optionData.path)
                if #childConfigList ~= 0 then
                  tremove(childConfigList, page)
                  setPage(id, optionData.path, min(#childConfigList, page))
                  WeakAuras.Add(childData)
                end
              end
              WeakAuras.ClearAndUpdateOptions(data.id, true)
            end,
            disabled = function()
              return skipSubOptions
            end,
            width = 0.15,
            image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\delete",
            imageWidth = 18,
            imageHeight = 18,
            control = "WeakAurasIcon"
          }
        end
        if option.nameSource ~= -1 and not option.hideReorder then
          args[prefix .. "moveEntryUp"] = {
            type = "execute",
            name = L["Move Entry Up"],
            order = order(),
            func = function()
              for id, optionData in pairs(option.references) do
                local childOption = optionData.options[optionData.index]
                local childConfigList = optionData.config[childOption.key]
                local childData = optionData.data
                local childPage = getPage(id, optionData.path, #childConfigList)
                if childConfigList[childPage] then
                  childConfigList[childPage], childConfigList[childPage - 1] = childConfigList[childPage - 1], childConfigList[childPage]
                  setPage(id, optionData.path, childPage - 1)
                  WeakAuras.Add(childData)
                end
              end
              WeakAuras.ClearAndUpdateOptions(data.id, true)
            end,
            disabled = function()
              for id, optionData in pairs(option.references) do
                if getPage(id, optionData.path) <= 1 then
                  return true
                end
              end
            end,
            width = 0.15,
            image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\moveup",
            imageWidth = 18,
            imageHeight = 18,
            control = "WeakAurasIcon"
          }
          args[prefix .. "moveEntryDown"] = {
            type = "execute",
            name = L["Move Entry Down"],
            order = order(),
            func = function()
              for id, optionData in pairs(option.references) do
                local childOption = optionData.options[optionData.index]
                local childConfigList = optionData.config[childOption.key]
                local childData = optionData.data
                local childPage = getPage(id, optionData.path, #childConfigList)
                if childConfigList[childPage] then
                  childConfigList[childPage], childConfigList[childPage + 1] = childConfigList[childPage + 1], childConfigList[childPage]
                  setPage(id, optionData.path, childPage + 1)
                  WeakAuras.Add(childData)
                end
              end
              WeakAuras.ClearAndUpdateOptions(data.id, true)
            end,
            disabled = function()
              for id, optionData in pairs(option.references) do
                local childPage = getPage(id, optionData.path)
                local childOption = optionData.options[optionData.index]
                local childConfigList = optionData.config[childOption.key]
                if childPage >= #childConfigList then
                  return true
                end
              end
            end,
            width = 0.15,
            image = "Interface\\AddOns\\WeakAuras\\Media\\Textures\\movedown",
            imageWidth = 18,
            imageHeight = 18,
            control = "WeakAurasIcon"
          }
        end
      end

      if not skipSubOptions then
        local subPrefix = prefix .. "subOption"
        for j = 1, #option.subOptions do
          addUserModeOption(option.subOptions, args, data, order, subPrefix .. j, j)
        end
      end
    end
  end
  args[prefix] = userOption

  -- convert from weakauras option type to ace option type
  if optionClass == "simple" then
    -- toggle and input don't need any extra love
    if optionType == "input" then
      userOption.multiline = option.multiline
    elseif optionType == "number" then
      userOption.type = "input"
      userOption.get = getUserNumAsString(option)
      userOption.set = setUserNum(data, option)
    elseif optionType == "range" then
      userOption.softMax = option.softMax
      userOption.softMin = option.softMin
      userOption.bigStep = option.bigStep
      userOption.min = option.min
      userOption.max = option.max
      local effectiveMin = userOption.softMin or userOption.min or 0
      local effectiveMax = userOption.softMax or userOption.max or 100
      if (effectiveMin > effectiveMax) then
        -- This will cause a error inside the slider
        -- Fix up either softMax or max, depending on which one is the effective one
        if userOption.softMax then
          userOption.softMax = effectiveMin
        elseif userOption.max then
          userOption.max = effectiveMin
        else
          userOption.softMax = effectiveMin
        end
      end
      userOption.step = option.step
    elseif optionType == "color" then
      userOption.hasAlpha = true
      userOption.get = getUserColor(option)
      userOption.set = setUserColor(data, option)
    elseif optionType == "select" then
      userOption.values = getUserValues(option)
    elseif optionType == "multiselect" then
      userOption.values = getUserValues(option)
      userOption.get = function(_, k)
        local value
        for id, optionData in pairs(option.references) do
          if value == nil then
            value = optionData.config[option.key][k]
          elseif value ~= optionData.config[option.key][k] then
            return
          end
        end
        return value
      end
      userOption.set = function(_, k, v)
        for _, optionData in pairs(option.references) do
          optionData.config[option.key][k] = v
          WeakAuras.Add(optionData.data)
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end
    elseif optionType == "media" then
      userOption.type = "select"
      userOption.dialogControl = OptionsPrivate.Private.author_option_media_controls[option.mediaType]
      userOption.itemControl = OptionsPrivate.Private.author_option_media_itemControls[option.mediaType]
      userOption.values = function()
        if option.mediaType == "sound" then
          return OptionsPrivate.Private.sound_file_types
        else
          return AceGUIWidgetLSMlists[option.mediaType]
        end
      end

      userOption.sorting = function()
        if option.mediaType == "sound" then
          return OptionsPrivate.Private.SortOrderForValues(OptionsPrivate.Private.sound_file_types)
        else
          return nil
        end
      end

      userOption.set = function(_, value)
        if option.mediaType == "sound" then
          PlaySoundFile(value, "Master")
        end
        for _, optionData in pairs(option.references) do
          local childData = optionData.data
          local childConfig = optionData.config
          childConfig[option.key] = value
          WeakAuras.Add(childData)
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end
    end
  elseif optionClass == "noninteractive" then
    if optionType == "header" then
      userOption.type = "header"
      local name = {}
      local firstName = nil
      local conflict = false
      for _, optionData in pairs(option.references) do
        local childOption = optionData.options[optionData.index]
        if childOption.useName and #childOption.text > 0 then
          if firstName == nil then
            firstName = childOption.text
            tinsert(name, (childOption.text:gsub("||", "|")))
          elseif childOption.text ~= firstName then
            conflict = true
            tinsert(name, (childOption.text:gsub("||", "|")))
          end
        end
      end
      userOption.name = (conflict and conflictBlue or "") .. tconcat(name, " / ")
    elseif optionType == "description" then
      userOption.name = nameUserDesc(option)
      userOption.fontSize = option.fontSize
    elseif optionType == "space" then
      if not option.variableWidth then
        userOption.width = "full"
      end
      if option.useHeight and (option.height or 1) > 1 then
        userOption.name = string.rep("\n", option.height - 1)
      else
        userOption.name = " "
      end
    end
  end
end

local function initReferences(mergedOption, data, options, index, config, path, parent)
  mergedOption.references = {
    [data.id] = {
      data = data,
      options = options,
      index = index,
      config = config,
      path = path,
      parent = parent,
    }
  }
  if mergedOption.subOptions then
    local subConfig
    if config then
      if mergedOption.groupType == "simple" then
        subConfig = config[mergedOption.key]
      else
        local configList = config[mergedOption.key]
        local page = getPage(data.id, path, #configList)
        subConfig = configList[page]
      end
    end
    local subOptions = options[index].subOptions
    local subPath
    local subParent = mergedOption
    for i, submergedOption in ipairs(mergedOption.subOptions) do -- ha, submerged
      subPath = CopyTable(path)
      subPath[#subPath + 1] = i
      initReferences(submergedOption, data, subOptions, i, subConfig, subPath, subParent)
    end
  end
end

-- all of these fields must be identical for an option to be merged
-- sometimes this just means that they are both nil, e.g. descriptions have no key
local significantFieldsForMerge = {
  type = true,
  name = true,
  key = true,
  groupType = true,
  limitType = true,
  size = true,
  mediaType = true,
}

-- these fields are special cases, generally reserved for when the UI displays something based on the merged options
-- e.g. array name source displays options in merged order, so the dereferenced source is not useful at that level.
local specialCasesForMerge = {
  nameSource = true
}

local function mergeOptions(mergedOptions, data, options, config, prepath, parent)
  local nextInsert = 1
  for i = 1, #options do
    local path = CopyTable(prepath)
    path[#path + 1] = i
    -- find the best place to start inserting the next option to merge
    local nextToMerge = options[i]
    local shouldMerge = false
    if not nextToMerge.noMerge then
      for j = nextInsert, #mergedOptions + 1 do
        local mergedOption = mergedOptions[j]
        if not mergedOption then
          break
        end -- no more options to check, so must insert
        local validMerge = not mergedOption.noMerge
        if validMerge then
          for field in pairs(significantFieldsForMerge) do
            if nextToMerge[field] ~= mergedOption[field] then
              validMerge = false
              break
            end
          end
        end
        if validMerge then
          shouldMerge = true
          nextInsert = j
          break
        end
      end
    end
    -- now we know at what point to add nextToMerge
    if shouldMerge then
      local mergedOption = mergedOptions[nextInsert]
      -- nil out all fields which aren't the same
      mergedOption.references[data.id] = {
        data = data,
        options = options,
        index = i,
        config = config,
        path = path,
        parent = parent,
      }
      for k, v in pairs(nextToMerge) do
        if k == "subOptions" then
          local subConfig
          if config then
            if mergedOption.groupType == "simple" then
              subConfig = config[mergedOption.key]
            else
              local configList = config[mergedOption.key]
              local page = getPage(data.id, path, #configList)
              subConfig = configList[page]
            end
          end
          local subParent = mergedOption
          mergeOptions(mergedOption.subOptions, data, v, subConfig, path, subParent)
          if mergedOption.groupType == "array" and mergedOption.nameSource ~= nil then
            -- special case merge of nameSource
            -- nameSource can be an optionID of the array's subOptions.
            -- Since the optionIDs are normally hidden away in references and options with different optionIDs can be merged together,
            -- we can't simply use nilmerge like we do with most other fields.
            -- Obviously, if the nameSource has already been set to nil then we do not need to do any more checks,
            -- as newly merged options can never resolve conflicts. Otherwise, we need to examine the semantic value of the nameSource.
            if nextToMerge.nameSource < 1 or mergedOption.nameSource < 1 then -- either the names are fixed, or they are auto-generated as "Entry #"
              -- in this case, nilmerge is the appropriate strategy
              if mergedOption.nameSource ~= nextToMerge.nameSource then
                mergedOption.nameSource = nil
              end
            else -- entry names are sourced from config of a particular subOption
              -- check if nextToMerge.nameSource was merged in the same spot as mergedOption.nameSource
              local subMergedOption = mergedOption.subOptions[mergedOption.nameSource]
              local optionData = subMergedOption.references[data.id]
              if not optionData or optionData.index ~= nextToMerge.nameSource then
                -- either an option was not merged at the name source's index, or the wrong option was.
                -- in both cases, the name source is conflicted. Fallback to "Entry #" as entry names
                mergedOption.nameSource = nil
              end
            end
          end
        elseif not specialCasesForMerge[k] and neq(mergedOption[k], v) then
          mergedOption[k] = nil
        end
      end
    else
      nextInsert = #mergedOptions + 1
      -- can't merge, should insert instead
      local newOption = CopyTable(nextToMerge)
      initReferences(newOption, data, options, i, config, path, parent)
      tinsert(mergedOptions, nextInsert, newOption)
    end
    -- never merge 2 options from the same child
    nextInsert = nextInsert + 1
  end
end

local function valuesAreEqual(t1, t2)
  if t1 == t2 then
    return true
  end
  local ty1 = type(t1)
  local ty2 = type(t2)
  if ty1 ~= ty2 then
    return false
  end
  if ty1 == "number" then
    return abs(t1 - t2) < 1e-9
  end
  if ty1 ~= "table" then
    return false
  end
  for k1, v1 in pairs(t1) do
    local v2 = t2[k1]
    if v2 == nil or not valuesAreEqual(v1, v2) then
      return false
    end
  end

  for k2, v2 in pairs(t2) do
    local v1 = t1[k2]
    if v1 == nil or not valuesAreEqual(v1, v2) then
      return false
    end
  end
  return true
end

local function allChoicesAreDefault(option, config, id, path)
  local optionClass = OptionsPrivate.Private.author_option_classes[option.type]
  if optionClass == "simple" then
    return valuesAreEqual(option.default, config[option.key])
  elseif optionClass == "group" then
    if option.groupType == "simple" then
      local subConfig = config[option.key]
      path[#path + 1] = 0
      for i, subOption in ipairs(option.subOptions) do
        path[#path] = i
        if not allChoicesAreDefault(subOption, subConfig, id, path) then
          return false
        end
      end
      path[#path] = nil
    elseif option.groupType == "array" then
      path[#path + 1] = 0
      for _, subConfig in ipairs(config[option.key]) do
        for i, subOption in ipairs(option.subOptions) do
          path[#path] = i
          if not allChoicesAreDefault(subOption, subConfig, id, path) then
            return false
          end
        end
      end
      path[#path] = nil
    end
    if option.useCollapse then
      local isCollapsed = OptionsPrivate.IsCollapsed(id, "config", path, option.collapse)
      if isCollapsed ~= option.collapse then
        return false
      end
    end
  end
  return true
end

local function createorder(startorder)
  local order = startorder or 1
  return function()
    order = order + 1
    return order
  end
end

function OptionsPrivate.GetAuthorOptions(data)
  -- initialize the process
  local authorOptions = {
    type = "group",
    name = L["Custom Options"],
    order = 100,
    args = {}
  }
  local args = authorOptions.args
  local isAuthorMode = true
  local options = {}
  local order = createorder(1)

  for child in OptionsPrivate.Private.TraverseLeafsOrAura(data) do
    mergeOptions(options, child, child.authorOptions, child.config, {})
    isAuthorMode = isAuthorMode and child.authorMode
  end

  if isAuthorMode then
    args["enterUserMode"] = {
      type = "execute",
      width = WeakAuras.normalWidth,
      name = L["Enter User Mode"],
      desc = L["Enter user mode."],
      order = order(),
      func = function()
        for child in OptionsPrivate.Private.TraverseLeafsOrAura(data) do
          child.authorMode = nil
          -- no need to add, author mode is picked up by ClearAndUpdateOptions
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end
    }
    args["enterUserModeSpacer"] = {
      type = "description",
      name = "",
      order = order()
    }
    for i = 1, #options do
      addAuthorModeOption(options, args, data, order, "option" .. i, i)
    end
    args["addOption"] = {
      type = "execute",
      width = WeakAuras.normalWidth,
      name = L["Add Option"],
      order = order(),
      func = function()
        for child in OptionsPrivate.Private.TraverseLeafsOrAura(data) do
          local i = #child.authorOptions + 1
          child.authorOptions[i] = {
            type = "toggle",
            key = generateKey("option", child.authorOptions, i),
            name = L["Option %i"]:format(i),
            default = false,
            width = 1,
            useDesc = false,
          }
          OptionsPrivate.SetCollapsed(child.id, "author", i, false)
          WeakAuras.Add(child)
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end
    }
  else
    for i = 1, #options do
      addUserModeOption(options, args, data, order, "userOption" .. i, i)
    end
    args["userConfigFooter"] = {
      type = "header",
      name = "",
      order = order()
    }
    args["resetToDefault"] = {
      type = "execute",
      width = WeakAuras.normalWidth,
      name = L["Reset to Defaults"],
      desc = L["Reset all options to their default values."],
      order = order(),
      func = function()
        for child in OptionsPrivate.Private.TraverseLeafsOrAura(data) do
          child.config = {} -- config validation in Add() will set all the needed keys to their defaults
          OptionsPrivate.ResetCollapsed(child.id, "config")
          WeakAuras.Add(child)
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end,
      disabled = function()
        local path = {}
        for child in OptionsPrivate.Private.TraverseLeafsOrAura(data) do
          local config = child.config
            for i, option in ipairs(child.authorOptions) do
              path[1] = i
              local result = allChoicesAreDefault(option, config, child.id, path)
              if result == false then
                return false
              end
            end
        end
        return true
      end
    }
    args["enterAuthorMode"] = {
      type = "execute",
      width = WeakAuras.normalWidth,
      name = L["Enter Author Mode"],
      desc = L["Configure what options appear on this panel."],
      order = order(),
      func = function()
        for configData in OptionsPrivate.Private.TraverseLeafsOrAura(data) do
          -- no need to add, author mode is picked up by ClearAndUpdateOptions
          configData.authorMode = true
        end
        WeakAuras.ClearAndUpdateOptions(data.id, true)
      end
    }
  end
  return authorOptions
end
