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

namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
{
	public class RuntimeNodeWriter : IntermediateNodeWriter
	{
		public virtual string WriteCSharpExpressionMethod { get; set; } = "Write";

		public virtual string WriteHtmlContentMethod { get; set; } = "WriteLiteral";

		public virtual string BeginWriteAttributeMethod { get; set; } = "BeginWriteAttribute";

		public virtual string EndWriteAttributeMethod { get; set; } = "EndWriteAttribute";

		public virtual string WriteAttributeValueMethod { get; set; } = "WriteAttributeValue";

		public virtual string PushWriterMethod { get; set; } = "PushWriter";

		public virtual string PopWriterMethod { get; set; } = "PopWriter";

		public string TemplateTypeName { get; set; } = "Microsoft.AspNetCore.Mvc.Razor.HelperResult";

		public override void WriteUsingDirective( CodeRenderingContext context, UsingDirectiveIntermediateNode node )
		{
			if ( node.Source.HasValue )
			{
				using ( context.CodeWriter.BuildLinePragma( node.Source.Value, context ) )
				{
					context.CodeWriter.WriteUsing( node.Content );
				}
			}
			else
			{
				context.CodeWriter.WriteUsing( node.Content );
			}
		}

		public override void WriteCSharpExpression( CodeRenderingContext context, CSharpExpressionIntermediateNode node )
		{
			if ( context == null )
			{
				throw new ArgumentNullException( nameof( context ) );
			}

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

			IDisposable linePragmaScope = null;
			if ( node.Source != null )
			{
				linePragmaScope = context.CodeWriter.BuildLinePragma( node.Source.Value, context );
				// if (!context.Options.UseEnhancedLinePragma)
				{
					//   context.CodeWriter.WritePadding(WriteCSharpExpressionMethod.Length + 1, node.Source, context);
				}
			}

			context.CodeWriter.WriteStartMethodInvocation( WriteCSharpExpressionMethod );

			for ( var i = 0; i < node.Children.Count; i++ )
			{
				if ( node.Children[i] is IntermediateToken token && token.IsCSharp )
				{
					context.CodeWriter.Write( token.Content );
				}
				else
				{
					// There may be something else inside the expression like a Template or another extension node.
					context.RenderNode( node.Children[i] );
				}
			}

			context.CodeWriter.WriteEndMethodInvocation();

			linePragmaScope?.Dispose();
		}

		public override void WriteCSharpCode( CodeRenderingContext context, CSharpCodeIntermediateNode node )
		{
			var isWhitespaceStatement = true;
			for ( var i = 0; i < node.Children.Count; i++ )
			{
				var token = node.Children[i] as IntermediateToken;
				if ( token == null || !string.IsNullOrWhiteSpace( token.Content ) )
				{
					isWhitespaceStatement = false;
					break;
				}
			}

			if ( isWhitespaceStatement )
			{
				return;
			}

			IDisposable linePragmaScope = null;
			if ( node.Source != null )
			{
				linePragmaScope = context.CodeWriter.BuildLinePragma( node.Source.Value, context );
				context.CodeWriter.WritePadding( 0, node.Source.Value, context );
			}

			for ( var i = 0; i < node.Children.Count; i++ )
			{
				if ( node.Children[i] is IntermediateToken token && token.IsCSharp )
				{
					context.CodeWriter.Write( token.Content );
				}
				else
				{
					// There may be something else inside the statement like an extension node.
					context.RenderNode( node.Children[i] );
				}
			}

			if ( linePragmaScope == null )
			{
				context.CodeWriter.WriteLine();
			}

			linePragmaScope?.Dispose();
		}

		public override void WriteHtmlAttribute( CodeRenderingContext context, HtmlAttributeIntermediateNode node )
		{
			var valuePieceCount = node
				.Children
				.Count( child =>
					child is HtmlAttributeValueIntermediateNode ||
					child is CSharpExpressionAttributeValueIntermediateNode ||
					child is CSharpCodeAttributeValueIntermediateNode ||
					child is ExtensionIntermediateNode );

			var prefixLocation = node.Source.Value.AbsoluteIndex;
			var suffixLocation = node.Source.Value.AbsoluteIndex + node.Source.Value.Length - node.Suffix.Length;
			context.CodeWriter
				.WriteStartMethodInvocation( BeginWriteAttributeMethod )
				.WriteStringLiteral( node.AttributeName )
				.WriteParameterSeparator()
				.WriteStringLiteral( node.Prefix )
				.WriteParameterSeparator()
				.Write( prefixLocation.ToString( CultureInfo.InvariantCulture ) )
				.WriteParameterSeparator()
				.WriteStringLiteral( node.Suffix )
				.WriteParameterSeparator()
				.Write( suffixLocation.ToString( CultureInfo.InvariantCulture ) )
				.WriteParameterSeparator()
				.Write( valuePieceCount.ToString( CultureInfo.InvariantCulture ) )
				.WriteEndMethodInvocation();

			context.RenderChildren( node );

			context.CodeWriter
				.WriteStartMethodInvocation( EndWriteAttributeMethod )
				.WriteEndMethodInvocation();
		}

		public override void WriteHtmlAttributeValue( CodeRenderingContext context, HtmlAttributeValueIntermediateNode node )
		{
			var prefixLocation = node.Source.Value.AbsoluteIndex;
			var valueLocation = node.Source.Value.AbsoluteIndex + node.Prefix.Length;
			var valueLength = node.Source.Value.Length;
			context.CodeWriter
				.WriteStartMethodInvocation( WriteAttributeValueMethod )
				.WriteStringLiteral( node.Prefix )
				.WriteParameterSeparator()
				.Write( prefixLocation.ToString( CultureInfo.InvariantCulture ) )
				.WriteParameterSeparator();

			// Write content
			for ( var i = 0; i < node.Children.Count; i++ )
			{
				if ( node.Children[i] is IntermediateToken token && token.IsHtml )
				{
					context.CodeWriter.WriteStringLiteral( token.Content );
				}
				else
				{
					// There may be something else inside the attribute value like an extension node.
					context.RenderNode( node.Children[i] );
				}
			}

			context.CodeWriter
				.WriteParameterSeparator()
				.Write( valueLocation.ToString( CultureInfo.InvariantCulture ) )
				.WriteParameterSeparator()
				.Write( valueLength.ToString( CultureInfo.InvariantCulture ) )
				.WriteParameterSeparator()
				.WriteBooleanLiteral( true )
				.WriteEndMethodInvocation();
		}

		public override void WriteCSharpExpressionAttributeValue( CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node )
		{
			var prefixLocation = node.Source.Value.AbsoluteIndex.ToString( CultureInfo.InvariantCulture );

			using ( context.CodeWriter.BuildLinePragma( node.Source.Value, context ) )
			{

				context.CodeWriter
					.WriteStartMethodInvocation( WriteAttributeValueMethod )
					.WriteStringLiteral( node.Prefix )
					.WriteParameterSeparator()
					.Write( prefixLocation )
					.WriteParameterSeparator();

				for ( var i = 0; i < node.Children.Count; i++ )
				{
					if ( node.Children[i] is IntermediateToken token && token.IsCSharp )
					{
						context.CodeWriter.Write( token.Content );
					}
					else
					{
						// There may be something else inside the expression like an extension node.
						context.RenderNode( node.Children[i] );
					}
				}

				var valueLocation = node.Source.Value.AbsoluteIndex + node.Prefix.Length;
				var valueLength = node.Source.Value.Length - node.Prefix.Length;
				context.CodeWriter
					.WriteParameterSeparator()
					.Write( valueLocation.ToString( CultureInfo.InvariantCulture ) )
					.WriteParameterSeparator()
					.Write( valueLength.ToString( CultureInfo.InvariantCulture ) )
					.WriteParameterSeparator()
					.WriteBooleanLiteral( false )
					.WriteEndMethodInvocation();
			}
		}

		public override void WriteCSharpCodeAttributeValue( CodeRenderingContext context, CSharpCodeAttributeValueIntermediateNode node )
		{
			const string ValueWriterName = "__razor_attribute_value_writer";

			var prefixLocation = node.Source.Value.AbsoluteIndex;
			var valueLocation = node.Source.Value.AbsoluteIndex + node.Prefix.Length;
			var valueLength = node.Source.Value.Length - node.Prefix.Length;
			context.CodeWriter
				.WriteStartMethodInvocation( WriteAttributeValueMethod )
				.WriteStringLiteral( node.Prefix )
				.WriteParameterSeparator()
				.Write( prefixLocation.ToString( CultureInfo.InvariantCulture ) )
				.WriteParameterSeparator();

			context.CodeWriter.WriteStartNewObject( TemplateTypeName );

			using ( context.CodeWriter.BuildAsyncLambda( ValueWriterName ) )
			{
				BeginWriterScope( context, ValueWriterName );

				for ( var i = 0; i < node.Children.Count; i++ )
				{
					if ( node.Children[i] is IntermediateToken token && token.IsCSharp )
					{
						var isWhitespaceStatement = string.IsNullOrWhiteSpace( token.Content );
						IDisposable linePragmaScope = null;
						if ( token.Source != null )
						{
							if ( !isWhitespaceStatement )
							{
								linePragmaScope = context.CodeWriter.BuildLinePragma( token.Source.Value, context );
							}

							context.CodeWriter.WritePadding( 0, token.Source.Value, context );
						}
						else if ( isWhitespaceStatement )
						{
							// Don't write whitespace if there is no line mapping for it.
							continue;
						}

						context.CodeWriter.Write( token.Content );

						if ( linePragmaScope != null )
						{
							linePragmaScope.Dispose();
						}
						else
						{
							context.CodeWriter.WriteLine();
						}
					}
					else
					{
						// There may be something else inside the statement like an extension node.
						context.RenderNode( node.Children[i] );
					}
				}

				EndWriterScope( context );
			}

			context.CodeWriter.WriteEndMethodInvocation( false );

			context.CodeWriter
				.WriteParameterSeparator()
				.Write( valueLocation.ToString( CultureInfo.InvariantCulture ) )
				.WriteParameterSeparator()
				.Write( valueLength.ToString( CultureInfo.InvariantCulture ) )
				.WriteParameterSeparator()
				.WriteBooleanLiteral( false )
				.WriteEndMethodInvocation();
		}

		public override void WriteHtmlContent( CodeRenderingContext context, HtmlContentIntermediateNode node )
		{
			const int MaxStringLiteralLength = 1024;

			var builder = new StringBuilder();
			for ( var i = 0; i < node.Children.Count; i++ )
			{
				if ( node.Children[i] is IntermediateToken token && token.IsHtml )
				{
					builder.Append( token.Content );
				}
			}

			var content = builder.ToString();

			WriteHtmlLiteral( context, MaxStringLiteralLength, content );
		}

		// Internal for testing
		internal void WriteHtmlLiteral( CodeRenderingContext context, int maxStringLiteralLength, string literal )
		{
			if ( literal.Length <= maxStringLiteralLength )
			{
				WriteLiteral( literal );
				return;
			}

			// String is too large, render the string in pieces to avoid Roslyn OOM exceptions at compile time: https://github.com/aspnet/External/issues/54
			var charactersConsumed = 0;
			do
			{
				var charactersRemaining = literal.Length - charactersConsumed;
				var charactersToSubstring = Math.Min( maxStringLiteralLength, charactersRemaining );
				var lastCharBeforeSplitIndex = charactersConsumed + charactersToSubstring - 1;
				var lastCharBeforeSplit = literal[lastCharBeforeSplitIndex];

				if ( char.IsHighSurrogate( lastCharBeforeSplit ) )
				{
					if ( charactersRemaining > 1 )
					{
						// Take one less character this iteration. We're attempting to split inbetween a surrogate pair.
						// This can happen when something like an emoji sits on the barrier between splits; if we were to
						// split the emoji we'd end up with invalid bytes in our output.
						charactersToSubstring--;
					}
					else
					{
						// The user has an invalid file with a partial surrogate a the splitting point.
						// We'll let the invalid character flow but we'll explode later on.
					}
				}

				var textToRender = literal.Substring( charactersConsumed, charactersToSubstring );

				WriteLiteral( textToRender );

				charactersConsumed += textToRender.Length;
			} while ( charactersConsumed < literal.Length );

			void WriteLiteral( string content )
			{
				context.CodeWriter
					.WriteStartMethodInvocation( WriteHtmlContentMethod )
					.WriteStringLiteral( content )
					.WriteEndMethodInvocation();
			}
		}

		public override void BeginWriterScope( CodeRenderingContext context, string writer )
		{
			context.CodeWriter.WriteMethodInvocation( PushWriterMethod, writer );
		}

		public override void EndWriterScope( CodeRenderingContext context )
		{
			context.CodeWriter.WriteMethodInvocation( PopWriterMethod );
		}
	}
}
