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

namespace NuGet.Packaging
{
    public class PackageFileExtractor
    {
        private readonly HashSet<string> _intellisenseXmlFiles;
        private readonly XmlDocFileSaveMode _xmlDocFileSaveMode;

        public PackageFileExtractor(IEnumerable<string> packageFiles, XmlDocFileSaveMode xmlDocFileSaveMode)
        {
            _xmlDocFileSaveMode = xmlDocFileSaveMode;
            if (xmlDocFileSaveMode == XmlDocFileSaveMode.Skip ||
                xmlDocFileSaveMode == XmlDocFileSaveMode.Compress)
            {
                _intellisenseXmlFiles = GatherIntellisenseXmlFiles(packageFiles);
            }
        }

        private static HashSet<string> GatherIntellisenseXmlFiles(IEnumerable<string> packageFiles)
        {
            var intellisenseXmlFiles = new HashSet<string>(StringComparer.Ordinal);

            var refAndLibFiles = packageFiles.Where(
                f => f.StartsWith("lib/", StringComparison.OrdinalIgnoreCase) ||
                f.StartsWith("ref/", StringComparison.OrdinalIgnoreCase))
                .ToArray();

            foreach (var file in refAndLibFiles)
            {
                if (!file.EndsWith(".xml"))
                {
                    continue;
                }

                // Xml files located next to the neutral language dll.
                var dllFile = Path.ChangeExtension(file, ".dll");
                if (refAndLibFiles.Contains(dllFile, StringComparer.OrdinalIgnoreCase))
                {
                    intellisenseXmlFiles.Add(file);
                    continue;
                }

                // For satellite assemblies, look for a corresponding .resources.dll
                var resourceDll = Path.ChangeExtension(file, ".resources.dll");
                if (refAndLibFiles.Contains(resourceDll, StringComparer.OrdinalIgnoreCase))
                {
                    intellisenseXmlFiles.Add(file);
                    continue;
                }

                // Xml files located in a language specific directory.
                var languageDllFile = GetBinaryForLanguageSpecificXml(file);
                if (languageDllFile != null && refAndLibFiles.Contains(languageDllFile, StringComparer.Ordinal))
                {
                    intellisenseXmlFiles.Add(file);
                }
            }

            return intellisenseXmlFiles;
        }

        private static string GetBinaryForLanguageSpecificXml(string file)
        {
            // For xml files located in language specific directories, look for a corresponding binary
            // in the parent directory.
            // e.g. ref/net45/fr/MyBinary.xml -> ref/net45/MyBinary.dll

            var fileSeparatorIndex = file.LastIndexOf('/');
            if (fileSeparatorIndex > 0)
            {
                var directorySeparatorIndex = file.LastIndexOf('/', fileSeparatorIndex - 1);
                if (directorySeparatorIndex >= 0)
                {
                    var fileAtParentDirectory =
                        file.Substring(0, directorySeparatorIndex) +
                        file.Substring(fileSeparatorIndex);
                    return Path.ChangeExtension(fileAtParentDirectory, ".dll");
                }
            }

            return null;
        }

        public string ExtractPackageFile(string source, string target, Stream stream)
        {
            if ((_xmlDocFileSaveMode == XmlDocFileSaveMode.Skip) && _intellisenseXmlFiles.Contains(source))
            {
                return null;
            }

            var extractDirectory = Path.GetDirectoryName(target);
            Directory.CreateDirectory(extractDirectory);

            if ((_xmlDocFileSaveMode == XmlDocFileSaveMode.Compress) && _intellisenseXmlFiles.Contains(source))
            {
                // If the package contains a file named {BinaryName}.xml.zip already exists, the result would be
                // ambigious.
                var targetZip = Path.ChangeExtension(target, ".xml.zip");
                using (var outputStream = File.Create(targetZip))
                using (var zipArchive = new ZipArchive(outputStream, ZipArchiveMode.Create))
                {
                    var entry = zipArchive.CreateEntry(Path.GetFileName(source));
                    using (var entryStream = entry.Open())
                    {
                        stream.CopyTo(entryStream);
                    }
                }

                return targetZip;
            }

            if (Path.IsPathRooted(source))
            {
                // Copying files stream-to-stream is less efficient. Attempt to copy using File.Copy if we the source
                // is a file on disk.
                File.Copy(source, target, overwrite: true);
            }
            else
            {
                stream.CopyToFile(target);
            }

            return target;
        }
    }
}
