/* =====================================================================
   daily-word-core.js  —  v1.0
   Extracted verbatim from "Reading Plan Builder v2.dc.html" lines 308–558
   (constructor … dateOf). renderVals() is deliberately NOT included: it is
   the DC view-model and gets rewritten as render-to-HTML functions.

   Nothing below the SHIM banner has been retyped or edited. If a line here
   differs from the prototype, that is a bug in the extraction, not a port
   decision.
   ===================================================================== */

/* ---------- DC SHIM (replaces DCLogic + localStorage under Node/Rock) ---------- */
class DCLogic {
  constructor(props) { this.props = props || {}; this.state = {}; }
  setState(patch, cb) {
    const p = (typeof patch === "function") ? patch(this.state) : patch;
    if (p) Object.assign(this.state, p);
    if (cb) cb();
  }
}

// Prototype's load()/save() read a global `localStorage`. Under Node it is
// absent (ReferenceError -> caught -> returns null), which is why the class
// runs unmodified. Provide a memory store so save/load round-trips testably.
if (typeof globalThis.localStorage === "undefined") {
  const mem = new Map();
  globalThis.localStorage = {
    getItem: (k) => (mem.has(k) ? mem.get(k) : null),
    setItem: (k, v) => { mem.set(k, String(v)); },
    removeItem: (k) => { mem.delete(k); },
    clear: () => mem.clear()
  };
}

/* ---------- END SHIM — verbatim extraction begins ---------- */

class Component extends DCLogic {
  constructor(props) {
    super(props);
    this.BOOKS = [
      ["Genesis",50],["Exodus",40],["Leviticus",27],["Numbers",36],["Deuteronomy",34],
      ["Joshua",24],["Judges",21],["Ruth",4],["1 Samuel",31],["2 Samuel",24],
      ["1 Kings",22],["2 Kings",25],["1 Chronicles",29],["2 Chronicles",36],["Ezra",10],
      ["Nehemiah",13],["Esther",10],["Job",42],["Psalms",150],["Proverbs",31],
      ["Ecclesiastes",12],["Song of Songs",8],["Isaiah",66],["Jeremiah",52],["Lamentations",5],
      ["Ezekiel",48],["Daniel",12],["Hosea",14],["Joel",3],["Amos",9],
      ["Obadiah",1],["Jonah",4],["Micah",7],["Nahum",3],["Habakkuk",3],
      ["Zephaniah",3],["Haggai",2],["Zechariah",14],["Malachi",4],
      ["Matthew",28],["Mark",16],["Luke",24],["John",21],["Acts",28],
      ["Romans",16],["1 Corinthians",16],["2 Corinthians",13],["Galatians",6],["Ephesians",6],
      ["Philippians",4],["Colossians",4],["1 Thessalonians",5],["2 Thessalonians",3],["1 Timothy",6],
      ["2 Timothy",4],["Titus",3],["Philemon",1],["Hebrews",13],["James",5],
      ["1 Peter",5],["2 Peter",3],["1 John",5],["2 John",1],["3 John",1],
      ["Jude",1],["Revelation",22]
    ];
    const names = this.BOOKS.map(b => b[0]);
    const range = (a, b) => names.slice(names.indexOf(a), names.indexOf(b) + 1);
    this.PRESETS = [
      { name: "Whole Bible", books: names },
      { name: "Old Testament", books: range("Genesis","Malachi") },
      { name: "New Testament", books: range("Matthew","Revelation") },
      { name: "Pentateuch", books: range("Genesis","Deuteronomy") },
      { name: "OT history", books: range("Joshua","Esther") },
      { name: "Wisdom & poetry", books: range("Job","Song of Songs") },
      { name: "Psalms", books: ["Psalms"] },
      { name: "Proverbs", books: ["Proverbs"] },
      { name: "Prophets", books: range("Isaiah","Malachi") },
      { name: "Major prophets", books: range("Isaiah","Daniel") },
      { name: "Minor prophets", books: range("Hosea","Malachi") },
      { name: "Gospels", books: range("Matthew","John") },
      { name: "Acts", books: ["Acts"] },
      { name: "Paul's letters", books: range("Romans","Philemon") },
      { name: "General letters", books: range("Hebrews","Jude") },
      { name: "Epistles", books: range("Romans","Jude") },
      { name: "Revelation", books: ["Revelation"] }
    ];
    this.FREQ_STEPS = [0.5, 1, 2, 3, 4, 6, 12, 24, 52];
    const otx = range("Genesis","Malachi").filter(n => n !== "Psalms");
    this.TEMPLATES = [
      { name: "Whole Bible in a year", desc: "Cover to cover — about 3¼ chapters a day.", sels: [
        { name: "Whole Bible", baseName: "Whole Bible", books: names, freq: 1 }] },
      { name: "M'Cheyne", desc: "The classic 1842 Scottish plan: OT once, NT and Psalms twice — about 4 chapters a day.", sels: [
        { name: "Rest of OT", baseName: "Old Testament", books: otx, freq: 1 },
        { name: "New Testament", baseName: "New Testament", books: range("Matthew","Revelation"), freq: 2 },
        { name: "Psalms", baseName: "Psalms", books: ["Psalms"], freq: 2 }] },
      { name: "New Testament in a year", desc: "A gentle pace — one chapter most days.", sels: [
        { name: "New Testament", baseName: "New Testament", books: range("Matthew","Revelation"), freq: 1 }] },
      { name: "Psalms & Proverbs monthly", desc: "The historic wisdom habit: five psalms and a proverb a day, twelve times a year.", sels: [
        { name: "Psalms", baseName: "Psalms", books: ["Psalms"], freq: 12 },
        { name: "Proverbs", baseName: "Proverbs", books: ["Proverbs"], freq: 12 }] },
      { name: "Gospels every season", desc: "Live in the life of Jesus — all four Gospels, four times a year.", sels: [
        { name: "Gospels", baseName: "Gospels", books: range("Matthew","John"), freq: 4 }] },
      { name: "Whole Bible in 90 days", desc: "Intensive: about 13 chapters a day, four passes in a year.", sels: [
        { name: "Whole Bible", baseName: "Whole Bible", books: names, freq: 4 }] }
    ];
    const saved = this.load();
    this.state = saved || {
      view: "setup",
      selections: [
        { name: "Psalms", books: ["Psalms"], freq: 3 },
        { name: "Gospels", books: range("Matthew","John"), freq: 4 },
        { name: "Acts", books: ["Acts"], freq: 12 },
        { name: "Rest of OT", baseName: "Old Testament", books: range("Genesis","Malachi").filter(n => n !== "Psalms"), freq: 0.5 }
      ],
      startDate: null, checks: {}, viewDay: 0
    };
  }

  load() {
    try {
      const raw = localStorage.getItem("gnc-reading-plan");
      if (!raw) return null;
      const s = JSON.parse(raw);
      if (s && s.startDate) s.viewDay = this.todayIndex(s.startDate);
      return s;
    } catch (e) { return null; }
  }
  save() {
    const { view, selections, startDate, checks, viewDay, readerFont } = this.state;
    try { localStorage.setItem("gnc-reading-plan", JSON.stringify({ view, selections, startDate, checks, viewDay, readerFont })); } catch (e) {}
  }
  setSt(patch) { this.setState(patch, () => this.save()); }
  // While editing an existing plan, changes are a draft: held in memory until "Save my changes"
  editSt(patch) {
    if (this.state.startDate && this.state.view === "setup") this.setState(patch);
    else this.setSt(patch);
  }

  chaptersOf(books) { return books.reduce((s, n) => s + this.BOOKS.find(b => b[0] === n)[1], 0); }

  // Add a selection; where it overlaps an existing one, the smaller (more specific) selection
  // keeps the books and the larger one becomes "Rest of X". Nothing is ever deleted.
  addSelection(name, books, freq) {
    const short = { "Old Testament": "OT", "New Testament": "NT" };
    const restName = (base) => "Rest of " + (short[base] || base);
    let newBooks = [...books];
    const sels = this.state.selections.map(s => ({ ...s }));
    sels.forEach(sel => {
      if (!sel.books.some(b => newBooks.includes(b))) return;
      if (this.chaptersOf(sel.books) > this.chaptersOf(newBooks)) {
        const base = sel.baseName || sel.name;
        sel.books = sel.books.filter(b => !newBooks.includes(b));
        sel.name = restName(base);
        sel.baseName = base;
      } else {
        newBooks = newBooks.filter(b => !sel.books.includes(b));
      }
    });
    if (!newBooks.length) return; // fully covered by equal or more specific selections
    const label = newBooks.length < books.length ? restName(name) : name;
    this.editSt({ selections: [...sels.filter(s => s.books.length), { name: label, baseName: name, books: newBooks, freq }] });
  }
  todayIndex(startDate) {
    const ms = Date.now() - new Date(startDate + "T00:00:00").getTime();
    return Math.max(0, Math.min(364, Math.floor(ms / 86400000)));
  }

  // chapters for selection `sel` on day i (0..364): list of {book, ch}
  dayChapters(sel, i) {
    const seq = [];
    sel.books.forEach(n => {
      const c = this.BOOKS.find(b => b[0] === n)[1];
      for (let k = 1; k <= c; k++) seq.push({ book: n, ch: k });
    });
    const C = seq.length, total = Math.round(C * sel.freq);
    if (!total) return [];
    const a = Math.floor(i * total / 365), b = Math.floor((i + 1) * total / 365);
    const out = [];
    for (let k = a; k < b; k++) out.push(seq[k % C]);
    return out;
  }

  abbrev(book) {
    if (!this.ABBR) this.ABBR = { "Genesis":"Gn","Exodus":"Ex","Leviticus":"Lv","Numbers":"Nm","Deuteronomy":"Dt","Joshua":"Jos","Judges":"Jdg","Ruth":"Ru","1 Samuel":"1Sm","2 Samuel":"2Sm","1 Kings":"1Kg","2 Kings":"2Kg","1 Chronicles":"1Ch","2 Chronicles":"2Ch","Ezra":"Ezr","Nehemiah":"Neh","Esther":"Est","Job":"Job","Psalms":"Ps","Proverbs":"Prv","Ecclesiastes":"Ecc","Song of Songs":"Sng","Isaiah":"Isa","Jeremiah":"Jer","Lamentations":"Lam","Ezekiel":"Ezk","Daniel":"Dan","Hosea":"Hos","Joel":"Joel","Amos":"Am","Obadiah":"Ob","Jonah":"Jon","Micah":"Mic","Nahum":"Nah","Habakkuk":"Hab","Zephaniah":"Zep","Haggai":"Hag","Zechariah":"Zec","Malachi":"Mal","Matthew":"Mt","Mark":"Mk","Luke":"Lk","John":"Jn","Acts":"Ac","Romans":"Rom","1 Corinthians":"1Co","2 Corinthians":"2Co","Galatians":"Gal","Ephesians":"Eph","Philippians":"Php","Colossians":"Col","1 Thessalonians":"1Th","2 Thessalonians":"2Th","1 Timothy":"1Tm","2 Timothy":"2Tm","Titus":"Ti","Philemon":"Phm","Hebrews":"Heb","James":"Jas","1 Peter":"1Pt","2 Peter":"2Pt","1 John":"1Jn","2 John":"2Jn","3 John":"3Jn","Jude":"Jud","Revelation":"Rev" };
    return this.ABBR[book] || book;
  }

  formatChapters(chs, abbr) {
    if (!chs.length) return "";
    const parts = [];
    let start = chs[0], prev = chs[0];
    const flush = () => {
      const bk = abbr ? this.abbrev(start.book) : (start.book === "Psalms" ? "Psalm" : start.book);
      const one = this.BOOKS.find(b => b[0] === start.book)[1] === 1;
      if (one) parts.push(bk);
      else if (start.ch === prev.ch) parts.push(bk + " " + start.ch);
      else parts.push(bk + " " + start.ch + "–" + prev.ch);
    };
    for (let i = 1; i < chs.length; i++) {
      const c = chs[i];
      if (c.book === prev.book && c.ch === prev.ch + 1) { prev = c; continue; }
      flush(); start = c; prev = c;
    }
    flush();
    return parts.join("; ");
  }

  freqLabel(f) {
    if (f < 0.5) return "every 2 years";
    if (f === 0.5) return "every 2 years";
    if (f === 1) return "once a year";
    if (f === 12) return "once a month";
    if (f === 52) return "once a week";
    if (f === 24) return "24× a year";
    return f + "× a year";
  }

  // --- API.Bible reader (rest.api.bible) ---
  osis(book) {
    if (!this.OSIS) this.OSIS = { "Genesis":"GEN","Exodus":"EXO","Leviticus":"LEV","Numbers":"NUM","Deuteronomy":"DEU","Joshua":"JOS","Judges":"JDG","Ruth":"RUT","1 Samuel":"1SA","2 Samuel":"2SA","1 Kings":"1KI","2 Kings":"2KI","1 Chronicles":"1CH","2 Chronicles":"2CH","Ezra":"EZR","Nehemiah":"NEH","Esther":"EST","Job":"JOB","Psalms":"PSA","Proverbs":"PRO","Ecclesiastes":"ECC","Song of Songs":"SNG","Isaiah":"ISA","Jeremiah":"JER","Lamentations":"LAM","Ezekiel":"EZK","Daniel":"DAN","Hosea":"HOS","Joel":"JOL","Amos":"AMO","Obadiah":"OBA","Jonah":"JON","Micah":"MIC","Nahum":"NAM","Habakkuk":"HAB","Zephaniah":"ZEP","Haggai":"HAG","Zechariah":"ZEC","Malachi":"MAL","Matthew":"MAT","Mark":"MRK","Luke":"LUK","John":"JHN","Acts":"ACT","Romans":"ROM","1 Corinthians":"1CO","2 Corinthians":"2CO","Galatians":"GAL","Ephesians":"EPH","Philippians":"PHP","Colossians":"COL","1 Thessalonians":"1TH","2 Thessalonians":"2TH","1 Timothy":"1TI","2 Timothy":"2TI","Titus":"TIT","Philemon":"PHM","Hebrews":"HEB","James":"JAS","1 Peter":"1PE","2 Peter":"2PE","1 John":"1JN","2 John":"2JN","3 John":"3JN","Jude":"JUD","Revelation":"REV" };
    return this.OSIS[book];
  }

  // contiguous same-book runs → API passage ids like "PSA.1-PSA.3"
  passageIds(chs) {
    const ids = [];
    let start = chs[0], prev = chs[0];
    const flush = () => {
      const b = this.osis(start.book);
      ids.push(start.ch === prev.ch ? b + "." + start.ch : b + "." + start.ch + "-" + b + "." + prev.ch);
    };
    for (let i = 1; i < chs.length; i++) {
      const c = chs[i];
      if (c.book === prev.book && c.ch === prev.ch + 1) { prev = c; continue; }
      flush(); start = c; prev = c;
    }
    flush();
    return ids;
  }

  /* PLANNED FEATURE — Audio Bible (removed 2026-07-31, re-add when licensed):
     A "Listen" pill in the reader played the day's chapters sequentially via
     GET /v1/audio-bibles/{id}/chapters/{OSIS}.{ch} → data.resourceUrl into an Audio()
     element (onended → next chapter; pause/resume; skip chapters w/o audio).
     Removed because this API key's only audio Bible is WEB13 Drama NT (New Testament
     only) — no NIV audio, no English OT audio. Re-enable once NIV/full-OT audio is
     granted on the key (request via API.Bible dashboard / support@api.bible); the
     picker preferred abbreviation ~ /niv/i, else first English audio Bible. */

  async openReader(source, chs, markKey) {
    this.setState({ reader: { source, title: this.formatChapters(chs), loading: true, paras: [], error: null, copyright: "", markKey, chs } });
    const API = "https://api.scripture.api.bible/v1";
    const H = { headers: { "api-key": "fXGrM-U7CKB_KuuXoBg49" } };
    const fail = (msg) => this.setState(s => s.reader ? { reader: { ...s.reader, loading: false, error: msg } } : null);
    try {
      if (!this.bibleId) {
        const r = await fetch(API + "/bibles?abbreviation=NIV", H);
        const j = await r.json();
        this.bibleId = j.data && j.data[0] && j.data[0].id;
        if (!this.bibleId) {
          const r2 = await fetch(API + "/bibles?language=eng", H);
          const j2 = await r2.json();
          this.bibleId = j2.data && j2.data[0] && j2.data[0].id;
          this.bibleNote = j2.data && j2.data[0] && j2.data[0].abbreviation;
        }
      }
      if (!this.bibleId) return fail("No Bible available for this API key.");
      const paras = []; let copyright = "";
      for (const pid of this.passageIds(chs)) {
        const r = await fetch(API + "/bibles/" + this.bibleId + "/passages/" + pid + "?content-type=text&include-verse-numbers=false&include-titles=false&include-chapter-numbers=false", H);
        const j = await r.json();
        if (!j.data) return fail("Couldn't load " + pid + " — the passage may not be available in this translation.");
        j.data.content.split(/\n+/).map(t => t.trim()).filter(Boolean).forEach(t => paras.push({ t }));
        copyright = j.data.copyright || copyright;
      }
      this.setState(s => s.reader ? { reader: { ...s.reader, loading: false, paras, copyright: (copyright || "").trim() + (this.bibleNote ? " (" + this.bibleNote + ")" : "") } } : null);
    } catch (e) { fail("Couldn't reach the Bible API. Check your connection and try again."); }
  }

  // consecutive fully-read days ending today (or yesterday if today isn't done yet)
  streak(todayIdx) {
    const dayDone = (i) => {
      let sched = 0, chk = 0;
      this.state.selections.forEach((sel, si) => { if (this.dayChapters(sel, i).length) { sched++; if (this.state.checks["d" + i + "s" + si]) chk++; } });
      return sched === 0 ? null : chk === sched;
    };
    let n = 0, i = todayIdx;
    if (dayDone(i) === false) i--;
    for (; i >= 0; i--) { const d = dayDone(i); if (d === false) break; if (d === true) n++; }
    return n;
  }

  dateOf(i) {
    const d = new Date(this.state.startDate + "T00:00:00");
    d.setDate(d.getDate() + i);
    return d;
  }
}

if (typeof module !== "undefined") module.exports = { Component, DCLogic };
