
-- module setup
local me = { name = "scrollframe"}
local mod = thismod
mod[me.name] = me

--[[
ScrollFrame.lua

This is a class to create arbitrary scrollframe controls. A scrollframe is a list of frames that is iterated using a scroll bar.

In the implementation, there is a finite number of frames that are visible, and they are changed as the list is scrolled. To setup a frame, you do not expose the underlying dataset, but give a series of callback functions that describe your data set or do small parts of drawing.

The format of the constructor is 

	local scrollframe = mod.scrollframe:createscrollframe(parent, height, scrollbarwidth, framecompression, newframecallback, countcallback, drawitemcallback)

<parent> is frame that holds the scrollframe. Optional, i guess.

<height> is the initial height of the scroll frame, in screen units (~ pixels). The height can be changed later by 
	
	scrollframe:setheight(value)

<scrollbarwidth> is the side length in screen units of the scroll bar boxes, and width of the bar itself.

<framecompression> is used to pack the elements more closely. Most frames have a few pixels on the edges that are invisible, causing odd gaps in the table, specifying a value for this will cancel it out. Recommended value is <mod.gui.invisedge>.

<newframecallback> creates a blank frame with the any child elements defined on it. The format is
	
	newframe = newframecallback(this)
	
<countcallback> returns the number of items in the data set. The format is

	count = countcallback()
	
<drawitemcallback> renders an item in the scrollframe. The format is

	drawitemcallback(frame, index)
	
<frame> is e.g. a return value from <newframecallback>, but it might be reused as well. 
<index> is the index of the item in the data set.
]]

me.test = function()
	
	me.dataset = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"}
	
	me.newframe = function(parent)
		
		local frame = mod.gui.createframe(parent, 200, 40, mod.helpmenu.background.frame)
		frame.text = mod.gui.createfontstring(frame, 18, true)
		frame.text:SetPoint("TOPLEFT", mod.gui.border, -mod.gui.border)
		
		return frame
	end
	
	me.count = function()
		
		return table.getn(me.dataset)
		
	end
	
	me.drawitem = function(frame, index)
		
		frame.text:SetText(me.dataset[index])
		
	end
	
	me.x = me.createscrollframe(mod.frame, 150, 30, mod.gui.invisedge, me.newframe, me.count, me.drawitem)
	
	me.x:SetPoint("BOTTOMLEFT", UIParent, 500, 500)
	
end

--[[
mod.scrollframe.createscrollframe(parent, height, scrollbarwidth, framecompression, newframecallback, countcallback, drawitemcallback)

For details, see the comments at the top of the file.

Returns: a scroll frame instance.
]]
me.createscrollframe = function(parent, height, scrollbarwidth, framecompression, newframecallback, countcallback, drawitemcallback)
	
	-- base frame / anchor point
	local this = CreateFrame("Frame", nil, parent)
	this:SetWidth(1)
	this:SetHeight(1)
	
	-- load arguments into ourself
	this.height = height
	this.newframe = newframecallback
	this.count = countcallback
	this.drawitem = drawitemcallback
	this.scrollbarwidth = scrollbarwidth
	this.framecompression = framecompression

	-- other data
	this.position = 0
	this.framepool = { }

	-- fill in instance methods
	this.createcomponents = me.createcomponents
	this.scroll = me.scroll
	this.updateposition = me.updateposition
	this.redraw = me.redraw
	
	-- create GUI components
	this:createcomponents()
	
	-- initial draw
	this:redraw()
	
	return this
	
end

--[[
scrollframe:createcomponents()

GUI constructor for a ScrollFrame instance.
]]
me.createcomponents = function(this)

	-- 1) Top Arrow; the scroll up arrow.
	this.toparrow = { }
	local arrow = this.toparrow
	
	arrow.container = mod.gui.createframe(this, this.scrollbarwidth + 2 * mod.gui.invisedge, this.scrollbarwidth+ 2 * mod.gui.invisedge)
	arrow.container:SetPoint("TOPLEFT", -mod.gui.invisedge, mod.gui.invisedge)
	
	arrow.button = CreateFrame("Button", nil, arrow.container)
	arrow.button:SetPoint("BOTTOMLEFT", mod.gui.border, mod.gui.border)
	arrow.button:SetPoint("TOPRIGHT", -mod.gui.border, -mod.gui.border)
	
	arrow.button:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Up")
	arrow.button:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Down")
	arrow.button:SetDisabledTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Disabled")
	arrow.button:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollUpButton-Highlight")
	
	arrow.button:GetNormalTexture():SetTexCoord(0.25, 0.75, 0.25, 0.70)
	arrow.button:GetPushedTexture():SetTexCoord(0.25, 0.75, 0.25, 0.70)
	arrow.button:GetDisabledTexture():SetTexCoord(0.25, 0.75, 0.25, 0.70)
	arrow.button:GetHighlightTexture():SetTexCoord(0.25, 0.75, 0.25, 0.70)
	
	arrow.button:SetScript("OnClick", me.toparrow_onclick)
	
	-- 2) Bottom Arrow; the scroll down arrow.
	this.bottomarrow = { }
	local arrow = this.bottomarrow
	
	arrow.container = mod.gui.createframe(this, this.scrollbarwidth + 2 * mod.gui.invisedge, this.scrollbarwidth+ 2 * mod.gui.invisedge)
	arrow.container:SetPoint("TOPLEFT", -mod.gui.invisedge, mod.gui.invisedge -this.height + this.scrollbarwidth)
	
	arrow.button = CreateFrame("Button", nil, arrow.container)
	arrow.button:SetPoint("BOTTOMLEFT", mod.gui.border, mod.gui.border)
	arrow.button:SetPoint("TOPRIGHT", -mod.gui.border, -mod.gui.border)
	
	arrow.button:SetNormalTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Up")
	arrow.button:SetPushedTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Down")
	arrow.button:SetDisabledTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Disabled")
	arrow.button:SetHighlightTexture("Interface\\Buttons\\UI-ScrollBar-ScrollDownButton-Highlight")
	
	arrow.button:GetNormalTexture():SetTexCoord(0.25, 0.75, 0.25, 0.70)
	arrow.button:GetPushedTexture():SetTexCoord(0.25, 0.75, 0.25, 0.70)
	arrow.button:GetDisabledTexture():SetTexCoord(0.25, 0.75, 0.25, 0.70)
	arrow.button:GetHighlightTexture():SetTexCoord(0.25, 0.75, 0.25, 0.70)
	
	arrow.button:SetScript("OnClick", me.bottomarrow_onclick)
	
	-- 3) Floating Box
	this.scrollbox = mod.gui.createframe(this, this.scrollbarwidth + 2 * mod.gui.invisedge, this.scrollbarwidth + 2 * mod.gui.invisedge, mod.helpmenu.background.box)
	this.scrollbox:SetPoint("TOPLEFT", -mod.gui.invisedge, mod.gui.invisedge - this.scrollbarwidth)
	
	this.scrollbox:SetMovable(true)
	this.scrollbox:RegisterForDrag("LeftButton")
	this.scrollbox:EnableMouse()
	
	-- Dragging Events
	this.scrollbox:SetScript("OnDragStart", me.scrollbox_ondragstart)
	this.scrollbox:SetScript("OnDragStop", me.scrollbox_ondragstop)
	this.scrollbox:SetScript("OnUpdate", me.scrollbox_onupdate)
		
end

--[[
---------------------------------------------------------------------------------------
			Scroll Bar Event Handlers - Clicking Scroll Buttons
---------------------------------------------------------------------------------------
]]

-- This is the "OnClick" event for the ScrollUp arrow.
me.toparrow_onclick = function(this)
	
	-- this:GetParent() is the fram that holds the arrow. Another GetParent() is the scrollframe itself
	local scrollframe = this:GetParent():GetParent()
	
	scrollframe:scroll(-1)
	
end

-- This is the "OnClick" event for the ScrollDown arrow.
me.bottomarrow_onclick = function(this)
	
	-- this:GetParent() is the fram that holds the arrow. Another GetParent() is the scrollframe itself
	local scrollframe = this:GetParent():GetParent()
	
	scrollframe:scroll(1)
	
end

--[[
scrollframe:scroll(direction)

Called when the user clicks on the of scroll arrows. Sort it out.

<direction> is -1 for up, 1 for down.
]]
me.scroll = function(this, direction)
	
	local count = this.count()
	
	-- if list is empty ignore scrolls
	if count == 0 then
		return
	end
	
	-- increment position
	this.position = this.position + direction / count

	-- bound
	this.position = math.max(0, math.min(1, this.position))
	
	-- update the scrollbox' position
	local offset = this.position * (this.height - 3 * this.scrollbarwidth)
	
	this.scrollbox:SetPoint("TOPLEFT", -mod.gui.invisedge, mod.gui.invisedge - this.scrollbarwidth - offset)
	
	-- redraw
	this:redraw()
	
end

--[[
------------------------------------------------------------------------------------
	Scroll Bar Event Handlers - Free Scrolling by Dragging the Bar 
------------------------------------------------------------------------------------
]]

-- This is the "OnDragStart" event for the <scrollframe.scrollbox> frame
me.scrollbox_ondragstart = function(this)
	
	this:StartMoving()
	this.ismoving = true
	
end

-- This is the "OnDragStop" event for the <scrollframe.scrollbox> frame
me.scrollbox_ondragstop = function(this)
	
	this:StopMovingOrSizing()
	this.ismoving = nil
	
end

-- This is the "OnUpdate" event for the <scrollframe.scrollbox> frame
me.scrollbox_onupdate = function(this)
	
	local scrollframe = this:GetParent()
	local topbox = scrollframe.toparrow.container
	local bottombox = scrollframe.bottomarrow.container
		
	if this.ismoving == true then
		-- realign him and bound him
		
		-- 1) set x right
		if this:GetLeft() ~= topbox:GetLeft() then
			this:SetPoint("TOPLEFT", -mod.gui.invisedge, this:GetTop() - scrollframe:GetTop())
		end
		
		-- 2) prevent upper bound overshoot
		if this:GetTop() > topbox:GetTop() - scrollframe.scrollbarwidth then
			this:SetPoint("TOPLEFT", -mod.gui.invisedge, mod.gui.invisedge - scrollframe.scrollbarwidth)
		
		-- 3) prevent lower bound overshoot
		elseif this:GetTop() < bottombox:GetTop() + this:GetParent().scrollbarwidth then
			this:SetPoint("TOPLEFT", -mod.gui.invisedge, mod.gui.invisedge - scrollframe.height + 2 * scrollframe.scrollbarwidth)
		end
		
		-- 4) now evaluate its position (fraction from 0 to 1)
		local maxtop = topbox:GetTop() - scrollframe.scrollbarwidth
		local mintop = bottombox:GetTop() + scrollframe.scrollbarwidth
		local current = this:GetTop()
		
		local position = (maxtop - current) / (maxtop - mintop)
		
		-- bound to (0, 1) (might get slight overruns)
		position = math.max(0, math.min(1, position))
		
		scrollframe:updateposition(position)
	end	
	
end

--[[
scrollframe:updateposition(position)

This is called when the user is dragging the scroll wheel manually
]]
me.updateposition = function(this, position)
	
	-- for the moment, try redrawing on potentially every update
	this.position = position
	this:redraw()

end

--[[
scrollframe:redraw()

Renders the items in the list of the scrollframe.

--> fix visibility issues
]]
me.redraw = function(this)
	
	-- get the top item
	local count = this.count()
	
	-- if count = 0, then nothing doing imo. well, there could be problems if count is decreasing
	if count == 0 then
		return
	end
	
	local topindex = math.floor(0.01 + this.position * count) + 1
	
	if topindex > count then
		topindex = count
	end
	
	local spaceused = 0
	local frameindex = 0
	local dataindex, frame
	
	for dataindex = topindex, count do
		
		-- 1) draw next index
		frameindex = frameindex + 1 				-- starts at 1
		dataindex = topindex + frameindex - 1	-- starts at topindex
		
		-- a) get a frame. Check framepool or make a new one
		if this.framepool[frameindex] == nil then
			this.framepool[frameindex] = this:newframe()
		end
		
		frame = this.framepool[frameindex]
		
		-- b) anchor the frame
		frame:SetPoint("TOPLEFT", - this.framecompression + this.scrollbarwidth, this.framecompression - spaceused)
		
		-- c) draw
		this.drawitem(frame, dataindex)
		
		-- d) check height bounds
		spaceused = spaceused + (frame:GetHeight() - 2 * this.framecompression)
		
		if spaceused > this.height then
			frame:Hide()
			break
		
		else
			frame:Show()
		end
		
	end
	
	-- hide any frames below the last one
	for x = frameindex + 1, #this.framepool do
		this.framepool[x]:Hide()
	end
	
end