--[[-----------------------------------------------------------------------------
TreeGroup Container
Container that uses a tree control to switch between groups.
-------------------------------------------------------------------------------]]
local Type, Version = 'TreeGroup', 30
local AceGUI = LibStub and LibStub('AceGUI-3.0', true)
if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then
  return
end

-- Lua APIs
local next, pairs, ipairs, assert, type = next, pairs, ipairs, assert, type
local math_min, math_max, floor = math.min, math.max, floor
local select, tremove, unpack = select, table.remove, unpack

-- WoW APIs
local CreateFrame, UIParent = CreateFrame, UIParent

-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
-- List them here for Mikk's FindGlobals script
-- GLOBALS: GameTooltip, FONT_COLOR_CODE_CLOSE

-- Recycling functions
local new, del
do
  local pool = setmetatable({}, { __mode = 'k' })
  function new()
    local t = next(pool)
    if t then
      pool[t] = nil
      return t
    else
      return {}
    end
  end
  function del(t)
    for k in pairs(t) do
      t[k] = nil
    end
    pool[t] = true
  end
end

local DEFAULT_TREE_WIDTH = 175
local DEFAULT_TREE_SIZABLE = true

--[[-----------------------------------------------------------------------------
Support functions
-------------------------------------------------------------------------------]]
local function GetButtonUniqueValue(line)
  local parent = line.parent
  if parent and parent.value then
    return GetButtonUniqueValue(parent) .. '\001' .. line.value
  else
    return line.value
  end
end

local function UpdateButton(button, treeline, selected, canExpand, isExpanded)
  local self = button.obj
  local toggle = button.toggle
  local frame = self.frame
  local text = treeline.text or ''
  local icon = treeline.icon
  local iconCoords = treeline.iconCoords
  local level = treeline.level
  local value = treeline.value
  local uniquevalue = treeline.uniquevalue
  local disabled = treeline.disabled

  button.treeline = treeline
  button.value = value
  button.uniquevalue = uniquevalue
  if selected then
    button:LockHighlight()
    button.selected = true
  else
    button:UnlockHighlight()
    button.selected = false
  end
  local normalTexture = button:GetNormalTexture()
  local line = button.line
  button.level = level
  if level == 1 then
    button:SetNormalFontObject('GameFontNormal')
    button:SetHighlightFontObject('GameFontHighlight')
    button.text:SetPoint('LEFT', (icon and 16 or 0) + 8, 2)
  else
    button:SetNormalFontObject('GameFontHighlightSmall')
    button:SetHighlightFontObject('GameFontHighlightSmall')
    button.text:SetPoint('LEFT', (icon and 16 or 0) + 8 * level, 2)
  end

  if disabled then
    button:EnableMouse(false)
    button.text:SetText('|cff808080' .. text .. FONT_COLOR_CODE_CLOSE)
  else
    button.text:SetText(text)
    button:EnableMouse(true)
  end

  if icon then
    button.icon:SetTexture(icon)
    button.icon:SetPoint('LEFT', 8 * level, (level == 1) and 0 or 1)
  else
    button.icon:SetTexture(nil)
  end

  if iconCoords then
    button.icon:SetTexCoord(unpack(iconCoords))
  else
    button.icon:SetTexCoord(0, 1, 0, 1)
  end

  if canExpand then
    if not isExpanded then
      toggle:SetNormalTexture('Interface\\Buttons\\UI-PlusButton-UP')
      toggle:SetPushedTexture('Interface\\Buttons\\UI-PlusButton-DOWN')
    else
      toggle:SetNormalTexture('Interface\\Buttons\\UI-MinusButton-UP')
      toggle:SetPushedTexture('Interface\\Buttons\\UI-MinusButton-DOWN')
    end
    toggle:Show()
  else
    toggle:Hide()
  end
end

local function ShouldDisplayLevel(tree)
  local result = false
  for k, v in ipairs(tree) do
    if v.children == nil and v.visible ~= false then
      result = true
    elseif v.children then
      result = result or ShouldDisplayLevel(v.children)
    end
    if result then
      return result
    end
  end
  return false
end

local function addLine(self, v, tree, level, parent)
  local line = new()
  line.value = v.value
  line.text = v.text
  line.icon = v.icon
  line.iconCoords = v.iconCoords
  line.disabled = v.disabled
  line.tree = tree
  line.level = level
  line.parent = parent
  line.visible = v.visible
  line.uniquevalue = GetButtonUniqueValue(line)
  if v.children then
    line.hasChildren = true
  else
    line.hasChildren = nil
  end
  self.lines[#self.lines + 1] = line
  return line
end

--fire an update after one frame to catch the treeframes height
local function FirstFrameUpdate(frame)
  local self = frame.obj
  frame:SetScript('OnUpdate', nil)
  self:RefreshTree()
end

local function BuildUniqueValue(...)
  local n = select('#', ...)
  if n == 1 then
    return ...
  else
    return (...) .. '\001' .. BuildUniqueValue(select(2, ...))
  end
end

--[[-----------------------------------------------------------------------------
Scripts
-------------------------------------------------------------------------------]]
local function Expand_OnClick(frame)
  local button = frame.button
  local self = button.obj
  local status = (self.status or self.localstatus).groups
  status[button.uniquevalue] = not status[button.uniquevalue]
  self:RefreshTree()
end

local function Button_OnClick(frame)
  local self = frame.obj
  self:Fire('OnClick', frame.uniquevalue, frame.selected)
  if not frame.selected then
    self:SetSelected(frame.uniquevalue)
    frame.selected = true
    frame:LockHighlight()
    self:RefreshTree()
  end
  AceGUI:ClearFocus()
end

local function Button_OnDoubleClick(button)
  local self = button.obj
  local status = self.status or self.localstatus
  local status = (self.status or self.localstatus).groups
  status[button.uniquevalue] = not status[button.uniquevalue]
  self:RefreshTree()
end

local function Button_OnEnter(frame)
  local self = frame.obj
  self:Fire('OnButtonEnter', frame.uniquevalue, frame)

  if self.enabletooltips then
    GameTooltip:SetOwner(frame, 'ANCHOR_NONE')
    GameTooltip:SetPoint('LEFT', frame, 'RIGHT')
    GameTooltip:SetText(frame.text:GetText() or '', 1, 0.82, 0, 1)

    GameTooltip:Show()
  end
end

local function Button_OnLeave(frame)
  local self = frame.obj
  self:Fire('OnButtonLeave', frame.uniquevalue, frame)

  if self.enabletooltips then
    GameTooltip:Hide()
  end
end

local function OnScrollValueChanged(frame, value)
  if frame.obj.noupdate then
    return
  end
  local self = frame.obj
  local status = self.status or self.localstatus
  status.scrollvalue = value
  self:RefreshTree()
  AceGUI:ClearFocus()
end

local function Tree_OnSizeChanged(frame)
  frame.obj:RefreshTree()
end

local function Tree_OnMouseWheel(frame, delta)
  local self = frame.obj
  if self.showscroll then
    local scrollbar = self.scrollbar
    local min, max = scrollbar:GetMinMaxValues()
    local value = scrollbar:GetValue()
    local newvalue = math_min(max, math_max(min, value - delta))
    if value ~= newvalue then
      scrollbar:SetValue(newvalue)
    end
  end
end

local function Dragger_OnLeave(frame)
  frame:SetBackdropColor(1, 1, 1, 0)
end

local function Dragger_OnEnter(frame)
  frame:SetBackdropColor(1, 1, 1, 0.8)
end

local function Dragger_OnMouseDown(frame)
  local treeframe = frame:GetParent()
  treeframe:StartSizing('RIGHT')
end

local function Dragger_OnMouseUp(frame)
  local treeframe = frame:GetParent()
  local self = treeframe.obj
  local frame = treeframe:GetParent()
  treeframe:StopMovingOrSizing()
  --treeframe:SetScript("OnUpdate", nil)
  treeframe:SetUserPlaced(false)
  --Without this :GetHeight will get stuck on the current height, causing the tree contents to not resize
  treeframe:SetHeight(0)
  treeframe:SetPoint('TOPLEFT', frame, 'TOPLEFT', 0, 0)
  treeframe:SetPoint('BOTTOMLEFT', frame, 'BOTTOMLEFT', 0, 0)

  local status = self.status or self.localstatus
  status.treewidth = treeframe:GetWidth()

  treeframe.obj:Fire('OnTreeResize', treeframe:GetWidth())
  -- recalculate the content width
  treeframe.obj:OnWidthSet(status.fullwidth)
  -- update the layout of the content
  treeframe.obj:DoLayout()
end

--[[-----------------------------------------------------------------------------
Methods
-------------------------------------------------------------------------------]]
local methods = {
  ['OnAcquire'] = function(self)
    self:SetTreeWidth(DEFAULT_TREE_WIDTH, DEFAULT_TREE_SIZABLE)
    self:EnableButtonTooltips(true)
  end,

  ['OnRelease'] = function(self)
    self.status = nil
    for k, v in pairs(self.localstatus) do
      if k == 'groups' then
        for k2 in pairs(v) do
          v[k2] = nil
        end
      else
        self.localstatus[k] = nil
      end
    end
    self.localstatus.scrollvalue = 0
    self.localstatus.treewidth = DEFAULT_TREE_WIDTH
    self.localstatus.treesizable = DEFAULT_TREE_SIZABLE
  end,

  ['EnableButtonTooltips'] = function(self, enable)
    self.enabletooltips = enable
  end,

  ['CreateButton'] = function(self)
    local num = AceGUI:GetNextWidgetNum('TreeGroupButton')
    local button = CreateFrame(
      'Button',
      ('AceGUI30TreeButton%d'):format(num),
      self.treeframe,
      'OptionsListButtonTemplate'
    )
    button.obj = self

    local icon = button:CreateTexture(nil, 'OVERLAY')
    icon:SetWidth(14)
    icon:SetHeight(14)
    button.icon = icon

    button:SetScript('OnClick', Button_OnClick)
    button:SetScript('OnDoubleClick', Button_OnDoubleClick)
    button:SetScript('OnEnter', Button_OnEnter)
    button:SetScript('OnLeave', Button_OnLeave)

    button.toggle.button = button
    button.toggle:SetScript('OnClick', Expand_OnClick)

    return button
  end,

  ['SetStatusTable'] = function(self, status)
    assert(type(status) == 'table')
    self.status = status
    if not status.groups then
      status.groups = {}
    end
    if not status.scrollvalue then
      status.scrollvalue = 0
    end
    if not status.treewidth then
      status.treewidth = DEFAULT_TREE_WIDTH
    end
    if not status.treesizable then
      status.treesizable = DEFAULT_TREE_SIZABLE
    end
    self:SetTreeWidth(status.treewidth, status.treesizable)
    self:RefreshTree()
  end,

  --sets the tree to be displayed
  ['SetTree'] = function(self, tree, filter)
    self.filter = filter
    if tree then
      assert(type(tree) == 'table')
    end
    self.tree = tree
    self:RefreshTree()
  end,

  ['BuildLevel'] = function(self, tree, level, parent)
    local groups = (self.status or self.localstatus).groups
    local hasChildren = self.hasChildren

    for i, v in ipairs(tree) do
      if v.children then
        if not self.filter or ShouldDisplayLevel(v.children) then
          local line = addLine(self, v, tree, level, parent)
          if groups[line.uniquevalue] then
            self:BuildLevel(v.children, level + 1, line)
          end
        end
      elseif v.visible ~= false or not self.filter then
        addLine(self, v, tree, level, parent)
      end
    end
  end,

  ['RefreshTree'] = function(self)
    local buttons = self.buttons
    local lines = self.lines

    for i, v in ipairs(buttons) do
      v:Hide()
    end
    while lines[1] do
      local t = tremove(lines)
      for k in pairs(t) do
        t[k] = nil
      end
      del(t)
    end

    if not self.tree then
      return
    end
    --Build the list of visible entries from the tree and status tables
    local status = self.status or self.localstatus
    local groupstatus = status.groups
    local tree = self.tree

    local treeframe = self.treeframe

    self:BuildLevel(tree, 1)

    local numlines = #lines

    local maxlines = (floor(((self.treeframe:GetHeight() or 0) - 20) / 18))

    local first, last

    if numlines <= maxlines then
      --the whole tree fits in the frame
      status.scrollvalue = 0
      self:ShowScroll(false)
      first, last = 1, numlines
    else
      self:ShowScroll(true)
      --scrolling will be needed
      self.noupdate = true
      self.scrollbar:SetMinMaxValues(0, numlines - maxlines)
      --check if we are scrolled down too far
      if numlines - status.scrollvalue < maxlines then
        status.scrollvalue = numlines - maxlines
        self.scrollbar:SetValue(status.scrollvalue)
      end
      self.noupdate = nil
      first, last = status.scrollvalue + 1, status.scrollvalue + maxlines
    end

    local buttonnum = 1
    for i = first, last do
      local line = lines[i]
      local button = buttons[buttonnum]
      if not button then
        button = self:CreateButton()

        buttons[buttonnum] = button
        button:SetParent(treeframe)
        button:SetFrameLevel(treeframe:GetFrameLevel() + 1)
        button:ClearAllPoints()
        if i == 1 then
          if self.showscroll then
            button:SetPoint('TOPRIGHT', self.treeframe, 'TOPRIGHT', -22, -10)
            button:SetPoint('TOPLEFT', self.treeframe, 'TOPLEFT', 0, -10)
          else
            button:SetPoint('TOPRIGHT', self.treeframe, 'TOPRIGHT', 0, -10)
            button:SetPoint('TOPLEFT', self.treeframe, 'TOPLEFT', 0, -10)
          end
        else
          button:SetPoint('TOPRIGHT', buttons[buttonnum - 1], 'BOTTOMRIGHT', 0, 0)
          button:SetPoint('TOPLEFT', buttons[buttonnum - 1], 'BOTTOMLEFT', 0, 0)
        end
      end

      UpdateButton(
        button,
        line,
        status.selected == line.uniquevalue,
        line.hasChildren,
        groupstatus[line.uniquevalue]
      )
      button:Show()
      buttonnum = buttonnum + 1
    end
  end,

  ['SetSelected'] = function(self, value)
    local status = self.status or self.localstatus
    if status.selected ~= value then
      status.selected = value
      self:Fire('OnGroupSelected', value)
    end
  end,

  ['Select'] = function(self, uniquevalue, ...)
    self.filter = false
    local status = self.status or self.localstatus
    local groups = status.groups
    for i = 1, select('#', ...) do
      groups[BuildUniqueValue(select(i, ...))] = true
    end
    status.selected = uniquevalue
    self:RefreshTree()
    self:Fire('OnGroupSelected', uniquevalue)
  end,

  ['SelectByPath'] = function(self, ...)
    self:Select(BuildUniqueValue(...), ...)
  end,

  ['SelectByValue'] = function(self, uniquevalue)
    self:Select(uniquevalue, ('\001'):split(uniquevalue))
  end,

  ['ShowScroll'] = function(self, show)
    self.showscroll = show
    if show then
      self.scrollbar:Show()
      if self.buttons[1] then
        self.buttons[1]:SetPoint('TOPRIGHT', self.treeframe, 'TOPRIGHT', -22, -10)
      end
    else
      self.scrollbar:Hide()
      if self.buttons[1] then
        self.buttons[1]:SetPoint('TOPRIGHT', self.treeframe, 'TOPRIGHT', 0, -10)
      end
    end
  end,

  ['OnWidthSet'] = function(self, width)
    local content = self.content
    local treeframe = self.treeframe
    local status = self.status or self.localstatus
    status.fullwidth = width

    local contentwidth = width - status.treewidth - 20
    if contentwidth < 0 then
      contentwidth = 0
    end
    content:SetWidth(contentwidth)
    content.width = contentwidth

    local maxtreewidth = math_min(400, width - 50)

    if maxtreewidth > 100 and status.treewidth > maxtreewidth then
      self:SetTreeWidth(maxtreewidth, status.treesizable)
    end
    treeframe:SetMaxResize(maxtreewidth, 1600)
  end,

  ['OnHeightSet'] = function(self, height)
    local content = self.content
    local contentheight = height - 20
    if contentheight < 0 then
      contentheight = 0
    end
    content:SetHeight(contentheight)
    content.height = contentheight
  end,

  ['SetTreeWidth'] = function(self, treewidth, resizable)
    if not resizable then
      if type(treewidth) == 'number' then
        resizable = false
      elseif type(treewidth) == 'boolean' then
        resizable = treewidth
        treewidth = DEFAULT_TREE_WIDTH
      else
        resizable = false
        treewidth = DEFAULT_TREE_WIDTH
      end
    end
    self.treeframe:SetWidth(treewidth)
    self.dragger:EnableMouse(resizable)

    local status = self.status or self.localstatus
    status.treewidth = treewidth
    status.treesizable = resizable

    -- recalculate the content width
    if status.fullwidth then
      self:OnWidthSet(status.fullwidth)
    end
  end,

  ['LayoutFinished'] = function(self, width, height)
    if self.noAutoHeight then
      return
    end
    self:SetHeight((height or 0) + 20)
  end,
}

--[[-----------------------------------------------------------------------------
Constructor
-------------------------------------------------------------------------------]]
local PaneBackdrop = {
  bgFile = 'Interface\\ChatFrame\\ChatFrameBackground',
  edgeFile = 'Interface\\Tooltips\\UI-Tooltip-Border',
  tile = true,
  tileSize = 16,
  edgeSize = 16,
  insets = { left = 3, right = 3, top = 5, bottom = 3 },
}

local DraggerBackdrop = {
  bgFile = 'Interface\\Tooltips\\UI-Tooltip-Background',
  edgeFile = nil,
  tile = true,
  tileSize = 16,
  edgeSize = 0,
  insets = { left = 3, right = 3, top = 7, bottom = 7 },
}

local function Constructor()
  local num = AceGUI:GetNextWidgetNum(Type)
  local frame = CreateFrame('Frame', nil, UIParent)

  local treeframe = CreateFrame('Frame', nil, frame)
  treeframe:SetPoint('TOPLEFT')
  treeframe:SetPoint('BOTTOMLEFT')
  treeframe:SetWidth(DEFAULT_TREE_WIDTH)
  treeframe:EnableMouseWheel(true)
  treeframe:SetBackdrop(PaneBackdrop)
  treeframe:SetBackdropColor(0.1, 0.1, 0.1, 0.5)
  treeframe:SetBackdropBorderColor(0.4, 0.4, 0.4)
  treeframe:SetResizable(true)
  treeframe:SetMinResize(100, 1)
  treeframe:SetMaxResize(400, 1600)
  treeframe:SetScript('OnUpdate', FirstFrameUpdate)
  treeframe:SetScript('OnSizeChanged', Tree_OnSizeChanged)
  treeframe:SetScript('OnMouseWheel', Tree_OnMouseWheel)

  local dragger = CreateFrame('Frame', nil, treeframe)
  dragger:SetWidth(8)
  dragger:SetPoint('TOP', treeframe, 'TOPRIGHT')
  dragger:SetPoint('BOTTOM', treeframe, 'BOTTOMRIGHT')
  dragger:SetBackdrop(DraggerBackdrop)
  dragger:SetBackdropColor(1, 1, 1, 0)
  dragger:SetScript('OnEnter', Dragger_OnEnter)
  dragger:SetScript('OnLeave', Dragger_OnLeave)
  dragger:SetScript('OnMouseDown', Dragger_OnMouseDown)
  dragger:SetScript('OnMouseUp', Dragger_OnMouseUp)

  local scrollbar = CreateFrame(
    'Slider',
    ('AceConfigDialogTreeGroup%dScrollBar'):format(num),
    treeframe,
    'UIPanelScrollBarTemplate'
  )
  scrollbar:SetScript('OnValueChanged', nil)
  scrollbar:SetPoint('TOPRIGHT', -10, -26)
  scrollbar:SetPoint('BOTTOMRIGHT', -10, 26)
  scrollbar:SetMinMaxValues(0, 0)
  scrollbar:SetValueStep(1)
  scrollbar:SetValue(0)
  scrollbar:SetWidth(16)
  scrollbar:SetScript('OnValueChanged', OnScrollValueChanged)

  local scrollbg = scrollbar:CreateTexture(nil, 'BACKGROUND')
  scrollbg:SetAllPoints(scrollbar)
  scrollbg:SetTexture(0, 0, 0, 0.4)

  local border = CreateFrame('Frame', nil, frame)
  border:SetPoint('TOPLEFT', treeframe, 'TOPRIGHT')
  border:SetPoint('BOTTOMRIGHT')
  border:SetBackdrop(PaneBackdrop)
  border:SetBackdropColor(0.1, 0.1, 0.1, 0.5)
  border:SetBackdropBorderColor(0.4, 0.4, 0.4)

  --Container Support
  local content = CreateFrame('Frame', nil, border)
  content:SetPoint('TOPLEFT', 10, -10)
  content:SetPoint('BOTTOMRIGHT', -10, 10)

  local widget = {
    frame = frame,
    lines = {},
    levels = {},
    buttons = {},
    hasChildren = {},
    localstatus = { groups = {}, scrollvalue = 0 },
    filter = false,
    treeframe = treeframe,
    dragger = dragger,
    scrollbar = scrollbar,
    border = border,
    content = content,
    type = Type,
  }
  for method, func in pairs(methods) do
    widget[method] = func
  end
  treeframe.obj, dragger.obj, scrollbar.obj = widget, widget, widget

  return AceGUI:RegisterAsContainer(widget)
end

AceGUI:RegisterWidgetType(Type, Constructor, Version)
