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

using Microsoft.TemplateEngine.Abstractions;
using Microsoft.TemplateEngine.Abstractions.TemplatePackage;
using Microsoft.TemplateEngine.Edge.Settings;
using Microsoft.TemplateEngine.Utils;

namespace Microsoft.TemplateEngine.Cli
{
    /// <summary>
    /// The class represents template group. Templates in single group:<br/>
    /// - should same group identity
    /// - should have different template identity <br/>
    /// - same short name (however different short names are also supported) <br/>
    /// - the templates may have different languages and types <br/>
    /// - the templates should have different precedence value in case same language is used <br/>
    /// - the templates in the group may have different parameters and different choices for parameter symbols defined<br/>
    /// In case the template does not have group identity defined it represents separate template group with single template.
    /// </summary>
    internal sealed class TemplateGroup
    {
        /// <summary>
        /// Constructor of TemplateGroup.
        /// </summary>
        /// <param name="templates">the templates of the template group.</param>
        /// <exception cref="ArgumentNullException">when <paramref name="templates"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentException">when <paramref name="templates"/> is empty or don't have same <see cref="ITemplateMetadata.GroupIdentity"/> defined.</exception>
        internal TemplateGroup(IEnumerable<CliTemplateInfo> templates)
        {
            _ = templates ?? throw new ArgumentNullException(paramName: nameof(templates));
            if (!templates.Any())
            {
                throw new ArgumentException(paramName: nameof(templates), message: "The templates collection cannot be empty");
            }

            try
            {
                //all templates in the group should have same group identity
                GroupIdentity = templates.Select(t => string.IsNullOrWhiteSpace(t.GroupIdentity) ? null : t.GroupIdentity)
                                            .Distinct(StringComparer.OrdinalIgnoreCase)
                                            .Single();
            }
            catch (InvalidOperationException)
            {
                throw new ArgumentException(paramName: nameof(templates), message: "The templates should have same group identity");
            }
            Templates = templates.ToList();
        }

        /// <summary>
        /// Group identity of template group. The value can be null if the template does not have group identity set.
        /// </summary>
        internal string? GroupIdentity { get; private set; }

        /// <summary>
        /// Returns the list of short names defined for templates in the group.
        /// In theory, template group templates can have different short names but they are treated equally.
        /// </summary>
        internal IReadOnlyList<string> ShortNames
        {
            get
            {
                HashSet<string> shortNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                foreach (ITemplateInfo template in Templates.OrderByDescending(t => t.Precedence))
                {
                    shortNames.UnionWith(template.ShortNameList);
                }
                return shortNames.ToList();
            }
        }

        /// <summary>
        /// Returns the list of languages defined for templates in the group.
        /// </summary>
        internal IReadOnlyList<string?> Languages
        {
            get
            {
                HashSet<string?> language = new HashSet<string?>(StringComparer.OrdinalIgnoreCase);
                foreach (ITemplateInfo template in Templates)
                {
                    language.Add(template.GetLanguage());
                }
                return language.ToList();
            }
        }

        /// <summary>
        /// Returns the list of types defined for templates in the group.
        /// </summary>
        internal IReadOnlyList<string?> Types
        {
            get
            {
                HashSet<string?> type = new HashSet<string?>(StringComparer.OrdinalIgnoreCase);
                foreach (ITemplateInfo template in Templates)
                {
                    type.Add(template.GetTemplateType());
                }
                return type.ToList();
            }
        }

        /// <summary>
        /// Returns the list of baselines defined for templates in the group.
        /// </summary>
        internal IReadOnlyList<string> Baselines
        {
            get
            {
                HashSet<string> baselines = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                foreach (ITemplateInfo template in Templates)
                {
                    foreach (var baseline in template.BaselineInfo)
                    {
                        baselines.Add(baseline.Key);
                    }
                }
                return baselines.ToList();
            }
        }

        /// <summary>
        /// Returns the full name of template group
        /// Template group name is the name of highest precedence template in the group.
        /// If multiple templates have the maximum precedence, the name of first one is returned.
        /// </summary>
        internal string Name
        {
            get
            {
                return GetHighestPrecedenceTemplates().First().Name;
            }
        }

        /// <summary>
        /// Returns the description of template group.
        /// Template group description is the description of the template in the group with the highest precedence.
        /// If multiple templates have the maximum precedence, the description of the first one is returned.
        /// </summary>
        internal string Description
        {
            get
            {
                return GetHighestPrecedenceTemplates().First().Description ?? string.Empty;
            }
        }

        /// <summary>
        /// Returns the authors of template group.
        /// If different templates have different authors, lists all of them.
        /// </summary>
        internal IReadOnlyList<string> Authors
        {
            get
            {
                HashSet<string> authors = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
                foreach (ITemplateInfo template in Templates)
                {
                    if (!string.IsNullOrWhiteSpace(template.Author))
                    {
                        authors.Add(template.Author);
                    }
                }
                return authors.ToList();
            }
        }

        /// <summary>
        /// Returns true when <see cref="GroupIdentity"/> is not <c>null</c> or empty.
        /// </summary>
        internal bool HasGroupIdentity => !string.IsNullOrWhiteSpace(GroupIdentity);

        /// <summary>
        /// Returns true when the template group has single template.
        /// </summary>
        internal bool HasSingleTemplate => Templates.Count == 1;

        /// <summary>
        /// Returns the list of templates in the group.
        /// </summary>
        internal IReadOnlyList<CliTemplateInfo> Templates { get; private set; }

        internal static IEnumerable<TemplateGroup> FromTemplateList(IEnumerable<CliTemplateInfo> templates)
        {
            return templates
              .GroupBy(x => x.GroupIdentity, x => !string.IsNullOrEmpty(x.GroupIdentity), StringComparer.OrdinalIgnoreCase)
              .Select(group => new TemplateGroup(group.ToList()));
        }

        /// <summary>
        /// Gets the list of <b>managed</b> template packages which contain templates of template group.
        /// </summary>
        /// <remarks>
        /// The method might throw exceptions if <see cref="TemplatePackageManager.GetTemplatePackageAsync(ITemplateInfo, CancellationToken)"/> call throws.
        /// </remarks>
        internal async Task<IReadOnlyList<IManagedTemplatePackage>> GetManagedTemplatePackagesAsync(
            TemplatePackageManager templatePackageManager,
            CancellationToken cancellationToken)
        {
            var templatePackages = await GetTemplatePackagesAsync(templatePackageManager, cancellationToken).ConfigureAwait(false);

            return templatePackages.OfType<IManagedTemplatePackage>().ToArray();
        }

        /// <summary>
        /// Gets the list of template packages which contain templates of template group.
        /// </summary>
        /// <remarks>
        /// The method might throw exceptions if <see cref="TemplatePackageManager.GetTemplatePackageAsync(ITemplateInfo, CancellationToken)"/> call throws.
        /// </remarks>
        internal async Task<IReadOnlyList<ITemplatePackage>> GetTemplatePackagesAsync(
            TemplatePackageManager templatePackageManager,
            CancellationToken cancellationToken)
        {
            var templatePackages = await Task.WhenAll(Templates.Select(t => templatePackageManager.GetTemplatePackageAsync(t, cancellationToken))).ConfigureAwait(false);
            return templatePackages.Distinct().ToArray();
        }

        private IEnumerable<ITemplateInfo> GetHighestPrecedenceTemplates()
        {
            if (!Templates.Any())
            {
                throw new Exception($"{nameof(Templates)} cannot be empty collection");
            }

            int highestPrecedence = Templates.Max(t => t.Precedence);
            return Templates.Where(t => t.Precedence == highestPrecedence);
        }
    }
}
