package main

// Chord parsing and normalisation.
//
// A chord is zero or more modifiers plus exactly one key: Ctrl+Shift+P.
// A sequence is one or more chords separated by whitespace: Ctrl+K Ctrl+S.
//
// The canonical form prints modifiers in a fixed order - Ctrl, Alt, Shift,
// Meta - which is the same order macOS itself uses when it renders a chord
// (control, option, shift, command / U+2303 U+2325 U+21E7 U+2318). Cmd, Command,
// Super, Win and Windows all normalise to Meta; Opt and Option normalise to Alt;
// Control and Ctl normalise to Ctrl. Key names normalise to a single canonical
// spelling, so ctrl-shift-p, ^⇧P and Shift+Ctrl+P are all one chord.

import (
	"fmt"
	"strings"
)

// Mod is a bit set of keyboard modifiers.
type Mod uint8

// Modifier bits. The values are internal; the canonical ORDER is modOrder.
const (
	ModCtrl Mod = 1 << iota
	ModAlt
	ModShift
	ModMeta
)

// modOrder fixes the canonical printing order of modifiers.
var modOrder = []struct {
	bit  Mod
	name string
}{
	{ModCtrl, "Ctrl"},
	{ModAlt, "Alt"},
	{ModShift, "Shift"},
	{ModMeta, "Meta"},
}

// modAliases maps every accepted spelling of a modifier (lower-cased) to its
// bit. Symbol spellings are handled earlier, by expandSymbols.
var modAliases = buildModAliases()

func buildModAliases() map[string]Mod {
	m := map[string]Mod{}
	for _, a := range []string{"ctrl", "control", "ctl", "ctrl_l", "ctrl_r"} {
		m[a] = ModCtrl
	}
	for _, a := range []string{"alt", "opt", "option", "alt_l", "alt_r", "altgr"} {
		m[a] = ModAlt
	}
	for _, a := range []string{"shift", "shft", "shift_l", "shift_r"} {
		m[a] = ModShift
	}
	for _, a := range []string{"meta", "cmd", "command", "super", "win", "windows", "hyper"} {
		m[a] = ModMeta
	}
	return m
}

// symbolMods maps the single-rune modifier glyphs people paste out of macOS
// menus (and the ASCII caret) to their canonical modifier names.
var symbolMods = map[rune]string{
	'⌃': "Ctrl",  // U+2303 UP ARROWHEAD - control
	'^': "Ctrl",  // ASCII stand-in for control
	'⌥': "Alt",   // U+2325 OPTION KEY
	'⎇': "Alt",   // U+2387 ALTERNATIVE KEY SYMBOL
	'⇧': "Shift", // U+21E7 UPWARDS WHITE ARROW
	'⌘': "Meta",  // U+2318 PLACE OF INTEREST SIGN - command
	'⊞': "Meta",  // U+229E SQUARED PLUS - Windows logo stand-in
}

// keyAliases maps every accepted spelling of a key (lower-cased) to its
// canonical name. Anything not in this table is rejected by name.
var keyAliases = buildKeyAliases()

func buildKeyAliases() map[string]string {
	m := map[string]string{}
	add := func(canon string, aliases ...string) {
		m[strings.ToLower(canon)] = canon
		for _, a := range aliases {
			m[a] = canon
		}
	}
	for c := 'a'; c <= 'z'; c++ {
		add(strings.ToUpper(string(c)))
	}
	for d := '0'; d <= '9'; d++ {
		add(string(d))
	}
	for i := 1; i <= 24; i++ {
		add(fmt.Sprintf("F%d", i))
	}
	for i := 0; i <= 9; i++ {
		add(fmt.Sprintf("Numpad%d", i), fmt.Sprintf("kp%d", i), fmt.Sprintf("kp_%d", i), fmt.Sprintf("num%d", i))
	}
	add("Space", " ", "spc", "spacebar", "sp")
	add("Tab")
	add("Enter", "return", "ret", "cr")
	add("Escape", "esc")
	add("Backspace", "bksp", "bs", "back")
	add("Delete", "del", "forwarddelete")
	add("Insert", "ins")
	add("Home")
	add("End")
	add("PageUp", "pgup", "prior", "pageup")
	add("PageDown", "pgdn", "next", "pagedown")
	add("Up", "uparrow", "arrowup", "cursorup")
	add("Down", "downarrow", "arrowdown", "cursordown")
	add("Left", "leftarrow", "arrowleft", "cursorleft")
	add("Right", "rightarrow", "arrowright", "cursorright")
	add("CapsLock", "caps", "capslock")
	add("NumLock", "numlock")
	add("ScrollLock", "scrolllock", "scroll")
	add("PrintScreen", "prtsc", "prtscn", "printscrn", "prntscrn", "sysrq")
	add("Pause", "break")
	add("Menu", "apps", "contextmenu", "application")
	add("Minus", "-", "hyphen", "dash", "underscore")
	add("Plus", "+")
	add("Equal", "=", "equals")
	add("Comma", ",", "<")
	add("Period", ".", "dot", "fullstop", ">")
	add("Slash", "/", "?", "forwardslash")
	add("Backslash", "\\", "|", "pipe")
	add("Semicolon", ";", ":", "colon")
	add("Quote", "'", "apostrophe", "\"", "doublequote")
	add("Backquote", "`", "grave", "backtick", "tilde", "~")
	add("LeftBracket", "[", "lbracket", "openbracket", "{")
	add("RightBracket", "]", "rbracket", "closebracket", "}")
	add("Caret", "^")
	add("NumpadAdd", "numpadplus", "kpadd", "kp_add", "kpplus")
	add("NumpadSubtract", "numpadminus", "kpsubtract", "kp_subtract", "kpminus")
	add("NumpadMultiply", "numpadstar", "kpmultiply", "kp_multiply")
	add("NumpadDivide", "kpdivide", "kp_divide")
	add("NumpadDecimal", "numpaddot", "kpdecimal", "kp_decimal")
	add("NumpadEnter", "kpenter", "kp_enter")
	return m
}

// Chord is a normalised modifier set plus one key.
type Chord struct {
	Mods Mod
	Key  string
}

// String renders the canonical form, modifiers in modOrder.
func (c Chord) String() string {
	parts := make([]string, 0, 5)
	for _, m := range modOrder {
		if c.Mods&m.bit != 0 {
			parts = append(parts, m.name)
		}
	}
	parts = append(parts, c.Key)
	return strings.Join(parts, "+")
}

// Sequence is one or more chords pressed in order.
type Sequence []Chord

func (s Sequence) String() string {
	parts := make([]string, len(s))
	for i, c := range s {
		parts[i] = c.String()
	}
	return strings.Join(parts, " ")
}

// HasPrefix reports whether p is a prefix of s (not necessarily strict).
func (s Sequence) HasPrefix(p Sequence) bool {
	if len(p) > len(s) {
		return false
	}
	for i := range p {
		if s[i] != p[i] {
			return false
		}
	}
	return true
}

// StrictPrefixOf reports whether s is a proper prefix of o: pressing s would be
// swallowed while the runtime waits to see whether o is coming.
func (s Sequence) StrictPrefixOf(o Sequence) bool {
	return len(s) < len(o) && o.HasPrefix(s)
}

const maxChordsPerSequence = 8

// ParseSequence parses whitespace-separated chords, e.g. "Ctrl+K Ctrl+S".
func ParseSequence(s string) (Sequence, error) {
	fields := strings.Fields(s)
	if len(fields) == 0 {
		return nil, fmt.Errorf("empty binding: expected a chord such as %q", "Ctrl+Shift+P")
	}
	if len(fields) > maxChordsPerSequence {
		return nil, fmt.Errorf("sequence %q has %d chords, the limit is %d", s, len(fields), maxChordsPerSequence)
	}
	seq := make(Sequence, 0, len(fields))
	for _, f := range fields {
		c, err := ParseChord(f)
		if err != nil {
			return nil, err
		}
		seq = append(seq, c)
	}
	return seq, nil
}

// ParseChord parses exactly one chord and returns its normalised form.
func ParseChord(s string) (Chord, error) {
	raw := strings.TrimSpace(s)
	if raw == "" {
		return Chord{}, fmt.Errorf("empty chord: expected something like %q", "Ctrl+Shift+P")
	}
	toks, err := tokenise(expandSymbols(raw))
	if err != nil {
		return Chord{}, fmt.Errorf("chord %q: %v", raw, err)
	}
	var c Chord
	for i, t := range toks {
		last := i == len(toks)-1
		if m, ok := modAliases[strings.ToLower(t)]; ok {
			if last {
				return Chord{}, fmt.Errorf("chord %q: %q is a modifier, not a key - a chord must end in a key", raw, t)
			}
			if c.Mods&m != 0 {
				return Chord{}, fmt.Errorf("chord %q: modifier %q is repeated", raw, t)
			}
			c.Mods |= m
			continue
		}
		if !last {
			return Chord{}, fmt.Errorf("chord %q: unknown modifier %q", raw, t)
		}
		k, ok := keyAliases[strings.ToLower(t)]
		if !ok {
			return Chord{}, fmt.Errorf("chord %q: unknown key %q", raw, t)
		}
		c.Key = k
	}
	if c.Key == "" {
		return Chord{}, fmt.Errorf("chord %q: no key found", raw)
	}
	return c, nil
}

// expandSymbols rewrites leading/embedded modifier glyphs into their word form,
// so the ordinary separator tokeniser can handle them. A glyph in the FINAL
// rune position is left alone, because there it is the key (Ctrl+^ is caret).
func expandSymbols(s string) string {
	rs := []rune(s)
	var b strings.Builder
	for i, r := range rs {
		name, isMod := symbolMods[r]
		if !isMod || i == len(rs)-1 {
			b.WriteRune(r)
			continue
		}
		b.WriteString(name)
		// Emit a separator unless the next rune already separates two tokens.
		if !(isSep(rs[i+1]) && i+1 < len(rs)-1) {
			b.WriteByte('+')
		}
	}
	return b.String()
}

func isSep(r rune) bool { return r == '+' || r == '-' }

// tokenise splits a chord on '+' and '-'. A separator only separates when a
// token is already accumulating, so Ctrl++ is Ctrl plus the "+" key and
// Ctrl+- is Ctrl plus the "-" key.
func tokenise(s string) ([]string, error) {
	rs := []rune(s)
	var toks []string
	var cur strings.Builder
	for i, r := range rs {
		if isSep(r) {
			if cur.Len() == 0 {
				cur.WriteRune(r)
				continue
			}
			if i == len(rs)-1 {
				return nil, fmt.Errorf("separator %q at the end with no key after it", string(r))
			}
			toks = append(toks, cur.String())
			cur.Reset()
			continue
		}
		if r == ' ' || r == '\t' {
			return nil, fmt.Errorf("unexpected whitespace - separate chords in a sequence with a single space")
		}
		cur.WriteRune(r)
	}
	if cur.Len() > 0 {
		toks = append(toks, cur.String())
	}
	if len(toks) == 0 {
		return nil, fmt.Errorf("no tokens")
	}
	return toks, nil
}

// mustSeq parses a sequence known at compile time; it is only used for the
// built-in reserved tables, and a failure there is a programming error.
func mustSeq(s string) Sequence {
	seq, err := ParseSequence(s)
	if err != nil {
		panic("windowdeck: built-in chord table is malformed: " + err.Error())
	}
	return seq
}
