// 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.Globalization;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using NuGet.Versioning;

namespace NuGet.Packaging.Core
{
    /// <summary>
    /// A very basic Nuspec reader that understands the Id, Version, PackageType, and MinClientVersion of a package.
    /// </summary>
    public abstract class NuspecCoreReaderBase : INuspecCoreReader
    {
        private readonly XDocument _xml;
        private XElement _metadataNode;

        protected const string Metadata = "metadata";
        protected const string Id = "id";
        protected const string Version = "version";
        protected const string MinClientVersion = "minClientVersion";
        protected const string DevelopmentDependency = "developmentDependency";
        protected const string PackageType = "packageType";
        protected const string PackageTypeName = "name";
        protected const string PackageTypeVersion = "version";

        /// <summary>
        /// Read a nuspec from a path.
        /// </summary>
        public NuspecCoreReaderBase(string path)
        {
            if (path == null)
            {
                throw new ArgumentNullException(nameof(path));
            }

            _xml = LoadXml(File.OpenRead(path), leaveStreamOpen: false);
        }

        /// <summary>
        /// Read a nuspec from a stream.
        /// </summary>
        public NuspecCoreReaderBase(Stream stream)
            : this(stream, leaveStreamOpen: false)
        {
        }

        /// <summary>
        /// Read a nuspec from a stream.
        /// </summary>
        public NuspecCoreReaderBase(Stream stream, bool leaveStreamOpen)
        {
            if (stream == null)
            {
                throw new ArgumentNullException(nameof(stream));
            }

            _xml = LoadXml(stream, leaveStreamOpen);
        }

        /// <summary>
        /// Reads a nuspec from XML
        /// </summary>
        public NuspecCoreReaderBase(XDocument xml)
        {
            if (xml == null)
            {
                throw new ArgumentNullException(nameof(xml));
            }

            _xml = xml;
        }

        /// <summary>
        /// Id of the package
        /// </summary>
        public virtual string GetId()
        {
            var node = MetadataNode.Elements(XName.Get(Id, MetadataNode.GetDefaultNamespace().NamespaceName)).FirstOrDefault();
            return node == null ? null : node.Value;
        }

        /// <summary>
        /// Version of the package
        /// </summary>
        public virtual NuGetVersion GetVersion()
        {
            var node = MetadataNode.Elements(XName.Get(Version, MetadataNode.GetDefaultNamespace().NamespaceName)).FirstOrDefault();
            return node == null ? null : NuGetVersion.Parse(node.Value);
        }

        /// <summary>
        /// The minimum client version this package supports.
        /// </summary>
        public virtual NuGetVersion GetMinClientVersion()
        {
            var node = MetadataNode.Attribute(XName.Get(MinClientVersion));
            return node == null ? null : NuGetVersion.Parse(node.Value);
        }

        /// <summary>
        /// Gets zero or more package types from the .nuspec.
        /// </summary>
        public virtual IReadOnlyList<PackageType> GetPackageTypes()
        {
            var nodes = MetadataNode.Elements(XName.Get(
                PackageType,
                MetadataNode.GetDefaultNamespace().NamespaceName));

            var packageTypes = new List<PackageType>();
            foreach (var node in nodes)
            {
                // Get the required package type name.
                var nameAttribute = node.Attribute(XName.Get(PackageTypeName));

                if (nameAttribute == null || string.IsNullOrWhiteSpace(nameAttribute.Value))
                {
                    throw new PackagingException(Strings.MissingPackageTypeName);
                }

                var name = nameAttribute.Value.Trim();

                // Get the optional package type version.
                var versionAttribute = node.Attribute(XName.Get(PackageTypeVersion));
                Version version;

                if (versionAttribute != null)
                {
                    if (!System.Version.TryParse(versionAttribute.Value, out version))
                    {
                        throw new PackagingException(string.Format(
                            CultureInfo.CurrentCulture,
                            Strings.InvalidPackageTypeVersion,
                            versionAttribute.Value));
                    }
                }
                else
                {
                    version = Core.PackageType.EmptyVersion;
                }

                packageTypes.Add(new PackageType(name, version));
            }

            return packageTypes;
        }

        /// <summary>
        /// The developmentDependency attribute
        /// </summary>
        public virtual bool GetDevelopmentDependency()
        {
            var node = MetadataNode.Elements(XName.Get(DevelopmentDependency, MetadataNode.GetDefaultNamespace().NamespaceName)).FirstOrDefault();
            return node == null ? false : bool.Parse(node.Value);
        }

        /// <summary>
        /// Nuspec Metadata
        /// </summary>
        public virtual IEnumerable<KeyValuePair<string, string>> GetMetadata()
        {
            // Remove the PackageType element prior to returning the resulting metadata.
            var filteredMetadataElements = MetadataNode.Elements().Where(
                element => !element.HasElements &&
                !String.IsNullOrEmpty(element.Value) &&
                !element.Name.LocalName.Equals(PackageType, StringComparison.OrdinalIgnoreCase));


            return filteredMetadataElements
                .Select(element => new KeyValuePair<string, string>(element.Name.LocalName, element.Value));
        }

        protected XElement MetadataNode
        {
            get
            {
                if (_metadataNode == null)
                {
                    // find the metadata node regardless of the NS, some legacy packages have the NS here instead of on package
                    _metadataNode = _xml.Root.Elements().Where(e => StringComparer.Ordinal.Equals(e.Name.LocalName, Metadata)).FirstOrDefault();

                    if (_metadataNode == null)
                    {
                        throw new PackagingException(string.Format(
                            CultureInfo.CurrentCulture,
                            Strings.MissingMetadataNode,
                            Metadata));
                    }
                }

                return _metadataNode;
            }
        }

        /// <summary>
        /// Raw XML doc
        /// </summary>
        public XDocument Xml
        {
            get { return _xml; }
        }

        public virtual PackageIdentity GetIdentity()
        {
            return new PackageIdentity(GetId(), GetVersion());
        }

        private static XDocument LoadXml(Stream stream, bool leaveStreamOpen)
        {
            using (var xmlReader = XmlReader.Create(stream, new XmlReaderSettings
            {
                CloseInput = !leaveStreamOpen,
                IgnoreWhitespace = true,
                IgnoreComments = true,
                IgnoreProcessingInstructions = true
            }))
            {
                return XDocument.Load(xmlReader, LoadOptions.None);
            }
        }
    }
}
