// Rhythmic Oscillations — transmission archive (table + grid views).
// Live source: SoundCloud set https://soundcloud.com/htxedifice/sets/rhythmic-oscillations
// Pulled at page load via the SoundCloud Widget API so adding a new track to
// the set automatically appears here on next visit.
//
// Cover artwork: site/assets/ro/<num>.png — matched to live tracks by parsing
// the episode number out of each SoundCloud title.
//
// Tracklists (when available) come from window.RO_TRACKLISTS in
// site/data/tracklists.js.

const SC_SET_URL = 'https://soundcloud.com/htxedifice/sets/rhythmic-oscillations';
const SC_IFRAME_SRC =
  'https://w.soundcloud.com/player/?url=' + encodeURIComponent(SC_SET_URL) +
  '&visual=false&show_artwork=false&show_user=false&auto_play=false';

const RO_COVER_COUNT = 91;

// Date sources, in priority order:
//   1. The (YYYY.MM.DD) parenthetical in the SC title (= broadcast date)
//   2. window.RO_DATES[num] from data/episode_dates.js (same source, baked in)
//   3. RO_ANCHORS interpolation as last-resort fallback (offline + no static date)
const RO_ANCHORS = [
  { num: 1,  ts: Date.UTC(2023, 0, 14) },
  { num: 42, ts: Date.UTC(2024, 4, 13) },
  { num: 91, ts: Date.UTC(2026, 3, 25) },
];
function epDateFallback(n) {
  const a = n <= 42 ? RO_ANCHORS[0] : RO_ANCHORS[1];
  const b = n <= 42 ? RO_ANCHORS[1] : RO_ANCHORS[2];
  const t = (n - a.num) / (b.num - a.num);
  return new Date(a.ts + t * (b.ts - a.ts));
}
// Parse "Rhythmic Oscillations 001 (2023.01.14) by ..." → Date(2023-01-14 UTC)
function epDateFromTitle(title) {
  if (!title) return null;
  const m = title.match(/\((\d{4})[./-](\d{1,2})[./-](\d{1,2})\)/);
  if (!m) return null;
  return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
}
function epDateStatic(n) {
  const s = (window.RO_DATES || {})[n];
  if (!s) return null;
  const m = s.match(/^(\d{4})[./-](\d{1,2})[./-](\d{1,2})$/);
  if (!m) return null;
  return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
}
function epDate(num, title) {
  return epDateFromTitle(title) || epDateStatic(num) || epDateFallback(num);
}
function fmtEpDate(d) {
  return `${d.getUTCFullYear()}.${String(d.getUTCMonth() + 1).padStart(2, '0')}.${String(d.getUTCDate()).padStart(2, '0')}`;
}
function fmtDuration(ms) {
  if (!ms || ms < 1000) return '—';
  const s = Math.round(ms / 1000);
  const m = Math.floor(s / 60);
  const r = s % 60;
  return `${String(m).padStart(2, '0')}:${String(r).padStart(2, '0')}`;
}

// Parse "Rhythmic Oscillations Episode 091" or "RO 91" or "Episode 091" → 91.
function epNumFromTitle(title) {
  if (!title) return null;
  const m = title.match(/(?:episode|ep\.?|ro)\D*?(\d{1,3})/i) || title.match(/\b(\d{1,3})\b/);
  return m ? parseInt(m[1], 10) : null;
}

// Seeded RNG so a "random" view stays stable per render but reshuffles when re-picked.
function mulberry32(a) {
  return function () {
    let t = (a = (a + 0x6d2b79f5) | 0);
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

// Static catalog used when SoundCloud is unreachable (offline / blocked).
function buildFallbackEpisodes() {
  return Array.from({ length: RO_COVER_COUNT }, (_, i) => {
    const num = i + 1;
    const d = epDate(num, null);
    return {
      num,
      title: `Rhythmic Oscillations · Episode ${String(num).padStart(3, '0')}`,
      url: SC_SET_URL,
      date: d,
      year: d.getUTCFullYear(),
      duration: null,
      cover: `assets/ro/${num}.png`,
      live: false,
    };
  });
}

// Convert a Widget-API sound payload into our episode shape.
function soundToEpisode(s) {
  const num = epNumFromTitle(s.title);
  const d = epDate(num, s.title);
  return {
    num,
    title: s.title || `Episode ${num || '—'}`,
    url: s.permalink_url || SC_SET_URL,
    date: d,
    year: d.getUTCFullYear(),
    duration: s.duration || null,
    cover: num && num >= 1 && num <= RO_COVER_COUNT ? `assets/ro/${num}.png` : null,
    live: true,
  };
}

function Radio({ t }) {
  const [year, setYear] = React.useState('ALL');
  const [sort, setSort] = React.useState('new');
  const [view, setView] = React.useState('list');
  const [seed, setSeed] = React.useState(() => (Math.random() * 1e9) | 0);
  const [visible, setVisible] = React.useState(24);
  const [expanded, setExpanded] = React.useState(null); // ep.num for tracklist accordion
  const [liveByNum, setLiveByNum] = React.useState(() => new Map());
  const [liveStatus, setLiveStatus] = React.useState('loading'); // loading | live | offline
  const iframeRef = React.useRef(null);

  // Subscribe to the SoundCloud set on mount. Polls a few times because the
  // Widget API sometimes returns the set in chunks; we accumulate the maximum.
  React.useEffect(() => {
    if (!iframeRef.current || typeof SC === 'undefined' || !SC.Widget) {
      setLiveStatus('offline');
      return;
    }
    const widget = SC.Widget(iframeRef.current);
    const seen = new Map();
    const timeouts = [];

    const refresh = (label) => {
      widget.getSounds((sounds) => {
        if (!sounds || !sounds.length) return;
        for (const s of sounds) {
          const ep = soundToEpisode(s);
          if (ep.num != null) seen.set(ep.num, ep);
        }
        // eslint-disable-next-line no-console
        console.log(`[Radio] ${label}: getSounds → ${sounds.length} sounds, ${seen.size} unique episodes accumulated`);
        setLiveByNum(new Map(seen));
        setLiveStatus(seen.size ? 'live' : 'offline');
      });
    };

    widget.bind(SC.Widget.Events.READY, () => {
      refresh('READY');
      // Poll in case more sounds load asynchronously after the iframe settles.
      [800, 2000, 4500, 9000].forEach((d) => {
        timeouts.push(setTimeout(() => refresh(`+${d}ms`), d));
      });
    });

    const giveUp = setTimeout(() => {
      if (!seen.size) setLiveStatus('offline');
    }, 12000);
    timeouts.push(giveUp);

    return () => timeouts.forEach(clearTimeout);
  }, []);

  // SoundCloud is the source of truth: only episodes published on SC appear here.
  // Live data merges with the static cover/date catalog so each row gets its
  // artwork. When SC is unreachable we fall back to the full static catalog so
  // the page isn't blank — but in normal operation, anything not on SC is
  // hidden, and new SC uploads appear automatically on next page load.
  const episodes = React.useMemo(() => {
    const base = buildFallbackEpisodes();
    if (!liveByNum.size) return base;
    const baseByNum = new Map(base.map((e) => [e.num, e]));
    return Array.from(liveByNum.values()).map((live) => {
      const baseEp = live.num != null ? baseByNum.get(live.num) : null;
      return baseEp ? { ...baseEp, ...live, live: true } : live;
    });
  }, [liveByNum]);

  const onSortChange = (next) => {
    if (next === 'rand') setSeed((Math.random() * 1e9) | 0);
    setSort(next);
  };

  const years = React.useMemo(
    () => ['ALL', ...Array.from(new Set(episodes.map((e) => e.year))).sort((a, b) => b - a)],
    [episodes]
  );

  const filtered = React.useMemo(() => {
    let arr = year === 'ALL' ? episodes.slice() : episodes.filter((e) => e.year === year);
    if (sort === 'new') arr.sort((a, b) => b.num - a.num);
    else if (sort === 'old') arr.sort((a, b) => a.num - b.num);
    else {
      const rng = mulberry32(seed);
      for (let i = arr.length - 1; i > 0; i--) {
        const j = Math.floor(rng() * (i + 1));
        [arr[i], arr[j]] = [arr[j], arr[i]];
      }
    }
    return arr;
  }, [episodes, year, sort, seed]);

  const shown = filtered.slice(0, visible);

  const liveCount = liveByNum.size;
  const statusLabel = liveStatus === 'loading'
    ? '◐ SYNCING WITH SOUNDCLOUD'
    : liveStatus === 'live'
      ? `● ${liveCount} OF ${RO_COVER_COUNT} PUBLISHED ON SOUNDCLOUD`
      : '○ OFFLINE CATALOG';

  return (
    <section id="radio" data-screen-label="04 Rhythmic Oscillations" className="sec sec-radio">
      <SectionHead num="04" title="Rhythmic Oscillations" sub="KTRU 96.1 FM · Houston · biweekly transmission" />

      <p className="ro-intro">
        Rhythmic Oscillations is broadcasted <em>Saturday 11 AM – 12 PM CST</em> on
        96.1 FM KTRU Houston and on <a href="https://www.ktru.org" target="_blank" rel="noreferrer">www.KTRU.org</a>.
        The show is dedicated to redefining the boundaries of electronic and dance
        music, exploring its many influences and genres, as well as taking listeners
        on a sonic journey that stretches the fabric of time and space.
      </p>

      {/* Hidden Widget-API iframe — drives the live episode list. */}
      <iframe
        ref={iframeRef}
        title="SoundCloud Widget Bridge"
        src={SC_IFRAME_SRC}
        className="ro-sc-bridge"
        aria-hidden="true"
      />

      <div className="ro-controls">
        <div className="ro-count-row">
          <div className="ro-count">[ {filtered.length} TRANSMISSIONS ]</div>
          <div className={`ro-status ro-status-${liveStatus}`}>{statusLabel}</div>
        </div>
        <div className="ro-filters">
          <div className="ro-filter">
            <label className="ro-filter-lbl">Year</label>
            <select
              value={year}
              onChange={(e) => {
                setYear(e.target.value === 'ALL' ? 'ALL' : Number(e.target.value));
                setVisible(24);
              }}
            >
              {years.map((y) => <option key={y} value={y}>{y === 'ALL' ? 'All' : y}</option>)}
            </select>
          </div>
          <div className="ro-filter">
            <label className="ro-filter-lbl">Sort</label>
            <select value={sort} onChange={(e) => onSortChange(e.target.value)}>
              <option value="new">New &gt; Old</option>
              <option value="old">Old &gt; New</option>
              <option value="rand">Random</option>
            </select>
          </div>
          {sort === 'rand' && (
            <button
              type="button"
              className="ro-reshuffle"
              aria-label="Reshuffle"
              onClick={() => setSeed((Math.random() * 1e9) | 0)}
            >
              ↻ Shuffle
            </button>
          )}
          <div className="ro-views">
            <button data-on={view === 'list'} onClick={() => setView('list')}>LIST</button>
            <button data-on={view === 'grid'} onClick={() => setView('grid')}>GRID</button>
          </div>
        </div>
      </div>

      {view === 'list' ? (
        <div className="ro-table">
          <div className="ro-row ro-head">
            <div>CAT</div>
            <div>DATE</div>
            <div>COVER</div>
            <div>TITLE</div>
            <div>LEN</div>
            <div></div>
          </div>
          {shown.map((ep) => {
            const tracks = ep.num != null && window.RO_TRACKLISTS ? window.RO_TRACKLISTS[ep.num] : null;
            const isOpen = expanded === ep.num;
            const cat = ep.num != null ? `RO${String(ep.num).padStart(3, '0')}` : 'RO???';
            return (
              <React.Fragment key={ep.num != null ? ep.num : ep.title}>
                <a
                  className="ro-row"
                  href={ep.url}
                  target="_blank"
                  rel="noreferrer"
                  data-open={isOpen}
                >
                  <div className="ro-cat">{cat}</div>
                  <div className="num">{fmtEpDate(ep.date)}</div>
                  <div className="ro-thumb">
                    {ep.cover
                      ? <img src={ep.cover} alt="" loading="lazy" decoding="async" />
                      : <div className="ro-thumb-blank" />}
                  </div>
                  <div className="ro-title-cell">
                    <span className="ro-title-name">{stripCatPrefix(ep.title, ep.num)}</span>
                    {tracks && (
                      <button
                        type="button"
                        className="ro-tracklist-toggle"
                        onClick={(e) => { e.preventDefault(); e.stopPropagation(); setExpanded(isOpen ? null : ep.num); }}
                        aria-expanded={isOpen}
                      >
                        {isOpen ? '▾' : '▸'} {tracks.length} TRACKS
                      </button>
                    )}
                  </div>
                  <div className="num">{fmtDuration(ep.duration)}</div>
                  <div className="ro-arrow">↗</div>
                </a>
                {isOpen && tracks && (
                  <div className="ro-tracklist">
                    <ol>
                      {tracks.map((tr, i) => (
                        <li key={i}>
                          <span className="ro-tracklist-n num">{String(i + 1).padStart(2, '0')}.</span>
                          <span className="ro-tracklist-track">{tr.track}</span>
                          <span className="ro-tracklist-artist dim">{tr.artist}</span>
                        </li>
                      ))}
                    </ol>
                  </div>
                )}
              </React.Fragment>
            );
          })}
        </div>
      ) : (
        <div className="ro-grid">
          {shown.map((ep) => {
            const cat = ep.num != null ? `RO${String(ep.num).padStart(3, '0')}` : 'RO???';
            return (
              <a key={ep.num != null ? ep.num : ep.title} className="ro-card" href={ep.url} target="_blank" rel="noreferrer">
                <div className="ro-card-cover">
                  {ep.cover
                    ? <img src={ep.cover} alt="" loading="lazy" decoding="async" />
                    : <div className="ro-thumb-blank" />}
                </div>
                <div className="ro-card-meta">
                  <div className="ro-card-cat">{cat} · {fmtEpDate(ep.date)}</div>
                  <div className="ro-card-djs">{stripCatPrefix(ep.title, ep.num)}</div>
                </div>
              </a>
            );
          })}
        </div>
      )}

      {visible < filtered.length && (
        <button className="ro-load" onClick={() => setVisible((v) => v + (view === 'grid' ? 18 : 24))}>
          Load More ↓
        </button>
      )}

      <div className="ro-foot">
        <div className="ro-foot-bracket">
          ━━ FULL PLAYLIST AT
          <a href={SC_SET_URL} target="_blank" rel="noreferrer">soundcloud.com/htxedifice/sets/rhythmic-oscillations</a>
          / KTRU 96.1 · since 2023
        </div>
      </div>
    </section>
  );
}

// Trim "Rhythmic Oscillations · Episode 091" → "Episode 091" or shorter, so the
// title cell doesn't repeat the section name on every row.
function stripCatPrefix(title, num) {
  if (!title) return num != null ? `Episode ${String(num).padStart(3, '0')}` : '—';
  let s = title.replace(/rhythmic\s*oscillations/i, '').trim();
  s = s.replace(/^[\s·:|\-—]+/, '').trim();
  return s || (num != null ? `Episode ${String(num).padStart(3, '0')}` : title);
}

window.Radio = Radio;
