local ADDON_NAME = ...
local NTP = _G.NotThatPortal or {}
_G.NotThatPortal = NTP

local function SafeMeta(field, fallback)
  if C_AddOns and C_AddOns.GetAddOnMetadata then
    return C_AddOns.GetAddOnMetadata(ADDON_NAME, field) or fallback
  end
  return fallback
end

NTP.META = {
  Title = SafeMeta("Title", ADDON_NAME),
  Version = SafeMeta("Version", "?.?.?"),
  Author = SafeMeta("Author", "Unknown"),
}

local ART = {
  parchment = "Interface\\AddOns\\NotThatPortal\\Art\\ui_partchment.png",
  check = "Interface\\AddOns\\NotThatPortal\\Art\\checkmark.png",
}

local COLOR = {
  border = { 0.55, 0.46, 0.28, 1 },
  bg = { 0.00, 0.00, 0.00, 0.62 },
  bgHover = { 0.12, 0.12, 0.12, 0.74 },
  bgSelected = { 0.42, 0.33, 0.18, 0.68 },
  gold = { 1.00, 0.86, 0.20, 1 },
}

local function GetOrderedPortals()
  local out = {}

  if type(NTP.PortalOrder) == "table" and #NTP.PortalOrder > 0 then
    for _, spellID in ipairs(NTP.PortalOrder) do
      if NTP.PortalByID and NTP.PortalByID[spellID] then
        table.insert(out, spellID)
      end
    end
    return out
  end

  if type(NTP.PortalByID) == "table" then
    for spellID in pairs(NTP.PortalByID) do
      table.insert(out, spellID)
    end
    table.sort(out)
  end

  return out
end

local function CreateBorder(frame, r, g, b, a)
  r = r or COLOR.border[1]
  g = g or COLOR.border[2]
  b = b or COLOR.border[3]
  a = a or COLOR.border[4]

  frame.borderTop = frame:CreateTexture(nil, "BORDER")
  frame.borderTop:SetPoint("TOPLEFT", -1, 1)
  frame.borderTop:SetPoint("TOPRIGHT", 1, 1)
  frame.borderTop:SetHeight(1)
  frame.borderTop:SetColorTexture(r, g, b, a)

  frame.borderBottom = frame:CreateTexture(nil, "BORDER")
  frame.borderBottom:SetPoint("BOTTOMLEFT", -1, -1)
  frame.borderBottom:SetPoint("BOTTOMRIGHT", 1, -1)
  frame.borderBottom:SetHeight(1)
  frame.borderBottom:SetColorTexture(r, g, b, a)

  frame.borderLeft = frame:CreateTexture(nil, "BORDER")
  frame.borderLeft:SetPoint("TOPLEFT", -1, 1)
  frame.borderLeft:SetPoint("BOTTOMLEFT", -1, -1)
  frame.borderLeft:SetWidth(1)
  frame.borderLeft:SetColorTexture(r, g, b, a)

  frame.borderRight = frame:CreateTexture(nil, "BORDER")
  frame.borderRight:SetPoint("TOPRIGHT", 1, 1)
  frame.borderRight:SetPoint("BOTTOMRIGHT", 1, -1)
  frame.borderRight:SetWidth(1)
  frame.borderRight:SetColorTexture(r, g, b, a)
end

local function CreateFlatPanel(parent, alpha)
  local panel = CreateFrame("Frame", nil, parent)
  panel.bg = panel:CreateTexture(nil, "BACKGROUND")
  panel.bg:SetAllPoints(true)
  panel.bg:SetColorTexture(0, 0, 0, alpha or COLOR.bg[4])
  CreateBorder(panel)
  return panel
end

local function CreateFlatButton(parent, width, height, text)
  local button = CreateFrame("Button", nil, parent)
  button:SetSize(width, height)

  button.bg = button:CreateTexture(nil, "BACKGROUND")
  button.bg:SetAllPoints(true)
  button.bg:SetColorTexture(COLOR.bg[1], COLOR.bg[2], COLOR.bg[3], COLOR.bg[4])

  CreateBorder(button)

  button.text = button:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
  button.text:SetPoint("CENTER")
  button.text:SetText(text or "")
  button.text:SetTextColor(0.96, 0.96, 0.96, 1)

  button:SetScript("OnEnter", function(self)
    if self._selected then
      return
    end
    self.bg:SetColorTexture(COLOR.bgHover[1], COLOR.bgHover[2], COLOR.bgHover[3], COLOR.bgHover[4])
  end)

  button:SetScript("OnLeave", function(self)
    if self._selected then
      return
    end
    self.bg:SetColorTexture(COLOR.bg[1], COLOR.bg[2], COLOR.bg[3], COLOR.bg[4])
  end)

  button:SetScript("OnMouseDown", function(self)
    if self._selected then
      return
    end
    self.bg:SetColorTexture(0.18, 0.16, 0.12, 0.75)
  end)

  button:SetScript("OnMouseUp", function(self)
    if self._selected then
      return
    end
    self.bg:SetColorTexture(COLOR.bgHover[1], COLOR.bgHover[2], COLOR.bgHover[3], COLOR.bgHover[4])
  end)

  function button:SetSelected(selected)
    self._selected = selected and true or false
    if self._selected then
      self.bg:SetColorTexture(COLOR.bgSelected[1], COLOR.bgSelected[2], COLOR.bgSelected[3], COLOR.bgSelected[4])
      self.text:SetTextColor(COLOR.gold[1], COLOR.gold[2], COLOR.gold[3], COLOR.gold[4])
    else
      self.bg:SetColorTexture(COLOR.bg[1], COLOR.bg[2], COLOR.bg[3], COLOR.bg[4])
      self.text:SetTextColor(0.96, 0.96, 0.96, 1)
    end
  end

  return button
end

local function CreateFlatCheckbox(parent, labelText, width)
  local row = CreateFrame("Button", nil, parent)
  row:SetSize(width or 420, 22)
  if row.SetMotionScriptsWhileDisabled then
    row:SetMotionScriptsWhileDisabled(true)
  end

  row.box = CreateFlatPanel(row, 0.72)
  row.box:SetSize(16, 16)
  row.box:SetPoint("LEFT", 0, 0)

  row.check = row.box:CreateTexture(nil, "ARTWORK")
  row.check:SetPoint("TOPLEFT", 0, 0)
  row.check:SetPoint("BOTTOMRIGHT", 0, 0)
  row.check:SetTexture(ART.check)
  row.check:SetVertexColor(COLOR.gold[1], COLOR.gold[2], COLOR.gold[3], 0.95)
  row.check:Hide()

  row.icon = row:CreateTexture(nil, "ARTWORK")
  row.icon:SetSize(16, 16)
  row.icon:SetPoint("LEFT", row.box, "RIGHT", 6, 0)
  row.icon:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
  row.icon:Hide()

  row.label = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
  row.label:SetPoint("LEFT", row.box, "RIGHT", 8, 0)
  row.label:SetPoint("RIGHT", row, "RIGHT", 0, 0)
  row.label:SetJustifyH("LEFT")
  row.label:SetText(labelText or "")

  function row:SetChecked(checked)
    self.checked = checked and true or false
    self.check:SetShown(self.checked)
  end

  function row:GetChecked()
    return self.checked == true
  end

  function row:SetIcon(texturePath)
    self.label:ClearAllPoints()
    if texturePath and texturePath ~= "" then
      self.icon:SetTexture(texturePath)
      self.icon:Show()
      self.label:SetPoint("LEFT", self.icon, "RIGHT", 6, 0)
    else
      self.icon:Hide()
      self.label:SetPoint("LEFT", self.box, "RIGHT", 8, 0)
    end
    self.label:SetPoint("RIGHT", self, "RIGHT", 0, 0)
  end

  function row:SetTooltipSpellID(spellID)
    self.tooltipSpellID = spellID
  end

  row:SetScript("OnClick", function(self)
    self:SetChecked(not self:GetChecked())
    if self.OnValueChanged then
      self:OnValueChanged(self:GetChecked())
    end
  end)

  row:SetScript("OnEnter", function(self)
    self.box.bg:SetColorTexture(0.10, 0.10, 0.10, 0.80)
    if self.tooltipSpellID and GameTooltip then
      GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
      GameTooltip:SetSpellByID(self.tooltipSpellID)
      GameTooltip:Show()
    end
  end)

  row:SetScript("OnLeave", function(self)
    self.box.bg:SetColorTexture(0, 0, 0, 0.72)
    if self.tooltipSpellID and GameTooltip then
      GameTooltip:Hide()
    end
  end)

  return row
end

local function CreateFlatInput(parent, width, height)
  local shell = CreateFlatPanel(parent, 0.72)
  shell:SetSize(width, height)

  local edit = CreateFrame("EditBox", nil, shell)
  edit:SetPoint("TOPLEFT", 6, -4)
  edit:SetPoint("BOTTOMRIGHT", -6, 4)
  edit:SetAutoFocus(false)
  edit:SetFontObject("ChatFontNormal")
  edit:SetTextColor(0.95, 0.95, 0.95, 1)

  edit:SetScript("OnEscapePressed", function(self)
    self:ClearFocus()
  end)

  edit:SetScript("OnEnterPressed", function(self)
    self:ClearFocus()
    if shell.OnSubmit then
      shell:OnSubmit(self:GetText() or "")
    end
  end)

  shell.edit = edit

  function shell:SetText(text)
    self.edit:SetText(tostring(text or ""))
  end

  function shell:GetText()
    return self.edit:GetText() or ""
  end

  function shell:SetNumeric(isNumeric)
    self.edit:SetNumeric(isNumeric and true or false)
  end

  return shell
end

local function CreateScrollBox(parent)
  local box = CreateFlatPanel(parent, 0.62)

  local scroll = CreateFrame("ScrollFrame", nil, box)
  scroll:SetPoint("TOPLEFT", 12, -12)
  scroll:SetPoint("BOTTOMRIGHT", -36, 12)
  scroll:EnableMouseWheel(true)

  local content = CreateFrame("Frame", nil, scroll)
  content:SetPoint("TOPLEFT", 0, 0)
  content:SetSize(1, 1)
  scroll:SetScrollChild(content)

  local slider = CreateFrame("Slider", nil, box, "UIPanelScrollBarTemplate")
  slider:SetPoint("TOPRIGHT", box, "TOPRIGHT", -6, -18)
  slider:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -6, 18)
  slider:SetMinMaxValues(0, 0)
  slider:SetValueStep(1)
  if slider.SetObeyStepOnDrag then
    slider:SetObeyStepOnDrag(false)
  end

  box.scroll = scroll
  box.content = content
  box.slider = slider

  function box:UpdateScroll()
    local view = self.scroll:GetHeight() or 0
    local total = self.content:GetHeight() or 0
    local range = math.max(0, total - view)

    self.slider:SetMinMaxValues(0, range)
    if range <= 0 then
      self.scroll:SetVerticalScroll(0)
      self.slider:SetValue(0)
      self.slider:Hide()
    else
      local current = self.scroll:GetVerticalScroll() or 0
      if current > range then
        current = range
        self.scroll:SetVerticalScroll(current)
      end
      self.slider:SetValue(current)
      self.slider:Show()
    end
  end

  slider:SetScript("OnValueChanged", function(_, value)
    box.scroll:SetVerticalScroll(value or 0)
  end)

  scroll:SetScript("OnMouseWheel", function(self, delta)
    local value = (box.slider:GetValue() or 0) - ((delta or 0) * 28)
    local minV, maxV = box.slider:GetMinMaxValues()
    if value < minV then
      value = minV
    elseif value > maxV then
      value = maxV
    end
    box.slider:SetValue(value)
    self:SetVerticalScroll(value)
  end)

  scroll:SetScript("OnSizeChanged", function()
    local w = scroll:GetWidth() or 1
    box.content:SetWidth(math.max(1, w))
    box:UpdateScroll()
  end)

  return box
end

function NTP:SetMinimapHidden(hidden)
  self.DB = self.DB or {}
  self.DB.minimap = self.DB.minimap or { hide = false }
  self.DB.minimap.hide = hidden and true or false

  local icon = LibStub and LibStub("LibDBIcon-1.0", true)
  if not icon then
    return
  end

  if self.DB.minimap.hide then
    icon:Hide("NotThatPortal")
  else
    icon:Show("NotThatPortal")
  end
end

function NTP:ShowMinimapMenu()
  if not self.MinimapMenu then
    self.MinimapMenu = CreateFrame("Frame", "NotThatPortal_MinimapMenu", UIParent, "UIDropDownMenuTemplate")
  end

  local function initMenu(_, level)
    local info = UIDropDownMenu_CreateInfo()

    info.isTitle = true
    info.text = "NotThatPortal"
    info.notCheckable = true
    UIDropDownMenu_AddButton(info, level)

    info = UIDropDownMenu_CreateInfo()
    info.text = "Open"
    info.notCheckable = true
    info.func = function()
      NTP:ToggleMainWindow()
    end
    UIDropDownMenu_AddButton(info, level)

    info = UIDropDownMenu_CreateInfo()
    info.text = (self.DB and self.DB.minimap and self.DB.minimap.hide) and "Show minimap button" or "Hide minimap button"
    info.notCheckable = true
    info.func = function()
      NTP:SetMinimapHidden(not (NTP.DB and NTP.DB.minimap and NTP.DB.minimap.hide))
    end
    UIDropDownMenu_AddButton(info, level)

    info = UIDropDownMenu_CreateInfo()
    info.text = "Reset window position"
    info.notCheckable = true
    info.func = function()
      NTP:ResetWindowPosition()
    end
    UIDropDownMenu_AddButton(info, level)

    info = UIDropDownMenu_CreateInfo()
    info.text = "Close"
    info.notCheckable = true
    info.func = function()
      CloseDropDownMenus()
    end
    UIDropDownMenu_AddButton(info, level)
  end

  UIDropDownMenu_Initialize(self.MinimapMenu, initMenu, "MENU")
  ToggleDropDownMenu(1, nil, self.MinimapMenu, "cursor", 0, 0)
end

function NTP:CreateMinimapButton()
  if self.MinimapButton then
    return
  end
  if not LibStub then
    return
  end

  self.DB = self.DB or {}
  self.DB.minimap = self.DB.minimap or { hide = false }

  local ldb = LibStub("LibDataBroker-1.1"):NewDataObject("NotThatPortal", {
    type = "data source",
    text = "NotThatPortal",
    icon = "Interface\\Icons\\Spell_Arcane_PortalDalaran",
    OnClick = function(_, button)
      if button == "LeftButton" then
        NTP:ToggleMainWindow()
      else
        NTP:ShowMinimapMenu()
      end
    end,
    OnTooltipShow = function(tt)
      tt:AddLine("NotThatPortal")
      tt:AddLine("Left-click: Open settings", 0.9, 0.9, 0.9)
      tt:AddLine("Right-click: Menu", 0.9, 0.9, 0.9)
      tt:AddLine("Slash: /ntp", 0.9, 0.9, 0.9)
    end,
  })

  local icon = LibStub("LibDBIcon-1.0", true)
  if icon then
    icon:Register("NotThatPortal", ldb, self.DB.minimap)
    self.MinimapButton = icon:GetMinimapButton("NotThatPortal")
    if self.DB.minimap.hide then
      icon:Hide("NotThatPortal")
    end
  end
end

function NTP:ResetWindowPosition()
  self.DB = self.DB or {}
  self.DB.settings = self.DB.settings or {}
  self.DB.settings.window = self.DB.settings.window or {}
  self.DB.settings.window.point = "CENTER"
  self.DB.settings.window.x = 0
  self.DB.settings.window.y = 0

  if self.MainFrame then
    self.MainFrame:ClearAllPoints()
    self.MainFrame:SetPoint("CENTER")
  end
end

local function SaveWindowPosition(frame)
  if not NTP.DB then
    return
  end

  local point, _, _, x, y = frame:GetPoint(1)
  NTP.DB.settings = NTP.DB.settings or {}
  NTP.DB.settings.window = NTP.DB.settings.window or {}
  NTP.DB.settings.window.point = point or "CENTER"
  NTP.DB.settings.window.x = x or 0
  NTP.DB.settings.window.y = y or 0
end

local function CreatePortalTab(parent)
  local tab = CreateFrame("Frame", nil, parent)
  tab:SetAllPoints(true)

  tab.header = tab:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
  tab.header:SetPoint("TOPLEFT", 8, -8)
  tab.header:SetWidth(760)
  tab.header:SetJustifyH("LEFT")
  tab.header:SetText("Choose which portal destinations are undesirable:")

  tab.scrollBox = CreateScrollBox(tab)
  tab.scrollBox:SetPoint("TOPLEFT", tab.header, "BOTTOMLEFT", 0, -8)
  tab.scrollBox:SetPoint("BOTTOMRIGHT", tab, "BOTTOMRIGHT", -8, 8)

  tab.rows = {}
  return tab
end

local function CreateOptionsTab(parent)
  local tab = CreateFrame("Frame", nil, parent)
  tab:SetAllPoints(true)

  tab.header = tab:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
  tab.header:SetPoint("TOPLEFT", 8, -8)
  tab.header:SetWidth(760)
  tab.header:SetJustifyH("LEFT")
  tab.header:SetText("Notification settings:")

  tab.announceCheck = CreateFlatCheckbox(tab, "Send warning to party/raid/instance chat", 620)
  tab.announceCheck:SetPoint("TOPLEFT", tab.header, "BOTTOMLEFT", 0, -10)

  tab.echoCheck = CreateFlatCheckbox(tab, "Also echo warning into your own chat frame", 620)
  tab.echoCheck:SetPoint("TOPLEFT", tab.announceCheck, "BOTTOMLEFT", 0, -8)

  tab.soloTestCheck = CreateFlatCheckbox(tab, "Solo test mode (print warning when not in a group)", 620)
  tab.soloTestCheck:SetPoint("TOPLEFT", tab.echoCheck, "BOTTOMLEFT", 0, -8)

  tab.throttleLabel = tab:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
  tab.throttleLabel:SetPoint("TOPLEFT", tab.soloTestCheck, "BOTTOMLEFT", 0, -16)
  tab.throttleLabel:SetText("Throttle (seconds per caster+portal):")

  tab.throttleInput = CreateFlatInput(tab, 64, 24)
  tab.throttleInput:SetPoint("LEFT", tab.throttleLabel, "RIGHT", 8, 0)
  tab.throttleInput:SetNumeric(true)

  tab.throttleSave = CreateFlatButton(tab, 70, 24, "Save")
  tab.throttleSave:SetPoint("LEFT", tab.throttleInput, "RIGHT", 8, 0)

  tab.templateLabel = tab:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
  tab.templateLabel:SetPoint("TOPLEFT", tab.throttleLabel, "BOTTOMLEFT", 0, -20)
  tab.templateLabel:SetText("Warning text (%caster% and %portal% supported):")

  tab.templateInput = CreateFlatInput(tab, 520, 24)
  tab.templateInput:SetPoint("TOPLEFT", tab.templateLabel, "BOTTOMLEFT", 0, -8)

  tab.templateSave = CreateFlatButton(tab, 70, 24, "Save")
  tab.templateSave:SetPoint("LEFT", tab.templateInput, "RIGHT", 8, 0)

  tab.templateDefault = CreateFlatButton(tab, 70, 24, "Default")
  tab.templateDefault:SetPoint("LEFT", tab.templateSave, "RIGHT", 6, 0)

  tab.footer = tab:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
  tab.footer:SetPoint("TOPLEFT", tab.templateInput, "BOTTOMLEFT", 0, -8)
  tab.footer:SetWidth(760)
  tab.footer:SetJustifyH("LEFT")
  tab.footer:SetText("Example: [NotThatPortal] %caster% opened %portal% (marked undesirable).")

  return tab
end

function NTP:SelectTab(tabID)
  if not self.MainFrame then
    return
  end

  local f = self.MainFrame
  f.selectedTab = tabID

  f.portalsTab:SetShown(tabID == 1)
  f.optionsTab:SetShown(tabID == 2)

  f.tabPortals:SetSelected(tabID == 1)
  f.tabOptions:SetSelected(tabID == 2)
end

function NTP:CreateMainWindow()
  if self.MainFrame then
    return
  end

  local f = CreateFrame("Frame", "NotThatPortal_MainFrame", UIParent)
  f:SetSize(860, 650)
  f:SetFrameStrata("DIALOG")

  local win = self.DB and self.DB.settings and self.DB.settings.window
  if win and win.point then
    f:SetPoint(win.point, UIParent, win.point, tonumber(win.x) or 0, tonumber(win.y) or 0)
  else
    f:SetPoint("CENTER")
  end

  f:SetMovable(true)
  f:EnableMouse(true)
  f:RegisterForDrag("LeftButton")
  f:SetScript("OnDragStart", f.StartMoving)
  f:SetScript("OnDragStop", function(frame)
    frame:StopMovingOrSizing()
    SaveWindowPosition(frame)
  end)

  f.bg = f:CreateTexture(nil, "BACKGROUND")
  f.bg:SetAllPoints(true)
  f.bg:SetTexture(ART.parchment)

  f.topShade = f:CreateTexture(nil, "ARTWORK")
  f.topShade:SetPoint("TOPLEFT", 0, -1)
  f.topShade:SetPoint("TOPRIGHT", 0, -1)
  f.topShade:SetHeight(74)
  f.topShade:SetTexture(ART.parchment)
  f.topShade:SetTexCoord(0, 1, 0, 0.18)
  f.topShade:SetVertexColor(0.22, 0.18, 0.12, 0.92)

  f.title = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
  f.title:SetPoint("TOPLEFT", 18, -20)
  f.title:SetText("NotThatPortal")

  f.subtitle = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
  f.subtitle:SetPoint("TOPLEFT", f.title, "BOTTOMLEFT", 0, -2)
  f.subtitle:SetText(string.format("v%s  by %s", NTP.META.Version or "?", NTP.META.Author or "Unknown"))

  f.close = CreateFlatButton(f, 24, 22, "X")
  f.close:SetPoint("TOPRIGHT", -16, -16)
  f.close:SetScript("OnClick", function()
    f:Hide()
  end)

  f.tabPortals = CreateFlatButton(f, 220, 28, "Undesireable Portals")
  f.tabPortals:SetPoint("TOPLEFT", 18, -78)
  f.tabPortals:SetScript("OnClick", function()
    NTP:SelectTab(1)
  end)

  f.tabOptions = CreateFlatButton(f, 120, 28, "Options")
  f.tabOptions:SetPoint("LEFT", f.tabPortals, "RIGHT", 8, 0)
  f.tabOptions:SetScript("OnClick", function()
    NTP:SelectTab(2)
  end)

  f.content = CreateFrame("Frame", nil, f)
  f.content:SetPoint("TOPLEFT", 18, -114)
  f.content:SetPoint("BOTTOMRIGHT", -18, 14)

  f.portalsTab = CreatePortalTab(f.content)
  f.optionsTab = CreateOptionsTab(f.content)

  f.optionsTab.announceCheck.OnValueChanged = function(_, checked)
    NTP.DB.settings.announceToGroup = checked and true or false
  end

  f.optionsTab.echoCheck.OnValueChanged = function(_, checked)
    NTP.DB.settings.echoToSelf = checked and true or false
  end

  f.optionsTab.soloTestCheck.OnValueChanged = function(_, checked)
    NTP.DB.settings.soloTestMode = checked and true or false
  end

  f.optionsTab.throttleSave:SetScript("OnClick", function()
    local value = tonumber(f.optionsTab.throttleInput:GetText() or "") or 15
    value = math.max(1, math.min(300, math.floor(value + 0.5)))
    NTP.DB.settings.throttleSeconds = value
    f.optionsTab.throttleInput:SetText(tostring(value))
  end)

  f.optionsTab.templateSave:SetScript("OnClick", function()
    NTP.DB.settings.warnTemplate = tostring(f.optionsTab.templateInput:GetText() or "")
    f.optionsTab.templateInput:SetText(NTP:GetWarnTemplate())
  end)

  f.optionsTab.templateDefault:SetScript("OnClick", function()
    NTP.DB.settings.warnTemplate = NTP.DEFAULT_WARN_TEMPLATE or "Troll Portal Detected. Player: %caster%, Portal: %portal%."
    f.optionsTab.templateInput:SetText(NTP:GetWarnTemplate())
  end)

  self.MainFrame = f
  f:Hide()
end

function NTP:RefreshPortalList()
  if not self.MainFrame then
    return
  end

  local tab = self.MainFrame.portalsTab
  local order = GetOrderedPortals()

  local y = -8
  for i, spellID in ipairs(order) do
    local row = tab.rows[i]
    if not row then
      row = CreateFlatCheckbox(tab.scrollBox.content, "", 680)
      row:SetScript("OnClick", function(self)
        self:SetChecked(not self:GetChecked())
        if self.OnValueChanged then
          self:OnValueChanged(self:GetChecked())
        end
      end)
      tab.rows[i] = row
    end

    row:ClearAllPoints()
    row:SetPoint("TOPLEFT", tab.scrollBox.content, "TOPLEFT", 8, y)
    row:SetWidth(math.max(560, (tab.scrollBox.content:GetWidth() or 700) - 16))

    local name = self:GetPortalDisplayName(spellID)
    row.label:SetText(string.format("%s  |cff888888(%d)|r", name, spellID))
    local icon = (C_Spell and C_Spell.GetSpellTexture and C_Spell.GetSpellTexture(spellID))
    if (not icon or icon == "") and GetSpellTexture then
      icon = GetSpellTexture(spellID)
    end
    row:SetIcon(icon)
    row:SetTooltipSpellID(spellID)
    row:SetChecked(self:IsUndesirablePortal(spellID))
    row.OnValueChanged = function(_, checked)
      NTP:SetPortalUndesirable(spellID, checked)
    end
    row:Show()

    y = y - 22
  end

  for i = #order + 1, #tab.rows do
    tab.rows[i]:Hide()
  end

  tab.scrollBox.content:SetHeight(math.max(1, -y + 6))
  tab.scrollBox:UpdateScroll()
end

function NTP:RefreshOptionsTab()
  if not self.MainFrame or not self.DB or not self.DB.settings then
    return
  end

  local tab = self.MainFrame.optionsTab
  tab.announceCheck:SetChecked(self.DB.settings.announceToGroup == true)
  tab.echoCheck:SetChecked(self.DB.settings.echoToSelf == true)
  tab.soloTestCheck:SetChecked(self.DB.settings.soloTestMode == true)
  tab.throttleInput:SetText(tostring(self.DB.settings.throttleSeconds or 15))
  tab.templateInput:SetText(self:GetWarnTemplate())
end

function NTP:RefreshUI()
  self:RefreshPortalList()
  self:RefreshOptionsTab()
end

function NTP:ToggleMainWindow()
  if not self.MainFrame then
    self:CreateMainWindow()
  end

  if self.MainFrame:IsShown() then
    self.MainFrame:Hide()
    return
  end

  self:RefreshUI()
  self:SelectTab(self.MainFrame.selectedTab or 1)
  self.MainFrame:Show()
end
