
header "sys" as sys

use core

- global EXE_PATH : ?String (null)
- global EXE_DIR : ?String (null)

+ value PATH_MAX (4096)

#if OS == win
+ value PATH_DIV ("\\")
+ value PATH_DIV_TWICE ("\\\\")
+ value PATH_DIV_REPLACE ("/")
+ value PATH_DOT_DIV (".\\")
#else
+ value PATH_DIV ("/")
+ value PATH_DIV_TWICE ("//")
+ value PATH_DIV_REPLACE ("\\")
+ value PATH_DOT_DIV ("./")
#end

+ mode Path for String {
    + static fn new(path: String) Path {
        return path
    }
    + fn add(part: String) Path {
        return add(this, part)
    }
    + fn pop() Path {
        return dir_of(this)
    }
    + fn dir_of() Path {
        return dir_of(this)
    }
    + fn resolve() Path {
        return resolve(this)
    }
}

+ fn path(path: String) Path { return path }

+ fn resolve(path: String) String {
    // Use correct slashes
    path = path.replace(PATH_DIV_REPLACE, PATH_DIV)
    // Remove double slashes (except at start)
    if path.contains(PATH_DIV_TWICE) {
        let starts_with_div2 = path.starts_with(PATH_DIV_TWICE)
        while path.contains(PATH_DIV_TWICE) : path = path.replace(PATH_DIV_TWICE, PATH_DIV)
        if starts_with_div2 : path = PATH_DIV + path
    }
    // Replace dots
    if path == "." : path = cwd()
    else if path.starts_with(PATH_DOT_DIV) : path = add(cwd(), path.part(1, path.length - 1))
    else if (path.get(0) != '/' && path.get(1) != ':') && !path.starts_with("\\\\wsl.") : path = add(cwd(), path)
    if path.contains("..") : path = realpath(path)
    // Consistency
    if path.ends_with(PATH_DIV) : path = path.rtrim(PATH_DIV)
    if path.is_empty() : path = PATH_DIV
    return path
}

+ fn add(dir: String, fn: String) String {
    let s1 = dir.ends_with(PATH_DIV) || dir.ends_with(PATH_DIV_REPLACE)
    let s2 = fn.starts_with(PATH_DIV) || fn.starts_with(PATH_DIV_REPLACE)
    if !s1 && !s2 : return dir + PATH_DIV + fn
    if s1 && s2 : return (dir + fn.ltrim(PATH_DIV).ltrim(PATH_DIV_REPLACE))
    return dir + fn
}

+ fn cwd() String {
    let buf : cstring = @stack([u8 x 4096])
    let dir = sys:getcwd(buf, 4096)
    if !isset(dir) : panic("cwd: OS failed to return the current directory")
    return dir
}

+ fn chdir(path: String) {
    sys:chdir(path.data)
}

+ fn exe_dir() String {
    let dir = EXE_DIR
    if isset(dir) : return dir
    let d = dir_of(exe_path())
    EXE_DIR = d
    return d
}

+ fn exe_path() String {
    let p = EXE_PATH
    if isset(p) {
        return p
    }
    // TODO: USE PATH_MAX
    let buf : ptr[u8 x 4096] = @stack([u8 x 4096])

    #if OS == win
    let len = sys:GetModuleFileNameA(null, buf, PATH_MAX)
    let str = String.make_from_ptr(buf, len)
       
    #elif OS == macos

    let size : u32 = PATH_MAX;
    if sys:_NSGetExecutablePath(buf, @ref(size)) < 0 {
        println("Could not determine macos valk executable path\n");
        exit(1);
    }
    let path : String = buf.@cast(cstring)
    return realpath(path)

    #else
    let link = "/proc/self/exe".data_cstring
    let len = sys:readlink(link, buf, PATH_MAX)
    let str = String.make_from_ptr(buf, len.to(u64))
    #end

    str = resolve(str)
    EXE_PATH = str
    return str
}

+ fn realpath(path: String) String {
    let buf : ptr[u8 x 4096] = @stack([u8 x 4096])
    #if OS == win
    return sys:_fullpath(buf, path.data, PATH_MAX);
    #elif OS == macos
    path = path.replace(PATH_DIV_REPLACE, PATH_DIV)
    if path.starts_with(".") {
        path = cwd() + PATH_DIV + path
    }
    if exists(path) {
        return sys:realpath(path.data, buf)
    }
    #else
    path = path.replace(PATH_DIV_REPLACE, PATH_DIV)
    if exists(path) {
        return sys:realpath(path.data, buf)
    }
    #end
    return path
}

+ fn ext(path: String, with_dot: bool (false)) String {
    let len = path.bytes
    let start = len
    let found = false
    while start > 0 {
        start--
        let ch = path.get(start)
        if ch == '.' {
            if with_dot == false : start++
            found = true
            break
        }
        if ch == '/' || ch == '\\' : break
    }
    if found == false : return ""
    return path.part(start, len - start)
}

// e.g. /etc/nginx/nginx.conf -> /etc/nginx
// e.g. /etc/nginx/ -> /etc
// e.g. /etc -> /
// e.g. "" -> ""
// e.g. etc -> etc
+ fn dir_of(path: String) String {
    let bytes = path.bytes
    if bytes == 0 : return ""
    let end = bytes - 1
    while end-- > 0 {
        let ch = path.get(end)
        if ch == '/' || ch == '\\' {
            if end == 0 : end++
            return path.part(0, end)
        }
    }
    return path
}

// e.g. /etc/nginx/nginx.conf -> nginx.conf
// e.g. /etc/nginx/ -> nginx
// e.g. / -> ""
// e.g. "" -> ""
+ fn basename(path: String) String {
    let len = path.bytes
    let start = len
    let rtrim : uint = 0
    while start > 0 {
        let ch = path.get(start - 1)
        if ch == '/' || ch == '\\' {
            start--
            rtrim++
            continue
        }
        break
    }
    while start-- > 0 {
        let ch = path.get(start)
        if(ch == '/' || ch == '\\'){
            start++
            break
        }
    }
    return path.part(start, len - start - rtrim)
}

+ fn home_dir() String !not_found {
    #if OS == win
    return core:getenv("USERPROFILE") ! throw not_found
    #end
    return core:getenv("HOME") ! throw not_found
}
