// 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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Formatting.Rules;
using Microsoft.CodeAnalysis.Internal.Log;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Formatting
{
    internal partial class FormattingContext
    {
        private class InitialContextFinder
        {
            private readonly TokenStream _tokenStream;
            private readonly ChainedFormattingRules _formattingRules;
            private readonly SyntaxNode _rootNode;

            public InitialContextFinder(
                TokenStream tokenStream,
                ChainedFormattingRules formattingRules,
                SyntaxNode rootNode)
            {
                Contract.ThrowIfNull(tokenStream);
                Contract.ThrowIfNull(formattingRules);
                Contract.ThrowIfNull(rootNode);

                _tokenStream = tokenStream;
                _formattingRules = formattingRules;
                _rootNode = rootNode;
            }

            public (List<IndentBlockOperation> indentOperations, List<SuppressOperation>? suppressOperations) Do(SyntaxToken startToken, SyntaxToken endToken)
            {
                // we are formatting part of document, try to find initial context that formatting will be based on such as
                // initial indentation and etc.
                using (Logger.LogBlock(FunctionId.Formatting_ContextInitialization, CancellationToken.None))
                {
                    // first try to set initial indentation information
                    var initialIndentationOperations = this.GetInitialIndentBlockOperations(startToken, endToken);

                    // second try to set suppress wrapping regions
                    var initialSuppressOperations = GetInitialSuppressOperations(startToken, endToken);
                    if (initialSuppressOperations != null)
                    {
                        Debug.Assert(
                            initialSuppressOperations.IsEmpty() ||
                            initialSuppressOperations.All(
                                o => o.TextSpan.Contains(startToken.SpanStart) ||
                                     o.TextSpan.Contains(endToken.SpanStart)));
                    }

                    return (initialIndentationOperations, initialSuppressOperations);
                }
            }

            private List<IndentBlockOperation> GetInitialIndentBlockOperations(SyntaxToken startToken, SyntaxToken endToken)
            {
                var span = TextSpan.FromBounds(startToken.SpanStart, endToken.Span.End);
                var node = startToken.GetCommonRoot(endToken)!.GetParentWithBiggerSpan();
                var previous = (SyntaxNode?)null;

                // starting from the common node, move up to the parent
                var operations = new List<IndentBlockOperation>();
                var list = new List<IndentBlockOperation>();
                while (node != null)
                {
                    // get all operations for the nodes that contains the formatting span, but not ones contained by the span
                    node.DescendantNodesAndSelf(n => n != previous && n.Span.IntersectsWith(span) && !span.Contains(n.Span))
                        .Do(n =>
                            {
                                _formattingRules.AddIndentBlockOperations(list, n);
                                foreach (var element in list)
                                {
                                    if (element != null)
                                    {
                                        operations.Add(element);
                                    }
                                }

                                list.Clear();
                            });

                    // found some. use these as initial indentation
                    if (operations.Any(o => o.TextSpan.Contains(span)))
                    {
                        break;
                    }

                    previous = node;
                    node = node.Parent;
                }

                // make sure operations we have has effects over the formatting span
                operations.RemoveAll(o => o == null || !o.TextSpan.IntersectsWith(span));

                // we couldn't find anything
                // return initial location so that we can get base indentation correctly
                if (operations.Count == 0)
                {
                    operations.Add(new IndentBlockOperation(
                        startToken: _rootNode.GetFirstToken(includeZeroWidth: true),
                        endToken: _rootNode.GetLastToken(includeZeroWidth: true),
                        textSpan: _rootNode.FullSpan,
                        indentationDelta: 0,
                        option: IndentBlockOption.AbsolutePosition));

                    return operations;
                }

                operations.Sort(CommonFormattingHelpers.IndentBlockOperationComparer);
                return operations;
            }

            private List<SuppressOperation>? GetInitialSuppressOperations(SyntaxToken startToken, SyntaxToken endToken)
            {
                var noWrapList = this.GetInitialSuppressOperations(startToken, endToken, SuppressOption.NoWrapping);
                var noSpaceList = this.GetInitialSuppressOperations(startToken, endToken, SuppressOption.NoSpacing);

                var list = noWrapList.Combine(noSpaceList);
                if (list == null)
                {
                    return null;
                }

                list.Sort(CommonFormattingHelpers.SuppressOperationComparer);
                return list;
            }

            private List<SuppressOperation>? GetInitialSuppressOperations(SyntaxToken startToken, SyntaxToken endToken, SuppressOption mask)
            {
                var startList = this.GetInitialSuppressOperations(startToken, mask);
                var endList = this.GetInitialSuppressOperations(endToken, mask);

                return startList.Combine(endList);
            }

            private List<SuppressOperation>? GetInitialSuppressOperations(SyntaxToken token, SuppressOption mask)
            {
                var startNode = token.Parent;
                var startPosition = token.SpanStart;

                // starting from given token, move up to root until the first meaningful
                // operation has found
                var list = new List<SuppressOperation>();

                bool predicate(SuppressOperation o)
                {
                    if (o == null)
                    {
                        return true;
                    }

                    if (!o.TextSpan.Contains(startPosition))
                    {
                        return true;
                    }

                    if (o.ContainsElasticTrivia(_tokenStream) && !o.Option.IsOn(SuppressOption.IgnoreElasticWrapping))
                    {
                        return true;
                    }

                    if (!o.Option.IsMaskOn(mask))
                    {
                        return true;
                    }

                    return false;
                }

                var currentIndentationNode = startNode;
                while (currentIndentationNode != null)
                {
                    _formattingRules.AddSuppressOperations(list, currentIndentationNode);

                    list.RemoveAll(predicate);
                    if (list.Count > 0)
                    {
                        return list;
                    }

                    currentIndentationNode = currentIndentationNode.Parent;
                }

                return null;
            }
        }
    }
}
