local MINOR = 8
local lib, minor = LibStub('LibDropDown')
if minor > MINOR then
	return
end

--[[ Menu:header
Documentation for the [Menu](Menu) object.
Created with [LibDropDown:NewMenu()](LibDropDown#libdropdownnewmenuparent-name).
--]]

local function OnShow(self)
	-- gather some size data
	local padding = self.parent.padding
	local spacing = self.parent.spacing
	local minWidth = self.parent.minWidth
	local maxWidth = self.parent.maxWidth
	local width = 0
	local height = -spacing

	for _, Line in next, self.lines do
		if(Line:IsShown()) then
			local lineWidth
			if(Line.Text:IsShown()) then
				lineWidth = Line.Text:GetWidth()
			else
				lineWidth = Line:GetTextWidth()
			end
			if(Line.Radio:IsShown()) then
				lineWidth = lineWidth + Line.Radio:GetWidth() + padding
			end
			lineWidth = math.max(lineWidth, minWidth)

			if(maxWidth) then
				lineWidth = math.min(lineWidth, maxWidth)
			end

			if(lineWidth > width) then
				width = lineWidth
			end

			height = height + (16 + spacing)
		end
	end

	-- make sure all lines are the same width
	for _, Line in next, self.lines do
		Line:SetWidth(width)
	end

	-- resize the entire frame
	self:SetSize(width, height)
	self.Backdrop:SetSize(width + padding * 2, height + padding * 2)

	-- positioning
	self:ClearAllPoints()
	if(self.parent == self) then
		-- this is the first menu
		if(self.parent.anchorCursor) then
			local x, y = GetCursorPosition()
			local scale = UIParent:GetScale()
			self:SetPoint('TOP', UIParent, 'BOTTOMLEFT', x / scale, (y / scale) + 8) -- 8 to let the cursor end up on the first line
		else
			self:SetPoint(unpack(self.parent.anchor))
		end
	else
		-- submenu
		self:SetPoint('TOPLEFT', self:GetParent(), 'TOPRIGHT', self.parent.gap, 0)
	end
end

local menuMixin = {}
--[[ Menu:Toggle()
Toggles the dropdown menu, closing all others (see [LibDropDown:CloseAll()](LibDropDown#libdropdowncloseall)).
--]]
function menuMixin:Toggle()
	-- hide everything first
	lib:CloseAll(self)

	-- toggle this
	self:SetShown(not self:IsShown())
end

local dummyFontInstance = CreateFont('LibDropDownDummyFontObject')
--[[ Menu:UpdateLine(_index, data_)
Update a line with the given index with the supplied data.

* `index`: menu line index _(integer)_
* `data`: line data _(table)_ (see [Menu:AddLine(_data_)](Menu#menuaddlinedata))
--]]
function menuMixin:UpdateLine(index, data)
	local Line = self.lines[index]
	if(not Line) then
		Line = lib:CreateLine(self)
		Line:SetPoint('TOPLEFT', 0, -((16 + self.parent.spacing) * (index - 1)))
		table.insert(self.lines, Line)
	end

	Line:Reset()

	Line.func = data.func
	Line.args = data.args
	Line.tooltip = data.tooltip
	Line.tooltipTitle = data.tooltipTitle
	Line.keepShown = data.keepShown

	local fontName, fontSize, fontFlags
	if(data.font) then
		fontName = data.font
		fontSize = data.fontSize or 12
		fontFlags = data.fontFlags
	elseif(data.disabled) then
		dummyFontInstance:SetFontObject(data.fontObjectDisabled or self.parent.disabledFont)
		fontName, fontSize, fontFlags = dummyFontInstance:GetFont()
	else
		dummyFontInstance:SetFontObject(data.fontObjectDisabled or self.parent.disabledFont)
		fontName, fontSize, fontFlags = dummyFontInstance:GetFont()
	end

	if(data.isSpacer) then
		Line.Spacer:Show()
		Line:EnableMouse(false)
	elseif(data.isTitle) then
		local text = data.text
		assert(text and type(text) == 'string', 'Missing required data "text"')
		Line.Text:SetText(text)
		Line:EnableMouse(false)
		Line:SetNormalFontObject(self.parent.titleFont)
	else
		Line:EnableMouse(true)

		local text = data.text
		assert(text and type(text) == 'string', 'Missing required data "text"')

		Line.Text:SetFont(fontName, fontSize, fontFlags)
		Line.Text:SetText(text)

		Line:SetTexture(data.texture, data.textureColor)

		if(data.icon) then
			local width = data.iconWidth or 16
			local height = data.iconHeight or 16
			local fileWidth = data.iconFileWidth or width
			local fileHeight = data.iconFileHeight or height

			local left, right, top, bottom = 0, 1, 0, 1
			if(data.iconTexCoords) then
				left, right, top, bottom = unpack(data.iconTexCoords)
			end

			Line:SetIcon(data.icon, fileWidth, fileHeight, width, height, left, right, top, bottom)
		end

		if(data.atlas) then
			local atlas = C_Texture.GetAtlasInfo(data.atlas)
			assert(atlas and (atlas.filename or atlas.file), 'No atlas \'' .. data.atlas .. '\' exists')

			local width = data.atlasWidth
			local height = data.atlasHeight

			if(not height) then
				-- default to line height
				height = 16
			end

			if(not width) then
				-- keeping aspect ratio of the atlas
				width = (atlas.width / atlas.height) * height
			end

			local x = data.atlasOffsetX or data.atlasOffset or 0
			local y = data.atlasOffsetY or data.atlasOffset or 0

			Line:SetAtlas(data.atlas, height, width, x, y)
		end

		if(data.disabled) then
			Line:Disable()
			Line:SetMotionScriptsWhileDisabled(not not data.forceMotion)
		else
			Line:Enable()
		end

		if(data.menu) then
			Line.Expand:Show()
		else
			if(data.isColorPicker) then
				local r, g, b, a = data.colorR, data.colorG, data.colorB, data.colorOpacity
				local callback = data.colorPickerCallback
				assert(r and type(r) == 'number', 'Missing required data "colorR"')
				assert(g and type(g) == 'number', 'Missing required data "colorG"')
				assert(b and type(b) == 'number', 'Missing required data "colorB"')
				assert(callback and type(callback) == 'function', 'Missing required data "colorPickerCallback"')

				if(not Line.colors) then
					Line.colors = CreateColor(r, g, b, a)
				else
					Line.colors:SetRGBA(r, g, b, a)
				end

				Line.colorPickerCallback = callback
				Line.ColorSwatch.Swatch:SetVertexColor(r, g, b, a or 1)
				Line.ColorSwatch:Show()
			else
				if(data.checked ~= nil) then
					Line.Radio:Show()

					if(type(data.checked) == 'function') then
						Line.checked = data.checked
						Line.isRadio = data.isRadio
					else
						if(data.isRadio) then
							Line:SetRadioState(not not data.checked)
						else
							Line:SetCheckedState(not not data.checked)
						end
					end

					if self.parent.checkButtonAlignment == 'LEFT' then
						Line.Radio:ClearAllPoints()
						Line.Radio:SetPoint('LEFT', Line)
						Line.Text:ClearAllPoints()
						Line.Text:SetPoint('LEFT', Line.Radio, 'RIGHT')
					else
						Line.Radio:ClearAllPoints()
						Line.Radio:SetPoint('RIGHT')
						Line.Text:ClearAllPoints()
						Line.Text:SetPoint('LEFT')
					end
				end
			end
		end

		-- TODO: slider
		-- TODO: editbox
		-- TODO: scrolling
	end

	Line:Show()
	Line:UpdateState()

	return Line
end

--[[ Menu:AddLines(_..._)
See [Menu:AddLine(_data_)](Menu#menuaddlinedata), this one does the exact same thing, except  
this one can add more than one line at a time.

* `...`: One or more tables containing line information.
--]]
function menuMixin:AddLines(...)
	for index = 1, select('#', ...) do
		self:AddLine(select(index, ...))
	end
end

--[[ Menu:AddLine(_data_)
Adds a line using the given data to the menu menu.  
Everything™ is optional, some are exclusive with others.

* `data`:
	* `text`: Text to show on the line _(string)_
	* `isTitle`: Turns the `text` into a title _(boolean)_
	* `isSpacer`: Turns the line into a spacer _(boolean)_
	* `func`: Function to execute when clicking the line _(function)_  
		Arguments passed: `button`, `args` (unpacked).
	* `keepShown`: Keeps the dropdown shown after clicking the line _(boolean)_
	* `args`: Table of arguments to pass through to the click function _(table)_
	* `tooltip`: Tooltip contents _(string)_
	* `tooltipTitle`: Tooltip title _(string)_
	* `tooltipWhileDisabled`: Enable tooltips while disabled _(boolean)_
	* `checked`: Show or hide a checkbox _(boolean/function)_
	* `isRadio`: Turns the checkbox into a radio button _(boolean)_
	* `isColorPicker`: Adds a color picker to the line _(boolean)_
	* `colorR`: Red color channel, 0-1 _(number)_
	* `colorG`: Green color channel, 0-1 _(number)_
	* `colorB`: Blue color channel, 0-1 _(number)_
	* `colorOpacity`: Alpha channel, 0-1 _(number)_
	* `colorPickerCallback`: Callback function for the color picker _(function)_  
		Arguments passed: `color`, see [SharedXML\Util.lua's ColorMixin](https://www.townlong-yak.com/framexml/live/go/ColorMixin).
	* `icon`: Texture path for the icon to embed into the start of `text` _(string)_
	* `iconTexCoords`: Texture coordinates for cropping the `icon` _(array)_
	* `iconWidth`: Width of the displayed `icon` _(number)_
	* `iconHeight`: Height of the displayed `icon` _(number)_
	* `iconFileWidth`: File width of the `icon` _(number)_
	* `iconFileHeight`: File height of the `icon` _(number)_
	* `atlas`: Atlas to embed into the start of `text` _(string)_
	* `atlasWidth`: Width of the displayed `atlas` _(number)_
	* `atlasHeight`: Height of the displayed `atlas` _(number)_
	* `atlasOffsetX`: Horizontal offset for `atlas` _(number)_
	* `atlasOffsetY`: Vertical offset for `atlas` _(number)_
	* `atlasOffset`: Common offset for both axis for `atlas` _(number)_
	* `disabled`: Disables the whole line _(boolean)_
	* `texture`: Sets background texture that spans the line _(string)_
	* `textureColor`: Sets the color of the background texture _([ColorMixin object](https://www.townlong-yak.com/framexml/live/go/ColorMixin))_
	* `font`: Font to use for the line _(string)_
	* `fontSize`: Font size to use for the line, requires `font` to be set _(number)_
	* `fontFlags`: Font flags to use for the line, requires `font` to be set _(string)_
	* `fontObject`: Font object to use for the line _(string/[FontInstance](http://wowprogramming.com/docs/widgets/FontInstance))_
	* `menu`: Sub-menu for the current menu line _(array)_  
		This needs to contain one or more tables of `data` (all of the above) in an  
		indexed array. Can be chained.

#### Notes

The following are exclusive options, only one can be used at a time:

* `isSpacer`
* `isTitle`
* `menu`
* `isColorPicker`
* `checked`
* `font`
* `fontObject`
--]]
function menuMixin:AddLine(data)
	if(not self.data) then
		self.data = {}
	end

	table.insert(self.data, data)
	local Line = self:UpdateLine(#self.data, data)

	if(data.menu) then
		local Menu = lib:NewMenu(Line)
		Menu:AddLines(unpack(data.menu))

		if(#data.menu == 0) then
			error('Sub-menu created but no entries found.')
		end

		table.insert(self.menus, Menu)
		Line.Menu = Menu
	else
		Line.Menu = nil
	end
end

--[[ Menu:RemoveLine(_index_)
Removes a specific line by index.

- `index`: Number between 1 and [Menu:NumLines()](Menu#menunumlines)
--]]
function menuMixin:RemoveLine(index)
	assert(index >= 1 and index <= self:NumLines(), 'index out of scope')
	table.remove(self.data, index)
	self.lines[index]:Hide()
end

--[[ Menu:ClearLines()
Removes all lines in the menu.
--]]
function menuMixin:ClearLines()
	if(self.data) then
		table.wipe(self.data)

		for _, Line in next, self.lines do
			Line:Hide()
		end
	end
end

--[[ Menu:NumLines()
Returns the number of lines in the menu.
--]]
function menuMixin:NumLines()
	return #self.lines
end

--[[ Menu:Refresh()
Iterates through every line and refreshes it with the latest data.

If used to refresh the checked state of a line, make sure to set the lines' `data.checked` to a
function that will return the current state.
--]]
function menuMixin:Refresh()
	for index, data in next, self.data do
		self:UpdateLine(index, data)
	end
end

--[[ Menu:SetStyle(_name_)
Sets the active style for all menus related to this one.

- `name`: Name of registered style (see [LibDropDown:RegisterStyle](LibDropDown#libdropdownregisterstyle))
--]]
function menuMixin:SetStyle(name)
	if(not name) then
		name = self.parent.style or 'DEFAULT'
	elseif(not lib.styles[name]) then
		error('Style "' .. name .. '" does not exist.')
	end

	self.parent.style = name

	local data = lib.styles[name]
	self.parent.spacing = data.spacing or 0
	self.parent.padding = data.padding or 10
	self.parent.minWidth = data.minWidth or 100
	self.parent.maxWidth = data.maxWidth
	self.parent.gap = data.gap or 10
	self.parent.normalFont = data.normalFont or 'GameFontHighlightSmallLeft'
	self.parent.highlightFont = data.highlightFont or self.parent.normalFont
	self.parent.disabledFont = data.disabledFont or 'GameFontDisableSmallLeft'
	self.parent.titleFont = data.titleFont or 'GameFontNormal'
	self.parent.radioTexture = data.radioTexture or [[Interface\Common\UI-DropDownRadioChecks]]
	self.parent.highlightTexture = data.highlightTexture or [[Interface\QuestFrame\UI-QuestTitleHighlight]]
	self.parent.expandTexture = data.expandTexture or [[Interface\ChatFrame\ChatFrameExpandArrow]]
	self.parent.checkButtonAlignment = data.checkButtonAlignment or 'RIGHT'

	self.Backdrop:SetBackdrop(data.backdrop)
	self.Backdrop:SetBackdropColor((data.backdropColor or HIGHLIGHT_FONT_COLOR):GetRGBA())
	self.Backdrop:SetBackdropBorderColor((data.backdropBorderColor or HIGHLIGHT_FONT_COLOR):GetRGBA())
end

--[[ Menu:GetStyle()
Returns the name of the active style for the menu (and child menus).
--]]
function menuMixin:GetStyle()
	return self.parent.style
end

--[[ Menu:SetAnchor(_point, anchor, relativePoint, x, y_)
Replaces the default anchor with a custom one.
Exact same parameters as in [Widgets:SetPoint](http://wowprogramming.com/docs/widgets/Region/SetPoint), read that documentation instead.
--]]
function menuMixin:SetAnchor(point, anchor, relativePoint, x, y)
	self.parent.anchor[1] = point
	self.parent.anchor[2] = anchor
	self.parent.anchor[3] = relativePoint
	self.parent.anchor[4] = x
	self.parent.anchor[5] = y
end

--[[ Menu:GetAnchor()
Returns the point data for the registered anchor (see [Widgets:GetPoint](http://wowprogramming.com/docs/widgets/Region/GetPoint)).
--]]
function menuMixin:GetAnchor()
	return unpack(self.parent.anchor)
end

--[[ Menu:SetAnchorCursor(_state_)
Allows the anchor to be overridden and places the menu on the cursor.

* `state`: Enables/disables cursor anchoring _(boolean)_
--]]
function menuMixin:SetAnchorCursor(state)
	self.parent.anchorCursor = state
end

--[[ Menu:IsAnchorCursor()
Returns the boolean state of whether the menu should be anchored to the cursor or not.
--]]
function menuMixin:IsAnchorCursor()
	return self.parent.anchorCursor
end

--[[ Menu:SetCheckAlignment(_alignment_)
Sets the alignment of check/radio buttons within the menu.

* `alignment`: Either "LEFT" or "RIGHT" (default) _(string)_
--]]
function menuMixin:SetCheckAlignment(alignment)
	if alignment == 'LEFT' or alignment == 'RIGHT' then
		self.parent.checkButtonAlignment = alignment
	end
end

--[[ LibDropDown:NewMenu(_parent_, _name_)
Creates and returns a new, empty dropdown [Menu](Menu).

* `parent`: Frame for parenting. _(frame/string)_
* `name`: Global name for the menu. Falls back to `parent` name with suffix. _(string)_
--]]
function lib:NewMenu(parent, name)
	assert(parent, 'A menu requires a given parent')

	if(type(parent) == 'string') then
		parent = _G[parent]
	end

	local Menu = Mixin(CreateFrame('Button', (name or parent:GetDebugName() .. 'Menu'), parent), menuMixin, CallbackRegistryMixin)
	Menu:Hide()
	Menu:EnableMouse(true)
	Menu:SetClampedToScreen(true)
	Menu:SetScript('OnShow', OnShow)

	local Backdrop = CreateFrame('Frame', '$parentBackdrop', Menu, 'BackdropTemplate')
	Backdrop:SetPoint('CENTER')
	Menu.Backdrop = Backdrop

	Menu.parent = parent.parent or Menu
	Menu.lines = {}
	Menu.menus = {}

	Menu:SetStyle()

	-- this taints the game menu or smth
	-- table.insert(UIMenus, Menu:GetDebugName())

	Menu.anchor = {'TOP', Menu:GetParent(), 'BOTTOM', 0, -12} -- 8, 22
	Menu.anchorCursor = false
	Menu:SetClampRectInsets(-20, 20, 20, -20)

	lib.dropdowns[Menu] = true
	return Menu
end
