/*---------------------------------------------------------------------------------------------
 *  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 'vs/css!./defineKeybinding';
import * as nls from 'vs/nls';
import { RunOnceScheduler } from 'vs/base/common/async';
import { MarkedString } from 'vs/base/common/htmlContent';
import { Keybinding, KeyCode, KeyMod, KeyChord } from 'vs/base/common/keyCodes';
import { KeybindingLabels } from 'vs/base/common/keybinding';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import * as dom from 'vs/base/browser/dom';
import { renderHtml } from 'vs/base/browser/htmlContentRenderer';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { StyleMutator } from 'vs/base/browser/styleMutator';
import { IOSupport } from 'vs/platform/keybinding/common/keybindingResolver';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
import { Range } from 'vs/editor/common/core/range';
import * as editorCommon from 'vs/editor/common/editorCommon';
import { editorAction, ServicesAccessor, EditorAction } from 'vs/editor/common/editorCommonExtensions';
import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition, OverlayWidgetPositionPreference } from 'vs/editor/browser/editorBrowser';
import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions';
import { CodeSnippet } from 'vs/editor/contrib/snippet/common/snippet';
import { SnippetController } from 'vs/editor/contrib/snippet/common/snippetController';
import { SmartSnippetInserter } from 'vs/editor/contrib/defineKeybinding/common/smartSnippetInserter';

import EditorContextKeys = editorCommon.EditorContextKeys;

const NLS_LAUNCH_MESSAGE = nls.localize('defineKeybinding.start', "Define Keybinding");
const NLS_DEFINE_MESSAGE = nls.localize('defineKeybinding.initial', "Press desired key combination and ENTER");
const NLS_KB_LAYOUT_INFO_MESSAGE = nls.localize('defineKeybinding.kbLayoutInfoMessage', "For your current keyboard layout press ");
const NLS_KB_LAYOUT_ERROR_MESSAGE = nls.localize('defineKeybinding.kbLayoutErrorMessage', "You won't be able to produce this key combination under your current keyboard layout.");

const INTERESTING_FILE = /keybindings\.json$/;

@editorContribution
export class DefineKeybindingController implements editorCommon.IEditorContribution {

	private static ID = 'editor.contrib.defineKeybinding';

	public static get(editor: editorCommon.ICommonCodeEditor): DefineKeybindingController {
		return editor.getContribution<DefineKeybindingController>(DefineKeybindingController.ID);
	}

	private _editor: ICodeEditor;
	private _keybindingService: IKeybindingService;
	private _launchWidget: DefineKeybindingLauncherWidget;
	private _defineWidget: DefineKeybindingWidget;
	private _toDispose: IDisposable[];
	private _modelToDispose: IDisposable[];
	private _updateDecorations: RunOnceScheduler;

	constructor(
		editor: ICodeEditor,
		@IKeybindingService keybindingService: IKeybindingService
	) {
		this._editor = editor;
		this._keybindingService = keybindingService;
		this._toDispose = [];
		this._launchWidget = new DefineKeybindingLauncherWidget(this._editor, keybindingService, () => this.launch());
		this._defineWidget = new DefineKeybindingWidget(this._editor, keybindingService, (keybinding) => this._onAccepted(keybinding));

		this._toDispose.push(this._editor.onDidChangeConfiguration((e) => {
			if (isInterestingEditorModel(this._editor)) {
				this._launchWidget.show();
			} else {
				this._launchWidget.hide();
			}
		}));
		this._toDispose.push(this._editor.onDidChangeModel((e) => {
			if (isInterestingEditorModel(this._editor)) {
				this._launchWidget.show();
			} else {
				this._launchWidget.hide();
			}
			this._onModel();
		}));

		this._updateDecorations = new RunOnceScheduler(() => this._updateDecorationsNow(), 500);
		this._toDispose.push(this._updateDecorations);

		this._modelToDispose = [];
		this._onModel();
	}

	public getId(): string {
		return DefineKeybindingController.ID;
	}

	public dispose(): void {
		this._modelToDispose = dispose(this._modelToDispose);
		this._toDispose = dispose(this._toDispose);
		this._launchWidget.dispose();
		this._launchWidget = null;
		this._defineWidget.dispose();
		this._defineWidget = null;
	}

	public launch(): void {
		if (isInterestingEditorModel(this._editor)) {
			this._defineWidget.start();
		}
	}

	private _onAccepted(keybinding: string): void {
		let snippetText = [
			'{',
			'\t"key": ' + JSON.stringify(keybinding) + ',',
			'\t"command": "${1:commandId}",',
			'\t"when": "${2:editorTextFocus}"',
			'}$0'
		].join('\n');

		let smartInsertInfo = SmartSnippetInserter.insertSnippet(this._editor.getModel(), this._editor.getPosition());
		snippetText = smartInsertInfo.prepend + snippetText + smartInsertInfo.append;
		this._editor.setPosition(smartInsertInfo.position);

		SnippetController.get(this._editor).run(CodeSnippet.fromTextmate(snippetText), 0, 0);
	}

	private _onModel(): void {
		this._modelToDispose = dispose(this._modelToDispose);

		let model = this._editor.getModel();
		if (!model) {
			return;
		}

		let url = model.uri.toString();
		if (!INTERESTING_FILE.test(url)) {
			return;
		}

		this._modelToDispose.push(model.onDidChangeContent((e) => this._updateDecorations.schedule()));
		this._modelToDispose.push({
			dispose: () => {
				this._dec = this._editor.deltaDecorations(this._dec, []);
				this._updateDecorations.cancel();
			}
		});
		this._updateDecorations.schedule();
	}

	private _dec: string[] = [];
	private _updateDecorationsNow(): void {
		let model = this._editor.getModel();
		let regex = KeybindingLabels.getUserSettingsKeybindingRegex();

		var m = model.findMatches(regex, false, true, false, false);

		let data = m.map((range) => {
			let text = model.getValueInRange(range);

			let strKeybinding = text.substring(1, text.length - 1);
			strKeybinding = strKeybinding.replace(/\\\\/g, '\\');

			let numKeybinding = IOSupport.readKeybinding(strKeybinding);

			let keybinding = new Keybinding(numKeybinding);

			return {
				strKeybinding: strKeybinding,
				keybinding: keybinding,
				usLabel: KeybindingLabels._toUSLabel(keybinding),
				label: this._keybindingService.getLabelFor(keybinding),
				range: range
			};
		});

		data = data.filter((entry) => {
			return (entry.usLabel !== entry.label);
		});

		let newDecorations: editorCommon.IModelDeltaDecoration[] = [];
		data.forEach((item) => {
			let msg: MarkedString[];
			let className: string;
			let inlineClassName: string;
			let overviewRulerColor: string;

			if (!item.label) {
				// this is the error case
				msg = [NLS_KB_LAYOUT_ERROR_MESSAGE];
				className = 'keybindingError';
				inlineClassName = 'inlineKeybindingError';
				overviewRulerColor = 'rgba(250, 100, 100, 0.6)';
			} else {
				// this is the info case
				msg = [NLS_KB_LAYOUT_INFO_MESSAGE];
				msg = msg.concat(this._keybindingService.getLabelFor(item.keybinding));
				className = 'keybindingInfo';
				inlineClassName = 'inlineKeybindingInfo';
				overviewRulerColor = 'rgba(100, 100, 250, 0.6)';
			}

			// icon decoration
			newDecorations.push({
				range: new Range(item.range.startLineNumber, item.range.startColumn, item.range.startLineNumber, item.range.startColumn + 1),
				options: {
					stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
					inlineClassName: inlineClassName
				}
			});

			// highlight + message decoration
			newDecorations.push({
				range: item.range,
				options: {
					stickiness: editorCommon.TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges,
					className: className,
					hoverMessage: msg,
					overviewRuler: {
						color: overviewRulerColor,
						darkColor: overviewRulerColor,
						position: editorCommon.OverviewRulerLane.Right
					}
				}
			});
		});

		this._dec = this._editor.deltaDecorations(this._dec, newDecorations);
	}
}

class DefineKeybindingLauncherWidget implements IOverlayWidget {

	private static ID = 'editor.contrib.defineKeybindingLauncherWidget';

	private _editor: ICodeEditor;

	private _domNode: HTMLElement;
	private _toDispose: IDisposable[];
	private _isVisible: boolean;

	constructor(editor: ICodeEditor, keybindingService: IKeybindingService, onLaunch: () => void) {
		this._editor = editor;
		this._domNode = document.createElement('div');
		this._domNode.className = 'defineKeybindingLauncher';
		this._domNode.style.display = 'none';
		this._isVisible = false;
		let keybinding = keybindingService.lookupKeybindings(DefineKeybindingAction.ID);
		let extra = '';
		if (keybinding.length > 0) {
			extra += ' (' + keybindingService.getLabelFor(keybinding[0]) + ')';
		}
		this._domNode.appendChild(document.createTextNode(NLS_LAUNCH_MESSAGE + extra));

		this._toDispose = [];
		this._toDispose.push(dom.addDisposableListener(this._domNode, 'click', (e) => {
			onLaunch();
		}));

		this._editor.addOverlayWidget(this);
	}

	public dispose(): void {
		this._editor.removeOverlayWidget(this);
		this._toDispose = dispose(this._toDispose);
	}

	public show(): void {
		if (this._isVisible) {
			return;
		}
		this._domNode.style.display = 'block';
		this._isVisible = true;
		this._editor.layoutOverlayWidget(this);
	}

	public hide(): void {
		if (!this._isVisible) {
			return;
		}
		this._domNode.style.display = 'none';
		this._isVisible = false;
		this._editor.layoutOverlayWidget(this);
	}

	// ----- IOverlayWidget API

	public getId(): string {
		return DefineKeybindingLauncherWidget.ID;
	}

	public getDomNode(): HTMLElement {
		return this._domNode;
	}

	public getPosition(): IOverlayWidgetPosition {
		return {
			preference: this._isVisible ? OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER : null
		};
	}
}


class DefineKeybindingWidget implements IOverlayWidget {

	private static ID = 'editor.contrib.defineKeybindingWidget';
	private static WIDTH = 340;
	private static HEIGHT = 90;

	private _editor: ICodeEditor;
	private _keybindingService: IKeybindingService;

	private _domNode: HTMLElement;
	private _toDispose: IDisposable[];

	private _messageNode: HTMLElement;
	private _inputNode: HTMLInputElement;
	private _outputNode: HTMLElement;

	private _lastKeybinding: Keybinding;
	private _onAccepted: (keybinding: string) => void;
	private _isVisible: boolean;

	constructor(editor: ICodeEditor, keybindingService: IKeybindingService, onAccepted: (keybinding: string) => void) {
		this._editor = editor;
		this._keybindingService = keybindingService;
		this._onAccepted = onAccepted;
		this._toDispose = [];
		this._lastKeybinding = null;

		this._domNode = document.createElement('div');
		this._domNode.className = 'defineKeybindingWidget';
		StyleMutator.setWidth(this._domNode, DefineKeybindingWidget.WIDTH);
		StyleMutator.setHeight(this._domNode, DefineKeybindingWidget.HEIGHT);

		this._domNode.style.display = 'none';
		this._isVisible = false;

		this._messageNode = document.createElement('div');
		this._messageNode.className = 'message';
		this._messageNode.innerText = NLS_DEFINE_MESSAGE;
		this._domNode.appendChild(this._messageNode);

		this._inputNode = document.createElement('input');
		this._inputNode.className = 'input';
		this._inputNode.type = 'text';
		this._domNode.appendChild(this._inputNode);

		this._outputNode = document.createElement('div');
		this._outputNode.className = 'output';
		this._domNode.appendChild(this._outputNode);

		this._toDispose.push(dom.addDisposableListener(this._inputNode, 'keydown', (e) => {
			let keyEvent = new StandardKeyboardEvent(e);
			keyEvent.preventDefault();
			keyEvent.stopPropagation();

			let kb = keyEvent.toKeybinding();
			switch (kb.value) {
				case KeyCode.Enter:
					if (this._lastKeybinding) {
						this._onAccepted(KeybindingLabels.toUserSettingsLabel(this._lastKeybinding.value));
					}
					this._stop();
					return;

				case KeyCode.Escape:
					this._stop();
					return;
			}

			this._lastKeybinding = kb;

			this._inputNode.value = KeybindingLabels.toUserSettingsLabel(this._lastKeybinding.value).toLowerCase();
			this._inputNode.title = 'keyCode: ' + keyEvent.browserEvent.keyCode;

			dom.clearNode(this._outputNode);
			let htmlkb = this._keybindingService.getHTMLLabelFor(this._lastKeybinding);
			htmlkb.forEach((item) => this._outputNode.appendChild(renderHtml(item)));
		}));
		this._toDispose.push(this._editor.onDidChangeConfiguration((e) => {
			if (this._isVisible) {
				this._layout();
			}
		}));

		this._toDispose.push(dom.addDisposableListener(this._inputNode, 'blur', (e) => this._stop()));

		this._editor.addOverlayWidget(this);
	}

	public dispose(): void {
		this._editor.removeOverlayWidget(this);
		this._toDispose = dispose(this._toDispose);
	}

	public getId(): string {
		return DefineKeybindingWidget.ID;
	}

	public getDomNode(): HTMLElement {
		return this._domNode;
	}

	public getPosition(): IOverlayWidgetPosition {
		return {
			preference: null
		};
	}

	private _show(): void {
		if (this._isVisible) {
			return;
		}
		this._isVisible = true;
		this._layout();
		this._domNode.style.display = 'block';
	}

	private _hide(): void {
		if (!this._isVisible) {
			return;
		}
		this._isVisible = false;
		this._domNode.style.display = 'none';
	}

	private _layout(): void {
		let editorLayout = this._editor.getLayoutInfo();

		let top = Math.round((editorLayout.height - DefineKeybindingWidget.HEIGHT) / 2);
		StyleMutator.setTop(this._domNode, top);

		let left = Math.round((editorLayout.width - DefineKeybindingWidget.WIDTH) / 2);
		StyleMutator.setLeft(this._domNode, left);
	}

	public start(): void {
		this._editor.revealPositionInCenterIfOutsideViewport(this._editor.getPosition());

		this._show();

		this._lastKeybinding = null;
		this._inputNode.value = '';
		dom.clearNode(this._outputNode);
		this._inputNode.focus();
	}

	private _stop(): void {
		this._editor.focus();
		this._hide();
	}
}

@editorAction
export class DefineKeybindingAction extends EditorAction {

	static ID = 'editor.action.defineKeybinding';

	constructor() {
		super({
			id: DefineKeybindingAction.ID,
			label: nls.localize('DefineKeybindingAction', "Define Keybinding"),
			alias: 'Define Keybinding',
			precondition: ContextKeyExpr.and(EditorContextKeys.Writable, EditorContextKeys.LanguageId.isEqualTo('json')),
			kbOpts: {
				kbExpr: EditorContextKeys.TextFocus,
				primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KEY_K, KeyMod.CtrlCmd | KeyCode.KEY_K)
			}
		});
	}

	public run(accessor: ServicesAccessor, editor: editorCommon.ICommonCodeEditor): void {
		if (!isInterestingEditorModel(editor)) {
			return;
		}
		let controller = DefineKeybindingController.get(editor);
		if (controller) {
			controller.launch();
		}
	}

}

function isInterestingEditorModel(editor: editorCommon.ICommonCodeEditor): boolean {
	if (editor.getConfiguration().readOnly) {
		return false;
	}
	let model = editor.getModel();
	if (!model) {
		return false;
	}
	let url = model.uri.toString();
	return INTERESTING_FILE.test(url);
}
