// ==++==
//
//   Copyright (c) Microsoft Corporation.  All rights reserved.
//
// ==--==
//============================================================
//
// Class:  Path
//
// Purpose: A collection of path manipulation methods.
//
//===========================================================  

using System;
using System.Text;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;
using System.Globalization;

namespace System.IO
{
    // Provides methods for processing directory strings in an ideally
    // cross-platform manner.  Most of the methods don't do a complete
    // full parsing (such as examining a UNC hostname), but they will
    // handle most string operations.
    //
    // File names cannot contain backslash (\), slash (/), colon (:),
    // asterisk (*), question mark (?), quote ("), less than (<),
    // greater than (>), or pipe (|).  The first three are used as directory
    // separators on various platforms.  Asterisk and question mark are treated
    // as wild cards.  Less than, Greater than, and pipe all redirect input
    // or output from a program to a file or some combination thereof.  Quotes
    // are special.
    //
    // We are guaranteeing that Path.SeparatorChar is the correct
    // directory separator on all platforms, and we will support
    // Path.AltSeparatorChar as well.  To write cross platform
    // code with minimal pain, you can use slash (/) as a directory separator in
    // your strings.
    // Class contains only static data, no need to serialize
    //| <include file='doc\Path.uex' path='docs/doc[@for="Path"]/*' />
    public sealed class Path
    {

        private Path() {
        }

        // Platform specific directory separator character.  This is backslash
        // ('\') on Windows, slash ('/') on Unix, and colon (':') on Mac.
        //
        // @TODO porting: Make this platform specific when we port.
        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.DirectorySeparatorChar"]/*' />
        public static readonly char DirectorySeparatorChar = '/';

        // Platform specific alternate directory separator character.
        // This is backslash ('\') on Unix, and slash ('/') on Windows
        // and MacOS.
        //
        // @TODO porting: Make this platform specific when we port.
        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.AltDirectorySeparatorChar"]/*' />
        public static readonly char AltDirectorySeparatorChar = '\\';

        // Platform specific volume separator character.  This is colon (':')
        // on Windows and MacOS, and slash ('/') on Unix.  This is mostly
        // useful for parsing paths like "c:\windows" or "MacVolume:System Folder".
        //
        // @TODO porting: Make this platform specific when we port.
        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.VolumeSeparatorChar"]/*' />
        public static readonly char VolumeSeparatorChar = ':';

        // Platform specific invalid list of characters in a path.
        //
        // @TODO porting: Make this platform specific when we port.
        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.InvalidPathChars"]/*' />
        public static readonly char[] InvalidPathChars = { '\"', '<', '>', '|', '\0', '\b', (Char)16, (Char)17, (Char)18, (Char)20, (Char)21, (Char)22, (Char)23, (Char)24, (Char)25 };

        internal static readonly char[] InternalInvalidPathChars = { '\"', '<', '>', '|', '\0', '\b', (Char)16, (Char)17, (Char)18, (Char)20, (Char)21, (Char)22, (Char)23, (Char)24, (Char)25 };

        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.PathSeparator"]/*' />
        public static readonly char PathSeparator = ';';


        // Changes the extension of a file path. The path parameter
        // specifies a file path, and the extension parameter
        // specifies a file extension (with a leading period, such as
        // ".exe" or ".cool").
        //
        // The function returns a file path with the same root, directory, and base
        // name parts as path, but with the file extension changed to
        // the specified extension. If path is null, the function
        // returns null. If path does not contain a file extension,
        // the new file extension is appended to the path. If extension
        // is null, any existing extension is removed from path.
        //
        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.ChangeExtension"]/*' />
        public static String ChangeExtension(String path, String extension) {
            if (path != null) {
                CheckInvalidPathChars(path);

                String s = path;
                for (int i = path.Length; --i >= 0;) {
                    char ch = path[i];
                    if (ch == '.') {
                        s = path.Substring(0, i);
                        break;
                    }
                    if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar) break;
                }
                if (extension != null && path.Length != 0) {
                    if (extension.Length == 0 || extension[0] != '.') {
                        s = s + ".";
                    }
                    s = s + extension;
                }
                return s;
            }
            return null;
        }


        // Returns the directory path of a file path. This method effectively
        // removes the last element of the given file path, i.e. it returns a
        // string consisting of all characters up to but not including the last
        // backslash ("\") in the file path. The returned value is null if the file
        // path is null or if the file path denotes a root (such as "\", "C:", or
        // "\\server\share").
        //
        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.GetDirectoryName"]/*' />
        public static String GetDirectoryName(String path) {
            if (path != null) {
                CheckInvalidPathChars(path);
                int root = GetRootLength(path);
                int i = path.Length;
                if (i > root) {
                    i = path.Length;
                    if (i == root) return null;
                    while (i > root && path[--i] != DirectorySeparatorChar && path[i] != AltDirectorySeparatorChar);
                    return path.Substring(0, i);
                }
            }
            return null;
        }

        // Gets the length of the root DirectoryInfo or whatever DirectoryInfo markers
        // are specified for the first part of the DirectoryInfo name.
        //
        internal static int GetRootLength(String! path) {
            CheckInvalidPathChars(path);

            int i = 0;
            int length = path.Length;
            if (length >= 1 && (IsDirectorySeparator(path[0]))) {
                // handles UNC names and directories off current drive's root.
                i = 1;
                if (length >= 2 && (IsDirectorySeparator(path[1]))) {
                    i = 2;
                    int n = 2;
                    while (i < length && ((path[i] != DirectorySeparatorChar && path[i] != AltDirectorySeparatorChar) || --n > 0)) i++;
                }
            }
            else if (length >= 2 && path[1] == VolumeSeparatorChar) {
                // handles A:\foo.
                i = 2;
                if (length >= 3 && (IsDirectorySeparator(path[2]))) i++;
            }
            return i;
        }

        internal static bool IsDirectorySeparator(char c) {
            return (c==DirectorySeparatorChar || c == AltDirectorySeparatorChar);
        }


        // Returns the extension of the given path. The returned value includes the
        // period (".") character of the extension except when you have a terminal period when you get String.Empty, such as ".exe" or
        // ".cpp". The returned value is null if the given path is
        // null or if the given path does not include an extension.
        //
        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.GetExtension"]/*' />
        public static String GetExtension(String path) {
            if (path == null)
                return null;

            CheckInvalidPathChars(path);
            int length = path.Length;
            for (int i = length; --i >= 0;) {
                char ch = path[i];
                if (ch == '.') {
                    if (i != length - 1)
                        return path.Substring(i, length - i);
                    else
                        return String.Empty;
                }
                if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar)
                    break;
            }
            return String.Empty;
        }

        // Expands the given path to a fully qualified path. The resulting string
        // consists of a drive letter, a colon, and a root relative path. This
        // function does not verify that the resulting path is valid or that it
        // refers to an existing file or DirectoryInfo on the associated volume.
        //
        // Your application must have unrestricted Environment permission.
        //

        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.GetFullPath"]/*' />
        public static String! GetFullPath(String! path) {
            String fullPath = GetFullPathInternal(path);
            return fullPath;
        }

        // This method is internal to let us quickly get a string name
        // while avoiding a security check.  This also serves a slightly
        // different purpose - when we open a file, we need to resolve the
        // path into a fully qualified, non-relative path name.  This
        // method does that, finding the current drive & directory.  But
        // as long as we don't return this info to the user, we're good.  However,
        // the public GetFullPath does need to do a security check.
        internal static String! GetFullPathInternal(String! path) {
            if (path == null) {
                throw new ArgumentNullException("path");
            }
            // BUGBUG: We removed a comparison for URI prefixes
            String newPath;
            int hresult = nGetFullPathHelper(path,
                                             InternalInvalidPathChars,
                                             new char[] {' '},
                                             DirectorySeparatorChar,
                                             AltDirectorySeparatorChar,
                                             true,
                                             out newPath);
            if (hresult != 0 || newPath == null) {
                __Error.WinIOError(hresult, path);
                assume false; // never get here
            }
            newPath = newPath.TrimStart();
            return newPath.ToLower();
        }

        internal static int nGetFullPathHelper(String! path,
                                               char[]! invalidPathChars,
                                               char[]! whitespaceChars,
                                               char directorySeparator,
                                               char altDirectorySeparator,
                                               char volumeSeparator,
                                               bool fullCheck,
                                               out String newPath) {
            return nGetFullPathHelper(path,invalidPathChars,whitespaceChars,directorySeparator,
                                      altDirectorySeparator,fullCheck,out newPath);
        }

        internal static int nGetFullPathHelper(String! path,
                                               char[]! invalidPathChars,
                                               char[]! whitespaceChars,
                                               char directorySeparator,
                                               char altDirectorySeparator,
                                               bool fullCheck,
                                               out String newPath) {
            int pathLength = path.Length;
            if (pathLength >= MAX_PATH) {
                throw new PathTooLongException("Max is "+MAX_PATH+" characters");
            }
            int whiteCount = whitespaceChars.Length;
            if (fullCheck) {
                while (pathLength > 0) {
                    bool foundMatch = false;
                    char lastChar = path[pathLength-1];
                    for (int whiteIndex = 0; whiteIndex < whiteCount; whiteIndex++) {
                        if (lastChar == whitespaceChars[whiteIndex]) {
                            foundMatch = true;
                            break;
                        }
                    }
                    if (!foundMatch) {
                        break;
                    }
                    pathLength--;
                }
            }
            if (pathLength == 0) {
                throw new ArgumentException("Empty path");
            }
            int invalidCount = invalidPathChars.Length;
            if (fullCheck) {
                for (int index = 0; index < pathLength; index++) {
                    char currentChar = path[index];
                    for (int invalidIndex = 0; invalidIndex < invalidCount; invalidIndex++) {
                        if (currentChar == invalidPathChars[invalidIndex]) {
                            throw new ArgumentException("Path has invalid character");
                        }
                    }
                }
            }
            int numDots = 0;
            int numSpaces = 0;
            bool fixupDirectorySeparator = false;
            bool fixupDotSeparator = true;
            int currentIndex = 0;
            char[] newBuffer = new char[pathLength+1];
            int newBufferIndex = 0;
            if (path[0] == '/' || path[0] == '\\') {
                newBuffer[newBufferIndex] = '/';
                newBufferIndex++;
                currentIndex++;
            }
            while (currentIndex < pathLength) {
                char currentChar = path[currentIndex];
                if (currentChar == directorySeparator ||
                    currentChar == altDirectorySeparator) {
                    if (fixupDotSeparator && numDots > 2) {
                        numDots = 2;
                    }
                    for (int count = 0; count < numDots; count++) {
                        newBuffer[newBufferIndex] = '.';
                        newBufferIndex++;
                    }
                    numSpaces = 0;
                    numDots = 0;
                    fixupDotSeparator = true;
                    if (!fixupDirectorySeparator) {
                        fixupDirectorySeparator = true;
                        newBuffer[newBufferIndex] = directorySeparator;
                        newBufferIndex++;
                    }
                }
                else if (currentChar == '.' && fixupDotSeparator) {
                    numDots++;
                    fixupDirectorySeparator = false;
                    numSpaces = 0;
                }
                else {
                    fixupDirectorySeparator = false;
                    if (currentChar == ' ') {
                        numSpaces++;
                    }
                    else {
                        for (int count = 0; count < numDots; count++) {
                            newBuffer[newBufferIndex] = '.';
                            newBufferIndex++;
                        }
                        numDots = 0;
                        fixupDotSeparator = false;
                        for (int count = 0; count < numSpaces; count++) {
                            newBuffer[newBufferIndex] = ' ';
                            newBufferIndex++;
                        }
                        numSpaces = 0;
                        newBuffer[newBufferIndex] = currentChar;
                        newBufferIndex++;
                    }
                }
                currentIndex++;
            }
            if (fixupDotSeparator && numDots > 2) {
                numDots = 2;
            }
            for (int count = 0; count < numDots; count++) {
                newBuffer[newBufferIndex] = '.';
                newBufferIndex++;
            }
            if (newBufferIndex == 0) {
                throw new ArgumentException("Empty path");
            }
            newBuffer[newBufferIndex] = '\0';
            if (newBuffer[0] == '\\' && newBuffer[1] == '\\') {
                int startIndex = 2;
                while (startIndex < newBufferIndex) {
                    if (newBuffer[startIndex] == '/') {
                        startIndex++;
                        break;
                    }
                    else {
                        startIndex++;
                    }
                }
                if (startIndex == newBufferIndex) {
                    throw new ArgumentException("UNC path without share name");
                }
            }
            if (fullCheck) {
                System.Text.StringBuilder resultBuffer =
                    new System.Text.StringBuilder(MAX_PATH+1);
                String initialPath =
                    String.StringCTOR(newBuffer, 0, newBufferIndex);
                int expandedLength = Native.GetFullPathName(initialPath,
                                                            MAX_PATH+1,
                                                            resultBuffer);
                if (expandedLength >= MAX_PATH) {
                    throw new PathTooLongException("FullPath is too long");
                }
                if (expandedLength == 0) {
                    newPath = null;
                    return -1;
                }
                newPath = resultBuffer.ToString();
            }
            else {
                newPath = String.StringCTOR(newBuffer, 0, newBufferIndex);
            }
            return 0;
        }

        // Returns the name and extension parts of the given path. The resulting
        // string contains the characters of path that follow the last
        // backslash ("\"), slash ("/"), or colon (":") character in
        // path. The resulting string is the entire path if path
        // contains no backslash after removing trailing slashes, slash, or colon characters. The resulting
        // string is null if path is null.
        //
        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.GetFileName"]/*' />
        public static String GetFileName(String path) {
            if (path != null) {
                CheckInvalidPathChars(path);

                int length = path.Length;
                for (int i = length; --i >= 0;) {
                    char ch = path[i];
                    if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar)
                        return path.Substring(i + 1, length - i - 1);

                }
            }
            return path;
        }

        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.GetFileNameWithoutExtension"]/*' />
        public static String GetFileNameWithoutExtension(String path) {
            path = GetFileName(path);
            if (path != null) {
                int i;
                if ((i = path.LastIndexOf('.')) == -1)
                    return path; // No path extension found
                else
                    return path.Substring(0,i);
            }
            return null;
        }



        // Returns the root portion of the given path. The resulting string
        // consists of those rightmost characters of the path that constitute the
        // root of the path. Possible patterns for the resulting string are: An
        // empty string (a relative path on the current drive), "\" (an absolute
        // path on the current drive), "X:" (a relative path on a given drive,
        // where X is the drive letter), "X:\" (an absolute path on a given drive),
        // and "\\server\share" (a UNC path for a given server and share name).
        // The resulting string is null if path is null.
        //
        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.GetPathRoot"]/*' />
        public static String GetPathRoot(String path) {
            if (path == null) return null;
            return path.Substring(0, GetRootLength(path));
        }

        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.GetTempPath"]/*' />
        public static String GetTempPath()
        {
            StringBuilder sb = new StringBuilder(MAX_PATH);
            uint r = Native.GetTempPath(MAX_PATH, sb);
            String path = sb.ToString();
            if (r == 0) __Error.WinIOError();
            return path;
        }

        // Returns a unique temporary file name, and creates a 0-byte file by that
        // name on disk.
        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.GetTempFileName"]/*' />
        public static String GetTempFileName()
        {
            String path = GetTempPath();
            StringBuilder sb = new StringBuilder(MAX_PATH);
            uint r = Native.GetTempFileName(path, "tmp", 0, sb);
            if (r == 0) __Error.WinIOError();
            return sb.ToString();
        }

        // Tests if a path includes a file extension. The result is
        // true if the characters that follow the last directory
        // separator ('\\' or '/') or volume separator (':') in the path include
        // a period (".") other than a terminal period. The result is false otherwise.
        //
        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.HasExtension"]/*' />
        public static bool HasExtension(String path) {
            if (path != null) {
                CheckInvalidPathChars(path);

                for (int i = path.Length; --i >= 0;) {
                    char ch = path[i];
                    if (ch == '.') {
                        if (i != path.Length - 1)
                            return true;
                        else
                            return false;
                    }
                    if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar) break;
                }
            }
            return false;
        }


        // Tests if the given path contains a root. A path is considered rooted
        // if it starts with a backslash ("\") or a drive letter and a colon (":").
        //
        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.IsPathRooted"]/*' />
        public static bool IsPathRooted(String path) {
            if (path != null) {
                CheckInvalidPathChars(path);

                int length = path.Length;
                if (length >= 1 && (path[0] == DirectorySeparatorChar || path[0] == AltDirectorySeparatorChar) ||
                    length >= 2 && path[1] == VolumeSeparatorChar) return true;
            }
            return false;
        }

        //| <include file='doc\Path.uex' path='docs/doc[@for="Path.Combine"]/*' />
        public static String Combine(String path1, String path2) {
            if (path1 == null || path2 == null)
                throw new ArgumentNullException((path1==null) ? "path1" : "path2");
            CheckInvalidPathChars(path1);
            CheckInvalidPathChars(path2);

            if (path2.Length == 0)
                return path1;

            if (path1.Length == 0)
                return path2;

            if (IsPathRooted(path2))
                return path2;

            char ch = path1[path1.Length - 1];
            if (ch != DirectorySeparatorChar && ch != AltDirectorySeparatorChar && ch != VolumeSeparatorChar)
                return path1 + DirectorySeparatorChar + path2;
            return path1 + path2;
        }


        internal static void CheckInvalidPathChars(String! path)
        {
            //if (path==null)
            //  return;
            if (-1 != path.IndexOfAny(InternalInvalidPathChars))
                throw new ArgumentException("Argument_InvalidPathChars");
        }


        internal static String! InternalCombine(String path1, String path2) {
            if (path1 == null || path2 == null)
                throw new ArgumentNullException((path1==null) ? "path1" : "path2");
            CheckInvalidPathChars(path1);
            CheckInvalidPathChars(path2);

            if (path2.Length == 0)
                throw new ArgumentException("Argument_PathEmpty", "path2");
            if (IsPathRooted(path2))
                throw new ArgumentException("Arg_Path2IsRooted", "path2");
            int i = path1.Length;
            if (i == 0) return path2;
            char ch = path1[i - 1];
            if (ch != DirectorySeparatorChar && ch != AltDirectorySeparatorChar && ch != VolumeSeparatorChar)
                return path1 + DirectorySeparatorChar + path2;
            return path1 + path2;
        }

        // Windows API definitions
        internal const int MAX_PATH = 260;  // From WinDef.h
        internal const int ERROR_SUCCESS = 0;  // From WinError.h
        internal const int MAX_DIRECTORY_PATH = 248;   // cannot create directories greater than 248 characters
    }
}
