// +build windows

package winterm

import (
	"fmt"
	"syscall"
	"unsafe"
)

//===========================================================================================================
// IMPORTANT NOTE:
//
//	The methods below make extensive use of the "unsafe" package to obtain the required pointers.
//	Beginning in Go 1.3, the garbage collector may release local variables (e.g., incoming arguments, stack
//	variables) the pointers reference *before* the API completes.
//
//  As a result, in those cases, the code must hint that the variables remain in active by invoking the
//	dummy method "use" (see below). Newer versions of Go are planned to change the mechanism to no longer
//	require unsafe pointers.
//
//	If you add or modify methods, ENSURE protection of local variables through the "use" builtin to inform
//	the garbage collector the variables remain in use if:
//
//	-- The value is not a pointer (e.g., int32, struct)
//	-- The value is not referenced by the method after passing the pointer to Windows
//
//	See http://golang.org/doc/go1.3.
//===========================================================================================================

var (
	kernel32DLL = syscall.NewLazyDLL("kernel32.dll")

	getConsoleCursorInfoProc       = kernel32DLL.NewProc("GetConsoleCursorInfo")
	setConsoleCursorInfoProc       = kernel32DLL.NewProc("SetConsoleCursorInfo")
	setConsoleCursorPositionProc   = kernel32DLL.NewProc("SetConsoleCursorPosition")
	setConsoleModeProc             = kernel32DLL.NewProc("SetConsoleMode")
	getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo")
	setConsoleScreenBufferSizeProc = kernel32DLL.NewProc("SetConsoleScreenBufferSize")
	scrollConsoleScreenBufferProc  = kernel32DLL.NewProc("ScrollConsoleScreenBufferA")
	setConsoleTextAttributeProc    = kernel32DLL.NewProc("SetConsoleTextAttribute")
	setConsoleWindowInfoProc       = kernel32DLL.NewProc("SetConsoleWindowInfo")
	writeConsoleOutputProc         = kernel32DLL.NewProc("WriteConsoleOutputW")
	readConsoleInputProc           = kernel32DLL.NewProc("ReadConsoleInputW")
	waitForSingleObjectProc        = kernel32DLL.NewProc("WaitForSingleObject")
)

// Windows Console constants
const (
	// Console modes
	// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
	ENABLE_PROCESSED_INPUT = 0x0001
	ENABLE_LINE_INPUT      = 0x0002
	ENABLE_ECHO_INPUT      = 0x0004
	ENABLE_WINDOW_INPUT    = 0x0008
	ENABLE_MOUSE_INPUT     = 0x0010
	ENABLE_INSERT_MODE     = 0x0020
	ENABLE_QUICK_EDIT_MODE = 0x0040
	ENABLE_EXTENDED_FLAGS  = 0x0080

	ENABLE_PROCESSED_OUTPUT   = 0x0001
	ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002

	// Character attributes
	// Note:
	// -- The attributes are combined to produce various colors (e.g., Blue + Green will create Cyan).
	//    Clearing all foreground or background colors results in black; setting all creates white.
	// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx#_win32_character_attributes.
	FOREGROUND_BLUE      WORD = 0x0001
	FOREGROUND_GREEN     WORD = 0x0002
	FOREGROUND_RED       WORD = 0x0004
	FOREGROUND_INTENSITY WORD = 0x0008
	FOREGROUND_MASK      WORD = 0x000F

	BACKGROUND_BLUE      WORD = 0x0010
	BACKGROUND_GREEN     WORD = 0x0020
	BACKGROUND_RED       WORD = 0x0040
	BACKGROUND_INTENSITY WORD = 0x0080
	BACKGROUND_MASK      WORD = 0x00F0

	COMMON_LVB_MASK          WORD = 0xFF00
	COMMON_LVB_REVERSE_VIDEO WORD = 0x4000
	COMMON_LVB_UNDERSCORE    WORD = 0x8000

	// Input event types
	// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
	KEY_EVENT                = 0x0001
	MOUSE_EVENT              = 0x0002
	WINDOW_BUFFER_SIZE_EVENT = 0x0004
	MENU_EVENT               = 0x0008
	FOCUS_EVENT              = 0x0010

	// WaitForSingleObject return codes
	WAIT_ABANDONED = 0x00000080
	WAIT_FAILED    = 0xFFFFFFFF
	WAIT_SIGNALED  = 0x0000000
	WAIT_TIMEOUT   = 0x00000102

	// WaitForSingleObject wait duration
	WAIT_INFINITE       = 0xFFFFFFFF
	WAIT_ONE_SECOND     = 1000
	WAIT_HALF_SECOND    = 500
	WAIT_QUARTER_SECOND = 250
)

// Windows API Console types
// -- See https://msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx for core types (e.g., SHORT)
// -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682101(v=vs.85).aspx for Console specific types (e.g., COORD)
// -- See https://msdn.microsoft.com/en-us/library/aa296569(v=vs.60).aspx for comments on alignment
type (
	SHORT int16
	BOOL  int32
	WORD  uint16
	WCHAR uint16
	DWORD uint32

	CHAR_INFO struct {
		UnicodeChar WCHAR
		Attributes  WORD
	}

	CONSOLE_CURSOR_INFO struct {
		Size    DWORD
		Visible BOOL
	}

	CONSOLE_SCREEN_BUFFER_INFO struct {
		Size              COORD
		CursorPosition    COORD
		Attributes        WORD
		Window            SMALL_RECT
		MaximumWindowSize COORD
	}

	COORD struct {
		X SHORT
		Y SHORT
	}

	SMALL_RECT struct {
		Left   SHORT
		Top    SHORT
		Right  SHORT
		Bottom SHORT
	}

	// INPUT_RECORD is a C/C++ union of which KEY_EVENT_RECORD is one case, it is also the largest
	// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
	INPUT_RECORD struct {
		EventType WORD
		KeyEvent  KEY_EVENT_RECORD
	}

	KEY_EVENT_RECORD struct {
		KeyDown         BOOL
		RepeatCount     WORD
		VirtualKeyCode  WORD
		VirtualScanCode WORD
		UnicodeChar     WCHAR
		ControlKeyState DWORD
	}

	WINDOW_BUFFER_SIZE struct {
		Size COORD
	}
)

// boolToBOOL converts a Go bool into a Windows BOOL.
func boolToBOOL(f bool) BOOL {
	if f {
		return BOOL(1)
	} else {
		return BOOL(0)
	}
}

// GetConsoleCursorInfo retrieves information about the size and visiblity of the console cursor.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683163(v=vs.85).aspx.
func GetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error {
	r1, r2, err := getConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0)
	return checkError(r1, r2, err)
}

// SetConsoleCursorInfo sets the size and visiblity of the console cursor.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686019(v=vs.85).aspx.
func SetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error {
	r1, r2, err := setConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0)
	return checkError(r1, r2, err)
}

// SetConsoleCursorPosition location of the console cursor.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx.
func SetConsoleCursorPosition(handle uintptr, coord COORD) error {
	r1, r2, err := setConsoleCursorPositionProc.Call(handle, coordToPointer(coord))
	use(coord)
	return checkError(r1, r2, err)
}

// GetConsoleMode gets the console mode for given file descriptor
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx.
func GetConsoleMode(handle uintptr) (mode uint32, err error) {
	err = syscall.GetConsoleMode(syscall.Handle(handle), &mode)
	return mode, err
}

// SetConsoleMode sets the console mode for given file descriptor
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
func SetConsoleMode(handle uintptr, mode uint32) error {
	r1, r2, err := setConsoleModeProc.Call(handle, uintptr(mode), 0)
	use(mode)
	return checkError(r1, r2, err)
}

// GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer.
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx.
func GetConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) {
	info := CONSOLE_SCREEN_BUFFER_INFO{}
	err := checkError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0))
	if err != nil {
		return nil, err
	}
	return &info, nil
}

func ScrollConsoleScreenBuffer(handle uintptr, scrollRect SMALL_RECT, clipRect SMALL_RECT, destOrigin COORD, char CHAR_INFO) error {
	r1, r2, err := scrollConsoleScreenBufferProc.Call(handle, uintptr(unsafe.Pointer(&scrollRect)), uintptr(unsafe.Pointer(&clipRect)), coordToPointer(destOrigin), uintptr(unsafe.Pointer(&char)))
	use(scrollRect)
	use(clipRect)
	use(destOrigin)
	use(char)
	return checkError(r1, r2, err)
}

// SetConsoleScreenBufferSize sets the size of the console screen buffer.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686044(v=vs.85).aspx.
func SetConsoleScreenBufferSize(handle uintptr, coord COORD) error {
	r1, r2, err := setConsoleScreenBufferSizeProc.Call(handle, coordToPointer(coord))
	use(coord)
	return checkError(r1, r2, err)
}

// SetConsoleTextAttribute sets the attributes of characters written to the
// console screen buffer by the WriteFile or WriteConsole function.
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686047(v=vs.85).aspx.
func SetConsoleTextAttribute(handle uintptr, attribute WORD) error {
	r1, r2, err := setConsoleTextAttributeProc.Call(handle, uintptr(attribute), 0)
	use(attribute)
	return checkError(r1, r2, err)
}

// SetConsoleWindowInfo sets the size and position of the console screen buffer's window.
// Note that the size and location must be within and no larger than the backing console screen buffer.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686125(v=vs.85).aspx.
func SetConsoleWindowInfo(handle uintptr, isAbsolute bool, rect SMALL_RECT) error {
	r1, r2, err := setConsoleWindowInfoProc.Call(handle, uintptr(boolToBOOL(isAbsolute)), uintptr(unsafe.Pointer(&rect)))
	use(isAbsolute)
	use(rect)
	return checkError(r1, r2, err)
}

// WriteConsoleOutput writes the CHAR_INFOs from the provided buffer to the active console buffer.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687404(v=vs.85).aspx.
func WriteConsoleOutput(handle uintptr, buffer []CHAR_INFO, bufferSize COORD, bufferCoord COORD, writeRegion *SMALL_RECT) error {
	r1, r2, err := writeConsoleOutputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), coordToPointer(bufferSize), coordToPointer(bufferCoord), uintptr(unsafe.Pointer(writeRegion)))
	use(buffer)
	use(bufferSize)
	use(bufferCoord)
	return checkError(r1, r2, err)
}

// ReadConsoleInput reads (and removes) data from the console input buffer.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx.
func ReadConsoleInput(handle uintptr, buffer []INPUT_RECORD, count *uint32) error {
	r1, r2, err := readConsoleInputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer)), uintptr(unsafe.Pointer(count)))
	use(buffer)
	return checkError(r1, r2, err)
}

// WaitForSingleObject waits for the passed handle to be signaled.
// It returns true if the handle was signaled; false otherwise.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx.
func WaitForSingleObject(handle uintptr, msWait uint32) (bool, error) {
	r1, _, err := waitForSingleObjectProc.Call(handle, uintptr(DWORD(msWait)))
	switch r1 {
	case WAIT_ABANDONED, WAIT_TIMEOUT:
		return false, nil
	case WAIT_SIGNALED:
		return true, nil
	}
	use(msWait)
	return false, err
}

// String helpers
func (info CONSOLE_SCREEN_BUFFER_INFO) String() string {
	return fmt.Sprintf("Size(%v) Cursor(%v) Window(%v) Max(%v)", info.Size, info.CursorPosition, info.Window, info.MaximumWindowSize)
}

func (coord COORD) String() string {
	return fmt.Sprintf("%v,%v", coord.X, coord.Y)
}

func (rect SMALL_RECT) String() string {
	return fmt.Sprintf("(%v,%v),(%v,%v)", rect.Left, rect.Top, rect.Right, rect.Bottom)
}

// checkError evaluates the results of a Windows API call and returns the error if it failed.
func checkError(r1, r2 uintptr, err error) error {
	// Windows APIs return non-zero to indicate success
	if r1 != 0 {
		return nil
	}

	// Return the error if provided, otherwise default to EINVAL
	if err != nil {
		return err
	}
	return syscall.EINVAL
}

// coordToPointer converts a COORD into a uintptr (by fooling the type system).
func coordToPointer(c COORD) uintptr {
	// Note: This code assumes the two SHORTs are correctly laid out; the "cast" to DWORD is just to get a pointer to pass.
	return uintptr(*((*DWORD)(unsafe.Pointer(&c))))
}

// use is a no-op, but the compiler cannot see that it is.
// Calling use(p) ensures that p is kept live until that point.
func use(p interface{}) {}
