// orb3d.jsx — the canonical Orb, made turnable. Same flat cut-paper look as
// the character reference (solid orange, one brown outline, fixed top-left
// gloss arc, flat blush, two cream eyes + catchlight, NO mouth) — but the
// eyes / blush are projected onto the sphere so we can show it from any angle.

function _shade(hex, amt) {
  const n = (i) => Math.max(0, Math.min(255, parseInt(hex.slice(i, i + 2), 16) + amt));
  return `#${[1, 3, 5].map((i) => n(i).toString(16).padStart(2, '0')).join('')}`;
}
const _clamp = (v, a, b) => Math.max(a, Math.min(b, v));

// expr: 'open' | 'wide' | 'happy' | 'sleepy' | 'starry' | 'curious' | 'wistful' | 'love'
function Orb3D({ size = 90, fill = '#EE5A24', yaw = 0, pitch = 0, expr = 'open', look = [0, 0],
  blush = false, brow, top, glow = false, style }) {
  const ink = '#2A211A',cream = '#FFFCF6';
  const C = { x: 50, y: 50 },R = 40;
  const blushC = _shade(fill, -55);
  const yw = yaw * Math.PI / 180,pt = pitch * Math.PI / 180;

  const rot = (v) => {
    const x = v.x * Math.cos(yw) + v.z * Math.sin(yw);
    const z = -v.x * Math.sin(yw) + v.z * Math.cos(yw);
    const y = v.y;
    return { x, y: y * Math.cos(pt) - z * Math.sin(pt), z: y * Math.sin(pt) + z * Math.cos(pt) };
  };
  const dir = (az, el) => ({ x: Math.sin(az) * Math.cos(el), y: Math.sin(el), z: Math.cos(az) * Math.cos(el) });
  const proj = (d) => ({ sx: C.x + R * d.x, sy: C.y - R * d.y, z: d.z });

  const eL = proj(rot(dir(-0.44, 0.04))); // eyes sit ~ where the canonical ones are
  const eR = proj(rot(dir(0.44, 0.04)));
  const bL = proj(rot(dir(-0.66, -0.26)));
  const bR = proj(rot(dir(0.66, -0.26)));
  const crown = proj(rot(dir(0, 1.5))); // top of the head (for accessories)
  const [lkx, lky] = look;

  function eye(e, key) {
    if (e.z < -0.05) return null; // far hemisphere → hidden
    const vis = _clamp(e.z, 0, 1);
    const s = 0.55 + 0.45 * vis;
    const sqx = 0.6 + 0.4 * vis; // squash toward the limb
    const op = e.z < 0.05 ? _clamp((e.z + 0.05) / 0.1, 0, 1) : 1;
    const wrap = (kids) => <g key={key} opacity={op} transform={`translate(${e.sx} ${e.sy}) scale(${sqx} 1)`}>{kids}</g>;
    const er = 9 * s;

    // eyes are ALWAYS open with visible pupils (no closed/curved faces)
    const eyeR = expr === 'wide' ? er * 1.14 : er;
    const pr = (expr === 'wide' ? 2.8 : expr === 'starry' ? 3.0 : 3.7) * s;
    const upGaze = (expr === 'wistful' ? -0.4 : expr === 'love' ? -0.2 : 0) * er;
    const px = lkx * 2.4 * s,py = (lky * 2.0 + (expr === 'wistful' ? -1.4 : 0)) * s + upGaze;
    return wrap(<>
      <circle r={eyeR} fill={cream} stroke={ink} strokeWidth={2 * s} />
      {expr === 'starry' ?
      <polygon points={starPts(px, py, 3.4 * s)} fill={ink} /> :
      <circle cx={px} cy={py} r={pr} fill={ink} />}
      <circle cx={px - 1.2 * s} cy={py - 1.4 * s} r={1.4 * s} fill={cream} />
      {expr === 'starry' && <circle cx={px + 1.4 * s} cy={py + 1.2 * s} r={0.8 * s} fill={cream} />}
    </>);
  }
  function starPts(cx, cy, r) {
    let p = '';
    for (let k = 0; k < 10; k++) {const a = -Math.PI / 2 + k * Math.PI / 5,rr = k % 2 ? r * 0.45 : r;p += `${(cx + Math.cos(a) * rr).toFixed(1)},${(cy + Math.sin(a) * rr).toFixed(1)} `;}
    return p.trim();
  }
  function browArc(e, side) {
    if (!brow || e.z < 0.06 || expr === 'happy' || expr === 'love' || expr === 'sleepy') return null;
    const vis = _clamp(e.z, 0, 1),s = 0.55 + 0.45 * vis,w = 7 * s;
    const lift = (brow === 'surprise' ? -15 : -12) * s,cy = e.sy + lift;
    if (brow === 'angry') return <path key={'b' + side} d={`M ${e.sx - w},${cy + (side === 'L' ? -2 : 2) * s} L ${e.sx + w},${cy + (side === 'L' ? 2 : -2) * s}`} stroke={ink} strokeWidth={2 * s} strokeLinecap="round" opacity={vis} />;
    return <path key={'b' + side} d={`M ${e.sx - w},${cy} Q ${e.sx},${cy - 3.6 * s} ${e.sx + w},${cy}`} fill="none" stroke={ink} strokeWidth={1.9 * s} strokeLinecap="round" opacity={vis} />;
  }

  function topPiece() {
    if (!top || crown.z < -0.25) return null;
    const k = 0.62 + 0.38 * _clamp(crown.z, 0, 1);
    const op = crown.z < 0 ? _clamp(crown.z + 0.35, 0, 1) : 1;
    const acc = _shade(fill, -46);
    let kids = null;
    if (top === 'cowlick') kids = <path d="M 0,2 C -3,-9 9,-13 6,-2 C 11,-6 9,3 2,0 Z" fill={acc} stroke={ink} strokeWidth="2" strokeLinejoin="round" />;else
    if (top === 'antenna') kids = <g><path d="M 0,2 Q -1,-7 -2,-13" fill="none" stroke={ink} strokeWidth="2" strokeLinecap="round" /><circle cx="-2" cy="-15" r="3.4" fill={acc} stroke={ink} strokeWidth="2" /></g>;else
    if (top === 'leaf') kids = <g><path d="M 1,2 Q 11,-13 18,-1 Q 10,4 1,2 Z" fill="#6FAE72" stroke={ink} strokeWidth="2" strokeLinejoin="round" /><path d="M 4,1 Q 10,-3 15,-1" fill="none" stroke={ink} strokeWidth="1.1" /></g>;else
    if (top === 'tuft') kids = <g fill={acc} stroke={ink} strokeWidth="1.8" strokeLinejoin="round"><path d="M -6,2 L -8,-9 L -1,-2 Z" /><path d="M 0,2 L 0,-12 L 5,-2 Z" /><path d="M 5,2 L 9,-8 L 9,-1 Z" /></g>;else
    if (top === 'sprout') kids = <g stroke={ink} strokeWidth="1.8" strokeLinejoin="round"><path d="M 0,2 Q -8,-8 -12,-1 Q -6,3 0,2 Z" fill="#6FAE72" /><path d="M 1,2 Q 8,-9 13,-2 Q 6,3 1,2 Z" fill="#7FBE82" /></g>;
    return <g opacity={op} transform={`translate(${crown.sx} ${crown.sy}) rotate(${yaw * 0.3}) scale(${k})`}>{kids}</g>;
  }

  return (
    <svg width={size} height={size} viewBox="0 0 100 100" style={{ display: 'block', overflow: 'visible', width: size, height: size, ...style }} aria-hidden="true">
      {glow && <circle cx="50" cy="50" r="56" fill={fill} opacity="0.16" />}
      {/* contact shadow */}
      <ellipse cx="50" cy="93" rx="22" ry="3.2" fill="rgba(42,33,26,0.15)" />
      {/* top accessory (base tucks behind the body) */}
      {topPiece()}
      {/* body — flat solid fill, one brown outline (canonical) */}
      <circle cx="50" cy="50" r={R} fill={fill} stroke={ink} strokeWidth="2.6" />
      {/* fixed top-left gloss arc (canonical) */}
      <path d="M26 40 A 40 40 0 0 1 54 18" fill="none" stroke="rgba(255,255,255,0.45)" strokeWidth="7" strokeLinecap="round" />
      {/* blush (projected so it rides the face), flat */}
      {blush && [bL, bR].map((b, i) => b.z > -0.02 &&
      <ellipse key={i} cx={b.sx} cy={b.sy} rx={5 * (0.55 + 0.45 * _clamp(b.z, 0, 1))} ry={2.6 * (0.55 + 0.45 * _clamp(b.z, 0, 1))} fill={blushC} opacity={0.5 * _clamp(b.z, 0, 1)} />
      )}
      {/* eyebrows removed */}
      {eye(eL, 'L')}{eye(eR, 'R')}
    </svg>);

}

Object.assign(window, { Orb3D });