// 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 Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Formatting.Rules
{
    internal class BaseIndentationFormattingRule : AbstractFormattingRule
    {
        private readonly AbstractFormattingRule? _vbHelperFormattingRule;
        private readonly int _baseIndentation;
        private readonly SyntaxToken _token1;
        private readonly SyntaxToken _token2;
        private readonly SyntaxNode? _commonNode;
        private readonly TextSpan _span;

        public BaseIndentationFormattingRule(SyntaxNode root, TextSpan span, int baseIndentation, AbstractFormattingRule? vbHelperFormattingRule = null)
        {
            _span = span;
            SetInnermostNodeForSpan(root, ref _span, out _token1, out _token2, out _commonNode);

            _baseIndentation = baseIndentation;
            _vbHelperFormattingRule = vbHelperFormattingRule;
        }

        public override void AddIndentBlockOperations(List<IndentBlockOperation> list, SyntaxNode node, in NextIndentBlockOperationAction nextOperation)
        {
            // for the common node itself, return absolute indentation
            if (_commonNode == node)
            {
                // TODO: If the first line of the span includes a node, we want to align with the position of that node 
                // in the primary buffer.  That's what Dev12 does for C#, but it doesn't match Roslyn's current model
                // of each statement being formatted independently with respect to it's parent.
                list.Add(new IndentBlockOperation(_token1, _token2, _span, _baseIndentation, IndentBlockOption.AbsolutePosition));
            }
            else if (node.Span.Contains(_span))
            {
                // any node bigger than our span is ignored.
                return;
            }

            // Add everything to the list.
            AddNextIndentBlockOperations(list, node, in nextOperation);

            // Filter out everything that encompasses our span.
            AdjustIndentBlockOperation(list);
        }

        private void AddNextIndentBlockOperations(List<IndentBlockOperation> list, SyntaxNode node, in NextIndentBlockOperationAction nextOperation)
        {
            if (_vbHelperFormattingRule == null)
            {
                base.AddIndentBlockOperations(list, node, in nextOperation);
                return;
            }

            _vbHelperFormattingRule.AddIndentBlockOperations(list, node, in nextOperation);
        }

        private void AdjustIndentBlockOperation(List<IndentBlockOperation> list)
        {
            list.RemoveOrTransformAll(
                (operation, self) =>
                {
                    // already filtered out operation
                    if (operation == null)
                    {
                        return null;
                    }

                    // if span is same as us, make sure we only include ourselves.
                    if (self._span == operation.TextSpan && !self.Myself(operation))
                    {
                        return null;
                    }

                    // inside of us, skip it.
                    if (self._span.Contains(operation.TextSpan))
                    {
                        return operation;
                    }

                    // throw away operation that encloses ourselves
                    if (operation.TextSpan.Contains(self._span))
                    {
                        return null;
                    }

                    // now we have an interesting case where indentation block intersects with us.
                    // this can happen if code is split in two different script blocks or nuggets.
                    // here, we will re-adjust block to be contained within our span.
                    if (operation.TextSpan.IntersectsWith(self._span))
                    {
                        return self.CloneAndAdjustFormattingOperation(operation);
                    }

                    return operation;
                },
                this);
        }

        private bool Myself(IndentBlockOperation operation)
        {
            return operation.TextSpan == _span &&
                   operation.StartToken == _token1 &&
                   operation.EndToken == _token2 &&
                   operation.IndentationDeltaOrPosition == _baseIndentation &&
                   operation.Option == IndentBlockOption.AbsolutePosition;
        }

        private IndentBlockOperation CloneAndAdjustFormattingOperation(IndentBlockOperation operation)
        {
            switch (operation.Option & IndentBlockOption.PositionMask)
            {
                case IndentBlockOption.RelativeToFirstTokenOnBaseTokenLine:
                    return FormattingOperations.CreateRelativeIndentBlockOperation(operation.BaseToken, operation.StartToken, operation.EndToken, AdjustTextSpan(operation.TextSpan), operation.IndentationDeltaOrPosition, operation.Option);
                case IndentBlockOption.RelativePosition:
                case IndentBlockOption.AbsolutePosition:
                    return FormattingOperations.CreateIndentBlockOperation(operation.StartToken, operation.EndToken, AdjustTextSpan(operation.TextSpan), operation.IndentationDeltaOrPosition, operation.Option);
                default:
                    throw ExceptionUtilities.UnexpectedValue(operation.Option);
            }
        }

        private TextSpan AdjustTextSpan(TextSpan textSpan)
            => TextSpan.FromBounds(Math.Max(_span.Start, textSpan.Start), Math.Min(_span.End, textSpan.End));

        private static void SetInnermostNodeForSpan(SyntaxNode root, ref TextSpan span, out SyntaxToken token1, out SyntaxToken token2, out SyntaxNode? commonNode)
        {
            commonNode = null;

            GetTokens(root, span, out token1, out token2);

            span = GetSpanFromTokens(span, token1, token2);

            if (token1.RawKind == 0 || token2.RawKind == 0)
            {
                return;
            }

            commonNode = token1.GetCommonRoot(token2);
        }

        private static void GetTokens(SyntaxNode root, TextSpan span, out SyntaxToken token1, out SyntaxToken token2)
        {
            // get tokens within given span
            token1 = root.FindToken(span.Start);
            token2 = root.FindTokenFromEnd(span.End);

            // It is possible the given span doesn't have any tokens in them. In that case, 
            // make tokens to be the adjacent ones to the given span.
            if (span.End < token1.Span.Start)
            {
                token1 = token1.GetPreviousToken();
            }

            if (token2.Span.End < span.Start)
            {
                token2 = token2.GetNextToken();
            }
        }

        private static TextSpan GetSpanFromTokens(TextSpan span, SyntaxToken token1, SyntaxToken token2)
        {
            var tree = token1.SyntaxTree;
            RoslynDebug.AssertNotNull(tree);

            // adjust span to include all whitespace before and after the given span.
            var start = token1.Span.End;

            // current token is inside of the given span, get previous token's end position
            if (span.Start <= token1.Span.Start)
            {
                token1 = token1.GetPreviousToken();
                start = token1.Span.End;

                // If token1, that was passed, is the first visible token of the tree then we want to
                // the beginning of the span to start from the beginning of the tree
                if (token1.RawKind == 0)
                {
                    start = 0;
                }
            }

            var end = token2.Span.Start;

            // current token is inside of the given span, get next token's start position.
            if (token2.Span.End <= span.End)
            {
                token2 = token2.GetNextToken();
                end = token2.Span.Start;

                // If token2, that was passed, was the last visible token of the tree then we want the
                // span to expand till the end of the tree
                if (token2.RawKind == 0)
                {
                    end = tree.Length;
                }
            }

            if (token1.Equals(token2) && end < start)
            {
                // This can happen if `token1.Span` is larger than `span` on each end (due to trivia) and occurs when
                // only a single token is projected into a buffer and the projection is sandwiched between two other
                // projections into the same backing buffer.  An example of this is during broken code scenarios when
                // typing certain Razor `@` directives.
                var temp = end;
                end = start;
                start = temp;
            }

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