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

using System;
using System.IO;
using System.Runtime.CompilerServices;
using Microsoft.Build.Shared.FileSystem;

#nullable disable

namespace Microsoft.Build.Shared
{
    /// <summary>
    /// This class contains utility methods for file IO.
    /// It is in a separate file so that it can be selectively included into an assembly.
    /// </summary>
    internal static partial class FileUtilities
    {
        // For the current user, these correspond to read, write, and execute permissions.
        // Lower order bits correspond to the same for "group" or "other" users.
        private const int userRWX = 0x100 | 0x80 | 0x40;
        private static string tempFileDirectory = null;
        internal static string TempFileDirectory
        {
            get
            {
                return tempFileDirectory ??= CreateFolderUnderTemp();
            }
        }

        internal static void ClearTempFileDirectory()
        {
            tempFileDirectory = null;
        }

        // For all native calls, directly check their return values to prevent bad actors from getting in between checking if a directory exists and returning it.
        private static string CreateFolderUnderTemp()
        {
            string basePath = Path.Combine(Path.GetTempPath(), $"MSBuildTemp{Environment.UserName}");

            if (NativeMethodsShared.IsLinux && NativeMethodsShared.mkdir(basePath, userRWX) != 0)
            {
                if (NativeMethodsShared.chmod(basePath, userRWX) == 0)
                {
                    // Current user owns this file; we can read and write to it. It is reasonable here to assume it was created properly by MSBuild and can be used
                    // for temporary files.
                }
                else
                {
                    // Another user created a folder pretending to be us! Find a folder we can actually use.
                    int extraBits = 0;
                    string pathToCheck = basePath + extraBits;
                    while (NativeMethodsShared.mkdir(pathToCheck, userRWX) != 0 && NativeMethodsShared.chmod(pathToCheck, userRWX) != 0)
                    {
                        extraBits++;
                        pathToCheck = basePath + extraBits;
                    }

                    basePath = pathToCheck;
                }
            }
            else
            {
                Directory.CreateDirectory(basePath);
            }

            return FileUtilities.EnsureTrailingSlash(basePath);
        }

        /// <summary>
        /// Generates a unique directory name in the temporary folder.
        /// Caller must delete when finished.
        /// </summary>
        /// <param name="createDirectory"></param>
        /// <param name="subfolder"></param>
        internal static string GetTemporaryDirectory(bool createDirectory = true, string subfolder = null)
        {
            string temporaryDirectory = Path.Combine(TempFileDirectory, "Temporary" + Guid.NewGuid().ToString("N"), subfolder ?? string.Empty);

            if (createDirectory)
            {
                Directory.CreateDirectory(temporaryDirectory);
            }

            return temporaryDirectory;
        }

        /// <summary>
        /// Generates a unique temporary file name with a given extension in the temporary folder.
        /// File is guaranteed to be unique.
        /// Extension may have an initial period.
        /// File will NOT be created.
        /// May throw IOException.
        /// </summary>
        internal static string GetTemporaryFileName()
        {
            return GetTemporaryFileName(".tmp");
        }

        /// <summary>
        /// Generates a unique temporary file name with a given extension in the temporary folder.
        /// File is guaranteed to be unique.
        /// Extension may have an initial period.
        /// File will NOT be created.
        /// May throw IOException.
        /// </summary>
        internal static string GetTemporaryFileName(string extension)
        {
            return GetTemporaryFile(null, null, extension, false);
        }

        /// <summary>
        /// Generates a unique temporary file name with a given extension in the temporary folder.
        /// If no extension is provided, uses ".tmp".
        /// File is guaranteed to be unique.
        /// Caller must delete it when finished.
        /// </summary>
        internal static string GetTemporaryFile()
        {
            return GetTemporaryFile(".tmp");
        }

        /// <summary>
        /// Generates a unique temporary file name with a given extension in the temporary folder.
        /// File is guaranteed to be unique.
        /// Caller must delete it when finished.
        /// </summary>
        internal static string GetTemporaryFile(string fileName, string extension, bool createFile)
        {
            return GetTemporaryFile(null, fileName, extension, createFile);
        }

        /// <summary>
        /// Generates a unique temporary file name with a given extension in the temporary folder.
        /// File is guaranteed to be unique.
        /// Extension may have an initial period.
        /// Caller must delete it when finished.
        /// May throw IOException.
        /// </summary>
        internal static string GetTemporaryFile(string extension)
        {
            return GetTemporaryFile(null, null, extension);
        }

        /// <summary>
        /// Creates a file with unique temporary file name with a given extension in the specified folder.
        /// File is guaranteed to be unique.
        /// Extension may have an initial period.
        /// If folder is null, the temporary folder will be used.
        /// Caller must delete it when finished.
        /// May throw IOException.
        /// </summary>
        internal static string GetTemporaryFile(string directory, string fileName, string extension, bool createFile = true)
        {
            ErrorUtilities.VerifyThrowArgumentLengthIfNotNull(directory, nameof(directory));

            try
            {
                directory ??= TempFileDirectory;

                // If the extension needs a dot prepended, do so.
                if (extension is null)
                {
                    extension = string.Empty;
                }
                else if (extension.Length > 0 && extension[0] != '.')
                {
                    extension = '.' + extension;
                }

                // If the fileName is null, use tmp{Guid}; otherwise use fileName.
                if (string.IsNullOrEmpty(fileName))
                {
                    fileName = $"tmp{Guid.NewGuid():N}";
                }

                Directory.CreateDirectory(directory);

                string file = Path.Combine(directory, $"{fileName}{extension}");

                ErrorUtilities.VerifyThrow(!FileSystems.Default.FileExists(file), "Guid should be unique");

                if (createFile)
                {
                    File.WriteAllText(file, string.Empty);
                }

                return file;
            }
            catch (Exception ex) when (ExceptionHandling.IsIoRelatedException(ex))
            {
                throw new IOException(ResourceUtilities.FormatResourceStringStripCodeAndKeyword("Shared.FailedCreatingTempFile", ex.Message), ex);
            }
        }

        internal static void CopyDirectory(string source, string dest)
        {
            Directory.CreateDirectory(dest);

            DirectoryInfo sourceInfo = new DirectoryInfo(source);
            foreach (var fileInfo in sourceInfo.GetFiles())
            {
                string destFile = Path.Combine(dest, fileInfo.Name);
                fileInfo.CopyTo(destFile);
            }
            foreach (var subdirInfo in sourceInfo.GetDirectories())
            {
                string destDir = Path.Combine(dest, subdirInfo.Name);
                CopyDirectory(subdirInfo.FullName, destDir);
            }
        }

        public sealed class TempWorkingDirectory : IDisposable
        {
            public string Path { get; }

            public TempWorkingDirectory(string sourcePath,
#if !CLR2COMPATIBILITY
                [CallerMemberName]
#endif
            string name = null)
            {
                Path = name == null
                    ? GetTemporaryDirectory()
                    : System.IO.Path.Combine(TempFileDirectory, name);

                if (FileSystems.Default.DirectoryExists(Path))
                {
                    Directory.Delete(Path, true);
                }

                CopyDirectory(sourcePath, Path);
            }

            public void Dispose()
            {
                Directory.Delete(Path, true);
            }
        }
    }
}
