_import {arr, clean, isEmpty} from '@amekusa/util.js';
import {Sanitizer} from './Sanitizer.js';
import {key} from './fn.js';

/**
 * @typedef {object|string} Keymap
 * A keymap definition which can be passed to {@link Rule#remap} as `from` or `to` properties.
 * It can be an object like `{ key_code: 'a', ... }`, or a string in the special format.
 *
 * #### Object Format
 * A plain object that loosely follows [the Karabiner's specifications](https://karabiner-elements.pqrs.org/docs/json/complex-modifications-manipulator-definition/from/).
 * {@link key} function returns in this format.
 *
 * #### String Format
 * A special expression that is only supported by Karabinerge for user's convenience.
 * Here are some examples:
 *
 * | Expression | Meaning |
 * |:-----------|:--------|
 * | `'a'` | `a` key |
 * | `'shift + a'` | `a` key with `shift` modifier |
 * | `'shift + control + a'` | `a` key with `shift` + `control` modifiers |
 * | `'shift + (control) + a'` | `a` key with `shift` + optional `control` modifiers |
 *
 **/

/**
 * A complex modification rule
 */
export class Rule {
	/**
	 * Instantiates a {@link Rule} from the given JSON string or object.
	 * @param {string|object} data - JSON string or object
	 * @return {Rule} new instance
	 */
	static fromJSON(data) {
		switch (typeof data) {
		case 'object':
			break;
		case 'string':
			data = JSON.parse(data);
			break;
		default:
			throw `invalid argument`;
		}
		let r = new this(data.description);
		if (data.manipulators) r.remaps = arr(data.manipulators);
		return r;
	}
	/**
	 * @param {string} desc - rule description
	 */
	constructor(desc) {
		/**
		 * Rule description.
		 * @type {string}
		 */
		this.desc = desc || '';
		/**
		 * Remap definitions.
		 * @type {object[]}
		 */
		this.remaps = [];
		/**
		 * Remap conditions.
		 * @type {object[]}
		 */
		this.conds = [];
	}
	/**
	 * Defines a `from-to` remap rule
	 * @param {object} map - Rule definition like: `{ from: ... , to: ... }`
	 * @param {Keymap} map.from - An object like `{ key_code: 'a' }`, or a string of the special expression. (See {@link Keymap})
	 * @param {Keymap|Keymap[]} map.to - An object like `{ key_code: 'a' }`, or a string of the special expression. Also can be an array for multiple keymaps. (See {@link Keymap})
	 * @param {any} map.* - Any property that Karabiner supports for [manipulator](https://karabiner-elements.pqrs.org/docs/json/complex-modifications-manipulator-definition/)
	 * @return {Rule} itself
	 * @example <caption>Remap control + H to backspace</caption>
	 * let rule = new Rule('control + H to backspace')
	 *   .remap({
	 *     from: key('h', 'control'),
	 *     to:   key('delete_or_backspace')
	 *   });
	 * @example <caption>Multiple remap rules</caption>
	 * let rule = new Rule('Various Remaps')
	 *   .remap( ... )
	 *   .remap( ... )
	 *   .remap( ... );
	 */
	remap(map) {
		if (!map.type) map.type = 'basic';
		if (this.conds.length) map = Object.assign(map, {conditions: this.conds});
		map = clean(remapSanitizer.sanitize(map));
		if (isEmpty(map)) console.warn(`Rule.remap: empty argument`);
		else this.remaps.push(map);
		return this;
	}
	/**
	 * Defines a condition
	 * @param {object} cond - condition definition like: `{ type: 'variable_if', ... }`
	 * @return {Rule} this
	 * @example <caption>Remap rules only for VSCode</caption>
	 * let rule = new Rule('VSCode Rules')
	 *   .cond(if_app('com.microsoft.VSCode'))
	 *   .remap( ... )
	 *   .remap( ... );
	 * @example <caption>Multiple conditions</caption>
	 * let rule = new Rule('VSCode Rules')
	 *   .cond(if_var('foo', 1))  // if variable 'foo' is 1
	 *   .cond(if_app('com.microsoft.VSCode'))
	 *   .remap( ... )
	 *   .remap( ... );
	 */
	cond(cond) {
		cond = clean(cond);
		if (isEmpty(cond)) console.warn(`Rule.cond: empty argument`);
		else this.conds.push(cond);
		return this;
	}
	/**
	 * Returns a plain object representation of this rule
	 * @return {object} an object like: `{ description: ... , manipulators: ... }`
	 */
	toJSON() {
		return {
			description: this.desc,
			manipulators: this.remaps
		};
	}
}

const remapSanitizer = new Sanitizer()
	.addFilter([
		'from',
		'to',
		'to[]',
	], prop => {
		if (typeof prop == 'string') return key(prop);
		return prop;
	})
	.addFilter('from.modifiers', prop => {
		if (Array.isArray(prop)) return {mandatory: prop};
		switch (typeof prop) {
		case 'string':
			return {mandatory: [prop]};
		}
		return prop;
	})
	.addFilter([
		'from.modifiers.mandatory',
		'from.modifiers.optional',
		'to',
		'to[].modifiers',
		'to_if_alone',
		'to_if_held_down',
		'to_after_key_up',
		'to_delayed_action.to_if_invoked',
		'to_delayed_action.to_if_canceled'
	], prop => {
		return arr(prop);
	});