import { Disposable } from 'vscode';
import { debug } from '@gitlens/utils/decorators/log.js';
import { getScopedLogger } from '@gitlens/utils/logger.scoped.js';
import type { Keys } from '../../constants.js';
import { extensionPrefix, keys } from '../../constants.js';
import { registerCommand } from './command.js';
import { setContext } from './context.js';

export declare interface KeyCommand {
	onDidPressKey?(key: Keys): void | Promise<void>;
}

const keyNoopCommand = Object.create(null) as KeyCommand;
export { keyNoopCommand as KeyNoopCommand };

export type KeyMapping = { [K in Keys]?: KeyCommand | (() => Promise<KeyCommand>) };
type IndexableKeyMapping = KeyMapping & Record<string, KeyCommand | (() => Promise<KeyCommand>) | undefined>;

const mappings: KeyMapping[] = [];

export class KeyboardScope implements Disposable {
	private readonly _mapping: IndexableKeyMapping;
	constructor(mapping: KeyMapping) {
		this._mapping = mapping;
		for (const key in this._mapping) {
			this._mapping[key] = this._mapping[key] ?? keyNoopCommand;
		}

		mappings.push(this._mapping);
	}

	@debug({
		args: false,
		prefix: context => `${context.prefix}[${mappings.length}]`,
	})
	async dispose(): Promise<void> {
		const index = mappings.indexOf(this._mapping);

		const scope = getScopedLogger();
		scope?.addExitInfo(`index=${index}`);

		if (index === mappings.length - 1) {
			mappings.pop();
			await this.updateKeyCommandsContext(mappings.at(-1)!);
		} else {
			mappings.splice(index, 1);
		}
	}

	private _paused = true;
	get paused(): boolean {
		return this._paused;
	}

	@debug({
		args: false,
		prefix: (context, key) => `${context.prefix}[${mappings.length}](${key})`,
	})
	async clearKeyCommand(key: Keys): Promise<void> {
		const scope = getScopedLogger();

		const mapping = mappings.at(-1);
		if (mapping !== this._mapping || mapping[key] == null) {
			scope?.addExitInfo('skipped');

			return;
		}

		mapping[key] = undefined;
		await setContext(`${extensionPrefix}:key:${key}`, false);
	}

	@debug({
		args: false,
		prefix: context => `${context.prefix}(paused=${context.instance._paused})`,
	})
	async pause(keys?: Keys[]): Promise<void> {
		if (this._paused) return;

		this._paused = true;
		const mapping = (Object.keys(this._mapping) as Keys[]).reduce<KeyMapping>((accumulator, key) => {
			accumulator[key] = keys == null || keys.includes(key) ? undefined : this._mapping[key];
			return accumulator;
		}, Object.create(null));

		await this.updateKeyCommandsContext(mapping);
	}

	@debug({
		args: false,
		prefix: context => `${context.prefix}(paused=${context.instance._paused})`,
	})
	async resume(): Promise<void> {
		if (!this._paused) return;

		this._paused = false;
		await this.updateKeyCommandsContext(this._mapping);
	}

	async start(): Promise<void> {
		await this.resume();
	}

	@debug({
		args: false,
		prefix: (context, key) => `${context.prefix}[${mappings.length}](${key})`,
	})
	async setKeyCommand(key: Keys, command: KeyCommand | (() => Promise<KeyCommand>)): Promise<void> {
		const scope = getScopedLogger();

		const mapping = mappings.at(-1);
		if (mapping !== this._mapping) {
			scope?.addExitInfo('skipped');

			return;
		}

		const set = Boolean(mapping[key]);

		mapping[key] = command;
		if (!set) {
			await setContext(`${extensionPrefix}:key:${key}`, true);
		}
	}

	private async updateKeyCommandsContext(mapping: KeyMapping) {
		await Promise.all(keys.map(key => setContext(`${extensionPrefix}:key:${key}`, Boolean(mapping?.[key]))));
	}
}

export class Keyboard implements Disposable {
	private readonly _disposable: Disposable;

	constructor() {
		const subscriptions = keys.map(key =>
			registerCommand(`${extensionPrefix}.key.${key}`, () => this.execute(key), this),
		);
		this._disposable = Disposable.from(...subscriptions);
	}

	dispose(): void {
		this._disposable.dispose();
	}

	@debug({
		args: false,
		prefix: (context, mapping) =>
			`${context.prefix}[${mappings.length}](${mapping === undefined ? '' : Object.keys(mapping).join(',')})`,
	})
	createScope(mapping?: KeyMapping): KeyboardScope {
		return new KeyboardScope({ ...mapping });
	}

	@debug({
		args: false,
		prefix: (context, mapping) =>
			`${context.prefix}[${mappings.length}](${mapping === undefined ? '' : Object.keys(mapping).join(',')})`,
	})
	async beginScope(mapping?: KeyMapping): Promise<KeyboardScope> {
		const scope = this.createScope(mapping);
		await scope.start();
		return scope;
	}

	@debug()
	async execute(key: Keys): Promise<void> {
		const scope = getScopedLogger();

		if (!mappings.length) {
			scope?.addExitInfo('skipped, no mappings');

			return;
		}

		try {
			const mapping = mappings.at(-1)!;

			let command = mapping[key] as KeyCommand | (() => Promise<KeyCommand>);
			if (typeof command === 'function') {
				command = await command();
			}
			if (typeof command?.onDidPressKey !== 'function') {
				scope?.addExitInfo('skipped, no callback');

				return;
			}

			await command.onDidPressKey(key);
		} catch (ex) {
			scope?.error(ex);
		}
	}
}
