/*
 *  Guacamole - Pure JavaScript/HTML VNC Client
 *  Copyright (C) 2010  Michael Jumper
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Affero General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Affero General Public License for more details.
 *
 *  You should have received a copy of the GNU Affero General Public License
 *  along with this program.  If not, see .
 */
function VNCClient(display) {
    var STATE_IDLE          = 0;
    var STATE_CONNECTING    = 1;
    var STATE_WAITING       = 2;
    var STATE_CONNECTED     = 3;
    var STATE_DISCONNECTING = 4;
    var STATE_DISCONNECTED  = 5;
    var currentState = STATE_IDLE;
    var stateChangeHandler = null;
    function setState(state) {
        if (state != currentState) {
            currentState = state;
            if (stateChangeHandler)
                stateChangeHandler(currentState);
        }
    }
    this.setOnStateChangeHandler = function(handler) {
        stateChangeHandler = handler;
    }
    function isConnected() {
        return currentState == STATE_CONNECTED
            || currentState == STATE_WAITING;
    }
    // Layers
    var background = null;
    var cursor = null;
    var cursorImage = null;
    var cursorHotspotX = 0;
    var cursorHotspotY = 0;
    // FIXME: Make object. Clean up.
    var cursorRectX = 0;
    var cursorRectY = 0;
    var cursorRectW = 0;
    var cursorRectH = 0;
    var cursorHidden = 0;
    function redrawCursor() {
        // Hide hardware cursor
        if (cursorHidden == 0) {
            display.className += " hideCursor";
            cursorHidden = 1;
        }
        // Erase old cursor
        cursor.clearRect(cursorRectX, cursorRectY, cursorRectW, cursorRectH);
        // Update rect
        cursorRectX = mouse.getX() - cursorHotspotX;
        cursorRectY = mouse.getY() - cursorHotspotY;
        cursorRectW = cursorImage.width;
        cursorRectH = cursorImage.height;
        // Draw new cursor
        cursor.drawImage(cursorRectX, cursorRectY, cursorImage);
    }
	/*****************************************/
	/*** Keyboard                          ***/
	/*****************************************/
    var keyboard = new GuacamoleKeyboard(document);
    this.disableKeyboard = function() {
        keyboard.setKeyPressedHandler(null);
        keyboard.setKeyReleasedHandler(null);
    };
    this.enableKeyboard = function() {
        keyboard.setKeyPressedHandler(
            function (keysym) {
                sendKeyEvent(1, keysym);
            }
        );
        keyboard.setKeyReleasedHandler(
            function (keysym) {
                sendKeyEvent(0, keysym);
            }
        );
    };
    // Enable keyboard by default
    this.enableKeyboard();
    function sendKeyEvent(pressed, keysym) {
        // Do not send requests if not connected
        if (!isConnected())
            return;
        sendMessage("key:" +  keysym + "," + pressed + ";");
    }
    this.pressKey = function(keysym) {
        sendKeyEvent(1, keysym);
    };
    this.releaseKey = function(keysym) {
        sendKeyEvent(0, keysym);
    };
	/*****************************************/
	/*** Mouse                             ***/
	/*****************************************/
    var mouse = new GuacamoleMouse(display);
    mouse.setButtonPressedHandler(
        function(mouseState) {
            sendMouseState(mouseState);
        }
    );
    mouse.setButtonReleasedHandler(
        function(mouseState) {
            sendMouseState(mouseState);
        }
    );
    mouse.setMovementHandler(
        function(mouseState) {
            // Draw client-side cursor
            if (cursorImage != null) {
                redrawCursor();
            }
            sendMouseState(mouseState);
        }
    );
    function sendMouseState(mouseState) {
        // Do not send requests if not connected
        if (!isConnected())
            return;
        // Build mask
        var buttonMask = 0;
        if (mouseState.getLeft())   buttonMask |= 1;
        if (mouseState.getMiddle()) buttonMask |= 2;
        if (mouseState.getRight())  buttonMask |= 4;
        if (mouseState.getUp())     buttonMask |= 8;
        if (mouseState.getDown())   buttonMask |= 16;
        // Send message
        sendMessage("mouse:" + mouseState.getX() + "," + mouseState.getY() + "," + buttonMask + ";");
    }
    var sendingMessages = 0;
    var outputMessageBuffer = "";
    function sendMessage(message) {
        // Add event to queue, restart send loop if finished.
        outputMessageBuffer += message;
        if (sendingMessages == 0)
            sendPendingMessages();
    }
    function sendPendingMessages() {
        if (outputMessageBuffer.length > 0) {
            sendingMessages = 1;
            var message_xmlhttprequest = new XMLHttpRequest();
            message_xmlhttprequest.open("POST", "inbound");
            message_xmlhttprequest.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            message_xmlhttprequest.setRequestHeader("Content-length", outputMessageBuffer.length);
            // Once response received, send next queued event.
            message_xmlhttprequest.onreadystatechange = function() {
                if (message_xmlhttprequest.readyState == 4)
                    sendPendingMessages();
            }
            message_xmlhttprequest.send(outputMessageBuffer);
            outputMessageBuffer = ""; // Clear buffer
        }
        else
            sendingMessages = 0;
    }
	/*****************************************/
	/*** Clipboard                         ***/
	/*****************************************/
    this.setClipboard = function(data) {
        // Do not send requests if not connected
        if (!isConnected())
            return;
        sendMessage("clipboard:" + escapeGuacamoleString(data) + ";");
    }
    function desaturateFilter(data, width, height) {
        for (var i=0; i= 2 && nextRequest == null)
                nextRequest = makeRequest();
            // Parse stream when data is received and when complete.
            if (xmlhttprequest.readyState == 3 ||
                xmlhttprequest.readyState == 4) {
                // Halt on error during request
                if (xmlhttprequest.status == 0) {
                    showError("Request canceled by browser.");
                    return;
                }
                else if (xmlhttprequest.status != 200) {
                    showError("Error during request (HTTP " + xmlhttprequest.status + "): " + xmlhttprequest.statusText);
                    return;
                }
                var current = xmlhttprequest.responseText;
                var instructionEnd;
                
                while ((instructionEnd = current.indexOf(";", startIndex)) != -1) {
                    // Start next search at next instruction
                    startIndex = instructionEnd+1;
                    var instruction = current.substr(instructionStart,
                            instructionEnd - instructionStart);
                    instructionStart = startIndex;
                    var opcodeEnd = instruction.indexOf(":");
                    var opcode;
                    var parameters;
                    if (opcodeEnd == -1) {
                        opcode = instruction;
                        parameters = new Array();
                    }
                    else {
                        opcode = instruction.substr(0, opcodeEnd);
                        parameters = instruction.substr(opcodeEnd+1).split(",");
                    }
                    // If we're done parsing, handle the next response.
                    if (opcode.length == 0) {
                        if (isConnected()) {
                            delete xmlhttprequest;
                            if (nextRequest)
                                handleResponse(nextRequest);
                        }
                        break;
                    }
                    // Call instruction handler.
                    doInstruction(opcode, parameters);
                }
                // Start search at end of string.
                startIndex = current.length;
                delete instruction;
                delete parameters;
            }
        }
        xmlhttprequest.onreadystatechange = parseResponse;
        parseResponse();
    }
    function makeRequest() {
        // Download self
        var xmlhttprequest = new XMLHttpRequest();
        xmlhttprequest.open("POST", "instructions");
        xmlhttprequest.send(null); 
        return xmlhttprequest;
    }
    function escapeGuacamoleString(str) {
        var escapedString = "";
        for (var i=0; i