// The PasspetSerializer XPCOM component serializes an array of strings.

// Shorthand for getting XPConnect interfaces, classes, objects, and services.
const XPI = Components.interfaces, XPC = Components.classes;
function XPC_(c) { return XPC[c.match(/^@/) ? c : '@mozilla.org/' + c]; }
function XPO(c, i) { return XPC_(c) ? XPC_(c).createInstance(i) : null; }
function XPS(c, i) { return XPC_(c) ? XPC_(c).getService(i) : null; }
function XQI(o, i) { return o.QueryInterface(i); }

const XPR = Components.results;
const CID = Components.ID('{b93ab2bc-cfe6-5a3d-9a89-76c143d8ac5d}');
const NAME = 'Passpet Serialization Service';
const CONTRACT = '@passpet.org/serializer;1';

// The module registers and unregisters the PasspetHash component interface.
function NSGetModule(manager, filespec) {
    return new function() {
        this.registerSelf = function(manager, filespec, location, type) {
            manager = XQI(manager, XPI.nsIComponentRegistrar);
            manager.registerFactoryLocation(
                CID, NAME, CONTRACT, filespec, location, type);
        };

        this.unregisterSelf = function(manager, location, type) {
            manager = XQI(manager, XPI.nsIComponentRegistrar);
            manager.unregisterFactoryLocation(CID, location);
        };

        this.canUnload = function(manager) {
            return true;
        };

        this.getClassObject = function(manager, cid, iid) {
            if (!iid.equals(XPI.nsIFactory)) throw XPR.NS_ERROR_NOT_IMPLEMENTED;
            if (!cid.equals(CID)) throw XPR.NS_ERROR_NO_INTERFACE;
            return new function() {
                this.createInstance = function(outer, iid) {
                    if (outer != null) throw XPR.NS_ERROR_NO_AGGREGATION;
                    return XQI(new class(), iid);
                };
            };
        };
    };
};

const class = function() {
    var items = {};
    var keys = [];
    var stale = false;

    this.length getter = function() { if (stale) update(); return keys.length; }
    this.key = function(i) { if (stale) update(); return keys[i]; }
    this.keys = function() { if (stale) update(); return keys.join(' '); }
    this.contains = function(key) { return key in items; }
    this.get = function(key) { return items[key]; }
    this.set = function(key, value) { items[key] = value; stale = true; }
    this.remove = function(key) { delete items[key]; stale = true; }
    this.clear = function() { items = {}; keys = []; }

    function update() {
        keys = [];
        for (var key in items) keys.push(key);
        keys.sort();
        stale = false;
    }

    this.serialize = function() {
        update();
        var data = '';
        for (var i = 0; i < keys.length; i++) {
            var value = items[keys[i]];
            data += keys[i] + '\n' + value.length + '\n' + value + '\n';
        }
        return data;
    }

    this.deserialize = function(data) {
        items = {};
        var pos = 0;
        while (pos < data.length) {
            var newline = data.indexOf('\n', pos);
            var key = data.substring(pos, newline);
            pos = newline + 1;
            newline = data.indexOf('\n', pos);
            var length = data.substring(pos, newline) - 0;
            var start = newline + 1, end = start + length;
            if (end >= data.length || data.charAt(end) != '\n') break;
            items[key] = data.substring(start, end);
            pos = end + 1;
        }
        update();
    }

    const chr = String.fromCharCode;

    // Encode a Unicode string using UTF-8.
    this.toUTF8 = function(chars) {
        var result = '';
        for (var i = 0; i < chars.length; i++) {
            var code = chars.charCodeAt(i);
            if (code < 0x80) result += chr(code);
            else {
                var limit = 0x40, top = 0x7f, chunk = '';
                while (code >= limit) {
                    chunk = chr(0x80 + (code & 0x3f)) + chunk;
                    code >>= 6;
                    limit >>= 1;
                    top >>= 1;
                }
                result += chr((~top & 0xff) + code) + chunk;
            }
        }
        return result;
    }

    // Decode UTF-8 bytes into a Unicode string.
    this.fromUTF8 = function(bytes) {
        var result = '';
        for (var i = 0; i < bytes.length; i++) {
            var code = bytes.charCodeAt(i);
            if (code < 0x80) result += chr(code);
            else {
                var flag = 0x80;
                for (var n = 0; code & flag; n++, flag >>= 1) code &= ~flag;
                for (var j = 1; j < n; j++) {
                    code = (code << 6) + (bytes.charCodeAt(i + j) & 0x3f);
                }
                result += chr(code);
                i += n - 1;
            }
        }
        return result;
    }

    // Encode 8-bit bytes into a string of hexadecimal digits.
    this.toHex = function(bytes) {
        var hex = '', digits = '0123456789abcdef';
        for (var i = 0; i < bytes.length; i++) {
            var b = bytes.charCodeAt(i);
            hex += digits.charAt(b >> 4) + digits.charAt(b & 0xf);
        }
        return hex;
    }

    // Decode a string of hexadecimal digits into 8-bit bytes.
    this.fromHex = function(hex) {
        var bytes = '';
        for (var i = 0; i < hex.length; i+=2) {
            bytes += chr(parseInt(hex.substring(i, i + 2), 16));
        }
        return bytes;
    }

    // This method implements the nsISupports interface.
    this.QueryInterface = function(iid) {
        if (iid.equals(XPI.IPasspetSerializer)) return this;
        if (iid.equals(XPI.nsISupports)) return this;
        throw XPR.NS_ERROR_NO_INTERFACE;
    };
};

