﻿// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

using Microsoft.CodeAnalysis.Completion;
using Microsoft.VisualStudio.Text;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Editor.Implementation.IntelliSense.Completion
{
    internal partial class Controller
    {
        internal partial class Session : Session<Controller, Model, ICompletionPresenterSession>
        {
            #region Fields that can be accessed from either thread

            // When we issue filter tasks, provide them with a (monotonically increasing) id.  That
            // way, when they run we can bail on computation if they've been superseded by another
            // filter task.
            private int _filterId;

            #endregion

            public Session(Controller controller, ModelComputation<Model> computation, ICompletionPresenterSession presenterSession)
                : base(controller, computation, presenterSession)
            {
                this.PresenterSession.ItemCommitted += OnPresenterSessionItemCommitted;
                this.PresenterSession.ItemSelected += OnPresenterSessionItemSelected;
                this.PresenterSession.FilterStateChanged += OnPresenterSessionCompletionItemFilterStateChanged;
            }

            private ITextBuffer SubjectBuffer
            {
                get
                {
                    AssertIsForeground();
                    return this.Controller.SubjectBuffer;
                }
            }

            internal void UpdateModelTrackingSpan(SnapshotPoint initialPosition)
            {
                AssertIsForeground();
                var currentPosition = Controller.GetCaretPointInViewBuffer();

                Computation.ChainTaskAndNotifyControllerWhenFinished(
                    model =>
                {
                    if (model == null)
                    {
                        return model;
                    }

                    // If our tracking point maps to the caret before the edit, it should also map to the 
                    // caret after the edit.  This is for the automatic brace completion scenario where
                    // we don't want the completion commit span to include the auto-inserted ')'
                    if (model.CommitTrackingSpanEndPoint.GetPosition(initialPosition.Snapshot) == initialPosition.Position)
                    {
                        return model.WithTrackingSpanEnd(currentPosition.Snapshot.Version.CreateTrackingPoint(currentPosition.Position, PointTrackingMode.Positive));
                    }

                    return model;
                });
            }

            public override void Stop()
            {
                AssertIsForeground();
                this.PresenterSession.ItemSelected -= OnPresenterSessionItemSelected;
                this.PresenterSession.ItemCommitted -= OnPresenterSessionItemCommitted;
                this.PresenterSession.FilterStateChanged -= OnPresenterSessionCompletionItemFilterStateChanged;
                base.Stop();
            }

            private SnapshotPoint GetCaretPointInViewBuffer()
            {
                AssertIsForeground();
                return Controller.GetCaretPointInViewBuffer();
            }

            private void OnPresenterSessionItemCommitted(object sender, CompletionItemEventArgs e)
            {
                AssertIsForeground();
                Contract.ThrowIfFalse(ReferenceEquals(this.PresenterSession, sender));

                this.Controller.CommitItem(e.CompletionItem);
            }

            private void OnPresenterSessionItemSelected(object sender, CompletionItemEventArgs e)
            {
                AssertIsForeground();
                Contract.ThrowIfFalse(ReferenceEquals(this.PresenterSession, sender));

                SetModelSelectedItem(m => e.CompletionItem);
            }

            private void OnPresenterSessionCompletionItemFilterStateChanged(
                object sender, CompletionItemFilterStateChangedEventArgs e)
            {
                AssertIsForeground();
                Contract.ThrowIfFalse(ReferenceEquals(this.PresenterSession, sender));

                // Update the filter state for the model.  Note: if we end up filtering everything
                // out we do *not* want to dismiss the completion list. 
                this.FilterModel(CompletionFilterReason.ItemFiltersChanged,
                    dismissIfEmptyAllowed: false,
                    recheckCaretPosition: false,
                    filterState: e.FilterState);
            }
        }
    }
}