-- lx-bundle:true

--- Render and interact with the shared status bar
-- @module core.status.view

local core = require("core")
local common = require("core.common")
local command = require("core.command")
local config = require("core.config")
local style = require("core.style")
local LogView = require("core.log.view")
local status_messages = require("core.status.messages")
local status_items = require("core.status.items")
local Tooltip = require("core.ui.tooltip")
local View = require("core.ui.view")

local StatusView = View:extend()

StatusView.separator = "    "
StatusView.separator2 = "  |  "

--- Initialize status view state and transient message storage
-- @return nil
function StatusView:new()
	StatusView.super.new(self)
	self.message_timeout = 0
	self.message = {}
	self.pointer = { x = 0, y = 0 }
	self.left_entries = {}
	self.right_entries = {}
	self.status_context = nil
	self.hovered_entry = nil
	self.tooltip_entry = nil
	self.tooltip_show_time = 0
	self.tooltip_anchor = { x = 0, y = 0 }
	self.tooltip = Tooltip({
		placement = "top",
	})
end

--- Return true when one entry exposes a hover callback
-- @param entry table|nil resolved status entry
-- @return boolean true when the entry reacts to hover
local function is_entry_hoverable(entry)
	return entry and type(entry.on_hover) == "function" or false
end

--- Return true when one entry exposes a click action
-- @param entry table|nil resolved status entry
-- @return boolean true when the entry reacts to clicks
local function is_entry_clickable(entry)
	return entry
		and (type(entry.command) == "string" or type(entry.on_click) == "function")
		or false
end

--- Return the configured status tooltip hover delay
-- @return number hover delay in seconds
local function get_tooltip_delay()
	return math.max(0, tonumber(config.status_tooltip_delay) or 0)
end

--- Return the configured tooltip hide grace period
-- @return number hide grace in seconds
local function get_tooltip_hide_grace()
	return math.max(0, tonumber(config.tooltip_hide_grace) or 0)
end

--- Return the vertical gap between the status bar and its tooltip
-- @return number tooltip gap in pixels
local function get_tooltip_spacing()
	return math.max(1, common.round(2 * SCALE))
end

local notify_entry_hover
local refresh_tooltip
local update_hovered_entry
local refresh_entries

function StatusView:show_message(icon, icon_color, text)
	self.message = status_messages.build_message(icon, icon_color, text, self.separator2)
	self.message_timeout = system.get_time() + status_messages.get_timeout_duration(text)
end

-- Show various types of messsages
function StatusView:show_info(text)
	self.message = status_messages.show_info(text, self.separator2)
	self.message_timeout = system.get_time() + status_messages.get_timeout_duration(text)
end

function StatusView:show_success(text)
	self.message = status_messages.show_success(text, self.separator2)
	self.message_timeout = system.get_time() + status_messages.get_timeout_duration(text)
end

function StatusView:show_error(text)
	self.message = status_messages.show_error(text, self.separator2)
	self.message_timeout = system.get_time() + status_messages.get_timeout_duration(text)
end

function StatusView:show_warning(text)
	self.message = status_messages.show_warning(text, self.separator2)
	self.message_timeout = system.get_time() + status_messages.get_timeout_duration(text)
end

function StatusView:show_notice(text)
	self.message = status_messages.show_notice(text, self.separator2)
	self.message_timeout = system.get_time() + status_messages.get_timeout_duration(text)
end

--- Register one live status item on the shared registry
-- @param spec table status item specification
-- @return table registered item
function StatusView:add_item(spec)
	return status_items.register(spec)
end

--- Return one registered status item by id
-- @param id string item id
-- @return table|nil registered item or nil
function StatusView:get_item(id)
	return status_items.get_item(id)
end

--- Return the full registered status item list
-- @return table registered item list
function StatusView:get_items_list()
	return status_items.get_items_list()
end

--- Update one registered status item in place
-- @param id string item id
-- @param patch table field patch
-- @return table|nil updated item or nil
function StatusView:update_item(id, patch)
	return status_items.update(id, patch)
end

--- Set one registered item visible or hidden
-- @param id string item id
-- @param visible boolean visible flag
-- @return table|nil updated item or nil
function StatusView:set_item_visible(id, visible)
	return status_items.set_visible(id, visible)
end

--- Remove one registered item from the shared registry
-- @param id string item id
-- @return table|nil removed item or nil
function StatusView:remove_item(id)
	return status_items.remove(id)
end

--- Keep status bar height and message slide offset updated each frame
-- @return nil
function StatusView:update()
	self.size.y = style.font:get_height() + style.padding.y + 2 -- was: * 2

	if system.get_time() < self.message_timeout then
		self.scroll.to.y = self.size.y
	else
		self.scroll.to.y = 0
	end

	StatusView.super.update(self)
	refresh_entries(self)
end

--- Update hover state for interactive status entries
-- @param x number pointer x position
-- @param y number pointer y position
-- @param dx number pointer delta x
-- @param dy number pointer delta y
-- @return nil
function StatusView:on_mouse_move(x, y, dx, dy)
	self.pointer.x = x
	self.pointer.y = y
	update_hovered_entry(self, x, y)
end

--- Clear status-entry hover state when the pointer leaves the bar
-- @return nil
function StatusView:on_mouse_left()
	notify_entry_hover(self, self.hovered_entry, "leave", self.pointer.x, self.pointer.y)
	self.hovered_entry = nil
	refresh_tooltip(self, nil, self.pointer.x, self.pointer.y)
	self.mouse_cursor = "arrow"
	core.redraw = true
end

--- Draw an interleaved item list starting at one screen position
-- @param self core.status.view
-- @param items table interleaved font, color, and text tokens
-- @param x number start x position
-- @param y number start y position
-- @param draw_fn function width or draw callback
-- @param default_color table|nil default foreground color
-- @return number next x position
local function draw_items(self, items, x, y, draw_fn, default_color)
	local font = style.font
	local color = default_color or style.text

	for _, item in ipairs(items) do
		if type(item) == "userdata" then
			font = item
		elseif type(item) == "table" then
			-- Support structured status items { text = "...", bold = bool }
			if item.text ~= nil then
				x = draw_fn(font, color, item.text, item.bold, x, y, 0, self.size.y)
			else
				color = item
			end
		else
			x = draw_fn(font, color, item, nil, x, y, 0, self.size.y)
		end
	end

	return x
end

--- Measure cumulative width of status item text
-- @param font renderer.font font used to measure
-- @param color table|nil ignored text color placeholder
-- @param text string text to measure
-- @param bold boolean|nil ignored bold placeholder
-- @param x number current x position
-- @return number next x position
local function text_width(font, _, text, _, x)
	return renderer.measure_transient_text(font, text, x)
end

--- Draw status text with optional faux-bold styling
-- @param font renderer.font font used for drawing
-- @param color table text color
-- @param text string text to draw
-- @param bold boolean|nil draw one extra pixel offset copy when true
-- @param x number draw x position
-- @param y number draw y position
-- @param ... any forwarded transient text arguments
-- @return number next x position
local function draw_status_text(font, color, text, bold, x, y, ...)
	-- Faux-bold rendering for selected status text (drawn twice with 1px offset)
	local nx = common.draw_transient_text(font, color, text, nil, x, y, ...)
	if bold then
		common.draw_transient_text(font, color, text, nil, x + 1, y, ...)
	end
	return nx
end

--- Measure one resolved status entry including its text and separator widths
-- @param self core.status.view
-- @param entry table resolved status entry
-- @return nil
local function measure_entry(self, entry)
	entry.width = draw_items(self, entry.tokens, 0, 0, text_width, entry.fg)
	entry.separator_width = draw_items(self, entry.separator_tokens or {}, 0, 0, text_width)
	entry.hidden = false
end

--- Measure every entry in one side list
-- @param self core.status.view
-- @param entries table resolved entry list
-- @return nil
local function measure_entries(self, entries)
	for _, entry in ipairs(entries) do
		measure_entry(self, entry)
	end
end

--- Return the visible width of one fitted side list
-- @param entries table resolved entry list
-- @return number visible side width
local function get_visible_entries_width(entries)
	local width = 0
	local first_visible = true

	for _, entry in ipairs(entries) do
		if entry.hidden ~= true then
			if not first_visible then
				width = width + (entry.separator_width or 0)
			end
			width = width + (entry.width or 0)
			first_visible = false
		end
	end

	return width
end

--- Pick the next lowest-priority visible entry to hide
-- @param left_entries table left-side entries
-- @param right_entries table right-side entries
-- @return table|nil next entry to hide or nil
local function get_next_hidden_entry(left_entries, right_entries)
	local candidate = nil

	local function consider(entry)
		if entry.hidden == true then
			return
		end

		if not candidate then
			candidate = entry
			return
		end

		if entry.priority ~= candidate.priority then
			if entry.priority < candidate.priority then
				candidate = entry
			end
			return
		end

		if entry.order ~= candidate.order then
			if entry.order < candidate.order then
				candidate = entry
			end
			return
		end

		if entry.sequence < candidate.sequence then
			candidate = entry
		end
	end

	for _, entry in ipairs(left_entries) do
		consider(entry)
	end

	for _, entry in ipairs(right_entries) do
		consider(entry)
	end

	return candidate
end

--- Hide low-priority entries until both sides fit the current width
-- @param self core.status.view
-- @param left_entries table left-side entries
-- @param right_entries table right-side entries
-- @return nil
local function fit_entries_to_width(self, left_entries, right_entries)
	local available_width = math.max(0, self.size.x - style.padding.x * 2)

	while (get_visible_entries_width(left_entries) + get_visible_entries_width(right_entries)) > available_width do
		local entry = get_next_hidden_entry(left_entries, right_entries)
		if not entry then
			return
		end

		entry.hidden = true
	end
end

--- Assign final screen rectangles to one fitted side list
-- @param self core.status.view
-- @param entries table resolved entry list
-- @param right_align boolean true when laying out the right side
-- @return nil
local function layout_entries(self, entries, right_align)
	local x, y = self:get_content_offset()
	if right_align then
		x = x + self.size.x - get_visible_entries_width(entries) - style.padding.x
	else
		x = x + style.padding.x
	end

	local first_visible = true
	for _, entry in ipairs(entries) do
		entry.draw_x = nil
		entry.draw_y = nil
		entry.draw_w = 0
		entry.draw_h = 0

		if entry.hidden ~= true then
			if not first_visible then
				x = x + (entry.separator_width or 0)
			end

			entry.draw_x = x
			entry.draw_y = y
			entry.draw_w = entry.width or 0
			entry.draw_h = self.size.y
			x = x + (entry.width or 0)
			first_visible = false
		end
	end
end

--- Notify one entry hover callback and swallow plugin errors
-- @param self core.status.view
-- @param entry table|nil resolved status entry
-- @param phase string hover phase name
-- @param x number pointer x position
-- @param y number pointer y position
-- @return nil
notify_entry_hover = function(self, entry, phase, x, y)
	if not is_entry_hoverable(entry) then
		return
	end

	pcall(entry.on_hover, phase, x, y, entry, self.status_context)
end

--- Return the tooltip content for one resolved entry
-- @param self core.status.view
-- @param entry table|nil resolved status entry
-- @return string|table|nil tooltip text or styled content
local function get_entry_tooltip(self, entry)
	local item = entry and entry.item or nil
	if not item then
		return nil
	end

	if type(item.get_tooltip) == "function" then
		local ok, tooltip = pcall(item.get_tooltip, self.status_context, item)
		if ok then
			return tooltip
		end
	end

	return item.tooltip
end

--- Show or hide the shared tooltip for one hovered entry
-- @param self core.status.view
-- @param entry table|nil resolved status entry
-- @param x number pointer x position
-- @param y number pointer y position
-- @return nil
refresh_tooltip = function(self, entry, x, y)
	local tooltip = get_entry_tooltip(self, entry)
	local tooltip_item = entry and entry.item or nil
	if tooltip == nil or tooltip == "" then
		self.tooltip_entry = nil
		self.tooltip_show_time = 0
		self.tooltip:begin_hide(get_tooltip_hide_grace())
		return
	end

	if self.tooltip_entry ~= tooltip_item then
		self.tooltip_entry = tooltip_item
		self.tooltip_anchor.x = entry and entry.draw_x and entry.draw_w
			and (entry.draw_x + entry.draw_w / 2)
			or x
		self.tooltip_anchor.y = entry and entry.draw_y
			and (entry.draw_y - get_tooltip_spacing())
			or (y - get_tooltip_spacing())

		if self.tooltip.visible then
			self.tooltip:show_at(self.tooltip_anchor.x, self.tooltip_anchor.y, tooltip, {
				hook_x = "center",
				hook_y = "bottom",
			})
			return
		end

		self.tooltip_show_time = system.get_time() + get_tooltip_delay()
		return
	end

	if self.tooltip.visible then
		return
	end

	if system.get_time() < self.tooltip_show_time then
		core.redraw = true
		return
	end

	self.tooltip:show_at(self.tooltip_anchor.x, self.tooltip_anchor.y, tooltip, {
		hook_x = "center",
		hook_y = "bottom",
	})
end

--- Return the entry rectangle hit by one pointer position
-- @param self core.status.view
-- @param x number pointer x position
-- @param y number pointer y position
-- @return table|nil resolved entry or nil
local function get_entry_at(self, x, y)
	if y < self.position.y or y >= self.position.y + self.size.y then
		return nil
	end

	for _, side_entries in ipairs({ self.left_entries or {}, self.right_entries or {} }) do
		for _, entry in ipairs(side_entries) do
			if entry.hidden ~= true then
				local ex = entry.draw_x or 0
				local ey = entry.draw_y or self.position.y
				local ew = entry.draw_w or 0
				local eh = entry.draw_h or self.size.y
				if x >= ex and x < ex + ew and y >= ey and y < ey + eh then
					return entry
				end
			end
		end
	end

	return nil
end

--- Refresh hover state and cursor shape from the current pointer
-- @param self core.status.view
-- @param x number pointer x position
-- @param y number pointer y position
-- @return nil
update_hovered_entry = function(self, x, y)
	local next_entry = nil
	if system.get_time() >= self.message_timeout then
		next_entry = get_entry_at(self, x, y)
	end

	if self.hovered_entry ~= next_entry then
		notify_entry_hover(self, self.hovered_entry, "leave", x, y)
		self.hovered_entry = next_entry
		notify_entry_hover(self, self.hovered_entry, "enter", x, y)
		refresh_tooltip(self, self.hovered_entry, x, y)
		core.redraw = true
	end

	if self.hovered_entry then
		notify_entry_hover(self, self.hovered_entry, "move", x, y)
		refresh_tooltip(self, self.hovered_entry, x, y)
	else
		refresh_tooltip(self, nil, x, y)
	end

	self.mouse_cursor = is_entry_clickable(self.hovered_entry) and "hand" or "arrow"
end

--- Resolve, fit, and cache entries for draw and hit-testing
-- @param self core.status.view
-- @return nil
refresh_entries = function(self)
	local left_entries, right_entries, context = self:get_entries()
	measure_entries(self, left_entries)
	measure_entries(self, right_entries)
	fit_entries_to_width(self, left_entries, right_entries)
	layout_entries(self, left_entries, false)
	layout_entries(self, right_entries, true)
	self.left_entries = left_entries
	self.right_entries = right_entries
	self.status_context = context
	update_hovered_entry(self, self.pointer.x, self.pointer.y)
end

--- Draw one fitted side list at the current status-bar position
-- @param self core.status.view
-- @param entries table resolved entry list
-- @param right_align boolean true when drawing the right side
-- @param yoffset number|nil vertical offset
-- @return nil
local function draw_entry_list(self, entries, right_align, yoffset)
	for _, entry in ipairs(entries) do
		if entry.hidden ~= true then
			local x = entry.draw_x or 0
			-- Keep status entry draw positions aligned with the scrolling layout cache
			local y = entry.draw_y or (self.position.y + (yoffset or 0))

			if entry.bg then
				renderer.draw_rect(x, y, entry.width or 0, self.size.y, entry.bg)
			end

			draw_items(self, entry.tokens, x, y, draw_status_text, entry.fg)
		end
	end
end

-- Draw status items on either left or right side of the status bar
function StatusView:draw_items(items, right_align, yoffset)
	local x, y = self:get_content_offset()
	y = y + (yoffset or 0)
	if right_align then
		local w = draw_items(self, items, 0, 0, text_width)
		x = x + self.size.x - w - style.padding.x
		draw_items(self, items, x, y, draw_status_text)
	else
		x = x + style.padding.x
		draw_items(self, items, x, y, draw_status_text)
	end
end

--- Build left and right resolved status entry collections for the active view
-- @return table left-side entries
-- @return table right-side entries
-- @return table context snapshot
function StatusView:get_entries()
	status_items.ensure_defaults_registered()
	return status_items.resolve({
		separator = self.separator,
		separator2 = self.separator2,
	})
end

--- Focus the last active view and dispatch clicks to the current status target
-- @param button string|integer mouse button name or id
-- @param x number pointer x position
-- @param y number pointer y position
-- @param clicks integer click count
-- @return nil
function StatusView:on_mouse_down(button, x, y, clicks)
	core.set_active_view(core.last_active_view)
	if system.get_time() < self.message_timeout and not core.active_view:is(LogView) then
		command.perform("log:open")
		return
	end

	local entry = get_entry_at(self, x, y)
	if not entry then
		return
	end

	if type(entry.command) == "string" then
		command.perform(entry.command)
	elseif type(entry.on_click) == "function" then
		pcall(entry.on_click, button, x, y, clicks, entry, self.status_context)
	end

	-- A visible dynamic tooltip must reflect state changed by its own click
	if self.tooltip.visible and self.tooltip_entry == entry.item then
		self.tooltip_entry = nil
		refresh_tooltip(self, entry, x, y)
	end
end

-- Render the status bar, message strip, and left/right status items
function StatusView:draw()
	self:draw_background(style.background2)

	if self.message then
		self:draw_items(self.message, false, self.size.y)
	end

	draw_entry_list(self, self.left_entries or {}, false)
	draw_entry_list(self, self.right_entries or {}, true)
	self.tooltip:defer_draw()
end

return StatusView
