--[[ $Id: AceGUIWidget-DropDown.lua 916 2010-03-15 12:24:36Z nevcairiel $ ]]
--
local AceGUI = LibStub('AceGUI-3.0')

-- Lua APIs
local min, max, floor = math.min, math.max, math.floor
local select, pairs, ipairs = select, pairs, ipairs
local tsort = table.sort

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

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

local function fixlevels(parent, ...)
  local i = 1
  local child = select(i, ...)
  while child do
    child:SetFrameLevel(parent:GetFrameLevel() + 1)
    fixlevels(child, child:GetChildren())
    i = i + 1
    child = select(i, ...)
  end
end

local function fixstrata(strata, parent, ...)
  local i = 1
  local child = select(i, ...)
  parent:SetFrameStrata(strata)
  while child do
    fixstrata(strata, child, child:GetChildren())
    i = i + 1
    child = select(i, ...)
  end
end

do
  local widgetType = 'Dropdown-Pullout'
  local widgetVersion = 3

  --[[ Static data ]]
  --

  local backdrop = {
    bgFile = 'Interface\\ChatFrame\\ChatFrameBackground',
    edgeFile = 'Interface\\DialogFrame\\UI-DialogBox-Border',
    edgeSize = 32,
    tileSize = 32,
    tile = true,
    insets = { left = 11, right = 12, top = 12, bottom = 11 },
  }
  local sliderBackdrop = {
    bgFile = 'Interface\\Buttons\\UI-SliderBar-Background',
    edgeFile = 'Interface\\Buttons\\UI-SliderBar-Border',
    tile = true,
    tileSize = 8,
    edgeSize = 8,
    insets = { left = 3, right = 3, top = 3, bottom = 3 },
  }

  local defaultWidth = 200
  local defaultMaxHeight = 600

  --[[ UI Event Handlers ]]
  --

  -- HACK: This should be no part of the pullout, but there
  --       is no other 'clean' way to response to any item-OnEnter
  --       Used to close Submenus when an other item is entered
  local function OnEnter(item)
    local self = item.pullout
    for k, v in ipairs(self.items) do
      if v.CloseMenu and v ~= item then
        v:CloseMenu()
      end
    end
  end

  -- See the note in Constructor() for each scroll related function
  local function OnMouseWheel(this, value)
    this.obj:MoveScroll(value)
  end

  local function OnScrollValueChanged(this, value)
    this.obj:SetScroll(value)
  end

  local function OnSizeChanged(this)
    this.obj:FixScroll()
  end

  --[[ Exported methods ]]
  --

  -- exported
  local function SetScroll(self, value)
    local status = self.scrollStatus
    local frame, child = self.scrollFrame, self.itemFrame
    local height, viewheight = frame:GetHeight(), child:GetHeight()

    local offset
    if height > viewheight then
      offset = 0
    else
      offset = floor((viewheight - height) / 1000 * value)
    end
    child:ClearAllPoints()
    child:SetPoint('TOPLEFT', frame, 'TOPLEFT', 0, offset)
    child:SetPoint('TOPRIGHT', frame, 'TOPRIGHT', self.slider:IsShown() and -12 or 0, offset)
    status.offset = offset
    status.scrollvalue = value
  end

  -- exported
  local function MoveScroll(self, value)
    local status = self.scrollStatus
    local frame, child = self.scrollFrame, self.itemFrame
    local height, viewheight = frame:GetHeight(), child:GetHeight()

    if height > viewheight then
      self.slider:Hide()
    else
      self.slider:Show()
      local diff = height - viewheight
      local delta = 1
      if value < 0 then
        delta = -1
      end
      self.slider:SetValue(min(max(status.scrollvalue + delta * (1000 / (diff / 45)), 0), 1000))
    end
  end

  -- exported
  local function FixScroll(self)
    local status = self.scrollStatus
    local frame, child = self.scrollFrame, self.itemFrame
    local height, viewheight = frame:GetHeight(), child:GetHeight()
    local offset = status.offset or 0

    if viewheight < height then
      self.slider:Hide()
      child:SetPoint('TOPRIGHT', frame, 'TOPRIGHT', 0, offset)
      self.slider:SetValue(0)
    else
      self.slider:Show()
      local value = (offset / (viewheight - height) * 1000)
      if value > 1000 then
        value = 1000
      end
      self.slider:SetValue(value)
      self:SetScroll(value)
      if value < 1000 then
        child:ClearAllPoints()
        child:SetPoint('TOPLEFT', frame, 'TOPLEFT', 0, offset)
        child:SetPoint('TOPRIGHT', frame, 'TOPRIGHT', -12, offset)
        status.offset = offset
      end
    end
  end

  -- exported, AceGUI callback
  local function OnAcquire(self)
    self.frame:SetParent(UIParent)
    --self.itemFrame:SetToplevel(true)
  end

  -- exported, AceGUI callback
  local function OnRelease(self)
    self:Clear()
    self.frame:ClearAllPoints()
    self.frame:Hide()
  end

  -- exported
  local function AddItem(self, item)
    self.items[#self.items + 1] = item

    local h = #self.items * 16
    self.itemFrame:SetHeight(h)
    self.frame:SetHeight(min(h + 34, self.maxHeight)) -- +34: 20 for scrollFrame placement (10 offset) and +14 for item placement

    item.frame:SetPoint('LEFT', self.itemFrame, 'LEFT')
    item.frame:SetPoint('RIGHT', self.itemFrame, 'RIGHT')

    item:SetPullout(self)
    item:SetOnEnter(OnEnter)
  end

  -- exported
  local function Open(self, point, relFrame, relPoint, x, y)
    local items = self.items
    local frame = self.frame
    local itemFrame = self.itemFrame

    frame:SetPoint(point, relFrame, relPoint, x, y)

    local height = 8
    for i, item in pairs(items) do
      if i == 1 then
        item:SetPoint('TOP', itemFrame, 'TOP', 0, -2)
      else
        item:SetPoint('TOP', items[i - 1].frame, 'BOTTOM', 0, 1)
      end

      item:Show()

      height = height + 16
    end
    itemFrame:SetHeight(height)
    fixstrata('TOOLTIP', frame, frame:GetChildren())
    frame:Show()
    self:Fire('OnOpen')
  end

  -- exported
  local function Close(self)
    self.frame:Hide()
    self:Fire('OnClose')
  end

  -- exported
  local function Clear(self)
    local items = self.items
    for i, item in pairs(items) do
      AceGUI:Release(item)
      items[i] = nil
    end
  end

  -- exported
  local function IterateItems(self)
    return ipairs(self.items)
  end

  -- exported
  local function SetHideOnLeave(self, val)
    self.hideOnLeave = val
  end

  -- exported
  local function SetMaxHeight(self, height)
    self.maxHeight = height or defaultMaxHeight
    if self.frame:GetHeight() > height then
      self.frame:SetHeight(height)
    elseif (self.itemFrame:GetHeight() + 34) < height then
      self.frame:SetHeight(self.itemFrame:GetHeight() + 34) -- see :AddItem
    end
  end

  -- exported
  local function GetRightBorderWidth(self)
    return 6 + (self.slider:IsShown() and 12 or 0)
  end

  -- exported
  local function GetLeftBorderWidth(self)
    return 6
  end

  --[[ Constructor ]]
  --

  local function Constructor()
    local count = AceGUI:GetNextWidgetNum(widgetType)
    local frame = CreateFrame('Frame', 'AceGUI30Pullout' .. count, UIParent)
    local self = {}
    self.count = count
    self.type = widgetType
    self.frame = frame
    frame.obj = self

    self.OnAcquire = OnAcquire
    self.OnRelease = OnRelease

    self.AddItem = AddItem
    self.Open = Open
    self.Close = Close
    self.Clear = Clear
    self.IterateItems = IterateItems
    self.SetHideOnLeave = SetHideOnLeave

    self.SetScroll = SetScroll
    self.MoveScroll = MoveScroll
    self.FixScroll = FixScroll

    self.SetMaxHeight = SetMaxHeight
    self.GetRightBorderWidth = GetRightBorderWidth
    self.GetLeftBorderWidth = GetLeftBorderWidth

    self.items = {}

    self.scrollStatus = {
      scrollvalue = 0,
    }

    self.maxHeight = defaultMaxHeight

    frame:SetBackdrop(backdrop)
    frame:SetBackdropColor(0, 0, 0)
    frame:SetFrameStrata('FULLSCREEN_DIALOG')
    frame:SetClampedToScreen(true)
    frame:SetWidth(defaultWidth)
    frame:SetHeight(self.maxHeight)
    --frame:SetToplevel(true)

    -- NOTE: The whole scroll frame code is copied from the AceGUI-3.0 widget ScrollFrame
    local scrollFrame = CreateFrame('ScrollFrame', nil, frame)
    local itemFrame = CreateFrame('Frame', nil, scrollFrame)

    self.scrollFrame = scrollFrame
    self.itemFrame = itemFrame

    scrollFrame.obj = self
    itemFrame.obj = self

    local slider = CreateFrame('Slider', 'AceGUI30PulloutScrollbar' .. count, scrollFrame)
    slider:SetOrientation('VERTICAL')
    slider:SetHitRectInsets(0, 0, -10, 0)
    slider:SetBackdrop(sliderBackdrop)
    slider:SetWidth(8)
    slider:SetThumbTexture('Interface\\Buttons\\UI-SliderBar-Button-Vertical')
    slider:SetFrameStrata('FULLSCREEN_DIALOG')
    self.slider = slider
    slider.obj = self

    scrollFrame:SetScrollChild(itemFrame)
    scrollFrame:SetPoint('TOPLEFT', frame, 'TOPLEFT', 6, -12)
    scrollFrame:SetPoint('BOTTOMRIGHT', frame, 'BOTTOMRIGHT', -6, 12)
    scrollFrame:EnableMouseWheel(true)
    scrollFrame:SetScript('OnMouseWheel', OnMouseWheel)
    scrollFrame:SetScript('OnSizeChanged', OnSizeChanged)
    scrollFrame:SetToplevel(true)
    scrollFrame:SetFrameStrata('FULLSCREEN_DIALOG')

    itemFrame:SetPoint('TOPLEFT', scrollFrame, 'TOPLEFT', 0, 0)
    itemFrame:SetPoint('TOPRIGHT', scrollFrame, 'TOPRIGHT', -12, 0)
    itemFrame:SetHeight(400)
    itemFrame:SetToplevel(true)
    itemFrame:SetFrameStrata('FULLSCREEN_DIALOG')

    slider:SetPoint('TOPLEFT', scrollFrame, 'TOPRIGHT', -16, 0)
    slider:SetPoint('BOTTOMLEFT', scrollFrame, 'BOTTOMRIGHT', -16, 0)
    slider:SetScript('OnValueChanged', OnScrollValueChanged)
    slider:SetMinMaxValues(0, 1000)
    slider:SetValueStep(1)
    slider:SetValue(0)

    scrollFrame:Show()
    itemFrame:Show()
    slider:Hide()

    self:FixScroll()

    AceGUI:RegisterAsWidget(self)
    return self
  end

  AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion)
end

do
  local widgetType = 'Dropdown'
  local widgetVersion = 22

  --[[ Static data ]]
  --

  --[[ UI event handler ]]
  --

  local function Control_OnEnter(this)
    this.obj:Fire('OnEnter')
  end

  local function Control_OnLeave(this)
    this.obj:Fire('OnLeave')
  end

  local function Dropdown_OnHide(this)
    local self = this.obj
    if self.open then
      self.pullout:Close()
    end
  end

  local function Dropdown_TogglePullout(this)
    local self = this.obj
    PlaySound('igMainMenuOptionCheckBoxOn') -- missleading name, but the Blizzard code uses this sound
    if self.open then
      self.open = nil
      self.pullout:Close()
      AceGUI:ClearFocus()
    else
      self.open = true
      self.pullout:SetWidth(self.frame:GetWidth())
      self.pullout:Open('TOPLEFT', self.frame, 'BOTTOMLEFT', 0, self.label:IsShown() and -2 or 0)
      AceGUI:SetFocus(self)
    end
  end

  local function OnPulloutOpen(this)
    local self = this.userdata.obj
    local value = self.value

    if not self.multiselect then
      for i, item in this:IterateItems() do
        item:SetValue(item.userdata.value == value)
      end
    end

    self.open = true
  end

  local function OnPulloutClose(this)
    local self = this.userdata.obj
    self.open = nil
    self:Fire('OnClosed')
  end

  local function ShowMultiText(self)
    local text
    for i, widget in self.pullout:IterateItems() do
      if widget.type == 'Dropdown-Item-Toggle' then
        if widget:GetValue() then
          if text then
            text = text .. ', ' .. widget:GetText()
          else
            text = widget:GetText()
          end
        end
      end
    end
    self:SetText(text)
  end

  local function OnItemValueChanged(this, event, checked)
    local self = this.userdata.obj

    if self.multiselect then
      self:Fire('OnValueChanged', this.userdata.value, checked)
      ShowMultiText(self)
    else
      if checked then
        self:SetValue(this.userdata.value)
        self:Fire('OnValueChanged', this.userdata.value)
      else
        this:SetValue(true)
      end
      if self.open then
        self.pullout:Close()
      end
    end
  end

  --[[ Exported methods ]]
  --

  -- exported, AceGUI callback
  local function OnAcquire(self)
    local pullout = AceGUI:Create('Dropdown-Pullout')
    self.pullout = pullout
    pullout.userdata.obj = self
    pullout:SetCallback('OnClose', OnPulloutClose)
    pullout:SetCallback('OnOpen', OnPulloutOpen)
    self.pullout.frame:SetFrameLevel(self.frame:GetFrameLevel() + 1)
    fixlevels(self.pullout.frame, self.pullout.frame:GetChildren())

    self:SetHeight(44)
    self:SetWidth(200)
  end

  -- exported, AceGUI callback
  local function OnRelease(self)
    if self.open then
      self.pullout:Close()
    end
    AceGUI:Release(self.pullout)
    self.pullout = nil

    self:SetText('')
    self:SetLabel('')
    self:SetDisabled(false)
    self:SetMultiselect(false)

    self.value = nil
    self.list = nil
    self.open = nil
    self.hasClose = nil

    self.frame:ClearAllPoints()
    self.frame:Hide()
  end

  -- exported
  local function SetDisabled(self, disabled)
    self.disabled = disabled
    if disabled then
      self.text:SetTextColor(0.5, 0.5, 0.5)
      self.button:Disable()
      self.label:SetTextColor(0.5, 0.5, 0.5)
    else
      self.button:Enable()
      self.label:SetTextColor(1, 0.82, 0)
      self.text:SetTextColor(1, 1, 1)
    end
  end

  -- exported
  local function ClearFocus(self)
    if self.open then
      self.pullout:Close()
    end
  end

  -- exported
  local function SetText(self, text)
    self.text:SetText(text or '')
  end

  -- exported
  local function SetLabel(self, text)
    if text and text ~= '' then
      self.label:SetText(text)
      self.label:Show()
      self.dropdown:SetPoint('TOPLEFT', self.frame, 'TOPLEFT', -15, -18)
      self.frame:SetHeight(44)
    else
      self.label:SetText('')
      self.label:Hide()
      self.dropdown:SetPoint('TOPLEFT', self.frame, 'TOPLEFT', -15, 0)
      self.frame:SetHeight(26)
    end
  end

  -- exported
  local function SetValue(self, value)
    if self.list then
      self:SetText(self.list[value] or '')
    end
    self.value = value
  end

  -- exported
  local function GetValue(self)
    return self.value
  end

  -- exported
  local function SetItemValue(self, item, value)
    if not self.multiselect then
      return
    end
    for i, widget in self.pullout:IterateItems() do
      if widget.userdata.value == item then
        if widget.SetValue then
          widget:SetValue(value)
        end
      end
    end
    ShowMultiText(self)
  end

  -- exported
  local function SetItemDisabled(self, item, disabled)
    for i, widget in self.pullout:IterateItems() do
      if widget.userdata.value == item then
        widget:SetDisabled(disabled)
      end
    end
  end

  local function AddListItem(self, value, text)
    local item = AceGUI:Create('Dropdown-Item-Toggle')
    item:SetText(text)
    item.userdata.obj = self
    item.userdata.value = value
    item:SetCallback('OnValueChanged', OnItemValueChanged)
    self.pullout:AddItem(item)
  end

  local function AddCloseButton(self)
    if not self.hasClose then
      local close = AceGUI:Create('Dropdown-Item-Execute')
      close:SetText(CLOSE)
      self.pullout:AddItem(close)
      self.hasClose = true
    end
  end

  -- exported
  local sortlist = {}
  local function SetList(self, list)
    self.list = list
    self.pullout:Clear()
    self.hasClose = nil
    if not list then
      return
    end

    for v in pairs(list) do
      sortlist[#sortlist + 1] = v
    end
    tsort(sortlist)

    for i, value in pairs(sortlist) do
      AddListItem(self, value, list[value])
      sortlist[i] = nil
    end
    if self.multiselect then
      ShowMultiText(self)
      AddCloseButton(self)
    end
  end

  -- exported
  local function AddItem(self, value, text)
    if self.list then
      self.list[value] = text
      AddListItem(self, value, text)
    end
  end

  -- exported
  local function SetMultiselect(self, multi)
    self.multiselect = multi
    if multi then
      ShowMultiText(self)
      AddCloseButton(self)
    end
  end

  -- exported
  local function GetMultiselect(self)
    return self.multiselect
  end

  --[[ Constructor ]]
  --

  local function Constructor()
    local count = AceGUI:GetNextWidgetNum(widgetType)
    local frame = CreateFrame('Frame', nil, UIParent)
    local dropdown =
      CreateFrame('Frame', 'AceGUI30DropDown' .. count, frame, 'UIDropDownMenuTemplate')

    local self = {}
    self.type = widgetType
    self.frame = frame
    self.dropdown = dropdown
    self.count = count
    frame.obj = self
    dropdown.obj = self

    self.OnRelease = OnRelease
    self.OnAcquire = OnAcquire

    self.ClearFocus = ClearFocus

    self.SetText = SetText
    self.SetValue = SetValue
    self.GetValue = GetValue
    self.SetList = SetList
    self.SetLabel = SetLabel
    self.SetDisabled = SetDisabled
    self.AddItem = AddItem
    self.SetMultiselect = SetMultiselect
    self.GetMultiselect = GetMultiselect
    self.SetItemValue = SetItemValue
    self.SetItemDisabled = SetItemDisabled

    self.alignoffset = 31

    frame:SetHeight(44)
    frame:SetWidth(200)
    frame:SetScript('OnHide', Dropdown_OnHide)

    dropdown:ClearAllPoints()
    dropdown:SetPoint('TOPLEFT', frame, 'TOPLEFT', -15, 0)
    dropdown:SetPoint('BOTTOMRIGHT', frame, 'BOTTOMRIGHT', 17, 0)
    dropdown:SetScript('OnHide', nil)

    local left = _G[dropdown:GetName() .. 'Left']
    local middle = _G[dropdown:GetName() .. 'Middle']
    local right = _G[dropdown:GetName() .. 'Right']

    middle:ClearAllPoints()
    right:ClearAllPoints()

    middle:SetPoint('LEFT', left, 'RIGHT', 0, 0)
    middle:SetPoint('RIGHT', right, 'LEFT', 0, 0)
    right:SetPoint('TOPRIGHT', dropdown, 'TOPRIGHT', 0, 17)

    local button = _G[dropdown:GetName() .. 'Button']
    self.button = button
    button.obj = self
    button:SetScript('OnEnter', Control_OnEnter)
    button:SetScript('OnLeave', Control_OnLeave)
    button:SetScript('OnClick', Dropdown_TogglePullout)

    local text = _G[dropdown:GetName() .. 'Text']
    self.text = text
    text.obj = self
    text:ClearAllPoints()
    text:SetPoint('RIGHT', right, 'RIGHT', -43, 2)
    text:SetPoint('LEFT', left, 'LEFT', 25, 2)

    local label = frame:CreateFontString(nil, 'OVERLAY', 'GameFontNormalSmall')
    label:SetPoint('TOPLEFT', frame, 'TOPLEFT', 0, 0)
    label:SetPoint('TOPRIGHT', frame, 'TOPRIGHT', 0, 0)
    label:SetJustifyH('LEFT')
    label:SetHeight(18)
    label:Hide()
    self.label = label

    AceGUI:RegisterAsWidget(self)
    return self
  end

  AceGUI:RegisterWidgetType(widgetType, Constructor, widgetVersion)
end
