// This module performs low-level WASM manipulations, such as linking several
// modules together and inspecting the debug zone at the bottom of the memory.
// This module is aware of the compilation strategy of KaRaMeL. It can be
// included from either a browser or a shell context, as long as my_print is
// defined in scope.

"use strict";

var my_print, my_random;

// When loaded "as is" via d8 or ch, we inherit the caller's scope. When loaded
// via node, we don't, so we redefine my_print.
if (typeof process !== "undefined") {
  my_print = console.log;
  let crypto = require("crypto");
  my_random = (mem, addr, len) => {
    var m8 = new Uint8Array(mem.buffer);
    crypto.randomFillSync(m8, addr, len);
    return true;
  };
} else {
  my_random = (mem, addr, len) => {
    throw new Error("TODO: random bytes for non-Node platforms");
  };
}

/******************************************************************************/
/* Some printing helpers                                                      */
/******************************************************************************/

function p8(n) {
  return ("0"+Number(n).toString(16)).slice(-2);
}

function hex(m8, start, len) {
  let s = "";
  for (let i = 0; i < len; ++i)
    s += p8(m8[start + i]);
  return s;
}

function hexle(m8, i, m) {
  let buf = "";
  let n = m - 1;
  while (n >= 0) {
    buf += p8(m8[i+n]);
    n--;
  }
  return buf;
}

function hex32(m8, start) {
  return hexle(m8, start, 4);
}

function hex64(m8, start) {
  return hexle(m8, start, 8);
}

function p32(n) {
  return p8((n >>> 24) & 255) + p8((n >>> 16) & 255) + p8((n >>> 8) & 255) + p8(n & 255);
}

// Print the contents of the memory in a format similar to xxd or hexdump.
// @param {WebAssembly.Memory} mem  The memory to print
// @param {Number} size             The amount of memory to print out, in bytes
// @param {Number} ofs              Optional. The offset at which printing
//                                  starts.
function dump(mem, size, ofs) {
  let m8 = new Uint8Array(mem.buffer);
  if (!ofs)
    ofs = 0;
  let i = 0;
  let buf = "";
  while (i < size) {
    buf += p32(ofs + i) + "    ";
    for (let j = 0; j < 32; ++j) {
      buf += p8(m8[ofs+i+j]);
      if (j % 4 == 3)
        buf += " ";
    }
    buf += "    ";
    for (let j = 0; j < 32; ++j) {
      let c = m8[ofs+i+j];
      if (32 <= c && c < 128)
        buf += String.fromCharCode(c);
      else
        buf += ".";
    }
    buf += "\n";
    i += 32;
  }
  my_print(buf);
}

/******************************************************************************/
/* Implementations of the karamel runtime support library                     */
/******************************************************************************/

function stringAtAddr(mem, addr) {
  let m8 = new Uint8Array(mem.buffer);
  let buf = "";
  while (m8[addr] != 0) {
    buf += String.fromCharCode(m8[addr++]);
  }
  return buf;
}

function readLeAtAddr(mem, addr, bytes) {
  let m8 = new Uint8Array(mem.buffer);
  let i = 0;
  while (bytes-- > 0)
    i = (i << 8) + m8[addr + bytes];
  return i;
}

function writeLeAtAddr(mem, addr, n, bytes) {
  let m8 = new Uint8Array(mem.buffer);
  for (let i = 0; i < bytes; ++i) {
    m8[addr + i] = n & 255;
    n >>>= 8;
  }
}

function writeBeAtAddr(mem, addr, n, bytes) {
  let m8 = new Uint8Array(mem.buffer);
  while (bytes > 0) {
    m8[addr + bytes - 1] = n & 255;
    bytes--;
    n >>>= 8;
  }
}

function dummyModule(funcs, globals) {
  let module = {};
  for (let f of funcs)
    module[f] = () => {
      my_print("Not implemented: " + s);
      throw new Error();
    };
  for (let g of globals)
    module[g] = 0;
  return module;
}

let mkWasmSupport = (mem) => ({
  WasmSupport_trap: (s) => {
    dump(mem, 2*1024);
    my_print(stringAtAddr(mem, s));
    my_print("Run-time trap, e.g. zero-sized array or abort. See error message above.");
    throw new Error();
  },
  WasmSupport_malloc: (sz) => {
    // We now provide experimental support for heap allocations. This only
    // caters to a very specific scenario, wherein the entirety of heap
    // allocations performed within a function call are reachable via this
    // function's return value. (In effect, this captures all of the
    // `create_in` functions from HACL.). The caller, upon dealing with such a
    // return value, copies the entire object graph using a combination of:
    // - a JSON description of the layout generated by krml, and
    // - the header tag of heap allocations, which contains their length.
    //
    // The allocator is bare-bones: the final word in memory contains the amount
    // of heap data allocated (default: zero). Allocations are aligned on a
    // 64-bit boundary.
    let m32 = new Uint32Array(mem.buffer);
    let memSize = m32.byteLength;
    let heap_size_addr = m32.byteLength/4 - 1;
    let heap_size = m32[heap_size_addr];
    // Size of stack + size of heap + desired allocation + 4 for allocator bump
    // pointer + 8 for rounding on a 64-bit boundary + 8 for header.
    if (m32[0] + heap_size + sz + 20 > memSize)
      throw new Error("malloc: out of memory (TODO: resize & move)");

    // Header.
    //   First four bytes are allocation size, next four bytes are
    //   reserved. Data follows.
    my_print("malloc: requested " + p32(sz) + " bytes");
    sz += 8;

    // Tentative pointer.
    let p = (memSize - 4) - heap_size - sz;
    // Adjusted to fall on a 64-bit boundary; header is 64 bits so data is
    // aligned, too.
    p -= (p % 8);
    // The heap size doesn't take into account the heap size counter itself.
    let new_heap_size = (memSize - 4) - p;
    m32[heap_size_addr] = new_heap_size;
    // Header contains size of allocation block.
    m32[p/4] = sz;
    my_print("total mem_size: " + p32(memSize) + "; new allocation: " + p32(sz) + " bytes, header: 0x" + p32(p));
    my_print("new heap size: "+p32(new_heap_size));
    return p+8;
  }
});

let mkFStar = () => dummyModule(
  [ "FStar_UInt128_constant_time_carry_ok", "FStar_PropositionalExtensionality_axiom" ],
  [ "FStar_Monotonic_Heap_lemma_mref_injectivity" ]);

let mkLowStar = (mem) => ({
  LowStar_Monotonic_Buffer_is_null: (addr) => (addr == 0)
});

let mkSteelReference = (mem) => ({
  Steel_Reference_is_null: (addr) => (addr == 0)
});

let mkPrims = (mem) => ({
  Prims_op_Addition: (x, y) => { return x + y; }
});

let mkC = (mem) => ({
  srand: () => { throw new Error("todo: srand") },
  rand: () => { throw new Error("todo: rand") },
  exit: (errno) => { throw new Error("Program exited via C.exit [errno="+errno+"]") },
  portable_exit: (errno) => { throw new Error("Program exited via C.portable_exit [errno="+errno+"]") },
  print_bytes: () => { throw new Error("todo: print_bytes") },

  char_uint8: () => { throw new Error("todo: char_uint8") },
  uint8_char: () => { throw new Error("todo: uint8_char") },
  char_of_uint8: (x) => { throw new Error("todo: char_of_uint8") },
  uint8_of_char: (x) => { throw new Error("todo: uint8_of_char") },

  // A Prims_string generates a literal allocated in the data segment;
  // string_of_literal is just a typing trick.
  string_of_literal: (x) => x,
  print_string: (addr) => my_print(stringAtAddr(mem, addr)),

  // Truncated
  clock: () => Date.now(),
});

let mkLibRandomBufferSystem = (mem) => ({
  Lib_RandomBuffer_System_randombytes: (addr, len) => { return my_random(mem, addr, len); }
});

let mkLibMemzero = (mem) => ({
  Lib_Memzero_clear_words_u8: (nwords, addr) => {
    let m8 = new Uint8Array(mem.buffer);
    for (let i = 0; i < nwords; ++i) {
      m8[addr+i] = 0;
    }
  },
  Lib_Memzero_clear_words_u16: (nwords, addr) => {
    let m8 = new Uint8Array(mem.buffer);
    for (let i = 0; i < nwords; ++i) {
      m8[addr+2*i] = 0;
      m8[addr+2*i+1] = 0;
    }
  }
});

let mkLowStarEndianness = (mem) => ({
  htole16: (x) => { throw new Error("todo: htole16") },
  le16toh: (x) => { throw new Error("todo: le16toh") },
  htole32: (x) => { throw new Error("todo: htole32") },
  le32toh: (x) => { throw new Error("todo: le32toh") },
  htole64: (x) => { throw new Error("todo: htole64") },
  le64toh: (x) => { throw new Error("todo: le64toh") },
  htobe16: (x) => { throw new Error("todo: htobe16") },
  be16toh: (x) => { throw new Error("todo: be16toh") },
  htobe32: (x) => { throw new Error("todo: htobe32") },
  be32toh: (x) => { throw new Error("todo: be32toh") },
  htobe64: (x) => { throw new Error("todo: htobe64") },
  be64toh: (x) => { throw new Error("todo: be64toh") },
  index_32_be: (x) => { throw new Error("todo: index_32_be") },
  index_32_le: (x) => { throw new Error("todo: index_32_le") },
  index_64_be: (x) => { throw new Error("todo: index_64_be") },
  index_64_le: (x) => { throw new Error("todo: index_64_le") },
  // all of these: implemented natively in CFlatToWasm
  store16_le: (addr, n) => { throw new Error("todo: store16_le") },
  load16_le: (addr) => { throw new Error("todo: load16_le") },
  store16_be: (addr, n) => { throw new Error("todo: store16_be") },
  load16_be: (addr) => { throw new Error("todo: load16_be") },
  store32_le: (addr, n) => { throw new error("todo: store32_le") },
  store32_be: (addr, n) => { throw new error("todo: store32_be") },
  load32_be: (addr) => { throw new Error("todo: load32_be") },
  store64_le: (addr, n) => { throw new Error("todo: store64_le") },
  load64_le: (addr) => { throw new Error("todo: load64_le") },
  store64_be: (addr, n) => { throw new Error("todo: store64_be") },
  load64_be: (addr) => { throw new Error("todo: load64_be") },
  store128_le: (addr, n) => { throw new Error("todo: store128_le") },
  load128_le: (addr) => { throw new Error("todo: load128_le") },
  store128_be: (addr, n) => { throw new Error("todo: store128_be") },
  load128_be: (addr) => { throw new Error("todo: load128_be") },
  // this definition works for both C.Endianness and LowStar.Endianness
  store16_le_i: (addr, n) => { throw new Error("todo_i: store16_le") },
  load16_le_i: (addr) => { throw new Error("todo_i: load16_le") },
  store16_be_i: (addr, n) => { throw new Error("todo_i: store16_be") },
  load16_be_i: (addr) => { throw new Error("todo_i: load16_be") },
  store32_le_i: (addr, n) => { throw new error("todo_i: store32_le") },
  store32_be_i: (addr, n) => { throw new error("todo_i: store32_be") },
  load32_be_i: (addr) => { throw new Error("todo_i: load32_be") },
  store64_le_i: (addr, n) => { throw new Error("todo_i: store64_le") },
  load64_le_i: (addr) => { throw new Error("todo_i: load64_le") },
  store64_be_i: (addr, n) => { throw new Error("todo_i: store64_be") },
  load64_be_i: (addr) => { throw new Error("todo_i: load64_be") },
  store128_le_i: (addr, n) => { throw new Error("todo_i: store128_le") },
  load128_le_i: (addr) => { throw new Error("todo_i: load128_le") },
  store128_be_i: (addr, n) => { throw new Error("todo_i: store128_be") },
  load128_be_i: (addr) => { throw new Error("todo_i: load128_be") }
});

let mkCNullity = (mem) => ({
  C_Nullity_is_null: () => { throw new Error("todo: is_null") }
});

let mkCString = (mem, prefix) => ({
  [prefix + '_print']: (ptr) => my_print(stringAtAddr(mem, ptr)),
  [prefix + '_of_literal']: (ptr) => { throw new Error("primitive: "+prefix+"_of_literal") },
  [prefix + '_strlen']: (ptr) => {
    let m8 = new Uint8Array(mem.buffer);
    let ofs = 0;
    while (m8[ptr+ofs] != 0)
      ofs++;
    return ofs;
  },
  [prefix + '_memcpy']: (dst, src, n) => {
    let m8 = new Uint8Array(mem.buffer);
    let i = 0;
    while (i < n) {
      m8[dst+i] = m8[src+i];
      i++;
    }
  },
});

function checkEq(mem, name) {
  return (x1, x2) => {
    if (x1 != x2) {
      dump(mem, 2*1024);
      my_print(name + ": equality failure, "+x1+" != "+x2);
      throw new Error();
    }
    return 0;
  };
}

let mkTestLib = (mem, prefix) => ({
  [prefix + '_touch']: () => 0,
  [prefix + '_check8']: checkEq(mem, prefix+"_check8"),
  [prefix + '_check16']: checkEq(mem, prefix+"_check16"),
  [prefix + '_check32']: checkEq(mem, prefix+"_check32"),
  [prefix + '_check64']: checkEq(mem, prefix+"_check64"),
  [prefix + '_checku8']: checkEq(mem, prefix+"_checku8"),
  [prefix + '_checku16']: checkEq(mem, prefix+"_checku16"),
  [prefix + '_checku32']: checkEq(mem, prefix+"_checku32"),
  [prefix + '_checku64']: checkEq(mem, prefix+"_checku64"),
  [prefix + '_check']: (b) => {
    if (!b) {
      dump(mem, 2*1024);
      my_print(prefix+"_check: assertion failure");
      throw new Error();
    }
    return 0;
  },
  [prefix + '_unsafe_malloc']: () => { throw new Error("todo: unsafe_malloc") },
  [prefix + '_perr']: (err) => {
    my_print("Got error code "+err);
    return 0;
  },
  [prefix + '_uint8_p_null']: 0,
  [prefix + '_uint32_p_null']: 0,
  [prefix + '_uint64_p_null']: 0,
  [prefix + '_compare_and_print']: (addr, b1, b2, len) => {
    let m8 = new Uint8Array(mem.buffer);
    let str = stringAtAddr(m8, addr);
    let hex1 = hex(m8, b1, len);
    let hex2 = hex(m8, b2, len);
    my_print("[test] expected output "+str+" is "+hex1);
    my_print("[test] computed output "+str+" is "+hex2);
    for (let i = 0; i < len; ++i) {
      if (m8[b1+i] != m8[b2+i]) {
        my_print("[test] reference "+str+" and expected "+str+" differ at byte "+i);
        my_print("b1="+p32(b1)+",b2="+p32(b2));
        dump(mem, 4*1024);
        throw new Error("test failure");
      }
    }
    my_print("[test] "+str+" is a success\n");
  },
  [prefix + '_print_clock_diff']: (t1, t2) => {
    my_print("[benchmark] took: " + (t2 - t1) + "ms\n");
  },
});

/******************************************************************************/
/* Memory layout                                                              */
/******************************************************************************/

// mem[0..3] = top-of-the-stack pointer (I32);
// mem[4..127] = scratch space for debugging.
// Conventions for debugging and "debugging format" (lolz) are in WasmDebug.ml
const header_size = 128;

// this sets up the base data pointer and the debug entry point for the WASM
// context; WASM-generated code expects to have these defined.
function init() {
  let mem = new WebAssembly.Memory({ initial: 16 });
  let m8 = new Uint8Array(mem.buffer);
  let nest = 0;

  let debug = () => {
    let i = 0;
    let buf = "";
    let string = () => {
      let ptr = m8[i] + (m8[i+1] << 8) + (m8[i+2] << 16) + (m8[i+3] << 24);
      i += 4;
      while (m8[ptr] != 0) {
        buf += String.fromCharCode(m8[ptr]);
        ptr++;
      }
    };
    let int32 = () => {
      buf += "0x";
      buf += hex32(m8, i);
      i += 4;
    };
    let int64 = () => {
      buf += "0x";
      buf += hex64(m8, i);
      i += 8;
    };
    // Dump the stack pointer
    int32();
    buf += " | ";
    buf += " ".repeat(nest);
    while (m8[i] != 0 && i < 128) {
      let c = m8[i];
      i++;
      switch (c) {
        case 1:
          string();
          break;
        case 2:
          int32();
          break;
        case 3:
          int64();
          break;
        case 4:
          nest++;
          break;
        case 5:
          nest--;
          break;
        default:
          my_print(hex(m8, 0, 128));
          throw "unrecognized debug format:\n  buf="+buf+"\n  c=0x"+p8(c)+"\n  i="+i;
      }
      buf += " ";
    }
    my_print(buf);
  };

  // Initialize the highwater mark.
  new Uint32Array(mem.buffer)[0] = header_size;

  // Base stuff that appears as requirement, because they're exposed in TestLib
  // and/or C.
  let imports = {
    Karamel: {
      mem: mem,
      debug: debug,
      data_start: header_size
    },
    WasmSupport: mkWasmSupport(mem),
    FStar: mkFStar(mem),
    LowStar: mkLowStar(mem),
    Steel_Reference: mkSteelReference(mem),
    Prims: mkPrims(mem),
    LowStar_Endianness: mkLowStarEndianness(mem),
    C: mkC(mem),
    C_Compat: mkC(mem),
    C_Endianness: mkLowStarEndianness(mem),
    C_Compat_Endianness: mkLowStarEndianness(mem),
    C_Nullity: mkCNullity(mem),
    C_String: mkCString(mem, 'C_String'),
    C_Compat_String: mkCString(mem, 'C_Compat_String'),
    TestLib: mkTestLib(mem, 'TestLib'),
    TestLib_Compat: mkTestLib(mem, 'TestLib_Compat'),
    Lib_RandomBuffer_System: mkLibRandomBufferSystem(mem),
    Lib_Memzero: mkLibMemzero(mem)
  };
  return imports;
}

// One MUST call this function after loading a module.
function propagate(module_name, imports, instance) {
  my_print("Module", module_name, "successfully loaded");
  my_print("Adding exports into global import table:\n ", Object.keys(instance.exports));
  if (!(module_name in imports))
    imports[module_name] = {};
  for (let o of Object.keys(instance.exports)) {
    imports[module_name][o] = instance.exports[o];
  }
  my_print("This module has a data segment of size: ", instance.exports.data_size);
  imports.Karamel.data_start += instance.exports.data_size;
  my_print("Next data segment will start at: ", imports.Karamel.data_start);
  // Set the highwater mark right after the data segment
  new Uint32Array(imports.Karamel.mem.buffer)[0] = imports.Karamel.data_start;
  my_print();
}

// One MAY only call this function after all modules have been loaded and
// suitable calls to propagate have been performed.
function reserve(mem, size, alignment) {
  if (!alignment)
    alignment = 1;

  let m32 = new Uint32Array(mem.buffer);
  let p = m32[0];
  if ((p % alignment) != 0)
    p += alignment - (p % alignment);
  let curr = m32.byteLength;
  let want = p + size;
  if (want > curr) {
    // The number of WebAssembly pages you want to grow the memory by (each one is 64KiB in size).
    let need = Math.ceil((want - curr) / (64 * 1024)) + 1;
    // One extra page for suitable stack space.
    mem.grow(need);
  }
  m32[0] = want;
  // my_print("Reserved memory area 0x"+p32(p)+"..0x"+p32(m32[0]));
  // NOTE: we don't care for malloc'd data since we expect the caller to copy
  // everything out right after a WASM call and prior to any calls to `reserve`.
  return p;
}

function link(imports, modules) {
  let fold = async (imports, modules) => {
    if (!modules.length)
      return imports;

    let [{ name, buf }, ...ms] = modules;
    // Note: this may throw with:
    //   error: module is not an object or function
    // This merely indicates a missing import. Look at the .wast to figure it
    // out.
    my_print("Attempting to instantiate", name);
    let { module, instance } = await WebAssembly.instantiate(buf, imports);
    propagate(name, imports, instance);
    return fold(imports, ms);
  };

  let i = init ();
  my_print("Custom imports:", imports);
  for (let k in imports)
    i[k] = imports[k](i.Karamel.mem);

  return fold(i, modules);
}

function setMyPrint(f) {
  my_print = f;
  return;
}

if (typeof module !== "undefined")
  module.exports = {
    setMyPrint: setMyPrint,
    link: link,
    reserve: reserve,
    dump: dump,
    hex: hex,
    p32: p32
  };
