using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; namespace Wabbajack.Paths; public enum PathFormat : byte { Windows = 0, Unix } public struct AbsolutePath : IPath, IComparable, IEquatable { public static readonly AbsolutePath Empty = "".ToAbsolutePath(); public PathFormat PathFormat { get; } private int _hashCode = 0; internal readonly string[] Parts; public string[] PathParts => Parts == default ? Array.Empty() : Parts; public Extension Extension => Extension.FromPath(Parts[^1]); public RelativePath FileName => new(Parts[^1..]); internal AbsolutePath(string[] parts, PathFormat format) { Parts = parts; PathFormat = format; } internal static readonly char[] StringSplits = {'/', '\\'}; private static AbsolutePath Parse(string path) { if (string.IsNullOrWhiteSpace(path)) return default; var parts = path.Split(StringSplits, StringSplitOptions.RemoveEmptyEntries); return new AbsolutePath(parts, DetectPathType(path)); } private static readonly HashSet DriveLetters = new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"); private static PathFormat DetectPathType(string path) { if (path.StartsWith("/")) return PathFormat.Unix; if (path.StartsWith(@"\\")) return PathFormat.Windows; if (path.Length >= 2 && DriveLetters.Contains(path[0]) && path[1] == ':') return PathFormat.Windows; throw new PathException($"Invalid Path format: {path}"); } public AbsolutePath Parent { get { { if (Parts.Length <= 1) throw new PathException($"Path {this} does not have a parent folder"); var newParts = new string[Parts.Length - 1]; Array.Copy(Parts, newParts, newParts.Length); return new AbsolutePath(newParts, PathFormat); } } } public int Depth => Parts?.Length ?? 0; public IEnumerable ThisAndAllParents() { var p = this; while (true) { yield return p; if (p.Depth == 1) yield break; p = p.Parent; } } /// /// Returns a new path that is this path with the extension changed. /// /// /// public readonly AbsolutePath ReplaceExtension(Extension newExtension) { var paths = new string[Parts.Length]; Array.Copy(Parts, paths, paths.Length); var oldName = paths[^1]; var newName = RelativePath.ReplaceExtension(oldName, newExtension); paths[^1] = newName; return new AbsolutePath(paths, PathFormat); } public static explicit operator AbsolutePath(string input) { return Parse(input); } public override string ToString() { if (Parts == default) return ""; if (PathFormat == PathFormat.Windows) return string.Join('\\', Parts); return '/' + string.Join('/', Parts); } public override int GetHashCode() { if (_hashCode != 0) return _hashCode; var result = 0; foreach (var part in Parts) result = result ^ part.GetHashCode(StringComparison.CurrentCultureIgnoreCase); _hashCode = result; return _hashCode; } public override bool Equals(object? obj) { return obj is AbsolutePath path && Equals(path); } public int CompareTo(AbsolutePath other) { return ArrayExtensions.CompareString(Parts, other.Parts); } public bool Equals(AbsolutePath other) { // ReSharper disable once ConditionIsAlwaysTrueOrFalse if (other.Parts == null) return other.Parts == Parts; if (Parts == null) return false; if (Parts.Length != other.Parts.Length) return false; for (var idx = 0; idx < Parts.Length; idx++) if (!Parts[idx].Equals(other.Parts[idx], StringComparison.InvariantCultureIgnoreCase)) return false; return true; } public RelativePath RelativeTo(AbsolutePath basePath) { if (!ArrayExtensions.AreEqualIgnoreCase(basePath.Parts, 0, Parts, 0, basePath.Parts.Length)) throw new PathException($"{basePath} is not a base path of {this}"); var newParts = new string[Parts.Length - basePath.Parts.Length]; Array.Copy(Parts, basePath.Parts.Length, newParts, 0, newParts.Length); return new RelativePath(newParts); } public bool InFolder(AbsolutePath parent) { return ArrayExtensions.AreEqualIgnoreCase(parent.Parts, 0, Parts, 0, parent.Parts.Length); } public AbsolutePath Combine(params object[] paths) { var converted = paths.Select(p => { return p switch { string s => (RelativePath) s, RelativePath path => path, _ => throw new PathException($"Cannot cast {p} of type {p.GetType()} to Path") }; }).ToArray(); return Combine(converted); } public AbsolutePath Combine(params RelativePath[] paths) { var newLen = Parts.Length + paths.Sum(p => p.Parts.Length); var newParts = new string[newLen]; Array.Copy(Parts, newParts, Parts.Length); var toIdx = Parts.Length; foreach (var p in paths) { Array.Copy(p.Parts, 0, newParts, toIdx, p.Parts.Length); toIdx += p.Parts.Length; } return new AbsolutePath(newParts, PathFormat); } public static bool operator ==(AbsolutePath a, AbsolutePath b) { return a.Equals(b); } public static bool operator !=(AbsolutePath a, AbsolutePath b) { return !a.Equals(b); } public AbsolutePath WithExtension(Extension? ext) { var parts = new string[Parts.Length]; Array.Copy(Parts, parts, Parts.Length); parts[^1] = parts[^1] + ext; return new AbsolutePath(parts, PathFormat); } public AbsolutePath AppendToName(string append) { return Parent.Combine((FileName.WithoutExtension() + append).ToRelativePath() .WithExtension(Extension)); } public static AbsolutePath ConvertNoFailure(string value) { try { return (AbsolutePath) value; } catch (Exception) { return default; } } }