/*
 *
 *  ===============
 *  Duktape license
 *  ===============
 *  
 *  (http://opensource.org/licenses/MIT)
 *  
 *  Copyright (c) 2013-2017 by Duktape authors (see AUTHORS.rst)
 *  
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 *  
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *  THE SOFTWARE.
 */

/* AUTHORS.rst */
/*
 *  ===============
 *  Duktape authors
 *  ===============
 *  
 *  Copyright
 *  =========
 *  
 *  Duktape copyrights are held by its authors.  Each author has a copyright
 *  to their contribution, and agrees to irrevocably license the contribution
 *  under the Duktape ``LICENSE.txt``.
 *  
 *  Authors
 *  =======
 *  
 *  Please include an e-mail address, a link to your GitHub profile, or something
 *  similar to allow your contribution to be identified accurately.
 *  
 *  The following people have contributed code, website contents, or Wiki contents,
 *  and agreed to irrevocably license their contributions under the Duktape
 *  ``LICENSE.txt`` (in order of appearance):
 *  
 *  * Sami Vaarala <sami.vaarala@iki.fi>
 *  * Niki Dobrev
 *  * Andreas \u00d6man <andreas@lonelycoder.com>
 *  * L\u00e1szl\u00f3 Lang\u00f3 <llango.u-szeged@partner.samsung.com>
 *  * Legimet <legimet.calc@gmail.com>
 *  * Karl Skomski <karl@skomski.com>
 *  * Bruce Pascoe <fatcerberus1@gmail.com>
 *  * Ren\u00e9 Hollander <rene@rene8888.at>
 *  * Julien Hamaide (https://github.com/crazyjul)
 *  * Sebastian G\u00f6tte (https://github.com/jaseg)
 *  * Tomasz Magulski (https://github.com/magul)
 *  * \D. Bohdan (https://github.com/dbohdan)
 *  * Ond\u0159ej Jirman (https://github.com/megous)
 *  * Sa\u00fal Ibarra Corretg\u00e9 <saghul@gmail.com>
 *  * Jeremy HU <huxingyi@msn.com>
 *  * Ole Andr\u00e9 Vadla Ravn\u00e5s (https://github.com/oleavr)
 *  * Harold Brenes (https://github.com/harold-b)
 *  * Oliver Crow (https://github.com/ocrow)
 *  * Jakub Ch\u0142api\u0144ski (https://github.com/jchlapinski)
 *  * Brett Vickers (https://github.com/beevik)
 *  * Dominik Okwieka (https://github.com/okitec)
 *  * Remko Tron\u00e7on (https://el-tramo.be)
 *  * Romero Malaquias (rbsm@ic.ufal.br)
 *  * Michael Drake <michael.drake@codethink.co.uk>
 *  * Steven Don (https://github.com/shdon)
 *  * Simon Stone (https://github.com/sstone1)
 *  * \J. McC. (https://github.com/jmhmccr)
 *  
 *  Other contributions
 *  ===================
 *  
 *  The following people have contributed something other than code (e.g. reported
 *  bugs, provided ideas, etc; roughly in order of appearance):
 *  
 *  * Greg Burns
 *  * Anthony Rabine
 *  * Carlos Costa
 *  * Aur\u00e9lien Bouilland
 *  * Preet Desai (Pris Matic)
 *  * judofyr (http://www.reddit.com/user/judofyr)
 *  * Jason Woofenden
 *  * Micha\u0142 Przyby\u015b
 *  * Anthony Howe
 *  * Conrad Pankoff
 *  * Jim Schimpf
 *  * Rajaran Gaunker (https://github.com/zimbabao)
 *  * Andreas \u00d6man
 *  * Doug Sanden
 *  * Josh Engebretson (https://github.com/JoshEngebretson)
 *  * Remo Eichenberger (https://github.com/remoe)
 *  * Mamod Mehyar (https://github.com/mamod)
 *  * David Demelier (https://github.com/markand)
 *  * Tim Caswell (https://github.com/creationix)
 *  * Mitchell Blank Jr (https://github.com/mitchblank)
 *  * https://github.com/yushli
 *  * Seo Sanghyeon (https://github.com/sanxiyn)
 *  * Han ChoongWoo (https://github.com/tunz)
 *  * Joshua Peek (https://github.com/josh)
 *  * Bruce E. Pascoe (https://github.com/fatcerberus)
 *  * https://github.com/Kelledin
 *  * https://github.com/sstruchtrup
 *  * Michael Drake (https://github.com/tlsa)
 *  * https://github.com/chris-y
 *  * Laurent Zubiaur (https://github.com/lzubiaur)
 *  * Neil Kolban (https://github.com/nkolban)
 *  
 *  If you are accidentally missing from this list, send me an e-mail
 *  (``sami.vaarala@iki.fi``) and I'll fix the omission.
 *
 *
 * =================================================================
 * Modifications made by Bryan Roe, Copyright 2018 Intel Corporation
 * =================================================================
 *
 *
 */


var events = require('events');
var Promise = require('promise');
var dbgHTML = ' ';

var optTargetHost = '127.0.0.1';
var optTargetPort = 9091;
var optHttpPort = 9092;
var optJsonProxyPort = 9093;
var optJsonProxy = false;
var optSourceSearchDirs = ['../tests/ecmascript'];
var optDumpDebugRead = null;
var optDumpDebugWrite = null;
var optDumpDebugPretty = null;
var optLogMessages = true;

// Marker objects for special protocol values
var DVAL_EOM = { type: 'eom' };
var DVAL_REQ = { type: 'req' };
var DVAL_REP = { type: 'rep' };
var DVAL_ERR = { type: 'err' };
var DVAL_NFY = { type: 'nfy' };


// Commands initiated by Duktape
var CMD_STATUS = 0x01;
var CMD_UNUSED_2 = 0x02;  // Duktape 1.x: print notify
var CMD_UNUSED_3 = 0x03;  // Duktape 1.x: alert notify
var CMD_UNUSED_4 = 0x04;  // Duktape 1.x: log notify
var CMD_THROW = 0x05;
var CMD_DETACHING = 0x06;
var CMD_APPNOTIFY = 0x07;

// Commands initiated by the debug client (= us)
var CMD_BASICINFO = 0x10;
var CMD_TRIGGERSTATUS = 0x11;
var CMD_PAUSE = 0x12;
var CMD_RESUME = 0x13;
var CMD_STEPINTO = 0x14;
var CMD_STEPOVER = 0x15;
var CMD_STEPOUT = 0x16;
var CMD_LISTBREAK = 0x17;
var CMD_ADDBREAK = 0x18;
var CMD_DELBREAK = 0x19;
var CMD_GETVAR = 0x1a;
var CMD_PUTVAR = 0x1b;
var CMD_GETCALLSTACK = 0x1c;
var CMD_GETLOCALS = 0x1d;
var CMD_EVAL = 0x1e;
var CMD_DETACH = 0x1f;
var CMD_DUMPHEAP = 0x20;
var CMD_GETBYTECODE = 0x21;

// Constants
var UI_MESSAGE_CLIPLEN = 128;
var LOCALS_CLIPLEN = 64;
var EVAL_CLIPLEN = 4096;
var GETVAR_CLIPLEN = 4096;
var SUPPORTED_DEBUG_PROTOCOL_VERSION = 2;


var debugCommandNames = {};
debugCommandNames['0'] = 'Reserved_0';
debugCommandNames['1'] = 'Status';
debugCommandNames['2'] = 'Reserved_2';
debugCommandNames['3'] = 'Reserved_3';
debugCommandNames['4'] = 'Reserved_4';
debugCommandNames['5'] = 'Throw';
debugCommandNames['6'] = 'Detaching';
debugCommandNames['7'] = 'AppNotify';
debugCommandNames['8'] = 'Reserved_8';
debugCommandNames['9'] = 'Reserved_9';
debugCommandNames['10'] = 'Reserved_10';
debugCommandNames['11'] = 'Reserved_11';
debugCommandNames['12'] = 'Reserved_12';
debugCommandNames['13'] = 'Reserved_13';
debugCommandNames['14'] = 'Reserved_14';
debugCommandNames['15'] = 'Reserved_15';
debugCommandNames['16'] = 'BasicInfo';
debugCommandNames['17'] = 'TriggerStatus';
debugCommandNames['18'] = 'Pause';
debugCommandNames['19'] = 'Resume';
debugCommandNames['20'] = 'StepInto';
debugCommandNames['21'] = 'StepOver';
debugCommandNames['22'] = 'StepOut';
debugCommandNames['23'] = 'ListBreak';
debugCommandNames['24'] = 'AddBreak';
debugCommandNames['25'] = 'DelBreak';
debugCommandNames['26'] = 'GetVar';
debugCommandNames['27'] = 'PutVar';
debugCommandNames['28'] = 'GetCallStack';
debugCommandNames['29'] = 'GetLocals';
debugCommandNames['30'] = 'Eval';
debugCommandNames['31'] = 'Detach';
debugCommandNames['32'] = 'DumpHeap';
debugCommandNames['33'] = 'GetBytecode';
debugCommandNames['34'] = 'AppRequest';
debugCommandNames['35'] = 'GetHeapObjInfo';
debugCommandNames['36'] = 'GetObjPropDesc';
debugCommandNames['37'] = 'GetObjPropDescRange';

// Duktape heaphdr type constants, must match C headers
var DUK_HTYPE_STRING = 0;
var DUK_HTYPE_OBJECT = 1;
var DUK_HTYPE_BUFFER = 2;

/*
 *  Debugger output formatting
 */

function formatDebugValue(v) {
    var buf, dec, len;

    // See doc/debugger.rst for format description.

    if (typeof v === 'object' && v !== null) {
        // Note: typeof null === 'object', so null special case explicitly
        if (v.type === 'eom') {
            return new Buffer([0x00]);
        } else if (v.type === 'req') {
            return new Buffer([0x01]);
        } else if (v.type === 'rep') {
            return new Buffer([0x02]);
        } else if (v.type === 'err') {
            return new Buffer([0x03]);
        } else if (v.type === 'nfy') {
            return new Buffer([0x04]);
        } else if (v.type === 'unused') {
            return new Buffer([0x15]);
        } else if (v.type === 'undefined') {
            return new Buffer([0x16]);
        } else if (v.type === 'number') {
            dec = new Buffer(v.data, 'hex');
            len = dec.length;
            if (len !== 8) {
                throw new TypeError('value cannot be converted to dvalue: ' + JSON.stringify(v));
            }
            buf = Buffer.alloc(1 + len);
            buf[0] = 0x1a;
            dec.copy(buf, 1);
            return buf;
        } else if (v.type === 'buffer') {
            dec = new Buffer(v.data, 'hex');
            len = dec.length;
            if (len <= 0xffff) {
                buf = Buffer.alloc(3 + len);
                buf[0] = 0x14;
                buf[1] = (len >> 8) & 0xff;
                buf[2] = (len >> 0) & 0xff;
                dec.copy(buf, 3);
                return buf;
            } else {
                buf = Buffer.alloc(5 + len);
                buf[0] = 0x13;
                buf[1] = (len >> 24) & 0xff;
                buf[2] = (len >> 16) & 0xff;
                buf[3] = (len >> 8) & 0xff;
                buf[4] = (len >> 0) & 0xff;
                dec.copy(buf, 5);
                return buf;
            }
        } else if (v.type === 'object') {
            dec = new Buffer(v.pointer, 'hex');
            len = dec.length;
            buf = Buffer.alloc(3 + len);
            buf[0] = 0x1b;
            buf[1] = v.class;
            buf[2] = len;
            dec.copy(buf, 3);
            return buf;
        } else if (v.type === 'pointer') {
            dec = new Buffer(v.pointer, 'hex');
            len = dec.length;
            buf = Buffer.alloc(2 + len);
            buf[0] = 0x1c;
            buf[1] = len;
            dec.copy(buf, 2);
            return buf;
        } else if (v.type === 'lightfunc') {
            dec = new Buffer(v.pointer, 'hex');
            len = dec.length;
            buf = Buffer.alloc(4 + len);
            buf[0] = 0x1d;
            buf[1] = (v.flags >> 8) & 0xff;
            buf[2] = v.flags & 0xff;
            buf[3] = len;
            dec.copy(buf, 4);
            return buf;
        } else if (v.type === 'heapptr') {
            dec = new Buffer(v.pointer, 'hex');
            len = dec.length;
            buf = Buffer.alloc(2 + len);
            buf[0] = 0x1e;
            buf[1] = len;
            dec.copy(buf, 2);
            return buf;
        }
    } else if (v === null) {
        return new Buffer([0x17]);
    } else if (typeof v === 'boolean') {
        return new Buffer([v ? 0x18 : 0x19]);
    } else if (typeof v === 'number') {
        if (Math.floor(v) === v &&     /* whole */
            (v !== 0 || 1 / v > 0) &&  /* not negative zero */
            v >= -0x80000000 && v <= 0x7fffffff) {
            // Represented signed 32-bit integers as plain integers.
            // Debugger code expects this for all fields that are not
            // duk_tval representations (e.g. command numbers and such).
            if (v >= 0x00 && v <= 0x3f) {
                return new Buffer([0x80 + v]);
            } else if (v >= 0x0000 && v <= 0x3fff) {
                return new Buffer([0xc0 + (v >> 8), v & 0xff]);
            } else if (v >= -0x80000000 && v <= 0x7fffffff) {
                return new Buffer([0x10,
                                    (v >> 24) & 0xff,
                                    (v >> 16) & 0xff,
                                    (v >> 8) & 0xff,
                                    (v >> 0) & 0xff]);
            } else {
                throw new Error('internal error when encoding integer to dvalue: ' + v);
            }
        } else {
            // Represent non-integers as IEEE double dvalues
            buf = Buffer.alloc(1 + 8);
            buf[0] = 0x1a;
            buf.writeDoubleBE(v, 1);
            return buf;
        }
    } else if (typeof v === 'string') {
        if (v.length < 0 || v.length > 0xffffffff) {
            // Not possible in practice.
            throw new TypeError('cannot convert to dvalue, invalid string length: ' + v.length);
        }
        if (v.length <= 0x1f) {
            buf = Buffer.alloc(1 + v.length);
            buf[0] = 0x60 + v.length;
            writeDebugStringToBuffer(v, buf, 1);
            return buf;
        } else if (v.length <= 0xffff) {
            buf = Buffer.alloc(3 + v.length);
            buf[0] = 0x12;
            buf[1] = (v.length >> 8) & 0xff;
            buf[2] = (v.length >> 0) & 0xff;
            writeDebugStringToBuffer(v, buf, 3);
            return buf;
        } else {
            buf = Buffer.alloc(5 + v.length);
            buf[0] = 0x11;
            buf[1] = (v.length >> 24) & 0xff;
            buf[2] = (v.length >> 16) & 0xff;
            buf[3] = (v.length >> 8) & 0xff;
            buf[4] = (v.length >> 0) & 0xff;
            writeDebugStringToBuffer(v, buf, 5);
            return buf;
        }
    }

    // Shouldn't come here.
    throw new TypeError('value cannot be converted to dvalue: ' + JSON.stringify(v));
}


function DebugProtocolParser(inputStream,
                             protocolVersion,
                             rawDumpFileName,
                             textDumpFileName,
                             textDumpFilePrefix,
                             hexDumpConsolePrefix,
                             textDumpConsolePrefix)
{
    var _this = this;
    this._ObjectID = 'DebugProtocolParser';
    this.inputStream = inputStream;
    this.closed = false;       // stream is closed/broken, don't parse anymore
    this.bytes = 0;
    this.dvalues = 0;
    this.messages = 0;
    this.requests = 0;
    this.prevBytes = 0;
    this.bytesPerSec = 0;
    this.statsTimer = null;
    this.readableNumberValue = true;

    events.EventEmitter.call(this);

    var buf = Buffer.alloc(0);    // accumulate data
    var msg = [];               // accumulated message until EOM
    var versionIdentification;

    var statsInterval = 2000;
    var statsIntervalSec = statsInterval / 1000;
    this.statsTimer = setInterval(function () {
        _this.bytesPerSec = (_this.bytes - _this.prevBytes) / statsIntervalSec;
        _this.prevBytes = _this.bytes;
        _this.emit('stats-update');
    }, statsInterval);

    function consume(n) {
        var tmp = Buffer.alloc(buf.length - n);
        buf.copy(tmp, 0, n);
        buf = tmp;
    }

    inputStream.on('data', function (data) {
        var i, n, x, v, gotValue, len, t, tmpbuf, verstr;
        var prettyMsg;

        if (_this.closed || !_this.inputStream)
        {
            console.error('Ignoring incoming data from closed input stream, len ' + data.length);
            return;
        }

        _this.bytes += data.length;
        if (rawDumpFileName) {
            fs.appendFileSync(rawDumpFileName, data);
        }
        if (hexDumpConsolePrefix) {
            console.info1(hexDumpConsolePrefix + data.toString('hex'));
        }

        buf = Buffer.concat([buf, data]);

        // Protocol version handling.  When dumping an output stream, the
        // caller gives a non-null protocolVersion so we don't read one here.

        if (protocolVersion == null) {
            if (buf.length > 1024) {
                _this.emit('transport-error', 'Parse error (version identification too long), dropping connection');
                _this.close();
                return;
            }


            for (i = 0, n = buf.length; i < n; i++)
            {
                if (buf[i] == 0x0a)
                {
                    tmpbuf = Buffer.alloc(i);
                    buf.copy(tmpbuf, 0, 0, i);
                    consume(i + 1);
                    verstr = tmpbuf.toString();
                    t = verstr.split(' ');
                    protocolVersion = Number(t[0]);
                    versionIdentification = verstr;

                    _this.emit('protocol-version', {
                        protocolVersion: protocolVersion,
                        versionIdentification: versionIdentification
                    });
                    break;
                }
            }

            if (protocolVersion == null)
            {
                // Still waiting for version identification to complete.
                return;
            }
        }

        // Parse complete dvalues (quite inefficient now) by trial parsing.
        // Consume a value only when it's fully present in 'buf'.
        // See doc/debugger.rst for format description.

        while (buf.length > 0) {
            x = buf[0];
            v = undefined;
            gotValue = false;  // used to flag special values like undefined

            if (x >= 0xc0) {
                // 0xc0...0xff: integers 0-16383
                if (buf.length >= 2) {
                    v = ((x - 0xc0) << 8) + buf[1];
                    consume(2);
                }
            } else if (x >= 0x80) {
                // 0x80...0xbf: integers 0-63
                v = x - 0x80;
                consume(1);
            } else if (x >= 0x60) {
                // 0x60...0x7f: strings with length 0-31
                len = x - 0x60;
                if (buf.length >= 1 + len) {
                    v = Buffer.alloc(len);
                    buf.copy(v, 0, 1, 1 + len);
                    v = bufferToDebugString(v);
                    consume(1 + len);
                }
            } else {
                switch (x) {
                    case 0x00: v = DVAL_EOM; consume(1); break;
                    case 0x01: v = DVAL_REQ; consume(1); break;
                    case 0x02: v = DVAL_REP; consume(1); break;
                    case 0x03: v = DVAL_ERR; consume(1); break;
                    case 0x04: v = DVAL_NFY; consume(1); break;
                    case 0x10:  // 4-byte signed integer
                        if (buf.length >= 5) {
                            v = buf.readInt32BE(1);
                            consume(5);
                        }
                        break;
                    case 0x11:  // 4-byte string
                        if (buf.length >= 5) {
                            len = buf.readUInt32BE(1);
                            if (buf.length >= 5 + len) {
                                v = Buffer.alloc(len);
                                buf.copy(v, 0, 5, 5 + len);
                                v = bufferToDebugString(v);
                                consume(5 + len);
                            }
                        }
                        break;
                    case 0x12:  // 2-byte string
                        if (buf.length >= 3) {
                            len = buf.readUInt16BE(1);
                            if (buf.length >= 3 + len) {
                                v = Buffer.alloc(len);
                                buf.copy(v, 0, 3, 3 + len);
                                v = bufferToDebugString(v);
                                consume(3 + len);
                            }
                        }
                        break;
                    case 0x13:  // 4-byte buffer
                        if (buf.length >= 5) {
                            len = buf.readUInt32BE(1);
                            if (buf.length >= 5 + len) {
                                v = Buffer.alloc(len);
                                buf.copy(v, 0, 5, 5 + len);
                                v = { type: 'buffer', data: v.toString('hex') };
                                consume(5 + len);
                                // Value could be a Node.js buffer directly, but
                                // we prefer all dvalues to be JSON compatible
                            }
                        }
                        break;
                    case 0x14:  // 2-byte buffer
                        if (buf.length >= 3) {
                            len = buf.readUInt16BE(1);
                            if (buf.length >= 3 + len) {
                                v = Buffer.alloc(len);
                                buf.copy(v, 0, 3, 3 + len);
                                v = { type: 'buffer', data: v.toString('hex') };
                                consume(3 + len);
                                // Value could be a Node.js buffer directly, but
                                // we prefer all dvalues to be JSON compatible
                            }
                        }
                        break;
                    case 0x15:  // unused/none
                        v = { type: 'unused' };
                        consume(1);
                        break;
                    case 0x16:  // undefined
                        v = { type: 'undefined' };
                        gotValue = true;  // indicate 'v' is actually set
                        consume(1);
                        break;
                    case 0x17:  // null
                        v = null;
                        gotValue = true;  // indicate 'v' is actually set
                        consume(1);
                        break;
                    case 0x18:  // true
                        v = true;
                        consume(1);
                        break;
                    case 0x19:  // false
                        v = false;
                        consume(1);
                        break;
                    case 0x1a:  // number (IEEE double), big endian
                        if (buf.length >= 9) {
                            v = Buffer.alloc(8);
                            buf.copy(v, 0, 1, 9);
                            v = { type: 'number', data: v.toString('hex') };

                            if (_this.readableNumberValue) {
                                // The value key should not be used programmatically,
                                // it is just there to make the dumps more readable.
                                v.value = buf.readDoubleBE(1);
                            }
                            consume(9);
                        }
                        break;
                    case 0x1b:  // object
                        if (buf.length >= 3) {
                            len = buf[2];
                            if (buf.length >= 3 + len) {
                                v = Buffer.alloc(len);
                                buf.copy(v, 0, 3, 3 + len);
                                v = { type: 'object', 'class': buf[1], pointer: v.toString('hex') };
                                consume(3 + len);
                            }
                        }
                        break;
                    case 0x1c:  // pointer
                        if (buf.length >= 2) {
                            len = buf[1];
                            if (buf.length >= 2 + len) {
                                v = Buffer.alloc(len);
                                buf.copy(v, 0, 2, 2 + len);
                                v = { type: 'pointer', pointer: v.toString('hex') };
                                consume(2 + len);
                            }
                        }
                        break;
                    case 0x1d:  // lightfunc
                        if (buf.length >= 4) {
                            len = buf[3];
                            if (buf.length >= 4 + len) {
                                v = Buffer.alloc(len);
                                buf.copy(v, 0, 4, 4 + len);
                                v = { type: 'lightfunc', flags: buf.readUInt16BE(1), pointer: v.toString('hex') };
                                consume(4 + len);
                            }
                        }
                        break;
                    case 0x1e:  // heapptr
                        if (buf.length >= 2) {
                            len = buf[1];
                            if (buf.length >= 2 + len) {
                                v = Buffer.alloc(len);
                                buf.copy(v, 0, 2, 2 + len);
                                v = { type: 'heapptr', pointer: v.toString('hex') };
                                consume(2 + len);
                            }
                        }
                        break;
                    default:
                        _this.emit('transport-error', 'Parse error, dropping connection');
                        _this.close();
                }
            }

            if (typeof v === 'undefined' && !gotValue) {
                break;
            }
            msg.push(v);
            _this.dvalues++;

            // Could emit a 'debug-value' event here, but that's not necessary
            // because the receiver will just collect statistics which can also
            // be done using the finished message.

            if (v === DVAL_EOM)
            {
                _this.messages++;

                if (textDumpFileName || textDumpConsolePrefix) {
                    prettyMsg = prettyDebugMessage(msg);
                    if (textDumpFileName) {
                        fs.appendFileSync(textDumpFileName, (textDumpFilePrefix || '') + prettyMsg + '\n');
                    }
                    if (textDumpConsolePrefix) {
                        console.info1(textDumpConsolePrefix + prettyMsg);
                    }
                }

                _this.emit('debug-message', msg);
                msg = [];  // new object, old may be in circulation for a while
            }
        }
    });

    // Not all streams will emit this.
    inputStream.on('error', function (err) {
        _this.emit('transport-error', err);
        _this.close();
    });

    // Not all streams will emit this.
    inputStream.on('close', function () {
        _this.close();
    });
}

DebugProtocolParser.prototype.close = function () {
    // Although the underlying transport may not have a close() or destroy()
    // method or even a 'close' event, this method is always available and
    // will generate a 'transport-close'.
    //
    // The caller is responsible for closing the underlying stream if that
    // is necessary.

    if (this.closed) { return; }

    this.closed = true;
    if (this.statsTimer) {
        clearInterval(this.statsTimer);
        this.statsTimer = null;
    }
    this.emit('transport-close');
};


/*
 *  Debugger implementation
 *
 *  A debugger instance communicates with the debug target and maintains
 *  persistent debug state so that the current state can be resent to the
 *  socket.io client (web UI) if it reconnects.  Whenever the debugger state
 *  is changed an event is generated.  The socket.io handler will listen to
 *  state change events and push the necessary updates to the web UI, often
 *  in a rate limited fashion or using a client pull to ensure the web UI
 *  is not overloaded.
 *
 *  The debugger instance assumes that if the debug protocol connection is
 *  re-established, it is always to the same target.  There is no separate
 *  abstraction for a debugger session.
 */

function Debugger()
{
    events.EventEmitter.call(this);
    this._ObjectID = 'Debugger';
    this.web = null;                      // web UI singleton
    this.targetStream = null;             // transport connection to target
    this.outputPassThroughStream = null;  // dummy passthrough for message dumping
    this.inputParser = null;              // parser for incoming debug messages
    this.outputParser = null;             // parser for outgoing debug messages (stats, dumping)
    this.protocolVersion = null;
    this.dukVersion = null;
    this.dukGitDescribe = null;
    this.targetInfo = null;
    this.debugger_attached = false;
    this.handshook = false;
    this.reqQueue = null;
    this.stats = {                        // stats for current debug connection
        rxBytes: 0, rxDvalues: 0, rxMessages: 0, rxBytesPerSec: 0,
        txBytes: 0, txDvalues: 0, txMessages: 0, txBytesPerSec: 0
    };
    this.execStatus = {
        attached: false,
        state: 'detached',
        fileName: '',
        funcName: '',
        line: 0,
        pc: 0
    };
    this.breakpoints = [];
    this.callstack = [];
    this.locals = [];
    this.messageLines = [];
    this.messageScrollBack = 100;

    this.on('attached', function OnAttached()
    {
        this.websocket.write({ cmd: 'ATTACH' });
        console.info1('Debug transport connected');

        this.debugger_attached = true;
        this.reqQueue = [];
        this.uiMessage('debugger-info', 'Debug transport connected');

        this.sendStatusRequest().then(function () { statusPending = false; console.info1('finally'); }, console.info1);
        hostCooperate();
    });
}


Debugger.prototype.decodeBytecodeFromBuffer = function (buf, consts, funcs)
{
    var i, j, n, m, ins, pc;
    var res = [];
    var op, str, args, comments;

    // XXX: add constants inline to preformatted output (e.g. for strings,
    // add a short escaped snippet as a comment on the line after the
    // compact argument list).

    for (i = 0, n = buf.length; i < n; i += 4) {
        pc = i / 4;

        // shift forces unsigned
        if (this.endianness === 'little') {
            ins = buf.readInt32LE(i) >>> 0;
        } else {
            ins = buf.readInt32BE(i) >>> 0;
        }

        op = dukOpcodes.opcodes[ins & 0xff];

        args = [];
        comments = [];
        if (op.args) {
            for (j = 0, m = op.args.length; j < m; j++) {
                var A = (ins >>> 8) & 0xff;
                var B = (ins >>> 16) & 0xff;
                var C = (ins >>> 24) & 0xff;
                var BC = (ins >>> 16) & 0xffff;
                var ABC = (ins >>> 8) & 0xffffff;
                var Bconst = op & 0x01;
                var Cconst = op & 0x02;

                switch (op.args[j]) {
                    case 'A_R': args.push('r' + A); break;
                    case 'A_RI': args.push('r' + A + '(indirect)'); break;
                    case 'A_C': args.push('c' + A); break;
                    case 'A_H': args.push('0x' + A.toString(16)); break;
                    case 'A_I': args.push(A.toString(10)); break;
                    case 'A_B': args.push(A ? 'true' : 'false'); break;
                    case 'B_RC': args.push((Bconst ? 'c' : 'r') + B); break;
                    case 'B_R': args.push('r' + B); break;
                    case 'B_RI': args.push('r' + B + '(indirect)'); break;
                    case 'B_C': args.push('c' + B); break;
                    case 'B_H': args.push('0x' + B.toString(16)); break;
                    case 'B_I': args.push(B.toString(10)); break;
                    case 'C_RC': args.push((Cconst ? 'c' : 'r') + C); break;
                    case 'C_R': args.push('r' + C); break;
                    case 'C_RI': args.push('r' + C + '(indirect)'); break;
                    case 'C_C': args.push('c' + C); break;
                    case 'C_H': args.push('0x' + C.toString(16)); break;
                    case 'C_I': args.push(C.toString(10)); break;
                    case 'BC_R': args.push('r' + BC); break;
                    case 'BC_C': args.push('c' + BC); break;
                    case 'BC_H': args.push('0x' + BC.toString(16)); break;
                    case 'BC_I': args.push(BC.toString(10)); break;
                    case 'ABC_H': args.push(ABC.toString(16)); break;
                    case 'ABC_I': args.push(ABC.toString(10)); break;
                    case 'BC_LDINT': args.push(BC - (1 << 15)); break;
                    case 'BC_LDINTX': args.push(BC - 0); break;  // no bias in LDINTX
                    case 'ABC_JUMP': {
                        var pc_add = ABC - (1 << 23) + 1;  // pc is preincremented before adding
                        var pc_dst = pc + pc_add;
                        args.push(pc_dst + ' (' + (pc_add >= 0 ? '+' : '') + pc_add + ')');
                        break;
                    }
                    default: args.push('?'); break;
                }
            }
        }
        if (op.flags) {
            for (j = 0, m = op.flags.length; j < m; j++) {
                if (ins & op.flags[j].mask) {
                    comments.push(op.flags[j].name);
                }
            }
        }

        if (args.length > 0) {
            str = sprintf('%05d %08x   %-10s %s', pc, ins, op.name, args.join(', '));
        } else {
            str = sprintf('%05d %08x   %-10s', pc, ins, op.name);
        }
        if (comments.length > 0) {
            str = sprintf('%-44s ; %s', str, comments.join(', '));
        }

        res.push({
            str: str,
            ins: ins
        });
    }

    return res;
};

Debugger.prototype.uiMessage = function (type, val)
{
    var msg;
    if (typeof type === 'object') {
        msg = type;
    } else if (typeof type === 'string') {
        msg = { type: type, message: val };
    } else {
        throw new TypeError('invalid ui message: ' + type);
    }
    this.messageLines.push(msg);
    while (this.messageLines.length > this.messageScrollBack) {
        this.messageLines.shift();
    }
    this.emit('ui-message-update');  // just trigger a sync, gets rate limited
};

Debugger.prototype.sendRequest = function (msg)
{
    var _this = this;
    return new Promise(function (resolve, reject) {
        var dvals = [];
        var dval;
        var data;
        var i;

        if (!_this.debugger_attached || !_this.handshook || !_this.reqQueue || !_this.targetStream)
        {
            console.error(_this.debugger_attached, _this.handshook, _this.reqQueue);
            throw new Error('invalid state for sendRequest');
        }

        for (i = 0; i < msg.length; i++) {
            try {
                dval = formatDebugValue(msg[i]);
            } catch (e) {
                console.error('Failed to format dvalue, dropping connection: ' + e);
                console.error(e.stack || e);
                _this.targetStream.destroy();
                throw new Error('failed to format dvalue');
            }
            dvals.push(dval);
        }

        data = Buffer.concat(dvals);

        //console.log('Writing: ' + data.toString('hex'));

        _this.targetStream.write(data);

        if (_this.outputPassThroughStream) { _this.outputPassThroughStream.write(data); } // stats and dumping
        if (optLogMessages) {
            console.info1('Request ' + prettyDebugCommand(msg[1]) + ': ' + prettyDebugMessage(msg));
        }

        if (!_this.reqQueue) {
            throw new Error('no reqQueue');
        }
        _this.reqQueue.push({
            reqMsg: msg,
            reqCmd: msg[1],
            resolveCb: resolve,
            rejectCb: reject
        });
    });
};

Debugger.prototype.sendBasicInfoRequest = function () {
    var _this = this;
    return this.sendRequest([DVAL_REQ, CMD_BASICINFO, DVAL_EOM]).then(function (msg) {
        _this.dukVersion = msg[1];
        _this.dukGitDescribe = msg[2];
        _this.targetInfo = msg[3];
        _this.endianness = { 1: 'little', 2: 'mixed', 3: 'big' }[msg[4]] || 'unknown';
        _this.emit('basic-info-update');
        return msg;
    });
};

Debugger.prototype.sendGetVarRequest = function (varname, level) {
    var _this = this;
    return this.sendRequest([DVAL_REQ, CMD_GETVAR, (typeof level === 'number' ? level : -1), varname, DVAL_EOM]).then(function (msg) {
        return { found: msg[1] === 1, value: msg[2] };
    });
};

Debugger.prototype.sendPutVarRequest = function (varname, varvalue, level) {
    var _this = this;
    return this.sendRequest([DVAL_REQ, CMD_PUTVAR, (typeof level === 'number' ? level : -1), varname, varvalue, DVAL_EOM]);
};

Debugger.prototype.sendInvalidCommandTestRequest = function () {
    // Intentional invalid command
    var _this = this;
    return this.sendRequest([DVAL_REQ, 0xdeadbeef, DVAL_EOM]);
};

Debugger.prototype.sendStatusRequest = function () {
    // Send a status request to trigger a status notify, result is ignored:
    // target sends a status notify instead of a meaningful reply
    //console.log('this', this.sendRequest);
    return (this.sendRequest([DVAL_REQ, CMD_TRIGGERSTATUS, DVAL_EOM]));
};

Debugger.prototype.sendBreakpointListRequest = function () {
    var _this = this;
    return this.sendRequest([DVAL_REQ, CMD_LISTBREAK, DVAL_EOM]).then(function (msg) {
        var i, n;
        var breakpts = [];

        for (i = 1, n = msg.length - 1; i < n; i += 2) {
            breakpts.push({ fileName: msg[i], lineNumber: msg[i + 1] });
        }

        _this.breakpoints = breakpts;
        _this.emit('breakpoints-update', breakpts);
        return msg;
    });
};

Debugger.prototype.sendGetLocalsRequest = function (level) {
    var _this = this;
    return this.sendRequest([DVAL_REQ, CMD_GETLOCALS, (typeof level === 'number' ? level : -1), DVAL_EOM]).then(function (msg) {
        var i;
        var locals = [];

        for (i = 1; i <= msg.length - 2; i += 2)
        {
            // XXX: do pretty printing in debug client for now
            locals.push({ key: msg[i], value: prettyUiDebugValue(msg[i + 1], LOCALS_CLIPLEN), type: (msg[i+1]?msg[i+1].type:'undefined') });
        }

        _this.locals = locals;
        _this.emit('locals-update');
        return locals;
    });
};

Debugger.prototype.sendGetCallStackRequest = function () {
    var _this = this;
    return this.sendRequest([DVAL_REQ, CMD_GETCALLSTACK, DVAL_EOM]).then(function (msg) {
        var i;
        var stack = [];

        for (i = 1; i + 3 <= msg.length - 1; i += 4)
        {
            stack.push({
                fileName: msg[i],
                funcName: msg[i + 1],
                lineNumber: msg[i + 2],
                pc: msg[i + 3]
            });
        }

        _this.callstack = stack;
        _this.emit('callstack-update');
        return msg;
    });
};

Debugger.prototype.sendStepIntoRequest = function () {
    var _this = this;
    return this.sendRequest([DVAL_REQ, CMD_STEPINTO, DVAL_EOM]);
};

Debugger.prototype.sendStepOverRequest = function () {
    var _this = this;
    return this.sendRequest([DVAL_REQ, CMD_STEPOVER, DVAL_EOM]);
};

Debugger.prototype.sendStepOutRequest = function () {
    var _this = this;
    return this.sendRequest([DVAL_REQ, CMD_STEPOUT, DVAL_EOM]);
};

Debugger.prototype.sendPauseRequest = function () {
    var _this = this;
    return this.sendRequest([DVAL_REQ, CMD_PAUSE, DVAL_EOM]);
};

Debugger.prototype.sendResumeRequest = function () {
    var _this = this;
    return this.sendRequest([DVAL_REQ, CMD_RESUME, DVAL_EOM]);
};

Debugger.prototype.sendEvalRequest = function (evalInput, level) {
    var _this = this;
    // Use explicit level if given.  If no level is given, use null if the call
    // stack is empty, -1 otherwise.  This works well when we're paused and the
    // callstack information is not liable to change before we do an Eval.
    console.info1('levelType = ' + typeof (level));
    if (typeof level !== 'number') {
        level = this.callstack && this.callstack.length > 0 ? -1 : null;
    }
    console.info1('level=' + level);
    return this.sendRequest([DVAL_REQ, CMD_EVAL, level, evalInput, DVAL_EOM]).then(function (msg) {
        return { error: msg[1] === 1 /*error*/, value: msg[2] };
    });
};

Debugger.prototype.sendDetachRequest = function () {
    var _this = this;
    return this.sendRequest([DVAL_REQ, CMD_DETACH, DVAL_EOM]);
};

Debugger.prototype.sendDumpHeapRequest = function () {
    var _this = this;

    return this.sendRequest([DVAL_REQ, CMD_DUMPHEAP, DVAL_EOM]).then(function (msg) {
        var res = {};
        var objs = [];
        var i, j, n, m, o, prop;

        res.type = 'heapDump';
        res.heapObjects = objs;

        for (i = 1, n = msg.length - 1; i < n; /*nop*/) {
            o = {};
            o.ptr = msg[i++];
            o.type = msg[i++];
            o.flags = msg[i++] >>> 0;  /* unsigned */
            o.refc = msg[i++];

            if (o.type === DUK_HTYPE_STRING) {
                o.blen = msg[i++];
                o.clen = msg[i++];
                o.hash = msg[i++] >>> 0;  /* unsigned */
                o.data = msg[i++];
            } else if (o.type === DUK_HTYPE_BUFFER) {
                o.len = msg[i++];
                o.data = msg[i++];
            } else if (o.type === DUK_HTYPE_OBJECT) {
                o['class'] = msg[i++];
                o.proto = msg[i++];
                o.esize = msg[i++];
                o.enext = msg[i++];
                o.asize = msg[i++];
                o.hsize = msg[i++];
                o.props = [];
                for (j = 0, m = o.enext; j < m; j++) {
                    prop = {};
                    prop.flags = msg[i++];
                    prop.key = msg[i++];
                    prop.accessor = (msg[i++] == 1);
                    if (prop.accessor) {
                        prop.getter = msg[i++];
                        prop.setter = msg[i++];
                    } else {
                        prop.value = msg[i++];
                    }
                    o.props.push(prop);
                }
                o.array = [];
                for (j = 0, m = o.asize; j < m; j++) {
                    prop = {};
                    prop.value = msg[i++];
                    o.array.push(prop);
                }
            } else {
                console.error('invalid htype: ' + o.type + ', disconnect');
                _this.disconnectDebugger();
                throw new Error('invalid htype');
                return;
            }

            objs.push(o);
        }

        return res;
    });
};

Debugger.prototype.sendGetBytecodeRequest = function () {
    var _this = this;

    return this.sendRequest([DVAL_REQ, CMD_GETBYTECODE, -1 /* level; could be other than -1 too */, DVAL_EOM]).then(function (msg) {
        var idx = 1;
        var nconst;
        var nfunc;
        var val;
        var buf;
        var i, n;
        var consts = [];
        var funcs = [];
        var bcode;
        var preformatted;
        var ret;
        var idxPreformattedInstructions;

        //console.log(JSON.stringify(msg));

        nconst = msg[idx++];
        for (i = 0; i < nconst; i++) {
            val = msg[idx++];
            consts.push(val);
        }

        nfunc = msg[idx++];
        for (i = 0; i < nfunc; i++) {
            val = msg[idx++];
            funcs.push(val);
        }
        val = msg[idx++];

        // Right now bytecode is a string containing a direct dump of the
        // bytecode in target endianness.  Decode here so that the web UI
        // doesn't need to.

        buf = Buffer.alloc(val.length);
        writeDebugStringToBuffer(val, buf, 0);
        bcode = _this.decodeBytecodeFromBuffer(buf, consts, funcs);

        preformatted = [];
        consts.forEach(function (v, i) {
            preformatted.push('; c' + i + ' ' + JSON.stringify(v));
        });
        preformatted.push('');
        idxPreformattedInstructions = preformatted.length;
        bcode.forEach(function (v) {
            preformatted.push(v.str);
        });
        preformatted = preformatted.join('\n') + '\n';

        ret = {
            constants: consts,
            functions: funcs,
            bytecode: bcode,
            preformatted: preformatted,
            idxPreformattedInstructions: idxPreformattedInstructions
        };

        return ret;
    });
};

Debugger.prototype.changeBreakpoint = function (fileName, lineNumber, mode) {
    var _this = this;

    return this.sendRequest([DVAL_REQ, CMD_LISTBREAK, DVAL_EOM]).then(function (msg) {
        var i, n;
        var breakpts = [];
        var deleted = false;

        // Up-to-date list of breakpoints on target
        for (i = 1, n = msg.length - 1; i < n; i += 2) {
            breakpts.push({ fileName: msg[i], lineNumber: msg[i + 1] });
        }

        // Delete matching breakpoints in reverse order so that indices
        // remain valid.  We do this for all operations so that duplicates
        // are eliminated if present.
        for (i = breakpts.length - 1; i >= 0; i--) {
            var bp = breakpts[i];
            if (mode === 'deleteall' || (bp.fileName === fileName && bp.lineNumber === lineNumber))
            {
                deleted = true;
                _this.sendRequest([DVAL_REQ, CMD_DELBREAK, i, DVAL_EOM], function (msg)
                {
                    // nop
                }, function (err) {
                    // nop
                });
            }
        }

        // Technically we should wait for each delbreak reply but because
        // target processes the requests in order, it doesn't matter.
        if ((mode === 'add') || (mode === 'toggle' && !deleted)) {
            _this.sendRequest([DVAL_REQ, CMD_ADDBREAK, fileName, lineNumber, DVAL_EOM], function (msg)
            {
                // nop
            }, function (err) {
                _this.uiMessage('debugger-info', 'Failed to add breakpoint: ' + err);
            });
        }

        // Read final, effective breakpoints from the target
        _this.sendBreakpointListRequest();
        hostCooperate();
    });
};

Debugger.prototype.disconnectDebugger = function ()
{
    console.info1('Debugger.disconnectDebugger();');
    if (this.targetStream)
    {
        // We require a destroy() method from the actual target stream
        this.targetStream.end();
        this.targetStream = null;
    }
    if (this.inputParser) {
        this.inputParser.close();
        this.inputParser = null;
    }
    if (this.outputPassThroughStream) {
        // There is no close() or destroy() for a passthrough stream, so just
        // close the outputParser which will cancel timers etc.
    }
    if (this.outputParser) {
        this.outputParser.close();
        this.outputParser = null;
    }
    console.info1('disconnecting');
    this.debugger_attached = false;
    this.handshook = false;
    this.reqQueue = null;
    this.execStatus = {
        attached: false,
        state: 'detached',
        fileName: '',
        funcName: '',
        line: 0,
        pc: 0
    };
};

Debugger.prototype.connectDebugger = function connectDebugger()
{
    var _this = this;
    this.disconnectDebugger();  // close previous target connection

    // CUSTOMTRANSPORT: to use a custom transport, change this.targetStream to
    // use your custom transport.

    console.info1('Connecting to ' + optTargetHost + ':' + optTargetPort + '...');
    this.targetStream = require('net').createConnection(optTargetPort, optTargetHost);
    this.targetStream.Debugger = this;

    this.targetStream.on('connect', function OnDebugConnect()
    {
        console.info1('Debugger Transport Connection Attached...');
        hostCooperate();
    });
    //this.targetStream.on('data', function (msg) { console.log('recv');});

    try
    {
        this.inputParser = new DebugProtocolParser(
            this.targetStream,
            null,
            optDumpDebugRead,
            optDumpDebugPretty,
            optDumpDebugPretty ? 'Recv: ' : null,
            null,
            null   // console logging is done at a higher level to match request/response
        );
    }
    catch(ee)
    {
        console.error(ee);
    }

    //// Use a PassThrough stream to debug dump and get stats for output messages.
    //// Simply write outgoing data to both the targetStream and this passthrough
    //// separately.
    //this.outputPassThroughStream = stream.PassThrough();
    //this.outputParser = new DebugProtocolParser(
    //    this.outputPassThroughStream,
    //    1,
    //    optDumpDebugWrite,
    //    optDumpDebugPretty,
    //    optDumpDebugPretty ? 'Send: ' : null,
    //    null,
    //    null   // console logging is done at a higher level to match request/response
    //);

    this.inputParser.on('transport-close', function ()
    {
        console.info1('TRANSPORT-CLOSE');
        _this.uiMessage('debugger-info', 'Debug transport closed');
        _this.disconnectDebugger();
        _this.emit('exec-status-update');
        _this.emit('detached');
    });

    this.inputParser.on('transport-error', function (err)
    {
        console.info1('TRANSPORT-ERROR', err);
        _this.uiMessage('debugger-info', 'Debug transport error: ' + err);
        _this.disconnectDebugger();
    });

    this.inputParser.on('protocol-version', function (msg) {
        var ver = msg.protocolVersion;
        console.info1('Debug version identification:', msg.versionIdentification);
        _this.protocolVersion = ver;
        _this.uiMessage('debugger-info', 'Debug version identification: ' + msg.versionIdentification);
        if (ver !== SUPPORTED_DEBUG_PROTOCOL_VERSION) {
            console.error('Unsupported debug protocol version (got ' + ver + ', support ' + SUPPORTED_DEBUG_PROTOCOL_VERSION + ')');
            _this.uiMessage('debugger-info', 'Protocol version ' + ver + ' unsupported, dropping connection');
            _this.targetStream.destroy();
        } else {
            _this.uiMessage('debugger-info', 'Debug protocol version: ' + ver);
            _this.handshook = true;
            _this.execStatus = {
                attached: true,
                state: 'attached',
                fileName: '',
                funcName: '',
                line: 0,
                pc: 0
            };
            _this.debugger_attached = true;
            _this.emit('exec-status-update');
            _this.emit('attached');  // inform web UI

            // Fetch basic info right away
            _this.sendBasicInfoRequest();
        }
    });

    this.inputParser.on('debug-message', function (msg)
    {
        _this.processDebugMessage(msg);
    });

    this.inputParser.on('stats-update', function () {
        _this.stats.rxBytes = this.bytes;
        _this.stats.rxDvalues = this.dvalues;
        _this.stats.rxMessages = this.messages;
        _this.stats.rxBytesPerSec = this.bytesPerSec;
        _this.emit('debug-stats-update');
    });

    if (this.outputParser)
    {
        this.outputParser.on('stats-update', function () {
            _this.stats.txBytes = this.bytes;
            _this.stats.txDvalues = this.dvalues;
            _this.stats.txMessages = this.messages;
            _this.stats.txBytesPerSec = this.bytesPerSec;
            _this.emit('debug-stats-update');
        });
    }
};

Debugger.prototype.processDebugMessage = function (msg) {
    var req;
    var prevState, newState;
    var err;

    if (msg[0] === DVAL_REQ) {
        // No actual requests sent by the target right now (just notifys).
        console.error('Unsolicited reply message, dropping connection: ' + prettyDebugMessage(msg));
    } else if (msg[0] === DVAL_REP) {
        if (this.reqQueue.length <= 0) {
            console.error('Unsolicited reply message, dropping connection: [' + this.reqQueue.length + '] ' + prettyDebugMessage(msg));
            this.targetStream.destroy();
        }
        req = this.reqQueue.shift();

        if (optLogMessages) {
            console.info1('Reply for ' + prettyDebugCommand(req.reqCmd) + ': ' + prettyDebugMessage(msg));
        }

        if (req.resolveCb) {
            req.resolveCb(msg);
        } else {
            // nop: no callback
        }
    } else if (msg[0] === DVAL_ERR) {
        if (this.reqQueue.length <= 0) {
            console.error('Unsolicited error message, dropping connection: ' + prettyDebugMessage(msg));
            this.targetStream.destroy();
        }
        err = new Error(String(msg[2]) + ' (code ' + String(msg[1]) + ')');
        err.errorCode = msg[1] || 0;
        req = this.reqQueue.shift();

        if (optLogMessages) {
            console.info1('Error for ' + prettyDebugCommand(req.reqCmd) + ': ' + prettyDebugMessage(msg));
        }

        console.info1(req.rejectCb, err);

        if (req.rejectCb)
        {
            req.rejectCb(err);
        } else
        {
            // nop: no callback
        }
    } else if (msg[0] === DVAL_NFY) {
        if (optLogMessages) {
            console.info1('Notify ' + prettyDebugCommand(msg[1]) + ': ' + prettyDebugMessage(msg));
        }

        if (msg[1] === CMD_STATUS)
        {
            prevState = this.execStatus.state;
            newState = msg[2] === 0 ? 'running' : 'paused';
            this.execStatus = {
                attached: true,
                state: newState,
                fileName: (typeof(msg[3])=='string'?msg[3]:''),
                funcName: (typeof (msg[4]) == 'string' ? msg[4] : ''),
                line: msg[5],
                pc: msg[6]
            };
            console.info1('MSG=>', msg);
            for (var ii in msg)
            {
                console.info1('[' + ii + ']', msg[ii]);
            }
            if (prevState !== newState && newState === 'paused') {
                // update run state now that we're paused
                if(typeof(msg[3])=='string')
                {
                    this.sendBreakpointListRequest();
                    this.sendGetLocalsRequest();
                    this.sendGetCallStackRequest();
                }
            }

            this.emit('exec-status-update');
        } else if (msg[1] === CMD_THROW)
        {
            console.info1(msg);
            this.emit('throw', { fatal: msg[2], message: (msg[2] ? 'UNCAUGHT: ' : 'THROW: ') + prettyUiStringUnquoted(msg[3], UI_MESSAGE_CLIPLEN), fileName: msg[4], lineNumber: msg[5] });
            this.uiMessage({ type: 'throw', fatal: msg[2], message: (msg[2] ? 'UNCAUGHT: ' : 'THROW: ') + prettyUiStringUnquoted(msg[3], UI_MESSAGE_CLIPLEN), fileName: msg[4], lineNumber: msg[5] });
        } else if (msg[1] === CMD_DETACHING) {
            this.uiMessage({ type: 'detaching', reason: msg[2], message: 'DETACH: ' + (msg.length >= 5 ? prettyUiStringUnquoted(msg[3]) : 'detaching') });
        } else if(msg[1] === CMD_APPNOTIFY)
        {
            switch(msg[2])
            {
                case 'MemoryAllocations':
                    console.info1('MemoryAllocation: ' + msg[3]);
                    this.emit('memory-allocation', { total: parseInt(msg[3]) });
                    break;
                default:
                    console.info1('Unknown AppNotify: ' + msg[2]);
                    break;
            }
        }
        else
        {
            // Ignore unknown notify messages
            console.info1('Unknown notify, ignoring: ' + prettyDebugMessage(msg));
        }
    } else {
        console.error('Invalid initial dvalue, dropping connection: ' + prettyDebugMessage(msg));
        this.targetStream.destroy();
    }
};

Debugger.prototype.run = function ()
{
    var _this = this;
    optTargetPort = transport; 

    this.WebServer = require('http').createServer();
    this.WebServer.Debugger = this;
    this.WebServer.listen({ port: webport });
    updateWebPort(this.WebServer.address().port);
    this.WebServer.on('request', function OnDebugWebsiteRequest(imsg, rsp)
    {
        console.info1('Received inbound request: ' + imsg.method + ' ' + imsg.url);
        if (imsg.url == '/')
        {
            rsp.statusCode = 200;
            rsp.statusText = 'OK';
            rsp.setHeader('Content-Type', 'text/html');

            if (require('fs').existsSync('debugger.html'))
            {
                rsp.fs = require('fs').createReadStream('debugger.html');
                rsp.fs.pipe(rsp);
            }
            else
            {
                rsp.end(Buffer.from(dbgHTML, 'base64'));
            }
        }
        else
        {
            rsp.statusCode = 404;
            rsp.end();
        }
    });
    this.WebServer.on('upgrade', function OnDebugWebSocket(req, soc, head)
    {
        console.info1('WebSocket request on: ', req.url);
        this.Debugger.websocket = soc.upgradeWebSocket();
        this.Debugger.websocket.Debugger = this.Debugger;
        this.Debugger.websocket.on('end', function ()
        {
            this.Debugger.disconnectDebugger();
            console.info1('web socket closed');
        });
        this.Debugger.websocket.on('data', function OnDebugCmd(buf)
        {
            var j = null;

            try
            {
                j = JSON.parse(buf);
            }
            catch(e)
            {
                console.error('Parse Error: ' + e);
                return;
            }

            switch(j.cmd)
            {
                case 'ATTACH':
                    console.info1('Attaching Debugger');
                    this.Debugger.connectDebugger();
                    break;
                case 'GC':
                    _debugGC(); // Debugger Heap
                    hostGC();   // Host Heap
                    console.info1('Force GC');
                    break;
                case 'STEPOVER':
                    this.Debugger.sendStepOverRequest();
                    break;
                case 'STEPINTO':
                    this.Debugger.sendStepIntoRequest();
                    break;
                case 'STEPOUT':
                    this.Debugger.sendStepOutRequest();
                    break;
                case 'CALLSTACK':
                    this.Debugger.sendGetCallStackRequest();
                    break;
                case 'HEAP':
                    var p = this.Debugger.sendDumpHeapRequest();
                    p.Debugger = this.Debugger;
                    p.then(function onSendDumpHeap(obj)
                    {
                        console.info1('\n\n\n\n');
                        var heapObjects = obj.heapObjects;
                        for(var i in heapObjects)
                        {
                            console.info1(heapObjects[i].ptr, heapObjects[i].type);
                        }
                        //console.info1('this', this);
                        //process.stdout.write('THEN\n');
                        //process.stdout.write(JSON.stringify(this) + '\n');
                        //process.stdout.write(JSON.stringify(obj) + '\n');
                    }, function OnError(err) { console.info1('ERROR=>', err); });
                    break;
                case 'PAUSE':
                    this.Debugger.sendPauseRequest();
                    break;
                case 'RESUME':
                    this.Debugger.sendResumeRequest();
                    break;
                case 'DETACH':
                    console.info1('Detach Debugger');
                    this.Debugger.disconnectDebugger();
                    break;
                case 'BREAKPOINT':
                    console.info1(j);
                    if (j.file == '')
                    {
                        if ((j.file = this.Debugger.execStatus.fileName) == '')
                        {
                            j.file = _scriptPath;
                        }                        
                        console.info1('using filePath: ' + j.file);
                    }

                    console.info1('ChangeBreakpoint => ' + j.file + ':' + j.line + ' (' + j.mode + ')');
                    this.Debugger.changeBreakpoint(j.file, j.line, j.mode);
                    break;
                case 'QUERY':
                    console.info1(j);
                    var p = this.Debugger.sendEvalRequest('JSON.stringify(' + j.var + ')', j.level);
                    p.var = j.var;
                    p.then(function (msg)
                    {
                        _this.websocket.write({ cmd: 'QUERY', var: this.promise.var, val: msg.value });
                    });
                    break;
                case 'EVAL':
                    console.info1(j);
                    var p = this.Debugger.sendEvalRequest(j.eval, j.level);
                    p.eval = j.eval;
                    p.then(function (msg)
                    {
                        _this.websocket.write({ cmd: 'EVAL', eval: this.promise.eval, val: msg.value });
                    });
                    break;
                    break;
                case 'LOCALS':
                    console.info1(j);
                    this.Debugger.sendGetLocalsRequest(j.level).then(function (locals)
                    {
                        var vals = [];
                        for (var v in locals)
                        {
                            console.info1('Pushing: [' + locals[v].key + '] = ' + locals[v].type);
                            vals.push({ key: locals[v].key, type: locals[v].type });
                        }
                        _this.websocket.write({ cmd: 'LOCALS', val: vals });
                    });

                    
                    break
                case 'SOURCE':
                    console.info1('SOURCE', j);
                    if (j.name != '')
                    {
                        if (j.name.toLowerCase().endsWith('.js') || j.name == '<<NATIVE>>')
                        {
                            console.info1(JSON.stringify({ cmd: 'SOURCE', name: j.name, source: _scriptTokens.join('\n') }));
                            _this.websocket.write({ cmd: 'SOURCE', name: j.name, source: _scriptTokens.join('\n') });
                        }
                        else
                        {
                            var p = _this.sendEvalRequest("getJSModule('" + j.name + "');");
                            p.websocket = _this.websocket;
                            p.name = j.name;

                            p.then(function (result) {
                                this.promise.websocket.write({ cmd: 'SOURCE', name: this.promise.name, source: result.value });
                            });
                        }
                    }
                    break;
            }

            hostCooperate();          
        });

        this.Debugger.on('memory-allocation', function (mem)
        {
            var cmd = { cmd: 'MEMORY', total: mem.total };
            this.websocket.write(cmd);
        });
        this.Debugger.on('detached', function ()
        {
            detachCleanup();
        });

        this.Debugger.on('debug-stats-update', function ()
        {
           // _this.debugStatsLimiter.trigger();
        });

        this.Debugger.on('ui-message-update', function ()
        {
            // Explicit rate limiter because this is a source of a lot of traffic.
            //_this.uiMessageLimiter.trigger();
        });

        this.Debugger.on('basic-info-update', function ()
        {
           // _this.emitBasicInfo();
        });

        this.Debugger.on('breakpoints-update', function (bpts)
        {
            console.info1('breakpoints updated');
            var cmd = { cmd: 'BREAKPOINT', list: bpts };
            this.websocket.write(cmd);
            //_this.emitBreakpoints();
        });

        this.Debugger.on('exec-status-update', function ()
        {
            if (!this.previousState || this.previousState != this.execStatus.state)
            {
                this.previousState = this.execStatus.state;
                console.info1('new state', this.execStatus.state);
                if (this.execStatus.state == 'attached') { console.info1('forcing cooperate'); hostCooperate(); }
                if (this.execStatus.attached)
                {
                    this.websocket.pausedNative = false;
                    switch(this.execStatus.state)
                    {
                        case 'paused':
                            console.info1('PAUSED', this.execStatus);
                            var src = _scriptPath == this.execStatus.fileName ? _scriptTokens[this.execStatus.line - 1] : null;
                            if (src == null)
                            {
                                if (this.execStatus.fileName != '')
                                {
                                    var p = this.sendEvalRequest("getJSModule('" + this.execStatus.fileName + "');");
                                    p.websocket = this.websocket;
                                    p.execStatus = this.execStatus;

                                    p.then(function (result) {
                                        var tk = result.value.split('\n');
                                        this.promise.websocket.write({ cmd: 'PAUSE', native: false, file: this.promise.execStatus.fileName, line: this.promise.execStatus.line, source: tk[this.promise.execStatus.line - 1] });
                                    });
                                }
                                else
                                {
                                    this.websocket.pausedNative = true;
                                    this.websocket.write({ cmd: 'PAUSE', native: true, file: this.execStatus.fileName, line: this.execStatus.line, source: '(Native)' });
                                }
                            }
                            else
                            {
                                this.websocket.write({ cmd: 'PAUSE', native: false, file: this.execStatus.fileName, line: this.execStatus.line, source: src });
                            }
                            break;
                        case 'running':
                            this.websocket.write({ cmd: 'RUNNING', file: _scriptPath });
                            break;
                    }
                }
                else if (this.execStatus.state == 'detached')
                {
                    this.websocket.write({ cmd: 'DETACH'});
                }
            }
            // Explicit rate limiter because this is a source of a lot of traffic.
            //_this.execStatusLimiter.trigger();
        });
        this.Debugger.on('throw', function OnThrown(err)
        {
            var source = err.fileName == _scriptPath ? (_scriptTokens[err.lineNumber - 1]) : null;
            if (source == null) {
                var p = this.sendEvalRequest("getJSModule('" + err.fileName + "');");
                p.ws = this.websocket;
                p.err = err;

                p.then(function (result)
                {
                    var tk = result.value.split('\n');
                    this.promise.ws.write({ cmd: 'THROW', line: this.promise.err.lineNumber, msg: this.promise.err.message, file: this.promise.err.fileName, source: tk[this.promise.err.lineNumber - 1] });
                });
            }
            else
            {
                this.websocket.write({ cmd: 'THROW', line: err.lineNumber, msg: err.message, file: err.fileName, source: source });
            }
        });

        this.Debugger.on('locals-update', function ()
        {
            //_this.emitLocals();
        });

        this.Debugger.on('callstack-update', function ()
        {
            this.websocket.write({ cmd: 'CALLSTACK', callstack: this.callstack });
        });



    });






    //// Initial debugger connection

    //this.connectDebugger();

    //// Poll various state items when running

    //var sendRound = 0;
    //var statusPending = false;
    //var bplistPending = false;
    //var localsPending = false;
    //var callStackPending = false;

    //setInterval(function () {
    //    if (_this.execStatus.state !== 'running') {
    //        return;
    //    }

    //    // Could also check for an empty request queue, but that's probably
    //    // too strict?

    //    // Pending flags are used to avoid requesting the same thing twice
    //    // while a previous request is pending.  The flag-based approach is
    //    // quite awkward.  Rework to use promises.

    //    switch (sendRound) {
    //        case 0:
    //            if (!statusPending) {
    //                statusPending = true;
    //                _this.sendStatusRequest().finally(function () { statusPending = false; });
    //            }
    //            break;
    //        case 1:
    //            if (!bplistPending) {
    //                bplistPending = true;
    //                _this.sendBreakpointListRequest().finally(function () { bplistPending = false; });
    //            }
    //            break;
    //        case 2:
    //            if (!localsPending) {
    //                localsPending = true;
    //                _this.sendGetLocalsRequest().finally(function () { localsPending = false; });
    //            }
    //            break;
    //        case 3:
    //            if (!callStackPending) {
    //                callStackPending = true;
    //                _this.sendGetCallStackRequest().finally(function () { callStackPending = false; });
    //            }
    //            break;
    //    }
    //    sendRound = (sendRound + 1) % 4;
    //}, 500);
};





/*
 *  Miscellaneous helpers
 */

var nybbles = '0123456789abcdef';

/* Convert a buffer into a string using Unicode codepoints U+0000...U+00FF.
 * This is the NodeJS 'binary' encoding, but since it's being deprecated,
 * reimplement it here.  We need to avoid parsing strings as e.g. UTF-8:
 * although Duktape strings are usually UTF-8/CESU-8 that's not always the
 * case, e.g. for internal strings.  Buffer values are also represented as
 * strings in the debug protocol, so we must deal accurately with arbitrary
 * byte arrays.
 */
function bufferToDebugString(buf) {
    var cp = [];
    var i, n;

    /*
        // This fails with "RangeError: Maximum call stack size exceeded" for some
        // reason, so use a much slower variant.
    
        for (i = 0, n = buf.length; i < n; i++) {
            cp[i] = buf[i];
        }
    
        return String.fromCharCode.apply(String, cp);
    */

    for (i = 0, n = buf.length; i < n; i++) {
        cp[i] = String.fromCharCode(buf[i]);
    }

    return cp.join('');
}

/* Write a string into a buffer interpreting codepoints U+0000...U+00FF
 * as bytes.  Drop higher bits.
 */
function writeDebugStringToBuffer(str, buf, off) {
    var i, n;

    for (i = 0, n = str.length; i < n; i++) {
        buf[off + i] = str.charCodeAt(i) & 0xff;  // truncate higher bits
    }
}

/* Encode an ordinary Unicode string into a dvalue compatible format, i.e.
 * into a byte array represented as codepoints U+0000...U+00FF.  Concretely,
 * encode with UTF-8 and then represent the bytes with U+0000...U+00FF.
 */
function stringToDebugString(str) {
    return utf8.encode(str);
}

/* Pretty print a dvalue.  Useful for dumping etc. */
function prettyDebugValue(x) {
    if (typeof x === 'object' && x !== null) {
        if (x.type === 'eom') {
            return 'EOM';
        } else if (x.type === 'req') {
            return 'REQ';
        } else if (x.type === 'rep') {
            return 'REP';
        } else if (x.type === 'err') {
            return 'ERR';
        } else if (x.type === 'nfy') {
            return 'NFY';
        }
    }
    return JSON.stringify(x);
}

/* Pretty print a number for UI usage.  Types and values should be easy to
 * read and typing should be obvious.  For numbers, support Infinity, NaN,
 * and signed zeroes properly.
 */
function prettyUiNumber(x) {
    if (x === 1 / 0) { return 'Infinity'; }
    if (x === -1 / 0) { return '-Infinity'; }
    if (isNaN(x)) { return 'NaN'; }
    if (x === 0 && 1 / x > 0) { return '0'; }
    if (x === 0 && 1 / x < 0) { return '-0'; }
    return x.toString();
}

/* Pretty print a dvalue string (bytes represented as U+0000...U+00FF)
 * for UI usage.  Try UTF-8 decoding to get a nice Unicode string (JSON
 * encoded) but if that fails, ensure that bytes are encoded transparently.
 * The result is a quoted string with a special quote marker for a "raw"
 * string when UTF-8 decoding fails.  Very long strings are optionally
 * clipped.
 */
function prettyUiString(x, cliplen) {
    var ret;

    if (typeof x !== 'string') {
        throw new Error('invalid input to prettyUiString: ' + typeof x);
    }
    try {
        // Here utf8.decode() is better than decoding using NodeJS buffer
        // operations because we want strict UTF-8 interpretation.
        ret = JSON.stringify(utf8.decode(x));
    } catch (e) {
        // When we fall back to representing bytes, indicate that the string
        // is "raw" with a 'r"' prefix (a somewhat arbitrary convention).
        // U+0022 = ", U+0027 = '
        ret = 'r"' + x.replace(/[\u0022\u0027\u0000-\u001f\u0080-\uffff]/g, function (match) {
            var cp = match.charCodeAt(0);
            return '\\x' + nybbles[(cp >> 4) & 0x0f] + nybbles[cp & 0x0f];
        }) + '"';
    }

    if (cliplen && ret.length > cliplen) {
        ret = ret.substring(0, cliplen) + '...';  // trailing '"' intentionally missing
    }
    return ret;
}

/* Pretty print a dvalue string (bytes represented as U+0000...U+00FF)
 * for UI usage without quotes.
 */
function prettyUiStringUnquoted(x, cliplen) {
    var ret;

    if (typeof x !== 'string') {
        throw new Error('invalid input to prettyUiStringUnquoted: ' + typeof x);
    }

    try {
        // Here utf8.decode() is better than decoding using NodeJS buffer
        // operations because we want strict UTF-8 interpretation.

        // XXX: unprintable characters etc?  In some UI cases we'd want to
        // e.g. escape newlines and in others not.
        ret = utf8.decode(x);
    } catch (e) {
        // For the unquoted version we don't need to escape single or double
        // quotes.
        ret = x.replace(/[\u0000-\u001f\u0080-\uffff]/g, function (match) {
            var cp = match.charCodeAt(0);
            return '\\x' + nybbles[(cp >> 4) & 0x0f] + nybbles[cp & 0x0f];
        });
    }

    if (cliplen && ret.length > cliplen) {
        ret = ret.substring(0, cliplen) + '...';
    }
    return ret;
}

/* Pretty print a dvalue for UI usage.  Everything comes out as a ready-to-use
 * string.
 *
 * XXX: Currently the debug client formats all values for UI use.  A better
 * solution would be to pass values in typed form and let the UI format them,
 * so that styling etc. could take typing into account.
 */
function prettyUiDebugValue(x, cliplen) {
    if (typeof x === 'object' && x !== null) {
        // Note: typeof null === 'object', so null special case explicitly
        if (x.type === 'eom') {
            return 'EOM';
        } else if (x.type === 'req') {
            return 'REQ';
        } else if (x.type === 'rep') {
            return 'REP';
        } else if (x.type === 'err') {
            return 'ERR';
        } else if (x.type === 'nfy') {
            return 'NFY';
        } else if (x.type === 'unused') {
            return 'unused';
        } else if (x.type === 'undefined') {
            return 'undefined';
        } else if (x.type === 'buffer') {
            return '|' + x.data + '|';
        } else if (x.type === 'object') {
            console.info1('OBJECT => ', JSON.stringify(x));
           // return '[object ' + (dukClassNames[x.class] || ('class ' + x.class)) + ' ' + x.pointer + ']';
            return '[object ' + ('class ' + x.class) + ' ' + x.pointer + ']';
        } else if (x.type === 'pointer') {
            return '<pointer ' + x.pointer + '>';
        } else if (x.type === 'lightfunc') {
            return '<lightfunc 0x' + x.flags.toString(16) + ' ' + x.pointer + '>';
        } else if (x.type === 'number') {
            // duk_tval number, any IEEE double
            var tmp = new Buffer(x.data, 'hex');  // decode into hex
            var val = tmp.readDoubleBE(0);        // big endian ieee double
            return prettyUiNumber(val);
        }
    } else if (x === null) {
        return 'null';
    } else if (typeof x === 'boolean') {
        return x ? 'true' : 'false';
    } else if (typeof x === 'string') {
        return prettyUiString(x, cliplen);
    } else if (typeof x === 'number') {
        // Debug protocol integer
        return prettyUiNumber(x);
    }

    // We shouldn't come here, but if we do, JSON is a reasonable default.
    return JSON.stringify(x);
}

/* Pretty print a debugger message given as an array of parsed dvalues.
 * Result should be a pure ASCII one-liner.
 */
function prettyDebugMessage(msg) {
    return msg.map(prettyDebugValue).join(' ');
}

/* Pretty print a debugger command. */
function prettyDebugCommand(cmd)
{
    return debugCommandNames[cmd] || String(cmd);
}

/* Decode and normalize source file contents: UTF-8, tabs to 8,
 * CR LF to LF.
 */
function decodeAndNormalizeSource(data) {
    var tmp;
    var lines, line, repl;
    var i, n;
    var j, m;

    try {
        tmp = data.toString('utf8');
    } catch (e) {
        console.error('Failed to UTF-8 decode source file, ignoring: ' + e);
        tmp = String(data);
    }

    lines = tmp.split(/\r?\n/);
    for (i = 0, n = lines.length; i < n; i++) {
        line = lines[i];
        if (/\t/.test(line)) {
            repl = '';
            for (j = 0, m = line.length; j < m; j++) {
                if (line.charAt(j) === '\t') {
                    repl += ' ';
                    while ((repl.length % 8) != 0) {
                        repl += ' ';
                    }
                } else {
                    repl += line.charAt(j);
                }
            }
            lines[i] = repl;
        }
    }

    // XXX: normalize last newline (i.e. force a newline if contents don't
    // end with a newline)?

    return lines.join('\n');
}



module.exports = Debugger;