// 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.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Razor.Language.Syntax.InternalSyntax;

namespace Microsoft.AspNetCore.Razor.Language.Legacy
{
    internal abstract class TokenizerBackedParser<TTokenizer> : ParserBase
        where TTokenizer : Tokenizer
    {
        private readonly SyntaxListPool _pool = new SyntaxListPool();
        private readonly TokenizerView<TTokenizer> _tokenizer;
        private SyntaxListBuilder<SyntaxToken>? _tokenBuilder;

        // Following four high traffic methods cached as using method groups would cause allocation on every invocation.
        protected static readonly Func<SyntaxToken, bool> IsSpacingToken = (token) =>
        {
            return token.Kind == SyntaxKind.Whitespace;
        };

        protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingNewLines = (token) =>
        {
            return IsSpacingToken(token) || token.Kind == SyntaxKind.NewLine;
        };

        protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingComments = (token) =>
        {
            return IsSpacingToken(token) || token.Kind == SyntaxKind.CSharpComment;
        };

        protected static readonly Func<SyntaxToken, bool> IsSpacingTokenIncludingNewLinesAndComments = (token) =>
        {
            return IsSpacingTokenIncludingNewLines(token) || token.Kind == SyntaxKind.CSharpComment;
        };

        protected TokenizerBackedParser(LanguageCharacteristics<TTokenizer> language, ParserContext context)
            : base(context)
        {
            Language = language;
            LanguageTokenizeString = Language.TokenizeString;

            var languageTokenizer = Language.CreateTokenizer(Context.Source);
            _tokenizer = new TokenizerView<TTokenizer>(languageTokenizer);
            SpanContext = new SpanContextBuilder();
        }

        protected SyntaxListPool Pool => _pool;

        protected SyntaxListBuilder<SyntaxToken> TokenBuilder
        {
            get
            {
                if (_tokenBuilder == null)
                {
                    var result = _pool.Allocate<SyntaxToken>();
                    _tokenBuilder = result.Builder;
                }

                return _tokenBuilder.Value;
            }
        }

        protected SpanContextBuilder SpanContext { get; private set; }

        protected Action<SpanContextBuilder> SpanContextConfig { get; set; }

        protected SyntaxToken CurrentToken
        {
            get { return _tokenizer.Current; }
        }

        protected SyntaxToken PreviousToken { get; private set; }

        protected SourceLocation CurrentStart => _tokenizer.Tokenizer.CurrentStart;

        protected bool EndOfFile
        {
            get { return _tokenizer.EndOfFile; }
        }

        protected LanguageCharacteristics<TTokenizer> Language { get; }
        protected Func<string, IEnumerable<SyntaxToken>> LanguageTokenizeString { get; }

        protected SyntaxToken Lookahead(int count)
        {
            if (count < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(count));
            }
            else if (count == 0)
            {
                return CurrentToken;
            }

            // We add 1 in order to store the current token.
            var tokens = new SyntaxToken[count + 1];
            var currentToken = CurrentToken;

            tokens[0] = currentToken;

            // We need to look forward "count" many times.
            for (var i = 1; i <= count; i++)
            {
                NextToken();
                tokens[i] = CurrentToken;
            }

            // Restore Tokenizer's location to where it was pointing before the look-ahead.
            for (var i = count; i >= 0; i--)
            {
                PutBack(tokens[i]);
            }

            // The PutBacks above will set CurrentToken to null. EnsureCurrent will set our CurrentToken to the
            // next token.
            EnsureCurrent();

            return tokens[count];
        }

        /// <summary>
        /// Looks forward until the specified condition is met.
        /// </summary>
        /// <param name="condition">A predicate accepting the token being evaluated and the list of tokens which have been looped through.</param>
        /// <returns>true, if the condition was met. false - if the condition wasn't met and the last token has already been processed.</returns>
        /// <remarks>The list of previous tokens is passed in the reverse order. So the last processed element will be the first one in the list.</remarks>
        protected bool LookaheadUntil(Func<SyntaxToken, IEnumerable<SyntaxToken>, bool> condition)
        {
            if (condition == null)
            {
                throw new ArgumentNullException(nameof(condition));
            }

            var matchFound = false;

            var tokens = new List<SyntaxToken>();
            tokens.Add(CurrentToken);

            while (true)
            {
                if (!NextToken())
                {
                    break;
                }

                tokens.Add(CurrentToken);
                if (condition(CurrentToken, tokens))
                {
                    matchFound = true;
                    break;
                }
            }

            // Restore Tokenizer's location to where it was pointing before the look-ahead.
            for (var i = tokens.Count - 1; i >= 0; i--)
            {
                PutBack(tokens[i]);
            }

            // The PutBacks above will set CurrentToken to null. EnsureCurrent will set our CurrentToken to the
            // next token.
            EnsureCurrent();

            return matchFound;
        }

        protected internal bool NextToken()
        {
            PreviousToken = CurrentToken;
            return _tokenizer.Next();
        }

        // Helpers
        [Conditional("DEBUG")]
        internal void Assert(SyntaxKind expectedType)
        {
            Debug.Assert(!EndOfFile && CurrentToken.Kind == expectedType);
        }

        protected internal void PutBack(SyntaxToken token)
        {
            if (token != null)
            {
                _tokenizer.PutBack(token);
            }
        }

        /// <summary>
        /// Put the specified tokens back in the input stream. The provided list MUST be in the ORDER THE TOKENS WERE READ. The
        /// list WILL be reversed and the Putback(SyntaxToken) will be called on each item.
        /// </summary>
        /// <remarks>
        /// If a document contains tokens: a, b, c, d, e, f
        /// and AcceptWhile or AcceptUntil is used to collect until d
        /// the list returned by AcceptWhile/Until will contain: a, b, c IN THAT ORDER
        /// that is the correct format for providing to this method. The caller of this method would,
        /// in that case, want to put c, b and a back into the stream, so "a, b, c" is the CORRECT order
        /// </remarks>
        protected internal void PutBack(IEnumerable<SyntaxToken> tokens)
        {
            foreach (var token in tokens.Reverse())
            {
                PutBack(token);
            }
        }

        protected internal void PutBack(IReadOnlyList<SyntaxToken> tokens)
        {
            for (int i = tokens.Count - 1; i >= 0; i--)
            {
                PutBack(tokens[i]);
            }
        }

        protected internal void PutCurrentBack()
        {
            if (!EndOfFile && CurrentToken != null)
            {
                PutBack(CurrentToken);
            }
        }

        protected internal bool NextIs(SyntaxKind type)
        {
            // Duplicated logic with NextIs(Func...) to prevent allocation
            var cur = CurrentToken;
            var result = false;
            if (NextToken())
            {
                result = (type == CurrentToken.Kind);
                PutCurrentBack();
            }

            PutBack(cur);
            EnsureCurrent();

            return result;
        }

        protected internal bool NextIs(params SyntaxKind[] types)
        {
            return NextIs(token => token != null && types.Any(t => t == token.Kind));
        }

        protected internal bool NextIs(Func<SyntaxToken, bool> condition)
        {
            var cur = CurrentToken;
            var result = false;
            if (NextToken())
            {
                result = condition(CurrentToken);
                PutCurrentBack();
            }

            PutBack(cur);
            EnsureCurrent();

            return result;
        }

        protected internal bool Was(SyntaxKind type)
        {
            return PreviousToken != null && PreviousToken.Kind == type;
        }

        protected internal bool At(SyntaxKind type)
        {
            return !EndOfFile && CurrentToken != null && CurrentToken.Kind == type;
        }

        protected bool TokenExistsAfterWhitespace(SyntaxKind kind, bool includeNewLines = true)
        {
            var tokenFound = false;
            var whitespace = ReadWhile(
                static (token, includeNewLines) =>
                    token.Kind == SyntaxKind.Whitespace || (includeNewLines && token.Kind == SyntaxKind.NewLine),
                includeNewLines);
            tokenFound = At(kind);

            PutCurrentBack();
            PutBack(whitespace);
            EnsureCurrent();

            return tokenFound;
        }

        protected bool EnsureCurrent()
        {
            if (CurrentToken == null)
            {
                return NextToken();
            }

            return true;
        }

        protected internal IReadOnlyList<SyntaxToken> ReadWhile(Func<SyntaxToken, bool> condition)
            => ReadWhile(static (token, condition) => condition(token), condition);

        protected internal IReadOnlyList<SyntaxToken> ReadWhile<TArg>(Func<SyntaxToken, TArg, bool> condition, TArg arg)
        {
            if (!EnsureCurrent() || !condition(CurrentToken, arg))
            {
                return Array.Empty<SyntaxToken>();
            }

            var result = new List<SyntaxToken>();
            do
            {
                result.Add(CurrentToken);
                NextToken();
            }
            while (EnsureCurrent() && condition(CurrentToken, arg));

            return result;
        }

        protected bool AtIdentifier(bool allowKeywords)
        {
            return CurrentToken != null &&
                   (Language.IsIdentifier(CurrentToken) ||
                    (allowKeywords && Language.IsKeyword(CurrentToken)));
        }

        protected RazorCommentBlockSyntax ParseRazorComment()
        {
            if (!Language.KnowsTokenType(KnownTokenType.CommentStart) ||
                !Language.KnowsTokenType(KnownTokenType.CommentStar) ||
                !Language.KnowsTokenType(KnownTokenType.CommentBody))
            {
                throw new InvalidOperationException(Resources.Language_Does_Not_Support_RazorComment);
            }

            RazorCommentBlockSyntax commentBlock;
            using (PushSpanContextConfig(CommentSpanContextConfig))
            {
                EnsureCurrent();
                var start = CurrentStart;
                Debug.Assert(At(SyntaxKind.RazorCommentTransition));
                var startTransition = EatExpectedToken(SyntaxKind.RazorCommentTransition);
                var startStar = EatExpectedToken(SyntaxKind.RazorCommentStar);
                var comment = GetOptionalToken(SyntaxKind.RazorCommentLiteral);
                if (comment == null)
                {
                    comment = SyntaxFactory.MissingToken(SyntaxKind.RazorCommentLiteral);
                }
                var endStar = GetOptionalToken(SyntaxKind.RazorCommentStar);
                if (endStar == null)
                {
                    var diagnostic = RazorDiagnosticFactory.CreateParsing_RazorCommentNotTerminated(
                        new SourceSpan(start, contentLength: 2 /* @* */));
                    endStar = SyntaxFactory.MissingToken(SyntaxKind.RazorCommentStar, diagnostic);
                    Context.ErrorSink.OnError(diagnostic);
                }
                var endTransition = GetOptionalToken(SyntaxKind.RazorCommentTransition);
                if (endTransition == null)
                {
                    if (!endStar.IsMissing)
                    {
                        var diagnostic = RazorDiagnosticFactory.CreateParsing_RazorCommentNotTerminated(
                            new SourceSpan(start, contentLength: 2 /* @* */));
                        Context.ErrorSink.OnError(diagnostic);
                        endTransition = SyntaxFactory.MissingToken(SyntaxKind.RazorCommentTransition, diagnostic);
                    }

                    endTransition = SyntaxFactory.MissingToken(SyntaxKind.RazorCommentTransition);
                }

                commentBlock = SyntaxFactory.RazorCommentBlock(startTransition, startStar, comment, endStar, endTransition);

                // Make sure we generate a marker symbol after a comment if necessary.
                if (!comment.IsMissing || !endStar.IsMissing || !endTransition.IsMissing)
                {
                    Context.LastAcceptedCharacters = AcceptedCharactersInternal.None;
                }
            }

            InitializeContext(SpanContext);

            return commentBlock;
        }

        private void CommentSpanContextConfig(SpanContextBuilder spanContext)
        {
            spanContext.ChunkGenerator = SpanChunkGenerator.Null;
            spanContext.EditHandler = SpanEditHandler.CreateDefault(LanguageTokenizeString);
        }

        protected SyntaxToken EatCurrentToken()
        {
            Debug.Assert(!EndOfFile && CurrentToken != null);
            var token = CurrentToken;
            NextToken();
            return token;
        }

        protected SyntaxToken EatExpectedToken(params SyntaxKind[] kinds)
        {
            Debug.Assert(!EndOfFile && CurrentToken != null && kinds.Contains(CurrentToken.Kind));
            var token = CurrentToken;
            NextToken();
            return token;
        }

        protected SyntaxToken GetOptionalToken(SyntaxKind kind)
        {
            if (At(kind))
            {
                var token = CurrentToken;
                NextToken();
                return token;
            }

            return null;
        }

        protected internal void AcceptWhile(SyntaxKind type)
        {
            AcceptWhile(static (token, type) => type == token.Kind, type);
        }

        // We want to avoid array allocations and enumeration where possible, so we use the same technique as string.Format
        protected internal void AcceptWhile(SyntaxKind type1, SyntaxKind type2)
        {
            AcceptWhile(static (token, arg) => arg.type1 == token.Kind || arg.type2 == token.Kind, (type1, type2));
        }

        protected internal void AcceptWhile(SyntaxKind type1, SyntaxKind type2, SyntaxKind type3)
        {
            AcceptWhile(static (token, arg) => arg.type1 == token.Kind || arg.type2 == token.Kind || arg.type3 == token.Kind, (type1, type2, type3));
        }

        protected internal void AcceptWhile(params SyntaxKind[] types)
        {
            AcceptWhile(static (token, types) => types.Any(expected => expected == token.Kind), types);
        }

        protected internal void AcceptUntil(SyntaxKind type)
        {
            AcceptWhile(static (token, type) => type != token.Kind, type);
        }

        // We want to avoid array allocations and enumeration where possible, so we use the same technique as string.Format
        protected internal void AcceptUntil(SyntaxKind type1, SyntaxKind type2)
        {
            AcceptWhile(static (token, arg) => arg.type1 != token.Kind && arg.type2 != token.Kind, (type1, type2));
        }

        protected internal void AcceptUntil(SyntaxKind type1, SyntaxKind type2, SyntaxKind type3)
        {
            AcceptWhile(static (token, arg) => arg.type1 != token.Kind && arg.type2 != token.Kind && arg.type3 != token.Kind, (type1, type2, type3));
        }

        protected internal void AcceptUntil(params SyntaxKind[] types)
        {
            AcceptWhile(static (token, types) => types.All(expected => expected != token.Kind), types);
        }

        protected internal void AcceptWhile(Func<SyntaxToken, bool> condition)
        {
            Accept(ReadWhile(condition));
        }

        protected internal void AcceptWhile<TArg>(Func<SyntaxToken, TArg, bool> condition, TArg arg)
        {
            Accept(ReadWhile(condition, arg));
        }

        protected internal void Accept(IReadOnlyList<SyntaxToken> tokens)
        {
            for(int i = 0; i < tokens.Count; i++)
            {
                var token = tokens[i];
                Accept(token);
            }
        }

        protected internal void Accept(SyntaxToken token)
        {
            if (token != null)
            {
                if (token.Kind == SyntaxKind.NewLine)
                {
                    Context.StartOfLine = true;
                }
                else if (token.Kind != SyntaxKind.Whitespace)
                {
                    Context.StartOfLine = false;
                }

                foreach (var error in token.GetDiagnostics())
                {
                    Context.ErrorSink.OnError(error);
                }

                TokenBuilder.Add(token);
            }
        }

        protected internal bool AcceptAll(params SyntaxKind[] kinds)
        {
            foreach (var kind in kinds)
            {
                if (CurrentToken == null || CurrentToken.Kind != kind)
                {
                    return false;
                }
                AcceptAndMoveNext();
            }
            return true;
        }

        protected internal bool AcceptAndMoveNext()
        {
            Accept(CurrentToken);
            return NextToken();
        }

        protected SyntaxList<SyntaxToken> Output()
        {
            var list = TokenBuilder.ToList();
            TokenBuilder.Clear();
            return list;
        }

        protected SyntaxToken AcceptWhitespaceInLines()
        {
            SyntaxToken lastWs = null;
            while (Language.IsWhitespace(CurrentToken) || Language.IsNewLine(CurrentToken))
            {
                // Capture the previous whitespace node
                if (lastWs != null)
                {
                    Accept(lastWs);
                }

                if (Language.IsWhitespace(CurrentToken))
                {
                    lastWs = CurrentToken;
                }
                else if (Language.IsNewLine(CurrentToken))
                {
                    // Accept newline and reset last whitespace tracker
                    Accept(CurrentToken);
                    lastWs = null;
                }

                NextToken();
            }

            return lastWs;
        }

        protected internal bool TryAccept(SyntaxKind type)
        {
            if (At(type))
            {
                AcceptAndMoveNext();
                return true;
            }
            return false;
        }

        protected internal void AcceptMarkerTokenIfNecessary()
        {
            if (TokenBuilder.Count == 0 && Context.LastAcceptedCharacters != AcceptedCharactersInternal.Any)
            {
                Accept(Language.CreateMarkerToken());
            }
        }

        protected MarkupTextLiteralSyntax OutputAsMarkupLiteral()
        {
            var tokens = Output();
            if (tokens.Count == 0)
            {
                return null;
            }

            return GetNodeWithSpanContext(SyntaxFactory.MarkupTextLiteral(tokens));
        }

        protected MarkupEphemeralTextLiteralSyntax OutputAsMarkupEphemeralLiteral()
        {
            var tokens = Output();
            if (tokens.Count == 0)
            {
                return null;
            }

            return GetNodeWithSpanContext(SyntaxFactory.MarkupEphemeralTextLiteral(tokens));
        }

        protected RazorMetaCodeSyntax OutputAsMetaCode(SyntaxList<SyntaxToken> tokens, AcceptedCharactersInternal? accepted = null)
        {
            if (tokens.Count == 0)
            {
                return null;
            }

            var metacode = SyntaxFactory.RazorMetaCode(tokens);
            SpanContext.ChunkGenerator = SpanChunkGenerator.Null;
            SpanContext.EditHandler.AcceptedCharacters = accepted ?? AcceptedCharactersInternal.None;

            return GetNodeWithSpanContext(metacode);
        }

        protected TNode GetNodeWithSpanContext<TNode>(TNode node) where TNode : Syntax.GreenNode
        {
            var spanContext = SpanContext.Build();
            Context.LastAcceptedCharacters = spanContext.EditHandler.AcceptedCharacters;
            InitializeContext(SpanContext);
            var annotation = new Syntax.SyntaxAnnotation(SyntaxConstants.SpanContextKind, spanContext);

            return (TNode)node.SetAnnotations(new[] { annotation });
        }

        protected IDisposable PushSpanContextConfig()
        {
            return PushSpanContextConfig(newConfig: (Action<SpanContextBuilder, Action<SpanContextBuilder>>)null);
        }

        protected IDisposable PushSpanContextConfig(Action<SpanContextBuilder> newConfig)
        {
            return PushSpanContextConfig(newConfig == null ? (Action<SpanContextBuilder, Action<SpanContextBuilder>>)null : (span, _) => newConfig(span));
        }

        protected IDisposable PushSpanContextConfig(Action<SpanContextBuilder, Action<SpanContextBuilder>> newConfig)
        {
            var old = SpanContextConfig;
            ConfigureSpanContext(newConfig);
            return new DisposableAction(() => SpanContextConfig = old);
        }

        protected void ConfigureSpanContext(Action<SpanContextBuilder> config)
        {
            SpanContextConfig = config;
            InitializeContext(SpanContext);
        }

        protected void ConfigureSpanContext(Action<SpanContextBuilder, Action<SpanContextBuilder>> config)
        {
            var prev = SpanContextConfig;
            if (config == null)
            {
                SpanContextConfig = null;
            }
            else
            {
                SpanContextConfig = span => config(span, prev);
            }
            InitializeContext(SpanContext);
        }

        protected void InitializeContext(SpanContextBuilder spanContext)
        {
            SpanContextConfig?.Invoke(spanContext);
        }
    }
}
