// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Shared.Utilities
{
    internal static class CommonFormattingHelpers
    {
        public static readonly Comparison<SuppressOperation> SuppressOperationComparer = (o1, o2) =>
        {
            return o1.TextSpan.Start - o2.TextSpan.Start;
        };

        public static readonly Comparison<IndentBlockOperation> IndentBlockOperationComparer = (o1, o2) =>
        {
            // smaller one goes left
            var s = o1.TextSpan.Start - o2.TextSpan.Start;
            if (s != 0)
            {
                return s;
            }

            // bigger one goes left
            var e = o2.TextSpan.End - o1.TextSpan.End;
            if (e != 0)
            {
                return e;
            }

            return 0;
        };

        public static IEnumerable<(SyntaxToken, SyntaxToken)> ConvertToTokenPairs(this SyntaxNode root, IReadOnlyList<TextSpan> spans)
        {
            Contract.ThrowIfNull(root);
            Contract.ThrowIfFalse(spans.Count > 0);

            if (spans.Count == 1)
            {
                // special case, if there is only one span, return right away
                yield return root.ConvertToTokenPair(spans[0]);
                yield break;
            }

            var previousOne = root.ConvertToTokenPair(spans[0]);

            // iterate through each spans and make sure each one doesn't overlap each other
            for (var i = 1; i < spans.Count; i++)
            {
                var currentOne = root.ConvertToTokenPair(spans[i]);
                if (currentOne.Item1.SpanStart <= previousOne.Item2.Span.End)
                {
                    // oops, looks like two spans are overlapping each other. merge them
                    previousOne = ValueTuple.Create(previousOne.Item1, previousOne.Item2.Span.End < currentOne.Item2.Span.End ? currentOne.Item2 : previousOne.Item2);
                    continue;
                }

                // okay, looks like things are in good shape
                yield return previousOne;

                // move to next one
                previousOne = currentOne;
            }

            // give out the last one
            yield return previousOne;
        }

        public static ValueTuple<SyntaxToken, SyntaxToken> ConvertToTokenPair(this SyntaxNode root, TextSpan textSpan)
        {
            Contract.ThrowIfNull(root);
            Contract.ThrowIfTrue(textSpan.IsEmpty);

            var startToken = root.FindToken(textSpan.Start);

            // empty token, get previous non-zero length token
            if (startToken.IsMissing)
            {
                // if there is no previous token, startToken will be set to SyntaxKind.None
                startToken = startToken.GetPreviousToken();
            }

            // span is on leading trivia
            if (textSpan.Start < startToken.SpanStart)
            {
                // if there is no previous token, startToken will be set to SyntaxKind.None
                startToken = startToken.GetPreviousToken();
            }

            // adjust position where we try to search end token
            var endToken = (root.FullSpan.End <= textSpan.End) ?
                root.GetLastToken(includeZeroWidth: true) : root.FindToken(textSpan.End);

            // empty token, get next token
            if (endToken.IsMissing)
            {
                endToken = endToken.GetNextToken();
            }

            // span is on trailing trivia
            if (endToken.Span.End < textSpan.End)
            {
                endToken = endToken.GetNextToken();
            }

            // make sure tokens are not SyntaxKind.None
            startToken = (startToken.RawKind != 0) ? startToken : root.GetFirstToken(includeZeroWidth: true);
            endToken = (endToken.RawKind != 0) ? endToken : root.GetLastToken(includeZeroWidth: true);

            // token is in right order
            Contract.ThrowIfFalse(startToken.Equals(endToken) || startToken.Span.End <= endToken.SpanStart);
            return ValueTuple.Create(startToken, endToken);
        }

        public static bool IsInvalidTokenRange(this SyntaxNode root, SyntaxToken startToken, SyntaxToken endToken)
        {
            // given token must be token exist excluding EndOfFile token.
            if (startToken.RawKind == 0 || endToken.RawKind == 0)
            {
                return true;
            }

            if (startToken.Equals(endToken))
            {
                return false;
            }

            // regular case. 
            // start token can't be end of file token and start token must be before end token if it's not the same token.
            return root.FullSpan.End == startToken.SpanStart || startToken.FullSpan.End > endToken.FullSpan.Start;
        }

        public static int GetTokenColumn(this SyntaxTree tree, SyntaxToken token, int tabSize)
        {
            Contract.ThrowIfNull(tree);
            Contract.ThrowIfTrue(token.RawKind == 0);

            var startPosition = token.SpanStart;
            var line = tree.GetText().Lines.GetLineFromPosition(startPosition);

            return line.GetColumnFromLineOffset(startPosition - line.Start, tabSize);
        }

        public static string GetText(this SourceText text, SyntaxToken token1, SyntaxToken token2)
            => (token1.RawKind == 0) ? text.ToString(TextSpan.FromBounds(0, token2.SpanStart)) : text.ToString(TextSpan.FromBounds(token1.Span.End, token2.SpanStart));

        public static string GetTextBetween(SyntaxToken token1, SyntaxToken token2)
        {
            var builder = new StringBuilder();
            AppendTextBetween(token1, token2, builder);

            return builder.ToString();
        }

        public static void AppendTextBetween(SyntaxToken token1, SyntaxToken token2, StringBuilder builder)
        {
            Contract.ThrowIfTrue(token1.RawKind == 0 && token2.RawKind == 0);
            Contract.ThrowIfTrue(token1.Equals(token2));

            if (token1.RawKind == 0)
            {
                AppendLeadingTriviaText(token2, builder);
                return;
            }

            if (token2.RawKind == 0)
            {
                AppendTrailingTriviaText(token1, builder);
                return;
            }

            if (token1.FullSpan.End == token2.FullSpan.Start)
            {
                AppendTextBetweenTwoAdjacentTokens(token1, token2, builder);
                return;
            }

            AppendTrailingTriviaText(token1, builder);

            for (var token = token1.GetNextToken(includeZeroWidth: true); token.FullSpan.End <= token2.FullSpan.Start; token = token.GetNextToken(includeZeroWidth: true))
            {
                builder.Append(token.ToFullString());
            }

            AppendPartialLeadingTriviaText(token2, builder, token1.TrailingTrivia.FullSpan.End);
        }

        private static void AppendTextBetweenTwoAdjacentTokens(SyntaxToken token1, SyntaxToken token2, StringBuilder builder)
        {
            AppendTrailingTriviaText(token1, builder);
            AppendLeadingTriviaText(token2, builder);
        }

        private static void AppendLeadingTriviaText(SyntaxToken token, StringBuilder builder)
        {
            if (!token.HasLeadingTrivia)
            {
                return;
            }

            foreach (var trivia in token.LeadingTrivia)
            {
                builder.Append(trivia.ToFullString());
            }
        }

        /// <summary>
        /// If the token1 is expected to be part of the leading trivia of the token2 then the trivia
        /// before the token1FullSpanEnd, which the fullspan end of the token1 should be ignored
        /// </summary>
        private static void AppendPartialLeadingTriviaText(SyntaxToken token, StringBuilder builder, int token1FullSpanEnd)
        {
            if (!token.HasLeadingTrivia)
            {
                return;
            }

            foreach (var trivia in token.LeadingTrivia)
            {
                if (trivia.FullSpan.End <= token1FullSpanEnd)
                {
                    continue;
                }

                builder.Append(trivia.ToFullString());
            }
        }

        private static void AppendTrailingTriviaText(SyntaxToken token, StringBuilder builder)
        {
            if (!token.HasTrailingTrivia)
            {
                return;
            }

            foreach (var trivia in token.TrailingTrivia)
            {
                builder.Append(trivia.ToFullString());
            }
        }

        /// <summary>
        /// this will create a span that includes its trailing trivia of its previous token and leading trivia of its next token
        /// for example, for code such as "class A { int ...", if given tokens are "A" and "{", this will return span [] of "class[ A { ]int ..."
        /// which included trailing trivia of "class" which is previous token of "A", and leading trivia of "int" which is next token of "{"
        /// </summary>
        public static TextSpan GetSpanIncludingTrailingAndLeadingTriviaOfAdjacentTokens(SyntaxToken startToken, SyntaxToken endToken)
        {
            // most of cases we can just ask previous and next token to create the span, but in some corner cases such as omitted token case,
            // those navigation function doesn't work, so we have to explore the tree ourselves to create correct span
            var startPosition = GetStartPositionOfSpan(startToken);
            var endPosition = GetEndPositionOfSpan(endToken);

            return TextSpan.FromBounds(startPosition, endPosition);
        }

        private static int GetEndPositionOfSpan(SyntaxToken token)
        {
            var nextToken = token.GetNextToken();
            if (nextToken.RawKind != 0)
            {
                return nextToken.SpanStart;
            }

            var backwardPosition = token.FullSpan.End;
            var parentNode = GetParentThatContainsGivenSpan(token.Parent, backwardPosition, forward: false);
            if (parentNode == null)
            {
                // reached the end of tree
                return token.FullSpan.End;
            }

            Contract.ThrowIfFalse(backwardPosition < parentNode.FullSpan.End);

            nextToken = parentNode.FindToken(backwardPosition + 1);

            Contract.ThrowIfTrue(nextToken.RawKind == 0);

            return nextToken.SpanStart;
        }

        public static int GetStartPositionOfSpan(SyntaxToken token)
        {
            var previousToken = token.GetPreviousToken();
            if (previousToken.RawKind != 0)
            {
                return previousToken.Span.End;
            }

            // first token in the tree
            var forwardPosition = token.FullSpan.Start;
            if (forwardPosition <= 0)
            {
                return 0;
            }

            var parentNode = GetParentThatContainsGivenSpan(token.Parent, forwardPosition, forward: true);
            Contract.ThrowIfNull(parentNode);
            Contract.ThrowIfFalse(parentNode.FullSpan.Start < forwardPosition);

            previousToken = parentNode.FindToken(forwardPosition + 1);

            Contract.ThrowIfTrue(previousToken.RawKind == 0);

            return previousToken.Span.End;
        }

        private static SyntaxNode? GetParentThatContainsGivenSpan(SyntaxNode? node, int position, bool forward)
        {
            while (node != null)
            {
                var fullSpan = node.FullSpan;
                if (forward)
                {
                    if (fullSpan.Start < position)
                    {
                        return node;
                    }
                }
                else
                {
                    if (position > fullSpan.End)
                    {
                        return node;
                    }
                }

                node = node.Parent;
            }

            return null;
        }

        public static bool HasAnyWhitespaceElasticTrivia(SyntaxToken previousToken, SyntaxToken currentToken)
        {
            if ((!previousToken.ContainsAnnotations && !currentToken.ContainsAnnotations) ||
                (!previousToken.HasTrailingTrivia && !currentToken.HasLeadingTrivia))
            {
                return false;
            }

            return previousToken.TrailingTrivia.HasAnyWhitespaceElasticTrivia() || currentToken.LeadingTrivia.HasAnyWhitespaceElasticTrivia();
        }

        public static bool IsNull<T>(T t) where T : class
            => t == null;

        public static bool IsNotNull<T>(T t) where T : class
            => !IsNull(t);

        public static TextSpan GetFormattingSpan(SyntaxNode root, TextSpan span)
        {
            Contract.ThrowIfNull(root);

            var startToken = root.FindToken(span.Start).GetPreviousToken();
            var endToken = root.FindTokenFromEnd(span.End).GetNextToken();

            var startPosition = startToken.SpanStart;
            var endPosition = endToken.RawKind == 0 ? root.Span.End : endToken.Span.End;

            return TextSpan.FromBounds(startPosition, endPosition);
        }
    }
}
