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

namespace Microsoft.AspNetCore.Razor.Language.Components
{
	// This pass:
	// 1. Adds diagnostics for missing generic type arguments
	// 2. Rewrites the type name of the component to substitute generic type arguments
	// 3. Rewrites the type names of parameters/child content to substitute generic type arguments
	internal class ComponentGenericTypePass : ComponentIntermediateNodePassBase, IRazorOptimizationPass
	{
		private TypeNameFeature _typeNameFeature;

		// Runs after components/eventhandlers/ref/bind/templates. We want to validate every component
		// and it's usage of ChildContent.
		public override int Order => 160;

		private TypeNameFeature TypeNameFeature
		{
			get
			{
				// Doing lazy intialization here to avoid making things really complicated when we don't
				// need to exercise this code in tests.
				if ( _typeNameFeature == null )
				{
					_typeNameFeature = GetRequiredFeature<TypeNameFeature>();
				}

				return _typeNameFeature;
			}
		}

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

			var visitor = new Visitor( this );
			visitor.Visit( documentNode );
		}

		private class Visitor : IntermediateNodeWalker
		{
			private readonly ComponentGenericTypePass _pass;

			// Incrementing ID for type inference method names
			private int _id;

			public Visitor( ComponentGenericTypePass pass )
			{
				_pass = pass;
			}

			public override void VisitComponent( ComponentIntermediateNode node )
			{
				if ( node.Component.IsGenericTypedComponent() )
				{
					// Not generic, ignore.
					Process( node );
				}

				base.VisitDefault( node );
			}

			private void Process( ComponentIntermediateNode node )
			{
				// First collect all of the information we have about each type parameter
				//
				// Listing all type parameters that exist
				var bindings = new Dictionary<string, Binding>();
				var componentTypeParameters = node.Component.GetTypeParameters().ToList();
				var supplyCascadingTypeParameters = componentTypeParameters
					.Where( p => p.IsCascadingTypeParameterProperty() )
					.Select( p => p.Name )
					.ToList();
				foreach ( var attribute in componentTypeParameters )
				{
					bindings.Add( attribute.Name, new Binding() { Attribute = attribute, } );
				}

				// Listing all type arguments that have been specified.
				var hasTypeArgumentSpecified = false;
				foreach ( var typeArgumentNode in node.TypeArguments )
				{
					hasTypeArgumentSpecified = true;

					var binding = bindings[typeArgumentNode.TypeParameterName];
					binding.Node = typeArgumentNode;
					binding.Content = GetContent( typeArgumentNode );

					// Offer this explicit type argument to descendants too
					if ( supplyCascadingTypeParameters.Contains( typeArgumentNode.TypeParameterName ) )
					{
						node.ProvidesCascadingGenericTypes ??= new();
						node.ProvidesCascadingGenericTypes[typeArgumentNode.TypeParameterName] = new CascadingGenericTypeParameter
						{
							GenericTypeNames = new[] { typeArgumentNode.TypeParameterName },
							ValueType = typeArgumentNode.TypeParameterName,
							ValueExpression = $"default({binding.Content})",
						};
					}
				}

				if ( hasTypeArgumentSpecified )
				{
					// OK this means that the developer has specified at least one type parameter.
					// Either they specified everything and its OK to rewrite, or its an error.
					if ( ValidateTypeArguments( node, bindings ) )
					{
						var mappings = bindings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.Content );
						RewriteTypeNames( _pass.TypeNameFeature.CreateGenericTypeRewriter( mappings ), node );
					}

					return;
				}

				// OK if we get here that means that no type arguments were specified, so we will try to infer
				// the type.
				//
				// The actual inference is done by the C# compiler, we just emit an a method that represents the
				// use of this component.

				// Since we're generating code in a different namespace, we need to 'global qualify' all of the types
				// to avoid clashes with our generated code.
				RewriteTypeNames( _pass.TypeNameFeature.CreateGlobalQualifiedTypeNameRewriter( bindings.Keys ), node );

				//
				// We need to verify that an argument was provided that 'covers' each type parameter.
				//
				// For example, consider a repeater where the generic type is the 'item' type, but the developer has
				// not set the items. We won't be able to do type inference on this and so it will just be nonsense.
				foreach ( var attribute in node.Attributes )
				{
					if ( attribute != null && TryFindGenericTypeNames( attribute.BoundAttribute, out var typeParameters ) )
					{
						var attributeValueIsLambda = _pass.TypeNameFeature.IsLambda( GetContent( attribute ) );
						var provideCascadingGenericTypes = new CascadingGenericTypeParameter
						{
							GenericTypeNames = typeParameters,
							ValueType = attribute.BoundAttribute.TypeName,
							ValueSourceNode = attribute,
						};

						foreach ( var typeName in typeParameters )
						{
							if ( supplyCascadingTypeParameters.Contains( typeName ) )
							{
								// Advertise that this particular inferred generic type is available to descendants.
								// There might be multiple sources for each generic type, so pick the one that has the
								// fewest other generic types on it. For example if we could infer from either List<T>
								// or Dictionary<T, U>, we prefer List<T>.
								node.ProvidesCascadingGenericTypes ??= new();
								if ( !node.ProvidesCascadingGenericTypes.TryGetValue( typeName, out var existingValue )
									|| existingValue.GenericTypeNames.Count > typeParameters.Count )
								{
									node.ProvidesCascadingGenericTypes[typeName] = provideCascadingGenericTypes;
								}
							}

							if ( attributeValueIsLambda )
							{
								// For attributes whose values are lambdas, we don't know whether or not the value
								// covers the generic type - it depends on the content of the lambda.
								// For example, "() => 123" can cover Func<T>, but "() => null" cannot. So we'll
								// accept cascaded generic types from ancestors if they are compatible with the lambda,
								// hence we don't remove it from the list of uncovered generic types until after
								// we try matching against ancestor cascades.
								if ( bindings.TryGetValue( typeName, out var binding ) )
								{
									binding.CoveredByLambda = true;
								}
							}
							else
							{
								bindings.Remove( typeName );
							}
						}
					}
				}

				// For any remaining bindings, scan up the hierarchy of ancestor components and try to match them
				// with a cascaded generic parameter that can cover this one
				List<CascadingGenericTypeParameter> receivesCascadingGenericTypes = null;
				foreach ( var uncoveredBindingKey in bindings.Keys.ToList() )
				{
					var uncoveredBinding = bindings[uncoveredBindingKey];
					foreach ( var candidateAncestor in Ancestors.OfType<ComponentIntermediateNode>() )
					{
						if ( candidateAncestor.ProvidesCascadingGenericTypes != null
							&& candidateAncestor.ProvidesCascadingGenericTypes.TryGetValue( uncoveredBindingKey, out var genericTypeProvider ) )
						{
							// If the parameter value is an expression that includes multiple generic types, we only want
							// to use it if we want *all* those generic types. That is, a parameter of type MyType<T0, T1>
							// can supply types to a Child<T0, T1>, but not to a Child<T0>.
							// This is purely to avoid blowing up the complexity of the implementation here and could be
							// overcome in the future if we want. We'd need to figure out which extra types are unwanted,
							// and rewrite them to some unique name, and add that to the generic parameters list of the
							// inference methods.
							if ( genericTypeProvider.GenericTypeNames.All( GenericTypeIsUsed ) )
							{
								bindings.Remove( uncoveredBindingKey );
								receivesCascadingGenericTypes ??= new();
								receivesCascadingGenericTypes.Add( genericTypeProvider );

								// It's sufficient to identify the closest provider for each type parameter
								break;
							}

							bool GenericTypeIsUsed( string typeName ) => componentTypeParameters
								.Select( t => t.Name )
								.Contains( typeName, StringComparer.Ordinal );
						}
					}
				}

				// There are two remaining sources of possible generic type info which we consider
				// lower-priority than cascades from ancestors. Since these two sources *may* actually
				// resolve generic type ambiguities in some cases, we treat them as covering.
				//
				// [1] Attributes given as lambda expressions. These are lower priority than ancestor
				//     cascades because in most cases, lambdas don't provide type info
				foreach ( var entryToRemove in bindings.Where( e => e.Value.CoveredByLambda ).ToList() )
				{
					// Treat this binding as covered, because it's possible that the lambda does provide
					// enough info for type inference to succeed.
					bindings.Remove( entryToRemove.Key );
				}

				// [2] Child content parameters, which are nearly always defined as untyped lambdas
				//     (at least, that's what the Razor compiler produces), but can technically be
				//     hardcoded as a RenderFragment<Something> and hence actually give type info.
				foreach ( var attribute in node.ChildContents )
				{
					if ( TryFindGenericTypeNames( attribute.BoundAttribute, out var typeParameters ) )
					{
						foreach ( var typeName in typeParameters )
						{
							bindings.Remove( typeName );
						}
					}
				}

				// If any bindings remain then this means we would never be able to infer the arguments of this
				// component usage because the user hasn't set properties that include all of the types.
				if ( bindings.Count > 0 )
				{
					// However we still want to generate 'type inference' code because we want the errors to be as
					// helpful as possible. So let's substitute 'object' for all of those type parameters, and add
					// an error.
					var mappings = bindings.ToDictionary( kvp => kvp.Key, kvp => kvp.Value.Content );
					RewriteTypeNames( _pass.TypeNameFeature.CreateGenericTypeRewriter( mappings ), node );

					node.Diagnostics.Add( ComponentDiagnosticFactory.Create_GenericComponentTypeInferenceUnderspecified( node.Source, node, node.Component.GetTypeParameters() ) );
				}

				// Next we need to generate a type inference 'method' node. This represents a method that we will codegen that
				// contains all of the operations on the render tree building. Calling a method to operate on the builder
				// will allow the C# compiler to perform type inference.
				var documentNode = (DocumentIntermediateNode)Ancestors[Ancestors.Count - 1];
				CreateTypeInferenceMethod( documentNode, node, receivesCascadingGenericTypes );
			}

			private bool TryFindGenericTypeNames( BoundAttributeDescriptor boundAttribute, out IReadOnlyList<string> typeParameters )
			{
				if ( boundAttribute == null )
				{
					// Will be null for attributes set on the component that don't match a declared component parameter
					typeParameters = null;
					return false;
				}

				if ( !boundAttribute.IsGenericTypedProperty() )
				{
					typeParameters = null;
					return false;
				}

				// Now we need to parse the type name and extract the generic parameters.
				// Two cases;
				// 1. name is a simple identifier like TItem
				// 2. name contains type parameters like Dictionary<string, TItem>
				typeParameters = _pass.TypeNameFeature.ParseTypeParameters( boundAttribute.TypeName );
				if ( typeParameters.Count == 0 )
				{
					typeParameters = new[] { boundAttribute.TypeName };
				}

				return true;
			}

			private string GetContent( ComponentTypeArgumentIntermediateNode node )
			{
				return string.Join( string.Empty, node.FindDescendantNodes<IntermediateToken>().Where( t => t.IsCSharp ).Select( t => t.Content ) );
			}

			private string GetContent( ComponentAttributeIntermediateNode node )
			{
				return string.Join( string.Empty, node.FindDescendantNodes<IntermediateToken>().Where( t => t.IsCSharp ).Select( t => t.Content ) );
			}

			private static bool ValidateTypeArguments( ComponentIntermediateNode node, Dictionary<string, Binding> bindings )
			{
				var missing = new List<BoundAttributeDescriptor>();
				foreach ( var binding in bindings )
				{
					if ( binding.Value.Node == null || string.IsNullOrWhiteSpace( binding.Value.Content ) )
					{
						missing.Add( binding.Value.Attribute );
					}
				}

				if ( missing.Count > 0 )
				{
					// We add our own error for this because its likely the user will see other errors due
					// to incorrect codegen without the types. Our errors message will pretty clearly indicate
					// what to do, whereas the other errors might be confusing.
					node.Diagnostics.Add( ComponentDiagnosticFactory.Create_GenericComponentMissingTypeArgument( node.Source, node, missing ) );
					return false;
				}

				return true;
			}

			private void RewriteTypeNames( TypeNameRewriter rewriter, ComponentIntermediateNode node )
			{
				// Rewrite the component type name
				node.TypeName = rewriter.Rewrite( node.TypeName );

				foreach ( var attribute in node.Attributes )
				{
					string globallyQualifiedTypeName = null;

					if ( attribute.TypeName != null )
					{
						globallyQualifiedTypeName = rewriter.Rewrite( attribute.TypeName );
						attribute.GloballyQualifiedTypeName = globallyQualifiedTypeName;
					}

					if ( attribute.BoundAttribute?.IsGenericTypedProperty() ?? false && attribute.TypeName != null )
					{
						// If we know the type name, then replace any generic type parameter inside it with
						// the known types.
						attribute.TypeName = globallyQualifiedTypeName;
					}
					else if ( attribute.TypeName == null && (attribute.BoundAttribute?.IsDelegateProperty() ?? false) )
					{
						// This is a weakly typed delegate, treat it as Action<object>
						attribute.TypeName = "System.Action<System.Object>";
					}
					else if ( attribute.TypeName == null && (attribute.BoundAttribute?.IsEventCallbackProperty() ?? false) )
					{
						// This is a weakly typed event-callback, treat it as EventCallback (non-generic)
						attribute.TypeName = ComponentsApi.EventCallback.FullTypeName;
					}
					else if ( attribute.TypeName == null )
					{
						// This is a weakly typed attribute, treat it as System.Object
						attribute.TypeName = "System.Object";
					}
				}

				foreach ( var childContent in node.ChildContents )
				{
					if ( childContent.BoundAttribute?.IsGenericTypedProperty() ?? false && childContent.TypeName != null )
					{
						// If we know the type name, then replace any generic type parameter inside it with
						// the known types.
						childContent.TypeName = rewriter.Rewrite( childContent.TypeName );
					}
					else if ( childContent.IsParameterized )
					{
						// This is a non-generic parameterized child content like RenderFragment<int>, leave it as is.
					}
					else
					{
						// This is a weakly typed child content, treat it as RenderFragment
						childContent.TypeName = ComponentsApi.RenderFragment.FullTypeName;
					}
				}
			}

			private void CreateTypeInferenceMethod( DocumentIntermediateNode documentNode, ComponentIntermediateNode node, List<CascadingGenericTypeParameter> receivesCascadingGenericTypes )
			{
				var @namespace = documentNode.FindPrimaryNamespace().Content;
				@namespace = string.IsNullOrEmpty( @namespace ) ? "__Blazor" : "__Blazor." + @namespace;
				@namespace += "." + documentNode.FindPrimaryClass().ClassName;

				var genericTypeConstraints = node.Component.BoundAttributes
					.Where( t => t.Metadata.ContainsKey( ComponentMetadata.Component.TypeParameterConstraintsKey ) )
					.Select( t => t.Metadata[ComponentMetadata.Component.TypeParameterConstraintsKey] );

				var typeInferenceNode = new ComponentTypeInferenceMethodIntermediateNode()
				{
					Component = node,

					// Method name is generated and guaranteed not to collide, since it's unique for each
					// component call site.
					MethodName = $"Create{CSharpIdentifier.SanitizeIdentifier( node.TagName )}_{_id++}",
					FullTypeName = @namespace + ".TypeInference",

					ReceivesCascadingGenericTypes = receivesCascadingGenericTypes,
					GenericTypeConstraints = genericTypeConstraints
				};

				node.TypeInferenceNode = typeInferenceNode;

				// Now we need to insert the type inference node into the tree.
				var namespaceNode = documentNode.Children
					.OfType<NamespaceDeclarationIntermediateNode>()
					.Where( n => n.Annotations.Contains( new KeyValuePair<object, object>( ComponentMetadata.Component.GenericTypedKey, bool.TrueString ) ) )
					.FirstOrDefault();
				if ( namespaceNode == null )
				{
					namespaceNode = new NamespaceDeclarationIntermediateNode()
					{
						Annotations =
						{
							{ ComponentMetadata.Component.GenericTypedKey, bool.TrueString },
						},
						Content = @namespace,
					};

					documentNode.Children.Add( namespaceNode );
				}

				var classNode = namespaceNode.Children
					.OfType<ClassDeclarationIntermediateNode>()
					.Where( n => n.ClassName == "TypeInference" )
					.FirstOrDefault();
				if ( classNode == null )
				{
					classNode = new ClassDeclarationIntermediateNode()
					{
						ClassName = "TypeInference",
						Modifiers =
						{
							"internal",
							"static",
						},
					};
					namespaceNode.Children.Add( classNode );
				}

				classNode.Children.Add( typeInferenceNode );
			}
		}

		private class Binding
		{
			public BoundAttributeDescriptor Attribute { get; set; }

			public string Content { get; set; }

			public ComponentTypeArgumentIntermediateNode Node { get; set; }

			public bool CoveredByLambda { get; set; }
		}
	}
}
