local const = require("druid.const")

-- Localize functions for better performance
local gui_get_node = gui.get_node
local gui_get = gui.get
local gui_pick_node = gui.pick_node

---The helper module contains various functions that are used in the Druid library.
---You can use these functions in your projects as well.
---@class druid.helper
local M = {}

local POSITION_X = hash("position.x")
local SCALE_X = hash("scale.x")
local SIZE_X = hash("size.x")

local function get_text_width(text_node)
	if text_node then
		local text_metrics = M.get_text_metrics_from_node(text_node)
		local text_scale = gui_get(text_node, SCALE_X)
		return text_metrics.width * text_scale
	end

	return 0
end


local function get_icon_width(icon_node)
	if icon_node then
		return gui_get(icon_node, SIZE_X) * gui_get(icon_node, SCALE_X) -- icon width
	end

	return 0
end


local function is_text_node(node)
	return gui.get_text(node) ~= nil
end


---Text node or icon node can be nil
local function get_width(node)
	return is_text_node(node) and get_text_width(node) or get_icon_width(node)
end


---Center two nodes.
---Nodes will be center around 0 x position
---text_node will be first (at left side)
---@param text_node node|nil Gui text node
---@param icon_node node|nil Gui box node
---@param margin number Offset between nodes
---@return number width Total width of the centrated elements
---@local
function M.centrate_text_with_icon(text_node, icon_node, margin)
	return M.centrate_nodes(margin, text_node, icon_node)
end


---Center two nodes.
---Nodes will be center around 0 x position
---icon_node will be first (at left side)
---@param icon_node node|nil Gui box node
---@param text_node node|nil Gui text node
---@param margin number|nil Offset between nodes
---@return number width Total width of the centrated elements
---@local
function M.centrate_icon_with_text(icon_node, text_node, margin)
	return M.centrate_nodes(margin, icon_node, text_node)
end


---Centerate nodes by x position with margin.
---
---This functions calculate total width of nodes and set position for each node.
---The centrate will be around 0 x position.
---@param margin number|nil Offset between nodes
---@param ... node Nodes to centrate
---@return number width Total width of the centrated elements
function M.centrate_nodes(margin, ...)
	margin = margin or 0

	local width = 0
	local count = select("#", ...)
	local node_widths = {}

	-- We need to get total width
	for i = 1, count do
		local node = select(i, ...)
		node_widths[i] = get_width(node)
		width = width + node_widths[i]
	end
	width = width + margin * (count - 1)

	-- Posing all elements
	local pos_x = 0
	for i = 1, count do
		local node = select(i, ...)
		local node_width = node_widths[i]

		pos_x = pos_x + node_width/2 -- made offset for single item

		local pivot_offset = M.get_pivot_offset(gui.get_pivot(node))
		local new_pos_x = pos_x - width/2 + pivot_offset.x * node_width -- centrate node
		gui.set(node, POSITION_X, new_pos_x)

		pos_x = pos_x + node_widths[i]/2 + margin -- add second part of offset
	end

	return width
end


---Get GUI node from string name, node itself, or template/nodes structure
---@param node_id string|node The node name or node itself
---@param template string|nil Full path to the template
---@param nodes table<hash, node>|nil Nodes created with gui.clone_tree
---@return node The requested node
function M.get_node(node_id, template, nodes)
	if type(node_id) ~= "string" then
		-- Assume it's already node from gui.get_node
		return node_id
	end

	-- If template is set, then add it to the node_id
	if template and #template > 0 then
		node_id = template .. "/" .. node_id
	end

	-- If nodes is set, then try to find node in it
	if nodes then
		return nodes[node_id]
	end

	return gui_get_node(node_id)
end


---Get current screen stretch multiplier for each side
---@return number stretch_x
---@return number stretch_y
function M.get_screen_aspect_koef()
	local window_x, window_y = window.get_size()

	local stretch_x = window_x / gui.get_width()
	local stretch_y = window_y / gui.get_height()
	local stretch_koef = math.min(stretch_x, stretch_y)

	local koef_x = window_x / (stretch_koef * sys.get_config_int("display.width"))
	local koef_y = window_y / (stretch_koef * sys.get_config_int("display.height"))

	return koef_x, koef_y
end


---Get current GUI scale
---@return number scale
function M.get_gui_scale()
	local window_x, window_y = window.get_size()
	return math.min(window_x / gui.get_width(), window_y / gui.get_height())
end


---Move value from current to target value with step amount
---@param current number Current value
---@param target number Target value
---@param step number Step amount
---@return number New value
function M.step(current, target, step)
	if current < target then
		return math.min(current + step, target)
	else
		return math.max(target, current - step)
	end
end


---Clamp value between min and max. Works with nil values and swap min and max if needed.
---@param value number Value
---@param v1 number|nil Min value. If nil, value will be clamped to positive infinity
---@param v2 number|nil Max value If nil, value will be clamped to negative infinity
---@return number value Clamped value
function M.clamp(value, v1, v2)
	if v1 and v2 then
		if v1 > v2 then
			v1, v2 = v2, v1
		end
	end

	if v1 and value < v1 then
		return v1
	end

	if v2 and value > v2 then
		return v2
	end

	return value
end


---Calculate distance between two points
---@param x1 number First point x
---@param y1 number First point y
---@param x2 number Second point x
---@param y2 number Second point y
---@return number distance
function M.distance(x1, y1, x2, y2)
	return math.sqrt((x2 - x1) ^ 2 + (y2 - y1) ^ 2)
end


---Return sign of value
---@param val number Value
---@return number sign Sign of value, -1, 0 or 1
function M.sign(val)
	if val == 0 then
		return 0
	end

	return (val < 0) and -1 or 1
end


---Round number to specified decimal places
---@param num number Number
---@param num_decimal_places number|nil Decimal places
---@return number value Rounded number
function M.round(num, num_decimal_places)
	local mult = 10^(num_decimal_places or 0)
	return math.floor(num * mult + 0.5) / mult
end


---Lerp between two values
---@param a number First value
---@param b number Second value
---@param t number Lerp amount
---@return number value Lerped value
function M.lerp(a, b, t)
	return a + (b - a) * t
end


---Check if value contains in array
---@param array any[] Array to check
---@param value any Value
---@return number|nil index Index of value in array or nil if value not found
function M.contains(array, value)
	for index = 1, #array do
		if array[index] == value then
			return index
		end
	end
	return nil
end


---Make a copy table with all nested tables
---@param orig_table table Original table
---@return table Copy of original table
function M.deepcopy(orig_table)
	local orig_type = type(orig_table)
	local copy
	if orig_type == 'table' then
		copy = {}
		for orig_key, orig_value in next, orig_table, nil do
			copy[M.deepcopy(orig_key)] = M.deepcopy(orig_value)
		end
	else -- number, string, boolean, etc
		copy = orig_table
	end
	return copy
end


---Add all elements from source array to the target array
---@param target any[] Array to put elements from source
---@param source any[]|nil The source array to get elements from
---@return any[] The target array
function M.add_array(target, source)
	assert(target)

	if not source then
		return target
	end

	for index = 1, #source do
		table.insert(target, source[index])
	end

	return target
end


---Make a check with gui.pick_node, but with additional node_click_area check.
---@param node node
---@param x number
---@param y number
---@param node_click_area node|nil Additional node to check for click area. If nil, only node will be checked
---@local
function M.pick_node(node, x, y, node_click_area)
	local is_pick = gui_pick_node(node, x, y)

	if node_click_area then
		is_pick = is_pick and gui_pick_node(node_click_area, x, y)
	end

	return is_pick
end


---Get size of node with scale multiplier
---@param node node GUI node
---@return vector3 scaled_size
function M.get_scaled_size(node)
	return vmath.mul_per_elem(gui.get_size(node), gui.get_scale(node)) --[[@as vector3]]
end


---Get cumulative parent's node scale
---@param node node Gui node
---@param include_passed_node_scale boolean|nil True if add current node scale to result
---@return vector3 The scene node scale
function M.get_scene_scale(node, include_passed_node_scale)
	local scale = include_passed_node_scale and gui.get_scale(node) or vmath.vector3(1)
	local parent = gui.get_parent(node)
	while parent do
		scale = vmath.mul_per_elem(scale, gui.get_scale(parent))
		parent = gui.get_parent(parent)
	end

	return scale
end


---Return closest non inverted clipping parent node for given node
---@param node node GUI node
---@return node|nil stencil_node The closest stencil node or nil
function M.get_closest_stencil_node(node)
	if not node then
		return nil
	end

	local parent = gui.get_parent(node)
	while parent do
		local clipping_mode = gui.get_clipping_mode(parent)
		local is_clipping_normal = not gui.get_clipping_inverted(parent)

		if is_clipping_normal and clipping_mode == gui.CLIPPING_MODE_STENCIL then
			return parent
		end

		parent = gui.get_parent(parent)
	end

	return nil
end


---Get pivot offset for given pivot or node
---Offset shown in [-0.5 .. 0.5] range, where -0.5 is left or bottom, 0.5 is right or top.
---@param pivot_or_node number|node GUI pivot or node
---@return vector3 offset The pivot offset
function M.get_pivot_offset(pivot_or_node)
	if type(pivot_or_node) == "number" then
		return const.PIVOTS[pivot_or_node]
	end
	return const.PIVOTS[gui.get_pivot(pivot_or_node)]
end


---Check if device is desktop
---@return boolean
function M.is_desktop()
	return const.CURRENT_SYSTEM_NAME == const.OS.WINDOWS or const.CURRENT_SYSTEM_NAME == const.OS.MAC or const.CURRENT_SYSTEM_NAME == const.OS.LINUX
end



---Check if device is native mobile (Android or iOS)
---@return boolean Is mobile
function M.is_mobile()
	local sys_name = const.CURRENT_SYSTEM_NAME
	return sys_name == const.OS.IOS or sys_name == const.OS.ANDROID
end


---Check if device is HTML5
---@return boolean
function M.is_web()
	return const.CURRENT_SYSTEM_NAME == const.OS.BROWSER
end


---Check if device is HTML5 mobile
---@return boolean
function M.is_web_mobile()
	if html5 then
		return html5.run("(typeof window.orientation !== 'undefined') || (navigator.userAgent.indexOf('IEMobile') !== -1);") == "true"
	end
	return false
end


---Check if device is mobile and can support multitouch
---@return boolean is_multitouch Is multitouch supported
function M.is_multitouch_supported()
	return M.is_mobile() or M.is_web_mobile()
end


---Simple table to one-line string converter
---@param t table
---@return string
function M.table_to_string(t)
	if not t then
		return ""
	end

	local result = "{"

	for key, value in pairs(t) do
		if #result > 1 then
			result = result .. ","
		end
		result = result .. key .. ": " .. value
	end

	return result .. "}"
end


---Distance from node position to his borders
---@param node node GUI node
---@param offset vector3|nil Offset from node position. Pass current node position to get non relative border values
---@return vector4 border Vector4 with border values (left, top, right, down)
function M.get_border(node, offset)
	local pivot = gui.get_pivot(node)
	local pivot_offset = M.get_pivot_offset(pivot)
	local size = M.get_scaled_size(node)
	local border = vmath.vector4(
		-size.x * (0.5 + pivot_offset.x),
		size.y * (0.5 - pivot_offset.y),
		size.x * (0.5 - pivot_offset.x),
		-size.y * (0.5 + pivot_offset.y)
	)

	if offset then
		border.x = border.x + offset.x
		border.y = border.y + offset.y
		border.z = border.z + offset.x
		border.w = border.w + offset.y
	end

	return border
end


local TEXT_METRICS_OPTIONS = {
	line_break = false,
	tracking = 0,
	leading = 0,
	width = 0,
}

---Get text metric from GUI node.
---@param text_node node
---@return GUITextMetrics
function M.get_text_metrics_from_node(text_node)
	local options = TEXT_METRICS_OPTIONS
	options.tracking = gui.get_tracking(text_node)
	options.line_break = gui.get_line_break(text_node)

	options.width = 0
	options.leading = 0

	-- Gather other options only if it used in node
	if options.line_break then
		options.width = gui.get_size(text_node).x
		options.leading = gui.get_leading(text_node)
	end

	local font_resource = gui.get_font_resource(gui.get_font(text_node))
	return resource.get_text_metrics(font_resource, gui.get_text(text_node), options)
end


---Add value to array with shift policy
---Shift policy can be: left, right, no_shift
---@param array table Array
---@param item any Item to insert
---@param index number|nil Index to insert. If nil, item will be inserted at the end of array
---@param shift_policy number|nil The druid_const.SHIFT.* constant
---@return any Inserted item
function M.insert_with_shift(array, item, index, shift_policy)
	shift_policy = shift_policy or const.SHIFT.RIGHT

	local len = #array
	index = index or len + 1

	if array[index] and shift_policy ~= const.SHIFT.NO_SHIFT then
		local check_index = index
		local next_element = array[check_index]
		while next_element or (check_index >= 1 and check_index <= len) do
			check_index = check_index + shift_policy
			local check_element = array[check_index]
			array[check_index] = next_element
			next_element = check_element
		end
	end

	array[index] = item

	return item
end


---Remove value from array with shift policy
-- Shift policy can be: left, right, no_shift
---@param array any[] Array
---@param index number|nil Index to remove. If nil, item will be removed from the end of array
---@param shift_policy number|nil  The druid_const.SHIFT.* constant
---@return any Removed item
function M.remove_with_shift(array, index, shift_policy)
	shift_policy = shift_policy or const.SHIFT.RIGHT

	local len = #array
	index = index or len

	local item = array[index]
	array[index] = nil

	if shift_policy ~= const.SHIFT.NO_SHIFT then
		local check_index = index + shift_policy
		local next_element = array[check_index]
		while next_element or (check_index >= 0 and check_index <= len + 1) do
			array[check_index - shift_policy] = next_element
			array[check_index] = nil
			check_index = check_index + shift_policy
			next_element = array[check_index]
		end
	end

	return item
end


---Get full position of node in the GUI tree
---@param node node GUI node
---@param root node|nil GUI root node to stop search
function M.get_full_position(node, root)
	local position = gui.get_position(node)
	local parent = gui.get_parent(node)
	while parent and parent ~= root do
		local parent_position = gui.get_position(parent)
		position.x = position.x + parent_position.x
		position.y = position.y + parent_position.y
		parent = gui.get_parent(parent)
	end

	return position
end


---@class druid.system.animation_data
---@field frames table<number, table<string, number>> List of frames with uv coordinates and size
---@field width number Width of the animation
---@field height number Height of the animation
---@field fps number Frames per second
---@field current_frame number Current frame
---@field node node Node with flipbook animation
---@field v vector4 Vector with UV coordinates and size


---Source: https://github.com/Dragosha/defold-sprite-repeat/blob/main/node_repeat/node_repeat.lua
---Thanks to Dragosha! ( ・ω・ ) <  Hey friend!
---@param node node
---@param atlas_path string Path to the atlas
---@return druid.system.animation_data
function M.get_animation_data_from_node(node, atlas_path)
	local atlas_data = resource.get_atlas(atlas_path)
	local tex_info = resource.get_texture_info(atlas_data.texture)
	local tex_w = tex_info.width
	local tex_h = tex_info.height

	local animation_data

	local sprite_image_id = gui.get_flipbook(node)
	for _, animation in ipairs(atlas_data.animations) do
		if hash(animation.id) == sprite_image_id then
			animation_data = animation
			break
		end
	end
	assert(animation_data, "Unable to find image " .. sprite_image_id)

	local frames = {}
	for index = animation_data.frame_start, animation_data.frame_end - 1 do
		local uvs = atlas_data.geometries[index].uvs
		assert(#uvs == 8, "Sprite trim mode should be disabled for the images.")

		--   UV texture coordinates
		--   1
		--   ^ V
		--   |
		--   |
		--   |       U
		--   0-------> 1

		-- uvs = {
		-- 0,     0,
		-- 0,     height,
		-- width, height,
		-- width, 0
		-- },
		-- Point indeces (Point number {uv_index_x, uv_index_y})
		-- geometries.indices = {0 (1,2),  1(3,4),  2(5,6),  0(1,2),  2(5,6),  3(7,8)}
		--   1------2
		--   |    / |
		--   | A /  |
		--   |  / B |
		--   | /    |
		--   0------3

		local width = uvs[5] - uvs[1] -- Width of sprite region
		local height = uvs[2] - uvs[4] -- Height of sprite region
		local is_rotated = height < 0 -- In case of rotated sprite

		local x_left = uvs[1]
		local y_bottom = uvs[2]
		local x_right = uvs[5]
		local y_top = uvs[6]

		-- Okay now it's correct for non rotated
		local uv_coord = vmath.vector4(
			x_left / tex_w,
			(tex_h - y_bottom) / tex_h,
			x_right / tex_w,
			(tex_h - y_top) / tex_h
		)

		if is_rotated then
			-- In case the atlas has clockwise rotated sprite.
			--   0---------------1
			--   | \        A    |
			--   |     \         |
			--   |         \     |
			--   | B           \ |
			--   3---------------2
			height = -height

			uv_coord.x, uv_coord.y, uv_coord.z, uv_coord.w = uv_coord.y, uv_coord.z, uv_coord.w, uv_coord.x

			-- Update uv_coord
			--uv_coord = vmath.vector4(
			--	u1 / tex_w,
			--	(tex_h - v2) / tex_h,
			--	u2 / tex_w,
			--	(tex_h - v1) / tex_h
			--)
		end

		local frame = {
			uv_coord = uv_coord,
			w = width,
			h = height,
			uv_rotated = is_rotated and vmath.vector4(0, 1, 0, 0) or vmath.vector4(1, 0, 0, 0)
		}

		table.insert(frames, frame)
	end

	return {
		frames 	= frames,
		width   = animation_data.width,
		height 	= animation_data.height,
		fps     = animation_data.fps,
		v       = vmath.vector4(1, 1, animation_data.width, animation_data.height),
		current_frame = 1,
		node    = node,
	}
end


return M
