// 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.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Components;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.CodeAnalysis.Razor.Formatting;

using Microsoft.AspNetCore.Razor.Language.Syntax;

internal sealed class FormattingVisitor : SyntaxWalker
{
    private const string HtmlTag = "html";

    private readonly ImmutableArray<FormattingSpan>.Builder _spans;
    private readonly bool _inGlobalNamespace;
    private FormattingBlockKind _currentBlockKind;
    private int _currentHtmlIndentationLevel = 0;
    private int _currentRazorIndentationLevel = 0;
    private int _currentComponentIndentationLevel = 0;
    private bool _isInClassBody = false;

    private FormattingVisitor(ImmutableArray<FormattingSpan>.Builder spans, bool inGlobalNamespace)
    {
        _inGlobalNamespace = inGlobalNamespace;
        _spans = spans;
        _currentBlockKind = FormattingBlockKind.Markup;
    }

    public static void VisitRoot(
        RazorSyntaxTree syntaxTree, ImmutableArray<FormattingSpan>.Builder spans, bool inGlobalNamespace)
    {
        var visitor = new FormattingVisitor(spans, inGlobalNamespace);
        visitor.Visit(syntaxTree.Root);
    }

    public override void VisitRazorCommentBlock(RazorCommentBlockSyntax node)
    {
        using (CommentBlock())
        {
            // We only want to move the start of the comment into the right spot, so we only
            // create spans for the start.
            // The body of the comment, including whitespace before the "*@" is left exactly
            // as the user has it in the file.
            AddSpan(node.StartCommentTransition, FormattingSpanKind.Transition);
            AddSpan(node.StartCommentStar, FormattingSpanKind.MetaCode);
        }
    }

    public override void VisitCSharpCodeBlock(CSharpCodeBlockSyntax node)
    {
        if (node.Parent is CSharpStatementBodySyntax or
                           CSharpImplicitExpressionBodySyntax or
                           RazorDirectiveBodySyntax ||
            (_currentBlockKind == FormattingBlockKind.Directive && node.Parent?.Parent is RazorDirectiveBodySyntax))
        {
            // If we get here, it means we don't want this code block to be considered significant.
            // Without this, we would have double indentation in places where
            // CSharpCodeBlock is used as a wrapper block in the syntax tree.

            if (node.Parent is not RazorDirectiveBodySyntax)
            {
                _currentRazorIndentationLevel++;
            }

            var isInCodeBlockDirective =
                node.Parent?.Parent?.Parent is RazorDirectiveSyntax directive &&
                directive.DirectiveDescriptor.Kind == DirectiveKind.CodeBlock;

            if (isInCodeBlockDirective)
            {
                // This means this is the code portion of an @code or @functions kind of block.
                _isInClassBody = true;
            }

            base.VisitCSharpCodeBlock(node);

            if (isInCodeBlockDirective)
            {
                // Finished visiting the code portion. We are no longer in it.
                _isInClassBody = false;
            }

            if (node.Parent is not RazorDirectiveBodySyntax)
            {
                _currentRazorIndentationLevel--;
            }

            return;
        }

        using (StatementBlock())
        {
            base.VisitCSharpCodeBlock(node);
        }
    }

    public override void VisitCSharpStatement(CSharpStatementSyntax node)
    {
        using (StatementBlock())
        {
            base.VisitCSharpStatement(node);
        }
    }

    public override void VisitCSharpExplicitExpression(CSharpExplicitExpressionSyntax node)
    {
        using (ExpressionBlock())
        {
            base.VisitCSharpExplicitExpression(node);
        }
    }

    public override void VisitCSharpImplicitExpression(CSharpImplicitExpressionSyntax node)
    {
        using (ExpressionBlock())
        {
            base.VisitCSharpImplicitExpression(node);
        }
    }

    public override void VisitRazorDirective(RazorDirectiveSyntax node)
    {
        using (DirectiveBlock())
        {
            base.VisitRazorDirective(node);
        }
    }

    public override void VisitCSharpTemplateBlock(CSharpTemplateBlockSyntax node)
    {
        using (TemplateBlock())
        {
            base.VisitCSharpTemplateBlock(node);
        }
    }

    public override void VisitMarkupBlock(MarkupBlockSyntax node)
    {
        using (MarkupBlock())
        {
            base.VisitMarkupBlock(node);
        }
    }

    public override void VisitMarkupElement(MarkupElementSyntax node)
    {
        Visit(node.StartTag);

        // Void elements, like <meta> or <input> which don't need an end tag don't cause indentation.
        // We also cheat and treat the <html> tag as a void element, so it doesn't cause indentation,
        // as that's what the Html formatter does, to avoid one level of indentation in every html file.
        var voidElement = node.StartTag is { } startTag &&
            (startTag.IsVoidElement() || string.Equals(startTag.Name.Content, HtmlTag, StringComparison.OrdinalIgnoreCase));

        if (!voidElement)
        {
            _currentHtmlIndentationLevel++;
        }

        foreach (var child in node.Body)
        {
            Visit(child);
        }

        if (!voidElement)
        {
            _currentHtmlIndentationLevel--;
        }

        Visit(node.EndTag);
    }

    public override void VisitMarkupStartTag(MarkupStartTagSyntax node)
    {
        using (TagBlock())
        {
            var children = SyntaxUtilities.GetRewrittenMarkupStartTagChildren(node);

            foreach (var child in children)
            {
                Visit(child);
            }
        }
    }

    public override void VisitMarkupEndTag(MarkupEndTagSyntax node)
    {
        using (TagBlock())
        {
            var children = SyntaxUtilities.GetRewrittenMarkupEndTagChildren(node);

            foreach (var child in children)
            {
                Visit(child);
            }
        }
    }

    public override void VisitMarkupTagHelperElement(MarkupTagHelperElementSyntax node)
    {
        var isComponent = IsComponentTagHelperNode(node);
        // Components with cascading type parameters cause an extra level of indentation
        var componentIndentationLevels = isComponent && HasUnspecifiedCascadingTypeParameter(node) ? 2 : 1;

        var causesIndentation = isComponent;
        if (node.Parent is MarkupTagHelperElementSyntax parentComponent &&
            IsComponentTagHelperNode(parentComponent) &&
            ParentHasProperty(parentComponent, node.TagHelperInfo?.TagName))
        {
            causesIndentation = false;
        }

        Visit(node.StartTag);

        _currentHtmlIndentationLevel++;
        if (causesIndentation)
        {
            _currentComponentIndentationLevel += componentIndentationLevels;
        }

        foreach (var child in node.Body)
        {
            Visit(child);
        }

        if (causesIndentation)
        {
            Debug.Assert(_currentComponentIndentationLevel > 0, "Component indentation level should not be at 0.");
            _currentComponentIndentationLevel -= componentIndentationLevels;
        }

        _currentHtmlIndentationLevel--;

        Visit(node.EndTag);

        static bool IsComponentTagHelperNode(MarkupTagHelperElementSyntax node)
        {
            return node.TagHelperInfo?.BindingResult?.Descriptors is { Length: > 0 } descriptors &&
                   descriptors.Any(static d => d.IsComponentOrChildContentTagHelper());
        }

        static bool ParentHasProperty(MarkupTagHelperElementSyntax parentComponent, string? propertyName)
        {
            // If this is a child tag helper that match a property of its parent tag helper
            // then it means this specific node won't actually cause a change in indentation.
            // For example, the following two bits of Razor generate identical C# code, even though the code block is
            // nested in a different number of tag helper elements:
            //
            // <Component>
            //     @if (true)
            //     {
            //     }
            // </Component>
            //
            // and
            //
            // <Component>
            //     <ChildContent>
            //         @if (true)
            //         {
            //         }
            //     </ChildContent>
            // </Component>
            //
            // This code will not count "ChildContent" as causing indentation because its parent
            // has a property called "ChildContent".
            if (parentComponent.TagHelperInfo?.BindingResult.Descriptors.Any(d => d.BoundAttributes.Any(a => a.Name == propertyName)) ?? false)
            {
                return true;
            }

            return false;
        }

        static bool HasUnspecifiedCascadingTypeParameter(MarkupTagHelperElementSyntax node)
        {
            if (node.TagHelperInfo?.BindingResult?.Descriptors is not { Length: > 0 } descriptors)
            {
                return false;
            }

            // A cascading type parameter will mean the generated code will get a TypeInference class generated
            // for it, which we need to account for with an extra level of indentation in our expected C# indentation
            var hasCascadingGenericParameters = descriptors.Any(static d => d.SuppliesCascadingGenericParameters());
            if (!hasCascadingGenericParameters)
            {
                return false;
            }

            // BUT, because life wasn't mean to be easy, the indentation is only affected when the developer
            // doesn't specify any type parameter in the element itself as an attribute.

            // Get all type parameters for later use. Array is fine to use as the list should be tiny (I hope!!)
            var typeParameterNames = descriptors.SelectMany(d => d.GetTypeParameters().Select(p => p.Name)).ToArray();

            var attributes = node.StartTag.Attributes.OfType<MarkupTagHelperAttributeSyntax>();
            foreach (var attribute in attributes)
            {
                if (attribute.TagHelperAttributeInfo.Bound)
                {
                    var name = attribute.TagHelperAttributeInfo.Name;
                    if (typeParameterNames.Contains(name))
                    {
                        return false;
                    }
                }
            }

            return true;
        }
    }

    public override void VisitMarkupTagHelperStartTag(MarkupTagHelperStartTagSyntax node)
    {
        using (TagBlock())
        {
            foreach (var child in node.LegacyChildren)
            {
                Visit(child);
            }
        }
    }

    public override void VisitMarkupTagHelperEndTag(MarkupTagHelperEndTagSyntax node)
    {
        using (TagBlock())
        {
            foreach (var child in node.LegacyChildren)
            {
                Visit(child);
            }
        }
    }

    public override void VisitMarkupAttributeBlock(MarkupAttributeBlockSyntax node)
    {
        using (MarkupBlock())
        {
            // For attributes, we add a single span from the start of the name prefix to the end of the value prefix.
            var spanComputer = new SpanComputer();
            spanComputer.Add(node.NamePrefix);
            spanComputer.Add(node.Name);
            spanComputer.Add(node.NameSuffix);
            spanComputer.Add(node.EqualsToken);
            spanComputer.Add(node.ValuePrefix);

            var textSpan = spanComputer.ToTextSpan();

            AddSpan(textSpan, FormattingSpanKind.Markup);

            // Visit the value and value suffix separately.
            Visit(node.Value);
            Visit(node.ValueSuffix);
        }
    }

    public override void VisitMarkupTagHelperAttribute(MarkupTagHelperAttributeSyntax node)
    {
        using (TagBlock())
        {
            // For attributes, we add a single span from the start of the name prefix to the end of the value prefix.
            var spanComputer = new SpanComputer();
            spanComputer.Add(node.NamePrefix);
            spanComputer.Add(node.Name);
            spanComputer.Add(node.NameSuffix);
            spanComputer.Add(node.EqualsToken);
            spanComputer.Add(node.ValuePrefix);

            var textSpan = spanComputer.ToTextSpan();

            AddSpan(textSpan, FormattingSpanKind.Markup);

            // Visit the value and value suffix separately.
            Visit(node.Value);
            Visit(node.ValueSuffix);
        }
    }

    public override void VisitMarkupTagHelperDirectiveAttribute(MarkupTagHelperDirectiveAttributeSyntax node)
    {
        Visit(node.Transition);
        Visit(node.Colon);
        Visit(node.Value);
    }

    public override void VisitMarkupMinimizedTagHelperDirectiveAttribute(MarkupMinimizedTagHelperDirectiveAttributeSyntax node)
    {
        Visit(node.Transition);
        Visit(node.Colon);
    }

    public override void VisitMarkupMinimizedAttributeBlock(MarkupMinimizedAttributeBlockSyntax node)
    {
        using (MarkupBlock())
        {
            // For minimized attributes, we add a single span for the attribute name along with the name prefix.
            var spanComputer = new SpanComputer();
            spanComputer.Add(node.NamePrefix);
            spanComputer.Add(node.Name);

            var textSpan = spanComputer.ToTextSpan();

            AddSpan(textSpan, FormattingSpanKind.Markup);
        }
    }

    public override void VisitMarkupCommentBlock(MarkupCommentBlockSyntax node)
    {
        using (HtmlCommentBlock())
        {
            base.VisitMarkupCommentBlock(node);
        }
    }

    public override void VisitMarkupDynamicAttributeValue(MarkupDynamicAttributeValueSyntax node)
    {
        using (MarkupBlock())
        {
            base.VisitMarkupDynamicAttributeValue(node);
        }
    }

    public override void VisitMarkupTagHelperAttributeValue(MarkupTagHelperAttributeValueSyntax node)
    {
        using (MarkupBlock())
        {
            base.VisitMarkupTagHelperAttributeValue(node);
        }
    }

    public override void VisitRazorMetaCode(RazorMetaCodeSyntax node)
    {
        if (node.Parent is MarkupTagHelperDirectiveAttributeSyntax { TagHelperAttributeInfo.Bound: true })
        {
            // For @bind attributes we want to pretend that we're in a Html context, so write this span as markup
            AddSpan(node, FormattingSpanKind.Markup);
        }
        else
        {
            AddSpan(node, FormattingSpanKind.MetaCode);
        }

        base.VisitRazorMetaCode(node);
    }

    public override void VisitCSharpTransition(CSharpTransitionSyntax node)
    {
        AddSpan(node, FormattingSpanKind.Transition);
        base.VisitCSharpTransition(node);
    }

    public override void VisitMarkupTransition(MarkupTransitionSyntax node)
    {
        AddSpan(node, FormattingSpanKind.Transition);
        base.VisitMarkupTransition(node);
    }

    public override void VisitCSharpStatementLiteral(CSharpStatementLiteralSyntax node)
    {
        // Workaround for a quirk of runtime code gen, where an empty marker is inserted before the close brace
        // of an explicit expression. ie, at the $$ below:
        //
        // @{
        //     something
        // $$}
        //
        // Writing a span for this empty marker will cause the close brace to be incorrectly indented, because its seen as
        // being "inside" the block.
        if (node.LiteralTokens is not [{ Kind: SyntaxKind.Marker }])
        {
            AddSpan(node, FormattingSpanKind.Code);
        }

        base.VisitCSharpStatementLiteral(node);
    }

    public override void VisitCSharpExpressionLiteral(CSharpExpressionLiteralSyntax node)
    {
        AddSpan(node, FormattingSpanKind.Code);
        base.VisitCSharpExpressionLiteral(node);
    }

    public override void VisitCSharpEphemeralTextLiteral(CSharpEphemeralTextLiteralSyntax node)
    {
        AddSpan(node, FormattingSpanKind.Code);
        base.VisitCSharpEphemeralTextLiteral(node);
    }

    public override void VisitUnclassifiedTextLiteral(UnclassifiedTextLiteralSyntax node)
    {
        AddSpan(node, FormattingSpanKind.None);
        base.VisitUnclassifiedTextLiteral(node);
    }

    public override void VisitMarkupLiteralAttributeValue(MarkupLiteralAttributeValueSyntax node)
    {
        AddSpan(node, FormattingSpanKind.Markup);
        base.VisitMarkupLiteralAttributeValue(node);
    }

    public override void VisitMarkupTextLiteral(MarkupTextLiteralSyntax node)
    {
        if (node.Parent is MarkupLiteralAttributeValueSyntax)
        {
            base.VisitMarkupTextLiteral(node);
            return;
        }

        AddSpan(node, FormattingSpanKind.Markup);
        base.VisitMarkupTextLiteral(node);
    }

    public override void VisitMarkupEphemeralTextLiteral(MarkupEphemeralTextLiteralSyntax node)
    {
        AddSpan(node, FormattingSpanKind.Markup);
        base.VisitMarkupEphemeralTextLiteral(node);
    }

    private BlockSaver CommentBlock()
        => Block(FormattingBlockKind.Comment);

    private BlockSaver DirectiveBlock()
        => Block(FormattingBlockKind.Directive);

    private BlockSaver ExpressionBlock()
        => Block(FormattingBlockKind.Expression);

    private BlockSaver HtmlCommentBlock()
        => Block(FormattingBlockKind.HtmlComment);

    private BlockSaver MarkupBlock()
        => Block(FormattingBlockKind.Markup);

    private BlockSaver StatementBlock()
        => Block(FormattingBlockKind.Statement);

    private BlockSaver TagBlock()
        => Block(FormattingBlockKind.Tag);

    private BlockSaver TemplateBlock()
        => Block(FormattingBlockKind.Template);

    private BlockSaver Block(FormattingBlockKind kind)
    {
        var saver = new BlockSaver(this);

        _currentBlockKind = kind;

        return saver;
    }

    private readonly ref struct BlockSaver(FormattingVisitor visitor)
    {
        private readonly FormattingBlockKind _previousKind = visitor._currentBlockKind;

        public void Dispose()
        {
            visitor._currentBlockKind = _previousKind;
        }
    }

    private void AddSpan(SyntaxNode node, FormattingSpanKind kind)
    {
        if (node.IsMissing)
        {
            return;
        }

        AddSpan(node.Span, kind);
    }

    private void AddSpan(SyntaxToken token, FormattingSpanKind kind)
    {
        if (token.IsMissing)
        {
            return;
        }

        AddSpan(token.Span, kind);
    }

    private void AddSpan(TextSpan textSpan, FormattingSpanKind kind)
    {
        if (textSpan.IsEmpty)
        {
            return;
        }

        var span = new FormattingSpan(
            textSpan,
            kind,
            _currentRazorIndentationLevel,
            _currentHtmlIndentationLevel,
            IsInGlobalNamespace: _inGlobalNamespace,
            IsInClassBody: _isInClassBody,
            _currentComponentIndentationLevel);

        _spans.Add(span);
    }
}
