/* dragdrop.jsx
 *
 * <window.DragDropQuestion question={q} onSubmit={fn} onReveal={fn}/>
 *
 * Renders a drag-and-drop question matching the schema in
 * docs/DRAGDROP_SCHEMA.md. Items appear in a tray below an SVG diagram
 * with labeled drop zones. User drags items onto zones; pointer events
 * mean it works on both mouse and touch.
 *
 * Why pointer events, not HTML5 drag-and-drop:
 *   HTML5 drag-and-drop doesn't fire on touch devices. Mobile users
 *   would be locked out. Pointer events (pointerdown/move/up) unify
 *   mouse, touch, and stylus into one API, supported by all modern
 *   browsers (Safari finally fixed it in 13.1, 2020).
 *
 * State model:
 *   placements: { [itemId]: zoneId | null }   — current placement of each item
 *   dragging:   { itemId, dx, dy, pointerId } — when a drag is in progress
 *   submitted:  bool                          — has the user clicked Submit
 *
 * Coordinate spaces:
 *   - SVG diagram uses viewBox from question.diagram.viewBox (default "0 0 500 400")
 *   - Item positions while dragging are in client pixel coordinates,
 *     then converted to SVG coords for hit-testing against zones.
 *
 * Grading:
 *   - On submit, compare placements against question.ans.
 *   - "partial" mode: count correct placements / total items.
 *   - "all-or-nothing" mode: 1 if all correct, else 0.
 *   - After submit, zones show green/red outline based on contents.
 *   - Wrong placements may show an inline explanation from question.wrong[item:zone].
 */
(function () {
  "use strict";

  // ──────────────── helpers ────────────────

  // Hit-test: given client (x, y) and an SVG element + zone rect,
  // return zoneId if the point is inside, else null.
  function hitTestZone(svg, zones, clientX, clientY) {
    if (!svg) return null;
    const pt = svg.createSVGPoint();
    pt.x = clientX;
    pt.y = clientY;
    const ctm = svg.getScreenCTM();
    if (!ctm) return null;
    const local = pt.matrixTransform(ctm.inverse());
    for (const z of zones) {
      if (
        local.x >= z.x && local.x <= z.x + z.w &&
        local.y >= z.y && local.y <= z.y + z.h
      ) {
        return z.id;
      }
    }
    return null;
  }

  // Compute correct/incorrect lookup once placements + ans are known.
  function gradePlacements(placements, ans, grade) {
    let correct = 0;
    const total = Object.keys(ans).length;
    const perItem = {};   // itemId -> "correct" | "wrong" | "empty"
    for (const itemId of Object.keys(ans)) {
      const expected = ans[itemId];
      const placed = placements[itemId];
      if (placed == null) perItem[itemId] = "empty";
      else if (placed === expected) {
        perItem[itemId] = "correct";
        correct++;
      } else {
        perItem[itemId] = "wrong";
      }
    }
    const score = grade === "all-or-nothing"
      ? (correct === total ? 1 : 0)
      : (total === 0 ? 0 : correct / total);
    return { score, correct, total, perItem };
  }

  // ──────────────── DragDropQuestion ────────────────

  function DragDropQuestion(props) {
    const q = props.question;
    const onSubmit = props.onSubmit || (() => {});
    const onReveal = props.onReveal || (() => {});

    // Defensive: validate the question shape so a malformed entry doesn't crash.
    if (!q || !q.diagram || !Array.isArray(q.diagram.zones) ||
        !Array.isArray(q.items) || !q.ans) {
      return React.createElement(
        "div",
        { className: "dd-error" },
        "Malformed drag-and-drop question. Check the question data."
      );
    }

    const viewBox = q.diagram.viewBox || "0 0 500 400";
    const zones = q.diagram.zones;
    const items = q.items;
    const grade = q.grade || "partial";

    // placements[itemId] = zoneId or null. Initialized empty.
    const [placements, setPlacements] = React.useState(() => {
      const init = {};
      for (const it of items) init[it.id] = null;
      return init;
    });
    const [submitted, setSubmitted] = React.useState(false);
    const [dragging, setDragging] = React.useState(null);
    // dragging shape: { itemId, pointerId, clientX, clientY, overZoneId }
    const svgRef = React.useRef(null);

    // ──── Pointer event handlers ────

    function handlePointerDown(e, itemId) {
      if (submitted) return;
      // Only respond to primary pointer (left mouse, first touch).
      if (e.button != null && e.button !== 0) return;
      e.preventDefault();
      // Capture the pointer so we keep getting events even if the cursor
      // leaves the item element.
      try { e.currentTarget.setPointerCapture(e.pointerId); } catch (_) {}
      setDragging({
        itemId,
        pointerId: e.pointerId,
        clientX: e.clientX,
        clientY: e.clientY,
        overZoneId: hitTestZone(svgRef.current, zones, e.clientX, e.clientY),
      });
    }

    function handlePointerMove(e) {
      if (!dragging || dragging.pointerId !== e.pointerId) return;
      const over = hitTestZone(svgRef.current, zones, e.clientX, e.clientY);
      setDragging({
        ...dragging,
        clientX: e.clientX,
        clientY: e.clientY,
        overZoneId: over,
      });
    }

    function handlePointerUp(e) {
      if (!dragging || dragging.pointerId !== e.pointerId) return;
      const drop = hitTestZone(svgRef.current, zones, e.clientX, e.clientY);
      if (drop) {
        // Place item in zone. (Allow multiple items per zone — we don't
        // enforce one-per-zone since a question might intentionally need it.)
        setPlacements((prev) => ({ ...prev, [dragging.itemId]: drop }));
      }
      // Releasing outside any zone: return item to tray (placement = null).
      else {
        setPlacements((prev) => ({ ...prev, [dragging.itemId]: null }));
      }
      try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (_) {}
      setDragging(null);
    }

    function handlePointerCancel() {
      // OS interrupted the drag (e.g., notification on mobile). Treat
      // as cancel — don't change placement, just clear drag state.
      setDragging(null);
    }

    // Reset placements without re-mounting. Useful in a "Try again" flow.
    function handleReset() {
      const init = {};
      for (const it of items) init[it.id] = null;
      setPlacements(init);
      setSubmitted(false);
    }

    function handleSubmit() {
      if (submitted) return;
      const result = gradePlacements(placements, q.ans, grade);
      setSubmitted(true);
      onSubmit(result, placements);
      onReveal(result, placements);
    }

    // ──── Derived state ────

    // How many items are placed somewhere (vs in the tray)?
    const placedCount = items.filter((it) => placements[it.id] != null).length;
    const allPlaced = placedCount === items.length;

    // After submit, compute grading details for visual feedback.
    const result = submitted ? gradePlacements(placements, q.ans, grade) : null;

    // Items currently in each zone, indexed by zone id, for SVG rendering.
    const itemsByZone = {};
    for (const z of zones) itemsByZone[z.id] = [];
    for (const it of items) {
      const placed = placements[it.id];
      if (placed && itemsByZone[placed]) itemsByZone[placed].push(it);
    }

    // ──── Render ────

    return React.createElement(
      "div",
      { className: "dd-question", onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, onPointerCancel: handlePointerCancel },
      // Stem
      React.createElement("div", { className: "dd-stem" }, q.stem),
      // Diagram
      React.createElement(
        "div",
        { className: "dd-diagram-wrap" },
        React.createElement(
          "svg",
          {
            ref: svgRef,
            className: "dd-diagram",
            viewBox: viewBox,
            preserveAspectRatio: "xMidYMid meet",
          },
          // Render each zone
          zones.map((z) => {
            // Color outline based on grading state, if submitted
            let zoneClass = "dd-zone";
            if (dragging && dragging.overZoneId === z.id) zoneClass += " dd-zone-hover";
            if (submitted) {
              // Determine if this zone's contents are correct.
              // Zone is correct if every item in it is placed there per ans.
              const containedItems = itemsByZone[z.id];
              const allCorrect = containedItems.length > 0 &&
                containedItems.every((it) => q.ans[it.id] === z.id);
              const anyWrong = containedItems.some((it) => q.ans[it.id] !== z.id);
              if (allCorrect) zoneClass += " dd-zone-correct";
              else if (anyWrong) zoneClass += " dd-zone-wrong";
            }
            return React.createElement(
              "g",
              { key: z.id, className: zoneClass },
              React.createElement("rect", {
                x: z.x, y: z.y, width: z.w, height: z.h,
                rx: 4, ry: 4,
                className: "dd-zone-rect",
              }),
              React.createElement(
                "text",
                {
                  x: z.x + z.w / 2,
                  y: z.y + 18,
                  textAnchor: "middle",
                  className: "dd-zone-label",
                },
                z.label
              ),
              // Render items placed in this zone, stacked vertically below the label
              itemsByZone[z.id].map((it, i) => {
                const itemY = z.y + 36 + i * 22;
                // Skip rendering if it would overflow the zone height
                if (itemY + 18 > z.y + z.h) return null;
                let itemClass = "dd-zoneitem";
                if (submitted) {
                  itemClass += q.ans[it.id] === z.id ? " dd-zoneitem-correct" : " dd-zoneitem-wrong";
                }
                return React.createElement(
                  "g",
                  {
                    key: it.id,
                    className: itemClass,
                    onPointerDown: submitted ? undefined : (e) => handlePointerDown(e, it.id),
                  },
                  React.createElement("rect", {
                    x: z.x + 4, y: itemY, width: z.w - 8, height: 18,
                    rx: 2, ry: 2,
                  }),
                  React.createElement(
                    "text",
                    {
                      x: z.x + z.w / 2, y: itemY + 13,
                      textAnchor: "middle",
                      className: "dd-zoneitem-text",
                    },
                    it.t
                  )
                );
              })
            );
          })
        )
      ),
      // Tray (unplaced items)
      React.createElement(
        "div",
        { className: "dd-tray" },
        React.createElement(
          "div",
          { className: "dd-tray-label" },
          submitted ? "Items" : (allPlaced ? "All items placed" : `Drag items to a zone (${placedCount}/${items.length} placed)`)
        ),
        React.createElement(
          "div",
          { className: "dd-tray-items" },
          items
            .filter((it) => placements[it.id] == null)
            .map((it) =>
              React.createElement(
                "div",
                {
                  key: it.id,
                  className: "dd-trayitem" + (dragging && dragging.itemId === it.id ? " dd-trayitem-dragging" : ""),
                  onPointerDown: submitted ? undefined : (e) => handlePointerDown(e, it.id),
                },
                it.t
              )
            )
        )
      ),
      // Action buttons
      React.createElement(
        "div",
        { className: "dd-actions" },
        !submitted && React.createElement(
          "button",
          {
            className: "btn btn-primary",
            onClick: handleSubmit,
            disabled: !allPlaced,
          },
          allPlaced ? "Submit answer" : `Place all items (${placedCount}/${items.length})`
        ),
        submitted && React.createElement(
          "button",
          { className: "btn", onClick: handleReset },
          "Try again"
        )
      ),
      // Feedback (after submit)
      submitted && React.createElement(
        "div",
        { className: "dd-feedback" },
        React.createElement(
          "div",
          { className: result.score === 1 ? "dd-result dd-result-pass" : (result.score >= 0.5 ? "dd-result dd-result-partial" : "dd-result dd-result-fail") },
          grade === "all-or-nothing"
            ? (result.correct === result.total ? "Correct" : `${result.correct} of ${result.total} placements correct (need all)`)
            : `${result.correct} of ${result.total} correct (${Math.round(result.score * 100)}%)`
        ),
        q.exp && React.createElement("div", { className: "dd-exp" }, q.exp),
        // Per-misplacement explanations
        items.map((it) => {
          const placed = placements[it.id];
          if (!placed || placed === q.ans[it.id]) return null;
          const key = `${it.id}:${placed}`;
          const note = q.wrong && q.wrong[key];
          if (!note) return null;
          return React.createElement(
            "div",
            { key: it.id, className: "dd-wrongnote" },
            React.createElement("strong", null, `"${it.t}" → ${zones.find(z => z.id === placed)?.label || placed}: `),
            note
          );
        })
      ),
      // Floating ghost of the dragged item, follows cursor
      dragging && (() => {
        // Render outside the SVG, in fixed positioning over the page.
        // Center the ghost on the cursor for both mouse and touch.
        const it = items.find((x) => x.id === dragging.itemId);
        if (!it) return null;
        return React.createElement(
          "div",
          {
            className: "dd-ghost",
            style: {
              left: dragging.clientX + "px",
              top: dragging.clientY + "px",
            },
          },
          it.t
        );
      })()
    );
  }

  window.DragDropQuestion = DragDropQuestion;
})();
