// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NuGet.Packaging.Core;

namespace NuGet.Packaging
{
    public static class TopologicalSortUtility
    {
        /// <summary>
        /// Order dependencies by children first.
        /// </summary>
        /// <param name="items">Items to sort.</param>
        /// <param name="comparer">Comparer for Ids.</param>
        /// <param name="getId">Retrieve the id of the item.</param>
        /// <param name="getDependencies">Retrieve dependency ids.</param>
        /// <returns></returns>
        public static IReadOnlyList<T> SortPackagesByDependencyOrder<T>(
            IEnumerable<T> items,
            StringComparer comparer,
            Func<T, string> getId,
            Func<T, string[]> getDependencies) where T : class
        {
            if (items == null)
            {
                throw new ArgumentNullException(nameof(items));
            }

            if (comparer == null)
            {
                throw new ArgumentNullException(nameof(comparer));
            }

            if (getId == null)
            {
                throw new ArgumentNullException(nameof(getId));
            }

            if (getDependencies == null)
            {
                throw new ArgumentNullException(nameof(getDependencies));
            }

            // De-dupe and create a lookup table for mapping items back after sorting
            var lookup = new Dictionary<string, T>(comparer);
            var itemInfos = new List<ItemDependencyInfo>();

            foreach (var item in items)
            {
                var id = getId(item);
                var deps = getDependencies(item);

                if (!lookup.ContainsKey(id))
                {
                    lookup.Add(id, item);
                    itemInfos.Add(new ItemDependencyInfo(id, deps));
                }
            }

            // Sort
            var sortedInfos = SortPackagesByDependencyOrder(itemInfos, comparer);

            // ItemInfo -> Original item
            var sorted = new List<T>(sortedInfos.Count);
            foreach (var item in sortedInfos)
            {
                sorted.Add(lookup[item.Id]);
            }

            return sorted;
        }

        /// <summary>
        /// Order dependencies by children first.
        /// </summary>
        public static IReadOnlyList<PackageDependencyInfo> SortPackagesByDependencyOrder(
            IEnumerable<PackageDependencyInfo> packages)
        {
            return SortPackagesByDependencyOrder(
                packages,
                StringComparer.OrdinalIgnoreCase,
                GetPackageDependencyInfoId,
                GetPackageDependencyInfoDependencies);
        }

        /// <summary>
        /// Order dependencies by children first.
        /// </summary>
        private static List<ItemDependencyInfo> SortPackagesByDependencyOrder(List<ItemDependencyInfo> items, StringComparer comparer)
        {
            var lookup = new Dictionary<string, ItemDependencyInfo>(comparer);
            var itemComparer = new PackageInfoComparer(comparer);

            //Deduplicate references
            foreach (var item in items)
            {
                // These are deduped before they are added here
                lookup.Add(item.Id, item);
            }

            // Extract the deduplicated values
            var toSort = lookup.Values.ToArray();
            var sorted = new List<ItemDependencyInfo>(toSort.Length);

            CalculateRelationships(toSort, lookup);

            for (var i = 0; i < toSort.Length; i++)
            {
                Array.Sort(toSort, i, toSort.Length - i, itemComparer);
                // take the child with the lowest number of children
                var package = toSort[i];
                sorted.Add(package);
                UpdateChildCounts(package);
            }

            // the list is ordered by parents first, reverse to run children first
            sorted.Reverse();

            return sorted;
        }

        private static void UpdateChildCounts(ItemDependencyInfo package)
        {
            // Decrement the parent count for each child of this package.
            var children = package.Children;
            if (children != null)
            {
                var count = children.Count;
                for (var i = 0; i < count; i++)
                {
                    children[i].ActiveParents--;
                }
            }
        }

        private static void CalculateRelationships(ItemDependencyInfo[] packages, Dictionary<string, ItemDependencyInfo> lookup)
        {
            foreach (var package in packages)
            {
                var dependencies = package.DependencyIds ?? Array.Empty<string>();

                foreach (var id in dependencies)
                {
                    if (lookup.TryGetValue(id, out var dependencyPackage))
                    {
                        // Mark the current package as a parent
                        var parents = dependencyPackage.Parents;
                        if (parents == null)
                        {
                            parents = new List<ItemDependencyInfo>();
                            dependencyPackage.Parents = parents;
                        }
                        parents.Add(package);

                        // Add a child package for the current package
                        var packageChildren = package.Children;
                        if (packageChildren == null)
                        {
                            packageChildren = new List<ItemDependencyInfo>();
                            package.Children = packageChildren;
                        }
                        packageChildren.Add(dependencyPackage);
                    }
                }
            }

            foreach (var package in packages)
            {
                package.ActiveParents = package.Parents?.Count ?? 0;
            }
        }


        private static string GetPackageDependencyInfoId(PackageDependencyInfo info)
        {
            return info.Id;
        }

        private static string[] GetPackageDependencyInfoDependencies(PackageDependencyInfo info)
        {
            return info.Dependencies.Select(e => e.Id).ToArray();
        }

        private class PackageInfoComparer : IComparer<ItemDependencyInfo>
        {
            private readonly StringComparer _comparer;

            public PackageInfoComparer(StringComparer comparer)
            {
                _comparer = comparer ?? throw new ArgumentNullException(nameof(comparer));
            }

            public int Compare(ItemDependencyInfo x, ItemDependencyInfo y)
            {
                // Order packages by parent count
                if (x.ActiveParents < y.ActiveParents)
                {
                    return -1;
                }
                if (x.ActiveParents > y.ActiveParents)
                {
                    return 1;
                }

                return _comparer.Compare(x.Id, y.Id);
            }
        }

        [DebuggerDisplay("{Package.Id} Active: {ActiveParents}")]
        private sealed class ItemDependencyInfo
        {
            public ItemDependencyInfo(string id, string[] dependencyIds)
            {
                ActiveParents = 0;
                Id = id;
                DependencyIds = dependencyIds;
            }

            public string Id;
            public string[] DependencyIds;

            public int ActiveParents;
            public List<ItemDependencyInfo> Parents;
            public List<ItemDependencyInfo> Children;
        }
    }
}
