/********************************************************************++
Copyright (c) Microsoft Corporation.  All rights reserved.
--********************************************************************/

using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;

using System.Management.Automation.Internal;
using Dbg = System.Management.Automation.Diagnostics;

namespace System.Management.Automation
{
    /// <summary>
    /// Defines generic utilities and helper methods for PowerShell
    /// </summary>
    internal static class PathUtils
    {
        /// <summary>
        /// THE method for opening a file for writing.
        /// Should be used by all cmdlets that write to a file.
        /// </summary>
        /// <param name="cmdlet">cmdlet that is opening the file (used mainly for error reporting)</param>
        /// <param name="filePath">path to the file (as specified on the command line - this method will resolve the path)</param>
        /// <param name="encoding">encoding (this method will convert the command line string to an Encoding instance)</param>
        /// <param name="defaultEncoding">if <c>true</c>, then we will use default .NET encoding instead of the encoding specified in <paramref name="encoding"/> parameter</param>
        /// <param name="Append"></param>
        /// <param name="Force"></param>
        /// <param name="NoClobber"></param>
        /// <param name="fileStream">Result1: <see cref="FileStream"/> opened for writing</param>
        /// <param name="streamWriter">Result2: <see cref="StreamWriter"/> (inherits from <see cref="TextWriter"/>) opened for writing</param>
        /// <param name="readOnlyFileInfo">Result3: file info that should be used to restore file attributes after done with the file (<c>null</c> is this is not needed)</param>
        /// <param name="isLiteralPath">True if wildcard expansion should be bypassed.</param>
        internal static void MasterStreamOpen(
            PSCmdlet cmdlet,
            string filePath,
            string encoding,
            bool defaultEncoding,
            bool Append,
            bool Force,
            bool NoClobber,
            out FileStream fileStream,
            out StreamWriter streamWriter,
            out FileInfo readOnlyFileInfo,
            bool isLiteralPath
            )
        {
            Encoding resolvedEncoding = EncodingConversion.Convert(cmdlet, encoding);

            MasterStreamOpen(cmdlet, filePath, resolvedEncoding, defaultEncoding, Append, Force, NoClobber, out fileStream, out streamWriter, out readOnlyFileInfo, isLiteralPath);
        }

        /// <summary>
        /// THE method for opening a file for writing.
        /// Should be used by all cmdlets that write to a file.
        /// </summary>
        /// <param name="cmdlet">cmdlet that is opening the file (used mainly for error reporting)</param>
        /// <param name="filePath">path to the file (as specified on the command line - this method will resolve the path)</param>
        /// <param name="resolvedEncoding">encoding (this method will convert the command line string to an Encoding instance)</param>
        /// <param name="defaultEncoding">if <c>true</c>, then we will use default .NET encoding instead of the encoding specified in <paramref name="encoding"/> parameter</param>
        /// <param name="Append"></param>
        /// <param name="Force"></param>
        /// <param name="NoClobber"></param>
        /// <param name="fileStream">Result1: <see cref="FileStream"/> opened for writing</param>
        /// <param name="streamWriter">Result2: <see cref="StreamWriter"/> (inherits from <see cref="TextWriter"/>) opened for writing</param>
        /// <param name="readOnlyFileInfo">Result3: file info that should be used to restore file attributes after done with the file (<c>null</c> is this is not needed)</param>
        /// <param name="isLiteralPath">True if wildcard expansion should be bypassed.</param>
        internal static void MasterStreamOpen(
            PSCmdlet cmdlet,
            string filePath,
            Encoding resolvedEncoding,
            bool defaultEncoding,
            bool Append,
            bool Force,
            bool NoClobber,
            out FileStream fileStream,
            out StreamWriter streamWriter,
            out FileInfo readOnlyFileInfo,
            bool isLiteralPath)
        {
            fileStream = null;
            streamWriter = null;
            readOnlyFileInfo = null;

            // resolve the path and the encoding
            string resolvedPath = ResolveFilePath(filePath, cmdlet, isLiteralPath);

            try
            {
                // variable to track file open mode
                // this is controlled by append/force parameters
                FileMode mode = FileMode.Create;
                if (Append)
                {
                    mode = FileMode.Append;
                }
                else if (NoClobber)
                {
                    // throw IOException if file exists
                    mode = FileMode.CreateNew;
                }

                if (Force && (Append || !NoClobber))
                {
                    if (File.Exists(resolvedPath))
                    {
                        FileInfo fInfo = new FileInfo(resolvedPath);
                        if ((fInfo.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
                        {
                            // remember to reset the read-only attribute later
                            readOnlyFileInfo = fInfo;
                            // Clear the read-only attribute
                            fInfo.Attributes &= ~(FileAttributes.ReadOnly);
                        }
                    }
                }

                // if the user knows what he/she is doing and uses "-Force" switch,
                // then we let more than 1 process write to the same file at the same time
                FileShare fileShare = Force ? FileShare.ReadWrite : FileShare.Read;

                // mode is controlled by force and ShouldContinue()
                fileStream = new FileStream(resolvedPath, mode, FileAccess.Write, fileShare);

                // create stream writer
                // NTRAID#Windows Out Of Band Releases-931008-2006/03/27
                // For some reason, calling this without specifying
                // the encoding is different from passing Encoding.Default.
                if (defaultEncoding)
                    streamWriter = new StreamWriter(fileStream);
                else
                    streamWriter = new StreamWriter(fileStream, resolvedEncoding);
            }
            // These are the known exceptions for File.Load and StreamWriter.ctor
            catch (ArgumentException e)
            {
                // NOTE: this call will throw
                ReportFileOpenFailure(cmdlet, resolvedPath, e);
            }
            catch (IOException e)
            {
                if (NoClobber && File.Exists(resolvedPath))
                {
                    // This probably happened because the file already exists
                    ErrorRecord errorRecord = new ErrorRecord(
                        e, "NoClobber", ErrorCategory.ResourceExists, resolvedPath);
                    errorRecord.ErrorDetails = new ErrorDetails(
                        cmdlet,
                        "PathUtilsStrings",
                        "UtilityFileExistsNoClobber",
                        filePath,
                        "NoClobber"); // prevents localization

                    // NOTE: this call will throw
                    cmdlet.ThrowTerminatingError(errorRecord);
                }
                // NOTE: this call will throw
                ReportFileOpenFailure(cmdlet, resolvedPath, e);
            }
            catch (UnauthorizedAccessException e)
            {
                // NOTE: this call will throw
                ReportFileOpenFailure(cmdlet, resolvedPath, e);
            }
            catch (NotSupportedException e)
            {
                // NOTE: this call will throw
                ReportFileOpenFailure(cmdlet, resolvedPath, e);
            }
            catch (System.Security.SecurityException e)
            {
                // NOTE: this call will throw
                ReportFileOpenFailure(cmdlet, resolvedPath, e);
            }
        } // void MasterStreamOpen



        internal static void ReportFileOpenFailure(Cmdlet cmdlet, string filePath, Exception e)
        {
            ErrorRecord errorRecord = new ErrorRecord(
                e,
                "FileOpenFailure",
                ErrorCategory.OpenError,
                null);

            cmdlet.ThrowTerminatingError(errorRecord);
        }

        internal static StreamReader OpenStreamReader(PSCmdlet command, string filePath, string encoding, bool isLiteralPath)
        {
            FileStream fileStream = OpenFileStream(filePath, command, isLiteralPath);
            if (encoding == null)
            {
                return new StreamReader(fileStream);
            }
            else
            {
                return new StreamReader(fileStream, EncodingConversion.Convert(command, encoding));
            }
        }

        internal static FileStream OpenFileStream(string filePath, PSCmdlet command, bool isLiteralPath)
        {
            string resolvedPath = PathUtils.ResolveFilePath(filePath, command, isLiteralPath);

            try
            {
                return new FileStream(resolvedPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
            }
            // These are the known exceptions for FileStream.ctor
            catch (ArgumentException e)
            {
                PathUtils.ReportFileOpenFailure(command, filePath, e);
                return null; // the line above will throw - silencing the compiler
            }
            catch (IOException e)
            {
                PathUtils.ReportFileOpenFailure(command, filePath, e);
                return null; // the line above will throw - silencing the compiler
            }
            catch (UnauthorizedAccessException e)
            {
                PathUtils.ReportFileOpenFailure(command, filePath, e);
                return null; // the line above will throw - silencing the compiler
            }
            catch (NotSupportedException e)
            {
                PathUtils.ReportFileOpenFailure(command, filePath, e);
                return null; // the line above will throw - silencing the compiler
            }
            catch (System.Management.Automation.DriveNotFoundException e)
            {
                PathUtils.ReportFileOpenFailure(command, filePath, e);
                return null; // the line above will throw - silencing the compiler
            }
        }

        /// <summary>
        /// resolve a user provided file name or path (including globbing characters)
        /// to a fully qualified file path, using the file system provider
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="command"></param>
        /// <returns></returns>
        internal static string ResolveFilePath(string filePath, PSCmdlet command)
        {
            return ResolveFilePath(filePath, command, false);
        }

        /// <summary>
        /// resolve a user provided file name or path (including globbing characters)
        /// to a fully qualified file path, using the file system provider
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="command"></param>
        /// <param name="isLiteralPath"></param>
        /// <returns></returns>
        internal static string ResolveFilePath(string filePath, PSCmdlet command, bool isLiteralPath)
        {
            string path = null;

            try
            {
                ProviderInfo provider = null;
                PSDriveInfo drive = null;
                List<string> filePaths = new List<string>();

                if (isLiteralPath)
                {
                    filePaths.Add(command.SessionState.Path.GetUnresolvedProviderPathFromPSPath(filePath, out provider, out drive));
                }
                else
                {
                    filePaths.AddRange(command.SessionState.Path.GetResolvedProviderPathFromPSPath(filePath, out provider));
                }

                if (!provider.NameEquals(command.Context.ProviderNames.FileSystem))
                {
                    ReportWrongProviderType(command, provider.FullName);
                }

                if (filePaths.Count > 1)
                {
                    ReportMultipleFilesNotSupported(command);
                }
                if (filePaths.Count == 0)
                {
                    ReportWildcardingFailure(command, filePath);
                }

                path = filePaths[0];
            }
            catch (ItemNotFoundException)
            {
                path = null;
            }
            if (string.IsNullOrEmpty(path))
            {
                CmdletProviderContext cmdletProviderContext = new CmdletProviderContext(command);
                ProviderInfo provider = null;
                PSDriveInfo drive = null;
                path =
                    command.SessionState.Path.GetUnresolvedProviderPathFromPSPath(
                        filePath, cmdletProviderContext, out provider, out drive);
                cmdletProviderContext.ThrowFirstErrorOrDoNothing();
                if (!provider.NameEquals(command.Context.ProviderNames.FileSystem))
                {
                    ReportWrongProviderType(command, provider.FullName);
                }
            }
            return path;
        }

        internal static void ReportWrongProviderType(Cmdlet cmdlet, string providerId)
        {
            string msg = StringUtil.Format(PathUtilsStrings.OutFile_ReadWriteFileNotFileSystemProvider, providerId);

            ErrorRecord errorRecord = new ErrorRecord(
                PSTraceSource.NewInvalidOperationException(),
                "ReadWriteFileNotFileSystemProvider",
                ErrorCategory.InvalidArgument,
                null);

            errorRecord.ErrorDetails = new ErrorDetails(msg);
            cmdlet.ThrowTerminatingError(errorRecord);
        }

        internal static void ReportMultipleFilesNotSupported(Cmdlet cmdlet)
        {
            string msg = StringUtil.Format(PathUtilsStrings.OutFile_MultipleFilesNotSupported);

            ErrorRecord errorRecord = new ErrorRecord(
                PSTraceSource.NewInvalidOperationException(),
                "ReadWriteMultipleFilesNotSupported",
                ErrorCategory.InvalidArgument,
                null);

            errorRecord.ErrorDetails = new ErrorDetails(msg);
            cmdlet.ThrowTerminatingError(errorRecord);
        }

        internal static void ReportWildcardingFailure(Cmdlet cmdlet, string filePath)
        {
            string msg = StringUtil.Format(PathUtilsStrings.OutFile_DidNotResolveFile, filePath);

            ErrorRecord errorRecord = new ErrorRecord(
                new FileNotFoundException(),
                "FileOpenFailure",
                ErrorCategory.OpenError,
                filePath);

            errorRecord.ErrorDetails = new ErrorDetails(msg);
            cmdlet.ThrowTerminatingError(errorRecord);
        }

        internal static DirectoryInfo CreateModuleDirectory(PSCmdlet cmdlet, string moduleNameOrPath, bool force)
        {
            Dbg.Assert(cmdlet != null, "Caller should verify cmdlet != null");
            Dbg.Assert(!string.IsNullOrEmpty(moduleNameOrPath), "Caller should verify !string.IsNullOrEmpty(moduleNameOrPath)");

            DirectoryInfo directoryInfo = null;
            try
            {
                string rootedPath = Microsoft.PowerShell.Commands.ModuleCmdletBase.ResolveRootedFilePath(moduleNameOrPath, cmdlet.Context);
                if (string.IsNullOrEmpty(rootedPath) && moduleNameOrPath.StartsWith(".", StringComparison.OrdinalIgnoreCase))
                {
                    PathInfo currentPath = cmdlet.CurrentProviderLocation(cmdlet.Context.ProviderNames.FileSystem);
                    rootedPath = Path.Combine(currentPath.ProviderPath, moduleNameOrPath);
                }
                if (string.IsNullOrEmpty(rootedPath))
                {
                    string personalModuleRoot = ModuleIntrinsics.GetPersonalModulePath();
                    rootedPath = Path.Combine(personalModuleRoot, moduleNameOrPath);
                }

                directoryInfo = new DirectoryInfo(rootedPath);
                if (directoryInfo.Exists)
                {
                    if (!force)
                    {
                        string errorMessage = string.Format(
                            CultureInfo.InvariantCulture, // directory name should be treated as culture-invariant
                            PathUtilsStrings.ExportPSSession_ErrorDirectoryExists,
                            directoryInfo.FullName);
                        ErrorDetails details = new ErrorDetails(errorMessage);
                        ErrorRecord errorRecord = new ErrorRecord(
                            new ArgumentException(details.Message),
                            "ExportProxyCommand_OutputDirectoryExists",
                            ErrorCategory.ResourceExists,
                            directoryInfo);
                        cmdlet.ThrowTerminatingError(errorRecord);
                    }
                }
                else
                {
                    directoryInfo.Create();
                }
            }
            catch (Exception e)
            {
                string errorMessage = string.Format(
                    CultureInfo.InvariantCulture, // directory name should be treated as culture-invariant
                    PathUtilsStrings.ExportPSSession_CannotCreateOutputDirectory,
                    moduleNameOrPath,
                    e.Message);
                ErrorDetails details = new ErrorDetails(errorMessage);
                ErrorRecord errorRecord = new ErrorRecord(
                    new ArgumentException(details.Message, e),
                    "ExportProxyCommand_CannotCreateOutputDirectory",
                    ErrorCategory.ResourceExists,
                    moduleNameOrPath);
                cmdlet.ThrowTerminatingError(errorRecord);
            }

            return directoryInfo;
        }

        internal static DirectoryInfo CreateTemporaryDirectory()
        {
            DirectoryInfo temporaryDirectory = new DirectoryInfo(Path.GetTempPath());
            DirectoryInfo moduleDirectory;
            do
            {
                moduleDirectory = new DirectoryInfo(
                    Path.Combine(
                        temporaryDirectory.FullName,
                        string.Format(
                            null,
                            "tmp_{0}",
                            Path.GetRandomFileName())));
            } while (moduleDirectory.Exists);

            Directory.CreateDirectory(moduleDirectory.FullName);
            return new DirectoryInfo(moduleDirectory.FullName);
        }
    }

    internal static class EncodingConversion
    {
        internal const string Unknown = "unknown";
        internal const string String = "string";
        internal const string Unicode = "unicode";
        internal const string BigEndianUnicode = "bigendianunicode";
        internal const string Ascii = "ascii";
        internal const string Utf8 = "utf8";
        internal const string Utf7 = "utf7";
        internal const string Utf32 = "utf32";
        internal const string Default = "default";
        internal const string OEM = "oem";

        /// <summary>
        /// retrieve the encoding parameter from the command line
        /// it throws if the encoding does not match the known ones
        /// </summary>
        /// <returns>a System.Text.Encoding object (null if no encoding specified)</returns>
        internal static Encoding Convert(Cmdlet cmdlet, string encoding)
        {
            if (string.IsNullOrEmpty(encoding))
            {
                // no parameter passed, default to Unicode (OS preferred)
                return System.Text.Encoding.Unicode;
            }

            // Default to unicode (this matches Get-Content)
            if (string.Equals(encoding, Unknown, StringComparison.OrdinalIgnoreCase))
                return System.Text.Encoding.Unicode;

            if (string.Equals(encoding, String, StringComparison.OrdinalIgnoreCase))
                return System.Text.Encoding.Unicode;

            // these are the encodings the CLR supports
            if (string.Equals(encoding, Unicode, StringComparison.OrdinalIgnoreCase))
                return System.Text.Encoding.Unicode;

            if (string.Equals(encoding, BigEndianUnicode, StringComparison.OrdinalIgnoreCase))
                return System.Text.Encoding.BigEndianUnicode;

            if (string.Equals(encoding, Utf8, StringComparison.OrdinalIgnoreCase))
                return System.Text.Encoding.UTF8;

            if (string.Equals(encoding, Ascii, StringComparison.OrdinalIgnoreCase))
                return System.Text.Encoding.ASCII;

            if (string.Equals(encoding, Utf7, StringComparison.OrdinalIgnoreCase))
                return System.Text.Encoding.UTF7;

            if (string.Equals(encoding, Utf32, StringComparison.OrdinalIgnoreCase))
                return System.Text.Encoding.UTF32;

            if (string.Equals(encoding, Default, StringComparison.OrdinalIgnoreCase))
                return ClrFacade.GetDefaultEncoding();

            if (string.Equals(encoding, OEM, StringComparison.OrdinalIgnoreCase))
            {
                return ClrFacade.GetOEMEncoding();
            }

            // error condition: unknown encoding value
            string validEncodingValues = string.Join(
                ", ",
                new string[] { Unknown, String, Unicode, BigEndianUnicode, Ascii, Utf8, Utf7, Utf32, Default, OEM });
            string msg = StringUtil.Format(PathUtilsStrings.OutFile_WriteToFileEncodingUnknown,
                encoding, validEncodingValues);

            ErrorRecord errorRecord = new ErrorRecord(
                PSTraceSource.NewArgumentException("Encoding"),
                "WriteToFileEncodingUnknown",
                ErrorCategory.InvalidArgument,
                null);

            errorRecord.ErrorDetails = new ErrorDetails(msg);
            cmdlet.ThrowTerminatingError(errorRecord);

            return null;
        }
    }
}
