import * as cp from "child_process";

import { getPathFromAmdModule } from "vs/base/common/amd";
import { VSBuffer } from "vs/base/common/buffer";
import { Emitter } from "vs/base/common/event";
import { ISocket } from "vs/base/parts/ipc/common/ipc.net";
import { NodeSocket, WebSocketNodeSocket } from "vs/base/parts/ipc/node/ipc.net";
import { ILogService } from "vs/platform/log/common/log";
import { uriTransformerPath } from "vs/server/channel";
import { IExtHostReadyMessage, IExtHostSocketMessage } from "vs/workbench/services/extensions/common/extensionHostProtocol";

import { Protocol } from "vs/server/protocol";

export abstract class Connection {
	private readonly _onClose = new Emitter<void>();
	public readonly onClose = this._onClose.event;

	private timeout: NodeJS.Timeout | undefined;
	private readonly wait = 1000 * 60;

	private closed: boolean = false;

	public constructor(protected protocol: Protocol) {
		// onClose seems to mean we want to disconnect, so close immediately.
		protocol.onClose(() => this.close());

		// If the socket closes, we want to wait before closing so we can
		// reconnect in the meantime.
		protocol.onSocketClose(() => {
			this.timeout = setTimeout(() => {
				this.close();
			}, this.wait);
		});
	}

	/**
	 * Set up the connection on a new socket.
	 */
	public reconnect(protocol: Protocol, buffer: VSBuffer): void {
		if (this.closed) {
			throw new Error("Cannot reconnect to closed connection");
		}
		clearTimeout(this.timeout as any); // Not sure why the type doesn't work.
		this.protocol = protocol;
		this.connect(protocol.getSocket(), buffer);
	}

	/**
	 * Close and clean up connection. This will also kill the socket the
	 * connection is on. Probably not safe to reconnect once this has happened.
	 */
	protected close(): void {
		if (!this.closed) {
			this.closed = true;
			this.protocol.sendDisconnect();
			this.dispose();
			this.protocol.dispose();
			this._onClose.fire();
		}
	}

	/**
	 * Clean up the connection.
	 */
	protected abstract dispose(): void;

	/**
	 * Connect to a new socket.
	 */
	protected abstract connect(socket: ISocket, buffer: VSBuffer): void;
}

/**
 * Used for all the IPC channels.
 */
export class ManagementConnection extends Connection {
	protected dispose(): void {
		// Nothing extra to do here.
	}

	protected connect(socket: ISocket, buffer: VSBuffer): void {
		this.protocol.beginAcceptReconnection(socket, buffer);
		this.protocol.endAcceptReconnection();
	}
}

/**
 * Manage the extension host process.
 */
export class ExtensionHostConnection extends Connection {
	private process: cp.ChildProcess;

	public constructor(protocol: Protocol, private readonly log: ILogService) {
		super(protocol);
		const socket = this.protocol.getSocket();
		const buffer = this.protocol.readEntireBuffer();
		this.process = this.spawn(socket, buffer);
	}

	protected dispose(): void {
		this.process.kill();
	}

	protected connect(socket: ISocket, buffer: VSBuffer): void {
		this.sendInitMessage(socket, buffer);
	}

	private sendInitMessage(nodeSocket: ISocket, buffer: VSBuffer): void {
		const socket = nodeSocket instanceof NodeSocket
			? nodeSocket.socket
			: (nodeSocket as WebSocketNodeSocket).socket.socket;

		socket.pause();

		const initMessage: IExtHostSocketMessage = {
			type: "VSCODE_EXTHOST_IPC_SOCKET",
			initialDataChunk: (buffer.buffer as Buffer).toString("base64"),
			skipWebSocketFrames: nodeSocket instanceof NodeSocket,
		};

		this.process.send(initMessage, socket);
	}

	private spawn(socket: ISocket, buffer: VSBuffer): cp.ChildProcess {
		const proc = cp.fork(
			getPathFromAmdModule(require, "bootstrap-fork"),
			[
				"--type=extensionHost",
				`--uriTransformerPath=${uriTransformerPath}`
			],
			{
				env: {
					...process.env,
					AMD_ENTRYPOINT: "vs/workbench/services/extensions/node/extensionHostProcess",
					PIPE_LOGGING: "true",
					VERBOSE_LOGGING: "true",
					VSCODE_EXTHOST_WILL_SEND_SOCKET: "true",
					VSCODE_HANDLES_UNCAUGHT_ERRORS: "true",
					VSCODE_LOG_STACK: "false",
				},
				silent: true,
			},
		);

		proc.on("error", (error) => {
			console.error(error);
			this.close();
		});

		proc.on("exit", (code, signal) => {
			console.error("Extension host exited", { code, signal });
			this.close();
		});

		proc.stdout.setEncoding("utf8");
		proc.stderr.setEncoding("utf8");
		proc.stdout.on("data", (data) => this.log.info("Extension host stdout", data));
		proc.stderr.on("data", (data) => this.log.error("Extension host stderr", data));
		proc.on("message", (event) => {
			if (event && event.type === "__$console") {
				const severity = this.log[event.severity] ? event.severity : "info";
				this.log[severity]("Extension host", event.arguments);
			}
		});

		const listen = (message: IExtHostReadyMessage) => {
			if (message.type === "VSCODE_EXTHOST_IPC_READY") {
				proc.removeListener("message", listen);
				this.sendInitMessage(socket, buffer);
			}
		};

		proc.on("message", listen);

		return proc;
	}
}
