// 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.Diagnostics;
using System.Text;
using Microsoft.AspNetCore.Analyzers.Infrastructure.VirtualChars;
using Microsoft.CodeAnalysis.Text;

namespace Microsoft.AspNetCore.Analyzers.Infrastructure.EmbeddedSyntax;

/// <summary>
/// Root of the embedded language syntax hierarchy.  EmbeddedSyntaxNodes are very similar to 
/// Roslyn Red-Nodes in concept, though there are differences for ease of implementation.
/// </summary>
internal abstract class EmbeddedSyntaxNode<TSyntaxKind, TSyntaxNode>
    where TSyntaxKind : struct
    where TSyntaxNode : EmbeddedSyntaxNode<TSyntaxKind, TSyntaxNode>
{
    public readonly TSyntaxKind Kind;
    private TextSpan? _fullSpan;

    protected EmbeddedSyntaxNode(TSyntaxKind kind)
    {
        Debug.Assert((int)(object)kind != 0);
        Kind = kind;
    }

    internal abstract int ChildCount { get; }
    internal abstract EmbeddedSyntaxNodeOrToken<TSyntaxKind, TSyntaxNode> ChildAt(int index);

    public EmbeddedSyntaxNodeOrToken<TSyntaxKind, TSyntaxNode> this[int index] => ChildAt(index);

    public TextSpan GetSpan()
    {
        var start = int.MaxValue;
        var end = 0;

        GetSpan(ref start, ref end);

        return TextSpan.FromBounds(start, end);
    }

    public TextSpan? GetFullSpan()
        => _fullSpan ??= ComputeFullSpan();

    private TextSpan? ComputeFullSpan()
    {
        var start = ComputeStart();
        var end = ComputeEnd();
        if (start == null || end == null)
        {
            return null;
        }

        return TextSpan.FromBounds(start.Value, end.Value);

        int? ComputeStart()
        {
            for (int i = 0, n = ChildCount; i < n; i++)
            {
                var child = ChildAt(i);
                var span = child.GetFullSpan();
                if (span != null)
                {
                    return span.Value.Start;
                }
            }

            return null;
        }

        int? ComputeEnd()
        {
            for (var i = ChildCount - 1; i >= 0; i--)
            {
                var child = ChildAt(i);
                var span = child.GetFullSpan();
                if (span != null)
                {
                    return span.Value.End;
                }
            }

            return null;
        }
    }

    private void GetSpan(ref int start, ref int end)
    {
        foreach (var child in this)
        {
            if (child.IsNode)
            {
                child.Node.GetSpan(ref start, ref end);
            }
            else
            {
                var token = child.Token;
                if (!token.IsMissing)
                {
                    start = Math.Min(token.VirtualChars[0].Span.Start, start);
                    end = Math.Max(token.VirtualChars.Last().Span.End, end);
                }
            }
        }
    }

    public bool Contains(VirtualChar virtualChar)
    {
        foreach (var child in this)
        {
            if (child.IsNode)
            {
                if (child.Node.Contains(virtualChar))
                {
                    return true;
                }
            }
            else
            {
                if (child.Token.VirtualChars.Contains(virtualChar))
                {
                    return true;
                }
            }
        }

        return false;
    }

    /// <summary>
    /// Returns the string representation of this node, not including its leading and trailing trivia.
    /// </summary>
    /// <returns>The string representation of this node, not including its leading and trailing trivia.</returns>
    /// <remarks>The length of the returned string is always the same as Span.Length</remarks>
    public override string ToString()
    {
        var sb = new StringBuilder();
        WriteTo(sb);
        return sb.ToString();
    }

    /// <summary>
    /// Returns full string representation of this node including its leading and trailing trivia.
    /// </summary>
    /// <returns>The full string representation of this node including its leading and trailing trivia.</returns>
    /// <remarks>The length of the returned string is always the same as FullSpan.Length</remarks>
    public string ToFullString()
    {
        var sb = new StringBuilder();
        WriteTo(sb);
        return sb.ToString();
    }

    /// <summary>
    /// Writes the node to a stringbuilder.
    /// </summary>
    /// <param name="leading">If false, leading trivia will not be added</param>
    /// <param name="trailing">If false, trailing trivia will not be added</param>
    public void WriteTo(StringBuilder sb)
    {
        for (var i = 0; i < ChildCount; i++)
        {
            var child = this[i];

            if (child.IsNode)
            {
                child.Node.WriteTo(sb);
            }
            else
            {
                child.Token.WriteTo(sb);
            }
        }
    }

    public Enumerator GetEnumerator()
        => new(this);

    public struct Enumerator
    {
        private readonly EmbeddedSyntaxNode<TSyntaxKind, TSyntaxNode> _node;
        private readonly int _childCount;
        private int _currentIndex;

        public Enumerator(EmbeddedSyntaxNode<TSyntaxKind, TSyntaxNode> node)
        {
            _node = node;
            _childCount = _node.ChildCount;
            _currentIndex = -1;
            Current = default;
        }

        public EmbeddedSyntaxNodeOrToken<TSyntaxKind, TSyntaxNode> Current { get; private set; }

        public bool MoveNext()
        {
            _currentIndex++;
            if (_currentIndex >= _childCount)
            {
                Current = default;
                return false;
            }

            Current = _node.ChildAt(_currentIndex);
            return true;
        }
    }
}
