﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.InteractiveWindow;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Threading;
using Roslyn.Utilities;
using ThreadHelper = Microsoft.VisualStudio.Shell.ThreadHelper;

namespace Microsoft.VisualStudio.IntegrationTest.Utilities.InProcess
{
    /// <summary>
    /// Provides a means of accessing the <see cref="IInteractiveWindow"/> service in the Visual Studio host.
    /// </summary>
    /// <remarks>
    /// This object exists in the Visual Studio host and is marhsalled across the process boundary.
    /// </remarks>
    internal abstract class InteractiveWindow_InProc : TextViewWindow_InProc
    {
        private static readonly Func<string, string, bool> s_equals = (expected, actual) => actual.Equals(expected);
        private static readonly Func<string, string, bool> s_contains = (expected, actual) => actual.Contains(expected);
        private static readonly Func<string, string, bool> s_endsWith = (expected, actual) => actual.EndsWith(expected);

        private const string NewLineFollowedByReplSubmissionText = "\n. ";
        private const string ReplSubmissionText = ". ";
        private const string ReplPromptText = "> ";

        private readonly string _viewCommand;
        private readonly Guid _windowId;
        private IInteractiveWindow? _interactiveWindow;

        protected InteractiveWindow_InProc(string viewCommand, Guid windowId)
        {
            _viewCommand = viewCommand;
            _windowId = windowId;
        }

        public void Initialize()
        {
            // We have to show the window at least once to ensure the interactive service is loaded.
            ShowWindow(waitForPrompt: false);
            CloseWindow();

            _interactiveWindow = AcquireInteractiveWindow();

            Contract.ThrowIfNull(_interactiveWindow);
        }

        protected abstract IInteractiveWindow AcquireInteractiveWindow();

        public string GetReplText()
        {
            Contract.ThrowIfNull(_interactiveWindow);
            return InvokeOnUIThread(cancellationToken => _interactiveWindow.TextView.TextBuffer.CurrentSnapshot.GetText());
        }

        protected override bool HasActiveTextView()
        {
            Contract.ThrowIfNull(_interactiveWindow);
            return InvokeOnUIThread(cancellationToken => _interactiveWindow.TextView) is object;
        }

        protected override IWpfTextView GetActiveTextView()
        {
            Contract.ThrowIfNull(_interactiveWindow);
            return InvokeOnUIThread(cancellationToken => _interactiveWindow.TextView);
        }

        /// <summary>
        /// Gets the contents of the REPL window without the prompt text.
        /// </summary>
        public string GetReplTextWithoutPrompt()
        {
            var replText = GetReplText();

            // find last prompt and remove
            var lastPromptIndex = replText.LastIndexOf(ReplPromptText);

            if (lastPromptIndex > 0)
            {
                replText = replText[..lastPromptIndex];
            }

            // it's possible for the editor text to contain a trailing newline, remove it
            return replText.EndsWith(Environment.NewLine)
                ? replText[..^Environment.NewLine.Length]
                : replText;
        }

        /// <summary>
        /// Gets the last output from the REPL.
        /// </summary>
        public string GetLastReplOutput()
        {
            // TODO: This may be flaky if the last submission contains ReplPromptText

            var replText = GetReplTextWithoutPrompt();
            var lastPromptIndex = replText.LastIndexOf(ReplPromptText);
            if (lastPromptIndex > 0)
                replText = replText[lastPromptIndex..];

            var lastSubmissionIndex = replText.LastIndexOf(NewLineFollowedByReplSubmissionText);

            if (lastSubmissionIndex > 0)
            {
                replText = replText[lastSubmissionIndex..];
            }
            else if (!replText.StartsWith(ReplPromptText))
            {
                return replText;
            }

            var firstNewLineIndex = replText.IndexOf(Environment.NewLine);

            if (firstNewLineIndex <= 0)
            {
                return replText;
            }

            firstNewLineIndex += Environment.NewLine.Length;
            return replText[firstNewLineIndex..];
        }

        /// <summary>
        /// Gets the last input from the REPL.
        /// </summary>
        public string GetLastReplInput()
        {
            // TODO: This may be flaky if the last submission contains ReplPromptText or ReplSubmissionText

            var replText = GetReplText();
            var lastPromptIndex = replText.LastIndexOf(ReplPromptText);
            replText = replText[(lastPromptIndex + ReplPromptText.Length)..];

            var lastSubmissionTextIndex = replText.LastIndexOf(NewLineFollowedByReplSubmissionText);

            int firstNewLineIndex;
            if (lastSubmissionTextIndex < 0)
            {
                firstNewLineIndex = replText.IndexOf(Environment.NewLine);
            }
            else
            {
                firstNewLineIndex = replText.IndexOf(Environment.NewLine, lastSubmissionTextIndex);
            }

            var lastReplInputWithReplSubmissionText = (firstNewLineIndex <= 0) ? replText : replText[..firstNewLineIndex];

            return lastReplInputWithReplSubmissionText.Replace(ReplSubmissionText, string.Empty);
        }

        public void Reset(bool waitForPrompt = true)
        {
            JoinableTaskFactory.Run(async () =>
            {
                await JoinableTaskFactory.SwitchToMainThreadAsync();

                var interactiveWindow = AcquireInteractiveWindow();
                var operations = (IInteractiveWindowOperations)interactiveWindow;
                var result = await operations.ResetAsync();
                Contract.ThrowIfFalse(result.IsSuccessful);
            });

            if (waitForPrompt)
            {
                WaitForReplPrompt();
            }
        }

        public void SubmitText(string text)
        {
            Contract.ThrowIfNull(_interactiveWindow);

            using var cts = new CancellationTokenSource(Helper.HangMitigatingTimeout);
            _interactiveWindow.SubmitAsync(new[] { text }).WithCancellation(cts.Token).Wait();
        }

        public void CloseWindow()
        {
            InvokeOnUIThread(cancellationToken =>
            {
                var shell = GetGlobalService<SVsUIShell, IVsUIShell>();
                if (ErrorHandler.Succeeded(shell.FindToolWindow((uint)__VSFINDTOOLWIN.FTW_fFrameOnly, _windowId, out var windowFrame)))
                {
                    ErrorHandler.ThrowOnFailure(windowFrame.CloseFrame((uint)__FRAMECLOSE.FRAMECLOSE_NoSave));
                }
            });
        }

        public void ShowWindow(bool waitForPrompt = true)
        {
            ExecuteCommand(_viewCommand);

            if (waitForPrompt)
            {
                WaitForReplPrompt();
            }
        }

        public void WaitForReplPrompt()
            => WaitForPredicate(GetReplText, ReplPromptText, s_endsWith, "end with");

        public void WaitForReplOutput(string outputText)
            => WaitForPredicate(GetReplText, outputText + Environment.NewLine + ReplPromptText, s_endsWith, "end with");

        public void ClearScreen()
            => ExecuteCommand(WellKnownCommandNames.InteractiveConsole_ClearScreen);

        public void InsertCode(string text)
        {
            Contract.ThrowIfNull(_interactiveWindow);
            InvokeOnUIThread(cancellationToken => _interactiveWindow.InsertCode(text));
        }

        public void WaitForLastReplOutput(string outputText)
            => WaitForPredicate(GetLastReplOutput, outputText, s_equals, "is");

        public void WaitForLastReplOutputContains(string outputText)
            => WaitForPredicate(GetLastReplOutput, outputText, s_contains, "contain");

        public void WaitForLastReplInput(string outputText)
            => WaitForPredicate(GetLastReplInput, outputText, s_equals, "is");

        public void WaitForLastReplInputContains(string outputText)
            => WaitForPredicate(GetLastReplInput, outputText, s_contains, "contain");

        private static void WaitForPredicate(Func<string> getValue, string expectedValue, Func<string, string, bool> valueComparer, string verb)
        {
            var beginTime = DateTime.UtcNow;

            while (true)
            {
                var actualValue = getValue();

                if (valueComparer(expectedValue, actualValue))
                {
                    return;
                }

                if (DateTime.UtcNow > beginTime + Helper.HangMitigatingTimeout)
                {
                    throw new Exception(
                        $"Unable to find expected content in REPL within {Helper.HangMitigatingTimeout.TotalMilliseconds} milliseconds and no exceptions were thrown.{Environment.NewLine}" +
                        $"Buffer content is expected to {verb}: {Environment.NewLine}" +
                        $"[[{expectedValue}]]" +
                        $"Actual content:{Environment.NewLine}" +
                        $"[[{actualValue}]]");
                }

                Thread.Sleep(50);
            }
        }

        protected override ITextBuffer GetBufferContainingCaret(IWpfTextView view)
        {
            Contract.ThrowIfNull(_interactiveWindow);
            return InvokeOnUIThread(cancellationToken => _interactiveWindow.TextView.TextBuffer);
        }
    }
}
