// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable disable

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
{
    protected delegate void SpanContextConfigAction(SpanEditHandlerBuilder editHandlerBuilder, ref ISpanChunkGenerator chunkGenerator);
    protected delegate void SpanContextConfigActionWithPreviousConfig(SpanEditHandlerBuilder editHandlerBuilder, ref ISpanChunkGenerator chunkGenerator, SpanContextConfigAction previousConfig);

    private readonly SyntaxListPool _pool = new SyntaxListPool();
    private readonly TokenizerView<TTokenizer> _tokenizer;
    private SyntaxListBuilder<SyntaxToken>? _tokenBuilder;

    protected SpanEditHandlerBuilder editHandlerBuilder;
    protected ISpanChunkGenerator chunkGenerator;

    // 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);
        editHandlerBuilder = new SpanEditHandlerBuilder(LanguageTokenizeString);
    }

    protected SyntaxListPool Pool => _pool;

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

            return _tokenBuilder.Value;
        }
    }

    protected SpanContextConfigAction 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();

        return commentBlock;
    }

    private void CommentSpanContextConfig(SpanEditHandlerBuilder editHandler, ref ISpanChunkGenerator generator)
    {
        generator = SpanChunkGenerator.Null;
        editHandlerBuilder.Reset();
        editHandlerBuilder.Tokenizer = 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 GetNodeWithEditHandler(SyntaxFactory.MarkupTextLiteral(tokens, chunkGenerator));
    }

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

        return GetNodeWithEditHandler(SyntaxFactory.MarkupEphemeralTextLiteral(tokens, chunkGenerator));
    }

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

        var metacode = SyntaxFactory.RazorMetaCode(tokens, SpanChunkGenerator.Null);
        chunkGenerator = SpanChunkGenerator.Null;
        editHandlerBuilder.AcceptedCharacters = accepted ?? AcceptedCharactersInternal.None;

        return GetNodeWithEditHandler(metacode);
    }

    protected TNode GetNodeWithEditHandler<TNode>(TNode node) where TNode : Syntax.GreenNode
    {
        var editHandlerBuilder = this.editHandlerBuilder.Build();
        Context.LastAcceptedCharacters = editHandlerBuilder.AcceptedCharacters;
        InitializeContext();
        var annotation = new Syntax.SyntaxAnnotation(SyntaxConstants.EditHandlerKind, editHandlerBuilder);

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

    protected DisposableAction PushSpanContextConfig()
    {
        return PushSpanContextConfig(newConfig: (SpanContextConfigActionWithPreviousConfig)null);
    }

    protected DisposableAction PushSpanContextConfig(SpanContextConfigAction newConfig)
    {
        return PushSpanContextConfig(newConfig == null ? null : (SpanEditHandlerBuilder span, ref ISpanChunkGenerator chunkGenerator, SpanContextConfigAction _) => newConfig(span, ref chunkGenerator));
    }

    protected DisposableAction PushSpanContextConfig(SpanContextConfigActionWithPreviousConfig  newConfig)
    {
        var old = SpanContextConfig;
        ConfigureSpanContext(newConfig);
        return new DisposableAction(() => SpanContextConfig = old);
    }

    protected void ConfigureSpanContext(SpanContextConfigAction config)
    {
        SpanContextConfig = config;
        InitializeContext();
    }

    protected void ConfigureSpanContext(SpanContextConfigActionWithPreviousConfig config)
    {
        var prev = SpanContextConfig;
        if (config == null)
        {
            SpanContextConfig = null;
        }
        else
        {
            SpanContextConfig = (SpanEditHandlerBuilder span, ref ISpanChunkGenerator chunkGenerator) => config(span, ref chunkGenerator, prev);
        }
        InitializeContext();
    }

    protected void InitializeContext()
    {
        SpanContextConfig?.Invoke(editHandlerBuilder, ref chunkGenerator);
    }
}
