// postcard.jsx — 电子明信片
// 概念:访客把馆藏文物转到自己喜欢的角度,那个角度就是明信片的正面。
//       转 → 写 → 寄,三步构成仪式;同一张图将来也用于馆内纸质版。

// 邮寄通道开关。后端需配齐 RESEND_API_KEY + POSTCARD_FROM + TURNSTILE_SECRET
// 才能真正发信;在那之前主动作是「保存 / 分享」,不摆一个填了发不出去的表单。
const MAIL_ON = false;

// 手机上真正有用的不是「下载」而是系统分享面板 —— 一点就能直接发微信。
// iOS Safari 对 <a download> 的支持很差,Web Share API 才是正路。
async function shareCard(png, filename, title) {
  try {
    const blob = await (await fetch(png)).blob();
    const file = new File([blob], filename, { type: 'image/png' });
    if (navigator.canShare && navigator.canShare({ files: [file] })) {
      await navigator.share({ files: [file], title });
      return 'shared';
    }
  } catch (e) {
    if (e && e.name === 'AbortError') return 'cancelled';
  }
  return 'unsupported';
}

function canShareFiles() {
  try {
    return !!(navigator.canShare && navigator.canShare({
      files: [new File([new Blob([1])], 'a.png', { type: 'image/png' })]
    }));
  } catch (e) { return false; }
}

// ── 印刷质感处理 ────────────────────────────────────────────────
// 参考本馆那两张画稿的语言:半调网点 + 平涂专色 / 水墨。
// 在画布里实时做,访客转到哪个角度就印哪个角度。

function toImage(src) {
  return new Promise(r => {
    const im = new Image();
    im.onload = () => r(im); im.onerror = () => r(null);
    im.src = src;
  });
}

// 半调网点:按亮度画大小不等的圆点,网格旋转 15° 才像真的印刷
function halftone(img, { cell = 7, angle = 0.26, ink = '#1a1a1a', bg = null, gamma = 1.0 } = {}) {
  const w = img.width, h = img.height;
  const src = document.createElement('canvas'); src.width = w; src.height = h;
  const sx = src.getContext('2d'); sx.drawImage(img, 0, 0);
  const d = sx.getImageData(0, 0, w, h).data;

  const out = document.createElement('canvas'); out.width = w; out.height = h;
  const x = out.getContext('2d');
  if (bg) { x.fillStyle = bg; x.fillRect(0, 0, w, h); }
  x.fillStyle = ink;

  const cos = Math.cos(angle), sin = Math.sin(angle);
  const diag = Math.ceil(Math.hypot(w, h));
  for (let v = -diag; v < diag; v += cell) {
    for (let u = -diag; u < diag; u += cell) {
      const px = Math.round(u * cos - v * sin + w / 2);
      const py = Math.round(u * sin + v * cos + h / 2);
      if (px < 0 || py < 0 || px >= w || py >= h) continue;
      const i = (py * w + px) * 4;
      if (d[i + 3] < 24) continue;                       // 透明处不印
      const lum = (0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2]) / 255;
      const k = Math.pow(1 - lum, gamma) * (d[i + 3] / 255);
      const r = k * cell * 0.78;
      if (r < 0.35) continue;
      x.beginPath(); x.arc(px, py, r, 0, Math.PI * 2); x.fill();
    }
  }
  return out;
}

// 双色印刷:亮度映射到两种油墨之间
function duotone(img, dark, light) {
  const w = img.width, h = img.height;
  const c = document.createElement('canvas'); c.width = w; c.height = h;
  const x = c.getContext('2d'); x.drawImage(img, 0, 0);
  const im = x.getImageData(0, 0, w, h), d = im.data;
  const A = hex(dark), B = hex(light);
  for (let i = 0; i < d.length; i += 4) {
    if (d[i + 3] < 8) continue;
    const t = (0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2]) / 255;
    d[i]     = A[0] + (B[0] - A[0]) * t;
    d[i + 1] = A[1] + (B[1] - A[1]) * t;
    d[i + 2] = A[2] + (B[2] - A[2]) * t;
  }
  x.putImageData(im, 0, 0);
  return c;
}

// 水墨:压暗、拉对比、留飞白 —— 不做描边,靠灰阶自己出笔触感
function inkwash(img, ink = [26, 24, 22]) {
  const w = img.width, h = img.height;
  const c = document.createElement('canvas'); c.width = w; c.height = h;
  const x = c.getContext('2d'); x.drawImage(img, 0, 0);
  const im = x.getImageData(0, 0, w, h), d = im.data;
  for (let i = 0; i < d.length; i += 4) {
    if (d[i + 3] < 8) continue;
    let t = (0.299 * d[i] + 0.587 * d[i + 1] + 0.114 * d[i + 2]) / 255;
    t = Math.min(1, Math.max(0, (t - 0.16) / 0.62));      // 拉对比
    t = Math.pow(t, 1.35);
    const n = (Math.random() - 0.5) * 0.07;               // 纸面颗粒,制造飞白
    const k = Math.min(1, Math.max(0, 1 - t + n));
    d[i] = ink[0]; d[i + 1] = ink[1]; d[i + 2] = ink[2];
    d[i + 3] = Math.round(d[i + 3] * k);
  }
  x.putImageData(im, 0, 0);
  return c;
}

// 中英混排按字符逐个量宽换行,英文优先在空格处断
function wrap(ctx, text, maxW, maxLines) {
  const out = []; let line = '';
  for (const ch of text.replace(/\s+/g, ' ')) {
    if (ctx.measureText(line + ch).width > maxW && line) {
      const sp = /[A-Za-z0-9]/.test(ch) ? line.lastIndexOf(' ') : -1;
      if (sp > maxW / 40) { out.push(line.slice(0, sp)); line = line.slice(sp + 1) + ch; }
      else { out.push(line); line = ch; }
      if (out.length === maxLines) return out;
    } else line += ch;
  }
  if (line && out.length < maxLines) out.push(line);
  if (out.length === maxLines && line && out[maxLines-1] !== line) out[maxLines-1] += '…';
  return out;
}

function hex(s) {
  const n = parseInt(s.slice(1), 16);
  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
}

// 按版式把器物图处理成对应质感,返回可直接 drawImage 的 canvas
async function treat(objPng, mode) {
  const img = await toImage(objPng);
  if (!img) return null;
  if (mode === 'riso') {
    // 蓝印半调:一层专色网点 + 一层黑网点,网格错开角度,像双色套印
    const w = img.width, h = img.height;
    const c = document.createElement('canvas'); c.width = w; c.height = h;
    const x = c.getContext('2d');
    x.globalAlpha = 0.9;  x.drawImage(halftone(img, { cell: 8, angle: 0.26, ink: '#1b3fd8', gamma: 1.7 }), 0, 0);
    x.globalAlpha = 0.85; x.drawImage(halftone(img, { cell: 7, angle: 1.05, ink: '#1d1a14', gamma: 1.15 }), 0, 0);
    return c;
  }
  if (mode === 'ink')  return inkwash(img);
  if (mode === 'duo')  return duotone(img, '#2b2118', '#e8dcc4');
  return img;               // 纸本:原样
}

// 两套版式:横版纸本(3:2)与竖版珂罗版(2:3,用本馆那张蓝印建筑稿做底)
// 四套版式,各自是一种完整的印刷语言
const CARDS = {
  paper: { w:1500, h:1000, bg:null,                   treat:'photo', objTop:0.36, objBox:[0.62,0.50], plateY:0.705,
           paper:'#f4efe6', ink:'#161412', mute:'#6b645b', rule:'#b4392b', foot:'rgba(26,24,22,0.14)', shadow:true,  grain:true  },
  riso:  { w:1000, h:1500, bg:'assets/bldg-tall.jpg', treat:'riso',  objTop:0.40, objBox:[0.70,0.34], plateY:0.055, plateAlign:'center',
           paper:'#e2cc9c', ink:'#1d1a14', mute:'#6f6248', rule:'#1b3fd8', foot:'rgba(29,26,20,0.20)', shadow:false, grain:false },
  ink:   { w:1500, h:1000, bg:'assets/bldg-wide.jpg', treat:'ink',   objTop:0.32, objBox:[0.54,0.44], plateY:0.70, plateAlign:'left',
           paper:'#f4ecdf', ink:'#161412', mute:'#6b645b', rule:'#161412', foot:'rgba(26,24,22,0.14)', shadow:false, grain:false },
  duo:   { w:1500, h:1000, bg:null,                   treat:'duo',   objTop:0.36, objBox:[0.62,0.50], plateY:0.705,
           paper:'#efe6d4', ink:'#2b2118', mute:'#7a6a52', rule:'#2b2118', foot:'rgba(43,33,24,0.18)', shadow:true,  grain:true  }
};

// 把三维截图合成到一张卡上:纸底 / 画稿、器物、展签、馆名。
// 全部用 2D canvas 画,不依赖字体加载顺序以外的东西。
function composeCard(objPng, name, sub, styleKey, message) {
  const S = CARDS[styleKey] || CARDS.paper;
  const W = S.w, H = S.h, k = W / 1500 * (styleKey === 'riso' ? 1.28 : 1);
  return new Promise(resolve => {
    const c = document.createElement('canvas');
    c.width = W; c.height = H;
    const x = c.getContext('2d');

    x.fillStyle = S.paper;
    x.fillRect(0, 0, W, H);

    if (S.grain) {
      // 中心微亮四周略沉,模仿纸张受光
      const g = x.createRadialGradient(W*0.5, H*0.42, 40, W*0.5, H*0.42, W*0.62);
      g.addColorStop(0, 'rgba(255,253,248,0.85)');
      g.addColorStop(1, 'rgba(232,224,209,0)');
      x.fillStyle = g; x.fillRect(0, 0, W, H);
      // 纸纹
      const n = x.createImageData(W, H), d = n.data;
      for (let i = 0; i < d.length; i += 4) {
        const v = 244 + (Math.random()*16 - 8);
        d[i]=v; d[i+1]=v-5; d[i+2]=v-14; d[i+3]=18;
      }
      const nc = document.createElement('canvas'); nc.width=W; nc.height=H;
      nc.getContext('2d').putImageData(n, 0, 0);
      x.globalAlpha = 0.55; x.drawImage(nc, 0, 0); x.globalAlpha = 1;
    }

    function plate() {
      // 有留言时展签整块上移,给三行字让出空间(顶部题头的珂罗版不用让)
      const hasMsg = !!(message && message.trim());
      const py = H * (hasMsg && S.plateY > 0.5 ? S.plateY - 0.09 : S.plateY);
      const left = S.plateAlign === 'left';
      const cx = left ? W * 0.065 : W / 2;
      x.fillStyle = S.rule;
      x.fillRect(left ? cx : cx - 16*k, py, 32*k, Math.max(1.5*k, 1.2));
      x.textAlign = left ? 'left' : 'center'; x.fillStyle = S.ink;
      x.font = '500 ' + Math.round(30*k) + 'px "Avenir Next", Avenir, Nunito, Inter, "PingFang SC", sans-serif';
      x.fillText(name, cx, py + 46*k);
      x.fillStyle = S.mute;
      x.font = '400 ' + Math.round(17*k) + 'px "JetBrains Mono", ui-monospace, Menlo, monospace';
      x.fillText(sub, cx, py + 80*k);
      x.fillText('Nantong, China', cx, py + 112*k);

      // 访客写的话直接印在卡上 —— 不经过任何后端,保存/分享出去就带着
      if (hasMsg) {
        const fs = Math.round(21*k);
        x.font = '400 ' + fs + 'px "Noto Serif SC", "Songti SC", "PingFang SC", Georgia, serif';
        x.fillStyle = S.ink;
        const maxW = W * (left ? 0.52 : 0.68);
        const lines = wrap(x, message.trim(), maxW, 3);
        let ly = py + 152*k;
        lines.forEach(t => { x.fillText(t, cx, ly); ly += fs * 1.62; });
      }

      const pad = 70*k, fy = H - 78*k;
      x.strokeStyle = S.foot; x.lineWidth = 1;
      x.beginPath(); x.moveTo(pad, fy); x.lineTo(W - pad, fy); x.stroke();
      x.font = '400 ' + Math.round(15*k) + 'px "JetBrains Mono", ui-monospace, Menlo, monospace';
      x.fillStyle = S.mute; x.textAlign = 'left';
      x.fillText('南通市富美帽饰博物馆', pad, fy + 30*k);
      x.textAlign = 'right';
      x.fillText('hatsmuseum.org', W - pad, fy + 30*k);

      resolve(c.toDataURL('image/png'));
    }

    function shadowThenPlate() {
      if (!S.shadow) return plate();      // 珂罗版底稿自带地面
      x.save();
      x.translate(W/2, H*0.635);
      x.scale(1, 0.13);
      const sh = x.createRadialGradient(0,0,0, 0,0,210);
      sh.addColorStop(0, 'rgba(34,28,22,0.22)');
      sh.addColorStop(1, 'rgba(34,28,22,0)');
      x.fillStyle = sh; x.beginPath(); x.arc(0,0,210,0,Math.PI*2); x.fill();
      x.restore();
      plate();
    }

    async function drawObj() {
      if (!objPng) return shadowThenPlate();
      const layer = await treat(objPng, S.treat);      // 按版式做半调 / 双色 / 水墨
      if (!layer) return shadowThenPlate();
      const s = Math.min(W*S.objBox[0]/layer.width, H*S.objBox[1]/layer.height);
      const w = layer.width*s, h = layer.height*s;
      x.drawImage(layer, (W-w)/2, H*S.objTop - h/2, w, h);
      shadowThenPlate();
    }

    if (S.bg) {
      const bg = new Image();
      bg.onload = () => {                 // 画稿等比铺满卡面,底对底
        const s = Math.max(W/bg.width, H/bg.height);
        const w = bg.width*s, h = bg.height*s;
        x.drawImage(bg, (W-w)/2, H-h, w, h);
        drawObj();
      };
      bg.onerror = drawObj;
      bg.src = S.bg;
    } else drawObj();
  });
}

// 截图可能返回一张「合法但空白」的图(弹窗刚提交 DOM 时出现过),
// 所以不能只判非空,必须抽样验证真的有像素,否则重试。
async function hasContent(dataUrl) {
  if (!dataUrl) return false;
  const img = new Image();
  await new Promise(r => { img.onload = r; img.onerror = r; img.src = dataUrl; });
  if (!img.width) return false;
  const c = document.createElement('canvas');
  c.width = 60; c.height = 40;
  const x = c.getContext('2d');
  x.drawImage(img, 0, 0, 60, 40);
  const d = x.getImageData(0, 0, 60, 40).data;
  let op = 0;
  for (let i = 3; i < d.length; i += 4) if (d[i] > 12) op++;
  return op > 40;
}

async function grabShot(v) {
  for (let i = 0; i < 6; i++) {
    let shot = null;
    try { shot = v && v.capture ? v.capture(1100, 760) : null; } catch (e) { shot = null; }
    if (await hasContent(shot)) return shot;
    await new Promise(r => setTimeout(r, 150));
  }
  return null;
}

function Postcard({ lang, shot, object, onClose }) {
  const t = T.card[lang];
  const [png, setPng] = React.useState(null);
  const [msg, setMsg] = React.useState('');
  const [from, setFrom] = React.useState('');
  const [to, setTo] = React.useState('');
  const [state, setState] = React.useState('idle');   // idle | sending | sent | error
  const [style, setStyle] = React.useState('paper');  // paper | riso | ink | duo
  const [sharing, setSharing] = React.useState(false);
  const [shared, setShared] = React.useState(false);
  const [err, setErr] = React.useState('');
  const name = lang === 'zh' ? object.zh : object.en;
  const sub = lang === 'zh' ? object.zhSub : object.enSub;

  // shot 由首屏在「弹窗尚未覆盖画布」时截好后传进来 —— 这里只负责排版。
  // 换版式时用同一帧重新合成,角度保持一致。
  // 留言会印到卡上,所以改字也要重排;打字时防抖,不每个键都重绘
  const [deb, setDeb] = React.useState('');
  React.useEffect(() => {
    const id = setTimeout(() => setDeb(msg), 420);
    return () => clearTimeout(id);
  }, [msg]);
  React.useEffect(() => {
    let alive = true;
    setPng(null);
    composeCard(shot, name, sub, style, deb).then(v => { if (alive) setPng(v); });
    return () => { alive = false; };
  }, [shot, name, sub, style, deb]);

  React.useEffect(() => {
    const esc = e => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', esc);
    document.body.style.overflow = 'hidden';
    return () => { window.removeEventListener('keydown', esc); document.body.style.overflow = ''; };
  }, [onClose]);

  const send = async () => {
    setErr('');
    if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(to)) { setErr(t.errMail); return; }
    if (!msg.trim()) { setErr(t.errMsg); return; }
    setState('sending');
    try {
      const r = await fetch('/api/postcard', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ to, from: from.trim(), message: msg.trim(), lang, object: name, image: png })
      });
      // 501 = 后端未配齐;404 = 本地预览没有 Functions —— 都提示先保存图片
      if (r.status === 501 || r.status === 404) { setState('error'); setErr(t.offline); return; }
      if (!r.ok) throw new Error(String(r.status));
      setState('sent');
    } catch (e) { setState('error'); setErr(t.errNet); }
  };

  return (
    <div className="pc-veil" role="dialog" aria-modal="true" aria-label={t.title} onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div className="pc">
        <button className="pc-x" onClick={onClose} aria-label={t.close}>×</button>
        <div className="pc-grid">

          <div className="pc-side">
            <div className={'pc-card pc-card-' + style}>
              {png
                ? <img src={png} alt={name} />
                : <div className="pc-skel mono">…</div>}
            </div>
            <div className="pc-styles mono">
              <span className="pc-styles-l">{t.styleLabel}</span>
              {[['paper', t.stylePaper], ['riso', t.styleRiso], ['ink', t.styleInk], ['duo', t.styleDuo]].map(([k, lab]) => (
                <button key={k} className={'pc-chip' + (style === k ? ' on' : '')}
                  onClick={() => setStyle(k)}>{lab}</button>
              ))}
            </div>
            <div className="pc-tools">
              {png && canShareFiles() && (
                <button className="pc-act mono" disabled={sharing}
                  onClick={async () => {
                    setSharing(true);
                    const r = await shareCard(png, `hatsmuseum-${object.slug}-${style}.png`, name);
                    setSharing(false);
                    if (r === 'shared') { setShared(true); setTimeout(() => setShared(false), 2600); }
                  }}>{sharing ? t.sharing : (shared ? t.shared : t.share)}</button>)}
              {png && <a className={'pc-ghost mono' + (canShareFiles() ? '' : ' pc-act-a')}
                href={png} download={`hatsmuseum-${object.slug}-${style}.png`}>{t.download}</a>}
            </div>
            <p className="pc-note mono">{t.longpress}</p>
            <p className="pc-note mono">{t.paper}</p>
          </div>

          <div className="pc-side">
            <h3 className="pc-title">{t.title}</h3>
            <p className="pc-lede">{t.lede}</p>
            <p className="pc-guide mono">{t.guide}</p>

            {!MAIL_ON && <p className="pc-soon mono">{t.mailSoon}</p>}

            <label className="pc-lab mono" htmlFor="pc-msg">{t.msgLabel}</label>
            <textarea id="pc-msg" className="pc-msg" rows={4} maxLength={140}
              placeholder={t.msgPh} value={msg} onChange={e => setMsg(e.target.value)} />
            <div className="pc-count mono">{msg.length} / 140 {t.count}</div>

            {MAIL_ON && (
              <>
                <div className="pc-row">
                  <div>
                    <label className="pc-lab mono" htmlFor="pc-from">{t.fromLabel}</label>
                    <input id="pc-from" className="pc-in" value={from} placeholder={t.fromPh}
                      onChange={e => setFrom(e.target.value)} />
                  </div>
                  <div>
                    <label className="pc-lab mono" htmlFor="pc-to">{t.toLabel}</label>
                    <input id="pc-to" className="pc-in" type="email" value={to} placeholder={t.toPh}
                      onChange={e => setTo(e.target.value)} />
                  </div>
                </div>
                {state === 'sent'
                  ? <p className="pc-ok mono">{t.sent}</p>
                  : <button className="pc-send" onClick={send} disabled={state === 'sending' || !png}>
                      {state === 'sending' ? t.sending : t.send}
                    </button>}
                {err && <p className="pc-err mono">{err}</p>}
              </>)}
          </div>

        </div>
      </div>
    </div>);
}

Object.assign(window, { Postcard, composeCard, grabShot });
