-- lx-bundle:true

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

--- Provide the first reusable Lua-owned UI Control
-- @module qui.button

local command = require("core.command")
local qui = require("qui")
local Accelerator = require("qui.accelerator")
local Command = require("qui.command")
local colors = require("qui.colors")
local metrics = require("qui.metrics")
local theme = require("qui.theme")
local Font = require("qui.font")

local Button = {}
local color_flags = setmetatable({}, { __mode = "k" })	-- One reusable flag set follows each live Button table

--- Keep a Button dimension inside a sensible non-negative DIP range
-- @param value number|nil requested value
-- @param fallback number default value
-- @param name string readable option name
-- @return number value checked whole-DIP value
local function dip_value(value, fallback, name)
	value = value == nil and fallback or value
	assert(type(value) == "number" and value >= 0 and value < math.huge, name .. " must be a non-negative number")

	return math.floor(value + 0.5)
end

--- Report whether one control-local point remains inside the latest arranged Button rectangle
-- @param self table Button control
-- @param x number local horizontal position
-- @param y number local vertical position
-- @return boolean inside
local function contains_point(self, x, y)
	return x >= 0 and y >= 0 and x < self.arranged_width and y < self.arranged_height
end

--- Run the Button's custom click handler or named application command
-- @param self table Button control
-- @return boolean handled whether an action was available
local function activate(self)
	if self.enabled == false then
		return false
	end
	if self.command then
		return self.command:run()
	end
	if self.on_click then
		self:on_click()
		return true
	end
	if self.action then
		command.perform(self.action, self.action_context)
		return true
	end

	return false
end

--- Select one central color identifier for every Button instance
-- The central catalog, rather than this handler, owns all absence colors and fallback relationships
-- @param self table Button control
-- @param type string requested visual type
-- @param flags table independent current Button conditions
-- @return string color_identifier
function Button.default_color(self, type, flags)
	if type == "background" then
		if flags.grayed then
			return colors.button_disabled_bg
		elseif flags.down then
			return colors.button_down_bg
		elseif flags.pressing then
			return colors.button_pressing_bg
		end

		return colors.button_bg
	elseif type == "frame" then
		if flags.grayed then
			return colors.button_disabled_frame
		elseif flags.down then
			return colors.button_down_frame
		elseif flags.pressing then
			return colors.button_pressing_frame
		elseif flags.focused then
			return colors.button_focused_frame
		elseif flags.hovering then
			return colors.button_hover_frame
		end

		return colors.button_frame
	elseif type == "inner_frame" then
		if flags.down then
			return colors.button_down_inner_frame
		elseif flags.pressing then
			return colors.button_pressing_inner_frame
		end
	elseif type == "text" then
		if flags.grayed then
			return colors.button_disabled_text
		elseif flags.down then
			return colors.button_down_text
		elseif flags.pressing then
			return colors.button_pressing_text
		elseif flags.hovering then
			return colors.button_hover_text
		end

		return colors.button_text
	end

	return nil
end

--- Create one reusable Lua Button control for use inside 'qui.pane.controls'
-- The returned table remains open for application state, methods, and later customization
-- @param options table caption, Command or local action, appearance, and placement
-- @return table definition unrestricted Lua Control declaration
function Button.new(options)
	assert(type(options) == "table", "Button expects a table")
	assert(options.caption == nil or type(options.caption) == "string", "Button caption must be a string")
	assert(options.action == nil or type(options.action) == "string", "Button action must be a string")
	assert(options.command == nil or Command.is(options.command), "Button command must be a qui.command")
	assert(options.shortcut == nil or type(options.shortcut) == "string", "Button shortcut must be a string")
	assert(options.accelerator == nil, "Button must mark its accelerator with & inside caption")
	assert(options.show_accelerators == nil or type(options.show_accelerators) == "boolean",
		"Button show_accelerators must be a boolean")
	assert(options.dialog_action == nil or
		options.dialog_action == "default" or options.dialog_action == "cancel",
		"Button dialog_action must be default or cancel")
	assert(options.on_click == nil or type(options.on_click) == "function", "Button on_click must be a function")
	assert(options.command == nil or options.on_click == nil and options.action == nil and options.shortcut == nil,
		"Button command cannot be combined with on_click, action, or shortcut")
	assert(options.on_color == nil or type(options.on_color) == "function", "Button on_color must be a function")
	assert(options.default_color == nil, "Button default_color is fixed; use on_color for dynamic changes")

	local control = options
	if control.command then
		control.caption = control.caption or control.command.text
		control.enabled = control.command.enabled
		control.down = control.command.checked
	end
	if control.shortcut ~= nil then
		control.shortcut = qui.shortcut(control.shortcut)
	end
	control.caption, control.accelerator, control.accelerator_column =
		Accelerator.parse(control.caption or "", "Button caption")
	control.show_accelerators = control.show_accelerators ~= false
	control.font = control.font or Font.compact	-- Button captions use the quieter stock size by default
	assert(control.font, "Button requires a font")

	control.padding_x = dip_value(
		control.padding_x, theme.resolve_metric(metrics.button_padding_x), "Button padding_x")
	control.padding_y = dip_value(
		control.padding_y, theme.resolve_metric(metrics.button_padding_y), "Button padding_y")
	control.width = control.width and dip_value(control.width, 0, "Button width") or nil
	control.height = control.height and dip_value(control.height, 0, "Button height") or nil
	control.enabled = control.enabled ~= false
	control.focusable = control.focusable ~= false
	control.accepts_pointer = control.accepts_pointer ~= false
	control.default_color = Button.default_color
	control.down = control.down == true
	control.hovered = false
	control.focused = false
	control.pointer_pressed = false
	control.keyboard_pressed = false
	control.arranged_width = control.width or 0
	control.arranged_height = control.height or 0
	color_flags[control] = {
		grayed = not control.enabled,
		down = control.down,
		pressing = false,
		hovering = false,
		focused = false,
	}

	--- Measure the caption and padding without creating drawing resources
	function control:measure(control_canvas, minimum_width, minimum_height, maximum_width, maximum_height)
		local text_width, text_height = control_canvas:measure_text(self.font, self.caption)
		local natural_width = self.width or text_width + self.padding_x * 2
		local natural_height = self.height or text_height + self.padding_y * 2
		local width = math.max(minimum_width, math.min(natural_width, maximum_width))
		local height = math.max(minimum_height, math.min(natural_height, maximum_height))

		return width, height
	end

	--- Draw the complete Button from its current Lua-owned interaction state
	function control:draw(control_canvas)
		local width, height = control_canvas:size()
		self.arranged_width = width
		self.arranged_height = height
		local visibly_pressed = (self.pointer_pressed and self.hovered) or self.keyboard_pressed
		local flags = assert(color_flags[self], "Button color flags are unavailable")
		flags.grayed = not self.enabled
		flags.down = self.down == true
		flags.pressing = visibly_pressed
		flags.hovering = self.hovered
		flags.focused = self.focused
		local background = self:get_color("background", flags)
		local frame = self:get_color("frame", flags)
		local text_color = self:get_color("text", flags)

		-- Button fill and frame share the exact stepped corners while the surrounding Pane shows through
		control_canvas:fill_frame(0, 0, width, height, background)
		control_canvas:draw_frame(0, 0, width, height, 1, frame)
		if flags.down or flags.pressing then
			-- The inner frame belongs only to a held or permanently down Button
			local inner_frame = self:get_color("inner_frame", flags)
			control_canvas:draw_frame(1, 1, width - 2, height - 2, 1, inner_frame)
		end

		local text_width, text_height = control_canvas:measure_text(self.font, self.caption)
		-- The down frame changes around the caption without moving the caption itself
		local text_x = math.floor((width - text_width) / 2)
		local text_y = math.floor((height - text_height) / 2)
		control_canvas:draw_text(self.font, self.caption, text_x, text_y, text_color)
		if self.show_accelerators then
			Accelerator.draw(
				control_canvas,
				self.font,
				self.caption,
				self.accelerator_column,
				text_x,
				text_y,
				text_height,
				text_color
			)
		end
	end

	--- Handle pointer, focus, Space, and Return behavior without native widget code
	function control:event(event)
		if event.kind == "pointer_enter" then
			self.hovered = true
			return { handled = true, redraw = true }
		elseif event.kind == "pointer_leave" then
			self.hovered = false
			return { handled = true, redraw = true }
		elseif event.kind == "pointer_down" and event.button == "left" then
			self.pointer_pressed = true
			return { handled = true, redraw = true, focus = "acquire", capture = "acquire" }
		elseif event.kind == "pointer_up" and event.button == "left" and self.pointer_pressed then
			self.pointer_pressed = false
			local inside = contains_point(self, event.x, event.y)
			if inside then
				activate(self)
			end

			return { handled = true, redraw = true, capture = "release" }
		elseif event.kind == "focus_gained" then
			self.focused = true
			return { redraw = true }
		elseif event.kind == "focus_lost" then
			self.focused = false
			self.pointer_pressed = false
			self.keyboard_pressed = false
			return { redraw = true, capture = "release" }
		elseif event.kind == "key_down" and (event.key == "space" or event.key == "return") then
			if not event["repeat"] then
				self.keyboard_pressed = true
			end

			return { handled = true, redraw = true }
		elseif event.kind == "key_up" and (event.key == "space" or event.key == "return") then
			local should_activate = self.keyboard_pressed
			self.keyboard_pressed = false
			if should_activate then
				activate(self)
			end

			return { handled = true, redraw = true }
		end

		return nil
	end

	--- Activate the Button through the same public path used by pointer and keyboard input
	function control:activate()
		return activate(self)
	end

	--- Apply live shared Command state without changing the Button's own presentation
	-- @param shared_command table attached Command
	-- @param field string changed field or 'all'
	function control:command_changed(shared_command, field)
		if field == "enabled" or field == "all" then
			self:set_enabled(shared_command.enabled)
		end
		if field == "checked" or field == "all" then
			self.down = shared_command.checked
			self:invalidate()
		end
	end

	local declared = qui.control(control)
	if declared.command then
		Command.bind(declared.command, declared)
	end

	return declared
end

-- Callable module syntax keeps declarations readable as 'Button { ... }'
setmetatable(Button, {
	__call = function(_, options)
		return Button.new(options)
	end,
})

return Button
