import { Breakpoint, IBackend, Thread, Stack, SSHArguments, Variable, VariableObject, MIError } from "../backend";
import * as ChildProcess from "child_process";
import { EventEmitter } from "events";
import { parseMI, MINode } from '../mi_parse';
import * as linuxTerm from '../linux/console';
import * as net from "net";
import * as fs from "fs";
import { posix } from "path";
import * as nativePath from "path";
const path = posix;
import { Client } from "ssh2";
import { TreeItemCollapsibleState } from "vscode";
import { setFlagsFromString } from "v8";

export function escape(str: string) {
	return str.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
}

//const nonOutput = /^(?:\d*|undefined)[\*\+\=]|[\~\@\&\^]/;
const nonOutput = /^(?:\d*|undefined)[\*\=]|[\~\@\&\^]/; // Dubner removed '+'
const gdbMatch = /(?:\d*|undefined)\(gdb\)/;
const numRegex = /\d+/;

//function couldBeOutput(line: string) {
//	if (nonOutput.exec(line))
//		return false;
//	return true;
//}
function couldBeOutput(line: string) {
// Return false if line starts with GDB/MI keyword
const gdbmi =
	[
		"(gdb)",
	// Result records
		"^done",
		"^running",
		"^connected",
		"^error",
		"^exit",

	// Stream records
		"~\"",
		"@\"",
		"&\"",

	// Async records
		"*running",
		"*stopped",
		"=thread-",
		"=library-",
		"=traceframe-",
		"=tsv-",
		"=tsv-",
		"=breakpoint-",
		"=record-",
		"=cmd-",
		"=memory-",

	];

	// Skip leading digits:
	var nstart = 0;
	var patt = new RegExp("^[0-9]");
	while( nstart<line.length ) {
		var onechar = line.slice(nstart,nstart+1) ;
		if( !patt.test(onechar) ) {
			break;
		}
		nstart += 1;
	}

	for( const possible of gdbmi ) {
		var chunk = line.slice(nstart,nstart+possible.length);
		if( possible == chunk ) {
			return false;
		}
  	}

return true;
}

const trace = false;

export class MI2 extends EventEmitter implements IBackend {
	constructor(public application: string, public preargs: string[], public extraargs: string[], procEnv: any) {
		super();

		if (procEnv) {
			const env = {};
			// Duplicate process.env so we don't override it
			for (const key in process.env)
				if (process.env.hasOwnProperty(key))
					env[key] = process.env[key];

			// Overwrite with user specified variables
			for (const key in procEnv) {
				if (procEnv.hasOwnProperty(key)) {
					if (procEnv === null)
						delete env[key];
					else
						env[key] = procEnv[key];
				}
			}
			this.procEnv = env;
		}
	}

	load(cwd: string, program: string, procArgs: string, separateConsole: string): Thenable<any> {
		if (!nativePath.isAbsolute(program))
		program = nativePath.join(cwd, program);
		return new Promise((resolve, reject) => {
			this.isSSH = false;
			const args = this.preargs.concat(this.extraargs || []);
			this.process = ChildProcess.spawn(this.application, args, { cwd: cwd, env: this.procEnv });
			this.process.stdout.on("data", this.stdout.bind(this));
			this.process.stderr.on("data", this.stderr.bind(this));
			this.process.on("exit", (() => { this.emit("quit"); }).bind(this));
			this.process.on("error", ((err) => { this.emit("launcherror", err); }).bind(this));
			const promises = this.initCommands(program, cwd);
			if (procArgs && procArgs.length)
				promises.push(this.sendCommand("exec-arguments " + procArgs));
			if (process.platform == "win32") {
				if (separateConsole !== undefined)
					promises.push(this.sendCommand("gdb-set new-console on"));
				Promise.all(promises).then(() => {
					this.emit("debug-ready");
					resolve();
				}, reject);
			} else {
				if (separateConsole !== undefined) {
					linuxTerm.spawnTerminalEmulator(separateConsole).then(tty => {
						promises.push(this.sendCommand("inferior-tty-set " + tty));
						Promise.all(promises).then(() => {
							this.emit("debug-ready");
							resolve();
						}, reject);
					});
				} else {
					Promise.all(promises).then(() => {
						this.emit("debug-ready");
						resolve();
					}, reject);
				}
			}
		});
	}

	ssh(args: SSHArguments, cwd: string, program: string, procArgs: string, separateConsole: string, attach: boolean): Thenable<any> {
		return new Promise((resolve, reject) => {
			this.isSSH = true;
			this.sshReady = false;
			this.sshConn = new Client();

			if (separateConsole !== undefined)
				this.log("stderr", "WARNING: Output to terminal emulators are not supported over SSH");

			if (args.forwardX11) {
				this.sshConn.on("x11", (info, accept, reject) => {
					const xserversock = new net.Socket();
					xserversock.on("error", (err) => {
						this.log("stderr", "Could not connect to local X11 server! Did you enable it in your display manager?\n" + err);
					});
					xserversock.on("connect", () => {
						const xclientsock = accept();
						xclientsock.pipe(xserversock).pipe(xclientsock);
					});
					xserversock.connect(args.x11port, args.x11host);
				});
			}

			const connectionArgs: any = {
				host: args.host,
				port: args.port,
				username: args.user
			};

			if (args.useAgent) {
				connectionArgs.agent = process.env.SSH_AUTH_SOCK;
			} else if (args.keyfile) {
				if (require("fs").existsSync(args.keyfile))
					connectionArgs.privateKey = require("fs").readFileSync(args.keyfile);
				else {
					this.log("stderr", "SSH key file does not exist!");
					this.emit("quit");
					reject();
					return;
				}
			} else {
				connectionArgs.password = args.password;
			}

			this.sshConn.on("ready", () => {
				this.log("stdout", "Running " + this.application + " over ssh...");
				const execArgs: any = {};
				if (args.forwardX11) {
					execArgs.x11 = {
						single: false,
						screen: args.remotex11screen
					};
				}
				let sshCMD = this.application + " " + this.preargs.join(" ");
				if (args.bootstrap) sshCMD = args.bootstrap + " && " + sshCMD;
				if (attach)
					sshCMD += " -p " + program;
				this.sshConn.exec(sshCMD, execArgs, (err, stream) => {
					if (err) {
						this.log("stderr", "Could not run " + this.application + " over ssh!");
						this.log("stderr", err.toString());
						this.emit("quit");
						reject();
						return;
					}
					this.sshReady = true;
					this.stream = stream;
					stream.on("data", this.stdout.bind(this));
					stream.stderr.on("data", this.stderr.bind(this));
					stream.on("exit", (() => {
						this.emit("quit");
						this.sshConn.end();
					}).bind(this));
					const promises = this.initCommands(program, cwd, true, attach);
					promises.push(this.sendCommand("environment-cd \"" + escape(cwd) + "\""));
					if (procArgs && procArgs.length && !attach)
						promises.push(this.sendCommand("exec-arguments " + procArgs));
					Promise.all(promises).then(() => {
						this.emit("debug-ready");
						resolve();
					}, reject);
				});
			}).on("error", (err) => {
				this.log("stderr", "Could not run " + this.application + " over ssh!");
				this.log("stderr", err.toString());
				this.emit("quit");
				reject();
			}).connect(connectionArgs);
		});
	}

	protected initCommands(target: string, cwd: string, ssh: boolean = false, attach: boolean = false) {
		if (ssh) {
			if (!path.isAbsolute(target))
				target = path.join(cwd, target);
		} else {
			if (!nativePath.isAbsolute(target))
				target = nativePath.join(cwd, target);
		}
		const cmds = [
			this.sendCommand("gdb-set target-async on", true),
			this.sendCommand("environment-directory \"" + escape(cwd) + "\"", true)
		];
		if (!attach)
			cmds.push(this.sendCommand("file-exec-and-symbols \"" + escape(target) + "\""));
		if (this.prettyPrint)
			cmds.push(this.sendCommand("enable-pretty-printing"));

		return cmds;
	}

	attach(cwd: string, executable: string, target: string, iexclause: string, ixscript:string, primdebug:string): Thenable<any> {
		return new Promise((resolve, reject) => {
			let args = [];
			args = args.concat(this.preargs);
			if (executable && !nativePath.isAbsolute(executable)) {
				executable = nativePath.join(cwd, executable);
			}
			if (!executable && target) {
				executable = "-p";
			}
			let isExtendedRemote = false;
			if (target && target.startsWith("extended-remote")) {
				isExtendedRemote = true;
				args = this.preargs;
			} else{
				if( iexclause != undefined ){
					args = args.concat("-iex");
					args = args.concat("set 'solib-search-path " + iexclause + "'")
				}

				if( ixscript ) {
					args = args.concat("-ix");
					args = args.concat(ixscript);
				}
				if ( primdebug ){
					//  One of the numerous possibilities is that the user has supplied us
					//  with a path to a file, or fifo, containing a single line:
					//      PID appname\n
					//
					//  We need to read that file, and turn it into -ex commands:
					//
					// Get the contents of the file
					try {
						var primdebugstring:string = fs.readFileSync(primdebug,"UTF-8");
						// Strip off the newline at the end
						primdebugstring = primdebugstring.slice(0,-1);
						var tokens = primdebugstring.split("\t");
						if( tokens.length == 2 ){
							// Put in the -p PID
							args = args.concat("-p");
							args = args.concat(tokens[0]);
							// Put in the -ex 'b app_'

							args = args.concat("-ex");
							args = args.concat("b " + tokens[1] + "_");
							
							// Put in the -ex 'signal SIGCONT'
							args = args.concat("-ex");
							args = args.concat("signal SIGCONT");
						}
					}
					catch(error){
					}
				}
				if (executable) {
					args = args.concat(executable);
				}
				if (target) {
					args = args.concat(target);
				}
			}
			this.process = ChildProcess.spawn(this.application, args, { cwd: cwd, env: this.procEnv });
			this.process.stdout.on("data", this.stdout.bind(this));
			this.process.stderr.on("data", this.stderr.bind(this));
			this.process.on("exit", (() => { this.emit("quit"); }).bind(this));
			this.process.on("error", ((err) => { this.emit("launcherror", err); }).bind(this));
			const commands = [
				//this.sendCommand("gdb-set target-async on"), // Dubner eliminated this to prevent crashing
				this.sendCommand("environment-directory \"" + escape(cwd) + "\"")
			];
			if (isExtendedRemote) {
				commands.push(this.sendCommand("target-select " + target));
				commands.push(this.sendCommand("file-symbol-file \"" + escape(executable) + "\""));
			}
			Promise.all(commands).then(() => {
				this.emit("debug-ready");
				resolve();
			}, reject);
		});
	}

	connect(cwd: string, executable: string, target: string): Thenable<any> {
		return new Promise((resolve, reject) => {
			let args = [];
			if (executable && !nativePath.isAbsolute(executable))
				executable = nativePath.join(cwd, executable);
			if (executable)
				args = args.concat([executable], this.preargs);
			else
				args = this.preargs;
			this.process = ChildProcess.spawn(this.application, args, { cwd: cwd, env: this.procEnv });
			this.process.stdout.on("data", this.stdout.bind(this));
			this.process.stderr.on("data", this.stderr.bind(this));
			this.process.on("exit", (() => { this.emit("quit"); }).bind(this));
			this.process.on("error", ((err) => { this.emit("launcherror", err); }).bind(this));
			Promise.all([
				this.sendCommand("gdb-set target-async on"),
				this.sendCommand("environment-directory \"" + escape(cwd) + "\""),
				this.sendCommand("target-select remote " + target)
			]).then(() => {
				this.emit("debug-ready");
				resolve();
			}, reject);
		});
	}

	convert_mi_variables(buf:string):any {
		var retval:any = buf;

		if( typeof buf == "string" ) {
		
			// The cobcd.py extension to gdb returns a string when issued the
			// command "p/m ?".  The processing up to this point converts that
			// string to a string, meaning any embedded control characters
			// have been converted to '\"', '\\', '\n,' and so on.

			// I don't know who is doing that, or why.  Rather than dig down,
			// I decided to minimize the impact and just transmogrify it back
			// here.

			const leading_variables:string = '~"variables=' ;
			const leading_value:string = '~"value=' ;

			if( buf.substr(0,leading_variables.length) == leading_variables || buf.substr(0,leading_value.length) == leading_value) {
				//console.log("convert_mi_variables(1): ", buf); //DUBNERDEBUG
				var n1 = buf.indexOf('\n');
				if( n1 > -1) {
					var n2 = buf.indexOf('\n',n1+1);
					if( n2 > -1) {
						retval = buf.substr(n1+1,n2-(n1+1));
						retval += ','
						retval += buf.substr(2,(n1-1)-2); // Skip the leading '~"' and the trailing '"'
					}
				}
				var temp:string = retval;
				retval = "";

				for(var i=0; i<temp.length; ) {
					var ch = temp.charAt(i++);
					if( ch == "\\" ) {
						ch = temp.charAt(i++);
						if( ch == 'n' ) {
							retval += '\n';
						}
						else {
							retval += ch;
						}
					}
					else {
					retval += ch ;
					}
				}
				temp = retval;
				retval = "";
				for(var i=0; i<temp.length; i++) {
					var ch = temp.charAt(i);
					if( ch == '/'){
						ch = '/';   // '\u203F'; // undertie
					}
					retval += ch ;
				}
				console.log("convert_mi_variables(2): ", retval); // DUBNERDEBUG
//				var nfound = retval.indexOf('[');
//				if(nfound != -1){
//					var newr = retval.substr(0,nfound)
//					newr = newr + "[{name=\"L01\",value=\"{_groupvalue=\\\"Snap!\\\",fullname=\\\"RobertDubner\\\",L02={_groupvalue=\\\"Crackle!\\\",firstname = \\\"Robert\\\", lastname = \\\"Dubner\\\",L03={_groupvalue=\\\"Pop!\\\",firstname = \\\"Judy\\\", lastname = \\\"Ruderman\\\"}}}\"}]\n";
//					console.log("convert_mi_variables(3): ", newr); // DUBNERDEBUG
//					retval = newr;
//				}

			}
		}

		return retval;
	}

	stdout(data) {
		if( this.buffer == undefined ) {
			this.buffer = "";
		}

		if (trace)
			this.log("stderr", "stdout: " + data);
		if (typeof data == "string") {		
			this.buffer += data;
			//console.log("(A): ", data); //DUBNERDEBUG
		}
		else {
			this.buffer += data.toString("utf8");
			//console.log("(B): ", data.toString("utf8"));  //DUBNERDEBUG
		}

		var end = this.buffer.lastIndexOf('\n');

		// The 'p/m ?' request is not an MI request, so sometimes the
		// p/m response comes in, and then a little while later the token^done
		// response comes in.  This following code only runs when a
		// 'p/m ?' request has been sent.  It will strip out the '&p/m ?' response
		// that the machine-interface sends back.  And then it'll accumulate 
		// strings until it sees ^done


		if( this.recent_variables_request > -1 ) {
			// We did a 'p/m ?' request.
			if( this.buffer.length>0 && this.buffer.charAt(0) == '&') {
				// Strip out the '&...' echo
				end = this.buffer.indexOf('\n');
				if( end != -1 ) {
					this.buffer = this.buffer.substr(end + 1);
				}
   			end = this.buffer.lastIndexOf('\n');	
			}
			// Just keep accumulating characters until we get to the ^done\n, which
			// the machine-interface will send out after the ~response
			if( this.buffer.indexOf("^done\n") == -1 ) {
				end = -1;
			}
		}

		if (end != -1) {
			this.buffer = this.convert_mi_variables(this.buffer);
			// We need to find end again, in case convert_mi_variables
			// changed the length.
			end = this.buffer.lastIndexOf('\n');
			this.onOutput(this.buffer.substr(0, end));
			this.buffer = this.buffer.substr(end + 1);
		}
		if (this.buffer.length) {
			if (this.onOutputPartial(this.buffer)) {
				this.buffer = "";
			}
		}
	}

	stderr(data) {
		if (typeof data == "string")
			this.errbuf += data;
		else
			this.errbuf += data.toString("utf8");
		const end = this.errbuf.lastIndexOf('\n');
		if (end != -1) {
			this.onOutputStderr(this.errbuf.substr(0, end));
			this.errbuf = this.errbuf.substr(end + 1);
		}
		if (this.errbuf.length) {
			this.logNoNewLine("stderr", this.errbuf);
			this.errbuf = "";
		}
	}

	onOutputStderr(lines) {
		lines = <string[]> lines.split('\n');
		lines.forEach(line => {
			this.log("stderr", line);
		});
	}

	onOutputPartial(line) {
		if (couldBeOutput(line)) {
			this.logNoNewLine("stdout", line);
			return true;
		}
		return false;
	}

	onOutput(lines) {
		lines = <string[]> lines.split('\n');
		lines.forEach(line => {
			if (couldBeOutput(line)) {
				if (!gdbMatch.exec(line))
					this.log("stdout", line);
			} else {
				const parsed = parseMI(line);
				if (this.debugOutput)
					this.log("log", "GDB -> App: " + JSON.stringify(parsed));
				let handled = false;
				if (parsed.token !== undefined) {
					if (this.handlers[parsed.token]) {
						this.handlers[parsed.token](parsed);
						delete this.handlers[parsed.token];
						handled = true;
					}
				}
				if (!handled && parsed.resultRecords && parsed.resultRecords.resultClass == "error") {
					this.log("stderr", parsed.result("msg") || line);
				}
				if (parsed.outOfBandRecord) {
					parsed.outOfBandRecord.forEach(record => {
						if (record.isStream) {
							this.log(record.type, record.content);
						} else {
							if (record.type == "exec") {
								this.emit("exec-async-output", parsed);
								if (record.asyncClass == "running")
									this.emit("running", parsed);
								else if (record.asyncClass == "stopped") {
									const reason = parsed.record("reason");
									if (trace)
										this.log("stderr", "stop: " + reason);
									if (reason == "breakpoint-hit")
										this.emit("breakpoint", parsed);
									else if (reason == "end-stepping-range")
										this.emit("step-end", parsed);
									else if (reason == "function-finished")
										this.emit("step-out-end", parsed);
									else if (reason == "signal-received")
										this.emit("signal-stop", parsed);
									else if (reason == "exited-normally")
										this.emit("exited-normally", parsed);
									else if (reason == "exited") { // exit with error code != 0
										this.log("stderr", "Program exited with code " + parsed.record("exit-code"));
										this.emit("exited-normally", parsed);
									} else {
										this.log("console", "Not implemented stop reason (assuming exception): " + reason);
										this.emit("stopped", parsed);
									}
								} else
									this.log("log", JSON.stringify(parsed));
							} else if (record.type == "notify") {
								if (record.asyncClass == "thread-created") {
									this.emit("thread-created", parsed);
								} else if (record.asyncClass == "thread-exited") {
									this.emit("thread-exited", parsed);
								}
							}
						}
					});
					handled = true;
				}
				if (parsed.token == undefined && parsed.resultRecords == undefined && parsed.outOfBandRecord.length == 0)
					handled = true;
				if (!handled)
					this.log("log", "Unhandled: " + JSON.stringify(parsed));
			}
		});
	}

	start(): Thenable<boolean> {
		return new Promise((resolve, reject) => {
			this.once("ui-break-done", () => {
				this.log("console", "Running executable");
				this.sendCommand("exec-run").then((info) => {
					if (info.resultRecords.resultClass == "running")
						resolve();
					else
						reject();
				}, reject);
			});
		});
	}

	stop() {
		if (this.isSSH) {
			const proc = this.stream;
			const to = setTimeout(() => {
				proc.signal("KILL");
			}, 1000);
			this.stream.on("exit", function (code) {
				clearTimeout(to);
			});
			this.sendRaw("-gdb-exit");
		} else {
			const proc = this.process;
			const to = setTimeout(() => {
				process.kill(-proc.pid);
			}, 1000);
			this.process.on("exit", function (code) {
				clearTimeout(to);
			});
			this.sendRaw("-gdb-exit");
		}
	}

	detach() {
		const proc = this.process;
		const to = setTimeout(() => {
			process.kill(-proc.pid);
		}, 1000);
		this.process.on("exit", function (code) {
			clearTimeout(to);
		});
		this.sendRaw("-target-detach");
	}

	interrupt(): Thenable<boolean> {
		if (trace)
			this.log("stderr", "interrupt");
		return new Promise((resolve, reject) => {
			this.sendCommand("exec-interrupt").then((info) => {
				resolve(info.resultRecords.resultClass == "done");
			}, reject);
		});
	}

	continue(reverse: boolean = false): Thenable<boolean> {
		if (trace)
			this.log("stderr", "continue");
		return new Promise((resolve, reject) => {
			this.sendCommand("exec-continue" + (reverse ? " --reverse" : "")).then((info) => {
				resolve(info.resultRecords.resultClass == "running");
			}, reject);
		});
	}

	next(reverse: boolean = false): Thenable<boolean> {
		if (trace)
			this.log("stderr", "next");
		return new Promise((resolve, reject) => {
			this.sendCommand("exec-next" + (reverse ? " --reverse" : "")).then((info) => {
				resolve(info.resultRecords.resultClass == "running");
			}, reject);
		});
	}

	step(reverse: boolean = false): Thenable<boolean> {
		if (trace)
			this.log("stderr", "step");
		return new Promise((resolve, reject) => {
			this.sendCommand("exec-step" + (reverse ? " --reverse" : "")).then((info) => {
				resolve(info.resultRecords.resultClass == "running");
			}, reject);
		});
	}

	stepOut(reverse: boolean = false): Thenable<boolean> {
		if (trace)
			this.log("stderr", "stepOut");
		return new Promise((resolve, reject) => {
			this.sendCommand("exec-finish" + (reverse ? " --reverse" : "")).then((info) => {
				resolve(info.resultRecords.resultClass == "running");
			}, reject);
		});
	}

	goto(filename: string, line: number): Thenable<Boolean> {
		if (trace)
			this.log("stderr", "goto");
		return new Promise((resolve, reject) => {
			const target: string = '"' + (filename ? escape(filename) + ":" : "") + line + '"';
			this.sendCommand("break-insert -t " + target).then(() => {
				this.sendCommand("exec-jump " + target).then((info) => {
					resolve(info.resultRecords.resultClass == "running");
				}, reject);
			}, reject);
		});
	}

	changeVariableOriginal(name: string, rawValue: string): Thenable<any> {
		if (trace)
			this.log("stderr", "changeVariable");
		return this.sendCommand("gdb-set var " + name + "=" + rawValue);
	}

	changeVariable(name: string, rawValue: string): Thenable<any> {
		if (trace)
			this.log("stderr", "changeVariable");
			return this.sendStraightCommand("p/m " + name.substr(3) + "=" + rawValue);
		}

	loadBreakPoints(breakpoints: Breakpoint[]): Thenable<[boolean, Breakpoint][]> {
		if (trace)
			this.log("stderr", "loadBreakPoints");
		const promisses = [];
		breakpoints.forEach(breakpoint => {
			promisses.push(this.addBreakPoint(breakpoint));
		});
		return Promise.all(promisses);
	}

	setBreakPointCondition(bkptNum, condition): Thenable<any> {
		if (trace)
			this.log("stderr", "setBreakPointCondition");
		return this.sendCommand("break-condition " + bkptNum + " " + condition);
	}

	addBreakPoint(breakpoint: Breakpoint): Thenable<[boolean, Breakpoint]> {
		if (trace)
			this.log("stderr", "addBreakPoint");
		return new Promise((resolve, reject) => {
			if (this.breakpoints.has(breakpoint))
				return resolve([false, undefined]);
			let location = "";
			if (breakpoint.countCondition) {
				if (breakpoint.countCondition[0] == ">")
					location += "-i " + numRegex.exec(breakpoint.countCondition.substr(1))[0] + " ";
				else {
					const match = numRegex.exec(breakpoint.countCondition)[0];
					if (match.length != breakpoint.countCondition.length) {
						this.log("stderr", "Unsupported break count expression: '" + breakpoint.countCondition + "'. Only supports 'X' for breaking once after X times or '>X' for ignoring the first X breaks");
						location += "-t ";
					} else if (parseInt(match) != 0)
						location += "-t -i " + parseInt(match) + " ";
				}
			}
			if (breakpoint.raw)
				location += '"' + escape(breakpoint.raw) + '"';
			else
				location += '"' + escape(breakpoint.file) + ":" + breakpoint.line + '"';
			this.sendCommand("break-insert -f " + location).then((result) => {
				if (result.resultRecords.resultClass == "done") {
					const bkptNum = parseInt(result.result("bkpt.number"));
					const newBrk = {
						file: result.result("bkpt.file"),
						line: parseInt(result.result("bkpt.line")),
						condition: breakpoint.condition
					};
					if (breakpoint.condition) {
						this.setBreakPointCondition(bkptNum, breakpoint.condition).then((result) => {
							if (result.resultRecords.resultClass == "done") {
								this.breakpoints.set(newBrk, bkptNum);
								resolve([true, newBrk]);
							} else {
								resolve([false, undefined]);
							}
						}, reject);
					} else {
						this.breakpoints.set(newBrk, bkptNum);
						resolve([true, newBrk]);
					}
				} else {
					reject(result);
				}
			}, reject);
		});
	}

	removeBreakPoint(breakpoint: Breakpoint): Thenable<boolean> {
		if (trace)
			this.log("stderr", "removeBreakPoint");
		return new Promise((resolve, reject) => {
			if (!this.breakpoints.has(breakpoint))
				return resolve(false);
			this.sendCommand("break-delete " + this.breakpoints.get(breakpoint)).then((result) => {
				if (result.resultRecords.resultClass == "done") {
					this.breakpoints.delete(breakpoint);
					resolve(true);
				} else resolve(false);
			});
		});
	}

	clearBreakPoints(): Thenable<any> {
		if (trace)
			this.log("stderr", "clearBreakPoints");
		return new Promise((resolve, reject) => {
			this.sendCommand("break-delete").then((result) => {
				if (result.resultRecords.resultClass == "done") {
					this.breakpoints.clear();
					resolve(true);
				} else resolve(false);
			}, () => {
				resolve(false);
			});
		});
	}

	async getThreads(): Promise<Thread[]> {
		if (trace) this.log("stderr", "getThreads");

		const command = "thread-info";
		const result = await this.sendCommand(command);
		const threads = result.result("threads");
		const ret: Thread[] = [];
		return threads.map(element => {
			const ret: Thread = {
				id: parseInt(MINode.valueOf(element, "id")),
				targetId: MINode.valueOf(element, "target-id")
			};

			const name = MINode.valueOf(element, "name");
			if (name) {
				ret.name = name;
			}

			return ret;
		});
	}

	async getStack(maxLevels: number, thread: number): Promise<Stack[]> {
		if (trace) this.log("stderr", "getStack");

		let command = "stack-list-frames";
		if (thread != 0) {
			command += ` --thread ${thread}`;
		}
		if (maxLevels) {
			command += " 0 " + maxLevels;
		}
		const result = await this.sendCommand(command);
		const stack = result.result("stack");
		const ret: Stack[] = [];
		return stack.map(element => {
			const level = MINode.valueOf(element, "@frame.level");
			const addr = MINode.valueOf(element, "@frame.addr");
			const func = MINode.valueOf(element, "@frame.func");
			const filename = MINode.valueOf(element, "@frame.file");
			let file: string = MINode.valueOf(element, "@frame.fullname");
			if (file) {
				if (this.isSSH)
					file = posix.normalize(file);
				else
					file = nativePath.normalize(file);
			}

			let line = 0;
			const lnstr = MINode.valueOf(element, "@frame.line");
			if (lnstr)
				line = parseInt(lnstr);
			const from = parseInt(MINode.valueOf(element, "@frame.from"));
			return {
				address: addr,
				fileName: filename,
				file: file,
				function: func || from,
				level: level,
				line: line
			};
		});
	}

	async getStackVariablesOriginal(thread: number, frame: number): Promise<Variable[]> {
		if (trace)
			this.log("stderr", "getStackVariables");

		const result = await this.sendCommand(`stack-list-variables --thread ${thread} --frame ${frame} --simple-values`);
		const variables = result.result("variables");
		const ret: Variable[] = [];
		for (const element of variables) {
			const key = MINode.valueOf(element, "name");
			const value = MINode.valueOf(element, "value");
			const type = MINode.valueOf(element, "type");
			ret.push({
				name: key,
				valueStr: value,
				type: type,
				raw: element
			});
		}
		return ret;
	}
	async getStackVariables(thread: number, frame: number): Promise<Variable[]> {
		if (trace)
			this.log("stderr", "getStackVariables");

		const result = await this.sendStraightCommand(`p/m ?`);
		const variables = result.result("variables");
		const ret: Variable[] = [];
		for (const element of variables) {
			const key = MINode.valueOf(element, "name");
			const value = MINode.valueOf(element, "value");
			const type = MINode.valueOf(element, "type");
			ret.push({
				name: key,
				valueStr: value,
				type: type,
				raw: element
			});
		}
		return ret;
	}

	examineMemory(from: number, length: number): Thenable<any> {7
		if (trace)
			this.log("stderr", "examineMemory");
		return new Promise((resolve, reject) => {
			this.sendCommand("data-read-memory-bytes 0x" + from.toString(16) + " " + length).then((result) => {
				resolve(result.result("memory[0].contents"));
			}, reject);
		});
	}

	async evalExpression(name: string, thread: number, frame: number): Promise<MINode> {
		if (trace) {
			this.log("stderr", "evalExpression");
		}

		let command = "p/m ";
		command += name;
		//console.log("evalExpression(): ", command); // DUBNERDEBUG
		return await this.sendStraightCommand(command);

//		let command = "data-evaluate-expression ";
//		if (thread != 0) {
//			command += `--thread ${thread} --frame ${frame} `;
//		}
//		command += name;
//		//console.log(command); // DUBNERDEBUG
//		return await this.sendCommand(command);
	}

	async varCreate(expression: string, name: string = "-"): Promise<VariableObject> {
		if (trace)
			this.log("stderr", "varCreate");
		const res = await this.sendCommand(`var-create ${name} @ "${expression}"`);
		return new VariableObject(res.result(""));
	}

	async varEvalExpression(name: string): Promise<MINode> {
		if (trace)
			this.log("stderr", "varEvalExpression");
		return this.sendCommand(`var-evaluate-expression ${name}`);
	}

	async varListChildren(name: string): Promise<VariableObject[]> {
		if (trace)
			this.log("stderr", "varListChildren");
		//TODO: add `from` and `to` arguments
		const res = await this.sendCommand(`var-list-children --all-values ${name}`);
		const children = res.result("children") || [];
		const omg: VariableObject[] = children.map(child => new VariableObject(child[1]));
		return omg;
	}

	async varUpdate(name: string = "*"): Promise<MINode> {
		if (trace)
			this.log("stderr", "varUpdate");
		return this.sendCommand(`var-update --all-values ${name}`);
	}

	async varAssign(name: string, rawValue: string): Promise<MINode> {
		if (trace)
			this.log("stderr", "varAssign");
		return this.sendCommand(`var-assign ${name} ${rawValue}`);
	}

	logNoNewLine(type: string, msg: string) {
		this.emit("msg", type, msg);
	}

	log(type: string, msg: string) {
		this.emit("msg", type, msg[msg.length - 1] == '\n' ? msg : (msg + "\n"));
	}

	sendUserInput(command: string, threadId: number = 0, frameLevel: number = 0): Thenable<any> {
	if (command.startsWith("-")) {
			return this.sendCommand(command.substr(1));
		} else {
			return this.sendCliCommand(command, threadId, frameLevel);
		}
	}

	sendRaw(raw: string) {
		//console.log("sendRaw():", raw);	// DUBNERDEBUG
		if (this.printCalls)
			this.log("log", raw);
		if (this.isSSH)
			this.stream.write(raw + "\n");
		else
			this.process.stdin.write(raw + "\n");
	}

	async sendCliCommand(command: string, threadId: number = 0, frameLevel: number = 0) {
		// For some reason, somebody, somewhere, is sending a console command "process.pid", which
		// just confuses everybody and creates an error message the user can see.
		// Let's nip this one in the bud.
		
		if( command == "process.pid" ){
			return;
		}

		let miCommand = "interpreter-exec ";
		if (threadId != 0) {
			miCommand += `--thread ${threadId} --frame ${frameLevel} `;
		}
		miCommand += `console "${command.replace(/[\\"']/g, '\\$&')}"`;
		await this.sendCommand(miCommand);
	}

	sendCommand(command: string, suppressFailure: boolean = false): Thenable<MINode> {
		const sel = this.currentToken++;
		this.recent_variables_request = -1;
		return new Promise((resolve, reject) => {
			this.handlers[sel] = (node: MINode) => {
				if (node && node.resultRecords && node.resultRecords.resultClass === "error") {
					if (suppressFailure) {
						this.log("stderr", `WARNING: Error executing command '${command}'`);
						resolve(node);
					} else
						reject(new MIError(node.result("msg") || "Internal error", command));
				} else
					resolve(node);
			};
			this.sendRaw(sel + "-" + command);
		});
	}

	sendStraightCommand(command: string, suppressFailure: boolean = false): Thenable<MINode> {
		const sel = this.currentToken++;
		this.recent_variables_request = sel;
		return new Promise((resolve, reject) => {
			this.handlers[sel] = (node: MINode) => {
				if (node && node.resultRecords && node.resultRecords.resultClass === "error") {
					if (suppressFailure) {
						this.log("stderr", `WARNING: Error executing command '${command}'`);
						resolve(node);
					} else
						reject(new MIError(node.result("msg") || "Internal error", command));
				} else
					resolve(node);
			};
			this.sendRaw(sel + command);
		});
	}


	isReady(): boolean {
		return this.isSSH ? this.sshReady : !!this.process;
	}

	prettyPrint: boolean = true;
	printCalls: boolean;
	debugOutput: boolean;
	public procEnv: any;
	protected isSSH: boolean;
	protected sshReady: boolean;
	protected currentToken: number = 1;
	protected handlers: { [index: number]: (info: MINode) => any } = {};
	protected breakpoints: Map<Breakpoint, Number> = new Map();
	protected buffer: string;
	protected errbuf: string;
	protected process: ChildProcess.ChildProcess;
	protected stream;
	protected sshConn;
	protected recent_variables_request: number; // Most recent MI token for 'p/m ?' variables request
}