Code/RichText.razor
@using Sandbox
@using Sandbox.UI
@using System.Collections.Generic
@using System.Text.RegularExpressions

<root>

</root>

@code
{
	public string Text { get; set; } = "";

	protected override void OnParametersSet ()
	{
		base.OnParametersSet();
		DeleteChildren( true );

		var components = ParseText( Text );

		foreach ( var (panelType, innerText) in components )
		{
			if ( panelType == typeof( Label ) )
			{
				var split = innerText.Split( " " );
				foreach ( var s in split )
				{
					if(string.IsNullOrWhiteSpace(s))
					{
						continue;
					}
					AddChild( new Label( s ) );
				}
			}
			else
			{
				var panel = TypeLibrary.Create<Panel>( panelType );
				AddChild( panel );

				if ( panel is IRichTextPanel richTextPanel )
				{
					richTextPanel.ParseRichText( innerText );
				}
				else
				{
					panel.AddChild( new Label( innerText ) );
				}
			}
		}
	}

	/// <summary>
	/// Parses rich text and returns a list of components to create
	/// </summary>
	/// <param name="text">The text to parse</param>
	/// <returns>List of (ComponentType, InnerText) pairs where ComponentType is either a Panel type or typeof(Label)</returns>
	public static List<(System.Type Type, string Text)> ParseText ( string text )
	{
		var result = new List<(System.Type Type, string Text)>();

		if ( string.IsNullOrEmpty( text ) )
			return result;

		var allPanels = TypeLibrary.GetTypesWithAttribute<RichTextPanelAttribute>();

		// Create a list of all matches with their positions
		var matches = new List<(Match Match, System.Type PanelType, int Priority, string Pattern)>();

		foreach ( var panelType in allPanels )
		{
			var pattern = panelType.Attribute.Pattern;
			if ( string.IsNullOrEmpty( pattern ) )
				continue;

			var regex = new Regex( pattern );
			var regexMatches = regex.Matches( text );

			// Priority based on specificity - longer patterns get higher priority
			// Also give higher priority to patterns with more complex regex features
			var priority = pattern.Length;
			if ( pattern.Contains( "(?<!" ) || pattern.Contains( "(?!" ) )
				priority += 100; // Boost priority for lookahead/lookbehind patterns

			foreach ( Match match in regexMatches )
			{
				matches.Add( (match, panelType.Type.TargetType, priority, pattern) );
			}
		}

		// Sort matches by length (longer matches first for proper nesting),
		// then by priority (higher priority first), then by position
		matches.Sort( ( a, b ) =>
		{
			// First priority: longer matches first (this handles nesting properly)
			var lengthComparison = b.Match.Length.CompareTo( a.Match.Length );
			if ( lengthComparison != 0 )
				return lengthComparison;

			// Second priority: higher pattern priority first
			var priorityComparison = b.Priority.CompareTo( a.Priority );
			if ( priorityComparison != 0 )
				return priorityComparison;

			// Third priority: earlier position first
			return a.Match.Index.CompareTo( b.Match.Index );
		} );

		// Remove overlapping matches (keep higher priority ones)
		var finalMatches = new List<(Match Match, System.Type PanelType)>();
		foreach ( var (match, panelType, priority, pattern) in matches )
		{
			var overlaps = false;
			foreach ( var (existingMatch, _) in finalMatches )
			{
				if ( match.Index < existingMatch.Index + existingMatch.Length &&
					match.Index + match.Length > existingMatch.Index )
				{
					overlaps = true;
					break;
				}
			}

			if ( !overlaps )
			{
				finalMatches.Add( (match, panelType) );
			}
		}

		// Sort final matches by position
		finalMatches.Sort( ( a, b ) => a.Match.Index.CompareTo( b.Match.Index ) );

		// Build the component list by processing text segments and matches
		var lastIndex = 0;
		foreach ( var (match, panelType) in finalMatches )
		{
			// Add any text before this match as Label components
			if ( match.Index > lastIndex )
			{
				var beforeText = text.Substring( lastIndex, match.Index - lastIndex );
				if ( !string.IsNullOrEmpty( beforeText ) )
				{
					result.Add( (typeof( Label ), beforeText) );
				}
			}

			// Add the panel component
			// Use full match by default. Only use first capture group if it's clearly extracting content from wrapper syntax
			var innerText = match.Value;
			if ( match.Groups.Count > 1 && !string.IsNullOrEmpty( match.Groups[1].Value ) )
			{
				var captureGroup = match.Groups[1].Value;
				var fullMatch = match.Value;

				// Only use capture group if the full match starts and ends with wrapper characters
				// that are NOT part of the capture group (indicating wrapper syntax like **text** or <i>text</i>)
				var beforeCapture = fullMatch.Substring( 0, match.Groups[1].Index - match.Index );
				var afterCapture = fullMatch.Substring( match.Groups[1].Index - match.Index + match.Groups[1].Length );

				// Use capture group only if there are clear wrapper characters before AND after
				if ( !string.IsNullOrEmpty( beforeCapture ) && !string.IsNullOrEmpty( afterCapture ) )
				{
					innerText = captureGroup;
				}
			}
			result.Add( (panelType, innerText) );

			lastIndex = match.Index + match.Length;
		}

		// Add any remaining text as a Label component
		if ( lastIndex < text.Length )
		{
			var remainingText = text.Substring( lastIndex );
			if ( !string.IsNullOrEmpty( remainingText ) )
			{
				result.Add( (typeof( Label ), remainingText) );
			}
		}

		return result;
	}
}