// 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
{
    /// <summary>
    /// Enables retrieval of <see cref="TagHelperBinding"/>'s.
    /// </summary>
    internal sealed class TagHelperBinder
    {
        private readonly Dictionary<string, HashSet<TagHelperDescriptor>> _registrations;
        private readonly string _tagHelperPrefix;

        /// <summary>
        /// Instantiates a new instance of the <see cref="TagHelperBinder"/>.
        /// </summary>
        /// <param name="tagHelperPrefix">The tag helper prefix being used by the document.</param>
        /// <param name="descriptors">The descriptors that the <see cref="TagHelperBinder"/> will pull from.</param>
        public TagHelperBinder(string tagHelperPrefix, IEnumerable<TagHelperDescriptor> descriptors)
        {
            _tagHelperPrefix = tagHelperPrefix;
            _registrations = new Dictionary<string, HashSet<TagHelperDescriptor>>(StringComparer.OrdinalIgnoreCase);

            // Populate our registrations
            foreach (var descriptor in descriptors)
            {
                Register(descriptor);
            }
        }

        /// <summary>
        /// Gets all tag helpers that match the given HTML tag criteria.
        /// </summary>
        /// <param name="tagName">The name of the HTML tag to match. Providing a '*' tag name
        /// retrieves catch-all <see cref="TagHelperDescriptor"/>s (descriptors that target every tag).</param>
        /// <param name="attributes">Attributes on the HTML tag.</param>
        /// <param name="parentTagName">The parent tag name of the given <paramref name="tagName"/> tag.</param>
        /// <param name="parentIsTagHelper">Is the parent tag of the given <paramref name="tagName"/> tag a tag helper.</param>
        /// <returns><see cref="TagHelperDescriptor"/>s that apply to the given HTML tag criteria.
        /// Will return <c>null</c> if no <see cref="TagHelperDescriptor"/>s are a match.</returns>
        public TagHelperBinding GetBinding(
            string tagName,
            IReadOnlyList<KeyValuePair<string, string>> attributes,
            string parentTagName,
            bool parentIsTagHelper)
        {
            if (!string.IsNullOrEmpty(_tagHelperPrefix) &&
                (tagName.Length <= _tagHelperPrefix.Length ||
                !tagName.StartsWith(_tagHelperPrefix, StringComparison.OrdinalIgnoreCase)))
            {
                // The tagName doesn't have the tag helper prefix, we can short circuit.
                return null;
            }

            IEnumerable<TagHelperDescriptor> descriptors;

            // Ensure there's a HashSet to use.
            if (!_registrations.TryGetValue(TagHelperMatchingConventions.ElementCatchAllName, out HashSet<TagHelperDescriptor> catchAllDescriptors))
            {
                descriptors = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
            }
            else
            {
                descriptors = catchAllDescriptors;
            }

            // If we have a tag name associated with the requested name, we need to combine matchingDescriptors
            // with all the catch-all descriptors.
            if (_registrations.TryGetValue(tagName, out HashSet<TagHelperDescriptor> matchingDescriptors))
            {
                descriptors = matchingDescriptors.Concat(descriptors);
            }

            var tagNameWithoutPrefix = _tagHelperPrefix != null ? new StringSegment(tagName, _tagHelperPrefix.Length) : tagName;
            StringSegment parentTagNameWithoutPrefix = parentTagName;
            if (_tagHelperPrefix != null && parentIsTagHelper)
            {
                parentTagNameWithoutPrefix = new StringSegment(parentTagName, _tagHelperPrefix.Length);
            }

            Dictionary<TagHelperDescriptor, IReadOnlyList<TagMatchingRuleDescriptor>> applicableDescriptorMappings = null;
            foreach (var descriptor in descriptors)
            {
                // We're avoiding desccriptor.TagMatchingRules.Where and applicableRules.Any() to avoid
                // Enumerator allocations on this hotpath
                List<TagMatchingRuleDescriptor> applicableRules = null;
                for (var i = 0; i < descriptor.TagMatchingRules.Count; i++)
                {
                    var rule = descriptor.TagMatchingRules[i];
                    if (TagHelperMatchingConventions.SatisfiesRule(tagNameWithoutPrefix, parentTagNameWithoutPrefix, attributes, rule))
                    {
                        if (applicableRules is null)
                        {
                            applicableRules = new List<TagMatchingRuleDescriptor>();
                        }

                        applicableRules.Add(rule);
                    }
                }

                if (applicableRules != null && applicableRules.Count > 0)
                {
                    if (applicableDescriptorMappings == null)
                    {
                        applicableDescriptorMappings = new Dictionary<TagHelperDescriptor, IReadOnlyList<TagMatchingRuleDescriptor>>();
                    }

                    applicableDescriptorMappings[descriptor] = applicableRules;
                }
            }

            if (applicableDescriptorMappings == null)
            {
                return null;
            }

            var tagHelperBinding = new TagHelperBinding(
                tagName,
                attributes,
                parentTagName,
                applicableDescriptorMappings,
                _tagHelperPrefix);

            return tagHelperBinding;
        }

        private void Register(TagHelperDescriptor descriptor)
        {
            var count = descriptor.TagMatchingRules.Count;
            for (var i = 0; i < count; i++)
            {
                var rule = descriptor.TagMatchingRules[i];
                var registrationKey =
                    string.Equals(rule.TagName, TagHelperMatchingConventions.ElementCatchAllName, StringComparison.Ordinal) ?
                    TagHelperMatchingConventions.ElementCatchAllName :
                    _tagHelperPrefix + rule.TagName;

                // Ensure there's a HashSet to add the descriptor to.
                if (!_registrations.TryGetValue(registrationKey, out HashSet<TagHelperDescriptor> descriptorSet))
                {
                    descriptorSet = new HashSet<TagHelperDescriptor>(TagHelperDescriptorComparer.Default);
                    _registrations[registrationKey] = descriptorSet;
                }

                descriptorSet.Add(descriptor);
            }
        }
    }
}
