---
format: 
  html:
    css: styles.css
title: Quarto Filter Trace Viewer
execute: 
  echo: false
filters:
  - load_trace.lua
---

```{ojs}
dz = htl.html`<div id="dropzone">
<div id="trace1">Trace 1.</div>
<div id="trace2">Trace 2.</div>
</div>`
```


```{ojs}
b64Data = (docId) => {
  const el = document.querySelector(`#${docId}`);
  if (!el) {
    return;
  }
  return atob(el.textContent);
}
jsonb64Data = (docId) => {
  const d = b64Data(docId);
  if (!d) {
    return;
  }
  return JSON.parse(d)
}
// Based on 
// - https://observablehq.com/@triptych/loading-a-file-via-drag-and-drop-in-observable
// - https://observablehq.com/@tuner/using-event-listeners
trace = Generators.observe((change) => {
  let traces = {
    trace1: jsonb64Data("trace_1_data"),
    trace2: jsonb64Data("trace_2_data")
  }
  const t1Name = b64Data("trace_1_name")
  const t2Name = b64Data("trace_2_name")
  if (t1Name) {
      dz.querySelector("#trace1").textContent = `Trace 1: ${t1Name}`;    
  }
  if (t2Name) {
      dz.querySelector("#trace2").textContent = `Trace 2: ${t2Name}`;    
  }

  const emitChange = () => {
    if (traces.trace1 !== undefined && traces.trace2 === undefined) {
      change({
        ...traces.trace1,
        kind: "single"
      });
    } else if (traces.trace2 !== undefined && traces.trace1 === undefined) {
      change({
        ...traces.trace2,
        kind: "single"
      });
    } else if (traces.trace1 !== undefined && traces.trace2 !== undefined) {
      change({
        kind: "double",
        traces: [traces.trace1, traces.trace2]
      });
    } else {
      change(undefined);
    }
  }

  const getAsFile = async (item) => {
    const file = item.getAsFile();
    const text = await file.text();
    const json = JSON.parse(text);
    return {json, name: file.name};
  }
  const dropped = async (ev) => {
    // Prevent default behavior (Prevent file from being opened)
    ev.preventDefault();

    if (!ev.dataTransfer.items) {
      throw new Error("Unimplemented...? shrug");
    }
    if (ev.dataTransfer.items.length === 0) {
      throw new Error("Expected at least one file");
    }
    if (ev.dataTransfer.items.length > 2) {
      throw new Error("Expected at most two files");
    }

    if (ev.dataTransfer.items.length === 1) {
      // If dropped items aren't files, reject them
      if (ev.dataTransfer.items[0].kind !== 'file') {
        throw new Error("Expected files to be dropped");
      }
      const json = await getAsFile(ev.dataTransfer.items[0]);
      debugger;
      traces[ev.target.id] = json.json;
      ev.target.textContent = `Trace ${ev.target.id === "trace1" ? 1 : 2}: ${json.name}`;
    } else {
      if (ev.dataTransfer.items[0].kind !== 'file') {
        throw new Error("Expected files to be dropped");
      }
      if (ev.dataTransfer.items[1].kind !== 'file') {
        throw new Error("Expected files to be dropped");
      }
      dz.querySelector("#trace1").textContent = `Trace 1: ${ev.dataTransfer.files[0].name}`;
      dz.querySelector("#trace2").textContent = `Trace 2: ${ev.dataTransfer.files[1].name}`;

      // we need to do grab all promises before the first await
      // or else stuff goes wrong
      //
      // see https://developer.mozilla.org/en-US/docs/Web/API/DataTransfer/files
      const promises = [ev.dataTransfer.files[0].text(), ev.dataTransfer.files[1].text()];
      const [json1, json2] = await Promise.all(promises);
      // const json1 = await ev.dataTransfer.files[0].text();
      // const json2 = await ev.dataTransfer.files[1].text();
      traces.trace1 = JSON.parse(json1);
      traces.trace2 = JSON.parse(json2);
    }
    emitChange();
  }
  for (const id of ["#trace1", "#trace2"]) {
    const el = dz.querySelector(id);
    el.addEventListener("drop", dropped);
    el.addEventListener("dragover", (ev) => ev.preventDefault());
  }
  emitChange();
  return () => {};
})
```

```{ojs}
import { convertDoc } from "./convert-pandoc-json.js";
import { drawTree } from "./draw-tree.js";
import { jsonpatch } from "./jsonpatch.js";
import { editDistance } from "./edit-distance.ts";
```

::: {.column-screen-inset}

```{ojs}
{
  if (trace === undefined) {
    return "No trace uploaded";
  }

  const output = htl.html`<div></div>`;
  const sel = d3.select(output);

  if (trace.kind === "single") {
    sel.append("h2").text("Starting doc");

    drawTree(sel, convertDoc(trace.data[0].doc), "Doc");

    let isNoOp = true;

    for (let i = 1; i < trace.data.length; ++i) {
      const ops = jsonpatch.compare(
        convertDoc(trace.data[i - 1].doc),
        convertDoc(trace.data[i].doc)
      );
      if (ops.length === 0) {
        sel.append("h2").text(`Filter: ${trace.data[i].state} (no op)`);
        if (!isNoOp) {
          drawTree(sel, convertDoc(trace.data[i].doc), "Doc");
          isNoOp = true;
        }
        continue;
      }
      isNoOp = false;

      sel.append("h2").text(`Filter: ${trace.data[i].state}`);
      drawTree(sel, convertDoc(trace.data[i].doc), "Doc");
      drawTree(sel, ops, "Ops")
        .style("margin-bottom", "0.1em")
        .style("margin-top", "0.1em");
    }
  } else if (trace.kind == "double") {
    debugger;
    const aFilters = trace.traces[0].data.slice(1).map(data => data.state);
    const bFilters = trace.traces[1].data.slice(1).map(data => data.state);
    const edits = editDistance(aFilters, bFilters);
    sel.append("h2").text("Trace diff");
    const table = sel.append("table");
    let docA = convertDoc(trace.traces[0].data[0].doc);
    let docB = convertDoc(trace.traces[1].data[0].doc);
    let filterA = trace.traces[0].data[0].state;
    let filterB = trace.traces[1].data[0].state;
    let changedA = true;
    let changedB = true;

    const drawDocRow = (drawLeft, drawRight) => {
      const firstTr = table.append("tr").classed("doc-row", true);
      
      firstTr.append("td").classed("filter-name", true).text(drawLeft ? filterA : "");
      if (changedA) {
        drawTree(firstTr.append("td").classed("filter-details", true), docA, "Doc");
      } else {
        firstTr.append("td");
      }
      const diff = jsonpatch.compare(docA, docB);
      if (diff.length && (changedA || changedB) && (drawLeft && drawRight)) {
        drawTree(firstTr.append("td").classed("filter-details", true), diff, "Ops");
      } else {
        firstTr.append("td");
      }
      if (changedB) {
        drawTree(firstTr.append("td").classed("filter-details", true), docB, "Doc");
      } else {
        firstTr.append("td");
      }
      firstTr.append("td").classed("filter-name", true).text(drawRight ? filterB : "");
    }

    const drawDiffRow = (diffA, diffB) => {
      changedA = diffA.length;
      changedB = diffB.length;

      const verticalOpsRow = table.append("tr");

      verticalOpsRow.append("td");
      if (changedA) {
        drawTree(verticalOpsRow.append("td").classed("filter-details", true), diffA, "Ops");
      } else {
        verticalOpsRow.append("td");
      }
      verticalOpsRow.append("td");
      if (changedB) {
        drawTree(verticalOpsRow.append("td").classed("filter-details", true), diffB, "Ops");
      } else {
        verticalOpsRow.append("td");
      }
      verticalOpsRow.append("td");
    }

    drawDocRow(true, true);

    for (const edit of edits) {
      if (edit.type === "replace") {
        const verticalOpsRow = table.append("tr");
        verticalOpsRow.append("td");
        const newDocA = convertDoc(trace.traces[0].data[edit.newLocation[0]].doc);
        const newDocB = convertDoc(trace.traces[1].data[edit.newLocation[1]].doc);
        const diffA = jsonpatch.compare(docA, newDocA);
        const diffB = jsonpatch.compare(docB, newDocB);

        drawDiffRow(diffA, diffB);

        docA = newDocA;
        docB = newDocB;
        filterA = trace.traces[0].data[edit.newLocation[0]].state;
        filterB = trace.traces[1].data[edit.newLocation[1]].state;

        drawDocRow(true, true);
      } else if (edit.type === "insert") {
        const newDocB = convertDoc(trace.traces[1].data[edit.newLocation[1]].doc);
        drawDiffRow([], jsonpatch.compare(docB, newDocB));

        docB = newDocB;
        filterB = trace.traces[1].data[edit.newLocation[1]].state;
        drawDocRow(false, true);
      } else if (edit.type === "delete") {
        const newDocA = convertDoc(trace.traces[0].data[edit.newLocation[0]].doc);
        drawDiffRow(jsonpatch.compare(docA, newDocA), []);

        docA = newDocA;
        filterA = trace.traces[0].data[edit.newLocation[0]].state;
        drawDocRow(true, false);
      } else {
        throw new Error("Unimplemented");
      }
    }

  }

  return output;
}
```

:::
