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

namespace Microsoft.AspNetCore.Razor.Language
{
	internal static class TagHelperMatchingConventions
	{
		public const string ElementCatchAllName = "*";

		public const char ElementOptOutCharacter = '!';

		public static bool SatisfiesRule(
			StringSegment tagNameWithoutPrefix,
			StringSegment parentTagNameWithoutPrefix,
			IReadOnlyList<KeyValuePair<string, string>> tagAttributes,
			TagMatchingRuleDescriptor rule )
		{
			var satisfiesTagName = SatisfiesTagName( tagNameWithoutPrefix, rule );
			if ( !satisfiesTagName )
			{
				return false;
			}

			var satisfiesParentTag = SatisfiesParentTag( parentTagNameWithoutPrefix, rule );
			if ( !satisfiesParentTag )
			{
				return false;
			}

			var satisfiesAttributes = SatisfiesAttributes( tagAttributes, rule );
			if ( !satisfiesAttributes )
			{
				return false;
			}

			return true;
		}

		public static bool SatisfiesTagName( StringSegment tagNameWithoutPrefix, TagMatchingRuleDescriptor rule )
		{
			if ( StringSegment.IsNullOrEmpty( tagNameWithoutPrefix ) )
			{
				return false;
			}

			if ( tagNameWithoutPrefix[0] == ElementOptOutCharacter )
			{
				// TagHelpers can never satisfy tag names that are prefixed with the opt-out character.
				return false;
			}

			if ( rule.TagName != ElementCatchAllName &&
				rule.TagName != null &&
				!tagNameWithoutPrefix.Equals( rule.TagName, rule.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase ) )
			{
				return false;
			}

			return true;
		}

		public static bool SatisfiesParentTag( StringSegment parentTagNameWithoutPrefix, TagMatchingRuleDescriptor rule )
		{
			if ( rule.ParentTag != null &&
				!parentTagNameWithoutPrefix.Equals( rule.ParentTag, rule.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase ) )
			{
				return false;
			}

			return true;
		}

		public static bool SatisfiesAttributes( IReadOnlyList<KeyValuePair<string, string>> tagAttributes, TagMatchingRuleDescriptor rule )
		{
			if ( !rule.Attributes.All(
				static ( requiredAttribute, tagAttributes ) => tagAttributes.Any(
					static ( attribute, requiredAttribute ) => SatisfiesRequiredAttribute( attribute.Key, attribute.Value, requiredAttribute ),
					requiredAttribute ),
				tagAttributes ) )
			{
				return false;
			}

			return true;
		}

		public static bool CanSatisfyBoundAttribute( string name, BoundAttributeDescriptor descriptor )
		{
			return SatisfiesBoundAttributeName( name, descriptor ) ||
				SatisfiesBoundAttributeIndexer( name, descriptor ) ||
				GetSatifyingBoundAttributeWithParameter( name, descriptor, descriptor.BoundAttributeParameters ) is not null;
		}

		private static BoundAttributeParameterDescriptor GetSatifyingBoundAttributeWithParameter( string name, BoundAttributeDescriptor descriptor, IReadOnlyList<BoundAttributeParameterDescriptor> boundAttributeParameters )
		{
			var count = boundAttributeParameters.Count;
			for ( var i = 0; i < count; i++ )
			{
				if ( SatisfiesBoundAttributeWithParameter( name, descriptor, boundAttributeParameters[i] ) )
				{
					return boundAttributeParameters[i];
				}
			}

			return null;
		}

		public static bool SatisfiesBoundAttributeIndexer( StringSegment name, BoundAttributeDescriptor descriptor )
		{
			return descriptor.IndexerNamePrefix != null &&
				!SatisfiesBoundAttributeName( name, descriptor ) &&
				name.StartsWith( descriptor.IndexerNamePrefix, descriptor.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase );
		}

		public static bool SatisfiesBoundAttributeWithParameter( string name, BoundAttributeDescriptor parent, BoundAttributeParameterDescriptor descriptor )
		{
			if ( TryGetBoundAttributeParameter( name, out var attributeName, out var parameterName ) )
			{
				var satisfiesBoundAttributeName = SatisfiesBoundAttributeName( attributeName, parent );
				var satisfiesBoundAttributeIndexer = SatisfiesBoundAttributeIndexer( attributeName, parent );
				var matchesParameter = parameterName.Equals( descriptor.Name, descriptor.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase );
				return (satisfiesBoundAttributeName || satisfiesBoundAttributeIndexer) && matchesParameter;
			}

			return false;
		}

		public static bool TryGetBoundAttributeParameter( string fullAttributeName, out StringSegment boundAttributeName )
		{
			boundAttributeName = default;

			if ( string.IsNullOrEmpty( fullAttributeName ) )
			{
				return false;
			}

			var index = fullAttributeName.IndexOf( ':' );
			if ( index == -1 )
			{
				return false;
			}

			boundAttributeName = new StringSegment( fullAttributeName, 0, index );
			return true;
		}

		public static bool TryGetBoundAttributeParameter( string fullAttributeName, out StringSegment boundAttributeName, out StringSegment parameterName )
		{
			boundAttributeName = default;
			parameterName = default;

			if ( string.IsNullOrEmpty( fullAttributeName ) )
			{
				return false;
			}

			var index = fullAttributeName.IndexOf( ':' );
			if ( index == -1 )
			{
				return false;
			}

			boundAttributeName = new StringSegment( fullAttributeName, 0, index );
			parameterName = new StringSegment( fullAttributeName, index + 1 );
			return true;
		}

		public static bool TryGetFirstBoundAttributeMatch(
			string name,
			TagHelperDescriptor descriptor,
			out BoundAttributeDescriptor boundAttribute,
			out bool indexerMatch,
			out bool parameterMatch,
			out BoundAttributeParameterDescriptor boundAttributeParameter )
		{
			indexerMatch = false;
			parameterMatch = false;
			boundAttribute = null;
			boundAttributeParameter = null;

			if ( string.IsNullOrEmpty( name ) || descriptor == null )
			{
				return false;
			}

			// First, check if we have a bound attribute descriptor that matches the parameter if it exists.
			foreach ( var attribute in descriptor.BoundAttributes )
			{
				boundAttributeParameter = GetSatifyingBoundAttributeWithParameter( name, attribute, attribute.BoundAttributeParameters );

				if ( boundAttributeParameter != null )
				{
					boundAttribute = attribute;
					indexerMatch = SatisfiesBoundAttributeIndexer( name, attribute );
					parameterMatch = true;
					return true;
				}
			}

			// If we reach here, either the attribute name doesn't contain a parameter portion or
			// the specified parameter isn't supported by any of the BoundAttributeDescriptors.
			foreach ( var attribute in descriptor.BoundAttributes )
			{
				if ( CanSatisfyBoundAttribute( name, attribute ) )
				{
					boundAttribute = attribute;
					indexerMatch = SatisfiesBoundAttributeIndexer( name, attribute );
					return true;
				}
			}

			// No matches found.
			return false;
		}

		private static bool SatisfiesBoundAttributeName( StringSegment name, BoundAttributeDescriptor descriptor )
		{
			return name.Equals( descriptor.Name, descriptor.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase );
		}

		// Internal for testing
		internal static bool SatisfiesRequiredAttribute( string attributeName, string attributeValue, RequiredAttributeDescriptor descriptor )
		{
			var nameMatches = false;
			if ( descriptor.NameComparison == RequiredAttributeDescriptor.NameComparisonMode.FullMatch )
			{
				nameMatches = string.Equals( descriptor.Name, attributeName, descriptor.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase );
			}
			else if ( descriptor.NameComparison == RequiredAttributeDescriptor.NameComparisonMode.PrefixMatch )
			{
				// attributeName cannot equal the Name if comparing as a PrefixMatch.
				nameMatches = attributeName.Length != descriptor.Name.Length &&
					attributeName.StartsWith( descriptor.Name, descriptor.CaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase );
			}
			else
			{
				Debug.Assert( false, "Unknown name comparison." );
			}

			if ( !nameMatches )
			{
				return false;
			}

			switch ( descriptor.ValueComparison )
			{
				case RequiredAttributeDescriptor.ValueComparisonMode.None:
					return true;
				case RequiredAttributeDescriptor.ValueComparisonMode.PrefixMatch: // Value starts with
					return attributeValue.StartsWith( descriptor.Value, StringComparison.Ordinal );
				case RequiredAttributeDescriptor.ValueComparisonMode.SuffixMatch: // Value ends with
					return attributeValue.EndsWith( descriptor.Value, StringComparison.Ordinal );
				case RequiredAttributeDescriptor.ValueComparisonMode.FullMatch: // Value equals
					return string.Equals( attributeValue, descriptor.Value, StringComparison.Ordinal );
				default:
					Debug.Assert( false, "Unknown value comparison." );
					return false;
			}
		}
	}
}
