// 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.Diagnostics;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.CSharp.Formatting
{
    internal partial class TriviaDataFactory
    {
        private struct CodeShapeAnalyzer
        {
            private readonly FormattingContext _context;
            private readonly SyntaxFormattingOptions _options;
            private readonly TriviaList _triviaList;

            private int _indentation;
            private bool _hasTrailingSpace;
            private int _lastLineBreakIndex;
            private bool _touchedNoisyCharacterOnCurrentLine;

            public static bool ShouldFormatMultiLine(FormattingContext context, bool firstTriviaInTree, TriviaList triviaList)
            {
                var analyzer = new CodeShapeAnalyzer(context, firstTriviaInTree, triviaList);
                return analyzer.ShouldFormat();
            }

            public static bool ShouldFormatSingleLine(TriviaList list)
            {
                foreach (var trivia in list)
                {
                    Contract.ThrowIfTrue(trivia.Kind() == SyntaxKind.EndOfLineTrivia);
                    Contract.ThrowIfTrue(trivia.Kind() == SyntaxKind.SkippedTokensTrivia);
                    Contract.ThrowIfTrue(trivia.Kind() == SyntaxKind.PreprocessingMessageTrivia);

                    // if it contains elastic trivia, always format
                    if (trivia.IsElastic())
                    {
                        return true;
                    }

                    if (trivia.Kind() == SyntaxKind.WhitespaceTrivia)
                    {
                        Debug.Assert(trivia.ToString() == trivia.ToFullString());
                        var text = trivia.ToString();
                        if (text.IndexOf('\t') >= 0)
                        {
                            return true;
                        }
                    }

                    // we don't touch space between two tokens on a single line that contains
                    // multiline comments between them
                    if (trivia.IsRegularOrDocComment())
                    {
                        return false;
                    }

                    if (trivia.Kind() == SyntaxKind.RegionDirectiveTrivia ||
                        trivia.Kind() == SyntaxKind.EndRegionDirectiveTrivia ||
                        SyntaxFacts.IsPreprocessorDirective(trivia.Kind()))
                    {
                        return false;
                    }
                }

                return true;
            }

            public static bool ContainsSkippedTokensOrText(TriviaList list)
            {
                foreach (var trivia in list)
                {
                    if (trivia.Kind() is SyntaxKind.SkippedTokensTrivia or
                        SyntaxKind.PreprocessingMessageTrivia)
                    {
                        return true;
                    }
                }

                return false;
            }

            private CodeShapeAnalyzer(FormattingContext context, bool firstTriviaInTree, TriviaList triviaList)
            {
                _context = context;
                _options = context.Options;
                _triviaList = triviaList;

                _indentation = 0;
                _hasTrailingSpace = false;
                _lastLineBreakIndex = firstTriviaInTree ? 0 : -1;
                _touchedNoisyCharacterOnCurrentLine = false;
            }

            private readonly bool UseIndentation
            {
                get { return _lastLineBreakIndex >= 0; }
            }

            private static bool OnElastic(SyntaxTrivia trivia)
            {
                // if this is structured trivia then we need to check for elastic trivia in any descendant
                if (trivia.GetStructure() is { ContainsAnnotations: true } structure)
                {
                    foreach (var t in structure.DescendantTrivia())
                    {
                        if (t.IsElastic())
                        {
                            return true;
                        }
                    }
                }

                // if it contains elastic trivia, always format
                return trivia.IsElastic();
            }

            private bool OnWhitespace(SyntaxTrivia trivia)
            {
                if (trivia.Kind() != SyntaxKind.WhitespaceTrivia)
                {
                    return false;
                }

                // there was noisy char after end of line trivia
                if (!this.UseIndentation || _touchedNoisyCharacterOnCurrentLine)
                {
                    _hasTrailingSpace = true;
                    return false;
                }

                // right after end of line trivia. calculate indentation for current line
                Debug.Assert(trivia.ToString() == trivia.ToFullString());
                var text = trivia.ToString();

                // if text contains tab, we will give up perf optimization and use more expensive one to see whether we need to replace this trivia
                if (text.IndexOf('\t') >= 0)
                {
                    return true;
                }

                _indentation += text.ConvertTabToSpace(_options.TabSize, _indentation, text.Length);

                return false;
            }

            private bool OnEndOfLine(SyntaxTrivia trivia, int currentIndex)
            {
                if (trivia.Kind() != SyntaxKind.EndOfLineTrivia)
                {
                    return false;
                }

                // end of line trivia right after whitespace trivia
                if (_hasTrailingSpace)
                {
                    // has trailing whitespace
                    return true;
                }

                // empty line with spaces. remove it.
                if (_indentation > 0 && !_touchedNoisyCharacterOnCurrentLine)
                {
                    return true;
                }

                ResetStateAfterNewLine(currentIndex);
                return false;
            }

            private void ResetStateAfterNewLine(int currentIndex)
            {
                // reset states for current line
                _indentation = 0;
                _touchedNoisyCharacterOnCurrentLine = false;
                _hasTrailingSpace = false;

                // remember last line break index
                _lastLineBreakIndex = currentIndex;
            }

            private readonly bool OnComment(SyntaxTrivia trivia)
            {
                if (!trivia.IsRegularOrDocComment())
                {
                    return false;
                }

                // check whether indentation are right
                if (this.UseIndentation && _indentation != _context.GetBaseIndentation(trivia.SpanStart))
                {
                    // comment has wrong indentation
                    return true;
                }

                // go deep down for single line documentation comment
                if (trivia.IsSingleLineDocComment() &&
                    ShouldFormatSingleLineDocumentationComment(_indentation, _options.TabSize, trivia))
                {
                    return true;
                }

                return false;
            }

            private static bool OnSkippedTokensOrText(SyntaxTrivia trivia)
            {
                if (trivia.Kind() is not SyntaxKind.SkippedTokensTrivia and
                    not SyntaxKind.PreprocessingMessageTrivia)
                {
                    return false;
                }

                throw ExceptionUtilities.Unreachable();
            }

            private bool OnRegion(SyntaxTrivia trivia, int currentIndex)
            {
                if (trivia.Kind() is not SyntaxKind.RegionDirectiveTrivia and
                    not SyntaxKind.EndRegionDirectiveTrivia)
                {
                    return false;
                }

                if (!this.UseIndentation)
                {
                    return true;
                }

                if (_indentation != _context.GetBaseIndentation(trivia.SpanStart))
                {
                    return true;
                }

                ResetStateAfterNewLine(currentIndex);
                return false;
            }

            private bool OnPreprocessor(SyntaxTrivia trivia, int currentIndex)
            {
                if (!SyntaxFacts.IsPreprocessorDirective(trivia.Kind()))
                {
                    return false;
                }

                if (!this.UseIndentation)
                {
                    return true;
                }

                // preprocessor must be at from column 0
                if (_indentation != 0)
                {
                    return true;
                }

                ResetStateAfterNewLine(currentIndex);
                return false;
            }

            private bool OnTouchedNoisyCharacter(SyntaxTrivia trivia)
            {
                if (trivia.IsElastic() ||
                    trivia.Kind() == SyntaxKind.WhitespaceTrivia ||
                    trivia.Kind() == SyntaxKind.EndOfLineTrivia)
                {
                    return false;
                }

                _touchedNoisyCharacterOnCurrentLine = true;
                _hasTrailingSpace = false;

                return false;
            }

            private bool ShouldFormat()
            {
                var index = -1;
                foreach (var trivia in _triviaList)
                {
                    index++;

                    // order in which these methods run has a side effect. don't change the order
                    // each method run
                    if (OnElastic(trivia) ||
                        OnWhitespace(trivia) ||
                        OnEndOfLine(trivia, index) ||
                        OnTouchedNoisyCharacter(trivia) ||
                        OnComment(trivia) ||
                        OnSkippedTokensOrText(trivia) ||
                        OnRegion(trivia, index) ||
                        OnPreprocessor(trivia, index) ||
                        OnDisabledTextTrivia(trivia, index))
                    {
                        return true;
                    }
                }

                return false;
            }

            private bool OnDisabledTextTrivia(SyntaxTrivia trivia, int index)
            {
                if (trivia.IsKind(SyntaxKind.DisabledTextTrivia))
                {
                    var triviaString = trivia.ToString();
                    if (!string.IsNullOrEmpty(triviaString) && SyntaxFacts.IsNewLine(triviaString.Last()))
                    {
                        ResetStateAfterNewLine(index);
                    }
                }

                return false;
            }

            private static bool ShouldFormatSingleLineDocumentationComment(int indentation, int tabSize, SyntaxTrivia trivia)
            {
                Debug.Assert(trivia.HasStructure);

                var xmlComment = (DocumentationCommentTriviaSyntax)trivia.GetStructure()!;

                var sawFirstOne = false;
                foreach (var token in xmlComment.DescendantTokens())
                {
                    foreach (var xmlTrivia in token.LeadingTrivia)
                    {
                        if (xmlTrivia.Kind() == SyntaxKind.DocumentationCommentExteriorTrivia)
                        {
                            // skip first one since its leading whitespace will belong to syntax tree's syntax token
                            // not xml doc comment's token
                            if (!sawFirstOne)
                            {
                                sawFirstOne = true;
                                break;
                            }

                            var xmlCommentText = xmlTrivia.ToString();

                            // "///" == 3.
                            if (xmlCommentText.GetColumnFromLineOffset(xmlCommentText.Length - 3, tabSize) != indentation)
                            {
                                return true;
                            }

                            break;
                        }
                    }
                }

                return false;
            }
        }
    }
}
