/*---------------------------------------------------------------------------------------------
 *  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 {TPromise} from 'vs/base/common/winjs.base';
import nls = require('vs/nls');
import env = require('vs/base/common/flags');
import {MainThreadService as CommonMainThreadService} from 'vs/platform/thread/common/mainThreadService';
import pluginsIPC = require('vs/platform/plugins/common/ipcRemoteCom');
import remote = require('vs/base/common/remote');
import marshalling = require('vs/base/common/marshalling');
import json = require('vs/base/common/json');
import strings = require('vs/base/common/strings');
import objects = require('vs/base/common/objects');
import uri from 'vs/base/common/uri';
import errors = require('vs/base/common/errors');
import {SyncDescriptor0} from 'vs/platform/instantiation/common/descriptors';
import {IMessageService, Severity} from 'vs/platform/message/common/message';
import {IWorkspaceContextService, IConfiguration} from 'vs/platform/workspace/common/workspace';
import {IWindowService} from 'vs/workbench/services/window/electron-browser/windowService';
import ports = require('vs/base/node/ports');

import cp = require('child_process');
import ipc = require('ipc');

export const PLUGIN_LOG_BROADCAST_CHANNEL = 'vscode:pluginLog';

// Enable to see detailed message communication between window and plugin host
const logPluginHostCommunication = false;

export interface ILogEntry {
	type: string;
	severity: string;
	arguments: any;
}

export class MainThreadService extends CommonMainThreadService {
	private pluginHostProcessManager: PluginHostProcessManager;
	private remoteCom: pluginsIPC.IPluginsIPC;

	constructor(contextService: IWorkspaceContextService, messageService: IMessageService, windowService: IWindowService) {
		super(contextService, 'vs/editor/common/worker/editorWorkerServer');

		this.pluginHostProcessManager = new PluginHostProcessManager(contextService, messageService, windowService);

		let logCommunication = logPluginHostCommunication || contextService.getConfiguration().env.logPluginHostCommunication;

		// Message: Window --> Plugin Host
		this.remoteCom = pluginsIPC.create((msg) => {
			if (logCommunication) {
				console.log('%c[Window \u2192 Plugin]%c[len: ' + strings.pad(msg.length, 5, ' ') + ']', 'color: darkgreen', 'color: grey', JSON.parse(msg));
			}

			this.pluginHostProcessManager.postMessage(msg);
		});

		// Message: Plugin Host --> Window
		this.pluginHostProcessManager.startPluginHostProcess((msg) => {
			if (logCommunication) {
				console.log('%c[Plugin \u2192 Window]%c[len: ' + strings.pad(msg.length, 5, ' ') + ']', 'color: darkgreen', 'color: grey', JSON.parse(msg));
			}

			this.remoteCom.handle(msg)
		});

		this.remoteCom.registerBigHandler(this);
	}

	public dispose(): void {
		this.pluginHostProcessManager.terminate();
	}

	protected _registerAndInstantiatePluginHostActor<T>(id: string, descriptor: SyncDescriptor0<T>): T {
		return this._getOrCreateProxyInstance(this.remoteCom, id, descriptor);
	}
}

class PluginHostProcessManager {
	private messageService: IMessageService;
	private contextService: IWorkspaceContextService;
	private windowService: IWindowService;

	private initializePluginHostProcess: TPromise<cp.ChildProcess>;
	private pluginHostProcessHandle: cp.ChildProcess;
	private initializeTimer: number;

	private lastPluginHostError: string;
	private unsentMessages: any[];
	private terminating: boolean;

	private isPluginDevelopmentHost: boolean;

	constructor(contextService: IWorkspaceContextService, messageService: IMessageService, windowService: IWindowService) {
		this.messageService = messageService;
		this.contextService = contextService;
		this.windowService = windowService;

		// handle plugin host lifecycle a bit special when we know we are developing an extension that runs inside
		this.isPluginDevelopmentHost = !!this.contextService.getConfiguration().env.pluginDevelopmentPath;

		this.unsentMessages = [];
	}

	public startPluginHostProcess(onPluginHostMessage: (msg: any) => void): void {
		let config = this.contextService.getConfiguration();
		let isDev = !config.env.isBuilt || !!config.env.pluginDevelopmentPath;

		let opts: any = {
			env: objects.mixin(objects.clone(process.env), { AMD_ENTRYPOINT: 'vs/workbench/node/pluginHostProcess', PIPE_LOGGING: 'true', VERBOSE_LOGGING: true })
		};

		// Help in case we fail to start it
		if (isDev) {
			this.initializeTimer = setTimeout(() => {
				const msg = config.env.debugBrkPluginHost ? nls.localize('pluginHostProcess.startupFailDebug', "Plugin host did not start in 10 seconds, it might be stopped on the first line and needs a debugger to continue.") : nls.localize('pluginHostProcess.startupFail', "Plugin host did not start in 10 seconds, that might be a problem.");

				this.messageService.show(Severity.Warning, msg);
			}, 10000);
		}

		// Initialize plugin host process with hand shakes
		this.initializePluginHostProcess = new TPromise<cp.ChildProcess>((c, e) => {

			// Resolve additional execution args (e.g. debug)
			return this.resolveExecArgv(config, (execArgv) => {
				if (execArgv) {
					opts.execArgv = execArgv;
				}

				// Run Plugin Host as fork of current process
				this.pluginHostProcessHandle = cp.fork(uri.parse(require.toUrl('bootstrap')).fsPath, ['--type=pluginHost'], opts);

				// Messages from Plugin host
				this.pluginHostProcessHandle.on('message', (msg) => {

					// 1) Host is ready to receive messages, initialize it
					if (msg === 'ready') {
						if (this.initializeTimer) {
							window.clearTimeout(this.initializeTimer);
						}

						let initPayload = marshalling.serialize({
							parentPid: process.pid,
							contextService: {
								workspace: this.contextService.getWorkspace(),
								configuration: this.contextService.getConfiguration(),
								options: this.contextService.getOptions()
							},
						});

						this.pluginHostProcessHandle.send(initPayload);
					}

					// 2) Host is initialized
					else if (msg === 'initialized') {
						this.unsentMessages.forEach(m => this.postMessage(m));
						this.unsentMessages = [];

						c(this.pluginHostProcessHandle);
					}

					// Support logging from plugin host
					else if (msg && (<ILogEntry>msg).type === '__$console') {
						let logEntry:ILogEntry = msg;

						let args = ['%c[Plugin Host]', 'color: blue'];
						try {
							let parsed = JSON.parse(logEntry.arguments);
							args.push(...Object.getOwnPropertyNames(parsed).map(o => parsed[o]));
						} catch (error) {
							args.push(logEntry.arguments);
						}

						// Send to local console
						console[logEntry.severity].apply(console, args);

						// Broadcast to other windows if we are in development mode
						if (isDev) {
							this.windowService.broadcast({
								channel: PLUGIN_LOG_BROADCAST_CHANNEL,
								payload: logEntry
							});
						}
					}

					// Any other message goes to the callback
					else {
						onPluginHostMessage(msg);
					}
				});

				// Lifecycle
				let onExit = () => this.terminate();
				process.once('exit', onExit);

				this.pluginHostProcessHandle.on('error', (err) => {
					let errorMessage = errors.toErrorMessage(err);
					if (errorMessage === this.lastPluginHostError) {
						return; // prevent error spam
					}

					this.lastPluginHostError = errorMessage;

					this.messageService.show(Severity.Error, nls.localize('pluginHostProcess.error', "Error from the plugin host: {0}", errorMessage));
				});

				this.pluginHostProcessHandle.on('exit', (code: any, signal: any) => {
					process.removeListener('exit', onExit);

					if (!this.terminating) {

						// Unexpected termination
						if (!this.isPluginDevelopmentHost) {
							this.messageService.show(Severity.Error, nls.localize('pluginHostProcess.crash', "Plugin host terminated unexpectedly. Please restart VSCode to recover."));
							console.error('Plugin host terminated unexpectedly. Code: ', code, ' Signal: ', signal);
						}

						// Expected development plugin termination: When the plugin host goes down we also shutdown the window
						else {
							this.windowService.getWindow().close();
						}
					}
				});
			});
		}, () => this.terminate());
	}

	private resolveExecArgv(config: IConfiguration, clb: (execArgv: any) => void): void {

		// Check for a free debugging port
		if (typeof config.env.debugPluginHostPort === 'number') {
			return ports.findFreePort(config.env.debugPluginHostPort, 10 /* try 10 ports */, (port) => {
				if (!port) {
					console.warn('%c[Plugin Host] %cCould not find a free port for debugging', 'color: blue', 'color: black');

					return clb(void 0);
				}

				if (port !== config.env.debugPluginHostPort) {
					console.warn('%c[Plugin Host] %cProvided debugging port ' + config.env.debugPluginHostPort + ' is not free, using ' + port + ' instead.', 'color: blue', 'color: black');
				}

				if (config.env.debugBrkPluginHost) {
					console.warn('%c[Plugin Host] %cSTOPPED on first line for debugging on port ' + port, 'color: blue', 'color: black');
				} else {
					console.info('%c[Plugin Host] %cdebugger listening on port ' + port, 'color: blue', 'color: black');
				}

				return clb(['--nolazy', (config.env.debugBrkPluginHost ? '--debug-brk=' : '--debug=') + port]);
			});
		}

		// Nothing to do here
		else {
			return clb(void 0);
		}
	}

	public postMessage(msg: any): void {
		if (this.initializePluginHostProcess) {
			this.initializePluginHostProcess.done(p => p.send(msg));
		} else {
			this.unsentMessages.push(msg);
		}
	}

	public terminate(): void {
		this.terminating = true;

		if (this.pluginHostProcessHandle) {
			this.pluginHostProcessHandle.kill();
		}
	}
}