package coninput

import (
	"encoding/binary"
	"fmt"
	"strconv"
	"strings"
)

const (
	maxEventSize     = 16
	wordPaddingBytes = 2
)

// EventType denots the type of an event
type EventType uint16

// EventUnion is the union data type that contains the data for any event.
type EventUnion [maxEventSize]byte

// InputRecord corresponds to the INPUT_RECORD structure from the Windows
// console API (see
// https://docs.microsoft.com/en-us/windows/console/input-record-str).
type InputRecord struct {
	// EventType specifies the type of event that helt in Event.
	EventType EventType

	// Padding of the 16-bit EventType to a whole 32-bit dword.
	_ [wordPaddingBytes]byte

	// Event holds the actual event data. Use Unrap to access it as its
	// respective event type.
	Event EventUnion
}

// String implements fmt.Stringer for InputRecord.
func (ir InputRecord) String() string {
	return ir.Unwrap().String()
}

// Unwrap parses the event data into an EventRecord of the respective event
// type. The data in the returned EventRecord does not contain any references to
// the passed InputRecord.
func (ir InputRecord) Unwrap() EventRecord {
	switch ir.EventType {
	case FocusEventType:
		return FocusEventRecord{SetFocus: ir.Event[0] > 0}
	case KeyEventType:
		return KeyEventRecord{
			KeyDown:         binary.LittleEndian.Uint32(ir.Event[0:4]) > 0,
			RepeatCount:     binary.LittleEndian.Uint16(ir.Event[4:6]),
			VirtualKeyCode:  VirtualKeyCode(binary.LittleEndian.Uint16(ir.Event[6:8])),
			VirtualScanCode: VirtualKeyCode(binary.LittleEndian.Uint16(ir.Event[8:10])),
			Char:            rune(binary.LittleEndian.Uint16(ir.Event[10:12])),
			ControlKeyState: ControlKeyState(binary.LittleEndian.Uint32(ir.Event[12:16])),
		}
	case MouseEventType:
		m := MouseEventRecord{
			MousePositon: Coord{
				X: binary.LittleEndian.Uint16(ir.Event[0:2]),
				Y: binary.LittleEndian.Uint16(ir.Event[2:4]),
			},
			ButtonState:     ButtonState(binary.LittleEndian.Uint32(ir.Event[4:8])),
			ControlKeyState: ControlKeyState(binary.LittleEndian.Uint32(ir.Event[8:12])),
			EventFlags:      EventFlags(binary.LittleEndian.Uint32(ir.Event[12:16])),
		}

		if (m.EventFlags&MOUSE_WHEELED > 0) || (m.EventFlags&MOUSE_HWHEELED > 0) {
			if int16(highWord(uint32(m.ButtonState))) > 0 {
				m.WheelDirection = 1
			} else {
				m.WheelDirection = -1
			}
		}

		return m
	case WindowBufferSizeEventType:
		return WindowBufferSizeEventRecord{
			Size: Coord{
				X: binary.LittleEndian.Uint16(ir.Event[0:2]),
				Y: binary.LittleEndian.Uint16(ir.Event[2:4]),
			},
		}
	case MenuEventType:
		return MenuEventRecord{
			CommandID: binary.LittleEndian.Uint32(ir.Event[0:4]),
		}
	default:
		return &UnknownEvent{InputRecord: ir}
	}
}

// EventRecord represents one of the following event types:
// TypeFocusEventRecord, TypeKeyEventRecord, TypeMouseEventRecord,
// TypeWindowBufferSizeEvent, TypeMenuEventRecord and UnknownEvent.
type EventRecord interface {
	Type() string
	fmt.Stringer
}

// FocusEventType is the event type for a FocusEventRecord (see
// https://docs.microsoft.com/en-us/windows/console/input-record-str).
const FocusEventType EventType = 0x0010

// FocusEventRecord represent the FOCUS_EVENT_RECORD structure from the Windows
// console API (see
// https://docs.microsoft.com/en-us/windows/console/focus-event-record-str).
// These events are used internally by the Windows console API and should be
// ignored.
type FocusEventRecord struct {
	// SetFocus is reserved and should not be used.
	SetFocus bool
}

// Ensure that FocusEventRecord satisfies EventRecord interface.
var _ EventRecord = FocusEventRecord{}

// Type ensures that FocusEventRecord satisfies EventRecord interface.
func (e FocusEventRecord) Type() string { return "FocusEvent" }

// String ensures that FocusEventRecord satisfies EventRecord and fmt.Stringer
// interfaces.
func (e FocusEventRecord) String() string { return fmt.Sprintf("%s[%v]", e.Type(), e.SetFocus) }

// KeyEventType is the event type for a KeyEventRecord (see
// https://docs.microsoft.com/en-us/windows/console/input-record-str).
const KeyEventType EventType = 0x0001

// KeyEventRecord represent the KEY_EVENT_RECORD structure from the Windows
// console API (see
// https://docs.microsoft.com/en-us/windows/console/key-event-record-str).
type KeyEventRecord struct {
	// KeyDown specified whether the key is pressed or released.
	KeyDown bool

	//  RepeatCount indicates that a key is being held down. For example, when a
	//  key is held down, five events with RepeatCount equal to 1 may be
	//  generated, one event with RepeatCount equal to 5, or multiple events
	//  with RepeatCount greater than or equal to 1.
	RepeatCount uint16

	// VirtualKeyCode identifies the given key in a device-independent manner
	// (see
	// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes).
	VirtualKeyCode VirtualKeyCode

	//  VirtualScanCode represents the device-dependent value generated by the
	//  keyboard hardware.
	VirtualScanCode VirtualKeyCode

	// Char is the character that corresponds to the pressed key. Char can be
	// zero for some keys.
	Char rune

	//ControlKeyState holds the state of the control keys.
	ControlKeyState ControlKeyState
}

// Ensure that KeyEventRecord satisfies EventRecord interface.
var _ EventRecord = KeyEventRecord{}

// Type ensures that KeyEventRecord satisfies EventRecord interface.
func (e KeyEventRecord) Type() string { return "KeyEvent" }

// String ensures that KeyEventRecord satisfies EventRecord and fmt.Stringer
// interfaces.
func (e KeyEventRecord) String() string {
	infos := []string{}

	repeat := ""
	if e.RepeatCount > 1 {
		repeat = "x" + strconv.Itoa(int(e.RepeatCount))
	}

	infos = append(infos, fmt.Sprintf("%q%s", e.Char, repeat))

	direction := "up"
	if e.KeyDown {
		direction = "down"
	}

	infos = append(infos, direction)

	if e.ControlKeyState != NO_CONTROL_KEY {
		infos = append(infos, e.ControlKeyState.String())
	}

	infos = append(infos, fmt.Sprintf("KeyCode: %d", e.VirtualKeyCode))
	infos = append(infos, fmt.Sprintf("ScanCode: %d", e.VirtualScanCode))

	return fmt.Sprintf("%s[%s]", e.Type(), strings.Join(infos, ", "))
}

// MenuEventType is the event type for a MenuEventRecord (see
// https://docs.microsoft.com/en-us/windows/console/input-record-str).
const MenuEventType EventType = 0x0008

// MenuEventRecord represent the MENU_EVENT_RECORD structure from the Windows
// console API (see
// https://docs.microsoft.com/en-us/windows/console/menu-event-record-str).
// These events are deprecated by the Windows console API and should be ignored.
type MenuEventRecord struct {
	CommandID uint32
}

// Ensure that MenuEventRecord satisfies EventRecord interface.
var _ EventRecord = MenuEventRecord{}

// Type ensures that MenuEventRecord satisfies EventRecord interface.
func (e MenuEventRecord) Type() string { return "MenuEvent" }

// String ensures that MenuEventRecord satisfies EventRecord and fmt.Stringer
// interfaces.
func (e MenuEventRecord) String() string { return fmt.Sprintf("MenuEvent[%d]", e.CommandID) }

// MouseEventType is the event type for a MouseEventRecord (see
// https://docs.microsoft.com/en-us/windows/console/input-record-str).
const MouseEventType EventType = 0x0002

// MouseEventRecord represent the MOUSE_EVENT_RECORD structure from the Windows
// console API (see
// https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str).
type MouseEventRecord struct {
	// MousePosition contains the location of the cursor, in terms of the
	// console screen buffer's character-cell coordinates.
	MousePositon Coord

	// ButtonState holds the status of the mouse buttons.
	ButtonState ButtonState

	// ControlKeyState holds the state of the control keys.
	ControlKeyState ControlKeyState

	// EventFlags specify tge type of mouse event.
	EventFlags EventFlags

	// WheelDirection specified the direction in which the mouse wheel is
	// spinning when EventFlags contains MOUSE_HWHEELED or MOUSE_WHEELED. When
	// the event flags specify MOUSE_WHEELED it is 1 if the wheel rotated
	// forward (away from the user) or -1 when it rotates backwards. When
	// MOUSE_HWHEELED is specified it is 1 when the wheel rotates right and -1
	// when it rotates left. When the EventFlags do not indicate a mouse wheel
	// event it is 0.
	WheelDirection int
}

// Ensure that MouseEventRecord satisfies EventRecord interface.
var _ EventRecord = MouseEventRecord{}

func (e MouseEventRecord) WheelDirectionName() string {
	if e.EventFlags&MOUSE_WHEELED > 0 {
		if e.WheelDirection > 0 {
			return "Forward"
		}

		return "Backward"
	} else if e.EventFlags&MOUSE_HWHEELED > 0 {
		if e.WheelDirection > 0 {
			return "Right"
		}

		return "Left"
	}

	return ""
}

// Type ensures that MouseEventRecord satisfies EventRecord interface.
func (e MouseEventRecord) Type() string { return "MouseEvent" }

// String ensures that MouseEventRecord satisfies EventRecord and fmt.Stringer
// interfaces.
func (e MouseEventRecord) String() string {
	infos := []string{e.MousePositon.String()}

	if e.ButtonState&0xFF != 0 {
		infos = append(infos, e.ButtonState.String())
	}

	eventDescription := e.EventFlags.String()

	wheelDirection := e.WheelDirectionName()
	if wheelDirection != "" {
		eventDescription += "(" + wheelDirection + ")"
	}

	infos = append(infos, eventDescription)

	if e.ControlKeyState != NO_CONTROL_KEY {
		infos = append(infos, e.ControlKeyState.String())
	}

	return fmt.Sprintf("%s[%s]", e.Type(), strings.Join(infos, ", "))
}

// WindowBufferSizeEventType is the event type for a WindowBufferSizeEventRecord
// (see https://docs.microsoft.com/en-us/windows/console/input-record-str).
const WindowBufferSizeEventType EventType = 0x0004

// WindowBufferSizeEventRecord represent the WINDOW_BUFFER_SIZE_RECORD structure
// from the Windows console API (see
// https://docs.microsoft.com/en-us/windows/console/window-buffer-size-record-str).
type WindowBufferSizeEventRecord struct {
	// Size contains the size of the console screen buffer, in character cell columns and rows.
	Size Coord
}

// Ensure that WindowBufferSizeEventRecord satisfies EventRecord interface.
var _ EventRecord = WindowBufferSizeEventRecord{}

// Type ensures that WindowBufferSizeEventRecord satisfies EventRecord interface.
func (e WindowBufferSizeEventRecord) Type() string { return "WindowBufferSizeEvent" }

// String ensures that WindowBufferSizeEventRecord satisfies EventRecord and fmt.Stringer
// interfaces.
func (e WindowBufferSizeEventRecord) String() string {
	return fmt.Sprintf("WindowBufferSizeEvent[%s]", e.Size)
}

// UnknownEvent is generated when the event type does not match one of the
// following types: TypeFocusEventRecord, TypeKeyEventRecord,
// TypeMouseEventRecord, TypeWindowBufferSizeEvent, TypeMenuEventRecord and
// UnknownEvent.
type UnknownEvent struct {
	InputRecord
}

// Ensure that UnknownEvent satisfies EventRecord interface.
var _ EventRecord = UnknownEvent{}

// Type ensures that UnknownEvent satisfies EventRecord interface.
func (e UnknownEvent) Type() string { return "UnknownEvent" }

// String ensures that UnknownEvent satisfies EventRecord and fmt.Stringer
// interfaces.
func (e UnknownEvent) String() string {
	return fmt.Sprintf("%s[Type: %d, Data: %v]", e.Type(), e.InputRecord.EventType, e.InputRecord.Event[:])
}

// Coord represent the COORD structure from the Windows
// console API (see https://docs.microsoft.com/en-us/windows/console/coord-str).
type Coord struct {
	// X is the horizontal coordinate or column value. The units depend on the function call.
	X uint16
	// Y is the vertical coordinate or row value. The units depend on the function call.
	Y uint16
}

// String ensures that Coord satisfies the fmt.Stringer interface.
func (c Coord) String() string {
	return fmt.Sprintf("(%d, %d)", c.X, c.Y)
}

// ButtonState holds the state of the mouse buttons (see
// https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str).
type ButtonState uint32

func (bs ButtonState) Contains(state ButtonState) bool {
	return bs&state > 0
}

// String ensures that ButtonState satisfies the fmt.Stringer interface.
func (bs ButtonState) String() string {
	switch {
	case bs&FROM_LEFT_1ST_BUTTON_PRESSED > 0:
		return "Left"
	case bs&FROM_LEFT_2ND_BUTTON_PRESSED > 0:
		return "2"
	case bs&FROM_LEFT_3RD_BUTTON_PRESSED > 0:
		return "3"
	case bs&FROM_LEFT_4TH_BUTTON_PRESSED > 0:
		return "4"
	case bs&RIGHTMOST_BUTTON_PRESSED > 0:
		return "Right"
	case bs&0xFF == 0:
		return "No Button"
	default:
		return fmt.Sprintf("Unknown(%d)", bs)
	}
}

func (bs ButtonState) IsReleased() bool {
	return bs&0xff > 0
}

// Valid values for ButtonState.
const (
	FROM_LEFT_1ST_BUTTON_PRESSED ButtonState = 0x0001
	RIGHTMOST_BUTTON_PRESSED     ButtonState = 0x0002
	FROM_LEFT_2ND_BUTTON_PRESSED ButtonState = 0x0004
	FROM_LEFT_3RD_BUTTON_PRESSED ButtonState = 0x0008
	FROM_LEFT_4TH_BUTTON_PRESSED ButtonState = 0x0010
)

// ControlKeyState holds the state of the control keys for key and mouse events
// (see https://docs.microsoft.com/en-us/windows/console/key-event-record-str
// and https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str).
type ControlKeyState uint32

func (cks ControlKeyState) Contains(state ControlKeyState) bool {
	return cks&state > 0
}

// Valid values for ControlKeyState.
const (
	CAPSLOCK_ON        ControlKeyState = 0x0080
	ENHANCED_KEY       ControlKeyState = 0x0100
	LEFT_ALT_PRESSED   ControlKeyState = 0x0002
	LEFT_CTRL_PRESSED  ControlKeyState = 0x0008
	NUMLOCK_ON         ControlKeyState = 0x0020
	RIGHT_ALT_PRESSED  ControlKeyState = 0x0001
	RIGHT_CTRL_PRESSED ControlKeyState = 0x0004
	SCROLLLOCK_ON      ControlKeyState = 0x0040
	SHIFT_PRESSED      ControlKeyState = 0x0010
	NO_CONTROL_KEY     ControlKeyState = 0x0000
)

// String ensures that ControlKeyState satisfies the fmt.Stringer interface.
func (cks ControlKeyState) String() string {
	controlKeys := []string{}

	switch {
	case cks&CAPSLOCK_ON > 0:
		controlKeys = append(controlKeys, "CapsLock")
	case cks&ENHANCED_KEY > 0:
		controlKeys = append(controlKeys, "Enhanced")
	case cks&LEFT_ALT_PRESSED > 0:
		controlKeys = append(controlKeys, "Alt")
	case cks&LEFT_CTRL_PRESSED > 0:
		controlKeys = append(controlKeys, "CTRL")
	case cks&NUMLOCK_ON > 0:
		controlKeys = append(controlKeys, "NumLock")
	case cks&RIGHT_ALT_PRESSED > 0:
		controlKeys = append(controlKeys, "RightAlt")
	case cks&RIGHT_CTRL_PRESSED > 0:
		controlKeys = append(controlKeys, "RightCTRL")
	case cks&SCROLLLOCK_ON > 0:
		controlKeys = append(controlKeys, "ScrollLock")
	case cks&SHIFT_PRESSED > 0:
		controlKeys = append(controlKeys, "Shift")
	case cks == NO_CONTROL_KEY:
	default:
		return fmt.Sprintf("Unknown(%d)", cks)
	}

	return strings.Join(controlKeys, ",")
}

// EventFlags specifies the type of a mouse event (see
// https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str).
type EventFlags uint32

// String ensures that EventFlags satisfies the fmt.Stringer interface.
func (ef EventFlags) String() string {
	switch {
	case ef&DOUBLE_CLICK > 0:
		return "DoubleClick"
	case ef&MOUSE_WHEELED > 0:
		return "Wheeled"
	case ef&MOUSE_MOVED > 0:
		return "Moved"
	case ef&MOUSE_HWHEELED > 0:
		return "HWheeld"
	case ef == CLICK:
		return "Click"
	default:
		return fmt.Sprintf("Unknown(%d)", ef)
	}
}

func (ef EventFlags) Contains(flag EventFlags) bool {
	return ef&flag > 0
}

// Valid values for EventFlags.
const (
	CLICK          EventFlags = 0x0000
	MOUSE_MOVED    EventFlags = 0x0001
	DOUBLE_CLICK   EventFlags = 0x0002
	MOUSE_WHEELED  EventFlags = 0x0004
	MOUSE_HWHEELED EventFlags = 0x0008
)

func highWord(data uint32) uint16 {
	return uint16((data & 0xFFFF0000) >> 16)
}
