// |reftest| skip-if(!xulRuntime.shell)
// -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
// Any copyright is dedicated to the Public Domain.
// http://creativecommons.org/licenses/publicdomain/

// Set of properties on a cloned object that are legitimately non-enumerable,
// grouped by object type.
var non_enumerable = { 'Array': [ 'length' ],
                       'String': [ 'length' ] };

// Set of properties on a cloned object that are legitimately non-configurable,
// grouped by object type. The property name '0' stands in for any indexed
// property.
var non_configurable = { 'String': [ 0 ],
                         '(typed array)': [ 0 ] };

// Set of properties on a cloned object that are legitimately non-writable,
// grouped by object type. The property name '0' stands in for any indexed
// property.
var non_writable = { 'String': [ 0 ] };

function classOf(obj) {
    var classString = Object.prototype.toString.call(obj);
    var [ all, classname ] = classString.match(/\[object (\w+)/);
    return classname;
}

function isIndex(p) {
    var u = p >>> 0;
    return ("" + u == p && u != 0xffffffff);
}

function notIndex(p) {
    return !isIndex(p);
}

function tableContains(table, cls, prop) {
    if (isIndex(prop))
        prop = 0;
    if (cls.match(/\wArray$/))
        cls = "(typed array)";
    var exceptionalProps = table[cls] || [];
    return exceptionalProps.indexOf(prop) != -1;
}

function shouldBeConfigurable(cls, prop) {
    return !tableContains(non_configurable, cls, prop);
}

function shouldBeWritable(cls, prop) {
    return !tableContains(non_writable, cls, prop);
}

function ownProperties(obj) {
    return Object.getOwnPropertyNames(obj).
        map(function (p) { return [p, Object.getOwnPropertyDescriptor(obj, p)]; });
}

function isCloneable(pair) {
    return typeof pair[0] === 'string' && pair[1].enumerable;
}

function compareProperties(a, b, stack, path) {
    var ca = classOf(a);

    // 'b', the original object, may have non-enumerable or XMLName properties;
    // ignore them. 'a', the clone, should not have any non-enumerable
    // properties (except .length, if it's an Array or String) or XMLName
    // properties.
    var pb = ownProperties(b).filter(isCloneable);
    var pa = ownProperties(a);
    for (var i = 0; i < pa.length; i++) {
        var propname = pa[i][0];
        assertEq(typeof propname, "string", "clone should not have E4X properties " + path);
        if (!pa[i][1].enumerable) {
            if (tableContains(non_enumerable, ca, propname)) {
                // remove it so that the comparisons below will work
                pa.splice(i, 1);
                i--;
            } else {
                throw new Error("non-enumerable clone property " + uneval(pa[i][0]) + " " + path);
            }
        }
    }

    // Check that, apart from properties whose names are array indexes,
    // the enumerable properties appear in the same order.
    var aNames = pa.map(function (pair) { return pair[1]; }).filter(notIndex);
    var bNames = pa.map(function (pair) { return pair[1]; }).filter(notIndex);
    assertEq(aNames.join(","), bNames.join(","), path);

    // Check that the lists are the same when including array indexes.
    function byName(a, b) { a = a[0]; b = b[0]; return a < b ? -1 : a === b ? 0 : 1; }
    pa.sort(byName);
    pb.sort(byName);
    assertEq(pa.length, pb.length, "should see the same number of properties " + path);
    for (var i = 0; i < pa.length; i++) {
        var aName = pa[i][0];
        var bName = pb[i][0];
        assertEq(aName, bName, path);

        var path2 = isIndex(aName) ? path + "[" + aName + "]" : path + "." + aName;
        var da = pa[i][1];
        var db = pb[i][1];
        assertEq(da.configurable, shouldBeConfigurable(ca, aName), path2);
        assertEq(da.writable, shouldBeWritable(ca, aName), path2);
        assertEq("value" in da, true, path2);
        var va = da.value;
        var vb = b[pb[i][0]];
        stack.push([va, vb, path2]);
    }
}

function isClone(a, b) {
    var stack = [[a, b, 'obj']];
    var memory = new WeakMap();
    var rmemory = new WeakMap();

    while (stack.length > 0) {
        var pair = stack.pop();
        var x = pair[0], y = pair[1], path = pair[2];
        if (typeof x !== "object" || x === null) {
            // x is primitive.
            assertEq(x, y, "equal primitives");
        } else if (x instanceof Date) {
            assertEq(x.getTime(), y.getTime(), "equal times for cloned Dates");
        } else if (memory.has(x)) {
            // x is an object we have seen before in a.
            assertEq(y, memory.get(x), "repeated object the same");
            assertEq(rmemory.get(y), x, "repeated object's clone already seen");
        } else {
            // x is an object we have not seen before.
	    // Check that we have not seen y before either.
            assertEq(rmemory.has(y), false);

            var xcls = classOf(x);
            var ycls = classOf(y);
            assertEq(xcls, ycls, "same [[Class]]");

            // clone objects should have the default prototype of the class
            assertEq(Object.getPrototypeOf(x), this[xcls].prototype);

            compareProperties(x, y, stack, path);

            // Record that we have seen this pair of objects.
            memory.set(x, y);
            rmemory.set(y, x);
        }
    }
    return true;
}

function check(val) {
    var clone = deserialize(serialize(val));
    assertEq(isClone(val, clone), true);
    return clone;
}

// Various recursive objects

// Recursive array.
var a = [];
a[0] = a;
check(a);

// Recursive Object.
var b = {};
b.next = b;
check(b);

// Mutually recursive objects.
var a = [];
var b = {};
var c = {};
a[0] = b;
a[1] = b;
a[2] = b;
b.next = a;
check(a);
check(b);

// A date
check(new Date);

// A recursive object that is very large.
a = [];
b = a;
for (var i = 0; i < 10000; i++) {
    b[0] = {};
    b[1] = [];
    b = b[1];
}
b[0] = {owner: a};
b[1] = [];
check(a);

// Date objects should not be identical even if representing the same date
var ar = [ new Date(1000), new Date(1000) ];
var clone = check(ar);
assertEq(clone[0] === clone[1], false);

// Identity preservation for various types of objects

function checkSimpleIdentity(v)
{
    a = check([ v, v ]);
    assertEq(a[0] === a[1], true);
    return a;
}

var v = new Boolean(true);
checkSimpleIdentity(v);

v = new Number(17);
checkSimpleIdentity(v);

v = new String("yo");
checkSimpleIdentity(v);

v = "fish";
checkSimpleIdentity(v);

v = new Int8Array([ 10, 20 ]);
checkSimpleIdentity(v);

v = new ArrayBuffer(7);
checkSimpleIdentity(v);

v = new Date(1000);
b = [ v, v, { 'date': v } ];
clone = check(b);
assertEq(clone[0] === clone[1], true);
assertEq(clone[0], clone[2]['date']);
assertEq(clone[0] === v, false);

// Reduced and modified from postMessage_structured_clone test
let foo = { };
let baz = { };
let obj = { 'foo': foo,
            'bar': { 'foo': foo },
            'expando': { 'expando': baz },
            'baz': baz };
check(obj);

for (var obj of new getTestContent)
    check(obj);

// Stolen wholesale from postMessage_structured_clone_helper.js
function getTestContent()
{
  yield "hello";
  yield 2+3;
  yield 12;
  yield null;
  yield "complex" + "string";
  yield new Object();
  yield new Date(1306113544);
  yield [1, 2, 3, 4, 5];
  let obj = new Object();
  obj.foo = 3;
  obj.bar = "hi";
  obj.baz = new Date(1306113544);
  obj.boo = obj;
  yield obj;

  let recursiveobj = new Object();
  recursiveobj.a = recursiveobj;
  recursiveobj.foo = new Object();
  recursiveobj.foo.bar = "bar";
  recursiveobj.foo.backref = recursiveobj;
  recursiveobj.foo.baz = 84;
  recursiveobj.foo.backref2 = recursiveobj;
  recursiveobj.bar = new Object();
  recursiveobj.bar.foo = "foo";
  recursiveobj.bar.backref = recursiveobj;
  recursiveobj.bar.baz = new Date(1306113544);
  recursiveobj.bar.backref2 = recursiveobj;
  recursiveobj.expando = recursiveobj;
  yield recursiveobj;

  obj = new Object();
  obj.expando1 = 1;
  obj.foo = new Object();
  obj.foo.bar = 2;
  obj.bar = new Object();
  obj.bar.foo = obj.foo;
  obj.expando = new Object();
  obj.expando.expando = new Object();
  obj.expando.expando.obj = obj;
  obj.expando2 = 4;
  obj.baz = obj.expando.expando;
  obj.blah = obj.bar;
  obj.foo.baz = obj.blah;
  obj.foo.blah = obj.blah;
  yield obj;

  let diamond = new Object();
  obj = new Object();
  obj.foo = "foo";
  obj.bar = 92;
  obj.backref = diamond;
  diamond.ref1 = obj;
  diamond.ref2 = obj;
  yield diamond;

  let doubleref = new Object();
  obj = new Object();
  doubleref.ref1 = obj;
  doubleref.ref2 = obj;
  yield doubleref;
}

reportCompare(0, 0, 'ok');
