-- 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.

--- Core FPS overlay drawing and refresh service
-- @module core.ui.fps_overlay

local common = require("core.common")
local config = require("core.config")
local style = require("core.style")
local renderer = require("renderer")

local fps_overlay = {}

-- Keep overlay settings available even when every plugin is disabled or absent
config.fps_overlay = common.merge({
	enabled = true,
	update_interval = 0.25,
	update_rect_when_idle = true,
	xmargin = 16,
	ymargin = 4,
	padding = 4,
	background = true,
	position = "top-right",
}, config.fps_overlay)

local state = {
	last_time = nil,
	next_update = 0,
	accum = 0.0,
	frames = 0,
	text = "-- fps",
	fps = 0.0,
	last_rect = nil,
	started = false,
}

--- Update the sampled FPS text and schedule the next overlay refresh
-- @param now number current frame time
local function update_fps_state(now)
	if state.last_time then
		local elapsed = now - state.last_time
		if elapsed > 0 then
			state.accum = state.accum + elapsed
			state.frames = state.frames + 1
		end
	end
	state.last_time = now

	if now < state.next_update then
		return
	end

	if state.accum > 0 and state.frames > 0 then
		state.fps = state.frames / state.accum
	end
	state.text = string.format("%.1f fps", state.fps)
	state.accum = 0.0
	state.frames = 0
	state.next_update = now + math.max(0.05, tonumber(config.fps_overlay.update_interval) or 0.25)
end

--- Reset FPS timing so the next draw starts from a clean sample window
local function reset_fps_state()
	state.last_time = nil
	state.next_update = 0
	state.accum = 0.0
	state.frames = 0
	state.text = "-- fps"
	state.fps = 0.0
end

--- Return true when both overlay rectangles describe the same bounds
-- @param left table|nil first overlay rectangle
-- @param right table|nil second overlay rectangle
-- @return boolean same bounds
local function rects_equal(left, right)
	if left == right then
		return true
	end

	if not left or not right then
		return false
	end

	return left.x == right.x
		and left.y == right.y
		and left.width == right.width
		and left.height == right.height
end

--- Mark one overlay rectangle dirty in the renderer cache
-- @param rect table|nil overlay rectangle
local function invalidate_overlay_rect(rect)
	if not rect then
		return
	end

	renderer.invalidate_rect(rect.x, rect.y, rect.width, rect.height)
end

--- Measure and place the overlay box for one text value
-- @param text string overlay text
-- @return table layout data
local function get_overlay_layout(text)
	local options = config.fps_overlay
	local offset_x = math.floor((options.xmargin or 8) * SCALE)
	local offset_y = math.floor((options.ymargin or 8) * SCALE)
	local padding = math.floor((options.padding or 4) * SCALE)
	local font = style.font
	local text_width = renderer.measure_transient_text(font, text)
	local text_height = font:get_height()
	local box_width = text_width + padding * 2
	local box_height = text_height + padding
	local renderer_width, renderer_height = renderer.get_size()
	local x = offset_x
	local y = offset_y

	if options.position == "top-right" then
		x = renderer_width - box_width - offset_x
		y = offset_y
	elseif options.position == "bottom-left" then
		x = offset_x
		y = renderer_height - box_height - offset_y
	elseif options.position == "bottom-right" then
		x = renderer_width - box_width - offset_x
		y = renderer_height - box_height - offset_y
	end

	return {
		font = font,
		padding = padding,
		x = x,
		y = y,
		width = box_width,
		height = box_height,
	}
end

--- Draw the FPS overlay after the complete workspace
-- @param now number|nil current frame time
function fps_overlay.draw(now)
	local options = config.fps_overlay
	if not options.enabled then
		invalidate_overlay_rect(state.last_rect)
		state.last_rect = nil
		reset_fps_state()
		return
	end

	update_fps_state(now or system.get_time())

	local layout = get_overlay_layout(state.text)
	local rect = {
		x = layout.x,
		y = layout.y,
		width = layout.width,
		height = layout.height,
	}

	if not rects_equal(state.last_rect, rect) then
		invalidate_overlay_rect(state.last_rect)
		invalidate_overlay_rect(rect)
	end
	state.last_rect = rect

	if options.background then
		renderer.draw_rect(layout.x, layout.y, layout.width, layout.height, style.background2)
	end
	renderer.draw_transient_text(
		layout.font,
		state.text,
		layout.x + layout.padding,
		layout.y + math.floor(layout.padding * 0.5),
		style.text
	)
end

--- Start the idle refresh service once core coroutine state is ready
-- @param core table core runtime
function fps_overlay.start(core)
	if state.started then
		return
	end
	state.started = true

	-- Keep the overlay updating on its own interval when the rest of the UI is idle
	core.add_coroutine(function()
		while true do
			local options = config.fps_overlay
			local interval = math.max(0.05, tonumber(options.update_interval) or 0.25)

			if options.enabled and options.update_rect_when_idle then
				core.redraw = true
			end

			coroutine.yield(interval)
		end
	end, state)
end

--- Toggle the FPS overlay and redraw its previous screen area
-- @param core table core runtime
function fps_overlay.toggle(core)
	config.fps_overlay.enabled = not config.fps_overlay.enabled
	core.log("FPS overlay %s", config.fps_overlay.enabled and "enabled" or "disabled")

	if not config.fps_overlay.enabled then
		invalidate_overlay_rect(state.last_rect)
		state.last_rect = nil
		reset_fps_state()
	end
	core.redraw = true
end

return fps_overlay
