const { Instance, Module, LinkError } = WebAssembly;

// Locally-defined globals
assertErrorMessage(() => wasmEvalText(`(module (global))`), SyntaxError, /parsing/);
assertErrorMessage(() => wasmEvalText(`(module (global i32))`), SyntaxError, /parsing/);
assertErrorMessage(() => wasmEvalText(`(module (global (mut i32)))`), SyntaxError, /parsing/);

// Initializer expressions.
wasmFailValidateText(`(module (global i32 (f32.const 13.37)))`, /type mismatch/);
wasmFailValidateText(`(module (global f64 (f32.const 13.37)))`, /type mismatch/);
wasmFailValidateText(`(module (global i32 (i32.add (i32.const 13) (i32.const 37))))`, /failed to read end/);

wasmFailValidateText(`(module (global i32 (global.get 0)))`, /out of range/);
wasmFailValidateText(`(module (global i32 (global.get 1)) (global i32 (i32.const 1)))`, /out of range/);

// Test a well-defined global section.
function testInner(type, initialValue, nextValue, coercion)
{
    var module = wasmEvalText(`(module
        (global (mut ${type}) (${type}.const ${initialValue}))
        (global ${type} (${type}.const ${initialValue}))

        (func $get (result ${type}) (global.get 0))
        (func $set (param ${type}) (global.set 0 (local.get 0)))

        (func $get_cst (result ${type}) (global.get 1))

        (export "get" $get)
        (export "get_cst" $get_cst)

        (export "set" $set)
    )`).exports;

    assertEq(module.get(), coercion(initialValue));
    assertEq(module.set(coercion(nextValue)), undefined);
    assertEq(module.get(), coercion(nextValue));

    assertEq(module.get_cst(), coercion(initialValue));
}

testInner('i32', 13, 37, x => x|0);
testInner('f32', 13.37, 0.1989, Math.fround);
testInner('f64', 13.37, 0.1989, x => +x);

// Semantic errors.
wasmFailValidateText(`(module (global (mut i32) (i32.const 1337)) (func (global.set 1 (i32.const 0))))`, /out of range/);
wasmFailValidateText(`(module (global i32 (i32.const 1337)) (func (global.set 0 (i32.const 0))))`, /can't write an immutable global/);

// Big module with many variables: test that setting one doesn't overwrite the
// other ones.
function get_set(i, type) {
    return `
        (func $get_${i} (result ${type}) (global.get ${i}))
        (func $set_${i} (param ${type}) (global.set ${i} (local.get 0)))
    `;
}

var module = wasmEvalText(`(module
    (global (mut i32) (i32.const 42))
    (global (mut i32) (i32.const 10))
    (global (mut f32) (f32.const 13.37))
    (global (mut f64) (f64.const 13.37))
    (global (mut i32) (i32.const -18))

    ${get_set(0, 'i32')}
    ${get_set(1, 'i32')}
    ${get_set(2, 'f32')}
    ${get_set(3, 'f64')}
    ${get_set(4, 'i32')}

    (export "get0" $get_0) (export "set0" $set_0)
    (export "get1" $get_1) (export "set1" $set_1)
    (export "get2" $get_2) (export "set2" $set_2)
    (export "get3" $get_3) (export "set3" $set_3)
    (export "get4" $get_4) (export "set4" $set_4)
)`).exports;

let values = [42, 10, Math.fround(13.37), 13.37, -18];
let nextValues = [13, 37, Math.fround(-17.89), 9.3, -13];
for (let i = 0; i < 5; i++) {
    assertEq(module[`get${i}`](), values[i]);
    assertEq(module[`set${i}`](nextValues[i]), undefined);
    assertEq(module[`get${i}`](), nextValues[i]);
    for (let j = 0; j < 5; j++) {
        if (i === j)
            continue;
        assertEq(module[`get${j}`](), values[j]);
    }
    assertEq(module[`set${i}`](values[i]), undefined);
    assertEq(module[`get${i}`](), values[i]);
}

// Initializer expressions can also be used in elem section initializers.
wasmFailValidateText(`(module (import "globals" "a" (global f32)) (table 4 funcref) (elem (global.get 0) $f) (func $f))`, /type mismatch/);

module = wasmEvalText(`(module
    (import "globals" "a" (global i32))
    (table (export "tbl") 4 funcref)
    (elem (global.get 0) $f)
    (func $f)
    (export "f" $f)
)`, {
    globals: {
        a: 1
    }
}).exports;
assertEq(module.f, module.tbl.get(1));

// Import/export semantics.
module = wasmEvalText(`(module
 (import $g "globals" "x" (global i32))
 (func $get (result i32) (global.get $g))
 (export "getter" $get)
 (export "value" global 0)
)`, { globals: {x: 42} }).exports;

assertEq(module.getter(), 42);

// assertEq() will not trigger @@toPrimitive, so we must have a cast here.
assertEq(Number(module.value), 42);

// Can only import numbers (no implicit coercions).
module = new Module(wasmTextToBinary(`(module
    (global (import "globs" "i32") i32)
    (global (import "globs" "f32") f32)
    (global (import "globs" "f64") f32)
)`));

const assertLinkFails = (m, imp, err) => {
    assertErrorMessage(() => new Instance(m, imp), LinkError, err);
}

var imp = {
    globs: {
        i32: 0,
        f32: Infinity,
        f64: NaN
    }
};

let i = new Instance(module, imp);

for (let v of [
    null,
    {},
    "42",
    /not a number/,
    false,
    undefined,
    Symbol(),
    { valueOf() { return 42; } }
]) {
    imp.globs.i32 = v;
    assertLinkFails(module, imp, /not a Number/);

    imp.globs.i32 = 0;
    imp.globs.f32 = v;
    assertLinkFails(module, imp, /not a Number/);

    imp.globs.f32 = Math.fround(13.37);
    imp.globs.f64 = v;
    assertLinkFails(module, imp, /not a Number/);

    imp.globs.f64 = 13.37;
}

// Imported globals and locally defined globals use the same index space.
module = wasmEvalText(`(module
 (import "globals" "x" (global i32))
 (global i32 (i32.const 1337))
 (export "imported" global 0)
 (export "defined" global 1)
)`, { globals: {x: 42} }).exports;

assertEq(Number(module.imported), 42);
assertEq(Number(module.defined), 1337);

// Initializer expressions can reference an imported immutable global.
wasmFailValidateText(`(module (global f32 (f32.const 13.37)) (global i32 (global.get 0)))`, /must reference a global immutable import/);
wasmFailValidateText(`(module (global (mut f32) (f32.const 13.37)) (global i32 (global.get 0)))`, /must reference a global immutable import/);
wasmFailValidateText(`(module (global (mut i32) (i32.const 0)) (global i32 (global.get 0)))`, /must reference a global immutable import/);

wasmFailValidateText(`(module (import "globals" "a" (global f32)) (global i32 (global.get 0)))`, /type mismatch/);

function testInitExpr(type, initialValue, nextValue, coercion, assertFunc = assertEq) {
    var module = wasmEvalText(`(module
        (import "globals" "a" (global ${type}))

        (global $glob_mut (mut ${type}) (global.get 0))
        (global $glob_imm ${type} (global.get 0))

        (func $get0 (result ${type}) (global.get 0))

        (func $get1 (result ${type}) (global.get 1))
        (func $set1 (param ${type}) (global.set 1 (local.get 0)))

        (func $get_cst (result ${type}) (global.get 2))

        (export "get0" $get0)
        (export "get1" $get1)
        (export "get_cst" $get_cst)

        (export "set1" $set1)
        (export "global_imm" (global $glob_imm))
    )`, {
        globals: {
            a: coercion(initialValue)
        }
    }).exports;

    assertFunc(module.get0(), coercion(initialValue));
    assertFunc(module.get1(), coercion(initialValue));
    assertFunc(Number(module.global_imm), coercion(initialValue));

    assertEq(module.set1(coercion(nextValue)), undefined);
    assertFunc(module.get1(), coercion(nextValue));
    assertFunc(module.get0(), coercion(initialValue));
    assertFunc(Number(module.global_imm), coercion(initialValue));

    assertFunc(module.get_cst(), coercion(initialValue));
}

testInitExpr('i32', 13, 37, x => x|0);
testInitExpr('f32', 13.37, 0.1989, Math.fround);
testInitExpr('f64', 13.37, 0.1989, x => +x);

// Int64.

// Import and export

// Test inner
var initialValue = '0x123456789abcdef0';
var nextValue = '0x531642753864975F';
wasmAssert(`(module
    (global (mut i64) (i64.const ${initialValue}))
    (global i64 (i64.const ${initialValue}))
    (func $get (result i64) (global.get 0))
    (func $set (param i64) (global.set 0 (local.get 0)))
    (func $get_cst (result i64) (global.get 1))
    (export "get" $get)
    (export "get_cst" $get_cst)
    (export "set" $set)
)`, [
    {type: 'i64', func: '$get', expected: initialValue},
    {type: 'i64', func: '$set', args: [`i64.const ${nextValue}`]},
    {type: 'i64', func: '$get', expected: nextValue},
    {type: 'i64', func: '$get_cst', expected: initialValue},
]);

// Custom NaN.
{
    let dv = new DataView(new ArrayBuffer(8));
    module = wasmEvalText(`(module
     (global $g f64 (f64.const -nan:0xe7ffff1591120))
     (global $h f32 (f32.const -nan:0x651234))
     (export "nan64" (global $g))(export "nan32" (global $h))
    )`, {}).exports;

    dv.setFloat64(0, module.nan64, true);
    assertEq(dv.getUint32(4, true), 0x7ff80000);
    assertEq(dv.getUint32(0, true), 0x00000000);

    dv.setFloat32(0, module.nan32, true);
    assertEq(dv.getUint32(0, true), 0x7fc00000);
}

// WebAssembly.Global
{
    const Global = WebAssembly.Global;

    // These types should work:
    assertEq(new Global({value: "i32"}) instanceof Global, true);
    assertEq(new Global({value: "f32"}) instanceof Global, true);
    assertEq(new Global({value: "f64"}) instanceof Global, true);
    assertEq(new Global({value: "i64"}) instanceof Global, true); // No initial value works

    // Coercion of init value; ".value" accessor
    assertEq((new Global({value: "i32"}, 3.14)).value, 3);
    assertEq((new Global({value: "f32"}, { valueOf: () => 33.5 })).value, 33.5);
    assertEq((new Global({value: "f64"}, "3.25")).value, 3.25);

    // Nothing special about NaN, it coerces just fine
    assertEq((new Global({value: "i32"}, NaN)).value, 0);

    // The default init value is zero.
    assertEq((new Global({value: "i32"})).value, 0);
    assertEq((new Global({value: "f32"})).value, 0);
    assertEq((new Global({value: "f64"})).value, 0);
    let mod = wasmEvalText(`(module
                             (import "" "g" (global i64))
                             (func (export "f") (result i32)
                              (i64.eqz (global.get 0))))`,
                           {"":{g: new Global({value: "i64"})}});
    assertEq(mod.exports.f(), 1);

    {
        // "value" is enumerable
        let x = new Global({value: "i32"});
        let s = "";
        for ( let i in x )
            s = s + i + ",";
        assertEq(s, "valueOf,value,");
    }

    // "value" is defined on the prototype, not on the object
    assertEq("value" in Global.prototype, true);

    // Can't set the value of an immutable global
    assertErrorMessage(() => (new Global({value: "i32"})).value = 10,
                       TypeError,
                       /can't set value of immutable global/);

    {
        // Can set the value of a mutable global
        let g = new Global({value: "i32", mutable: true}, 37);
        g.value = 10;
        assertEq(g.value, 10);
    }

    {
        // Misc internal conversions
        let g = new Global({value: "i32"}, 42);

        // valueOf
        assertEq(g - 5, 37);

        // @@toStringTag
        assertEq(g.toString(), "[object WebAssembly.Global]");
    }

    {
        // An exported global should appear as a Global instance:
        let i = wasmEvalText(`(module (global (export "g") i32 (i32.const 42)))`);

        assertEq(typeof i.exports.g, "object");
        assertEq(i.exports.g instanceof Global, true);

        // An exported global can be imported into another instance even if
        // it is an object:
        let j = wasmEvalText(`(module
                               (global (import "" "g") i32)
                               (func (export "f") (result i32)
                                (global.get 0)))`,
                             { "": { "g": i.exports.g }});

        // And when it is then accessed it has the right value:
        assertEq(j.exports.f(), 42);
    }

    // Identity of Global objects (independent of mutablity).
    {
        // When a global is exported twice, the two objects are the same.
        let i = wasmEvalText(`(module
                               (global i32 (i32.const 0))
                               (export "a" global 0)
                               (export "b" global 0))`);
        assertEq(i.exports.a, i.exports.b);

        // When a global is imported and then exported, the exported object is
        // the same as the imported object.
        let j = wasmEvalText(`(module
                               (import "" "a" (global i32))
                               (export "x" global 0))`,
                             { "": {a: i.exports.a}});

        assertEq(i.exports.a, j.exports.x);

        // When a global is imported twice (ie aliased) and then exported twice,
        // the exported objects are the same, and are also the same as the
        // imported object.
        let k = wasmEvalText(`(module
                               (import "" "a" (global i32))
                               (import "" "b" (global i32))
                               (export "x" global 0)
                               (export "y" global 1))`,
                             { "": {a: i.exports.a,
                                    b: i.exports.a}});

        assertEq(i.exports.a, k.exports.x);
        assertEq(k.exports.x, k.exports.y);
    }

    // Mutability
    {
        let i = wasmEvalText(`(module
                               (global (export "g") (mut i32) (i32.const 37))
                               (func (export "getter") (result i32)
                                (global.get 0))
                               (func (export "setter") (param i32)
                                (global.set 0 (local.get 0))))`);

        let j = wasmEvalText(`(module
                               (import "" "g" (global (mut i32)))
                               (func (export "getter") (result i32)
                                (global.get 0))
                               (func (export "setter") (param i32)
                                (global.set 0 (local.get 0))))`,
                             {"": {g: i.exports.g}});

        // Initial values
        assertEq(i.exports.g.value, 37);
        assertEq(i.exports.getter(), 37);
        assertEq(j.exports.getter(), 37);

        // Set in i, observe everywhere
        i.exports.setter(42);

        assertEq(i.exports.g.value, 42);
        assertEq(i.exports.getter(), 42);
        assertEq(j.exports.getter(), 42);

        // Set in j, observe everywhere
        j.exports.setter(78);

        assertEq(i.exports.g.value, 78);
        assertEq(i.exports.getter(), 78);
        assertEq(j.exports.getter(), 78);

        // Set on global object, observe everywhere
        i.exports.g.value = 197;

        assertEq(i.exports.g.value, 197);
        assertEq(i.exports.getter(), 197);
        assertEq(j.exports.getter(), 197);
    }

    // Mutability of import declaration and imported value have to match
    {
        const mutErr = /imported global mutability mismatch/;
        const i64Err = /cannot pass i64 to or from JS/;

        let m1 = new Module(wasmTextToBinary(`(module
                                               (import "m" "g" (global i32)))`));

        // Mutable Global matched to immutable import
        let gm = new Global({value: "i32", mutable: true}, 42);
        assertErrorMessage(() => new Instance(m1, {m: {g: gm}}),
                           LinkError,
                           mutErr);

        let m2 = new Module(wasmTextToBinary(`(module
                                               (import "m" "g" (global (mut i32))))`));

        // Immutable Global matched to mutable import
        let gi = new Global({value: "i32", mutable: false}, 42);
        assertErrorMessage(() => new Instance(m2, {m: {g: gi}}),
                           LinkError,
                           mutErr);

        // Constant value is the same as immutable Global
        assertErrorMessage(() => new Instance(m2, {m: {g: 42}}),
                           LinkError,
                           mutErr);
    }

    // TEST THIS LAST

    // "value" is deletable
    assertEq(delete Global.prototype.value, true);
    assertEq("value" in Global.prototype, false);

    // ADD NO MORE TESTS HERE!
}

// Standard wat syntax: the parens around the initializer expression are
// optional.
{
    let i1 = wasmEvalText(
        `(module
           (global $g i32 i32.const 37)
           (func (export "f") (result i32) (global.get $g)))`);
    assertEq(i1.exports.f(), 37);

    let i2 = wasmEvalText(
        `(module
           (global $g (mut f64) f64.const 42.0)
           (func (export "f") (result f64) (global.get $g)))`);
    assertEq(i2.exports.f(), 42);

    let i3 = wasmEvalText(
        `(module
           (global $x (import "m" "x") i32)
           (global $g i32 global.get $x)
           (func (export "f") (result i32) (global.get $g)))`,
        {m:{x:86}});
    assertEq(i3.exports.f(), 86);
}
