import Clutter from 'gi://Clutter';
import GObject from 'gi://GObject';
import St from 'gi://St';

import { gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js';
import * as BoxPointer from 'resource:///org/gnome/shell/ui/boxpointer.js';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';

import { ItemType, ItemTypes, Tags } from '../common/constants.js';
import { enumParamSpec, registerClass } from '../common/gjs.js';
import { Icon, loadIcon } from '../common/icons.js';
import { TagsItem } from './components/tagsItem.js';

var __decorate =
	(this && this.__decorate) ||
	function (decorators, target, key, desc) {
		var c = arguments.length,
			r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc,
			d;
		if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function')
			r = Reflect.decorate(decorators, target, key, desc);
		else
			for (var i = decorators.length - 1; i >= 0; i--)
				if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
		return (c > 3 && r && Object.defineProperty(target, key, r), r);
	};
var SearchQuery_1;

function localeContains(text, query) {
	const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
	for (let offset = 0; offset <= text.length - query.length; offset++) {
		const comparison = collator.compare(text.substring(offset, offset + query.length), query);
		if (comparison === 0) return true;
	}
	return false;
}

export const SearchChange = {
	Same: 0,
	Different: 1,
	LessStrict: 2,
	MoreStrict: 3,
};

let SearchQuery = (SearchQuery_1 = class SearchQuery extends GObject.Object {
	change;
	query;
	pinned;
	excludePinned;
	tag;
	excludeTagged;
	type;

	constructor(change, query, pinned, excludePinned, tag, excludeTagged, type) {
		super();
		this.change = change;
		this.query = query;
		this.pinned = pinned;
		this.excludePinned = excludePinned;
		this.tag = tag;
		this.excludeTagged = excludeTagged;
		this.type = type;
	}

	matchesPinned(pinned) {
		return (!this.pinned && !this.excludePinned) || this.pinned === pinned;
	}

	matchesTag(tag) {
		return (this.tag === null && !this.excludeTagged) || this.tag === tag;
	}

	matchesType(type) {
		return this.type === null || this.type === type;
	}

	matchesProperties(pinned, tag, type) {
		return this.matchesPinned(pinned) && this.matchesTag(tag) && this.matchesType(type);
	}

	matchesQuery(...text) {
		if (this.query.length === 0) return true;
		if (text.length === 0) return false;
		return text.some((s) => localeContains(s, this.query));
	}

	matchesEntry(state, entry, ...text) {
		if (this.change === SearchChange.Same) return state;
		if (this.change === SearchChange.LessStrict && state) return true;
		if (this.change === SearchChange.MoreStrict && !state) return false;

		// Include entry title in search texts
		const searchTexts = entry.title ? [...text, entry.title] : text;
		return this.matchesProperties(entry.pinned, entry.tag, entry.type) && this.matchesQuery(...searchTexts);
	}

	withChange(change) {
		return new SearchQuery_1(
			change,
			this.query,
			this.pinned,
			this.excludePinned,
			this.tag,
			this.excludeTagged,
			this.type,
		);
	}
});
SearchQuery = SearchQuery_1 = __decorate(
	[
		registerClass({
			Properties: {
				'change': enumParamSpec('change', GObject.ParamFlags.READABLE, SearchChange, 0),
				'query': GObject.ParamSpec.string('query', null, null, GObject.ParamFlags.READABLE, ''),
				'pinned': GObject.ParamSpec.boolean('pinned', null, null, GObject.ParamFlags.READABLE, false),
				'exclude-pinned': GObject.ParamSpec.boolean(
					'exclude-pinned',
					null,
					null,
					GObject.ParamFlags.READABLE,
					false,
				),
				'tag': GObject.ParamSpec.string('tag', null, null, GObject.ParamFlags.READABLE, ''),
				'exclude-tagged': GObject.ParamSpec.boolean(
					'exclude-tagged',
					null,
					null,
					GObject.ParamFlags.READABLE,
					false,
				),
				'type': GObject.ParamSpec.string('type', null, null, GObject.ParamFlags.READABLE, ''),
			},
		}),
	],
	SearchQuery,
);

export { SearchQuery };

let ItemPopupMenuItem = class ItemPopupMenuItem extends PopupMenu.PopupMenuItem {
	_mnemonic = null;

	constructor(text) {
		super(text.replace(/__(.)/, (_m, s) => `<u>${s}</u>`));
		this.label.clutter_text.use_markup = true;

		// Mnemonic __.
		this._mnemonic = text.match(/__(.)/)?.[1]?.toLocaleLowerCase() ?? null;
	}

	get mnemonic() {
		return this._mnemonic;
	}
};
ItemPopupMenuItem = __decorate([registerClass()], ItemPopupMenuItem);

class ItemPopupMenu extends PopupMenu.PopupMenu {
	_tagsItem;
	_all;
	_options;
	_selected = null;

	constructor(sourceActor, arrowAlignment, arrowSide) {
		super(sourceActor, arrowAlignment, arrowSide);
		this.actor.add_style_class_name('search-popup-menu');

		// Tags
		this._tagsItem = new TagsItem();
		this._tagsItem.connect('tag-changed', () => {
			this.close(BoxPointer.PopupAnimation.FADE);
			this.emit('tag-changed', this._tagsItem.tag);
		});
		this.addMenuItem(this._tagsItem);

		// Types
		this._all = this.addItem(_('__All'), null);
		this._all.setOrnament(PopupMenu.Ornament.CHECK);
		this._options = {
			[ItemType.Text]: this.addItem(_('__Text'), ItemType.Text),
			[ItemType.Code]: this.addItem(_('__Code'), ItemType.Code),
			[ItemType.Image]: this.addItem(_('__Image'), ItemType.Image),
			[ItemType.File]: this.addItem(_('__File'), ItemType.File),
			[ItemType.Files]: this.addItem(_('File__s'), ItemType.Files),
			[ItemType.Link]: this.addItem(_('__Link'), ItemType.Link),
			[ItemType.Character]: this.addItem(_('C__haracter'), ItemType.Character),
			[ItemType.Color]: this.addItem(_('Colo__r'), ItemType.Color),
		};
		this.actor.hide();
		Main.layoutManager.uiGroup.add_child(this.actor);
		this.actor.connect('captured-event', (_actor, event) => {
			if (event.type() === Clutter.EventType.KEY_PRESS) {
				const key = event.get_key_symbol();
				const unicode = Clutter.keysym_to_unicode(key);
				if (unicode === 0) return;

				// Select tag with number
				if (key === Clutter.KEY_0 || key === Clutter.KEY_KP_0) {
					this._tagsItem.tag = null;
					this.close(BoxPointer.PopupAnimation.FADE);
					this.emit('tag-changed', null);
					return;
				}
				const isNum = key >= Clutter.KEY_1 && key <= Clutter.KEY_9;
				if (isNum || (key >= Clutter.KEY_KP_1 && key <= Clutter.KEY_KP_9)) {
					const i = isNum ? key - Clutter.KEY_1 : key - Clutter.KEY_KP_1;
					let tag = Tags[i] ?? null;
					tag = this._tagsItem.tag === tag ? null : tag;
					this._tagsItem.tag = tag;
					this.close(BoxPointer.PopupAnimation.FADE);
					this.emit('tag-changed', tag);
					return;
				}

				// Activate the item with the mnemonic
				const char = String.fromCharCode(unicode).toLocaleLowerCase();
				for (const menuItem of [this._all, ...Object.values(this._options)]) {
					if (char === menuItem.mnemonic) {
						menuItem.activate(event);
						return;
					}
				}
			}
		});
	}

	addItem(text, type) {
		const item = new ItemPopupMenuItem(text);
		item.setOrnament(PopupMenu.Ornament.NONE);
		item.connect('activate', () => (this.selected = type));
		this.addMenuItem(item);
		return item;
	}

	get tag() {
		return this._tagsItem.tag;
	}

	set tag(tag) {
		if (this._tagsItem.tag === tag) return;
		this._tagsItem.tag = tag;
		this.emit('tag-changed', tag);
	}

	get selected() {
		return this._selected;
	}

	set selected(type) {
		if (this._selected === type) return;
		this._all.setOrnament(PopupMenu.Ornament.NONE);
		for (const menuItem of Object.values(this._options)) {
			menuItem.setOrnament(PopupMenu.Ornament.NONE);
		}
		if (type) {
			this._options[type].setOrnament(PopupMenu.Ornament.CHECK);
		} else {
			this._all.setOrnament(PopupMenu.Ornament.CHECK);
		}
		this._selected = type ?? null;
		this.emit('selected-changed', this._selected);
	}
}

let SearchEntry = class SearchEntry extends St.Entry {
	ext;
	_prevSearch = null;
	_pinned = false;
	_tag = null;
	_type = null;
	_icons;
	_itemButton;
	_menu;

	constructor(ext) {
		super({
			style_class: 'clipboard-search-entry',
			hint_text: _('Type to search'),
			can_focus: true,
			x_align: Clutter.ActorAlign.CENTER,
			x_expand: true,
		});
		this.ext = ext;
		this._icons = {
			search: loadIcon(ext, Icon.Search),
			[ItemType.Text]: loadIcon(ext, Icon.Text),
			[ItemType.Code]: loadIcon(ext, Icon.Code),
			[ItemType.Image]: loadIcon(ext, Icon.Image),
			[ItemType.File]: loadIcon(ext, Icon.File),
			[ItemType.Files]: loadIcon(ext, Icon.Folder),
			[ItemType.Link]: loadIcon(ext, Icon.Link),
			[ItemType.Character]: loadIcon(ext, Icon.Character),
			[ItemType.Color]: loadIcon(ext, Icon.Color),
		};

		// Item button
		const left = new St.BoxLayout();
		this.primary_icon = left;
		left.connect('key-press-event', (_btn, event) => this._leftKeyPressEvent(event));
		this._itemButton = new St.Button({
			style_class: 'search-entry-button item-button',
			button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO,
		});
		left.add_child(this._itemButton);
		this._itemButton.connect('clicked', (_btn, button) => {
			if (button === Clutter.BUTTON_PRIMARY) {
				this._menu.toggle();
			} else if (button === Clutter.BUTTON_MIDDLE) {
				this.type = null;
				this.tag = null;
			}
		});
		const itemButtonContent = new St.BoxLayout();
		this._itemButton.add_child(itemButtonContent);
		const searchIcon = new St.Icon({
			style_class: 'search-entry-icon',
			gicon: this._icons.search,
		});
		itemButtonContent.add_child(searchIcon);
		itemButtonContent.add_child(
			new St.Icon({
				style_class: 'arrow-icon',
				icon_name: Icon.Down,
			}),
		);

		// Menu
		this._menu = new ItemPopupMenu(this._itemButton, 0.5, St.Side.TOP);
		const menuManager = new PopupMenu.PopupMenuManager(this);
		menuManager.addMenu(this._menu, 0);
		this._menu.connect('tag-changed', (_menu, tag) => {
			this.tag = tag;
			return undefined;
		});
		this._menu.connect('selected-changed', (_menu, type) => {
			this.type = type;
			if (type) {
				searchIcon.gicon = this._icons[type];
			} else {
				searchIcon.gicon = this._icons.search;
			}
			return undefined;
		});

		// Right
		const right = new St.BoxLayout();
		this.secondary_icon = right;
		right.connect('key-press-event', (_btn, event) => this._rightKeyPressEvent(event));

		// Pin button
		const pinButton = new St.Button({
			style_class: 'search-entry-button pin-button',
			toggle_mode: true,
			child: new St.Bin({
				style_class: 'pin-button-content',
				child: new St.Icon({
					style_class: 'search-entry-icon',
					gicon: loadIcon(ext, Icon.Pin),
				}),
			}),
		});
		right.add_child(pinButton);

		// Bind properties
		pinButton.bind_property('checked', this, 'pinned', GObject.BindingFlags.BIDIRECTIONAL);
		this.ext.settings.connectObject(
			'changed::exclude-pinned',
			this.search.bind(this),
			'changed::exclude-tagged',
			this.search.bind(this),
			this,
		);

		// Connect signals
		this.connect('notify::text', this.search.bind(this));
		this.clutter_text.connect('key-press-event', (_text, event) => this._keyPressEvent(event));
	}

	get pinned() {
		return this._pinned;
	}

	set pinned(pinned) {
		this._pinned = pinned;
		this.search();
		this.notify('pinned');
	}

	get tag() {
		return this._tag;
	}

	set tag(tag) {
		this._tag = tag;
		this._menu.tag = tag;
		this.search();
		this._itemButton.style_class = 'search-entry-button item-button' + (tag ? ` ${tag}` : '');
		this.notify('tag');
	}

	get type() {
		return this._type;
	}

	set type(type) {
		this._type = type;
		this._menu.selected = type;
		this.search();
		this.notify('type');
	}

	get searchQuery() {
		const excludePinned = this.ext.settings.get_boolean('exclude-pinned');
		const excludeTagged = this.ext.settings.get_boolean('exclude-tagged');
		let change;
		if (!this._prevSearch) {
			change = SearchChange.Different;
		} else {
			// Query
			let queryChange;
			if (this.text === this._prevSearch.query) queryChange = SearchChange.Same;
			else if (this.text.includes(this._prevSearch.query)) queryChange = SearchChange.MoreStrict;
			else if (this._prevSearch.query.includes(this.text)) queryChange = SearchChange.LessStrict;
			else queryChange = SearchChange.Different;
			change = queryChange;

			// Pinned
			const prevUnpinned = this._prevSearch.matchesPinned(false);
			const prevPinned = this._prevSearch.matchesPinned(true);
			const unpinned = (!this.pinned && !excludePinned) || !this.pinned;
			const pinned = (!this.pinned && !excludePinned) || this.pinned;
			let pinnedChange;
			if (prevUnpinned === unpinned && prevPinned === pinned) pinnedChange = SearchChange.Same;
			else if (prevUnpinned !== unpinned && prevPinned !== pinned) pinnedChange = SearchChange.Different;
			else if ((prevUnpinned && !unpinned) || (prevPinned && !pinned)) pinnedChange = SearchChange.MoreStrict;
			else pinnedChange = SearchChange.LessStrict;
			if (pinnedChange !== SearchChange.Same)
				change =
					change === SearchChange.Same || change === pinnedChange ? pinnedChange : SearchChange.Different;

			// Tagged
			let taggedChange;
			if (this._prevSearch.excludeTagged !== excludeTagged) taggedChange = SearchChange.Different;
			else if (this.tag !== null) {
				if (this._prevSearch.tag === null && !excludeTagged) taggedChange = SearchChange.MoreStrict;
				else if (this._prevSearch.tag === this.tag) taggedChange = SearchChange.Same;
				else taggedChange = SearchChange.Different;
			} else {
				if (this._prevSearch.tag === null) taggedChange = SearchChange.Same;
				else if (excludeTagged) taggedChange = SearchChange.Different;
				else taggedChange = SearchChange.LessStrict;
			}
			if (taggedChange !== SearchChange.Same)
				change =
					change === SearchChange.Same || change === taggedChange ? taggedChange : SearchChange.Different;

			// Type
			let typeChange;
			if (this._prevSearch.type === this.type) typeChange = SearchChange.Same;
			else if (this.type === null) typeChange = SearchChange.LessStrict;
			else if (this._prevSearch.type === null) typeChange = SearchChange.MoreStrict;
			else typeChange = SearchChange.Different;
			if (typeChange !== SearchChange.Same)
				change = change === SearchChange.Same || change === typeChange ? typeChange : SearchChange.Different;
		}
		return new SearchQuery(change, this.text, this.pinned, excludePinned, this.tag, excludeTagged, this.type);
	}

	search() {
		this._prevSearch = this.searchQuery;
		this.emit('search', this._prevSearch);
	}

	selectTag(index) {
		const tag = Tags[index] ?? null;
		this.tag = this.tag === tag ? null : tag;
	}

	nextTag() {
		this.tag = this.tag ? (Tags[Tags.indexOf(this.tag) + 1] ?? null) : Tags[0];
	}

	prevTag() {
		this.tag = this.tag ? (Tags[Tags.indexOf(this.tag) - 1] ?? null) : Tags[Tags.length - 1];
	}

	nextType() {
		this.type = this.type ? (ItemTypes[ItemTypes.indexOf(this.type) + 1] ?? null) : ItemTypes[0];
	}

	prevType() {
		this.type = this.type ? (ItemTypes[ItemTypes.indexOf(this.type) - 1] ?? null) : ItemTypes[ItemTypes.length - 1];
	}

	_keyPressEvent(event) {
		const key = event.get_key_symbol();

		// Navigate focus to type button: left + left & cursor at start
		const cursor = this.clutter_text.get_cursor_position();
		const start = cursor === 0 || (this.text.length === 0 && cursor === -1);
		if ((key === Clutter.KEY_Left || key === Clutter.KEY_KP_Left) && start) {
			this.primary_icon.first_child.grab_key_focus();
			return Clutter.EVENT_STOP;
		}

		// Navigate focus to pin button: right + right & cursor at end
		const end = cursor === this.text.length || cursor === -1;
		if ((key === Clutter.KEY_Right || key === Clutter.KEY_KP_Right) && end) {
			this.secondary_icon.first_child.grab_key_focus();
			return Clutter.EVENT_STOP;
		}

		// Toggle pin: alt
		if (key === Clutter.KEY_Alt_L || key === Clutter.KEY_Alt_R || key === Clutter.KEY_ISO_Level3_Shift) {
			this.pinned = !this.pinned;
			return Clutter.EVENT_STOP;
		}

		// Clear tag/type: backspace
		if (this.text.length === 0 && key === Clutter.KEY_BackSpace) {
			this.tag = null;
			this.type = null;
			return Clutter.EVENT_STOP;
		}

		// Submit: Enter
		if (key === Clutter.KEY_Return || key === Clutter.KEY_KP_Enter) {
			this.emit('activate');
			return Clutter.EVENT_STOP;
		}
		return Clutter.EVENT_PROPAGATE;
	}

	_leftKeyPressEvent(event) {
		const key = event.get_key_symbol();
		if (key === Clutter.KEY_Right || key === Clutter.KEY_KP_Right) {
			this.clutter_text.grab_key_focus();
			return Clutter.EVENT_STOP;
		}
		return Clutter.EVENT_PROPAGATE;
	}

	_rightKeyPressEvent(event) {
		const key = event.get_key_symbol();
		if (key === Clutter.KEY_Left || key === Clutter.KEY_KP_Left) {
			this.clutter_text.grab_key_focus();
			return Clutter.EVENT_STOP;
		}
		return Clutter.EVENT_PROPAGATE;
	}

	vfunc_style_changed() {
		super.vfunc_style_changed();
		const themeNode = this.get_theme_node();
		this.natural_width = themeNode.get_max_width();
	}

	vfunc_scroll_event(event) {
		const direction = event.get_scroll_direction();
		const swap = this.ext.settings.get_boolean('swap-scroll-shortcut');
		if (direction === Clutter.ScrollDirection.UP || direction === Clutter.ScrollDirection.LEFT) {
			if (event.has_control_modifier() !== swap) {
				this.prevTag();
			} else {
				this.prevType();
			}
		} else if (direction === Clutter.ScrollDirection.DOWN || direction === Clutter.ScrollDirection.RIGHT) {
			if (event.has_control_modifier() !== swap) {
				this.nextTag();
			} else {
				this.nextType();
			}
		}
		return Clutter.EVENT_PROPAGATE;
	}

	vfunc_unmap() {
		super.vfunc_unmap();
		if (!this.ext.settings.get_boolean('remember-search')) {
			this.text = '';
			this.pinned = false;
			this.tag = null;
			this.type = null;
		}
	}

	destroy() {
		this.ext.settings.disconnectObject(this);
		this._menu.destroy();
		super.destroy();
	}
};
SearchEntry = __decorate(
	[
		registerClass({
			Properties: {
				pinned: GObject.ParamSpec.boolean('pinned', null, null, GObject.ParamFlags.READWRITE, false),
				tag: GObject.ParamSpec.string('tag', null, null, GObject.ParamFlags.READWRITE, ''),
				type: GObject.ParamSpec.string('type', null, null, GObject.ParamFlags.READWRITE, ''),
			},
			Signals: {
				search: {
					param_types: [SearchQuery.$gtype],
				},
				activate: {},
			},
		}),
	],
	SearchEntry,
);

export { SearchEntry };
