// 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;
using System.Collections.Generic;
using System.Linq;

namespace Microsoft.AspNetCore.Razor.Language.Syntax;

internal static class SyntaxReplacer
{
    internal static SyntaxNode Replace<TNode>(
        SyntaxNode root,
        IEnumerable<TNode> nodes = null,
        Func<TNode, TNode, SyntaxNode> computeReplacementNode = null)
        where TNode : SyntaxNode
    {
        var replacer = new Replacer<TNode>(nodes, computeReplacementNode);

        if (replacer.HasWork)
        {
            return replacer.Visit(root);
        }
        else
        {
            return root;
        }
    }

    internal static SyntaxNode ReplaceNodeInList(SyntaxNode root, SyntaxNode originalNode, IEnumerable<SyntaxNode> newNodes)
    {
        return new NodeListEditor(originalNode, newNodes, ListEditKind.Replace).Visit(root);
    }

    internal static SyntaxNode InsertNodeInList(SyntaxNode root, SyntaxNode nodeInList, IEnumerable<SyntaxNode> nodesToInsert, bool insertBefore)
    {
        return new NodeListEditor(nodeInList, nodesToInsert, insertBefore ? ListEditKind.InsertBefore : ListEditKind.InsertAfter).Visit(root);
    }

    private class Replacer<TNode> : SyntaxRewriter where TNode : SyntaxNode
    {
        private readonly Func<TNode, TNode, SyntaxNode> _computeReplacementNode;
        private readonly HashSet<SyntaxNode> _nodeSet;
        private readonly HashSet<TextSpan> _spanSet;
        private readonly TextSpan _totalSpan;

        public Replacer(IEnumerable<TNode> nodes, Func<TNode, TNode, SyntaxNode> computeReplacementNode)
        {
            _computeReplacementNode = computeReplacementNode;
            _nodeSet = nodes != null ? new HashSet<SyntaxNode>(nodes) : new HashSet<SyntaxNode>();
            _spanSet = new HashSet<TextSpan>(_nodeSet.Select(n => n.FullSpan));
            _totalSpan = ComputeTotalSpan(_spanSet);
        }

        public bool HasWork => _nodeSet.Count > 0;

        public override SyntaxNode Visit(SyntaxNode node)
        {
            var rewritten = node;

            if (node != null)
            {
                if (ShouldVisit(node.FullSpan))
                {
                    rewritten = base.Visit(node);
                }

                if (_nodeSet.Contains(node) && _computeReplacementNode != null)
                {
                    rewritten = _computeReplacementNode((TNode)node, (TNode)rewritten);
                }
            }

            return rewritten;
        }

        private static TextSpan ComputeTotalSpan(IEnumerable<TextSpan> spans)
        {
            var first = true;
            var start = 0;
            var end = 0;

            foreach (var span in spans)
            {
                if (first)
                {
                    start = span.Start;
                    end = span.End;
                    first = false;
                }
                else
                {
                    start = Math.Min(start, span.Start);
                    end = Math.Max(end, span.End);
                }
            }

            return new TextSpan(start, end - start);
        }

        private bool ShouldVisit(TextSpan span)
        {
            // first do quick check against total span
            if (!span.IntersectsWith(_totalSpan))
            {
                // if the node is outside the total span of the nodes to be replaced
                // then we won't find any nodes to replace below it.
                return false;
            }

            foreach (var s in _spanSet)
            {
                if (span.IntersectsWith(s))
                {
                    // node's full span intersects with at least one node to be replaced
                    // so we need to visit node's children to find it.
                    return true;
                }
            }

            return false;
        }
    }

    private class NodeListEditor : SyntaxRewriter
    {
        private readonly TextSpan _elementSpan;
        private readonly SyntaxNode _originalNode;
        private readonly IEnumerable<SyntaxNode> _newNodes;
        private readonly ListEditKind _editKind;

        public NodeListEditor(
            SyntaxNode originalNode,
            IEnumerable<SyntaxNode> replacementNodes,
            ListEditKind editKind)
        {
            _elementSpan = originalNode.Span;
            _originalNode = originalNode;
            _newNodes = replacementNodes;
            _editKind = editKind;
        }

        private bool ShouldVisit(TextSpan span)
        {
            if (span.IntersectsWith(_elementSpan))
            {
                // node's full span intersects with at least one node to be replaced
                // so we need to visit node's children to find it.
                return true;
            }

            return false;
        }

        public override SyntaxNode Visit(SyntaxNode node)
        {
            if (node == _originalNode)
            {
                throw new InvalidOperationException("Expecting a list");
            }

            var rewritten = node;

            if (node != null)
            {
                if (ShouldVisit(node.FullSpan))
                {
                    rewritten = base.Visit(node);
                }
            }

            return rewritten;
        }

        public override SyntaxList<TNode> VisitList<TNode>(SyntaxList<TNode> list)
        {
            if (_originalNode is TNode)
            {
                var index = list.IndexOf((TNode)_originalNode);
                if (index >= 0 && index < list.Count)
                {
                    switch (_editKind)
                    {
                        case ListEditKind.Replace:
                            return list.ReplaceRange((TNode)_originalNode, _newNodes.Cast<TNode>());

                        case ListEditKind.InsertAfter:
                            return list.InsertRange(index + 1, _newNodes.Cast<TNode>());

                        case ListEditKind.InsertBefore:
                            return list.InsertRange(index, _newNodes.Cast<TNode>());
                    }
                }
            }

            return base.VisitList<TNode>(list);
        }
    }

    private enum ListEditKind
    {
        InsertBefore,
        InsertAfter,
        Replace
    }
}
