require 'scripts/gui/gui.lua'

--------------------------------------------------------------------------------
-- GUIScroll Object script
--! 
--! States
--! * idle
--! * disabled
--!
--! Attributes
--! * listener : script component that listens to the element events.
--! * idleVisual : visual for idle state.
--! * focusedVisual : visual for focused state.
--! * disabledVisual : visual for disabled state.
--! * direction : either "horizontal" or "vertical" or "both". Default : "horizontal"
--! * elements : world node that will contain all the list elements. If not provided, it will be automatically created
--! * layout : determines the elements arrangement within the list. Supported values : "horizontal_list", "vertical_list", "grid"
--! * hasInertia : determines if the scroll should have inertia when drag is released
--! * friction : friction value for inertia computation
--! * dragFactorX : factor to be applied on drag for X axis
--! * dragFactorY : factor to be applied on drag for Y axis
--! * columnsCount : Default : 2 if layout == "grid"
--! * rowsCount : Default : 2 if layout == "grid"
--! * hasInnerScroll : Scroll if content is smaller than the scroll area. Default : false
--! 
--! Events
--! * onElementFocus(element)
--! * onElementUnfocus(element)
--! * onScroll(scroller, valueX, valueY)
--!
--------------------------------------------------------------------------------

GUIScroll = class(GUIElement)

--------------------------------------------------------------------------------
-- start
--! @brief Callback when the object is added to the world
--------------------------------------------------------------------------------
function GUIScroll:start()

	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll:start()", "runtime")

	GUIElement.start(self)
	
	self.nodes = {}
	self.nodesIndexes = {}

	
	self.scrollXValue = 0
	self.scrollYValue = 0
	
	self.speedX = 0
	self.speedY = 0
	
	self.startX = 0
	self.startY = 0
	
	self.width = 0
	self.height = 0
	
	self.elementsCount = 0
	
	self.isDragging = false
	self.isDragEngaged = false
	
	self.computeInertia = false
	
	if not self.dragTreshold then
		self.dragTreshold = 5
	end
	
	if not self.direction then
		self.direction = "horizontal"
	end
	
	if not self.layout then
		if self.direction == "horizontal" then
			self.layout = "horizontal_list"
		elseif self.direction == "vertical" then
			self.layout = "vertical_list"
		end
	end

	if self.layout == "horizontal_list" then
		self.rowsCount = 1
		self.columnsCount = math.huge
	elseif self.layout == "vertical_list" then
		self.rowsCount = math.huge
		self.columnsCount = 1
	elseif self.layout == "grid" then
		if self.rowsCount == nil then
			self.rowsCount = 2
		end
		
		if self.columnsCount == nil then
			self.columnsCount = 2
		end
	end

	if not self.hasInnerScroll then
		self.hasInnerScroll = false
	end

	self.columnsWidth = {}
	self.rowsHeight = {}
	
	if not self.elements then
		self.elements = WorldNode_getChildByName(self.worldNode, "elements")
		if not self.elements then
			self.elements = createWorldNode("elements")
			WorldNode_addChildNode(self.worldNode, self.elements)
		end
	end
	
	self:setupClickableArea()
	
	local clickWidth, clickHeight = ClickableComponent_getBoxShapeSize(self.clickable)
	
	if not self.maxWidth then
		self.maxWidth = clickWidth
	end
	if not self.maxHeight then
		self.maxHeight = clickHeight
	end
	
	if self.dragFactorX == nil then self.dragFactorX = 1 end
	if self.dragFactorY == nil then self.dragFactorY = 1 end
	-- if self.xMin == nil then self.xMin = -math.huge end
	-- if self.yMin == nil then self.yMin = -math.huge end
	-- if self.xMax == nil then self.xMax = math.huge end
	-- if self.yMax == nil then self.yMax = math.huge end
	if self.friction == nil then self.friction = 5 end
	
	self.xMin = math.huge
	self.xMax = -math.huge
	self.yMin = math.huge
	self.yMax = -math.huge
	
end

--------------------------------------------------------------------------------
-- update
--! @brief Callback when the object is updated
--------------------------------------------------------------------------------
function GUIScroll:update(dt)
	self.dt  = dt
	
	if self.hasInertia and self.computeInertia then -- apply inertia
		self:scrollBy(self.speedX * dt, self.speedY * dt)
		self.speedX = Math.lerp(self.speedX, 0, dt * self.friction)
		self.speedY = Math.lerp(self.speedY, 0, dt * self.friction)
		
		if math.abs(self.speedX) < 1 and math.abs(self.speedY) < 1 then
			self.computeInertia = false
		end
	end

	GUIElement.update(self, dt)
end

--------------------------------------------------------------------------------
-- sendCommand
--! @brief Function to handle a gui command sent by the GUI system to the element
--! @param command command to handle
--------------------------------------------------------------------------------
function GUIScroll:sendCommand(command)
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll:sendCommand() " .. tostring(command.id), "commands")
	
	if command.id == GUI_COMMAND_DRAG then
		
		local localPosX, localPosY = WorldNode_worldToLocalPosition(self.worldNode, command.posX, command.posY)
		local localPrevPosX, localPrevPosY = WorldNode_worldToLocalPosition(self.worldNode, command.previousPosX, command.previousPosY)
		
		if command.first then
			self.isDragEngaged = false
		end
		
		if not self.isDragging then
			
			if not self.isDragEngaged then
			
				self.isDragEngaged = true
				GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll drag engaged", "dragging")
				
				local isHorizontalDrag = math.abs(localPosX - localPrevPosX) > math.abs(localPosY - localPrevPosY)
				
				if self.direction == "horizontal" and not isHorizontalDrag or
				   self.direction == "vertical" and isHorizontalDrag then
				   	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll wrong direction ", "dragging")
				   	return false
				end
				
				local clickWidth, clickHeight = ClickableComponent_getBoxShapeSize(self.clickable)
				if isHorizontalDrag and (not self.hasInnerScroll and (self.xMax - self.xMin) < clickWidth) then
					return false
				end
				
				if not isHorizontalDrag and (not self.hasInnerScroll and (self.yMax - self.yMin) < clickHeight) then
					return false
				end
				
				self.startX = localPrevPosX
				self.startY = localPrevPosY
					
				self.speedX = 0
				self.speedY = 0
				
				self.isDragging = true
				
			else
				GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll drag already engaged", "dragging")
				return false
			end
			
		else
		
			local dx = localPosX - localPrevPosX
			local dy = localPosY - localPrevPosY
	
			self.speedX = dx / self.dt
			self.speedY = dy / self.dt
			
			self.startX = localPosX
			self.startY = localPosY
			
			self:scrollBy(dx, dy)
			
			GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll dragging ", "dragging")
		end

		return true
	elseif command.id == GUI_COMMAND_RELEASE then
		GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll released ", "actions")
		
		self.computeInertia = true
		self.isDragging = false
		self.isDragEngaged = false
		
		return true
	end
	
	return GUIElement.sendCommand(self, command)
end

--------------------------------------------------------------------------------
-- getBoundaries
--! @brief Gets the scroll area boundaries
--! @return minX, maxX, minY, maxY
--------------------------------------------------------------------------------
function GUIScroll:getBoundaries()
	local clickableCenterX, clickableCenterY = ClickableComponent_getCenter(self.clickable)
	local clickableWidth, clickableHeight = ClickableComponent_getBoxShapeSize(self.clickable)
	
	local borderXMin = self.xMin - clickableWidth / 2 - clickableCenterX
	local borderXMax = clickableWidth / 2 - clickableCenterX - self.xMax
	local borderYMin = self.yMin - clickableHeight / 2 - clickableCenterY
	local borderYMax = clickableHeight / 2 - clickableCenterY - self.yMax
	local clampXMin = math.min(borderXMin, borderXMax)
	local clampXMax = math.max(borderXMin, borderXMax)
	local clampYMin = math.min(borderYMin, borderYMax)
	local clampYMax = math.max(borderYMin, borderYMax)

	return clampXMin, clampXMax, clampYMin, clampYMax
end

--------------------------------------------------------------------------------
-- scrollBy
--! @brief Scrolls the elements by the provided world unit increments. Applies the direction constraint.
--! @param dx scroll value on X (world unit)
--! @param dy scroll value on Y (world unit)
--------------------------------------------------------------------------------
function GUIScroll:scrollBy(dx, dy)
	local clickableWidth, clickableHeight = ClickableComponent_getBoxShapeSize(self.clickable)
	
	if self.direction == "vertical" then
		dx = 0
	end
	if self.direction == "horizontal" then
		dy = 0
	end
	
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll:scrollBy() " .. tostring(dx) .. " " ..tostring(dy), "scroll_layout")
	local x, y = WorldNode_getLocalPosition(self.elements)
	self:scrollTo(x + dx * self.dragFactorX, y + dy * self.dragFactorY)
end

--------------------------------------------------------------------------------
-- scrollTo
--! @brief Scrolls the elements to the specified position in world units
--! @param x position on x axis (world unit)
--! @param y position on y axis (world unit)
--------------------------------------------------------------------------------
function GUIScroll:scrollTo(x, y)
	
	local clampXMin , clampXMax , clampYMin , clampYMax = self:getBoundaries()
	
	x = Math.clamp(x, clampXMin, clampXMax)
	y = Math.clamp(y, clampYMin, clampYMax)
	
	if clampXMax ~= clampXMin then
		self.scrollXValue = (x - clampXMin) / (clampXMax - clampXMin)
	else
		self.scrollXValue = 0
	end
	
	if clampYMax ~= clampYMin then
		self.scrollYValue = (y - clampYMin) / (clampYMax - clampYMin)
	else
		self.scrollYValue = 0
	end
	
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll:scrollTo() : scroll values = " .. tostring(self.scrollXValue) .. "," .. tostring(self.scrollYValue), "scroll_layout")
	
	WorldNode_setLocalPosition(self.elements, x, y)
	
	if self.listener ~= nil and self.listener.onScroll ~= nil then
		self.listener:onScroll(self, self.scrollXValue, self.scrollYValue)
	end
end

--------------------------------------------------------------------------------
-- scrollPercentXY
--! @brief Scrolls the elements to the specified ratios on X and Y axises
--! @param scrollXValue ratio in range [0, 1] on the X axis
--! @param scrollYValue ratio in range [0, 1] on the Y axis
--------------------------------------------------------------------------------
function GUIScroll:scrollPercentXY(scrollXValue, scrollYValue)
	
	self.scrollXValue = Math.clamp(scrollXValue, 0, 1)
	self.scrollYValue = Math.clamp(scrollYValue, 0, 1)
	
	local clampXMin , clampXMax , clampYMin , clampYMax = self:getBoundaries()
	
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll:scrollXY() : clamp X = [" .. clampXMin .. ":" .. clampXMax .. "]", "scroll_layout")
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll:scrollXY() : clamp Y = [" .. clampYMin .. ":" .. clampYMax .. "]", "scroll_layout")
	
	x = self.scrollXValue * (clampXMax - clampXMin) + clampXMin
	y = self.scrollYValue * (clampYMax - clampYMin) + clampYMin
	
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll:scrollXY() : pos  = " .. tostring(x) .. "," .. tostring(y), "scroll_layout")
	
	WorldNode_setLocalPosition(self.elements, x, y)

end

--------------------------------------------------------------------------------
-- scrollPercentX
--! @brief Scrolls the elements to the specified ratios on X axis
--! @param scrollXValue ratio in range [0, 1] on the X axis
--------------------------------------------------------------------------------
function GUIScroll:scrollPercentX(scrollXValue)
	
	self.scrollXValue = Math.clamp(scrollXValue, 0, 1)

	local clampXMin , clampXMax , clampYMin , clampYMax = self:getBoundaries()
	
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll:scrollXY() : clamp X = [" .. clampXMin .. ":" .. clampXMax .. "]", "scroll_layout")
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll:scrollXY() : clamp Y = [" .. clampYMin .. ":" .. clampYMax .. "]", "scroll_layout")
	
	x = self.scrollXValue * (clampXMax - clampXMin) + clampXMin
	y = self.scrollYValue * (clampYMax - clampYMin) + clampYMin
	
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll:scrollXY() : pos  = " .. tostring(x) .. "," .. tostring(y), "scroll_layout")
	
	WorldNode_setLocalPosition(self.elements, x, y)

end

--------------------------------------------------------------------------------
-- scrollPercentY
--! @brief Scrolls the elements to the specified ratios on Y axis
--! @param scrollYValue ratio in range [0, 1] on the Y axis
--------------------------------------------------------------------------------
function GUIScroll:scrollPercentY(scrollYValue)
	
	self.scrollYValue = Math.clamp(scrollYValue, 0, 1)
	
	local clampXMin , clampXMax , clampYMin , clampYMax = self:getBoundaries()
	
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll:scrollXY() : clamp X = [" .. clampXMin .. ":" .. clampXMax .. "]", "scroll_layout")
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll:scrollXY() : clamp Y = [" .. clampYMin .. ":" .. clampYMax .. "]", "scroll_layout")
	
	x = self.scrollXValue * (clampXMax - clampXMin) + clampXMin
	y = self.scrollYValue * (clampYMax - clampYMin) + clampYMin
	
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll:scrollXY() : pos  = " .. tostring(x) .. "," .. tostring(y), "scroll_layout")
	
	WorldNode_setLocalPosition(self.elements, x, y)

end
--------------------------------------------------------------------------------
-- insertNode
--! @brief Inserts a new node into the elements at the specified position in the list
--! @param _node node to insert
--! @param _index index in the list to insert
--! @param _width width of the node
--! @param _height height of the node
--------------------------------------------------------------------------------
function GUIScroll:insertNode(_node, _index, _width, _height)
	
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUISCroll:insertNode() : " .. tostring(_node) .. " to " .. tostring(#self.nodesIndexes) .. " elements", "scroll_layout")
	
	local nodeInfo = { node = _node, index = _index, width = _width, height = _height }
	self.nodes[_node] = nodeInfo
	table.insert(self.nodesIndexes, _index, nodeInfo )
	
	for i=_index+1, #self.nodesIndexes do
		self.nodesIndexes[i].index = self.nodesIndexes[i].index + 1
	end
	
	if REED_DEBUG then
		self:checkConsistency()
	end
	
	WorldNode_addChildNode(self.elements, _node)
	
	-- Re-compute all elements position
	self:refreshLayout()
	
	-- Compute new scroll position
	self:scrollPercentXY(self.scrollXValue, self.scrollYValue)
	
	self.elementsCount = self.elementsCount + 1
end

--------------------------------------------------------------------------------
-- pushBackNode
--! @brief Adds a node at the end of the list
--! @param _node node to insert
--! @param _width width of the node
--! @param _height height of the node
--------------------------------------------------------------------------------
function GUIScroll:pushBackNode(_node, _width, _height)
	
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUISCroll:pushBackNode() : " .. tostring(_node) .. " to " .. tostring(#self.nodesIndexes) .. " elements", "scroll_layout")
	
	local index = #self.nodesIndexes + 1
	local nodeInfo = { node = _node, index = index, width = _width, height = _height }
	self.nodes[_node] = nodeInfo
	table.insert(self.nodesIndexes, nodeInfo )
	
	if REED_DEBUG then
		self:checkConsistency()
	end
	
	WorldNode_addChildNode(self.elements, _node)
	
	-- Re-compute all elements position
	self:refreshLayout()
	
	-- Compute new scroll position
	self:scrollPercentXY(self.scrollXValue, self.scrollYValue)

	self.elementsCount = self.elementsCount + 1
end

--------------------------------------------------------------------------------
-- removeNodeByIndex
--! @brief Removes a node from the list given its index
--! @param nodeIndex index of the node to remove
--------------------------------------------------------------------------------
function GUIScroll:removeNodeByIndex(nodeIndex)
	
	GUI:debugPrint("[" .. tostring(self) .. "]\tGUISCroll:removeNodeByIndex() : @ " .. tostring(nodeIndex) .. " from " .. tostring(#self.nodesIndexes) .. " elements", "scroll_layout")
	local node = self.nodesIndexes[nodeIndex].node
	self.nodes[node] = nil
	table.remove(self.nodesIndexes, nodeIndex)

	for i=nodeIndex, #self.nodesIndexes do
		self.nodesIndexes[i].index = self.nodesIndexes[i].index - 1
	end
	
	if REED_DEBUG then
		self:checkConsistency()
	end
	
	WorldNode_removeChildNode(self.elements, node)
	
	-- Re-compute all elements position
	self:refreshLayout()
	
	-- Compute new scroll position
	self:scrollPercentXY(self.scrollXValue, self.scrollYValue)

	self.elementsCount = self.elementsCount - 1
end

--------------------------------------------------------------------------------
-- removeNode
--! @brief Removes the specified node from the list
--! @param node node to remove
--------------------------------------------------------------------------------
function GUIScroll:removeNode(node)
	
	local nodeInfo = self.nodes[node]
	if not nodeInfo then
		GUI:error("[" .. tostring(self) .. "]\tGUIScroll:removeNode() : node does not exist in the nodes list")
		return
	end
	
	self:removeNodeByIndex(nodeInfo.index)
	
end

if REED_DEBUG then
--------------------------------------------------------------------------------
-- checkConsistency
--! @brief Checks the consistency of the internal lists
--------------------------------------------------------------------------------
	function GUIScroll:checkConsistency()
		for i=1, #self.nodesIndexes do
			if self.nodesIndexes[i].index ~= i then
				GUI:error("[" .. tostring(self) .. "]\tGUIScroll:checkConsistency() failed at index ".. tostring(i) .. " ;currentIndex = " .. self.nodesIndexes[i].index)
				return
			end
		end
	end
end

--------------------------------------------------------------------------------
-- refreshLayout
--! @brief Recomputes the position of all elements in the list according to the layout
--------------------------------------------------------------------------------
function GUIScroll:refreshLayout()
	
	self.xMin = math.huge
	self.xMax = -math.huge
	self.yMin = math.huge
	self.yMax = -math.huge
	
	for i = 1, #self.nodesIndexes do
		local nodeInfo = self.nodesIndexes[i]
		
		local row = 1
		local column = 1

		if self.layout == "horizontal_list" then
			column = i
		elseif self.layout == "vertical_list" then
			row = i
		elseif self.layout == "grid" then
			row = math.floor((i - 1) / self.columnsCount) + 1
			column = math.floor((i - 1) % self.columnsCount) + 1
		end
		
		if not self.rowsHeight[row] or self.rowsHeight[row] < nodeInfo.height then
			self.rowsHeight[row] = nodeInfo.height
			
			GUI:debugPrint("[" .. tostring(self) .. "]\trow height [" .. row .. "] = " .. tostring(self.rowsHeight[row]), "scroll_layout")
		end
		if not self.columnsWidth[column] or self.columnsWidth[column] < nodeInfo.width then
			self.columnsWidth[column] = nodeInfo.width
			
			GUI:debugPrint("[" .. tostring(self) .. "]\tcolumn width [" .. column .. "] = " .. tostring(self.columnsWidth[column]), "scroll_layout")
		end

		local posX, posY = self:computePositionForNodeAtIndex(i)
		
		nodeInfo.posX = posX
		nodeInfo.posY = posY
		WorldNode_setLocalPosition(nodeInfo.node, posX, posY)
		
		if posX < self.xMin then
			self.xMin = posX + self.columnsWidth[column] / 2
		end
		if posX > self.xMax then
			self.xMax = posX + self.columnsWidth[column] / 2
		end
		if posY < self.yMin then
			self.yMin = posY + self.rowsHeight[row] / 2
		end
		if posY > self.yMax then
			self.yMax = posY + self.rowsHeight[row] / 2
		end
		
		GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll node @ " .. tostring(i) .. " is at " .. tostring(posX) .. "," .. tostring(posY), "scroll_layout")
		GUI:debugPrint("[" .. tostring(self) .. "]\tGUIScroll min = " .. tostring(self.xMin) .. "," .. tostring(self.yMin) .. " max = " .. tostring(self.xMax) .. "," .. tostring(self.yMax), "scroll_layout")
	end
end


--------------------------------------------------------------------------------
-- computePositionForNodeAtIndex
--! @brief Computes the position of the node at the provided index in the list
--! @param index Index of the node in the list
--! @return position x and y
--------------------------------------------------------------------------------
function GUIScroll:computePositionForNodeAtIndex(index)
	
	local posX = 0
	local posY = 0
	
	if index == 1 then
		return 0, 0
	else
		
		local row = 1
		local column = 1

		if self.layout == "horizontal_list" then
			column = index
		elseif self.layout == "vertical_list" then
			row = index
		elseif self.layout == "grid" then
			row = math.floor((index - 1) / self.columnsCount) + 1
			column = math.floor((index - 1) % self.columnsCount) + 1
		end
		
		for i = 1, column - 1 do
			posX = posX + self.columnsWidth[i]
		end
		
		for j = 1, row - 1 do
			posY = posY + self.rowsHeight[j]
		end
	
	end

	return posX, posY
end


