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

using System.Globalization;

namespace Microsoft.NET.Sdk.Razor.Tool
{
    // Note that this class has no thread-safety guarantees. The caller should use a lock 
    // if concurrency is required.
    internal class ShadowCopyManager : IDisposable
    {
        // Note that this class uses the *existance* of the Mutex to lock a directory.
        //
        // Nothing in this code actually ever acquires the Mutex, we just try to see if it exists
        // already.
        private readonly Mutex _mutex;

        private int _counter;

        public ShadowCopyManager(string baseDirectory = null)
        {
            BaseDirectory = baseDirectory ?? Path.Combine(Path.GetTempPath(), "Razor", "ShadowCopy");

            var guid = Guid.NewGuid().ToString("N").ToLowerInvariant();
            UniqueDirectory = Path.Combine(BaseDirectory, guid);

            _mutex = new Mutex(initiallyOwned: false, name: guid);

            Directory.CreateDirectory(UniqueDirectory);
        }

        public string BaseDirectory { get; }

        public string UniqueDirectory { get; }

        public string AddAssembly(string filePath)
        {
            var assemblyDirectory = CreateUniqueDirectory();

            var destination = Path.Combine(assemblyDirectory, Path.GetFileName(filePath));
            CopyFile(filePath, destination);

            var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(filePath);
            var resourcesNameWithoutExtension = fileNameWithoutExtension + ".resources";
            var resourcesNameWithExtension = resourcesNameWithoutExtension + ".dll";

            foreach (var directory in Directory.EnumerateDirectories(Path.GetDirectoryName(filePath)))
            {
                var directoryName = Path.GetFileName(directory);

                var resourcesPath = Path.Combine(directory, resourcesNameWithExtension);
                if (File.Exists(resourcesPath))
                {
                    var resourcesShadowCopyPath = Path.Combine(assemblyDirectory, directoryName, resourcesNameWithExtension);
                    CopyFile(resourcesPath, resourcesShadowCopyPath);
                }

                resourcesPath = Path.Combine(directory, resourcesNameWithoutExtension, resourcesNameWithExtension);
                if (File.Exists(resourcesPath))
                {
                    var resourcesShadowCopyPath = Path.Combine(assemblyDirectory, directoryName, resourcesNameWithoutExtension, resourcesNameWithExtension);
                    CopyFile(resourcesPath, resourcesShadowCopyPath);
                }
            }

            return destination;
        }

        public void Dispose()
        {
            _mutex.ReleaseMutex();
        }

        public Task PurgeUnusedDirectoriesAsync()
        {
            return Task.Run((Action)PurgeUnusedDirectories);
        }

        private string CreateUniqueDirectory()
        {
            var id = _counter++;

            var directory = Path.Combine(UniqueDirectory, id.ToString(CultureInfo.InvariantCulture));
            Directory.CreateDirectory(directory);
            return directory;
        }

        private void CopyFile(string originalPath, string shadowCopyPath)
        {
            var directory = Path.GetDirectoryName(shadowCopyPath);
            Directory.CreateDirectory(directory);

            File.Copy(originalPath, shadowCopyPath);

            MakeWritable(new FileInfo(shadowCopyPath));
        }

        private void MakeWritable(string directoryPath)
        {
            var directory = new DirectoryInfo(directoryPath);

            foreach (var file in directory.EnumerateFiles(searchPattern: "*", searchOption: SearchOption.AllDirectories))
            {
                MakeWritable(file);
            }
        }

        private void MakeWritable(FileInfo file)
        {
            try
            {
                if (file.IsReadOnly)
                {
                    file.IsReadOnly = false;
                }
            }
            catch
            {
                // There are many reasons this could fail. Ignore it and keep going.
            }
        }

        private void PurgeUnusedDirectories()
        {
            IEnumerable<string> directories;
            try
            {
                directories = Directory.EnumerateDirectories(BaseDirectory);
            }
            catch (DirectoryNotFoundException)
            {
                return;
            }

            foreach (var directory in directories)
            {
                Mutex mutex = null;
                try
                {
                    // We only want to try deleting the directory if no-one else is currently using it.
                    //
                    // Note that the mutex name is the name of the directory. This is OK because we're using
                    // GUIDs as directory/mutex names.
                    if (!Mutex.TryOpenExisting(Path.GetFileName(directory).ToLowerInvariant(), out mutex))
                    {
                        MakeWritable(directory);
                        Directory.Delete(directory, recursive: true);
                    }
                }
                catch
                {
                    // If something goes wrong we will leave it to the next run to clean up.
                    // Just swallow the exception and move on.
                }
                finally
                {
                    if (mutex != null)
                    {
                        mutex.Dispose();
                    }
                }
            }
        }
    }
}
