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

namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
    internal static class LegacySyntaxNodeExtensions
    {
        private static readonly SyntaxKind[] TransitionSpanKinds = new SyntaxKind[]
        {
            SyntaxKind.CSharpTransition,
            SyntaxKind.MarkupTransition,
        };

        private static readonly SyntaxKind[] MetaCodeSpanKinds = new SyntaxKind[]
        {
            SyntaxKind.RazorMetaCode,
        };

        private static readonly SyntaxKind[] CommentSpanKinds = new SyntaxKind[]
        {
            SyntaxKind.RazorCommentTransition,
            SyntaxKind.RazorCommentStar,
            SyntaxKind.RazorCommentLiteral,
        };

        private static readonly SyntaxKind[] CodeSpanKinds = new SyntaxKind[]
        {
            SyntaxKind.CSharpStatementLiteral,
            SyntaxKind.CSharpExpressionLiteral,
            SyntaxKind.CSharpEphemeralTextLiteral,
        };

        private static readonly SyntaxKind[] MarkupSpanKinds = new SyntaxKind[]
        {
            SyntaxKind.MarkupTextLiteral,
            SyntaxKind.MarkupEphemeralTextLiteral,
        };

        private static readonly SyntaxKind[] NoneSpanKinds = new SyntaxKind[]
        {
            SyntaxKind.UnclassifiedTextLiteral,
        };

        public static SpanContext GetSpanContext(this SyntaxNode node)
        {
            var context = node.GetAnnotationValue(SyntaxConstants.SpanContextKind);

            return context is SpanContext ? (SpanContext)context : null;
        }

        public static TNode WithSpanContext<TNode>(this TNode node, SpanContext spanContext) where TNode : SyntaxNode
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            var newAnnotation = new SyntaxAnnotation(SyntaxConstants.SpanContextKind, spanContext);

            List<SyntaxAnnotation> newAnnotations = null;
            if (node.ContainsAnnotations)
            {
                var existingNodeAnnotations = node.GetAnnotations();
                for (int i = 0; i < existingNodeAnnotations.Length; i++)
                {
                    var annotation = existingNodeAnnotations[i];
                    if (annotation.Kind != newAnnotation.Kind)
                    {
                        if (newAnnotations == null)
                        {
                            newAnnotations = new List<SyntaxAnnotation>();
                            newAnnotations.Add(newAnnotation);
                        }

                        newAnnotations.Add(annotation);
                    }
                }
            }

            var newAnnotationsArray = newAnnotations == null ? new[] { newAnnotation } : newAnnotations.ToArray();

            return node.WithAnnotations(newAnnotationsArray);
        }

        public static SyntaxNode LocateOwner(this SyntaxNode node, SourceChange change)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            if (change.Span.AbsoluteIndex < node.Position)
            {
                // Early escape for cases where changes overlap multiple spans
                // In those cases, the span will return false, and we don't want to search the whole tree
                // So if the current span starts after the change, we know we've searched as far as we need to
                return null;
            }

            if (node.EndPosition < change.Span.AbsoluteIndex)
            {
                // no need to look into this node as it completely precedes the change
                return null;
            }

            if (IsSpanKind(node))
            {
                var editHandler = node.GetSpanContext()?.EditHandler ?? SpanEditHandler.CreateDefault();
                return editHandler.OwnsChange(node, change) ? node : null;
            }

            IReadOnlyList<SyntaxNode> children;
            if (node is MarkupStartTagSyntax startTag)
            {
                children = startTag.Children;
            }
            else if (node is MarkupEndTagSyntax endTag)
            {
                children = endTag.Children;
            }
            else if (node is MarkupTagHelperStartTagSyntax startTagHelper)
            {
                children = startTagHelper.Children;
            }
            else if (node is MarkupTagHelperEndTagSyntax endTagHelper)
            {
                children = endTagHelper.Children;
            }
            else
            {
                children = node.ChildNodes();
            }

            SyntaxNode owner = null;
            for (int i = 0; i < children.Count; i++)
            {
                var child = children[i];
                owner = LocateOwner(child, change);
                if (owner != null)
                {
                    break;
                }
            }

            return owner;
        }

        public static bool IsTransitionSpanKind(this SyntaxNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            return TransitionSpanKinds.Contains(node.Kind);
        }

        public static bool IsMetaCodeSpanKind(this SyntaxNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            return MetaCodeSpanKinds.Contains(node.Kind);
        }

        public static bool IsCommentSpanKind(this SyntaxNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            return CommentSpanKinds.Contains(node.Kind);
        }

        public static bool IsCodeSpanKind(this SyntaxNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            return CodeSpanKinds.Contains(node.Kind);
        }

        public static bool IsMarkupSpanKind(this SyntaxNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            return MarkupSpanKinds.Contains(node.Kind);
        }

        public static bool IsNoneSpanKind(this SyntaxNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            return NoneSpanKinds.Contains(node.Kind);
        }

        public static bool IsSpanKind(this SyntaxNode node)
        {
            return IsTransitionSpanKind(node) ||
                IsMetaCodeSpanKind(node) ||
                IsCommentSpanKind(node) ||
                IsCodeSpanKind(node) ||
                IsMarkupSpanKind(node) ||
                IsNoneSpanKind(node);
        }

        public static IEnumerable<SyntaxNode> FlattenSpans(this SyntaxNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            foreach (var child in node.DescendantNodes())
            {
                if (child is MarkupStartTagSyntax startTag)
                {
                    foreach (var tagChild in startTag.Children)
                    {
                        if (tagChild.IsSpanKind())
                        {
                            yield return tagChild;
                        }
                    }
                }
                else if (child is MarkupEndTagSyntax endTag)
                {
                    foreach (var tagChild in endTag.Children)
                    {
                        if (tagChild.IsSpanKind())
                        {
                            yield return tagChild;
                        }
                    }
                }
                else if (child.IsSpanKind())
                {
                    yield return child;
                }
            }
        }

        public static SyntaxNode PreviousSpan(this SyntaxNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            var parent = node.Parent;
            while (parent != null)
            {
                var flattenedSpans = parent.FlattenSpans();
                var prevSpan = flattenedSpans.LastOrDefault(n => n.EndPosition <= node.Position && n != node);
                if (prevSpan != null)
                {
                    return prevSpan;
                }

                parent = parent.Parent;
            }

            return null;
        }

        public static SyntaxNode NextSpan(this SyntaxNode node)
        {
            if (node == null)
            {
                throw new ArgumentNullException(nameof(node));
            }

            var parent = node.Parent;
            while (parent != null)
            {
                var flattenedSpans = parent.FlattenSpans();
                var nextSpan = flattenedSpans.FirstOrDefault(n => n.Position >= node.EndPosition && n != node);
                if (nextSpan != null)
                {
                    return nextSpan;
                }

                parent = parent.Parent;
            }

            return null;
        }
    }
}
