Editor/Canvas/ZoomHud.cs
using System;
using Editor;
using Grains.RazorDesigner.Selection;
using Sandbox;

namespace Grains.RazorDesigner.Canvas;

public sealed class ZoomHud : Widget
{
	private const string LogPrefix = "[Grains.RazorDesigner]";

	// Visual tuning. All in widget px.
	private const float Inset           = 12f;
	private const float HudHeight       = 26f;
	private const float CornerRadius    = 5f;
	private const int   FontSizePx      = 12;
	private static readonly Color BgColor      = new( 0.078f, 0.086f, 0.110f, 0.92f ); // ~ rgba(20,22,28,0.92)
	private static readonly Color BorderColor  = new( 0.227f, 0.255f, 0.314f, 1f );    // ~ #3a4150
	private static readonly Color DividerColor = new( 0.165f, 0.180f, 0.220f, 1f );    // ~ #2a2e38
	private static readonly Color TextColor    = new( 0.878f, 0.894f, 0.918f, 1f );    // ~ #e0e3ea
	private static readonly Color PctColor     = new( 0.667f, 0.698f, 0.769f, 1f );    // ~ #aab2c4
	private static readonly Color IconColor    = new( 0.541f, 0.573f, 0.643f, 1f );    // ~ #8a92a4
	private static readonly Color HoverOverlay = new( 1f, 1f, 1f, 0.05f );
	private static readonly Color DisabledFg   = new( 1f, 1f, 1f, 0.25f );

	private readonly CanvasViewportFrame _frame;

	private SelectionController CurrentSelection => _frame.Selection;

	// Six segments, ordered left-to-right.
	private enum Segment { ZoomOut, Percent, ZoomIn, Fit, Reset, Selection }
	private static readonly string[] SegmentLabels = { "−", "100%", "+", "Fit", "1:1", "Sel" };
	private static readonly string[] SegmentTooltips =
	{
		"Zoom out (Ctrl+−)",
		"",
		"Zoom in (Ctrl+=)",
		"Fit content (Ctrl+0)",
		"Reset to 100% (Ctrl+1)",
		"Zoom to selection (Ctrl+Shift+0)",
	};

	private int _hoveredSegment = -1;
	private float[] _segmentWidths;

	private DesignerCanvas _canvas;
	private int _lastGeometryHash = -1;

	public ZoomHud( CanvasViewportFrame parent, DesignerCanvas canvas ) : base( parent )
	{
		_frame = parent ?? throw new ArgumentNullException( nameof( parent ) );
		_canvas = canvas ?? throw new ArgumentNullException( nameof( canvas ) );

		// Editor-side widgets do not draw at all by default; opt in.
		TranslucentBackground = true;
		NoSystemBackground = true;

		WindowFlags = WindowFlags.FramelessWindowHint | WindowFlags.Tool;

		MouseTracking = true;

		// Subscribe to view changes — refresh on every push.
		_frame.ZoomChanged += OnZoomChanged;

		Log.Info( $"{LogPrefix} ZoomHud ctor (top-level frameless window)" );
	}

	public override void OnDestroyed()
	{
		_frame.ZoomChanged -= OnZoomChanged;
		base.OnDestroyed();
	}

	[EditorEvent.Frame]
	private void TrackCanvas()
	{
		if ( !_canvas.IsValid() ) { Visible = false; return; }

		var shouldShow = _canvas.Visible && _frame.CanPanZoom;
		if ( Visible != shouldShow ) { Visible = shouldShow; if ( !shouldShow ) return; }
		if ( !shouldShow ) return;

		EnsureWidthsMeasured();
		var pref = PreferredSize;
		var canvasScreen = _canvas.ScreenPosition;
		var canvasSize = _canvas.Size;

		var hash = HashCode.Combine( canvasScreen, canvasSize, pref );
		if ( hash == _lastGeometryHash ) return;
		_lastGeometryHash = hash;

		Size = pref;
		Position = new Vector2(
			canvasScreen.x + canvasSize.x - pref.x - 12f,
			canvasScreen.y + canvasSize.y - pref.y - 12f );
		Update();
	}

	private void OnZoomChanged()
	{
		Update(); // schedule repaint; the % label re-renders from frame.Zoom.
	}

	public Vector2 PreferredSize
	{
		get
		{
			EnsureWidthsMeasured();
			float total = 0f;
			foreach ( var w in _segmentWidths ) total += w;
			return new Vector2( total, HudHeight );
		}
	}

	private void EnsureWidthsMeasured()
	{
		if ( _segmentWidths is not null ) return;
		_segmentWidths = new float[ SegmentLabels.Length ];
		Paint.SetDefaultFont( FontSizePx );
		for ( var i = 0; i < SegmentLabels.Length; i++ )
		{
			var text = i == (int)Segment.Percent ? "1000%" : SegmentLabels[i];
			var w = Paint.MeasureText( text ).x;
			_segmentWidths[i] = MathF.Max( 28f, w + 18f ); // 9px padding each side
		}
	}

	protected override void OnPaint()
	{
		EnsureWidthsMeasured();

		var rect = LocalRect;

		// Background + border.
		Paint.SetBrush( BgColor );
		Paint.SetPen( BorderColor );
		Paint.DrawRect( rect, CornerRadius );

		// Segments.
		float x = rect.Left;
		for ( var i = 0; i < SegmentLabels.Length; i++ )
		{
			var w = _segmentWidths[i];
			var segRect = new Rect( x, rect.Top, w, rect.Height );

			// Hover background. Don't highlight disabled Sel segment.
			if ( _hoveredSegment == i && IsSegmentEnabled( (Segment)i ) )
			{
				Paint.ClearPen();
				Paint.SetBrush( HoverOverlay );
				Paint.DrawRect( segRect, 0f );
			}

			// Divider (between segments).
			if ( i > 0 )
			{
				Paint.SetPen( DividerColor );
				Paint.DrawLine( new Vector2( x, rect.Top + 4f ), new Vector2( x, rect.Bottom - 4f ) );
			}

			// Label.
			var enabled = IsSegmentEnabled( (Segment)i );
			Paint.SetDefaultFont( FontSizePx );
			Paint.SetPen( ColorFor( (Segment)i, enabled ) );
			var text = (Segment)i == Segment.Percent ? CurrentPercentText() : SegmentLabels[i];
			Paint.DrawText( segRect, text, TextFlag.Center );

			x += w;
		}
	}

	private string CurrentPercentText()
	{
		var pct = (int)MathF.Round( _frame.Zoom * 100f );
		return pct + "%";
	}

	private bool IsSegmentEnabled( Segment seg ) =>
		seg != Segment.Selection || CurrentSelection?.Selected is not null;

	private Color ColorFor( Segment seg, bool enabled )
	{
		if ( !enabled ) return DisabledFg;
		return seg switch
		{
			Segment.ZoomOut or Segment.ZoomIn  => IconColor,
			Segment.Percent                    => PctColor,
			_                                  => TextColor,
		};
	}

	protected override void OnMouseMove( MouseEvent e )
	{
		base.OnMouseMove( e );
		var newHover = SegmentAt( e.LocalPosition );
		if ( newHover != _hoveredSegment )
		{
			_hoveredSegment = newHover;
			Update();
		}
		if ( _hoveredSegment >= 0 )
			ToolTip = SegmentTooltips[_hoveredSegment];
	}

	protected override void OnMouseLeave()
	{
		base.OnMouseLeave();
		if ( _hoveredSegment != -1 )
		{
			_hoveredSegment = -1;
			Update();
		}
	}

	protected override void OnMousePress( MouseEvent e )
	{
		base.OnMousePress( e );
		if ( !e.LeftMouseButton ) return;

		var seg = SegmentAt( e.LocalPosition );
		if ( seg < 0 ) return;
		if ( !IsSegmentEnabled( (Segment)seg ) ) return;

		DispatchSegment( (Segment)seg );
		e.Accepted = true;
	}

	private void DispatchSegment( Segment seg )
	{
		switch ( seg )
		{
			case Segment.ZoomOut:   _frame.ZoomBy( 1f / 1.25f ); break;
			case Segment.ZoomIn:    _frame.ZoomBy( 1.25f ); break;
			case Segment.Percent:   /* passive label — no-op */ break;
			case Segment.Fit:       _frame.ApplyFit(); break;
			case Segment.Reset:     _frame.ResetZoomOnly(); break;
			case Segment.Selection: ZoomToSelection(); break;
		}
	}

	private void ZoomToSelection()
	{
		var record = CurrentSelection?.Selected;
		var live = record?.LivePanel;
		if ( live is null || !live.IsValid )
		{
			Log.Info( $"{LogPrefix} ZoomHud Sel: no valid selection" );
			return;
		}
		_frame.ApplyZoomToRect( live.Box.Rect, /* maxZoom */ 4f, /* padding */ 0.10f );
	}

	private int SegmentAt( Vector2 localPos )
	{
		EnsureWidthsMeasured();
		if ( localPos.y < 0f || localPos.y > HudHeight ) return -1;
		float x = 0f;
		for ( var i = 0; i < _segmentWidths.Length; i++ )
		{
			x += _segmentWidths[i];
			if ( localPos.x < x ) return i;
		}
		return -1;
	}
}