#+private
#+build windows
package console

import "core:fmt"
import "core:sys/windows"

// https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences
// https://github.com/odin-lang/Odin/blob/master/core/sys/windows/kernel32.odin

SMALL_RECT :: struct {
	Left:   windows.SHORT,
	Top:    windows.SHORT,
	Right:  windows.SHORT,
	Bottom: windows.SHORT,
}

COORD :: struct {
	X: windows.SHORT,
	Y: windows.SHORT,
}

CONSOLE_SCREEN_BUFFER_INFO :: struct {
	dwSize:              COORD,
	dwCursorPosition:    COORD,
	wAttributes:         windows.WORD,
	srWindow:            SMALL_RECT,
	dwMaximumWindowSize: COORD,
}

foreign import kernel32 "system:Kernel32.lib"
@(default_calling_convention = "stdcall")
foreign kernel32 {
	GetConsoleWindow :: proc() -> windows.HWND ---

	AllocConsole :: proc() -> windows.BOOL ---
	AttachConsole :: proc(dwProcessId: windows.DWORD) -> windows.BOOL ---
	SetConsoleCP :: proc(wCodePageID: windows.UINT) -> windows.BOOL ---
}

ATTACH_PARENT_PROCESS: i32 : -1

_Console :: struct {
	prev_output_mode: windows.DWORD,
	prev_input_mode:  windows.DWORD,
	output_handle:    windows.HANDLE,
	input_handle:     windows.HANDLE,
}

should_quit_flag := false
should_quit_handler :: proc "stdcall" (ctrl_type: windows.DWORD) -> windows.BOOL {
	switch ctrl_type {
	case windows.CTRL_C_EVENT, windows.CTRL_BREAK_EVENT, windows.CTRL_CLOSE_EVENT:
		should_quit_flag = true
		return windows.TRUE
	}
	return windows.FALSE
}

_create :: proc() -> Console {
	con: Console

	if !AttachConsole(transmute(windows.DWORD)ATTACH_PARENT_PROCESS) == windows.TRUE {
		AllocConsole()
		cx := windows.GetSystemMetrics(windows.SM_CXSCREEN) / 2
		cy := windows.GetSystemMetrics(windows.SM_CYSCREEN) / 2
		windows.MoveWindow(GetConsoleWindow(), cx - 640, cy - 360, 1280, 720, windows.TRUE)
	}

	con.output_handle = windows.GetStdHandle(windows.STD_OUTPUT_HANDLE)
	con.input_handle = windows.GetStdHandle(windows.STD_INPUT_HANDLE)

	windows.GetConsoleMode(con.output_handle, &con.prev_output_mode)
	windows.SetConsoleMode(
		con.output_handle,
		windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING,
	)
	windows.SetConsoleOutputCP(.UTF8)

	windows.GetConsoleMode(con.input_handle, &con.prev_input_mode)
	windows.SetConsoleMode(con.input_handle, windows.ENABLE_PROCESSED_INPUT | windows.ENABLE_VIRTUAL_TERMINAL_INPUT)
	SetConsoleCP(windows.CP_UTF8)

	_write(con, CURSOR_HIDE)

	con.width, con.height = _get_size(con)

	windows.SetConsoleCtrlHandler(should_quit_handler, windows.TRUE)

	return con
}

_destroy :: proc(con: ^Console) {
	_write(con^, CURSOR_SHOW)

	windows.SetConsoleMode(con.output_handle, con.prev_output_mode)
	windows.SetConsoleMode(con.input_handle, con.prev_input_mode)

	windows.FreeConsole()
}

_should_quit :: proc() -> bool {
	// FIXME: implement
	return false
}

_clear :: proc(con: Console) {
	_write(con, CLEAR)
}

_write :: proc(con: Console, msg: string) {
	windows.WriteConsoleW(con.output_handle, rawptr(windows.utf8_to_wstring(msg)), windows.DWORD(len(msg)), nil, nil)
}

_write_at :: proc(con: Console, msg: string, x, y: int) {
	_write(con, fmt.tprintf(CURSOR_POSITION + "%s", y, x, msg))
}

_putch :: proc(con: Console, c: rune, x, y: int) {
	_write(con, fmt.tprintf(CURSOR_POSITION + "%c", y, x, c))
}

_wait_for_input :: proc(con: Console) -> Keys {
	buffer: [256]u8
	num_read: u32
	windows.ReadConsoleW(con.input_handle, &buffer, len(buffer), &num_read, nil)

	// NOTE: Check two bytes due to W.
	if (buffer[0] == 27 && buffer[2] == 0) || buffer[0] == 3 || buffer[0] == 113 { 	// 27 = Escape, 3 = CTRL+C, 113 = q
		return .quit
	} else if buffer[0] == 32 { 	// 32 = Space
		return .forward
	} else {
		input := [?]u8{buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5]}
		if (input == [?]u8{27, 0, 91, 0, 68, 0}) {
			return .back
		} else if (input == [?]u8{27, 0, 91, 0, 67, 0}) {
			return .forward
		}
	}
	return .unk
}

_get_size :: proc(con: Console) -> (width: int, height: int) {
	screen_buffer_info: windows.CONSOLE_SCREEN_BUFFER_INFO
	windows.GetConsoleScreenBufferInfo(con.output_handle, &screen_buffer_info)
	width = int(screen_buffer_info.dwSize.X)
	height = int(screen_buffer_info.dwSize.Y)
	return
}