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

import * as dom from 'vs/base/browser/dom';
import { localize } from 'vs/nls';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IDataSource, ITree, IRenderer } from 'vs/base/parts/tree/browser/tree';
import { Action } from 'vs/base/common/actions';
import { IExtensionsWorkbenchService, IExtension } from 'vs/workbench/contrib/extensions/common/extensions';
import { Event } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { WorkbenchTreeController, WorkbenchTree, IListService } from 'vs/platform/list/browser/listService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IThemeService } from 'vs/platform/theme/common/themeService';

export interface IExtensionTemplateData {
	icon: HTMLImageElement;
	name: HTMLElement;
	identifier: HTMLElement;
	author: HTMLElement;
	extensionDisposables: IDisposable[];
	extensionData: IExtensionData;
}

export interface IUnknownExtensionTemplateData {
	identifier: HTMLElement;
}

export interface IExtensionData {
	extension: IExtension;
	hasChildren: boolean;
	getChildren: () => Promise<IExtensionData[] | null>;
	parent: IExtensionData | null;
}

export class DataSource implements IDataSource {

	public getId(tree: ITree, { extension, parent }: IExtensionData): string {
		return parent ? this.getId(tree, parent) + '/' + extension.identifier.id : extension.identifier.id;
	}

	public hasChildren(tree: ITree, { hasChildren }: IExtensionData): boolean {
		return hasChildren;
	}

	public getChildren(tree: ITree, extensionData: IExtensionData): Promise<any> {
		return extensionData.getChildren();
	}

	public getParent(tree: ITree, { parent }: IExtensionData): Promise<any> {
		return Promise.resolve(parent);
	}
}

export class Renderer implements IRenderer {

	private static readonly EXTENSION_TEMPLATE_ID = 'extension-template';
	private static readonly UNKNOWN_EXTENSION_TEMPLATE_ID = 'unknown-extension-template';

	constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) {
	}

	public getHeight(tree: ITree, element: IExtensionData): number {
		return 62;
	}

	public getTemplateId(tree: ITree, { extension }: IExtensionData): string {
		return extension ? Renderer.EXTENSION_TEMPLATE_ID : Renderer.UNKNOWN_EXTENSION_TEMPLATE_ID;
	}

	public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any {
		if (Renderer.EXTENSION_TEMPLATE_ID === templateId) {
			return this.renderExtensionTemplate(tree, container);
		}
		return this.renderUnknownExtensionTemplate(tree, container);
	}

	private renderExtensionTemplate(tree: ITree, container: HTMLElement): IExtensionTemplateData {
		dom.addClass(container, 'extension');

		const icon = dom.append(container, dom.$<HTMLImageElement>('img.icon'));
		const details = dom.append(container, dom.$('.details'));

		const header = dom.append(details, dom.$('.header'));
		const name = dom.append(header, dom.$('span.name'));
		const openExtensionAction = this.instantiationService.createInstance(OpenExtensionAction);
		const extensionDisposables = [dom.addDisposableListener(name, 'click', (e: MouseEvent) => {
			tree.setFocus(openExtensionAction.extensionData);
			tree.setSelection([openExtensionAction.extensionData]);
			openExtensionAction.run(e.ctrlKey || e.metaKey);
			e.stopPropagation();
			e.preventDefault();
		})];
		const identifier = dom.append(header, dom.$('span.identifier'));

		const footer = dom.append(details, dom.$('.footer'));
		const author = dom.append(footer, dom.$('.author'));
		return {
			icon,
			name,
			identifier,
			author,
			extensionDisposables,
			set extensionData(extensionData: IExtensionData) {
				openExtensionAction.extensionData = extensionData;
			}
		};
	}

	private renderUnknownExtensionTemplate(tree: ITree, container: HTMLElement): IUnknownExtensionTemplateData {
		const messageContainer = dom.append(container, dom.$('div.unknown-extension'));
		dom.append(messageContainer, dom.$('span.error-marker')).textContent = localize('error', "Error");
		dom.append(messageContainer, dom.$('span.message')).textContent = localize('Unknown Extension', "Unknown Extension:");

		const identifier = dom.append(messageContainer, dom.$('span.message'));
		return { identifier };
	}

	public renderElement(tree: ITree, element: IExtensionData, templateId: string, templateData: any): void {
		if (templateId === Renderer.EXTENSION_TEMPLATE_ID) {
			this.renderExtension(tree, element, templateData);
			return;
		}
		this.renderUnknownExtension(tree, element, templateData);
	}

	private renderExtension(tree: ITree, extensionData: IExtensionData, data: IExtensionTemplateData): void {
		const extension = extensionData.extension;
		const onError = Event.once(domEvent(data.icon, 'error'));
		onError(() => data.icon.src = extension.iconUrlFallback, null, data.extensionDisposables);
		data.icon.src = extension.iconUrl;

		if (!data.icon.complete) {
			data.icon.style.visibility = 'hidden';
			data.icon.onload = () => data.icon.style.visibility = 'inherit';
		} else {
			data.icon.style.visibility = 'inherit';
		}

		data.name.textContent = extension.displayName;
		data.identifier.textContent = extension.identifier.id;
		data.author.textContent = extension.publisherDisplayName;
		data.extensionData = extensionData;
	}

	private renderUnknownExtension(tree: ITree, { extension }: IExtensionData, data: IUnknownExtensionTemplateData): void {
		data.identifier.textContent = extension.identifier.id;
	}

	public disposeTemplate(tree: ITree, templateId: string, templateData: any): void {
		if (templateId === Renderer.EXTENSION_TEMPLATE_ID) {
			templateData.extensionDisposables = dispose((<IExtensionTemplateData>templateData).extensionDisposables);
		}
	}
}

export class Controller extends WorkbenchTreeController {

	constructor(
		@IExtensionsWorkbenchService private readonly extensionsWorkdbenchService: IExtensionsWorkbenchService,
		@IConfigurationService configurationService: IConfigurationService
	) {
		super({}, configurationService);

		// TODO@Sandeep this should be a command
		this.downKeyBindingDispatcher.set(KeyMod.CtrlCmd | KeyCode.Enter, (tree: ITree, event: any) => this.openExtension(tree, true));
	}

	protected onLeftClick(tree: ITree, element: IExtensionData, event: IMouseEvent): boolean {
		let currentFocused = tree.getFocus();
		if (super.onLeftClick(tree, element, event)) {
			if (element.parent === null) {
				if (currentFocused) {
					tree.setFocus(currentFocused);
				} else {
					tree.focusFirst();
				}
				return true;
			}
		}
		return false;
	}

	public openExtension(tree: ITree, sideByside: boolean): boolean {
		const element: IExtensionData = tree.getFocus();
		if (element.extension) {
			this.extensionsWorkdbenchService.open(element.extension, sideByside);
			return true;
		}
		return false;
	}
}

class OpenExtensionAction extends Action {

	private _extensionData: IExtensionData;

	constructor(@IExtensionsWorkbenchService private readonly extensionsWorkdbenchService: IExtensionsWorkbenchService) {
		super('extensions.action.openExtension', '');
	}

	public set extensionData(extension: IExtensionData) {
		this._extensionData = extension;
	}

	public get extensionData(): IExtensionData {
		return this._extensionData;
	}

	run(sideByside: boolean): Promise<any> {
		return this.extensionsWorkdbenchService.open(this.extensionData.extension, sideByside);
	}
}

export class ExtensionsTree extends WorkbenchTree {

	constructor(
		input: IExtensionData,
		container: HTMLElement,
		@IContextKeyService contextKeyService: IContextKeyService,
		@IListService listService: IListService,
		@IThemeService themeService: IThemeService,
		@IInstantiationService instantiationService: IInstantiationService,
		@IConfigurationService configurationService: IConfigurationService
	) {
		const renderer = instantiationService.createInstance(Renderer);
		const controller = instantiationService.createInstance(Controller);

		super(
			container,
			{
				dataSource: new DataSource(),
				renderer,
				controller
			}, {
				indentPixels: 40,
				twistiePixels: 20
			},
			contextKeyService, listService, themeService, instantiationService, configurationService
		);

		this.setInput(input);

		this.disposables.push(this.onDidChangeSelection(event => {
			if (event && event.payload && event.payload.origin === 'keyboard') {
				controller.openExtension(this, false);
			}
		}));
	}
}