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

using System.Diagnostics;
using Microsoft.Build.Framework;
using NuGet.Packaging.Core;
using NuGet.ProjectModel;

namespace Microsoft.NET.Build.Tasks
{
    internal class ProjectContext
    {
        public class RuntimeFramework
        {
            public string Name { get; set; }
            public string Version { get; set; }

            public RuntimeFramework() { }

            public RuntimeFramework(ITaskItem item)
            {
                Name = item.ItemSpec;
                Version = item.GetMetadata(MetadataKeys.Version);
            }
        }

        private const string NetCorePlatformLibrary = "Microsoft.NETCore.App";
        private readonly LockFile _lockFile;
        private readonly LockFileTarget _lockFileTarget;
        internal HashSet<PackageIdentity> PackagesToBeFiltered { get; set; }

        /// <summary>
        /// A value indicating that this project runs on a shared system-wide framework.
        /// (ex. Microsoft.NETCore.App for .NET Core)
        /// </summary>
        public bool IsFrameworkDependent { get; }

        /// <summary>
        /// A value indicating that this project is portable across operating systems, processor architectures, etc.
        /// </summary>
        /// <remarks>
        /// Returns <c>true</c> for projects running on shared frameworks (<see cref="IsFrameworkDependent" />)
        /// that do not target a specific RID.
        /// </remarks>
        public bool IsPortable => IsFrameworkDependent && string.IsNullOrEmpty(_lockFileTarget.RuntimeIdentifier);

        public LockFileTargetLibrary PlatformLibrary { get; }

        public RuntimeFramework[] RuntimeFrameworks { get; }

        public LockFile LockFile => _lockFile;
        public LockFileTarget LockFileTarget => _lockFileTarget;

        public LockFileTarget CompilationLockFileTarget { get; }

        public ProjectContext(LockFile lockFile, LockFileTarget lockFileTarget,
            //  Trimmed from publish output, and if there are no runtimeFrameworks, written to runtimeconfig.json
            LockFileTargetLibrary platformLibrary,
            //  Written to runtimeconfig.json
            RuntimeFramework[] runtimeFrameworks,
            bool isFrameworkDependent)
        {
            Debug.Assert(lockFile != null);
            Debug.Assert(lockFileTarget != null);
            if (isFrameworkDependent)
            {
                Debug.Assert(platformLibrary != null ||
                    (runtimeFrameworks != null && runtimeFrameworks.Any()));
            }

            _lockFile = lockFile;
            _lockFileTarget = lockFileTarget;
            if (string.IsNullOrEmpty(lockFileTarget.RuntimeIdentifier))
            {
                CompilationLockFileTarget = lockFileTarget;
            }
            else
            {
                var frameworkAlias = lockFile.GetLockFileTargetAlias(lockFileTarget);
                CompilationLockFileTarget = lockFile.GetTargetAndThrowIfNotFound(frameworkAlias, null);
            }

            PlatformLibrary = platformLibrary;
            RuntimeFrameworks = runtimeFrameworks;
            IsFrameworkDependent = isFrameworkDependent;
        }

        public IEnumerable<LockFileTargetLibrary> GetRuntimeLibraries(IEnumerable<string> excludeFromPublishPackageIds)
        {
            IEnumerable<LockFileTargetLibrary> runtimeLibraries = _lockFileTarget.Libraries;
            Dictionary<string, LockFileTargetLibrary> libraryLookup =
                runtimeLibraries.ToDictionary(e => e.Name, StringComparer.OrdinalIgnoreCase);

            HashSet<string> allExclusionList = new(StringComparer.OrdinalIgnoreCase);

            if (IsFrameworkDependent && PlatformLibrary != null)
            {
                allExclusionList.UnionWith(_lockFileTarget.GetPlatformExclusionList(PlatformLibrary, libraryLookup));

                // If the platform library is not Microsoft.NETCore.App, treat it as an implicit dependency.
                // This makes it so Microsoft.AspNet.* 2.x platforms also exclude Microsoft.NETCore.App files.
                if (PlatformLibrary.Name.Length > 0 && !string.Equals(PlatformLibrary.Name, NetCorePlatformLibrary, StringComparison.OrdinalIgnoreCase))
                {
                    var library = _lockFileTarget.GetLibrary(NetCorePlatformLibrary);
                    if (library != null)
                    {
                        allExclusionList.UnionWith(_lockFileTarget.GetPlatformExclusionList(library, libraryLookup));
                    }
                }
            }

            if (excludeFromPublishPackageIds?.Any() == true)
            {
                HashSet<string> excludeFromPublishList =
                    GetExcludeFromPublishList(
                        excludeFromPublishPackageIds,
                        libraryLookup);

                allExclusionList.UnionWith(excludeFromPublishList);
            }

            if (PackagesToBeFiltered != null)
            {
                var filterLookup = new Dictionary<string, HashSet<PackageIdentity>>(StringComparer.OrdinalIgnoreCase);
                foreach (var pkg in PackagesToBeFiltered)
                {
                    HashSet<PackageIdentity> packageinfos;
                    if (filterLookup.TryGetValue(pkg.Id, out packageinfos))
                    {
                        packageinfos.Add(pkg);
                    }
                    else
                    {
                        packageinfos = new HashSet<PackageIdentity>();
                        packageinfos.Add(pkg);
                        filterLookup.Add(pkg.Id, packageinfos);
                    }
                }

                allExclusionList.UnionWith(GetPackagesToBeFiltered(filterLookup, libraryLookup));
            }

            return runtimeLibraries.Filter(allExclusionList).ToArray();
        }

        internal IEnumerable<PackageIdentity> GetTransitiveList(string package, bool ignoreIfNotFound = false)
        {
            LockFileTargetLibrary platformLibrary = _lockFileTarget.GetLibrary(package);
            if (platformLibrary == null && ignoreIfNotFound)
            {
                return Enumerable.Empty<PackageIdentity>();
            }
            IEnumerable<LockFileTargetLibrary> runtimeLibraries = _lockFileTarget.Libraries;
            Dictionary<string, LockFileTargetLibrary> libraryLookup =
                runtimeLibraries.ToDictionary(e => e.Name, StringComparer.OrdinalIgnoreCase);

            return _lockFileTarget.GetTransitivePackagesList(platformLibrary, libraryLookup);
        }

        public IEnumerable<LockFileTargetLibrary> GetCompileLibraries(IEnumerable<string> compileExcludeFromPublishPackageIds)
        {
            IEnumerable<LockFileTargetLibrary> compileLibraries = _lockFileTarget.Libraries;

            if (compileExcludeFromPublishPackageIds?.Any() == true)
            {
                Dictionary<string, LockFileTargetLibrary> libraryLookup =
                    compileLibraries.ToDictionary(e => e.Name, StringComparer.OrdinalIgnoreCase);

                HashSet<string> excludeFromPublishList =
                    GetExcludeFromPublishList(
                        compileExcludeFromPublishPackageIds,
                        libraryLookup);

                compileLibraries = compileLibraries.Filter(excludeFromPublishList);
            }

            return compileLibraries.ToArray();
        }

        public IEnumerable<string> GetTopLevelDependencies()
        {
            return GetTopLevelDependencies(LockFile, LockFileTarget);
        }

        public static IEnumerable<string> GetTopLevelDependencies(LockFile lockFile, LockFileTarget lockFileTarget)
        {
            Dictionary<string, LockFileTargetLibrary> libraryLookup =
                lockFileTarget.Libraries.ToDictionary(l => l.Name, StringComparer.OrdinalIgnoreCase);

            string lockFileTargetFramework = lockFileTarget.Name.Split('/')[0];

            return lockFile
                .ProjectFileDependencyGroups
                .Where(dg => dg.FrameworkName == string.Empty ||
                             dg.FrameworkName == lockFileTargetFramework)
                .SelectMany(g => g.Dependencies)
                .Select(projectFileDependency =>
                {
                    int separatorIndex = projectFileDependency.IndexOf(' ');
                    string libraryName = separatorIndex > 0 ?
                        projectFileDependency.Substring(0, separatorIndex) :
                        projectFileDependency;

                    if (!string.IsNullOrEmpty(libraryName) && libraryLookup.ContainsKey(libraryName))
                    {
                        return libraryName;
                    }

                    return null;
                })
                .Where(libraryName => libraryName != null)
                .ToArray();
        }

        public HashSet<string> GetExcludeFromPublishList(
            IEnumerable<string> excludeFromPublishPackageIds,
            IDictionary<string, LockFileTargetLibrary> libraryLookup)
        {
            var nonExcludeFromPublishAssets = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            var nonExcludeFromPublishAssetsToSearch = new Stack<string>();
            var excludeFromPublishAssetsToSearch = new Stack<string>();

            // Start with the top-level dependencies, and put them into "private" or "non-private" buckets
            var excludeFromPublishPackagesLookup = new HashSet<string>(excludeFromPublishPackageIds, StringComparer.OrdinalIgnoreCase);
            foreach (var topLevelDependency in GetTopLevelDependencies())
            {
                if (!excludeFromPublishPackagesLookup.Contains(topLevelDependency))
                {
                    nonExcludeFromPublishAssetsToSearch.Push(topLevelDependency);
                    nonExcludeFromPublishAssets.Add(topLevelDependency);
                }
                else
                {
                    excludeFromPublishAssetsToSearch.Push(topLevelDependency);
                }
            }

            LockFileTargetLibrary library;
            string libraryName;

            // Walk all the non-private assets' dependencies and mark them as non-private
            while (nonExcludeFromPublishAssetsToSearch.Count > 0)
            {
                libraryName = nonExcludeFromPublishAssetsToSearch.Pop();
                if (libraryLookup.TryGetValue(libraryName, out library))
                {
                    foreach (var dependency in library.Dependencies)
                    {
                        if (!nonExcludeFromPublishAssets.Contains(dependency.Id))
                        {
                            nonExcludeFromPublishAssetsToSearch.Push(dependency.Id);
                            nonExcludeFromPublishAssets.Add(dependency.Id);
                        }
                    }
                }
            }

            // Go through assets marked private and their dependencies
            // For libraries not marked as non-private, mark them down as private
            var assetsToExcludeFromPublish = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
            while (excludeFromPublishAssetsToSearch.Count > 0)
            {
                libraryName = excludeFromPublishAssetsToSearch.Pop();
                if (libraryLookup.TryGetValue(libraryName, out library))
                {
                    assetsToExcludeFromPublish.Add(libraryName);

                    foreach (var dependency in library.Dependencies)
                    {
                        if (!nonExcludeFromPublishAssets.Contains(dependency.Id))
                        {
                            excludeFromPublishAssetsToSearch.Push(dependency.Id);
                        }
                    }
                }
            }

            return assetsToExcludeFromPublish;
        }
        private static HashSet<string> GetPackagesToBeFiltered(
          IDictionary<string, HashSet<PackageIdentity>> packagesToBeFiltered,
          IDictionary<string, LockFileTargetLibrary> packagesToBePublished)
        {
            var exclusionList = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

            foreach (var entry in packagesToBePublished)
            {
                HashSet<PackageIdentity> librarySet;

                if (packagesToBeFiltered.TryGetValue(entry.Key, out librarySet))
                {
                    LockFileTargetLibrary dependency = entry.Value;
                    foreach (var library in librarySet)
                    {
                        if (dependency.Version.Equals(library.Version))
                        {
                            exclusionList.Add(entry.Key);
                            break;
                        }
                    }
                }
            }

            return exclusionList;
        }
    }
}
