/* pandoc.js: JavaScript interface to pandoc.wasm.
   Copyright (c) 2025 Tweag I/O Limited and John MacFarlane. MIT License.

   Interface:

   await convert(options, stdin, files)

   - options is a JavaScript object representing pandoc options: this should
     correspond to the format used in pandoc's default files.
   - stdin is a string or nil
   - files is a JavaScript object whose keys are filenames and whose values
     are the data in the corresponding file, as Blobs.

   The return value is a JavaScript object with 3 properties, stdout,
   stderr, and warnings, all strings. warnings is a JSON-encoded
   version of the warnings produced by pandoc. If the pandoc process
   produces an output file, it will be added to files.

   await query(options)

    - options is a JavaScript object with a 'query' property and in
      some cases a 'format' property. Possible queries include
      'version', 'highlight-styles', 'highlight-languages', 'input-formats',
      'output-formats', 'default-template' (requires 'format'),
      and 'extensions-for-format' (requires 'format').

   The return value is a JavaScript string or in some cases a list
   of strings.
*/

import {
  WASI,
  OpenFile,
  File,
  ConsoleStdout,
  PreopenDirectory,
} from "@bjorn3/browser_wasi_shim";

const args = ["pandoc.wasm", "+RTS", "-H64m", "-RTS"];
const env = [];
const in_file = new File(new Uint8Array(), { readonly: true });
const out_file = new File(new Uint8Array(), { readonly: false });
const err_file = new File(new Uint8Array(), { readonly: false });
const warnings_file = new File(new Uint8Array(), { readonly: false });
const fileSystem = new Map();
const fds = [
  new OpenFile(new File(new Uint8Array(), { readonly: true })),
  ConsoleStdout.lineBuffered((msg) => console.log(`[WASI stdout] ${msg}`)),
  ConsoleStdout.lineBuffered((msg) => console.warn(`[WASI stderr] ${msg}`)),
  new PreopenDirectory("/", fileSystem),
];
const options = { debug: false };
const wasi = new WASI(args, env, fds, options);
const { instance } = await WebAssembly.instantiateStreaming(
  fetch("/convert/wasm/pandoc.wasm"),
  {
    wasi_snapshot_preview1: wasi.wasiImport,
  }
);

wasi.initialize(instance);
instance.exports.__wasm_call_ctors();

function memory_data_view() {
  return new DataView(instance.exports.memory.buffer);
}

const argc_ptr = instance.exports.malloc(4);
memory_data_view().setUint32(argc_ptr, args.length, true);
const argv = instance.exports.malloc(4 * (args.length + 1));
for (let i = 0; i < args.length; ++i) {
  const arg = instance.exports.malloc(args[i].length + 1);
  new TextEncoder().encodeInto(
    args[i],
    new Uint8Array(instance.exports.memory.buffer, arg, args[i].length)
  );
  memory_data_view().setUint8(arg + args[i].length, 0);
  memory_data_view().setUint32(argv + 4 * i, arg, true);
}
memory_data_view().setUint32(argv + 4 * args.length, 0, true);
const argv_ptr = instance.exports.malloc(4);
memory_data_view().setUint32(argv_ptr, argv, true);

instance.exports.hs_init_with_rtsopts(argc_ptr, argv_ptr);

export async function query(options) {
  const opts_str = JSON.stringify(options);
  const opts_bytes = new TextEncoder().encode(opts_str);
  const opts_ptr = instance.exports.malloc(opts_bytes.length);
  new Uint8Array(instance.exports.memory.buffer, opts_ptr, opts_bytes.length)
    .set(opts_bytes);
  // add input files to fileSystem
  fileSystem.clear()
  const out_file = new File(new Uint8Array(), { readonly: false });
  const err_file = new File(new Uint8Array(), { readonly: false });
  fileSystem.set("stdout", out_file);
  fileSystem.set("stderr", err_file);
  instance.exports.query(opts_ptr, opts_bytes.length);

  const err_text = new TextDecoder("utf-8", { fatal: true }).decode(err_file.data);
  if (err_text) console.log(err_text);
  const out_text = new TextDecoder("utf-8", { fatal: true }).decode(out_file.data);
  return JSON.parse(out_text);
}


export async function convert(options, stdin, files) {
  const opts_str = JSON.stringify(options);
  const opts_bytes = new TextEncoder().encode(opts_str);
  const opts_ptr = instance.exports.malloc(opts_bytes.length);
  new Uint8Array(instance.exports.memory.buffer, opts_ptr, opts_bytes.length)
    .set(opts_bytes);
  // add input files to fileSystem
  fileSystem.clear()
  const in_file = new File(new Uint8Array(), { readonly: true });
  const out_file = new File(new Uint8Array(), { readonly: false });
  const err_file = new File(new Uint8Array(), { readonly: false });
  const warnings_file = new File(new Uint8Array(), { readonly: false });
  fileSystem.set("stdin", in_file);
  fileSystem.set("stdout", out_file);
  fileSystem.set("stderr", err_file);
  fileSystem.set("warnings", warnings_file);
  for (const file in files) {
    await addFile(file, files[file], true);
  }
  // add output file if any
  if (options["output-file"]) {
    await addFile(options["output-file"], new Blob(), false);
  }
  // add media file for extracted media
  if (options["extract-media"]) {
    await addFile(options["extract-media"], new Blob(), false);
  }
  if (stdin) {
    in_file.data = new TextEncoder().encode(stdin);
  }
  instance.exports.convert(opts_ptr, opts_bytes.length);

  if (options["output-file"]) {
    files[options["output-file"]] =
      new Blob([fileSystem.get(options["output-file"]).data]);
  }
  if (options["extract-media"]) {
    const mediaFile = fileSystem.get(options["extract-media"]);
    if (mediaFile && mediaFile.data && mediaFile.data.length > 0) {
      files[options["extract-media"]] =
        new Blob([mediaFile.data], { type: 'application/zip' });
    }
  }
  const rawWarnings = new TextDecoder("utf-8", { fatal: true })
    .decode(warnings_file.data);
  let warnings = [];
  if (rawWarnings) {
    warnings = JSON.parse(rawWarnings);
  }
  return {
    stdout: new TextDecoder("utf-8", { fatal: true }).decode(out_file.data),
    stderr: new TextDecoder("utf-8", { fatal: true }).decode(err_file.data),
    warnings: warnings
  };
}

async function addFile(filename, blob, readonly) {
  const buffer = await blob.arrayBuffer();
  const file = new File(new Uint8Array(buffer), { readonly: readonly });
  fileSystem.set(filename, file);
}
