// 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.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.AspNetCore.Razor.Language.Legacy;
using Microsoft.AspNetCore.Razor.Language.Syntax;

namespace Microsoft.AspNetCore.Razor.Language
{
	internal class DefaultRazorTagHelperBinderPhase : RazorEnginePhaseBase, IRazorTagHelperBinderPhase
	{
		protected override void ExecuteCore( RazorCodeDocument codeDocument )
		{
			var syntaxTree = codeDocument.GetSyntaxTree();
			ThrowForMissingDocumentDependency( syntaxTree );

			var descriptors = codeDocument.GetTagHelpers();
			if ( descriptors == null )
			{
				var feature = Engine.GetFeature<ITagHelperFeature>();
				if ( feature == null )
				{
					// No feature, nothing to do.
					return;
				}

				descriptors = feature.GetDescriptors();
			}

			var parserOptions = codeDocument.GetParserOptions();

			// We need to find directives in all of the *imports* as well as in the main razor file
			//
			// The imports come logically before the main razor file and are in the order they
			// should be processed.
			DirectiveVisitor visitor;
			if ( FileKinds.IsComponent( codeDocument.GetFileKind() ) &&
				(parserOptions == null || parserOptions.FeatureFlags.AllowComponentFileKind) )
			{
				codeDocument.TryComputeNamespace( fallbackToRootNamespace: true, out var currentNamespace );
				visitor = new ComponentDirectiveVisitor( codeDocument.Source.FilePath, descriptors, currentNamespace );
			}
			else
			{
				visitor = new TagHelperDirectiveVisitor( descriptors );
			}
			var imports = codeDocument.GetImportSyntaxTrees();
			if ( imports != null )
			{
				for ( var i = 0; i < imports.Count; i++ )
				{
					var import = imports[i];
					visitor.Visit( import );
				}
			}

			visitor.Visit( syntaxTree );

			// This will always be null for a component document.
			var tagHelperPrefix = visitor.TagHelperPrefix;

			descriptors = visitor.Matches.ToArray();

			var context = TagHelperDocumentContext.Create( tagHelperPrefix, descriptors );
			codeDocument.SetTagHelperContext( context );

			if ( descriptors.Count == 0 )
			{
				// No descriptors, no-op.
				return;
			}

			var rewrittenSyntaxTree = TagHelperParseTreeRewriter.Rewrite( syntaxTree, tagHelperPrefix, descriptors );

			codeDocument.SetSyntaxTree( rewrittenSyntaxTree );
		}

		private static bool MatchesDirective( TagHelperDescriptor descriptor, string typePattern, string assemblyName )
		{
			if ( !string.Equals( descriptor.AssemblyName, assemblyName, StringComparison.Ordinal ) )
			{
				return false;
			}

			if ( typePattern.EndsWith( "*", StringComparison.Ordinal ) )
			{
				if ( typePattern.Length == 1 )
				{
					// TypePattern is "*".
					return true;
				}

				return new StringSegment( descriptor.Name ).StartsWith( new StringSegment( typePattern, 0, typePattern.Length - 1 ), StringComparison.Ordinal );
			}

			return string.Equals( descriptor.Name, typePattern, StringComparison.Ordinal );
		}

		internal abstract class DirectiveVisitor : SyntaxWalker
		{
			public abstract HashSet<TagHelperDescriptor> Matches { get; }

			public abstract string TagHelperPrefix { get; }

			public abstract void Visit( RazorSyntaxTree tree );
		}

		internal sealed class TagHelperDirectiveVisitor : DirectiveVisitor
		{
			private readonly List<TagHelperDescriptor> _tagHelpers;
			private string _tagHelperPrefix;

			public TagHelperDirectiveVisitor( IReadOnlyList<TagHelperDescriptor> tagHelpers )
			{
				// We don't want to consider components in a view document.
				_tagHelpers = new();
				for ( var i = 0; i < tagHelpers.Count; i++ )
				{
					var tagHelper = tagHelpers[i];
					if ( !tagHelper.IsAnyComponentDocumentTagHelper() )
					{
						_tagHelpers.Add( tagHelper );
					}
				}
			}

			public override string TagHelperPrefix => _tagHelperPrefix;

			public override HashSet<TagHelperDescriptor> Matches { get; } = new HashSet<TagHelperDescriptor>();

			public override void Visit( RazorSyntaxTree tree )
			{
				Visit( tree.Root );
			}

			public override void VisitRazorDirective( RazorDirectiveSyntax node )
			{
				var descendantLiterals = node.DescendantNodes();
				foreach ( var child in descendantLiterals )
				{
					if ( !(child is CSharpStatementLiteralSyntax literal) )
					{
						continue;
					}

					var context = literal.GetSpanContext();
					if ( context == null )
					{
						// We can't find a chunk generator.
						continue;
					}
					else if ( context.ChunkGenerator is AddTagHelperChunkGenerator addTagHelper )
					{
						if ( addTagHelper.AssemblyName == null )
						{
							// Skip this one, it's an error
							continue;
						}

						if ( !AssemblyContainsTagHelpers( addTagHelper.AssemblyName, _tagHelpers ) )
						{
							// No tag helpers in the assembly.
							continue;
						}

						for ( var i = 0; i < _tagHelpers.Count; i++ )
						{
							var tagHelper = _tagHelpers[i];
							if ( MatchesDirective( tagHelper, addTagHelper.TypePattern, addTagHelper.AssemblyName ) )
							{
								Matches.Add( tagHelper );
							}
						}
					}
					else if ( context.ChunkGenerator is RemoveTagHelperChunkGenerator removeTagHelper )
					{
						if ( removeTagHelper.AssemblyName == null )
						{
							// Skip this one, it's an error
							continue;
						}


						if ( !AssemblyContainsTagHelpers( removeTagHelper.AssemblyName, _tagHelpers ) )
						{
							// No tag helpers in the assembly.
							continue;
						}

						for ( var i = 0; i < _tagHelpers.Count; i++ )
						{
							var tagHelper = _tagHelpers[i];
							if ( MatchesDirective( tagHelper, removeTagHelper.TypePattern, removeTagHelper.AssemblyName ) )
							{
								Matches.Remove( tagHelper );
							}
						}
					}
					else if ( context.ChunkGenerator is TagHelperPrefixDirectiveChunkGenerator tagHelperPrefix )
					{
						if ( !string.IsNullOrEmpty( tagHelperPrefix.DirectiveText ) )
						{
							// We only expect to see a single one of these per file, but that's enforced at another level.
							_tagHelperPrefix = tagHelperPrefix.DirectiveText;
						}
					}
				}
			}

			private bool AssemblyContainsTagHelpers( string assemblyName, IReadOnlyList<TagHelperDescriptor> tagHelpers )
			{
				for ( var i = 0; i < tagHelpers.Count; i++ )
				{
					if ( string.Equals( tagHelpers[i].AssemblyName, assemblyName, StringComparison.Ordinal ) )
					{
						return true;
					}
				}

				return false;
			}
		}

		internal sealed class ComponentDirectiveVisitor : DirectiveVisitor
		{
			private readonly List<TagHelperDescriptor> _notFullyQualifiedComponents;
			private readonly string _filePath;
			private RazorSourceDocument _source;

			public ComponentDirectiveVisitor( string filePath, IReadOnlyList<TagHelperDescriptor> tagHelpers, string currentNamespace )
			{
				_filePath = filePath;

				for ( var i = 0; i < tagHelpers.Count; i++ )
				{
					var tagHelper = tagHelpers[i];
					// We don't want to consider non-component tag helpers in a component document.
					if ( !tagHelper.IsAnyComponentDocumentTagHelper() || IsTagHelperFromMangledClass( tagHelper ) )
					{
						continue;
					}

					if ( tagHelper.IsComponentFullyQualifiedNameMatch() )
					{
						// If the component descriptor matches for a fully qualified name, using directives shouldn't matter.
						Matches.Add( tagHelper );
						continue;
					}

					_notFullyQualifiedComponents ??= new();
					_notFullyQualifiedComponents.Add( tagHelper );

					if ( currentNamespace is null )
					{
						continue;
					}

					if ( tagHelper.IsChildContentTagHelper() )
					{
						// If this is a child content tag helper, we want to add it if it's original type is in scope.
						// E.g, if the type name is `Test.MyComponent.ChildContent`, we want to add it if `Test.MyComponent` is in scope.
						TrySplitNamespaceAndType( tagHelper, out var typeNamespace );
						if ( !typeNamespace.IsEmpty && IsTypeInScope( typeNamespace, currentNamespace ) )
						{
							Matches.Add( tagHelper );
						}
					}
					else if ( IsTypeInScope( tagHelper, currentNamespace ) )
					{
						// Also, if the type is already in scope of the document's namespace, using isn't necessary.
						Matches.Add( tagHelper );
					}
				}
			}

			public override HashSet<TagHelperDescriptor> Matches { get; } = new HashSet<TagHelperDescriptor>();

			// There is no support for tag helper prefix in component documents.
			public override string TagHelperPrefix => null;

			public override void Visit( RazorSyntaxTree tree )
			{
				_source = tree.Source;
				Visit( tree.Root );
			}

			public override void VisitRazorDirective( RazorDirectiveSyntax node )
			{
				var descendantLiterals = node.DescendantNodes();
				foreach ( var child in descendantLiterals )
				{
					if ( child is not CSharpStatementLiteralSyntax literal )
					{
						continue;
					}

					var context = literal.GetSpanContext();
					if ( context == null )
					{
						// We can't find a chunk generator.
						continue;
					}
					else if ( context.ChunkGenerator is AddTagHelperChunkGenerator addTagHelper )
					{
						// Make sure this node exists in the file we're parsing and not in its imports.
						if ( _filePath.Equals( _source.FilePath, StringComparison.Ordinal ) )
						{
							addTagHelper.Diagnostics.Add(
								ComponentDiagnosticFactory.Create_UnsupportedTagHelperDirective( node.GetSourceSpan( _source ) ) );
						}
					}
					else if ( context.ChunkGenerator is RemoveTagHelperChunkGenerator removeTagHelper )
					{
						// Make sure this node exists in the file we're parsing and not in its imports.
						if ( _filePath.Equals( _source.FilePath, StringComparison.Ordinal ) )
						{
							removeTagHelper.Diagnostics.Add(
								ComponentDiagnosticFactory.Create_UnsupportedTagHelperDirective( node.GetSourceSpan( _source ) ) );
						}
					}
					else if ( context.ChunkGenerator is TagHelperPrefixDirectiveChunkGenerator tagHelperPrefix )
					{
						// Make sure this node exists in the file we're parsing and not in its imports.
						if ( _filePath.Equals( _source.FilePath, StringComparison.Ordinal ) )
						{
							tagHelperPrefix.Diagnostics.Add(
								ComponentDiagnosticFactory.Create_UnsupportedTagHelperDirective( node.GetSourceSpan( _source ) ) );
						}
					}
					else if ( context.ChunkGenerator is AddImportChunkGenerator usingStatement && !usingStatement.IsStatic )
					{
						// Get the namespace from the using statement.
						var @namespace = usingStatement.ParsedNamespace;
						if ( @namespace.IndexOf( '=' ) != -1 )
						{
							// We don't support usings with alias.
							continue;
						}

						for ( var i = 0; _notFullyQualifiedComponents is not null && i < _notFullyQualifiedComponents.Count; i++ )
						{
							var tagHelper = _notFullyQualifiedComponents[i];
							Debug.Assert( !tagHelper.IsComponentFullyQualifiedNameMatch(), "We've already processed these." );

							if ( tagHelper.IsChildContentTagHelper() )
							{
								// If this is a child content tag helper, we want to add it if it's original type is in scope of the given namespace.
								// E.g, if the type name is `Test.MyComponent.ChildContent`, we want to add it if `Test.MyComponent` is in this namespace.
								TrySplitNamespaceAndType( tagHelper, out var typeName );
								if ( !typeName.IsEmpty && IsTypeInNamespace( typeName, @namespace ) )
								{
									Matches.Add( tagHelper );
								}
							}
							else if ( IsTypeInNamespace( tagHelper, @namespace ) )
							{
								// If the type is at the top-level or if the type's namespace matches the using's namespace, add it.
								Matches.Add( tagHelper );
							}
						}
					}
				}
			}

			internal static bool IsTypeInNamespace( StringSegment typeName, string @namespace )
			{
				if ( !TrySplitNamespaceAndType( typeName, out var typeNamespace, out var _ ) || typeNamespace.IsEmpty )
				{
					// Either the typeName is not the full type name or this type is at the top level.
					return true;
				}

				return typeNamespace.Equals( @namespace, StringComparison.Ordinal );
			}

			internal static bool IsTypeInNamespace( TagHelperDescriptor tagHelper, string @namespace )
			{
				if ( !TrySplitNamespaceAndType( tagHelper, out var typeNamespace ) || typeNamespace.IsEmpty )
				{
					// Either the typeName is not the full type name or this type is at the top level.
					return true;
				}

				return typeNamespace.Equals( @namespace, StringComparison.Ordinal );
			}

			// Check if the given type is already in scope given the namespace of the current document.
			// E.g,
			// If the namespace of the document is `MyComponents.Components.Shared`,
			// then the types `MyComponents.FooComponent`, `MyComponents.Components.BarComponent`, `MyComponents.Components.Shared.BazComponent` are all in scope.
			// Whereas `MyComponents.SomethingElse.OtherComponent` is not in scope.
			internal static bool IsTypeInScope( TagHelperDescriptor descriptor, string currentNamespace )
			{
				if ( !TrySplitNamespaceAndType( descriptor, out var typeNamespace, out _ ) || typeNamespace.IsEmpty )
				{
					// Either the typeName is not the full type name or this type is at the top level.
					return true;
				}

				return IsTypeInScopeCore( currentNamespace, typeNamespace );
			}

			// Check if the given type is already in scope given the namespace of the current document.
			// E.g,
			// If the namespace of the document is `MyComponents.Components.Shared`,
			// then the types `MyComponents.FooComponent`, `MyComponents.Components.BarComponent`, `MyComponents.Components.Shared.BazComponent` are all in scope.
			// Whereas `MyComponents.SomethingElse.OtherComponent` is not in scope.
			internal static bool IsTypeInScope( StringSegment typeName, string currentNamespace )
			{
				if ( !TrySplitNamespaceAndType( typeName, out var typeNamespace, out _ ) || typeNamespace.IsEmpty )
				{
					// Either the typeName is not the full type name or this type is at the top level.
					return true;
				}

				return IsTypeInScopeCore( currentNamespace, typeNamespace );
			}

			private static bool IsTypeInScopeCore( string currentNamespace, StringSegment typeNamespace )
			{
				if ( !new StringSegment( currentNamespace ).StartsWith( typeNamespace, StringComparison.Ordinal ) )
				{
					// typeName: MyComponents.Shared.SomeCoolNamespace
					// currentNamespace: MyComponents.Shared
					return false;
				}

				if ( typeNamespace.Length > currentNamespace.Length && typeNamespace[currentNamespace.Length] != '.' )
				{
					// typeName: MyComponents.SharedFoo
					// currentNamespace: MyComponent.Shared
					return false;
				}

				return true;
			}

			// We need to filter out the duplicate tag helper descriptors that come from the
			// open file in the editor. We mangle the class name for its generated code, so using that here to filter these out.
			internal static bool IsTagHelperFromMangledClass( TagHelperDescriptor tagHelper )
			{
				StringSegment className;
				if ( tagHelper.IsChildContentTagHelper() )
				{
					// If this is a child content tag helper, we want to look at it's original type.
					// E.g, if the type name is `Test.__generated__MyComponent.ChildContent`, we want to look at `Test.__generated__MyComponent`.
					TrySplitNamespaceAndType( tagHelper, out var typeNamespace );
					return TrySplitNamespaceAndType( typeNamespace, out var _, out className )
						&& ComponentMetadata.IsMangledClass( className );
				}

				return TrySplitNamespaceAndType( tagHelper, out var _, out className ) &&
					ComponentMetadata.IsMangledClass( className );
			}

			internal static bool TrySplitNamespaceAndType( TagHelperDescriptor tagHelperDescriptor, out StringSegment @namespace )
				=> TrySplitNamespaceAndType( tagHelperDescriptor, out @namespace, out _ );

			internal static bool TrySplitNamespaceAndType( TagHelperDescriptor tagHelperDescriptor, out StringSegment @namespace, out StringSegment typeName )
			{
				if ( tagHelperDescriptor.ParsedTypeInfo is { } value )
				{
					@namespace = value.Namespace;
					typeName = value.TypeName;
					return value.Success;
				}

				var success = TrySplitNamespaceAndType( tagHelperDescriptor.GetTypeName(), out @namespace, out typeName );
				tagHelperDescriptor.ParsedTypeInfo = new( success, @namespace, typeName );
				return success;
			}

			// Internal for testing.
			internal static bool TrySplitNamespaceAndType( StringSegment fullTypeName, out StringSegment @namespace, out StringSegment typeName )
			{
				@namespace = StringSegment.Empty;
				typeName = StringSegment.Empty;

				if ( fullTypeName.IsEmpty )
				{
					return false;
				}

				var nestingLevel = 0;
				var splitLocation = -1;
				for ( var i = fullTypeName.Length - 1; i >= 0; i-- )
				{
					var c = fullTypeName[i];
					if ( c == Type.Delimiter && nestingLevel == 0 )
					{
						splitLocation = i;
						break;
					}
					else if ( c == '>' )
					{
						nestingLevel++;
					}
					else if ( c == '<' )
					{
						nestingLevel--;
					}
				}

				if ( splitLocation == -1 )
				{
					typeName = fullTypeName;
					return true;
				}

				@namespace = fullTypeName.Subsegment( 0, splitLocation );

				var typeNameStartLocation = splitLocation + 1;
				if ( typeNameStartLocation < fullTypeName.Length )
				{
					typeName = fullTypeName.Subsegment( typeNameStartLocation, fullTypeName.Length - typeNameStartLocation );
				}

				return true;
			}
		}
	}
}
