/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as nls from 'vs/nls';
import * as Objects from 'vs/base/common/objects';
import { Task, ContributedTask, CustomTask, ConfiguringTask, TaskSorter, KeyedTaskIdentifier } from 'vs/workbench/contrib/tasks/common/tasks';
import { IWorkspace, IWorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import * as Types from 'vs/base/common/types';
import { ITaskService, IWorkspaceFolderTaskResult } from 'vs/workbench/contrib/tasks/common/taskService';
import { IQuickPickItem, QuickPickInput, IQuickPick, IQuickInputButton, IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
import { Event } from 'vs/base/common/event';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { Codicon } from 'vs/base/common/codicons';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ThemeIcon } from 'vs/base/common/themables';
import { registerIcon } from 'vs/platform/theme/common/iconRegistry';
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
import { getColorClass, createColorStyleElement } from 'vs/workbench/contrib/terminal/browser/terminalIcon';
import { TaskQuickPickEntryType } from 'vs/workbench/contrib/tasks/browser/abstractTaskService';
import { showWithPinnedItems } from 'vs/platform/quickinput/browser/quickPickPin';
import { IStorageService } from 'vs/platform/storage/common/storage';

export const QUICKOPEN_DETAIL_CONFIG = 'task.quickOpen.detail';
export const QUICKOPEN_SKIP_CONFIG = 'task.quickOpen.skip';
export function isWorkspaceFolder(folder: IWorkspace | IWorkspaceFolder): folder is IWorkspaceFolder {
	return 'uri' in folder;
}

export interface ITaskQuickPickEntry extends IQuickPickItem {
	task: Task | undefined | null;
}

export interface ITaskTwoLevelQuickPickEntry extends IQuickPickItem {
	task: Task | ConfiguringTask | string | undefined | null;
	settingType?: string;
}

const SHOW_ALL: string = nls.localize('taskQuickPick.showAll', "Show All Tasks...");

export const configureTaskIcon = registerIcon('tasks-list-configure', Codicon.gear, nls.localize('configureTaskIcon', 'Configuration icon in the tasks selection list.'));
const removeTaskIcon = registerIcon('tasks-remove', Codicon.close, nls.localize('removeTaskIcon', 'Icon for remove in the tasks selection list.'));

const runTaskStorageKey = 'runTaskStorageKey';

export class TaskQuickPick extends Disposable {
	private _sorter: TaskSorter;
	private _topLevelEntries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[] | undefined;
	constructor(
		@ITaskService private _taskService: ITaskService,
		@IConfigurationService private _configurationService: IConfigurationService,
		@IQuickInputService private _quickInputService: IQuickInputService,
		@INotificationService private _notificationService: INotificationService,
		@IThemeService private _themeService: IThemeService,
		@IDialogService private _dialogService: IDialogService,
		@IStorageService private _storageService: IStorageService) {
		super();
		this._sorter = this._taskService.createSorter();
	}

	private _showDetail(): boolean {
		// Ensure invalid values get converted into boolean values
		return !!this._configurationService.getValue(QUICKOPEN_DETAIL_CONFIG);
	}

	private _guessTaskLabel(task: Task | ConfiguringTask): string {
		if (task._label) {
			return task._label;
		}
		if (ConfiguringTask.is(task)) {
			let label: string = task.configures.type;
			const configures: Partial<KeyedTaskIdentifier> = Objects.deepClone(task.configures);
			delete configures['_key'];
			delete configures['type'];
			Object.keys(configures).forEach(key => label += `: ${configures[key]}`);
			return label;
		}
		return '';
	}

	public static getTaskLabelWithIcon(task: Task | ConfiguringTask, labelGuess?: string): string {
		const label = labelGuess || task._label;
		const icon = task.configurationProperties.icon;
		if (!icon) {
			return `${label}`;
		}
		return icon.id ? `$(${icon.id}) ${label}` : `$(${Codicon.tools.id}) ${label}`;
	}

	public static applyColorStyles(task: Task | ConfiguringTask, entry: TaskQuickPickEntryType | ITaskTwoLevelQuickPickEntry, themeService: IThemeService): IDisposable | undefined {
		if (task.configurationProperties.icon?.color) {
			const colorTheme = themeService.getColorTheme();
			const disposable = createColorStyleElement(colorTheme);
			entry.iconClasses = [getColorClass(task.configurationProperties.icon.color)];
			return disposable;
		}
		return;
	}

	private _createTaskEntry(task: Task | ConfiguringTask, extraButtons: IQuickInputButton[] = []): ITaskTwoLevelQuickPickEntry {
		const buttons: IQuickInputButton[] = [
			{ iconClass: ThemeIcon.asClassName(configureTaskIcon), tooltip: nls.localize('configureTask', "Configure Task") },
			...extraButtons
		];
		const entry: ITaskTwoLevelQuickPickEntry = { label: TaskQuickPick.getTaskLabelWithIcon(task, this._guessTaskLabel(task)), description: this._taskService.getTaskDescription(task), task, detail: this._showDetail() ? task.configurationProperties.detail : undefined, buttons };
		const disposable = TaskQuickPick.applyColorStyles(task, entry, this._themeService);
		if (disposable) {
			this._register(disposable);
		}
		return entry;
	}

	private _createEntriesForGroup(entries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[], tasks: (Task | ConfiguringTask)[],
		groupLabel: string, extraButtons: IQuickInputButton[] = []) {
		entries.push({ type: 'separator', label: groupLabel });
		tasks.forEach(task => {
			if (!task.configurationProperties.hide) {
				entries.push(this._createTaskEntry(task, extraButtons));
			}
		});
	}

	private _createTypeEntries(entries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[], types: string[]) {
		entries.push({ type: 'separator', label: nls.localize('contributedTasks', "contributed") });
		types.forEach(type => {
			entries.push({ label: `$(folder) ${type}`, task: type, ariaLabel: nls.localize('taskType', "All {0} tasks", type) });
		});
		entries.push({ label: SHOW_ALL, task: SHOW_ALL, alwaysShow: true });
	}

	private _handleFolderTaskResult(result: Map<string, IWorkspaceFolderTaskResult>): (Task | ConfiguringTask)[] {
		const tasks: (Task | ConfiguringTask)[] = [];
		Array.from(result).forEach(([key, folderTasks]) => {
			if (folderTasks.set) {
				tasks.push(...folderTasks.set.tasks);
			}
			if (folderTasks.configurations) {
				for (const configuration in folderTasks.configurations.byIdentifier) {
					tasks.push(folderTasks.configurations.byIdentifier[configuration]);
				}
			}
		});
		return tasks;
	}

	private _dedupeConfiguredAndRecent(recentTasks: (Task | ConfiguringTask)[], configuredTasks: (Task | ConfiguringTask)[]): { configuredTasks: (Task | ConfiguringTask)[]; recentTasks: (Task | ConfiguringTask)[] } {
		let dedupedConfiguredTasks: (Task | ConfiguringTask)[] = [];
		const foundRecentTasks: boolean[] = Array(recentTasks.length).fill(false);
		for (let j = 0; j < configuredTasks.length; j++) {
			const workspaceFolder = configuredTasks[j].getWorkspaceFolder()?.uri.toString();
			const definition = configuredTasks[j].getDefinition()?._key;
			const type = configuredTasks[j].type;
			const label = configuredTasks[j]._label;
			const recentKey = configuredTasks[j].getKey();
			const findIndex = recentTasks.findIndex((value) => {
				return (workspaceFolder && definition && value.getWorkspaceFolder()?.uri.toString() === workspaceFolder
					&& ((value.getDefinition()?._key === definition) || (value.type === type && value._label === label)))
					|| (recentKey && value.getKey() === recentKey);
			});
			if (findIndex === -1) {
				dedupedConfiguredTasks.push(configuredTasks[j]);
			} else {
				recentTasks[findIndex] = configuredTasks[j];
				foundRecentTasks[findIndex] = true;
			}
		}
		dedupedConfiguredTasks = dedupedConfiguredTasks.sort((a, b) => this._sorter.compare(a, b));
		const prunedRecentTasks: (Task | ConfiguringTask)[] = [];
		for (let i = 0; i < recentTasks.length; i++) {
			if (foundRecentTasks[i] || ConfiguringTask.is(recentTasks[i])) {
				prunedRecentTasks.push(recentTasks[i]);
			}
		}
		return { configuredTasks: dedupedConfiguredTasks, recentTasks: prunedRecentTasks };
	}

	public async getTopLevelEntries(defaultEntry?: ITaskQuickPickEntry): Promise<{ entries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[]; isSingleConfigured?: Task | ConfiguringTask }> {
		if (this._topLevelEntries !== undefined) {
			return { entries: this._topLevelEntries };
		}
		let recentTasks: (Task | ConfiguringTask)[] = (await this._taskService.getSavedTasks('historical')).reverse();
		const configuredTasks: (Task | ConfiguringTask)[] = this._handleFolderTaskResult(await this._taskService.getWorkspaceTasks());
		const extensionTaskTypes = this._taskService.taskTypes();
		this._topLevelEntries = [];
		// Dedupe will update recent tasks if they've changed in tasks.json.
		const dedupeAndPrune = this._dedupeConfiguredAndRecent(recentTasks, configuredTasks);
		const dedupedConfiguredTasks: (Task | ConfiguringTask)[] = dedupeAndPrune.configuredTasks;
		recentTasks = dedupeAndPrune.recentTasks;
		if (recentTasks.length > 0) {
			const removeRecentButton: IQuickInputButton = {
				iconClass: ThemeIcon.asClassName(removeTaskIcon),
				tooltip: nls.localize('removeRecent', 'Remove Recently Used Task')
			};
			this._createEntriesForGroup(this._topLevelEntries, recentTasks, nls.localize('recentlyUsed', 'recently used'), [removeRecentButton]);
		}
		if (configuredTasks.length > 0) {
			if (dedupedConfiguredTasks.length > 0) {
				this._createEntriesForGroup(this._topLevelEntries, dedupedConfiguredTasks, nls.localize('configured', 'configured'));
			}
		}

		if (defaultEntry && (configuredTasks.length === 0)) {
			this._topLevelEntries.push({ type: 'separator', label: nls.localize('configured', 'configured') });
			this._topLevelEntries.push(defaultEntry);
		}

		if (extensionTaskTypes.length > 0) {
			this._createTypeEntries(this._topLevelEntries, extensionTaskTypes);
		}
		return { entries: this._topLevelEntries, isSingleConfigured: configuredTasks.length === 1 ? configuredTasks[0] : undefined };
	}

	public async handleSettingOption(selectedType: string) {
		const { confirmed } = await this._dialogService.confirm({
			type: Severity.Warning,
			message: nls.localize('TaskQuickPick.changeSettingDetails',
				"Task detection for {0} tasks causes files in any workspace you open to be run as code. Enabling {0} task detection is a user setting and will apply to any workspace you open. \n\n Do you want to enable {0} task detection for all workspaces?", selectedType),
			cancelButton: nls.localize('TaskQuickPick.changeSettingNo', "No")
		});
		if (confirmed) {
			await this._configurationService.updateValue(`${selectedType}.autoDetect`, 'on');
			await new Promise<void>(resolve => setTimeout(() => resolve(), 100));
			return this.show(nls.localize('TaskService.pickRunTask', 'Select the task to run'), undefined, selectedType);
		}
		return undefined;
	}

	public async show(placeHolder: string, defaultEntry?: ITaskQuickPickEntry, startAtType?: string, name?: string): Promise<Task | undefined | null> {
		const picker: IQuickPick<ITaskTwoLevelQuickPickEntry> = this._quickInputService.createQuickPick();
		picker.placeholder = placeHolder;
		picker.matchOnDescription = true;
		picker.ignoreFocusOut = false;
		picker.onDidTriggerItemButton(async (context) => {
			const task = context.item.task;
			if (context.button.iconClass === ThemeIcon.asClassName(removeTaskIcon)) {
				const key = (task && !Types.isString(task)) ? task.getKey() : undefined;
				if (key) {
					this._taskService.removeRecentlyUsedTask(key);
				}
				const indexToRemove = picker.items.indexOf(context.item);
				if (indexToRemove >= 0) {
					picker.items = [...picker.items.slice(0, indexToRemove), ...picker.items.slice(indexToRemove + 1)];
				}
			} else if (context.button.iconClass === ThemeIcon.asClassName(configureTaskIcon)) {
				this._quickInputService.cancel();
				if (ContributedTask.is(task)) {
					this._taskService.customize(task, undefined, true);
				} else if (CustomTask.is(task) || ConfiguringTask.is(task)) {
					let canOpenConfig: boolean = false;
					try {
						canOpenConfig = await this._taskService.openConfig(task);
					} catch (e) {
						// do nothing.
					}
					if (!canOpenConfig) {
						this._taskService.customize(task, undefined, true);
					}
				}
			}
		});
		if (name) {
			picker.value = name;
		}
		let firstLevelTask: Task | ConfiguringTask | string | undefined | null = startAtType;
		if (!firstLevelTask) {
			// First show recent tasks configured tasks. Other tasks will be available at a second level
			const topLevelEntriesResult = await this.getTopLevelEntries(defaultEntry);
			if (topLevelEntriesResult.isSingleConfigured && this._configurationService.getValue<boolean>(QUICKOPEN_SKIP_CONFIG)) {
				picker.dispose();
				return this._toTask(topLevelEntriesResult.isSingleConfigured);
			}
			const taskQuickPickEntries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[] = topLevelEntriesResult.entries;
			firstLevelTask = await this._doPickerFirstLevel(picker, taskQuickPickEntries);
		}
		do {
			if (Types.isString(firstLevelTask)) {
				if (name) {
					await this._doPickerFirstLevel(picker, (await this.getTopLevelEntries(defaultEntry)).entries);
					picker.dispose();
					return undefined;
				}
				const selectedEntry = await this.doPickerSecondLevel(picker, firstLevelTask);
				// Proceed to second level of quick pick
				if (selectedEntry && !selectedEntry.settingType && selectedEntry.task === null) {
					// The user has chosen to go back to the first level
					picker.value = '';
					firstLevelTask = await this._doPickerFirstLevel(picker, (await this.getTopLevelEntries(defaultEntry)).entries);
				} else if (selectedEntry && Types.isString(selectedEntry.settingType)) {
					picker.dispose();
					return this.handleSettingOption(selectedEntry.settingType);
				} else {
					picker.dispose();
					return (selectedEntry?.task && !Types.isString(selectedEntry?.task)) ? this._toTask(selectedEntry?.task) : undefined;
				}
			} else if (firstLevelTask) {
				picker.dispose();
				return this._toTask(firstLevelTask);
			} else {
				picker.dispose();
				return firstLevelTask;
			}
		} while (1);
		return;
	}



	private async _doPickerFirstLevel(picker: IQuickPick<ITaskTwoLevelQuickPickEntry>, taskQuickPickEntries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[]): Promise<Task | ConfiguringTask | string | null | undefined> {
		picker.items = taskQuickPickEntries;
		showWithPinnedItems(this._storageService, runTaskStorageKey, picker, true);
		const firstLevelPickerResult = await new Promise<ITaskTwoLevelQuickPickEntry | undefined | null>(resolve => {
			Event.once(picker.onDidAccept)(async () => {
				resolve(picker.selectedItems ? picker.selectedItems[0] : undefined);
			});
		});
		return firstLevelPickerResult?.task;
	}

	public async doPickerSecondLevel(picker: IQuickPick<ITaskTwoLevelQuickPickEntry>, type: string, name?: string) {
		picker.busy = true;
		if (type === SHOW_ALL) {
			const items = (await this._taskService.tasks()).filter(t => !t.configurationProperties.hide).sort((a, b) => this._sorter.compare(a, b)).map(task => this._createTaskEntry(task));
			items.push(...TaskQuickPick.allSettingEntries(this._configurationService));
			picker.items = items;
		} else {
			picker.value = name || '';
			picker.items = await this._getEntriesForProvider(type);
		}
		await picker.show();
		picker.busy = false;
		const secondLevelPickerResult = await new Promise<ITaskTwoLevelQuickPickEntry | undefined | null>(resolve => {
			Event.once(picker.onDidAccept)(async () => {
				resolve(picker.selectedItems ? picker.selectedItems[0] : undefined);
			});
		});
		return secondLevelPickerResult;
	}

	public static allSettingEntries(configurationService: IConfigurationService): (ITaskTwoLevelQuickPickEntry & { settingType: string })[] {
		const entries: (ITaskTwoLevelQuickPickEntry & { settingType: string })[] = [];
		const gruntEntry = TaskQuickPick.getSettingEntry(configurationService, 'grunt');
		if (gruntEntry) {
			entries.push(gruntEntry);
		}
		const gulpEntry = TaskQuickPick.getSettingEntry(configurationService, 'gulp');
		if (gulpEntry) {
			entries.push(gulpEntry);
		}
		const jakeEntry = TaskQuickPick.getSettingEntry(configurationService, 'jake');
		if (jakeEntry) {
			entries.push(jakeEntry);
		}
		return entries;
	}

	public static getSettingEntry(configurationService: IConfigurationService, type: string): (ITaskTwoLevelQuickPickEntry & { settingType: string }) | undefined {
		if (configurationService.getValue(`${type}.autoDetect`) === 'off') {
			return {
				label: nls.localize('TaskQuickPick.changeSettingsOptions', "$(gear) {0} task detection is turned off. Enable {1} task detection...",
					type[0].toUpperCase() + type.slice(1), type),
				task: null,
				settingType: type,
				alwaysShow: true
			};
		}
		return undefined;
	}

	private async _getEntriesForProvider(type: string): Promise<QuickPickInput<ITaskTwoLevelQuickPickEntry>[]> {
		const tasks = (await this._taskService.tasks({ type })).sort((a, b) => this._sorter.compare(a, b));
		let taskQuickPickEntries: QuickPickInput<ITaskTwoLevelQuickPickEntry>[] = [];
		if (tasks.length > 0) {
			for (const task of tasks) {
				if (!task.configurationProperties.hide) {
					taskQuickPickEntries.push(this._createTaskEntry(task));
				}
			}
			taskQuickPickEntries.push({
				type: 'separator'
			}, {
				label: nls.localize('TaskQuickPick.goBack', 'Go back ↩'),
				task: null,
				alwaysShow: true
			});
		} else {
			taskQuickPickEntries = [{
				label: nls.localize('TaskQuickPick.noTasksForType', 'No {0} tasks found. Go back ↩', type),
				task: null,
				alwaysShow: true
			}];
		}

		const settingEntry = TaskQuickPick.getSettingEntry(this._configurationService, type);
		if (settingEntry) {
			taskQuickPickEntries.push(settingEntry);
		}
		return taskQuickPickEntries;
	}

	private async _toTask(task: Task | ConfiguringTask): Promise<Task | undefined> {
		if (!ConfiguringTask.is(task)) {
			return task;
		}

		const resolvedTask = await this._taskService.tryResolveTask(task);

		if (!resolvedTask) {
			this._notificationService.error(nls.localize('noProviderForTask', "There is no task provider registered for tasks of type \"{0}\".", task.type));
		}
		return resolvedTask;
	}
}
