// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Intermediate;

namespace Microsoft.AspNetCore.Razor.Language.Components
{
	internal class ComponentWhitespacePass : ComponentIntermediateNodePassBase, IRazorDirectiveClassifierPass
	{
		protected override void ExecuteCore( RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode )
		{
			if ( codeDocument == null )
			{
				throw new ArgumentNullException( nameof( codeDocument ) );
			}

			if ( documentNode == null )
			{
				throw new ArgumentNullException( nameof( documentNode ) );
			}

			if ( !IsComponentDocument( documentNode ) )
			{
				return;
			}

			var razorLanguageVersion = codeDocument.GetParserOptions().Version;
			var useLegacyBehavior = razorLanguageVersion.CompareTo( RazorLanguageVersion.Version_5_0 ) < 0;
			if ( useLegacyBehavior )
			{
				// Prior to 5.0, the whitespace pass only applied to the BuildRenderTree method, and
				// only removed the top-level leading and trailing whitespace

				var method = documentNode.FindPrimaryMethod();
				if ( method != null )
				{
					RemoveContiguousWhitespace( method.Children, TraversalDirection.Forwards );
					RemoveContiguousWhitespace( method.Children, TraversalDirection.Backwards );
				}
			}
			else
			{
				// From 5.0 onwards, the whitespace pass applies as broadly as possible. It removes leading
				// and trailing whitespace from all methods, elements, and child component blocks. There's
				// also a directive that can disable it.

				// Respect @preservewhitespace directives
				if ( PreserveWhitespaceIsEnabled( documentNode ) )
				{
					return;
				}

				var @class = documentNode.FindPrimaryClass();
				if ( @class != null )
				{
					var visitor = new Visitor();
					visitor.Visit( @class );
				}
			}
		}

		private static bool PreserveWhitespaceIsEnabled( DocumentIntermediateNode documentNode )
		{
			// If there's no @preservewhitespace attribute, the default is that we *don't* preserve whitespace
			var shouldPreserveWhitespace = false;

			foreach ( var preserveWhitespaceDirective in documentNode.FindDirectiveReferences( ComponentPreserveWhitespaceDirective.Directive ) )
			{
				var token = ((DirectiveIntermediateNode)preserveWhitespaceDirective.Node).Tokens.FirstOrDefault();
				var shouldPreserveWhitespaceContent = token?.Content;
				if ( shouldPreserveWhitespaceContent != null )
				{
					shouldPreserveWhitespace = string.Equals( shouldPreserveWhitespaceContent, "true", StringComparison.Ordinal );
				}
			}

			return shouldPreserveWhitespace;
		}

		private static int RemoveContiguousWhitespace( IntermediateNodeCollection nodes, TraversalDirection direction, int? startIndex = null )
		{
			var position = startIndex.GetValueOrDefault( direction == TraversalDirection.Forwards ? 0 : nodes.Count - 1 );
			var countRemoved = 0;
			while ( position >= 0 && position < nodes.Count )
			{
				var node = nodes[position];
				bool shouldRemoveNode;
				bool shouldContinueIteration;

				switch ( node )
				{
					case IntermediateToken intermediateToken:
						shouldRemoveNode = string.IsNullOrWhiteSpace( intermediateToken.Content );
						shouldContinueIteration = shouldRemoveNode;
						break;

					case HtmlContentIntermediateNode htmlContentIntermediateNode:
						RemoveContiguousWhitespace( htmlContentIntermediateNode.Children, direction );
						shouldRemoveNode = htmlContentIntermediateNode.Children.Count == 0;
						shouldContinueIteration = shouldRemoveNode;
						break;

					case MarkupElementIntermediateNode _:
					case CSharpExpressionIntermediateNode _:
					case TagHelperIntermediateNode _:
						// These node types may produce non-whitespace output at runtime
						shouldRemoveNode = false;
						shouldContinueIteration = false;
						break;

					case CSharpCodeIntermediateNode _:
						shouldRemoveNode = false;
						shouldContinueIteration = false;
						break;

					default:
						shouldRemoveNode = false;
						shouldContinueIteration = true; // Because other types of nodes don't produce output
						break;
				}

				if ( shouldRemoveNode )
				{
					nodes.RemoveAt( position );
					countRemoved++;
					if ( direction == TraversalDirection.Forwards )
					{
						position--;
					}
				}

				position += direction == TraversalDirection.Forwards ? 1 : -1;

				if ( !shouldContinueIteration )
				{
					break;
				}
			}

			return countRemoved;
		}

		enum TraversalDirection
		{
			Forwards,
			Backwards
		}

		class Visitor : IntermediateNodeWalker
		{
			public override void VisitMethodDeclaration( MethodDeclarationIntermediateNode node )
			{
				RemoveContiguousWhitespace( node.Children, TraversalDirection.Forwards );
				RemoveContiguousWhitespace( node.Children, TraversalDirection.Backwards );
				VisitDefault( node );
			}

			public override void VisitMarkupElement( MarkupElementIntermediateNode node )
			{
				RemoveContiguousWhitespace( node.Children, TraversalDirection.Forwards );
				RemoveContiguousWhitespace( node.Children, TraversalDirection.Backwards );
				VisitDefault( node );
			}

			public override void VisitTagHelperBody( TagHelperBodyIntermediateNode node )
			{
				// The goal here is to remove leading/trailing whitespace inside component child content. However,
				// at the time this whitespace pass runs, ComponentChildContent is still TagHelperBody in the tree.
				RemoveContiguousWhitespace( node.Children, TraversalDirection.Forwards );
				RemoveContiguousWhitespace( node.Children, TraversalDirection.Backwards );
				VisitDefault( node );
			}

			public override void VisitDefault( IntermediateNode node )
			{
				// For any CSharpCodeIntermediateNode children, remove their preceding and trailing whitespace
				for ( var childIndex = 0; childIndex < node.Children.Count; childIndex++ )
				{
					if ( node.Children[childIndex] is CSharpCodeIntermediateNode )
					{
						childIndex -= RemoveContiguousWhitespace( node.Children, TraversalDirection.Backwards, childIndex - 1 );
						RemoveContiguousWhitespace( node.Children, TraversalDirection.Forwards, childIndex + 1 );
					}
				}

				base.VisitDefault( node );
			}
		}
	}
}
