// Copyright 2015 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Flags: --expose-wasm --allow-natives-syntax

load("test/mjsunit/wasm/wasm-module-builder.js");

function testCallFFI(func, check) {
  var builder = new WasmModuleBuilder();

  var sig_index = builder.addType(kSig_i_dd);
  builder.addImport("", "func", sig_index);
  builder.addFunction("main", sig_index)
    .addBody([
      kExprGetLocal, 0,            // --
      kExprGetLocal, 1,            // --
      kExprCallFunction, 0  // --
    ])        // --
    .exportFunc();

  var main = builder.instantiate({"": {func: func}}).exports.main;

  for (var i = 0; i < 100000; i += 10003) {
    var a = 22.5 + i, b = 10.5 + i;
    var r = main(a, b);
    if (check) {
      check(r, a, b);
    }
  }
}

var global = (function() { return this; })();
var params = [-99, -99, -99, -99, -99];
var was_called = false;
var length = -1;

function FOREIGN_SUB(a, b) {
//  print("FOREIGN_SUB(" + a + ", " + b + ")");
  was_called = true;
  params[0] = this;
  params[1] = a;
  params[2] = b;
  return (a - b) | 0;
}

function check_FOREIGN_SUB(r, a, b) {
    assertEquals(a - b | 0, r);
    assertTrue(was_called);
//    assertEquals(global, params[0]);  // sloppy mode
    assertEquals(a, params[1]);
    assertEquals(b, params[2]);
    was_called = false;
}

// Test calling a normal JSFunction.
print("JSFunction");
testCallFFI(FOREIGN_SUB, check_FOREIGN_SUB);

// Test calling a proxy.
print("Proxy");
var proxy_sub = new Proxy(FOREIGN_SUB, {});
testCallFFI(proxy_sub, check_FOREIGN_SUB);

// Test calling a bind function.
print("Bind function");
var bind_sub = FOREIGN_SUB.bind();
testCallFFI(bind_sub, check_FOREIGN_SUB);

(function testCallConstructor() {
  print(arguments.callee.name);
  class C {}
  var builder = new WasmModuleBuilder();

  var sig_index = builder.addType(kSig_i_dd);
  builder.addImport("", "func", sig_index);
  builder.addFunction("main", sig_index)
    .addBody([
      kExprGetLocal, 0,            // --
      kExprGetLocal, 1,            // --
      kExprCallFunction, 0  // --
    ])        // --
    .exportFunc();

  let main_for_constructor_test = builder.instantiate({"": {func: C}}).exports.main;

  assertThrows(_ => main_for_constructor_test(12, 43), TypeError);
}) ();

(function testCallConstructorWithSuperClass() {
  print(arguments.callee.name);
  let builder = new WasmModuleBuilder();
  let sig_index = builder.addType(kSig_v_v);
  let func_index = builder.addImport('', 'func', sig_index);
  builder.addExport('exp', func_index);

  class B {}
  class C extends B {
    constructor() {
      super();
    }
  };
  let exports = builder.instantiate({'': {func: C}}).exports;
  assertThrows(_ => exports.exp(), TypeError);
})();

(function test_ffi_call_to_native() {
  print(arguments.callee.name);

  var builder = new WasmModuleBuilder();

  var sig_index = builder.addType(kSig_d_v);
  builder.addImport("", "func", sig_index);
  builder.addFunction("main", sig_index)
    .addBody([
      kExprCallFunction, 0  // --
    ])        // --
    .exportFunc();

  var main = builder.instantiate({"": {func: Object.prototype.toString}}).exports.main;
  // The result of the call to Object.prototype.toString should be
  // [object Undefined]. However, we cannot test for this result because wasm
  // cannot return objects but converts them to float64 in this test.
  assertEquals(NaN, main());
})();

print("Callable JSObject");
testCallFFI(%GetCallable(), function check(r, a, b) {assertEquals(a - b, r);});

function FOREIGN_ABCD(a, b, c, d) {
//  print("FOREIGN_ABCD(" + a + ", " + b + ", " + c + ", " + d + ")");
  was_called = true;
  params[0] = this;
  params[1] = a;
  params[2] = b;
  params[3] = c;
  params[4] = d;
  return (a * b * 6) | 0;
}

function check_FOREIGN_ABCD(r, a, b) {
    assertEquals((a * b * 6) | 0, r);
    assertTrue(was_called);
//    assertEquals(global, params[0]);  // sloppy mode.
    assertEquals(a, params[1]);
    assertEquals(b, params[2]);
    assertEquals(undefined, params[3]);
    assertEquals(undefined, params[4]);
    was_called = false;
}

testCallFFI(FOREIGN_ABCD, check_FOREIGN_ABCD);

function FOREIGN_ARGUMENTS0() {
  print("FOREIGN_ARGUMENTS0");
  was_called = true;
  length = arguments.length;
  for (var i = 0; i < arguments.length; i++) {
    params[i] = arguments[i];
  }
  return (arguments[0] * arguments[1] * 7) | 0;
}

function FOREIGN_ARGUMENTS1(a) {
  print("FOREIGN_ARGUMENTS1", a);
  was_called = true;
  length = arguments.length;
  for (var i = 0; i < arguments.length; i++) {
    params[i] = arguments[i];
  }
  return (arguments[0] * arguments[1] * 7) | 0;
}

function FOREIGN_ARGUMENTS2(a, b) {
  print("FOREIGN_ARGUMENTS2", a, b);
  was_called = true;
  length = arguments.length;
  for (var i = 0; i < arguments.length; i++) {
    params[i] = arguments[i];
  }
  return (a * b * 7) | 0;
}

function FOREIGN_ARGUMENTS3(a, b, c) {
  print("FOREIGN_ARGUMENTS3", a, b, c);
  was_called = true;
  length = arguments.length;
  for (var i = 0; i < arguments.length; i++) {
    params[i] = arguments[i];
  }
  return (a * b * 7) | 0;
}

function FOREIGN_ARGUMENTS4(a, b, c, d) {
  print("FOREIGN_ARGUMENTS4", a, b, c, d);
  was_called = true;
  length = arguments.length;
  for (var i = 0; i < arguments.length; i++) {
    params[i] = arguments[i];
  }
  return (a * b * 7) | 0;
}

function check_FOREIGN_ARGUMENTS(r, a, b) {
  assertEquals((a * b * 7) | 0, r);
  assertTrue(was_called);
  assertEquals(2, length);
  assertEquals(a, params[0]);
  assertEquals(b, params[1]);
  was_called = false;
}

// Check a bunch of uses of the arguments object.
testCallFFI(FOREIGN_ARGUMENTS0, check_FOREIGN_ARGUMENTS);
testCallFFI(FOREIGN_ARGUMENTS1, check_FOREIGN_ARGUMENTS);
testCallFFI(FOREIGN_ARGUMENTS2, check_FOREIGN_ARGUMENTS);
testCallFFI(FOREIGN_ARGUMENTS3, check_FOREIGN_ARGUMENTS);
testCallFFI(FOREIGN_ARGUMENTS4, check_FOREIGN_ARGUMENTS);

function returnValue(val) {
  return function(a, b) {
    print("RETURN_VALUE ", val);
    return val;
  }
}


function checkReturn(expected) {
  return function(r, a, b) { assertEquals(expected, r); }
}

// Check that returning weird values doesn't crash
testCallFFI(returnValue(undefined), checkReturn(0));
testCallFFI(returnValue(null), checkReturn(0));
testCallFFI(returnValue("0"), checkReturn(0));
testCallFFI(returnValue("-77"), checkReturn(-77));

var objWithValueOf = {valueOf: function() { return 198; }}

testCallFFI(returnValue(objWithValueOf), checkReturn(198));

function testCallBinopVoid(type, func, check) {
  var passed_length = -1;
  var passed_a = -1;
  var passed_b = -1;
  var args_a = -1;
  var args_b = -1;

  ffi = {"": {func: function(a, b) {
    passed_length = arguments.length;
    passed_a = a;
    passed_b = b;
    args_a = arguments[0];
    args_b = arguments[1];
  }}};

  var builder = new WasmModuleBuilder();

  builder.addImport("", "func", makeSig_v_xx(type));
  builder.addFunction("main", makeSig_r_xx(kWasmI32, type))
    .addBody([
      kExprGetLocal, 0,            // --
      kExprGetLocal, 1,            // --
      kExprCallFunction, 0,        // --
      kExprI32Const, 33            // --
    ])                             // --
    .exportFunc()

  var main = builder.instantiate(ffi).exports.main;

  print("testCallBinopVoid", type);

  for (var i = 0; i < 100000; i += 10003.1) {
    var a = 22.5 + i, b = 10.5 + i;
    var r = main(a, b);
    assertEquals(33, r);
    assertEquals(2, passed_length);
    var expected_a, expected_b;
    switch (type) {
      case kWasmI32: {
        expected_a = a | 0;
        expected_b = b | 0;
        break;
      }
      case kWasmF32: {
        expected_a = Math.fround(a);
        expected_b = Math.fround(b);
        break;
      }
      case kWasmF64: {
        expected_a = a;
        expected_b = b;
        break;
      }
    }

    assertEquals(expected_a, args_a);
    assertEquals(expected_b, args_b);
    assertEquals(expected_a, passed_a);
    assertEquals(expected_b, passed_b);
  }
}


testCallBinopVoid(kWasmI32);
// TODO testCallBinopVoid(kWasmI64);
testCallBinopVoid(kWasmF32);
testCallBinopVoid(kWasmF64);

(function testCallPrint() {
  var builder = new WasmModuleBuilder();

  builder.addImport("", "print", makeSig_v_x(kWasmI32));
  builder.addImport("", "print", makeSig_v_x(kWasmF64));
  builder.addFunction("main", makeSig_v_x(kWasmF64))
    .addBody([
      kExprI32Const, 37,     // --
      kExprCallFunction, 0,  // --
      kExprGetLocal, 0,      // --
      kExprCallFunction, 1   // --
    ])                       // --
    .exportFunc()

  var main = builder.instantiate({"": {print: print}}).exports.main;
  for (var i = -9; i < 900; i += 6.125) main(i);
})();


(function testImportNumbers() {
  print("TestImportNumbers...");
  var builder = new WasmModuleBuilder();

  builder.addImport("", '0', kSig_v_i);

  builder.instantiate({"": {0: print}});
})();

(function testImportNumbers2() {
  print("TestImportNumbers2...");
  var builder = new WasmModuleBuilder();

  builder.addImport('foo', '0', kSig_v_i);
  builder.addImport('0', 'foo', kSig_v_i);
  builder.addImport('0', '0', kSig_v_i);
  builder.addImport('18', '-3', kSig_v_i);
  builder.addImport('-3', '18', kSig_v_i);

  builder.instantiate({
    foo: {0: print},
    0: {0: print, foo: print},
    18: {'-3': print},
    '-3': {18: print}
  });
})();

(function ImportSymbolAsVoidDoesNotThrow() {
  print("ImportSymbolAsVoidDoesNotThrow...");
  var builder = new WasmModuleBuilder();
  // Return type is void, so there should be no ToNumber conversion.
  var index = builder.addImport("", "func", kSig_v_v);
  builder.addFunction("main", kSig_v_v)
      .addBody([kExprCallFunction, 0])
      .exportFunc();
  var func = () => Symbol();
  var main = builder.instantiate({"": {func: func}}).exports.main;
  main();
})();

(function ToNumberCalledOnImport() {
  var builder = new WasmModuleBuilder();
  // Return type is int, so there should be a ToNumber conversion.
  var index = builder.addImport("", "func", kSig_i_v);
  builder.addFunction("main", kSig_i_v)
      .addBody([kExprCallFunction, 0])
      .exportFunc();
  var num_valueOf = 0;
  function Foo() {}
  Foo.prototype.valueOf = () => ++num_valueOf;
  var func = () => new Foo();
  var main = builder.instantiate({"": {func: func}}).exports.main;
  main();
  assertEquals(1, num_valueOf);
  main();
  assertEquals(2, num_valueOf);
})();

(function ToNumberNotCalledOnVoidImport() {
  var builder = new WasmModuleBuilder();
  // Return type is void, so there should be no ToNumber conversion.
  var index = builder.addImport("", "func", kSig_v_v);
  builder.addFunction("main", kSig_v_v)
      .addBody([kExprCallFunction, 0])
      .exportFunc();
  var num_valueOf = 0;
  function Foo() {}
  Foo.prototype.valueOf = () => ++num_valueOf;
  var func = () => new Foo();
  var main = builder.instantiate({"": {func: func}}).exports.main;
  main();
  main();
  assertEquals(0, num_valueOf);
})();

(function ImportWithCustomGetter() {
  print(arguments.callee.name);
  const builder = new WasmModuleBuilder();
  builder.addImport("import", "func", kSig_v_v);

  const ffi = {};
  Object.defineProperty(ffi, 'import', {
    get: _ => {
      return {func: () => null };
    }
  });

  builder.instantiate(ffi);
})();