if not WeakAuras.IsLibsOK() then return end
local AddonName = ...
local OptionsPrivate = select(2, ...)

-- Lua APIs
local pairs, type, ipairs = pairs, type, ipairs
local gsub = gsub

-- WoW APIs
local CreateFrame = CreateFrame

local AceGUI = LibStub("AceGUI-3.0")
local SharedMedia = LibStub("LibSharedMedia-3.0")
local IndentationLib = IndentationLib

local WeakAuras = WeakAuras
local L = WeakAuras.L

local textEditor

local editor_themes = {
  ["Standard"] = {
    ["Table"] = "|c00ff3333",
    ["Arithmetic"] = "|c00ff3333",
    ["Relational"] = "|c00ff3333",
    ["Logical"] = "|c004444ff",
    ["Special"] = "|c00ff3333",
    ["Keyword"] = "|c004444ff",
    ["Comment"] = "|c0000aa00",
    ["Number"] = "|c00ff9900",
    ["String"] = "|c00999999"
  },
  ["Monokai"] = {
    ["Table"] = "|c00ffffff",
    ["Arithmetic"] = "|c00f92672",
    ["Relational"] = "|c00ff3333",
    ["Logical"] = "|c00f92672",
    ["Special"] = "|c0066d9ef",
    ["Keyword"] = "|c00f92672",
    ["Comment"] = "|c0075715e",
    ["Number"] = "|c00ae81ff",
    ["String"] = "|c00e6db74"
  },
  ["Obsidian"] = {
    ["Table"] = "|c00AFC0E5",
    ["Arithmetic"] = "|c00E0E2E4",
    ["Relational"] = "|c00B3B689",
    ["Logical"] = "|c0093C763",
    ["Special"] = "|c00AFC0E5",
    ["Keyword"] = "|c0093C763",
    ["Comment"] = "|c0066747B",
    ["Number"] = "|c00FFCD22",
    ["String"] = "|c00EC7600"
  }
}

if not WeakAurasSaved.editor_tab_spaces then WeakAurasSaved.editor_tab_spaces = 4 end
if not WeakAurasSaved.editor_font_size then WeakAurasSaved.editor_font_size = 12 end -- set default font size if missing
local color_scheme = {[0] = "|r"}
local function set_scheme()
  if not WeakAurasSaved.editor_theme then
    WeakAurasSaved.editor_theme = "Monokai"
  end
  local theme = editor_themes[WeakAurasSaved.editor_theme]
  color_scheme[IndentationLib.tokens.TOKEN_SPECIAL] = theme["Special"]
  color_scheme[IndentationLib.tokens.TOKEN_KEYWORD] = theme["Keyword"]
  color_scheme[IndentationLib.tokens.TOKEN_COMMENT_SHORT] = theme["Comment"]
  color_scheme[IndentationLib.tokens.TOKEN_COMMENT_LONG] = theme["Comment"]
  color_scheme[IndentationLib.tokens.TOKEN_NUMBER] = theme["Number"]
  color_scheme[IndentationLib.tokens.TOKEN_STRING] = theme["String"]

  color_scheme["..."] = theme["Table"]
  color_scheme["{"] = theme["Table"]
  color_scheme["}"] = theme["Table"]
  color_scheme["["] = theme["Table"]
  color_scheme["]"] = theme["Table"]

  color_scheme["+"] = theme["Arithmetic"]
  color_scheme["-"] = theme["Arithmetic"]
  color_scheme["/"] = theme["Arithmetic"]
  color_scheme["*"] = theme["Arithmetic"]
  color_scheme[".."] = theme["Arithmetic"]

  color_scheme["=="] = theme["Relational"]
  color_scheme["<"] = theme["Relational"]
  color_scheme["<="] = theme["Relational"]
  color_scheme[">"] = theme["Relational"]
  color_scheme[">="] = theme["Relational"]
  color_scheme["~="] = theme["Relational"]

  color_scheme["and"] = theme["Logical"]
  color_scheme["or"] = theme["Logical"]
  color_scheme["not"] = theme["Logical"]
end

-- Define the premade snippets
local premadeSnippets = {
  {
    name = "Basic function",
    snippet = [=[
function()

    return
end]=]
  },
  {
    name = "Custom Activation",
    snippet = [=[
function(trigger)
    return trigger[1] and (trigger[2] or trigger[3])
end]=]
  },
  {
    name = "Trigger: CLEU",
    snippet = [=[
function(event, timestamp, subEvent, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...)

    return
end]=]
  },
  {
    name = "Simple throttle",
    snippet = [=[
if not aura_env.last or aura_env.last < GetTime() - 1 then
    aura_env.last = GetTime()

end]=]
  },
  {
    name = "Trigger State Updater",
    snippet = [=[
function(allstates, event, ...)
    allstates:Update("", {
        progressType = "static"||"timed",
        value = ,
        total = ,
        duration = ,
        expirationTime = ,
        autoHide = true,
        name = ,
        icon = ,
        stacks = ,
        index = ,
    })
    -- allstates:Remove("")
    -- allstates:RemoveAll()
end]=]
  },
}

local function ConstructTextEditor(frame)
  local group = AceGUI:Create("WeakAurasInlineGroup")
  group.frame:SetParent(frame)
  group.frame:SetPoint("TOPLEFT", frame, "TOPLEFT", 17, -63);
  group.frame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -17, 46);
  group.frame:Hide()
  group:SetLayout("flow")

  local editor = AceGUI:Create("MultiLineEditBox")
  editor.editBox.group = group
  editor:SetFullWidth(true)
  editor:SetFullHeight(true)
  editor:DisableButton(true)
  local fontPath = SharedMedia:Fetch("font", "Fira Mono Medium")
  if (fontPath) then
    editor.editBox:SetFont(fontPath, WeakAurasSaved.editor_font_size)
  end
  group:AddChild(editor)

  local originalOnCursorChanged = editor.editBox:GetScript("OnCursorChanged")
  editor.editBox:SetScript("OnCursorChanged", function(self, ...)
    -- WORKAROUND the editbox sends spurious OnCursorChanged events if its resized
    -- That makes AceGUI scroll the editbox to make the cursor visible, leading to unintended
    -- movements. Prevent all of that by checking if the edit box has focus, as otherwise the cursor
    -- is invisible, and we don't care about making it visible
    if not self:HasFocus() then
      return
    end
    originalOnCursorChanged(self, ...)
  end)

  -- The indention lib overrides GetText, but for the line number
  -- display we ned the original, so save it here.
  local originalGetText = editor.editBox.GetText
  local originalSetText = editor.editBox.SetText
  set_scheme()
  IndentationLib.enable(editor.editBox, color_scheme, WeakAurasSaved.editor_tab_spaces)

  local cancel = CreateFrame("Button", nil, group.frame, "UIPanelButtonTemplate")
  cancel:SetScript(
    "OnClick",
    function()
      group:CancelClose()
    end
  )
  cancel:SetPoint("BOTTOMRIGHT", -20, -24)
  cancel:SetFrameLevel(cancel:GetFrameLevel() + 1)
  cancel:SetHeight(20)
  cancel:SetWidth(100)
  cancel:SetText(L["Cancel"])

  local close = CreateFrame("Button", nil, group.frame, "UIPanelButtonTemplate")
  close:SetScript(
    "OnClick",
    function()
      group:Close()
    end
  )
  close:SetPoint("RIGHT", cancel, "LEFT", -10, 0)
  close:SetFrameLevel(close:GetFrameLevel() + 1)
  close:SetHeight(20)
  close:SetWidth(100)
  close:SetText(L["Done"])

  local settings_frame = CreateFrame("Button", "WASettingsButton", close, "UIPanelButtonTemplate")
  settings_frame:SetPoint("RIGHT", close, "LEFT", -10, 0)
  settings_frame:SetHeight(20)
  settings_frame:SetWidth(100)
  settings_frame:SetText(L["Settings"])
  settings_frame:RegisterForClicks("LeftButtonUp")

  local helpButton = CreateFrame("Button", nil, group.frame, "UIPanelButtonTemplate")
  helpButton:SetPoint("BOTTOMLEFT", 0, -24)
  helpButton:SetFrameLevel(cancel:GetFrameLevel() + 1)
  helpButton:SetHeight(20)
  helpButton:SetWidth(100)
  helpButton:SetText(L["Help"])

  local dropdown = CreateFrame("Frame", "SettingsMenuFrame", settings_frame, "UIDropDownMenuTemplate")

  local function settings_dropdown_initialize(frame, level, menu)
    if level == 1 then
      for k, v in pairs(editor_themes) do
        local item = {
          text = k,
          isNotRadio = false,
          checked = function()
            return WeakAurasSaved.editor_theme == k
          end,
          func = function()
            WeakAurasSaved.editor_theme = k
            set_scheme()
            editor.editBox:SetText(editor.editBox:GetText())
          end
        }
        UIDropDownMenu_AddButton(item, level)
      end
      UIDropDownMenu_AddButton(
        {
          text = L["Bracket Matching"],
          isNotRadio = true,
          checked = function()
            return WeakAurasSaved.editor_bracket_matching
          end,
          func = function()
            WeakAurasSaved.editor_bracket_matching = not WeakAurasSaved.editor_bracket_matching
          end
        },
      level)
      UIDropDownMenu_AddButton(
        {
          text = L["Indent Size"],
          hasArrow = true,
          notCheckable = true,
          menuList = "spaces"
        },
      level)
      UIDropDownMenu_AddButton(
        {
          text = WeakAuras.newFeatureString .. L["Font Size"],
          hasArrow = true,
          notCheckable = true,
          menuList = "sizes"
        },
      level)
    elseif menu == "spaces" then
      local spaces = {2,4}
      for _, i in pairs(spaces) do
        UIDropDownMenu_AddButton(
          {
            text = i,
            isNotRadio = false,
            checked = function()
              return WeakAurasSaved.editor_tab_spaces == i
            end,
            func = function()
              WeakAurasSaved.editor_tab_spaces = i
              IndentationLib.enable(editor.editBox, color_scheme, WeakAurasSaved.editor_tab_spaces)
              editor.editBox:SetText(editor.editBox:GetText().."\n")
              IndentationLib.indentEditbox(editor.editBox)
            end
          },
        level)
      end
    elseif menu == "sizes" then
      local sizes = {10, 12, 14, 16}
      for _, i in pairs(sizes) do
        UIDropDownMenu_AddButton(
          {
            text = i,
            isNotRadio = false,
            checked = function()
              return WeakAurasSaved.editor_font_size == i
            end,
            func = function()
              WeakAurasSaved.editor_font_size = i
              editor.editBox:SetFont(fontPath, WeakAurasSaved.editor_font_size)
            end
          },
        level)
      end
    end
  end
  UIDropDownMenu_Initialize(dropdown, settings_dropdown_initialize, "MENU")

  settings_frame:SetScript(
    "OnClick",
    function(self, button, down)
      ToggleDropDownMenu(1, nil, dropdown, settings_frame, 0, 0)
    end
  )

  -- Make Snippets button (top right, near the line number)
  local snippetsButton = CreateFrame("Button", "WASnippetsButton", group.frame, "UIPanelButtonTemplate")
  snippetsButton:SetPoint("BOTTOMRIGHT", editor.frame, "TOPRIGHT", -20, -10)
  snippetsButton:SetFrameLevel(group.frame:GetFrameLevel() + 2)
  snippetsButton:SetHeight(20)
  snippetsButton:SetWidth(100)
  snippetsButton:SetText(L["Snippets"])
  snippetsButton:RegisterForClicks("LeftButtonUp")

  -- Get the saved snippets from SavedVars
  WeakAurasOptionsSaved.savedSnippets = WeakAurasOptionsSaved.savedSnippets or {}
  local savedSnippets = WeakAurasOptionsSaved.savedSnippets

  -- function to build snippet selection list
  local function UpdateSnippets(frame)
    -- release first before rebuilding
    frame:ReleaseChildren()
    table.sort(
      savedSnippets,
      function(a, b)
        return a.name < b.name
      end
    )

    local heading1 = AceGUI:Create("Heading")
    heading1:SetText(L["Premade Snippets"])
    heading1:SetRelativeWidth(0.7)
    frame:AddChild(heading1)

    -- Iterate premade snippets and make buttons for them
    for order, snippet in ipairs(premadeSnippets) do
      local button = AceGUI:Create("WeakAurasSnippetButton")
      button:SetTitle(snippet.name)
      button:SetDescription(snippet.snippet)
      button:SetCallback(
        "OnClick",
        function()
          editor.editBox:Insert(snippet.snippet)
          editor:SetFocus()
        end
      )
      button:SetRelativeWidth(1)
      frame:AddChild(button)
    end

    local heading2 = AceGUI:Create("Heading")
    heading2:SetText(L["Your Saved Snippets"])
    heading2:SetRelativeWidth(1)
    frame:AddChild(heading2)

    -- iterate saved snippets and make buttons
    for order, snippet in ipairs(savedSnippets) do
      local button = AceGUI:Create("WeakAurasSnippetButton")
      local snippetInsert = gsub(snippet.snippet, "|", "||")
      button:SetTitle(snippet.name)
      button:SetDescription(snippetInsert)
      button:SetEditable(true)
      button:SetRelativeWidth(1)
      button:SetNew(snippet.new)
      snippet.new = false
      button:SetCallback(
        "OnClick",
        function()
          editor.editBox:Insert(snippetInsert)
          editor:SetFocus()
        end
      )
      button.deleteButton:SetScript(
        "OnClick",
        function()
          table.remove(savedSnippets, order)
          UpdateSnippets(frame)
        end
      )
      button:SetCallback(
        "OnEnterPressed",
        function()
          local newName = button.renameEditBox:GetText()
          if newName and #newName > 0 then
            local found = false
            for _, snippet in ipairs(savedSnippets) do
              if snippet.name == newName then
                found = true
                break
              end
            end
            if not found then
              savedSnippets[order].name = newName
              UpdateSnippets(frame)
            end
          end
        end
      )
      frame:AddChild(button)
    end
  end

  local apiSearchFrame

  -- Make sidebar for snippets
  local snippetsFrame = CreateFrame("Frame", "WeakAurasSnippets", group.frame)
  WeakAuras.XMLTemplates["PortraitFrameTemplate"](snippetsFrame)
  snippetsFrame:HidePortrait()
  snippetsFrame:SetPoint("TOPLEFT", group.frame, "TOPRIGHT", 20, 0)
  snippetsFrame:SetPoint("BOTTOMLEFT", group.frame, "BOTTOMRIGHT", 20, 0)
  snippetsFrame:SetWidth(250)

  -- Add button to save new snippet
  local AddSnippetButton = CreateFrame("Button", nil, snippetsFrame, "UIPanelButtonTemplate")
  AddSnippetButton:SetPoint("TOPLEFT", snippetsFrame, "TOPLEFT", 13, -25)
  AddSnippetButton:SetPoint("TOPRIGHT", snippetsFrame, "TOPRIGHT", -13, -25)
  AddSnippetButton:SetHeight(20)
  AddSnippetButton:SetText(L["Add Snippet"])
  AddSnippetButton:RegisterForClicks("LeftButtonUp")

  -- house the buttons in a scroll frame
  -- All AceGUI from this point, so that buttons can be released and reused
  local snippetsScrollContainer = AceGUI:Create("SimpleGroup")
  snippetsScrollContainer:SetFullWidth(true)
  snippetsScrollContainer:SetFullHeight(true)
  snippetsScrollContainer:SetLayout("Fill")
  snippetsScrollContainer.frame:SetParent(snippetsFrame)
  snippetsScrollContainer.frame:SetPoint("TOPLEFT", snippetsFrame, "TOPLEFT", 17, -50)
  snippetsScrollContainer.frame:SetPoint("BOTTOMRIGHT", snippetsFrame, "BOTTOMRIGHT", -10, 10)
  local snippetsScroll = AceGUI:Create("ScrollFrame")
  snippetsScroll:SetLayout("List")
  snippetsScrollContainer:AddChild(snippetsScroll)
  snippetsScroll:FixScroll(true)
  snippetsScroll.scrollframe:SetScript(
    "OnScrollRangeChanged",
    function(frame)
      frame.obj:DoLayout()
    end
  )

  snippetsFrame:Hide()

  -- Toggle the side bar on click
  snippetsButton:SetScript(
    "OnClick",
    function(self, button, down)
      if not snippetsFrame:IsShown() then
        snippetsFrame:Show()
        if apiSearchFrame and apiSearchFrame:IsShown() then
          apiSearchFrame:Hide()
        end
        UpdateSnippets(snippetsScroll)
      else
        snippetsFrame:Hide()
      end
    end
  )

  AddSnippetButton:SetScript(
    "OnClick",
    function(self)
      local snippet = editor.editBox:GetText()
      if snippet and #snippet > 0 then
        local baseName, name, index = "New Snippet", "New Snippet", 0
        local snippetExists = function(name)
          for _, snippet in ipairs(savedSnippets) do
            if snippet.name == name then
              return true
            end
          end
        end
        while snippetExists(name) do
          index = index + 1
          name = format("%s %d", baseName, index)
        end
        table.insert(savedSnippets, {name = name, snippet = snippet, new = true})
        UpdateSnippets(snippetsScroll)
        end
      end
  )

  -- Make ApiSearch button
  local apiSearchButton = CreateFrame("Button", "WAAPISearchButton", group.frame, "UIPanelButtonTemplate")
  apiSearchButton:SetPoint("BOTTOMRIGHT", editor.frame, "TOPRIGHT", -20, 15)
  apiSearchButton:SetFrameLevel(group.frame:GetFrameLevel() + 2)
  apiSearchButton:SetHeight(20)
  apiSearchButton:SetWidth(100)
  apiSearchButton:SetText(L["Search API"])
  apiSearchButton:RegisterForClicks("LeftButtonUp")

  -- Make sidebar for apiSearch
  apiSearchFrame = CreateFrame("Frame", "WeakAurasAPISearchFrame", group.frame)
  WeakAuras.XMLTemplates["PortraitFrameTemplate"](apiSearchFrame)
  apiSearchFrame:HidePortrait()
  apiSearchFrame:SetWidth(350)

  local makeAPISearch
  local APISearchTextChangeDelay = 0.3
  local APISearchCTimer

  -- filter line
  local filterInput = CreateFrame("EditBox", "WeakAurasAPISearchFilterInput", apiSearchFrame)
  WeakAuras.XMLTemplates["SearchBoxTemplate"](filterInput)
  filterInput:SetFrameLevel(5)
  filterInput:SetScript("OnTextChanged", function(self)
    WA_SearchBoxTemplate_OnTextChanged(self)
    if APISearchCTimer and WeakAuras.timer:TimeLeft(APISearchCTimer) then
      WeakAuras.timer:CancelTimer(APISearchCTimer)
    end
    APISearchCTimer = WeakAuras.timer:ScheduleTimer(
      function()
        makeAPISearch(filterInput:GetText())
      end,
      APISearchTextChangeDelay
    )
  end)
  filterInput:SetHeight(15)
  filterInput:SetPoint("TOPLEFT", apiSearchFrame, "TOPLEFT", 17, -30)
  filterInput:SetPoint("TOPRIGHT", apiSearchFrame, "TOPRIGHT", -10, -30)
  filterInput:SetFont(STANDARD_TEXT_FONT, 10)

  local apiSearchScrollContainer = AceGUI:Create("SimpleGroup")
  apiSearchScrollContainer:SetFullWidth(true)
  apiSearchScrollContainer:SetFullHeight(true)
  apiSearchScrollContainer:SetLayout("Fill")
  apiSearchScrollContainer.frame:SetParent(apiSearchFrame)
  apiSearchScrollContainer.frame:SetPoint("TOPLEFT", apiSearchFrame, "TOPLEFT", 17, -50)
  apiSearchScrollContainer.frame:SetPoint("BOTTOMRIGHT", apiSearchFrame, "BOTTOMRIGHT", -10, 10)

  local apiSearchScroll = AceGUI:Create("ScrollFrame")
  apiSearchScroll:SetLayout("List")
  apiSearchScrollContainer:AddChild(apiSearchScroll)
  apiSearchScroll:FixScroll(true)
  apiSearchScroll.scrollframe:SetScript(
    "OnScrollRangeChanged",
    function(frame)
      frame.obj:DoLayout()
    end
  )

  local snippetOnClickCallback = function(self)
    if self.isSystem then
      filterInput:SetText(self.name)
    else
      self.editor.editBox:Insert(self.name)
      self.editor:SetFocus()
    end
  end

  local function loadBlizzardAPIDocumentation()
    local apiAddonName = "APIDocumentation"
    local _, loaded = IsAddOnLoaded(apiAddonName)
    if not loaded then
      local ok, ret = LoadAddOn(apiAddonName)
      if not ok then
        local messages = { L["AddOn: APIDocumentation is %s."]:format(ret) }
        if ret == "DISABLED" then
          table.insert(messages, L["Please enable it in your AddOn list."])
        elseif ret == "MISSING" then
          table.insert(messages, L["Please install it."])
        end
        WeakAuras.prettyPrint(table.concat(messages, " "))
        return
      end
    end
    if type(APIDocumentation) ~= "table" or type(APIDocumentation.systems) ~= "table" then
      WeakAuras.prettyPrint(L["AddOn: APIDocumentation is not loaded correctly."])
      return
    end
    if #APIDocumentation.systems == 0 then
      APIDocumentation:OnLoad()
    end
    return true
  end

  local function addLine(results, apiInfo)
    local name
    if apiInfo.Type == "System" then
      name = apiInfo.Namespace
    elseif apiInfo.Type == "Function" then
      name = apiInfo:GetFullName()
    elseif apiInfo.Type == "Event" then
      name = apiInfo.LiteralName
    end
    table.insert(results, { name = name, apiInfo = apiInfo })
  end

  local function APIListSystems()
    local results = {}
    for i, systemInfo in ipairs(APIDocumentation.systems) do
      if systemInfo.Namespace and #systemInfo.Functions > 0 then
        addLine(results, systemInfo)
      end
    end
    table.sort(results, function(a, b)
      return a.name < b.name
    end)
    return results
  end

  local function APISearch(word)
    local lowerWord = word:lower()
    local results = {}

    -- if search match name of namespace, show all functions & events for the namespace, and also show all other functions & events matching the search
    -- if search is composed with name of a namespace and a word separated by a dot, show matching function for matching namespace

    local nsName, rest = lowerWord:match("^([%w%_]+)(.*)")
    local funcName = rest and rest:match("^%.([%w%_]+)")

    for _, systemInfo in ipairs(APIDocumentation.systems) do
      -- search for namespaceName or namespaceName.functionName
      local systemMatch = nsName and #nsName >= 4
        and systemInfo.Namespace and systemInfo.Namespace:lower():match(nsName)

      for _, apiInfo in ipairs(systemInfo.Functions) do
        if systemMatch then
          if funcName then
            if apiInfo:MatchesSearchString(funcName) then
              addLine(results, apiInfo)
            end
          else
            addLine(results, apiInfo)
          end
        else
          if apiInfo:MatchesSearchString(lowerWord) then
            addLine(results, apiInfo)
          end
        end
      end

      if systemMatch and rest == "" then
        for _, apiInfo in ipairs(systemInfo.Events) do
          addLine(results, apiInfo)
        end
      else
        for _, apiInfo in ipairs(systemInfo.Events) do
          if apiInfo:MatchesSearchString(lowerWord) then
            addLine(results, apiInfo)
          end
        end
      end
    end

    return results
  end

  local lastSearch = nil
  makeAPISearch = function(apiToSearchFor)
    if not loadBlizzardAPIDocumentation() then
      return
    end
    local results
    if not apiToSearchFor or #apiToSearchFor < 4 then
      if lastSearch == "" then return end
      results = APIListSystems()
      lastSearch = ""
    else
      if lastSearch == apiToSearchFor then return end
      results = APISearch(apiToSearchFor)
      lastSearch = apiToSearchFor
    end
    apiSearchScroll:ReleaseChildren()
    for _, element in ipairs(results) do
      local apiInfo = element.apiInfo
      if apiInfo then
        local button = AceGUI:Create("WeakAurasSnippetButton")
        button:SetTitle(element.name)
        button:SetEditable(false)
        button:SetHeight(20)
        button:SetRelativeWidth(1)
        if apiInfo.Type ~= "System" and apiInfo.GetDetailedOutputLines then
          local desc = table.concat(apiInfo:GetDetailedOutputLines(), "\n")
          button:SetDescription(desc)
        else
          button:SetDescription()
        end
        button.name = element.name
        button.editor = editor
        button.isSystem = apiInfo.Type == "System"
        button:SetCallback("OnClick", snippetOnClickCallback)
        apiSearchScroll:AddChild(button)
      end
    end
  end

  apiSearchFrame:Hide()

  -- Toggle the side bar on click
  apiSearchButton:SetScript(
    "OnClick",
    function()
      if apiSearchFrame:IsShown() then
        apiSearchFrame:Hide()
      else
        apiSearchFrame:Show()
        apiSearchFrame:ClearAllPoints()
        apiSearchFrame:SetPoint("TOPLEFT", group.frame, "TOPRIGHT", 20, 0)
        apiSearchFrame:SetPoint("BOTTOMLEFT", group.frame, "BOTTOMRIGHT", 20, 0)
        filterInput:SetFocus()
        if snippetsFrame and snippetsFrame:IsShown() then
          snippetsFrame:Hide()
        end
      end
    end
  )

  editor.editBox.timeMachine = {}
  editor.editBox.timeMachinePos = 1
  local TimeMachineMaximumRollback = 10

  -- These events aren’t supported in the editbox, so we add undo/redo buttons below instead...
  --[[editor.editBox:HookScript(
    "OnKeyDown",
    function(self, key)
      -- CTRL + S saves and closes
      if IsControlKeyDown() and key == "S" then
        group:Close()
      elseif key == "Z" and IsControlKeyDown() then
        self:SetPropagateKeyboardInput(false)
        if self.timeMachine[self.timeMachinePos + 1] then
          self.timeMachinePos = self.timeMachinePos + 1
          self.skipOnTextChanged = true
          originalSetText(self, self.timeMachine[self.timeMachinePos][1])
          self:SetCursorPosition(self.timeMachine[self.timeMachinePos][2])
        end
      elseif key == "Y" and IsControlKeyDown() then
        self:SetPropagateKeyboardInput(false)
        if self.timeMachine[self.timeMachinePos - 1] then
          self.timeMachinePos = self.timeMachinePos - 1
          self.skipOnTextChanged = true
          originalSetText(self, self.timeMachine[self.timeMachinePos][1])
          self:SetCursorPosition(self.timeMachine[self.timeMachinePos][2])
        end
      end
    end
  )]]

  editor.editBox:HookScript(
    "OnTextChanged",
    function(self, userInput)
      local str = editor.editBox:GetText()
      if not str or str:trim() == "" or editor.combinedText == true then
        self.group.editorError:SetText("")
      else
        local func, errorString
        if (self.group.enclose) then
          func, errorString = OptionsPrivate.Private.LoadFunction("return function() " .. str .. "\n end", self.group.data.id, true)
        else
          func, errorString = OptionsPrivate.Private.LoadFunction("return " .. str, self.group.data.id, true)
        end
        if not errorString and self.group.validator then
          errorString = self.group.validator(func)
        end
        if errorString then
          if self.url then
            helpButton:Show()
          end
          self.group.editorError:Show()
          self.group.editorError:SetText(errorString)
        else
          self.group.editorError:SetText("")
        end
      end

      if not userInput then return end
      if self.skipOnTextChanged then
        self.skipOnTextChanged = false
        return
      end
      local cursorPosition = self:GetCursorPosition()
      local text = originalGetText(self)
      if IndentationLib then
        text, cursorPosition = IndentationLib.stripWowColorsWithPos(text, cursorPosition)
      end
      if self.timeMachine[1] and text == self.timeMachine[1][1] then
        return
      end
      -- if cursor is not at position 1, remove elements before cursor
      for i = 2, self.timeMachinePos do
        table.remove(self.timeMachine, 1)
      end
      -- insert current text
      table.insert(self.timeMachine, 1, {text, cursorPosition - 1})
      -- timeMachine is limited to a number of TimeMachineMaximumRollback elements
      for i = #self.timeMachine, TimeMachineMaximumRollback + 1, -1 do
        table.remove(self.timeMachine, i)
      end
      self.timeMachinePos = 1
    end
  )

  -- bracket matching
  editor.editBox:HookScript(
    "OnChar",
    function(_, char)
      if not IsControlKeyDown() and WeakAurasSaved.editor_bracket_matching then
        if char == "(" then
          editor.editBox:Insert(")")
          editor.editBox:SetCursorPosition(editor.editBox:GetCursorPosition() - 1)
        elseif char == "{" then
          editor.editBox:Insert("}")
          editor.editBox:SetCursorPosition(editor.editBox:GetCursorPosition() - 1)
        elseif char == "[" then
          editor.editBox:Insert("]")
          editor.editBox:SetCursorPosition(editor.editBox:GetCursorPosition() - 1)
        end
      end
    end
  )

  local editorError = group.frame:CreateFontString(nil, "OVERLAY")
  editorError:SetFont(STANDARD_TEXT_FONT, 12)
  editorError:SetJustifyH("LEFT")
  editorError:SetJustifyV("TOP")
  editorError:SetTextColor(1, 0, 0)
  editorError:SetPoint("LEFT", helpButton, "RIGHT", 0, 4)
  editorError:SetPoint("RIGHT", settings_frame, "LEFT")
  group.editorError = editorError

  local editorLine = CreateFrame("EditBox", nil, group.frame)
  WeakAuras.XMLTemplates["InputBoxTemplate"](editorLine)
  -- Set script on enter pressed..
  editorLine:SetPoint("RIGHT", snippetsButton, "LEFT", -10, 0)
  editorLine:SetFont(STANDARD_TEXT_FONT, 10)
  editorLine:SetJustifyH("RIGHT")
  editorLine:SetWidth(30)
  editorLine:SetHeight(20)
  editorLine:SetNumeric(true)
  editorLine:SetTextInsets(0, 5, 0, 0)
  editorLine:SetAutoFocus(false)

  local editorLineText = group.frame:CreateFontString(nil, "OVERLAY")
  editorLineText:SetFont(STANDARD_TEXT_FONT, 10)
  editorLineText:SetTextColor(1, 1, 1)
  editorLineText:SetText(L["Line"])
  editorLineText:SetPoint("RIGHT", editorLine, "LEFT", -8, 0)

  local redoButton = CreateFrame("Button", nil, editorLine)
  redoButton:SetPoint("RIGHT", editorLineText, "LEFT", -10, 0)
  redoButton:SetSize(20, 20)
  redoButton:SetNormalTexture("Interface\\AddOns\\WeakAuras\\Media\\Textures\\undo")
  local redoNormal = redoButton:GetNormalTexture()
  redoNormal:SetAllPoints()
  redoNormal:SetTexCoord(1, 0, 0, 1)
  redoNormal:SetBlendMode("BLEND")
  redoButton:SetHighlightTexture("Interface\\AddOns\\WeakAuras\\Media\\Textures\\undo-highlight")
  local redoHighlight = redoButton:GetHighlightTexture()
  redoHighlight:SetAllPoints()
  redoHighlight:SetTexCoord(1, 0, 0, 1)
  redoHighlight:SetBlendMode("BLEND")

  local undoButton = CreateFrame("Button", nil, redoButton)
  undoButton:SetPoint("RIGHT", redoButton, "LEFT", -10, 0)
  undoButton:SetSize(20, 20)
  undoButton:SetNormalTexture("Interface\\AddOns\\WeakAuras\\Media\\Textures\\undo")
  local undoNormal = undoButton:GetNormalTexture()
  undoNormal:SetAllPoints()
  undoNormal:SetTexCoord(0, 1, 0, 1)
  undoNormal:SetBlendMode("BLEND")
  undoButton:SetHighlightTexture("Interface\\AddOns\\WeakAuras\\Media\\Textures\\undo-highlight")
  local undoHighlight = undoButton:GetHighlightTexture()
  undoHighlight:SetAllPoints()
  undoHighlight:SetTexCoord(0, 1, 0, 1)
  undoHighlight:SetBlendMode("BLEND")

  redoButton:SetScript("OnClick", function()
    local self = editor.editBox
    if self.timeMachine[self.timeMachinePos - 1] then
      self.timeMachinePos = self.timeMachinePos - 1
      self.skipOnTextChanged = true
      originalSetText(self, self.timeMachine[self.timeMachinePos][1])
      self:SetCursorPosition(self.timeMachine[self.timeMachinePos][2])
    end
  end)

  undoButton:SetScript("OnClick", function()
    local self = editor.editBox
    if self.timeMachine[self.timeMachinePos + 1] then
      self.timeMachinePos = self.timeMachinePos + 1
      self.skipOnTextChanged = true
      originalSetText(self, self.timeMachine[self.timeMachinePos][1])
      self:SetCursorPosition(self.timeMachine[self.timeMachinePos][2])
    end
  end)

  helpButton:SetScript("OnClick", function()
    OptionsPrivate.ToggleTip(helpButton, group.url, L["Help"], "")
  end)

  local oldOnCursorChanged = editor.editBox:GetScript("OnCursorChanged")
  editor.editBox:SetScript(
    "OnCursorChanged",
    function(...)
      oldOnCursorChanged(...)
      local cursorPosition = editor.editBox:GetCursorPosition()
      local next = -1
      local line = 0
      while (next and cursorPosition >= next) do
        next = originalGetText(editor.editBox):find("[\n]", next + 1)
        line = line + 1
      end
      editorLine:SetNumber(line)
    end
  )

  editorLine:SetScript(
    "OnEnterPressed",
    function()
      local newLine = editorLine:GetNumber()
      local newPosition = 0
      while (newLine > 1 and newPosition) do
        newPosition = originalGetText(editor.editBox):find("[\n]", newPosition + 1)
        newLine = newLine - 1
      end

      if (newPosition) then
        editor.editBox:SetCursorPosition(newPosition)
        editor.editBox:SetFocus()
      end
    end
  )

  function group.Open(self, data, path, enclose, multipath, reloadOptions, setOnParent, url, validator)
    self.data = data
    self.path = path
    self.multipath = multipath
    self.reloadOptions = reloadOptions
    self.setOnParent = setOnParent
    self.url = url
    self.enclose = enclose
    self.validator = validator
    if url then
      helpButton:Show()
    else
      helpButton:Hide()
    end
    if (frame.window == "texture") then
      local texturepicker = OptionsPrivate.TexturePicker(frame, true)
      if texturepicker then
        texturepicker:CancelClose()
      end
    elseif (frame.window == "icon") then
      local iconpicker = OptionsPrivate.IconPicker(frame, true)
      if iconpicker then
        iconpicker:CancelClose()
      end
    end
    frame.window = "texteditor"
    frame:UpdateFrameVisible()
    local title = (type(data.id) == "string" and data.id or L["Temporary Group"]) .. " -"
    if (not multipath) then
      for index, field in pairs(path) do
        if (type(field) == "number") then
          field = "Trigger " .. field
        end
        title = title .. " " .. field:sub(1, 1):upper() .. field:sub(2)
      end
    end
    editor:SetLabel(title)
    editor.editBox.timeMachine = {}
    editor.editBox.timeMachinePos = 1
    editor.editBox:SetScript(
      "OnEscapePressed",
      function()
        -- catch it so that escape doesn't default to losing focus (after which another escape would close config)
      end
    )

    if setOnParent then
      editor:SetText(OptionsPrivate.Private.ValueFromPath(data, path) or "")
    else
      local singleText
      local sameTexts = true
      local combinedText = ""
      for child in OptionsPrivate.Private.TraverseLeafsOrAura(data) do
        local text
        if multipath then
          text = path[child.id] and OptionsPrivate.Private.ValueFromPath(child, path[child.id])
        else
          text = OptionsPrivate.Private.ValueFromPath(child, path)
        end
        if text then
          if not (singleText) then
            singleText = text
          else
            if singleText ~= text then
              sameTexts = false
            end
          end
          if combinedText ~= "" then
            combinedText = combinedText .. "\n\n"
          end

          combinedText =
            combinedText .. L["-- Do not remove this comment, it is part of this aura: "] .. child.id .. "\n"
          combinedText = combinedText .. (text or "")
        end
      end
      if (sameTexts) then
        editor:SetText(singleText or "")
        editor.combinedText = false
      else
        editor:SetText(combinedText)
        editor.combinedText = true
      end
    end
    editor:SetFocus()
  end

  function group.CancelClose(self)
    editor:ClearFocus()
    frame:HideTip()
    frame.window = "default"
    frame:UpdateFrameVisible()
  end

  local function extractTexts(input)
    local texts = {}

    local currentPos, id, startIdLine, startId, endId, endIdLine
    while (true) do
      startIdLine, startId =
        string.find(input, L["-- Do not remove this comment, it is part of this aura: "], currentPos, true)
      if (not startId) then
        break
      end

      endId, endIdLine = string.find(input, "\n", startId, true)
      if (not endId) then
        break
      end

      if (currentPos) then
        local trimmedPosition = startIdLine - 1
        while (string.sub(input, trimmedPosition, trimmedPosition) == "\n") do
          trimmedPosition = trimmedPosition - 1
        end

        texts[id] = string.sub(input, currentPos, trimmedPosition)
      end

      id = string.sub(input, startId + 1, endId - 1)

      currentPos = endIdLine + 1
    end

    if (id) then
      texts[id] = string.sub(input, currentPos, string.len(input))
    end

    return texts
  end

  function group.Close(self)
    if self.setOnParent then
      OptionsPrivate.Private.ValueToPath(self.data, self.path, editor:GetText())
      WeakAuras.Add(self.data)
    else
      local textById = editor.combinedText and extractTexts(editor:GetText())
      for child in OptionsPrivate.Private.TraverseLeafsOrAura(self.data) do
        local text = editor.combinedText and (textById[child.id] or "") or editor:GetText()
        OptionsPrivate.Private.ValueToPath(child, self.multipath and self.path[child.id] or self.path, text)
        WeakAuras.Add(child)
        OptionsPrivate.ClearOptions(child.id)
      end
    end

    WeakAuras.ClearAndUpdateOptions(self.data.id)

    editor:ClearFocus()

    frame.window = "default"
    frame:UpdateFrameVisible()
    WeakAuras.FillOptions()
  end

  return group
end

function OptionsPrivate.TextEditor(frame, noConstruct)
  textEditor = textEditor or (not noConstruct and ConstructTextEditor(frame))
  return textEditor
end
