using Lucene.Net.Support.Text;
using Lucene.Net.Util;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Lucene.Net.Support.IO
{
    /*
     * Licensed to the Apache Software Foundation (ASF) under one or more
     * contributor license agreements.  See the NOTICE file distributed with
     * this work for additional information regarding copyright ownership.
     * The ASF 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.
     */

    /// <summary>
    /// Represents the methods to support some operations over files.
    /// </summary>
    internal static class FileSupport
    {
        private static readonly char[] INVALID_FILENAME_CHARS = Path.GetInvalidFileNameChars();

        // LUCNENENET NOTE: Lookup the HResult value we are interested in for the current OS
        // by provoking the exception during initialization and caching its HResult value for later.
        // We optimize for Windows because those HResult values are known and documented, but for
        // other platforms, this is the only way we can reliably determine the HResult values
        // we are interested in.
        //
        // Reference: https://stackoverflow.com/q/46380483
        private const int WIN_HRESULT_FILE_ALREADY_EXISTS = unchecked((int)0x80070050);
        private static readonly int? HRESULT_FILE_ALREADY_EXISTS = LoadFileAlreadyExistsHResult();

        private static int? LoadFileAlreadyExistsHResult()
        {
            if (Constants.WINDOWS)
                return WIN_HRESULT_FILE_ALREADY_EXISTS;

            return GetFileIOExceptionHResult(provokeException: (fileName) =>
            {
                //Try to create the file again -this should throw an IOException with the correct HResult for the current platform
                using var stream = new FileStream(fileName, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
            });
        }

        internal static int? GetFileIOExceptionHResult(Action<string> provokeException)
        {
            string fileName;
            try
            {
                // This could throw, but we don't care about this HResult value.
                fileName = Path.GetTempFileName();
            }
            catch
            {
                return null; // We couldn't create a temp file
            }
            try
            {
                provokeException(fileName);
            }
            catch (IOException ex) when (ex.HResult != 0) // Assume 0 means the platform is not completely implemented, thus unknown
            {
                return ex.HResult;
            }
            catch
            {
                return null; // Unknown exception
            }
            finally
            {
                try
                {
                    File.Delete(fileName);
                }
                catch { }
            }
            return null; // Should never get here
        }

        /// <summary>
        /// Creates a new empty file in a random subdirectory of <see cref="Path.GetTempPath()"/>, using the given prefix and 
        /// suffix strings to generate its name.
        /// </summary>
        /// <remarks>
        /// If this method returns successfully then it is guaranteed that:
        /// <list type="number">
        /// <item><description>The file denoted by the returned abstract pathname did not exist before this method was invoked, and</description></item>
        /// <item><description>Neither this method nor any of its variants will return the same abstract pathname again in the current invocation of the virtual machine.</description></item>
        /// </list>
        /// This method provides only part of a temporary-file facility. However, the file will not be deleted automatically, 
        /// it must be deleted by the caller.
        /// <para/>
        /// The prefix argument must be at least three characters long. It is recommended that the prefix be a short, meaningful 
        /// string such as "hjb" or "mail". 
        /// <para/>
        /// The suffix argument may be null, in which case a random suffix will be used.
        /// <para/>
        /// Both prefix and suffix must be provided with valid characters for the underlying system, as specified by 
        /// <see cref="Path.GetInvalidFileNameChars()"/>.
        /// <para/>
        /// If the directory argument is null then the system-dependent default temporary-file directory will be used, 
        /// with a random subdirectory name. The default temporary-file directory is specified by the 
        /// <see cref="Path.GetTempPath()"/> method. On UNIX systems the default value of this property is typically 
        /// "/tmp" or "/var/tmp"; on Microsoft Windows systems it is typically "C:\\Users\\[UserName]\\AppData\Local\Temp".
        /// </remarks>
        /// <param name="prefix">The prefix string to be used in generating the file's name; must be at least three characters long</param>
        /// <param name="suffix">The suffix string to be used in generating the file's name; may be null, in which case a random suffix will be generated</param>
        /// <returns>A <see cref="FileInfo"/> instance representing the temp file that was created.</returns>
        public static FileInfo CreateTempFile(string prefix, string suffix)
        {
            return CreateTempFile(prefix, suffix, null);
        }

        /// <summary>
        /// Creates a new empty file in the specified directory, using the given prefix and suffix strings to generate its name.
        /// </summary>
        /// <remarks>
        /// If this method returns successfully then it is guaranteed that:
        /// <list type="number">
        /// <item><description>The file denoted by the returned abstract pathname did not exist before this method was invoked, and</description></item>
        /// <item><description>Neither this method nor any of its variants will return the same abstract pathname again in the current invocation of the virtual machine.</description></item>
        /// </list>
        /// This method provides only part of a temporary-file facility. However, the file will not be deleted automatically, 
        /// it must be deleted by the caller.
        /// <para/>
        /// The prefix argument must be at least three characters long. It is recommended that the prefix be a short, meaningful 
        /// string such as "hjb" or "mail". 
        /// <para/>
        /// The suffix argument may be null, in which case a random suffix will be used.
        /// <para/>
        /// Both prefix and suffix must be provided with valid characters for the underlying system, as specified by 
        /// <see cref="Path.GetInvalidFileNameChars()"/>.
        /// <para/>
        /// If the directory argument is null then the system-dependent default temporary-file directory will be used, 
        /// with a random subdirectory name. The default temporary-file directory is specified by the 
        /// <see cref="Path.GetTempPath()"/> method. On UNIX systems the default value of this property is typically 
        /// "/tmp" or "/var/tmp"; on Microsoft Windows systems it is typically "C:\\Users\\[UserName]\\AppData\Local\Temp".
        /// </remarks>
        /// <param name="prefix">The prefix string to be used in generating the file's name; must be at least three characters long</param>
        /// <param name="suffix">The suffix string to be used in generating the file's name; may be null, in which case a random suffix will be generated</param>
        /// <param name="directory">The directory in which the file is to be created, or null if the default temporary-file directory is to be used</param>
        /// <returns>A <see cref="FileInfo"/> instance representing the temp file that was created.</returns>
        public static FileInfo CreateTempFile(string prefix, string suffix, DirectoryInfo directory)
        {
            if (string.IsNullOrEmpty(prefix))
                throw new ArgumentNullException(nameof(prefix));
            if (prefix.Length < 3)
                throw new ArgumentException("Prefix string too short");

            // Ensure the strings passed don't contain invalid characters
            if (prefix.ContainsAny(INVALID_FILENAME_CHARS))
                throw new ArgumentException(string.Format("Prefix contains invalid characters. You may not use any of '{0}'", string.Join(", ", INVALID_FILENAME_CHARS)));
            if (suffix != null && suffix.ContainsAny(INVALID_FILENAME_CHARS))
                throw new ArgumentException(string.Format("Suffix contains invalid characters. You may not use any of '{0}'", string.Join(", ", INVALID_FILENAME_CHARS)));

            // If no directory supplied, create one.
            if (directory == null)
            {
                directory = new DirectoryInfo(Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(Path.GetRandomFileName())));
            }
            // Ensure the directory exists (this does nothing if it already exists, although may throw exceptions in cases where permissions are changed)
            directory.Create();
            string fileName;

            while (true)
            {
                fileName = NewTempFileName(prefix, suffix, directory);

                if (File.Exists(fileName))
                {
                    continue;
                }

                try
                {
                    // Create the file, and close it immediately
                    using var stream = new FileStream(fileName, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
                    break;
                }
                catch (IOException e) when (IsFileAlreadyExistsException(e, fileName))
                {
                    // If the error was because the file exists, try again.
                    continue;
                }
            }
            return new FileInfo(fileName);
        }

        /// <summary>
        /// Tests whether the passed in <see cref="Exception"/> is an <see cref="IOException"/>
        /// corresponding to the underlying operating system's "File Already Exists" violation.
        /// This works by forcing the exception to occur during initialization and caching the
        /// <see cref="Exception.HResult"/> value for the current OS.
        /// </summary>
        /// <param name="ex">An exception, for comparison.</param>
        /// <param name="filePath">The path of the file to check. This is used as a fallback in case the
        /// current OS doesn't have an HResult (an edge case).</param>
        /// <returns><c>true</c> if the exception passed is an <see cref="IOException"/> with an 
        /// <see cref="Exception.HResult"/> corresponding to the operating system's "File Already Exists" violation, which
        /// occurs when an attempt is made to create a file that already exists.</returns>
        public static bool IsFileAlreadyExistsException(Exception ex, string filePath)
        {
            if (!typeof(IOException).Equals(ex))
                return false;
            else if (HRESULT_FILE_ALREADY_EXISTS.HasValue)
                return ex.HResult == HRESULT_FILE_ALREADY_EXISTS;
            else
                return File.Exists(filePath);
        }

        /// <summary>
        /// Generates a new random file name with the provided <paramref name="directory"/>, 
        /// <paramref name="prefix"/> and optional <paramref name="suffix"/>.
        /// </summary>
        /// <param name="prefix">The prefix string to be used in generating the file's name</param>
        /// <param name="suffix">The suffix string to be used in generating the file's name; may be null, in which case a random suffix will be generated</param>
        /// <param name="directory">A <see cref="DirectoryInfo"/> object containing the temp directory path. Must not be null.</param>
        /// <returns>A random file name</returns>
        internal static string NewTempFileName(string prefix, string suffix, DirectoryInfo directory)
        {
            string randomFileName = Path.GetRandomFileName();

            if (suffix != null)
            {
                randomFileName = string.Concat(
                    Path.GetFileNameWithoutExtension(randomFileName),
                    suffix.StartsWith(".", StringComparison.Ordinal) ? suffix : '.' + suffix
                );
            }

            return Path.Combine(directory.FullName, string.Concat(prefix, randomFileName));
        }

        private static readonly ConcurrentDictionary<string, string> fileCanonPathCache = new ConcurrentDictionary<string, string>();

        /// <summary>
        /// Returns the absolute path of this <see cref="FileSystemInfo"/> with all references resolved and
        /// any drive letters normalized to upper case on Windows. An
        /// <em>absolute</em> path is one that begins at the root of the file
        /// system. The canonical path is one in which all references have been
        /// resolved. For the cases of '..' and '.', where the file system supports
        /// parent and working directory respectively, these are removed and replaced
        /// with a direct directory reference. 
        /// </summary>
        /// <param name="path">This <see cref="FileSystemInfo"/> instance.</param>
        /// <returns>The canonical path of this file.</returns>
        // LUCENENET NOTE: Implementation ported mostly from Apache Harmony
        public static string GetCanonicalPath(this FileSystemInfo path)
        {
            string absPath = path.FullName; // LUCENENET NOTE: This internally calls GetFullPath(), which resolves relative paths
            byte[] result = Encoding.UTF8.GetBytes(absPath);

            if (fileCanonPathCache.TryGetValue(absPath, out string canonPath) && canonPath != null)
            {
                return canonPath;
            }

            // LUCENENET TODO: On Unix, this resolves symbolic links. Not sure
            // if it is safe to assume Path.GetFullPath() does that for us.
            //if (Path.DirectorySeparatorChar == '/')
            //{
            //    //// resolve the full path first
            //    //result = resolveLink(result, result.Length, false);
            //    //// resolve the parent directories
            //    //result = resolve(result);
            //}
            int numSeparators = 1;
            for (int i = 0; i < result.Length; i++)
            {
                if (result[i] == Path.DirectorySeparatorChar)
                {
                    numSeparators++;
                }
            }
            int[] sepLocations = new int[numSeparators];
            int rootLoc = 0;
            if (Path.DirectorySeparatorChar == '\\')
            {
                if (result[0] == '\\')
                {
                    rootLoc = (result.Length > 1 && result[1] == '\\') ? 1 : 0;
                }
                else
                {
                    rootLoc = 2; // skip drive i.e. c:
                }
            }
            byte[] newResult = new byte[result.Length + 1];
            int newLength = 0, lastSlash = 0, foundDots = 0;
            sepLocations[lastSlash] = rootLoc;
            for (int i = 0; i <= result.Length; i++)
            {
                if (i < rootLoc)
                {
                    // Normalize case of Windows drive letter to upper
                    newResult[newLength++] = (byte)char.ToUpperInvariant((char)result[i]);
                }
                else
                {
                    if (i == result.Length || result[i] == Path.DirectorySeparatorChar)
                    {
                        if (i == result.Length && foundDots == 0)
                        {
                            break;
                        }
                        if (foundDots == 1)
                        {
                            /* Don't write anything, just reset and continue */
                            foundDots = 0;
                            continue;
                        }
                        if (foundDots > 1)
                        {
                            /* Go back N levels */
                            lastSlash = lastSlash > (foundDots - 1) ? lastSlash
                                    - (foundDots - 1) : 0;
                            newLength = sepLocations[lastSlash] + 1;
                            foundDots = 0;
                            continue;
                        }
                        sepLocations[++lastSlash] = newLength;
                        newResult[newLength++] = (byte)Path.DirectorySeparatorChar;
                        continue;
                    }
                    if (result[i] == '.')
                    {
                        foundDots++;
                        continue;
                    }
                    /* Found some dots within text, write them out */
                    if (foundDots > 0)
                    {
                        for (int j = 0; j < foundDots; j++)
                        {
                            newResult[newLength++] = (byte)'.';
                        }
                    }
                    newResult[newLength++] = result[i];
                    foundDots = 0;
                }
            }
            // remove trailing slash
            if (newLength > (rootLoc + 1)
                    && newResult[newLength - 1] == Path.DirectorySeparatorChar)
            {
                newLength--;
            }
            newResult[newLength] = 0;
            //newResult = getCanonImpl(newResult);
            newLength = newResult.Length;
            canonPath = fileCanonPathCache.GetOrAdd(
                absPath,
                k => Encoding.UTF8.GetString(newResult, 0, newLength).TrimEnd('\0')); // LUCENENET: Eliminate null terminator char
            return canonPath;
        }
    }
}