import * as std from "qjs:std";
import * as os from "qjs:os";
import * as bjson from "qjs:bjson";

// See quickjs.h
const JS_READ_OBJ_BYTECODE = 1 << 0;
const JS_READ_OBJ_REFERENCE = 1 << 3;
const JS_WRITE_OBJ_BYTECODE = 1 << 0;
const JS_WRITE_OBJ_REFERENCE = 1 << 3;
const JS_WRITE_OBJ_STRIP_SOURCE = 1 << 4;

/**
 * Trailer for standalone binaries. When some code gets bundled with the qjs
 * executable we add a 12 byte trailer. The first 8 bytes are the magic
 * string that helps us understand this is a standalone binary, and the
 * remaining 4 are the offset (from the beginning of the binary) where the
 * bundled data is located.
 *
 * The offset is stored as a 32bit little-endian number.
 */
const Trailer = {
    Magic: 'quickjs2',
    MagicSize: 8,
    DataSize: 4,
    Size: 12
};

function encodeAscii(txt) {
  return new Uint8Array(txt.split('').map(c => c.charCodeAt(0)));
}

function decodeAscii(buf) {
  return Array.from(buf).map(c => String.fromCharCode(c)).join('')
}

export function compileStandalone(inFile, outFile, targetExe) {
  // Step 1: compile the source file to bytecode
  const js = std.loadFile(inFile);

  if (!js) {
    throw new Error(`failed to open ${inFile}`);
  }

  const code = std.evalScript(js, {
    compile_only: true,
    compile_module: true
  });
  const bytecode = new Uint8Array(bjson.write(code, JS_WRITE_OBJ_BYTECODE | JS_WRITE_OBJ_REFERENCE | JS_WRITE_OBJ_STRIP_SOURCE));

  // Step 2: copy the bytecode to the end of the executable and add a marker.
  const exeFileName = targetExe ?? os.exePath() ?? globalThis.argv0;
  const exe = std.loadFile(exeFileName, { binary: true });

  if (!exe) {
    throw new Error(`failed to open executable: ${exeFileName}`);
  }

  const exeSize = exe.length;
  const newBuffer = exe.buffer.transfer(exeSize + bytecode.length + Trailer.Size);
  const newExe = new Uint8Array(newBuffer);

  newExe.set(bytecode, exeSize);
  newExe.set(encodeAscii(Trailer.Magic), exeSize + bytecode.length);

  const dw = new DataView(newBuffer, exeSize + bytecode.length + Trailer.MagicSize, Trailer.DataSize);

  dw.setUint32(0, exeSize, true /* little-endian */);

  // We use os.open() so we can set the permissions mask.
  const newFd = os.open(outFile, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o755);

  if (newFd < 0) {
    throw new Error(`failed to create ${outFile}`);
  }
  if (os.write(newFd, newBuffer, 0, newBuffer.byteLength) < 0) {
    os.close(newFd);
    throw new Error(`failed to write to output file`);
  }
  os.close(newFd);
}

export function runStandalone() {
  const file = os.exePath() ?? globalThis.argv0;
  const exe = std.open(file, 'rb');

  if (!exe) {
    throw new Error(`failed to open executable: ${file}`);
  }

  let r = exe.seek(-Trailer.Size, std.SEEK_END);
  if (r < 0) {
    throw new Error(`seek error: ${-r}`);
  }

  const trailer = new Uint8Array(Trailer.Size);

  exe.read(trailer.buffer, 0, Trailer.Size);

  const magic = new Uint8Array(trailer.buffer, 0, Trailer.MagicSize);

  // Shouldn't happen since qjs.c checks for it.
  if (decodeAscii(magic) !== Trailer.Magic) {
    exe.close();
    throw new Error('corrupted binary, magic mismatch');
  }

  const dw = new DataView(trailer.buffer, Trailer.MagicSize, Trailer.DataSize);
  const offset = dw.getUint32(0, true /* little-endian */);
  const bytecode = new Uint8Array(offset - Trailer.Size);

  r = exe.seek(offset, std.SEEK_SET);
  if (r < 0) {
    exe.close();
    throw new Error(`seek error: ${-r}`);
  }

  exe.read(bytecode.buffer, 0, bytecode.length);
  if (exe.error()) {
    exe.close();
    throw new Error('read error');
  }
  exe.close();

  const code = bjson.read(bytecode.buffer, 0, bytecode.length, JS_READ_OBJ_BYTECODE | JS_READ_OBJ_REFERENCE);

  return std.evalScript(code, {
    eval_module: true
  });
}
