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

'use strict';

import nls = require('vs/nls');
import { TPromise } from 'vs/base/common/winjs.base';
import types = require('vs/base/common/types');
import URI from 'vs/base/common/uri';
import { ITree, IActionProvider } from 'vs/base/parts/tree/browser/tree';
import { IconLabel, IIconLabelOptions } from 'vs/base/browser/ui/iconLabel/iconLabel';
import { IQuickNavigateConfiguration, IModel, IDataSource, IFilter, IAccessiblityProvider, IRenderer, IRunner, Mode } from 'vs/base/parts/quickopen/common/quickOpen';
import { Action, IAction, IActionRunner } from 'vs/base/common/actions';
import { compareAnything } from 'vs/base/common/comparers';
import { ActionBar, IActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import DOM = require('vs/base/browser/dom');
import { IQuickOpenStyles } from 'vs/base/parts/quickopen/browser/quickOpenWidget';
import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel';
import { OS } from 'vs/base/common/platform';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { IItemAccessor } from 'vs/base/parts/quickopen/common/quickOpenScorer';

export interface IContext {
	event: any;
	quickNavigateConfiguration: IQuickNavigateConfiguration;
}

export interface IHighlight {
	start: number;
	end: number;
}

let IDS = 0;

export class QuickOpenItemAccessorClass implements IItemAccessor<QuickOpenEntry> {

	public getItemLabel(entry: QuickOpenEntry): string {
		return entry.getLabel();
	}

	public getItemDescription(entry: QuickOpenEntry): string {
		return entry.getDescription();
	}

	public getItemPath(entry: QuickOpenEntry): string {
		const resource = entry.getResource();

		return resource ? resource.fsPath : void 0;
	}
}

export const QuickOpenItemAccessor = new QuickOpenItemAccessorClass();

export class QuickOpenEntry {
	private id: string;
	private labelHighlights: IHighlight[];
	private descriptionHighlights: IHighlight[];
	private detailHighlights: IHighlight[];
	private hidden: boolean;

	constructor(highlights: IHighlight[] = []) {
		this.id = (IDS++).toString();
		this.labelHighlights = highlights;
		this.descriptionHighlights = [];
	}

	/**
	 * A unique identifier for the entry
	 */
	public getId(): string {
		return this.id;
	}

	/**
	 * The label of the entry to identify it from others in the list
	 */
	public getLabel(): string {
		return null;
	}

	/**
	 * The options for the label to use for this entry
	 */
	public getLabelOptions(): IIconLabelOptions {
		return null;
	}

	/**
	 * The label of the entry to use when a screen reader wants to read about the entry
	 */
	public getAriaLabel(): string {
		return this.getLabel();
	}

	/**
	 * Detail information about the entry that is optional and can be shown below the label
	 */
	public getDetail(): string {
		return null;
	}

	/**
	 * The icon of the entry to identify it from others in the list
	 */
	public getIcon(): string {
		return null;
	}

	/**
	 * A secondary description that is optional and can be shown right to the label
	 */
	public getDescription(): string {
		return null;
	}

	/**
	 * An optional keybinding to show for an entry.
	 */
	public getKeybinding(): ResolvedKeybinding {
		return null;
	}

	/**
	 * A resource for this entry. Resource URIs can be used to compare different kinds of entries and group
	 * them together.
	 */
	public getResource(): URI {
		return null;
	}

	/**
	 * Allows to reuse the same model while filtering. Hidden entries will not show up in the viewer.
	 */
	public isHidden(): boolean {
		return this.hidden;
	}

	/**
	 * Allows to reuse the same model while filtering. Hidden entries will not show up in the viewer.
	 */
	public setHidden(hidden: boolean): void {
		this.hidden = hidden;
	}

	/**
	 * Allows to set highlight ranges that should show up for the entry label and optionally description if set.
	 */
	public setHighlights(labelHighlights: IHighlight[], descriptionHighlights?: IHighlight[], detailHighlights?: IHighlight[]): void {
		this.labelHighlights = labelHighlights;
		this.descriptionHighlights = descriptionHighlights;
		this.detailHighlights = detailHighlights;
	}

	/**
	 * Allows to return highlight ranges that should show up for the entry label and description.
	 */
	public getHighlights(): [IHighlight[] /* Label */, IHighlight[] /* Description */, IHighlight[] /* Detail */] {
		return [this.labelHighlights, this.descriptionHighlights, this.detailHighlights];
	}

	/**
	 * Called when the entry is selected for opening. Returns a boolean value indicating if an action was performed or not.
	 * The mode parameter gives an indication if the element is previewed (using arrow keys) or opened.
	 *
	 * The context parameter provides additional context information how the run was triggered.
	 */
	public run(mode: Mode, context: IContext): boolean {
		return false;
	}

	public isFile(): boolean {
		return false; // TODO@Ben debt with editor history merging
	}
}

export class QuickOpenEntryGroup extends QuickOpenEntry {
	private entry: QuickOpenEntry;
	private groupLabel: string;
	private withBorder: boolean;

	constructor(entry?: QuickOpenEntry, groupLabel?: string, withBorder?: boolean) {
		super();

		this.entry = entry;
		this.groupLabel = groupLabel;
		this.withBorder = withBorder;
	}

	/**
	 * The label of the group or null if none.
	 */
	public getGroupLabel(): string {
		return this.groupLabel;
	}

	public setGroupLabel(groupLabel: string): void {
		this.groupLabel = groupLabel;
	}

	/**
	 * Whether to show a border on top of the group entry or not.
	 */
	public showBorder(): boolean {
		return this.withBorder;
	}

	public setShowBorder(showBorder: boolean): void {
		this.withBorder = showBorder;
	}

	public getLabel(): string {
		return this.entry ? this.entry.getLabel() : super.getLabel();
	}

	public getLabelOptions(): IIconLabelOptions {
		return this.entry ? this.entry.getLabelOptions() : super.getLabelOptions();
	}

	public getAriaLabel(): string {
		return this.entry ? this.entry.getAriaLabel() : super.getAriaLabel();
	}

	public getDetail(): string {
		return this.entry ? this.entry.getDetail() : super.getDetail();
	}

	public getResource(): URI {
		return this.entry ? this.entry.getResource() : super.getResource();
	}

	public getIcon(): string {
		return this.entry ? this.entry.getIcon() : super.getIcon();
	}

	public getDescription(): string {
		return this.entry ? this.entry.getDescription() : super.getDescription();
	}

	public getEntry(): QuickOpenEntry {
		return this.entry;
	}

	public getHighlights(): [IHighlight[], IHighlight[], IHighlight[]] {
		return this.entry ? this.entry.getHighlights() : super.getHighlights();
	}

	public isHidden(): boolean {
		return this.entry ? this.entry.isHidden() : super.isHidden();
	}

	public setHighlights(labelHighlights: IHighlight[], descriptionHighlights?: IHighlight[], detailHighlights?: IHighlight[]): void {
		this.entry ? this.entry.setHighlights(labelHighlights, descriptionHighlights, detailHighlights) : super.setHighlights(labelHighlights, descriptionHighlights, detailHighlights);
	}

	public setHidden(hidden: boolean): void {
		this.entry ? this.entry.setHidden(hidden) : super.setHidden(hidden);
	}

	public run(mode: Mode, context: IContext): boolean {
		return this.entry ? this.entry.run(mode, context) : super.run(mode, context);
	}
}

class NoActionProvider implements IActionProvider {

	public hasActions(tree: ITree, element: any): boolean {
		return false;
	}

	public getActions(tree: ITree, element: any): TPromise<IAction[]> {
		return TPromise.as(null);
	}

	public hasSecondaryActions(tree: ITree, element: any): boolean {
		return false;
	}

	public getSecondaryActions(tree: ITree, element: any): TPromise<IAction[]> {
		return TPromise.as(null);
	}

	public getActionItem(tree: ITree, element: any, action: Action): IActionItem {
		return null;
	}
}

export interface IQuickOpenEntryTemplateData {
	container: HTMLElement;
	entry: HTMLElement;
	icon: HTMLSpanElement;
	label: IconLabel;
	detail: HighlightedLabel;
	description: HighlightedLabel;
	keybinding: KeybindingLabel;
	actionBar: ActionBar;
}

export interface IQuickOpenEntryGroupTemplateData extends IQuickOpenEntryTemplateData {
	group: HTMLDivElement;
}

const templateEntry = 'quickOpenEntry';
const templateEntryGroup = 'quickOpenEntryGroup';

class Renderer implements IRenderer<QuickOpenEntry> {

	private actionProvider: IActionProvider;
	private actionRunner: IActionRunner;

	constructor(actionProvider: IActionProvider = new NoActionProvider(), actionRunner: IActionRunner = null) {
		this.actionProvider = actionProvider;
		this.actionRunner = actionRunner;
	}

	public getHeight(entry: QuickOpenEntry): number {
		if (entry.getDetail()) {
			return 44;
		}

		return 22;
	}

	public getTemplateId(entry: QuickOpenEntry): string {
		if (entry instanceof QuickOpenEntryGroup) {
			return templateEntryGroup;
		}

		return templateEntry;
	}

	public renderTemplate(templateId: string, container: HTMLElement, styles: IQuickOpenStyles): IQuickOpenEntryGroupTemplateData {
		const entryContainer = document.createElement('div');
		DOM.addClass(entryContainer, 'sub-content');
		container.appendChild(entryContainer);

		// Entry
		const row1 = DOM.$('.quick-open-row');
		const row2 = DOM.$('.quick-open-row');
		const entry = DOM.$('.quick-open-entry', null, row1, row2);
		entryContainer.appendChild(entry);

		// Icon
		const icon = document.createElement('span');
		row1.appendChild(icon);

		// Label
		const label = new IconLabel(row1, { supportHighlights: true });

		// Description
		const descriptionContainer = document.createElement('span');
		row1.appendChild(descriptionContainer);
		DOM.addClass(descriptionContainer, 'quick-open-entry-description');
		const description = new HighlightedLabel(descriptionContainer);

		// Keybinding
		const keybindingContainer = document.createElement('span');
		row1.appendChild(keybindingContainer);
		DOM.addClass(keybindingContainer, 'quick-open-entry-keybinding');
		const keybinding = new KeybindingLabel(keybindingContainer, OS);

		// Detail
		const detailContainer = document.createElement('div');
		row2.appendChild(detailContainer);
		DOM.addClass(detailContainer, 'quick-open-entry-meta');
		const detail = new HighlightedLabel(detailContainer);

		// Entry Group
		let group: HTMLDivElement;
		if (templateId === templateEntryGroup) {
			group = document.createElement('div');
			DOM.addClass(group, 'results-group');
			container.appendChild(group);
		}

		// Actions
		DOM.addClass(container, 'actions');

		const actionBarContainer = document.createElement('div');
		DOM.addClass(actionBarContainer, 'primary-action-bar');
		container.appendChild(actionBarContainer);

		const actionBar = new ActionBar(actionBarContainer, {
			actionRunner: this.actionRunner
		});

		return {
			container,
			entry,
			icon,
			label,
			detail,
			description,
			keybinding,
			group,
			actionBar
		};
	}

	public renderElement(entry: QuickOpenEntry, templateId: string, templateData: any, styles: IQuickOpenStyles): void {
		const data: IQuickOpenEntryTemplateData = templateData;

		// Action Bar
		if (this.actionProvider.hasActions(null, entry)) {
			DOM.addClass(data.container, 'has-actions');
		} else {
			DOM.removeClass(data.container, 'has-actions');
		}

		data.actionBar.context = entry; // make sure the context is the current element

		this.actionProvider.getActions(null, entry).then((actions) => {
			// TODO@Ben this will not work anymore as soon as quick open has more actions
			// but as long as there is only one are ok
			if (data.actionBar.isEmpty() && actions && actions.length > 0) {
				data.actionBar.push(actions, { icon: true, label: false });
			} else if (!data.actionBar.isEmpty() && (!actions || actions.length === 0)) {
				data.actionBar.clear();
			}
		});

		// Entry group class
		if (entry instanceof QuickOpenEntryGroup && entry.getGroupLabel()) {
			DOM.addClass(data.container, 'has-group-label');
		} else {
			DOM.removeClass(data.container, 'has-group-label');
		}

		// Entry group
		if (entry instanceof QuickOpenEntryGroup) {
			const group = <QuickOpenEntryGroup>entry;
			const groupData = <IQuickOpenEntryGroupTemplateData>templateData;

			// Border
			if (group.showBorder()) {
				DOM.addClass(groupData.container, 'results-group-separator');
				groupData.container.style.borderTopColor = styles.pickerGroupBorder.toString();
			} else {
				DOM.removeClass(groupData.container, 'results-group-separator');
				groupData.container.style.borderTopColor = null;
			}

			// Group Label
			const groupLabel = group.getGroupLabel() || '';
			groupData.group.textContent = groupLabel;
			groupData.group.style.color = styles.pickerGroupForeground.toString();
		}

		// Normal Entry
		if (entry instanceof QuickOpenEntry) {
			const [labelHighlights, descriptionHighlights, detailHighlights] = entry.getHighlights();

			// Icon
			const iconClass = entry.getIcon() ? ('quick-open-entry-icon ' + entry.getIcon()) : '';
			data.icon.className = iconClass;

			// Label
			const options: IIconLabelOptions = entry.getLabelOptions() || Object.create(null);
			options.matches = labelHighlights || [];
			data.label.setValue(entry.getLabel(), null, options);

			// Meta
			data.detail.set(entry.getDetail(), detailHighlights);

			// Description
			data.description.set(entry.getDescription(), descriptionHighlights || []);
			data.description.element.title = entry.getDescription();

			// Keybinding
			data.keybinding.set(entry.getKeybinding(), null);
		}
	}

	public disposeTemplate(templateId: string, templateData: any): void {
		const data = templateData as IQuickOpenEntryGroupTemplateData;
		data.actionBar.dispose();
		data.actionBar = null;
		data.container = null;
		data.entry = null;
		data.description.dispose();
		data.description = null;
		data.keybinding.dispose();
		data.keybinding = null;
		data.detail.dispose();
		data.detail = null;
		data.group = null;
		data.icon = null;
		data.label.dispose();
		data.label = null;
	}
}

export class QuickOpenModel implements
	IModel<QuickOpenEntry>,
	IDataSource<QuickOpenEntry>,
	IFilter<QuickOpenEntry>,
	IRunner<QuickOpenEntry>
{
	private _entries: QuickOpenEntry[];
	private _dataSource: IDataSource<QuickOpenEntry>;
	private _renderer: IRenderer<QuickOpenEntry>;
	private _filter: IFilter<QuickOpenEntry>;
	private _runner: IRunner<QuickOpenEntry>;
	private _accessibilityProvider: IAccessiblityProvider<QuickOpenEntry>;

	constructor(entries: QuickOpenEntry[] = [], actionProvider: IActionProvider = new NoActionProvider()) {
		this._entries = entries;
		this._dataSource = this;
		this._renderer = new Renderer(actionProvider);
		this._filter = this;
		this._runner = this;
		this._accessibilityProvider = this;
	}

	public get entries() { return this._entries; }
	public get dataSource() { return this._dataSource; }
	public get renderer() { return this._renderer; }
	public get filter() { return this._filter; }
	public get runner() { return this._runner; }
	public get accessibilityProvider() { return this._accessibilityProvider; }

	public set entries(entries: QuickOpenEntry[]) {
		this._entries = entries;
	}

	/**
	 * Adds entries that should show up in the quick open viewer.
	 */
	public addEntries(entries: QuickOpenEntry[]): void {
		if (types.isArray(entries)) {
			this._entries = this._entries.concat(entries);
		}
	}

	/**
	 * Set the entries that should show up in the quick open viewer.
	 */
	public setEntries(entries: QuickOpenEntry[]): void {
		if (types.isArray(entries)) {
			this._entries = entries;
		}
	}

	/**
	 * Get the entries that should show up in the quick open viewer.
	 *
	 * @visibleOnly optional parameter to only return visible entries
	 */
	public getEntries(visibleOnly?: boolean): QuickOpenEntry[] {
		if (visibleOnly) {
			return this._entries.filter((e) => !e.isHidden());
		}

		return this._entries;
	}

	public getId(entry: QuickOpenEntry): string {
		return entry.getId();
	}

	public getLabel(entry: QuickOpenEntry): string {
		return entry.getLabel();
	}

	public getAriaLabel(entry: QuickOpenEntry): string {
		const ariaLabel = entry.getAriaLabel();
		if (ariaLabel) {
			return nls.localize('quickOpenAriaLabelEntry', "{0}, picker", entry.getAriaLabel());
		}

		return nls.localize('quickOpenAriaLabel', "picker");
	}

	public isVisible(entry: QuickOpenEntry): boolean {
		return !entry.isHidden();
	}

	public run(entry: QuickOpenEntry, mode: Mode, context: IContext): boolean {
		return entry.run(mode, context);
	}
}

/**
 * A good default sort implementation for quick open entries respecting highlight information
 * as well as associated resources.
 */
export function compareEntries(elementA: QuickOpenEntry, elementB: QuickOpenEntry, lookFor: string): number {

	// Give matches with label highlights higher priority over
	// those with only description highlights
	const labelHighlightsA = elementA.getHighlights()[0] || [];
	const labelHighlightsB = elementB.getHighlights()[0] || [];
	if (labelHighlightsA.length && !labelHighlightsB.length) {
		return -1;
	}

	if (!labelHighlightsA.length && labelHighlightsB.length) {
		return 1;
	}

	// Fallback to the full path if labels are identical and we have associated resources
	let nameA = elementA.getLabel();
	let nameB = elementB.getLabel();
	if (nameA === nameB) {
		const resourceA = elementA.getResource();
		const resourceB = elementB.getResource();

		if (resourceA && resourceB) {
			nameA = resourceA.fsPath;
			nameB = resourceB.fsPath;
		}
	}

	return compareAnything(nameA, nameB, lookFor);
}