// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
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.Tokenizer;
using System.Web.Razor.Tokenizer.Symbols;

namespace System.Web.Razor.Parser
{
    public partial class CSharpCodeParser : TokenizerBackedParser<CSharpTokenizer, CSharpSymbol, CSharpSymbolType>
    {
        internal static readonly int UsingKeywordLength = 5; // using

        internal static ISet<string> DefaultKeywords = new HashSet<string>()
        {
            "if",
            "do",
            "try",
            "for",
            "foreach",
            "while",
            "switch",
            "lock",
            "using",
            "section",
            "inherits",
            "helper",
            "functions",
            "namespace",
            "class",
            "layout",
            "sessionstate"
        };

        private Dictionary<string, Action> _directiveParsers = new Dictionary<string, Action>();
        private Dictionary<CSharpKeyword, Action<bool>> _keywordParsers = new Dictionary<CSharpKeyword, Action<bool>>();

        public CSharpCodeParser()
        {
            Keywords = new HashSet<string>();
            SetUpKeywords();
            SetupDirectives();
        }

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

        public bool IsNested { get; set; }

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

        protected override LanguageCharacteristics<CSharpTokenizer, CSharpSymbol, CSharpSymbolType> Language
        {
            get { return CSharpLanguageCharacteristics.Instance; }
        }

        protected void MapDirectives(Action handler, params string[] directives)
        {
            foreach (string directive in directives)
            {
                _directiveParsers.Add(directive, handler);
                Keywords.Add(directive);
            }
        }

        protected bool TryGetDirectiveHandler(string directive, out Action handler)
        {
            return _directiveParsers.TryGetValue(directive, out handler);
        }

        private void MapKeywords(Action<bool> handler, params CSharpKeyword[] keywords)
        {
            MapKeywords(handler, topLevel: true, keywords: keywords);
        }

        private void MapKeywords(Action<bool> handler, bool topLevel, params CSharpKeyword[] keywords)
        {
            foreach (CSharpKeyword keyword in keywords)
            {
                _keywordParsers.Add(keyword, handler);
                if (topLevel)
                {
                    Keywords.Add(CSharpLanguageCharacteristics.GetKeyword(keyword));
                }
            }
        }

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

        protected internal bool At(CSharpKeyword keyword)
        {
            return At(CSharpSymbolType.Keyword) && CurrentSymbol.Keyword.HasValue && CurrentSymbol.Keyword.Value == keyword;
        }

        protected internal bool AcceptIf(CSharpKeyword keyword)
        {
            if (At(keyword))
            {
                AcceptAndMoveNext();
                return true;
            }
            return false;
        }

        protected static Func<CSharpSymbol, bool> IsSpacingToken(bool includeNewLines, bool includeComments)
        {
            return sym => sym.Type == CSharpSymbolType.WhiteSpace ||
                          (includeNewLines && sym.Type == CSharpSymbolType.NewLine) ||
                          (includeComments && sym.Type == CSharpSymbolType.Comment);
        }

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

                // Unless changed, the block is a statement block
                using (Context.StartBlock(BlockType.Statement))
                {
                    NextToken();

                    AcceptWhile(IsSpacingToken(includeNewLines: true, includeComments: true));

                    CSharpSymbol current = CurrentSymbol;
                    if (At(CSharpSymbolType.StringLiteral) && CurrentSymbol.Content.Length > 0 && CurrentSymbol.Content[0] == SyntaxConstants.TransitionCharacter)
                    {
                        Tuple<CSharpSymbol, CSharpSymbol> split = Language.SplitSymbol(CurrentSymbol, 1, CSharpSymbolType.Transition);
                        current = split.Item1;
                        Context.Source.Position = split.Item2.Start.AbsoluteIndex;
                        NextToken();
                    }
                    else if (At(CSharpSymbolType.Transition))
                    {
                        NextToken();
                    }

                    // Accept "@" if we see it, but if we don't, that's OK. We assume we were started for a good reason
                    if (current.Type == CSharpSymbolType.Transition)
                    {
                        if (Span.Symbols.Count > 0)
                        {
                            Output(SpanKind.Code);
                        }
                        AtTransition(current);
                    }
                    else
                    {
                        // No "@" => Jump straight to AfterTransition
                        AfterTransition();
                    }
                    Output(SpanKind.Code);
                }
            }
        }

        private void DefaultSpanConfig(SpanBuilder span)
        {
            span.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString);
            span.CodeGenerator = new StatementCodeGenerator();
        }

        private void AtTransition(CSharpSymbol current)
        {
            Debug.Assert(current.Type == CSharpSymbolType.Transition);
            Accept(current);
            Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
            Span.CodeGenerator = SpanCodeGenerator.Null;

            // Output the "@" span and continue here
            Output(SpanKind.Transition);
            AfterTransition();
        }

        private void AfterTransition()
        {
            using (PushSpanConfig(DefaultSpanConfig))
            {
                EnsureCurrent();
                // What type of block is this?
                if (!EndOfFile)
                {
                    if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis)
                    {
                        Context.CurrentBlock.Type = BlockType.Expression;
                        Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
                        ExplicitExpression();
                        return;
                    }
                    else if (CurrentSymbol.Type == CSharpSymbolType.Identifier)
                    {
                        Action handler;
                        if (TryGetDirectiveHandler(CurrentSymbol.Content, out handler))
                        {
                            Span.CodeGenerator = SpanCodeGenerator.Null;
                            handler();
                            return;
                        }
                        else
                        {
                            Context.CurrentBlock.Type = BlockType.Expression;
                            Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
                            ImplicitExpression();
                            return;
                        }
                    }
                    else if (CurrentSymbol.Type == CSharpSymbolType.Keyword)
                    {
                        KeywordBlock(topLevel: true);
                        return;
                    }
                    else if (CurrentSymbol.Type == CSharpSymbolType.LeftBrace)
                    {
                        VerbatimBlock();
                        return;
                    }
                }

                // Invalid character
                Context.CurrentBlock.Type = BlockType.Expression;
                Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();
                AddMarkerSymbolIfNecessary();
                Span.CodeGenerator = new ExpressionCodeGenerator();
                Span.EditHandler = new ImplicitExpressionEditHandler(
                    Language.TokenizeString,
                    DefaultKeywords,
                    acceptTrailingDot: IsNested)
                {
                    AcceptedCharacters = AcceptedCharacters.NonWhiteSpace
                };
                if (At(CSharpSymbolType.WhiteSpace) || At(CSharpSymbolType.NewLine))
                {
                    Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_WhiteSpace_At_Start_Of_CodeBlock_CS);
                }
                else if (EndOfFile)
                {
                    Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_EndOfFile_At_Start_Of_CodeBlock);
                }
                else
                {
                    Context.OnError(CurrentLocation, RazorResources.ParseError_Unexpected_Character_At_Start_Of_CodeBlock_CS, CurrentSymbol.Content);
                }
                PutCurrentBack();
            }
        }

        private void VerbatimBlock()
        {
            Assert(CSharpSymbolType.LeftBrace);
            Block block = new Block(RazorResources.BlockName_Code, CurrentLocation);
            AcceptAndMoveNext();

            // Set up the "{" span and output
            Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
            Span.CodeGenerator = SpanCodeGenerator.Null;
            Output(SpanKind.MetaCode);

            // Set up auto-complete and parse the code block
            AutoCompleteEditHandler editHandler = new AutoCompleteEditHandler(Language.TokenizeString);
            Span.EditHandler = editHandler;
            CodeBlock(false, block);

            Span.CodeGenerator = new StatementCodeGenerator();
            AddMarkerSymbolIfNecessary();
            if (!At(CSharpSymbolType.RightBrace))
            {
                editHandler.AutoCompleteString = "}";
            }
            Output(SpanKind.Code);

            if (Optional(CSharpSymbolType.RightBrace))
            {
                // Set up the "}" span
                Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
                Span.CodeGenerator = SpanCodeGenerator.Null;
            }

            if (!At(CSharpSymbolType.WhiteSpace) && !At(CSharpSymbolType.NewLine))
            {
                PutCurrentBack();
            }

            CompleteBlock(insertMarkerIfNecessary: false);
            Output(SpanKind.MetaCode);
        }

        private void ImplicitExpression()
        {
            Context.CurrentBlock.Type = BlockType.Expression;
            Context.CurrentBlock.CodeGenerator = new ExpressionCodeGenerator();

            using (PushSpanConfig(span =>
            {
                span.EditHandler = new ImplicitExpressionEditHandler(Language.TokenizeString, Keywords, acceptTrailingDot: IsNested);
                span.EditHandler.AcceptedCharacters = AcceptedCharacters.NonWhiteSpace;
                span.CodeGenerator = new ExpressionCodeGenerator();
            }))
            {
                do
                {
                    if (AtIdentifier(allowKeywords: true))
                    {
                        AcceptAndMoveNext();
                    }
                }
                while (MethodCallOrArrayIndex());

                PutCurrentBack();
                Output(SpanKind.Code);
            }
        }

        private bool MethodCallOrArrayIndex()
        {
            if (!EndOfFile)
            {
                if (CurrentSymbol.Type == CSharpSymbolType.LeftParenthesis || CurrentSymbol.Type == CSharpSymbolType.LeftBracket)
                {
                    // If we end within "(", whitespace is fine
                    Span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;

                    CSharpSymbolType right;
                    bool success;

                    using (PushSpanConfig((span, prev) =>
                    {
                        prev(span);
                        span.EditHandler.AcceptedCharacters = AcceptedCharacters.Any;
                    }))
                    {
                        right = Language.FlipBracket(CurrentSymbol.Type);
                        success = Balance(BalancingModes.BacktrackOnFailure | BalancingModes.AllowCommentsAndTemplates);
                    }

                    if (!success)
                    {
                        AcceptUntil(CSharpSymbolType.LessThan);
                    }
                    if (At(right))
                    {
                        AcceptAndMoveNext();
                        Span.EditHandler.AcceptedCharacters = AcceptedCharacters.NonWhiteSpace;
                    }
                    return MethodCallOrArrayIndex();
                }
                if (CurrentSymbol.Type == CSharpSymbolType.Dot)
                {
                    CSharpSymbol dot = CurrentSymbol;
                    if (NextToken())
                    {
                        if (At(CSharpSymbolType.Identifier) || At(CSharpSymbolType.Keyword))
                        {
                            // Accept the dot and return to the start
                            Accept(dot);
                            return true; // continue
                        }
                        else
                        {
                            // Put the symbol back
                            PutCurrentBack();
                        }
                    }
                    if (!IsNested)
                    {
                        // Put the "." back
                        PutBack(dot);
                    }
                    else
                    {
                        Accept(dot);
                    }
                }
                else if (!At(CSharpSymbolType.WhiteSpace) && !At(CSharpSymbolType.NewLine))
                {
                    PutCurrentBack();
                }
            }

            // Implicit Expression is complete
            return false;
        }

        private void CompleteBlock()
        {
            CompleteBlock(insertMarkerIfNecessary: true);
        }

        private void CompleteBlock(bool insertMarkerIfNecessary)
        {
            CompleteBlock(insertMarkerIfNecessary, captureWhitespaceToEndOfLine: insertMarkerIfNecessary);
        }

        private void CompleteBlock(bool insertMarkerIfNecessary, bool captureWhitespaceToEndOfLine)
        {
            if (insertMarkerIfNecessary && Context.LastAcceptedCharacters != AcceptedCharacters.Any)
            {
                AddMarkerSymbolIfNecessary();
            }

            EnsureCurrent();

            // Read whitespace, but not newlines
            // If we're not inserting a marker span, we don't need to capture whitespace
            if (!Context.WhiteSpaceIsSignificantToAncestorBlock &&
                Context.CurrentBlock.Type != BlockType.Expression &&
                captureWhitespaceToEndOfLine &&
                !Context.DesignTimeMode &&
                !IsNested)
            {
                CaptureWhitespaceAtEndOfCodeOnlyLine();
            }
            else
            {
                PutCurrentBack();
            }
        }

        private void CaptureWhitespaceAtEndOfCodeOnlyLine()
        {
            IEnumerable<CSharpSymbol> ws = ReadWhile(sym => sym.Type == CSharpSymbolType.WhiteSpace);
            if (At(CSharpSymbolType.NewLine))
            {
                Accept(ws);
                AcceptAndMoveNext();
                PutCurrentBack();
            }
            else
            {
                PutCurrentBack();
                PutBack(ws);
            }
        }

        private void ConfigureExplicitExpressionSpan(SpanBuilder sb)
        {
            sb.EditHandler = SpanEditHandler.CreateDefault(Language.TokenizeString);
            sb.CodeGenerator = new ExpressionCodeGenerator();
        }

        private void ExplicitExpression()
        {
            Block block = new Block(RazorResources.BlockName_ExplicitExpression, CurrentLocation);
            Assert(CSharpSymbolType.LeftParenthesis);
            AcceptAndMoveNext();
            Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
            Span.CodeGenerator = SpanCodeGenerator.Null;
            Output(SpanKind.MetaCode);
            using (PushSpanConfig(ConfigureExplicitExpressionSpan))
            {
                bool success = Balance(
                    BalancingModes.BacktrackOnFailure | BalancingModes.NoErrorOnFailure | BalancingModes.AllowCommentsAndTemplates,
                    CSharpSymbolType.LeftParenthesis,
                    CSharpSymbolType.RightParenthesis,
                    block.Start);

                if (!success)
                {
                    AcceptUntil(CSharpSymbolType.LessThan);
                    Context.OnError(block.Start, RazorResources.ParseError_Expected_EndOfBlock_Before_EOF, block.Name, ")", "(");
                }

                // If necessary, put an empty-content marker symbol here
                if (Span.Symbols.Count == 0)
                {
                    Accept(new CSharpSymbol(CurrentLocation, String.Empty, CSharpSymbolType.Unknown));
                }

                // Output the content span and then capture the ")"
                Output(SpanKind.Code);
            }
            Optional(CSharpSymbolType.RightParenthesis);
            if (!EndOfFile)
            {
                PutCurrentBack();
            }
            Span.EditHandler.AcceptedCharacters = AcceptedCharacters.None;
            Span.CodeGenerator = SpanCodeGenerator.Null;
            CompleteBlock(insertMarkerIfNecessary: false);
            Output(SpanKind.MetaCode);
        }

        private void Template()
        {
            if (Context.IsWithin(BlockType.Template))
            {
                Context.OnError(CurrentLocation, RazorResources.ParseError_InlineMarkup_Blocks_Cannot_Be_Nested);
            }
            Output(SpanKind.Code);
            using (Context.StartBlock(BlockType.Template))
            {
                Context.CurrentBlock.CodeGenerator = new TemplateBlockCodeGenerator();
                PutCurrentBack();
                OtherParserBlock();
            }
        }

        private void OtherParserBlock()
        {
            ParseWithOtherParser(p => p.ParseBlock());
        }

        private void SectionBlock(string left, string right, bool caseSensitive)
        {
            ParseWithOtherParser(p => p.ParseSection(Tuple.Create(left, right), caseSensitive));
        }

        private void NestedBlock()
        {
            Output(SpanKind.Code);
            bool wasNested = IsNested;
            IsNested = true;
            using (PushSpanConfig())
            {
                ParseBlock();
            }
            Initialize(Span);
            IsNested = wasNested;
            NextToken();
        }

        protected override bool IsAtEmbeddedTransition(bool allowTemplatesAndComments, bool allowTransitions)
        {
            // No embedded transitions in C#, so ignore that param
            return allowTemplatesAndComments
                   && ((Language.IsTransition(CurrentSymbol)
                        && NextIs(CSharpSymbolType.LessThan, CSharpSymbolType.Colon))
                       || Language.IsCommentStart(CurrentSymbol));
        }

        protected override void HandleEmbeddedTransition()
        {
            if (Language.IsTransition(CurrentSymbol))
            {
                PutCurrentBack();
                Template();
            }
            else if (Language.IsCommentStart(CurrentSymbol))
            {
                RazorComment();
            }
        }

        private void ParseWithOtherParser(Action<ParserBase> parseAction)
        {
            using (PushSpanConfig())
            {
                Context.SwitchActiveParser();
                parseAction(Context.MarkupParser);
                Context.SwitchActiveParser();
            }
            Initialize(Span);
            NextToken();
        }
    }
}
