const domConnectionStatus = document.getElementById("connectionStatus");
const domFirefoxWebSocketBullshitExplainer = document.getElementById("firefoxWebSocketBullshitExplainer");

const domMain = document.getElementsByTagName("main")[0];
const domSummary = {
  stepCount: document.getElementById("summaryStepCount"),
  status: document.getElementById("summaryStatus"),
};
const domButtonRebuild = document.getElementById("buttonRebuild");
const domStepList = document.getElementById("stepList");
let domSteps = [];

let wasm_promise = fetch("main.wasm");
let wasm_exports = null;

const text_decoder = new TextDecoder();
const text_encoder = new TextEncoder();

domButtonRebuild.addEventListener("click", () => wasm_exports.rebuild());

setConnectionStatus("Loading WebAssembly...", false);
WebAssembly.instantiateStreaming(wasm_promise, {
  core: {
    log: function(ptr, len) {
      const msg = decodeString(ptr, len);
      console.log(msg);
    },
    panic: function (ptr, len) {
      const msg = decodeString(ptr, len);
      throw new Error("panic: " + msg);
    },
    timestamp: function () {
      return BigInt(new Date());
    },
    hello: hello,
    updateBuildStatus: updateBuildStatus,
    updateStepStatus: updateStepStatus,
    sendWsMessage: (ptr, len) => ws.send(new Uint8Array(wasm_exports.memory.buffer, ptr, len)),
  },
  fuzz: {
    requestSources: fuzzRequestSources,
    ready: fuzzReady,
    updateStats: fuzzUpdateStats,
    updateEntryPoints: fuzzUpdateEntryPoints,
    updateSource: fuzzUpdateSource,
    updateCoverage: fuzzUpdateCoverage,
  },
  time_report: {
    updateCompile: timeReportUpdateCompile,
    updateGeneric: timeReportUpdateGeneric,
  },
}).then(function(obj) {
  setConnectionStatus("Connecting to WebSocket...", true);
  connectWebSocket();

  wasm_exports = obj.instance.exports;
  window.wasm = obj; // for debugging
});

function connectWebSocket() {
  const host = document.location.host;
  const pathname = document.location.pathname;
  const isHttps = document.location.protocol === 'https:';
  const match = host.match(/^(.+):(\d+)$/);
  const defaultPort = isHttps ? 443 : 80;
  const port = match ? parseInt(match[2], 10) : defaultPort;
  const hostName = match ? match[1] : host;
  const wsProto = isHttps ? "wss:" : "ws:";
  const wsUrl = wsProto + '//' + hostName + ':' + port + pathname;
  ws = new WebSocket(wsUrl);
  ws.binaryType = "arraybuffer";
  ws.addEventListener('message', onWebSocketMessage, false);
  ws.addEventListener('error', onWebSocketClose, false);
  ws.addEventListener('close', onWebSocketClose, false);
  ws.addEventListener('open', onWebSocketOpen, false);
}
function onWebSocketOpen() {
  setConnectionStatus("Waiting for data...", false);
}
function onWebSocketMessage(ev) {
  const jsArray = new Uint8Array(ev.data);
  const ptr = wasm_exports.message_begin(jsArray.length);
  const wasmArray = new Uint8Array(wasm_exports.memory.buffer, ptr, jsArray.length);
  wasmArray.set(jsArray);
  wasm_exports.message_end();
}
function onWebSocketClose() {
  setConnectionStatus("WebSocket connection closed. Re-connecting...", true);
  ws.removeEventListener('message', onWebSocketMessage, false);
  ws.removeEventListener('error', onWebSocketClose, false);
  ws.removeEventListener('close', onWebSocketClose, false);
  ws.removeEventListener('open', onWebSocketOpen, false);
  ws = null;
  setTimeout(connectWebSocket, 1000);
}

function setConnectionStatus(msg, is_websocket_connect) {
  domConnectionStatus.textContent = msg;
  if (msg.length > 0) {
    domConnectionStatus.classList.remove("hidden");
    domMain.classList.add("hidden");
  } else {
    domConnectionStatus.classList.add("hidden");
    domMain.classList.remove("hidden");
  }
  if (is_websocket_connect) {
    domFirefoxWebSocketBullshitExplainer.classList.remove("hidden");
  } else {
    domFirefoxWebSocketBullshitExplainer.classList.add("hidden");
  }
}

function hello(
  steps_len,
  build_status,
  time_report,
) {
  domSummary.stepCount.textContent = steps_len;
  updateBuildStatus(build_status);
  setConnectionStatus("", false);

  {
    let entries = [];
    for (let i = 0; i < steps_len; i += 1) {
      const step_name = unwrapString(wasm_exports.stepName(i));
      const code = document.createElement("code");
      code.textContent = step_name;
      const li = document.createElement("li");
      li.appendChild(code);
      entries.push(li);
    }
    domStepList.replaceChildren(...entries);
    for (let i = 0; i < steps_len; i += 1) {
      updateStepStatus(i);
    }
  }

  if (time_report) timeReportReset(steps_len);
  fuzzReset();
}

function updateBuildStatus(s) {
  let text;
  let active = false;
  let reset_time_reports = false;
  if (s == 0) {
    text = "Idle";
  } else if (s == 1) {
    text = "Watching for changes...";
  } else if (s == 2) {
    text = "Running...";
    active = true;
    reset_time_reports = true;
  } else if (s == 3) {
    text = "Starting fuzzer...";
    active = true;
  } else {
    console.log(`bad build status: ${s}`);
  }
  domSummary.status.textContent = text;
  if (active) {
    domSummary.status.classList.add("status-running");
    domSummary.status.classList.remove("status-idle");
    domButtonRebuild.disabled = true;
  } else {
    domSummary.status.classList.remove("status-running");
    domSummary.status.classList.add("status-idle");
    domButtonRebuild.disabled = false;
  }
  if (reset_time_reports) {
    // Grey out and collapse all the time reports
    for (const time_report_host of domTimeReportList.children) {
      const details = time_report_host.shadowRoot.querySelector(":host > details");
      details.classList.add("pending");
      details.open = false;
    }
  }
}
function updateStepStatus(step_idx) {
  const li = domStepList.children[step_idx];
  const step_status = wasm_exports.stepStatus(step_idx);
  li.classList.remove("step-wip", "step-success", "step-failure");
  if (step_status == 0) {
    // pending
  } else if (step_status == 1) {
    li.classList.add("step-wip");
  } else if (step_status == 2) {
    li.classList.add("step-success");
  } else if (step_status == 3) {
    li.classList.add("step-failure");
  } else {
    console.log(`bad step status: ${step_status}`);
  }
}

function decodeString(ptr, len) {
  if (len === 0) return "";
  return text_decoder.decode(new Uint8Array(wasm_exports.memory.buffer, ptr, len));
}
function getU32Array(ptr, len) {
  if (len === 0) return new Uint32Array();
  return new Uint32Array(wasm_exports.memory.buffer, ptr, len);
}
function unwrapString(bigint) {
  const ptr = Number(bigint & 0xffffffffn);
  const len = Number(bigint >> 32n);
  return decodeString(ptr, len);
}

const time_report_entry_template = document.getElementById("timeReportEntryTemplate").content;
const domTimeReport = document.getElementById("timeReport");
const domTimeReportList = document.getElementById("timeReportList");
function timeReportReset(steps_len) {
  let entries = [];
  for (let i = 0; i < steps_len; i += 1) {
    const step_name = unwrapString(wasm_exports.stepName(i));
    const host = document.createElement("div");
    const shadow = host.attachShadow({ mode: "open" });
    shadow.appendChild(time_report_entry_template.cloneNode(true));
    shadow.querySelector(":host > details").classList.add("pending");
    const slotted_name = document.createElement("code");
    slotted_name.setAttribute("slot", "step-name");
    slotted_name.textContent = step_name;
    host.appendChild(slotted_name);
    entries.push(host);
  }
  domTimeReportList.replaceChildren(...entries);
  domTimeReport.classList.remove("hidden");
}
function timeReportUpdateCompile(
  step_idx,
  inner_html_ptr,
  inner_html_len,
  file_table_html_ptr,
  file_table_html_len,
  decl_table_html_ptr,
  decl_table_html_len,
  use_llvm,
) {
  const inner_html = decodeString(inner_html_ptr, inner_html_len);
  const file_table_html = decodeString(file_table_html_ptr, file_table_html_len);
  const decl_table_html = decodeString(decl_table_html_ptr, decl_table_html_len);

  const host = domTimeReportList.children.item(step_idx);
  const shadow = host.shadowRoot;

  shadow.querySelector(":host > details").classList.remove("pending", "no-llvm");

  shadow.getElementById("genericReport").classList.add("hidden");
  shadow.getElementById("compileReport").classList.remove("hidden");

  if (!use_llvm) shadow.querySelector(":host > details").classList.add("no-llvm");
  host.innerHTML = inner_html;
  shadow.getElementById("fileTableBody").innerHTML = file_table_html;
  shadow.getElementById("declTableBody").innerHTML = decl_table_html;
}
function timeReportUpdateGeneric(
  step_idx,
  inner_html_ptr,
  inner_html_len,
) {
  const inner_html = decodeString(inner_html_ptr, inner_html_len);
  const host = domTimeReportList.children.item(step_idx);
  const shadow = host.shadowRoot;
  shadow.querySelector(":host > details").classList.remove("pending", "no-llvm");
  shadow.getElementById("genericReport").classList.remove("hidden");
  shadow.getElementById("compileReport").classList.add("hidden");
  host.innerHTML = inner_html;
}

const fuzz_entry_template = document.getElementById("fuzzEntryTemplate").content;
const domFuzz = document.getElementById("fuzz");
const domFuzzStatus = document.getElementById("fuzzStatus");
const domFuzzEntries = document.getElementById("fuzzEntries");
let domFuzzInstance = null;
function fuzzRequestSources() {
  domFuzzStatus.classList.remove("hidden");
  domFuzzStatus.textContent = "Loading sources tarball...";
  fetch("sources.tar").then(function(response) {
    if (!response.ok) throw new Error("unable to download sources");
    domFuzzStatus.textContent = "Parsing fuzz test sources...";
    return response.arrayBuffer();
  }).then(function(buffer) {
    if (buffer.length === 0) throw new Error("sources.tar was empty");
    const js_array = new Uint8Array(buffer);
    const ptr = wasm_exports.alloc(js_array.length);
    const wasm_array = new Uint8Array(wasm_exports.memory.buffer, ptr, js_array.length);
    wasm_array.set(js_array);
    wasm_exports.fuzzUnpackSources(ptr, js_array.length);
    domFuzzStatus.textContent = "";
    domFuzzStatus.classList.add("hidden");
  });
}
function fuzzReady() {
  domFuzz.classList.remove("hidden");

  // TODO: multiple fuzzer instances
  if (domFuzzInstance !== null) return;

  const host = document.createElement("div");
  const shadow = host.attachShadow({ mode: "open" });
  shadow.appendChild(fuzz_entry_template.cloneNode(true));

  domFuzzInstance = host;
  domFuzzEntries.appendChild(host);
}
function fuzzReset() {
  domFuzz.classList.add("hidden");
  domFuzzEntries.replaceChildren();
  domFuzzInstance = null;
}
function fuzzUpdateStats(stats_html_ptr, stats_html_len) {
  if (domFuzzInstance === null) throw new Error("fuzzUpdateStats called when fuzzer inactive");
  const stats_html = decodeString(stats_html_ptr, stats_html_len);
  const host = domFuzzInstance;
  host.innerHTML = stats_html;
}
function fuzzUpdateEntryPoints(entry_points_html_ptr, entry_points_html_len) {
  if (domFuzzInstance === null) throw new Error("fuzzUpdateEntryPoints called when fuzzer inactive");
  const entry_points_html = decodeString(entry_points_html_ptr, entry_points_html_len);
  const domEntryPointList = domFuzzInstance.shadowRoot.getElementById("entryPointList");
  domEntryPointList.innerHTML = entry_points_html;
}
function fuzzUpdateSource(source_html_ptr, source_html_len) {
  if (domFuzzInstance === null) throw new Error("fuzzUpdateSource called when fuzzer inactive");
  const source_html = decodeString(source_html_ptr, source_html_len);
  const domSourceText = domFuzzInstance.shadowRoot.getElementById("sourceText");
  domSourceText.innerHTML = source_html;
  domFuzzInstance.shadowRoot.getElementById("source").classList.remove("hidden");
}
function fuzzUpdateCoverage(covered_ptr, covered_len) {
  if (domFuzzInstance === null) throw new Error("fuzzUpdateCoverage called when fuzzer inactive");
  const shadow = domFuzzInstance.shadowRoot;
  const domSourceText = shadow.getElementById("sourceText");
  const covered = getU32Array(covered_ptr, covered_len);
  for (let i = 0; i < domSourceText.children.length; i += 1) {
    const childDom = domSourceText.children[i];
    if (childDom.id != null && childDom.id[0] == "l") {
      childDom.classList.add("l");
      childDom.classList.remove("c");
    }
  }
  for (const sli of covered) {
    shadow.getElementById(`l${sli}`).classList.add("c");
  }
}
