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

extern alias InteractiveHost;
extern alias Scripting;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Debugging;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.Scripting.Hosting;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Text;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.Interactive
{
    using InteractiveHost::Microsoft.CodeAnalysis.Interactive;
    using RelativePathResolver = Scripting::Microsoft.CodeAnalysis.RelativePathResolver;

    internal sealed class InteractiveSession : IDisposable
    {
        public InteractiveHost Host { get; }

        private readonly IThreadingContext _threadingContext;
        private readonly InteractiveEvaluatorLanguageInfoProvider _languageInfo;
        private readonly InteractiveWorkspace _workspace;
        private readonly ITextDocumentFactoryService _textDocumentFactoryService;
        private readonly EditorOptionsService _editorOptionsService;

        private readonly CancellationTokenSource _shutdownCancellationSource;

        /// <summary>
        /// The top level directory where all the interactive host extensions are installed (both Core and Desktop).
        /// </summary>
        private readonly string _hostDirectory;

        #region State only accessible by queued tasks

        // Use to serialize InteractiveHost process initialization and code execution.
        // The process may restart any time and we need to react to that by clearing 
        // the current solution and setting up the first submission project. 
        // At the same time a code submission might be in progress.
        // If we left these operations run in parallel we might get into a state
        // inconsistent with the state of the host.
        private readonly TaskQueue _taskQueue;

        private ProjectId? _lastSuccessfulSubmissionProjectId;
        private ProjectId? _currentSubmissionProjectId;
        public int SubmissionCount { get; private set; }

        private RemoteInitializationResult? _initializationResult;
        private InteractiveHostPlatformInfo _platformInfo;
        private ImmutableArray<string> _referenceSearchPaths;
        private ImmutableArray<string> _sourceSearchPaths;
        private string _workingDirectory;
        private InteractiveHostOptions? _hostOptions;

        /// <summary>
        /// Buffers that need to be associated with a submission project once the process initialization completes.
        /// </summary>
        private readonly List<(ITextBuffer buffer, string name)> _pendingBuffers = new();

        #endregion

        public InteractiveSession(
            InteractiveWorkspace workspace,
            IThreadingContext threadingContext,
            IAsynchronousOperationListener listener,
            ITextDocumentFactoryService documentFactory,
            EditorOptionsService editorOptionsService,
            InteractiveEvaluatorLanguageInfoProvider languageInfo,
            string initialWorkingDirectory)
        {
            _workspace = workspace;
            _threadingContext = threadingContext;
            _languageInfo = languageInfo;
            _textDocumentFactoryService = documentFactory;
            _editorOptionsService = editorOptionsService;

            _taskQueue = new TaskQueue(listener, TaskScheduler.Default);
            _shutdownCancellationSource = new CancellationTokenSource();

            // The following settings will apply when the REPL starts without .rsp file.
            // They are discarded once the REPL is reset.
            _referenceSearchPaths = ImmutableArray<string>.Empty;
            _sourceSearchPaths = ImmutableArray<string>.Empty;
            _workingDirectory = initialWorkingDirectory;

            _hostDirectory = Path.Combine(Path.GetDirectoryName(typeof(InteractiveSession).Assembly.Location)!, "InteractiveHost");

            Host = new InteractiveHost(languageInfo.ReplServiceProviderType, initialWorkingDirectory);
            Host.ProcessInitialized += ProcessInitialized;
        }

        public void Dispose()
        {
            _shutdownCancellationSource.Cancel();
            _shutdownCancellationSource.Dispose();

            Host.Dispose();
        }

        /// <summary>
        /// Invoked by <see cref="InteractiveHost"/> when a new process initialization completes.
        /// </summary>
        private void ProcessInitialized(InteractiveHostPlatformInfo platformInfo, InteractiveHostOptions options, RemoteExecutionResult result)
        {
            Contract.ThrowIfFalse(result.InitializationResult != null);

            _ = _taskQueue.ScheduleTask(nameof(ProcessInitialized), () =>
            {
                _workspace.ResetSolution();

                _currentSubmissionProjectId = null;
                _lastSuccessfulSubmissionProjectId = null;

                // update host state:
                _platformInfo = platformInfo;
                _initializationResult = result.InitializationResult;
                _hostOptions = options;
                UpdatePathsNoLock(result);

                // Create submission projects for buffers that were added by the Interactive Window 
                // before the process initialization completed.
                foreach (var (buffer, languageName) in _pendingBuffers)
                {
                    AddSubmissionProjectNoLock(buffer, languageName);
                }

                _pendingBuffers.Clear();
            }, _shutdownCancellationSource.Token);
        }

        /// <summary>
        /// Invoked on UI thread when a new language buffer is created and before it is added to the projection.
        /// </summary>
        internal void AddSubmissionProject(ITextBuffer submissionBuffer)
        {
            _taskQueue.ScheduleTask(
                nameof(AddSubmissionProject),
                () => AddSubmissionProjectNoLock(submissionBuffer, _languageInfo.LanguageName),
                _shutdownCancellationSource.Token);
        }

        private void AddSubmissionProjectNoLock(ITextBuffer submissionBuffer, string languageName)
        {
            var initResult = _initializationResult;

            var imports = ImmutableArray<string>.Empty;
            var references = ImmutableArray<MetadataReference>.Empty;

            var initializationScriptImports = ImmutableArray<string>.Empty;
            var initializationScriptReferences = ImmutableArray<MetadataReference>.Empty;

            ProjectId? initializationScriptProjectId = null;
            string? initializationScriptPath = null;

            if (_currentSubmissionProjectId == null)
            {
                Debug.Assert(_lastSuccessfulSubmissionProjectId == null);

                // The Interactive Window may have added the first language buffer before 
                // the host initialization has completed. Do not create a submission project 
                // for the buffer in such case. It will be created when the initialization completes.
                if (initResult == null)
                {
                    _pendingBuffers.Add((submissionBuffer, languageName));
                    return;
                }

                imports = initResult.Imports.ToImmutableArrayOrEmpty();

                var metadataService = _workspace.Services.GetRequiredService<IMetadataService>();
                references = initResult.MetadataReferencePaths.ToImmutableArrayOrEmpty().SelectAsArray(
                    (path, metadataService) => (MetadataReference)metadataService.GetReference(path, MetadataReferenceProperties.Assembly),
                    metadataService);

                // if a script was specified in .rps file insert a project with a document that represents it:
                initializationScriptPath = initResult!.ScriptPath;
                if (initializationScriptPath != null)
                {
                    initializationScriptProjectId = ProjectId.CreateNewId(CreateNewSubmissionName());

                    _lastSuccessfulSubmissionProjectId = initializationScriptProjectId;

                    // imports and references will be inherited:
                    initializationScriptImports = imports;
                    initializationScriptReferences = references;
                    imports = ImmutableArray<string>.Empty;
                    references = ImmutableArray<MetadataReference>.Empty;
                }
            }

            var newSubmissionProjectName = CreateNewSubmissionName();
            var newSubmissionText = submissionBuffer.CurrentSnapshot.AsText();
            _currentSubmissionProjectId = ProjectId.CreateNewId(newSubmissionProjectName);
            var newSubmissionDocumentId = DocumentId.CreateNewId(_currentSubmissionProjectId, newSubmissionProjectName);

            // If the _initializationResult is not null we must also have the host options.
            RoslynDebug.AssertNotNull(_hostOptions);
            // Retrieve the directory that the host path exe is located in.
            var hostPathDirectory = Path.GetDirectoryName(_hostOptions.HostPath);
            RoslynDebug.AssertNotNull(hostPathDirectory);

            // Create a file path for the submission located in the interactive host directory.
            var newSubmissionFilePath = Path.Combine(hostPathDirectory, $"Submission{SubmissionCount}{_languageInfo.Extension}");

            // Associate the path with both the editor document and our roslyn document so LSP can make requests on it.
            _textDocumentFactoryService.TryGetTextDocument(submissionBuffer, out var textDocument);
            textDocument.Rename(newSubmissionFilePath);

            // Chain projects to the the last submission that successfully executed.
            _workspace.SetCurrentSolution(solution =>
            {
                if (initializationScriptProjectId != null)
                {
                    RoslynDebug.AssertNotNull(initializationScriptPath);

                    var initProject = CreateSubmissionProjectNoLock(solution, initializationScriptProjectId, previousSubmissionProjectId: null, languageName, initializationScriptImports, initializationScriptReferences);
                    solution = initProject.Solution.AddDocument(
                        DocumentId.CreateNewId(initializationScriptProjectId, debugName: initializationScriptPath),
                        Path.GetFileName(initializationScriptPath),
                        new WorkspaceFileTextLoader(solution.Services, initializationScriptPath, defaultEncoding: null));
                }

                var newSubmissionProject = CreateSubmissionProjectNoLock(solution, _currentSubmissionProjectId, _lastSuccessfulSubmissionProjectId, languageName, imports, references);
                solution = newSubmissionProject.Solution.AddDocument(
                    newSubmissionDocumentId,
                    newSubmissionProjectName,
                    newSubmissionText,
                    filePath: newSubmissionFilePath);

                return solution;
            }, WorkspaceChangeKind.SolutionChanged);

            // opening document will start workspace listening to changes in this text container
            _workspace.OpenDocument(newSubmissionDocumentId, submissionBuffer.AsTextContainer());
        }

        private string CreateNewSubmissionName()
            => "Submission#" + SubmissionCount++;

        private Project CreateSubmissionProjectNoLock(Solution solution, ProjectId newSubmissionProjectId, ProjectId? previousSubmissionProjectId, string languageName, ImmutableArray<string> imports, ImmutableArray<MetadataReference> references)
        {
            var name = newSubmissionProjectId.DebugName;
            RoslynDebug.AssertNotNull(name);

            CompilationOptions compilationOptions;
            if (previousSubmissionProjectId != null)
            {
                compilationOptions = solution.GetRequiredProject(previousSubmissionProjectId).CompilationOptions!;

                var metadataResolver = (RuntimeMetadataReferenceResolver)compilationOptions.MetadataReferenceResolver!;
                if (metadataResolver.PathResolver.BaseDirectory != _workingDirectory ||
                    !metadataResolver.PathResolver.SearchPaths.SequenceEqual(_referenceSearchPaths))
                {
                    compilationOptions = compilationOptions.WithMetadataReferenceResolver(metadataResolver.WithRelativePathResolver(new RelativePathResolver(_referenceSearchPaths, _workingDirectory)));
                }

                var sourceResolver = (SourceFileResolver)compilationOptions.SourceReferenceResolver!;
                if (sourceResolver.BaseDirectory != _workingDirectory ||
                    !sourceResolver.SearchPaths.SequenceEqual(_sourceSearchPaths))
                {
                    compilationOptions = compilationOptions.WithSourceReferenceResolver(CreateSourceReferenceResolver(sourceResolver.SearchPaths, _workingDirectory));
                }
            }
            else
            {
                var metadataService = _workspace.Services.GetRequiredService<IMetadataService>();
                compilationOptions = _languageInfo.GetSubmissionCompilationOptions(
                    name,
                    CreateMetadataReferenceResolver(metadataService, _platformInfo, _referenceSearchPaths, _workingDirectory),
                    CreateSourceReferenceResolver(_sourceSearchPaths, _workingDirectory),
                    imports);
            }

            solution = solution.AddProject(
                ProjectInfo.Create(
                    new ProjectInfo.ProjectAttributes(
                        id: newSubmissionProjectId,
                        version: VersionStamp.Create(),
                        name: name,
                        assemblyName: name,
                        language: languageName,
                        compilationOutputFilePaths: default,
                        checksumAlgorithm: SourceHashAlgorithms.Default,
                        isSubmission: true),
                    compilationOptions: compilationOptions,
                    parseOptions: _languageInfo.ParseOptions,
                    documents: null,
                    projectReferences: null,
                    metadataReferences: references,
                    hostObjectType: typeof(InteractiveScriptGlobals)));

            if (previousSubmissionProjectId != null)
            {
                solution = solution.AddProjectReference(newSubmissionProjectId, new ProjectReference(previousSubmissionProjectId));
            }

            return solution.GetRequiredProject(newSubmissionProjectId);
        }

        /// <summary>
        /// Called once a code snippet is submitted.
        /// Followed by creation of a new language buffer and call to <see cref="AddSubmissionProject(ITextBuffer)"/>.
        /// </summary>
        internal Task<bool> ExecuteCodeAsync(string text)
        {
            return _taskQueue.ScheduleTask(nameof(ExecuteCodeAsync), async () =>
            {
                var result = await Host.ExecuteAsync(text).ConfigureAwait(false);
                if (result.Success)
                {
                    _lastSuccessfulSubmissionProjectId = _currentSubmissionProjectId;

                    // update local search paths - remote paths has already been updated
                    UpdatePathsNoLock(result);
                }

                return result.Success;
            }, _shutdownCancellationSource.Token);
        }

        internal async Task<bool> ResetAsync(InteractiveHostOptions options)
        {
            try
            {
                // Do not queue reset operation - invoke it directly.
                // Code execution might be in progress when the user requests reset (via a reset button, or process terminating on its own).
                // We need the execution to be interrupted and the process restarted, not wait for it to complete.

                var result = await Host.ResetAsync(options).ConfigureAwait(false);

                // Note: Not calling UpdatePathsNoLock here. The paths will be updated by ProcessInitialized 
                // which is executed once the new host process finishes its initialization.

                return result.Success;
            }
            catch (Exception e) when (FatalError.ReportAndPropagate(e))
            {
                throw ExceptionUtilities.Unreachable();
            }
        }

        public InteractiveHostOptions GetHostOptions(bool initialize, InteractiveHostPlatform? platform)
            => InteractiveHostOptions.CreateFromDirectory(
                _hostDirectory,
                initialize ? _languageInfo.InteractiveResponseFileName : null,
                CultureInfo.CurrentCulture,
                CultureInfo.CurrentUICulture,
                 platform ?? Host.OptionsOpt?.Platform ?? InteractiveHost.DefaultPlatform);

        private static RuntimeMetadataReferenceResolver CreateMetadataReferenceResolver(IMetadataService metadataService, InteractiveHostPlatformInfo platformInfo, ImmutableArray<string> searchPaths, string baseDirectory)
        {
            return new RuntimeMetadataReferenceResolver(
                searchPaths,
                baseDirectory,
                gacFileResolver: platformInfo.HasGlobalAssemblyCache ? new GacFileResolver(preferredCulture: CultureInfo.CurrentCulture) : null,
                platformAssemblyPaths: platformInfo.PlatformAssemblyPaths,
                fileReferenceProvider: (path, properties) => metadataService.GetReference(path, properties));
        }

        private static SourceReferenceResolver CreateSourceReferenceResolver(ImmutableArray<string> searchPaths, string baseDirectory)
            => new SourceFileResolver(searchPaths, baseDirectory);

        public Task SetPathsAsync(ImmutableArray<string> referenceSearchPaths, ImmutableArray<string> sourceSearchPaths, string workingDirectory)
        {
            return _taskQueue.ScheduleTask(nameof(ExecuteCodeAsync), async () =>
            {
                var result = await Host.SetPathsAsync(referenceSearchPaths, sourceSearchPaths, workingDirectory).ConfigureAwait(false);
                UpdatePathsNoLock(result);
            }, _shutdownCancellationSource.Token);
        }

        private void UpdatePathsNoLock(RemoteExecutionResult result)
        {
            _workingDirectory = result.WorkingDirectory;
            _referenceSearchPaths = result.ReferencePaths;
            _sourceSearchPaths = result.SourcePaths;
        }
    }
}
