// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.AspNetCore.Razor.PooledObjects;

namespace Microsoft.AspNetCore.Razor.Language.Components;

internal class ComponentEventHandlerLoweringPass : ComponentIntermediateNodePassBase, IRazorOptimizationPass
{
    public override int Order => 50;

    protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
    {
        if (!IsComponentDocument(documentNode))
        {
            return;
        }

        var @namespace = documentNode.FindPrimaryNamespace();
        var @class = documentNode.FindPrimaryClass();
        if (@namespace == null || @class == null)
        {
            // Nothing to do, bail. We can't function without the standard structure.
            return;
        }

        // For each event handler *usage* we need to rewrite the tag helper node to map to basic constructs.
        // Each usage will be represented by a tag helper property that is a descendant of either
        // a component or element.
        var references = documentNode.FindDescendantReferences<TagHelperDirectiveAttributeIntermediateNode>();
        using var _ = ReferenceEqualityHashSetPool<IntermediateNode>.GetPooledObject(out var parents);

        for (var i = 0; i < references.Count; i++)
        {
            parents.Add(references[i].Parent);
        }

        // We need to do something similar for directive attribute parameters like @onclick:preventDefault.
        var parameterReferences = documentNode.FindDescendantReferences<TagHelperDirectiveAttributeParameterIntermediateNode>();
        for (var i = 0; i < parameterReferences.Count; i++)
        {
            parents.Add(parameterReferences[i].Parent);
        }

        foreach (var parent in parents)
        {
            ProcessDuplicates(parent);
        }

        for (var i = 0; i < references.Count; i++)
        {
            var reference = references[i];
            var node = (TagHelperDirectiveAttributeIntermediateNode)reference.Node;

            if (!reference.Parent.Children.Contains(node))
            {
                // This node was removed as a duplicate, skip it.
                continue;
            }

            if (node.TagHelper.IsEventHandlerTagHelper())
            {
                reference.Replace(RewriteUsage(reference.Parent, node));
            }
        }

        for (var i = 0; i < parameterReferences.Count; i++)
        {
            var reference = parameterReferences[i];
            var node = (TagHelperDirectiveAttributeParameterIntermediateNode)reference.Node;

            if (!reference.Parent.Children.Contains(node))
            {
                // This node was removed as a duplicate, skip it.
                continue;
            }

            if (node.TagHelper.IsEventHandlerTagHelper())
            {
                reference.Replace(RewriteParameterUsage(node));
            }
        }
    }

    private static void ProcessDuplicates(IntermediateNode parent)
    {
        // Reverse order because we will remove nodes.
        //
        // Each 'property' node could be duplicated if there are multiple tag helpers that match that
        // particular attribute. This is likely to happen when a component also defines something like
        // OnClick. We want to remove the 'onclick' and let it fall back to be handled by the component.
        for (var i = parent.Children.Count - 1; i >= 0; i--)
        {
            if (parent.Children[i] is TagHelperPropertyIntermediateNode eventHandler &&
                eventHandler.TagHelper != null &&
                eventHandler.TagHelper.IsEventHandlerTagHelper())
            {
                for (var j = 0; j < parent.Children.Count; j++)
                {
                    if (parent.Children[j] is ComponentAttributeIntermediateNode componentAttribute &&
                        componentAttribute.TagHelper != null &&
                        componentAttribute.TagHelper.IsComponentTagHelper() &&
                        componentAttribute.AttributeName == eventHandler.AttributeName)
                    {
                        // Found a duplicate - remove the 'fallback' in favor of the component's own handling.
                        parent.Children.RemoveAt(i);
                        break;
                    }
                }
            }
        }

        // If we still have duplicates at this point then they are genuine conflicts.
        var duplicates = parent.Children
            .OfType<TagHelperDirectiveAttributeIntermediateNode>()
            .Where(p => p.TagHelper?.IsEventHandlerTagHelper() ?? false)
            .GroupBy(p => p.AttributeName)
            .Where(g => g.Count() > 1);

        foreach (var duplicate in duplicates)
        {
            parent.Diagnostics.Add(ComponentDiagnosticFactory.CreateEventHandler_Duplicates(
                parent.Source,
                duplicate.Key,
                duplicate.ToArray()));

            foreach (var property in duplicate)
            {
                parent.Children.Remove(property);
            }
        }

        var parameterDuplicates = parent.Children
            .OfType<TagHelperDirectiveAttributeParameterIntermediateNode>()
            .Where(p => p.TagHelper?.IsEventHandlerTagHelper() ?? false)
            .GroupBy(p => p.AttributeName)
            .Where(g => g.Count() > 1);

        foreach (var duplicate in parameterDuplicates)
        {
            parent.Diagnostics.Add(ComponentDiagnosticFactory.CreateEventHandlerParameter_Duplicates(
                parent.Source,
                duplicate.Key,
                duplicate.ToArray()));

            foreach (var property in duplicate)
            {
                parent.Children.Remove(property);
            }
        }
    }

    private static IntermediateNode RewriteUsage(IntermediateNode parent, TagHelperDirectiveAttributeIntermediateNode node)
    {
        var original = GetAttributeContent(node);
        if (original.Count == 0)
        {
            // This can happen in error cases, the parser will already have flagged this
            // as an error, so ignore it.
            return node;
        }

        // Now rewrite the content of the value node to look like:
        //
        // EventCallback.Factory.Create<T>(this, <code>)
        //
        // This method is overloaded on string and T, which means that it will put the code in the
        // correct context for intellisense when typing in the attribute.
        var eventArgsType = node.TagHelper.GetEventArgsType();

        using var _ = ListPool<IntermediateToken>.GetPooledObject(out var tokens);
        tokens.SetCapacityIfLarger(original.Count + 2);

        tokens.Add(
            new IntermediateToken()
            {
                Content = $"global::{ComponentsApi.EventCallback.FactoryAccessor}.{ComponentsApi.EventCallbackFactory.CreateMethod}<{TypeNameHelper.GetGloballyQualifiedNameIfNeeded(eventArgsType)}>(this, ",
                Kind = TokenKind.CSharp
            });

        for (var i = 0; i < original.Count; i++)
        {
            tokens.Add(original[i]);
        }

        tokens.Add(
            new IntermediateToken()
            {
                Content = $")",
                Kind = TokenKind.CSharp
            });

        var attributeName = node.AttributeName;

        if (parent is MarkupElementIntermediateNode)
        {
            var result = new HtmlAttributeIntermediateNode()
            {
                Annotations =
                    {
                        [ComponentMetadata.Common.OriginalAttributeName] = node.OriginalAttributeName,
                    },
                AttributeName = attributeName,
                Source = node.Source,

                Prefix = attributeName + "=\"",
                Suffix = "\"",
            };

            for (var i = 0; i < node.Diagnostics.Count; i++)
            {
                result.Diagnostics.Add(node.Diagnostics[i]);
            }

            var attributeValueNode = new CSharpExpressionAttributeValueIntermediateNode();
            result.Children.Add(attributeValueNode);

            foreach (var token in tokens)
            {
                attributeValueNode.Children.Add(token);
            }

            return result;
        }
        else
        {
            var result = new ComponentAttributeIntermediateNode(node)
            {
                Annotations =
                    {
                        [ComponentMetadata.Common.OriginalAttributeName] = node.OriginalAttributeName,
                    },
            };

            result.Children.Clear();

            var expressionNode = new CSharpExpressionIntermediateNode();
            result.Children.Add(expressionNode);

            foreach (var token in tokens)
            {
                expressionNode.Children.Add(token);
            }

            return result;
        }
    }

    private static IReadOnlyList<IntermediateToken> GetAttributeContent(IntermediateNode node)
    {
        var nodes = node.FindDescendantNodes<TemplateIntermediateNode>();
        var template = nodes.Count > 0 ? nodes[0] : default;
        if (template != null)
        {
            // See comments in TemplateDiagnosticPass
            node.Diagnostics.Add(ComponentDiagnosticFactory.Create_TemplateInvalidLocation(template.Source));
            return new[] { new IntermediateToken() { Kind = TokenKind.CSharp, Content = string.Empty, }, };
        }

        if (node.Children.Count == 1 && node.Children[0] is HtmlContentIntermediateNode htmlContentNode)
        {
            // This case can be hit for a 'string' attribute. We want to turn it into
            // an expression.
            var tokens = htmlContentNode.FindDescendantNodes<IntermediateToken>();

            var content = "\"" + string.Join(string.Empty, tokens.Select(t => t.Content.Replace("\"", "\\\""))) + "\"";
            return new[] { new IntermediateToken() { Content = content, Kind = TokenKind.CSharp, } };
        }
        else
        {
            return node.FindDescendantNodes<IntermediateToken>();
        }
    }

    private static IntermediateNode RewriteParameterUsage(TagHelperDirectiveAttributeParameterIntermediateNode node)
    {
        // Now rewrite the node to look like:
        //
        // builder.AddEventPreventDefaultAttribute(2, "onclick", true); // If minimized.
        // or
        // builder.AddEventPreventDefaultAttribute(2, "onclick", someBoolExpression); // If a bool expression is provided in the value.

        string eventHandlerMethod;
        if (node.BoundAttributeParameter.Name == "preventDefault")
        {
            eventHandlerMethod = ComponentsApi.RenderTreeBuilder.AddEventPreventDefaultAttribute;
        }
        else if (node.BoundAttributeParameter.Name == "stopPropagation")
        {
            eventHandlerMethod = ComponentsApi.RenderTreeBuilder.AddEventStopPropagationAttribute;
        }
        else
        {
            // Unsupported event handler attribute parameter. This can only happen if bound attribute descriptor
            // is configured to expect a parameter other than 'preventDefault' and 'stopPropagation'.
            return node;
        }

        var result = new ComponentAttributeIntermediateNode(node)
        {
            Annotations =
            {
                [ComponentMetadata.Common.OriginalAttributeName] = node.OriginalAttributeName,
                [ComponentMetadata.Common.AddAttributeMethodName] = eventHandlerMethod,
            },
        };

        result.Children.Clear();

        if (node.AttributeStructure != AttributeStructure.Minimized)
        {
            var tokens = GetAttributeContent(node);
            var expressionNode = new CSharpExpressionIntermediateNode();
            result.Children.Add(expressionNode);
            expressionNode.Children.AddRange(tokens);
        }

        return result;
    }
}
