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

import * as nls from 'vs/nls';
import { Action } from 'vs/base/common/actions';
import * as platform from 'vs/base/common/platform';
import * as touch from 'vs/base/browser/touch';
import * as errors from 'vs/base/common/errors';
import * as dom from 'vs/base/browser/dom';
import * as mouse from 'vs/base/browser/mouseEvent';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import * as _ from 'sql/base/parts/tree/browser/tree';
import { IDragAndDropData } from 'vs/base/browser/dnd';
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { Keybinding, decodeKeybinding } from 'vs/base/common/keybindings';

export interface IKeyBindingCallback {
	(tree: _.ITree, event: IKeyboardEvent): void;
}

export interface ICancelableEvent {
	preventDefault(): void;
	stopPropagation(): void;
}

export const enum ClickBehavior {

	/**
	 * Handle the click when the mouse button is pressed but not released yet.
	 */
	ON_MOUSE_DOWN,

	/**
	 * Handle the click when the mouse button is released.
	 */
	ON_MOUSE_UP
}

export const enum OpenMode {
	SINGLE_CLICK,
	DOUBLE_CLICK
}

export interface IControllerOptions {
	clickBehavior?: ClickBehavior;
	openMode?: OpenMode;
	keyboardSupport?: boolean;
}

interface IKeybindingDispatcherItem {
	keybinding: Keybinding | null;
	callback: IKeyBindingCallback;
}

export class KeybindingDispatcher {

	private _arr: IKeybindingDispatcherItem[];

	constructor() {
		this._arr = [];
	}

	public has(keybinding: KeyCode): boolean {
		let target = decodeKeybinding(keybinding, platform.OS);
		if (target !== null) {
			for (const a of this._arr) {
				if (target.equals(a.keybinding)) {
					return true;
				}
			}
		}
		return false;
	}

	public set(keybinding: number, callback: IKeyBindingCallback) {
		this._arr.push({
			keybinding: decodeKeybinding(keybinding, platform.OS),
			callback: callback
		});
	}

	public dispatch(keybinding: Keybinding): IKeyBindingCallback | null {
		// Loop from the last to the first to handle overwrites
		for (let i = this._arr.length - 1; i >= 0; i--) {
			let item = this._arr[i];
			if (keybinding.equals(item.keybinding)) {
				return item.callback;
			}
		}
		return null;
	}
}

export class DefaultController implements _.IController {

	protected downKeyBindingDispatcher: KeybindingDispatcher;
	protected upKeyBindingDispatcher: KeybindingDispatcher;

	private options: IControllerOptions;

	constructor(options: IControllerOptions = { clickBehavior: ClickBehavior.ON_MOUSE_DOWN, keyboardSupport: true, openMode: OpenMode.SINGLE_CLICK }) {
		this.options = options;

		this.downKeyBindingDispatcher = new KeybindingDispatcher();
		this.upKeyBindingDispatcher = new KeybindingDispatcher();

		if (typeof options.keyboardSupport !== 'boolean' || options.keyboardSupport) {
			this.downKeyBindingDispatcher.set(KeyCode.UpArrow, (t, e) => this.onUp(t, e));
			this.downKeyBindingDispatcher.set(KeyCode.DownArrow, (t, e) => this.onDown(t, e));
			this.downKeyBindingDispatcher.set(KeyCode.LeftArrow, (t, e) => this.onLeft(t, e));
			this.downKeyBindingDispatcher.set(KeyCode.RightArrow, (t, e) => this.onRight(t, e));
			if (platform.isMacintosh) {
				this.downKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.UpArrow, (t, e) => this.onLeft(t, e));
				this.downKeyBindingDispatcher.set(KeyMod.WinCtrl | KeyCode.KeyN, (t, e) => this.onDown(t, e));
				this.downKeyBindingDispatcher.set(KeyMod.WinCtrl | KeyCode.KeyP, (t, e) => this.onUp(t, e));
			}
			this.downKeyBindingDispatcher.set(KeyCode.PageUp, (t, e) => this.onPageUp(t, e));
			this.downKeyBindingDispatcher.set(KeyCode.PageDown, (t, e) => this.onPageDown(t, e));
			this.downKeyBindingDispatcher.set(KeyCode.Home, (t, e) => this.onHome(t, e));
			this.downKeyBindingDispatcher.set(KeyCode.End, (t, e) => this.onEnd(t, e));

			this.downKeyBindingDispatcher.set(KeyCode.Space, (t, e) => this.onSpace(t, e));
			this.downKeyBindingDispatcher.set(KeyCode.Escape, (t, e) => this.onEscape(t, e));

			this.upKeyBindingDispatcher.set(KeyCode.Enter, this.onEnter.bind(this));
			this.upKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, this.onEnter.bind(this));
		}
	}

	public onMouseDown(tree: _.ITree, element: any, event: mouse.IMouseEvent, origin: string = 'mouse'): boolean {
		if (this.options.clickBehavior === ClickBehavior.ON_MOUSE_DOWN && (event.leftButton || event.middleButton)) {
			if (event.target) {
				if (event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
					return false; // Ignore event if target is a form input field (avoids browser specific issues)
				}

				if (dom.findParentWithClass(event.target, 'scrollbar', 'monaco-tree')) {
					return false;
				}

				if (dom.findParentWithClass(event.target, 'monaco-action-bar', 'row')) { // TODO@Joao not very nice way of checking for the action bar (implicit knowledge)
					return false; // Ignore event if target is over an action bar of the row
				}
			}

			// Propagate to onLeftClick now
			return this.onLeftClick(tree, element, event, origin);
		}

		return false;
	}

	public onClick(tree: _.ITree, element: any, event: mouse.IMouseEvent): boolean {
		const isMac = platform.isMacintosh;

		// A Ctrl click on the Mac is a context menu event
		if (isMac && event.ctrlKey) {
			event.preventDefault();
			event.stopPropagation();
			return false;
		}

		if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
			return false; // Ignore event if target is a form input field (avoids browser specific issues)
		}

		if (this.options.clickBehavior === ClickBehavior.ON_MOUSE_DOWN && (event.leftButton || event.middleButton)) {
			return false; // Already handled by onMouseDown
		}

		return this.onLeftClick(tree, element, event);
	}

	protected onLeftClick(tree: _.ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean {
		const event = <mouse.IMouseEvent>eventish;
		const payload = { origin: origin, originalEvent: eventish, didClickOnTwistie: this.isClickOnTwistie(event) };

		if (tree.getInput() === element) {
			tree.clearFocus(payload);
			tree.clearSelection(payload);
		} else {
			const isSingleMouseDown = eventish && event.browserEvent && event.browserEvent.type === 'mousedown' && event.browserEvent.detail === 1;
			if (!isSingleMouseDown) {
				eventish.preventDefault(); // we cannot preventDefault onMouseDown with single click because this would break DND otherwise
			}
			eventish.stopPropagation();

			tree.domFocus();
			tree.setSelection([element], payload);
			tree.setFocus(element, payload);

			if (this.shouldToggleExpansion(element, event, origin)) {
				if (tree.isExpanded(element)) {
					tree.collapse(element).then(undefined, errors.onUnexpectedError);
				} else {
					tree.expand(element).then(undefined, errors.onUnexpectedError);
				}
			}
		}

		return true;
	}

	protected shouldToggleExpansion(element: any, event: mouse.IMouseEvent, origin: string): boolean {
		const isDoubleClick = (origin === 'mouse' && event.detail === 2);
		return this.openOnSingleClick || isDoubleClick || this.isClickOnTwistie(event);
	}

	protected setOpenMode(openMode: OpenMode) {
		this.options.openMode = openMode;
	}

	protected get openOnSingleClick(): boolean {
		return this.options.openMode === OpenMode.SINGLE_CLICK;
	}

	protected isClickOnTwistie(event: mouse.IMouseEvent): boolean {
		let element = event.target as HTMLElement;

		if (!element.classList.contains('content')) {
			return false;
		}

		const twistieStyle = window.getComputedStyle(element, ':before');

		if (twistieStyle.backgroundImage === 'none' || twistieStyle.display === 'none') {
			return false;
		}

		const twistieWidth = parseInt(twistieStyle.width!) + parseInt(twistieStyle.paddingRight!);
		return event.browserEvent.offsetX <= twistieWidth;
	}

	public onContextMenu(tree: _.ITree, element: any, event: _.ContextMenuEvent): boolean {
		if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
			return false; // allow context menu on input fields
		}

		// Prevent native context menu from showing up
		if (event) {
			event.preventDefault();
			event.stopPropagation();
		}

		return false;
	}

	public onTap(tree: _.ITree, element: any, event: touch.GestureEvent): boolean {
		const target = <HTMLElement>event.initialTarget;

		if (target && target.tagName && target.tagName.toLowerCase() === 'input') {
			return false; // Ignore event if target is a form input field (avoids browser specific issues)
		}

		return this.onLeftClick(tree, element, event, 'touch');
	}

	public onKeyDown(tree: _.ITree, event: IKeyboardEvent): boolean {
		return this.onKey(this.downKeyBindingDispatcher, tree, event);
	}

	public onKeyUp(tree: _.ITree, event: IKeyboardEvent): boolean {
		return this.onKey(this.upKeyBindingDispatcher, tree, event);
	}

	private onKey(bindings: KeybindingDispatcher, tree: _.ITree, event: IKeyboardEvent): boolean {
		const handler: any = bindings.dispatch(event.toKeyCodeChord().toKeybinding());
		if (handler) {
			// TODO: TS 3.1 upgrade. Why are we checking against void?
			if (handler(tree, event)) {
				event.preventDefault();
				event.stopPropagation();
				return true;
			}
		}
		return false;
	}

	protected onUp(tree: _.ITree, event: IKeyboardEvent): boolean {
		const payload = { origin: 'keyboard', originalEvent: event };

		if (tree.getHighlight()) {
			tree.clearHighlight(payload);
		} else {
			tree.focusPrevious(1, payload);
			tree.reveal(tree.getFocus()).then(undefined, errors.onUnexpectedError);
		}
		return true;
	}

	protected onPageUp(tree: _.ITree, event: IKeyboardEvent): boolean {
		const payload = { origin: 'keyboard', originalEvent: event };

		if (tree.getHighlight()) {
			tree.clearHighlight(payload);
		} else {
			tree.focusPreviousPage(payload);
			tree.reveal(tree.getFocus()).then(undefined, errors.onUnexpectedError);
		}
		return true;
	}

	protected onDown(tree: _.ITree, event: IKeyboardEvent): boolean {
		const payload = { origin: 'keyboard', originalEvent: event };

		if (tree.getHighlight()) {
			tree.clearHighlight(payload);
		} else {
			tree.focusNext(1, payload);
			tree.reveal(tree.getFocus()).then(undefined, errors.onUnexpectedError);
		}
		return true;
	}

	protected onPageDown(tree: _.ITree, event: IKeyboardEvent): boolean {
		const payload = { origin: 'keyboard', originalEvent: event };

		if (tree.getHighlight()) {
			tree.clearHighlight(payload);
		} else {
			tree.focusNextPage(payload);
			tree.reveal(tree.getFocus()).then(undefined, errors.onUnexpectedError);
		}
		return true;
	}

	protected onHome(tree: _.ITree, event: IKeyboardEvent): boolean {
		const payload = { origin: 'keyboard', originalEvent: event };

		if (tree.getHighlight()) {
			tree.clearHighlight(payload);
		} else {
			tree.focusFirst(payload);
			tree.reveal(tree.getFocus()).then(undefined, errors.onUnexpectedError);
		}
		return true;
	}

	protected onEnd(tree: _.ITree, event: IKeyboardEvent): boolean {
		const payload = { origin: 'keyboard', originalEvent: event };

		if (tree.getHighlight()) {
			tree.clearHighlight(payload);
		} else {
			tree.focusLast(payload);
			tree.reveal(tree.getFocus()).then(undefined, errors.onUnexpectedError);
		}
		return true;
	}

	protected onLeft(tree: _.ITree, event: IKeyboardEvent): boolean {
		const payload = { origin: 'keyboard', originalEvent: event };

		if (tree.getHighlight()) {
			tree.clearHighlight(payload);
		} else {
			const focus = tree.getFocus();
			tree.collapse(focus).then(didCollapse => {
				if (focus && !didCollapse) {
					tree.focusParent(payload);
					return tree.reveal(tree.getFocus());
				}
				return undefined;
			}).then(undefined, errors.onUnexpectedError);
		}
		return true;
	}

	protected onRight(tree: _.ITree, event: IKeyboardEvent): boolean {
		const payload = { origin: 'keyboard', originalEvent: event };

		if (tree.getHighlight()) {
			tree.clearHighlight(payload);
		} else {
			const focus = tree.getFocus();
			tree.expand(focus).then(didExpand => {
				if (focus && !didExpand) {
					tree.focusFirstChild(payload);
					return tree.reveal(tree.getFocus());
				}
				return undefined;
			}).then(undefined, errors.onUnexpectedError);
		}
		return true;
	}

	protected onEnter(tree: _.ITree, event: IKeyboardEvent): boolean {
		const payload = { origin: 'keyboard', originalEvent: event };

		if (tree.getHighlight()) {
			return false;
		}
		const focus = tree.getFocus();
		if (focus) {
			tree.setSelection([focus], payload);
		}
		return true;
	}

	protected onSpace(tree: _.ITree, event: IKeyboardEvent): boolean {
		if (tree.getHighlight()) {
			return false;
		}
		const focus = tree.getFocus();
		if (focus) {
			tree.toggleExpansion(focus);
		}
		return true;
	}

	protected onEscape(tree: _.ITree, event: IKeyboardEvent): boolean {
		const payload = { origin: 'keyboard', originalEvent: event };

		if (tree.getHighlight()) {
			tree.clearHighlight(payload);
			return true;
		}

		if (tree.getSelection().length) {
			tree.clearSelection(payload);
			return true;
		}

		if (tree.getFocus()) {
			tree.clearFocus(payload);
			return true;
		}

		return false;
	}
}

export class DefaultDragAndDrop implements _.IDragAndDrop {

	public getDragURI(tree: _.ITree, element: any): string | null {
		return null;
	}

	public onDragStart(tree: _.ITree, data: IDragAndDropData, originalEvent: mouse.DragMouseEvent): void {
		return;
	}

	public onDragOver(tree: _.ITree, data: IDragAndDropData, targetElement: any, originalEvent: mouse.DragMouseEvent): _.IDragOverReaction | null {
		return null;
	}

	public drop(tree: _.ITree, data: IDragAndDropData, targetElement: any, originalEvent: mouse.DragMouseEvent): void {
		return;
	}

	public dropAbort(tree: _.ITree, data: IDragAndDropData): void { }
}

export class DefaultFilter implements _.IFilter {

	public isVisible(tree: _.ITree, element: any): boolean {
		return true;
	}
}

export class DefaultSorter implements _.ISorter {

	public compare(tree: _.ITree, element: any, otherElement: any): number {
		return 0;
	}
}

export class DefaultAccessibilityProvider implements _.IAccessibilityProvider {

	getAriaLabel(tree: _.ITree, element: any): string | null {
		return null;
	}
}

export class DefaultTreestyler implements _.ITreeStyler {

	constructor(private styleElement: HTMLStyleElement, private selectorSuffix?: string) { }

	style(styles: _.ITreeStyles): void {
		const suffix = this.selectorSuffix ? `.${this.selectorSuffix}` : '';
		const content: string[] = [];

		if (styles.listFocusBackground) {
			content.push(`.monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.focused:not(.highlighted) { background-color: ${styles.listFocusBackground}; }`);
		}

		if (styles.listFocusForeground) {
			content.push(`.monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.focused:not(.highlighted) { color: ${styles.listFocusForeground}; }`);
		}

		if (styles.listActiveSelectionBackground) {
			content.push(`.monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.selected:not(.highlighted) { background-color: ${styles.listActiveSelectionBackground}; }`);
		}

		if (styles.listActiveSelectionForeground) {
			content.push(`.monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.selected:not(.highlighted) { color: ${styles.listActiveSelectionForeground}; }`);
		}

		if (styles.listFocusAndSelectionBackground) {
			content.push(`
				.monaco-tree-drag-image,
				.monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.focused.selected:not(.highlighted) { background-color: ${styles.listFocusAndSelectionBackground}; }
			`);
		}

		if (styles.listFocusAndSelectionForeground) {
			content.push(`
				.monaco-tree-drag-image,
				.monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.focused.selected:not(.highlighted) { color: ${styles.listFocusAndSelectionForeground}; }
			`);
		}

		if (styles.listInactiveSelectionBackground) {
			content.push(`.monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row.selected:not(.highlighted) { background-color: ${styles.listInactiveSelectionBackground}; }`);
		}

		if (styles.listInactiveSelectionForeground) {
			content.push(`.monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row.selected:not(.highlighted) { color: ${styles.listInactiveSelectionForeground}; }`);
		}

		if (styles.listHoverBackground) {
			content.push(`.monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row:hover:not(.highlighted):not(.selected):not(.focused) { background-color: ${styles.listHoverBackground}; }`);
		}

		if (styles.listHoverForeground) {
			content.push(`.monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row:hover:not(.highlighted):not(.selected):not(.focused) { color: ${styles.listHoverForeground}; }`);
		}

		if (styles.listDropBackground) {
			content.push(`
				.monaco-tree${suffix} .monaco-tree-wrapper.drop-target,
				.monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row.drop-target { background-color: ${styles.listDropBackground} !important; color: inherit !important; }
			`);
		}

		if (styles.listFocusOutline) {
			content.push(`
				.monaco-tree-drag-image																															{ border: 1px solid ${styles.listFocusOutline}; background: #000; }
				.monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row 														{ border: 1px solid transparent; }
				.monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.focused:not(.highlighted) 						{ border: 1px dotted ${styles.listFocusOutline}; }
				.monaco-tree${suffix}.focused .monaco-tree-rows > .monaco-tree-row.selected:not(.highlighted) 						{ border: 1px solid ${styles.listFocusOutline}; }
				.monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row.selected:not(.highlighted)  							{ border: 1px solid ${styles.listFocusOutline}; }
				.monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row:hover:not(.highlighted):not(.selected):not(.focused)  	{ border: 1px dashed ${styles.listFocusOutline}; }
				.monaco-tree${suffix} .monaco-tree-wrapper.drop-target,
				.monaco-tree${suffix} .monaco-tree-rows > .monaco-tree-row.drop-target												{ border: 1px dashed ${styles.listFocusOutline}; }
			`);
		}

		const newStyles = content.join('\n');
		if (newStyles !== this.styleElement.innerHTML) {
			this.styleElement.innerHTML = newStyles;
		}
	}
}

export class CollapseAllAction extends Action {

	constructor(private viewer: _.ITree, enabled: boolean) {
		super('vs.tree.collapse', nls.localize('collapse all', "Collapse All"), 'monaco-tree-action collapse-all', enabled);
	}

	public override run(context?: any): Promise<any> {
		if (this.viewer.getHighlight()) {
			return Promise.resolve(); // Global action disabled if user is in edit mode from another action
		}

		this.viewer.collapseAll();
		this.viewer.clearSelection();
		this.viewer.clearFocus();
		this.viewer.domFocus();
		this.viewer.focusFirst();

		return Promise.resolve();
	}
}
