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

namespace Microsoft.AspNetCore.Razor.Language.Extensions
{
	internal class PreallocatedAttributeTargetExtension : IPreallocatedAttributeTargetExtension
	{
		public string TagHelperAttributeTypeName { get; set; } = "Microsoft.AspNetCore.Razor.TagHelpers.TagHelperAttribute";

		public string EncodedHtmlStringTypeName { get; set; } = "Microsoft.AspNetCore.Html.HtmlString";

		public string ExecutionContextVariableName { get; set; } = "__tagHelperExecutionContext";

		public string ExecutionContextAddHtmlAttributeMethodName { get; set; } = "AddHtmlAttribute";

		public string ExecutionContextAddTagHelperAttributeMethodName { get; set; } = "AddTagHelperAttribute";

		public string FormatInvalidIndexerAssignmentMethodName { get; set; } = "InvalidTagHelperIndexerAssignment";

		public void WriteTagHelperHtmlAttributeValue( CodeRenderingContext context, PreallocatedTagHelperHtmlAttributeValueIntermediateNode node )
		{
			context.CodeWriter
				.Write( "private static readonly global::" )
				.Write( TagHelperAttributeTypeName )
				.Write( " " )
				.Write( node.VariableName )
				.Write( " = " )
				.WriteStartNewObject( "global::" + TagHelperAttributeTypeName )
				.WriteStringLiteral( node.AttributeName );

			if ( node.AttributeStructure == AttributeStructure.Minimized )
			{
				context.CodeWriter.WriteEndMethodInvocation();
			}
			else
			{
				context.CodeWriter
					.WriteParameterSeparator()
					.WriteStartNewObject( "global::" + EncodedHtmlStringTypeName )
					.WriteStringLiteral( node.Value )
					.WriteEndMethodInvocation( endLine: false )
					.WriteParameterSeparator()
					.Write( $"global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.{node.AttributeStructure}" )
					.WriteEndMethodInvocation();
			}
		}

		public void WriteTagHelperHtmlAttribute( CodeRenderingContext context, PreallocatedTagHelperHtmlAttributeIntermediateNode node )
		{
			if ( context.Parent as TagHelperIntermediateNode == null )
			{
				throw new InvalidOperationException();
			}

			context.CodeWriter
				.WriteStartInstanceMethodInvocation( ExecutionContextVariableName, ExecutionContextAddHtmlAttributeMethodName )
				.Write( node.VariableName )
				.WriteEndMethodInvocation();
		}

		public void WriteTagHelperPropertyValue( CodeRenderingContext context, PreallocatedTagHelperPropertyValueIntermediateNode node )
		{
			context.CodeWriter
				.Write( "private static readonly global::" )
				.Write( TagHelperAttributeTypeName )
				.Write( " " )
				.Write( node.VariableName )
				.Write( " = " )
				.WriteStartNewObject( "global::" + TagHelperAttributeTypeName )
				.WriteStringLiteral( node.AttributeName )
				.WriteParameterSeparator()
				.WriteStringLiteral( node.Value )
				.WriteParameterSeparator()
				.Write( $"global::Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeValueStyle.{node.AttributeStructure}" )
				.WriteEndMethodInvocation();
		}

		public void WriteTagHelperProperty( CodeRenderingContext context, PreallocatedTagHelperPropertyIntermediateNode node )
		{
			var tagHelperNode = context.Parent as TagHelperIntermediateNode;
			if ( tagHelperNode == null )
			{
				throw new InvalidOperationException();
			}

			// Ensure that the property we're trying to set has initialized its dictionary bound properties.
			if ( node.IsIndexerNameMatch &&
				object.ReferenceEquals( FindFirstUseOfIndexer( tagHelperNode, node ), node ) )
			{
				// Throw a reasonable Exception at runtime if the dictionary property is null.
				context.CodeWriter
					.Write( "if (" )
					.Write( node.FieldName )
					.Write( "." )
					.Write( node.PropertyName )
					.WriteLine( " == null)" );
				using ( context.CodeWriter.BuildScope() )
				{
					// System is in Host.NamespaceImports for all MVC scenarios. No need to generate FullName
					// of InvalidOperationException type.
					context.CodeWriter
						.Write( "throw " )
						.WriteStartNewObject( nameof( InvalidOperationException ) )
						.WriteStartMethodInvocation( FormatInvalidIndexerAssignmentMethodName )
						.WriteStringLiteral( node.AttributeName )
						.WriteParameterSeparator()
						.WriteStringLiteral( node.TagHelper.GetTypeName() )
						.WriteParameterSeparator()
						.WriteStringLiteral( node.PropertyName )
						.WriteEndMethodInvocation( endLine: false )   // End of method call
						.WriteEndMethodInvocation();   // End of new expression / throw statement
				}
			}

			context.CodeWriter
				.WriteStartAssignment( GetPropertyAccessor( node ) )
				.Write( "(string)" )
				.Write( $"{node.VariableName}.Value" )
				.WriteLine( ";" )
				.WriteStartInstanceMethodInvocation( ExecutionContextVariableName, ExecutionContextAddTagHelperAttributeMethodName )
				.Write( node.VariableName )
				.WriteEndMethodInvocation();
		}

		private static PreallocatedTagHelperPropertyIntermediateNode FindFirstUseOfIndexer(
			TagHelperIntermediateNode tagHelperNode,
			PreallocatedTagHelperPropertyIntermediateNode propertyNode )
		{
			Debug.Assert( tagHelperNode.Children.Contains( propertyNode ) );
			Debug.Assert( propertyNode.IsIndexerNameMatch );

			for ( var i = 0; i < tagHelperNode.Children.Count; i++ )
			{
				if ( tagHelperNode.Children[i] is PreallocatedTagHelperPropertyIntermediateNode otherPropertyNode &&
					otherPropertyNode.TagHelper.Equals( propertyNode.TagHelper ) &&
					otherPropertyNode.BoundAttribute.Equals( propertyNode.BoundAttribute ) &&
					otherPropertyNode.IsIndexerNameMatch )
				{
					return otherPropertyNode;
				}
			}

			// This is unreachable, we should find 'propertyNode' in the list of children.
			throw new InvalidOperationException();
		}

		private static string GetPropertyAccessor( PreallocatedTagHelperPropertyIntermediateNode node )
		{
			var propertyAccessor = $"{node.FieldName}.{node.PropertyName}";

			if ( node.IsIndexerNameMatch )
			{
				var dictionaryKey = node.AttributeName.Substring( node.BoundAttribute.IndexerNamePrefix.Length );
				propertyAccessor += $"[\"{dictionaryKey}\"]";
			}

			return propertyAccessor;
		}
	}
}
