//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

/**
 * AutoQUEST - HTML Monitor
 * 
 * Description: This script records the interactions done by an user on an
 * HTML-Website and sends them to a server. It does not register actions on
 * Flash, Java, or other special inputs. This script is tested on Firefox
 * 15.0.1.
 * 
 * To insert it on your HTML-side, you need to write <script
 * language="JavaScript" type="text/javascript" src="autoquest-htmlmonitor.js"></script> in the
 * head and change the src-attribute to the location, you have chosen for this
 * script.
 * 
 * To change the recorded events, edit the action config array. If you want to change
 * the server where the data is send to, rewrite the destination variable. The
 * records are send to the server, JSON-formatted, if there are 10 inputs or the
 * user changes/closes the site.
 * 
 * Authors: Simon Leidenbach, Simon Reuss, Patrick Harms
 * 
 * Version: 0.1
 */

/**
 * the server to send the recorded data to
 */
var autoquestDestination;

/**
 * an ID that is more or less unique for the client
 */
var autoquestClientId;

/**
 * the maximum number of recorded events to be put into one package sent to the server
 */
var autoquestPackageSize = 10;

/**
 * this variable defines the tags for which event handling shall be added, as well as the
 * event handling action to be monitored
 */
var autoquestActionConfig = [
    { "tag": "a", "actions": [ "onclick",
                               "onfocus" ] },
    //{ "tag": "abbr", "actions": [  ] },
    //{ "tag": "address", "actions": [  ] },
    //{ "tag": "applet", "actions": [  ] },
    { "tag": "area", "actions": [ "onclick",
                                  "onfocus" ] },
    //{ "tag": "article", "actions": [  ] },
    //{ "tag": "aside", "actions": [  ] },
    { "tag": "audio", "actions": [ "onplaying",
                                   "onpause",
                                   "ontimeupdate" ] },
    { "tag": "b", "actions": [ "onclick" ] },
    //{ "tag": "bdi", "actions": [  ] },
    //{ "tag": "bdo", "actions": [  ] },
    //{ "tag": "blockquote", "actions": [  ] },
    { "tag": "body", "actions": [ "onclick",
                                  "onkeydown",
                                  "onpagehide",
                                  "onpageshow",
                                  "onscroll",
                                  "onundo" ] },
    { "tag": "button", "actions": [ "onclick",
                                    "onfocus" ] },
    { "tag": "canvas", "actions": [ "onclick" ] },
    //{ "tag": "caption", "actions": [  ] },
    { "tag": "cite", "actions": [ "onclick" ] },
    { "tag": "code", "actions": [ "onclick" ] },
    //{ "tag": "col", "actions": [  ] },
    //{ "tag": "colgroup", "actions": [  ] },
    { "tag": "command", "actions": [ "onclick",
                                     "onfocus" ] },
    //{ "tag": "datalist", "actions": [  ] },
    { "tag": "dd", "actions": [ "onclick" ] },
    { "tag": "del", "actions": [ "onclick" ] },
    //{ "tag": "details", "actions": [  ] },
    { "tag": "dfn", "actions": [ "onclick" ] },
    { "tag": "div", "actions": [ "onclick",
                                 "onscroll" ] },
    //{ "tag": "dl", "actions": [  ] },
    { "tag": "dt", "actions": [ "onclick" ] },
    { "tag": "em", "actions": [ "onclick" ] },
    { "tag": "embed", "actions": [ "onclick" ] },
    //{ "tag": "fieldset", "actions": [  ] },
    //{ "tag": "figcaption", "actions": [  ] },
    //{ "tag": "figure", "actions": [  ] },
    //{ "tag": "footer", "actions": [  ] },
    { "tag": "form", "actions": [ "onreset",
                                  "onsubmit" ] },
    //{ "tag": "header", "actions": [  ] },
    //{ "tag": "hgroup", "actions": [  ] },
    { "tag": "h1", "actions": [ "onclick" ] },
    { "tag": "h2", "actions": [ "onclick" ] },
    { "tag": "h3", "actions": [ "onclick" ] },
    { "tag": "h4", "actions": [ "onclick" ] },
    { "tag": "h5", "actions": [ "onclick" ] },
    { "tag": "h6", "actions": [ "onclick" ] },
    //{ "tag": "hr", "actions": [  ] },
    { "tag": "i", "actions": [ "onclick" ] },
    //{ "tag": "iframe", "actions": [  ] },
    { "tag": "img", "actions": [ "onabort",
                                 "onclick" ] },
    { "tag": "input_button", "actions": [ "onclick",
                                          "onfocus" ] },
    { "tag": "input_checkbox", "actions": [ "onchange",
                                            "onclick",
                                            "onfocus" ] },
    { "tag": "input_color", "actions": [ "onchange",
                                         "onclick",
                                         "onfocus",
                                         "onselect" ] },
    { "tag": "input_date", "actions": [ "onchange",
                                        "onclick",
                                        "onfocus",
                                        "onselect" ] },
    { "tag": "input_datetime", "actions": [ "onchange",
                                            "onclick",
                                            "onfocus",
                                            "onselect" ] },
    { "tag": "input_datetime-local", "actions": [ "onchange",
                                                  "onclick",
                                                  "onfocus",
                                                  "onselect" ] },
    { "tag": "input_email", "actions": [ "onchange",
                                         "onclick",
                                         "onfocus",
                                         "onselect" ] },
    { "tag": "input_file", "actions": [ "onchange",
                                        "onclick",
                                        "onfocus",
                                        "onselect" ] },
    { "tag": "input_image", "actions": [ "onclick",
                                         "onfocus" ] },
    { "tag": "input_month", "actions": [ "onchange",
                                         "onclick",
                                         "onfocus",
                                         "onselect" ] },
    { "tag": "input_number", "actions": [ "onchange",
                                          "onclick",
                                          "onfocus",
                                          "onselect" ] },
    { "tag": "input_password", "actions": [ "onchange",
                                            "onclick",
                                            "onfocus" ] },
    { "tag": "input_radio", "actions": [ "onchange",
                                         "onclick",
                                         "onfocus" ] },
    { "tag": "input_range", "actions": [ "onchange",
                                         "onclick",
                                         "onfocus",
                                         "onselect" ] },
    { "tag": "input_reset", "actions": [ "onclick",
                                         "onfocus" ] },
    { "tag": "input_search", "actions": [ "onchange",
                                          "onclick",
                                          "onfocus",
                                          "onselect" ] },
    { "tag": "input_submit", "actions": [ "onclick",
                                          "onfocus" ] },
    { "tag": "input_tel", "actions": [ "onchange",
                                       "onclick",
                                       "onfocus",
                                       "onselect" ] },
    { "tag": "input_text", "actions": [ "onchange",
                                        "onclick",
                                        "onfocus",
                                        "onselect" ] },
    { "tag": "input_time", "actions": [ "onchange",
                                        "onclick",
                                        "onfocus",
                                        "onselect" ] },
    { "tag": "input_url", "actions": [ "onchange",
                                       "onclick",
                                       "onfocus",
                                       "onselect" ] },
    { "tag": "input_week", "actions": [ "onchange",
                                        "onclick",
                                        "onfocus",
                                        "onselect" ] },
    { "tag": "input", "actions": [ "onchange",
                                   "onclick",
                                   "onfocus" ] },
    { "tag": "ins", "actions": [ "onclick" ] },
    { "tag": "kbd", "actions": [ "onclick" ] },
    { "tag": "keygen", "actions": [ "onchange",
                                    "onfocus" ] },
    //{ "tag": "label", "actions": [  ] },
    //{ "tag": "legend", "actions": [  ] },
    { "tag": "li", "actions": [ "onclick" ] },
    //{ "tag": "map", "actions": [  ] },
    { "tag": "mark", "actions": [ "onclick" ] },
    { "tag": "menu", "actions": [ "onclick" ] },
    { "tag": "meter", "actions": [ "onclick" ] },
    //{ "tag": "nav", "actions": [  ] },
    //{ "tag": "noscript", "actions": [  ] },
    { "tag": "object", "actions": [ "onclick" ] },
    //{ "tag": "ol", "actions": [  ] },
    //{ "tag": "optgroup", "actions": [  ] },
    //{ "tag": "option", "actions": [  ] },
    { "tag": "output", "actions": [ "onclick" ] },
    { "tag": "p", "actions": [ "onclick" ] },
    //{ "tag": "param", "actions": [  ] },
    //{ "tag": "pre", "actions": [  ] },
    { "tag": "progress", "actions": [ "onclick" ] },
    { "tag": "q", "actions": [ "onclick" ] },
    //{ "tag": "rp", "actions": [  ] },
    //{ "tag": "rt", "actions": [  ] },
    //{ "tag": "ruby", "actions": [  ] },
    { "tag": "s", "actions": [ "onclick" ] },
    { "tag": "samp", "actions": [ "onclick" ] },
    //{ "tag": "section", "actions": [  ] },
    { "tag": "select", "actions": [ "onchange",
                                    "onfocus" ] },
    { "tag": "small", "actions": [ "onclick" ] },
    //{ "tag": "source", "actions": [  ] },
    { "tag": "span", "actions": [ "onclick" ] },
    { "tag": "strong", "actions": [ "onclick" ] },
    //{ "tag": "sub", "actions": [  ] },
    //{ "tag": "summary", "actions": [  ] },
    //{ "tag": "sup", "actions": [  ] },
    //{ "tag": "table", "actions": [  ] },
    //{ "tag": "tbody", "actions": [  ] },
    { "tag": "td", "actions": [ "onclick" ] },
    { "tag": "textarea", "actions": [ "onchange",
                                      "onfocus",
                                      "onselect" ] },
    //{ "tag": "tfoot", "actions": [  ] },
    { "tag": "th", "actions": [ "onclick" ] },
    //{ "tag": "thead", "actions": [  ] },
    { "tag": "time", "actions": [ "onclick" ] },
    //{ "tag": "tr", "actions": [  ] },
    //{ "tag": "track", "actions": [  ] },
    { "tag": "u", "actions": [ "onclick" ] },
    //{ "tag": "ul", "actions": [  ] },
    { "tag": "var", "actions": [ "onclick" ] },
    { "tag": "video", "actions": [ "onplaying",
                                   "onpause",
                                   "ontimeupdate" ] }
    //{ "tag": "wbr", "actions": [  ] },
];

/**
 * a possibility to trace, what is going on
 */
var autoquestDoLog = false;

/**
 * stores the structure of the GUI of the current page
 */
var autoquestGUIModel;

/**
 * stores events, which were recorded but not sent to the server yet
 */
var autoquestRecordedEvents = [];

/**
 * stores the interval for sending data of inactive browser windows
 */
var autoquestSendInterval;

/**
 * automatically executed to initialize the event handling
 */
(function() {
    initEventHandling();
}());


/**
 * initializes the event handling after the document is loaded completely
 */
function initEventHandling() {
    if (document.body) {
        if (document.readyState !== "complete") {
            // if the document is not loaded yet, try to add further event handling later
            setTimeout(initEventHandling, 200);
        }
        else if (!autoquestSendInterval) {
            log("adding event handling attributes");
            determineDestination();
            autoquestGUIModel =
                addEventHandlingAndGetJSONRepresentation(document.documentElement, "");
            
            addDefaultEventHandling();
            
            // recall sending data each 100 seconds to ensure, that for browser windows staying
            // open the data will be send, as well.
            autoquestSendInterval = setTimeout(sendRequest, 100000);
        }
    }
    else {
        setTimeout(initEventHandling, 200);
    }         
}

/**
 * traverses the DOM-structure of the HTML-site and determines the URL of this script. Based on
 * this URL, it calculates the destination to which the traced interactions must be sent
 */
function determineDestination() {
    var scriptElements = document.getElementsByTagName("script");
    var i;
    var index;
    
    for (i = 0; i < scriptElements.length; i++) {
        if ((scriptElements[i].type === "text/javascript") && (scriptElements[i].src)) {
            index = scriptElements[i].src.lastIndexOf("script/autoquest-htmlmonitor.js");
            if (index > -1) {
                autoquestDestination = scriptElements[i].src.substring(0, index - 1);
                log("using destination " + autoquestDestination);
            }
        }
    }
}

/**
 * traverses the DOM-structure of the HTML-site and adds event handling attributes to each
 * relevant node. Furthermore returns a JSON representation of the node including the children
 * 
 * @param node       the node of the DOM structure that shall be adapted and whose children shall
 *                   be traversed
 * @param parentPath the path to the parent node of the provided node within the DOM-structure of
 *                   the HTML-site
 */
function addEventHandlingAndGetJSONRepresentation(node, parentPath) {
    var nodePath;
    var i;
    var jsonRepresentation = null;
    var childRepresentation;
    var childRepresentations = null;
    
    if (node.nodeType === Node.ELEMENT_NODE) {
        jsonRepresentation = "{\"tagName\":\"" + getTagName(node) + "\",";
        
        if ((node.id) && (node.id !== "")) {
            jsonRepresentation += "\"htmlId\":\"" + node.id + "\"";
        }
        else {
            jsonRepresentation += "\"index\":\"" + getNodeIndex(node) + "\"";
        }
        
        addEventHandling(node, parentPath);
        
        if (node.childNodes.length > 0) {
            nodePath = getNodePath(node, parentPath);
            
            for (i = 0; i < node.childNodes.length; i++) {
                childRepresentation =
                    addEventHandlingAndGetJSONRepresentation(node.childNodes[i], nodePath);
                
                if (childRepresentation) {
                    if (!childRepresentations) {
                        childRepresentations = childRepresentation;
                    }
                    else {
                        childRepresentations += "," + childRepresentation;
                    }
                }
            }

            if (childRepresentations) {
                jsonRepresentation += ",\"children\":[" + childRepresentations + "]";
            }
        }
        
        jsonRepresentation += "}";
    }
    
    return jsonRepresentation;
}

/**
 * adds event handling functionality to the provided node. Calls
 * {@link #addEventHandlingWithJQuery(node,parentPath)} or
 * {@link #addEventHandlingWithoutJQuery(node,parentPath)} depending on the fact if jQuery is
 * available and must therefore be used, or not.
 * 
 * @param node       the node of the DOM structure that shall be equipped with event handling
 * @param parentPath the path to the parent node of the provided node within the DOM-structure of
 *                   the HTML-site
 */
function addEventHandling(node, parentPath) {
    if (typeof jQuery === 'undefined') {
        addEventHandlingWithoutJQuery(node, parentPath);
    }
    else {
        addEventHandlingWithJQuery(node, parentPath);
    }
}

/**
 * adds event handling functionality to the provided node using onxxx attributes
 * 
 * @param node       the node of the DOM structure that shall be equipped with event handling
 * @param parentPath the path to the parent node of the provided node within the DOM-structure of
 *                   the HTML-site
 */
function addEventHandlingWithoutJQuery(node, parentPath) {
    var nodePath = getNodePath(node, parentPath);
    var tagName = getTagName(node);
    var i;
    var k;
    
    for (i = 0; i < autoquestActionConfig.length; i++) {
        if (tagName === autoquestActionConfig[i].tag.toLowerCase()) {
            for (k = 0; k < autoquestActionConfig[i].actions.length; k++) {
                adaptEventHandlingAttribute(node, nodePath, autoquestActionConfig[i].actions[k]);
            }
        }
    }
}

/**
 * adds event handling functionality to the provided node using jQuery attributes. If the node
 * already used onxxx attributes, these are extended instead of using jQuery.
 * 
 * @param node       the node of the DOM structure that shall be equipped with event handling
 * @param parentPath the path to the parent node of the provided node within the DOM-structure of
 *                   the HTML-site
 */
function addEventHandlingWithJQuery(node, parentPath) {
    var nodePath = getNodePath(node, parentPath);
    var tagName = getTagName(node);
    var action;
    var i;
    var k;
    
    for (i = 0; i < autoquestActionConfig.length; i++) {
        if (tagName === autoquestActionConfig[i].tag.toLowerCase()) {
            for (k = 0; k < autoquestActionConfig[i].actions.length; k++) {
                action = autoquestActionConfig[i].actions[k];
                if (jQuery(node).attr(action)) {
                    // if there is an event handling attribute although jquery is present
                    // edit this attribute accordingly
                    adaptEventHandlingAttribute(node, nodePath, action);
                }
                else {
                    registerEventHandler(node, nodePath, action);
                }
            }
        }
    }
}

/**
 * adapts the event handling attributed provided by the action parameter so that it calls
 * the {@link #handleEvent(node, action, path, event)} function in the case the event occurs.
 * Either the method creates an appropriate onxxx attribute on the node if there is none, or it
 * adds a call to the function as first thing called by the onxxx attribute.
 * 
 * @param node     the node of the DOM structure that shall be equipped with event handling
 * @param nodePath the path to the node within the DOM-structure of the HTML-site
 * @param action   the event for which event handling shall be enabled
 */
function adaptEventHandlingAttribute(node, nodePath, action) {
    var value = "handleEvent(this, '" + action + "', '" + nodePath + "', event);";
    var oldValue;
    
    if (!node.getAttribute(action)) {
        node.setAttribute(action, value);
    }
    else {
        oldValue = node.getAttribute(action);
        if (oldValue.indexOf(value) < 0) {
            node.setAttribute(action, value + ' ' + oldValue);
        }
    }
}

/**
 * registers an event handler using jQuery for the provided action on the given object so that it
 * calls the {@link #handleJQueryEvent(event)} function in the case the event occurs.
 * 
 * @param node     the node of the DOM structure that shall be equipped with event handling
 * @param nodePath the path to the node within the DOM-structure of the HTML-site
 * @param action   the event for which event handling shall be enabled
 */
function registerEventHandler(node, nodePath, action) {
    var parameters = { action : action, path : nodePath};
    
    if ((action === "onscroll") && (node === document.body)) {
        node = window;
    }
    
    if (jQuery(node).on) {
        jQuery(node).on(action.substring(2), parameters, handleJQueryEvent);
    }
    else {
        jQuery(node).bind(action.substring(2), parameters, handleJQueryEvent);
    }
}

/**
 * adds default event handling functionality for receiving load, unload, and other document
 * relevant events. The registration for events is done depending on the availability of jQuery.
 * If jQuery is available and must therefore be used, then the registration is done on the window
 * object. Otherwise, the appropriate attributes of the document's body tag are changed
 */
function addDefaultEventHandling() {
    var body;

    if (typeof jQuery === 'undefined') {
        body = document.getElementsByTagName("body").item(0);
        adaptEventHandlingAttribute(body, "/html[0]/body[0]", "onbeforeunload");
        adaptEventHandlingAttribute(body, "/html[0]/body[0]", "onload");
        adaptEventHandlingAttribute(body, "/html[0]/body[0]", "onunload");
        //adaptEventHandlingAttribute(body, "/html[0]/body[0]", "onerror");
    }
    else {
        registerEventHandler(window, "/html[0]/body[0]", "onbeforeunload");
        registerEventHandler(window, "/html[0]/body[0]", "onload");
        registerEventHandler(window, "/html[0]/body[0]", "onunload");
        //registerEventHandler(body, "/html[0]/body[0]", "onerror");
    }
}

/**
 * generates a path through the DOM-structure of the HTML-site depending on a node and the path
 * to its parent node. The result is the parent path plus a path element for the provided node.
 * The first part of the path element generated for the node is the tag name returned by
 * {@link #getTagName(node)}. If the node has an id, it becomes the second part of the path
 * element. If the node does not have an id, the method calculates the index of the node within
 * all children of the same type within the parent node. This index becomes then the second part
 * of the path element generated for the node. 
 * 
 * @param node       the node of the DOM structure for which the path shall be created
 * @param parentPath the path to the parent node of the provided node
 * 
 * @returns a path in the DOM-structure of the HTML-site including the parent path an a path
 *          element for the provided node
 */
function getNodePath(node, parentPath) {
    var nodePath = parentPath + "/" + getTagName(node);
    
    if ((node.id) && (node.id !== "")) {
        nodePath += "(htmlId=" + node.id + ")";
    }
    else {
        nodePath += "[" + getNodeIndex(node) + "]";
    }
    
    return nodePath;
}

/**
 * called to handle events caused by the jQuery event handling mechanism. Forwards the event to
 * {@link #handleEvent(node, eventName, nodePath, event)}
 * 
 * @param the event to be handled
 */
function handleJQueryEvent(event) {
    handleEvent(this, event.data.action, event.data.path, event);
}

/**
 * handles an event that happened on a node. This method is called by the event handling attributes
 * of the nodes. These attributes are generated by the
 * {@link #addEventHandlingAttributes(node,parentPath)} function. It creates a new Event object and
 * adds it to the list of <code>autoquestRecordedEvents</code>. If this list achieves the maximum
 * <code>autoquestPackageSize</code> the events in the list are sent to the server asynchronously
 * through calling {@link #sendRequest()}.
 * 
 * @param node      the node that fired the event
 * @param eventName the name of the event, e.g. onscroll
 * @param nodePath  the path to the node in the HTML DOM on which the event occurred
 * @param event     the HTML event that occured
 */
function handleEvent(node, eventName, nodePath, event) {
    var eventType;
    var eventObj = null;
    var tagName;
    var unmonitored;

    if (!autoquestDestination) {
        // do nothing if we have no destination to send data to
        return;
    }
    
    log("handling event " + eventName + " on " + node);
    
    eventType = eventName.toLowerCase();
    
    // filter onkeydown events, which are not of interest
    if ((eventType === "onkeydown") && (node === document.body)) {
        // do not handle key downs on the body which are other than tabs (keycode 9)
        if (event.keyCode !== 9) {
            return;
        }
    }

    // check, if the preceeding event is more concrete, i.e. the new event to be handled is only
    // a propagation through the DOM and needs, therefore, not be handled
    if (autoquestRecordedEvents.length > 0) {
        eventObj = autoquestRecordedEvents[autoquestRecordedEvents.length - 1];

        if ((eventObj.type === eventName) && (eventObj.nodePath.indexOf(nodePath) === 0)) {
            // the event is of the same type.
            if (eventObj.nodePath.length > nodePath.length) {
                // the new event is the same event as the previous one, but fired for the parent
                // GUI element of the previous event. This must not be handled. So ignore it
                log("discarding event " + eventName + " on " + node +
                    " as it is already handled by a child");
                return;
            }
            else if (eventType !== "onscroll") {
                // we have the same event on the same element. If it is an onscroll, we should
                // reuse it. But it is not an onscroll. So we ignore the existing event.
                eventObj = null;
            }
        }
        else {
            // the event is not of an equal type as the previous one. So we will not reuse it
            eventObj = null;
        }
    }
    
    if (!eventObj) {
        // we can not reuse the previous event. So create a new event and add it to the list
        eventObj = new Event(eventType, nodePath);
        log("storing event " + eventName);
        autoquestRecordedEvents.push(eventObj);
    }

    // now add further event parameters
    if ((eventType === "onclick") || (eventType === "ondblclick")) {
        eventObj.setClickCoordinates(getEventCoordinates(event));
    }

    tagName = getTagName(node);
    
    // ensure to not monitor details for GUI elements marked as unmonitored
    if (node.getAttribute && node.getAttribute("class")) {
        unmonitored = node.getAttribute("class").indexOf("autoquest-unmonitored") >= 0;
    }
    else {
        unmonitored = false;
    }
    
    if (!unmonitored && ("input_password" !== tagName)) {
        // add further parameters for GUI elements which are monitored and which are no password
        // fields
        if ((eventType === "onkeypress") ||
            (eventType === "onkeydown") ||
            (eventType === "onkeyup"))
        {
            eventObj.setKey(event.keyCode);
        }
        else if (eventType === "onchange") {
            if ((tagName.indexOf("input") === 0) ||
                (tagName === "textarea") ||
                (tagName === "keygen"))
            {
                eventObj.setSelectedValue(node.value);
            }
            else if (tagName === "select") {
                eventObj.setSelectedValue(node.options.item(node.options.selectedIndex));
            }
        }
    }
    
    if (eventType === "onscroll") {
        eventObj.setScrollPosition(getScrollCoordinates(node));
    }

    // finally send the recorded data
    if (autoquestRecordedEvents.length >= autoquestPackageSize) {
        log("initiating sending events");
        setTimeout(sendRequest, 100);
    }
    else if ((eventType === "onunload") || (eventType === "onbeforeunload")) {
        log("initiating sending events");
        sendRequest();
    }

}

/**
 * determines a tag name of a node. If the node is an input element, the tag name includes the
 * type of element. Otherwise the method simply returns the tag name.
 * 
 * @param node the node for which the name must be determined
 * 
 * @return the name as described
 */
function getTagName(node) {
    var tagName = null;
    
    if (node.tagName) {
        tagName = node.tagName.toLowerCase();
        if ("input" === tagName) {
            if (node.type && (node.type !== "")) {
                tagName += "_" + node.type;
            }
            else {
                tagName += "_text";
            }
        }
    }

    return tagName;
}
    
/**
 * determines the index of a node considering all nodes of the parent having the same name. If the
 * node has no parent, the method returns index 0.
 * 
 * @param node the node for which the index must be determined
 * 
 * @return the index as described
 */
function getNodeIndex(node) {
    var i;
    var index = 0;
    
    if (node.parentNode) {
        for (i = 0; i < node.parentNode.childNodes.length; i++) {
            if (node.parentNode.childNodes[i].tagName === node.tagName) {
                // if === also returns true if the nodes are not identical but only equal,
                // this may fail.
                if (node.parentNode.childNodes[i] === node) {
                    break;
                }
                index++;
            }
        }
        
    }

    return index;
}
    
/**
 * sends the collected data to the server, named in the destination-variable. For this it generates
 * a JSON formatted message and uses Ajax and the <code>autoquestDestination</code> to send it to
 * the server
 */
function sendRequest() {
    var eventList = autoquestRecordedEvents;
    var message;
    var clientId;
    var i = 0;
    var request;
    
    if (eventList.length > 1) {
        log("creating message");
        
        // put the last event into the new list to allow for checks for reoccurence of the same
        // event
        autoquestRecordedEvents = [ eventList.pop() ];
        
        message = "{\"message\":{\"clientInfos\":{";
        
        log("reading client id");
        clientId = getClientId();
        if ((clientId) && (clientId !== "")) {
            message += "\"clientId\":\"" + clientId + "\",";
        }
        
        log("adding other infos");
        message += "\"userAgent\":\"" + navigator.userAgent + "\",";
        message += "\"title\":\"" + document.title + "\",";
        message += "\"url\":\"" + document.URL + "\"},";
        
        message += "\"guiModel\":" + autoquestGUIModel + ",";
        
        message += "\"events\":[";
        
        for (i = 0; i < eventList.length; i++) {
            if (i > 0) {
                message += ",";
            }
            message += eventList[i].toJSON();
        }
        
        message += "]}}";
        
        request = null;
        
        // Mozilla
        if (window.XMLHttpRequest) {
            request = new XMLHttpRequest();
        }
        // IE
        else if (window.ActiveXObject) {
            request = new ActiveXObject("Microsoft.XMLHTTP");
        }
        
        request.open("POST", autoquestDestination, false);
        request.setRequestHeader("Content-Type", "application/json");

        log("sending " + message);
        request.send(message);
    }
}

/**
 * determines the scroll coordinates of the scrolled element
 * 
 * @param node the element that was scrolled
 * 
 * @returns the coordinates of the scrolling as an array with x and y coordinate
 */
function getScrollCoordinates(node) {
    if (node.scrollLeft || node.scrollTop) {
        return [node.scrollLeft, node.scrollTop];
    }
    else if ((node === window) && window.pageYOffset) {
        return [window.pageXOffset, window.pageYOffset];
    }
    else if ((node == document) || (node == document.body) || (node == document.documentElement)) { 
        if (document.body && (document.body.scrollLeft || document.body.scrollTop)) {
            return [document.body.scrollLeft, document.body.scrollTop];
        }
        else if (document.documentElement &&
                 (document.documentElement.scrollLeft || document.documentElement.scrollTop))
        {
            return [document.documentElement.scrollLeft, document.documentElement.scrollTop];
        }
    }

    return [-1, -1];
}

/**
 * determines the coordinates of an onclick or ondblclick event. If the coordinates to not come
 * with the provided event, they are determined based on the surrounding object
 * 
 * @param event the event to extract the coordinates of
 * 
 * @returns the coordinates of the event as an array with x and y coordinate
 */
function getEventCoordinates(event) {
    if (event.layerX) {
        return [event.layerX, event.layerY];
    }
    else if (event.offsetX) {
        return [event.offsetX, event.offsetY];
    }

    var obj = event.target || event.srcElement;
    var objOffset = getPageOffset(obj);

    return [(event.clientX - objOffset[0]), (event.clientY - objOffset[1])];
}

/**
 * determines the page offset of an object using the parent objects offset
 */
function getPageOffset(object) {
    var top = 0;
    var left = 0;
    var obj = object;

    while (obj.offsetParent) {
        left += obj.offsetLeft;
        top += obj.offsetTop;
        obj = obj.offsetParent;
    }

    return [left, top];
}

/**
 * generates a client id based on several information retrieved from the environment. The client
 * id is not always unique
 * 
 * @returns the client id
 */
function getClientId() {
    var clientIdStr;
    var clientId;
    var i = 0;
    
    if (!autoquestClientId) {
        // create something like a more or less unique checksum.
        clientIdStr =
            navigator.appCodeName + navigator.appName + navigator.appVersion +
            navigator.cookieEnabled + navigator.language + navigator.platform +
            navigator.userAgent + navigator.javaEnabled() + window.location.protocol +
            window.location.host + new Date().getTimezoneOffset();

        clientId = clientIdStr.length;

        for (i = 0; i < clientIdStr.length; i++) {
            clientId += clientIdStr.charCodeAt(i);
        }
        
        autoquestClientId = clientId;
    }

    return autoquestClientId;
}

/**
 * performs a simple logging by adding a specific div to the HTML
 * 
 * @param text the text to be logged
 */
function log(text) {
    if (autoquestDoLog) {
        var loggingInfo = document.getElementById("loggingInfoParagraph");
        
        if (!loggingInfo) {
            var div = document.createElement("div");
            div.setAttribute("style", "z-index:1000000; background-color:#afa; " +
                             "border:1px black solid; position:absolute; top:10px; right:10px; " +
                             "width:350px; height:auto; font-size:8pt; font-family:Helvetica");
            
            loggingInfo = document.createElement("div");
            loggingInfo.id = "loggingInfoParagraph";
            div.appendChild(loggingInfo);
            
            var body = document.getElementsByTagName("body")[0];
            if (!body) {
                alert("could not enable logging");
            }
            else {
                body.appendChild(div);
            }
        }
        
        loggingInfo.appendChild(document.createTextNode(text));
        loggingInfo.appendChild(document.createElement("br"));
    }
}

/**
 * this class represents a single event
 * 
 * @param type     the type of the event
 * @param nodePath the path through the HTML DOM to the event target
 */
function Event(type, nodePath) {
    this.type = type;
    this.nodePath = nodePath;
    this.time = new Date().getTime();
    
    this.setClickCoordinates = function(coordinates) {
          this.clickCoordinates = coordinates;
    };
    
    this.setKey = function(key) {
          this.key = key;
    };
    
    this.setSelectedValue = function(value) {
        this.selectedValue = encodeText(value);
    };
  
    this.setScrollPosition = function(scrollPosition) {
        this.scrollPosition = scrollPosition;
    };
    
    this.toJSON = function() {
        var eventInJSON =
            "{\"path\":\"" + this.nodePath + "\",\"time\":" + this.time + ",\"eventType\":\"" +
            this.type + "\"";

        if ((this.clickCoordinates) && (!isNaN(this.clickCoordinates[0]))) {
            eventInJSON += ",\"coordinates\":[" + this.clickCoordinates + "]";
        }

        if (this.key) {
            eventInJSON += ",\"key\":" + this.key;
        }
        
        if (this.selectedValue) {
            eventInJSON += ",\"selectedValue\":\"" + this.selectedValue + "\"";
        }
        
        if ("onscroll" === this.type) {
            eventInJSON += ",\"scrollPosition\":[" + this.scrollPosition + "]";
        }

        eventInJSON += "}";
        
        return eventInJSON;
    };
    
    function encodeText(text) {
        return text.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"");
    };
} 
