/*
 * noVNC: HTML5 VNC client
 * Copyright (C) 2012 Joel Martin
 * Copyright (C) 2013 Samuel Mannehed for Cendio AB
 * Licensed under MPL 2.0 (see LICENSE.txt)
 *
 * See README.md for usage and integration instructions.
 *
 * TIGHT decoder portion:
 * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
 */

/*jslint white: false, browser: true, bitwise: false, plusplus: false */
/*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */


function RFB(defaults) {
"use strict";

var that           = {},  // Public API methods
    conf           = {},  // Configuration attributes

    // Pre-declare private functions used before definitions (jslint)
    init_vars, updateState, fail, handle_message,
    init_msg, normal_msg, framebufferUpdate, print_stats,

    pixelFormat, clientEncodings, fbUpdateRequest, fbUpdateRequests,
    keyEvent, pointerEvent, clientCutText,

    getTightCLength, extract_data_uri,
    keyPress, mouseButton, mouseMove,

    checkEvents,  // Overridable for testing


    //
    // Private RFB namespace variables
    //
    rfb_host       = '',
    rfb_port       = 5900,
    rfb_password   = '',
    rfb_path       = '',

    rfb_state      = 'disconnected',
    rfb_version    = 0,
    rfb_max_version= 3.8,
    rfb_auth_scheme= '',


    // In preference order
    encodings      = [
        ['COPYRECT',         0x01 ],
        ['TIGHT',            0x07 ],
        ['TIGHT_PNG',        -260 ],
        ['HEXTILE',          0x05 ],
        ['RRE',              0x02 ],
        ['RAW',              0x00 ],
        ['DesktopSize',      -223 ],
        ['Cursor',           -239 ],

        // Psuedo-encoding settings
        //['JPEG_quality_lo',   -32 ],
        ['JPEG_quality_med',    -26 ],
        //['JPEG_quality_hi',   -23 ],
        //['compress_lo',      -255 ],
        ['compress_hi',        -247 ],
        ['last_rect',          -224 ]
        ],

    encHandlers    = {},
    encNames       = {}, 
    encStats       = {},     // [rectCnt, rectCntTot]

    ws             = null,   // Websock object
    display        = null,   // Display object
    keyboard       = null,   // Keyboard input handler object
    mouse          = null,   // Mouse input handler object
    sendTimer      = null,   // Send Queue check timer
    connTimer      = null,   // connection timer
    disconnTimer   = null,   // disconnection timer
    msgTimer       = null,   // queued handle_message timer

    // Frame buffer update state
    FBU            = {
        rects          : 0,
        subrects       : 0,  // RRE
        lines          : 0,  // RAW
        tiles          : 0,  // HEXTILE
        bytes          : 0,
        x              : 0,
        y              : 0,
        width          : 0, 
        height         : 0,
        encoding       : 0,
        subencoding    : -1,
        background     : null,
        zlibs          : []   // TIGHT zlib streams
    },

    fb_Bpp         = 4,
    fb_depth       = 3,
    fb_width       = 0,
    fb_height      = 0,
    fb_name        = "",

    rre_chunk_sz   = 100,

    timing         = {
        last_fbu       : 0,
        fbu_total      : 0,
        fbu_total_cnt  : 0,
        full_fbu_total : 0,
        full_fbu_cnt   : 0,

        fbu_rt_start   : 0,
        fbu_rt_total   : 0,
        fbu_rt_cnt     : 0,
        pixels         : 0
    },

    test_mode        = false,

    def_con_timeout  = Websock_native ? 2 : 5,

    /* Mouse state */
    mouse_buttonMask = 0,
    mouse_arr        = [],
    viewportDragging = false,
    viewportDragPos  = {};

// Configuration attributes
Util.conf_defaults(conf, that, defaults, [
    ['target',             'wo', 'dom', null, 'VNC display rendering Canvas object'],
    ['focusContainer',     'wo', 'dom', document, 'DOM element that captures keyboard input'],

    ['encrypt',            'rw', 'bool', false, 'Use TLS/SSL/wss encryption'],
    ['true_color',         'rw', 'bool', true,  'Request true color pixel data'],
    ['local_cursor',       'rw', 'bool', false, 'Request locally rendered cursor'],
    ['shared',             'rw', 'bool', true,  'Request shared mode'],
    ['view_only',          'rw', 'bool', false, 'Disable client mouse/keyboard'],

    ['connectTimeout',     'rw', 'int', def_con_timeout, 'Time (s) to wait for connection'],
    ['disconnectTimeout',  'rw', 'int', 3,    'Time (s) to wait for disconnection'],

    // UltraVNC repeater ID to connect to
    ['repeaterID',         'rw', 'str',  '',    'RepeaterID to connect to'],

    ['viewportDrag',       'rw', 'bool', false, 'Move the viewport on mouse drags'],

    // Callback functions
    ['onUpdateState',      'rw', 'func', function() { },
        'onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change '],
    ['onPasswordRequired', 'rw', 'func', function() { },
        'onPasswordRequired(rfb): VNC password is required '],
    ['onClipboard',        'rw', 'func', function() { },
        'onClipboard(rfb, text): RFB clipboard contents received'],
    ['onBell',             'rw', 'func', function() { },
        'onBell(rfb): RFB Bell message received '],
    ['onFBUReceive',       'rw', 'func', function() { },
        'onFBUReceive(rfb, fbu): RFB FBU received but not yet processed '],
    ['onFBUComplete',      'rw', 'func', function() { },
        'onFBUComplete(rfb, fbu): RFB FBU received and processed '],
    ['onFBResize',         'rw', 'func', function() { },
        'onFBResize(rfb, width, height): frame buffer resized'],
    ['onDesktopName',      'rw', 'func', function() { },
        'onDesktopName(rfb, name): desktop name received'],

    // These callback names are deprecated
    ['updateState',        'rw', 'func', function() { },
        'obsolete, use onUpdateState'],
    ['clipboardReceive',   'rw', 'func', function() { },
        'obsolete, use onClipboard']
    ]);


// Override/add some specific configuration getters/setters
that.set_local_cursor = function(cursor) {
    if ((!cursor) || (cursor in {'0':1, 'no':1, 'false':1})) {
        conf.local_cursor = false;
    } else {
        if (display.get_cursor_uri()) {
            conf.local_cursor = true;
        } else {
            Util.Warn("Browser does not support local cursor");
        }
    }
};

// These are fake configuration getters
that.get_display = function() { return display; };

that.get_keyboard = function() { return keyboard; };

that.get_mouse = function() { return mouse; };



//
// Setup routines
//

// Create the public API interface and initialize values that stay
// constant across connect/disconnect
function constructor() {
    var i, rmode;
    Util.Debug(">> RFB.constructor");

    // Create lookup tables based encoding number
    for (i=0; i < encodings.length; i+=1) {
        encHandlers[encodings[i][1]] = encHandlers[encodings[i][0]];
        encNames[encodings[i][1]] = encodings[i][0];
        encStats[encodings[i][1]] = [0, 0];
    }
    // Initialize display, mouse, keyboard, and websock
    try {
        display   = new Display({'target': conf.target});
    } catch (exc) {
        Util.Error("Display exception: " + exc);
        updateState('fatal', "No working Display");
    }
    keyboard = new Keyboard({'target': conf.focusContainer,
                                'onKeyPress': keyPress});
    mouse    = new Mouse({'target': conf.target,
                            'onMouseButton': mouseButton,
                            'onMouseMove': mouseMove});

    rmode = display.get_render_mode();

    ws = new Websock();
    ws.on('message', handle_message);
    ws.on('open', function() {
        if (rfb_state === "connect") {
            updateState('ProtocolVersion', "Starting VNC handshake");
        } else {
            fail("Got unexpected WebSockets connection");
        }
    });
    ws.on('close', function(e) {
        Util.Warn("WebSocket on-close event");
        var msg = "";
        if (e.code) {
            msg = " (code: " + e.code;
            if (e.reason) {
                msg += ", reason: " + e.reason;
            }
            msg += ")";
        }
        if (rfb_state === 'disconnect') {
            updateState('disconnected', 'VNC disconnected' + msg);
        } else if (rfb_state === 'ProtocolVersion') {
            fail('Failed to connect to server' + msg);
        } else if (rfb_state in {'failed':1, 'disconnected':1}) {
            Util.Error("Received onclose while disconnected" + msg);
        } else  {
            fail('Server disconnected' + msg);
        }
    });
    ws.on('error', function(e) {
        Util.Warn("WebSocket on-error event");
        //fail("WebSock reported an error");
    });


    init_vars();

    /* Check web-socket-js if no builtin WebSocket support */
    if (Websock_native) {
        Util.Info("Using native WebSockets");
        updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode);
    } else {
        Util.Warn("Using web-socket-js bridge. Flash version: " +
                  Util.Flash.version);
        if ((! Util.Flash) ||
            (Util.Flash.version < 9)) {
            updateState('fatal', "WebSockets or <a href='http://get.adobe.com/flashplayer'>Adobe Flash<\/a> is required");
        } else if (document.location.href.substr(0, 7) === "file://") {
            updateState('fatal',
                    "'file://' URL is incompatible with Adobe Flash");
        } else {
            updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode);
        }
    }

    Util.Debug("<< RFB.constructor");
    return that;  // Return the public API interface
}

function connect() {
    Util.Debug(">> RFB.connect");
    var uri;
    
    if (typeof UsingSocketIO !== "undefined") {
        uri = "http://" + rfb_host + ":" + rfb_port + "/" + rfb_path;
    } else {
        if (conf.encrypt) {
            uri = "wss://";
        } else {
            uri = "ws://";
        }
        uri += rfb_host + ":" + rfb_port + "/" + rfb_path;
    }
    Util.Info("connecting to " + uri);
    // TODO: make protocols a configurable
    ws.open(uri, ['binary', 'base64']);

    Util.Debug("<< RFB.connect");
}

// Initialize variables that are reset before each connection
init_vars = function() {
    var i;

    /* Reset state */
    ws.init();

    FBU.rects        = 0;
    FBU.subrects     = 0;  // RRE and HEXTILE
    FBU.lines        = 0;  // RAW
    FBU.tiles        = 0;  // HEXTILE
    FBU.zlibs        = []; // TIGHT zlib encoders
    mouse_buttonMask = 0;
    mouse_arr        = [];

    // Clear the per connection encoding stats
    for (i=0; i < encodings.length; i+=1) {
        encStats[encodings[i][1]][0] = 0;
    }
    
    for (i=0; i < 4; i++) {
        //FBU.zlibs[i] = new InflateStream();
        FBU.zlibs[i] = new TINF();
        FBU.zlibs[i].init();
    }
};

// Print statistics
print_stats = function() {
    var i, s;
    Util.Info("Encoding stats for this connection:");
    for (i=0; i < encodings.length; i+=1) {
        s = encStats[encodings[i][1]];
        if ((s[0] + s[1]) > 0) {
            Util.Info("    " + encodings[i][0] + ": " +
                      s[0] + " rects");
        }
    }
    Util.Info("Encoding stats since page load:");
    for (i=0; i < encodings.length; i+=1) {
        s = encStats[encodings[i][1]];
        if ((s[0] + s[1]) > 0) {
            Util.Info("    " + encodings[i][0] + ": " +
                      s[1] + " rects");
        }
    }
};

//
// Utility routines
//


/*
 * Page states:
 *   loaded       - page load, equivalent to disconnected
 *   disconnected - idle state
 *   connect      - starting to connect (to ProtocolVersion)
 *   normal       - connected
 *   disconnect   - starting to disconnect
 *   failed       - abnormal disconnect
 *   fatal        - failed to load page, or fatal error
 *
 * RFB protocol initialization states:
 *   ProtocolVersion 
 *   Security
 *   Authentication
 *   password     - waiting for password, not part of RFB
 *   SecurityResult
 *   ClientInitialization - not triggered by server message
 *   ServerInitialization (to normal)
 */
updateState = function(state, statusMsg) {
    var func, cmsg, oldstate = rfb_state;

    if (state === oldstate) {
        /* Already here, ignore */
        Util.Debug("Already in state '" + state + "', ignoring.");
        return;
    }

    /* 
     * These are disconnected states. A previous connect may
     * asynchronously cause a connection so make sure we are closed.
     */
    if (state in {'disconnected':1, 'loaded':1, 'connect':1,
                  'disconnect':1, 'failed':1, 'fatal':1}) {
        if (sendTimer) {
            clearInterval(sendTimer);
            sendTimer = null;
        }

        if (msgTimer) {
            clearTimeout(msgTimer);
            msgTimer = null;
        }

        if (display && display.get_context()) {
            keyboard.ungrab();
            mouse.ungrab();
            display.defaultCursor();
            if ((Util.get_logging() !== 'debug') ||
                (state === 'loaded')) {
                // Show noVNC logo on load and when disconnected if
                // debug is off
                display.clear();
            }
        }

        ws.close();
    }

    if (oldstate === 'fatal') {
        Util.Error("Fatal error, cannot continue");
    }

    if ((state === 'failed') || (state === 'fatal')) {
        func = Util.Error;
    } else {
        func = Util.Warn;
    }

    cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : "";
    func("New state '" + state + "', was '" + oldstate + "'." + cmsg);

    if ((oldstate === 'failed') && (state === 'disconnected')) {
        // Do disconnect action, but stay in failed state
        rfb_state = 'failed';
    } else {
        rfb_state = state;
    }

    if (connTimer && (rfb_state !== 'connect')) {
        Util.Debug("Clearing connect timer");
        clearTimeout(connTimer);
        connTimer = null;
    }

    if (disconnTimer && (rfb_state !== 'disconnect')) {
        Util.Debug("Clearing disconnect timer");
        clearTimeout(disconnTimer);
        disconnTimer = null;
    }

    switch (state) {
    case 'normal':
        if ((oldstate === 'disconnected') || (oldstate === 'failed')) {
            Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'");
        }

        break;


    case 'connect':
        
        connTimer = setTimeout(function () {
                fail("Connect timeout");
            }, conf.connectTimeout * 1000);

        init_vars();
        connect();

        // WebSocket.onopen transitions to 'ProtocolVersion'
        break;


    case 'disconnect':

        if (! test_mode) {
            disconnTimer = setTimeout(function () {
                    fail("Disconnect timeout");
                }, conf.disconnectTimeout * 1000);
        }

        print_stats();

        // WebSocket.onclose transitions to 'disconnected'
        break;


    case 'failed':
        if (oldstate === 'disconnected') {
            Util.Error("Invalid transition from 'disconnected' to 'failed'");
        }
        if (oldstate === 'normal') {
            Util.Error("Error while connected.");
        }
        if (oldstate === 'init') {
            Util.Error("Error while initializing.");
        }

        // Make sure we transition to disconnected
        setTimeout(function() { updateState('disconnected'); }, 50);

        break;


    default:
        // No state change action to take

    }

    if ((oldstate === 'failed') && (state === 'disconnected')) {
        // Leave the failed message
        conf.updateState(that, state, oldstate); // Obsolete
        conf.onUpdateState(that, state, oldstate);
    } else {
        conf.updateState(that, state, oldstate, statusMsg); // Obsolete
        conf.onUpdateState(that, state, oldstate, statusMsg);
    }
};

fail = function(msg) {
    updateState('failed', msg);
    return false;
};

handle_message = function() {
    //Util.Debug(">> handle_message ws.rQlen(): " + ws.rQlen());
    //Util.Debug("ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")");
    if (ws.rQlen() === 0) {
        Util.Warn("handle_message called on empty receive queue");
        return;
    }
    switch (rfb_state) {
    case 'disconnected':
    case 'failed':
        Util.Error("Got data while disconnected");
        break;
    case 'normal':
        if (normal_msg() && ws.rQlen() > 0) {
            // true means we can continue processing
            // Give other events a chance to run
            if (msgTimer === null) {
                Util.Debug("More data to process, creating timer");
                msgTimer = setTimeout(function () {
                            msgTimer = null;
                            handle_message();
                        }, 10);
            } else {
                Util.Debug("More data to process, existing timer");
            }
        }
        break;
    default:
        init_msg();
        break;
    }
};


function genDES(password, challenge) {
    var i, passwd = [];
    for (i=0; i < password.length; i += 1) {
        passwd.push(password.charCodeAt(i));
    }
    return (new DES(passwd)).encrypt(challenge);
}

// overridable for testing
checkEvents = function() {
    if (rfb_state === 'normal' && !viewportDragging && mouse_arr.length > 0) {
        ws.send(mouse_arr);
        mouse_arr = [];
    }
};

keyPress = function(keysym, down) {
    if (conf.view_only) { return; } // View only, skip keyboard events

    ws.send(keyEvent(keysym, down));
};

mouseButton = function(x, y, down, bmask) {
    if (down) {
        mouse_buttonMask |= bmask;
    } else {
        mouse_buttonMask ^= bmask;
    }

    if (conf.viewportDrag) {
        if (down && !viewportDragging) {
            viewportDragging = true;
            viewportDragPos = {'x': x, 'y': y};

            // Skip sending mouse events
            return;
        } else {
            viewportDragging = false;
        }
    }

    if (conf.view_only) { return; } // View only, skip mouse events

    mouse_arr = mouse_arr.concat(
            pointerEvent(display.absX(x), display.absY(y)) );
    ws.send(mouse_arr);
    mouse_arr = [];
};

mouseMove = function(x, y) {
    //Util.Debug('>> mouseMove ' + x + "," + y);
    var deltaX, deltaY;

    if (viewportDragging) {
        //deltaX = x - viewportDragPos.x; // drag viewport
        deltaX = viewportDragPos.x - x; // drag frame buffer
        //deltaY = y - viewportDragPos.y; // drag viewport
        deltaY = viewportDragPos.y - y; // drag frame buffer
        viewportDragPos = {'x': x, 'y': y};

        display.viewportChange(deltaX, deltaY);

        // Skip sending mouse events
        return;
    }

    if (conf.view_only) { return; } // View only, skip mouse events

    mouse_arr = mouse_arr.concat(
            pointerEvent(display.absX(x), display.absY(y)));
    
    checkEvents();
};


//
// Server message handlers
//

// RFB/VNC initialisation message handler
init_msg = function() {
    //Util.Debug(">> init_msg [rfb_state '" + rfb_state + "']");

    var strlen, reason, length, sversion, cversion, repeaterID,
        i, types, num_types, challenge, response, bpp, depth,
        big_endian, red_max, green_max, blue_max, red_shift,
        green_shift, blue_shift, true_color, name_length, is_repeater;

    //Util.Debug("ws.rQ (" + ws.rQlen() + ") " + ws.rQslice(0));
    switch (rfb_state) {

    case 'ProtocolVersion' :
        if (ws.rQlen() < 12) {
            return fail("Incomplete protocol version");
        }
        sversion = ws.rQshiftStr(12).substr(4,7);
        Util.Info("Server ProtocolVersion: " + sversion);
        is_repeater = 0;
        switch (sversion) {
            case "000.000": is_repeater = 1; break; // UltraVNC repeater
            case "003.003": rfb_version = 3.3; break;
            case "003.006": rfb_version = 3.3; break;  // UltraVNC
            case "003.889": rfb_version = 3.3; break;  // Apple Remote Desktop
            case "003.007": rfb_version = 3.7; break;
            case "003.008": rfb_version = 3.8; break;
            case "004.000": rfb_version = 3.8; break;  // Intel AMT KVM
            case "004.001": rfb_version = 3.8; break;  // RealVNC 4.6
            default:
                return fail("Invalid server version " + sversion);
        }
        if (is_repeater) { 
            repeaterID = conf.repeaterID;
            while (repeaterID.length < 250) {
                repeaterID += "\0";
            }
            ws.send_string(repeaterID);
            break;
        }
        if (rfb_version > rfb_max_version) { 
            rfb_version = rfb_max_version;
        }

        if (! test_mode) {
            sendTimer = setInterval(function() {
                    // Send updates either at a rate of one update
                    // every 50ms, or whatever slower rate the network
                    // can handle.
                    ws.flush();
                }, 50);
        }

        cversion = "00" + parseInt(rfb_version,10) +
                   ".00" + ((rfb_version * 10) % 10);
        ws.send_string("RFB " + cversion + "\n");
        updateState('Security', "Sent ProtocolVersion: " + cversion);
        break;

    case 'Security' :
        if (rfb_version >= 3.7) {
            // Server sends supported list, client decides 
            num_types = ws.rQshift8();
            if (ws.rQwait("security type", num_types, 1)) { return false; }
            if (num_types === 0) {
                strlen = ws.rQshift32();
                reason = ws.rQshiftStr(strlen);
                return fail("Security failure: " + reason);
            }
            rfb_auth_scheme = 0;
            types = ws.rQshiftBytes(num_types);
            Util.Debug("Server security types: " + types);
            for (i=0; i < types.length; i+=1) {
                if ((types[i] > rfb_auth_scheme) && (types[i] < 3)) {
                    rfb_auth_scheme = types[i];
                }
            }
            if (rfb_auth_scheme === 0) {
                return fail("Unsupported security types: " + types);
            }
            
            ws.send([rfb_auth_scheme]);
        } else {
            // Server decides
            if (ws.rQwait("security scheme", 4)) { return false; }
            rfb_auth_scheme = ws.rQshift32();
        }
        updateState('Authentication',
                "Authenticating using scheme: " + rfb_auth_scheme);
        init_msg();  // Recursive fallthrough (workaround JSLint complaint)
        break;

    // Triggered by fallthough, not by server message
    case 'Authentication' :
        //Util.Debug("Security auth scheme: " + rfb_auth_scheme);
        switch (rfb_auth_scheme) {
            case 0:  // connection failed
                if (ws.rQwait("auth reason", 4)) { return false; }
                strlen = ws.rQshift32();
                reason = ws.rQshiftStr(strlen);
                return fail("Auth failure: " + reason);
            case 1:  // no authentication
                if (rfb_version >= 3.8) {
                    updateState('SecurityResult');
                    return;
                }
                // Fall through to ClientInitialisation
                break;
            case 2:  // VNC authentication
                if (rfb_password.length === 0) {
                    // Notify via both callbacks since it is kind of
                    // a RFB state change and a UI interface issue.
                    updateState('password', "Password Required");
                    conf.onPasswordRequired(that);
                    return;
                }
                if (ws.rQwait("auth challenge", 16)) { return false; }
                challenge = ws.rQshiftBytes(16);
                //Util.Debug("Password: " + rfb_password);
                //Util.Debug("Challenge: " + challenge +
                //           " (" + challenge.length + ")");
                response = genDES(rfb_password, challenge);
                //Util.Debug("Response: " + response +
                //           " (" + response.length + ")");
                
                //Util.Debug("Sending DES encrypted auth response");
                ws.send(response);
                updateState('SecurityResult');
                return;
            default:
                fail("Unsupported auth scheme: " + rfb_auth_scheme);
                return;
        }
        updateState('ClientInitialisation', "No auth required");
        init_msg();  // Recursive fallthrough (workaround JSLint complaint)
        break;

    case 'SecurityResult' :
        if (ws.rQwait("VNC auth response ", 4)) { return false; }
        switch (ws.rQshift32()) {
            case 0:  // OK
                // Fall through to ClientInitialisation
                break;
            case 1:  // failed
                if (rfb_version >= 3.8) {
                    length = ws.rQshift32();
                    if (ws.rQwait("SecurityResult reason", length, 8)) {
                        return false;
                    }
                    reason = ws.rQshiftStr(length);
                    fail(reason);
                } else {
                    fail("Authentication failed");
                }
                return;
            case 2:  // too-many
                return fail("Too many auth attempts");
        }
        updateState('ClientInitialisation', "Authentication OK");
        init_msg();  // Recursive fallthrough (workaround JSLint complaint)
        break;

    // Triggered by fallthough, not by server message
    case 'ClientInitialisation' :
        ws.send([conf.shared ? 1 : 0]); // ClientInitialisation
        updateState('ServerInitialisation', "Authentication OK");
        break;

    case 'ServerInitialisation' :
        if (ws.rQwait("server initialization", 24)) { return false; }

        /* Screen size */
        fb_width  = ws.rQshift16();
        fb_height = ws.rQshift16();

        /* PIXEL_FORMAT */
        bpp            = ws.rQshift8();
        depth          = ws.rQshift8();
        big_endian     = ws.rQshift8();
        true_color     = ws.rQshift8();

        red_max        = ws.rQshift16();
        green_max      = ws.rQshift16();
        blue_max       = ws.rQshift16();
        red_shift      = ws.rQshift8();
        green_shift    = ws.rQshift8();
        blue_shift     = ws.rQshift8();
        ws.rQshiftStr(3); // padding

        Util.Info("Screen: " + fb_width + "x" + fb_height + 
                  ", bpp: " + bpp + ", depth: " + depth +
                  ", big_endian: " + big_endian +
                  ", true_color: " + true_color +
                  ", red_max: " + red_max +
                  ", green_max: " + green_max +
                  ", blue_max: " + blue_max +
                  ", red_shift: " + red_shift +
                  ", green_shift: " + green_shift +
                  ", blue_shift: " + blue_shift);

        if (big_endian !== 0) {
            Util.Warn("Server native endian is not little endian");
        }
        if (red_shift !== 16) {
            Util.Warn("Server native red-shift is not 16");
        }
        if (blue_shift !== 0) {
            Util.Warn("Server native blue-shift is not 0");
        }

        /* Connection name/title */
        name_length   = ws.rQshift32();
        fb_name = ws.rQshiftStr(name_length);
        conf.onDesktopName(that, fb_name);
        
        if (conf.true_color && fb_name === "Intel(r) AMT KVM")
        {
            Util.Warn("Intel AMT KVM only support 8/16 bit depths. Disabling true color");
            conf.true_color = false;
        }

        display.set_true_color(conf.true_color);
        conf.onFBResize(that, fb_width, fb_height);
        display.resize(fb_width, fb_height);
        keyboard.grab();
        mouse.grab();

        if (conf.true_color) {
            fb_Bpp           = 4;
            fb_depth         = 3;
        } else {
            fb_Bpp           = 1;
            fb_depth         = 1;
        }

        response = pixelFormat();
        response = response.concat(clientEncodings());
        response = response.concat(fbUpdateRequests()); // initial fbu-request
        timing.fbu_rt_start = (new Date()).getTime();
        timing.pixels = 0;
        ws.send(response);
        
        checkEvents();

        if (conf.encrypt) {
            updateState('normal', "Connected (encrypted) to: " + fb_name);
        } else {
            updateState('normal', "Connected (unencrypted) to: " + fb_name);
        }
        break;
    }
    //Util.Debug("<< init_msg");
};


/* Normal RFB/VNC server message handler */
normal_msg = function() {
    //Util.Debug(">> normal_msg");

    var ret = true, msg_type, length, text,
        c, first_colour, num_colours, red, green, blue;

    if (FBU.rects > 0) {
        msg_type = 0;
    } else {
        msg_type = ws.rQshift8();
    }
    switch (msg_type) {
    case 0:  // FramebufferUpdate
        ret = framebufferUpdate(); // false means need more data
        if (ret) {
            // only allow one outstanding fbu-request at a time
            ws.send(fbUpdateRequests());
        }
        break;
    case 1:  // SetColourMapEntries
        Util.Debug("SetColourMapEntries");
        ws.rQshift8();  // Padding
        first_colour = ws.rQshift16(); // First colour
        num_colours = ws.rQshift16();
        if (ws.rQwait("SetColourMapEntries", num_colours*6, 6)) { return false; }
        
        for (c=0; c < num_colours; c+=1) { 
            red = ws.rQshift16();
            //Util.Debug("red before: " + red);
            red = parseInt(red / 256, 10);
            //Util.Debug("red after: " + red);
            green = parseInt(ws.rQshift16() / 256, 10);
            blue = parseInt(ws.rQshift16() / 256, 10);
            display.set_colourMap([blue, green, red], first_colour + c);
        }
        Util.Debug("colourMap: " + display.get_colourMap());
        Util.Info("Registered " + num_colours + " colourMap entries");
        //Util.Debug("colourMap: " + display.get_colourMap());
        break;
    case 2:  // Bell
        Util.Debug("Bell");
        conf.onBell(that);
        break;
    case 3:  // ServerCutText
        Util.Debug("ServerCutText");
        if (ws.rQwait("ServerCutText header", 7, 1)) { return false; }
        ws.rQshiftBytes(3);  // Padding
        length = ws.rQshift32();
        if (ws.rQwait("ServerCutText", length, 8)) { return false; }

        text = ws.rQshiftStr(length);
        conf.clipboardReceive(that, text); // Obsolete
        conf.onClipboard(that, text);
        break;
    default:
        fail("Disconnected: illegal server message type " + msg_type);
        Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30));
        break;
    }
    //Util.Debug("<< normal_msg");
    return ret;
};

framebufferUpdate = function() {
    var now, hdr, fbu_rt_diff, ret = true;

    if (FBU.rects === 0) {
        //Util.Debug("New FBU: ws.rQslice(0,20): " + ws.rQslice(0,20));
        if (ws.rQwait("FBU header", 3)) {
            ws.rQunshift8(0);  // FBU msg_type
            return false;
        }
        ws.rQshift8();  // padding
        FBU.rects = ws.rQshift16();
        //Util.Debug("FramebufferUpdate, rects:" + FBU.rects);
        FBU.bytes = 0;
        timing.cur_fbu = 0;
        if (timing.fbu_rt_start > 0) {
            now = (new Date()).getTime();
            Util.Info("First FBU latency: " + (now - timing.fbu_rt_start));
        }
    }

    while (FBU.rects > 0) {
        if (rfb_state !== "normal") {
            return false;
        }
        if (ws.rQwait("FBU", FBU.bytes)) { return false; }
        if (FBU.bytes === 0) {
            if (ws.rQwait("rect header", 12)) { return false; }
            /* New FramebufferUpdate */

            hdr = ws.rQshiftBytes(12);
            FBU.x      = (hdr[0] << 8) + hdr[1];
            FBU.y      = (hdr[2] << 8) + hdr[3];
            FBU.width  = (hdr[4] << 8) + hdr[5];
            FBU.height = (hdr[6] << 8) + hdr[7];
            FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
                                    (hdr[10] << 8) +  hdr[11], 10);

            conf.onFBUReceive(that,
                    {'x': FBU.x, 'y': FBU.y,
                     'width': FBU.width, 'height': FBU.height,
                     'encoding': FBU.encoding,
                     'encodingName': encNames[FBU.encoding]});

            if (encNames[FBU.encoding]) {
                // Debug:
                /*
                var msg =  "FramebufferUpdate rects:" + FBU.rects;
                msg += " x: " + FBU.x + " y: " + FBU.y;
                msg += " width: " + FBU.width + " height: " + FBU.height;
                msg += " encoding:" + FBU.encoding;
                msg += "(" + encNames[FBU.encoding] + ")";
                msg += ", ws.rQlen(): " + ws.rQlen();
                Util.Debug(msg);
                */
            } else {
                fail("Disconnected: unsupported encoding " +
                    FBU.encoding);
                return false;
            }
        }

        timing.last_fbu = (new Date()).getTime();

        ret = encHandlers[FBU.encoding]();

        now = (new Date()).getTime();
        timing.cur_fbu += (now - timing.last_fbu);

        if (ret) {
            encStats[FBU.encoding][0] += 1;
            encStats[FBU.encoding][1] += 1;
            timing.pixels += FBU.width * FBU.height;
        }

        if (timing.pixels >= (fb_width * fb_height)) {
            if (((FBU.width === fb_width) &&
                        (FBU.height === fb_height)) ||
                    (timing.fbu_rt_start > 0)) {
                timing.full_fbu_total += timing.cur_fbu;
                timing.full_fbu_cnt += 1;
                Util.Info("Timing of full FBU, cur: " +
                          timing.cur_fbu + ", total: " +
                          timing.full_fbu_total + ", cnt: " +
                          timing.full_fbu_cnt + ", avg: " +
                          (timing.full_fbu_total /
                              timing.full_fbu_cnt));
            }
            if (timing.fbu_rt_start > 0) {
                fbu_rt_diff = now - timing.fbu_rt_start;
                timing.fbu_rt_total += fbu_rt_diff;
                timing.fbu_rt_cnt += 1;
                Util.Info("full FBU round-trip, cur: " +
                          fbu_rt_diff + ", total: " +
                          timing.fbu_rt_total + ", cnt: " +
                          timing.fbu_rt_cnt + ", avg: " +
                          (timing.fbu_rt_total /
                              timing.fbu_rt_cnt));
                timing.fbu_rt_start = 0;
            }
        }
        if (! ret) {
            return ret; // false ret means need more data
        }
    }

    conf.onFBUComplete(that,
            {'x': FBU.x, 'y': FBU.y,
                'width': FBU.width, 'height': FBU.height,
                'encoding': FBU.encoding,
                'encodingName': encNames[FBU.encoding]});

    return true; // We finished this FBU
};

//
// FramebufferUpdate encodings
//

encHandlers.RAW = function display_raw() {
    //Util.Debug(">> display_raw (" + ws.rQlen() + " bytes)");

    var cur_y, cur_height;

    if (FBU.lines === 0) {
        FBU.lines = FBU.height;
    }
    FBU.bytes = FBU.width * fb_Bpp; // At least a line
    if (ws.rQwait("RAW", FBU.bytes)) { return false; }
    cur_y = FBU.y + (FBU.height - FBU.lines);
    cur_height = Math.min(FBU.lines,
                          Math.floor(ws.rQlen()/(FBU.width * fb_Bpp)));
    display.blitImage(FBU.x, cur_y, FBU.width, cur_height,
            ws.get_rQ(), ws.get_rQi());
    ws.rQshiftBytes(FBU.width * cur_height * fb_Bpp);
    FBU.lines -= cur_height;

    if (FBU.lines > 0) {
        FBU.bytes = FBU.width * fb_Bpp; // At least another line
    } else {
        FBU.rects -= 1;
        FBU.bytes = 0;
    }
    //Util.Debug("<< display_raw (" + ws.rQlen() + " bytes)");
    return true;
};

encHandlers.COPYRECT = function display_copy_rect() {
    //Util.Debug(">> display_copy_rect");

    var old_x, old_y;

    FBU.bytes = 4;
    if (ws.rQwait("COPYRECT", 4)) { return false; }
    display.renderQ_push({
            'type': 'copy',
            'old_x': ws.rQshift16(),
            'old_y': ws.rQshift16(),
            'x': FBU.x,
            'y': FBU.y,
            'width': FBU.width,
            'height': FBU.height});
    FBU.rects -= 1;
    FBU.bytes = 0;
    return true;
};

encHandlers.RRE = function display_rre() {
    //Util.Debug(">> display_rre (" + ws.rQlen() + " bytes)");
    var color, x, y, width, height, chunk;

    if (FBU.subrects === 0) {
        FBU.bytes = 4+fb_Bpp;
        if (ws.rQwait("RRE", 4+fb_Bpp)) { return false; }
        FBU.subrects = ws.rQshift32();
        color = ws.rQshiftBytes(fb_Bpp); // Background
        display.fillRect(FBU.x, FBU.y, FBU.width, FBU.height, color);
    }
    while ((FBU.subrects > 0) && (ws.rQlen() >= (fb_Bpp + 8))) {
        color = ws.rQshiftBytes(fb_Bpp);
        x = ws.rQshift16();
        y = ws.rQshift16();
        width = ws.rQshift16();
        height = ws.rQshift16();
        display.fillRect(FBU.x + x, FBU.y + y, width, height, color);
        FBU.subrects -= 1;
    }
    //Util.Debug("   display_rre: rects: " + FBU.rects +
    //           ", FBU.subrects: " + FBU.subrects);

    if (FBU.subrects > 0) {
        chunk = Math.min(rre_chunk_sz, FBU.subrects);
        FBU.bytes = (fb_Bpp + 8) * chunk;
    } else {
        FBU.rects -= 1;
        FBU.bytes = 0;
    }
    //Util.Debug("<< display_rre, FBU.bytes: " + FBU.bytes);
    return true;
};

encHandlers.HEXTILE = function display_hextile() {
    //Util.Debug(">> display_hextile");
    var subencoding, subrects, color, cur_tile,
        tile_x, x, w, tile_y, y, h, xy, s, sx, sy, wh, sw, sh,
        rQ = ws.get_rQ(), rQi = ws.get_rQi(); 

    if (FBU.tiles === 0) {
        FBU.tiles_x = Math.ceil(FBU.width/16);
        FBU.tiles_y = Math.ceil(FBU.height/16);
        FBU.total_tiles = FBU.tiles_x * FBU.tiles_y;
        FBU.tiles = FBU.total_tiles;
    }

    /* FBU.bytes comes in as 1, ws.rQlen() at least 1 */
    while (FBU.tiles > 0) {
        FBU.bytes = 1;
        if (ws.rQwait("HEXTILE subencoding", FBU.bytes)) { return false; }
        subencoding = rQ[rQi];  // Peek
        if (subencoding > 30) { // Raw
            fail("Disconnected: illegal hextile subencoding " + subencoding);
            //Util.Debug("ws.rQslice(0,30):" + ws.rQslice(0,30));
            return false;
        }
        subrects = 0;
        cur_tile = FBU.total_tiles - FBU.tiles;
        tile_x = cur_tile % FBU.tiles_x;
        tile_y = Math.floor(cur_tile / FBU.tiles_x);
        x = FBU.x + tile_x * 16;
        y = FBU.y + tile_y * 16;
        w = Math.min(16, (FBU.x + FBU.width) - x);
        h = Math.min(16, (FBU.y + FBU.height) - y);

        /* Figure out how much we are expecting */
        if (subencoding & 0x01) { // Raw
            //Util.Debug("   Raw subencoding");
            FBU.bytes += w * h * fb_Bpp;
        } else {
            if (subencoding & 0x02) { // Background
                FBU.bytes += fb_Bpp;
            }
            if (subencoding & 0x04) { // Foreground
                FBU.bytes += fb_Bpp;
            }
            if (subencoding & 0x08) { // AnySubrects
                FBU.bytes += 1;   // Since we aren't shifting it off
                if (ws.rQwait("hextile subrects header", FBU.bytes)) { return false; }
                subrects = rQ[rQi + FBU.bytes-1]; // Peek
                if (subencoding & 0x10) { // SubrectsColoured
                    FBU.bytes += subrects * (fb_Bpp + 2);
                } else {
                    FBU.bytes += subrects * 2;
                }
            }
        }

        /*
        Util.Debug("   tile:" + cur_tile + "/" + (FBU.total_tiles - 1) +
              " (" + tile_x + "," + tile_y + ")" +
              " [" + x + "," + y + "]@" + w + "x" + h +
              ", subenc:" + subencoding +
              "(last: " + FBU.lastsubencoding + "), subrects:" +
              subrects +
              ", ws.rQlen():" + ws.rQlen() + ", FBU.bytes:" + FBU.bytes +
              " last:" + ws.rQslice(FBU.bytes-10, FBU.bytes) +
              " next:" + ws.rQslice(FBU.bytes-1, FBU.bytes+10));
        */
        if (ws.rQwait("hextile", FBU.bytes)) { return false; }

        /* We know the encoding and have a whole tile */
        FBU.subencoding = rQ[rQi];
        rQi += 1;
        if (FBU.subencoding === 0) {
            if (FBU.lastsubencoding & 0x01) {
                /* Weird: ignore blanks after RAW */
                Util.Debug("     Ignoring blank after RAW");
            } else {
                display.fillRect(x, y, w, h, FBU.background);
            }
        } else if (FBU.subencoding & 0x01) { // Raw
            display.blitImage(x, y, w, h, rQ, rQi);
            rQi += FBU.bytes - 1;
        } else {
            if (FBU.subencoding & 0x02) { // Background
                FBU.background = rQ.slice(rQi, rQi + fb_Bpp);
                rQi += fb_Bpp;
            }
            if (FBU.subencoding & 0x04) { // Foreground
                FBU.foreground = rQ.slice(rQi, rQi + fb_Bpp);
                rQi += fb_Bpp;
            }

            display.startTile(x, y, w, h, FBU.background);
            if (FBU.subencoding & 0x08) { // AnySubrects
                subrects = rQ[rQi];
                rQi += 1;
                for (s = 0; s < subrects; s += 1) {
                    if (FBU.subencoding & 0x10) { // SubrectsColoured
                        color = rQ.slice(rQi, rQi + fb_Bpp);
                        rQi += fb_Bpp;
                    } else {
                        color = FBU.foreground;
                    }
                    xy = rQ[rQi];
                    rQi += 1;
                    sx = (xy >> 4);
                    sy = (xy & 0x0f);

                    wh = rQ[rQi];
                    rQi += 1;
                    sw = (wh >> 4)   + 1;
                    sh = (wh & 0x0f) + 1;

                    display.subTile(sx, sy, sw, sh, color);
                }
            }
            display.finishTile();
        }
        ws.set_rQi(rQi);
        FBU.lastsubencoding = FBU.subencoding;
        FBU.bytes = 0;
        FBU.tiles -= 1;
    }

    if (FBU.tiles === 0) {
        FBU.rects -= 1;
    }

    //Util.Debug("<< display_hextile");
    return true;
};


// Get 'compact length' header and data size
getTightCLength = function (arr) {
    var header = 1, data = 0;
    data += arr[0] & 0x7f;
    if (arr[0] & 0x80) {
        header += 1;
        data += (arr[1] & 0x7f) << 7;
        if (arr[1] & 0x80) {
            header += 1;
            data += arr[2] << 14;
        }
    }
    return [header, data];
};

function display_tight(isTightPNG) {
    //Util.Debug(">> display_tight");

    if (fb_depth === 1) {
        fail("Tight protocol handler only implements true color mode");
    }

    var ctl, cmode, clength, color, img, data;
    var filterId = -1, resetStreams = 0, streamId = -1;
    var rQ = ws.get_rQ(), rQi = ws.get_rQi(); 

    FBU.bytes = 1; // compression-control byte
    if (ws.rQwait("TIGHT compression-control", FBU.bytes)) { return false; }

    var checksum = function(data) {
        var sum=0, i;
        for (i=0; i<data.length;i++) {
            sum += data[i];
            if (sum > 65536) sum -= 65536;
        }
        return sum;
    }

    var decompress = function(data) {
        for (var i=0; i<4; i++) {
            if ((resetStreams >> i) & 1) {
                FBU.zlibs[i].reset();
                Util.Info("Reset zlib stream " + i);
            }
        }
        var uncompressed = FBU.zlibs[streamId].uncompress(data, 0);
        if (uncompressed.status !== 0) {
            Util.Error("Invalid data in zlib stream");
        }
        //Util.Warn("Decompressed " + data.length + " to " +
        //    uncompressed.data.length + " checksums " +
        //    checksum(data) + ":" + checksum(uncompressed.data));

        return uncompressed.data;
    }

    var indexedToRGB = function (data, numColors, palette, width, height) {
        // Convert indexed (palette based) image data to RGB
        // TODO: reduce number of calculations inside loop
        var dest = [];
        var x, y, b, w, w1, dp, sp;
        if (numColors === 2) {
            w = Math.floor((width + 7) / 8);
            w1 = Math.floor(width / 8);
            for (y = 0; y < height; y++) {
                for (x = 0; x < w1; x++) {
                    for (b = 7; b >= 0; b--) {
                        dp = (y*width + x*8 + 7-b) * 3;
                        sp = (data[y*w + x] >> b & 1) * 3;
                        dest[dp  ] = palette[sp  ];
                        dest[dp+1] = palette[sp+1];
                        dest[dp+2] = palette[sp+2];
                    }
                }
                for (b = 7; b >= 8 - width % 8; b--) {
                    dp = (y*width + x*8 + 7-b) * 3;
                    sp = (data[y*w + x] >> b & 1) * 3;
                    dest[dp  ] = palette[sp  ];
                    dest[dp+1] = palette[sp+1];
                    dest[dp+2] = palette[sp+2];
                }
            }
        } else {
            for (y = 0; y < height; y++) {
                for (x = 0; x < width; x++) {
                    dp = (y*width + x) * 3;
                    sp = data[y*width + x] * 3;
                    dest[dp  ] = palette[sp  ];
                    dest[dp+1] = palette[sp+1];
                    dest[dp+2] = palette[sp+2];
                }
            }
        }
        return dest;
    };
    var handlePalette = function() {
        var numColors = rQ[rQi + 2] + 1;
        var paletteSize = numColors * fb_depth; 
        FBU.bytes += paletteSize;
        if (ws.rQwait("TIGHT palette " + cmode, FBU.bytes)) { return false; }

        var bpp = (numColors <= 2) ? 1 : 8;
        var rowSize = Math.floor((FBU.width * bpp + 7) / 8);
        var raw = false;
        if (rowSize * FBU.height < 12) {
            raw = true;
            clength = [0, rowSize * FBU.height];
        } else {
            clength = getTightCLength(ws.rQslice(3 + paletteSize,
                                                 3 + paletteSize + 3));
        }
        FBU.bytes += clength[0] + clength[1];
        if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; }

        // Shift ctl, filter id, num colors, palette entries, and clength off
        ws.rQshiftBytes(3); 
        var palette = ws.rQshiftBytes(paletteSize);
        ws.rQshiftBytes(clength[0]);

        if (raw) {
            data = ws.rQshiftBytes(clength[1]);
        } else {
            data = decompress(ws.rQshiftBytes(clength[1]));
        }

        // Convert indexed (palette based) image data to RGB
        var rgb = indexedToRGB(data, numColors, palette, FBU.width, FBU.height);

        // Add it to the render queue
        display.renderQ_push({
                'type': 'blitRgb',
                'data': rgb,
                'x': FBU.x,
                'y': FBU.y,
                'width': FBU.width,
                'height': FBU.height});
        return true;
    }

    var handleCopy = function() {
        var raw = false;
        var uncompressedSize = FBU.width * FBU.height * fb_depth;
        if (uncompressedSize < 12) {
            raw = true;
            clength = [0, uncompressedSize];
        } else {
            clength = getTightCLength(ws.rQslice(1, 4));
        }
        FBU.bytes = 1 + clength[0] + clength[1];
        if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; }

        // Shift ctl, clength off
        ws.rQshiftBytes(1 + clength[0]);

        if (raw) {
            data = ws.rQshiftBytes(clength[1]);
        } else {
            data = decompress(ws.rQshiftBytes(clength[1]));
        }

        display.renderQ_push({
                'type': 'blitRgb',
                'data': data,
                'x': FBU.x,
                'y': FBU.y,
                'width': FBU.width,
                'height': FBU.height});
        return true;
    }

    ctl = ws.rQpeek8();

    // Keep tight reset bits
    resetStreams = ctl & 0xF;

    // Figure out filter
    ctl = ctl >> 4; 
    streamId = ctl & 0x3;

    if (ctl === 0x08)      cmode = "fill";
    else if (ctl === 0x09) cmode = "jpeg";
    else if (ctl === 0x0A) cmode = "png";
    else if (ctl & 0x04)   cmode = "filter";
    else if (ctl < 0x04)   cmode = "copy";
    else return fail("Illegal tight compression received, ctl: " + ctl);

    if (isTightPNG && (cmode === "filter" || cmode === "copy")) {
        return fail("filter/copy received in tightPNG mode");
    }

    switch (cmode) {
        // fill uses fb_depth because TPIXELs drop the padding byte
        case "fill":   FBU.bytes += fb_depth; break; // TPIXEL
        case "jpeg":   FBU.bytes += 3;        break; // max clength
        case "png":    FBU.bytes += 3;        break; // max clength
        case "filter": FBU.bytes += 2;        break; // filter id + num colors if palette
        case "copy":                          break;
    }

    if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; }

    //Util.Debug("   ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")");
    //Util.Debug("   cmode: " + cmode);

    // Determine FBU.bytes
    switch (cmode) {
    case "fill":
        ws.rQshift8(); // shift off ctl
        color = ws.rQshiftBytes(fb_depth);
        display.renderQ_push({
                'type': 'fill',
                'x': FBU.x,
                'y': FBU.y,
                'width': FBU.width,
                'height': FBU.height,
                'color': [color[2], color[1], color[0]] });
        break;
    case "png":
    case "jpeg":
        clength = getTightCLength(ws.rQslice(1, 4));
        FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data
        if (ws.rQwait("TIGHT " + cmode, FBU.bytes)) { return false; }

        // We have everything, render it
        //Util.Debug("   jpeg, ws.rQlen(): " + ws.rQlen() + ", clength[0]: " +
        //           clength[0] + ", clength[1]: " + clength[1]);
        ws.rQshiftBytes(1 + clength[0]); // shift off ctl + compact length
        img = new Image();
        img.src = "data:image/" + cmode +
            extract_data_uri(ws.rQshiftBytes(clength[1]));
        display.renderQ_push({
                'type': 'img',
                'img': img,
                'x': FBU.x,
                'y': FBU.y});
        img = null;
        break;
    case "filter":
        filterId = rQ[rQi + 1];
        if (filterId === 1) {
            if (!handlePalette()) { return false; }
        } else {
            // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter
            // Filter 2, Gradient is valid but not used if jpeg is enabled
            throw("Unsupported tight subencoding received, filter: " + filterId);
        }
        break;
    case "copy":
        if (!handleCopy()) { return false; }
        break;
    }

    FBU.bytes = 0;
    FBU.rects -= 1;
    //Util.Debug("   ending ws.rQslice(0,20): " + ws.rQslice(0,20) + " (" + ws.rQlen() + ")");
    //Util.Debug("<< display_tight_png");
    return true;
}

extract_data_uri = function(arr) {
    //var i, stra = [];
    //for (i=0; i< arr.length; i += 1) {
    //    stra.push(String.fromCharCode(arr[i]));
    //}
    //return "," + escape(stra.join(''));
    return ";base64," + Base64.encode(arr);
};

encHandlers.TIGHT = function () { return display_tight(false); };
encHandlers.TIGHT_PNG = function () { return display_tight(true); };

encHandlers.last_rect = function last_rect() {
    //Util.Debug(">> last_rect");
    FBU.rects = 0;
    //Util.Debug("<< last_rect");
    return true;
};

encHandlers.DesktopSize = function set_desktopsize() {
    Util.Debug(">> set_desktopsize");
    fb_width = FBU.width;
    fb_height = FBU.height;
    conf.onFBResize(that, fb_width, fb_height);
    display.resize(fb_width, fb_height);
    timing.fbu_rt_start = (new Date()).getTime();

    FBU.bytes = 0;
    FBU.rects -= 1;

    Util.Debug("<< set_desktopsize");
    return true;
};

encHandlers.Cursor = function set_cursor() {
    var x, y, w, h, pixelslength, masklength;
    Util.Debug(">> set_cursor");
    x = FBU.x;  // hotspot-x
    y = FBU.y;  // hotspot-y
    w = FBU.width;
    h = FBU.height;

    pixelslength = w * h * fb_Bpp;
    masklength = Math.floor((w + 7) / 8) * h;

    FBU.bytes = pixelslength + masklength;
    if (ws.rQwait("cursor encoding", FBU.bytes)) { return false; }

    //Util.Debug("   set_cursor, x: " + x + ", y: " + y + ", w: " + w + ", h: " + h);

    display.changeCursor(ws.rQshiftBytes(pixelslength),
                            ws.rQshiftBytes(masklength),
                            x, y, w, h);

    FBU.bytes = 0;
    FBU.rects -= 1;

    Util.Debug("<< set_cursor");
    return true;
};

encHandlers.JPEG_quality_lo = function set_jpeg_quality() {
    Util.Error("Server sent jpeg_quality pseudo-encoding");
};

encHandlers.compress_lo = function set_compress_level() {
    Util.Error("Server sent compress level pseudo-encoding");
};

/*
 * Client message routines
 */

pixelFormat = function() {
    //Util.Debug(">> pixelFormat");
    var arr;
    arr = [0];     // msg-type
    arr.push8(0);  // padding
    arr.push8(0);  // padding
    arr.push8(0);  // padding

    arr.push8(fb_Bpp * 8); // bits-per-pixel
    arr.push8(fb_depth * 8); // depth
    arr.push8(0);  // little-endian
    arr.push8(conf.true_color ? 1 : 0);  // true-color

    arr.push16(255);  // red-max
    arr.push16(255);  // green-max
    arr.push16(255);  // blue-max
    arr.push8(16);    // red-shift
    arr.push8(8);     // green-shift
    arr.push8(0);     // blue-shift

    arr.push8(0);     // padding
    arr.push8(0);     // padding
    arr.push8(0);     // padding
    //Util.Debug("<< pixelFormat");
    return arr;
};

clientEncodings = function() {
    //Util.Debug(">> clientEncodings");
    var arr, i, encList = [];

    for (i=0; i<encodings.length; i += 1) {
        if ((encodings[i][0] === "Cursor") &&
            (! conf.local_cursor)) {
            Util.Debug("Skipping Cursor pseudo-encoding");

        // TODO: remove this when we have tight+non-true-color
        } else if ((encodings[i][0] === "TIGHT") && 
                   (! conf.true_color)) {
            Util.Warn("Skipping tight, only support with true color");
        } else {
            //Util.Debug("Adding encoding: " + encodings[i][0]);
            encList.push(encodings[i][1]);
        }
    }

    arr = [2];     // msg-type
    arr.push8(0);  // padding

    arr.push16(encList.length); // encoding count
    for (i=0; i < encList.length; i += 1) {
        arr.push32(encList[i]);
    }
    //Util.Debug("<< clientEncodings: " + arr);
    return arr;
};

fbUpdateRequest = function(incremental, x, y, xw, yw) {
    //Util.Debug(">> fbUpdateRequest");
    if (typeof(x) === "undefined") { x = 0; }
    if (typeof(y) === "undefined") { y = 0; }
    if (typeof(xw) === "undefined") { xw = fb_width; }
    if (typeof(yw) === "undefined") { yw = fb_height; }
    var arr;
    arr = [3];  // msg-type
    arr.push8(incremental);
    arr.push16(x);
    arr.push16(y);
    arr.push16(xw);
    arr.push16(yw);
    //Util.Debug("<< fbUpdateRequest");
    return arr;
};

// Based on clean/dirty areas, generate requests to send
fbUpdateRequests = function() {
    var cleanDirty = display.getCleanDirtyReset(),
        arr = [], i, cb, db;

    cb = cleanDirty.cleanBox;
    if (cb.w > 0 && cb.h > 0) {
        // Request incremental for clean box
        arr = arr.concat(fbUpdateRequest(1, cb.x, cb.y, cb.w, cb.h));
    }
    for (i = 0; i < cleanDirty.dirtyBoxes.length; i++) {
        db = cleanDirty.dirtyBoxes[i];
        // Force all (non-incremental for dirty box
        arr = arr.concat(fbUpdateRequest(0, db.x, db.y, db.w, db.h));
    }
    return arr;
};



keyEvent = function(keysym, down) {
    //Util.Debug(">> keyEvent, keysym: " + keysym + ", down: " + down);
    var arr;
    arr = [4];  // msg-type
    arr.push8(down);
    arr.push16(0);
    arr.push32(keysym);
    //Util.Debug("<< keyEvent");
    return arr;
};

pointerEvent = function(x, y) {
    //Util.Debug(">> pointerEvent, x,y: " + x + "," + y +
    //           " , mask: " + mouse_buttonMask);
    var arr;
    arr = [5];  // msg-type
    arr.push8(mouse_buttonMask);
    arr.push16(x);
    arr.push16(y);
    //Util.Debug("<< pointerEvent");
    return arr;
};

clientCutText = function(text) {
    //Util.Debug(">> clientCutText");
    var arr, i, n;
    arr = [6];     // msg-type
    arr.push8(0);  // padding
    arr.push8(0);  // padding
    arr.push8(0);  // padding
    arr.push32(text.length);
    n = text.length;
    for (i=0; i < n; i+=1) {
        arr.push(text.charCodeAt(i));
    }
    //Util.Debug("<< clientCutText:" + arr);
    return arr;
};



//
// Public API interface functions
//

that.connect = function(host, port, password, path) {
    //Util.Debug(">> connect");

    rfb_host       = host;
    rfb_port       = port;
    rfb_password   = (password !== undefined)   ? password : "";
    rfb_path       = (path !== undefined) ? path : "";

    if ((!rfb_host) || (!rfb_port)) {
        return fail("Must set host and port");
    }

    updateState('connect');
    //Util.Debug("<< connect");

};

that.disconnect = function() {
    //Util.Debug(">> disconnect");
    updateState('disconnect', 'Disconnecting');
    //Util.Debug("<< disconnect");
};

that.sendPassword = function(passwd) {
    rfb_password = passwd;
    rfb_state = "Authentication";
    setTimeout(init_msg, 1);
};

that.sendCtrlAltDel = function() {
    if (rfb_state !== "normal" || conf.view_only) { return false; }
    Util.Info("Sending Ctrl-Alt-Del");
    var arr = [];
    arr = arr.concat(keyEvent(0xFFE3, 1)); // Control
    arr = arr.concat(keyEvent(0xFFE9, 1)); // Alt
    arr = arr.concat(keyEvent(0xFFFF, 1)); // Delete
    arr = arr.concat(keyEvent(0xFFFF, 0)); // Delete
    arr = arr.concat(keyEvent(0xFFE9, 0)); // Alt
    arr = arr.concat(keyEvent(0xFFE3, 0)); // Control
    ws.send(arr);
};

// Send a key press. If 'down' is not specified then send a down key
// followed by an up key.
that.sendKey = function(code, down) {
    if (rfb_state !== "normal" || conf.view_only) { return false; }
    var arr = [];
    if (typeof down !== 'undefined') {
        Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code);
        arr = arr.concat(keyEvent(code, down ? 1 : 0));
    } else {
        Util.Info("Sending key code (down + up): " + code);
        arr = arr.concat(keyEvent(code, 1));
        arr = arr.concat(keyEvent(code, 0));
    }
    ws.send(arr);
};

that.clipboardPasteFrom = function(text) {
    if (rfb_state !== "normal") { return; }
    //Util.Debug(">> clipboardPasteFrom: " + text.substr(0,40) + "...");
    ws.send(clientCutText(text));
    //Util.Debug("<< clipboardPasteFrom");
};

// Override internal functions for testing
that.testMode = function(override_send, data_mode) {
    test_mode = true;
    that.recv_message = ws.testMode(override_send, data_mode);

    checkEvents = function () { /* Stub Out */ };
    that.connect = function(host, port, password) {
            rfb_host = host;
            rfb_port = port;
            rfb_password = password;
            init_vars();
            updateState('ProtocolVersion', "Starting VNC handshake");
        };
};


return constructor();  // Return the public API interface

}  // End of RFB()
