package libc

import "core:c"
import "core:io"

when ODIN_OS == .Windows {
	foreign import libc {
		"system:libucrt.lib",
		"system:legacy_stdio_definitions.lib",
	}
} else when ODIN_OS == .Darwin {
	foreign import libc "system:System"
} else {
	foreign import libc "system:c"
}

// 7.21 Input/output

FILE :: c.FILE

Whence :: enum int {
	SET = SEEK_SET,
	CUR = SEEK_CUR,
	END = SEEK_END,
}

// MSVCRT compatible.
when ODIN_OS == .Windows {
	_IOFBF       :: 0x0000
	_IONBF       :: 0x0004
	_IOLBF       :: 0x0040

	BUFSIZ       :: 512

	EOF          :: int(-1)

	FOPEN_MAX    :: 20

	FILENAME_MAX :: 260

	L_tmpnam     :: 15 // "\\" + 12 + NUL

	SEEK_SET     :: 0
	SEEK_CUR     :: 1
	SEEK_END     :: 2

	TMP_MAX      :: 32767 // SHRT_MAX

	fpos_t       :: distinct i64

	@(private="file")
	@(default_calling_convention="c")
	foreign libc {
		__acrt_iob_func :: proc (index: uint) -> ^FILE ---
	}

	stdin  := __acrt_iob_func(0)
	stdout := __acrt_iob_func(1)
	stderr := __acrt_iob_func(2)
}

// GLIBC and MUSL compatible.
when ODIN_OS == .Linux {
	fpos_t        :: struct #raw_union { _: [16]char, _: longlong, _: double, }

	_IOFBF        :: 0
	_IOLBF        :: 1
	_IONBF        :: 2

	BUFSIZ        :: 1024

	EOF           :: int(-1)

	FOPEN_MAX     :: 1000

	FILENAME_MAX  :: 4096

	L_tmpnam      :: 20

	SEEK_SET      :: 0
	SEEK_CUR      :: 1
	SEEK_END      :: 2

	TMP_MAX       :: 308915776

	foreign libc {
		stderr: ^FILE
		stdin:  ^FILE
		stdout: ^FILE
	}
}

when ODIN_OS == .JS {
	fpos_t        :: struct #raw_union { _: [16]char, _: longlong, _: double, }

	_IOFBF        :: 0
	_IOLBF        :: 1
	_IONBF        :: 2

	BUFSIZ        :: 1024

	EOF           :: int(-1)

	FOPEN_MAX     :: 1000

	FILENAME_MAX  :: 4096

	L_tmpnam      :: 20

	SEEK_SET      :: 0
	SEEK_CUR      :: 1
	SEEK_END      :: 2

	TMP_MAX       :: 308915776
}

when ODIN_OS == .OpenBSD || ODIN_OS == .NetBSD {
	fpos_t :: distinct i64

	_IOFBF :: 0
	_IOLBF :: 1
	_IONBF :: 1

	BUFSIZ :: 1024

	EOF :: int(-1)

	FOPEN_MAX	:: 20
	FILENAME_MAX	:: 1024

	SEEK_SET :: 0
	SEEK_CUR :: 1
	SEEK_END :: 2

	TMP_MAX :: 308915776

	foreign libc {
		__sF: [3]FILE
	}

	stdin:  ^FILE = &__sF[0]
	stdout: ^FILE = &__sF[1]
	stderr: ^FILE = &__sF[2]
}

when ODIN_OS == .FreeBSD {
	fpos_t :: distinct i64

	_IOFBF :: 0
	_IOLBF :: 1
	_IONBF :: 1

	BUFSIZ :: 1024

	EOF :: int(-1)

	FOPEN_MAX	:: 20
	FILENAME_MAX	:: 1024

	SEEK_SET :: 0
	SEEK_CUR :: 1
	SEEK_END :: 2

	TMP_MAX :: 308915776

	foreign libc {
		@(link_name="__stderrp") stderr: ^FILE
		@(link_name="__stdinp")  stdin:  ^FILE
		@(link_name="__stdoutp") stdout: ^FILE
	}
}

when ODIN_OS == .Darwin {
	fpos_t :: distinct i64
	
	_IOFBF        :: 0
	_IOLBF        :: 1
	_IONBF        :: 2

	BUFSIZ        :: 1024

	EOF           :: int(-1)

	FOPEN_MAX     :: 20

	FILENAME_MAX  :: 1024

	L_tmpnam      :: 1024

	SEEK_SET      :: 0
	SEEK_CUR      :: 1
	SEEK_END      :: 2

	TMP_MAX       :: 308915776

	foreign libc {
		@(link_name="__stderrp") stderr: ^FILE
		@(link_name="__stdinp")  stdin:  ^FILE
		@(link_name="__stdoutp") stdout: ^FILE
	}
}

when ODIN_OS == .Haiku {
	fpos_t :: distinct i64
	
	_IOFBF        :: 0
	_IOLBF        :: 1
	_IONBF        :: 2

	BUFSIZ        :: 8192

	EOF           :: int(-1)

	FOPEN_MAX     :: 128

	FILENAME_MAX  :: 256

	L_tmpnam      :: 512

	SEEK_SET      :: 0
	SEEK_CUR      :: 1
	SEEK_END      :: 2

	TMP_MAX       :: 32768

	foreign libc {
		stderr: ^FILE
		stdin:  ^FILE
		stdout: ^FILE
	}
}

when ODIN_OS == .NetBSD {
	@(private) LRENAME  :: "__posix_rename"
	@(private) LFGETPOS :: "__fgetpos50"
	@(private) LFSETPOS :: "__fsetpos50"
} else {
	@(private) LRENAME  :: "rename"
	@(private) LFGETPOS :: "fgetpos"
	@(private) LFSETPOS :: "fsetpos"
}

@(default_calling_convention="c")
foreign libc {
	// 7.21.4 Operations on files
	remove    :: proc(filename: cstring) -> int ---
	@(link_name=LRENAME)
	rename    :: proc(old, new: cstring) -> int ---
	tmpfile   :: proc() -> ^FILE ---
	tmpnam    :: proc(s: [^]char) -> [^]char ---

	// 7.21.5 File access functions
	fclose    :: proc(stream: ^FILE) -> int ---
	fflush    :: proc(stream: ^FILE) -> int ---
	fopen     :: proc(filename, mode: cstring) -> ^FILE ---
	freopen   :: proc(filename, mode: cstring, stream: ^FILE) -> ^FILE ---
	setbuf    :: proc(stream: ^FILE, buf: [^]char) ---
	setvbuf   :: proc(stream: ^FILE, buf: [^]char, mode: int, size: size_t) -> int ---

	// 7.21.6 Formatted input/output functions
	fprintf   :: proc(stream: ^FILE, format: cstring, #c_vararg args: ..any) -> int ---
	fscanf    :: proc(stream: ^FILE, format: cstring, #c_vararg args: ..any) -> int ---
	printf    :: proc(format: cstring, #c_vararg args: ..any) -> int ---
	scanf     :: proc(format: cstring, #c_vararg args: ..any) -> int ---
	snprintf  :: proc(s: [^]char, n: size_t, format: cstring, #c_vararg args: ..any) -> int ---
	sscanf    :: proc(s, format: cstring, #c_vararg args: ..any) -> int ---
	vfprintf  :: proc(stream: ^FILE, format: cstring, arg: ^va_list) -> int ---
	vfscanf   :: proc(stream: ^FILE, format: cstring, arg: ^va_list) -> int ---
	vprintf   :: proc(format: cstring, arg: ^va_list) -> int ---
	vscanf    :: proc(format: cstring, arg: ^va_list) -> int ---
	vsnprintf :: proc(s: [^]char, n: size_t, format: cstring, arg: ^va_list) -> int ---
	vsprintf  :: proc(s: [^]char, format: cstring, arg: ^va_list) -> int ---
	vsscanf   :: proc(s, format: cstring, arg: ^va_list) -> int ---

	// 7.21.7 Character input/output functions
	fgetc     :: proc(stream: ^FILE) -> int ---
	fgets     :: proc(s: [^]char, n: int, stream: ^FILE) -> [^]char ---
	fputc     :: proc(s: c.int, stream: ^FILE) -> int ---
	getc      :: proc(stream: ^FILE) -> int ---
	getchar   :: proc() -> int ---
	putc      :: proc(c: int, stream: ^FILE) -> int ---
	putchar   :: proc(c: int) -> int ---
	puts      :: proc(s: cstring) -> int ---
	ungetc    :: proc(c: int, stream: ^FILE) -> int ---
	fread     :: proc(ptr: rawptr, size: size_t, nmemb: size_t, stream: ^FILE) -> size_t ---
	fwrite    :: proc(ptr: rawptr, size: size_t, nmemb: size_t, stream: ^FILE) -> size_t ---

	// 7.21.9 File positioning functions
	@(link_name=LFGETPOS)
	fgetpos   :: proc(stream: ^FILE, pos: ^fpos_t) -> int ---
	fseek     :: proc(stream: ^FILE, offset: long, whence: Whence) -> int ---
	@(link_name=LFSETPOS)
	fsetpos   :: proc(stream: ^FILE, pos: ^fpos_t) -> int ---
	ftell     :: proc(stream: ^FILE) -> long ---
	rewind    :: proc(stream: ^FILE) ---

	// 7.21.10 Error-handling functions
	clearerr  :: proc(stream: ^FILE) ---
	feof      :: proc(stream: ^FILE) -> int ---
	ferror    :: proc(stream: ^FILE) -> int ---
	perror    :: proc(s: cstring) ---
}

to_stream :: proc(file: ^FILE) -> io.Stream {
	stream_proc :: proc(stream_data: rawptr, mode: io.Stream_Mode, p: []byte, offset: i64, whence: io.Seek_From) -> (n: i64, err: io.Error) {
		unknown_or_eof :: proc(f: ^FILE) -> io.Error {
			switch {
			case ferror(f) != 0:
				return .Unknown
			case feof(f) != 0:
				return .EOF
			case:
				return nil
			}
		}

		file := (^FILE)(stream_data)
		switch mode {
		case .Close:
			if fclose(file) != 0 {
				return 0, unknown_or_eof(file)
			}

		case .Flush:
			if fflush(file) != 0 {
				return 0, unknown_or_eof(file)
			}

		case .Read:
			n = i64(fread(raw_data(p), size_of(byte), len(p), file))
			if n == 0 { err = unknown_or_eof(file) }

		case .Read_At:
			curr := ftell(file)
			if curr == -1 {
				return 0, unknown_or_eof(file)
			}

			if fseek(file, long(offset), .SET) != 0 {
				return 0, unknown_or_eof(file)
			}

			defer fseek(file, long(curr), .SET)

			n = i64(fread(raw_data(p), size_of(byte), len(p), file))
			if n == 0 { err = unknown_or_eof(file) }
		
		case .Write:
			n = i64(fwrite(raw_data(p), size_of(byte), len(p), file))
			if n == 0 { err = unknown_or_eof(file) }

		case .Write_At:
			curr := ftell(file)
			if curr == -1 {
				return 0, unknown_or_eof(file)
			}

			if fseek(file, long(offset), .SET) != 0 {
				return 0, unknown_or_eof(file)
			}

			defer fseek(file, long(curr), .SET)

			n = i64(fwrite(raw_data(p), size_of(byte), len(p), file))
			if n == 0 { err = unknown_or_eof(file) }

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

			if fseek(file, long(offset), Whence(whence)) != 0 {
				return 0, unknown_or_eof(file)
			}
		
		case .Size:
			curr := ftell(file)
			if curr == -1 {
				return 0, unknown_or_eof(file)
			}
			defer fseek(file, curr, .SET)

			if fseek(file, 0, .END) != 0 {
				return 0, unknown_or_eof(file)
			}

			n = i64(ftell(file))
			if n == -1 {
				return 0, unknown_or_eof(file)
			}

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

	return {
		data      = file,
		procedure = stream_proc,
	}
}
