// ─────────────────────────────────────────────────────────────────
// PHOTO ADMIN — visual photo management for the gallery pages.
//
// · Drag an image file from your desktop onto ANY frame (series pages,
//   covers on the Photographs index, the featured photo on the home page).
//   The image is stored in the browser (IndexedDB) and survives reloads.
// · Turn on "Edit tools" in the Tweaks panel to also:
//     – drag inside a photo to set its crop focus
//     – reorder frames (← →), resize them (1×/2×/3× grid span)
//     – set a frame as the series cover, remove frames, add new ones
//   Layout choices persist in localStorage.
// ─────────────────────────────────────────────────────────────────

const PP_LAYOUT_KEY = 'pp-photo-layout-v1';
const PhotoEditCtx = React.createContext(false);

const ppIdb = {
  open() {
    if (this._p) return this._p;
    this._p = new Promise((res, rej) => {
      const r = indexedDB.open('pp-photos', 1);
      r.onupgradeneeded = () => r.result.createObjectStore('imgs');
      r.onsuccess = () => res(r.result);
      r.onerror = () => rej(r.error);
    });
    return this._p;
  },
  async get(k) { const db = await this.open(); return new Promise((res) => { const q = db.transaction('imgs').objectStore('imgs').get(k); q.onsuccess = () => res(q.result || null); q.onerror = () => res(null); }); },
  async set(k, v) { const db = await this.open(); return new Promise((res) => { const t = db.transaction('imgs', 'readwrite'); t.objectStore('imgs').put(v, k); t.oncomplete = res; t.onerror = res; }); },
  async del(k) { const db = await this.open(); return new Promise((res) => { const t = db.transaction('imgs', 'readwrite'); t.objectStore('imgs').delete(k); t.oncomplete = res; t.onerror = res; }); },
  async keys() { const db = await this.open(); return new Promise((res) => { const q = db.transaction('imgs').objectStore('imgs').getAllKeys(); q.onsuccess = () => res(q.result || []); q.onerror = () => res([]); }); },
};

function ppParsePos(pos) {
  const m = /([\d.]+)%\s+([\d.]+)%/.exec(pos || '');
  return m ? [parseFloat(m[1]), parseFloat(m[2])] : [50, 50];
}
function ppClamp(v) { return Math.max(0, Math.min(100, v)); }

const PhotoStore = {
  layout: (() => { try { return JSON.parse(localStorage.getItem(PP_LAYOUT_KEY)) || {}; } catch (e) { return {}; } })(),
  urls: {},              // slot key -> object URL for dropped images
  listeners: new Set(),
  emit() { this.listeners.forEach((f) => f()); },
  saveLayout() { try { localStorage.setItem(PP_LAYOUT_KEY, JSON.stringify(this.layout)); } catch (e) {} this.emit(); },

  async init() {
    if (this._init) return; this._init = true;
    try {
      const keys = await ppIdb.keys();
      for (const k of keys) {
        const b = await ppIdb.get(k);
        if (b) this.urls[k] = URL.createObjectURL(b);
      }
    } catch (e) {}
    this.emit();
  },

  async drop(slot, file) {
    if (!file || !/^image\//.test(file.type)) return;
    await ppIdb.set(slot, file);
    if (this.urls[slot]) URL.revokeObjectURL(this.urls[slot]);
    this.urls[slot] = URL.createObjectURL(file);
    this.emit();
  },
  async clearImg(slot) { await ppIdb.del(slot); delete this.urls[slot]; this.emit(); },

  // ── series frame layout (copy-on-write over the base data) ──
  slotKey(seriesId, no) { return 'img/' + seriesId + '/' + no; },
  ensure(id, base) {
    if (!this.layout[id]) this.layout[id] = { frames: base.map((f) => ({ ...f })) };
    return this.layout[id];
  },
  frames(id, base) { return (this.layout[id] && this.layout[id].frames) || base; },
  coverPos(id) { return this.layout[id] && this.layout[id].coverPos; },

  patch(id, base, no, p) {
    const L = this.ensure(id, base);
    const f = L.frames.find((x) => x.no === no);
    if (f) Object.assign(f, p);
    this.saveLayout();
  },
  patchCover(id, pos) {
    if (!this.layout[id]) this.layout[id] = { frames: null };
    this.layout[id].coverPos = pos;
    this.saveLayout();
  },
  move(id, base, no, dir) {
    const L = this.ensure(id, base);
    const i = L.frames.findIndex((x) => x.no === no);
    const j = i + dir;
    if (i < 0 || j < 0 || j >= L.frames.length) return;
    const [f] = L.frames.splice(i, 1);
    L.frames.splice(j, 0, f);
    this.saveLayout();
  },
  cycleSpan(id, base, no) {
    const L = this.ensure(id, base);
    const f = L.frames.find((x) => x.no === no);
    if (!f) return;
    f.span = f.span === 'wide' ? 'full' : f.span === 'full' ? undefined : 'wide';
    this.saveLayout();
  },
  // aspect / orientation: landscape → portrait → square → cinematic
  cycleRatio(id, base, no) {
    const cycle = ['3/2', '4/5', '1/1', '16/9'];
    const L = this.ensure(id, base);
    const f = L.frames.find((x) => x.no === no);
    if (!f) return;
    const i = cycle.indexOf(f.ratio);
    f.ratio = cycle[(i + 1) % cycle.length];
    this.saveLayout();
  },
  add(id, base, extra) {
    const L = this.ensure(id, base);
    const max = L.frames.reduce((m, f) => Math.max(m, parseInt(f.no, 10) || 0), 0);
    const no = String(max + 1).padStart(2, '0');
    L.frames.push(Object.assign({ no, caption: '', meta: '', ratio: '4/5' }, extra));
    this.saveLayout();
  },
  addText(id, base) {
    this.add(id, base, { type: 'text', text: 'New text block — click to edit…', span: 'wide', ratio: undefined });
  },
  remove(id, base, no) {
    const L = this.ensure(id, base);
    L.frames = L.frames.filter((x) => x.no !== no);
    this.clearImg(this.slotKey(id, no));
    this.saveLayout();
  },
  async setCover(id, no) {
    const b = await ppIdb.get(this.slotKey(id, no));
    if (!b) return;
    await ppIdb.set('cover/' + id, b);
    if (this.urls['cover/' + id]) URL.revokeObjectURL(this.urls['cover/' + id]);
    this.urls['cover/' + id] = URL.createObjectURL(b);
    this.emit();
  },
  reset(id) {
    delete this.layout[id];
    this.saveLayout();
  },

  // ── site content: user-added / hidden photo stories & essays ──
  content() {
    if (!this.layout.__content) this.layout.__content = { series: { added: [], hidden: {}, over: {} }, essays: { added: [], hidden: {}, over: {} } };
    return this.layout.__content;
  },
  seriesList(base) {
    const c = this.content();
    const kept = base.filter((s) => !c.series.hidden[s.id]).map((s) => ({ ...s, ...(c.series.over[s.id] || {}) }));
    const added = c.series.added.map((s, i) => ({ ...s, added: true, no: s.no || String(base.length + i + 1).padStart(2, '0') }));
    return kept.concat(added);
  },
  seriesOver(id) { return this.content().series.over[id] || {}; },
  addSeries() {
    const c = this.content();
    const id = 'u' + Date.now().toString(36);
    c.series.added.push({ id, year: String(new Date().getFullYear()), frames: 6, title: 'New photo story', sub: 'A sentence about this series.' });
    this.saveLayout();
    return id;
  },
  removeSeries(id) {
    const c = this.content();
    const i = c.series.added.findIndex((s) => s.id === id);
    if (i >= 0) c.series.added.splice(i, 1); else c.series.hidden[id] = 1;
    this.saveLayout();
  },
  patchSeries(id, p) {
    const c = this.content();
    const a = c.series.added.find((s) => s.id === id);
    if (a) Object.assign(a, p); else c.series.over[id] = { ...(c.series.over[id] || {}), ...p };
    this.saveLayout();
  },
  essaysList(base) {
    const c = this.content();
    const kept = base.filter((e) => !c.essays.hidden[e.id]).map((e) => ({ ...e, ...(c.essays.over[e.id] || {}) }));
    return kept.concat(c.essays.added.map((e) => ({ ...e, added: true })));
  },
  essayOver(id) { return this.content().essays.over[id] || {}; },
  addEssay(sec) {
    const c = this.content();
    const id = 'u' + Date.now().toString(36);
    const d = new Date();
    c.essays.added.push({ id, sec: sec || 'mine', date: d.getFullYear() + '.' + String(d.getMonth() + 1).padStart(2, '0'), title: 'New essay', body: '' });
    this.saveLayout();
    return id;
  },
  removeEssay(id) {
    const c = this.content();
    const i = c.essays.added.findIndex((e) => e.id === id);
    if (i >= 0) c.essays.added.splice(i, 1); else c.essays.hidden[id] = 1;
    this.saveLayout();
  },
  patchEssay(id, p) {
    const c = this.content();
    const a = c.essays.added.find((e) => e.id === id);
    if (a) Object.assign(a, p); else c.essays.over[id] = { ...(c.essays.over[id] || {}), ...p };
    this.saveLayout();
  },
  restoreHidden() {
    const c = this.content();
    c.series.hidden = {}; c.essays.hidden = {}; if (c.projects) c.projects.hidden = {};
    this.saveLayout();
  },

  // ── projects ──
  projContent() {
    const c = this.content();
    if (!c.projects) c.projects = { added: [], hidden: {}, over: {} };
    return c.projects;
  },
  projectsList(base) {
    const c = this.projContent();
    const kept = base.map((p, i) => ({ ...p, _idx: i })).filter((p) => !c.hidden[p.no]).map((p) => ({ ...p, ...(c.over[p.no] || {}) }));
    const added = c.added.map((p, i) => ({ ...p, added: true, no: p.no || String(base.length + i + 1).padStart(2, '0') }));
    return kept.concat(added);
  },
  addProject() {
    const c = this.projContent();
    c.added.push({ id: 'u' + Date.now().toString(36), name: 'New project', year: String(new Date().getFullYear()), href: '', host: '', desc: '', role: '', context: '', tags: [] });
    this.saveLayout();
  },
  removeProject(p) {
    const c = this.projContent();
    if (p.added) { const i = c.added.findIndex((x) => x.id === p.id); if (i >= 0) c.added.splice(i, 1); }
    else c.hidden[p.no] = 1;
    this.saveLayout();
  },
  patchProject(p, patch) {
    const c = this.projContent();
    const a = p.added && c.added.find((x) => x.id === p.id);
    if (a) Object.assign(a, patch); else c.over[p.no] = { ...(c.over[p.no] || {}), ...patch };
    this.saveLayout();
  },
};

function usePhotoStore() {
  const [, force] = React.useReducer((x) => x + 1, 0);
  React.useEffect(() => {
    PhotoStore.listeners.add(force);
    PhotoStore.init();
    return () => PhotoStore.listeners.delete(force);
  }, []);
  return PhotoStore;
}

// small toolbar button used by the edit overlay
function PhotoTool({ onClick, title, children }) {
  return <button className="pp-toolbtn" title={title} onClick={onClick}>{children}</button>;
}

Object.assign(window, { PhotoStore, usePhotoStore, PhotoEditCtx, PhotoTool, ppParsePos, ppClamp });
