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

using System.CodeDom;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using System.Web.Razor.Editor;
using System.Web.Razor.Parser.SyntaxTree;
using System.Web.Razor.Resources;
using System.Web.Razor.Text;
using Microsoft.Internal.Web.Utils;

namespace System.Web.Razor
{
    /// <summary>
    /// Parser used by editors to avoid reparsing the entire document on each text change
    /// </summary>
    /// <remarks>
    /// This parser is designed to allow editors to avoid having to worry about incremental parsing.
    /// The CheckForStructureChanges method can be called with every change made by a user in an editor and
    /// the parser will provide a result indicating if it was able to incrementally reparse the document.
    /// 
    /// The general workflow for editors with this parser is:
    /// 0. User edits document
    /// 1. Editor builds TextChange structure describing the edit and providing a reference to the _updated_ text buffer
    /// 2. Editor calls CheckForStructureChanges passing in that change.
    /// 3. Parser determines if the change can be simply applied to an existing parse tree node
    ///   a.  If it can, the Parser updates its parse tree and returns PartialParseResult.Accepted
    ///   b.  If it can not, the Parser starts a background parse task and return PartialParseResult.Rejected
    /// NOTE: Additional flags can be applied to the PartialParseResult, see that enum for more details.  However,
    ///       the Accepted or Rejected flags will ALWAYS be present
    /// 
    /// A change can only be incrementally parsed if a single, unique, Span (see System.Web.Razor.Parser.SyntaxTree) in the syntax tree can
    /// be identified as owning the entire change.  For example, if a change overlaps with multiple spans, the change cannot be
    /// parsed incrementally and a full reparse is necessary.  A Span "owns" a change if the change occurs either a) entirely
    /// within it's boundaries or b) it is a pure insertion (see TextChange) at the end of a Span whose CanGrow flag (see Span) is
    /// true.
    /// 
    /// Even if a single unique Span owner can be identified, it's possible the edit will cause the Span to split or merge with other
    /// Spans, in which case, a full reparse is necessary to identify the extent of the changes to the tree.
    /// 
    /// When the RazorEditorParser returns Accepted, it updates CurrentParseTree immediately.  However, the editor is expected to
    /// update it's own data structures independently.  It can use CurrentParseTree to do this, as soon as the editor returns from
    /// CheckForStructureChanges, but it should (ideally) have logic for doing so without needing the new tree.
    /// 
    /// When Rejected is returned by CheckForStructureChanges, a background parse task has _already_ been started.  When that task
    /// finishes, the DocumentStructureChanged event will be fired containing the new generated code, parse tree and a reference to
    /// the original TextChange that caused the reparse, to allow the editor to resolve the new tree against any changes made since 
    /// calling CheckForStructureChanges.
    /// 
    /// If a call to CheckForStructureChanges occurs while a reparse is already in-progress, the reparse is cancelled IMMEDIATELY
    /// and Rejected is returned without attempting to reparse.  This means that if a conusmer calls CheckForStructureChanges, which
    /// returns Rejected, then calls it again before DocumentParseComplete is fired, it will only recieve one DocumentParseComplete
    /// event, for the second change.
    /// </remarks>
    public class RazorEditorParser : IDisposable
    {
        // Lock for this document
        private object _lock = new object();
        private Stack<BackgroundParseTask> _outstandingParserTasks = new Stack<BackgroundParseTask>();
        private Span _lastChangeOwner;
        private Span _lastAutoCompleteSpan;
#if DEBUG
        private CodeCompileUnit _currentCompileUnit;
#endif

        /// <summary>
        /// Constructs the editor parser.  One instance should be used per active editor.  This
        /// instance _can_ be shared among reparses, but should _never_ be shared between documents.
        /// </summary>
        /// <param name="host">The <see cref="RazorEngineHost"/> which defines the environment in which the generated code will live.  <see cref="F:RazorEngineHost.DesignTimeMode"/> should be set if design-time code mappings are desired</param>
        /// <param name="sourceFileName">The physical path to use in line pragmas</param>
        public RazorEditorParser(RazorEngineHost host, string sourceFileName)
        {
            if (host == null)
            {
                throw new ArgumentNullException("host");
            }
            if (String.IsNullOrEmpty(sourceFileName))
            {
                throw new ArgumentException(CommonResources.Argument_Cannot_Be_Null_Or_Empty, "sourceFileName");
            }

            Host = host;
            FileName = sourceFileName;
        }

        /// <summary>
        /// Event fired when a full reparse of the document completes
        /// </summary>
        public event EventHandler<DocumentParseCompleteEventArgs> DocumentParseComplete;

        public RazorEngineHost Host { get; private set; }
        public string FileName { get; private set; }
        public bool LastResultProvisional { get; private set; }
        public Block CurrentParseTree { get; private set; }

        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate", Justification = "Since this method is heavily affected by side-effects, particularly calls to CheckForStructureChanges, it should not be made into a property")]
        public virtual string GetAutoCompleteString()
        {
            if (_lastAutoCompleteSpan != null)
            {
                AutoCompleteEditHandler editHandler = _lastAutoCompleteSpan.EditHandler as AutoCompleteEditHandler;
                if (editHandler != null)
                {
                    return editHandler.AutoCompleteString;
                }
            }
            return null;
        }

        /// <summary>
        /// Determines if a change will cause a structural change to the document and if not, applies it to the existing tree.
        /// If a structural change would occur, automatically starts a reparse
        /// </summary>
        /// <remarks>
        /// NOTE: The initial incremental parsing check and actual incremental parsing (if possible) occurs
        /// on the callers thread.  However, if a full reparse is needed, this occurs on a background thread.
        /// </remarks>
        /// <param name="change">The change to apply to the parse tree</param>
        /// <returns>A PartialParseResult value indicating the result of the incremental parse</returns>
        public virtual PartialParseResult CheckForStructureChanges(TextChange change)
        {
            // Validate the change
            if (change.NewBuffer == null)
            {
                throw new ArgumentException(String.Format(CultureInfo.CurrentUICulture,
                                                          RazorResources.Structure_Member_CannotBeNull,
                                                          "Buffer",
                                                          "TextChange"), "change");
            }

            PartialParseResult result = PartialParseResult.Rejected;

            // Lock the state objects
            lock (_lock)
            {
                // If there isn't already a parse underway, try partial-parsing
                if (CurrentParseTree != null && _outstandingParserTasks.Count == 0)
                {
                    result = TryPartialParse(change);
                }

                // If partial parsing failed or there were outstanding parser tasks, start a full reparse
                if (result.HasFlag(PartialParseResult.Rejected))
                {
                    QueueFullReparse(change);
                }
#if DEBUG
                else
                {
                    if (CurrentParseTree != null)
                    {
                        RazorDebugHelpers.WriteDebugTree(FileName, CurrentParseTree, result, change, this, false);
                    }
                    if (_currentCompileUnit != null)
                    {
                        RazorDebugHelpers.WriteGeneratedCode(FileName, _currentCompileUnit);
                    }
                }
#endif

                // Otherwise, remember if this was provisionally accepted for next partial parse
                LastResultProvisional = result.HasFlag(PartialParseResult.Provisional);
            }
            VerifyFlagsAreValid(result);
            return result;
        }

        /// <summary>
        /// Disposes of this parser.  Should be called when the editor window is closed and the document is unloaded.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_cancelTokenSource", Justification = "The cancellation token is owned by the worker thread, so it is disposed there")]
        [SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_changeReceived", Justification = "The change received event is owned by the worker thread, so it is disposed there")]
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                lock (_lock)
                {
                    if (_outstandingParserTasks.Count > 0)
                    {
                        foreach (var task in _outstandingParserTasks)
                        {
                            task.Dispose();
                        }
                    }
                }
            }
        }

        private void QueueFullReparse(TextChange change)
        {
            if (_outstandingParserTasks.Count > 0)
            {
                _outstandingParserTasks.Peek().Cancel();
            }
            _outstandingParserTasks.Push(CreateBackgroundTask(Host, FileName, change).ContinueWith(OnParseCompleted));
        }

        private PartialParseResult TryPartialParse(TextChange change)
        {
            PartialParseResult result = PartialParseResult.Rejected;

            // Try the last change owner
            if (_lastChangeOwner != null && _lastChangeOwner.EditHandler.OwnsChange(_lastChangeOwner, change))
            {
                EditResult editResult = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
                result = editResult.Result;
                if (!editResult.Result.HasFlag(PartialParseResult.Rejected))
                {
                    _lastChangeOwner.ReplaceWith(editResult.EditedSpan);
                }

                // If the last change was provisional, then the result of this span's attempt to parse partially goes
                // Otherwise, accept the change if this span accepted it, but if it didn't, just do the standard search.
                if (LastResultProvisional || result.HasFlag(PartialParseResult.Accepted))
                {
                    return result;
                }
            }

            // Locate the span responsible for this change
            _lastChangeOwner = CurrentParseTree.LocateOwner(change);

            if (LastResultProvisional)
            {
                // Last change owner couldn't accept this, so we must do a full reparse
                result = PartialParseResult.Rejected;
            }
            else if (_lastChangeOwner != null)
            {
                EditResult editRes = _lastChangeOwner.EditHandler.ApplyChange(_lastChangeOwner, change);
                result = editRes.Result;
                if (!editRes.Result.HasFlag(PartialParseResult.Rejected))
                {
                    _lastChangeOwner.ReplaceWith(editRes.EditedSpan);
                }
                if (result.HasFlag(PartialParseResult.AutoCompleteBlock))
                {
                    _lastAutoCompleteSpan = _lastChangeOwner;
                }
                else
                {
                    _lastAutoCompleteSpan = null;
                }
            }
            return result;
        }

        private void OnParseCompleted(GeneratorResults results, BackgroundParseTask parseTask)
        {
            try
            {
                // Lock the state objects
                bool treeStructureChanged = true;
                TextChange lastChange;
                lock (_lock)
                {
                    // Are we still active?
                    if (_outstandingParserTasks.Count == 0 || !ReferenceEquals(parseTask, _outstandingParserTasks.Peek()))
                    {
                        // We aren't, just abort
                        return;
                    }

                    // Ok, we're active. Flush out the changes from all the parser tasks and clear the stack of outstanding parse tasks
                    TextChange[] changes = _outstandingParserTasks.Select(t => t.Change).Reverse().ToArray();
                    lastChange = changes.Last();
                    _outstandingParserTasks.Clear();

                    // Take the current tree and check for differences
                    treeStructureChanged = CurrentParseTree == null || TreesAreDifferent(CurrentParseTree, results.Document, changes);
                    CurrentParseTree = results.Document;
#if DEBUG
                    _currentCompileUnit = results.GeneratedCode;
#endif
                    _lastChangeOwner = null;
                }

                // Done, now exit the lock and fire the event
                OnDocumentParseComplete(new DocumentParseCompleteEventArgs()
                {
                    GeneratorResults = results,
                    SourceChange = lastChange,
                    TreeStructureChanged = treeStructureChanged
                });
            }
            finally
            {
                parseTask.Dispose();
            }
        }

        internal static bool TreesAreDifferent(Block leftTree, Block rightTree, TextChange[] changes)
        {
            // Apply all the pending changes to the original tree
            // PERF: If this becomes a bottleneck, we can probably do it the other way around,
            //  i.e. visit the tree and find applicable changes for each node.
            foreach (TextChange change in changes)
            {
                Span changeOwner = leftTree.LocateOwner(change);

                // Apply the change to the tree
                if (changeOwner == null)
                {
                    return true;
                }
                EditResult result = changeOwner.EditHandler.ApplyChange(changeOwner, change, force: true);
                changeOwner.ReplaceWith(result.EditedSpan);
            }

            // Now compare the trees
            bool treesDifferent = !leftTree.EquivalentTo(rightTree);
#if DEBUG
            if (RazorDebugHelpers.OutputDebuggingEnabled)
            {
                Debug.WriteLine(String.Format(CultureInfo.CurrentCulture, "Processed {0} changes, trees were{1} different", changes.Length, treesDifferent ? String.Empty : " not"));
            }
#endif
            return treesDifferent;
        }

        private void OnDocumentParseComplete(DocumentParseCompleteEventArgs args)
        {
            Debug.Assert(args != null, "Event arguments cannot be null");
            EventHandler<DocumentParseCompleteEventArgs> handler = DocumentParseComplete;
            if (handler != null)
            {
                handler(this, args);
            }
#if DEBUG
            RazorDebugHelpers.WriteDebugTree(FileName, args.GeneratorResults.Document, PartialParseResult.Rejected, args.SourceChange, this, args.TreeStructureChanged);
            RazorDebugHelpers.WriteGeneratedCode(FileName, args.GeneratorResults.GeneratedCode);
#endif
        }

        [Conditional("DEBUG")]
        private static void VerifyFlagsAreValid(PartialParseResult result)
        {
            Debug.Assert(result.HasFlag(PartialParseResult.Accepted) ||
                         result.HasFlag(PartialParseResult.Rejected),
                         "Partial Parse result does not have either of Accepted or Rejected flags set");
            Debug.Assert(result.HasFlag(PartialParseResult.Rejected) ||
                         !result.HasFlag(PartialParseResult.SpanContextChanged),
                         "Partial Parse result was Accepted AND had SpanContextChanged flag set");
            Debug.Assert(result.HasFlag(PartialParseResult.Rejected) ||
                         !result.HasFlag(PartialParseResult.AutoCompleteBlock),
                         "Partial Parse result was Accepted AND had AutoCompleteBlock flag set");
            Debug.Assert(result.HasFlag(PartialParseResult.Accepted) ||
                         !result.HasFlag(PartialParseResult.Provisional),
                         "Partial Parse result was Rejected AND had Provisional flag set");
        }

        internal virtual BackgroundParseTask CreateBackgroundTask(RazorEngineHost host, string fileName, TextChange change)
        {
            return BackgroundParseTask.StartNew(new RazorTemplateEngine(Host), FileName, change);
        }
    }
}
