-- lx-bundle:true

-- This file is part of Lx, created by Liqube <www.liqube.com>
-- Copyright Liqube Audio. All rights reserved.
-- Lx Interim License. See LICENSE for details.

--- Draw a lightweight floating tooltip overlay
-- @module core.ui.tooltip

local core = require("core")
local common = require("core.common")
local config = require("core.config")
local style = require("core.style")
local Object = require("core.object")

local Tooltip = Object:extend()
local active_tooltip = nil

--- Resolve one horizontal hook mode into a box origin
-- @param hook_x string horizontal hook name
-- @param anchor_x number anchor x position
-- @param box_width number tooltip box width
-- @return number draw x position
local function resolve_hook_x(hook_x, anchor_x, box_width)
	if hook_x == "left" then
		return anchor_x
	end

	if hook_x == "right" then
		return anchor_x - box_width
	end

	return anchor_x - box_width / 2
end

--- Resolve one vertical hook mode into a box origin
-- @param hook_y string vertical hook name
-- @param anchor_y number anchor y position
-- @param box_height number tooltip box height
-- @return number draw y position
local function resolve_hook_y(hook_y, anchor_y, box_height)
	if hook_y == "top" then
		return anchor_y
	end

	if hook_y == "bottom" then
		return anchor_y - box_height
	end

	return anchor_y - box_height / 2
end

--- Copy one tooltip content value into a flat styled-text array
-- @param text string|table|nil tooltip text or styled-text array
-- @return table normalized styled-text array
local function normalize_items(text)
	if type(text) == "table" then
		local items = {}
		for index, value in ipairs(text) do
			items[index] = value
		end
		return items
	end

	if text == nil or text == "" then
		return {}
	end

	return { tostring(text) }
end

--- Measure width and tallest font of one styled-text array
-- @param items table styled-text array
-- @return number measured width
-- @return number measured height
local function measure_items(items)
	local font = style.font
	local color = style.text
	local width = 0
	local height = font:get_height()

	for _, item in ipairs(items) do
		if type(item) == "userdata" then
			font = item
		elseif type(item) == "table" then
			color = item
		else
			width = renderer.measure_transient_text(font, tostring(item), width)
			height = math.max(height, font:get_height())
		end
	end

	return width, height
end

--- Draw one styled-text array at one position
-- @param items table styled-text array
-- @param x number draw x position
-- @param y number draw y position
-- @return number next x position
local function draw_items(items, x, y)
	local font = style.font
	local color = style.text

	for _, item in ipairs(items) do
		if type(item) == "userdata" then
			font = item
		elseif type(item) == "table" then
			color = item
		else
			x = common.draw_text(font, color, tostring(item), nil, x, y, 0, font:get_height())
		end
	end

	return x
end

--- Initialize one tooltip instance
-- @param options table|nil optional tooltip settings
-- @return nil
function Tooltip:new(options)
	options = options or {}
	self.items = {}
	self.visible = false
	self.hide_deadline = 0
	self.anchor_x = 0
	self.anchor_y = 0
	self.offset_x = tonumber(options.offset_x) or 0
	self.offset_y = tonumber(options.offset_y) or 0
	self.placement = options.placement or "top"
	self.hook_x = options.hook_x or "center"
	self.hook_y = options.hook_y or (self.placement == "top" and "bottom" or "top")
	self.background_color = options.background_color or style.background3
	self.border_color = options.border_color or style.divider
	self.padding_x = math.max(0, tonumber(options.padding_x) or style.padding.x)
	self.padding_y = math.max(0, tonumber(options.padding_y) or style.padding.y)
	self.margin = math.max(0, tonumber(options.margin) or style.padding.x)
end

--- Replace the current tooltip content
-- @param text string|table|nil tooltip text or styled-text array
-- @return nil
function Tooltip:set_text(text)
	self.items = normalize_items(text)
end

--- Update the tooltip anchor position
-- @param x number anchor x position
-- @param y number anchor y position
-- @return nil
function Tooltip:set_anchor(x, y)
	self.anchor_x = tonumber(x) or 0
	self.anchor_y = tonumber(y) or 0
end

--- Update which point on the tooltip box is hooked to the anchor
-- @param hook_x string horizontal hook name
-- @param hook_y string vertical hook name
-- @return nil
function Tooltip:set_hook(hook_x, hook_y)
	if hook_x ~= nil then
		self.hook_x = hook_x
	end

	if hook_y ~= nil then
		self.hook_y = hook_y
	end
end

--- Hide the tooltip immediately
-- @return nil
function Tooltip:hide()
	self.hide_deadline = 0
	self.visible = false

	if active_tooltip == self then
		active_tooltip = nil
	end
end

--- Start hiding the tooltip after an optional grace delay
-- @param delay number|nil grace delay in seconds
-- @return nil
function Tooltip:begin_hide(delay)
	local hide_delay = math.max(0, tonumber(delay) or tonumber(config.tooltip_hide_grace) or 0)
	if hide_delay <= 0 or self.visible ~= true then
		self:hide()
		return
	end

	if self.hide_deadline > 0 then
		return
	end

	self.hide_deadline = system.get_time() + hide_delay
end

--- Update pending hide timers for this tooltip
-- @return nil
function Tooltip:update()
	if self.hide_deadline <= 0 then
		return
	end

	if system.get_time() >= self.hide_deadline then
		self:hide()
	else
		core.redraw = true
	end
end

--- Show the tooltip at one anchor position
-- @param x number anchor x position
-- @param y number anchor y position
-- @param text string|table|nil tooltip text or styled-text array
-- @param options table|nil optional placement overrides
-- @return nil
function Tooltip:show_at(x, y, text, options)
	options = options or {}
	if text ~= nil then
		self:set_text(text)
	end

	if active_tooltip and active_tooltip ~= self then
		active_tooltip:hide()
	end

	self:set_anchor(x, y)
	self.hide_deadline = 0
	self.visible = #self.items > 0

	if self.visible then
		active_tooltip = self
	end

	if options.placement ~= nil then
		self.placement = options.placement
		if options.hook_y == nil then
			-- Placement chooses corresponding box edge unless caller supplies one
			self.hook_y = self.placement == "top" and "bottom" or "top"
		end
	end

	if options.hook_x ~= nil or options.hook_y ~= nil then
		self:set_hook(options.hook_x, options.hook_y)
	end

	if options.offset_x ~= nil then
		self.offset_x = tonumber(options.offset_x) or 0
	end

	if options.offset_y ~= nil then
		self.offset_y = tonumber(options.offset_y) or 0
	end
end

--- Return the tooltip content size including padding
-- @return number box width
-- @return number box height
function Tooltip:get_size()
	local text_width, text_height = measure_items(self.items)

	return text_width + self.padding_x * 2, text_height + self.padding_y * 2
end

--- Resolve the tooltip draw rectangle with viewport clamping
-- @return number draw x position
-- @return number draw y position
-- @return number box width
-- @return number box height
local function get_draw_rect(self)
	local box_width, box_height = self:get_size()
	local viewport_width = core.workspace_view and core.workspace_view.size.x or box_width
	local viewport_height = core.workspace_view and core.workspace_view.size.y or box_height
	local x = resolve_hook_x(self.hook_x, self.anchor_x, box_width) + self.offset_x
	local y = resolve_hook_y(self.hook_y, self.anchor_y, box_height) + self.offset_y

	if y < self.margin and self.hook_y == "bottom" then
		y = resolve_hook_y("top", self.anchor_y, box_height) + self.margin + self.offset_y
	elseif y + box_height > viewport_height - self.margin and self.hook_y == "top" then
		y = resolve_hook_y("bottom", self.anchor_y, box_height) - self.margin + self.offset_y
	end

	x = common.clamp(x, self.margin, math.max(self.margin, viewport_width - box_width - self.margin))
	y = common.clamp(y, self.margin, math.max(self.margin, viewport_height - box_height - self.margin))

	return common.round(x), common.round(y), common.round(box_width), common.round(box_height)
end

--- Draw the tooltip immediately when visible
-- @return nil
function Tooltip:draw()
	if not self.visible or #self.items == 0 then
		return
	end

	local x, y, box_width, box_height = get_draw_rect(self)
	local text_x = x + self.padding_x
	local text_y = y + self.padding_y

	renderer.draw_rect(x, y, box_width, box_height, self.background_color)

	if self.border_color then
		renderer.draw_rect(x, y, box_width, 1, self.border_color)
		renderer.draw_rect(x, y + box_height - 1, box_width, 1, self.border_color)
		renderer.draw_rect(x, y, 1, box_height, self.border_color)
		renderer.draw_rect(x + box_width - 1, y, 1, box_height, self.border_color)
	end

	draw_items(self.items, text_x, text_y)
end

--- Queue tooltip drawing after the normal workspace pass
-- @return nil
function Tooltip:defer_draw()
	self:update()

	if not self.visible or #self.items == 0 or not core.workspace_view then
		return
	end

	core.workspace_view:defer_draw(function(tooltip)
		tooltip:draw()
	end, self)
end

return Tooltip
