local AceGUI = LibStub('AceGUI-3.0')

-- 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

--------------
-- TreeView --
--------------

do
  local Type = 'TreeGroup'
  local Version = 20

  local DEFAULT_TREE_WIDTH = 175
  local DEFAULT_TREE_SIZABLE = true

  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 OnAcquire(self)
    self:SetTreeWidth(DEFAULT_TREE_WIDTH, DEFAULT_TREE_SIZABLE)
    self:EnableButtonTooltips(true)
  end

  local function OnRelease(self)
    self.frame:ClearAllPoints()
    self.frame:Hide()
    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

  local function GetButtonParents(line)
    local parent = line.parent
    if parent and parent.value then
      return parent.value, GetButtonParents(parent)
    end
  end

  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 ButtonOnClick(this)
    local self = this.obj
    self:Fire('OnClick', this.uniquevalue, this.selected)
    if not this.selected then
      self:SetSelected(this.uniquevalue)
      this.selected = true
      this:LockHighlight()
      self:RefreshTree()
    end
    AceGUI:ClearFocus()
  end

  local function ExpandOnClick(this)
    local button = this.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 ButtonOnDoubleClick(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 EnableButtonTooltips(self, enable)
    self.enabletooltips = enable
  end

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

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

      GameTooltip:Show()
    end
  end

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

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

  local buttoncount = 1
  local function CreateButton(self)
    local button = CreateFrame(
      'Button',
      ('AceGUI30TreeButton%d'):format(buttoncount),
      self.treeframe,
      'OptionsListButtonTemplate'
    )
    buttoncount = buttoncount + 1
    button.obj = self

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

    button:SetScript('OnClick', ButtonOnClick)
    button:SetScript('OnDoubleClick', ButtonOnDoubleClick)
    button:SetScript('OnEnter', Button_OnEnter)
    button:SetScript('OnLeave', Button_OnLeave)

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

    return button
  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 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', button, 'LEFT', 8 * level, (level == 1) and 0 or 1)
    else
      button.icon:SetTexture(nil)
    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 OnScrollValueChanged(this, value)
    if this.obj.noupdate then
      return
    end
    local self = this.obj
    local status = self.status or self.localstatus
    status.scrollvalue = value
    self:RefreshTree()
    AceGUI:ClearFocus()
  end

  -- called to set an external table to store status in
  local function SetStatusTable(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
  --[[
		example tree
		
		Alpha
		Bravo
		  -Charlie
		  -Delta
			-Echo
		Foxtrot
		
		tree = { 
			{ 
				value = "A",
				text = "Alpha"
			},
			{
				value = "B",
				text = "Bravo",
				children = {
					{ 
						value = "C", 
						text = "Charlie"
					},
					{
						value = "D",	
						text = "Delta"
						children = { 
							{ 
								value = "E",
								text = "Echo"
							} 
						} 
					}
				}
			},
			{ 
				value = "F", 
				text = "Foxtrot" 
			},
		}
	]]
  local function SetTree(self, tree, filter)
    self.filter = filter
    if tree then
      assert(type(tree) == 'table')
    end
    self.tree = tree
    self:RefreshTree()
  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.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

  local function BuildLevel(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

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

  local function ResizeUpdate(this)
    this.obj:RefreshTree()
  end

  local function RefreshTree(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 = (math.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

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

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

  local function Select(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

  local function SelectByPath(self, ...)
    self:Select(BuildUniqueValue(...), ...)
  end

  --Selects a tree node by UniqueValue
  local function SelectByValue(self, uniquevalue)
    self:Select(uniquevalue, string.split('\001', uniquevalue))
  end

  local function ShowScroll(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

  local function OnWidthSet(self, width)
    local content = self.content
    local treeframe = self.treeframe
    local status = self.status or self.localstatus

    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

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

  local function TreeOnMouseWheel(this, delta)
    local self = this.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 SetTreeWidth(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
  end

  local function draggerLeave(this)
    this:SetBackdropColor(1, 1, 1, 0)
  end

  local function draggerEnter(this)
    this:SetBackdropColor(1, 1, 1, 0.8)
  end

  local function draggerDown(this)
    local treeframe = this:GetParent()
    treeframe:StartSizing('RIGHT')
  end

  local function draggerUp(this)
    local treeframe = this: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)
    treeframe.obj:Fire('OnTreeResize', treeframe:GetWidth())

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

  local function LayoutFinished(self, width, height)
    if self.noAutoHeight then
      return
    end
    self:SetHeight((height or 0) + 20)
  end

  local createdcount = 0
  local function Constructor()
    local frame = CreateFrame('Frame', nil, UIParent)
    local self = {}
    self.type = Type
    self.lines = {}
    self.levels = {}
    self.buttons = {}
    self.hasChildren = {}
    self.localstatus = {}
    self.localstatus.groups = {}
    self.filter = false

    local treeframe = CreateFrame('Frame', nil, frame)
    treeframe.obj = self
    treeframe:SetPoint('TOPLEFT', frame, 'TOPLEFT', 0, 0)
    treeframe:SetPoint('BOTTOMLEFT', frame, 'BOTTOMLEFT', 0, 0)
    treeframe:SetWidth(DEFAULT_TREE_WIDTH)
    treeframe:SetScript('OnUpdate', FirstFrameUpdate)
    treeframe:SetScript('OnSizeChanged', ResizeUpdate)

    treeframe:EnableMouseWheel(true)
    treeframe:SetScript('OnMouseWheel', TreeOnMouseWheel)
    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)
    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('OnMouseDown', draggerDown)
    dragger:SetScript('OnMouseUp', draggerUp)
    dragger:SetScript('OnEnter', draggerEnter)
    dragger:SetScript('OnLeave', draggerLeave)

    self.dragger = dragger
    self.treeframe = treeframe
    self.OnRelease = OnRelease
    self.OnAcquire = OnAcquire

    self.SetTree = SetTree
    self.SetTreeWidth = SetTreeWidth
    self.RefreshTree = RefreshTree
    self.SetStatusTable = SetStatusTable
    self.BuildLevel = BuildLevel
    self.CreateButton = CreateButton
    self.SetSelected = SetSelected
    self.ShowScroll = ShowScroll
    self.SetStatusTable = SetStatusTable
    self.Select = Select
    self.SelectByValue = SelectByValue
    self.SelectByPath = SelectByPath
    self.OnWidthSet = OnWidthSet
    self.OnHeightSet = OnHeightSet
    self.EnableButtonTooltips = EnableButtonTooltips
    self.Filter = Filter
    self.LayoutFinished = LayoutFinished

    self.frame = frame
    frame.obj = self

    createdcount = createdcount + 1
    local scrollbar = CreateFrame(
      'Slider',
      ('AceConfigDialogTreeGroup%dScrollBar'):format(createdcount),
      treeframe,
      'UIPanelScrollBarTemplate'
    )
    self.scrollbar = scrollbar
    local scrollbg = scrollbar:CreateTexture(nil, 'BACKGROUND')
    scrollbg:SetAllPoints(scrollbar)
    scrollbg:SetTexture(0, 0, 0, 0.4)
    scrollbar.obj = self
    self.noupdate = true
    scrollbar:SetPoint('TOPRIGHT', treeframe, 'TOPRIGHT', -10, -26)
    scrollbar:SetPoint('BOTTOMRIGHT', treeframe, 'BOTTOMRIGHT', -10, 26)
    scrollbar:SetScript('OnValueChanged', OnScrollValueChanged)
    scrollbar:SetMinMaxValues(0, 0)
    self.localstatus.scrollvalue = 0
    scrollbar:SetValueStep(1)
    scrollbar:SetValue(0)
    scrollbar:SetWidth(16)
    self.noupdate = nil

    local border = CreateFrame('Frame', nil, frame)
    self.border = border
    border:SetPoint('TOPLEFT', treeframe, 'TOPRIGHT', 0, 0)
    border:SetPoint('BOTTOMRIGHT', frame, 'BOTTOMRIGHT', 0, 0)

    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)
    self.content = content
    content.obj = self
    content:SetPoint('TOPLEFT', border, 'TOPLEFT', 10, -10)
    content:SetPoint('BOTTOMRIGHT', border, 'BOTTOMRIGHT', -10, 10)

    AceGUI:RegisterAsContainer(self)
    --AceGUI:RegisterAsWidget(self)
    return self
  end

  AceGUI:RegisterWidgetType(Type, Constructor, Version)
end
