require 'scripts/core/core.lua'

--------------------------------------------------------------------------------
-- GUI Commands
--------------------------------------------------------------------------------
GUI_COMMAND_FOCUS = "COMMAND_FOCUS"
GUI_COMMAND_UNFOCUS = "COMMAND_UNFOCUS"
GUI_COMMAND_PRESS = "COMMAND_PRESS"
GUI_COMMAND_DRAG = "COMMAND_DRAG"
GUI_COMMAND_RELEASE = "COMMAND_RELEASE"
GUI_COMMAND_CLICK = "COMMAND_CLICK"
GUI_COMMAND_LEFT = "COMMAND_LEFT"
GUI_COMMAND_RIGHT = "COMMAND_RIGHT"
GUI_COMMAND_UP = "COMMAND_UP"
GUI_COMMAND_DOWN = "COMMAND_DOWN"


--------------------------------------------------------------------------------
-- GUI
--------------------------------------------------------------------------------
GUI = class()
GUI.isTouchBanned = false
GUI.pressedCommandTarget = nil
GUI.focusedElement = nil
GUI.unfocusedElementByTouched = nil -- added by iQue hyang
GUI.pressMode = "once"  --< "repeat" or "once", -- added by iQue hyang
GUI.pressedEnterMode = true --< "ture" or "false", -- added by iQue hyang
GUI.clickMode = "release" --< "release" or "push"
GUI.buttonMode = "push"   --< "release" or "push"
GUI.dragThresholdX = 4
GUI.dragThresholdY = 4
GUI.hasAutomaticNavigation = false

-- Neighbour finding
GUI_SIDE_L, GUI_SIDE_B, GUI_SIDE_R, GUI_SIDE_T  = 0, 1, 2, 3
GUI_NEIGHBOUR_SELECTIVITY = 0.5

-- debug verbosity : set a category to true for more verbose
GUI.DEBUG_LEVEL = { runtime = false, hit_test_ll = false, hit_test = false, inputs = false, input_repeat = false, dragging = false, commands = false, actions = false, states = false, callbacks = false, animations = false, info = true, scroll_layout = false }

--------------------------------------------------------------------------------
-- debugPrint
--! @brief Prints a text for debugging purposes
--! @param text text to print
--! @param level debug level of the message
--------------------------------------------------------------------------------
function GUI:debugPrint(text, level)
	if not level or GUI.DEBUG_LEVEL[level] == true then
		print("<GUI> " .. text)
	end
end

--------------------------------------------------------------------------------
-- warning
--! @brief Prints a warning text
--! @param text text to print
--------------------------------------------------------------------------------
function GUI:warning(text)
	print("<GUI> <WARNING> " .. text)
end

--------------------------------------------------------------------------------
-- error
--! @brief Prints a error text and throws a lua error
--! @param text text to print
--------------------------------------------------------------------------------
function GUI:error(text)
	error("<GUI> <ERROR> " .. text)
end

--------------------------------------------------------------------------------
-- registerButton
--! @brief Add a button to the list of candidates
--! @param button Button to register
--------------------------------------------------------------------------------
function GUI:registerButton(button)
	if self.buttonTable == nil then
		self.buttonTable = {}
	end
	table.insert(self.buttonTable, button)
end

--------------------------------------------------------------------------------
-- getButtonCenter
--! @brief Get the center of a button in screen space
--! @param button Button to compute
--! @return screenspace position X, screenspace position Y
--------------------------------------------------------------------------------
function GUI:getButtonCenter(button)
	local x, y = WorldNode_getWorldPosition(button.worldNode)
	local lX, lY = ClickableComponent_getCenter(button.clickable)
	x = x + lX;
	y = y + lY;
	x, y = CameraComponent_worldToScreenPosition(self.camera, x, y)
	return x, y
end

--------------------------------------------------------------------------------
-- getButtonCorners
--! @brief Get a list of all corners in a button, in screen-space
--! @param button Button to use
--! @param outputList Table of {button, screenspace x, screenspace y)
--------------------------------------------------------------------------------
function GUI:getButtonCorners(button, outputList)
	
	local x, y = WorldNode_getWorldPosition(button.worldNode)
	local lX, lY = ClickableComponent_getCenter(button.clickable)
	local w, h = ClickableComponent_getBoxShapeSize(button.clickable)
	x = x + lX;
	y = y + lY;
	
	local tx, ty = CameraComponent_worldToScreenPosition(self.camera, x + w / 2, y + h / 2)
	table.insert(outputList, {tx, ty})
	tx, ty =  CameraComponent_worldToScreenPosition(self.camera, x + w / 2, y - h / 2)
	table.insert(outputList, {tx, ty})
	tx, ty =  CameraComponent_worldToScreenPosition(self.camera, x - w / 2, y - h / 2)
	table.insert(outputList, {tx, ty})
	tx, ty =  CameraComponent_worldToScreenPosition(self.camera, x - w / 2, y + h / 2)
	table.insert(outputList, {tx, ty})
end

--------------------------------------------------------------------------------
-- getButtonNearestCorner
--! @brief  Get the nearest corner from a reference
--! @param cornerList List of corners defining a button
--! @param xRef screenspace X of reference
--! @param yRef screenspace Y of reference
--! @return screenspace xmin, screenspace ymin
--------------------------------------------------------------------------------
function GUI:getButtonNearestCorner(cornerList, xRef, yRef)
	
	local xBestDistance = math.huge
	local yBestDistance = math.huge
	local xMin = 0
	local yMin = 0
	
	for i = 1, #cornerList do
		local xDistance = math.abs(cornerList[i][1] - xRef)
		local yDistance = math.abs(cornerList[i][2] - yRef)
		
		if xDistance < xBestDistance then
			xBestDistance = xDistance
			xMin = cornerList[i][1]
		end
		if yDistance < yBestDistance then
			yBestDistance = yDistance
			yMin = cornerList[i][2]
		end
	end
	
	return xMin, yMin
end

--------------------------------------------------------------------------------
-- getMinMax
--! @brief  Get a table of the min and max coordinates
--! @param cornerList List of corners defining a button
--! @return screenspace xmin, screenspace ymin, screenspace xmax, screenspace ymax
--------------------------------------------------------------------------------
function GUI:getMinMax(cornerList)
	
	local xMin = math.huge
	local yMin = math.huge
	local xMax = -math.huge
	local yMax = -math.huge
	
	for i = 1, #cornerList do
		local xDistance = cornerList[i][1]
		local yDistance = cornerList[i][2]
		
		if xDistance < xMin then
			xMin = xDistance
		end
		if xDistance > xMax then
			xMax = xDistance
		end
		if yDistance < yMin then
			yMin = yDistance
		end
		if yDistance > yMax then
			yMax = yDistance
		end
	end

	return {xMin, xMax, yMin, yMax}
end

--------------------------------------------------------------------------------
-- isInPartition
--! @brief  Check if a button is in a partition
--! @param cornerList List of corners defining the candidate button
--! @param sourceCornerList List of corners defining the partitionnibng button
--! @param side Direction to check
--! @return true if in partition else false
--------------------------------------------------------------------------------
function GUI:isInPartition(cornerList, sourceCornerList, side)
	
	local candidateAABB = self:getMinMax(cornerList)
	local sourceAABB = self:getMinMax(sourceCornerList)
	
	if     side == GUI_SIDE_L and candidateAABB[2] < sourceAABB[1] then
		return true
	elseif side == GUI_SIDE_R and candidateAABB[1] > sourceAABB[2]  then
		return true
	elseif side == GUI_SIDE_B and candidateAABB[4] < sourceAABB[3]  then
		return true
	elseif side == GUI_SIDE_T and candidateAABB[3] > sourceAABB[4]  then
		return true
	else
		return false
	end
end

--------------------------------------------------------------------------------
-- getNeighbour
--! @brief  Get the best button to move to in a specific direction
--! @param buttonTable Table of all buttons
--! @param button Current focus
--! @param side Direction to check
--! @return the new button, or nil
--------------------------------------------------------------------------------
function GUI:getNeighbour(buttonTable, button, side)

	if not GUI.hasAutomaticNavigation then
		return nil
	end
	
	local bestButton = nil
	local bestScore = math.huge
	local sourceCornerList = {}
	self:getButtonCorners(button, sourceCornerList)
	
	-- Evaluate all candidate buttons
	for item = 1, #buttonTable do
		
		-- Partition test
		local cornerList = {}
		local tempButton = buttonTable[item]
		
		if Component_isEnabled(tempButton._ptr) then
			self:getButtonCorners(tempButton, cornerList)
			if self:isInPartition(cornerList, sourceCornerList, side) then
				
				-- Nearest corner test
				local buttonX, buttonY = self:getButtonCenter(button)
				local minX, minY = self:getButtonNearestCorner(cornerList, buttonX, buttonY)
				
				-- Distance from corner
				local score = 0
				local distX = math.abs(buttonX - minX)
				local distY = math.abs(buttonY - minY)
				
				-- Compute score
				if side == GUI_SIDE_L or side == GUI_SIDE_R then
					score = GUI_NEIGHBOUR_SELECTIVITY * distX + distY
				else
					score = GUI_NEIGHBOUR_SELECTIVITY * distY + distX
				end
				 	
			 	-- Nearest button
				if score < bestScore then
					bestScore = score
					bestButton = tempButton
				end
	 		end
	 	end
	end

	-- Result
	if bestButton == nil then
		GUI:debugPrint('No neighbour found', "neighbors")
	else
		GUI:debugPrint('Neighbour button is ' .. tostring(button) .. ' in ' .. WorldNode_getName(bestButton.worldNode), "neighbors")
	end
	return bestButton
end

--------------------------------------------------------------------------------
-- setInputCamera
--! @brief Sets the camera that is used for input detection
--! @param camera camera component reference
--! @param cameraViewportWidth width in pixels of the camera viewport
--! @param cameraViewportHeight height in pixels of the camera viewport
--------------------------------------------------------------------------------
function GUI:setInputCamera(camera, cameraViewportWidth, cameraViewportHeight)
	self.camera = camera
	self.cameraViewportWidth = cameraViewportWidth
	self.cameraViewportHeight = cameraViewportHeight
end

--------------------------------------------------------------------------------
-- inputToElementLocalPosition
--! @brief Converts a screen position to the element local coordinate system
--! @param element gui element
--! @param posX Screen position X
--! @param posY Screen position Y
--! @return local position X, local position Y
--------------------------------------------------------------------------------
function GUI:inputToElementLocalPosition(element, posX, posY)
	local screenPosX = posX
	local screenPosY = self.cameraViewportHeight - posY

	GUI:debugPrint("screen pos = " .. tostring(screenPosX) .. ", " .. tostring(screenPosY), "hit_test_ll")
	
	local worldPosX, worldPosY = CameraComponent_screenToWorldPosition(self.camera, screenPosX, screenPosY)
	
	return WorldNode_worldToLocalPosition(element.worldNode, worldPosX, worldPosY)
end

--------------------------------------------------------------------------------
-- getClickableAt
--! @brief Returns the clickable under a provided screen position
--! @param posX Screen position X
--! @param posY Screen position Y
--! @return clickable component [, local hit position X, local hit position Y]
--------------------------------------------------------------------------------
function GUI:getClickableAt(posX, posY)
	
	GUI:debugPrint("touch pos = " .. tostring(posX) .. ", " .. tostring(posY), "hit_test_ll")

	local screenPosX = posX
	local screenPosY = self.cameraViewportHeight - posY

	GUI:debugPrint("screen pos = " .. tostring(screenPosX) .. ", " .. tostring(screenPosY), "hit_test_ll")
	
	local worldPosX, worldPosY = CameraComponent_screenToWorldPosition(self.camera, screenPosX, screenPosY)
	
	GUI:debugPrint("world pos = " .. tostring(worldPosX) .. ", " .. tostring(worldPosY), "hit_test_ll")
	
	local hitClickable = ClickableComponentManager_raycastNearest(worldPosX, worldPosY)
	
	-- local localPosX = 0
	-- local localPosY = 0
	
	-- if hitClickable ~= nil then
	-- 	localPosX, localPosY = WorldNode_worldToLocalPosition(Component_getNode(hitClickable), worldPosX, worldPosY)
	-- end
	
	return hitClickable, worldPosX, worldPosY
end

--------------------------------------------------------------------------------
-- getElementAt
--! @brief Returns a gui element under a provided screen position
--! @param posX Screen position X
--! @param posY Screen position Y
--! @return gui element [, local hit position X, local hit position Y]
--------------------------------------------------------------------------------
function GUI:getElementAt(posX, posY)
	
	local clickable, worldPosX, worldPosY = self:getClickableAt(posX, posY)
	
	if clickable ~= nil then

		GUI:debugPrint("hit clickable " .. tostring(clickable), "hit_test")
			
		local scriptComp = ClickableComponent_getClickListener(clickable)
		
		if scriptComp ~= nil then
			local element = ScriptComponent_getScriptTable(scriptComp)
			
			if element.isGUIElement then -- Check it is a valid gui element
				return element, worldPosX, worldPosY
			end
		end
		
	end
	
	return nil, worldPosX, worldPosY
end

--------------------------------------------------------------------------------
-- sendCommand
--! @brief Sends a GUI command to a gui element and returns whether this command was handled or not.
--! @param element gui element to send the command to
--! @param command command to send
--! @return true if the command was handled, else false
--------------------------------------------------------------------------------
function GUI:sendCommand(element, command)
	
	if element.currentState ~= "disabled" then
		return element:sendCommand(command)
	end
	
end

--------------------------------------------------------------------------------
-- sendBubbleCommand
--! @brief Sends a GUI command to a gui element and fallback onto parents when command is not handled.
--! @param element gui element to send the command to
--! @param command command to send
--! @return element which handled this command. nil if the command has not been handled by any element in the ancestors hierarchy
--------------------------------------------------------------------------------
function GUI:sendBubbleCommand(element, command)
	
	if element.currentState ~= "disabled" then
		while element ~= nil and not element:sendCommand(command) do
			element = element.parent
		end
		
		if element then
			return element
		end
	end

	return nil	
end

--------------------------------------------------------------------------------
-- updateInputs
--! @brief Performs processing of the user inputs and send the commands accordingly to the corresponding elements
--! @param dt delta Time since last update
--------------------------------------------------------------------------------
function GUI:updateInputs(dt)
	
	self.currentTime = self.currentTime + dt
	
	local isTouched = GUI:isTouched()
	local wasTouched = GUI:wasTouched()
	
	local isTouchPressed = isTouched and not wasTouched
	local isTouchReleased = not isTouched and wasTouched
	
	-- Handle key buttons events
	if self.focusedElement ~= nil then
		if GUI:isLeftPressed(g_menu.invokerDevice) then
			GUI:debugPrint("Key left pressed", "inputs")
			if not self:sendCommand(self.focusedElement, { id = GUI_COMMAND_LEFT }) then
				local neighbour = self:getNeighbour(self.buttonTable, self.focusedElement, GUI_SIDE_L)
				if GUI.hasAutomaticNavigation and neighbour ~= nil then
					self:focusElement(neighbour)
				end
			end
		elseif GUI:isRightPressed(g_menu.invokerDevice) then
			GUI:debugPrint("Key right pressed", "inputs")
			if not self:sendCommand(self.focusedElement, { id = GUI_COMMAND_RIGHT }) then
				local neighbour = self:getNeighbour(self.buttonTable, self.focusedElement, GUI_SIDE_R)
				if GUI.hasAutomaticNavigation and neighbour ~= nil then
					self:focusElement(neighbour)
				end
			end
		elseif GUI:isUpPressed(g_menu.invokerDevice) then
			GUI:debugPrint("Key up pressed", "inputs")
			if not self:sendCommand(self.focusedElement, { id = GUI_COMMAND_UP }) then
				local neighbour = self:getNeighbour(self.buttonTable, self.focusedElement, GUI_SIDE_T)
				if GUI.hasAutomaticNavigation and neighbour ~= nil then
					self:focusElement(neighbour)
				end
			end
		elseif GUI:isDownPressed(g_menu.invokerDevice) then
			GUI:debugPrint("Key down pressed", "inputs")
			if not self:sendCommand(self.focusedElement, { id = GUI_COMMAND_DOWN }) then
				local neighbour = self:getNeighbour(self.buttonTable, self.focusedElement, GUI_SIDE_B)
				if GUI.hasAutomaticNavigation and neighbour ~= nil then
					self:focusElement(neighbour)
				end
			end
		elseif GUI:isValidatePressed(g_menu.invokerDevice) then
			GUI:debugPrint("Key validate pressed", "inputs")
			self:sendCommand(self.focusedElement, { id = GUI_COMMAND_PRESS })
			if self.buttonMode == "push" then
				self:sendCommand(self.focusedElement, { id = GUI_COMMAND_CLICK })
			end
		elseif GUI:isValidateDown(g_menu.invokerDevice) then
			GUI:debugPrint("Key validate down", "inputs")
			-- self:sendCommand(self.focusedElement, { id = GUI_COMMAND_PRESS })
		elseif GUI:isValidateReleased(g_menu.invokerDevice) then
			GUI:debugPrint("Key validate released", "inputs")
			if self.buttonMode == "release" then
				self:sendCommand(self.focusedElement, { id = GUI_COMMAND_CLICK })
			end
			self:sendCommand(self.focusedElement, { id = GUI_COMMAND_RELEASE })
		end
	end
	
	-- Handle touch/pointer events
	if (isTouchPressed or isTouchReleased) and not GUI.isTouchBanned then
	
		local touchPosX = 0
		local touchPosY = 0
	
		if isTouchPressed then
			GUI:debugPrint("Touch pressed", "inputs")
			touchPosX = Input_getState(TOUCH_DEVICE, TOUCH_X)
			touchPosY = Input_getState(TOUCH_DEVICE, TOUCH_Y)
			if self.focusedElement ~= nil then	
				-- added by iQue hyang
				GUI.unfocusedElementByTouched = GUI.focusedElement
				-- Unfocus
				self:focusElement(nil)
		
			end	
		elseif isTouchReleased then
			GUI:debugPrint("Touch released", "inputs")
			touchPosX = Input_getPreviousState(TOUCH_DEVICE, TOUCH_X)
			touchPosY = Input_getPreviousState(TOUCH_DEVICE, TOUCH_Y)
		end
		
		local element, worldPosX, worldPosY = self:getElementAt(touchPosX, touchPosY)
			
		if element ~= nil then
				
			GUI:debugPrint("element found " .. tostring(element), "hit_test")
			
			if isTouchPressed then
				self.pressedCommandTarget = element
				self.lastTouchedTarget = element
				self:sendCommand(element, { id = GUI_COMMAND_PRESS, posX = worldPosX, posY = worldPosY })

				if self.clickMode == "push" then
					self:sendCommand(element, { id = GUI_COMMAND_CLICK, posX = worldPosX, posY = worldPosY })
				end
				
				self.startDragPosX = worldPosX
				self.startDragPosY = worldPosY
				self.isDragging = false
			elseif isTouchReleased then
				if self.isDragging then -- Send release on dragged element
					self:sendCommand(self.dragCommandTarget, { id = GUI_COMMAND_RELEASE, posX = worldPosX, posY = worldPosY })
				elseif element == self.pressedCommandTarget then
					if self.clickMode == "release" then
						self:sendCommand(element, { id = GUI_COMMAND_CLICK, posX = worldPosX, posY = worldPosY })
					end

					self:sendCommand(element, { id = GUI_COMMAND_RELEASE, posX = worldPosX, posY = worldPosY })
				end
				self.pressedCommandTarget = nil
			end
		else
			GUI:debugPrint("non gui component", "hit_test")
		end
			
	elseif isTouched then -- drag
		GUI:debugPrint("Touch down", "inputs")
		
		local touchPosX = Input_getState(TOUCH_DEVICE, TOUCH_X)
		local touchPosY = Input_getState(TOUCH_DEVICE, TOUCH_Y)

		local element, worldPosX, worldPosY = GUI:getElementAt(touchPosX, touchPosY)
		
		if element then
			 GUI:debugPrint("element touched " .. tostring(element) .. " pressedTarget " .. tostring(self.pressedCommandTarget), "hit_test")
		end
		
		if self.pressedCommandTarget ~= nil or self.dragCommandTarget ~= nil then

			if not self.isDragging then
				-- Handle entering and leaving target click area
				if element ~= self.pressedCommandTarget then
					if self.lastTouchedTarget == self.pressedCommandTarget then -- Ensure RELEASE command is sent only once after leaving the pressedCommandTarget
						self:sendCommand(self.pressedCommandTarget, { id = GUI_COMMAND_RELEASE })
						if not GUI.pressedEnterMode then
							self.pressedCommandTarget = nil
						end
					end
				else
					if GUI.pressMode == "repeat" then -- Ensure repeat the PRESS command when press the pressedCommand Target
						self:sendCommand(self.pressedCommandTarget, { id = GUI_COMMAND_PRESS, posX = worldPosX, posY = worldPosY })
					elseif GUI.pressMode == "once" and self.lastTouchedTarget ~= self.pressedCommandTarget then -- Ensure PRESS command is sent only once after entering the pressedCommandTarget in press once mode
						self:sendCommand(self.pressedCommandTarget, { id = GUI_COMMAND_PRESS, posX = worldPosX, posY = worldPosY })
					end
				end
				
				-- Test if threshold has been reached for dragging
				if math.abs(self.startDragPosX - worldPosX) > GUI.dragThresholdX or
				   math.abs(self.startDragPosY - worldPosY) > GUI.dragThresholdY then
				   	if self.pressedCommandTarget ~= nil then
				   		self.dragCommandTarget = self:sendBubbleCommand(self.pressedCommandTarget, { id = GUI_COMMAND_DRAG, posX = worldPosX, posY = worldPosY, previousPosX = self.startDragPosX, previousPosY = self.startDragPosY })
				   	end	
				   	-- If drag command has a response
				   	if self.dragCommandTarget ~= nil then
				   		GUI:debugPrint("Start dragging target " .. tostring(self.dragCommandTarget), "dragging")
						self.isDragging = true
						self.startDragPosX = worldPosX
						self.startDragPosY = worldPosY
						
						if self.lastTouchedTarget == self.pressedCommandTarget then -- Release pressed target if not already released
							self:sendCommand(self.pressedCommandTarget, { id = GUI_COMMAND_RELEASE })
						end
						
						self.pressedCommandTarget = nil
					end
				end
			else -- Handle dragging
				self:sendCommand(self.dragCommandTarget, { id = GUI_COMMAND_DRAG, posX = worldPosX, posY = worldPosY, previousPosX = self.startDragPosX, previousPosY = self.startDragPosY })
				self.startDragPosX = worldPosX
				self.startDragPosY = worldPosY
			end

		end
	
		self.lastTouchedTarget = element

	else
		self.pressedCommandTarget = nil
		self.dragCommandTarget = nil
		self.isDragging = false
	end
end

--------------------------------------------------------------------------------
-- focusElement
--! @brief Focus a gui element. Focused element will first receive validation commands. (ie. A button on a common Pad device)
--! @note Can't focus a disabled element (ie. that is in the disabled state)
--! @param element Element to focus. Can be nil to force unfocus.
--------------------------------------------------------------------------------
function GUI:focusElement(element)
	GUI:debugPrint("Focus element " .. tostring(element), "focus")
	if self.focusedElement ~= element and self.pressedCommandTarget == nil  then 
		-- if self.pressedCommandTarget ~= nil then
		-- 	self:sendCommand(self.pressedCommandTarget, { id = GUI_COMMAND_RELEASE })
		-- 	self.pressedCommandTarget = nil
		-- end

		if self.focusedElement ~= nil then
			self:sendCommand(self.focusedElement, { id = GUI_COMMAND_RELEASE })
			self:sendCommand(self.focusedElement, { id = GUI_COMMAND_UNFOCUS })
		end
		if element ~= nil and element.currentState ~= "disabled" then
			self.focusedElement = element
		else
			self.focusedElement = nil
		end
		if self.focusedElement ~= nil then
			self:sendCommand(self.focusedElement, { id = GUI_COMMAND_FOCUS })
		end
	end
	
end

