Editor/HotCodeEditor/CodeView/SignaturePopup.cs
using Sandbox;
using Editor;
using System;
using System.Collections.Generic;
using System.Linq;

/// <summary>
/// Parameter hints shown above the line while typing a call's arguments:
/// <code>
/// [1 of 2]  void Medic.Heal(Player p, int amount)      &lt;- current parameter highlighted
/// Heals by an amount.                                  &lt;- method docs
/// amount: How much to heal.                            &lt;- current parameter's docs
/// </code>
/// Like <see cref="CompletionPopup"/> it never takes focus; the editor drives it.
/// </summary>
public class SignaturePopup : Widget
{
	const float MaxWidth = 640;
	const float Padding = 6;
	const float SignatureFontSize = 9;
	const float DocFontSize = 8;
	const int MaxDocLines = 3;

	private SignatureHelpResult _result;
	private int _index;
	private int _activeParameter;

	// Laid out in SetResult, drawn in OnPaint
	private record struct Piece( string Text, Vector2 Position, bool Highlight, bool Dim );
	private readonly List<Piece> _signature = new();
	private readonly List<string> _summaryLines = new();
	private readonly List<string> _parameterLines = new();
	private string _parameterName;
	private float _signatureLineHeight;
	private float _docLineHeight;
	private float _signatureHeight;

	public SignaturePopup( Widget parent ) : base( parent )
	{
		FocusMode = FocusMode.None;
		TransparentForMouseEvents = true;
		Visible = false;
	}

	public int Index => _index;
	public int Count => _result?.Signatures.Count ?? 0;

	public void SetResult( SignatureHelpResult result, int index )
	{
		_result = result;
		_index = Math.Clamp( index, 0, result.Signatures.Count - 1 );

		// The argument index applies to whichever overload is showing, as long as it has that many parameters
		var signature = result.Signatures[_index];
		_activeParameter = result.ActiveParameter;
		if ( _activeParameter >= signature.Parameters.Count )
			_activeParameter = signature.Parameters.LastOrDefault()?.Display.StartsWith( "params " ) == true ? signature.Parameters.Count - 1 : -1;

		LayoutContent();
		Update();
	}

	/// <summary>
	/// Step through overloads (Up/Down).
	/// </summary>
	public void Cycle( int delta ) => SetResult( _result, (_index + delta + Count) % Count );

	private void LayoutContent()
	{
		var signature = _result.Signatures[_index];

		// Signature: pieces flow left to right and wrap (indented) when the line is full
		Paint.SetFont( HotCodeSettings.CodeFont, SignatureFontSize );
		_signatureLineHeight = MathF.Ceiling( Paint.MeasureText( "M" ).y + 2 );
		_signature.Clear();

		var pieces = new List<(string Text, bool Highlight, bool Dim)>();
		if ( Count > 1 ) pieces.Add( ($"{_index + 1} of {Count}  ", false, true) );
		pieces.Add( (signature.Prefix, false, false) );
		for ( int i = 0; i < signature.Parameters.Count; i++ )
			pieces.Add( (signature.Parameters[i].Display + (i < signature.Parameters.Count - 1 ? ", " : ""), i == _activeParameter, false) );
		pieces.Add( (signature.Suffix, false, false) );

		var x = Padding;
		var y = Padding;
		var right = Padding;
		var indent = Padding + Paint.MeasureText( "    " ).x;

		foreach ( var (text, highlight, dim) in pieces )
		{
			var width = Paint.MeasureText( text ).x;
			if ( x + width > MaxWidth - Padding && x > indent )
			{
				x = indent;
				y += _signatureLineHeight;
			}

			_signature.Add( new Piece( text, new Vector2( x, y ), highlight, dim ) );
			x += width;
			right = MathF.Max( right, x );
		}

		_signatureHeight = y + _signatureLineHeight;

		// Docs: method summary, then the current parameter's description
		Paint.SetDefaultFont( DocFontSize );
		_docLineHeight = MathF.Ceiling( Paint.MeasureText( "M" ).y + 2 );

		var parameter = _activeParameter >= 0 ? signature.Parameters[_activeParameter] : null;
		_parameterName = string.IsNullOrEmpty( parameter?.Documentation ) ? null : parameter.Name;

		var docWidth = MathF.Max( right, 320 ) - Padding;
		_summaryLines.Clear();
		_summaryLines.AddRange( Wrap( signature.Documentation, docWidth, MaxDocLines ) );
		_parameterLines.Clear();
		if ( _parameterName is not null )
			_parameterLines.AddRange( Wrap( $"{_parameterName}: {parameter.Documentation}", docWidth, MaxDocLines ) );

		var docLines = _summaryLines.Count + _parameterLines.Count;
		var height = _signatureHeight + (docLines > 0 ? 4 + docLines * _docLineHeight : 0) + Padding;
		var widest = MathF.Max( right, docLines > 0 ? docWidth + Padding : 0 );

		Size = new Vector2( MathF.Min( MaxWidth, widest + Padding ), height );
	}

	/// <summary>
	/// Greedy word wrap with the current font; the last line gets "..." if there's more.
	/// </summary>
	private static List<string> Wrap( string text, float width, int maxLines )
	{
		var lines = new List<string>();
		if ( string.IsNullOrWhiteSpace( text ) ) return lines;

		var line = "";
		foreach ( var word in text.Split( ' ', StringSplitOptions.RemoveEmptyEntries ) )
		{
			var candidate = line.Length == 0 ? word : line + " " + word;
			if ( Paint.MeasureText( candidate ).x <= width || line.Length == 0 )
			{
				line = candidate;
				continue;
			}

			lines.Add( line );
			line = word;

			if ( lines.Count == maxLines )
			{
				lines[^1] = Paint.GetElidedText( lines[^1] + " " + line + " ...", width, ElideMode.Right );
				return lines;
			}
		}

		if ( line.Length > 0 ) lines.Add( line );
		return lines;
	}

	protected override void OnPaint()
	{
		if ( _result is null ) return;

		Paint.ClearPen();
		Paint.SetBrush( SyntaxColors.PopupBorder );
		Paint.DrawRect( LocalRect );
		Paint.SetBrush( SyntaxColors.PopupBackground );
		Paint.DrawRect( LocalRect.Shrink( 1 ) );

		foreach ( var piece in _signature )
		{
			Paint.SetFont( HotCodeSettings.CodeFont, SignatureFontSize, piece.Highlight ? 700 : 400 );
			Paint.SetPen( piece.Highlight ? SyntaxColors.Link : piece.Dim ? SyntaxColors.Text.WithAlpha( 0.5f ) : SyntaxColors.Text );
			Paint.DrawText( new Rect( piece.Position, new Vector2( MaxWidth, _signatureLineHeight ) ), piece.Text, TextFlag.LeftCenter | TextFlag.SingleLine );
		}

		var y = _signatureHeight + 4;

		Paint.SetDefaultFont( DocFontSize );
		Paint.SetPen( SyntaxColors.Text.WithAlpha( 0.8f ) );
		foreach ( var line in _summaryLines )
		{
			Paint.DrawText( new Rect( Padding, y, Width - Padding * 2, _docLineHeight ), line, TextFlag.LeftCenter | TextFlag.SingleLine );
			y += _docLineHeight;
		}

		// "amount: How much to heal." with the name picked out
		for ( int i = 0; i < _parameterLines.Count; i++ )
		{
			var rect = new Rect( Padding, y, Width - Padding * 2, _docLineHeight );
			var line = _parameterLines[i];

			if ( i == 0 && line.StartsWith( _parameterName + ":" ) )
			{
				var name = _parameterName + ":";
				Paint.SetDefaultFont( DocFontSize, 700 );
				Paint.SetPen( SyntaxColors.Link );
				Paint.DrawText( rect, name, TextFlag.LeftCenter | TextFlag.SingleLine );
				var nameWidth = Paint.MeasureText( name ).x;

				Paint.SetDefaultFont( DocFontSize );
				Paint.SetPen( SyntaxColors.Text.WithAlpha( 0.8f ) );
				Paint.DrawText( new Rect( rect.Left + nameWidth, rect.Top, rect.Width - nameWidth, rect.Height ), line[name.Length..], TextFlag.LeftCenter | TextFlag.SingleLine );
			}
			else
			{
				Paint.DrawText( rect, line, TextFlag.LeftCenter | TextFlag.SingleLine );
			}

			y += _docLineHeight;
		}
	}
}