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

import 'vs/css!./media/compositepart';
import * as nls from 'vs/nls';
import { defaultGenerator } from 'vs/base/common/idGenerator';
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import * as strings from 'vs/base/common/strings';
import { Emitter } from 'vs/base/common/event';
import * as errors from 'vs/base/common/errors';
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { IActionItem, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { prepareActions } from 'vs/workbench/browser/actions';
import { Action, IAction } from 'vs/base/common/actions';
import { Part, IPartOptions } from 'vs/workbench/browser/part';
import { Composite, CompositeRegistry } from 'vs/workbench/browser/composite';
import { IComposite } from 'vs/workbench/common/composite';
import { ScopedProgressService } from 'vs/workbench/services/progress/browser/progressService';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachProgressBarStyler } from 'vs/platform/theme/common/styler';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { Dimension, append, $, addClass, hide, show, addClasses } from 'vs/base/browser/dom';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { withNullAsUndefined } from 'vs/base/common/types';

export interface ICompositeTitleLabel {

	/**
	 * Asks to update the title for the composite with the given ID.
	 */
	updateTitle(id: string, title: string, keybinding?: string): void;

	/**
	 * Called when theming information changes.
	 */
	updateStyles(): void;
}

interface CompositeItem {
	composite: Composite;
	disposable: IDisposable;
	progressService: IProgressService;
}

export abstract class CompositePart<T extends Composite> extends Part {

	protected readonly onDidCompositeOpen = this._register(new Emitter<{ composite: IComposite, focus: boolean }>());
	protected readonly onDidCompositeClose = this._register(new Emitter<IComposite>());

	protected toolBar: ToolBar;

	private mapCompositeToCompositeContainer: { [compositeId: string]: HTMLElement; };
	private mapActionsBindingToComposite: { [compositeId: string]: () => void; };
	private activeComposite: Composite | null;
	private lastActiveCompositeId: string;
	private instantiatedCompositeItems: Map<string, CompositeItem>;
	private titleLabel: ICompositeTitleLabel;
	private progressBar: ProgressBar;
	private contentAreaSize: Dimension;
	private telemetryActionsListener: IDisposable | null;
	private currentCompositeOpenToken: string;

	constructor(
		private notificationService: INotificationService,
		protected storageService: IStorageService,
		private telemetryService: ITelemetryService,
		protected contextMenuService: IContextMenuService,
		protected layoutService: IWorkbenchLayoutService,
		protected keybindingService: IKeybindingService,
		protected instantiationService: IInstantiationService,
		themeService: IThemeService,
		protected readonly registry: CompositeRegistry<T>,
		private activeCompositeSettingsKey: string,
		private defaultCompositeId: string,
		private nameForTelemetry: string,
		private compositeCSSClass: string,
		private titleForegroundColor: string | undefined,
		id: string,
		options: IPartOptions
	) {
		super(id, options, themeService, storageService, layoutService);

		this.mapCompositeToCompositeContainer = {};
		this.mapActionsBindingToComposite = {};
		this.activeComposite = null;
		this.instantiatedCompositeItems = new Map<string, CompositeItem>();
		this.lastActiveCompositeId = storageService.get(activeCompositeSettingsKey, StorageScope.WORKSPACE, this.defaultCompositeId);
	}

	protected openComposite(id: string, focus?: boolean): Composite | undefined {

		// Check if composite already visible and just focus in that case
		if (this.activeComposite && this.activeComposite.getId() === id) {
			if (focus) {
				this.activeComposite.focus();
			}

			// Fullfill promise with composite that is being opened
			return this.activeComposite;
		}

		// Open
		return this.doOpenComposite(id, focus);
	}

	private doOpenComposite(id: string, focus: boolean = false): Composite | undefined {

		// Use a generated token to avoid race conditions from long running promises
		const currentCompositeOpenToken = defaultGenerator.nextId();
		this.currentCompositeOpenToken = currentCompositeOpenToken;

		// Hide current
		if (this.activeComposite) {
			this.hideActiveComposite();
		}

		// Update Title
		this.updateTitle(id);

		// Create composite
		const composite = this.createComposite(id, true);

		// Check if another composite opened meanwhile and return in that case
		if ((this.currentCompositeOpenToken !== currentCompositeOpenToken) || (this.activeComposite && this.activeComposite.getId() !== composite.getId())) {
			return undefined;
		}

		// Check if composite already visible and just focus in that case
		if (this.activeComposite && this.activeComposite.getId() === composite.getId()) {
			if (focus) {
				composite.focus();
			}

			this.onDidCompositeOpen.fire({ composite, focus });
			return composite;
		}

		// Show Composite and Focus
		this.showComposite(composite);
		if (focus) {
			composite.focus();
		}

		// Return with the composite that is being opened
		if (composite) {
			this.onDidCompositeOpen.fire({ composite, focus });
		}

		return composite;
	}

	protected createComposite(id: string, isActive?: boolean): Composite {

		// Check if composite is already created
		const compositeItem = this.instantiatedCompositeItems.get(id);
		if (compositeItem) {
			return compositeItem.composite;
		}

		// Instantiate composite from registry otherwise
		const compositeDescriptor = this.registry.getComposite(id);
		if (compositeDescriptor) {
			const progressService = this.instantiationService.createInstance(ScopedProgressService, this.progressBar, compositeDescriptor.id, isActive);
			const compositeInstantiationService = this.instantiationService.createChild(new ServiceCollection([IProgressService, progressService]));

			const composite = compositeDescriptor.instantiate(compositeInstantiationService);
			const disposables: IDisposable[] = [];

			// Remember as Instantiated
			this.instantiatedCompositeItems.set(id, { composite, disposable: toDisposable(() => dispose(disposables)), progressService });

			// Register to title area update events from the composite
			composite.onTitleAreaUpdate(() => this.onTitleAreaUpdate(composite.getId()), this, disposables);

			return composite;
		}

		throw new Error(`Unable to find composite with id ${id}`);
	}

	protected showComposite(composite: Composite): void {

		// Remember Composite
		this.activeComposite = composite;

		// Store in preferences
		const id = this.activeComposite.getId();
		if (id !== this.defaultCompositeId) {
			this.storageService.store(this.activeCompositeSettingsKey, id, StorageScope.WORKSPACE);
		} else {
			this.storageService.remove(this.activeCompositeSettingsKey, StorageScope.WORKSPACE);
		}

		// Remember
		this.lastActiveCompositeId = this.activeComposite.getId();

		// Composites created for the first time
		let compositeContainer = this.mapCompositeToCompositeContainer[composite.getId()];
		if (!compositeContainer) {

			// Build Container off-DOM
			compositeContainer = $('.composite');
			addClasses(compositeContainer, this.compositeCSSClass);
			compositeContainer.id = composite.getId();

			composite.create(compositeContainer);
			composite.updateStyles();

			// Remember composite container
			this.mapCompositeToCompositeContainer[composite.getId()] = compositeContainer;
		}

		// Report progress for slow loading composites (but only if we did not create the composites before already)
		const compositeItem = this.instantiatedCompositeItems.get(composite.getId());
		if (compositeItem && !compositeContainer) {
			compositeItem.progressService.showWhile(Promise.resolve(), this.layoutService.isRestored() ? 800 : 3200 /* less ugly initial startup */);
		}

		// Fill Content and Actions
		// Make sure that the user meanwhile did not open another composite or closed the part containing the composite
		if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {
			return undefined;
		}

		// Take Composite on-DOM and show
		const contentArea = this.getContentArea();
		if (contentArea) {
			contentArea.appendChild(compositeContainer);
		}
		show(compositeContainer);

		// Setup action runner
		this.toolBar.actionRunner = composite.getActionRunner();

		// Update title with composite title if it differs from descriptor
		const descriptor = this.registry.getComposite(composite.getId());
		if (descriptor && descriptor.name !== composite.getTitle()) {
			this.updateTitle(composite.getId(), withNullAsUndefined(composite.getTitle()));
		}

		// Handle Composite Actions
		let actionsBinding = this.mapActionsBindingToComposite[composite.getId()];
		if (!actionsBinding) {
			actionsBinding = this.collectCompositeActions(composite);
			this.mapActionsBindingToComposite[composite.getId()] = actionsBinding;
		}
		actionsBinding();

		if (this.telemetryActionsListener) {
			this.telemetryActionsListener.dispose();
			this.telemetryActionsListener = null;
		}

		// Action Run Handling
		this.telemetryActionsListener = this.toolBar.actionRunner.onDidRun(e => {

			// Check for Error
			if (e.error && !errors.isPromiseCanceledError(e.error)) {
				this.notificationService.error(e.error);
			}

			// Log in telemetry
			if (this.telemetryService) {
				/* __GDPR__
					"workbenchActionExecuted" : {
						"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
						"from": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
					}
				*/
				this.telemetryService.publicLog('workbenchActionExecuted', { id: e.action.id, from: this.nameForTelemetry });
			}
		});

		// Indicate to composite that it is now visible
		composite.setVisible(true);

		// Make sure that the user meanwhile did not open another composite or closed the part containing the composite
		if (!this.activeComposite || composite.getId() !== this.activeComposite.getId()) {
			return;
		}

		// Make sure the composite is layed out
		if (this.contentAreaSize) {
			composite.layout(this.contentAreaSize);
		}
	}

	protected onTitleAreaUpdate(compositeId: string): void {

		// Active Composite
		if (this.activeComposite && this.activeComposite.getId() === compositeId) {

			// Title
			this.updateTitle(this.activeComposite.getId(), this.activeComposite.getTitle() || undefined);

			// Actions
			const actionsBinding = this.collectCompositeActions(this.activeComposite);
			this.mapActionsBindingToComposite[this.activeComposite.getId()] = actionsBinding;
			actionsBinding();
		}

		// Otherwise invalidate actions binding for next time when the composite becomes visible
		else {
			delete this.mapActionsBindingToComposite[compositeId];
		}
	}

	private updateTitle(compositeId: string, compositeTitle?: string): void {
		const compositeDescriptor = this.registry.getComposite(compositeId);
		if (!compositeDescriptor || !this.titleLabel) {
			return;
		}

		if (!compositeTitle) {
			compositeTitle = compositeDescriptor.name;
		}

		const keybinding = this.keybindingService.lookupKeybinding(compositeId);

		this.titleLabel.updateTitle(compositeId, compositeTitle, (keybinding && keybinding.getLabel()) || undefined);

		this.toolBar.setAriaLabel(nls.localize('ariaCompositeToolbarLabel', "{0} actions", compositeTitle));
	}

	private collectCompositeActions(composite: Composite): () => void {

		// From Composite
		const primaryActions: IAction[] = composite.getActions().slice(0);
		const secondaryActions: IAction[] = composite.getSecondaryActions().slice(0);

		// From Part
		primaryActions.push(...this.getActions());
		secondaryActions.push(...this.getSecondaryActions());

		// Update context
		this.toolBar.context = this.actionsContextProvider();

		// Return fn to set into toolbar
		return this.toolBar.setActions(prepareActions(primaryActions), prepareActions(secondaryActions));
	}

	protected getActiveComposite(): IComposite | null {
		return this.activeComposite;
	}

	protected getLastActiveCompositetId(): string {
		return this.lastActiveCompositeId;
	}

	protected hideActiveComposite(): Composite | undefined {
		if (!this.activeComposite) {
			return undefined; // Nothing to do
		}

		const composite = this.activeComposite;
		this.activeComposite = null;

		const compositeContainer = this.mapCompositeToCompositeContainer[composite.getId()];

		// Indicate to Composite
		composite.setVisible(false);

		// Take Container Off-DOM and hide
		compositeContainer.remove();
		hide(compositeContainer);

		// Clear any running Progress
		this.progressBar.stop().hide();

		// Empty Actions
		this.toolBar.setActions([])();
		this.onDidCompositeClose.fire(composite);

		return composite;
	}

	createTitleArea(parent: HTMLElement): HTMLElement {

		// Title Area Container
		const titleArea = append(parent, $('.composite'));
		addClass(titleArea, 'title');

		// Left Title Label
		this.titleLabel = this.createTitleLabel(titleArea);

		// Right Actions Container
		const titleActionsContainer = append(titleArea, $('.title-actions'));

		// Toolbar
		this.toolBar = this._register(new ToolBar(titleActionsContainer, this.contextMenuService, {
			actionItemProvider: action => this.actionItemProvider(action as Action),
			orientation: ActionsOrientation.HORIZONTAL,
			getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),
			anchorAlignmentProvider: () => this.getTitleAreaDropDownAnchorAlignment()
		}));

		return titleArea;
	}

	protected createTitleLabel(parent: HTMLElement): ICompositeTitleLabel {
		const titleContainer = append(parent, $('.title-label'));
		const titleLabel = append(titleContainer, $('h2'));

		const $this = this;
		return {
			updateTitle: (id, title, keybinding) => {
				titleLabel.innerHTML = strings.escape(title);
				titleLabel.title = keybinding ? nls.localize('titleTooltip', "{0} ({1})", title, keybinding) : title;
			},

			updateStyles: () => {
				titleLabel.style.color = $this.titleForegroundColor ? $this.getColor($this.titleForegroundColor) : null;
			}
		};
	}

	updateStyles(): void {
		super.updateStyles();

		// Forward to title label
		this.titleLabel.updateStyles();
	}

	protected actionItemProvider(action: Action): IActionItem | undefined {

		// Check Active Composite
		if (this.activeComposite) {
			return this.activeComposite.getActionItem(action);
		}

		return undefined;
	}

	protected actionsContextProvider(): any {

		// Check Active Composite
		if (this.activeComposite) {
			return this.activeComposite.getActionsContext();
		}

		return null;
	}

	createContentArea(parent: HTMLElement): HTMLElement {
		const contentContainer = append(parent, $('.content'));

		this.progressBar = this._register(new ProgressBar(contentContainer));
		this._register(attachProgressBarStyler(this.progressBar, this.themeService));
		this.progressBar.hide();

		return contentContainer;
	}

	getProgressIndicator(id: string): IProgressService | null {
		const compositeItem = this.instantiatedCompositeItems.get(id);

		return compositeItem ? compositeItem.progressService : null;
	}

	protected getActions(): IAction[] {
		return [];
	}

	protected getSecondaryActions(): IAction[] {
		return [];
	}

	protected getTitleAreaDropDownAnchorAlignment(): AnchorAlignment {
		return AnchorAlignment.RIGHT;
	}

	layout(width: number, height: number): void {

		// Layout contents
		this.contentAreaSize = super.layoutContents(width, height).contentSize;

		// Layout composite
		if (this.activeComposite) {
			this.activeComposite.layout(this.contentAreaSize);
		}
	}

	protected removeComposite(compositeId: string): boolean {
		if (this.activeComposite && this.activeComposite.getId() === compositeId) {
			return false; // do not remove active composite
		}

		delete this.mapCompositeToCompositeContainer[compositeId];
		delete this.mapActionsBindingToComposite[compositeId];
		const compositeItem = this.instantiatedCompositeItems.get(compositeId);
		if (compositeItem) {
			compositeItem.composite.dispose();
			dispose(compositeItem.disposable);
			this.instantiatedCompositeItems.delete(compositeId);
		}

		return true;
	}

	dispose(): void {
		this.mapCompositeToCompositeContainer = null!; // StrictNullOverride: nulling out ok in dispose
		this.mapActionsBindingToComposite = null!; // StrictNullOverride: nulling out ok in dispose

		this.instantiatedCompositeItems.forEach(compositeItem => {
			compositeItem.composite.dispose();
			dispose(compositeItem.disposable);
		});

		this.instantiatedCompositeItems.clear();

		super.dispose();
	}
}
