/*
  Copyright (C) 2007 by Domenico De Felice

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License as published by
  the Free Software Foundation; either version 2 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 General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA

  Author: Domenico De Felice, <dfdcod [at] gmail [dot] com>
*/


// ----------------------------------------------------------------------
// XML ISLANDS EXPERIMENTS
// ----------------------------------------------------------------------


// GLOBAL DEFINITIONS
// ----------------------------------------------------------------------

var serializer = new XMLSerializer();
var parser = new DOMParser();
var ns_xhtml = new Namespace('http://www.w3.org/1999/xhtml');
var ns_app = new Namespace(stripUriFragment(window.location.href));

var prefix2nsTable = {
    xhtml: ns_xhtml,
    xul: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
    svg: "http://www.w3.org/2000/svg"
};
var ns2prefixTable = swapKeyItem(prefix2nsTable);

// DISPATCHER INITIALIZATION
// ----------------------------------------------------------------------

const period = 500; // Milliseconds
window.setInterval(function() { dispatchCommands(); }, period);

// NETWORK COMMANDS HANDLING
// ----------------------------------------------------------------------

function execute(command, sender, type, id) {
    var command_name = command.localName();

    // The island the command refers to
    var island = _(unescape(command.@island));

    switch(command_name) {
    case 'set-attr':
        var node = x(island, unescape(command.@node));
        var currentVersion = versionOf(node);
        var newVersion = Number(command.@version);

        if(currentVersion >= newVersion) return;

        node.setAttribute('version', newVersion);
        addPending(node, newVersion);
        node.setAttribute(
                          unescape(command.@attr),
                          unescape(command.@value));
	break;
    case 'insert':
        var parent = x(island, unescape(command.@parent));
        var currentVersion = versionOf(parent);
        var newVersion = Number(command.@version);

        if(currentVersion >= newVersion) return;

        parent.setAttribute('version', newVersion);
        addPending(parent, newVersion);

        var node = nodeFromString(unescape(command.@node), Number(command.@type));

        // command.@sibling contains the XPath to the
        // previousSibling of the node being inserted
        if(command.@sibling == "none") {
            if(parent.firstChild)
                parent.insertBefore(node, parent.firstChild);
            else
                parent.appendChild(node);
        } else {
            var sibling = x(island, unescape(command.@sibling));
            if(sibling.nextSibling)
                parent.insertBefore(node, sibling.nextSibling);
            else
                parent.appendChild(node);
        }

	break;
    case "remove":
        var node = x(island, unescape(command.@node));
        var currentVersion = versionOf(node.parentNode);
        var newVersion = Number(command.@version);

        if(currentVersion >= newVersion) return;

        node.parentNode.setAttribute('version', newVersion);
        addPending(node.parentNode, newVersion);
        node.parentNode.removeChild(node);
        break;
    default:
        throw new Error('Unknown command "' + command_name + '" (received by "' + sender + '").');
	break;
    }
}


// XML ISLAND INITIALIZATION AND BUSINESS LOGIC
// ----------------------------------------------------------------------

function initIsland(island) {
    var islandId, islandNode;

    if (typeof(island) == 'string') {
        islandId = island;
        islandNode = _(islandId);
    } else {
        islandNode = island;
        islandId = islandNode.id;
    }

    islandNode.addEventListener('DOMAttrModified', function(event) {
        if (event.attrName == 'version') return;
        var currentVersion = versionOf(event.target);
        var newVersion = currentVersion+1;

        if(isPending(event.target, currentVersion)) {
            removePending(event.target, currentVersion);
            return;
        }

        event.target.setAttribute('version', newVersion);

        addCommand({
            action: 'set-attr',
            island: islandId,
            node: event.target,
            nodeXPath: getXPath(event.target, islandNode),
            version: newVersion,
            attribute: event.attrName,
            value: event.newValue || ""});
    }, false);
    islandNode.addEventListener('DOMNodeInserted', function(event) {
        var currentVersion = versionOf(event.target.parentNode);
        var newVersion = currentVersion+1;

        if(isPending(event.target.parentNode, currentVersion)) {
            removePending(event.target.parentNode, currentVersion);
            return;
        }

        event.target.parentNode.setAttribute('version', newVersion);

        var previousSibling = event.target.previousSibling;
        addCommand({
            action: 'insert',
            island: islandId,
            parent: event.target.parentNode,
            parentXPath: getXPath(event.target.parentNode, islandNode),
            version: newVersion,
            previousSiblingXPath: previousSibling ? getXPath(previousSibling, islandNode) : "none",
            nodeType: event.target.nodeType,
            serializedNode: serializer.serializeToString(event.target)});
    }, false);
    islandNode.addEventListener('DOMNodeRemoved', function(event) {
        var currentVersion = versionOf(event.target.parentNode);
        var newVersion = currentVersion+1;

        if(isPending(event.target.parentNode, currentVersion)) {
            removePending(event.target.parentNode, currentVersion);
            return;
        }

        event.target.parentNode.setAttribute('version', newVersion);

        addCommand({
            action: 'remove',
            island: islandId,
            version: newVersion,
            node: event.target,
            nodeXPath: getXPath(event.target, islandNode)
        });
    }, false);
}

// Commands
// ----------------------------------------------------------------------

(function (out) {
    var commands = [];

    out.addCommand = function(command) {
        switch (command.action) {
            case 'set-attr':
            for (var i = 0; i < commands.length; i++)
               if(commands[i].action == 'set-attr' &&
                  commands[i].node == command.node &&
                  commands[i].attribute == command.attribute) {
                      commands.splice(i, 1);
                      commands.push(command);
                      return;
                  }

            commands.push(command);
            break;
            case 'insert':
            commands.push(command);
            break;
            case 'remove':
            commands.push(command);
            break;
        }
    }

    out.dispatchCommands = function() {
        if (!commands.length) return;

        var cmds = commands;
        commands = [];

        var payload = <app:x xmlns:app={ns_app} xmlns={ns_app}/>;
        for each (var cmd in cmds) payload.content = new XML(command2xml(cmd));

        remote.send(payload);
    }
})(this);

function command2xml(command) {
    var xml = '';
    switch(command.action) {
    case 'insert':
        xml = "<insert island='" + escape(command.island) + "' " +
            "parent='" + escape(command.parentXPath) + "' " +
            "version='" + escape(command.version) + "' " +
            "sibling='" + escape(command.previousSiblingXPath) + "' " +
            "type='" + escape(command.nodeType) + "' " +
            "node='" + escape(command.serializedNode) + "'/>";
        break;
    case 'remove':
        xml = "<remove island='" + escape(command.island) + "' " +
            "version='" + escape(command.version) + "' " +
            "node='" + escape(command.nodeXPath) + "'/>";
        break;
    case 'set-attr':
        xml = "<set-attr island='" + escape(command.island) + "' " +
            "node='" + escape(command.nodeXPath) + "' " +
            "version='" + escape(command.version) + "' " +
            "attr='" + escape(command.attribute) + "' " +
            "value='" + escape(command.value) + "'/>";
        break;
    }

    return xml;
}

// PENDING EVENTS
// ----------------------------------------------------------------------

(function(out) {
    var pending = {};

    var pendingForNode = function(node) {
        if(!pending[node]) pending[node] = [];
        return pending[node];        
    }

    out.addPending = function(node, version) {
        pendingForNode(node).push(Number(version));
    }

    out.removePending = function(node, version) {
        var arr = pendingForNode(node);
        arr.splice(arr.indexOf(Number(version)), 1);
    }

    out.isPending = function(node, version) {
        return pendingForNode(node).indexOf(Number(version)) != -1;
    }
})(this);


// UTILITIES
// ----------------------------------------------------------------------

function versionOf(node) {
    return Number(node.getAttribute('version') || 1);
}

function prefix2ns(prefix) {
    if(prefix2nsTable[prefix])
        return prefix2nsTable[prefix];

    return ns_xhtml;
}

function ns2prefix(ns) {
    if(ns2prefixTable[ns])
        return ns2prefixTable[ns];

    return 'unknown';
}

function swapKeyItem(hash) {
    var newHash = {};

    for (var key in hash)
        newHash[hash[key]] = key;

    return newHash;
}

function reportError(message) {
    alert("Error: " + message);
}

function x() {
    var contextNode, path;
    if(typeof(arguments[0]) == 'string') {
        contextNode = document;
        path = arguments[0];
    } else {
        contextNode = arguments[0];
        path = arguments[1];
    }

    return document.evaluate(
        path, contextNode, prefix2ns,
        XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue;
}


////////////////////////////////////////////////////////////////////////////
// Original getXPath function from https://bugzilla.mozilla.org/show_bug.cgi?id=208500

function getXPath(node, fromNode) {
    if(!fromNode) fromNode = node.ownerDocument;

    if(node == fromNode)
        // We're done recursing.
        return fromNode.nodeType == Node.DOCUMENT_NODE ? "" : ".";

    var nodeName;
    switch (node.nodeType) {
    case Node.ELEMENT_NODE:
        nodeName = (node.namespaceURI ?
            ns2prefix(node.namespaceURI) + ':' :
            "") + node.nodeName;
        break;
    case Node.ATTRIBUTE_NODE:
        nodeName = "@" + node.nodeName;
        break;
    case Node.TEXT_NODE:
        nodeName = "text()";
        break;
    case Node.PROCESSING_INSTRUCTION_NODE:
        nodeName = "processing-instruction()";
        break;
    case Node.COMMENT_NODE:
        nodeName = "comment()";
        break;
    case Node.DOCUMENT_NODE:
        nodeName = "";
        break;
    case Node.DOCUMENT_FRAGMENT_NODE:
        nodeName = "";
        break;
    case Node.CDATA_SECTION_NODE:
        reportError("Trying to get the XPath for a node of type CDATA_SECTION_NODE; this should never happen.\n");
        nodeName = "";
        break;
    case Node.ENTITY_REFERENCE_NODE:
        reportError("Trying to get the XPath for a node of type ENTITY_REFERENCE_NODE; this should never happen.\n");
        nodeName = "";
        break;
    case Node.ENTITY_NODE:
        reportError("Trying to get the XPath for a node of type ENTITY_NODE; this should never happen.\n");
        nodeName = "";
        break;
    case Node.DOCUMENT_TYPE_NODE:
        reportError("Trying to get the XPath for a node of type DOCUMENT_TYPE_NODE; this should never happen.\n");
        nodeName = "";
        break;
    case Node.NOTATION_NODE:
        reportError("Trying to get the XPath for a node of type NOTATION_NODE; this should never happen.\n");
        nodeName = "";
        break;
    default:
        reportError("Trying to get the XPath for a node of unknown type (" + node.nodeType + "); this should never happen.\n");
        nodeName = "";
        break;
    }
    var nodeIdentifier = getIdentifier(node);
    var pNode = node.parentNode;
    var myIndex = countTwins(node, nodeIdentifier);
    var totalIndex = countTwins(pNode.lastChild, nodeIdentifier);
    var brackets = ( totalIndex > 1 ? "[" + myIndex + "]" : "" );
    var xpath = getXPath(pNode, fromNode) + "/" + nodeName + brackets;
    return xpath;
}

function countTwins(node, identifier) {
    var count = 0;
    while (node) {
        var nodeIdentifier = getIdentifier(node);
        if(nodeIdentifier == identifier) count++;
        node = node.previousSibling;
    }
    return count;
}

function getIdentifier(node) {
    if(node.localName)
        return ( node.namespaceURI ? node.namespaceURI + ":" + node.localName : node.localName );
    else
        return node.nodeName;
}

////////////////////////////////////////////////////////////////////////////

function cloneBlueprint(name) {
    return x('//*[@id="blueprints"]' +
             '//*[@class="' + name + '"]')
        .cloneNode(true);
}

function getElementByAttribute(parent, name, value) {
    for(child = parent.firstChild; child; child = child.nextSibling)
        if(child.getAttribute && child.getAttribute(name) == value)
            return child;

    for(child = parent.firstChild; child; child = child.nextSibling) {
        var matchingChild = getElementByAttribute(child, name, value);
        if(matchingChild)
            return matchingChild;
    }
}

function stripUriFragment(uri) {
    var hashPos = uri.lastIndexOf('#');
    return (hashPos != -1 ?
            uri.slice(0, hashPos) :
            uri);
}

function sendCommand(commandName, attrs, to, id) {
    remote.send(commandFor(commandName, attrs || {}), to, id);
}

function commandFor(commandName, attrs) {
    var payload = <app:x xmlns:app={ns_app} xmlns={ns_app}/>;
    var command;
    if (typeof(commandName) == 'xml')
        command = commandName;
    else if (commandName.match("^<"))
	command = new XML(commandName);
    else
	command = new XML('<' + commandName + '/>');
    for (var attr in attrs) {
	command["@" + attr] = escape(attrs[attr]);
    }
    payload.content = command;
    return payload;
}

function toXML(domElement) {
    return new XML(serializer.serializeToString(domElement));
}

function toDOM(description) {
    return parser.parseFromString(
        (typeof(description) == 'xml' ?
         description.toXMLString() : description),
        'application/xhtml+xml').documentElement;
}

function nodeFromString(node, nodeType) {
    switch(nodeType) {
    case Node.ELEMENT_NODE:
    case Node.DOCUMENT_NODE:
    case Node.DOCUMENT_FRAGMENT_NODE:
    case Node.ATTRIBUTE_NODE:
        return document.importNode(toDOM(node), true);
        break;
    case Node.TEXT_NODE:
        return document.createTextNode(node);
        break;
    case Node.PROCESSING_INSTRUCTION_NODE:
        break;
    case Node.COMMENT_NODE:
        break;
    case Node.CDATA_SECTION_NODE:
        break;
    case Node.ENTITY_REFERENCE_NODE:
        break;
    case Node.ENTITY_NODE:
        break;
    case Node.DOCUMENT_TYPE_NODE:
        break;
    case Node.NOTATION_NODE:
        break;
    default:
        break;
    }
    return undefined;
}

function _(thing) {
    switch(typeof(thing)) {
    case 'string':
        return document.getElementById(thing);
        break;
    case 'xml':
        return document.getElementById(thing.toString());
        break;
    default:
        return thing;
    }
}

function idOf(thing) {
    return (typeof(thing) == 'string' ?
            thing : thing.getAttribute('id'));
}

function getWindowHeight() {
    if (window.self && self.innerHeight) {
	return self.innerHeight;
    }
    if (document.documentElement && document.documentElement.clientHeight) {
	return document.documentElement.clientHeight;
    }
    return 0;
}

function createUUID() {
    return [4, 2, 2, 2, 6].map(function(length) {
        var uuidpart = "";
        for (var i=0; i<length; i++) {
            var uuidchar = parseInt((Math.random() * 256)).toString(16);
            if (uuidchar.length == 1)
                uuidchar = "0" + uuidchar;
            uuidpart += uuidchar;
        }
        return uuidpart;
    }).join('-');
}


// NETWORK WRAPPERS
// ----------------------------------------------------------------------

var remote = {

    // ACTIONS
   
    send: function(command, to, id) {
	var message = <message/>
	message.content = command;
	if (to) {
	    message.@to = "/" + to;
	    message.@type = "chat";
	}
	if (id) {
	    message.@id = id;
	}
	remote.sendMessage(message);
    },

    sendMessage: function(message) {
        _('xmpp-outgoing').textContent = message.toXMLString();
    },

    // REACTIONS

    receivedMessage: function(message) {
	var sender = message.@from.toString();
	sender = sender.slice(sender.lastIndexOf('/')+1)
	var type = message.@type.toString();
	var id = message.@id.toString();

	for each (command in message.ns_app::x.*) {
	    execute(command, sender, type, id);
	}
    }
};

