using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using JetBrains.Annotations;
using UnityEngine;
namespace NugetForUnity.Helper
{
///
/// Helper class to handle path operations.
///
internal static class PathHelper
{
///
/// The path comparison type to use for the current platform the editor is running on.
///
internal static readonly StringComparison PathComparisonType =
Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.OSXEditor ?
StringComparison.OrdinalIgnoreCase :
StringComparison.Ordinal;
///
/// Create a relative path from one path to another. Paths will be resolved before calculating the difference.
/// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix).
///
///
/// Should the project upgrade to Unity version that has .NET Standard 2.1, this can be removed and instead we will use:
/// Path.GetRelativePath(relativeTo, path).
///
/// .
///
/// The source path the output should be relative to. This path is always considered to be a directory.
/// The destination path.
/// The relative path or if the paths don't share the same root.
[NotNull]
internal static string GetRelativePath([NotNull] string relativeTo, [NotNull] string path)
{
if (string.IsNullOrEmpty(relativeTo))
{
throw new ArgumentNullException(nameof(relativeTo));
}
if (string.IsNullOrEmpty(path))
{
return path;
}
relativeTo = Path.GetFullPath(relativeTo);
path = Path.GetFullPath(path);
// Need to check if the roots are different - if they are we need to return the "to" path.
if (!AreRootsEqual(relativeTo, path))
{
return path;
}
var commonLength = GetCommonPathLength(relativeTo, path, PathComparisonType == StringComparison.OrdinalIgnoreCase);
// If there is nothing in common they can't share the same root, return the "to" path as is.
if (commonLength == 0)
{
return path;
}
// Trailing separators aren't significant for comparison
var relativeToLength = relativeTo.Length;
if (EndsInDirectorySeparator(relativeTo))
{
relativeToLength--;
}
var pathEndsInSeparator = EndsInDirectorySeparator(path);
var pathLength = path.Length;
if (pathEndsInSeparator)
{
pathLength--;
}
// If we have effectively the same path, return "."
if (relativeToLength == pathLength && commonLength >= relativeToLength)
{
return ".";
}
// We have the same root, we need to calculate the difference now using the
// common Length and Segment count past the length.
//
// Some examples:
//
// C:\Foo C:\Bar L3, S1 -> ..\Bar
// C:\Foo C:\Foo\Bar L6, S0 -> Bar
// C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar
// C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar
var sb = new StringBuilder();
sb.EnsureCapacity(Math.Max(relativeTo.Length, path.Length));
// Add parent segments for segments past the common on the "from" path
if (commonLength < relativeToLength)
{
sb.Append("..");
for (var i = commonLength + 1; i < relativeToLength; i++)
{
if (IsDirectorySeparator(relativeTo[i]))
{
sb.Append(Path.DirectorySeparatorChar);
sb.Append("..");
}
}
}
else if (IsDirectorySeparator(path[commonLength]))
{
// No parent segments and we need to eat the initial separator
// (C:\Foo C:\Foo\Bar case)
commonLength++;
}
// Now add the rest of the "to" path, adding back the trailing separator
var differenceLength = pathLength - commonLength;
if (pathEndsInSeparator)
{
differenceLength++;
}
if (differenceLength > 0)
{
if (sb.Length > 0)
{
sb.Append(Path.DirectorySeparatorChar);
}
sb.Append(path.Substring(commonLength, differenceLength));
}
return sb.ToString();
}
///
/// Get the common path length from the start of the string.
///
private static int GetCommonPathLength([NotNull] string first, [NotNull] string second, bool ignoreCase)
{
var commonChars = EqualStartingCharacterCount(first, second, ignoreCase);
// If nothing matches
if (commonChars == 0)
{
return commonChars;
}
// Or we're a full string and equal length or match to a separator
if (commonChars == first.Length && (commonChars == second.Length || IsDirectorySeparator(second[commonChars])))
{
return commonChars;
}
if (commonChars == second.Length && IsDirectorySeparator(first[commonChars]))
{
return commonChars;
}
// It's possible we matched somewhere in the middle of a segment e.g. C:\Foodie and C:\Foobar.
while (commonChars > 0 && !IsDirectorySeparator(first[commonChars - 1]))
{
commonChars--;
}
return commonChars;
}
///
/// Gets the count of common characters from the left optionally ignoring case.
///
private static int EqualStartingCharacterCount([CanBeNull] string first, [CanBeNull] string second, bool ignoreCase)
{
if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second))
{
return 0;
}
var commonChars = 0;
for (; commonChars < first.Length && commonChars < second.Length; ++commonChars)
{
if (first[commonChars] == second[commonChars] ||
(ignoreCase && char.ToUpperInvariant(first[commonChars]) == char.ToUpperInvariant(second[commonChars])))
{
continue;
}
break;
}
return commonChars;
}
///
/// Returns true if the two paths have the same root.
///
private static bool AreRootsEqual([NotNull] string first, [NotNull] string second)
{
var firstRoot = Path.GetPathRoot(first);
var secondRoot = Path.GetPathRoot(second);
return string.Equals(firstRoot, secondRoot, PathComparisonType);
}
///
/// True if the given character is a directory separator.
///
/// The char to compare.
/// True if is a separator.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsDirectorySeparator(char c)
{
return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar;
}
///
/// Returns true if the path ends in a directory separator.
///
private static bool EndsInDirectorySeparator([CanBeNull] string path)
{
return !string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]);
}
}
}