// 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;

namespace Microsoft.AspNetCore.Razor.Language
{
	internal class DefaultBoundAttributeDescriptorBuilder : BoundAttributeDescriptorBuilder
	{
		private static readonly IReadOnlyDictionary<string, string> PrimitiveDisplayTypeNameLookups = new Dictionary<string, string>( StringComparer.Ordinal )
		{
			[typeof( byte ).FullName] = "byte",
			[typeof( sbyte ).FullName] = "sbyte",
			[typeof( int ).FullName] = "int",
			[typeof( uint ).FullName] = "uint",
			[typeof( short ).FullName] = "short",
			[typeof( ushort ).FullName] = "ushort",
			[typeof( long ).FullName] = "long",
			[typeof( ulong ).FullName] = "ulong",
			[typeof( float ).FullName] = "float",
			[typeof( double ).FullName] = "double",
			[typeof( char ).FullName] = "char",
			[typeof( bool ).FullName] = "bool",
			[typeof( object ).FullName] = "object",
			[typeof( string ).FullName] = "string",
			[typeof( decimal ).FullName] = "decimal",
		};

		private readonly DefaultTagHelperDescriptorBuilder _parent;
		private readonly string _kind;
		private readonly Dictionary<string, string> _metadata;
		private List<DefaultBoundAttributeParameterDescriptorBuilder> _attributeParameterBuilders;

		private RazorDiagnosticCollection _diagnostics;

		public DefaultBoundAttributeDescriptorBuilder( DefaultTagHelperDescriptorBuilder parent, string kind )
		{
			_parent = parent;
			_kind = kind;

			_metadata = new Dictionary<string, string>();
		}

		public override string Name { get; set; }

		public override string TypeName { get; set; }

		public override bool IsEnum { get; set; }

		public override bool IsDictionary { get; set; }

		public override string IndexerAttributeNamePrefix { get; set; }

		public override string IndexerValueTypeName { get; set; }

		public override string Documentation { get; set; }

		public override string DisplayName { get; set; }

		public override IDictionary<string, string> Metadata => _metadata;

		public override RazorDiagnosticCollection Diagnostics
		{
			get
			{
				if ( _diagnostics == null )
				{
					_diagnostics = new RazorDiagnosticCollection();
				}

				return _diagnostics;
			}
		}

		internal bool CaseSensitive => _parent.CaseSensitive;

		public override void BindAttributeParameter( Action<BoundAttributeParameterDescriptorBuilder> configure )
		{
			if ( configure == null )
			{
				throw new ArgumentNullException( nameof( configure ) );
			}

			EnsureAttributeParameterBuilders();

			var builder = new DefaultBoundAttributeParameterDescriptorBuilder( this, _kind );
			configure( builder );
			_attributeParameterBuilders.Add( builder );
		}

		public BoundAttributeDescriptor Build()
		{
			var diagnostics = Validate();
			if ( _diagnostics != null )
			{
				diagnostics ??= new();
				diagnostics.UnionWith( _diagnostics );
			}

			var parameters = Array.Empty<BoundAttributeParameterDescriptor>();
			if ( _attributeParameterBuilders != null )
			{
				// Attribute parameters are case-sensitive.
				var parameterset = new HashSet<BoundAttributeParameterDescriptor>( BoundAttributeParameterDescriptorComparer.Default );
				for ( var i = 0; i < _attributeParameterBuilders.Count; i++ )
				{
					parameterset.Add( _attributeParameterBuilders[i].Build() );
				}

				parameters = parameterset.ToArray();
			}

			var descriptor = new DefaultBoundAttributeDescriptor(
				_kind,
				Name,
				TypeName,
				IsEnum,
				IsDictionary,
				IndexerAttributeNamePrefix,
				IndexerValueTypeName,
				Documentation,
				GetDisplayName(),
				CaseSensitive,
				parameters,
				new Dictionary<string, string>( Metadata ),
				diagnostics?.ToArray() ?? Array.Empty<RazorDiagnostic>() )
			{
				IsEditorRequired = IsEditorRequired,
			};

			return descriptor;
		}

		private string GetDisplayName()
		{
			if ( DisplayName != null )
			{
				return DisplayName;
			}

			var parentTypeName = _parent.GetTypeName();
			var propertyName = this.GetPropertyName();

			if ( TypeName != null &&
				propertyName != null &&
				parentTypeName != null )
			{
				// This looks like a normal c# property, so lets compute a display name based on that.
				if ( !PrimitiveDisplayTypeNameLookups.TryGetValue( TypeName, out var simpleTypeName ) )
				{
					simpleTypeName = TypeName;
				}

				return $"{simpleTypeName} {parentTypeName}.{propertyName}";
			}

			return Name;
		}

		private HashSet<RazorDiagnostic> Validate()
		{
			// data-* attributes are explicitly not implemented by user agents and are not intended for use on
			// the server; therefore it's invalid for TagHelpers to bind to them.
			const string DataDashPrefix = "data-";
			var isDirectiveAttribute = this.IsDirectiveAttribute();

			HashSet<RazorDiagnostic> diagnostics = null;

			if ( string.IsNullOrWhiteSpace( Name ) )
			{
				if ( IndexerAttributeNamePrefix == null )
				{
					var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidBoundAttributeNullOrWhitespace(
						_parent.GetDisplayName(),
						GetDisplayName() );

					diagnostics ??= new();
					diagnostics.Add( diagnostic );
				}
			}
			else
			{
				if ( Name.StartsWith( DataDashPrefix, StringComparison.OrdinalIgnoreCase ) )
				{
					var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidBoundAttributeNameStartsWith(
						_parent.GetDisplayName(),
						GetDisplayName(),
						Name );

					diagnostics ??= new();
					diagnostics.Add( diagnostic );
				}

				StringSegment name = Name;
				if ( isDirectiveAttribute && name.StartsWith( "@", StringComparison.Ordinal ) )
				{
					name = name.Subsegment( 1 );
				}
				else if ( isDirectiveAttribute )
				{
					var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidBoundDirectiveAttributeName(
							_parent.GetDisplayName(),
							GetDisplayName(),
							Name );

					diagnostics ??= new();
					diagnostics.Add( diagnostic );
				}

				for ( var i = 0; i < name.Length; i++ )
				{
					var character = name[i];
					if ( char.IsWhiteSpace( character ) || HtmlConventions.IsInvalidNonWhitespaceHtmlCharacters( character ) )
					{
						var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidBoundAttributeName(
							_parent.GetDisplayName(),
							GetDisplayName(),
							name.Value,
							character );

						diagnostics ??= new();
						diagnostics.Add( diagnostic );
					}
				}
			}

			if ( IndexerAttributeNamePrefix != null )
			{
				if ( IndexerAttributeNamePrefix.StartsWith( DataDashPrefix, StringComparison.OrdinalIgnoreCase ) )
				{
					var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidBoundAttributePrefixStartsWith(
						_parent.GetDisplayName(),
						GetDisplayName(),
						IndexerAttributeNamePrefix );

					diagnostics ??= new();
					diagnostics.Add( diagnostic );
				}
				else if ( IndexerAttributeNamePrefix.Length > 0 && string.IsNullOrWhiteSpace( IndexerAttributeNamePrefix ) )
				{
					var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidBoundAttributeNullOrWhitespace(
						_parent.GetDisplayName(),
						GetDisplayName() );

					diagnostics ??= new();
					diagnostics.Add( diagnostic );
				}
				else
				{
					StringSegment indexerPrefix = IndexerAttributeNamePrefix;
					if ( isDirectiveAttribute && indexerPrefix.StartsWith( "@", StringComparison.Ordinal ) )
					{
						indexerPrefix = indexerPrefix.Subsegment( 1 );
					}
					else if ( isDirectiveAttribute )
					{
						var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidBoundDirectiveAttributePrefix(
							_parent.GetDisplayName(),
							GetDisplayName(),
							indexerPrefix.Value );

						diagnostics ??= new();
						diagnostics.Add( diagnostic );
					}

					for ( var i = 0; i < indexerPrefix.Length; i++ )
					{
						var character = indexerPrefix[i];
						if ( char.IsWhiteSpace( character ) || HtmlConventions.IsInvalidNonWhitespaceHtmlCharacters( character ) )
						{
							var diagnostic = RazorDiagnosticFactory.CreateTagHelper_InvalidBoundAttributePrefix(
								_parent.GetDisplayName(),
								GetDisplayName(),
								indexerPrefix.Value,
								character );

							diagnostics ??= new();
							diagnostics.Add( diagnostic );
						}
					}
				}
			}

			return diagnostics;
		}

		private void EnsureAttributeParameterBuilders()
		{
			if ( _attributeParameterBuilders == null )
			{
				_attributeParameterBuilders = new List<DefaultBoundAttributeParameterDescriptorBuilder>();
			}
		}
	}
}
