// 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 System.Security.Cryptography;
using Microsoft.AspNetCore.Razor.Language.Intermediate;

namespace Microsoft.AspNetCore.Razor.Language.CodeGeneration
{
	internal class DefaultDocumentWriter : DocumentWriter
	{
		private readonly CodeTarget _codeTarget;
		private readonly RazorCodeGenerationOptions _options;

		public DefaultDocumentWriter( CodeTarget codeTarget, RazorCodeGenerationOptions options )
		{
			_codeTarget = codeTarget;
			_options = options;
		}

		public override RazorCSharpDocument WriteDocument( RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode )
		{
			if ( codeDocument == null )
			{
				throw new ArgumentNullException( nameof( codeDocument ) );
			}

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

			var context = new DefaultCodeRenderingContext(
				new CodeWriter( Environment.NewLine, _options ),
				_codeTarget.CreateNodeWriter(),
				codeDocument,
				documentNode,
				_options );
			context.Visitor = new Visitor( _codeTarget, context );

			context.Visitor.VisitDocument( documentNode );

			var cSharp = context.CodeWriter.GenerateCode();

			var allOrderedDiagnostics = context.Diagnostics.OrderBy( diagnostic => diagnostic.Span.AbsoluteIndex );
			return new DefaultRazorCSharpDocument(
				cSharp,
				_options,
				allOrderedDiagnostics.ToArray(),
				context.SourceMappings.ToArray(),
				context.LinePragmas.ToArray() );
		}

		private class Visitor : IntermediateNodeVisitor
		{
			private readonly DefaultCodeRenderingContext _context;
			private readonly CodeTarget _target;

			public Visitor( CodeTarget target, DefaultCodeRenderingContext context )
			{
				_target = target;
				_context = context;
			}

			private DefaultCodeRenderingContext Context => _context;

			public override void VisitDocument( DocumentIntermediateNode node )
			{


				if ( !Context.Options.SuppressChecksum )
				{
					// See http://msdn.microsoft.com/en-us/library/system.codedom.codechecksumpragma.checksumalgorithmid.aspx
					// And https://github.com/dotnet/roslyn/blob/614299ff83da9959fa07131c6d0ffbc58873b6ae/src/Compilers/Core/Portable/PEWriter/DebugSourceDocument.cs#L67
					//
					// We only support algorithms that the debugger understands, which is currently SHA1 and SHA256.

					string algorithmId;
					var algorithm = Context.SourceDocument.GetChecksumAlgorithm();
					if ( string.Equals( algorithm, HashAlgorithmName.SHA256.Name, StringComparison.Ordinal ) )
					{
						algorithmId = "{8829d00f-11b8-4213-878b-770e8597ac16}";
					}
					else if ( string.Equals( algorithm, HashAlgorithmName.SHA1.Name, StringComparison.Ordinal ) ||

						// In 2.0, we didn't actually expose the name of the algorithm, so it's possible we could get null here.
						// If that's the case, we just assume SHA1 since that's the only thing we supported in 2.0.
						algorithm == null )
					{
						algorithmId = "{ff1816ec-aa5e-4d10-87f7-6f4963833460}";
					}
					else
					{
						var supportedAlgorithms = string.Join( " ", new string[]
						{
							HashAlgorithmName.SHA1.Name,
							HashAlgorithmName.SHA256.Name
						} );

						throw new InvalidOperationException();
					}

					var sourceDocument = Context.SourceDocument;

					var checksum = Checksum.BytesToString( sourceDocument.GetChecksum() );
					if ( !string.IsNullOrEmpty( checksum ) )
					{
						Context.CodeWriter
							.Write( "#pragma checksum \"" )
							.Write( sourceDocument.FilePath )
							.Write( "\" \"" )
							.Write( algorithmId )
							.Write( "\" \"" )
							.Write( checksum )
							.WriteLine( "\"" );
					}
				}

				Context.CodeWriter.WriteLine( "// <auto-generated/>" );
				Context.CodeWriter.WriteLine( "#pragma warning disable 1591" );
				Context.CodeWriter.WriteLine( "#pragma warning disable 0162" );

				VisitDefault( node );

				Context.CodeWriter.WriteLine( "#pragma warning restore 0162" );
				Context.CodeWriter.WriteLine( "#pragma warning restore 1591" );
			}

			public override void VisitUsingDirective( UsingDirectiveIntermediateNode node )
			{
				Context.NodeWriter.WriteUsingDirective( Context, node );
			}

			public override void VisitNamespaceDeclaration( NamespaceDeclarationIntermediateNode node )
			{
				using ( Context.CodeWriter.BuildNamespace( node.Content ) )
				{
					Context.CodeWriter.WriteLine( "#line hidden" );
					VisitDefault( node );
				}
			}

			public override void VisitClassDeclaration( ClassDeclarationIntermediateNode node )
			{
				using var writer = Context.CodeWriter.BuildClassDeclaration(
					node.Visibility,
					node.Modifiers,
					node.ClassName,
					node.BaseType,
					node.Interfaces,
					node.Attributes,
					node.TypeParameters.Select( p => (p.ParameterName, p.Constraints) ).ToArray() );

				//
				// garry: I guess this should be on CodeWriter in some stupidly abstracted way that makes
				//			no sense at all because it gets called once and the writer class is sealed anyway so wtf
				//
				Context.CodeWriter.WriteLine( "// Make sure everyone knows that we have a render tree" );
				Context.CodeWriter.WriteLine( "// (this is an optimization so we have a quick and definite way to check)" );
				Context.CodeWriter.WriteLine( $"protected override string GetRenderTreeChecksum() => \"{node.ChecksumHash}\";" );
				Context.CodeWriter.WriteLine( "" );

				VisitDefault( node );
			}

			public override void VisitMethodDeclaration( MethodDeclarationIntermediateNode node )
			{
				Context.CodeWriter.WriteLine( "#pragma warning disable 1998" );

				for ( var i = 0; i < node.Modifiers.Count; i++ )
				{
					Context.CodeWriter.Write( node.Modifiers[i] );
					Context.CodeWriter.Write( " " );
				}

				Context.CodeWriter.Write( node.ReturnType );
				Context.CodeWriter.Write( " " );

				Context.CodeWriter.Write( node.MethodName );
				Context.CodeWriter.Write( "(" );

				for ( var i = 0; i < node.Parameters.Count; i++ )
				{
					var parameter = node.Parameters[i];

					for ( var j = 0; j < parameter.Modifiers.Count; j++ )
					{
						Context.CodeWriter.Write( parameter.Modifiers[j] );
						Context.CodeWriter.Write( " " );
					}

					Context.CodeWriter.Write( parameter.TypeName );
					Context.CodeWriter.Write( " " );

					Context.CodeWriter.Write( parameter.ParameterName );

					if ( i < node.Parameters.Count - 1 )
					{
						Context.CodeWriter.Write( ", " );
					}
				}

				Context.CodeWriter.Write( ")" );
				Context.CodeWriter.WriteLine();

				using ( Context.CodeWriter.BuildScope() )
				{
					VisitDefault( node );
				}

				Context.CodeWriter.WriteLine( "#pragma warning restore 1998" );
			}

			public override void VisitFieldDeclaration( FieldDeclarationIntermediateNode node )
			{
				Context.CodeWriter.WriteField( node.SuppressWarnings, node.Modifiers, node.FieldType, node.FieldName );
			}

			public override void VisitPropertyDeclaration( PropertyDeclarationIntermediateNode node )
			{
				Context.CodeWriter.WriteAutoPropertyDeclaration( node.Modifiers, node.PropertyType, node.PropertyName );
			}

			public override void VisitExtension( ExtensionIntermediateNode node )
			{
				node.WriteNode( _target, Context );
			}

			public override void VisitCSharpExpression( CSharpExpressionIntermediateNode node )
			{
				Context.NodeWriter.WriteCSharpExpression( Context, node );
			}

			public override void VisitCSharpCode( CSharpCodeIntermediateNode node )
			{
				Context.NodeWriter.WriteCSharpCode( Context, node );
			}

			public override void VisitHtmlAttribute( HtmlAttributeIntermediateNode node )
			{
				Context.NodeWriter.WriteHtmlAttribute( Context, node );
			}

			public override void VisitHtmlAttributeValue( HtmlAttributeValueIntermediateNode node )
			{
				Context.NodeWriter.WriteHtmlAttributeValue( Context, node );
			}

			public override void VisitCSharpExpressionAttributeValue( CSharpExpressionAttributeValueIntermediateNode node )
			{
				Context.NodeWriter.WriteCSharpExpressionAttributeValue( Context, node );
			}

			public override void VisitCSharpCodeAttributeValue( CSharpCodeAttributeValueIntermediateNode node )
			{
				Context.NodeWriter.WriteCSharpCodeAttributeValue( Context, node );
			}

			public override void VisitHtml( HtmlContentIntermediateNode node )
			{
				Context.NodeWriter.WriteHtmlContent( Context, node );
			}

			public override void VisitTagHelper( TagHelperIntermediateNode node )
			{
				VisitDefault( node );
			}

			public override void VisitComponent( ComponentIntermediateNode node )
			{
				Context.NodeWriter.WriteComponent( Context, node );
			}

			public override void VisitComponentAttribute( ComponentAttributeIntermediateNode node )
			{
				Context.NodeWriter.WriteComponentAttribute( Context, node );
			}

			public override void VisitComponentChildContent( ComponentChildContentIntermediateNode node )
			{
				Context.NodeWriter.WriteComponentChildContent( Context, node );
			}

			public override void VisitComponentTypeArgument( ComponentTypeArgumentIntermediateNode node )
			{
				Context.NodeWriter.WriteComponentTypeArgument( Context, node );
			}

			public override void VisitComponentTypeInferenceMethod( ComponentTypeInferenceMethodIntermediateNode node )
			{
				Context.NodeWriter.WriteComponentTypeInferenceMethod( Context, node );
			}

			public override void VisitMarkupElement( MarkupElementIntermediateNode node )
			{
				Context.NodeWriter.WriteMarkupElement( Context, node );
			}

			public override void VisitMarkupBlock( MarkupBlockIntermediateNode node )
			{
				Context.NodeWriter.WriteMarkupBlock( Context, node );
			}

			public override void VisitSetKey( SetKeyIntermediateNode node )
			{
				Context.NodeWriter.WriteSetKey( Context, node );
			}

			public override void VisitSplat( SplatIntermediateNode node )
			{
				Context.NodeWriter.WriteSplat( Context, node );
			}

			public override void VisitDefault( IntermediateNode node )
			{
				Context.RenderChildren( node );
			}
		}
	}
}
