package keyboard

import (
	"context"
	"fmt"
	"log"

	"git.sr.ht/~guacamolie/faxmachine/xkbcommon"
	"github.com/holoplot/go-evdev"
)

type Keyboard struct {
	dev    *evdev.InputDevice
	ctx    *xkbcommon.Context
	keymap *xkbcommon.Keymap
	state  *xkbcommon.State
}

func Open(devnode string) (*Keyboard, error) {
	var err error
	k := &Keyboard{}

	k.dev, err = evdev.Open(devnode)
	if err != nil {
		return nil, err
	}

	err = k.dev.Grab()
	if err != nil {
		k.dev.Close()
		return nil, fmt.Errorf("failed to grab input: %w", err)
	}

	k.ctx, err = xkbcommon.NewContext(xkbcommon.ContextNoFlags)
	if err != nil {
		k.dev.Close()
		return nil, err
	}

	k.keymap, err = xkbcommon.NewKeymapFromNames(k.ctx, nil, xkbcommon.KeymapCompileFlagsNoFlags)
	if err != nil {
		k.ctx.Unref()
		k.dev.Close()
		return nil, err
	}

	k.state, err = xkbcommon.NewState(k.keymap)
	if err != nil {
		k.keymap.Unref()
		k.ctx.Unref()
		k.dev.Close()
		return nil, err
	}

	return k, nil
}

func (k *Keyboard) Close() {
	k.state.Unref()
	k.keymap.Unref()
	k.ctx.Unref()
	k.dev.Close()
}

func (k *Keyboard) Listen(ctx context.Context, ch chan<- KeyPress) {
	go func() {
		for {
			inputEvent, err := k.dev.ReadOne()
			if err != nil {
				log.Fatalf("keyboard read failed: %v\n", err)
			}

			if inputEvent.Type != evdev.EV_KEY {
				continue
			}

			if inputEvent.Value == 1 {
				keyPress := KeyPress{
					Sym:          k.state.GetOneSym(keycode(inputEvent)),
					ShortcutMods: k.getShortcutMods(keycode(inputEvent)),
					Char:         k.state.KeyGetUtf8(keycode(inputEvent)),
				}

				log.Printf("keypress: %#v\n", keyPress)

				ch <- keyPress
			}

			// This should be called _after_ procesing the key, so that the key
			// doesn't influence itself.
			if inputEvent.Value == 1 {
				k.state.UpdateKey(keycode(inputEvent), xkbcommon.KeyDown)
			} else if inputEvent.Value == 0 {
				k.state.UpdateKey(keycode(inputEvent), xkbcommon.KeyUp)
			}
		}
	}()
}

func (k *Keyboard) getShortcutMods(key xkbcommon.Keycode) ModMask {
	// This implements the algorithm as described here:
	// https://xkbcommon.org/doc/current/group__state.html#consumed-modifiers

	stateMods := k.state.SerializeMods(xkbcommon.StateModsEffective)
	consumedMods := k.state.KeyGetConsumedMods(key)
	significantMods := xkbcommon.ModMask(
		1<<k.keymap.ModGetIndex("Control") |
			1<<k.keymap.ModGetIndex("Alt") |
			1<<k.keymap.ModGetIndex("Shift") |
			1<<k.keymap.ModGetIndex("Super"))

	shortcutMods := stateMods & ^consumedMods & significantMods
	return k.parseModMask(shortcutMods)
}

func (k *Keyboard) parseModMask(modMask xkbcommon.ModMask) (mod ModMask) {
	if modMask&(1<<k.keymap.ModGetIndex("Control")) != 0 {
		mod |= Control
	}
	if modMask&(1<<k.keymap.ModGetIndex("Alt")) != 0 {
		mod |= Alt
	}
	if modMask&(1<<k.keymap.ModGetIndex("Shift")) != 0 {
		mod |= Shift
	}
	if modMask&(1<<k.keymap.ModGetIndex("Super")) != 0 {
		mod |= Super
	}
	return mod
}

func keycode(event *evdev.InputEvent) xkbcommon.Keycode {
	return xkbcommon.Keycode(event.Code + 8)
}

type KeyPress struct {
	Sym          xkbcommon.Keysym
	ShortcutMods ModMask
	Char         string
}

type ModMask int

const (
	Control ModMask = 1 << iota
	Alt
	Shift
	Super
)