// <copyright file="FileUtilities.cs" company="Selenium Committers">
// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.
// </copyright>

using OpenQA.Selenium.Internal.Logging;
using System;
using System.Globalization;
using System.IO;
using System.Reflection;

namespace OpenQA.Selenium.Internal;

/// <summary>
/// Encapsulates methods for working with files.
/// </summary>
internal static class FileUtilities
{
    private static readonly ILogger logger = Log.GetLogger(typeof(FileUtilities));

    /// <summary>
    /// Recursively copies a directory.
    /// </summary>
    /// <param name="sourceDirectory">The source directory to copy.</param>
    /// <param name="destinationDirectory">The destination directory.</param>
    /// <returns><see langword="true"/> if the copy is completed; otherwise <see langword="false"/>.</returns>
    public static bool CopyDirectory(string sourceDirectory, string destinationDirectory)
    {
        bool copyComplete;
        DirectoryInfo sourceDirectoryInfo = new DirectoryInfo(sourceDirectory);
        DirectoryInfo destinationDirectoryInfo = new DirectoryInfo(destinationDirectory);

        if (sourceDirectoryInfo.Exists)
        {
            if (!destinationDirectoryInfo.Exists)
            {
                destinationDirectoryInfo.Create();
            }

            foreach (FileInfo fileEntry in sourceDirectoryInfo.GetFiles())
            {
                fileEntry.CopyTo(Path.Combine(destinationDirectoryInfo.FullName, fileEntry.Name));
            }

            foreach (DirectoryInfo directoryEntry in sourceDirectoryInfo.GetDirectories())
            {
                if (!CopyDirectory(directoryEntry.FullName, Path.Combine(destinationDirectoryInfo.FullName, directoryEntry.Name)))
                {
                    copyComplete = false;
                }
            }
        }

        copyComplete = true;
        return copyComplete;
    }

    /// <summary>
    /// Recursively deletes a directory, retrying on error until a timeout.
    /// </summary>
    /// <param name="directoryToDelete">The directory to delete.</param>
    /// <remarks>This method does not throw an exception if the delete fails.</remarks>
    public static void DeleteDirectory(string? directoryToDelete)
    {
        int numberOfRetries = 0;
        while (Directory.Exists(directoryToDelete) && numberOfRetries < 10)
        {
            try
            {
                Directory.Delete(directoryToDelete, true);
            }
            catch (IOException)
            {
                // If we hit an exception (like file still in use), wait a half second
                // and try again. If we still hit an exception, go ahead and let it through.
                System.Threading.Thread.Sleep(500);
            }
            catch (UnauthorizedAccessException)
            {
                // If we hit an exception (like file still in use), wait a half second
                // and try again. If we still hit an exception, go ahead and let it through.
                System.Threading.Thread.Sleep(500);
            }
            finally
            {
                numberOfRetries++;
            }
        }

        if (Directory.Exists(directoryToDelete))
        {
            if (logger.IsEnabled(LogEventLevel.Trace))
            {
                logger.Trace($"Unable to delete directory '{directoryToDelete}'");
            }
        }
    }

    /// <summary>
    /// Searches for a file with the specified name.
    /// </summary>
    /// <param name="fileName">The name of the file to find.</param>
    /// <returns>The full path to the directory where the file can be found,
    /// or an empty string if the file does not exist in the locations searched.</returns>
    /// <remarks>
    /// This method looks first in the directory of the currently executing
    /// assembly. If the specified file is not there, the method then looks in
    /// each directory on the PATH environment variable, in order.
    /// </remarks>
    public static string FindFile(string fileName)
    {
        // Look first in the same directory as the executing assembly
        string currentDirectory = GetCurrentDirectory();
        if (File.Exists(Path.Combine(currentDirectory, fileName)))
        {
            return currentDirectory;
        }

        // If it's not in the same directory as the executing assembly,
        // try looking in the system path.
        string? systemPath = Environment.GetEnvironmentVariable("PATH");
        if (!string.IsNullOrEmpty(systemPath))
        {
            string expandedPath = Environment.ExpandEnvironmentVariables(systemPath);
            string[] directories = expandedPath.Split(Path.PathSeparator);
            foreach (string directory in directories)
            {
                // N.B., if the directory in the path contains an invalid character,
                // we will skip that directory, meaning no error will be thrown. This
                // may be confusing to the user, so we might want to revisit this.
                if (directory.IndexOfAny(Path.GetInvalidPathChars()) < 0)
                {
                    if (File.Exists(Path.Combine(directory, fileName)))
                    {
                        currentDirectory = directory;
                        return currentDirectory;
                    }
                }
            }
        }

        // Note that if it wasn't found on the system path, currentDirectory is still
        // set to the same directory as the executing assembly.
        return string.Empty;
    }

    /// <summary>
    /// Gets the directory of the currently executing assembly.
    /// </summary>
    /// <returns>The directory of the currently executing assembly.</returns>
    public static string GetCurrentDirectory()
    {
        Assembly executingAssembly = typeof(FileUtilities).Assembly;
        string? location = null;

        // Make sure not to call Path.GetDirectoryName if assembly location is null or empty
        string assemblyLocation = executingAssembly.Location;
        if (!string.IsNullOrEmpty(assemblyLocation))
        {
            location = Path.GetDirectoryName(assemblyLocation);
        }

        if (string.IsNullOrEmpty(location))
        {
            // If there is no location information from the executing
            // assembly, we will bail by using the current directory.
            // Note this is inaccurate, because the working directory
            // may not actually be the directory of the current assembly,
            // especially if the WebDriver assembly was embedded as a
            // resource.
            location = Directory.GetCurrentDirectory();
        }

        string currentDirectory = location!;

#if !NET8_0_OR_GREATER
        // If we're shadow copying, get the directory from the codebase instead
        if (AppDomain.CurrentDomain.ShadowCopyFiles)
        {
            var codeBase = executingAssembly.CodeBase;

            if (codeBase is not null)
            {
                currentDirectory = Path.GetDirectoryName(codeBase);
            }
        }
#endif

        return currentDirectory;
    }

    /// <summary>
    /// Generates the full path to a random directory name in the temporary directory, following a naming pattern..
    /// </summary>
    /// <param name="directoryPattern">The pattern to use in creating the directory name, following standard
    /// .NET string replacement tokens.</param>
    /// <returns>The full path to the random directory name in the temporary directory.</returns>
    public static string GenerateRandomTempDirectoryName(
#if NET8_0_OR_GREATER
        [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.CompositeFormat)]
#endif
    string directoryPattern)
    {
        string directoryName = string.Format(CultureInfo.InvariantCulture, directoryPattern, Guid.NewGuid().ToString("N"));
        return Path.Combine(Path.GetTempPath(), directoryName);
    }
}
