﻿// 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.CodeAnalysis.Editor.Implementation;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.VisualStudio.OLE.Interop;

namespace Microsoft.VisualStudio.LanguageServices.Packaging
{
    internal partial class PackageInstallerService
    {
        private async Task<bool> TryInstallAndAddUndoActionAsync(
            string source, string packageName, string? version, bool includePrerelease,
            Guid projectGuid, EnvDTE.DTE dte, EnvDTE.Project dteProject, IOleUndoManager undoManager,
            IProgressTracker progressTracker, CancellationToken cancellationToken)
        {
            var installed = await TryInstallPackageAsync(
                source, packageName, version, includePrerelease,
                projectGuid, dte, dteProject, progressTracker, cancellationToken).ConfigureAwait(false);
            if (installed)
            {
                // if the install succeeded, then add an uninstall item to the undo manager.
                await this.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
                undoManager?.Add(new UninstallPackageUndoUnit(
                    this, source, packageName,
                    version, includePrerelease,
                    projectGuid, dte, dteProject, undoManager));
            }

            return installed;
        }

        private async Task<bool> TryUninstallAndAddRedoActionAsync(
            string source, string packageName, string? version, bool includePrerelease,
            Guid projectGuid, EnvDTE.DTE dte, EnvDTE.Project dteProject, IOleUndoManager undoManager,
            IProgressTracker progressTracker, CancellationToken cancellationToken)
        {
            var uninstalled = await TryUninstallPackageAsync(
                packageName, projectGuid, dte, dteProject, progressTracker, cancellationToken).ConfigureAwait(false);
            if (uninstalled)
            {
                // if the install succeeded, then add an uninstall item to the undo manager.
                await this.ThreadingContext.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
                undoManager?.Add(new InstallPackageUndoUnit(
                    this, source, packageName,
                    version, includePrerelease,
                    projectGuid, dte, dteProject, undoManager));
            }

            return uninstalled;
        }

        private abstract class BaseUndoUnit : IOleUndoUnit
        {
            protected readonly Guid projectGuid;
            protected readonly EnvDTE.DTE dte;
            protected readonly EnvDTE.Project dteProject;
            protected readonly PackageInstallerService packageInstallerService;
            protected readonly string source;
            protected readonly string packageName;
            protected readonly IOleUndoManager undoManager;
            protected readonly string? version;
            protected readonly bool includePrerelease;

            protected BaseUndoUnit(
                PackageInstallerService packageInstallerService,
                string source,
                string packageName,
                string? version,
                bool includePrerelease,
                Guid projectGuid,
                EnvDTE.DTE dte,
                EnvDTE.Project dteProject,
                IOleUndoManager undoManager)
            {
                this.packageInstallerService = packageInstallerService;
                this.source = source;
                this.packageName = packageName;
                this.version = version;
                this.includePrerelease = includePrerelease;
                this.projectGuid = projectGuid;
                this.dte = dte;
                this.dteProject = dteProject;
                this.undoManager = undoManager;
            }

            protected abstract Task DoWorkerAsync(IOleUndoManager pUndoManager);
            public abstract void GetDescription(out string pBstr);

            public void Do(IOleUndoManager pUndoManager)
            {
                var token = this.packageInstallerService._listener.BeginAsyncOperation($"{GetType().Name}.{nameof(Do)}");
                DoAsync(pUndoManager).CompletesAsyncOperation(token);
            }

            private async Task DoAsync(IOleUndoManager pUndoManager)
            {
                try
                {
                    await DoWorkerAsync(pUndoManager).ConfigureAwait(false);
                }
                catch (OperationCanceledException)
                {
                }
                catch (Exception e) when (FatalError.ReportAndCatch(e))
                {
                }
            }

            public void GetUnitType(out Guid pClsid, out int plID)
                => throw new NotImplementedException();

            public void OnNextAdd()
            {

            }
        }

        private class UninstallPackageUndoUnit : BaseUndoUnit
        {
            public UninstallPackageUndoUnit(
                PackageInstallerService packageInstallerService,
                string source,
                string packageName,
                string? version,
                bool includePrerelease,
                Guid projectGuid,
                EnvDTE.DTE dte,
                EnvDTE.Project dteProject,
                IOleUndoManager undoManager)
                : base(packageInstallerService, source, packageName,
                       version, includePrerelease,
                       projectGuid, dte, dteProject, undoManager)
            {
            }

            protected override async Task DoWorkerAsync(IOleUndoManager pUndoManager)
            {
                var description = string.Format(ServicesVSResources.Uninstalling_0, packageName);
                using var context = this.packageInstallerService._operationExecutor.BeginExecute(NugetTitle, description, allowCancellation: true, showProgress: false);
                using var scope = context.AddScope(allowCancellation: true, description);

                await packageInstallerService.TryUninstallAndAddRedoActionAsync(
                    source, packageName, version, includePrerelease,
                    projectGuid, dte, dteProject, undoManager,
                    new UIThreadOperationContextProgressTracker(scope),
                    context.UserCancellationToken).ConfigureAwait(false);
            }

            public override void GetDescription(out string pBstr)
                => pBstr = string.Format(ServicesVSResources.Uninstall_0, packageName);
        }

        private class InstallPackageUndoUnit : BaseUndoUnit
        {
            public InstallPackageUndoUnit(
                PackageInstallerService packageInstallerService,
                string source,
                string packageName,
                string? version,
                bool includePrerelease,
                Guid projectGuid,
                EnvDTE.DTE dte,
                EnvDTE.Project dteProject,
                IOleUndoManager undoManager)
                : base(packageInstallerService, source, packageName,
                       version, includePrerelease,
                       projectGuid, dte, dteProject, undoManager)
            {
            }

            public override void GetDescription(out string pBstr)
                => pBstr = string.Format(ServicesVSResources.Install_0, packageName);

            protected override async Task DoWorkerAsync(IOleUndoManager pUndoManager)
            {
                var description = string.Format(ServicesVSResources.Installing_0, packageName);
                using var context = this.packageInstallerService._operationExecutor.BeginExecute(NugetTitle, description, allowCancellation: true, showProgress: false);
                using var scope = context.AddScope(allowCancellation: true, description);

                await packageInstallerService.TryInstallAndAddUndoActionAsync(
                    source, packageName, version, includePrerelease,
                    projectGuid, dte, dteProject, undoManager,
                    new UIThreadOperationContextProgressTracker(scope),
                    context.UserCancellationToken).ConfigureAwait(false);
            }
        }
    }
}
