// 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 System.Text;
using Microsoft.AspNetCore.Razor.Language.Syntax;

namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
    internal static class TagHelperParseTreeRewriter
    {
        public static RazorSyntaxTree Rewrite(RazorSyntaxTree syntaxTree, string tagHelperPrefix, IEnumerable<TagHelperDescriptor> descriptors)
        {
            var errorSink = new ErrorSink();

            var rewriter = new Rewriter(
                syntaxTree.Source,
                tagHelperPrefix,
                descriptors,
                syntaxTree.Options.FeatureFlags,
                errorSink);

            var rewritten = rewriter.Visit(syntaxTree.Root);

            var errorList = new List<RazorDiagnostic>();
            errorList.AddRange(errorSink.Errors);
            errorList.AddRange(descriptors.SelectMany(d => d.GetAllDiagnostics()));

            var diagnostics = CombineErrors(syntaxTree.Diagnostics, errorList).OrderBy(error => error.Span.AbsoluteIndex);

            var newSyntaxTree = RazorSyntaxTree.Create(rewritten, syntaxTree.Source, diagnostics, syntaxTree.Options);
            return newSyntaxTree;
        }

        private static IReadOnlyList<RazorDiagnostic> CombineErrors(IReadOnlyList<RazorDiagnostic> errors1, IReadOnlyList<RazorDiagnostic> errors2)
        {
            var combinedErrors = new List<RazorDiagnostic>(errors1.Count + errors2.Count);
            combinedErrors.AddRange(errors1);
            combinedErrors.AddRange(errors2);

            return combinedErrors;
        }

        // Internal for testing.
        internal sealed class Rewriter : SyntaxRewriter
        {
            // Internal for testing.
            // Null characters are invalid markup for HTML attribute values.
            internal const char InvalidAttributeValueMarker = '\0';

            private readonly RazorSourceDocument _source;
            private readonly string _tagHelperPrefix;
            private readonly List<KeyValuePair<string, string>> _htmlAttributeTracker;
            private readonly StringBuilder _attributeValueBuilder;
            private readonly TagHelperBinder _tagHelperBinder;
            private readonly Stack<TagTracker> _trackerStack;
            private readonly ErrorSink _errorSink;
            private readonly RazorParserFeatureFlags _featureFlags;

            public Rewriter(
                RazorSourceDocument source,
                string tagHelperPrefix,
                IEnumerable<TagHelperDescriptor> descriptors,
                RazorParserFeatureFlags featureFlags,
                ErrorSink errorSink)
            {
                _source = source;
                _tagHelperPrefix = tagHelperPrefix;
                _tagHelperBinder = new TagHelperBinder(tagHelperPrefix, descriptors);
                _trackerStack = new Stack<TagTracker>();
                _attributeValueBuilder = new StringBuilder();
                _htmlAttributeTracker = new List<KeyValuePair<string, string>>();
                _featureFlags = featureFlags;
                _errorSink = errorSink;
            }

            private TagTracker CurrentTracker => _trackerStack.Count > 0 ? _trackerStack.Peek() : null;

            private string CurrentParentTagName => CurrentTracker?.TagName;

            private bool CurrentParentIsTagHelper => CurrentTracker?.IsTagHelper ?? false;

            private TagHelperTracker CurrentTagHelperTracker => _trackerStack.FirstOrDefault(t => t.IsTagHelper) as TagHelperTracker;

            public override SyntaxNode VisitMarkupElement(MarkupElementSyntax node)
            {
                if (IsPartOfStartTag(node))
                {
                    // If this element is inside a start tag, it is some sort of malformed case like
                    // <p @do { someattribute=\"btn\"></p>, where the end "p" tag is inside the start "p" tag.
                    // We don't want to do tag helper parsing for this tag.
                    return base.VisitMarkupElement(node);
                }

                MarkupTagHelperStartTagSyntax tagHelperStart = null;
                MarkupTagHelperEndTagSyntax tagHelperEnd = null;
                TagHelperInfo tagHelperInfo = null;

                // Visit the start tag.
                var startTag = (MarkupStartTagSyntax)Visit(node.StartTag);
                if (startTag != null)
                {
                    var tagName = startTag.GetTagNameWithOptionalBang();
                    if (TryRewriteTagHelperStart(startTag, node.EndTag, out tagHelperStart, out tagHelperInfo))
                    {
                        // This is a tag helper.
                        if (tagHelperInfo.TagMode == TagMode.SelfClosing || tagHelperInfo.TagMode == TagMode.StartTagOnly)
                        {
                            var tagHelperElement = SyntaxFactory.MarkupTagHelperElement(tagHelperStart, body: new SyntaxList<RazorSyntaxNode>(), endTag: null);
                            var rewrittenTagHelper = tagHelperElement.WithTagHelperInfo(tagHelperInfo);
                            if (node.Body.Count == 0 && node.EndTag == null)
                            {
                                return rewrittenTagHelper;
                            }

                            // This tag contains a body and/or an end tag which needs to be moved to the parent.
                            var rewrittenNodes = SyntaxListBuilder<RazorSyntaxNode>.Create();
                            rewrittenNodes.Add(rewrittenTagHelper);
                            var rewrittenBody = VisitList(node.Body);
                            rewrittenNodes.AddRange(rewrittenBody);

                            return SyntaxFactory.MarkupElement(startTag: null, body: rewrittenNodes.ToList(), endTag: node.EndTag);
                        }
                        else if (node.EndTag == null)
                        {
                            // Start tag helper with no corresponding end tag.
                            _errorSink.OnError(
                                RazorDiagnosticFactory.CreateParsing_TagHelperFoundMalformedTagHelper(
                                    new SourceSpan(SourceLocationTracker.Advance(startTag.GetSourceLocation(_source), "<"), tagName.Length),
                                    tagName));
                        }
                        else
                        {
                            // Tag helper start tag. Keep track.
                            var tracker = new TagHelperTracker(_tagHelperPrefix, tagHelperInfo);
                            _trackerStack.Push(tracker);
                        }
                    }
                    else
                    {
                        // Non-TagHelper tag.
                        ValidateParentAllowsPlainStartTag(startTag);

                        if (node.EndTag != null || (!startTag.IsSelfClosing() && !startTag.IsVoidElement()))
                        {
                            // Ideally we don't want to keep track of self-closing or void tags.
                            // But if a matching end tag exists, keep track of the start tag no matter what.
                            // We will just assume the parser had a good reason to do this.
                            var tracker = new TagTracker(tagName, isTagHelper: false);
                            _trackerStack.Push(tracker);
                        }
                    }
                }

                // Visit body between start and end tags.
                var body = VisitList(node.Body);

                // Visit end tag.
                var endTag = (MarkupEndTagSyntax)Visit(node.EndTag);
                if (endTag != null)
                {
                    var tagName = endTag.GetTagNameWithOptionalBang();
                    if (TryRewriteTagHelperEnd(startTag, endTag, out tagHelperEnd))
                    {
                        // This is a tag helper
                        if (startTag == null)
                        {
                            // The end tag helper has no corresponding start tag, create an error.
                            _errorSink.OnError(
                                RazorDiagnosticFactory.CreateParsing_TagHelperFoundMalformedTagHelper(
                                    new SourceSpan(SourceLocationTracker.Advance(endTag.GetSourceLocation(_source), "</"), tagName.Length), tagName));
                        }
                    }
                    else
                    {
                        // Non tag helper end tag.
                        if (startTag == null)
                        {
                            // Standalone end tag. We may need to error if it is not supposed to be here.
                            // If there was a corresponding start tag, we would have already added this error.
                            ValidateParentAllowsPlainEndTag(endTag);
                        }
                        else
                        {
                            // Since a start tag exists, we must already be tracking it.
                            // Pop the stack as we're done with the end tag.
                            _trackerStack.Pop();
                        }
                    }
                }

                if (tagHelperInfo != null)
                {
                    // If we get here it means this element was rewritten as a tag helper.
                    var tagHelperElement = SyntaxFactory.MarkupTagHelperElement(tagHelperStart, body, tagHelperEnd);
                    return tagHelperElement.WithTagHelperInfo(tagHelperInfo);
                }

                // There was no matching tag helper for this element. Return.
                return node.Update(startTag, body, endTag);
            }

            public override SyntaxNode VisitMarkupTextLiteral(MarkupTextLiteralSyntax node)
            {
                var tagParent = node.FirstAncestorOrSelf<SyntaxNode>(n => n is MarkupStartTagSyntax || n is MarkupEndTagSyntax);
                var isPartofTagBlock = tagParent != null;
                if (!isPartofTagBlock)
                {
                    ValidateParentAllowsContent(node);
                }

                return base.VisitMarkupTextLiteral(node);
            }

            private bool TryRewriteTagHelperStart(
                MarkupStartTagSyntax startTag,
                MarkupEndTagSyntax endTag,
                out MarkupTagHelperStartTagSyntax rewritten,
                out TagHelperInfo tagHelperInfo)
            {
                rewritten = null;
                tagHelperInfo = null;

                // Get tag name of the current block
                var tagName = startTag.GetTagNameWithOptionalBang();

                // Could not determine tag name, it can't be a TagHelper, continue on and track the element.
                if (string.IsNullOrEmpty(tagName) || tagName.StartsWith("!", StringComparison.Ordinal))
                {
                    return false;
                }

                TagHelperBinding tagHelperBinding;

                if (!IsPotentialTagHelperStart(tagName, startTag))
                {
                    return false;
                }

                var tracker = CurrentTagHelperTracker;
                var tagNameScope = tracker?.TagName ?? string.Empty;

                // We're now in a start tag block, we first need to see if the tag block is a tag helper.
                var elementAttributes = GetAttributeNameValuePairs(startTag);

                tagHelperBinding = _tagHelperBinder.GetBinding(
                    tagName,
                    elementAttributes,
                    CurrentParentTagName,
                    CurrentParentIsTagHelper);

                // If there aren't any TagHelperDescriptors registered then we aren't a TagHelper
                if (tagHelperBinding == null)
                {
                    // If the current tag matches the current TagHelper scope it means the parent TagHelper matched
                    // all the required attributes but the current one did not; therefore, we need to increment the
                    // OpenMatchingTags counter for current the TagHelperBlock so we don't end it too early.
                    // ex: <myth req="..."><myth></myth></myth> We don't want the first myth to close on the inside
                    // tag.
                    if (string.Equals(tagNameScope, tagName, StringComparison.OrdinalIgnoreCase))
                    {
                        tracker.OpenMatchingTags++;
                    }

                    return false;
                }

                ValidateParentAllowsTagHelper(tagName, startTag);
                ValidateBinding(tagHelperBinding, tagName, startTag);

                // We're in a start TagHelper block.
                ValidateStartTagSyntax(tagName, startTag);

                var rewrittenStartTag = TagHelperBlockRewriter.Rewrite(
                    tagName,
                    _featureFlags,
                    startTag,
                    tagHelperBinding,
                    _errorSink,
                    _source);

                var tagMode = TagHelperBlockRewriter.GetTagMode(startTag, endTag, tagHelperBinding);
                tagHelperInfo = new TagHelperInfo(tagName, tagMode, tagHelperBinding);
                rewritten = rewrittenStartTag;

                return true;
            }

            private bool TryRewriteTagHelperEnd(MarkupStartTagSyntax startTag, MarkupEndTagSyntax endTag, out MarkupTagHelperEndTagSyntax rewritten)
            {
                rewritten = null;
                var tagName = endTag.GetTagNameWithOptionalBang();
                // Could not determine tag name, it can't be a TagHelper, continue on and track the element.
                if (string.IsNullOrEmpty(tagName) || tagName.StartsWith("!", StringComparison.Ordinal))
                {
                    return false;
                }

                var tracker = CurrentTagHelperTracker;
                var tagNameScope = tracker?.TagName ?? string.Empty;
                if (!IsPotentialTagHelperEnd(tagName, endTag))
                {
                    return false;
                }

                // Validate that our end tag matches the currently scoped tag, if not we may need to error.
                if (startTag != null && tagNameScope.Equals(tagName, StringComparison.OrdinalIgnoreCase))
                {
                    // If there are additional end tags required before we can build our block it means we're in a
                    // situation like this: <myth req="..."><myth></myth></myth> where we're at the inside </myth>.
                    if (tracker.OpenMatchingTags > 0)
                    {
                        tracker.OpenMatchingTags--;

                        return false;
                    }

                    ValidateEndTagSyntax(tagName, endTag);

                    _trackerStack.Pop();
                }
                else
                {
                    var tagHelperBinding = _tagHelperBinder.GetBinding(
                        tagName,
                        attributes: Array.Empty<KeyValuePair<string, string>>(),
                        parentTagName: CurrentParentTagName,
                        parentIsTagHelper: CurrentParentIsTagHelper);

                    // If there are not TagHelperDescriptors associated with the end tag block that also have no
                    // required attributes then it means we can't be a TagHelper, bail out.
                    if (tagHelperBinding == null)
                    {
                        return false;
                    }

                    foreach (var descriptor in tagHelperBinding.Descriptors)
                    {
                        var boundRules = tagHelperBinding.Mappings[descriptor];
                        var invalidRule = boundRules.FirstOrDefault(rule => rule.TagStructure == TagStructure.WithoutEndTag);

                        if (invalidRule != null)
                        {
                            // End tag TagHelper that states it shouldn't have an end tag.
                            _errorSink.OnError(
                                RazorDiagnosticFactory.CreateParsing_TagHelperMustNotHaveAnEndTag(
                                    new SourceSpan(SourceLocationTracker.Advance(endTag.GetSourceLocation(_source), "</"), tagName.Length),
                                    tagName,
                                    descriptor.DisplayName,
                                    invalidRule.TagStructure));

                            return false;
                        }
                    }
                }

                rewritten = SyntaxFactory.MarkupTagHelperEndTag(
                    endTag.OpenAngle, endTag.ForwardSlash, endTag.Bang, endTag.Name, endTag.MiscAttributeContent, endTag.CloseAngle);

                return true;
            }

            // Internal for testing
            internal IReadOnlyList<KeyValuePair<string, string>> GetAttributeNameValuePairs(MarkupStartTagSyntax tagBlock)
            {
                if (tagBlock.Attributes.Count == 0)
                {
                    return Array.Empty<KeyValuePair<string, string>>();
                }

                _htmlAttributeTracker.Clear();

                var attributes = _htmlAttributeTracker;

                for (var i = 0; i < tagBlock.Attributes.Count; i++)
                {
                    if (tagBlock.Attributes[i] is CSharpCodeBlockSyntax)
                    {
                        // Code blocks in the attribute area of tags mangles following attributes.
                        // It's also not supported by TagHelpers, bail early to avoid creating bad attribute value pairs.
                        break;
                    }

                    if (tagBlock.Attributes[i] is MarkupMinimizedAttributeBlockSyntax minimizedAttributeBlock)
                    {
                        if (minimizedAttributeBlock.Name == null)
                        {
                            _attributeValueBuilder.Append(InvalidAttributeValueMarker);
                            continue;
                        }

                        var minimizedAttribute = new KeyValuePair<string, string>(minimizedAttributeBlock.Name.GetContent(), string.Empty);
                        attributes.Add(minimizedAttribute);
                        continue;
                    }

                    if (!(tagBlock.Attributes[i] is MarkupAttributeBlockSyntax attributeBlock))
                    {
                        // If the parser thought these aren't attributes, we don't care about them. Move on.
                        continue;
                    }

                    if (attributeBlock.Name == null)
                    {
                        _attributeValueBuilder.Append(InvalidAttributeValueMarker);
                        continue;
                    }

                    if (attributeBlock.Value != null)
                    {
                        for (var j = 0; j < attributeBlock.Value.Children.Count; j++)
                        {
                            var child = attributeBlock.Value.Children[j];
                            if (child is MarkupLiteralAttributeValueSyntax literalValue)
                            {
                                _attributeValueBuilder.Append(literalValue.GetContent());
                            }
                            else
                            {
                                _attributeValueBuilder.Append(InvalidAttributeValueMarker);
                            }
                        }
                    }

                    var attributeName = attributeBlock.Name.GetContent();
                    var attributeValue = _attributeValueBuilder.ToString();
                    var attribute = new KeyValuePair<string, string>(attributeName, attributeValue);
                    attributes.Add(attribute);

                    _attributeValueBuilder.Clear();
                }

                return attributes;
            }

            private void ValidateParentAllowsTagHelper(string tagName, MarkupStartTagSyntax tagBlock)
            {
                if (HasAllowedChildren() &&
                    !CurrentTagHelperTracker.PrefixedAllowedChildren.Contains(tagName, StringComparer.OrdinalIgnoreCase))
                {
                    OnAllowedChildrenStartTagError(CurrentTagHelperTracker, tagName, tagBlock, _errorSink, _source);
                }
            }

            private void ValidateBinding(
                TagHelperBinding bindingResult,
                string tagName,
                MarkupStartTagSyntax tagBlock)
            {
                // Ensure that all descriptors associated with this tag have appropriate TagStructures. Cannot have
                // multiple descriptors that expect different TagStructures (other than TagStructure.Unspecified).
                TagHelperDescriptor baseDescriptor = null;
                TagStructure? baseStructure = null;
                foreach (var descriptor in bindingResult.Descriptors)
                {
                    var boundRules = bindingResult.Mappings[descriptor];
                    foreach (var rule in boundRules)
                    {
                        if (rule.TagStructure != TagStructure.Unspecified)
                        {
                            // Can't have a set of TagHelpers that expect different structures.
                            if (baseStructure.HasValue && baseStructure != rule.TagStructure)
                            {
                                _errorSink.OnError(
                                    RazorDiagnosticFactory.CreateTagHelper_InconsistentTagStructure(
                                        new SourceSpan(tagBlock.GetSourceLocation(_source), tagBlock.FullWidth),
                                        baseDescriptor.DisplayName,
                                        descriptor.DisplayName,
                                        tagName));
                            }

                            baseDescriptor = descriptor;
                            baseStructure = rule.TagStructure;
                        }
                    }
                }
            }

            private bool ValidateStartTagSyntax(string tagName, MarkupStartTagSyntax tag)
            {
                // We assume an invalid syntax until we verify that the tag meets all of our "valid syntax" criteria.
                if (IsPartialStartTag(tag))
                {
                    var errorStart = GetStartTagDeclarationErrorStart(tag, _source);

                    _errorSink.OnError(
                        RazorDiagnosticFactory.CreateParsing_TagHelperMissingCloseAngle(
                            new SourceSpan(errorStart, tagName.Length), tagName));

                    return false;
                }

                return true;
            }

            private bool ValidateEndTagSyntax(string tagName, MarkupEndTagSyntax tag)
            {
                // We assume an invalid syntax until we verify that the tag meets all of our "valid syntax" criteria.
                if (IsPartialEndTag(tag))
                {
                    var errorStart = GetEndTagDeclarationErrorStart(tag, _source);

                    _errorSink.OnError(
                        RazorDiagnosticFactory.CreateParsing_TagHelperMissingCloseAngle(
                            new SourceSpan(errorStart, tagName.Length), tagName));

                    return false;
                }

                return true;
            }

            private bool IsPotentialTagHelperStart(string tagName, MarkupStartTagSyntax startTag)
            {
                return !string.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) ||
                       !startTag.IsMarkupTransition;
            }

            private bool IsPotentialTagHelperEnd(string tagName, MarkupEndTagSyntax endTag)
            {
                return !string.Equals(tagName, SyntaxConstants.TextTagName, StringComparison.OrdinalIgnoreCase) ||
                       !endTag.IsMarkupTransition;
            }

            private static bool IsPartialStartTag(MarkupStartTagSyntax startTag)
            {
                return startTag.CloseAngle.IsMissing;
            }

            private static bool IsPartialEndTag(MarkupEndTagSyntax endTag)
            {
                return endTag.CloseAngle.IsMissing;
            }

            private void ValidateParentAllowsContent(SyntaxNode child)
            {
                if (HasAllowedChildren())
                {
                    var isDisallowedContent = true;
                    if (_featureFlags.AllowHtmlCommentsInTagHelpers)
                    {
                        isDisallowedContent = !IsComment(child) &&
                            !child.IsTransitionSpanKind() &&
                            !child.IsCodeSpanKind();
                    }

                    if (isDisallowedContent)
                    {
                        var content = child.GetContent();
                        if (!string.IsNullOrWhiteSpace(content))
                        {
                            var trimmedStart = content.TrimStart();
                            var whitespace = new StringSegment(content, 0, content.Length - trimmedStart.Length);
                            var errorStart = SourceLocationTracker.Advance(child.GetSourceLocation(_source), whitespace);
                            var length = trimmedStart.TrimEnd().Length;
                            var allowedChildren = CurrentTagHelperTracker.AllowedChildren;
                            var allowedChildrenString = string.Join(", ", allowedChildren);
                            _errorSink.OnError(
                                RazorDiagnosticFactory.CreateTagHelper_CannotHaveNonTagContent(
                                    new SourceSpan(errorStart, length),
                                    CurrentTagHelperTracker.TagName,
                                    allowedChildrenString));
                        }
                    }
                }
            }

            private void ValidateParentAllowsPlainStartTag(MarkupStartTagSyntax tagBlock)
            {
                var tagName = tagBlock.GetTagNameWithOptionalBang();

                // Treat partial tags such as '</' which have no tag names as content.
                if (string.IsNullOrEmpty(tagName))
                {
                    var firstChild = tagBlock.Children.First();
                    Debug.Assert(firstChild is MarkupTextLiteralSyntax);

                    ValidateParentAllowsContent(firstChild);
                    return;
                }

                if (!HasAllowedChildren())
                {
                    return;
                }

                var tagHelperBinding = _tagHelperBinder.GetBinding(
                    tagName,
                    attributes: Array.Empty<KeyValuePair<string, string>>(),
                    parentTagName: CurrentParentTagName,
                    parentIsTagHelper: CurrentParentIsTagHelper);

                // If we found a binding for the current tag, then it is a tag helper. Use the prefixed allowed children to compare.
                var allowedChildren = tagHelperBinding != null ? CurrentTagHelperTracker.PrefixedAllowedChildren : CurrentTagHelperTracker.AllowedChildren;
                if (!allowedChildren.Contains(tagName, StringComparer.OrdinalIgnoreCase))
                {
                    OnAllowedChildrenStartTagError(CurrentTagHelperTracker, tagName, tagBlock, _errorSink, _source);
                }
            }

            private void ValidateParentAllowsPlainEndTag(MarkupEndTagSyntax tagBlock)
            {
                var tagName = tagBlock.GetTagNameWithOptionalBang();

                // Treat partial tags such as '</' which have no tag names as content.
                if (string.IsNullOrEmpty(tagName))
                {
                    var firstChild = tagBlock.Children.First();
                    Debug.Assert(firstChild is MarkupTextLiteralSyntax);

                    ValidateParentAllowsContent(firstChild);
                    return;
                }

                if (!HasAllowedChildren())
                {
                    return;
                }

                var tagHelperBinding = _tagHelperBinder.GetBinding(
                    tagName,
                    attributes: Array.Empty<KeyValuePair<string, string>>(),
                    parentTagName: CurrentParentTagName,
                    parentIsTagHelper: CurrentParentIsTagHelper);

                // If we found a binding for the current tag, then it is a tag helper. Use the prefixed allowed children to compare.
                var allowedChildren = tagHelperBinding != null ? CurrentTagHelperTracker.PrefixedAllowedChildren : CurrentTagHelperTracker.AllowedChildren;
                if (!allowedChildren.Contains(tagName, StringComparer.OrdinalIgnoreCase))
                {
                    OnAllowedChildrenEndTagError(CurrentTagHelperTracker, tagName, tagBlock, _errorSink, _source);
                }
            }

            private bool HasAllowedChildren()
            {
                // TODO: Questionable logic. Need to revisit
                var currentTracker = _trackerStack.Count > 0 ? _trackerStack.Peek() : null;

                // If the current tracker is not a TagHelper then there's no AllowedChildren to enforce.
                if (currentTracker == null || !currentTracker.IsTagHelper)
                {
                    return false;
                }

                return CurrentTagHelperTracker.AllowedChildren != null && CurrentTagHelperTracker.AllowedChildren.Count > 0;
            }

            private bool IsPartOfStartTag(SyntaxNode node)
            {
                // Check if an ancestor is a start tag of a MarkupElement.
                var parent = node.FirstAncestorOrSelf<SyntaxNode>(n =>
                {
                    return n.Parent is MarkupElementSyntax element && element.StartTag == n;
                });

                return parent != null;
            }

            internal static bool IsComment(SyntaxNode node)
            {
                var commentParent = node.FirstAncestorOrSelf<SyntaxNode>(
                    n => n is RazorCommentBlockSyntax || n is MarkupCommentBlockSyntax);

                return commentParent != null;
            }

            private static void OnAllowedChildrenStartTagError(
                TagHelperTracker tracker,
                string tagName,
                MarkupStartTagSyntax tagBlock,
                ErrorSink errorSink,
                RazorSourceDocument source)
            {
                var allowedChildrenString = string.Join(", ", tracker.AllowedChildren);
                var errorStart = GetStartTagDeclarationErrorStart(tagBlock, source);

                errorSink.OnError(
                    RazorDiagnosticFactory.CreateTagHelper_InvalidNestedTag(
                        new SourceSpan(errorStart, tagName.Length),
                        tagName,
                        tracker.TagName,
                        allowedChildrenString));
            }

            private static void OnAllowedChildrenEndTagError(
                TagHelperTracker tracker,
                string tagName,
                MarkupEndTagSyntax tagBlock,
                ErrorSink errorSink,
                RazorSourceDocument source)
            {
                var allowedChildrenString = string.Join(", ", tracker.AllowedChildren);
                var errorStart = GetEndTagDeclarationErrorStart(tagBlock, source);

                errorSink.OnError(
                    RazorDiagnosticFactory.CreateTagHelper_InvalidNestedTag(
                        new SourceSpan(errorStart, tagName.Length),
                        tagName,
                        tracker.TagName,
                        allowedChildrenString));
            }

            private static SourceLocation GetStartTagDeclarationErrorStart(MarkupStartTagSyntax tagBlock, RazorSourceDocument source)
            {
                return SourceLocationTracker.Advance(tagBlock.GetSourceLocation(source), "<");
            }

            private static SourceLocation GetEndTagDeclarationErrorStart(MarkupEndTagSyntax tagBlock, RazorSourceDocument source)
            {
                return SourceLocationTracker.Advance(tagBlock.GetSourceLocation(source), "</");
            }

            private class TagTracker
            {
                public TagTracker(string tagName, bool isTagHelper)
                {
                    TagName = tagName;
                    IsTagHelper = isTagHelper;
                }

                public string TagName { get; }

                public bool IsTagHelper { get; }
            }

            private class TagHelperTracker : TagTracker
            {
                private IReadOnlyList<string> _prefixedAllowedChildren;
                private readonly string _tagHelperPrefix;

                public TagHelperTracker(string tagHelperPrefix, TagHelperInfo info)
                    : base(info.TagName, isTagHelper: true)
                {
                    _tagHelperPrefix = tagHelperPrefix;
                    Info = info;

                    if (Info.BindingResult.Descriptors.Any(descriptor => descriptor.AllowedChildTags != null))
                    {
                        AllowedChildren = Info.BindingResult.Descriptors
                            .Where(descriptor => descriptor.AllowedChildTags != null)
                            .SelectMany(descriptor => descriptor.AllowedChildTags.Select(childTag => childTag.Name))
                            .Distinct(StringComparer.OrdinalIgnoreCase)
                            .ToList();
                    }
                }

                public TagHelperInfo Info { get; }

                public uint OpenMatchingTags { get; set; }

                public IReadOnlyList<string> AllowedChildren { get; }

                public IReadOnlyList<string> PrefixedAllowedChildren
                {
                    get
                    {
                        if (AllowedChildren != null && _prefixedAllowedChildren == null)
                        {
                            Debug.Assert(Info.BindingResult.Descriptors.Any());

                            _prefixedAllowedChildren = AllowedChildren.Select(allowedChild => _tagHelperPrefix + allowedChild).ToList();
                        }

                        return _prefixedAllowedChildren;
                    }
                }
            }
        }
    }
}
