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

//! ColorPicker — RGBA sliders + hex input + preview swatch + a
//! continuous hue strip for picking arbitrary colours.
//!
//! Stateless — the application owns the current [`Color`] and updates
//! it from [`ColorPicker::on_change`]. Four sliders cover R / G / B
//! and (when [`ColorPicker::show_alpha`]) A; a hex input lets the user
//! type or paste `#RRGGBB` / `#RRGGBBAA`; a hue slider with a rainbow
//! track lets the user grab any pure hue at full saturation / value
//! in one drag, and the RGB sliders then fine-tune it.
//!
//! ```rust,no_run
//! # use ltk::{ color_picker, Color, ColorPicker };
//! # #[ derive( Clone ) ] enum Msg { AccentChanged( Color ) }
//! # struct App { accent: Color }
//! # impl App { fn _ex( &self ) -> ColorPicker<Msg> {
//! color_picker( self.accent )
//!     .show_alpha( false )
//!     .on_change( Msg::AccentChanged )
//! # }}
//! ```

use std::sync::Arc;

use crate::types::{ Color, Length };
use crate::layout::column::column;
use crate::layout::row::row;
use crate::layout::spacer::spacer;
use crate::theme::{ ColorStop, GradientSpace, LinearGradient, Paint };

use super::Element;

mod theme;
#[ cfg( test ) ]
mod tests;

/// Format a [`Color`] as `#RRGGBB` (or `#RRGGBBAA` when `with_alpha`
/// is true and the colour is not fully opaque). Bytes are clamped to
/// `0..=255`.
pub fn color_to_hex( c: Color, with_alpha: bool ) -> String
{
	let to_byte = | f: f32 | ( f.clamp( 0.0, 1.0 ) * 255.0 ).round() as u8;
	let r = to_byte( c.r );
	let g = to_byte( c.g );
	let b = to_byte( c.b );
	let a = to_byte( c.a );
	if with_alpha && a != 255
	{
		format!( "#{:02X}{:02X}{:02X}{:02X}", r, g, b, a )
	} else {
		format!( "#{:02X}{:02X}{:02X}", r, g, b )
	}
}

/// Parse a hex colour string (`"#RGB"` / `"#RGBA"` / `"#RRGGBB"` /
/// `"#RRGGBBAA"`, with or without the leading `#`, case-insensitive)
/// into a [`Color`]. Returns `None` for malformed input.
pub fn parse_hex( s: &str ) -> Option<Color>
{
	let s = s.trim();
	let s = s.strip_prefix( '#' ).unwrap_or( s );
	let parse_byte = | hi: char, lo: char | -> Option<u8>
	{
		let h = hi.to_digit( 16 )?;
		let l = lo.to_digit( 16 )?;
		Some( ( ( h << 4 ) | l ) as u8 )
	};
	let chars: Vec<char> = s.chars().collect();
	let to_color = | r: u8, g: u8, b: u8, a: u8 | Color::rgba(
		r as f32 / 255.0,
		g as f32 / 255.0,
		b as f32 / 255.0,
		a as f32 / 255.0,
	);
	match chars.len()
	{
		3 =>
		{
			let r = parse_byte( chars[0], chars[0] )?;
			let g = parse_byte( chars[1], chars[1] )?;
			let b = parse_byte( chars[2], chars[2] )?;
			Some( to_color( r, g, b, 255 ) )
		}
		4 =>
		{
			let r = parse_byte( chars[0], chars[0] )?;
			let g = parse_byte( chars[1], chars[1] )?;
			let b = parse_byte( chars[2], chars[2] )?;
			let a = parse_byte( chars[3], chars[3] )?;
			Some( to_color( r, g, b, a ) )
		}
		6 =>
		{
			let r = parse_byte( chars[0], chars[1] )?;
			let g = parse_byte( chars[2], chars[3] )?;
			let b = parse_byte( chars[4], chars[5] )?;
			Some( to_color( r, g, b, 255 ) )
		}
		8 =>
		{
			let r = parse_byte( chars[0], chars[1] )?;
			let g = parse_byte( chars[2], chars[3] )?;
			let b = parse_byte( chars[4], chars[5] )?;
			let a = parse_byte( chars[6], chars[7] )?;
			Some( to_color( r, g, b, a ) )
		}
		_ => None,
	}
}

/// RGBA colour selector with sliders, hex input, preview swatch and
/// a continuous hue strip.
pub struct ColorPicker<Msg: Clone>
{
	pub( crate ) value:        Color,
	pub( crate ) on_change:    Option<Arc<dyn Fn( Color ) -> Msg>>,
	/// When `true` the alpha slider is shown and the hex input
	/// accepts `#RRGGBBAA`. Default: `false` — most "pick a theme
	/// colour" flows are opaque.
	pub( crate ) show_alpha:   bool,
}

impl<Msg: Clone + 'static> ColorPicker<Msg>
{
	pub fn new( value: Color ) -> Self
	{
		Self
		{
			value,
			on_change:  None,
			show_alpha: false,
		}
	}

	pub fn on_change( mut self, f: impl Fn( Color ) -> Msg + 'static ) -> Self
	{
		self.on_change = Some( Arc::new( f ) );
		self
	}

	pub fn show_alpha( mut self, on: bool ) -> Self
	{
		self.show_alpha = on;
		self
	}

	/// Build the `Element` tree representing this color picker.
	pub fn build( self ) -> Element<Msg>
	{
		use super::{ container, text, text_edit };
		use super::slider::slider;

		let value      = self.value;
		let on_chg     = self.on_change.clone();
		let show_alpha = self.show_alpha;

		// Preview swatch.
		let swatch: Element<Msg> = container::<Msg>( spacer() )
			.background( value )
			.border( theme::divider(), 1.0 )
			.radius( 12.0 )
			.padding( 0.0 )
			.into();
		let mut preview_row = row::<Msg>().spacing( Length::widget( theme::SPACING ) ).push(
			container::<Msg>( swatch )
				.padding( 0.0 )
				.radius( 12.0 ),
		);
		// We size the swatch via an explicit container holding the
		// preview rect — the parent row will negotiate the space.
		let _ = theme::SWATCH_SZ;

		// Hex input — the `on_change` parses the typed string and
		// only fires the picker's callback on a successful parse, so
		// in-progress typing does not blank the preview every key.
		let hex_value = color_to_hex( value, show_alpha );
		let mut hex_edit = text_edit::<Msg>( "#RRGGBB", hex_value );
		if let Some( ref cb ) = on_chg
		{
			let cb = cb.clone();
			hex_edit = hex_edit.on_change( move |s|
			{
				match parse_hex( &s )
				{
					Some( c ) => cb( c ),
					None      => cb( value ),  // hold the previous value
				}
			} );
		}
		preview_row = preview_row.push( hex_edit );

		// One slider per channel. Each slider's on_change rebuilds
		// the colour by replacing only its channel — the others
		// snapshot at view-build time, which is correct because
		// every change goes through `on_change` and re-renders.
		let chan_slider = | label: &str, current: f32, build_color: Arc<dyn Fn( f32 ) -> Color> | -> Element<Msg>
		{
			let mut s = slider::<Msg>( current );
			if let Some( ref cb ) = on_chg
			{
				let cb_outer = cb.clone();
				s = s.on_change( move |v|
				{
					let c = build_color( v );
					cb_outer( c )
				} );
			}
			column::<Msg>().spacing( 4.0 )
				.push( text( label ).size( Length::widget( theme::LABEL_FS ) ).color( theme::text_muted() ) )
				.push( s )
				.into()
		};

		let r_build: Arc<dyn Fn( f32 ) -> Color> = Arc::new( move |v| Color::rgba( v, value.g, value.b, value.a ) );
		let g_build: Arc<dyn Fn( f32 ) -> Color> = Arc::new( move |v| Color::rgba( value.r, v, value.b, value.a ) );
		let b_build: Arc<dyn Fn( f32 ) -> Color> = Arc::new( move |v| Color::rgba( value.r, value.g, v, value.a ) );
		let a_build: Arc<dyn Fn( f32 ) -> Color> = Arc::new( move |v| Color::rgba( value.r, value.g, value.b, v ) );

		let mut sliders = column::<Msg>().spacing( Length::widget( theme::SPACING ) )
			.push( chan_slider( "R", value.r, r_build ) )
			.push( chan_slider( "G", value.g, g_build ) )
			.push( chan_slider( "B", value.b, b_build ) );
		if show_alpha
		{
			sliders = sliders.push( chan_slider( "A", value.a, a_build ) );
		}

		// Hue strip: a slider whose track is a multi-stop rainbow
		// gradient. Position 0 maps to red, position 1 to a hue
		// just *short* of the full wheel (see `HUE_RANGE` below)
		// so that dragging to the right edge does not snap the
		// thumb back to the left on the next render. Moving the
		// thumb fires `on_change` with the picked hue at full
		// saturation / value, preserving the current alpha — RGB
		// sliders fine-tune brightness afterwards.
		//
		// `HUE_RANGE = 359.0` instead of 360 closes the
		// round-trip: at position 1.0 we pick hue 359°, the colour
		// is almost-red (one degree off pure red), `rgb_to_hue`
		// returns ~359°, and `position = 359 / 359 = 1.0` lands
		// back where the user was. Mapping to 360° instead would
		// snap to hue 0 (pure red, same as position 0) and the
		// slider thumb would teleport to the left — the wheel
		// closes on itself, but a linear slider cannot represent
		// both endpoints. The 1° colour gap between the two ends
		// is imperceptible.
		//
		// Software backend: `fill_paint_rect` falls back to the
		// gradient's first stop (a flat red), so the slider still
		// works functionally; only the rainbow visual is missing
		// off the GLES path. Acceptable trade-off given the
		// software path is the fallback for compositors without GL.
		const HUE_RANGE: f32 = 359.0;
		let current_hue = rgb_to_hue( value.r, value.g, value.b );
		let hue_alpha   = value.a;
		let mut hue_slider = slider::<Msg>( ( current_hue / HUE_RANGE ).clamp( 0.0, 1.0 ) )
			.track_paint( rainbow_gradient() );
		if let Some( ref cb ) = on_chg
		{
			let cb = cb.clone();
			hue_slider = hue_slider.on_change( move |v|
			{
				let ( r, g, b ) = hue_to_rgb( v.clamp( 0.0, 1.0 ) * HUE_RANGE );
				cb( Color::rgba( r, g, b, hue_alpha ) )
			} );
		}
		let hue_row: Element<Msg> = column::<Msg>().spacing( 4.0 )
			.push( text( "Hue" ).size( Length::widget( theme::LABEL_FS ) ).color( theme::text_muted() ) )
			.push( hue_slider )
			.into();

		let body = column::<Msg>().spacing( Length::widget( theme::SPACING * 2.0 ) )
			.push( preview_row )
			.push( sliders )
			.push( hue_row );

		container::<Msg>( body )
			.background( theme::surface_alt() )
			.padding( Length::widget( theme::PADDING ) )
			.radius( theme::RADIUS )
			.into()
	}
}

/// Build the rainbow [`Paint::Linear`] used as the hue strip's track.
/// Seven stops across the wheel with a final repeat of red so the
/// gradient closes cleanly. CSS angle convention: `90deg` sweeps
/// left-to-right, matching the slider's value axis.
fn rainbow_gradient() -> Paint
{
	let stop = | pos: f32, c: Color | ColorStop { position: pos, color: c };
	Paint::Linear( LinearGradient
	{
		angle_deg: 90.0,
		stops: vec!
		[
			stop( 0.000, Color::rgba( 1.0, 0.0, 0.0, 1.0 ) ),  // red
			stop( 1.0 / 6.0, Color::rgba( 1.0, 1.0, 0.0, 1.0 ) ),  // yellow
			stop( 2.0 / 6.0, Color::rgba( 0.0, 1.0, 0.0, 1.0 ) ),  // green
			stop( 3.0 / 6.0, Color::rgba( 0.0, 1.0, 1.0, 1.0 ) ),  // cyan
			stop( 4.0 / 6.0, Color::rgba( 0.0, 0.0, 1.0, 1.0 ) ),  // blue
			stop( 5.0 / 6.0, Color::rgba( 1.0, 0.0, 1.0, 1.0 ) ),  // magenta
			stop( 1.000, Color::rgba( 1.0, 0.0, 0.0, 1.0 ) ),  // red (closes the wheel)
		],
		space: GradientSpace::Srgb,
	} )
}

/// Compute the HSV hue (in degrees, `0..360`) of an RGB triple. Returns
/// `0.0` for greys (where hue is undefined) so the slider reads at a
/// stable position when the user dials saturation down to zero via the
/// RGB sliders.
pub fn rgb_to_hue( r: f32, g: f32, b: f32 ) -> f32
{
	let max   = r.max( g ).max( b );
	let min   = r.min( g ).min( b );
	let delta = max - min;
	if delta <= f32::EPSILON { return 0.0; }
	let h = if max == r
	{
		60.0 * ( ( ( g - b ) / delta ).rem_euclid( 6.0 ) )
	}
	else if max == g
	{
		60.0 * ( ( b - r ) / delta + 2.0 )
	}
	else
	{
		60.0 * ( ( r - g ) / delta + 4.0 )
	};
	if h < 0.0 { h + 360.0 } else { h }
}

/// Build an RGB triple at full saturation and value from a hue in
/// degrees (`0..360`). Returns each channel in `0.0..=1.0`.
pub fn hue_to_rgb( hue_deg: f32 ) -> ( f32, f32, f32 )
{
	let h = hue_deg.rem_euclid( 360.0 ) / 60.0;
	let c = 1.0_f32;
	let x = c * ( 1.0 - ( ( h.rem_euclid( 2.0 ) ) - 1.0 ).abs() );
	let ( r, g, b ) = match h as u32
	{
		0 => ( c, x, 0.0 ),
		1 => ( x, c, 0.0 ),
		2 => ( 0.0, c, x ),
		3 => ( 0.0, x, c ),
		4 => ( x, 0.0, c ),
		_ => ( c, 0.0, x ),
	};
	( r, g, b )
}

impl<Msg: Clone + 'static> From<ColorPicker<Msg>> for Element<Msg>
{
	fn from( c: ColorPicker<Msg> ) -> Self { c.build() }
}

/// Create a [`ColorPicker`] starting from the given colour.
///
/// ```rust,no_run
/// # use ltk::{ color_picker, Color, ColorPicker };
/// # #[ derive( Clone ) ] enum Msg { AccentChanged( Color ) }
/// # struct App { accent: Color }
/// # impl App { fn _ex( &self ) -> ColorPicker<Msg> {
/// color_picker( self.accent )
///     .show_alpha( true )
///     .on_change( Msg::AccentChanged )
/// # }}
/// ```
pub fn color_picker<Msg: Clone + 'static>( value: Color ) -> ColorPicker<Msg>
{
	ColorPicker::new( value )
}
