// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Web.Razor.Editor;
using System.Web.Razor.Generator;
using System.Web.Razor.Parser.SyntaxTree;
using System.Web.Razor.Resources;
using System.Web.Razor.Text;
using System.Web.Razor.Tokenizer;
using System.Web.Razor.Tokenizer.Symbols;

namespace System.Web.Razor.Parser
{
    public partial class VBCodeParser : TokenizerBackedParser<VBTokenizer, VBSymbol, VBSymbolType>
    {
        internal static ISet<string> DefaultKeywords = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
        {
            "functions",
            "code",
            "section",
            "do",
            "while",
            "if",
            "select",
            "for",
            "try",
            "with",
            "synclock",
            "using",
            "imports",
            "inherits",
            "option",
            "helper",
            "namespace",
            "class",
            "layout",
            "sessionstate"
        };

        private Dictionary<VBKeyword, Func<bool>> _keywordHandlers = new Dictionary<VBKeyword, Func<bool>>();
        private Dictionary<string, Func<bool>> _directiveHandlers = new Dictionary<string, Func<bool>>(StringComparer.OrdinalIgnoreCase);

        [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "Necessary state is initialized before calling virtual methods")]
        public VBCodeParser()
        {
            DirectParentIsCode = false;
            Keywords = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            SetUpKeywords();
            SetUpDirectives();
        }

        protected internal ISet<string> Keywords { get; private set; }

        protected override LanguageCharacteristics<VBTokenizer, VBSymbol, VBSymbolType> Language
        {
            get { return VBLanguageCharacteristics.Instance; }
        }

        protected override ParserBase OtherParser
        {
            get { return Context.MarkupParser; }
        }

        private bool IsNested { get; set; }
        private bool DirectParentIsCode { get; set; }

        protected override bool IsAtEmbeddedTransition(bool allowTemplatesAndComments, bool allowTransitions)
        {
            return (allowTransitions && Language.IsTransition(CurrentSymbol) && !Was(VBSymbolType.Dot)) ||
                   (allowTemplatesAndComments && Language.IsCommentStart(CurrentSymbol)) ||
                   (Language.IsTransition(CurrentSymbol) && NextIs(VBSymbolType.Transition));
        }

        protected override void HandleEmbeddedTransition()
        {
            HandleEmbeddedTransition(null);
        }

        protected void HandleEmbeddedTransition(VBSymbol lastWhiteSpace)
        {
            if (At(VBSymbolType.RazorCommentTransition))
            {
                Accept(lastWhiteSpace);
                RazorComment();
            }
            else if ((At(VBSymbolType.Transition) && !Was(VBSymbolType.Dot)))
            {
                HandleTransition(lastWhiteSpace);
            }
        }

        public override void ParseBlock()
        {
            if (Context == null)
            {
                throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set);
            }
            using (PushSpanConfig())
            {
                if (Context == null)
                {
                    throw new InvalidOperationException(RazorResources.Parser_Context_Not_Set);
                }

                Initialize(Span);
                NextToken();
                using (Context.StartBlock())
                {
                    IEnumerable<VBSymbol> syms = ReadWhile(sym => sym.Type == VBSymbolType.WhiteSpace);
                    if (At(VBSymbolType.Transition))
                    {
                        Accept(syms);
                        Span.CodeGenerator = new StatementCodeGenerator();
                        Output(SpanKind.Code);
                    }
                    else
                    {
                        PutBack(syms);
                        EnsureCurrent();
                    }

                    // Allow a transition span, but don't require it
                    if (Optional(VBSymbolType.Transition))
                    {
                        Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
                        Span.CodeGenerator = SpanCodeGenerator.Null;
                        Output(SpanKind.Transition);
                    }

                    Context.CurrentBlock.Type = BlockType.Expression;
                    Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();

                    // Determine the type of the block
                    bool isComplete = false;
                    Action<SpanBuilder> config = null;
                    if (!EndOfFile)
                    {
                        switch (CurrentSymbol.Type)
                        {
                            case VBSymbolType.Identifier:
                                if (!TryDirectiveBlock(ref isComplete))
                                {
                                    ImplicitExpression();
                                }
                                break;
                            case VBSymbolType.LeftParenthesis:
                                isComplete = ExplicitExpression();
                                break;
                            case VBSymbolType.Keyword:
                                Context.CurrentBlock.Type = BlockType.Statement;
                                Context.CurrentBlock.CodeGenerator = BlockCodeGenerator.Null;
                                isComplete = KeywordBlock();
                                break;
                            case VBSymbolType.WhiteSpace:
                            case VBSymbolType.NewLine:
                                config = ImplictExpressionSpanConfig;
                                Context.OnError(CurrentLocation,
                                                RazorResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_VB);
                                break;
                            default:
                                config = ImplictExpressionSpanConfig;
                                Context.OnError(CurrentLocation,
                                                RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_VB,
                                                CurrentSymbol.Content);
                                break;
                        }
                    }
                    else
                    {
                        config = ImplictExpressionSpanConfig;
                        Context.OnError(CurrentLocation,
                                        RazorResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock);
                    }
                    using (PushSpanConfig(config))
                    {
                        if (!isComplete && Span.Symbols.Count == 0 && Context.LastAcceptedCharacters != AcceptedCharacters.Any)
                        {
                            AddMarkerSymbolIfNecessary();
                        }
                        Output(SpanKind.Code);
                        PutCurrentBack();
                    }
                }
            }
        }

        private void ImplictExpressionSpanConfig(SpanBuilder span)
        {
            span.CodeGenerator = new ExpressionCodeGenerator();
            span.EditHandler = new ImplicitExpressionEditHandler(
                Language.TokenizeString,
                Keywords,
                acceptTrailingDot: DirectParentIsCode)
            {
                AcceptedCharacters = AcceptedCharacters.NonWhiteSpace
            };
        }

        private Action<SpanBuilder> StatementBlockSpanConfiguration(SpanCodeGenerator codeGenerator)
        {
            return span =>
            {
                span.Kind = SpanKind.Code;
                span.CodeGenerator = codeGenerator;
                span.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString);
            };
        }

        // Pass "complete" flag by ref, not out because some paths may not change it.
        private bool TryDirectiveBlock(ref bool complete)
        {
            Assert(VBSymbolType.Identifier);
            Func<bool> handler;
            if (_directiveHandlers.TryGetValue(CurrentSymbol.Content, out handler))
            {
                Context.CurrentBlock.CodeGenerator = BlockCodeGenerator.Null;
                complete = handler();
                return true;
            }
            return false;
        }

        private bool KeywordBlock()
        {
            Assert(VBSymbolType.Keyword);
            Func<bool> handler;
            if (_keywordHandlers.TryGetValue(CurrentSymbol.Keyword.Value, out handler))
            {
                Span.CodeGenerator = new StatementCodeGenerator();
                Context.CurrentBlock.Type = BlockType.Statement;
                return handler();
            }
            else
            {
                ImplicitExpression();
                return false;
            }
        }

        private bool ExplicitExpression()
        {
            Context.CurrentBlock.Type = BlockType.Expression;
            Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
            SourceLocation start = CurrentLocation;
            Expected(VBSymbolType.LeftParenthesis);
            Span.CodeGenerator = SpanCodeGenerator.Null;
            Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
            Output(SpanKind.MetaCode);

            Span.CodeGenerator = new ExpressionCodeGenerator();
            using (PushSpanConfig(span => span.CodeGenerator = new ExpressionCodeGenerator()))
            {
                if (!Balance(BalancingModes.NoErrorOnFailure |
                             BalancingModes.BacktrackOnFailure |
                             BalancingModes.AllowCommentsAndTemplates,
                             VBSymbolType.LeftParenthesis,
                             VBSymbolType.RightParenthesis,
                             start))
                {
                    Context.OnError(start,
                                    RazorResources.ParseError_Expected_EndOfBlock_Before_EOF,
                                    RazorResources.BlockName_ExplicitExpression,
                                    VBSymbol.GetSample(VBSymbolType.RightParenthesis),
                                    VBSymbol.GetSample(VBSymbolType.LeftParenthesis));
                    AcceptUntil(VBSymbolType.NewLine);
                    AddMarkerSymbolIfNecessary();
                    Output(SpanKind.Code);
                    PutCurrentBack();
                    return false;
                }
                else
                {
                    AddMarkerSymbolIfNecessary();
                    Output(SpanKind.Code);
                    Expected(VBSymbolType.RightParenthesis);
                    Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
                    Span.CodeGenerator = SpanCodeGenerator.Null;
                    Output(SpanKind.MetaCode);
                    PutCurrentBack();
                    return true;
                }
            }
        }

        private void ImplicitExpression()
        {
            Context.CurrentBlock.Type = BlockType.Expression;
            Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
            using (PushSpanConfig(ImplictExpressionSpanConfig))
            {
                Expected(VBSymbolType.Identifier, VBSymbolType.Keyword);
                Span.CodeGenerator = new ExpressionCodeGenerator();
                while (!EndOfFile)
                {
                    switch (CurrentSymbol.Type)
                    {
                        case VBSymbolType.LeftParenthesis:
                            SourceLocation start = CurrentLocation;
                            AcceptAndMoveNext();

                            Action<SpanBuilder> oldConfig = SpanConfig;
                            using (PushSpanConfig())
                            {
                                ConfigureSpan(span =>
                                {
                                    oldConfig(span);
                                    span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
                                });
                                Balance(BalancingModes.AllowCommentsAndTemplates,
                                        VBSymbolType.LeftParenthesis,
                                        VBSymbolType.RightParenthesis,
                                        start);
                            }
                            if (Optional(VBSymbolType.RightParenthesis))
                            {
                                Span.EditHandler.AcceptedCharacters = AcceptedCharacters.NonWhiteSpace;
                            }
                            break;
                        case VBSymbolType.Dot:
                            VBSymbol dot = CurrentSymbol;
                            NextToken();
                            if (At(VBSymbolType.Identifier) || At(VBSymbolType.Keyword))
                            {
                                Accept(dot);
                                AcceptAndMoveNext();
                            }
                            else if (At(VBSymbolType.Transition))
                            {
                                VBSymbol at = CurrentSymbol;
                                NextToken();
                                if (At(VBSymbolType.Identifier) || At(VBSymbolType.Keyword))
                                {
                                    Accept(dot);
                                    Accept(at);
                                    AcceptAndMoveNext();
                                }
                                else
                                {
                                    PutBack(at);
                                    PutBack(dot);
                                }
                            }
                            else
                            {
                                PutCurrentBack();
                                if (IsNested)
                                {
                                    Accept(dot);
                                }
                                else
                                {
                                    PutBack(dot);
                                }
                                return;
                            }
                            break;
                        default:
                            PutCurrentBack();
                            return;
                    }
                }
            }
        }

        protected void MapKeyword(VBKeyword keyword, Func<bool> action)
        {
            _keywordHandlers[keyword] = action;
            Keywords.Add(keyword.ToString());
        }

        protected void MapDirective(string directive, Func<bool> action)
        {
            _directiveHandlers[directive] = action;
            Keywords.Add(directive);
        }

        [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", Justification = "This only occurs in Release builds, where this method is empty by design")]
        [Conditional("DEBUG")]
        protected void Assert(VBKeyword keyword)
        {
            Debug.Assert(CurrentSymbol.Type == VBSymbolType.Keyword && CurrentSymbol.Keyword == keyword);
        }

        protected bool At(VBKeyword keyword)
        {
            return At(VBSymbolType.Keyword) && CurrentSymbol.Keyword == keyword;
        }

        protected void OtherParserBlock()
        {
            OtherParserBlock(null, null);
        }

        protected void OtherParserBlock(string startSequence, string endSequence)
        {
            using (PushSpanConfig())
            {
                if (Span.Symbols.Count > 0)
                {
                    Output(SpanKind.Code);
                }

                Context.SwitchActiveParser();

                bool old = DirectParentIsCode;
                DirectParentIsCode = false;

                Debug.Assert(ReferenceEquals(Context.ActiveParser, Context.MarkupParser));
                if (!String.IsNullOrEmpty(startSequence) || !String.IsNullOrEmpty(endSequence))
                {
                    Context.MarkupParser.ParseSection(Tuple.Create(startSequence, endSequence), false);
                }
                else
                {
                    Context.MarkupParser.ParseBlock();
                }

                DirectParentIsCode = old;

                Context.SwitchActiveParser();
                EnsureCurrent();
            }
            Initialize(Span);
        }

        protected void HandleTransition(VBSymbol lastWhiteSpace)
        {
            if (At(VBSymbolType.RazorCommentTransition))
            {
                Accept(lastWhiteSpace);
                RazorComment();
                return;
            }

            // Check the next character
            VBSymbol transition = CurrentSymbol;
            NextToken();
            if (At(VBSymbolType.LessThan) || At(VBSymbolType.Colon))
            {
                // Put the transition back
                PutCurrentBack();
                PutBack(transition);

                // If we're in design-time mode, accept the whitespace, otherwise put it back
                if (Context.DesignTimeMode)
                {
                    Accept(lastWhiteSpace);
                }
                else
                {
                    PutBack(lastWhiteSpace);
                }

                // Switch to markup
                OtherParserBlock();
            }
            else if (At(VBSymbolType.Transition))
            {
                if (Context.IsWithin(BlockType.Template))
                {
                    Context.OnError(transition.Start, RazorResources.ParseError_InlineMarkup_Blocks_Cannot_Be_Nested);
                }
                Accept(lastWhiteSpace);
                VBSymbol transition2 = CurrentSymbol;
                NextToken();
                if (At(VBSymbolType.LessThan) || At(VBSymbolType.Colon))
                {
                    PutCurrentBack();
                    PutBack(transition2);
                    PutBack(transition);
                    Output(SpanKind.Code);

                    // Start a template block and switch to Markup
                    using (Context.StartBlock(BlockType.Template))
                    {
                        Context.CurrentBlock.CodeGenerator = new TemplateBlockCodeGenerator();
                        OtherParserBlock();
                        Initialize(Span);
                    }
                }
                else
                {
                    Accept(transition);
                    Accept(transition2);
                }
            }
            else
            {
                Accept(lastWhiteSpace);

                PutCurrentBack();
                PutBack(transition);

                bool old = IsNested;
                IsNested = true;
                NestedBlock();
                IsNested = old;
            }
        }

        protected override void OutputSpanBeforeRazorComment()
        {
            Output(SpanKind.Code);
        }

        protected bool ReservedWord()
        {
            Context.CurrentBlock.Type = BlockType.Directive;
            Context.OnError(CurrentLocation, RazorResources.ParseError_ReservedWord, CurrentSymbol.Content);
            Span.CodeGenerator = SpanCodeGenerator.Null;
            AcceptAndMoveNext();
            Output(SpanKind.MetaCode, AcceptedCharacters.None);
            return true;
        }

        protected void NestedBlock()
        {
            using (PushSpanConfig())
            {
                Output(SpanKind.Code);

                bool old = DirectParentIsCode;
                DirectParentIsCode = true;

                ParseBlock();

                DirectParentIsCode = old;
            }
            Initialize(Span);
        }

        protected bool Required(VBSymbolType expected, string errorBase)
        {
            if (!Optional(expected))
            {
                Context.OnError(CurrentLocation, errorBase, GetCurrentSymbolDisplay());
                return false;
            }
            return true;
        }

        protected bool Optional(VBKeyword keyword)
        {
            if (At(keyword))
            {
                AcceptAndMoveNext();
                return true;
            }
            return false;
        }

        protected void AcceptVBSpaces()
        {
            Accept(ReadVBSpacesLazy());
        }

        protected IEnumerable<VBSymbol> ReadVBSpaces()
        {
            return ReadVBSpacesLazy().ToList();
        }

        public bool IsDirectiveDefined(string directive)
        {
            return _directiveHandlers.ContainsKey(directive);
        }

        private IEnumerable<VBSymbol> ReadVBSpacesLazy()
        {
            foreach (var symbol in ReadWhileLazy(sym => sym.Type == VBSymbolType.WhiteSpace))
            {
                yield return symbol;
            }
            while (At(VBSymbolType.LineContinuation))
            {
                int bookmark = CurrentLocation.AbsoluteIndex;
                VBSymbol under = CurrentSymbol;
                NextToken();
                if (At(VBSymbolType.NewLine))
                {
                    yield return under;
                    yield return CurrentSymbol;
                    NextToken();
                    foreach (var symbol in ReadVBSpaces())
                    {
                        yield return symbol;
                    }
                }
                else
                {
                    Context.Source.Position = bookmark;
                    NextToken();
                    yield break;
                }
            }
        }
    }
}
