// 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.

#nullable disable

extern alias InteractiveHost;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading.Tasks;
using InteractiveHost::Microsoft.CodeAnalysis.Interactive;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Options;
using Microsoft.VisualStudio.InteractiveWindow;
using Microsoft.VisualStudio.Text.Editor.OptionsExtensionMethods;
using Microsoft.VisualStudio.Utilities;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Interactive;

/// <summary>
/// ResetInteractive class that implements base functionality for reset interactive command.
/// Does not depend on VS classes to make it easier to stub out and test.
/// </summary>
internal abstract class ResetInteractive
{
    private readonly Func<string, string> _createReference;

    private readonly Func<string, string> _createImport;

    private readonly EditorOptionsService _editorOptionsService;

    internal event EventHandler ExecutionCompleted;

    internal ResetInteractive(EditorOptionsService editorOptionsService, Func<string, string> createReference, Func<string, string> createImport)
    {
        _editorOptionsService = editorOptionsService;
        _createReference = createReference;
        _createImport = createImport;
    }

    internal async Task ExecuteAsync(IInteractiveWindow interactiveWindow, string title)
    {
        if (GetProjectProperties(out var references, out var referenceSearchPaths, out var sourceSearchPaths, out var projectNamespaces, out var projectDirectory, out var platform))
        {
            try
            {
                // Now, we're going to do a bunch of async operations.  So create a wait
                // indicator so the user knows something is happening, and also so they cancel.
                var uiThreadOperationExecutor = GetUIThreadOperationExecutor();
                using var context = uiThreadOperationExecutor.BeginExecute(title, EditorFeaturesWpfResources.Building_Project, allowCancellation: true, showProgress: false);

                // We want to come back onto the calling context to dismiss the wait indicator and to notify about
                // execution completion.
                await ResetInteractiveAsync(
                    interactiveWindow,
                    references,
                    referenceSearchPaths,
                    sourceSearchPaths,
                    projectNamespaces,
                    projectDirectory,
                    platform,
                    context).ConfigureAwait(true);
            }
            finally
            {
                // Once we're done resetting focus the REPL window.
                ExecutionCompleted?.Invoke(this, new EventArgs());
            }
        }
    }

    private async Task ResetInteractiveAsync(
        IInteractiveWindow interactiveWindow,
        ImmutableArray<string> referencePaths,
        ImmutableArray<string> referenceSearchPaths,
        ImmutableArray<string> sourceSearchPaths,
        ImmutableArray<string> projectNamespaces,
        string projectDirectory,
        InteractiveHostPlatform? platform,
        IUIThreadOperationContext uiThreadOperationContext)
    {
        // First, open the repl window.
        var evaluator = (IResettableInteractiveEvaluator)interactiveWindow.Evaluator;

        // If the user hits the cancel button on the wait indicator, then we want to stop the
        // build.
        using (uiThreadOperationContext.UserCancellationToken.Register(() =>
            CancelBuildProject(), useSynchronizationContext: true))
        {
            // First, start a build.
            // If the build fails do not reset the REPL.
            var builtSuccessfully = await BuildProjectAsync().ConfigureAwait(true);
            if (!builtSuccessfully)
            {
                return;
            }
        }

        // Then reset the REPL
        using var scope = uiThreadOperationContext.AddScope(allowCancellation: true, EditorFeaturesWpfResources.Resetting_Interactive);
        evaluator.ResetOptions = new InteractiveEvaluatorResetOptions(platform);
        await interactiveWindow.Operations.ResetAsync(initialize: true).ConfigureAwait(true);

        // TODO: load context from an rsp file.

        // Now send the reference paths we've collected to the repl.
        await evaluator.SetPathsAsync(referenceSearchPaths, sourceSearchPaths, projectDirectory).ConfigureAwait(true);

        var editorOptions = _editorOptionsService.Factory.GetOptions(interactiveWindow.CurrentLanguageBuffer);
        var importReferencesCommand = referencePaths.Select(_createReference);
        await interactiveWindow.SubmitAsync(importReferencesCommand).ConfigureAwait(true);

        // Project's default namespace might be different from namespace used within project.
        // Filter out namespace imports that do not exist in interactive compilation.
        var namespacesToImport = await GetNamespacesToImportAsync(projectNamespaces, interactiveWindow).ConfigureAwait(true);
        var importNamespacesCommand = namespacesToImport.Select(_createImport).Join(editorOptions.GetNewLineCharacter());

        if (!string.IsNullOrWhiteSpace(importNamespacesCommand))
        {
            await interactiveWindow.SubmitAsync([importNamespacesCommand]).ConfigureAwait(true);
        }
    }

    protected abstract Task<IEnumerable<string>> GetNamespacesToImportAsync(IEnumerable<string> namespacesToImport, IInteractiveWindow interactiveWindow);

    /// <summary>
    /// Gets the properties of the currently selected projects necessary for reset.
    /// </summary>
    protected abstract bool GetProjectProperties(
        out ImmutableArray<string> references,
        out ImmutableArray<string> referenceSearchPaths,
        out ImmutableArray<string> sourceSearchPaths,
        out ImmutableArray<string> projectNamespaces,
        out string projectDirectory,
        out InteractiveHostPlatform? platform);

    /// <summary>
    /// A method that should trigger an async project build.
    /// </summary>
    /// <returns>Whether or not the build was successful.</returns>
    protected abstract Task<bool> BuildProjectAsync();

    /// <summary>
    /// A method that should trigger a project cancellation.
    /// </summary>
    protected abstract void CancelBuildProject();

    protected abstract IUIThreadOperationExecutor GetUIThreadOperationExecutor();
}
