/* IJWestern — the shared modal system (step 5).
   Globals: Modal, VisitModal

   Two things live here, and the order matters.

   `Modal` is infrastructure: one overlay for the whole site, with no
   opinion about what goes inside it. Steps 6, 7 and 9 all consume it —
   the photo lightbox, the layout viewer, Build a home — so it takes its
   content as children and nothing about a form leaks into it.

   `VisitModal` is the first consumer. The form inside it lives in
   forms.jsx since 3 Nov 2026, when both of the site's forms were
   replaced. On the home page the header and footer "Schedule a visit"
   still scroll down to the inline form; everywhere else, and from the
   home page's launch cards, they open it here. */

/* Keep in step with --md-anim in styles.css. Only the exit needs it —
   the panel has to stay mounted long enough to fade out. */
const MD_ANIM_MS = 200;

/* ── Body scroll lock ──────────────────────────────────────────────
   Ref-counted, because from step 6 a lightbox can open over a modal
   and the inner one must not hand the page its scrollbar back on
   close. The scrollbar's own width goes onto a custom property: the
   header is `position: fixed` and would not otherwise follow the body
   in, leaving it a few pixels wide of the content it floats over. */
let mdLocks = 0;
let mdSaved = null;

function mdLockScroll() {
  if (mdLocks++ > 0) return;
  const b = document.body;
  const bar = window.innerWidth - document.documentElement.clientWidth;
  mdSaved = { pr: b.style.paddingRight };
  document.documentElement.style.setProperty("--md-sbw", `${Math.max(0, bar)}px`);
  b.classList.add("md-locked");
}

function mdUnlockScroll() {
  if (--mdLocks > 0) return;
  mdLocks = 0;
  const b = document.body;
  b.classList.remove("md-locked");
  b.style.paddingRight = (mdSaved && mdSaved.pr) || "";
  document.documentElement.style.removeProperty("--md-sbw");
  mdSaved = null;
}

const MD_FOCUSABLE = [
  "a[href]", "button:not([disabled])", "input:not([disabled])",
  "select:not([disabled])", "textarea:not([disabled])",
  '[tabindex]:not([tabindex="-1"])',
].join(",");

const mdReduced = () =>
  !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);

let mdSeq = 0;

/* ── Modal ─────────────────────────────────────────────────────────
   `open` drives it, and it stays mounted for one animation past
   `open` going false so the exit is visible. Portalled to <body>:
   the header is a backdrop-filtered stacking context, so a modal
   opened from the header button would otherwise be trapped inside it
   and clipped by it. */
function Modal({
  open,
  onClose,
  eyebrow = null,
  title = null,
  lead = null,
  size = "form",
  closeLabel = "Close",
  className = "",
  children,
}) {
  const [mounted, setMounted] = React.useState(!!open);
  const [shown, setShown] = React.useState(false);
  const panelRef = React.useRef(null);
  const returnRef = React.useRef(null);
  const idRef = React.useRef(null);
  if (idRef.current === null) idRef.current = `md-t${++mdSeq}`;
  const titleId = idRef.current;

  React.useEffect(() => {
    if (open) {
      setMounted(true);
      // A frame late, so the panel has a from-state to transition out of.
      const r = requestAnimationFrame(() => setShown(true));
      return () => cancelAnimationFrame(r);
    }
    setShown(false);
    const t = setTimeout(() => setMounted(false), mdReduced() ? 0 : MD_ANIM_MS);
    return () => clearTimeout(t);
  }, [open]);

  // Focus, keys and the scroll lock all belong to the mounted lifetime,
  // not to `open`: unwinding them on the way out would let the page
  // scroll behind a panel that is still fading.
  React.useEffect(() => {
    if (!mounted) return;
    const panel = panelRef.current;
    returnRef.current = document.activeElement;
    mdLockScroll();
    // The panel takes focus rather than the first field. Focusing an
    // input scrolls the modal's own heading out of frame on a phone,
    // and a form the reader has not read yet is a poor place to land.
    if (panel) panel.focus({ preventScroll: true });

    const onKey = (e) => {
      if (e.key === "Escape") {
        // Capture phase and stopped here: the header's own Escape
        // handler would otherwise close the nav panel underneath.
        e.stopPropagation();
        onClose();
        return;
      }
      if (e.key !== "Tab" || !panel) return;
      const items = Array.from(panel.querySelectorAll(MD_FOCUSABLE))
        .filter((el) => el.offsetWidth || el.offsetHeight || el === document.activeElement);
      if (!items.length) {
        e.preventDefault();
        panel.focus({ preventScroll: true });
        return;
      }
      const first = items[0];
      const last = items[items.length - 1];
      const here = document.activeElement;
      const inside = panel.contains(here) && here !== panel;
      if (e.shiftKey && (!inside || here === first)) {
        e.preventDefault();
        last.focus();
      } else if (!e.shiftKey && (!inside || here === last)) {
        e.preventDefault();
        first.focus();
      }
    };

    document.addEventListener("keydown", onKey, true);
    return () => {
      document.removeEventListener("keydown", onKey, true);
      mdUnlockScroll();
      // Focus goes back where it came from — but only if that is still
      // somewhere a reader can see. Below 640 the header CTA lives in
      // the menu panel, and the menu shuts as the modal opens: focusing
      // a hidden button would drop the caret somewhere invisible, so in
      // that one case the document keeps it.
      const back = returnRef.current;
      const visible = back && document.contains(back) &&
                      (back.offsetWidth || back.offsetHeight || back.getClientRects().length);
      if (visible && back.focus) back.focus({ preventScroll: true });
    };
  }, [mounted, onClose]);

  if (!mounted) return null;

  return ReactDOM.createPortal(
    <div className={`md-root md-${size}${shown ? " is-shown" : ""}${className ? ` ${className}` : ""}`}>
      {/* Fixed, so it keeps covering the page when a tall panel scrolls
          the overlay. A press inside the panel that releases out here
          resolves to the root, which has no handler — so a stray drag
          off the edge of a text field cannot shut the form. */}
      <div className="md-scrim" onClick={onClose} aria-hidden="true" />
      <div
        className="md-panel"
        role="dialog"
        aria-modal="true"
        aria-labelledby={title ? titleId : undefined}
        aria-label={title ? undefined : closeLabel}
        tabIndex={-1}
        ref={panelRef}
      >
        <button type="button" className="md-close" onClick={onClose} aria-label={closeLabel}>
          <svg width="13" height="13" viewBox="0 0 14 14" fill="none"
               stroke="currentColor" strokeWidth="1.6">
            <path d="M2 2 L12 12 M12 2 L2 12" strokeLinecap="round" />
          </svg>
        </button>

        {(eyebrow || title || lead) && (
          <header className="md-head">
            {eyebrow && <span className="md-eyebrow">{eyebrow}</span>}
            {title && <h2 className="md-title" id={titleId}>{title}</h2>}
            {lead && <p className="md-lead">{lead}</p>}
          </header>
        )}

        <div className="md-body">{children}</div>
      </div>
    </div>,
    document.body
  );
}

/* ── The Schedule a visit pop-up ──────────────────────────────────
   Every "Schedule a visit" off the home page, every project's "Buy
   now", and the home page's launch cards all open this. The form is
   forms.jsx's VisitForm. `ground` is a project's raw name: pass it from
   a CTA that belongs to one project and the form starts on it; the
   header and footer pass nothing and the form asks the reader to
   choose. */
function VisitModal({ open, onClose, ground = null }) {
  return (
    <Modal
      open={open}
      onClose={onClose}
      size="form"
      closeLabel="Close the visit form"
      title="Schedule a visit"
      lead="Walk the ground with us in person, or ask for a call back."
    >
      {/* No eyebrow on any form (client, 3 Nov). */}
      {/* The panel is already the card, so the form drops its own glass.
          Keyed on the ground, so opening it from another project's
          button starts a fresh form on that project. */}
      <VisitForm key={ground || "any"} className="register-form md-form" ground={ground} />
    </Modal>
  );
}

Object.assign(window, { Modal, VisitModal });
