--- **AceGUI-3.0** provides access to numerous widgets which can be used to create GUIs.
-- AceGUI is used by AceConfigDialog to create the option GUIs, but you can use it by itself
-- to create any custom GUI. There are more extensive examples in the test suite in the Ace3
-- stand-alone distribution.
--
-- **Note**: When using AceGUI-3.0 directly, please do not modify the frames of the widgets directly,
-- as any "unknown" change to the widgets will cause addons that get your widget out of the widget pool
-- to misbehave. If you think some part of a widget should be modifiable, please open a ticket, and we"ll
-- implement a proper API to modify it.
-- @usage
-- local AceGUI = LibStub("AceGUI-3.0")
-- -- Create a container frame
-- local f = AceGUI:Create("Frame")
-- f:SetCallback("OnClose",function(widget) AceGUI:Release(widget) end)
-- f:SetTitle("AceGUI-3.0 Example")
-- f:SetStatusText("Status Bar")
-- f:SetLayout("Flow")
-- -- Create a button
-- local btn = AceGUI:Create("Button")
-- btn:SetWidth(170)
-- btn:SetText("Button !")
-- btn:SetCallback("OnClick", function() print("Click!") end)
-- -- Add the button to the container
-- f:AddChild(btn)
-- @class file
-- @name AceGUI-3.0
-- @release $Id: AceGUI-3.0.lua 924 2010-05-13 15:12:20Z nevcairiel $
local ACEGUI_MAJOR, ACEGUI_MINOR = 'AceGUI-3.0', 33
local AceGUI, oldminor = LibStub:NewLibrary(ACEGUI_MAJOR, ACEGUI_MINOR)

if not AceGUI then
  return
end -- No upgrade needed

-- Lua APIs
local tconcat, tremove, tinsert = table.concat, table.remove, table.insert
local select, pairs, next, type = select, pairs, next, type
local error, assert, loadstring = error, assert, loadstring
local setmetatable, rawget, rawset = setmetatable, rawget, rawset
local math_max = math.max

-- WoW APIs
local UIParent = 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: geterrorhandler, LibStub

--local con = LibStub("AceConsole-3.0",true)

AceGUI.WidgetRegistry = AceGUI.WidgetRegistry or {}
AceGUI.LayoutRegistry = AceGUI.LayoutRegistry or {}
AceGUI.WidgetBase = AceGUI.WidgetBase or {}
AceGUI.WidgetContainerBase = AceGUI.WidgetContainerBase or {}
AceGUI.WidgetVersions = AceGUI.WidgetVersions or {}

-- local upvalues
local WidgetRegistry = AceGUI.WidgetRegistry
local LayoutRegistry = AceGUI.LayoutRegistry
local WidgetVersions = AceGUI.WidgetVersions

--[[
	 xpcall safecall implementation
]]
local xpcall = xpcall

local function errorhandler(err)
  return geterrorhandler()(err)
end

local function CreateDispatcher(argCount)
  local code = [[
		local xpcall, eh = ...
		local method, ARGS
		local function call() return method(ARGS) end
	
		local function dispatch(func, ...)
			method = func
			if not method then return end
			ARGS = ...
			return xpcall(call, eh)
		end
	
		return dispatch
	]]

  local ARGS = {}
  for i = 1, argCount do
    ARGS[i] = 'arg' .. i
  end
  code = code:gsub('ARGS', tconcat(ARGS, ', '))
  return assert(loadstring(code, 'safecall Dispatcher[' .. argCount .. ']'))(xpcall, errorhandler)
end

local Dispatchers = setmetatable({}, {
  __index = function(self, argCount)
    local dispatcher = CreateDispatcher(argCount)
    rawset(self, argCount, dispatcher)
    return dispatcher
  end,
})
Dispatchers[0] = function(func)
  return xpcall(func, errorhandler)
end

local function safecall(func, ...)
  return Dispatchers[select('#', ...)](func, ...)
end

-- Recycling functions
local newWidget, delWidget
do
  -- Version Upgrade in Minor 29
  -- Internal Storage of the objects changed, from an array table
  -- to a hash table, and additionally we introduced versioning on
  -- the widgets which would discard all widgets from a pre-29 version
  -- anyway, so we just clear the storage now, and don't try to
  -- convert the storage tables to the new format.
  -- This should generally not cause *many* widgets to end up in trash,
  -- since once dialogs are opened, all addons should be loaded already
  -- and AceGUI should be on the latest version available on the users
  -- setup.
  -- -- nevcairiel - Nov 2nd, 2009
  if oldminor and oldminor < 29 and AceGUI.objPools then
    AceGUI.objPools = nil
  end

  AceGUI.objPools = AceGUI.objPools or {}
  local objPools = AceGUI.objPools
  --Returns a new instance, if none are available either returns a new table or calls the given contructor
  function newWidget(type)
    if not WidgetRegistry[type] then
      error('Attempt to instantiate unknown widget type', 2)
    end

    if not objPools[type] then
      objPools[type] = {}
    end

    local newObj = next(objPools[type])
    if not newObj then
      newObj = WidgetRegistry[type]()
      newObj.AceGUIWidgetVersion = WidgetVersions[type]
    else
      objPools[type][newObj] = nil
      -- if the widget is older then the latest, don't even try to reuse it
      -- just forget about it, and grab a new one.
      if not newObj.AceGUIWidgetVersion or newObj.AceGUIWidgetVersion < WidgetVersions[type] then
        return newWidget(type)
      end
    end
    return newObj
  end
  -- Releases an instance to the Pool
  function delWidget(obj, type)
    if not objPools[type] then
      objPools[type] = {}
    end
    if objPools[type][obj] then
      error('Attempt to Release Widget that is already released', 2)
    end
    objPools[type][obj] = true
  end
end

-------------------
-- API Functions --
-------------------

-- Gets a widget Object

--- Create a new Widget of the given type.
-- This function will instantiate a new widget (or use one from the widget pool), and call the
-- OnAcquire function on it, before returning.
-- @param type The type of the widget.
-- @return The newly created widget.
function AceGUI:Create(type)
  if WidgetRegistry[type] then
    local widget = newWidget(type)

    if rawget(widget, 'Acquire') then
      widget.OnAcquire = widget.Acquire
      widget.Acquire = nil
    elseif rawget(widget, 'Aquire') then
      widget.OnAcquire = widget.Aquire
      widget.Aquire = nil
    end

    if rawget(widget, 'Release') then
      widget.OnRelease = rawget(widget, 'Release')
      widget.Release = nil
    end

    if widget.OnAcquire then
      widget:OnAcquire()
    else
      error(("Widget type %s doesn't supply an OnAcquire Function"):format(type))
    end
    -- Set the default Layout ("List")
    safecall(widget.SetLayout, widget, 'List')
    safecall(widget.ResumeLayout, widget)
    return widget
  end
end

--- Releases a widget Object.
-- This function calls OnRelease on the widget and places it back in the widget pool.
-- Any data on the widget is being erased, and the widget will be hidden.\\
-- If this widget is a Container-Widget, all of its Child-Widgets will be releases as well.
-- @param widget The widget to release
function AceGUI:Release(widget)
  safecall(widget.PauseLayout, widget)
  widget:Fire('OnRelease')
  safecall(widget.ReleaseChildren, widget)

  if widget.OnRelease then
    widget:OnRelease()
    --	else
    --		error(("Widget type %s doesn't supply an OnRelease Function"):format(widget.type))
  end
  for k in pairs(widget.userdata) do
    widget.userdata[k] = nil
  end
  for k in pairs(widget.events) do
    widget.events[k] = nil
  end
  widget.width = nil
  widget.relWidth = nil
  widget.height = nil
  widget.relHeight = nil
  widget.noAutoHeight = nil
  widget.frame:ClearAllPoints()
  widget.frame:Hide()
  widget.frame:SetParent(UIParent)
  widget.frame.width = nil
  widget.frame.height = nil
  if widget.content then
    widget.content.width = nil
    widget.content.height = nil
  end
  delWidget(widget, widget.type)
end

-----------
-- Focus --
-----------

--- Called when a widget has taken focus.
-- e.g. Dropdowns opening, Editboxes gaining kb focus
-- @param widget The widget that should be focused
function AceGUI:SetFocus(widget)
  if self.FocusedWidget and self.FocusedWidget ~= widget then
    safecall(self.FocusedWidget.ClearFocus, self.FocusedWidget)
  end
  self.FocusedWidget = widget
end

--- Called when something has happened that could cause widgets with focus to drop it
-- e.g. titlebar of a frame being clicked
function AceGUI:ClearFocus()
  if self.FocusedWidget then
    safecall(self.FocusedWidget.ClearFocus, self.FocusedWidget)
    self.FocusedWidget = nil
  end
end

-------------
-- Widgets --
-------------
--[[
	Widgets must provide the following functions
		OnAcquire() - Called when the object is acquired, should set everything to a default hidden state
		
	And the following members
		frame - the frame or derivitive object that will be treated as the widget for size and anchoring purposes
		type - the type of the object, same as the name given to :RegisterWidget()
		
	Widgets contain a table called userdata, this is a safe place to store data associated with the wigdet
	It will be cleared automatically when a widget is released
	Placing values directly into a widget object should be avoided
	
	If the Widget can act as a container for other Widgets the following
		content - frame or derivitive that children will be anchored to
		
	The Widget can supply the following Optional Members
		:OnRelease() - Called when the object is Released, should remove any additional anchors and clear any data
		:OnWidthSet(width) - Called when the width of the widget is changed
		:OnHeightSet(height) - Called when the height of the widget is changed
			Widgets should not use the OnSizeChanged events of thier frame or content members, use these methods instead
			AceGUI already sets a handler to the event
		:LayoutFinished(width, height) - called after a layout has finished, the width and height will be the width and height of the
			area used for controls. These can be nil if the layout used the existing size to layout the controls.

]]

--------------------------
-- Widget Base Template --
--------------------------
do
  local WidgetBase = AceGUI.WidgetBase

  WidgetBase.SetParent = function(self, parent)
    local frame = self.frame
    frame:SetParent(nil)
    frame:SetParent(parent.content)
    self.parent = parent
  end

  WidgetBase.SetCallback = function(self, name, func)
    if type(func) == 'function' then
      self.events[name] = func
    end
  end

  WidgetBase.Fire = function(self, name, ...)
    if self.events[name] then
      local success, ret = safecall(self.events[name], self, name, ...)
      if success then
        return ret
      end
    end
  end

  WidgetBase.SetWidth = function(self, width)
    self.frame:SetWidth(width)
    self.frame.width = width
    if self.OnWidthSet then
      self:OnWidthSet(width)
    end
  end

  WidgetBase.SetRelativeWidth = function(self, width)
    if width <= 0 or width > 1 then
      error(':SetRelativeWidth(width): Invalid relative width.', 2)
    end
    self.relWidth = width
    self.width = 'relative'
  end

  WidgetBase.SetHeight = function(self, height)
    self.frame:SetHeight(height)
    self.frame.height = height
    if self.OnHeightSet then
      self:OnHeightSet(height)
    end
  end

  --[[ WidgetBase.SetRelativeHeight = function(self, height)
		if height <= 0 or height > 1 then
			error(":SetRelativeHeight(height): Invalid relative height.", 2)
		end
		self.relHeight = height
		self.height = "relative"
	end ]]

  WidgetBase.IsVisible = function(self)
    return self.frame:IsVisible()
  end

  WidgetBase.IsShown = function(self)
    return self.frame:IsShown()
  end

  WidgetBase.Release = function(self)
    AceGUI:Release(self)
  end

  WidgetBase.SetPoint = function(self, ...)
    return self.frame:SetPoint(...)
  end

  WidgetBase.ClearAllPoints = function(self)
    return self.frame:ClearAllPoints()
  end

  WidgetBase.GetNumPoints = function(self)
    return self.frame:GetNumPoints()
  end

  WidgetBase.GetPoint = function(self, ...)
    return self.frame:GetPoint(...)
  end

  WidgetBase.GetUserDataTable = function(self)
    return self.userdata
  end

  WidgetBase.SetUserData = function(self, key, value)
    self.userdata[key] = value
  end

  WidgetBase.GetUserData = function(self, key)
    return self.userdata[key]
  end

  WidgetBase.IsFullHeight = function(self)
    return self.height == 'fill'
  end

  WidgetBase.SetFullHeight = function(self, isFull)
    if isFull then
      self.height = 'fill'
    else
      self.height = nil
    end
  end

  WidgetBase.IsFullWidth = function(self)
    return self.width == 'fill'
  end

  WidgetBase.SetFullWidth = function(self, isFull)
    if isFull then
      self.width = 'fill'
    else
      self.width = nil
    end
  end

  --	local function LayoutOnUpdate(this)
  --		this:SetScript("OnUpdate",nil)
  --		this.obj:PerformLayout()
  --	end

  local WidgetContainerBase = AceGUI.WidgetContainerBase

  WidgetContainerBase.PauseLayout = function(self)
    self.LayoutPaused = true
  end

  WidgetContainerBase.ResumeLayout = function(self)
    self.LayoutPaused = nil
  end

  WidgetContainerBase.PerformLayout = function(self)
    if self.LayoutPaused then
      return
    end
    safecall(self.LayoutFunc, self.content, self.children)
  end

  --call this function to layout, makes sure layed out objects get a frame to get sizes etc
  WidgetContainerBase.DoLayout = function(self)
    self:PerformLayout()
    --		if not self.parent then
    --			self.frame:SetScript("OnUpdate", LayoutOnUpdate)
    --		end
  end

  WidgetContainerBase.AddChild = function(self, child, beforeWidget)
    if beforeWidget then
      local siblingIndex = 1
      for _, widget in pairs(self.children) do
        if widget == beforeWidget then
          break
        end
        siblingIndex = siblingIndex + 1
      end
      tinsert(self.children, siblingIndex, child)
    else
      tinsert(self.children, child)
    end
    child:SetParent(self)
    child.frame:Show()
    self:DoLayout()
  end

  WidgetContainerBase.AddChildren = function(self, ...)
    for i = 1, select('#', ...) do
      local child = select(i, ...)
      tinsert(self.children, child)
      child:SetParent(self)
      child.frame:Show()
    end
    self:DoLayout()
  end

  WidgetContainerBase.ReleaseChildren = function(self)
    local children = self.children
    for i = 1, #children do
      AceGUI:Release(children[i])
      children[i] = nil
    end
  end

  WidgetContainerBase.SetLayout = function(self, Layout)
    self.LayoutFunc = AceGUI:GetLayout(Layout)
  end

  WidgetContainerBase.SetAutoAdjustHeight = function(self, adjust)
    if adjust then
      self.noAutoHeight = nil
    else
      self.noAutoHeight = true
    end
  end

  local function FrameResize(this)
    local self = this.obj
    if this:GetWidth() and this:GetHeight() then
      if self.OnWidthSet then
        self:OnWidthSet(this:GetWidth())
      end
      if self.OnHeightSet then
        self:OnHeightSet(this:GetHeight())
      end
    end
  end

  local function ContentResize(this)
    if this:GetWidth() and this:GetHeight() then
      this.width = this:GetWidth()
      this.height = this:GetHeight()
      this.obj:DoLayout()
    end
  end

  setmetatable(WidgetContainerBase, { __index = WidgetBase })

  --One of these function should be called on each Widget Instance as part of its creation process

  --- Register a widget-class as a container for newly created widgets.
  -- @param widget The widget class
  function AceGUI:RegisterAsContainer(widget)
    widget.children = {}
    widget.userdata = {}
    widget.events = {}
    widget.base = WidgetContainerBase
    widget.content.obj = widget
    widget.frame.obj = widget
    widget.content:SetScript('OnSizeChanged', ContentResize)
    widget.frame:SetScript('OnSizeChanged', FrameResize)
    setmetatable(widget, { __index = WidgetContainerBase })
    widget:SetLayout('List')
    return widget
  end

  --- Register a widget-class as a widget.
  -- @param widget The widget class
  function AceGUI:RegisterAsWidget(widget)
    widget.userdata = {}
    widget.events = {}
    widget.base = WidgetBase
    widget.frame.obj = widget
    widget.frame:SetScript('OnSizeChanged', FrameResize)
    setmetatable(widget, { __index = WidgetBase })
    return widget
  end
end

------------------
-- Widget API   --
------------------

--- Registers a widget Constructor, this function returns a new instance of the Widget
-- @param Name The name of the widget
-- @param Constructor The widget constructor function
-- @param Version The version of the widget
function AceGUI:RegisterWidgetType(Name, Constructor, Version)
  assert(type(Constructor) == 'function')
  assert(type(Version) == 'number')

  local oldVersion = WidgetVersions[Name]
  if oldVersion and oldVersion >= Version then
    return
  end

  WidgetVersions[Name] = Version
  WidgetRegistry[Name] = Constructor
end

--- Registers a Layout Function
-- @param Name The name of the layout
-- @param LayoutFunc Reference to the layout function
function AceGUI:RegisterLayout(Name, LayoutFunc)
  assert(type(LayoutFunc) == 'function')
  if type(Name) == 'string' then
    Name = Name:upper()
  end
  LayoutRegistry[Name] = LayoutFunc
end

--- Get a Layout Function from the registry
-- @param Name The name of the layout
function AceGUI:GetLayout(Name)
  if type(Name) == 'string' then
    Name = Name:upper()
  end
  return LayoutRegistry[Name]
end

AceGUI.counts = AceGUI.counts or {}

--- A type-based counter to count the number of widgets created.
-- This is used by widgets that require a named frame, e.g. when a Blizzard
-- Template requires it.
-- @param type The widget type
function AceGUI:GetNextWidgetNum(type)
  if not self.counts[type] then
    self.counts[type] = 0
  end
  self.counts[type] = self.counts[type] + 1
  return self.counts[type]
end

--- Return the number of created widgets for this type.
-- In contrast to GetNextWidgetNum, the number is not incremented.
-- @param type The widget type
function AceGUI:GetWidgetCount(type)
  return self.counts[type] or 0
end

--- Return the version of the currently registered widget type.
-- @param type The widget type
function AceGUI:GetWidgetVersion(type)
  return WidgetVersions[type]
end

-------------
-- Layouts --
-------------

--[[
	A Layout is a func that takes 2 parameters
		content - the frame that widgets will be placed inside
		children - a table containing the widgets to layout
]]

-- Very simple Layout, Children are stacked on top of each other down the left side
AceGUI:RegisterLayout('List', function(content, children)
  local height = 0
  local width = content.width or content:GetWidth() or 0
  for i = 1, #children do
    local child = children[i]

    local frame = child.frame
    frame:ClearAllPoints()
    frame:Show()
    if i == 1 then
      frame:SetPoint('TOPLEFT', content)
    else
      frame:SetPoint('TOPLEFT', children[i - 1].frame, 'BOTTOMLEFT')
    end

    if child.width == 'fill' then
      child:SetWidth(width)
      frame:SetPoint('RIGHT', content)

      if child.DoLayout then
        child:DoLayout()
      end
    elseif child.width == 'relative' then
      child:SetWidth(width * child.relWidth)

      if child.DoLayout then
        child:DoLayout()
      end
    end

    height = height + (frame.height or frame:GetHeight() or 0)
  end
  safecall(content.obj.LayoutFinished, content.obj, nil, height)
end)

-- A single control fills the whole content area
AceGUI:RegisterLayout('Fill', function(content, children)
  if children[1] then
    children[1]:SetWidth(content:GetWidth() or 0)
    children[1]:SetHeight(content:GetHeight() or 0)
    children[1].frame:SetAllPoints(content)
    children[1].frame:Show()
    safecall(content.obj.LayoutFinished, content.obj, nil, children[1].frame:GetHeight())
  end
end)

AceGUI:RegisterLayout('Flow', function(content, children)
  --used height so far
  local height = 0
  --width used in the current row
  local usedwidth = 0
  --height of the current row
  local rowheight = 0
  local rowoffset = 0
  local lastrowoffset

  local width = content.width or content:GetWidth() or 0

  --control at the start of the row
  local rowstart
  local rowstartoffset
  local lastrowstart
  local isfullheight

  local frameoffset
  local lastframeoffset
  local oversize
  for i = 1, #children do
    local child = children[i]
    oversize = nil
    local frame = child.frame
    local frameheight = frame.height or frame:GetHeight() or 0
    local framewidth = frame.width or frame:GetWidth() or 0
    lastframeoffset = frameoffset
    -- HACK: Why did we set a frameoffset of (frameheight / 2) ?
    -- That was moving all widgets half the widgets size down, is that intended?
    -- Actually, it seems to be neccessary for many cases, we'll leave it in for now.
    -- If widgets seem to anchor weirdly with this, provide a valid alignoffset for them.
    -- TODO: Investigate moar!
    frameoffset = child.alignoffset or (frameheight / 2)

    if child.width == 'relative' then
      framewidth = width * child.relWidth
    end

    frame:Show()
    frame:ClearAllPoints()
    if i == 1 then
      -- anchor the first control to the top left
      frame:SetPoint('TOPLEFT', content)
      rowheight = frameheight
      rowoffset = frameoffset
      rowstart = frame
      rowstartoffset = frameoffset
      usedwidth = framewidth
      if usedwidth > width then
        oversize = true
      end
    else
      -- if there isn't available width for the control start a new row
      -- if a control is "fill" it will be on a row of its own full width
      if usedwidth == 0 or (framewidth + usedwidth > width) or child.width == 'fill' then
        if isfullheight then
          -- a previous row has already filled the entire height, there's nothing we can usefully do anymore
          -- (maybe error/warn about this?)
          break
        end
        --anchor the previous row, we will now know its height and offset
        rowstart:SetPoint(
          'TOPLEFT',
          content,
          'TOPLEFT',
          0,
          -(height + (rowoffset - rowstartoffset) + 3)
        )
        height = height + rowheight + 3
        --save this as the rowstart so we can anchor it after the row is complete and we have the max height and offset of controls in it
        rowstart = frame
        rowstartoffset = frameoffset
        rowheight = frameheight
        rowoffset = frameoffset
        usedwidth = framewidth
        if usedwidth > width then
          oversize = true
        end
        -- put the control on the current row, adding it to the width and checking if the height needs to be increased
      else
        --handles cases where the new height is higher than either control because of the offsets
        --math.max(rowheight-rowoffset+frameoffset, frameheight-frameoffset+rowoffset)

        --offset is always the larger of the two offsets
        rowoffset = math_max(rowoffset, frameoffset)
        rowheight = math_max(rowheight, rowoffset + (frameheight / 2))

        frame:SetPoint(
          'TOPLEFT',
          children[i - 1].frame,
          'TOPRIGHT',
          0,
          frameoffset - lastframeoffset
        )
        usedwidth = framewidth + usedwidth
      end
    end

    if child.width == 'fill' then
      child:SetWidth(width)
      frame:SetPoint('RIGHT', content)

      usedwidth = 0
      rowstart = frame
      rowstartoffset = frameoffset

      if child.DoLayout then
        child:DoLayout()
      end
      rowheight = frame.height or frame:GetHeight() or 0
      rowoffset = child.alignoffset or (rowheight / 2)
      rowstartoffset = rowoffset
    elseif child.width == 'relative' then
      child:SetWidth(width * child.relWidth)

      if child.DoLayout then
        child:DoLayout()
      end
    elseif oversize then
      if width > 1 then
        frame:SetPoint('RIGHT', content)
      end
    end

    if child.height == 'fill' then
      frame:SetPoint('BOTTOM', content)
      isfullheight = true
    end
  end

  --anchor the last row, if its full height needs a special case since  its height has just been changed by the anchor
  if isfullheight then
    rowstart:SetPoint('TOPLEFT', content, 'TOPLEFT', 0, -height)
  elseif rowstart then
    rowstart:SetPoint(
      'TOPLEFT',
      content,
      'TOPLEFT',
      0,
      -(height + (rowoffset - rowstartoffset) + 3)
    )
  end

  height = height + rowheight + 3
  safecall(content.obj.LayoutFinished, content.obj, nil, height)
end)
