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

using Microsoft.Build.Framework;
using NuGet;
using NuGet.Versioning;
using NuGet.Packaging;
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using NuGet.Common;

namespace Microsoft.DotNet.Build.Tasks.Packaging
{
    public class NuGetPack : BuildTask
    {
        /// <summary>
        /// Target file paths to exclude when building the lib package for symbol server scenario
        /// Copied from https://github.com/NuGet/NuGet.Client/blob/59433c7bacaae435a2cfe343cd441ea710579304/src/NuGet.Core/NuGet.Commands/PackCommandRunner.cs#L48
        /// </summary>
        private static readonly string[] _libPackageExcludes = new[] {
            @"**\*.pdb".Replace('\\', Path.DirectorySeparatorChar),
            @"src\**\*".Replace('\\', Path.DirectorySeparatorChar)
        };

        /// <summary>
        /// Target file paths to exclude when building the symbols package for symbol server scenario
        /// </summary>
        private static readonly string[] _symbolPackageExcludes = new[] {
            @"content\**\*".Replace('\\', Path.DirectorySeparatorChar),
            @"tools\**\*.ps1".Replace('\\', Path.DirectorySeparatorChar)
        };

        private static readonly string _defaultPackedPackagePrefix = "transport";
        private static readonly string _symbolsPackageExtension = ".symbols.nupkg";
        private static readonly string _packageExtension = ".nupkg";

        [Required]
        public ITaskItem[] Nuspecs
        {
            get;
            set;
        }

        [Required]
        public string OutputDirectory
        {
            get;
            set;
        }

        public string BaseDirectory
        {
            get;
            set;
        }

        public string PackageVersion
        {
            get;
            set;
        }

        public bool ExcludeEmptyDirectories
        {
            get;
            set;
        }
        // Create an additional ".symbols.nupkg" package
        public bool CreateSymbolPackage
        {
            get;
            set;
        }
        // Include symbols in standard package
        public bool IncludeSymbolsInPackage
        {
            get;
            set;
        }
        // Create an additional "packed package" that includes lib and src / symbols
        public bool CreatePackedPackage
        {
            get;
            set;
        }
        /// <summary>
        /// Nuspec files can contain properties that are substituted with values at pack time
        /// This task property passes through the nuspec properties.
        /// Each item is a string with the syntax <key>=<value>
        /// String validation for <key> and <value> is deferred to the NuGet APIs
        /// </summary>
        public ITaskItem[] NuspecProperties
        {
            get;
            set;
        }

        // Library package excludes.
        // If set, only these excludes are used. Defaults and AdditionalLibPackageExcludes are not used.
        public ITaskItem[] LibPackageExcludes
        {
            get;
            set;
        }

        public ITaskItem[] AdditionalLibPackageExcludes
        {
            get;
            set;
        }

        public ITaskItem[] AdditionalSymbolPackageExcludes
        {
            get;
            set;
        }

        /// <summary>
        /// If set, the symbol package is placed in the given directory. Otherwise OutputDirectory is used.
        /// </summary>
        public string SymbolPackageOutputDirectory
        {
            get;
            set;
        }

        public string PackedPackageNamePrefix
        {
            get;
            set;
        }

        public override bool Execute()
        {
            if (Nuspecs == null || Nuspecs.Length == 0)
            {
                Log.LogError("Nuspecs argument must be specified");
                return false;
            }

            if (String.IsNullOrEmpty(OutputDirectory))
            {
                Log.LogError("OuputDirectory argument must be specified");
                return false;
            }

            if (!Directory.Exists(OutputDirectory))
            {
                Directory.CreateDirectory(OutputDirectory);
            }

            Func<string, string> nuspecPropertyProvider = GetNuspecPropertyProviderFunction(NuspecProperties);

            foreach (var nuspec in Nuspecs)
            {
                string nuspecPath = nuspec.GetMetadata("FullPath");

                if (!File.Exists(nuspecPath))
                {
                    Log.LogError($"Nuspec {nuspecPath} does not exist");
                    continue;
                }

                Manifest manifest = GetManifest(nuspecPath, nuspecPropertyProvider, false);
                string nupkgPath = GetPackageOutputPath(nuspecPath, manifest, false, false);
                Pack(nuspecPath, nupkgPath, manifest, IncludeSymbolsInPackage);

                bool packSymbols = CreateSymbolPackage || CreatePackedPackage;
                if (CreateSymbolPackage)
                {
                    Manifest symbolsManifest = GetManifest(nuspecPath, nuspecPropertyProvider, false);
                    nupkgPath = GetPackageOutputPath(nuspecPath, symbolsManifest, true, false);
                    Pack(nuspecPath, nupkgPath, symbolsManifest, packSymbols);
                }

                if (CreatePackedPackage)
                {
                    Manifest packedManifest = GetManifest(nuspecPath, nuspecPropertyProvider, true);
                    nupkgPath = GetPackageOutputPath(nuspecPath, packedManifest, false, true);
                    Pack(nuspecPath, nupkgPath, packedManifest, packSymbols);
                }
            }

            return !Log.HasLoggedErrors;
        }

        private static Func<string, string> GetNuspecPropertyProviderFunction(ITaskItem[] nuspecProperties)
        {
            return nuspecProperties == null ? null : NuspecPropertyStringProvider.GetNuspecPropertyProviderFunction(nuspecProperties.Select(p => p.ItemSpec).ToArray());
        }

        private Manifest GetManifest(string nuspecPath, Func<string, string> nuspecPropertyProvider, bool isPackedPackage)
        {
            using (var nuspecFile = File.Open(nuspecPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
            {
                string baseDirectoryPath = (string.IsNullOrEmpty(BaseDirectory)) ? Path.GetDirectoryName(nuspecPath) : BaseDirectory;
                Manifest manifest = Manifest.ReadFrom(nuspecFile, nuspecPropertyProvider, false);

                if (isPackedPackage)
                {
                    manifest = TransformManifestToPackedPackageManifest(manifest);
                }
                return manifest;
            }
        }

        private string GetPackageOutputPath(string nuspecPath, Manifest manifest, bool isSymbolsPackage, bool applyPrefix)
        {
            string id = manifest.Metadata.Id;

            if (String.IsNullOrEmpty(id))
            {
                Log.LogError($"Nuspec {nuspecPath} does not contain a valid Id");
                return string.Empty;
            }

            // Overriding the Version from the Metadata if one gets passed in.
            if (!string.IsNullOrEmpty(PackageVersion))
            {
                NuGetVersion overrideVersion;
                if (NuGetVersion.TryParse(PackageVersion, out overrideVersion))
                {
                    manifest.Metadata.Version = overrideVersion;
                }
                else
                {
                    Log.LogError($"Failed to parse Package Version: '{PackageVersion}' is not a valid version.");
                }
            }

            string version = manifest.Metadata.Version.ToString();

            if (String.IsNullOrEmpty(version))
            {
                Log.LogError($"Nuspec {nuspecPath} does not contain a valid version");
                return string.Empty;
            }

            string nupkgOutputDirectory = OutputDirectory;

            if (isSymbolsPackage && !string.IsNullOrEmpty(SymbolPackageOutputDirectory))
            {
                nupkgOutputDirectory = SymbolPackageOutputDirectory;
            }

            string nupkgExtension = isSymbolsPackage ? _symbolsPackageExtension : _packageExtension;
            return Path.Combine(nupkgOutputDirectory, $"{id}.{version}{nupkgExtension}");
        }

        public void Pack(string nuspecPath, string nupkgPath, Manifest manifest, bool packSymbols)
        {
            bool creatingSymbolsPackage = packSymbols && (Path.GetExtension(nupkgPath) == _symbolsPackageExtension);
            try
            {
                PackageBuilder builder = new PackageBuilder();

                string baseDirectoryPath = (string.IsNullOrEmpty(BaseDirectory)) ? Path.GetDirectoryName(nuspecPath) : BaseDirectory;
                builder.Populate(manifest.Metadata);
                builder.PopulateFiles(baseDirectoryPath, manifest.Files);

                if (creatingSymbolsPackage)
                {
                    // For symbols packages, filter out excludes
                    PathResolver.FilterPackageFiles(
                        builder.Files,
                        file => file.Path,
                        SymbolPackageExcludes);

                    // Symbol packages are only valid if they contain both symbols and sources.
                    Dictionary<string, bool> pathHasMatches = EffectiveLibPackageExcludes.ToDictionary(
                        path => path,
                        path => PathResolver.GetMatches(builder.Files, file => file.Path, new[] { path }).Any());

                    if (!pathHasMatches.Values.Any(i => i))
                    {
                        Log.LogMessage(LogImportance.Low, $"Nuspec {nuspecPath} does not contain symbol or source files. Not creating symbol package.");
                        return;
                    }
                    foreach (var pathPair in pathHasMatches.Where(pathMatchPair => !pathMatchPair.Value))
                    {
                        Log.LogMessage(LogImportance.Low, $"Nuspec {nuspecPath} does not contain any files matching {pathPair.Key}. Not creating symbol package.");
                        return;
                    }
                }
                else if(!packSymbols)
                {
                    // for packages which do not include symbols (not symbols or packed packages), filter lib excludes
                    PathResolver.FilterPackageFiles(
                        builder.Files,
                        file => file.Path,
                        EffectiveLibPackageExcludes);
                }

                var directory = Path.GetDirectoryName(nupkgPath);
                if (!Directory.Exists(directory))
                {
                    Directory.CreateDirectory(directory);
                }

                using (var fileStream = File.Create(nupkgPath))
                {
                    builder.Save(fileStream);
                }

                Log.LogMessage($"Created '{nupkgPath}'");
            }
            catch (Exception e)
            {
                string packageType = "lib";
                if (creatingSymbolsPackage)
                {
                    packageType = "symbol";
                }
                else if (packSymbols)
                {
                    packageType = "packed";
                }
                Log.LogError($"Error when creating nuget {packageType} package from {nuspecPath}. {e}");
            }
        }

        private Manifest TransformManifestToPackedPackageManifest(Manifest manifest)
        {
            ManifestMetadata manifestMetadata = manifest.Metadata;

            // Update Id
            string _packageNamePrefix = PackedPackageNamePrefix != null ? PackedPackageNamePrefix : _defaultPackedPackagePrefix;
            manifestMetadata.Id = $"{_packageNamePrefix}.{manifestMetadata.Id}";

            // Update dependencies
            List<PackageDependencyGroup> packedPackageDependencyGroups = new List<PackageDependencyGroup>();
            foreach(var dependencyGroup in manifestMetadata.DependencyGroups)
            {
                List<NuGet.Packaging.Core.PackageDependency> packages = new List<NuGet.Packaging.Core.PackageDependency>();
                foreach(var dependency in dependencyGroup.Packages)
                {
                    NuGet.Packaging.Core.PackageDependency package = new NuGet.Packaging.Core.PackageDependency($"{_packageNamePrefix}.{dependency.Id}", dependency.VersionRange, dependency.Include, dependency.Exclude);
                    packages.Add(package);
                }
                PackageDependencyGroup packageDependencyGroup = new PackageDependencyGroup(dependencyGroup.TargetFramework, packages);
                packedPackageDependencyGroups.Add(packageDependencyGroup);
            }
            manifestMetadata.DependencyGroups = packedPackageDependencyGroups;

            // Update runtime.json
            List<ManifestFile> manifestFiles = new List<ManifestFile>();

            foreach(ManifestFile file in manifest.Files)
            {
                string fileName = file.Source;
                if(Path.GetFileName(fileName) == "runtime.json" && file.Target == "")
                {
                    string packedPackageSourcePath = Path.Combine(Path.GetDirectoryName(fileName), string.Join(".", _packageNamePrefix, Path.GetFileName(fileName)));
                    file.Source = File.Exists(packedPackageSourcePath) ? packedPackageSourcePath : fileName;
                    file.Target = "runtime.json";
                }
                manifestFiles.Add(file);
            }
            Manifest packedPackageManifest = new Manifest(manifestMetadata, manifestFiles);
            return manifest;
        }

        private IEnumerable<string> EffectiveLibPackageExcludes
        {
            get
            {
                return LibPackageExcludes == null
                    ? _libPackageExcludes.Concat(AdditionalLibPackageExcludes?.Select(item => item.ItemSpec) ?? Enumerable.Empty<string>())
                    : LibPackageExcludes.Select(item => item.ItemSpec);
            }
        }

        private IEnumerable<string> SymbolPackageExcludes
        {
            get
            {
                return _symbolPackageExcludes
                    .Concat(AdditionalSymbolPackageExcludes?.Select(item => item.ItemSpec) ?? Enumerable.Empty<string>());
            }
        }
    }
}
