// 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.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Razor.Language.Intermediate;

namespace Microsoft.AspNetCore.Razor.Language.Components
{
	// Rewrites contiguous subtrees of HTML into a special node type to reduce the
	// size of the Render tree.
	//
	// Does not preserve insignificant details of the HTML, like tag closing style
	// or quote style.
	internal class ComponentMarkupBlockPass : ComponentIntermediateNodePassBase, IRazorOptimizationPass
	{
		// Runs LATE because we want to destroy structure.
		//
		// We also need to run after ComponentMarkupDiagnosticPass to avoid destroying diagnostics
		// added in that pass.
		public override int Order => ComponentMarkupDiagnosticPass.DefaultOrder + 10;

		protected override void ExecuteCore(
			RazorCodeDocument codeDocument,
			DocumentIntermediateNode documentNode )
		{
			if ( !IsComponentDocument( documentNode ) )
			{
				return;
			}

			var findVisitor = new FindHtmlTreeVisitor();
			findVisitor.Visit( documentNode );

			var trees = findVisitor.Trees;
			var rewriteVisitor = new RewriteVisitor( trees );

			while ( trees.Count > 0 )
			{
				SourceSpan? sourceInfo = null;
				// Walk backwards since we did a postorder traversal.
				var reference = trees[trees.Count - 1];

				// Forcibly remove a node to prevent infinite loops.
				trees.RemoveAt( trees.Count - 1 );

				// We want to fold together siblings where possible. To do this, first we find
				// the index of the node we're looking at now - then we need to walk backwards
				// and identify a set of contiguous nodes we can merge.
				var start = reference.Parent.Children.Count - 1;
				for ( ; start >= 0; start-- )
				{
					if ( ReferenceEquals( reference.Node, reference.Parent.Children[start] ) )
					{
						break;
					}
				}

				// This is the current node. Check if the left sibling is always a candidate
				// for rewriting. Due to the order we processed the nodes, we know that the
				// left sibling is next in the list to process if it's a candidate.
				var end = start;
				while ( start - 1 >= 0 )
				{
					var candidate = reference.Parent.Children[start - 1];
					if ( trees.Count == 0 || !ReferenceEquals( trees[trees.Count - 1].Node, candidate ) )
					{
						// This means the we're out of nodes, or the left sibling is not in the list.
						break;
					}



					// This means that the left sibling is valid to merge.
					start--;

					// Remove this since we're combining it.
					trees.RemoveAt( trees.Count - 1 );
				}

				// As a degenerate case, don't bother rewriting an single HtmlContent node
				// It doesn't add any value.
				if ( end - start == 0 && reference.Node is HtmlContentIntermediateNode )
				{
					continue;
				}

				// Now we know the range of nodes to rewrite (end is inclusive)
				var length = end + 1 - start;
				while ( length > 0 )
				{
					// Keep using start since we're removing nodes.
					var node = reference.Parent.Children[start];
					reference.Parent.Children.RemoveAt( start );

					// garry: don't record source lines for \n\r\n\r etc because they
					// don't create elements. Find the first element that DOES.
					if ( node is not HtmlContentIntermediateNode && !sourceInfo.HasValue )
					{
						sourceInfo = node.Source;
					}

					rewriteVisitor.Visit( node );

					length--;
				}

				reference.Parent.Children.Insert( start, new MarkupBlockIntermediateNode()
				{
					Content = rewriteVisitor.Builder.ToString(),
					Source = sourceInfo
				} );

				rewriteVisitor.Builder.Clear();
			}
		}

		// Finds HTML-blocks using a postorder traversal. We store nodes in an
		// ordered list so we can avoid redundant rewrites.
		//
		// Consider a case like:
		//  <div>
		//    <a href="...">click me</a>
		//  </div>
		//
		// We would store both the div and a tag in a list, but make sure to visit
		// the div first. Then when we process the div (recursively), we would remove
		// the a from the list.
		private class FindHtmlTreeVisitor : IntermediateNodeWalker
		{
			private bool _foundNonHtml;

			public List<IntermediateNodeReference> Trees { get; } = new List<IntermediateNodeReference>();

			public override void VisitDefault( IntermediateNode node )
			{
				// If we get here, we found a non-HTML node. Keep traversing.
				_foundNonHtml = true;
				base.VisitDefault( node );
			}

			public override void VisitMarkupElement( MarkupElementIntermediateNode node )
			{
				// We need to restore the state after processing this node.
				// We might have found a leaf-block of HTML, but that shouldn't
				// affect our parent's state.
				var originalState = _foundNonHtml;

				_foundNonHtml = false;

				if ( node.HasDiagnostics )
				{
					// Treat node with errors as non-HTML - don't let the parent rewrite this either.
					_foundNonHtml = true;
				}

				//
				// garry: don't treat this as a basic html node
				//
				if ( node.LooksLikeAPanelClass ||
					string.Equals( "root", node.TagName, StringComparison.OrdinalIgnoreCase ) ||
					string.Equals( "style", node.TagName, StringComparison.OrdinalIgnoreCase ) )
				{
					_foundNonHtml = true;
				}

				base.VisitDefault( node );

				if ( !_foundNonHtml )
				{
					Trees.Add( new IntermediateNodeReference( Parent, node ) );
				}

				_foundNonHtml = originalState |= _foundNonHtml;
			}

			public override void VisitHtmlAttribute( HtmlAttributeIntermediateNode node )
			{
				if ( node.HasDiagnostics )
				{
					// Treat node with errors as non-HTML
					_foundNonHtml = true;
				}

				if ( node.LooksLikeNonHtml )
				{
					_foundNonHtml = true;
				}

				// Visit Children
				base.VisitDefault( node );
			}

			public override void VisitHtmlAttributeValue( HtmlAttributeValueIntermediateNode node )
			{
				if ( node.HasDiagnostics )
				{
					// Treat node with errors as non-HTML
					_foundNonHtml = true;
				}



				// Visit Children
				base.VisitDefault( node );
			}

			public override void VisitHtml( HtmlContentIntermediateNode node )
			{
				// We need to restore the state after processing this node.
				// We might have found a leaf-block of HTML, but that shouldn't
				// affect our parent's state.
				var originalState = _foundNonHtml;

				_foundNonHtml = false;

				if ( node.HasDiagnostics )
				{
					// Treat node with errors as non-HTML
					_foundNonHtml = true;
				}

				// Visit Children
				base.VisitDefault( node );

				if ( !_foundNonHtml )
				{
					Trees.Add( new IntermediateNodeReference( Parent, node ) );
				}

				_foundNonHtml = originalState |= _foundNonHtml;
			}

			public override void VisitToken( IntermediateToken node )
			{
				if ( node.HasDiagnostics )
				{
					// Treat node with errors as non-HTML
					_foundNonHtml = true;
				}

				if ( node.IsCSharp )
				{
					_foundNonHtml = true;
				}
			}
		}

		private class RewriteVisitor : IntermediateNodeWalker
		{
			private readonly StringBuilder _encodingBuilder;

			private readonly List<IntermediateNodeReference> _trees;

			public RewriteVisitor( List<IntermediateNodeReference> trees )
			{
				_trees = trees;

				_encodingBuilder = new StringBuilder();
			}

			public StringBuilder Builder { get; } = new StringBuilder();

			public override void VisitMarkupElement( MarkupElementIntermediateNode node )
			{
				for ( var i = 0; i < _trees.Count; i++ )
				{
					// Remove this node if it's in the list. This ensures that we don't
					// do redundant operations.
					if ( ReferenceEquals( _trees[i].Node, node ) )
					{
						_trees.RemoveAt( i );
						break;
					}
				}

				var isVoid = Legacy.ParserHelpers.VoidElements.Contains( node.TagName );
				var hasBodyContent = node.Body.Any();

				Builder.Append( '<' );
				Builder.Append( node.TagName );

				foreach ( var attribute in node.Attributes )
				{
					Visit( attribute );
				}

				// If for some reason a void element contains body, then treat it as a
				// start/end tag.
				if ( !hasBodyContent && isVoid )
				{
					// void
					Builder.Append( "/>" );
					return;
				}
				else if ( !hasBodyContent )
				{
					// In HTML5, we can't have self-closing non-void elements, so explicitly
					// add a close tag
					Builder.Append( "></" );
					Builder.Append( node.TagName );
					Builder.Append( '>' );
					return;
				}

				// start/end tag with body.
				Builder.Append( '>' );

				foreach ( var item in node.Body )
				{
					Visit( item );
				}

				Builder.Append( "</" );
				Builder.Append( node.TagName );
				Builder.Append( '>' );
			}

			public override void VisitHtmlAttribute( HtmlAttributeIntermediateNode node )
			{
				Builder.Append( ' ' );
				Builder.Append( node.AttributeName );

				if ( node.Children.Count == 0 )
				{
					// Minimized attribute
					return;
				}

				// We examine the node.Prefix (e.g. " onfocus='" or " on focus=\"")
				// to preserve the quote type that is used in the original markup.
				var quoteType = node.Prefix.EndsWith( "'", StringComparison.Ordinal ) ? "'" : "\"";

				Builder.Append( '=' );
				Builder.Append( quoteType );

				// Visit Children
				base.VisitDefault( node );

				Builder.Append( quoteType );
			}

			public override void VisitHtmlAttributeValue( HtmlAttributeValueIntermediateNode node )
			{
				for ( var i = 0; i < node.Children.Count; i++ )
				{
					Builder.Append( node.Prefix );

					if ( node.Children[i] is IntermediateToken token )
					{
						Builder.Append( token.Content );
					}
				}
			}

			public override void VisitHtml( HtmlContentIntermediateNode node )
			{
				for ( var i = 0; i < node.Children.Count; i++ )
				{
					if ( node.Children[i] is IntermediateToken token )
					{
						Builder.Append( token.Content );
					}
				}
			}
		}
	}
}
