// 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.Linq;
using System.Xml.Linq;

using NuGet.Packaging.Core;
using NuGet.Versioning;

namespace NuGet.Packaging.Xml
{
    internal static class PackageMetadataXmlExtensions
    {
        private const string References = "references";
        private const string Reference = "reference";
        private const string Group = "group";
        private const string File = "file";
        private const string TargetFramework = "targetFramework";
        private const string FrameworkAssemblies = "frameworkAssemblies";
        private const string FrameworkAssembly = "frameworkAssembly";
        private const string AssemblyName = "assemblyName";
        private const string Dependencies = "dependencies";
        private const string Files = "files";

        public static XElement ToXElement(this ManifestMetadata metadata, XNamespace ns)
        {
            var elem = new XElement(ns + "metadata");
            if (metadata.MinClientVersionString != null)
            {
                elem.SetAttributeValue("minClientVersion", metadata.MinClientVersionString);
            }

            elem.Add(new XElement(ns + "id", metadata.Id));
            AddElementIfNotNull(elem, ns, "version", metadata.Version?.ToNormalizedString());
            AddElementIfNotNull(elem, ns, "title", metadata.Title);
            AddElementIfNotNull(elem, ns, "authors", metadata.Authors, authors => string.Join(",", authors));
            AddElementIfNotNull(elem, ns, "owners", metadata.Owners, owners => string.Join(",", owners));
            elem.Add(new XElement(ns + "requireLicenseAcceptance", metadata.RequireLicenseAcceptance));
            if (metadata.DevelopmentDependency)
            {
                elem.Add(new XElement(ns + "developmentDependency", metadata.DevelopmentDependency));
            }
            AddElementIfNotNull(elem, ns, "licenseUrl", metadata.LicenseUrl);
            AddElementIfNotNull(elem, ns, "projectUrl", metadata.ProjectUrl);
            AddElementIfNotNull(elem, ns, "iconUrl", metadata.IconUrl);
            AddElementIfNotNull(elem, ns, "description", metadata.Description);
            AddElementIfNotNull(elem, ns, "summary", metadata.Summary);
            AddElementIfNotNull(elem, ns, "releaseNotes", metadata.ReleaseNotes);
            AddElementIfNotNull(elem, ns, "copyright", metadata.Copyright);
            AddElementIfNotNull(elem, ns, "language", metadata.Language);
            AddElementIfNotNull(elem, ns, "tags", metadata.Tags);

            elem.Add(GetXElementFromGroupableItemSets(
                ns,
                metadata.DependencyGroups,
                set => set.TargetFramework.IsSpecificFramework || 
                       set.Packages.Any(dependency => dependency.Exclude.Count > 0 || dependency.Include.Count > 0),
                set => set.TargetFramework.IsSpecificFramework ? set.TargetFramework.GetFrameworkString() : null,
                set => set.Packages,
                GetXElementFromPackageDependency,
                Dependencies,
                TargetFramework));

            elem.Add(GetXElementFromGroupableItemSets(
                ns,
                metadata.PackageAssemblyReferences,
                set => set.TargetFramework?.IsSpecificFramework == true,
                set => set.TargetFramework?.GetFrameworkString(),
                set => set.References,
                GetXElementFromPackageReference,
                References,
                TargetFramework));

            elem.Add(GetXElementFromFrameworkAssemblies(ns, metadata.FrameworkReferences));
            elem.Add(GetXElementFromManifestContentFiles(ns, metadata.ContentFiles));

            return elem;
        }

        private static XElement GetXElementFromGroupableItemSets<TSet, TItem>(
            XNamespace ns,
            IEnumerable<TSet> objectSets,
            Func<TSet, bool> isGroupable,
            Func<TSet, string> getGroupIdentifer,
            Func<TSet, IEnumerable<TItem>> getItems,
            Func<XNamespace, TItem, XElement> getXElementFromItem,
            string parentName,
            string identiferAttributeName)
        {
            if (objectSets == null || !objectSets.Any())
            {
                return null;
            }

            var groupableSets = new List<TSet>();
            var ungroupableSets = new List<TSet>();

            foreach (var set in objectSets)
            {
                if (isGroupable(set))
                {
                    groupableSets.Add(set);
                }
                else
                {
                    ungroupableSets.Add(set);
                }
            }

            var childElements = new List<XElement>();
            if (!groupableSets.Any())
            {
                // none of the item sets are groupable, then flatten the items
                childElements.AddRange(objectSets.SelectMany(getItems).Select(item => getXElementFromItem(ns, item)));
            }
            else
            {
                // move the group with null target framework (if any) to the front just for nicer display in UI
                foreach (var set in ungroupableSets.Concat(groupableSets))
                {
                    var groupElem = new XElement(
                        ns + Group,
                        getItems(set).Select(item => getXElementFromItem(ns, item)).ToArray());

                    if (isGroupable(set))
                    {
                        var groupIdentifier = getGroupIdentifer(set);
                        if (groupIdentifier != null)
                        {
                            groupElem.SetAttributeValue(identiferAttributeName, groupIdentifier);
                        }
                    }

                    childElements.Add(groupElem);
                }
            }

            return new XElement(ns + parentName, childElements.ToArray());
        }

        private static XElement GetXElementFromPackageReference(XNamespace ns, string reference)
        {
            return new XElement(ns + Reference, new XAttribute(File, reference));
        }

        private static XElement GetXElementFromPackageDependency(XNamespace ns, PackageDependency dependency)
        {
            List<XAttribute> attributes = new List<XAttribute>();

            attributes.Add(new XAttribute("id", dependency.Id));

            if (dependency.VersionRange != null && dependency.VersionRange != VersionRange.All)
            {
                attributes.Add(new XAttribute("version", dependency.VersionRange.ToLegacyShortString()));
            }

            if (dependency.Include != null && dependency.Include.Any())
            {
                attributes.Add(new XAttribute("include", string.Join(",", dependency.Include)));
            }

            if (dependency.Exclude != null && dependency.Exclude.Any())
            {
                attributes.Add(new XAttribute("exclude", string.Join(",", dependency.Exclude)));
            }

            return new XElement(ns + "dependency", attributes);
        }

        private static XElement GetXElementFromFrameworkAssemblies(XNamespace ns, IEnumerable<FrameworkAssemblyReference> references)
        {
            if (references == null || !references.Any())
            {
                return null;
            }

            return new XElement(
                ns + FrameworkAssemblies,
                references.Select(reference =>
                    new XElement(ns + FrameworkAssembly,
                        new XAttribute(AssemblyName, reference.AssemblyName),
                        reference.SupportedFrameworks != null && reference.SupportedFrameworks.Any() ?
                            new XAttribute("targetFramework", string.Join(", ", reference.SupportedFrameworks.Where(f => f.IsSpecificFramework).Select(f => f.GetFrameworkString()))) :
                            null)));
        }

        private static XElement GetXElementFromManifestContentFiles(XNamespace ns, IEnumerable<ManifestContentFiles> contentFiles)
        {
            if (contentFiles == null || !contentFiles.Any())
            {
                return null;
            }

            return new XElement(ns + "contentFiles",
                contentFiles.Select(file => GetXElementFromManifestContentFile(ns, file)));
        }

        private static XElement GetXElementFromManifestContentFile(XNamespace ns, ManifestContentFiles file)
        {
            List<XAttribute> attributes = new List<XAttribute>();

            attributes.Add(GetXAttributeFromNameAndValue("include", file.Include));
            attributes.Add(GetXAttributeFromNameAndValue("exclude", file.Exclude));
            attributes.Add(GetXAttributeFromNameAndValue("buildAction", file.BuildAction));
            attributes.Add(GetXAttributeFromNameAndValue("copyToOutput", file.CopyToOutput));
            attributes.Add(GetXAttributeFromNameAndValue("flatten", file.Flatten));

            attributes = attributes.Where(xAtt => xAtt != null).ToList();

            return new XElement(ns + Files, attributes);
        }

        private static XAttribute GetXAttributeFromNameAndValue(string name, object value)
        {
            if (name == null || value == null)
            {
                return null;
            }

            return new XAttribute(name, value);
        }

        private static void AddElementIfNotNull<T>(XElement parent, XNamespace ns, string name, T value)
            where T : class
        {
            if (value != null)
            {
                parent.Add(new XElement(ns + name, value));
            }
        }

        private static void AddElementIfNotNull<T>(XElement parent, XNamespace ns, string name, T value, Func<T, object> process)
            where T : class
        {
            if (value != null)
            {
                var processed = process(value);
                if (processed != null)
                {
                    parent.Add(new XElement(ns + name, processed));
                }
            }
        }
    }
}
