// This file contains portions of code released by Microsoft under the MIT license as part
// of an open-sourcing initiative in 2014 of the C# core libraries.
// The original source was submitted to https://github.com/Microsoft/referencesource

using System.Text;
using System.Diagnostics;

namespace System.IO
{
	public static class Path
	{
#if BF_PLATFORM_WINDOWS
		public const char8 DirectorySeparatorChar = '\\';
#else
		public const char8 DirectorySeparatorChar = '/';
#endif //BF_PLATFORM_WINDOWS
		
		// Platform specific alternate directory separator char8acter.  
		// This is backslash ('\') on Unix, and slash ('/') on Windows 
		// and MacOS.
		// 
#if BF_PLATFORM_WINDOWS
		public const char8 AltDirectorySeparatorChar = '/';
#else
		public const char8 AltDirectorySeparatorChar = '\\';
#endif //BF_PLATFORM_WINDOWS
	
		// Platform specific volume separator char8acter.  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".  
		// 
#if BF_PLATFORM_WINDOWS
		public const char8 VolumeSeparatorChar = ':';
#else
		public const char8 VolumeSeparatorChar = '/';
#endif //BF_PLATFORM_WINDOWS

		// Make this public sometime.
		// The max total path is 260, and the max individual component length is 255. 
		// For example, D:\<256 char8 file name> isn't legal, even though it's under 260 char8s.
		protected const int32 MaxPath = 260;
		private const int32 MaxDirectoryLength = 255;

		public static void GetFullPath(String inPartialPath, String outFullPath)
		{
			Platform.GetStrHelper(outFullPath, scope (outPtr, outSize, outResult) =>
				{
					Platform.BfpFile_GetFullPath(inPartialPath, outPtr, outSize, (Platform.BfpFileResult*)outResult);
				});
		}

		protected static void CheckInvalidPathChars(StringView path, bool checkAdditional = false)
		{

		}

		public static void ChangeExtension(StringView path, StringView ext, String outPath)
		{
			if (path.IsNull)
				return;

			CheckInvalidPathChars(path);

			outPath.Set(path);
			for (int i = path.Length; --i >= 0;)
			{
				let ch = path[i];
				if (ch == '.')
				{
					outPath.Set(path.Substring(0, i));
					break;
				}
				if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar) break;
			}
			if (!ext.IsNull && path.Length != 0)
			{
				if (ext.IsEmpty || ext[0] != '.')
				{
					outPath.Append('.');
				}
				outPath.Append(ext);
			}
		}

		public static void GetFileName(StringView inPath, String outFileName)
		{
			if (inPath.IsEmpty)
				return;
			
			CheckInvalidPathChars(inPath);

			int length = inPath.Length;
			for (int i = length; --i >= 0; )
			{
				char8 ch = inPath[i];
				if (ch == DirectorySeparatorChar || ch == AltDirectorySeparatorChar || ch == VolumeSeparatorChar)
				{
					outFileName.Append(inPath, i + 1, length - i - 1);
					return;
				}
			}
			outFileName.Append(inPath);
		}

		static String NormalizePath(String path, bool fullCheck) 
        {
		    return NormalizePath(path, fullCheck, MaxPath);
		}

		
		static String NormalizePath(String path, bool fullCheck, bool expandShortPaths)
		{
		    return NormalizePath(path, fullCheck, MaxPath, expandShortPaths);
		}

		
		static String NormalizePath(String path, bool fullCheck, int32 maxPathLength) 
        {
		    return NormalizePath(path, fullCheck, maxPathLength, true);
		}

		static String NormalizePath(String path, bool fullCheck, int32 maxPathLength, bool expandShortPaths)
		{
			//TODO: Implement
			return path;
		}

		static String RemoveLongPathPrefix(String path)
		{
			//TODO: Implement
			return path;
		}

		public static Result<void> GetDirectoryPath(StringView path, String outDir)
		{
			String usePath = scope String(Math.Min(MaxPath, path.Length));
			usePath.Append(path);

			//String origPath = path;
			//if (usePath != null)
			{
				CheckInvalidPathChars(usePath);
				String normalizedPath = NormalizePath(usePath, false);

				// If there are no permissions for PathDiscovery to this path, we should NOT expand the short paths
				// as this would leak information about paths to which the user would not have access to.
				if (usePath.Length > 0)
				{
					// If we were passed in a path with \\?\ we need to remove it as FileIOPermission does not like it.
					String tempPath = Path.RemoveLongPathPrefix(usePath);

					// FileIOPermission cannot handle paths that contain ? or *
					// So we only pass to FileIOPermission the text up to them.
					int32 pos = 0;
					while (pos < tempPath.Length && (tempPath[pos] != '?' && tempPath[pos] != '*'))
						pos++;
					// GetFullPath will Demand that we have the PathDiscovery FileIOPermission and thus throw 
					// SecurityException if we don't. 
					// While we don't use the result of this call we are using it as a consistent way of 
					// doing the security checks.
					if (pos > 0)
					{
						//Path.GetFullPath(tempPath.Substring(0, pos));
						String newPath = scope String(usePath.Length - pos);
						newPath.Append(usePath, pos);
						usePath = newPath;
					}
					/*}
					catch (SecurityException) {
						// If the user did not have permissions to the path, make sure that we don't leak expanded short paths
						// Only re-normalize if the original path had a ~ in it.
						if (path.IndexOf("~", StringComparison.Ordinal) != -1)
						{
							normalizedPath = NormalizePath(path, /*fullCheck*/ false, /*expandShortPaths*/ false);
						}
					}
					catch (PathTooLongException) { }
					catch (NotSupportedException) { }  // Security can throw this on "c:\foo:"
					catch (IOException) { }
					catch (ArgumentException) { } // The normalizePath with fullCheck will throw this for file: and http:*/
				}

				usePath = normalizedPath;

				int root = GetRootLength(usePath);
				int i = usePath.Length;
				if (i > root)
				{
					i = usePath.Length;
					if (i == root) return .Err;
					while (i > root && usePath[--i] != DirectorySeparatorChar && usePath[i] != AltDirectorySeparatorChar) {}
						outDir.Append(usePath, 0, i);
					return .Ok;
				}
			}
			return .Err;
		}

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

			int i = 0;
			int length = path.Length;

#if BF_PLATFORM_WINDOWS
			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;
					int32 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;
#else    
			if (length >= 1 && (IsDirectorySeparator(path[0]))) {
				i = 1;
			}
			return i;
#endif //BF_PLATFORM_WINDOWS
        }

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

		/*
		public static char8[] GetInvalidPathChars()
		{
			return RealInvalidPathChars;
		}

		public static char8[] GetInvalidFileNameChars()
		{
			return (char8[]) InvalidFileNameChars.Clone();
		} */

		public static void GetFileNameWithoutExtension(String inPath, String outFileName)
		{
			int lastSlash = Math.Max(inPath.LastIndexOf('\\'), inPath.LastIndexOf('/'));

			int i;
			if ((i = inPath.LastIndexOf('.')) != -1)
			{
				int len = i - lastSlash - 1;
				if (len > 0)
					outFileName.Append(inPath, lastSlash + 1, i - lastSlash - 1);
			}
			else
				outFileName.Append(inPath, lastSlash + 1);
		}

		public static Result<void> GetExtension(StringView inPath, String outExt)
		{
			int i;
			if ((i = inPath.LastIndexOf('.')) != -1)
				outExt.Append(inPath, i);
			return .Ok;
		}

		public static Result<void> GetTempPath(String outPath)
		{
			Platform.GetStrHelper(outPath, scope (outPtr, outSize, outResult) =>
				{
					Platform.BfpFile_GetTempPath(outPtr, outSize, (Platform.BfpFileResult*)outResult);
				});

			return .Ok;
		}

		public static Result<void> GetTempFileName(String outPath)
		{
			Platform.GetStrHelper(outPath, scope (outPtr, outSize, outResult) =>
				{
					Platform.BfpFile_GetTempFileName(outPtr, outSize, (Platform.BfpFileResult*)outResult);
				});

			return .Ok;
		}

		public static void InternalCombine(String target, params String[] components)
		{
			for (var component in components)
			{
				if ((target.Length > 0) && (!target.EndsWith("\\")) && (!target.EndsWith("/")))
					target.Append(Path.DirectorySeparatorChar);
				target.Append(component);
			}
		}

		public static void GetActualPathName(StringView inPath, String outPath)
		{
			Platform.GetStrHelper(outPath, scope (outPtr, outSize, outResult) =>
				{
					Platform.BfpFile_GetActualPath(scope String()..Append(inPath), outPtr, outSize, (Platform.BfpFileResult*)outResult);
				});
		}

		public static bool Equals(StringView filePathA, StringView filePathB)
		{
		    Debug.Assert(!filePathA.Contains(Path.AltDirectorySeparatorChar));
		    Debug.Assert(!filePathB.Contains(Path.AltDirectorySeparatorChar));
		    return filePathA.Equals(filePathB, !Environment.IsFileSystemCaseSensitive);
		}

		static void GetDriveStringTo(String outDrive, String path)
		{
		    if ((path.Length >= 2) && (path[1] == ':'))
		        outDrive.Append(path, 0, 2);
		}

		/// 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 (":").
		public static bool IsPathRooted(StringView path)
		{
	        CheckInvalidPathChars(path);
	        int length = path.Length;
	        if ((length >= 1 && (path[0] == DirectorySeparatorChar || path[0] == AltDirectorySeparatorChar)) || (length >= 2 && path[1] == VolumeSeparatorChar))
	            return true;
		    return false;
		}

		public static void GetRelativePath(StringView fullPath, StringView curDir, String outRelPath)
		{
		    String curPath1 = scope String(curDir);
		    String curPath2 = scope String(fullPath);

		    if (curPath1.Contains(Path.AltDirectorySeparatorChar))
		        curPath1.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
		    if (curPath2.Contains(Path.AltDirectorySeparatorChar))
		        curPath1.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);

		    String driveString1 = scope String();
		    GetDriveStringTo(driveString1, curPath1);
		    String driveString2 = scope String();
		    GetDriveStringTo(driveString2, curPath2);

		    StringComparison compareType = Environment.IsFileSystemCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;

			// On seperate drives?
		    if (!String.Equals(driveString1, driveString2, compareType))
			{
				outRelPath.Set(fullPath);
		        return;
			}

		    if (driveString1.Length > 0)
		        curPath1.Remove(0, Math.Min(driveString1.Length + 1, curPath1.Length));
		    if (driveString2.Length > 0)
		        curPath2.Remove(0, Math.Min(driveString2.Length + 1, curPath2.Length));

		    while ((curPath1.Length > 0) && (curPath2.Length > 0))
		    {
		        int slashPos1 = curPath1.IndexOf(Path.DirectorySeparatorChar);
		        if (slashPos1 == -1)
		            slashPos1 = curPath1.Length;
		        int slashPos2 = curPath2.IndexOf(Path.DirectorySeparatorChar);
		        if (slashPos2 == -1)
		            slashPos2 = curPath2.Length;

		        String section1 = scope String();
		        section1.Append(curPath1, 0, slashPos1);
		        String section2 = scope String();
		        section2.Append(curPath2, 0, slashPos2);

		        if (!String.Equals(section1, section2, compareType))
		        {
					// a/b/c
					// d/e/f

		            while (curPath1.Length > 0)
		            {
		                slashPos1 = curPath1.IndexOf(Path.DirectorySeparatorChar);
		                if (slashPos1 == -1)
		                    slashPos1 = curPath1.Length;

		                if (slashPos1 + 1 >= curPath1.Length)
		                    curPath1.Clear();
		                else
		                    curPath1.Remove(0, slashPos1 + 1);
						if (Path.DirectorySeparatorChar == '\\')
		                	curPath2.Insert(0, "..\\");
						else
							curPath2.Insert(0, "../");
		            }
		        }
		        else
		        {
		            if (slashPos1 + 1 >= curPath1.Length)
		                curPath1.Clear();
		            else
		                curPath1.Remove(0, slashPos1 + 1);

		            if (slashPos2 + 2 >= curPath2.Length)
		                curPath1 = "";
		            else
		                curPath2.Remove(0, slashPos2 + 1);
		        }
		    }

		    outRelPath.Set(curPath2);
		}

		public static void GetAbsolutePath(StringView relPath, StringView relToAbsPath, String outAbsPath)
		{
		    String driveString = null;

			var relPath;
			if (relPath == outAbsPath)
				relPath = scope:: String(relPath);

			if ((relPath.Length >= 2) && (relPath[1] == ':'))
			{
				outAbsPath.Append(relPath);
			    return;
			}

			if ((relPath.Length > 1) &&
				(relPath[0] == '/') || (relPath[0] == '\\'))
			{
				outAbsPath.Append(relPath);
				return;
			}

			int startLen = outAbsPath.Length;
			outAbsPath.Append(relToAbsPath);
			//outAbsPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);

		    char8 slashChar = Path.DirectorySeparatorChar;

		    if ((outAbsPath.Length >= 2) && (outAbsPath[1] == ':'))
		    {
				driveString = scope:: String();
		        driveString.Append(outAbsPath, 0, 2);
		        outAbsPath.Remove(0, 2);
		    }

			// Append a trailing slash if necessary
		    if ((outAbsPath.Length > 0) && (outAbsPath[outAbsPath.Length - 1] != '\\') && (outAbsPath[outAbsPath.Length - 1] != '/'))
		        outAbsPath.Append(slashChar);

		    int32 relIdx = 0;
		    for (; ;)
		    {
		        if (outAbsPath.Length == 0)
		            break;

		        int32 firstSlash = -1;
				for (int32 i = relIdx; i < relPath.Length; i++)
					if ((relPath[i] == '\\') || (relPath[i] == '/'))
					{
						firstSlash = i;
						break;
					}
		        if (firstSlash == -1)
		            break;

		        String chDir = scope String();
				chDir.Append(relPath, relIdx, firstSlash - relIdx);

				//relPath.Substring(relIdx, firstSlash - relIdx, chDir);
				//chDir.Append(relPath.Ptr + relIdx, firstSlash - relIdx);

		        relIdx = firstSlash + 1;

		        if (chDir == "..")
		        {
		            int32 lastDirStart = (int32)outAbsPath.Length - 1;
		            while ((lastDirStart > 0) && (outAbsPath[lastDirStart - 1] != '\\') && (outAbsPath[lastDirStart - 1] != '/'))
		                lastDirStart--;

		            String lastDir = scope String();
		            lastDir.Append(outAbsPath, lastDirStart, outAbsPath.Length - lastDirStart - 1);
		            if (lastDir == "..")
		            {
		                outAbsPath.Append("..");
		                outAbsPath.Append(Path.DirectorySeparatorChar);
		            }
		            else
		            {
						//newPath.erase(newPath.begin() + lastDirStart, newPath.end());
		                outAbsPath.Remove(lastDirStart, outAbsPath.Length - lastDirStart);
		            }
		        }
		        else if (chDir == "")
		        {
		            outAbsPath.Append(Path.DirectorySeparatorChar);
		            break;
		        }
		        else if (chDir != ".")
		        {
					//newPath += chDir + slashChar;
		            outAbsPath.Append(chDir);
		            outAbsPath.Append(Path.DirectorySeparatorChar);
		            break;
		        }
		    }

		    if (driveString != null)
		        outAbsPath.Insert(0, driveString);
			//relPath.Substring(relIdx, outAbsPath);
			outAbsPath.Append(relPath, relIdx);
			//outAbsPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
			for (int i = startLen; i < outAbsPath.Length; i++)
			{
				if (outAbsPath[i] == Path.AltDirectorySeparatorChar)
					outAbsPath[i] = Path.DirectorySeparatorChar;
			}
		}

		public static bool WildcareCompare(StringView path, StringView wildcard)
		{
			bool matches = true;
			char8* afterLastWild = null; // The location after the last '*', if we’ve encountered one
			char8* afterLastPath = null; // The location in the path string, from which we started after last wildcard
			char8 t, w;

			char8* pathPtr = path.Ptr;
			char8* pathEnd = path.EndPtr;
			char8* wildPtr = wildcard.Ptr;
			char8* wildEnd = wildcard.EndPtr;

			// Walk the text strings one character at a time.
			while (true)
			{
				// How do you match a unique text string?
				if (pathPtr == pathEnd)
				{
					// Easy: unique up on it!
					if (wildPtr == wildEnd)
					{
						break; // "x" matches "x"
					}
					w = *wildPtr;
					if (w == '*')
					{
						wildPtr++;
						continue;// "x*" matches "x" or "xy"
					}
					else if (afterLastPath != null)
					{
						if (afterLastPath == pathEnd)
						{
							matches = false;
							break;
						}
						pathPtr = afterLastPath++;
						wildPtr = afterLastWild;
						continue;
					}

					matches = false;
					break; // "x" doesn't match "xy"
				}
				else
				{
					t = *pathPtr;
					w = *wildPtr;

					if (!Environment.IsFileSystemCaseSensitive)
					{
						t = t.ToUpper;
						w = w.ToUpper;
					}
		 
					// How do you match a tame text string?
					if (t != w)
					{
						// The tame way: unique up on it!
						if (w == '*')
						{
							afterLastWild = ++wildPtr;
							afterLastPath = pathPtr;
							if (wildPtr == wildEnd)
							{
								break; // "*" matches "x"
							}
							w = *wildPtr;
							continue; // "*y" matches "xy"
						}
						else if (afterLastWild != null)
						{
							if (afterLastWild != wildPtr)
							{
								wildPtr = afterLastWild;
								w = *wildPtr;

								if (!Environment.IsFileSystemCaseSensitive)
									w = w.ToUpper;

								if (t == w)
								{
									wildPtr++;
								}
							}
							pathPtr++;
							continue; // "*sip*" matches "mississippi"
						}
						else
						{
							matches = false;
							break; // "x" doesn't match "y"
						}
					}
				}

				pathPtr++;
				wildPtr++;
			}

			return matches;
		}
	}
}
