// REWIND — shared view primitives: background video, icon loops, the photo
// lightbox, and the hours block. Prices live in the PDF.
// Motion preference: decorative video loops hold their first frame when the
// visitor has asked for reduced motion — the still is a real frame of the same
// shot, so nothing disappears.
function rwReducedMotion() {
  try { return window.matchMedia("(prefers-reduced-motion: reduce)").matches; } catch (e) { return false; }
}
window.rwReducedMotion = rwReducedMotion;

// A display-contents region is an actual rendered DOM stamp without changing
// the site's layout. It lets the preview bridge associate a visible section
// with its editor field while preserving the bundle's markup and styling.
function RWStamps({ fields, children }) {
  return fields.reduceRight((content, field) => <div key={field} data-rw-field={field} style={{ display: "contents" }}>{content}</div>, children);
}
window.RWStamps = RWStamps;

// Autoplaying background video. React does not reliably reflect the `muted`
// prop to the DOM attribute, and iOS/Android refuse to autoplay a video that
// isn't muted+inline — so we force muted on the element via a ref and kick off
// play() on mount. This is the fix for "video won't autoplay on mobile".
function RWVideo({ src, poster, className, style }) {
  const ref = React.useRef(null);
  const reduce = rwReducedMotion();
  const [failed, setFailed] = React.useState(false);
  React.useEffect(() => setFailed(false), [src]);
  React.useEffect(() => {
    const v = ref.current;
    if (!v || reduce || failed) return;
    v.muted = true;
    v.defaultMuted = true;
    v.setAttribute("muted", "");
    v.setAttribute("playsinline", "");
    v.setAttribute("webkit-playsinline", "");
    let playing = false;
    const kick = () => {
      if (playing || !v.isConnected) return;
      const p = v.play();
      if (p && p.then) p.then(() => {playing = true;}).catch(() => {});
    };
    kick();
    // Streaming from a CDN means readiness lands well after mount, and each
    // browser fires a different event first — so retry on all of them.
    const evts = ["loadedmetadata", "loadeddata", "canplay", "canplaythrough"];
    evts.forEach((e) => v.addEventListener(e, kick));
    // Safety net for mobile: keep nudging for a few seconds if it is still paused.
    const iv = setInterval(() => {if (v.paused) {playing = false;kick();} else {playing = true;}}, 400);
    // REWIND refresh note: do not turn a temporarily paused autoplay into the
    // poster. Backgrounded tabs and slow CDNs can recover after ten seconds;
    // retain the mobile retry/gesture path until a real media error occurs.
    // iOS Low Power Mode and Android data-saver refuse programmatic autoplay
    // outright. The first touch anywhere is a valid gesture — use it to start.
    const unlock = () => {playing = false;kick();};
    document.addEventListener("touchstart", unlock, { passive: true });
    document.addEventListener("click", unlock);
    const vis = () => {if (!document.hidden) {playing = false;kick();}};
    document.addEventListener("visibilitychange", vis);
    return () => {
      evts.forEach((e) => v.removeEventListener(e, kick));
      clearInterval(iv);
      document.removeEventListener("touchstart", unlock);
      document.removeEventListener("click", unlock);
      document.removeEventListener("visibilitychange", vis);
    };
  }, [src, failed, reduce]);
  // No source, or reduced motion — render the still rather than a moving loop.
  // Keep the hero identity on its poster so rendering audits can distinguish
  // this real fallback from the deliberately static pimg sections.
  if (!src || reduce || failed) return <img data-rw-hero-video="true" className={className} style={style} src={poster} alt="" />;
  return (
    <video ref={ref} className={className} style={style} src={src} poster={poster}
    data-rw-hero-video="true" autoPlay muted loop playsInline preload="auto" onError={() => setFailed(true)}></video>);

}
window.RWVideo = RWVideo;

// Sync groups for RWIconLoop. Members of the same `syncGroup` hold their first
// frame until every member can play through, then all start in the same tick.
// A 4s fallback releases whoever is ready so one slow file can't stall the rest.
const RW_SYNC_GROUPS = {};
function rwSyncStart(name, force) {
  const g = RW_SYNC_GROUPS[name];
  if (!g || g.started) return;
  const recs = Array.from(g.recs);
  const armed = recs.filter((r) => r.el);
  if (!armed.length) return;
  if (!force && armed.length !== recs.length) return;
  g.started = true;
  clearTimeout(g.timer);
  armed.forEach((r) => { try { r.el.currentTime = 0; } catch (e) {} });
  armed.forEach((r) => { const p = r.el.play(); if (p && p.catch) p.catch(() => r.fallback && r.fallback()); });
}
function rwSyncRegister(name, rec) {
  const g = RW_SYNC_GROUPS[name] || (RW_SYNC_GROUPS[name] = { recs: new Set(), started: false, timer: null });
  g.recs.add(rec);
  if (!g.timer) g.timer = setTimeout(() => rwSyncStart(name, true), 4000);
  return () => {
    g.recs.delete(rec);
    if (!g.recs.size) { clearTimeout(g.timer); delete RW_SYNC_GROUPS[name]; }
  };
}

// Icon that starts as a still PNG and upgrades to an alpha-webm loop once the
// video can play through. Browsers without alpha-webm support keep the still.
// `syncGroup` makes several icons start their loop together.
function RWIconLoop({ still, video, hevcVideo, height, alt, syncGroup }) {
  const [ready, setReady] = React.useState(false);
  const [failed, setFailed] = React.useState(false);
  const [useHevc, setUseHevc] = React.useState(false);
  const vref = React.useRef(null);
  const rec = React.useRef({ el: null, fallback: null });
  React.useEffect(() => { setReady(false); setFailed(false); setUseHevc(false); }, [video, hevcVideo]);
  React.useEffect(() => {
    if (!syncGroup) return;
    return rwSyncRegister(syncGroup, rec.current);
  }, [syncGroup]);
  React.useEffect(() => {
    if (!video || rwReducedMotion() || failed) return;
    const probe = document.createElement("video");
    probe.muted = true; probe.defaultMuted = true; probe.loop = true;
    probe.playsInline = true; probe.preload = "auto";
    // REWIND refresh note: probe and render the VP9 WebM in Chrome exactly as
    // the delivered bundle did. HEVC is selected only when this browser says
    // it can play it, so Safari retains its alpha-video fallback without
    // letting an unsupported first source strand Chrome on the PNG.
    const hevcSupported = Boolean(hevcVideo && probe.canPlayType('video/mp4; codecs="hvc1"'));
    setUseHevc(hevcSupported);
    probe.src = hevcSupported ? hevcVideo : video;
    const ok = () => setReady(true);
    probe.addEventListener("canplaythrough", ok, { once: true });
    probe.load();
    return () => probe.removeEventListener("canplaythrough", ok);
  }, [video, hevcVideo, failed]);
  React.useEffect(() => {
    const v = vref.current;
    if (!v || !ready || failed) return;
    v.muted = true; v.defaultMuted = true;
    v.setAttribute("muted", ""); v.setAttribute("playsinline", "");
    rec.current.fallback = () => setFailed(true);
    if (syncGroup) { rec.current.el = v; rwSyncStart(syncGroup); return; }
    const p = v.play(); if (p && p.catch) p.catch(() => setFailed(true));
  }, [ready, syncGroup, failed]);
  const style = { height: height, width: "auto", display: "block" };
  if (!ready || failed) return <img src={still} alt={alt || ""} style={style} />;
  return <video ref={vref} src={useHevc ? hevcVideo : video} style={style} autoPlay={!syncGroup} muted loop playsInline preload="auto" onError={() => setFailed(true)}></video>;
}
window.RWIconLoop = RWIconLoop;

// ── Lightbox ────────────────────────────────────────────────────────────────
// Only photos that carry real information (drinks, dishes, rooms) open full-frame.
// The opened image GROWS OUT OF its thumbnail — it starts at the thumbnail's exact
// rect and animates to a centred box — so it always reads as "the one I tapped".
// Deliberately minimal: no caption, no gallery paging, no pinch-zoom.
const RW_ZOOM = { shot: null, subs: new Set() };
function rwZoomEmit() { RW_ZOOM.subs.forEach(function (f) { f(); }); }
function rwZoomOpen(img) {
  const r = img.getBoundingClientRect();
  const src = img.currentSrc || img.src;
  // Decode the full-resolution bitmap off the main thread first. Mounting an
  // undecoded 1200x1800 <img> blocks the next frame for hundreds of ms, which
  // stalls the open animation and makes the click feel dead.
  const pre = new Image();
  pre.src = src;
  RW_ZOOM.shot = {
    src: src,
    ready: pre.decode ? pre.decode().catch(function () {}) : Promise.resolve(),
    rect: { left: r.left, top: r.top, width: r.width, height: r.height },
    nat: { w: img.naturalWidth || r.width, h: img.naturalHeight || r.height } };

  rwZoomEmit();
}
function rwZoomClose() { RW_ZOOM.shot = null; rwZoomEmit(); }

// Centred box for the opened image. Never scales past the file's own pixels, so
// a 1200px photo stays crisp instead of being blown up into mush.
function rwZoomBox(nat) {
  const maxW = Math.min(window.innerWidth * 0.92, nat.w);
  const maxH = Math.min(window.innerHeight * 0.88, nat.h);
  const s = Math.min(maxW / nat.w, maxH / nat.h);
  const w = nat.w * s, h = nat.h * s;
  return { left: (window.innerWidth - w) / 2, top: (window.innerHeight - h) / 2, width: w, height: h };
}

// A thumbnail that opens the lightbox. Drop-in replacement for <img>.
function RWZoomImg(props) {
  const open = (e) => rwZoomOpen(e.currentTarget);
  const key = (e) => {
    if (e.key === "Enter" || e.key === " ") { e.preventDefault(); rwZoomOpen(e.currentTarget); }
  };
  return (
    <img {...props}
    className={props.className ? "zimg " + props.className : "zimg"}
    tabIndex={0} role="button" onClick={open} onKeyDown={key} />);

}
window.RWZoomImg = RWZoomImg;

function RWLightbox() {
  const [shot, setShot] = React.useState(null);
  const [lit, setLit] = React.useState(false); // scrim — no image, paints instantly
  const [shown, setShown] = React.useState(false); // photo mounted (decoded)
  const [open, setOpen] = React.useState(false); // grown to full size
  const [drag, setDrag] = React.useState(0);
  const touch = React.useRef(null);
  const closeRef = React.useRef(null);
  const opener = React.useRef(null);

  React.useEffect(() => {
    const sync = () => {
      const s = RW_ZOOM.shot;
      if (s) {
        setShot(s);setDrag(0);setLit(false);setShown(false);setOpen(false);
        // Scrim darkens on the very next frame — instant acknowledgement of the tap.
        requestAnimationFrame(() => {if (RW_ZOOM.shot === s) setLit(true);});
        s.ready.then(() => {
          if (RW_ZOOM.shot !== s) return;
          setShown(true); // cheap now: bitmap is already decoded
          requestAnimationFrame(() => {if (RW_ZOOM.shot === s) setOpen(true);});
        });
      } else {
        setOpen(false);setLit(false);
        setTimeout(() => {setShot(null);setShown(false);}, 420);
      }
    };
    RW_ZOOM.subs.add(sync);
    return () => RW_ZOOM.subs.delete(sync);
  }, []);

  // Freeze the page behind the overlay — the stored thumbnail rect is only
  // valid while the page cannot scroll out from under it.
  React.useEffect(() => {
    if (!shot) return;
    const b = document.body;
    const prevOv = b.style.overflow, prevPad = b.style.paddingRight;
    const gap = window.innerWidth - document.documentElement.clientWidth;
    b.style.overflow = "hidden";
    if (gap > 0) b.style.paddingRight = gap + "px";
    // Focus moves into the overlay and stays there, then returns to the thumbnail
    // that opened it — a keyboard user is never left behind the scrim.
    opener.current = document.activeElement;
    if (closeRef.current) closeRef.current.focus();
    const keys = (e) => {
      if (e.key === "Escape") { rwZoomClose(); return; }
      if (e.key === "Tab") { e.preventDefault(); if (closeRef.current) closeRef.current.focus(); }
    };
    window.addEventListener("keydown", keys);
    return () => {
      b.style.overflow = prevOv; b.style.paddingRight = prevPad;
      window.removeEventListener("keydown", keys);
      const o = opener.current;
      if (o && o.isConnected && o.focus) o.focus();
    };
  }, [shot]);

  if (!shot) return null;
  const b = open ? rwZoomBox(shot.nat) : shot.rect;
  const fade = Math.max(0, 1 - Math.abs(drag) / 320);

  const start = (e) => { touch.current = e.touches[0].clientY; };
  const move = (e) => {
    if (touch.current == null) return;
    setDrag(e.touches[0].clientY - touch.current);
  };
  const end = () => {
    touch.current = null;
    if (Math.abs(drag) > 80) rwZoomClose(); else setDrag(0);
  };

  return (
    <div className={"zoomlb" + (lit ? " is-lit" : "") + (open ? " is-open" : "")}
    role="dialog" aria-modal="true" aria-label="Photo"
    onClick={rwZoomClose} onTouchStart={start} onTouchMove={move} onTouchEnd={end}>
      <div className="zscrim" style={{ opacity: lit ? fade : 0 }}></div>
      <button ref={closeRef} type="button" className="zclose" onClick={rwZoomClose} aria-label="Close photo">&#10005;</button>
      {shown ?
      <img className="zfull" src={shot.src} alt=""
      style={{
        left: b.left + "px", top: b.top + "px", width: b.width + "px", height: b.height + "px",
        transform: drag ? "translateY(" + drag + "px)" : null,
        transition: drag ? "none" : null }} /> :

      null}
    </div>);

}
window.RWLightbox = RWLightbox;

// Shared hours block — the compact 7-day strip (previous layout). `mode` picks
// which schedule to show: "lounge" or "ktv". Each room page shows its own.
// Venue clock — every open/closed decision is made in the venue's own timezone,
// never the visitor's device time (Taipei 3PM is New York 3AM; the naive read
// would say "closed" while the room is full). DST is handled by Intl.
const RW_TZ = "America/New_York";
const RW_DAYNAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];

function rwParseMin(t) {
  const p = String(t).match(/(\d+)(?::(\d+))?\s*(AM|PM)/i);
  if (!p) return null;
  let h = parseInt(p[1], 10) % 12;
  if (/pm/i.test(p[3])) h += 12;
  return h * 60 + (p[2] ? parseInt(p[2], 10) : 0);
}
function rwFmtMin(v) {
  const t = ((v % 1440) + 1440) % 1440;
  let h = Math.floor(t / 60);
  const mm = t % 60;
  const ap = h >= 12 ? "PM" : "AM";
  h = h % 12; if (h === 0) h = 12;
  return h + (mm ? ":" + String(mm).padStart(2, "0") : "") + " " + ap;
}
function rwVenueNow() {
  try {
    const parts = new Intl.DateTimeFormat("en-US", { timeZone: RW_TZ, weekday: "long",
      hour: "2-digit", minute: "2-digit", hour12: false }).formatToParts(new Date());
    const get = (k) => (parts.find((p) => p.type === k) || {}).value;
    let h = parseInt(get("hour"), 10);
    if (h === 24) h = 0;
    const d = RW_DAYNAMES.indexOf(get("weekday"));
    return { dow: d < 0 ? new Date().getDay() : d, mins: h * 60 + parseInt(get("minute"), 10) };
  } catch (e) {
    const d = new Date();
    return { dow: d.getDay(), mins: d.getHours() * 60 + d.getMinutes() };
  }
}
// hoursTable is Monday-first; dow is 0=Sunday.
function rwRowFor(table, dow) { return table[(dow + 6) % 7]; }
// A session's close can run past midnight — 6PM–4AM becomes 1080 → 1680.
function rwSess(table, key, dow) {
  const row = rwRowFor(table, dow);
  const v = row && row[key];
  if (!v) return null;
  const o = rwParseMin(v[0]);
  let c = rwParseMin(v[1]);
  if (c <= o) c += 1440;
  return { open: o, close: c };
}
function rwStatus(table, key) {
  const now = rwVenueNow();
  const label = (close) => {
    const left = close - now.mins;
    return left <= 60 ? "Closing soon · " + rwFmtMin(close) : "Open · Closes " + rwFmtMin(close);
  };
  // still inside yesterday's after-midnight session
  const yd = (now.dow + 6) % 7;
  const y = rwSess(table, key, yd);
  if (y && y.close > 1440 && now.mins + 1440 < y.close) {
    return { open: true, label: label(y.close - 1440), dow: yd };
  }
  const t = rwSess(table, key, now.dow);
  if (t && now.mins >= t.open && now.mins < t.close) {
    return { open: true, label: label(t.close), dow: now.dow };
  }
  if (t && now.mins < t.open) {
    return { open: false, label: "Closed · Opens " + rwFmtMin(t.open), dow: now.dow };
  }
  for (let i = 1; i <= 7; i++) {
    const d = (now.dow + i) % 7;
    const s = rwSess(table, key, d);
    if (s) {
      return { open: false, label: (t ? "Closed · Opens " : "Closed today · Opens ") +
        RW_DAYNAMES[d].slice(0, 3) + " " + rwFmtMin(s.open), dow: now.dow };
    }
  }
  return { open: false, label: "Closed", dow: now.dow };
}

// Shared hours block. Desktop keeps the 7-column strip. Mobile shows a live
// status line in venue time plus the full week as a list, ordered from today.
function RWHours({ accent, onReserve, reserveLabel, mode }) {
  const D = window.RW_DATA;
  const key = mode === "ktv" ? "ktv" : "lounge";
  const shortT = (r) => {
    if (!r) return "—";
    const f = (t) => {const m = t.match(/(\d+)(?::(\d+))?\s*(AM|PM)/i);if (!m) return t;const mm = m[2] && m[2] !== "00" ? ":" + m[2] : "";return m[1] + mm + m[3].toUpperCase();};
    return f(r[0]) + "–" + f(r[1]);
  };
  const rows = D.hoursTable.map((h) => ({ day: h.day.slice(0, 3), time: shortT(h[key]), closed: !h[key] }));

  const [st, setSt] = React.useState(() => rwStatus(D.hoursTable, key));
  React.useEffect(() => {
    setSt(rwStatus(D.hoursTable, key));
    const id = setInterval(() => setSt(rwStatus(D.hoursTable, key)), 60000);
    return () => clearInterval(id);
  }, [key]);

  const week = [];
  for (let i = 0; i < 7; i++) {
    const d = (st.dow + i) % 7;
    const row = rwRowFor(D.hoursTable, d);
    const v = row && row[key];
    week.push({
      day: RW_DAYNAMES[d],
      time: v ? rwFmtMin(rwParseMin(v[0])) + " – " + rwFmtMin(rwParseMin(v[1])) : "Closed",
      closed: !v,
      today: i === 0 });
  }

  return (
    <section className="sec" style={{ paddingTop: "var(--sp-5)", paddingBottom: "var(--sp-5)" }}>
      <div className="wrap sec-head">
        <span className="eyebrow">{mode === "ktv" ? window.RW_CONTENT.ui.karaoke.hoursHeading : window.RW_CONTENT.ui.hours.loungeHeading}</span>

        {/* mobile — live status + full week */}
        <div className="hstatus">
          <span className={"hdot" + (st.open ? " on" : "")}></span>
          <span className="hstxt">{st.label}</span>
        </div>
        <div className="hours-list">
          {week.map((r) =>
          <div key={r.day} className={"hrow" + (r.closed ? " closed" : "") + (r.today ? " today" : "")}>
              <span className="hrday">{r.day}</span>
              <span className="hrtime">{r.time}</span>
            </div>
          )}
        </div>

        {/* desktop — the 7-column strip, unchanged */}
        <div className="hours-strip">
          {rows.map((r) =>
          <div key={r.day} className={"hcell" + (r.closed ? " closed" : "")}>
              <span className="hday">{r.day}</span>
              <span className="htime">{r.time}</span>
            </div>
          )}
        </div>

        {onReserve &&
        <div style={{ marginTop: "var(--vs-4)" }}>
            <button type="button" className={"btn btn--" + (accent || "champ")} onClick={onReserve}>{reserveLabel}</button>
          </div>
        }
      </div>
    </section>);

}
window.RWHours = RWHours;
