/* global React */
// Year-A traditional-hymn plan view.
//
// Renders window.HYMN_PLAN[sundayId] (built from the user's combined Year-A
// planner JSONL). Each Sunday has:
//   primary : a short headline list of recommended hymns for the day
//   passages: the full per-reading breakdown (OT / Psalm / Epistle / Gospel,
//             plus semi-continuous / complementary / Acts tracks as they occur)
//
// The primary picks show first; a "Show more" disclosure reveals the
// per-reading breakdown. Rows are selectable and feed the same service-plan
// and last-used machinery as the rest of the planner — this view reuses the
// helpers planner.jsx exports onto window (tuneFor, hymnUsage, LastUsedPill).

const { useState } = React;

// Map a free-text reading label ("OT (semi-continuous)", "First Reading
// (Acts)", "Palms Gospel", "Canticle"…) to one of the canonical buckets the
// service plan tags items with.
function canonPassage(label) {
  const s = (label || "").toLowerCase();
  if (s.includes("gospel")) return "gospel";
  if (s.includes("psalm") || s.includes("canticle")) return "psalm";
  if (s.includes("epistle")) return "epistle";
  if (s.includes("acts") || s.includes("first reading")) return "ot";
  if (s.includes("ot") || s.includes("old testament") || s.includes("reading")) return "ot";
  return "general";
}

const MATCH_LABEL = { direct: "Direct", thematic: "Thematic", atmospheric: "Atmospheric" };

// Pull the scripture book name out of a reference, e.g.
//   "Matthew 24:36-44" -> "Matthew"   "1 Corinthians 1:3-9" -> "1 Corinthians"
//   "Psalm 119:105-112" -> "Psalm"    "Song of Solomon 2:8-13" -> "Song of Solomon"
function bookOf(reference) {
  if (!reference) return "";
  const s = reference.replace(/\([^)]*\)/g, " ").trim();
  const m = s.match(/^((?:[1-3]\s)?[A-Za-z][A-Za-z ]*?)\s+\d/);
  if (m) return m[1].trim();
  return s.replace(/[\d:.,–-].*$/, "").trim();
}

const READING_ORDER = { ot: 0, psalm: 1, epistle: 2, gospel: 3, general: 4 };

// Which lectionary track a reading belongs to (season after Pentecost only).
// "OT (semi-continuous)" -> "semi", "Psalm (complementary)" -> "comp",
// "OT (both tracks)" -> "both", "CCT Provisional OT" -> "cct" (the
// Consultation on Common Texts' proposed revision to the RCL, shown only when
// the planner opts into it). Single-track readings (Epistle/Gospel, or a plain
// "OT") return null and always show.
function trackOf(label) {
  const s = (label || "").toLowerCase();
  if (s.includes("cct")) return "cct";
  if (s.includes("both track")) return "both";
  if (s.includes("semi")) return "semi";
  if (s.includes("complementary")) return "comp";
  return null;
}

// How many hymns to show per reading before the "Show more" disclosure —
// the DEFAULT for the per-reading "Show" knob (persisted under wp:hymnShowN).
const PER_PASSAGE_PRIMARY = 4;

// Preference order when picking which hymnal's number to DISPLAY for the
// current profile (matches the My-hymnals chip order).
const PROFILE_ORDER = ["UMH", "TFWS", "W&S", "GtG", "PH", "ELW"];

// Which (hymnal, num) to DISPLAY for this hymn under the active My-hymnals
// profile. The hymn's own book wins whenever it's selected (today's view,
// unchanged); otherwise the first selected hymnal its cross-hymnal `hymnals`
// map covers (master-ranking hymnal-profile display — e.g. a GtG-only profile
// sees "GtG 82" for Come, Thou Long-Expected Jesus). Identity (selection,
// tunes, orchestration, usage) stays keyed on the hymn's OWN hymnal+num.
function displayIdOf(h, profile) {
  if (!profile || !h.hymnals || profile.has(h.hymnal)) {
    return { hymnal: h.hymnal, num: h.num, foreign: false };
  }
  for (const c of PROFILE_ORDER) {
    if (profile.has(c) && h.hymnals[c]) {
      return { hymnal: c, num: h.hymnals[c], foreign: true };
    }
  }
  return { hymnal: h.hymnal, num: h.num, foreign: false };
}

function PlanHymnRow({ h, isSelected, onToggle, note, onNote, usage, currentDayId, currentTs, primary, books, also, pills, sundayId, dismissed, onDismiss, profile }) {
  const [openNote, setOpenNote] = useState(Boolean(note));
  const disp = displayIdOf(h, profile);
  const tune = window.tuneFor ? window.tuneFor(h) : null;
  const use = usage && window.hymnUsage ? window.hymnUsage(usage, h, currentDayId) : { text: null, tune: null };
  const match = h.match || "thematic";
  const hymnKey = `${h.hymnal}:${h.num}`;
  const sungDemoted = pills && pills.lastSung && pills.lastSung.within8;
  // The SUNG_LOG "Sung …" pill already states this hymn's most recent actual
  // performance, so drop the redundant "Hymn last used …" pill for any PAST
  // use. A future "Hymn scheduled …" use is unique (the Sung pill is past-only),
  // so it always stays.
  const sungShown = !!(pills && pills.lastSung);
  const showHymnLu = !!use.text && !(sungShown && use.text.ts < currentTs);
  return (
    <div className={`wp-hymn wp-hplan-row ${primary ? "is-primary" : ""} ${isSelected ? "is-selected" : ""} ${sungDemoted ? "is-sung-demoted" : ""}`}>
      <button className="wp-hymn-check" onClick={onToggle} aria-pressed={isSelected}>
        <svg width="14" height="14" viewBox="0 0 14 14">
          {isSelected ?
          <path d="M3 7 L6 10 L11 4" stroke="currentColor" strokeWidth="1.75" fill="none" /> :
          <circle cx="7" cy="7" r="5" stroke="currentColor" strokeWidth="1" fill="none" opacity="0.4" />}
        </svg>
      </button>
      <div className="wp-hymn-body">
        <div className="wp-hymn-line">
          <span className="wp-hymn-num"
          title={disp.foreign ? `${h.hymnal} ${h.num} shown by its ${disp.hymnal} number for your hymnal profile` : undefined}>
            {disp.hymnal} {disp.num}
          </span>
          <span className="wp-hymn-title">
            {h.title}
            {tune && <span className="wp-hymn-tune-incaps" title={`Tune: ${tune}`}> ({tune})</span>}
          </span>
          {books && books.length > 0 &&
          <span className="wp-hplan-readings" title="Tagged to this Sunday's reading(s)">
              {books.map((b) => <span key={b.book} className="wp-hplan-read" data-passage={b.canon}>{b.book}</span>)}
            </span>}
          {also && also.length > 0 &&
          <span className="wp-hplan-also" title="Also fits another reading this Sunday">Also {also.join(" · ")}</span>}
          <span className={`wp-tag wp-tag-match wp-tag-match-${match}`}>{MATCH_LABEL[match] || match}</span>
          {h.opening &&
          <span className="wp-hplan-oc wp-hplan-oc-open"
          title="United Methodist planner suggests this as an OPENING hymn">
            <span className="wp-hplan-oc-glyph">O</span>Opening
          </span>}
          {h.closing &&
          <span className="wp-hplan-oc wp-hplan-oc-close"
          title="United Methodist planner suggests this as a CLOSING hymn">
            <span className="wp-hplan-oc-glyph">C</span>Closing
          </span>}
          {sungDemoted &&
          <span className="wp-hplan-demoted-tag"
          title="Sung within the last 8 weeks, so it's sorted to the bottom of its tier. Still fully available — a soft nudge, not a block.">
            ↓ sorted down
          </span>}
        </div>
        <div className="wp-hymn-line wp-hymn-meta">
          {window.OrchBadge && <window.OrchBadge hymn={h} />}
          {h.reason && <span className="wp-hymn-note">{h.reason}</span>}
          {(showHymnLu || use.tune) &&
          <span className="wp-hymn-uses">
              {showHymnLu && <window.LastUsedPill kind="hymn" rec={use.text} currentTs={currentTs} />}
              {use.tune && (!use.text || use.tune.dayId !== use.text.dayId) &&
            <window.LastUsedPill kind="tune" rec={use.tune} currentTs={currentTs} tune={tune} />}
            </span>}
          {pills && window.HymnPillStack &&
          <window.HymnPillStack pills={pills} sundayId={sundayId} hymnKey={hymnKey}
          dismissed={dismissed} onDismiss={onDismiss} />}
        </div>
        {isSelected &&
        <div className="wp-hymn-pnote">
            {openNote ?
          <textarea className="wp-hymn-pnote-input" placeholder="Why this hymn here? (planner notes)"
          value={note || ""} onChange={(e) => onNote(e.target.value)} rows={2} /> :

          <button className="wp-link" onClick={() => setOpenNote(true)}>
                <svg width="11" height="11" viewBox="0 0 11 11"><path d="M2 2 L9 2 L9 9 L2 9 Z M4 5 L7 5 M4 7 L6 7" stroke="currentColor" fill="none" strokeWidth="1" /></svg>
                Add a note
              </button>}
          </div>}
      </div>
    </div>);

}

// One reading: its top picks shown, the rest behind a per-reading disclosure.
// Can be marked "skipped" for Sundays where a reading isn't used.
function PassageBlock({ p, pi, rowFor, appearancesOf, currentTs, showN }) {
  const [open, setOpen] = useState(false);
  const perPassage = showN || PER_PASSAGE_PRIMARY;
  const canon = canonPassage(p.label);
  const curBook = bookOf(p.reference);
  // Soft within-tier demotion: hymns actually sung in the last 8 weeks sink to
  // the bottom of this reading's list (stable otherwise) — never removed.
  const hymns = (() => {
    if (!window.sungWithin) return p.hymns;
    const fresh = [], recent = [];
    p.hymns.forEach((h) => (window.sungWithin(`${h.hymnal}:${h.num}`, currentTs, 56) ? recent : fresh).push(h));
    return recent.length ? [...fresh, ...recent] : p.hymns;
  })();
  const shown = open ? hymns : hymns.slice(0, perPassage);
  const extra = hymns.length - perPassage;
  // Other readings (by book) the hymn also fits this Sunday -> "Also Matthew".
  const alsoFor = (h) => {
    const apps = appearancesOf[`${h.hymnal}:${h.num}`] || [];
    return [...new Set(apps.map((a) => a.book).filter((b) => b && b !== curBook))];
  };
  return (
    <div className="wp-hplan-passage" data-passage={canon}>
      <div className="wp-hplan-passage-h">
        <span className="wp-passage-tag">{p.label}</span>
        <span className="wp-hplan-passage-ref">{p.reference}</span>
      </div>
      {window.PASSAGE_THEMES && window.PASSAGE_THEMES[p.reference] &&
      <p className="wp-hplan-passage-gist">{window.PASSAGE_THEMES[p.reference]}</p>}
      <div className="wp-bucket-list">
        {shown.map((h, i) => rowFor(h, `hplan:p${pi}:${h.hymnal}:${h.num}:${i}`, canon, false, { also: alsoFor(h) }))}
      </div>
      {extra > 0 &&
      <button className="wp-disclosure" onClick={() => setOpen((o) => !o)} aria-expanded={open}>
        <span className={`wp-disclosure-caret ${open ? "on" : ""}`}>
          <svg width="9" height="9" viewBox="0 0 9 9"><path d="M2 2 L7 4.5 L2 7 Z" fill="currentColor" /></svg>
        </span>
        {open ? "Show fewer" : "Show more"}
        {!open && <span className="wp-disclosure-count">{extra}</span>}
        <span className="wp-disclosure-hint">{open ? "top picks first" : "more hymns for this reading"}</span>
      </button>}
    </div>);

}

function HymnPlanView({ plan, sunday, selected, toggle, notes, setNote, usage, currentDayId, currentTs, hymnalAllowed, hymnalProfile, hidden, toggleHidden, track: trackProp, changeTrack, rclVersion: rclVersionProp, changeRclVersion }) {
  const allow = hymnalAllowed || (() => true);
  const hiddenSet = hidden || new Set();

  // Per-reading "Show N" display knob (master-ranking Phase 4) — how many hymns
  // each reading shows before its "Show more" disclosure. A display preference,
  // not a data property; persisted globally under wp:hymnShowN.
  const [showN, setShowN] = useState(() => {
    const v = parseInt(localStorage.getItem("wp:hymnShowN") || "", 10);
    return [3, 4, 6, 10].includes(v) ? v : PER_PASSAGE_PRIMARY;
  });
  const changeShowN = (n) => {
    setShowN(n);
    try {localStorage.setItem("wp:hymnShowN", String(n));} catch (_) {}
  };

  // Lectionary track switch: Pentecost-season Sundays that carry both a
  // semi-continuous and a complementary Old Testament + Psalm, and any Sunday
  // offering the CCT's proposed revision. Remembered PER SUNDAY, so returning
  // to a Sunday restores the track you last set there. A Sunday you've never
  // set inherits your most recent choice (season default).
  // Lectionary track selection now lives in WorshipPlanner (shared with the
  // Contemporary and Solo tabs — same wp:hymnTrackByDay persistence) and
  // arrives as props. "both" is the neutral default.
  const track = trackProp || "both";

  // Advisory-pill dismissals, persisted per hymn-per-Sunday-per-type.
  const [dismissed, setDismissed] = useState(() => {
    try {return JSON.parse(localStorage.getItem("wp:pillDismiss") || "{}");} catch (_) {return {};}
  });
  const dismissPill = (id) => setDismissed((prev) => {
    const next = { ...prev, [id]: true };
    try {localStorage.setItem("wp:pillDismiss", JSON.stringify(next));} catch (_) {}
    return next;
  });
  const pillsFor = (h) =>
  window.computePills ? window.computePills(`${h.hymnal}:${h.num}`, sunday.id, currentTs) : null;
  const primary = (plan.primary || []).filter((h) => allow(h.hymnal, h.hymnals));
  const allPassages = (plan.passages || []).
  map((p) => ({ ...p, hymns: (p.hymns || []).filter((h) => allow(h.hymnal, h.hymnals)) })).
  filter((p) => p.hymns.length);

  // Which track sets this Sunday actually carries.
  const hasSemi = allPassages.some((p) => trackOf(p.label) === "semi");
  const hasComp = allPassages.some((p) => trackOf(p.label) === "comp");
  const hasCct = allPassages.some((p) => trackOf(p.label) === "cct");

  // Build the semi/comp track switch (separate from the CCT version toggle).
  const trackOpts = [];
  if (hasSemi) trackOpts.push(["semi", "Semi-continuous"]);
  if (hasComp) trackOpts.push(["comp", "Complementary"]);
  const showTrackSeg = trackOpts.length >= 2;
  // RCL version — traditional vs CCT revision. Arrives as a prop from
  // WorshipPlanner (shared state with Contemporary + Solo tabs).
  const rclVersion = hasCct ? (rclVersionProp || "traditional") : "traditional";

  // Clamp the semi/comp track; compose with the CCT version into a compound
  // effective track. Never auto-select CCT — default to the standard RCL.
  const has = (k) => trackOpts.some(([o]) => o === k);
  const baseTrack = trackOpts.length ? (has(track) ? track : trackOpts[0][0]) : null;
  const effTrack = rclVersion === "cct"
    ? (baseTrack ? "cct-" + baseTrack : "cct")
    : (baseTrack || "n/a");
  const eff = window.parseEffTrack ? window.parseEffTrack(effTrack)
    : { isCct: rclVersion === "cct", lect: baseTrack };

  // When a single track is chosen, hide the others' OT/Psalm; readings shared
  // by both tracks (Epistle, Gospel, "both tracks") always show. The CCT
  // proposed-revision readings appear only when the CCT version is selected.
  const passages = allPassages.filter((p) => {
    const t = trackOf(p.label);
    if (t === null || t === "both") return true;
    if (t === "cct") return eff.isCct;
    if (t === "semi" || t === "comp") return eff.lect ? t === eff.lect : true;
    return true;
  });

  // Where each visible hymn appears this Sunday: { book, canon } per reading.
  // Built from the *visible* readings so book tags and "Also …" track the
  // selected lectionary track.
  const appearancesOf = {};
  for (const p of passages) {
    const c = canonPassage(p.label);
    const book = bookOf(p.reference);
    for (const h of p.hymns || []) {
      const k = `${h.hymnal}:${h.num}`;
      (appearancesOf[k] = appearancesOf[k] || []).push({ book, canon: c });
    }
  }
  // Distinct books a hymn is tagged to, in reading order encountered.
  const booksFor = (k) => {
    const seen = new Set(), out = [];
    for (const a of appearancesOf[k] || []) {
      if (a.book && !seen.has(a.book)) {seen.add(a.book);out.push(a);}
    }
    return out;
  };

  // A "Top picks" hymn stays as long as at least one of this Sunday's readings
  // it is keyed to is still in use (per the shared Scripture-used bar). Picks
  // not tied to any of the day's four readings — e.g. a seasonal/occasion
  // choice — carry no canonical reading and always remain. Keying is read from
  // ALL of the day's readings (every lectionary track), so a pick stays/drops
  // by its scripture regardless of which track is currently shown.
  const canonKeysFor = (() => {
    const map = {};
    for (const p of allPassages) {
      const c = canonPassage(p.label);
      if (!c || c === "general") continue;
      for (const h of p.hymns || []) {
        const k = `${h.hymnal}:${h.num}`;
        (map[k] = map[k] || new Set()).add(c);
      }
    }
    return map;
  })();

  // Fallback keying for "Top picks" that were never mirrored into a per-reading
  // bucket (a data gap: the pick lives only in `primary`, so the passage-join
  // above finds nothing). Recover its reading from the scripture named in the
  // prose `reason`, matched against this Sunday's actual readings — giving the
  // pick the same book chip and set-aside behavior as a bucketed hymn. Applied
  // only when the bucket join came up empty, so it never overrides real data.
  const readingBooks = (list) =>
  list.map((p) => ({ canon: canonPassage(p.label), book: bookOf(p.reference) })).
  filter((r) => r.book && r.canon && r.canon !== "general");
  const matchReason = (reason, list) => {
    const out = [], seen = new Set();
    for (const r of list) {
      // Word-boundary match so "John" doesn't fire inside "1 John", etc.
      const re = new RegExp("(^|[^\\w])" + r.book.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "(?![\\w])");
      if (!seen.has(r.book) && re.test(reason)) {seen.add(r.book);out.push(r);}
    }
    return out;
  };
  const visBooks = readingBooks(passages);
  const allBooks = readingBooks(allPassages);
  for (const h of plan.primary || []) {
    const k = `${h.hymnal}:${h.num}`;
    if (!(appearancesOf[k] && appearancesOf[k].length)) {
      const m = matchReason(h.reason || "", visBooks);
      if (m.length) appearancesOf[k] = m.map((r) => ({ book: r.book, canon: r.canon }));
    }
    if (!(canonKeysFor[k] && canonKeysFor[k].size)) {
      const m = matchReason(h.reason || "", allBooks);
      if (m.length) canonKeysFor[k] = new Set(m.map((r) => r.canon));
    }
  }

  const primaryVisible = (h) => {
    const canons = [...(canonKeysFor[`${h.hymnal}:${h.num}`] || [])];
    return !canons.length || canons.some((c) => !hiddenSet.has(c));
  };

  // ----- Top picks: always offer up to four, backfilling by score -----
  // Keep every still-valid curated pick, then top the list back up to four from
  // the highest-scoring hymns of the readings still in use — so setting a
  // reading aside repopulates the list with fresh recommendations instead of
  // just leaving it shorter.
  const TOP_PICKS = 4;
  const rankVal = (h) => {const m = /R(\d+)/.exec(h.rank || "");return m ? +m[1] : 0;};
  const MATCH_RANK = { direct: 3, thematic: 2, atmospheric: 1 };
  // Higher = stronger: lectionary rank dominates, then match quality, then
  // United-Methodist planner frequency, traditional-source count, cross bonus.
  const scoreOf = (h) =>
  rankVal(h) * 1000 + (MATCH_RANK[h.match] || 0) * 100 +
  (h.um_freq || 0) * 5 + (h.trad || 0) * 3 + (h.cross ? 2 : 0);
  // Hymns actually sung in the last 8 weeks sink to the bottom (never removed).
  const demoteSung = (arr) => {
    if (!window.sungWithin) return arr;
    const fresh = [], recent = [];
    arr.forEach((h) => (window.sungWithin(`${h.hymnal}:${h.num}`, currentTs, 56) ? recent : fresh).push(h));
    return recent.length ? [...fresh, ...recent] : arr;
  };
  const keptPrimary = demoteSung(primary.filter(primaryVisible));
  const topPicks = (() => {
    const out = [...keptPrimary];
    const seen = new Set(out.map((h) => `${h.hymnal}:${h.num}`));
    const need = TOP_PICKS - out.length;
    if (need > 0) {
      const pool = [];
      for (const p of passages) {
        // Pull backfill only from readings still in use this Sunday.
        if (hiddenSet.has(canonPassage(p.label))) continue;
        for (const h of p.hymns || []) {
          const k = `${h.hymnal}:${h.num}`;
          if (seen.has(k)) continue;
          seen.add(k);
          pool.push(h);
        }
      }
      pool.sort((a, b) => scoreOf(b) - scoreOf(a));
      out.push(...demoteSung(pool).slice(0, need));
    }
    return out;
  })();

  // A selectable hymn object the service plan + usage index understand:
  // reason becomes the row note; match==="direct" lights the "direct" tag.
  const selHymnOf = (h) => ({
    hymnal: h.hymnal, num: h.num, title: h.title,
    tune: window.tuneFor ? window.tuneFor(h) : null,
    note: h.reason, direct: h.match === "direct"
  });

  // Identity-aware selection: the same hymn shows in both "Top picks" and the
  // "By reading" list (and can recur across readings) under DIFFERENT keys. A
  // hymn is programmed once per day, so we mirror its selected state across
  // every appearance and route a click to the one entry that's actually
  // selected — selecting it as a Top pick lights it below, and vice versa, so
  // you can't accidentally schedule "O Jesus, I Have Promised" twice.
  const keyIdentity = (k) => {
    let m = /^hplan:primary:(.+)$/.exec(k);
    if (m) return m[1];
    m = /^hplan:p\d+:(.+):\d+$/.exec(k);
    if (m) return m[1];
    return null;
  };
  const selectedIds = new Set();
  selected.forEach((k) => {const id = keyIdentity(k);if (id) selectedIds.add(id);});
  const existingKeyForId = (id) => {
    for (const k of selected) if (keyIdentity(k) === id) return k;
    return null;
  };

  const rowFor = (h, key, passage, isPrimary, extra) => {
    const id = `${h.hymnal}:${h.num}`;
    const existing = existingKeyForId(id); // the key currently holding this hymn, if any
    const canonKey = existing || key; // notes follow the actual selected entry
    return (
      <PlanHymnRow key={key} h={h} primary={isPrimary} profile={hymnalProfile}
      books={extra && extra.books} also={extra && extra.also}
      pills={pillsFor(h)} sundayId={sunday.id} dismissed={dismissed} onDismiss={dismissPill}
      isSelected={selectedIds.has(id)}
      onToggle={() => toggle(existing || key, selHymnOf(h), passage, "traditional")}
      note={notes[canonKey]} onNote={(v) => setNote(canonKey, v)}
      usage={usage} currentDayId={currentDayId} currentTs={currentTs} />);

  };


  return (
    <section className="wp-hplan">
      {topPicks.length > 0 &&
      <>
          <div className="wp-cols-h">
            <span>Top picks for the day</span>
            <span className="wp-cols-h-hint">Cross-reading picks · tap to add to the plan</span>
          </div>
          <div className="wp-hplan-primary">
            {topPicks.map((h) => {
              // Identity-based keys keep each pick's selection stable as the
              // list repopulates when readings are set aside or restored.
              const books = booksFor(`${h.hymnal}:${h.num}`);
              return rowFor(h, `hplan:primary:${h.hymnal}:${h.num}`, (books[0] && books[0].canon) || "general", true, { books });
            })}
          </div>
        </>}

      {passages.length > 0 &&
      <>
          <div className="wp-cols-h" style={{ marginTop: primary.length ? 28 : 0 }}>
            <span>By reading</span>
            <span className="wp-cols-h-hint">Top {showN} per reading · open each for the full list</span>
            <span className="wp-seg wp-hplan-shown" role="group" aria-label="Hymns shown per reading"
            title="How many hymns each reading shows before its Show-more disclosure">
              <span className="wp-seg-label">Show</span>
              {[3, 4, 6, 10].map((n) =>
              <button key={n} className={`wp-seg-btn ${showN === n ? "on" : ""}`}
              onClick={() => changeShowN(n)} aria-pressed={showN === n}>{n}</button>
              )}
            </span>
          </div>
          <div className="wp-hplan-passages">
            {passages.filter((p) => !hiddenSet.has(canonPassage(p.label))).map((p, pi) =>
            <PassageBlock key={pi} p={p} pi={pi} rowFor={rowFor} showN={showN}
            appearancesOf={appearancesOf} currentTs={currentTs} />
            )}
          </div>
        </>}
    </section>);

}

window.HymnPlanView = HymnPlanView;
