package termbox

import "syscall"
import "unsafe"
import "unicode/utf16"
import "github.com/mattn/go-runewidth"

type (
	wchar     uint16
	short     int16
	dword     uint32
	word      uint16
	char_info struct {
		char wchar
		attr word
	}
	coord struct {
		x short
		y short
	}
	small_rect struct {
		left   short
		top    short
		right  short
		bottom short
	}
	console_screen_buffer_info struct {
		size                coord
		cursor_position     coord
		attributes          word
		window              small_rect
		maximum_window_size coord
	}
	console_cursor_info struct {
		size    dword
		visible int32
	}
	input_record struct {
		event_type word
		_          [2]byte
		event      [16]byte
	}
	key_event_record struct {
		key_down          int32
		repeat_count      word
		virtual_key_code  word
		virtual_scan_code word
		unicode_char      wchar
		control_key_state dword
	}
	window_buffer_size_record struct {
		size coord
	}
	mouse_event_record struct {
		mouse_pos         coord
		button_state      dword
		control_key_state dword
		event_flags       dword
	}
)

const (
	mouse_lmb = 0x1
	mouse_rmb = 0x2
	mouse_mmb = 0x4 | 0x8 | 0x10
)

func (this coord) uintptr() uintptr {
	return uintptr(*(*int32)(unsafe.Pointer(&this)))
}

var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var is_cjk = runewidth.IsEastAsian()

var (
	proc_set_console_active_screen_buffer = kernel32.NewProc("SetConsoleActiveScreenBuffer")
	proc_set_console_screen_buffer_size   = kernel32.NewProc("SetConsoleScreenBufferSize")
	proc_create_console_screen_buffer     = kernel32.NewProc("CreateConsoleScreenBuffer")
	proc_get_console_screen_buffer_info   = kernel32.NewProc("GetConsoleScreenBufferInfo")
	proc_write_console_output             = kernel32.NewProc("WriteConsoleOutputW")
	proc_write_console_output_character   = kernel32.NewProc("WriteConsoleOutputCharacterW")
	proc_write_console_output_attribute   = kernel32.NewProc("WriteConsoleOutputAttribute")
	proc_set_console_cursor_info          = kernel32.NewProc("SetConsoleCursorInfo")
	proc_set_console_cursor_position      = kernel32.NewProc("SetConsoleCursorPosition")
	proc_get_console_cursor_info          = kernel32.NewProc("GetConsoleCursorInfo")
	proc_read_console_input               = kernel32.NewProc("ReadConsoleInputW")
	proc_get_console_mode                 = kernel32.NewProc("GetConsoleMode")
	proc_set_console_mode                 = kernel32.NewProc("SetConsoleMode")
	proc_fill_console_output_character    = kernel32.NewProc("FillConsoleOutputCharacterW")
	proc_fill_console_output_attribute    = kernel32.NewProc("FillConsoleOutputAttribute")
	proc_create_event                     = kernel32.NewProc("CreateEventW")
	proc_wait_for_multiple_objects        = kernel32.NewProc("WaitForMultipleObjects")
	proc_set_event                        = kernel32.NewProc("SetEvent")
)

func set_console_active_screen_buffer(h syscall.Handle) (err error) {
	r0, _, e1 := syscall.Syscall(proc_set_console_active_screen_buffer.Addr(),
		1, uintptr(h), 0, 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func set_console_screen_buffer_size(h syscall.Handle, size coord) (err error) {
	r0, _, e1 := syscall.Syscall(proc_set_console_screen_buffer_size.Addr(),
		2, uintptr(h), size.uintptr(), 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func create_console_screen_buffer() (h syscall.Handle, err error) {
	r0, _, e1 := syscall.Syscall6(proc_create_console_screen_buffer.Addr(),
		5, uintptr(generic_read|generic_write), 0, 0, console_textmode_buffer, 0, 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return syscall.Handle(r0), nil
}

func get_console_screen_buffer_info(h syscall.Handle, info *console_screen_buffer_info) (err error) {
	r0, _, e1 := syscall.Syscall(proc_get_console_screen_buffer_info.Addr(),
		2, uintptr(h), uintptr(unsafe.Pointer(info)), 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func write_console_output(h syscall.Handle, chars []char_info, dst small_rect) (err error) {
	tmp_coord = coord{dst.right - dst.left + 1, dst.bottom - dst.top + 1}
	tmp_rect = dst
	r0, _, e1 := syscall.Syscall6(proc_write_console_output.Addr(),
		5, uintptr(h), uintptr(unsafe.Pointer(&chars[0])), tmp_coord.uintptr(),
		tmp_coord0.uintptr(), uintptr(unsafe.Pointer(&tmp_rect)), 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func write_console_output_character(h syscall.Handle, chars []wchar, pos coord) (err error) {
	r0, _, e1 := syscall.Syscall6(proc_write_console_output_character.Addr(),
		5, uintptr(h), uintptr(unsafe.Pointer(&chars[0])), uintptr(len(chars)),
		pos.uintptr(), uintptr(unsafe.Pointer(&tmp_arg)), 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func write_console_output_attribute(h syscall.Handle, attrs []word, pos coord) (err error) {
	r0, _, e1 := syscall.Syscall6(proc_write_console_output_attribute.Addr(),
		5, uintptr(h), uintptr(unsafe.Pointer(&attrs[0])), uintptr(len(attrs)),
		pos.uintptr(), uintptr(unsafe.Pointer(&tmp_arg)), 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func set_console_cursor_info(h syscall.Handle, info *console_cursor_info) (err error) {
	r0, _, e1 := syscall.Syscall(proc_set_console_cursor_info.Addr(),
		2, uintptr(h), uintptr(unsafe.Pointer(info)), 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func get_console_cursor_info(h syscall.Handle, info *console_cursor_info) (err error) {
	r0, _, e1 := syscall.Syscall(proc_get_console_cursor_info.Addr(),
		2, uintptr(h), uintptr(unsafe.Pointer(info)), 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func set_console_cursor_position(h syscall.Handle, pos coord) (err error) {
	r0, _, e1 := syscall.Syscall(proc_set_console_cursor_position.Addr(),
		2, uintptr(h), pos.uintptr(), 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func read_console_input(h syscall.Handle, record *input_record) (err error) {
	r0, _, e1 := syscall.Syscall6(proc_read_console_input.Addr(),
		4, uintptr(h), uintptr(unsafe.Pointer(record)), 1, uintptr(unsafe.Pointer(&tmp_arg)), 0, 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func get_console_mode(h syscall.Handle, mode *dword) (err error) {
	r0, _, e1 := syscall.Syscall(proc_get_console_mode.Addr(),
		2, uintptr(h), uintptr(unsafe.Pointer(mode)), 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func set_console_mode(h syscall.Handle, mode dword) (err error) {
	r0, _, e1 := syscall.Syscall(proc_set_console_mode.Addr(),
		2, uintptr(h), uintptr(mode), 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func fill_console_output_character(h syscall.Handle, char wchar, n int) (err error) {
	r0, _, e1 := syscall.Syscall6(proc_fill_console_output_character.Addr(),
		5, uintptr(h), uintptr(char), uintptr(n), tmp_coord.uintptr(),
		uintptr(unsafe.Pointer(&tmp_arg)), 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func fill_console_output_attribute(h syscall.Handle, attr word, n int) (err error) {
	r0, _, e1 := syscall.Syscall6(proc_fill_console_output_attribute.Addr(),
		5, uintptr(h), uintptr(attr), uintptr(n), tmp_coord.uintptr(),
		uintptr(unsafe.Pointer(&tmp_arg)), 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func create_event() (out syscall.Handle, err error) {
	r0, _, e1 := syscall.Syscall6(proc_create_event.Addr(),
		4, 0, 0, 0, 0, 0, 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return syscall.Handle(r0), nil
}

func wait_for_multiple_objects(objects []syscall.Handle) (err error) {
	r0, _, e1 := syscall.Syscall6(proc_wait_for_multiple_objects.Addr(),
		4, uintptr(len(objects)), uintptr(unsafe.Pointer(&objects[0])),
		0, 0xFFFFFFFF, 0, 0)
	if uint32(r0) == 0xFFFFFFFF {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

func set_event(ev syscall.Handle) (err error) {
	r0, _, e1 := syscall.Syscall(proc_set_event.Addr(),
		1, uintptr(ev), 0, 0)
	if int(r0) == 0 {
		if e1 != 0 {
			err = error(e1)
		} else {
			err = syscall.EINVAL
		}
	}
	return
}

type diff_msg struct {
	pos   short
	lines short
	chars []char_info
}

type input_event struct {
	event Event
	err   error
}

var (
	orig_cursor_info console_cursor_info
	orig_size        coord
	orig_mode        dword
	orig_screen      syscall.Handle
	back_buffer      cellbuf
	front_buffer     cellbuf
	term_size        coord
	input_mode       = InputEsc
	cursor_x         = cursor_hidden
	cursor_y         = cursor_hidden
	foreground       = ColorDefault
	background       = ColorDefault
	in               syscall.Handle
	out              syscall.Handle
	interrupt        syscall.Handle
	charbuf          []char_info
	diffbuf          []diff_msg
	beg_x            = -1
	beg_y            = -1
	beg_i            = -1
	input_comm       = make(chan Event)
	interrupt_comm   = make(chan struct{})
	cancel_comm      = make(chan bool, 1)
	cancel_done_comm = make(chan bool)
	alt_mode_esc     = false

	// these ones just to prevent heap allocs at all costs
	tmp_info   console_screen_buffer_info
	tmp_arg    dword
	tmp_coord0 = coord{0, 0}
	tmp_coord  = coord{0, 0}
	tmp_rect   = small_rect{0, 0, 0, 0}
)

func get_cursor_position(out syscall.Handle) coord {
	err := get_console_screen_buffer_info(out, &tmp_info)
	if err != nil {
		panic(err)
	}
	return tmp_info.cursor_position
}

func get_term_size(out syscall.Handle) coord {
	err := get_console_screen_buffer_info(out, &tmp_info)
	if err != nil {
		panic(err)
	}
	return tmp_info.size
}

func get_win_size(out syscall.Handle) coord {
	err := get_console_screen_buffer_info(out, &tmp_info)
	if err != nil {
		panic(err)
	}
	return coord{
		x: tmp_info.window.right - tmp_info.window.left + 1,
		y: tmp_info.window.bottom - tmp_info.window.top + 1,
	}
}

func update_size_maybe() {
	size := get_term_size(out)
	if size.x != term_size.x || size.y != term_size.y {
		term_size = size
		back_buffer.resize(int(size.x), int(size.y))
		front_buffer.resize(int(size.x), int(size.y))
		front_buffer.clear()
		clear()

		area := int(size.x) * int(size.y)
		if cap(charbuf) < area {
			charbuf = make([]char_info, 0, area)
		}
	}
}

var color_table_bg = []word{
	0, // default (black)
	0, // black
	background_red,
	background_green,
	background_red | background_green, // yellow
	background_blue,
	background_red | background_blue,                    // magenta
	background_green | background_blue,                  // cyan
	background_red | background_blue | background_green, // white
}

var color_table_fg = []word{
	foreground_red | foreground_blue | foreground_green, // default (white)
	0,
	foreground_red,
	foreground_green,
	foreground_red | foreground_green, // yellow
	foreground_blue,
	foreground_red | foreground_blue,                    // magenta
	foreground_green | foreground_blue,                  // cyan
	foreground_red | foreground_blue | foreground_green, // white
}

const (
	replacement_char = '\uFFFD'
	max_rune         = '\U0010FFFF'
	surr1            = 0xd800
	surr2            = 0xdc00
	surr3            = 0xe000
	surr_self        = 0x10000
)

func append_diff_line(y int) int {
	n := 0
	for x := 0; x < front_buffer.width; {
		cell_offset := y*front_buffer.width + x
		back := &back_buffer.cells[cell_offset]
		front := &front_buffer.cells[cell_offset]
		attr, char := cell_to_char_info(*back)
		charbuf = append(charbuf, char_info{attr: attr, char: char[0]})
		*front = *back
		n++
		w := runewidth.RuneWidth(back.Ch)
		if w == 0 || w == 2 && runewidth.IsAmbiguousWidth(back.Ch) {
			w = 1
		}
		x += w
		// If not CJK, fill trailing space with whitespace
		if !is_cjk && w == 2 {
			charbuf = append(charbuf, char_info{attr: attr, char: ' '})
		}
	}
	return n
}

// compares 'back_buffer' with 'front_buffer' and prepares all changes in the form of
// 'diff_msg's in the 'diff_buf'
func prepare_diff_messages() {
	// clear buffers
	diffbuf = diffbuf[:0]
	charbuf = charbuf[:0]

	var diff diff_msg
	gbeg := 0
	for y := 0; y < front_buffer.height; y++ {
		same := true
		line_offset := y * front_buffer.width
		for x := 0; x < front_buffer.width; x++ {
			cell_offset := line_offset + x
			back := &back_buffer.cells[cell_offset]
			front := &front_buffer.cells[cell_offset]
			if *back != *front {
				same = false
				break
			}
		}
		if same && diff.lines > 0 {
			diffbuf = append(diffbuf, diff)
			diff = diff_msg{}
		}
		if !same {
			beg := len(charbuf)
			end := beg + append_diff_line(y)
			if diff.lines == 0 {
				diff.pos = short(y)
				gbeg = beg
			}
			diff.lines++
			diff.chars = charbuf[gbeg:end]
		}
	}
	if diff.lines > 0 {
		diffbuf = append(diffbuf, diff)
		diff = diff_msg{}
	}
}

func cell_to_char_info(c Cell) (attr word, wc [2]wchar) {
	attr = color_table_fg[c.Fg&0x0F] | color_table_bg[c.Bg&0x0F]
	if c.Fg&AttrReverse|c.Bg&AttrReverse != 0 {
		attr = (attr&0xF0)>>4 | (attr&0x0F)<<4
	}
	if c.Fg&AttrBold != 0 {
		attr |= foreground_intensity
	}
	if c.Bg&AttrBold != 0 {
		attr |= background_intensity
	}

	r0, r1 := utf16.EncodeRune(c.Ch)
	if r0 == 0xFFFD {
		wc[0] = wchar(c.Ch)
		wc[1] = ' '
	} else {
		wc[0] = wchar(r0)
		wc[1] = wchar(r1)
	}
	return
}

func move_cursor(x, y int) {
	err := set_console_cursor_position(out, coord{short(x), short(y)})
	if err != nil {
		panic(err)
	}
}

func show_cursor(visible bool) {
	var v int32
	if visible {
		v = 1
	}

	var info console_cursor_info
	info.size = 100
	info.visible = v
	err := set_console_cursor_info(out, &info)
	if err != nil {
		panic(err)
	}
}

func clear() {
	var err error
	attr, char := cell_to_char_info(Cell{
		' ',
		foreground,
		background,
	})

	area := int(term_size.x) * int(term_size.y)
	err = fill_console_output_attribute(out, attr, area)
	if err != nil {
		panic(err)
	}
	err = fill_console_output_character(out, char[0], area)
	if err != nil {
		panic(err)
	}
	if !is_cursor_hidden(cursor_x, cursor_y) {
		move_cursor(cursor_x, cursor_y)
	}
}

func key_event_record_to_event(r *key_event_record) (Event, bool) {
	if r.key_down == 0 {
		return Event{}, false
	}

	e := Event{Type: EventKey}
	if input_mode&InputAlt != 0 {
		if alt_mode_esc {
			e.Mod = ModAlt
			alt_mode_esc = false
		}
		if r.control_key_state&(left_alt_pressed|right_alt_pressed) != 0 {
			e.Mod = ModAlt
		}
	}

	ctrlpressed := r.control_key_state&(left_ctrl_pressed|right_ctrl_pressed) != 0

	if r.virtual_key_code >= vk_f1 && r.virtual_key_code <= vk_f12 {
		switch r.virtual_key_code {
		case vk_f1:
			e.Key = KeyF1
		case vk_f2:
			e.Key = KeyF2
		case vk_f3:
			e.Key = KeyF3
		case vk_f4:
			e.Key = KeyF4
		case vk_f5:
			e.Key = KeyF5
		case vk_f6:
			e.Key = KeyF6
		case vk_f7:
			e.Key = KeyF7
		case vk_f8:
			e.Key = KeyF8
		case vk_f9:
			e.Key = KeyF9
		case vk_f10:
			e.Key = KeyF10
		case vk_f11:
			e.Key = KeyF11
		case vk_f12:
			e.Key = KeyF12
		default:
			panic("unreachable")
		}

		return e, true
	}

	if r.virtual_key_code <= vk_delete {
		switch r.virtual_key_code {
		case vk_insert:
			e.Key = KeyInsert
		case vk_delete:
			e.Key = KeyDelete
		case vk_home:
			e.Key = KeyHome
		case vk_end:
			e.Key = KeyEnd
		case vk_pgup:
			e.Key = KeyPgup
		case vk_pgdn:
			e.Key = KeyPgdn
		case vk_arrow_up:
			e.Key = KeyArrowUp
		case vk_arrow_down:
			e.Key = KeyArrowDown
		case vk_arrow_left:
			e.Key = KeyArrowLeft
		case vk_arrow_right:
			e.Key = KeyArrowRight
		case vk_backspace:
			if ctrlpressed {
				e.Key = KeyBackspace2
			} else {
				e.Key = KeyBackspace
			}
		case vk_tab:
			e.Key = KeyTab
		case vk_enter:
			e.Key = KeyEnter
		case vk_esc:
			switch {
			case input_mode&InputEsc != 0:
				e.Key = KeyEsc
			case input_mode&InputAlt != 0:
				alt_mode_esc = true
				return Event{}, false
			}
		case vk_space:
			if ctrlpressed {
				// manual return here, because KeyCtrlSpace is zero
				e.Key = KeyCtrlSpace
				return e, true
			} else {
				e.Key = KeySpace
			}
		}

		if e.Key != 0 {
			return e, true
		}
	}

	if ctrlpressed {
		if Key(r.unicode_char) >= KeyCtrlA && Key(r.unicode_char) <= KeyCtrlRsqBracket {
			e.Key = Key(r.unicode_char)
			if input_mode&InputAlt != 0 && e.Key == KeyEsc {
				alt_mode_esc = true
				return Event{}, false
			}
			return e, true
		}
		switch r.virtual_key_code {
		case 192, 50:
			// manual return here, because KeyCtrl2 is zero
			e.Key = KeyCtrl2
			return e, true
		case 51:
			if input_mode&InputAlt != 0 {
				alt_mode_esc = true
				return Event{}, false
			}
			e.Key = KeyCtrl3
		case 52:
			e.Key = KeyCtrl4
		case 53:
			e.Key = KeyCtrl5
		case 54:
			e.Key = KeyCtrl6
		case 189, 191, 55:
			e.Key = KeyCtrl7
		case 8, 56:
			e.Key = KeyCtrl8
		}

		if e.Key != 0 {
			return e, true
		}
	}

	if r.unicode_char != 0 {
		e.Ch = rune(r.unicode_char)
		return e, true
	}

	return Event{}, false
}

func input_event_producer() {
	var r input_record
	var err error
	var last_button Key
	var last_state = dword(0)
	handles := []syscall.Handle{in, interrupt}
	for {
		err = wait_for_multiple_objects(handles)
		if err != nil {
			input_comm <- Event{Type: EventError, Err: err}
		}

		select {
		case <-cancel_comm:
			cancel_done_comm <- true
			return
		default:
		}

		err = read_console_input(in, &r)
		if err != nil {
			input_comm <- Event{Type: EventError, Err: err}
		}

		switch r.event_type {
		case key_event:
			kr := (*key_event_record)(unsafe.Pointer(&r.event))
			ev, ok := key_event_record_to_event(kr)
			if ok {
				for i := 0; i < int(kr.repeat_count); i++ {
					input_comm <- ev
				}
			}
		case window_buffer_size_event:
			sr := *(*window_buffer_size_record)(unsafe.Pointer(&r.event))
			input_comm <- Event{
				Type:   EventResize,
				Width:  int(sr.size.x),
				Height: int(sr.size.y),
			}
		case mouse_event:
			mr := *(*mouse_event_record)(unsafe.Pointer(&r.event))

			// single or double click
			switch mr.event_flags {
			case 0:
				cur_state := mr.button_state
				switch {
				case last_state&mouse_lmb == 0 && cur_state&mouse_lmb != 0:
					last_button = MouseLeft
				case last_state&mouse_rmb == 0 && cur_state&mouse_rmb != 0:
					last_button = MouseRight
				case last_state&mouse_mmb == 0 && cur_state&mouse_mmb != 0:
					last_button = MouseMiddle
				default:
					last_state = cur_state
					continue
				}
				last_state = cur_state
				fallthrough
			case 2:
				input_comm <- Event{
					Type:   EventMouse,
					Key:    last_button,
					MouseX: int(mr.mouse_pos.x),
					MouseY: int(mr.mouse_pos.y),
				}
			}
		}
	}
}
