﻿Array.prototype.findIf = function(predicate) {
    for(var i in this) {
        if (predicate(this[i]))
            return i;
    }
    return -1;
};
Array.prototype.map = function(func) {
    var len = this.length;
    var result = [];
    for (var i=0; i<len; i++) {
        result[i] = func(this[i]);
    }
    return result;
};
Array.prototype.appended = function(value) {
    var copy = this.slice(0);
    copy[copy.length] = value;
    return copy;
};

//============================================================================//
//  SERIALIZATION                                                             //
//============================================================================//

function indented(n) {
    var s = '';
    for(var i=0; i<n; i++) s += '    ';
    return s;
}

function escape_string(str) {
    str = str   .replace(   new RegExp('\\\\', 'g') ,   '\\\\'  )
                .replace(   new RegExp('"', 'g')    ,   '\\"'   )
                .replace(   new RegExp('/', 'g')    ,   '\\/'   )
                .replace(   new RegExp('\b', 'g')   ,   '\\b'   )
                .replace(   new RegExp('\f', 'g')   ,   '\\f'   )
                .replace(   new RegExp('\n', 'g')   ,   '\\n'   )
                .replace(   new RegExp('\r', 'g')   ,   '\\r'   )
                .replace(   new RegExp('\t', 'g')   ,   '\\t'   );
    return str;
}

function serializePrimitive(value) {
    if (typeof value == 'string')
        return '"' + escape_string(value) + '"';
    else
        return '' + value;
}

function serializePrimitiveArray(arr) {
    var s = "[ ";
    s += arr.map(serializePrimitive).join(', ');
    s += " ]";
    return s;
}

function serializeArray(arr, seen, indices, depth) {
    seen[seen.length] = {obj: arr, indices: indices};
    
    if (arr.length == 0)
        return '[]';
    
    var result = '[\n';
    for (var i=0; i<arr.length; i++) {
        result += indented(depth + 1);
        result += serializeAny(arr[i], seen, indices.appended(i), depth + 1);
        result += (i == arr.length - 1) ? '' : ', ';
        result += '\n';
    }
    result += indented(depth) + ']';

    return result;
}

function serializeObject(obj, seen, indices, depth) {
    seen[seen.length] = {obj: obj, indices: indices};
    
    var properties = [];
    for (var prop_name in obj) {
        if (typeof obj[i] != 'function')
            properties[properties.length] = prop_name;
    }
    
    if (properties.length == 0)
        return '{}';
    
    var result = '{\n';
    for (var i = 0; i < properties.length; i++) {
        var prop_name = properties[i];
        result += indented(depth+1);
        result += '"' + escape_string(prop_name) + '": ';
        result += serializeAny(obj[prop_name], seen, 
                        indices.appended(prop_name), depth + 1);
        result += (i == properties.length - 1) ? '' : ', ';
        result += '\n';
    }
    result += indented(depth) + '}';
    
    return result;
}


function serializeAny(value, seen, indices, depth) {
    var t = typeof(value);
    var prevIndex;
    
    if (t == 'function') {
        throw new Error("Cannot serialize function. Keys from root: " + 
            serializePrimitiveArray(indices));
    }
    else if (t != 'object' || value == null) {
        return serializePrimitive(value);
    }
    else if ( (prevIndex = seen.findIf
                    ( function(obj) { return obj.obj == value } )
              ) >= 0) {
        return '{ _root_: ' + serializePrimitiveArray(seen[prevIndex].indices) + ' }';
    }
    else if (value.constructor == Array) {
        return serializeArray(value, seen, indices, depth);
    }
    else {
        return serializeObject(value, seen, indices, depth);
    }
};

function serialize(obj) {
   return serializeAny(obj, [], [], 0);
}



//============================================================================//
//  DESERIALIZATION                                                           //
//============================================================================//

function followIndices(obj, indices) {
    for (var i=0; i<indices.length; i++)
        obj = obj[indices[i]];
        
    return obj;
}

function replaceRootRefs(obj, root) {
    if (typeof obj != 'object' || obj == null)
        return;
    
    for (var i in obj) {
        var prop = obj[i];
        if (typeof prop == 'object' && prop != null) {
            if ('_root_' in prop)
                obj[i] = followIndices(root, prop._root_);
            replaceRootRefs(prop, root);
        }
    }
}

function deserialize(str) {
    try{
        var obj = eval( '(' + str + ')' );
        if (str.indexOf('_root_') < 0)
            return obj;
        
        replaceRootRefs(obj, obj);
        return obj;
    }catch(errSer){
        alert('Error');
        alert(str);
    }
}
