/*
  Keeping an opened block open while the reply is still being written.

  The steps container is swapped with innerHTML every time a round closes, and
  the `done` frame replaces the whole bubble at the end. Both tear down every
  <details> inside and build new ones, so anything the reader had expanded shut
  itself again -- which, at up to twelve frames a second, made a tool block
  impossible to open at all rather than merely annoying to.

  The fix is to write down which are open before the swap and put them back
  after. It works because the ids are stable: `tool-{message}-{step}-{n}` and
  `think-{message}-{step}` are built from the mark index, the marks are
  append-only, and the finished bubble emits the same ids the live one did. See
  services/steps.py.

  Not `hx-preserve`: that keeps a node and its contents, and these nodes have to
  update. Not a CSS checkbox either -- the input would live inside the container
  being replaced, so it would be destroyed with everything else.
*/
(function () {
  "use strict";

  /* Keyed by container id, so two replies streaming into one page cannot read
     each other's state. */
  var open = {};

  function remember(box) {
    var ids = [];
    box.querySelectorAll("details[id][open]").forEach(function (details) {
      ids.push(details.id);
    });
    open[box.id || "steps"] = ids;
  }

  function restore(box) {
    (open[box.id || "steps"] || []).forEach(function (id) {
      /* CSS.escape, never concatenation into a selector: an id is built from a
         message id and two integers today, but a selector assembled by hand is
         how that stops being true safely. */
      var found = box.querySelector("details[id='" + cssEscape(id) + "']");
      if (found) found.open = true;
    });
  }

  function cssEscape(value) {
    return window.CSS && window.CSS.escape ? window.CSS.escape(value) : value;
  }

  function container(target) {
    if (!target || !target.closest) return null;
    return target.closest("[data-steps]");
  }

  /* Cancellable: the SSE extension reads what listeners return to decide
     whether to swap at all. So nothing here may cancel the event -- doing so
     would freeze the transcript with no error anywhere. There is a test on it,
     which matches the source, comments included. */
  document.body.addEventListener("htmx:sseBeforeMessage", function (event) {
    var box = container(event.target);
    if (box) remember(box);
  });

  document.body.addEventListener("htmx:sseMessage", function (event) {
    var box = container(event.target);
    if (box) restore(box);
  });

  /* The `done` frame swaps the whole <article> with outerHTML, so the element
     that comes back is a different one carrying the same id -- the finished
     bubble's container repeats both the id and the marker for exactly this
     reason. Before the swap the old box is still reachable under the target;
     after it, the safe move is to look again across the page, because what
     `afterSwap` hands back for an outerHTML swap is not reliably the new node.
     Restoring every container is harmless: what is remembered is keyed on the
     container's own id. */
  document.body.addEventListener("htmx:beforeSwap", function (event) {
    var box = event.target && event.target.querySelector
      ? event.target.querySelector("[data-steps]")
      : null;
    if (box) remember(box);
  });

  document.body.addEventListener("htmx:afterSwap", function () {
    document.querySelectorAll("[data-steps]").forEach(restore);
  });
})();
