#+feature global-context
#+private
package os

import "base:runtime"

import "core:io"
import "core:sys/wasm/wasi"
import "core:time"

// NOTE: Don't know if there is a max in wasi.
MAX_RW :: 1 << 30

File_Impl :: struct {
	file:      File,
	name:      string,
	fd:        wasi.fd_t,
	allocator: runtime.Allocator,
}

// WASI works with "preopened" directories, the environment retrieves directories
// (for example with `wasmtime --dir=. module.wasm`) and those given directories
// are the only ones accessible by the application.
//
// So in order to facilitate the `os` API (absolute paths etc.) we keep a list
// of the given directories and match them when needed (notably `os.open`).
Preopen :: struct {
	fd:     wasi.fd_t,
	prefix: string,
}
preopens: []Preopen

@(init)
init_std_files :: proc "contextless" () {
	new_std :: proc "contextless" (impl: ^File_Impl, fd: wasi.fd_t, name: string) -> ^File {
		impl.file.impl = impl
		impl.allocator = runtime.nil_allocator()
		impl.fd = fd
		impl.name  = string(name)
		impl.file.stream = {
			data = impl,
			procedure = _file_stream_proc,
		}
		return &impl.file
	}

	@(static) files: [3]File_Impl
	stdin  = new_std(&files[0], 0, "/dev/stdin")
	stdout = new_std(&files[1], 1, "/dev/stdout")
	stderr = new_std(&files[2], 2, "/dev/stderr")
}

@(init)
init_preopens :: proc "contextless" () {
	strip_prefixes :: proc "contextless" (path: string) -> string {
		path := path
		loop: for len(path) > 0 {
			switch {
			case path[0] == '/':
				path = path[1:]
			case len(path) > 2  && path[0] == '.' && path[1] == '/':
				path = path[2:]
			case len(path) == 1 && path[0] == '.':
				path = path[1:]
			case:
				break loop
			}
		}
		return path
	}

	context = runtime.default_context()

	n: int
	n_loop: for fd := wasi.fd_t(3); ; fd += 1 {
		_, err := wasi.fd_prestat_get(fd)
		#partial switch err {
		case .BADF:    break n_loop
		case .SUCCESS: n += 1
		case:
			print_error(stderr, _get_platform_error(err), "unexpected error from wasi_prestat_get")
			break n_loop
		}
	}

	alloc_err: runtime.Allocator_Error
	preopens, alloc_err = make([]Preopen, n, file_allocator())
	if alloc_err != nil {
		print_error(stderr, alloc_err, "could not allocate memory for wasi preopens")
		return
	}

	loop: for &preopen, i in preopens {
		fd := wasi.fd_t(3 + i)

		desc, err := wasi.fd_prestat_get(fd)
		assert(err == .SUCCESS)

		switch desc.tag {
		case .DIR:
			buf: []byte
			buf, alloc_err = make([]byte, desc.dir.pr_name_len, file_allocator())
			if alloc_err != nil {
				print_error(stderr, alloc_err, "could not allocate memory for wasi preopen dir name")
				continue loop
			}

			if err = wasi.fd_prestat_dir_name(fd, buf); err != .SUCCESS {
				print_error(stderr, _get_platform_error(err), "could not get filesystem preopen dir name")
				continue loop
			}

			preopen.fd = fd
			preopen.prefix = strip_prefixes(string(buf))
		}
	}
}

@(require_results)
match_preopen :: proc(path: string) -> (wasi.fd_t, string, bool) {
	@(require_results)
	prefix_matches :: proc(prefix, path: string) -> bool {
		// Empty is valid for any relative path.
		if len(prefix) == 0 && len(path) > 0 && path[0] != '/' {
			return true
		}

		if len(path) < len(prefix) {
			return false
		}

		if path[:len(prefix)] != prefix {
			return false
		}

		// Only match on full components.
		i := len(prefix)
		for i > 0 && prefix[i-1] == '/' {
			i -= 1
		}
		return path[i] == '/'
	}

	path := path
	if path == "" {
		return 0, "", false
	}

	for len(path) > 0 && path[0] == '/' {
		path = path[1:]
	}

	match: Preopen
	#reverse for preopen in preopens {
		if (match.fd == 0 || len(preopen.prefix) > len(match.prefix)) && prefix_matches(preopen.prefix, path) {
			match = preopen
		}
	}

	if match.fd == 0 {
		return 0, "", false
	}

	relative := path[len(match.prefix):]
	for len(relative) > 0 && relative[0] == '/' {
		relative = relative[1:]
	}

	if len(relative) == 0 {
		relative = "."
	}

	return match.fd, relative, true
}

_open :: proc(name: string, flags: File_Flags, perm: Permissions) -> (f: ^File, err: Error) {
	dir_fd, relative, ok := match_preopen(name)
	if !ok {
		return nil, .Invalid_Path
	}

	oflags: wasi.oflags_t
	if .Create in flags { oflags += {.CREATE} }
	if .Excl   in flags { oflags += {.EXCL} }
	if .Trunc  in flags { oflags += {.TRUNC} }

	fdflags: wasi.fdflags_t
	if .Append       in flags { fdflags += {.APPEND} }
	if .Sync         in flags { fdflags += {.SYNC} }
	if .Non_Blocking in flags { fdflags += {.NONBLOCK} }

	// NOTE: rights are adjusted to what this package's functions might want to call.
	rights: wasi.rights_t
	if .Read  in flags { rights += {.FD_READ, .FD_FILESTAT_GET, .PATH_FILESTAT_GET} }
	if .Write in flags { rights += {.FD_WRITE, .FD_SYNC, .FD_FILESTAT_SET_SIZE, .FD_FILESTAT_SET_TIMES, .FD_SEEK} }

	fd, fderr := wasi.path_open(dir_fd, {.SYMLINK_FOLLOW}, relative, oflags, rights, {}, fdflags)
	if fderr != nil {
		err = _get_platform_error(fderr)
		return
	}

	return _new_file(uintptr(fd), name, file_allocator())
}

_new_file :: proc(handle: uintptr, name: string, allocator: runtime.Allocator) -> (f: ^File, err: Error) {
	if name == "" {
		err = .Invalid_Path
		return
	}

	impl := new(File_Impl, allocator) or_return
	defer if err != nil { free(impl, allocator) }

	impl.allocator = allocator
	// NOTE: wasi doesn't really do full paths afact.
	impl.name = clone_string(name, allocator) or_return
	impl.fd = wasi.fd_t(handle)
	impl.file.impl = impl
	impl.file.stream = {
		data = impl,
		procedure = _file_stream_proc,
	}

	return &impl.file, nil
}

_clone :: proc(f: ^File) -> (clone: ^File, err: Error) {
	if f == nil || f.impl == nil {
		return
	}

	dir_fd, relative, ok := match_preopen(name(f))
	if !ok {
		return nil, .Invalid_Path
	}

	fd, fderr := wasi.path_open(dir_fd, {.SYMLINK_FOLLOW}, relative, {}, {}, {}, {})
	if fderr != nil {
		err = _get_platform_error(fderr)
		return
	}
	defer if err != nil { wasi.fd_close(fd) }

	fderr = wasi.fd_renumber((^File_Impl)(f.impl).fd, fd)
	if fderr != nil {
		err = _get_platform_error(fderr)
		return
	}

	return _new_file(uintptr(fd), name(f), file_allocator())
}

_close :: proc(f: ^File_Impl) -> (err: Error) {
	if errno := wasi.fd_close(f.fd); errno != nil {
		err = _get_platform_error(errno)
	}

	delete(f.name, f.allocator)
	free(f, f.allocator)
	return
}

_fd :: proc(f: ^File) -> uintptr {
	return uintptr(__fd(f))
}

__fd :: proc(f: ^File) -> wasi.fd_t {
	if f != nil && f.impl != nil {
		return (^File_Impl)(f.impl).fd
	}
	return -1
}

_is_tty :: proc "contextless" (f: ^File) -> bool {
	return false
}

_name :: proc(f: ^File) -> string {
	if f != nil && f.impl != nil {
		return (^File_Impl)(f.impl).name
	}
	return ""
}

_sync :: proc(f: ^File) -> Error {
	return _get_platform_error(wasi.fd_sync(__fd(f)))
}

_truncate :: proc(f: ^File, size: i64) -> Error {
	return _get_platform_error(wasi.fd_filestat_set_size(__fd(f), wasi.filesize_t(size)))
}

_remove :: proc(name: string) -> Error {
	dir_fd, relative, ok := match_preopen(name)
	if !ok {
		return .Invalid_Path
	}

	err := wasi.path_remove_directory(dir_fd, relative)
	if err == .NOTDIR {
		err = wasi.path_unlink_file(dir_fd, relative)
	}

	return _get_platform_error(err)
}

_rename :: proc(old_path, new_path: string) -> Error {
	src_dir_fd, src_relative, src_ok := match_preopen(old_path)
	if !src_ok {
		return .Invalid_Path
	}

	new_dir_fd, new_relative, new_ok := match_preopen(new_path)
	if !new_ok {
		return .Invalid_Path
	}

	return _get_platform_error(wasi.path_rename(src_dir_fd, src_relative, new_dir_fd, new_relative))
}

_link :: proc(old_name, new_name: string) -> Error {
	src_dir_fd, src_relative, src_ok := match_preopen(old_name)
	if !src_ok {
		return .Invalid_Path
	}

	new_dir_fd, new_relative, new_ok := match_preopen(new_name)
	if !new_ok {
		return .Invalid_Path
	}

	return _get_platform_error(wasi.path_link(src_dir_fd, {.SYMLINK_FOLLOW}, src_relative, new_dir_fd, new_relative))
}

_symlink :: proc(old_name, new_name: string) -> Error {
	src_dir_fd, src_relative, src_ok := match_preopen(old_name)
	if !src_ok {
		return .Invalid_Path
	}

	new_dir_fd, new_relative, new_ok := match_preopen(new_name)
	if !new_ok {
		return .Invalid_Path
	}

	if src_dir_fd != new_dir_fd {
		return .Invalid_Path
	}

	return _get_platform_error(wasi.path_symlink(src_relative, src_dir_fd, new_relative))
}

_read_link :: proc(name: string, allocator: runtime.Allocator) -> (s: string, err: Error) {
	dir_fd, relative, ok := match_preopen(name)
	if !ok {
		return "", .Invalid_Path
	}

	n, _err := wasi.path_readlink(dir_fd, relative, nil)
	if _err != nil {
		err = _get_platform_error(_err)
		return
	}

	buf := make([]byte, n, allocator) or_return

	_, _err = wasi.path_readlink(dir_fd, relative, buf)
	s = string(buf)
	err = _get_platform_error(_err)
	return
}

_chdir :: proc(name: string) -> Error {
	return .Unsupported
}

_fchdir :: proc(f: ^File) -> Error {
	return .Unsupported
}

_fchmod :: proc(f: ^File, mode: Permissions) -> Error {
	return .Unsupported
}

_chmod :: proc(name: string, mode: Permissions) -> Error {
	return .Unsupported
}

_fchown :: proc(f: ^File, uid, gid: int) -> Error {
	return .Unsupported
}

_chown :: proc(name: string, uid, gid: int) -> Error {
	return .Unsupported
}

_lchown :: proc(name: string, uid, gid: int) -> Error {
	return .Unsupported
}

_chtimes :: proc(name: string, atime, mtime: time.Time) -> Error {
	dir_fd, relative, ok := match_preopen(name)
	if !ok {
		return .Invalid_Path
	}

	_atime := wasi.timestamp_t(atime._nsec)
	_mtime := wasi.timestamp_t(mtime._nsec)

	return _get_platform_error(wasi.path_filestat_set_times(dir_fd, {.SYMLINK_FOLLOW}, relative, _atime, _mtime, {.MTIM, .ATIM}))
}

_fchtimes :: proc(f: ^File, atime, mtime: time.Time) -> Error {
	_atime := wasi.timestamp_t(atime._nsec)
	_mtime := wasi.timestamp_t(mtime._nsec)

	return _get_platform_error(wasi.fd_filestat_set_times(__fd(f), _atime, _mtime, {.ATIM, .MTIM}))
}

_exists :: proc(path: string) -> bool {
	dir_fd, relative, ok := match_preopen(path)
	if !ok {
		return false
	}

	_, err := wasi.path_filestat_get(dir_fd, {.SYMLINK_FOLLOW}, relative)
	if err != nil {
		return false
	}

	return true
}

_file_stream_proc :: proc(stream_data: rawptr, mode: File_Stream_Mode, p: []byte, offset: i64, whence: io.Seek_From, allocator: runtime.Allocator) -> (n: i64, err: Error) {
	f  := (^File_Impl)(stream_data)
	fd := f.fd

	switch mode {
	case .Read:
		if len(p) <= 0 {
			return
		}

		to_read := min(len(p), MAX_RW)
		_n, _err := wasi.fd_read(fd, {p[:to_read]})
		n = i64(_n)

		if _err != nil {
			err = .Unknown
		} else if n == 0 {
			err = .EOF
		}

		return

	case .Read_At:
		if len(p) <= 0 {
			return
		}

		if offset < 0 {
			err = .Invalid_Offset
			return
		}

		to_read := min(len(p), MAX_RW)
		_n, _err := wasi.fd_pread(fd, {p[:to_read]}, wasi.filesize_t(offset))
		n = i64(_n)

		if _err != nil {
			err = .Unknown
		} else if n == 0 {
			err = .EOF
		}

		return

	case .Write:
		p := p
		for len(p) > 0 {
			to_write := min(len(p), MAX_RW)
			_n, _err := wasi.fd_write(fd, {p[:to_write]})
			if _err != nil {
				err = .Unknown
				return
			}
			p = p[_n:]
			n += i64(_n)
		}
		return

	case .Write_At:
		p := p
		offset := offset

		if offset < 0 {
			err = .Invalid_Offset
			return
		}

		for len(p) > 0 {
			to_write := min(len(p), MAX_RW)
			_n, _err := wasi.fd_pwrite(fd, {p[:to_write]}, wasi.filesize_t(offset))
			if _err != nil {
				err = .Unknown
				return
			}

			p = p[_n:]
			n += i64(_n)
			offset += i64(_n)
		}
		return

	case .Seek:
		#assert(int(wasi.whence_t.SET) == int(io.Seek_From.Start))
		#assert(int(wasi.whence_t.CUR) == int(io.Seek_From.Current))
		#assert(int(wasi.whence_t.END) == int(io.Seek_From.End))

		switch whence {
		case .Start, .Current, .End:
			break
		case:
			err = .Invalid_Whence
			return
		}

		_n, _err := wasi.fd_seek(fd, wasi.filedelta_t(offset), wasi.whence_t(whence))
		#partial switch _err {
		case .INVAL:
			err = .Invalid_Offset
		case:
			err = .Unknown
		case .SUCCESS:
			n = i64(_n)
		}
		return

	case .Size:
		stat, _err := wasi.fd_filestat_get(fd)
		if _err != nil {
			err = .Unknown
			return
		}

		n = i64(stat.size)
		return

	case .Flush:
		ferr := _sync(&f.file)
		err   = error_to_io_error(ferr)
		return

	case .Close, .Destroy:
		ferr := _close(f)
		err   = error_to_io_error(ferr)
		return

	case .Query:
		return io.query_utility({.Read, .Read_At, .Write, .Write_At, .Seek, .Size, .Flush, .Close, .Destroy, .Query})

	case .Fstat:
		err = file_stream_fstat_utility(f, p, allocator)
		return

	case:
		return 0, .Unsupported
	}
}
