import GLib from 'gi://GLib';
import Gio from 'gi://Gio';

import { ColorSpace } from './color.js';
import { ItemType, getActionsConfigPath } from './constants.js';

export const ActionOutput = {
	Ignore: 'ignore',
	Copy: 'copy',
	Paste: 'paste',
};

export function instanceofActionSubmenu(obj) {
	return 'name' in obj && 'actions' in obj;
}

export function instanceofAction(obj) {
	return 'kind' in obj && 'id' in obj && 'name' in obj && 'output' in obj;
}

export function instanceofCommandAction(obj) {
	return instanceofAction(obj) && obj.kind === 'command' && 'command' in obj;
}

export function instanceofColorAction(obj) {
	return instanceofAction(obj) && obj.kind === 'color' && 'space' in obj;
}

export function instanceofQrCodeAction(obj) {
	return instanceofAction(obj) && obj.kind === 'qrcode';
}

/**
 * Test if an action is applicable to a clipboard entry.
 * @param entry the clipboard entry to test the action against.
 * @param action the action.
 */
export function testAction(entry, action) {
	const type = !action.types?.length || action.types.includes(entry.type);
	if (!type) return false;
	if (!action.pattern) return true;
	try {
		const regex = new RegExp(action.pattern);
		return regex.test(entry.content);
	} catch {
		return false;
	}
}

/**
 * Test if an action is applicable to a clipboard entry and return the matches from the regular expression.
 * @param entry the clipboard entry to test the action against.
 * @param action the action.
 * @returns an array of matches if the action matches, null otherwise.
 */
export function matchAction(entry, action) {
	const type = !action.types?.length || action.types.includes(entry.type);
	if (!type) return null;
	if (!action.pattern) return [entry.content];
	try {
		const regex = new RegExp(action.pattern);
		return entry.content.match(regex);
	} catch {
		return null;
	}
}

/**
 * Find an action by its id in an action configuration.
 * @param config the action configuration.
 * @param id the id of the action.
 * @returns an action if an action with the id was found, undefined otherwise.
 */
export function findActionById(config, id) {
	function findAction(actions) {
		for (const action of actions) {
			if (instanceofActionSubmenu(action)) {
				const result = findAction(action.actions);
				if (result) return result;
			}
			if (instanceofAction(action) && action.id === id) return action;
		}
		return undefined;
	}

	return findAction(config.actions);
}

/**
 * Check if an action is the default action for an entry.
 * @param config the action configuration.
 * @param entry the entry.
 * @param action the action.
 */
export function isDefaultAction(config, entry, action) {
	return config.defaults?.[entry.type] === action.id;
}

/**
 * Find the default action for an entry.
 * @param config the action configuration.
 * @param entry the entry.
 * @returns an action if a default action was found, undefined otherwise.
 */
export function findDefaultAction(config, entry) {
	const id = config.defaults?.[entry.type];
	return id ? findActionById(config, id) : undefined;
}

/**
 * Get the default configuration.
 * @param ext extension or extension preferences.
 */
export function defaultConfig(ext) {
	const _ = ext.gettext.bind(ext);

	function colorAction(id, name, pattern, space) {
		return {
			kind: 'color',
			id,
			name,
			pattern,
			types: [ItemType.Color],
			space,
			output: ActionOutput.Paste,
			shortcut: [],
		};
	}

	return {
		actions: [
			{
				name: _('Open'),
				actions: [
					{
						kind: 'command',
						id: 'open-with-default',
						name: _('Open with Default'),
						pattern: null,
						types: [ItemType.Image, ItemType.File],
						command: 'xargs xdg-open',
						output: ActionOutput.Ignore,
						shortcut: [],
					},
					{
						kind: 'command',
						id: 'open-with-files',
						name: _('Open with Files'),
						pattern: '^(.*)',
						types: [ItemType.Image, ItemType.File, ItemType.Files],
						command: 'nautilus -s $1',
						output: ActionOutput.Ignore,
						shortcut: [],
					},
					{
						kind: 'command',
						id: 'open-with-browser',
						name: _('Open with Browser'),
						pattern: null,
						types: [ItemType.Link],
						command: 'xargs xdg-open',
						output: ActionOutput.Ignore,
						shortcut: [],
					},
				],
			},
			{
				kind: 'command',
				id: 'paste-as-path',
				name: _('Paste as Path'),
				pattern: null,
				types: [ItemType.Image, ItemType.File, ItemType.Files],
				command: 'cut -c8-',
				output: ActionOutput.Paste,
				shortcut: [],
			},
			{
				name: _('Convert'),
				actions: [
					colorAction('rgb', _('Rgb'), `^(?!rgb)`, ColorSpace.Rgb),
					colorAction('hex', _('Hex'), `^(?!#)`, ColorSpace.Hex),
					colorAction('hsl', _('Hsl'), `^(?!hsl)`, ColorSpace.Hsl),

					// colorAction('hwb', _('Hwb'), `^(?!hwb)`, ColorSpace.Hwb),
					// colorAction('linear-rgb', _('Linear Rgb'), `^(?!color\\(srgb-linear)`, ColorSpace.LinearRgb),
					// colorAction('xyz', _('Xyz'), `^(?!color\\(xyz)`, ColorSpace.Xyz),
					// colorAction('lab', _('Lab'), `^(?!lab)`, ColorSpace.Lab),
					// colorAction('lch', _('Lch'), `^(?!lch)`, ColorSpace.Lch),
					// colorAction('oklab', _('Oklab'), `^(?!oklab)`, ColorSpace.Oklab),
					colorAction('oklch', _('Oklch'), `^(?!oklch)`, ColorSpace.Oklch),
				],
			},
			{
				kind: 'qrcode',
				id: 'qrcode',
				name: _('QR Code'),
				pattern: null,
				types: [ItemType.Text, ItemType.Code, ItemType.Link, ItemType.Character, ItemType.Color],
				output: ActionOutput.Ignore,
				shortcut: ['<Control>q'],
			},
		],
		defaults: {
			[ItemType.File]: 'paste-as-path',
			[ItemType.Files]: 'paste-as-path',
			[ItemType.Link]: 'open-with-browser',
		},
	};
}

/**
 * Load the configuration.
 * @param ext the extension or extension preferences.
 * @param save whether to save a default configuration if no configuration was found.
 */
export function loadConfig(ext, save = false) {
	const environment = GLib.get_environ();
	const actions = GLib.environ_getenv(environment, 'DEBUG_COPYOUS_ACTIONS');
	if (actions === 'default') return defaultConfig(ext);
	const path = actions ? Gio.File.new_for_path(actions) : getActionsConfigPath(ext);
	try {
		if (path.query_exists(null)) {
			const [success, contents, _etag] = path.load_contents(null);
			if (success) {
				const text = new TextDecoder().decode(contents);
				return JSON.parse(text);
			}
		} else {
			const config = defaultConfig(ext);
			if (save) saveConfig(ext, config);
			return config;
		}
	} catch (err) {
		const logger = 'logger' in ext ? ext.logger : ext.getLogger();
		logger.error('Failed to load actions config', err);
	}
	return defaultConfig(ext);
}

/**
 * Save the configuration.
 * @param ext the extension or extension preferences.
 * @param config the configuration to save.
 * @param backup whether to back up the previous preferences.
 */
export function saveConfig(ext, config, backup = false) {
	const path = getActionsConfigPath(ext);
	const dir = path.get_parent();
	if (!dir.query_exists(null)) {
		dir.make_directory_with_parents(null);
	}
	path.replace_contents(
		JSON.stringify(config, null, '\t'),
		null,
		backup,
		Gio.FileCreateFlags.REPLACE_DESTINATION,
		null,
	);
}

/**
 * Get all ids in actions.
 * @param actions the actions to iterate.
 */
function* getIds(actions) {
	for (const action of actions) {
		if (instanceofActionSubmenu(action)) {
			yield* getIds(action.actions);
		} else if (instanceofAction(action)) {
			yield action.id;
		}
	}
}

/**
 * Count the number of action ids that occur in config2 but not in config1.
 * @param config1 the original configuration.
 * @param config2 the configuration to check.
 */
export function countDifference(config1, config2) {
	const ids1 = new Set(getIds(config1.actions));
	let count = 0;
	for (const id of getIds(config2.actions)) {
		if (!ids1.has(id)) count++;
	}
	return count;
}

/**
 * Merge all actions from config2 into config1 such.
 * @param config1 the original configuration.
 * @param config2 the configuration to merge into config1.
 */
export function mergeConfig(config1, config2) {
	function* filterActions(actions) {
		for (const action of actions) {
			if (instanceofActionSubmenu(action)) {
				const a = Array.from(filterActions(action.actions));
				if (a.length > 0) {
					yield {
						name: action.name,
						actions: a,
					};
				}
			} else if (instanceofAction(action)) {
				if (!ids.has(action.id)) {
					yield action;
				}
			}
		}
	}

	const ids = new Set(getIds(config1.actions));
	let filtered = Array.from(filterActions(config2.actions));

	// Group submenus in config2 with the first occurrence of the submenu in config1
	const actions = [];
	for (const action of config1.actions) {
		if (instanceofActionSubmenu(action)) {
			const indexes = new Set();
			const submenus = filtered.filter((x, i) => {
				const pred = instanceofActionSubmenu(x) && x.name === action.name;
				if (pred) indexes.add(i);
				return pred;
			});
			const submenuActions = submenus.flatMap((x) => x.actions);
			if (submenuActions.length > 0) {
				const submenu = {
					name: action.name,
					actions: [...action.actions, ...submenuActions],
				};
				actions.push(submenu);
				filtered = filtered.filter((_, i) => !indexes.has(i));
			} else {
				actions.push(action);
			}
		} else if (instanceofAction(action)) {
			actions.push(action);
		}
	}
	return {
		actions: [...actions, ...filtered],
		defaults: config1.defaults ?? {},
	};
}
