// SPDX-License-Identifier: Unlicense OR MIT

// +build linux,!android freebsd openbsd

// Package xkb implements a Go interface for the X Keyboard Extension library.
package xkb

import (
	"errors"
	"fmt"
	"os"
	"syscall"
	"unicode"
	"unicode/utf8"
	"unsafe"

	"gioui.org/io/event"
	"gioui.org/io/key"
)

/*
#cgo linux pkg-config: xkbcommon
#cgo freebsd openbsd CFLAGS: -I/usr/local/include
#cgo freebsd openbsd LDFLAGS: -L/usr/local/lib -lxkbcommon

#include <stdlib.h>
#include <xkbcommon/xkbcommon.h>
#include <xkbcommon/xkbcommon-compose.h>
*/
import "C"

type Context struct {
	Ctx       *C.struct_xkb_context
	keyMap    *C.struct_xkb_keymap
	state     *C.struct_xkb_state
	compTable *C.struct_xkb_compose_table
	compState *C.struct_xkb_compose_state
	utf8Buf   []byte
}

var (
	_XKB_MOD_NAME_CTRL  = []byte("Control\x00")
	_XKB_MOD_NAME_SHIFT = []byte("Shift\x00")
	_XKB_MOD_NAME_ALT   = []byte("Mod1\x00")
	_XKB_MOD_NAME_LOGO  = []byte("Mod4\x00")
)

func (x *Context) Destroy() {
	if x.compState != nil {
		C.xkb_compose_state_unref(x.compState)
		x.compState = nil
	}
	if x.compTable != nil {
		C.xkb_compose_table_unref(x.compTable)
		x.compTable = nil
	}
	x.DestroyKeymapState()
	if x.Ctx != nil {
		C.xkb_context_unref(x.Ctx)
		x.Ctx = nil
	}
}

func New() (*Context, error) {
	ctx := &Context{
		Ctx: C.xkb_context_new(C.XKB_CONTEXT_NO_FLAGS),
	}
	if ctx.Ctx == nil {
		return nil, errors.New("newXKB: xkb_context_new failed")
	}
	locale := os.Getenv("LC_ALL")
	if locale == "" {
		locale = os.Getenv("LC_CTYPE")
	}
	if locale == "" {
		locale = os.Getenv("LANG")
	}
	if locale == "" {
		locale = "C"
	}
	cloc := C.CString(locale)
	defer C.free(unsafe.Pointer(cloc))
	ctx.compTable = C.xkb_compose_table_new_from_locale(ctx.Ctx, cloc, C.XKB_COMPOSE_COMPILE_NO_FLAGS)
	if ctx.compTable == nil {
		ctx.Destroy()
		return nil, errors.New("newXKB: xkb_compose_table_new_from_locale failed")
	}
	ctx.compState = C.xkb_compose_state_new(ctx.compTable, C.XKB_COMPOSE_STATE_NO_FLAGS)
	if ctx.compState == nil {
		ctx.Destroy()
		return nil, errors.New("newXKB: xkb_compose_state_new failed")
	}
	return ctx, nil
}

func (x *Context) DestroyKeymapState() {
	if x.state != nil {
		C.xkb_state_unref(x.state)
		x.state = nil
	}
	if x.keyMap != nil {
		C.xkb_keymap_unref(x.keyMap)
		x.keyMap = nil
	}
}

// SetKeymap sets the keymap and state. The context takes ownership of the
// keymap and state and frees them in Destroy.
func (x *Context) SetKeymap(xkbKeyMap, xkbState unsafe.Pointer) {
	x.DestroyKeymapState()
	x.keyMap = (*C.struct_xkb_keymap)(xkbKeyMap)
	x.state = (*C.struct_xkb_state)(xkbState)
}

func (x *Context) LoadKeymap(format int, fd int, size int) error {
	x.DestroyKeymapState()
	mapData, err := syscall.Mmap(int(fd), 0, int(size), syscall.PROT_READ, syscall.MAP_SHARED)
	if err != nil {
		return fmt.Errorf("newXKB: mmap of keymap failed: %v", err)
	}
	defer syscall.Munmap(mapData)
	keyMap := C.xkb_keymap_new_from_buffer(x.Ctx, (*C.char)(unsafe.Pointer(&mapData[0])), C.size_t(size-1), C.XKB_KEYMAP_FORMAT_TEXT_V1, C.XKB_KEYMAP_COMPILE_NO_FLAGS)
	if keyMap == nil {
		return errors.New("newXKB: xkb_keymap_new_from_buffer failed")
	}
	state := C.xkb_state_new(keyMap)
	if state == nil {
		C.xkb_keymap_unref(keyMap)
		return errors.New("newXKB: xkb_state_new failed")
	}
	x.keyMap = keyMap
	x.state = state
	return nil
}

func (x *Context) Modifiers() key.Modifiers {
	var mods key.Modifiers
	if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_CTRL[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 {
		mods |= key.ModCtrl
	}
	if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_SHIFT[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 {
		mods |= key.ModShift
	}
	if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_ALT[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 {
		mods |= key.ModAlt
	}
	if C.xkb_state_mod_name_is_active(x.state, (*C.char)(unsafe.Pointer(&_XKB_MOD_NAME_LOGO[0])), C.XKB_STATE_MODS_EFFECTIVE) == 1 {
		mods |= key.ModSuper
	}
	return mods
}

func (x *Context) DispatchKey(keyCode uint32) (events []event.Event) {
	if x.state == nil {
		return
	}
	kc := C.xkb_keycode_t(keyCode)
	if len(x.utf8Buf) == 0 {
		x.utf8Buf = make([]byte, 1)
	}
	sym := C.xkb_state_key_get_one_sym(x.state, kc)
	if name, ok := convertKeysym(sym); ok {
		cmd := key.Event{
			Name:      name,
			Modifiers: x.Modifiers(),
		}
		// Ensure that a physical backtab key is translated to
		// Shift-Tab.
		if sym == C.XKB_KEY_ISO_Left_Tab {
			cmd.Modifiers |= key.ModShift
		}
		events = append(events, cmd)
	}
	C.xkb_compose_state_feed(x.compState, sym)
	var str []byte
	switch C.xkb_compose_state_get_status(x.compState) {
	case C.XKB_COMPOSE_CANCELLED, C.XKB_COMPOSE_COMPOSING:
		return
	case C.XKB_COMPOSE_COMPOSED:
		size := C.xkb_compose_state_get_utf8(x.compState, (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf)))
		if int(size) >= len(x.utf8Buf) {
			x.utf8Buf = make([]byte, size+1)
			size = C.xkb_compose_state_get_utf8(x.compState, (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf)))
		}
		C.xkb_compose_state_reset(x.compState)
		str = x.utf8Buf[:size]
	case C.XKB_COMPOSE_NOTHING:
		str = x.charsForKeycode(kc)
	}
	// Report only printable runes.
	var n int
	for n < len(str) {
		r, s := utf8.DecodeRune(str)
		if unicode.IsPrint(r) {
			n += s
		} else {
			copy(str[n:], str[n+s:])
			str = str[:len(str)-s]
		}
	}
	if len(str) > 0 {
		events = append(events, key.EditEvent{Text: string(str)})
	}
	return
}

func (x *Context) charsForKeycode(keyCode C.xkb_keycode_t) []byte {
	size := C.xkb_state_key_get_utf8(x.state, keyCode, (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf)))
	if int(size) >= len(x.utf8Buf) {
		x.utf8Buf = make([]byte, size+1)
		size = C.xkb_state_key_get_utf8(x.state, keyCode, (*C.char)(unsafe.Pointer(&x.utf8Buf[0])), C.size_t(len(x.utf8Buf)))
	}
	return x.utf8Buf[:size]
}

func (x *Context) IsRepeatKey(keyCode uint32) bool {
	kc := C.xkb_keycode_t(keyCode)
	return C.xkb_keymap_key_repeats(x.keyMap, kc) == 1
}

func (x *Context) UpdateMask(depressed, latched, locked, depressedGroup, latchedGroup, lockedGroup uint32) {
	if x.state == nil {
		return
	}
	C.xkb_state_update_mask(x.state, C.xkb_mod_mask_t(depressed), C.xkb_mod_mask_t(latched), C.xkb_mod_mask_t(locked),
		C.xkb_layout_index_t(depressedGroup), C.xkb_layout_index_t(latchedGroup), C.xkb_layout_index_t(lockedGroup))
}

func convertKeysym(s C.xkb_keysym_t) (string, bool) {
	if 'a' <= s && s <= 'z' {
		return string(rune(s - 'a' + 'A')), true
	}
	if ' ' <= s && s <= '~' {
		return string(rune(s)), true
	}
	var n string
	switch s {
	case C.XKB_KEY_Escape:
		n = key.NameEscape
	case C.XKB_KEY_Left:
		n = key.NameLeftArrow
	case C.XKB_KEY_Right:
		n = key.NameRightArrow
	case C.XKB_KEY_Return:
		n = key.NameReturn
	case C.XKB_KEY_KP_Enter:
		n = key.NameEnter
	case C.XKB_KEY_Up:
		n = key.NameUpArrow
	case C.XKB_KEY_Down:
		n = key.NameDownArrow
	case C.XKB_KEY_Home:
		n = key.NameHome
	case C.XKB_KEY_End:
		n = key.NameEnd
	case C.XKB_KEY_BackSpace:
		n = key.NameDeleteBackward
	case C.XKB_KEY_Delete:
		n = key.NameDeleteForward
	case C.XKB_KEY_Page_Up:
		n = key.NamePageUp
	case C.XKB_KEY_Page_Down:
		n = key.NamePageDown
	case C.XKB_KEY_F1:
		n = "F1"
	case C.XKB_KEY_F2:
		n = "F2"
	case C.XKB_KEY_F3:
		n = "F3"
	case C.XKB_KEY_F4:
		n = "F4"
	case C.XKB_KEY_F5:
		n = "F5"
	case C.XKB_KEY_F6:
		n = "F6"
	case C.XKB_KEY_F7:
		n = "F7"
	case C.XKB_KEY_F8:
		n = "F8"
	case C.XKB_KEY_F9:
		n = "F9"
	case C.XKB_KEY_F10:
		n = "F10"
	case C.XKB_KEY_F11:
		n = "F11"
	case C.XKB_KEY_F12:
		n = "F12"
	case C.XKB_KEY_Tab, C.XKB_KEY_KP_Tab, C.XKB_KEY_ISO_Left_Tab:
		n = key.NameTab
	case 0x20, C.XKB_KEY_KP_Space:
		n = "Space"
	default:
		return "", false
	}
	return n, true
}
