using System.Text;
using InnovEnergy.Lib.Utils;
using static System.IO.Path;

namespace InnovEnergy.Lib.SysTools;


public readonly struct SysPath
{
    private readonly String _Path;

    public static SysPath Root { get; } = "/";

    public Boolean IsRelative => _Path.ElementAtOrDefault(0) == '.';
    public Boolean IsAbsolute => _Path.ElementAtOrDefault(0) == '/';
    public Boolean IsEnvRef   => !_Path.Contains('/');
    public Boolean IsRoot     => _Path == "/";

    public SysPath Parent     => this / "..";
    public String  Head       => _Path.AfterLast(DirectorySeparatorChar);
    public String  Tail       => _Path.UntilLast(DirectorySeparatorChar);
    public String  Extension  => _Path.AfterLast('.');

    public SysPath(String path)
    {
        if (path.IsNullOrWhiteSpace())
            throw new ArgumentException(nameof(path));

        path = path
              .Replace(@"\", "/")
              .Trim()
              .TrimEnd('/');

        if      (path.Length == 0)    _Path = "/";   // root
        else if (!path.Contains('/')) _Path = path;  // env
        else
        {
            _Path = Normalize(path);
        }

    }

    public static SysPath FromString(String strPath) => new SysPath(strPath);

    public static SysPath FromUri(String uriPath)
    {
        var uri = new Uri(uriPath);
        return new SysPath(uri.AbsolutePath);
    }

    public SysPath ToRelative()
    {
        if (IsRelative)
            return this;

        if (IsAbsolute)
            return $".{this}";

        return $"./{this}";
    }

    public SysPath ToAbsolute()
    {
        if (IsAbsolute)
            return this;

        if (IsRelative)
            return $"/{_Path.AfterFirst('/')}";

        return $"/{this}";
    }

    public SysPath ToEnvRef()
    {
        if (IsEnvRef)
            return this;

        return $"/{_Path.AfterFirst('/')}";
    }


    private static String Normalize(String path)
    {
        if (path == "/")
            return path;

        var components = path.Split('/');

        if (components.Length == 1)
            return path; // envRef

        var stack = new Stack<String>();

        foreach (var comp in components)
        {
            if (comp == ".")
            {
                if (stack.Count == 0)
                    stack.Push(comp);

                // otherwise just ignore it
            }
            else if (comp == "..")
            {
                if (stack.Count == 0 || stack.Peek() == "..")
                    stack.Push(comp);
                else
                {
                    if (stack.Count == 1) // one parent
                    {
                        if (stack.Peek() == ".")
                        {
                            stack.Pop();
                            stack.Push("..");
                        }
                        else if (stack.Peek() == "")
                            throw new Exception("Malformed path. Cannot ascend above root.");
                        else
                            stack.Pop();
                    }
                    else
                    {
                        stack.Pop();
                    }
                }
            }
            else
            {
                stack.Push(comp);
            }
        }

        var sb = new StringBuilder();

        if (stack.Count == 0)
            return ".";

        foreach (var comp in stack.Reverse())
        {
            sb.Append(comp);
            sb.Append("/");
        }

        if (sb.Length > 1)
            sb.Remove(sb.Length - 1, 1);

        return sb.ToString();
    }


    public SysPath RelativeTo(SysPath referencePath)
    {
        // TODO

        if (IsRelative || referencePath.IsRelative )
            throw new InvalidOperationException();

        var refPath = referencePath._Path.Split();
        var path = _Path.Split();

        var prefixSize = Enumerable
                        .Zip(refPath, path, (l, r) => l == r)
                        .TakeWhile(e => e)
                        .Count();

        var nUps = refPath.Length - prefixSize;

        var dirs = Enumerable
                  .Repeat("..", nUps)
                  .Concat(path.Skip(prefixSize));

        var relative = String.Join(DirectorySeparatorChar.ToString(), dirs);

        return relative.IsNullOrEmpty() ? "." : relative;
    }

    public Boolean IsSubPathOf(SysPath referencePath)
    {
        // TODO: verify

        if (this == referencePath)
            return true;

        if (!_Path.StartsWith(referencePath))
            return false;

        return _Path.Length > referencePath._Path.Length &&
               _Path[referencePath._Path.Length] == DirectorySeparatorChar;
    }

    public SysPath Append(SysPath right)
    {
        if (right.IsAbsolute)
            throw new ArgumentException("right path cannot be absolute", nameof(right));

        return $"{_Path}/{right}";
    }


    #region overrides

    public override Boolean Equals(Object? obj) => _Path.Equals(obj?.ToString());
    public override Int32   GetHashCode()       => _Path.GetHashCode();
    public override String  ToString()          => _Path;

    #endregion

    #region operators

    public static implicit operator String(SysPath sysPath) => sysPath._Path;
    public static implicit operator SysPath(String path) => new SysPath(path);

    public static SysPath operator /(SysPath left, SysPath right) => left.Append(right);
    public static SysPath operator /(String  left, SysPath right) => FromString(left).Append(right);
    public static SysPath operator /(SysPath left, String  right) => left.Append(right);

    public static Boolean operator ==(SysPath left, SysPath right) => left._Path == right._Path;
    public static Boolean operator !=(SysPath left, SysPath right) => left._Path != right._Path;

    public static Boolean operator ==(SysPath left, String right) => left._Path == right;
    public static Boolean operator !=(SysPath left, String right) => left._Path != right;

    public static Boolean operator ==(String left, SysPath right) => left == right._Path;
    public static Boolean operator !=(String left, SysPath right) => left != right._Path;

    #endregion

}