// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Abstractions.Installer;
using Microsoft.TemplateEngine.Abstractions.TemplatePackage;
using Microsoft.TemplateEngine.Edge;
using Microsoft.TemplateEngine.Edge.Settings;
using Microsoft.TemplateEngine.Edge.Template;
using ITemplateCreationResult = Microsoft.TemplateEngine.Edge.Template.ITemplateCreationResult;
using ITemplateMatchInfo = Microsoft.TemplateEngine.Abstractions.TemplateFiltering.ITemplateMatchInfo;
using MatchInfo = Microsoft.TemplateEngine.Abstractions.TemplateFiltering.MatchInfo;
using TemplateCreator = Microsoft.TemplateEngine.Edge.Template.TemplateCreator;
using WellKnownSearchFilters = Microsoft.TemplateEngine.Utils.WellKnownSearchFilters;

namespace Microsoft.TemplateEngine.IDE
{
    public class Bootstrapper : IDisposable
    {
        private readonly ITemplateEngineHost _host;
        private readonly TemplateCreator _templateCreator;
        private readonly TemplatePackageManager _templatePackagesManager;
        private readonly EngineEnvironmentSettings _engineEnvironmentSettings;

        /// <summary>
        /// Creates the instance.
        /// </summary>
        /// <param name="host">caller <see cref="ITemplateEngineHost"/>.</param>
        /// <param name="virtualizeConfiguration">if true, settings will be stored in memory and will be disposed with instance.</param>
        /// <param name="loadDefaultComponents">if true, the default components (providers, installers, generator) will be loaded. Same as calling <see cref="LoadDefaultComponents()"/> after instance is created.</param>
        /// <param name="hostSettingsLocation">the file path to store host specific settings. Use null for default location.
        /// Note: this parameter changes only directory of host and host version specific settings. Global settings path remains unchanged.</param>
        /// <param name="environment">optional environment to be used (defaults to <see cref="DefaultEnvironment"/>).</param>
        public Bootstrapper(
            ITemplateEngineHost host,
            bool virtualizeConfiguration,
            bool loadDefaultComponents = true,
            string? hostSettingsLocation = null,
            IEnvironment? environment = null)
        {
            _host = host ?? throw new ArgumentNullException(nameof(host));
            environment ??= new DefaultEnvironment();

            if (string.IsNullOrWhiteSpace(hostSettingsLocation))
            {
                _engineEnvironmentSettings = new EngineEnvironmentSettings(host, virtualizeSettings: virtualizeConfiguration, environment: environment);
            }
            else
            {
                string hostSettingsDir = Path.Combine(hostSettingsLocation, host.HostIdentifier);
                string hostVersionSettingsDir = Path.Combine(hostSettingsLocation, host.HostIdentifier, host.Version);
                IPathInfo pathInfo = new DefaultPathInfo(environment, host, hostSettingsDir: hostSettingsDir, hostVersionSettingsDir: hostVersionSettingsDir);
                _engineEnvironmentSettings = new EngineEnvironmentSettings(
                    host,
                    virtualizeSettings: virtualizeConfiguration,
                    environment: environment,
                    pathInfo: pathInfo);
            }

            _templateCreator = new TemplateCreator(_engineEnvironmentSettings);
            _templatePackagesManager = new TemplatePackageManager(_engineEnvironmentSettings);
            if (loadDefaultComponents)
            {
                LoadDefaultComponents();
            }
        }

        /// <summary>
        /// Loads default components: template package providers and installers defined in Microsoft.TemplateEngine.Edge and default template generator defined in Microsoft.TemplateEngine.Orchestrator.RunnableProjects.
        /// </summary>
        public void LoadDefaultComponents()
        {
            foreach ((Type Type, IIdentifiedComponent Instance) component in Orchestrator.RunnableProjects.Components.AllComponents)
            {
                AddComponent(component.Type, component.Instance);
            }
            foreach ((Type Type, IIdentifiedComponent Instance) component in Components.AllComponents)
            {
                AddComponent(component.Type, component.Instance);
            }
        }

        /// <summary>
        /// Adds component to manager, which can be looked up later via <see cref="IComponentManager.TryGetComponent{T}(Guid, out T)"/> or <see cref="IComponentManager.OfType{T}"/>.
        /// Added components are not persisted and need to be called every time new instance of <see cref="Bootstrapper"/> is created.
        /// </summary>
        /// <param name="interfaceType">Interface type that added component implements.</param>
        /// <param name="component">Instance of type that implements <paramref name="interfaceType"/>.</param>
        public void AddComponent(Type interfaceType, IIdentifiedComponent component)
        {
            _engineEnvironmentSettings.Components.AddComponent(interfaceType, component);
        }

        /// <summary>
        /// Gets list of all available templates.
        /// </summary>
        /// <param name="cancellationToken"></param>
        /// <returns>The list of all available templates.</returns>
        public Task<IReadOnlyList<ITemplateInfo>> GetTemplatesAsync(CancellationToken cancellationToken)
        {
            return _templatePackagesManager.GetTemplatesAsync(cancellationToken);
        }

        /// <summary>
        /// Gets list of available templates, if <paramref name="filters"/> is provided returns only matching templates.
        /// </summary>
        /// <param name="filters">List of filters to apply. See <see cref="Utils.WellKnownSearchFilters"/> for predefined filters.</param>
        /// <param name="exactMatchesOnly">
        /// true: templates should match all filters; false: templates should match any filter.
        /// </param>
        /// <param name="cancellationToken"></param>
        /// <returns>Filtered list of available templates with details on the applied filters matches.</returns>
        public Task<IReadOnlyList<ITemplateMatchInfo>> GetTemplatesAsync(IEnumerable<Func<ITemplateInfo, MatchInfo?>> filters, bool exactMatchesOnly = true, CancellationToken cancellationToken = default)
        {
            Func<ITemplateMatchInfo, bool> criteria;
            if (filters == null || !filters.Any())
            {
                // returns all templates
                criteria = (t) => true;
                filters ??= Array.Empty<Func<ITemplateInfo, MatchInfo?>>();
            }
            else
            {
                criteria = exactMatchesOnly ? WellKnownSearchFilters.MatchesAllCriteria : WellKnownSearchFilters.MatchesAtLeastOneCriteria;
            }

            return _templatePackagesManager.GetTemplatesAsync(criteria, filters, cancellationToken);
        }

        /// <summary>
        /// Instantiates the template.
        /// </summary>
        /// <param name="info">The template to instantiate.</param>
        /// <param name="name">The name to use.</param>
        /// <param name="outputPath">The output directory for template instantiation.</param>
        /// <param name="parameters">The template parameters.</param>
        /// <param name="baselineName">The baseline configuration to use.</param>
        /// <param name="cancellationToken">A cancellation token to cancel the asynchronous operation.</param>
        /// <returns><see cref="ITemplateCreationResult"/> containing information on created template or error occurred.</returns>
#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
        public Task<ITemplateCreationResult> CreateAsync(
            ITemplateInfo info,
            string? name,
            string outputPath,
            IReadOnlyDictionary<string, string?> parameters,
            string? baselineName = null,
            CancellationToken cancellationToken = default)
#pragma warning restore RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
        {
            return _templateCreator.InstantiateAsync(
                info,
                name,
                fallbackName: null,
                outputPath: outputPath,
                inputParameters: parameters,
                forceCreation: false,
                baselineName: baselineName,
                dryRun: false,
                cancellationToken: cancellationToken);
        }

        /// <summary>
        /// Instantiates the template.
        /// </summary>
        /// <param name="info">The template to instantiate.</param>
        /// <param name="name">The name to use.</param>
        /// <param name="outputPath">The output directory for template instantiation.</param>
        /// <param name="inputParameters">The template parameters.</param>
        /// <param name="baselineName">The baseline configuration to use.</param>
        /// <param name="cancellationToken">A cancellation token to cancel the asynchronous operation.</param>
        /// <returns><see cref="ITemplateCreationResult"/> containing information on created template or error occurred.</returns>
#pragma warning disable RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
#pragma warning disable RS0026 // Do not add multiple public overloads with optional parameters
        public Task<ITemplateCreationResult> CreateAsync(
            ITemplateInfo info,
            string? name,
            string outputPath,
            InputDataSet? inputParameters,
            string? baselineName = null,
            CancellationToken cancellationToken = default)
#pragma warning restore RS0027 // Public API with optional parameter(s) should have the most parameters amongst its public overloads
#pragma warning restore RS0026 // Do not add multiple public overloads with optional parameters
        {
            return _templateCreator.InstantiateAsync(
                info,
                name,
                fallbackName: null,
                outputPath: outputPath,
                inputParameters: inputParameters,
                forceCreation: false,
                baselineName: baselineName,
                dryRun: false,
                cancellationToken: cancellationToken);
        }

        /// <summary>
        /// Dry runs the template with given parameters.
        /// </summary>
        /// <param name="info">The template to instantiate.</param>
        /// <param name="name">The name to use.</param>
        /// <param name="outputPath">The output directory for template instantiation.</param>
        /// <param name="parameters">The template parameters.</param>
        /// <param name="baselineName">The baseline configuration to use.</param>
        /// <param name="cancellationToken">A cancellation token to cancel the asynchronous operation.</param>
        /// <returns><see cref="ITemplateCreationResult"/> containing information on template that would be created or error occurred.</returns>
        public Task<ITemplateCreationResult> GetCreationEffectsAsync(
            ITemplateInfo info,
            string? name,
            string outputPath,
            IReadOnlyDictionary<string, string?> parameters,
            string? baselineName = null,
            CancellationToken cancellationToken = default)
        {
            return _templateCreator.InstantiateAsync(
                info,
                name,
                fallbackName: null,
                outputPath: outputPath,
                inputParameters: parameters,
                forceCreation: false,
                baselineName: baselineName,
                dryRun: true,
                cancellationToken: cancellationToken);
        }

        #region Template Package Management

        /// <summary>
        /// Gets the list of available template packages.
        /// </summary>
        /// <param name="cancellationToken"></param>
        /// <returns>the list of the template packages.</returns>
        public Task<IReadOnlyList<ITemplatePackage>> GetTemplatePackagesAsync(CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            return _templatePackagesManager.GetTemplatePackagesAsync(false, cancellationToken);
        }

        /// <summary>
        /// Gets the list of available managed template packages.
        /// </summary>
        /// <param name="cancellationToken"></param>
        /// <returns>the list of managed template packages.</returns>
        public Task<IReadOnlyList<IManagedTemplatePackage>> GetManagedTemplatePackagesAsync(CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            return _templatePackagesManager.GetManagedTemplatePackagesAsync(false, cancellationToken);
        }

        /// <summary>
        /// Installs the template packages
        /// The following template packages are supported by default:
        /// - the NuGet package from NuGet feed
        /// - the NuGet package available at the path
        /// - the folder containing the template.
        /// </summary>
        /// <param name="installRequests">the list of <see cref="InstallRequest"/> to install.</param>
        /// <param name="scope"><see cref="InstallationScope"/> to use.</param>
        /// <param name="cancellationToken"></param>
        /// <returns>the list of <see cref="InstallResult"/> containing installation result for each <see cref="InstallRequest"/>.</returns>
        public Task<IReadOnlyList<InstallResult>> InstallTemplatePackagesAsync(IEnumerable<InstallRequest> installRequests, InstallationScope scope = InstallationScope.Global, CancellationToken cancellationToken = default)
        {
            _ = installRequests ?? throw new ArgumentNullException(nameof(installRequests));
            cancellationToken.ThrowIfCancellationRequested();

            if (!installRequests.Any())
            {
                return Task.FromResult((IReadOnlyList<InstallResult>)new List<InstallResult>());
            }

            IManagedTemplatePackageProvider managedPackageProvider;
            switch (scope)
            {
                case InstallationScope.Global:
                default:
                    {
                        managedPackageProvider = _templatePackagesManager.GetBuiltInManagedProvider(InstallationScope.Global);
                        break;
                    }
            }

            return managedPackageProvider.InstallAsync(installRequests, cancellationToken);
        }

        /// <summary>
        /// Gets the latest template package version for <paramref name="managedPackages"/>.
        /// </summary>
        /// <param name="managedPackages">the template packages to check the version for.</param>
        /// <param name="cancellationToken"></param>
        /// <returns>the list of <see cref="CheckUpdateResult"/> containing the result for each <see cref="IManagedTemplatePackage"/>.</returns>
        public async Task<IReadOnlyList<CheckUpdateResult>> GetLatestVersionsAsync(IEnumerable<IManagedTemplatePackage> managedPackages, CancellationToken cancellationToken = default)
        {
            _ = managedPackages ?? throw new ArgumentNullException(nameof(managedPackages));
            cancellationToken.ThrowIfCancellationRequested();

            if (!managedPackages.Any())
            {
                return new List<CheckUpdateResult>();
            }

            IEnumerable<IGrouping<IManagedTemplatePackageProvider, IManagedTemplatePackage>> requestsGroupedByProvider = managedPackages.GroupBy(package => package.ManagedProvider, package => package);
            IReadOnlyList<CheckUpdateResult>[] results = await Task.WhenAll(requestsGroupedByProvider.Select(packages => packages.Key.GetLatestVersionsAsync(packages, cancellationToken))).ConfigureAwait(false);

            return results.SelectMany(result => result).ToList();
        }

        /// <summary>
        /// Updates the template packages to version specified in <see cref="UpdateRequest"/>.
        /// </summary>
        /// <param name="updateRequests">the list of <see cref="UpdateRequest"/> to perform.</param>
        /// <param name="cancellationToken"></param>
        /// <returns>the list of <see cref="UpdateResult"/> containing the result for each <see cref="UpdateRequest"/>.</returns>
        public async Task<IReadOnlyList<UpdateResult>> UpdateTemplatePackagesAsync(IEnumerable<UpdateRequest> updateRequests, CancellationToken cancellationToken = default)
        {
            _ = updateRequests ?? throw new ArgumentNullException(nameof(updateRequests));
            cancellationToken.ThrowIfCancellationRequested();

            if (!updateRequests.Any())
            {
                return new List<UpdateResult>();
            }

            IEnumerable<IGrouping<IManagedTemplatePackageProvider, UpdateRequest>> requestsGroupedByProvider = updateRequests.GroupBy(request => request.TemplatePackage.ManagedProvider, request => request);
            IReadOnlyList<UpdateResult>[] updateResults = await Task.WhenAll(requestsGroupedByProvider.Select(requests => requests.Key.UpdateAsync(requests, cancellationToken))).ConfigureAwait(false);

            return updateResults.SelectMany(result => result).ToList();
        }

        /// <summary>
        /// Uninstalls the template packages.
        /// </summary>
        /// <param name="managedPackages">the list of <see cref="IManagedTemplatePackage"/> to uninstall.</param>
        /// <param name="cancellationToken"></param>
        /// <returns>the list of <see cref="UninstallResult"/> containing the result for each <see cref="IManagedTemplatePackage"/>.</returns>
        public async Task<IReadOnlyList<UninstallResult>> UninstallTemplatePackagesAsync(IEnumerable<IManagedTemplatePackage> managedPackages, CancellationToken cancellationToken = default)
        {
            _ = managedPackages ?? throw new ArgumentNullException(nameof(managedPackages));
            cancellationToken.ThrowIfCancellationRequested();

            if (!managedPackages.Any())
            {
                return new List<UninstallResult>();
            }

            IEnumerable<IGrouping<IManagedTemplatePackageProvider, IManagedTemplatePackage>> requestsGroupedByProvider = managedPackages.GroupBy(package => package.ManagedProvider, package => package);
            IReadOnlyList<UninstallResult>[] uninstallResults = await Task.WhenAll(requestsGroupedByProvider.Select(packages => packages.Key.UninstallAsync(packages, cancellationToken))).ConfigureAwait(false);

            return uninstallResults.SelectMany(result => result).ToList();
        }

        #endregion Template Package Management

        public void Dispose()
        {
            _templatePackagesManager.Dispose();
            _engineEnvironmentSettings?.Dispose();
        }

        #region Obsolete

        [Obsolete("Use " + nameof(GetTemplatesAsync) + "instead")]
        public async Task<IReadOnlyCollection<Edge.Template.IFilteredTemplateInfo>> ListTemplates(bool exactMatchesOnly, params Func<ITemplateInfo, Edge.Template.MatchInfo?>[] filters)
        {
            return TemplateListFilter.FilterTemplates(await _templatePackagesManager.GetTemplatesAsync(default).ConfigureAwait(false), exactMatchesOnly, filters);
        }

        [Obsolete("Use ITemplateEngineHost.BuiltInComponents or AddComponent to add components.")]
        public void Register(Type type)
        {
            _engineEnvironmentSettings.Components.Register(type);
        }

        [Obsolete("Use ITemplateEngineHost.BuiltInComponents or AddComponent to add components.")]
        public void Register(Assembly assembly)
        {
            _engineEnvironmentSettings.Components.RegisterMany(assembly.GetTypes());
        }

        [Obsolete("use Task<IReadOnlyList<InstallResult>> InstallTemplatePackagesAsync(IEnumerable<InstallRequest> installRequests, InstallationScope scope = InstallationScope.Global, CancellationToken cancellationToken = default) instead")]
        public void Install(string path)
        {
            Install(new[] { path });
        }

        [Obsolete("use Task<IReadOnlyList<InstallResult>> InstallTemplatePackagesAsync(IEnumerable<InstallRequest> installRequests, InstallationScope scope = InstallationScope.Global, CancellationToken cancellationToken = default) instead")]
        public void Install(params string[] paths)
        {
            Install((IEnumerable<string>)paths);
        }

        [Obsolete("use Task<IReadOnlyList<InstallResult>> InstallTemplatePackagesAsync(IEnumerable<InstallRequest> installRequests, InstallationScope scope = InstallationScope.Global, CancellationToken cancellationToken = default) instead")]
        public void Install(IEnumerable<string> paths)
        {
            _ = paths ?? throw new ArgumentNullException(nameof(paths));

            if (!paths.Any())
            {
                return;
            }

            var installRequests = paths.Select(path => new InstallRequest(path)).ToList();
            Task<IReadOnlyList<InstallResult>> t = InstallTemplatePackagesAsync(installRequests);
            t.Wait();
        }

        [Obsolete("use Task<IReadOnlyList<UninstallResult>> UninstallTemplatePackagesAsync(IEnumerable<IManagedTemplatePackage> managedPackages, CancellationToken cancellationToken = default) instead")]
        public IEnumerable<string> Uninstall(string path)
        {
            return Uninstall(new[] { path });
        }

        [Obsolete("use Task<IReadOnlyList<UninstallResult>> UninstallTemplatePackagesAsync(IEnumerable<IManagedTemplatePackage> managedPackages, CancellationToken cancellationToken = default) instead")]
        public IEnumerable<string> Uninstall(params string[] paths)
        {
            return Uninstall((IEnumerable<string>)paths);
        }

        [Obsolete("use Task<IReadOnlyList<UninstallResult>> UninstallTemplatePackagesAsync(IEnumerable<IManagedTemplatePackage> managedPackages, CancellationToken cancellationToken = default) instead")]
        public IEnumerable<string> Uninstall(IEnumerable<string> paths)
        {
            _ = paths ?? throw new ArgumentNullException(nameof(paths));

            if (!paths.Any())
            {
                return Array.Empty<string>();
            }

            var task = GetManagedTemplatePackagesAsync();
            task.Wait();
            var templatePackages = task.Result;

            var packagesToUninstall = new List<IManagedTemplatePackage>();
            foreach (string path in paths)
            {
                packagesToUninstall.AddRange(templatePackages.Where(package => package.Identifier.Equals(path, StringComparison.OrdinalIgnoreCase)));
            }

            Task<IReadOnlyList<UninstallResult>> uninstallTask = UninstallTemplatePackagesAsync(packagesToUninstall);
            uninstallTask.Wait();
            return uninstallTask.Result
                .Where(result => result.Success)
                .Select(result => result.TemplatePackage!.Identifier);
        }

        [Obsolete("Use Task<TemplateCreationResult> CreateAsync(ITemplateInfo info, string? name, string outputPath, IReadOnlyDictionary<string, string> parameters, string? baselineName = null, CancellationToken cancellationToken = default) instead.")]
        public async Task<ICreationResult?> CreateAsync(ITemplateInfo info, string name, string outputPath, IReadOnlyDictionary<string, string?> parameters, bool skipUpdateCheck, string baselineName)
        {
            ITemplateCreationResult instantiateResult = await _templateCreator.InstantiateAsync(info, name, name, outputPath, parameters, false, baselineName).ConfigureAwait(false);
            return instantiateResult.CreationResult;
        }

        [Obsolete("Use Task<TemplateCreationResult> GetCreationEffectsAsync(ITemplateInfo info, string? name, string outputPath, IReadOnlyDictionary<string, string> parameters, string? baselineName = null, CancellationToken cancellationToken = default) instead.")]
        public async Task<ICreationEffects?> GetCreationEffectsAsync(ITemplateInfo info, string name, string outputPath, IReadOnlyDictionary<string, string?> parameters, string baselineName)
        {
            ITemplateCreationResult instantiateResult = await _templateCreator.InstantiateAsync(info, name, name, outputPath, parameters, false, baselineName, true).ConfigureAwait(false);
            return instantiateResult.CreationEffects;
        }

        #endregion Obsolete
    }
}
