// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

use std::sync::Arc;
use crate::types::{ Color, Length, Rect, WidgetId };
use crate::render::Canvas;
use super::Element;

// Theme colors driven by the process-wide palette (see `ltk::theme`).
// Non-colour geometry (radius, font size, focus width, etc.) is static — only
// palette tokens respond to light/dark mode.
mod theme;

#[ cfg( test ) ]
mod tests;

/// Visual style of a text button.
#[ derive( Clone, Default ) ]
pub enum ButtonVariant
{
	/// Filled with the brand color — use for the primary call-to-action.
	#[ default ]
	Primary,
	/// White background with a dark border — use for secondary actions.
	Secondary,
	/// Text-only, no background — use for low-emphasis actions.
	Tertiary,
}

/// Internal content of a button — either a text label or a PNG icon.
pub enum ButtonContent
{
	/// A text label rendered with the theme font.
	Text( String ),
	/// An RGBA image used as the button face (Arc avoids per-frame cloning).
	Icon { rgba: Arc<Vec<u8>>, img_w: u32, img_h: u32 },
}

/// A pressable button widget.
///
/// Create text buttons with [`button()`](crate::button()) and icon buttons with
/// [`icon_button()`](crate::icon_button()). Buttons that step a value
/// (date / time pickers, numeric spinners) can opt into press-and-
/// hold repeat via [`Self::repeating`] — the runtime then re-fires
/// `on_press` while the button is held, at the keyboard's repeat
/// cadence.
pub struct Button<Msg: Clone>
{
	/// The visual content of this button.
	pub( crate ) content:   ButtonContent,
	/// Message emitted when the button is pressed, or `None` if disabled.
	pub( crate ) on_press:  Option<Msg>,
	/// Message emitted when the user holds the button for
	/// [`App::long_press_duration`](crate::app::App::long_press_duration)
	/// without moving past the tolerance, OR when the user right-clicks
	/// with the mouse. `None` leaves the button without a context-menu
	/// equivalent. The fire does NOT by itself put the gesture into
	/// drag mode — that is governed by [`Self::on_drag_start`].
	pub( crate ) on_long_press: Option<Msg>,
	/// Drag-arm message. Fires when the press transitions into a drag:
	/// touch on hold-timer expiry (in addition to `on_long_press`),
	/// mouse on motion past the drag-promotion threshold (without
	/// firing the menu). Independent of `on_long_press` so a button
	/// can open a menu without becoming draggable, or be draggable
	/// without showing a menu.
	pub( crate ) on_drag_start: Option<Msg>,
	/// Visual variant controlling colors and borders.
	pub( crate ) variant:   ButtonVariant,
	/// Width and height in pixels for icon buttons. The `0.0` default
	/// follows the process [`crate::WidgetScaling`] mode (via
	/// [`crate::Canvas::geom_px`]); any positive value pins an explicit size.
	pub( crate ) icon_size: Option<Length>,
	/// Optional label font size for text buttons. `None` uses the theme's
	/// default (`theme::FONT_SIZE`); a [`Length`] scales the label with the
	/// surface (e.g. `Length::vmin( 2.2 ).clamp( 14.0, 22.0 )`).
	pub( crate ) font_size: Option<Length>,
	/// Optional height for text buttons. `None` uses the theme's default
	/// (`theme::HEIGHT`); a [`Length`] scales the button box with the
	/// surface so it does not stay frozen while the rest of a fluid layout
	/// grows. Resolved in physical layout space, like all geometry.
	pub( crate ) height:    Option<Length>,
	/// Optional fixed width for text buttons. `None` sizes the button to its
	/// label plus horizontal padding; a [`Length`] pins the width (e.g. to
	/// make a full-width or surface-proportional button), clamped to the
	/// available width. Resolved in physical layout space.
	pub( crate ) width:     Option<Length>,
	/// Optional stable identifier for focus management.
	pub( crate ) id:        Option<WidgetId>,
	/// Whether this button participates in keyboard focus (Tab). Default: `true`.
	pub( crate ) focusable: bool,
	/// Override the pointer cursor shape on hover. `None` falls back
	/// to the `Pointer` (hand) default for clickable widgets.
	pub( crate ) cursor:    Option<crate::types::CursorShape>,
	/// When `true`, holding the button down auto-fires the
	/// `on_press` message: one immediate fire on press, then an
	/// initial delay (≈ 500 ms — same as the keyboard) followed by
	/// repeats every ~120 ms (≈ 8 Hz, deliberately slower than the
	/// keyboard's 30 Hz so a stepper does not whip past the
	/// target). The runtime cancels the timer on release, on touch
	/// cancel, and on long-press promotion. Default `false` — most
	/// buttons fire on tap only.
	pub( crate ) repeating: bool,
	pub( crate ) tooltip:   Option<String>,
}

impl<Msg: Clone> Button<Msg>
{
	/// Create a text button with the given label.
	pub fn new( label: String ) -> Self
	{
		Self
		{
			content:       ButtonContent::Text( label ),
			on_press:      None,
			on_long_press: None,
			on_drag_start: None,
			variant:       ButtonVariant::Primary,
			icon_size:     None,
			font_size:     None,
			height:        None,
			width:         None,
			id:            None,
			focusable:     true,
			cursor:        None,
			repeating:     false,
			tooltip:       None,
		}
	}

	/// Hint shown after a 600 ms pointer dwell. Pointer-only.
	pub fn tooltip( mut self, text: impl Into<String> ) -> Self
	{
		self.tooltip = Some( text.into() );
		self
	}

	/// Override the pointer cursor shape shown on hover.
	pub fn cursor( mut self, shape: crate::types::CursorShape ) -> Self
	{
		self.cursor = Some( shape );
		self
	}

	/// Create an icon button from a shared RGBA buffer.
	///
	/// `img_w` and `img_h` must match the dimensions of `rgba`.
	pub fn new_icon( rgba: Arc<Vec<u8>>, img_w: u32, img_h: u32 ) -> Self
	{
		Self
		{
			content:       ButtonContent::Icon { rgba, img_w, img_h },
			on_press:      None,
			on_long_press: None,
			on_drag_start: None,
			variant:       ButtonVariant::Tertiary,
			icon_size:     None,
			font_size:     None,
			height:        None,
			width:         None,
			id:            None,
			focusable:     true,
			cursor:        None,
			repeating:     false,
			tooltip:       None,
		}
	}

	/// Set the message emitted when the button is pressed.
	pub fn on_press( mut self, msg: Msg ) -> Self
	{
		self.on_press = Some( msg );
		self
	}

	/// Optionally set the message — `None` leaves the button disabled.
	pub fn on_press_maybe( mut self, msg: Option<Msg> ) -> Self
	{
		self.on_press = msg;
		self
	}

	/// Auto-fire `on_press` while the button is held down. The
	/// runtime fires once on press, then re-fires after the
	/// keyboard's repeat *delay* (≈ 500 ms) and at a fixed ~120 ms
	/// (≈ 8 Hz) interval afterwards — slow enough to release on
	/// the value the user wants, fast enough to ramp. Each tick
	/// re-reads `on_press` from the live widget tree, so a
	/// stepper-style button whose message is `"go to value + 1"`
	/// keeps stepping correctly as the value updates.
	///
	/// Mutually compatible with `on_long_press` only in spirit —
	/// once the long-press message fires the gesture machine
	/// transitions to drag mode and the repeat timer is cancelled
	/// regardless of `repeating`. Default `false`.
	pub fn repeating( mut self, on: bool ) -> Self
	{
		self.repeating = on;
		self
	}

	/// Attach a long-press message. Fires when the press has been held
	/// stationary for [`App::long_press_duration`](crate::app::App::long_press_duration),
	/// or when the user right-clicks with the mouse. By itself this does
	/// NOT put the gesture into drag mode — that is governed by
	/// [`Self::on_drag_start`]. The regular `on_press` is suppressed
	/// only when the press has been promoted to a drag (drag-arm fired).
	pub fn on_long_press( mut self, msg: Msg ) -> Self
	{
		self.on_long_press = Some( msg );
		self
	}

	/// Attach a drag-arm message. Fires when the press transitions into
	/// drag mode — touch on hold-timer expiry (alongside `on_long_press`),
	/// mouse on motion past the drag-promotion threshold (without firing
	/// `on_long_press`). Independent of the menu so a button can be
	/// draggable without showing a menu, or open a menu without becoming
	/// draggable.
	pub fn on_drag_start( mut self, msg: Msg ) -> Self
	{
		self.on_drag_start = Some( msg );
		self
	}

	/// Control whether this button receives keyboard focus (Tab navigation).
	/// Set to `false` for purely decorative or status-indicator buttons.
	pub fn focusable( mut self, yes: bool ) -> Self
	{
		self.focusable = yes;
		self
	}

	/// Set the visual variant.
	pub fn variant( mut self, v: ButtonVariant ) -> Self
	{
		self.variant = v;
		self
	}

	/// Set the display size (width = height) for icon buttons.
	///
	/// A bare `f32` is `Length::px`, which pins the icon at that many canvas
	/// pixels. Pass `Length::widget( n )` to have it follow the active
	/// widget-scaling mode, which is what stock icons — a `list_item`
	/// disclosure arrow, for one — already do, so the two match on a surface
	/// where fluid sizing has grown them.
	pub fn icon_size( mut self, size: impl Into<Length> ) -> Self
	{
		self.icon_size = Some( size.into() );
		self
	}

	/// Set the label font size for text buttons. Accepts logical `f32`
	/// pixels or any [`Length`] (e.g. `Length::vmin( 2.2 ).clamp( 14.0,
	/// 22.0 )` to scale with the surface). No-op for icon buttons.
	pub fn font_size( mut self, size: impl Into<Length> ) -> Self
	{
		self.font_size = Some( size.into() );
		self
	}

	/// Set the button height for text buttons. Accepts logical `f32` pixels
	/// or any [`Length`] (e.g. `Length::vmin( 7.0 ).clamp( 40.0, 64.0 )` to
	/// scale the box with the surface). No-op for icon buttons, which are
	/// sized by [`Self::icon_size`].
	pub fn height( mut self, h: impl Into<Length> ) -> Self
	{
		self.height = Some( h.into() );
		self
	}

	/// Pin the button width for text buttons. Accepts logical `f32` pixels or
	/// any [`Length`] (e.g. `Length::orient( 95.0, 25.0 )` for a
	/// surface-proportional width). Without this the button sizes to its
	/// label. Clamped to the available width. No-op for icon buttons.
	pub fn width( mut self, w: impl Into<Length> ) -> Self
	{
		self.width = Some( w.into() );
		self
	}

	/// Resolve the label font size against the canvas viewport, matching how
	/// [`text`](crate::text) sizes its glyphs. An explicit override bypasses
	/// the mode; the default follows the process [`crate::WidgetScaling`].
	fn label_font_size( &self, canvas: &Canvas ) -> f32
	{
		self.font_size
			.map( |l| canvas.resolve_font( l ) )
			.unwrap_or_else( || canvas.font_px( theme::FONT_SIZE ) )
	}

	/// Resolve the button height against the physical layout viewport (like
	/// all geometry). An explicit override bypasses the mode; the default
	/// follows the process [`crate::WidgetScaling`].
	fn resolved_height( &self, canvas: &Canvas ) -> f32
	{
		self.height
			.map( |l| canvas.resolve_geom( l ) )
			.unwrap_or_else( || canvas.geom_px( theme::HEIGHT ) )
	}

	/// Resolve the icon-button size. Unset follows the widget-scaling mode
	/// through `theme::HEIGHT`; an explicit value resolves like any other
	/// geometry `Length`, so `Length::px` pins a physical size while
	/// `Length::widget` tracks the surface the way stock icons do.
	fn resolved_icon_size( &self, canvas: &Canvas ) -> f32
	{
		match self.icon_size
		{
			Some( l ) => canvas.resolve_geom( l ),
			None      => canvas.geom_px( theme::HEIGHT ),
		}
	}

	/// Assign a stable identifier for focus management.
	pub fn id( mut self, id: WidgetId ) -> Self
	{
		self.id = Some( id );
		self
	}

	/// Bounding box of everything the button can paint at `rect`, across every
	/// interaction state. This is the sum of: icon-button hover/press circle
	/// (radius `rect.min_dim / 2 + 8`), focus ring (grows `FOCUS_W + 1` beyond
	/// that), stroke half-width (`FOCUS_W / 2`), plus ~1 px of antialiasing
	/// bleed. Text buttons only have the focus ring.
	///
	/// The partial-redraw path uses this to know how much canvas area to
	/// invalidate when the button transitions in/out of a state.
	pub fn paint_bounds( &self, rect: crate::types::Rect ) -> crate::types::Rect
	{
		let stroke_bleed = theme::FOCUS_W * 0.5 + 1.0;
		match &self.content
		{
			ButtonContent::Icon { .. } =>
			{
				// The circle grows 8 px beyond the icon rect, the focus ring grows
				// `FOCUS_W + 1` beyond the circle.
				let circle_pad = 8.0_f32;
				let ring_pad   = theme::FOCUS_W + 1.0;
				rect.expand( circle_pad + ring_pad + stroke_bleed )
			}
			ButtonContent::Text( _ ) => match self.variant
			{
				ButtonVariant::Primary | ButtonVariant::Secondary =>
				{
					rect.expand( theme::FOCUS_W + 2.0 + stroke_bleed )
				}
				ButtonVariant::Tertiary => rect.expand( 2.0 + stroke_bleed ),
			},
		}
	}

	/// Return the preferred `(width, height)` given available `max_width`.
	pub fn preferred_size( &self, max_width: f32, canvas: &Canvas ) -> (f32, f32)
	{
		match &self.content
		{
			ButtonContent::Text( label ) =>
			{
				let w = match self.width
				{
					Some( l ) => canvas.resolve_geom( l ).min( max_width ),
					None      =>
					{
						let text_w = canvas.measure_text( label, self.label_font_size( canvas ) );
						( text_w + canvas.geom_px( theme::PAD_H ) * 2.0 ).min( max_width )
					}
				};
				( w, self.resolved_height( canvas ) )
			}
			ButtonContent::Icon { .. } =>
			{
				let s = self.resolved_icon_size( canvas ).min( max_width );
				( s, s )
			}
		}
	}

	/// Draw the button into `canvas` at `rect`.
	///
	/// `focused` draws a keyboard-focus ring; `hovered` and `pressed` apply
	/// pointer/touch state overlays (icon buttons only).
	pub fn draw( &self, canvas: &mut Canvas, rect: Rect, focused: bool, hovered: bool, pressed: bool )
	{
		match &self.content
		{
			ButtonContent::Text( label ) =>
			{
				self.draw_text_button( canvas, rect, focused, label );
			}
			ButtonContent::Icon { rgba, img_w, img_h } =>
			{
				self.draw_icon_button( canvas, rect, focused, hovered, pressed, rgba, *img_w, *img_h );
			}
		}
	}

	fn draw_text_button( &self, canvas: &mut Canvas, rect: Rect, focused: bool, label: &str )
	{
		let is_disabled = self.on_press.is_none();
		let fs          = self.label_font_size( canvas );
		let text_y      = rect.y + (rect.height + fs) / 2.0 - 2.0;
		// `rect` is what the layout granted, which is not always the
		// preferred width: a row that cannot fit its children shrinks them.
		let label       = super::elide( canvas, label, fs, rect.width - canvas.geom_px( theme::PAD_H ) * 2.0 );

		match self.variant
		{
			ButtonVariant::Primary =>
			{
				let bg       = if is_disabled { theme::p_disabled_bg()   } else { theme::p_default_bg()   };
				let text_c   = if is_disabled { theme::p_disabled_text() } else { theme::p_default_text() };
				let border_c = theme::p_default_border();
				canvas.fill_rect( rect, bg, theme::RADIUS );
				if !is_disabled
				{
					canvas.stroke_rect( rect, border_c, theme::P_BORDER_W, theme::RADIUS );
				}
				if focused
				{
					let ring = rect.expand( theme::FOCUS_W + 2.0 );
					canvas.stroke_rect(
						ring,
						theme::focus_color(),
						theme::FOCUS_W,
						theme::RADIUS + theme::FOCUS_W + 2.0,
					);
				}
				let text_w = canvas.measure_text( &label, fs );
				canvas.draw_text(
					&label,
					rect.x + (rect.width - text_w) / 2.0,
					text_y,
					fs,
					text_c,
				);
			}
			ButtonVariant::Secondary =>
			{
				let bg       = if is_disabled { theme::s_disabled_bg()     } else { theme::s_bg()     };
				let text_c   = if is_disabled { theme::p_disabled_text()   } else { theme::t_text()   };
				let border_c = if is_disabled { theme::s_disabled_border() } else { theme::s_border() };
				canvas.fill_rect( rect, bg, theme::RADIUS );
				canvas.stroke_rect( rect, border_c, theme::S_BORDER_W, theme::RADIUS );
				if focused
				{
					let ring = rect.expand( theme::FOCUS_W + 2.0 );
					canvas.stroke_rect(
						ring,
						theme::focus_color(),
						theme::FOCUS_W,
						theme::RADIUS + theme::FOCUS_W + 2.0,
					);
				}
				let text_w = canvas.measure_text( &label, fs );
				canvas.draw_text(
					&label,
					rect.x + (rect.width - text_w) / 2.0,
					text_y,
					fs,
					text_c,
				);
			}
			ButtonVariant::Tertiary =>
			{
				let text_c = if is_disabled { theme::p_disabled_text() } else { theme::t_text() };
				if focused
				{
					let ring = rect.expand( 2.0 );
					canvas.stroke_rect( ring, theme::focus_color(), theme::FOCUS_W, theme::RADIUS );
				}
				let text_w = canvas.measure_text( &label, fs );
				canvas.draw_text(
					&label,
					rect.x + (rect.width - text_w) / 2.0,
					text_y,
					fs,
					text_c,
				);
			}
		}
	}

	fn draw_icon_button(
		&self,
		canvas:  &mut Canvas,
		rect:    Rect,
		focused: bool,
		hovered: bool,
		pressed: bool,
		rgba:    &[u8],
		img_w:   u32,
		img_h:   u32,
	)
	{
		// Semi-transparent circular overlay behind the icon for hover / press feedback
		let circle_pad = 8.0_f32;
		let r          = rect.width.min( rect.height ) / 2.0 + circle_pad;
		let cx         = rect.x + rect.width  / 2.0;
		let cy         = rect.y + rect.height / 2.0;
		let circle     = Rect
		{
			x:      cx - r,
			y:      cy - r,
			width:  r * 2.0,
			height: r * 2.0,
		};
		// Hover / press feedback is the theme's primary text colour
		// at low alpha — works as a "lighten" in light mode (where
		// text_primary tends to be dark and the underlying icon is
		// dark) and as a subtle wash in dark mode without baking in
		// a fixed white.
		let fp = crate::theme::palette().text_primary;
		if pressed
		{
			canvas.fill_rect( circle, Color::rgba( fp.r, fp.g, fp.b, 0.18 ), r );
		} else if hovered {
			canvas.fill_rect( circle, Color::rgba( fp.r, fp.g, fp.b, 0.10 ), r );
		}
		if focused
		{
			let ring = circle.expand( theme::FOCUS_W + 1.0 );
			canvas.stroke_rect( ring, theme::focus_color(), theme::FOCUS_W, r + theme::FOCUS_W + 1.0 );
		}
		canvas.draw_image_data( rgba, img_w, img_h, rect, 1.0 );
	}

	/// Wrap this button in an [`Element`].
	pub fn into_element( self ) -> Element<Msg>
	{
		Element::Button( self )
	}

	/// Re-tag this button's three message slots through `f`. Called by
	/// [`Element::map`] while walking a sub-tree.
	pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> Button<U>
	where
		U: Clone + 'static,
		Msg: 'static,
	{
		Button
		{
			content:       self.content,
			on_press:      self.on_press.map( |m| ( *f )( m ) ),
			on_long_press: self.on_long_press.map( |m| ( *f )( m ) ),
			on_drag_start: self.on_drag_start.map( |m| ( *f )( m ) ),
			variant:       self.variant,
			icon_size:     self.icon_size,
			font_size:     self.font_size,
			height:        self.height,
			width:         self.width,
			id:            self.id,
			focusable:     self.focusable,
			cursor:        self.cursor,
			repeating:     self.repeating,
			tooltip:       self.tooltip,
		}
	}
}
