
header "sys" as sys

use io
use utils

#if OS != win
use core
#end

value default_read_size (32 * 1024)

+ fn open(path: String, writable: bool, append_on_write: bool) FD !open {
    return open_extend(path, writable, append_on_write, false) ! throw open
}
// TODO: use octal number for permissions
+ fn open_extend(path: String, writable: bool, append_on_write: bool, create_file_if_doesnt_exist: bool (false), create_file_permissions: u32 (420)) FD !open !access {

    // let flags : i32 = (sys:O_RDONLY | sys:O_NONBLOCK).@cast(i32)
    let flags : i32 = sys:O_RDONLY.@cast(i32)
    if writable : flags = sys:O_RDWR

    #if OS == win
    flags = flags | sys:O_BINARY
    #end

    if create_file_if_doesnt_exist : flags = flags | sys:O_CREAT
    if writable {
        flags = flags | (append_on_write ? sys:O_APPEND : sys:O_TRUNC)
    }

    let fd = sys:open(path.data_cstring, flags, create_file_permissions.to(i32))
    if fd < 0 {
        // let err = core:libc_errno()
        // if err == 13 {
        //     throw access
        // }
        throw open
    }

    return fd.@cast(FD)
}

- fn stat(path: String, buf: sys:libc_stat) !fail {
    let res = sys:stat(path.data_cstring, buf)
    if res == -1 : throw fail
}
+ fn size(path: String) uint {
    let buf = @stack(<sys:libc_stat>)
    stat(path, buf) ! return 0
    return buf.st_size.@cast(uint)
}

+ fn sync() {
    sys:sync()
}

+ fn read_bytes(path: String, buffer: utils:ByteBuffer (utils:ByteBuffer.new(default_read_size))) utils:ByteBuffer !open !read !close {

	let fd = open(path, false, false) ! throw open

	while true {
        buffer.minimum_free_space(default_read_size)
		let readcount = io:read(fd, buffer.data.to_ptr() + buffer.length, default_read_size) ! {
            io:close(fd)
            throw read
        }
        buffer.length += readcount
		if readcount != default_read_size : break
	}

	io:close(fd)
	return buffer
}

+ fn read(path: String) String !open !read !close {
    let buf = read_bytes(path) ! {
        match E {
            E.open => throw open
            E.close => throw close
            default => throw read
        }
    }
	return buf.to_string()
}

// TODO: !open instead of !open_err (err identifiers need to be split)
+ fn write(path: String, content: String, append: bool (false)) !open !write {
    let fd = open_extend(path, true, append, true) ! throw open
    io:write(fd, content) ! {
        io:close(fd)
        throw write
    }
    io:close(fd)
}

+ fn write_bytes(path: String, data: ptr, size: uint, append: bool (false)) !open !write {
    let fd = open_extend(path, true, append, true) ! throw open
    io:write_bytes(fd, data, size) ! {
        io:close(fd)
        throw write
    }
    io:close(fd)
}

+ fn exists(path: String) bool {
	#if OS == win
	let dwAttrib = sys:GetFileAttributesA(path.data_cstring)
    return dwAttrib != sys:INVALID_FILE_ATTRIBUTES_fix
	#else
    return sys:access(path.data_cstring, sys:F_OK) == 0
    #end
}
+ fn delete(path: String) !delete {
    // sys:unlink(path.data_cstring)
    let res = sys:unlink(path.data_cstring)
    if res == -1 : throw delete
}
+ fn delete_recursive(path: String) {
    let isdir = is_dir(path)
    if isdir {
        let list = Array[String].new()
        files_in(path, true, true, false, null, list)
        each list as file {
            delete(file) _
        }
        list.clear()
        files_in(path, true, false, true, null, list)
        list.reverse()
        each list as file {
            rmdir(file) _
        }
        // Delete dir itself
        rmdir(path) _
    } else {
        delete(path) _
    }
}
+ fn move(from_path: String, to_path: String) !fail {
    let res = sys:rename(from_path.data_cstring, to_path.data_cstring)
    if res != 0 : throw fail
}

+ fn copy(from_path: String, to_path: String, recursive: bool (false)) !fail {
    if from_path == to_path : return
    let isdir = is_dir(from_path)
    if isdir {
        if !exists(to_path) : mkdir(to_path) ! throw fail
        else if !is_dir(to_path) : throw fail
        // Copy files
        let files = files_in(from_path, false, true, true, "")
        each files as fn {
            let from = add(from_path, fn)
            let to = add(to_path, fn)
            copy(from, to, recursive) ! throw fail
        }
    } else {

        let buf = utils:ByteBuffer.new(32000)
        let in = open_extend(from_path, false, false) ! throw fail
        let out = open_extend(to_path, true, false, true) ! throw fail

        while true {
            let rcount = io:read(in, buf.data, 32000) ! {
                io:close(in)
                io:close(out)
                throw fail
            }
            io:write_bytes(out, buf.data, rcount) ! {
                io:close(in)
                io:close(out)
                throw fail
            }
            if rcount == 0 {
                io:close(in)
                io:close(out)
                break
            }
        }
    }
}

// TODO: use octal numbers
// 493 = 0755
+ fn mkdir(path: String, permissions: u32 (493)) !fail {
    if sys:mkdir(path.data_cstring, permissions) == -1 {
        throw fail
    }
}
+ fn rmdir(path: String) !fail {
    if sys:rmdir(path.data_cstring) == -1 : throw fail
}

+ fn is_file(path: String) bool {
    let buf = @stack(<sys:libc_stat>)
    stat(path, buf) ! return false
    return (buf.st_mode & sys:S_IFMT) == sys:S_IFREG
}
+ fn is_dir(path: String) bool {
    let buf = @stack(<sys:libc_stat>)
    stat(path, buf) ! return false
    return (buf.st_mode & sys:S_IFDIR) == sys:S_IFDIR
}

+ fn files_in(dir: String, recursive: bool (false), files: bool (true), dirs: bool (true), prefix: ?String (null), result: Array[String] (.{})) Array[String] {

    dir = resolve(dir)
    if !isset(prefix) : prefix = dir

    #if OS == win

    let glob : String = add(dir, "*")
    let pattern = glob.data_cstring
    let data = @stack(<sys:libc_WIN32_FIND_DATAA>)
    let hFind = sys:FindFirstFileA(pattern, data)
    if hFind != -1.to(ptr) {
        let loop = true
        while loop {
            let fn = data.cFileName.@cast(cstring).to_string()

            if fn.bytes < 3 && (fn == "." || fn == "..") {
                loop = sys:FindNextFileA(hFind, data)
                continue
            }

            // TODO: use 0x10, instead of 16
            let is_dir = (data.dwFileAttributes & 16) > 0
            let is_file = !is_dir
            let full = add(dir, fn)
            fn = prefix.length > 0 ? add(prefix, fn) : fn

            if (dirs && is_dir) || (files && is_file) {
                result.append(fn)
            }
            if is_dir && recursive {
                files_in(full, recursive, files, dirs, fn, result)
            }

            loop = sys:FindNextFileA(hFind, data)
        }
        sys:FindClose(hFind)
    }

    #else

    let d = sys:opendir(dir.data_cstring)

    if isset(d) {
        while true {
            let ent = sys:readdir(d)
            if isset(ent) {
                let fn = ent.d_name.@cast(cstring).to_string()

                if fn.bytes < 3 && (fn == "." || fn == ".." || fn == "") {
                    continue
                }

                let full = add(dir, fn)
                fn = prefix.length > 0 ? add(prefix, fn) : fn
                let _is_dir = is_dir(full)
                let _is_file = !_is_dir

                if (dirs && _is_dir) || (files && _is_file) {
                    result.append(fn)
                }

                if _is_dir && recursive {
                    files_in(full, recursive, files, dirs, fn, result)
                }

                continue
            }
            break

        }
        sys:closedir(d)
    }

    #end

    return result
}


+ fn symlink(link: String, target: String, is_directory: bool) !permissions !exists !other {
    #if OS == win
    let flag : u32 = sys:SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
    if is_directory : flag = flag | sys:SYMBOLIC_LINK_FLAG_DIRECTORY
    if sys:CreateSymbolicLinkA(link.data_cstring, target.data_cstring, flag) == 1 : return
    let error = sys:GetLastError()
    if error == sys:ERROR_PRIVILEGE_NOT_HELD : throw permissions
    if error == sys:ERROR_ALREADY_EXISTS : throw exists
    #else
    if sys:symlink(target.data_cstring, link.data_cstring) == 0 : return
    let error = core:libc_errno()
    if error == sys:EACCES : throw permissions
    if error == sys:EEXIST : throw exists
    #end

    throw other
}
