Editor/Widgets/SuiColorPickerPopup.cs
using System;
using Editor;
using Sandbox;

namespace SboxUiDesigner.EditorUi.Widgets;

/// <summary>
/// Custom color picker popup — replaces <c>Editor.ColorPicker.OpenColorPopup</c>
/// because the editor's built-in widget has multiple correctness bugs (ISSUE-001
/// to ISSUE-003 in the project ISSUES.md):
///
/// - SV gradient doesn't repaint on hue change
/// - Lag during slider drag
/// - Commit fires intermittently (subsequent picks ignored)
/// - Initial state mis-positioned for the current color
///
/// Architecture:
/// - State lives as <see cref="ColorHsv"/> internally so all subwidgets read/write
///   the same source of truth without lossy round-trips through hex/RGB.
/// - <see cref="ColorChanged"/> fires on every interactive change (live preview).
/// - <see cref="EditingFinished"/> fires when the user commits (closes popup or
///   blurs out of an input).
/// - Any change recomputes the cached SV gradient pixmap (only when hue moves).
/// </summary>
public sealed class SuiColorPickerPopup : Window
{
	private ColorHsv _color;
	private readonly Color _initial;

	private SvSquare _svSquare;
	private HueSlider _hueSlider;
	private AlphaSlider _alphaSlider;
	private LineEdit _hexInput;
	private LineEdit _rEdit, _gEdit, _bEdit, _aEdit;
	private Widget _newSwatch, _oldSwatch;
	private bool _suppressBack;

	public event Action<Color> ColorChanged;
	public event Action EditingFinished;

	public Color Current
	{
		get => (Color)_color;
		set
		{
			_color = (ColorHsv)value;
			RefreshAllFromState();
		}
	}

	public SuiColorPickerPopup( Color start, Action<Color> onLiveChange )
	{
		_initial = start;
		_color = (ColorHsv)start;
		ColorChanged += onLiveChange;

		Title = "Color Picker";
		WindowTitle = "Color Picker";
		Size = new Vector2( 280, 460 );
		MinimumSize = new Vector2( 280, 460 );
		SetWindowIcon( "palette" );
		DeleteOnClose = true;

		Canvas = new Widget( null );
		Canvas.Layout = Layout.Column();
		Canvas.Layout.Margin = 8;
		Canvas.Layout.Spacing = 6;

		BuildLayout();
		RefreshAllFromState();
		Show();
	}

	/// <summary>
	/// Drop-in replacement for <c>Editor.ColorPicker.OpenColorPopup</c>. Returns
	/// the popup so callers can hook <see cref="EditingFinished"/>.
	/// </summary>
	public static SuiColorPickerPopup OpenColorPopup( Color start, Action<Color> onLiveChange )
		=> new( start, onLiveChange );

	private void BuildLayout()
	{
		// SV square (saturation × value).
		_svSquare = new SvSquare( Canvas );
		_svSquare.OnSvChanged = ( s, v ) =>
		{
			_color = new ColorHsv( _color.Hue, s, v, _color.Alpha );
			RefreshAllFromState( exceptSvSquare: true );
			Notify();
		};
		Canvas.Layout.Add( _svSquare );

		// Hue slider. Subwidget API uses 0..1 (intuitive for sliders); we
		// scale to 0..360 at the boundary because ColorHsv.Hue expects degrees.
		_hueSlider = new HueSlider( Canvas );
		_hueSlider.OnHueChanged = h =>
		{
			_color = new ColorHsv( h * 360f, _color.Saturation, _color.Value, _color.Alpha );
			RefreshAllFromState( exceptHueSlider: true );
			Notify();
		};
		Canvas.Layout.Add( _hueSlider );

		// Alpha slider.
		_alphaSlider = new AlphaSlider( Canvas );
		_alphaSlider.OnAlphaChanged = a =>
		{
			_color = new ColorHsv( _color.Hue, _color.Saturation, _color.Value, a );
			RefreshAllFromState( exceptAlphaSlider: true );
			Notify();
		};
		Canvas.Layout.Add( _alphaSlider );

		// Compare swatches: old vs new.
		var swatchRow = Layout.Row();
		swatchRow.Spacing = 6;

		_oldSwatch = new Widget( Canvas );
		_oldSwatch.FixedHeight = 28;
		_oldSwatch.SetStyles( $"background-color: {ColorToCss( _initial )}; border: 1px solid #555; border-radius: 3px;" );
		_oldSwatch.ToolTip = "Original color (click to revert)";
		_oldSwatch.MouseLeftPress += () => { Current = _initial; Notify(); };
		swatchRow.Add( _oldSwatch, 1 );

		_newSwatch = new Widget( Canvas );
		_newSwatch.FixedHeight = 28;
		_newSwatch.SetStyles( "border: 1px solid #555; border-radius: 3px;" );
		swatchRow.Add( _newSwatch, 1 );

		Canvas.Layout.Add( swatchRow );

		// Hex input.
		var hexRow = Layout.Row();
		hexRow.Spacing = 4;
		var hexLabel = new Label( "Hex", Canvas );
		hexLabel.FixedWidth = 32;
		hexRow.Add( hexLabel );
		_hexInput = new LineEdit( Canvas );
		_hexInput.PlaceholderText = "#rrggbb or #rrggbbaa";
		_hexInput.EditingFinished += () =>
		{
			if ( _suppressBack ) return;
			if ( Color.TryParse( _hexInput.Text, out var parsed ) )
			{
				_color = (ColorHsv)parsed;
				RefreshAllFromState( exceptHex: true );
				Notify();
			}
		};
		hexRow.Add( _hexInput, 1 );
		Canvas.Layout.Add( hexRow );

		// RGB inputs (255 scale).
		var rgbRow = Layout.Row();
		rgbRow.Spacing = 4;
		_rEdit = AddInt255( rgbRow, "R", v => SetRgb( v, null, null, null ) );
		_gEdit = AddInt255( rgbRow, "G", v => SetRgb( null, v, null, null ) );
		_bEdit = AddInt255( rgbRow, "B", v => SetRgb( null, null, v, null ) );
		_aEdit = AddInt255( rgbRow, "A", v => SetRgb( null, null, null, v ) );
		Canvas.Layout.Add( rgbRow );

		Canvas.Layout.AddStretchCell();

		// Footer buttons.
		var footer = Layout.Row();
		footer.Spacing = 6;
		var clear = new Button( "Clear", "delete", Canvas );
		clear.ToolTip = "Remove the color (sets to empty/unset)";
		clear.Clicked += () =>
		{
			Cleared?.Invoke();
			EditingFinished?.Invoke();
			Close();
		};
		footer.Add( clear );
		footer.AddStretchCell();
		var cancel = new Button( "Cancel", "close", Canvas );
		cancel.Clicked += () => { Current = _initial; Notify(); EditingFinished?.Invoke(); Close(); };
		footer.Add( cancel );
		var ok = new Button( "OK", "check", Canvas );
		ok.Clicked += () => { EditingFinished?.Invoke(); Close(); };
		footer.Add( ok );
		Canvas.Layout.Add( footer );
	}

	/// <summary>Fires when the user clicks the "Clear" button to remove the color entirely.</summary>
	public event Action Cleared;

	private LineEdit AddInt255( Layout row, string label, Action<int> onCommit )
	{
		var lbl = new Label( label, Canvas );
		lbl.FixedWidth = 14;
		lbl.SetStyles( "color: #9ca3af;" );
		row.Add( lbl );
		var le = new LineEdit( Canvas );
		le.FixedWidth = 48;
		le.PlaceholderText = "0-255";
		le.EditingFinished += () =>
		{
			if ( _suppressBack ) return;
			if ( int.TryParse( le.Text, out var v ) )
				onCommit( Math.Clamp( v, 0, 255 ) );
		};
		row.Add( le, 1 );
		return le;
	}

	private void SetRgb( int? r, int? g, int? b, int? a )
	{
		var c = (Color)_color;
		var nr = r.HasValue ? r.Value / 255f : c.r;
		var ng = g.HasValue ? g.Value / 255f : c.g;
		var nb = b.HasValue ? b.Value / 255f : c.b;
		var na = a.HasValue ? a.Value / 255f : c.a;
		_color = (ColorHsv)new Color( nr, ng, nb, na );
		RefreshAllFromState();
		Notify();
	}

	private void RefreshAllFromState(
		bool exceptSvSquare = false,
		bool exceptHueSlider = false,
		bool exceptAlphaSlider = false,
		bool exceptHex = false )
	{
		_suppressBack = true;
		try
		{
			// Subwidgets work in 0..1 hue range; ColorHsv.Hue is 0..360.
			var hue01 = _color.Hue / 360f;

			if ( !exceptSvSquare ) _svSquare.SetState( hue01, _color.Saturation, _color.Value );
			else _svSquare.SetHueOnly( hue01 ); // still refresh gradient
			if ( !exceptHueSlider ) _hueSlider.SetHue( hue01 );
			if ( !exceptAlphaSlider ) _alphaSlider.SetState( (Color)_color, _color.Alpha );
			else _alphaSlider.SetBaseColor( (Color)_color );

			if ( !exceptHex ) _hexInput.Text = ColorToHex( (Color)_color );

			var c = (Color)_color;
			_rEdit.Text = ((int)Math.Round( c.r * 255 )).ToString();
			_gEdit.Text = ((int)Math.Round( c.g * 255 )).ToString();
			_bEdit.Text = ((int)Math.Round( c.b * 255 )).ToString();
			_aEdit.Text = ((int)Math.Round( c.a * 255 )).ToString();

			_newSwatch.SetStyles( $"background-color: {ColorToCss( (Color)_color )}; border: 1px solid #555; border-radius: 3px;" );
		}
		finally
		{
			_suppressBack = false;
		}
	}

	private void Notify()
	{
		ColorChanged?.Invoke( (Color)_color );
	}

	private static string ColorToHex( Color c )
	{
		var r = (int)Math.Clamp( c.r * 255, 0, 255 );
		var g = (int)Math.Clamp( c.g * 255, 0, 255 );
		var b = (int)Math.Clamp( c.b * 255, 0, 255 );
		var a = (int)Math.Clamp( c.a * 255, 0, 255 );
		return a < 255
			? $"#{r:x2}{g:x2}{b:x2}{a:x2}"
			: $"#{r:x2}{g:x2}{b:x2}";
	}

	private static string ColorToCss( Color c )
	{
		var r = (int)Math.Clamp( c.r * 255, 0, 255 );
		var g = (int)Math.Clamp( c.g * 255, 0, 255 );
		var b = (int)Math.Clamp( c.b * 255, 0, 255 );
		return $"rgba({r},{g},{b},{c.a:0.###})";
	}

	// ─────────────────────────────────────────────────────────────────────
	//  Subwidget: SV square (saturation × value, hue-driven gradient)
	// ─────────────────────────────────────────────────────────────────────

	private sealed class SvSquare : Widget
	{
		private float _hue, _sat, _val;
		private Pixmap _gradientCache;
		private float _cachedHue = -1;
		public Action<float, float> OnSvChanged;

		public SvSquare( Widget parent ) : base( parent )
		{
			FixedSize = new Vector2( 256, 200 );
			Cursor = CursorShape.Cross;
			MouseTracking = true;
		}

		public void SetState( float h, float s, float v )
		{
			_hue = h; _sat = s; _val = v;
			Update();
		}

		public void SetHueOnly( float h )
		{
			_hue = h;
			Update();
		}

		protected override void OnPaint()
		{
			var rect = LocalRect;

			// Cache the gradient pixmap per-hue. 64×64 is enough — we'll let the GPU
			// upscale via Paint.Draw bilinear; the SV gradient is smooth so artifacts
			// are imperceptible at picker resolution.
			if ( _gradientCache == null || MathF.Abs( _cachedHue - _hue ) > 0.001f )
			{
				_gradientCache = BuildSvPixmap( _hue, 64, 64 );
				_cachedHue = _hue;
			}
			Paint.Draw( rect, _gradientCache );

			// Crosshair at (s, 1-v).
			var px = rect.Left + _sat * rect.Width;
			var py = rect.Top + (1 - _val) * rect.Height;
			Paint.SetPen( Color.Black.WithAlpha( 0.7f ), 2 );
			Paint.ClearBrush();
			Paint.DrawCircle( new Vector2( px, py ), new Vector2( 8, 8 ) );
			Paint.SetPen( Color.White.WithAlpha( 0.95f ), 1 );
			Paint.DrawCircle( new Vector2( px, py ), new Vector2( 8, 8 ) );
		}

		private static Pixmap BuildSvPixmap( float hue01, int w, int h )
		{
			var pix = new Pixmap( w, h );
			var hueDeg = hue01 * 360f; // ColorHsv.Hue is 0..360 (degrees), not 0..1
			using ( Paint.ToPixmap( pix ) )
			{
				Paint.ClearPen();
				for ( int y = 0; y < h; y++ )
				{
					var v = 1f - y / (float)(h - 1);
					for ( int x = 0; x < w; x++ )
					{
						var s = x / (float)(w - 1);
						var c = (Color)new ColorHsv( hueDeg, s, v, 1f );
						Paint.SetBrush( c );
						Paint.DrawRect( new Rect( x, y, 1, 1 ) );
					}
				}
			}
			return pix;
		}

		protected override void OnMousePress( MouseEvent e )
		{
			if ( e.LeftMouseButton ) UpdateFromMouse( e.LocalPosition );
		}

		protected override void OnMouseMove( MouseEvent e )
		{
			if ( (e.ButtonState & MouseButtons.Left) != 0 ) UpdateFromMouse( e.LocalPosition );
		}

		private void UpdateFromMouse( Vector2 p )
		{
			var s = Math.Clamp( p.x / Width, 0f, 1f );
			var v = 1f - Math.Clamp( p.y / Height, 0f, 1f );
			_sat = s; _val = v;
			OnSvChanged?.Invoke( s, v );
			Update();
		}
	}

	// ─────────────────────────────────────────────────────────────────────
	//  Subwidget: Hue slider (horizontal rainbow bar)
	// ─────────────────────────────────────────────────────────────────────

	private sealed class HueSlider : Widget
	{
		private float _hue;
		// Per-instance cache (not static) — static fields don't survive hotload
		// cleanly and the hotload pipeline reports "Unable to find matching
		// substitution for a static method" when refs go stale.
		private Pixmap _cache;
		public Action<float> OnHueChanged;

		public HueSlider( Widget parent ) : base( parent )
		{
			FixedHeight = 18;
			MinimumWidth = 256;
			Cursor = CursorShape.SplitH;
			MouseTracking = true;
		}

		public void SetHue( float h )
		{
			_hue = h;
			Update();
		}

		protected override void OnPaint()
		{
			var rect = LocalRect;
			if ( _cache == null ) _cache = BuildHuePixmap( 360, 1 );
			Paint.Draw( rect, _cache );

			var x = rect.Left + _hue * rect.Width;
			Paint.SetPen( Color.White, 2 );
			Paint.ClearBrush();
			Paint.DrawLine( new Vector2( x, rect.Top - 1 ), new Vector2( x, rect.Bottom + 1 ) );
			Paint.SetPen( Color.Black, 1 );
			Paint.DrawLine( new Vector2( x - 1, rect.Top - 1 ), new Vector2( x - 1, rect.Bottom + 1 ) );
			Paint.DrawLine( new Vector2( x + 1, rect.Top - 1 ), new Vector2( x + 1, rect.Bottom + 1 ) );
		}

		private static Pixmap BuildHuePixmap( int w, int h )
		{
			var pix = new Pixmap( w, h );
			using ( Paint.ToPixmap( pix ) )
			{
				Paint.ClearPen();
				for ( int x = 0; x < w; x++ )
				{
					// ColorHsv.Hue is 0..360 (degrees) — span the rainbow that way.
					var hueDeg = (x / (float)(w - 1)) * 360f;
					var c = (Color)new ColorHsv( hueDeg, 1f, 1f, 1f );
					Paint.SetBrush( c );
					Paint.DrawRect( new Rect( x, 0, 1, h ) );
				}
			}
			return pix;
		}

		protected override void OnMousePress( MouseEvent e ) { if ( e.LeftMouseButton ) UpdateFromMouse( e.LocalPosition ); }
		protected override void OnMouseMove( MouseEvent e ) { if ( (e.ButtonState & MouseButtons.Left) != 0 ) UpdateFromMouse( e.LocalPosition ); }

		private void UpdateFromMouse( Vector2 p )
		{
			_hue = Math.Clamp( p.x / Width, 0f, 1f );
			OnHueChanged?.Invoke( _hue );
			Update();
		}
	}

	// ─────────────────────────────────────────────────────────────────────
	//  Subwidget: Alpha slider (transparent → currentColor)
	// ─────────────────────────────────────────────────────────────────────

	private sealed class AlphaSlider : Widget
	{
		private float _alpha;
		private Color _baseColor = Color.White;
		public Action<float> OnAlphaChanged;

		public AlphaSlider( Widget parent ) : base( parent )
		{
			FixedHeight = 18;
			MinimumWidth = 256;
			Cursor = CursorShape.SplitH;
			MouseTracking = true;
		}

		public void SetState( Color baseColor, float alpha )
		{
			_baseColor = baseColor;
			_alpha = alpha;
			Update();
		}

		public void SetBaseColor( Color baseColor )
		{
			_baseColor = baseColor;
			Update();
		}

		protected override void OnPaint()
		{
			var rect = LocalRect;

			// Checkerboard bg.
			Paint.ClearPen();
			const float cell = 8;
			var cols = (int)MathF.Ceiling( rect.Width / cell );
			var rows = (int)MathF.Ceiling( rect.Height / cell );
			Paint.SetBrush( new Color( 0.85f, 0.85f, 0.85f ) );
			Paint.DrawRect( rect );
			Paint.SetBrush( new Color( 0.5f, 0.5f, 0.5f ) );
			for ( int y = 0; y < rows; y++ )
				for ( int x = 0; x < cols; x++ )
					if ( ((x + y) & 1) != 0 )
						Paint.DrawRect( new Rect( rect.Left + x * cell, rect.Top + y * cell, cell, cell ) );

			// Manual horizontal gradient by drawing N narrow rects with alpha ramp.
			const int steps = 64;
			var stepW = rect.Width / steps;
			for ( int i = 0; i < steps; i++ )
			{
				var t = i / (float)(steps - 1);
				var c = _baseColor.WithAlpha( t );
				Paint.SetBrush( c );
				Paint.DrawRect( new Rect( rect.Left + i * stepW, rect.Top, stepW + 1, rect.Height ) );
			}

			// Position knob.
			var x2 = rect.Left + _alpha * rect.Width;
			Paint.SetPen( Color.White, 2 );
			Paint.ClearBrush();
			Paint.DrawLine( new Vector2( x2, rect.Top - 1 ), new Vector2( x2, rect.Bottom + 1 ) );
			Paint.SetPen( Color.Black, 1 );
			Paint.DrawLine( new Vector2( x2 - 1, rect.Top - 1 ), new Vector2( x2 - 1, rect.Bottom + 1 ) );
			Paint.DrawLine( new Vector2( x2 + 1, rect.Top - 1 ), new Vector2( x2 + 1, rect.Bottom + 1 ) );
		}

		protected override void OnMousePress( MouseEvent e ) { if ( e.LeftMouseButton ) UpdateFromMouse( e.LocalPosition ); }
		protected override void OnMouseMove( MouseEvent e ) { if ( (e.ButtonState & MouseButtons.Left) != 0 ) UpdateFromMouse( e.LocalPosition ); }

		private void UpdateFromMouse( Vector2 p )
		{
			_alpha = Math.Clamp( p.x / Width, 0f, 1f );
			OnAlphaChanged?.Invoke( _alpha );
			Update();
		}
	}
}