import keyCode from "./keycode";

type Callback = (ev: KeyboardEvent) => void;

type Keymap = Record<string, Callback>;

type Pattern = {
	which: string[];
	ctrl?: boolean;
	shift?: boolean;
	alt?: boolean;
};

type Action = {
	patterns: Pattern[];
	callback: Callback;
	allowRepeat: boolean;
};

const parseKeymap = (keymap: Keymap) =>
	Object.entries(keymap).map(([patterns, callback]): Action => {
		const result = {
			patterns: [],
			callback,
			allowRepeat: true,
		} as Action;

		if (patterns.match(/^\(.*\)$/) !== null) {
			result.allowRepeat = false;
			patterns = patterns.slice(1, -1);
		}

		result.patterns = patterns.split("|").map((part) => {
			const pattern = {
				which: [],
				ctrl: false,
				alt: false,
				shift: false,
			} as Pattern;

			const keys = part
				.trim()
				.split("+")
				.map((x) => x.trim().toLowerCase());
			for (const key of keys) {
				switch (key) {
					case "ctrl":
						pattern.ctrl = true;
						break;
					case "alt":
						pattern.alt = true;
						break;
					case "shift":
						pattern.shift = true;
						break;
					default:
						pattern.which = keyCode(key).map((k) => k.toLowerCase());
				}
			}

			return pattern;
		});

		return result;
	});

const ignoreElemens = ["input", "textarea"];

function match(ev: KeyboardEvent, patterns: Action["patterns"]): boolean {
	const key = ev.code.toLowerCase();
	return patterns.some(
		(pattern) =>
			pattern.which.includes(key) &&
			pattern.ctrl === ev.ctrlKey &&
			pattern.shift === ev.shiftKey &&
			pattern.alt === ev.altKey &&
			!ev.metaKey,
	);
}

export const makeHotkey = (keymap: Keymap) => {
	const actions = parseKeymap(keymap);

	return (ev: KeyboardEvent) => {
		if (document.activeElement) {
			if (ignoreElemens.some((el) => document.activeElement!.matches(el)))
				return;
			if (document.activeElement.attributes["contenteditable"]) return;
		}

		for (const action of actions) {
			const matched = match(ev, action.patterns);

			if (matched) {
				if (!action.allowRepeat && ev.repeat) return;

				ev.preventDefault();
				ev.stopPropagation();
				action.callback(ev);
				break;
			}
		}
	};
};
