#+private
#+build darwin, netbsd, freebsd, openbsd
package os2

import "base:runtime"

import "core:io"
import "core:c"
import "core:time"
import "core:sys/posix"

// Most implementations will EINVAL at some point when doing big writes.
// In practice a read/write call would probably never read/write these big buffers all at once,
// which is why the number of bytes is returned and why there are procs that will call this in a
// loop for you.
// We set a max of 1GB to keep alignment and to be safe.
MAX_RW :: 1 << 30

File_Impl :: struct {
	file:  File,
	name:  string,
	cname: cstring,
	fd:    posix.FD,
}

@(init)
init_std_files :: proc() {
	// NOTE: is this (paths) also the case on non darwin?

	stdin = __new_file(posix.STDIN_FILENO)
	(^File_Impl)(stdin.impl).name  = "/dev/stdin"
	(^File_Impl)(stdin.impl).cname = "/dev/stdin"

	stdout = __new_file(posix.STDIN_FILENO)
	(^File_Impl)(stdout.impl).name  = "/dev/stdout"
	(^File_Impl)(stdout.impl).cname = "/dev/stdout"

	stderr = __new_file(posix.STDIN_FILENO)
	(^File_Impl)(stderr.impl).name  = "/dev/stderr"
	(^File_Impl)(stderr.impl).cname = "/dev/stderr"
}

_open :: proc(name: string, flags: File_Flags, perm: int) -> (f: ^File, err: Error) {
	if name == "" {
		err = .Invalid_Path
		return
	}

	sys_flags := posix.O_Flags{.NOCTTY, .CLOEXEC}
	
	if .Write in flags {
		if .Read in flags {
			sys_flags += {.RDWR}
		} else {
			sys_flags += {.WRONLY}
		}
	}

	if .Append      in flags { sys_flags += {.APPEND} }
	if .Create      in flags { sys_flags += {.CREAT} }
	if .Excl        in flags { sys_flags += {.EXCL} }
	if .Sync        in flags { sys_flags += {.DSYNC} }
	if .Trunc       in flags { sys_flags += {.TRUNC} }
	if .Inheritable in flags { sys_flags -= {.CLOEXEC} }

	TEMP_ALLOCATOR_GUARD()
	cname := temp_cstring(name)

	fd := posix.open(cname, sys_flags, transmute(posix.mode_t)posix._mode_t(perm))
	if fd < 0 {
		err = _get_platform_error()
		return
	}

	return _new_file(uintptr(fd), name)
}

_new_file :: proc(handle: uintptr, name: string) -> (f: ^File, err: Error) {
	if name == "" {
		err = .Invalid_Path
		return
	} else if handle == ~uintptr(0) {
		err = .Invalid_File
		return
	}

	crname := _posix_absolute_path(posix.FD(handle), name, file_allocator()) or_return
	rname  := string(crname)

	f = __new_file(posix.FD(handle))
	impl := (^File_Impl)(f.impl)
	impl.name  = rname
	impl.cname = crname

	return f, nil
}

__new_file :: proc(handle: posix.FD) -> ^File {
	impl := new(File_Impl, file_allocator())
	impl.file.impl = impl
	impl.fd = posix.FD(handle)
	impl.file.stream = {
		data = impl,
		procedure = _file_stream_proc,
	}
	impl.file.fstat = _fstat
	return &impl.file
}

_close :: proc(f: ^File_Impl) -> (err: Error) {
	if f == nil { return nil }

	if posix.close(f.fd) != .OK {
		err = _get_platform_error()
	}

	delete(f.cname, file_allocator())
	free(f, file_allocator())
	return
}

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

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

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

_sync :: proc(f: ^File) -> Error {
	if posix.fsync(__fd(f)) != .OK {
		return _get_platform_error()
	}
	return nil
}

_truncate :: proc(f: ^File, size: i64) -> Error {
	if posix.ftruncate(__fd(f), posix.off_t(size)) != .OK {
		return _get_platform_error()
	}
	return nil
}

_remove :: proc(name: string) -> Error {
	TEMP_ALLOCATOR_GUARD()
	cname := temp_cstring(name)
	if posix.remove(cname) != 0 {
		return _get_platform_error()
	}
	return nil
}

_rename :: proc(old_path, new_path: string) -> Error {
	TEMP_ALLOCATOR_GUARD()
	cold := temp_cstring(old_path)
	cnew := temp_cstring(new_path)
	if posix.rename(cold, cnew) != 0 {
		return _get_platform_error()
	}
	return nil
}

_link :: proc(old_name, new_name: string) -> Error {
	TEMP_ALLOCATOR_GUARD()
	cold := temp_cstring(old_name)
	cnew := temp_cstring(new_name)
	if posix.link(cold, cnew) != .OK {
		return _get_platform_error()
	}
	return nil
}

_symlink :: proc(old_name, new_name: string) -> Error {
	TEMP_ALLOCATOR_GUARD()
	cold := temp_cstring(old_name)
	cnew := temp_cstring(new_name)
	if posix.symlink(cold, cnew) != .OK {
		return _get_platform_error()
	}
	return nil
}

_read_link :: proc(name: string, allocator: runtime.Allocator) -> (s: string, err: Error) {
	TEMP_ALLOCATOR_GUARD()
	cname := temp_cstring(name)

	buf: [dynamic]byte
	buf.allocator = allocator
	defer if err != nil { delete(buf) }

	// Loop this because the file might've grown between lstat() and readlink().
	for {
		stat: posix.stat_t
		if posix.lstat(cname, &stat) != .OK {
			err = _get_platform_error()
			return
		}

		bufsiz := int(stat.st_size + 1 if stat.st_size > 0 else posix.PATH_MAX)

		if bufsiz == len(buf) {
			bufsiz *= 2
		}

		// Overflow.
		if bufsiz <= 0 {
			err = Platform_Error(posix.Errno.E2BIG)
			return
		}

		resize(&buf, bufsiz) or_return

		size := posix.readlink(cname, raw_data(buf), uint(bufsiz))
		if size < 0 {
			err = _get_platform_error()
			return
		}

		// File has probably grown between lstat() and readlink().
		if size == bufsiz {
			continue
		}

		s = string(buf[:size])
		return
	}
}

_chdir :: proc(name: string) -> Error {
	TEMP_ALLOCATOR_GUARD()
	cname := temp_cstring(name)
	if posix.chdir(cname) != .OK {
		return _get_platform_error()
	}
	return nil
}

_fchdir :: proc(f: ^File) -> Error {
	if posix.fchdir(__fd(f)) != .OK {
		return _get_platform_error()
	}
	return nil
}

_fchmod :: proc(f: ^File, mode: int) -> Error {
	if posix.fchmod(__fd(f), transmute(posix.mode_t)posix._mode_t(mode)) != .OK {
		return _get_platform_error()
	}
	return nil
}

_chmod :: proc(name: string, mode: int) -> Error {
	TEMP_ALLOCATOR_GUARD()
	cname := temp_cstring(name)
	if posix.chmod(cname, transmute(posix.mode_t)posix._mode_t(mode)) != .OK {
		return _get_platform_error()
	}
	return nil
}

_fchown :: proc(f: ^File, uid, gid: int) -> Error {
	if posix.fchown(__fd(f), posix.uid_t(uid), posix.gid_t(gid)) != .OK {
		return _get_platform_error()
	}
	return nil
}

_chown :: proc(name: string, uid, gid: int) -> Error {
	TEMP_ALLOCATOR_GUARD()
	cname := temp_cstring(name)
	if posix.chown(cname, posix.uid_t(uid), posix.gid_t(gid)) != .OK {
		return _get_platform_error()
	}
	return nil
}

_lchown :: proc(name: string, uid, gid: int) -> Error {
	TEMP_ALLOCATOR_GUARD()
	cname := temp_cstring(name)
	if posix.lchown(cname, posix.uid_t(uid), posix.gid_t(gid)) != .OK {
		return _get_platform_error()
	}
	return nil
}

_chtimes :: proc(name: string, atime, mtime: time.Time) -> Error {
	times := [2]posix.timeval{
		{
			tv_sec  = posix.time_t(atime._nsec/1e9),           /* seconds */
			tv_usec = posix.suseconds_t(atime._nsec%1e9/1000), /* microseconds */
		},
		{
			tv_sec  = posix.time_t(mtime._nsec/1e9),           /* seconds */
			tv_usec = posix.suseconds_t(mtime._nsec%1e9/1000), /* microseconds */
		},
	}

	TEMP_ALLOCATOR_GUARD()
	cname := temp_cstring(name)

	if posix.utimes(cname, &times) != .OK {
		return _get_platform_error()
	}
	return nil
}

_fchtimes :: proc(f: ^File, atime, mtime: time.Time) -> Error {
	times := [2]posix.timespec{
		{
			tv_sec  = posix.time_t(atime._nsec/1e9), /* seconds */
			tv_nsec = c.long(atime._nsec%1e9),       /* nanoseconds */
		},
		{
			tv_sec  = posix.time_t(mtime._nsec/1e9), /* seconds */
			tv_nsec = c.long(mtime._nsec%1e9),       /* nanoseconds */
		},
	}

	if posix.futimens(__fd(f), &times) != .OK {
		return _get_platform_error()
	}
	return nil
}

_exists :: proc(path: string) -> bool {
	TEMP_ALLOCATOR_GUARD()
	cpath := temp_cstring(path)
	return posix.access(cpath) == .OK
}

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

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

		to_read := uint(min(len(p), MAX_RW))
		n = i64(posix.read(fd, raw_data(p), to_read))
		switch {
		case n == 0:
			err = .EOF
		case n < 0:
			err = .Unknown
		}
		return

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

		if offset < 0 {
			err = .Invalid_Offset
			return
		}

		to_read := uint(min(len(p), MAX_RW))
		n = i64(posix.pread(fd, raw_data(p), to_read, posix.off_t(offset)))
		switch {
		case n == 0:
			err = .EOF
		case n < 0:
			err = .Unknown
		}
		return

	case .Write:
		p := p
		for len(p) > 0 {
			to_write := uint(min(len(p), MAX_RW))
			if _n := i64(posix.write(fd, raw_data(p), to_write)); _n <= 0 {
				err = .Unknown
				return
			} else {
				p = p[_n:]
				n += _n
			}
		}
		return

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

		if offset < 0 {
			err = .Invalid_Offset
			return
		}

		for len(p) > 0 {
			to_write := uint(min(len(p), MAX_RW))
			if _n := i64(posix.pwrite(fd, raw_data(p), to_write, posix.off_t(offset))); _n <= 0 {
				err = .Unknown
				return
			} else {
				p = p[_n:]
				n += _n
				offset += _n
			}
		}
		return

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

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

		n = i64(posix.lseek(fd, posix.off_t(offset), posix.Whence(whence)))
		if n < 0 {
			#partial switch posix.get_errno() {
			case .EINVAL:
				err = .Invalid_Offset
			case:
				err = .Unknown
			}
		}
		return

	case .Size:
		stat: posix.stat_t
		if posix.fstat(fd, &stat) != .OK {
			err = .Unknown
			return
		}

		n = i64(stat.st_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:
		return 0, .Empty
	}
}
