/* ═══════════════════════════════════════════════════════
   scene-kit.jsx
   Shared helpers for painting Ghibli-inspired watercolor
   scenes with three.js — skies, hills, lanterns, fireflies
═══════════════════════════════════════════════════════ */

const SK = {};

/* ── palette ──────────────────────────────────────── */
SK.PAL = {
  sky:     ['#eadbb8','#e8c99a','#d9b89a','#c5d4e4','#b0cce0','#8fb2d0'],  // dawn→day
  dusk:    ['#e9b098','#d98c7a','#9f7698','#6a5a90','#3e3f6b'],            // sunset
  night:   ['#1c2540','#2b3a6c','#4a5d9a','#7494c8'],                      // night
  meadow:  ['#aacfaa','#7ab88a','#4c8c5e','#3a6848','#1e3c28'],
  forest:  ['#3a6848','#1e3c28','#0f2818'],
  cream:   '#f5f0e4',
  parch:   '#ede7d5',
  amber:   '#d4931a',
  agold:   '#f0c858',
  rose:    '#c4758a',
  lav:     '#8878b8',
  teal:    '#38c490',
  ink:     '#1a2030',
  inkw:    '#2c2416',
  dim:     '#8a7a60',
};

/* simple helpers */
SK.lerp = (a,b,t)=>a+(b-a)*t;
SK.rand = (a,b)=>a+Math.random()*(b-a);
SK.randInt = (a,b)=>Math.floor(SK.rand(a,b+1));
SK.shuffle = a=>{for(let i=a.length-1;i>0;i--){const j=SK.randInt(0,i);[a[i],a[j]]=[a[j],a[i]];}return a;};
SK.choice = a=>a[SK.randInt(0,a.length-1)];

/* ── canvas texture factory ───────────────────────── */
SK.mkTex = (w,h,fn) => {
  const c = document.createElement('canvas');
  c.width = w; c.height = h;
  fn(c.getContext('2d'), w, h, c);
  const t = new THREE.CanvasTexture(c);
  t.colorSpace = THREE.SRGBColorSpace;
  t.anisotropy = 4;
  return t;
};

SK.mkPlane = (w,h,tex,dbl=true) => {
  const g = new THREE.PlaneGeometry(w,h);
  const m = new THREE.MeshBasicMaterial({
    map:tex, transparent:true,
    side: dbl?THREE.DoubleSide:THREE.FrontSide,
    depthWrite:false,
  });
  return new THREE.Mesh(g,m);
};

/* ── watercolor sky (dawn → night variants) ──────── */
SK.paintSky = (ctx, W, H, stops, noiseAmt=.025) => {
  const g = ctx.createLinearGradient(0,0,0,H);
  const n = stops.length;
  stops.forEach((c,i) => g.addColorStop(i/(n-1), c));
  ctx.fillStyle = g; ctx.fillRect(0,0,W,H);

  // soft watercolor blooms
  for(let i=0;i<16;i++){
    const x = Math.random()*W, y = Math.random()*H*.7;
    const r = 60 + Math.random()*120;
    const rg = ctx.createRadialGradient(x,y,0,x,y,r);
    const col = stops[SK.randInt(0,stops.length-1)];
    rg.addColorStop(0, col + '22');
    rg.addColorStop(1, col + '00');
    ctx.fillStyle = rg;
    ctx.fillRect(x-r,y-r,r*2,r*2);
  }
  // subtle noise grain
  if(noiseAmt>0){
    const img = ctx.getImageData(0,0,W,H);
    for(let i=0;i<img.data.length;i+=4){
      const n = (Math.random()-.5)*255*noiseAmt;
      img.data[i] = Math.max(0,Math.min(255, img.data[i]+n));
      img.data[i+1] = Math.max(0,Math.min(255, img.data[i+1]+n));
      img.data[i+2] = Math.max(0,Math.min(255, img.data[i+2]+n));
    }
    ctx.putImageData(img,0,0);
  }
};

/* ── rolling hills silhouette (layered) ──────────── */
SK.paintHills = (ctx, W, H, layers) => {
  layers.forEach((layer, li) => {
    const {base, color, amp, freq, alpha=1} = layer;
    ctx.fillStyle = color;
    ctx.globalAlpha = alpha;
    ctx.beginPath();
    ctx.moveTo(0, H);
    for(let x=0;x<=W;x+=4){
      let y = base*H;
      y -= amp * (
        Math.sin(x*freq + li*1.3) * 0.6 +
        Math.sin(x*freq*2.3 + li*2.1) * 0.3 +
        Math.sin(x*freq*0.7 + li*0.8) * 0.5
      );
      ctx.lineTo(x,y);
    }
    ctx.lineTo(W,H); ctx.closePath(); ctx.fill();
  });
  ctx.globalAlpha = 1;
};

/* paint trees (soft blobby tops) */
SK.paintTrees = (ctx, W, H, opts={}) => {
  const n = opts.count || 12;
  const yMin = opts.yMin ?? .55;
  const yMax = opts.yMax ?? .82;
  for(let i=0;i<n;i++){
    const x = (i+0.3+Math.random()*.4) * W/n;
    const y = SK.lerp(yMin, yMax, Math.random()) * H;
    const r = 14 + Math.random()*22;
    const trunk = 3 + Math.random()*4;
    // trunk
    ctx.fillStyle = 'rgba(50,40,30,.75)';
    ctx.fillRect(x-trunk/2, y, trunk, r*.7);
    // canopy — stacked blobs
    const col = opts.colors ? SK.choice(opts.colors) : SK.choice(['#3a6848','#2e5238','#4c8c5e','#1e3c28']);
    for(let k=0;k<5;k++){
      const rx = x + (Math.random()-.5)*r*.8;
      const ry = y - r*.2 + (Math.random()-.5)*r*.6;
      ctx.fillStyle = col;
      ctx.globalAlpha = .7 + Math.random()*.3;
      ctx.beginPath(); ctx.arc(rx, ry, r*(.5+Math.random()*.4), 0, Math.PI*2); ctx.fill();
    }
  }
  ctx.globalAlpha = 1;
};

/* clouds — soft white wisps */
SK.paintClouds = (ctx, W, H, opts={}) => {
  const n = opts.count || 5;
  const col = opts.color || '#f5f0e4';
  for(let i=0;i<n;i++){
    const x = Math.random()*W;
    const y = Math.random()*H*.4 + H*.05;
    const r = 30 + Math.random()*50;
    for(let k=0;k<8;k++){
      const rx = x + (Math.random()-.5)*r*2.5;
      const ry = y + (Math.random()-.5)*r*.7;
      const rr = r*(.4+Math.random()*.6);
      const g = ctx.createRadialGradient(rx,ry,0,rx,ry,rr);
      g.addColorStop(0, col+'aa');
      g.addColorStop(.6, col+'44');
      g.addColorStop(1, col+'00');
      ctx.fillStyle = g;
      ctx.fillRect(rx-rr,ry-rr,rr*2,rr*2);
    }
  }
};

/* stars (for night scenes) */
SK.paintStars = (ctx, W, H, opts={}) => {
  const n = opts.count || 80;
  const col = opts.color || '#f5f0e4';
  for(let i=0;i<n;i++){
    const x = Math.random()*W;
    const y = Math.random()*H*.55;
    const a = .2 + Math.random()*.7;
    const r = .4 + Math.random()*1.4;
    ctx.fillStyle = col;
    ctx.globalAlpha = a;
    ctx.beginPath(); ctx.arc(x,y,r,0,Math.PI*2); ctx.fill();
    if(Math.random()<.1){
      // twinkle cross
      ctx.globalAlpha = a*.6;
      ctx.fillRect(x-r*3, y-.3, r*6, .6);
      ctx.fillRect(x-.3, y-r*3, .6, r*6);
    }
  }
  ctx.globalAlpha = 1;
};

/* soft glow sprite (for lanterns, fireflies) */
SK.makeGlow = (color, size=128) => {
  return SK.mkTex(size, size, (c,w,h) => {
    c.clearRect(0,0,w,h);
    const g = c.createRadialGradient(w/2,h/2,0,w/2,h/2,w/2);
    g.addColorStop(0, color+'ff');
    g.addColorStop(.25, color+'bb');
    g.addColorStop(.55, color+'55');
    g.addColorStop(1, color+'00');
    c.fillStyle = g; c.fillRect(0,0,w,h);
  });
};

/* make a sprite-sheet lantern (paper lantern with glow) */
SK.makeLantern = (color='#f0c858') => {
  return SK.mkTex(256, 320, (c,w,h) => {
    c.clearRect(0,0,w,h);
    // outer glow
    const og = c.createRadialGradient(w/2, h*.55, 10, w/2, h*.55, w*.55);
    og.addColorStop(0, color+'88');
    og.addColorStop(.4, color+'33');
    og.addColorStop(1, color+'00');
    c.fillStyle = og; c.fillRect(0,0,w,h);
    // top string
    c.strokeStyle = 'rgba(50,40,20,.7)'; c.lineWidth = 2;
    c.beginPath(); c.moveTo(w/2, 20); c.lineTo(w/2, 70); c.stroke();
    // cap
    c.fillStyle = '#5a4020';
    c.fillRect(w/2-32, 66, 64, 10);
    // body (paper)
    const paperG = c.createRadialGradient(w/2, h*.55, 10, w/2, h*.55, 90);
    paperG.addColorStop(0, '#fff4c8');
    paperG.addColorStop(.5, color);
    paperG.addColorStop(1, color+'aa');
    c.fillStyle = paperG;
    c.beginPath();
    c.ellipse(w/2, h*.55, 75, 95, 0, 0, Math.PI*2);
    c.fill();
    // ribs
    c.strokeStyle = 'rgba(80,50,20,.35)';
    c.lineWidth = 1.5;
    for(let i=-2;i<=2;i++){
      c.beginPath();
      c.ellipse(w/2, h*.55, 75-Math.abs(i)*8, 95, 0, 0, Math.PI*2);
      c.stroke();
    }
    // top & bottom bands
    c.fillStyle = 'rgba(80,50,20,.55)';
    c.fillRect(w/2-55, h*.55-95, 110, 6);
    c.fillRect(w/2-55, h*.55+89, 110, 6);
    // bottom tassel
    c.strokeStyle = 'rgba(120,80,30,.7)'; c.lineWidth = 2;
    c.beginPath(); c.moveTo(w/2, h*.55+95); c.lineTo(w/2, h*.55+140); c.stroke();
    c.fillStyle = '#b87030';
    c.beginPath(); c.arc(w/2, h*.55+150, 7, 0, Math.PI*2); c.fill();
  });
};

/* paper parchment panel */
SK.makeParch = (W=512, H=320, label='') => {
  return SK.mkTex(W, H, (c,w,h) => {
    c.clearRect(0,0,w,h);
    // subtle drop shadow
    c.fillStyle = 'rgba(60,40,20,.18)';
    c.fillRect(8, 10, w-12, h-14);
    // cream
    const gg = c.createLinearGradient(0,0,0,h);
    gg.addColorStop(0, '#f5f0e4');
    gg.addColorStop(1, '#ede7d5');
    c.fillStyle = gg;
    c.fillRect(4, 4, w-12, h-14);
    // rough edge
    c.strokeStyle = 'rgba(140,100,60,.4)';
    c.lineWidth = 1.5;
    c.strokeRect(4, 4, w-12, h-14);
    // stains
    for(let i=0;i<6;i++){
      const x = Math.random()*w, y = Math.random()*h, r = 10+Math.random()*20;
      const g = c.createRadialGradient(x,y,0,x,y,r);
      g.addColorStop(0, 'rgba(140,100,60,.10)');
      g.addColorStop(1, 'rgba(140,100,60,0)');
      c.fillStyle = g; c.fillRect(x-r,y-r,r*2,r*2);
    }
    if(label){
      c.font = "600 22px 'Noto Serif TC',serif";
      c.fillStyle = '#2c2416';
      c.textAlign = 'center'; c.textBaseline = 'middle';
      c.fillText(label, w/2, h/2);
    }
  });
};

/* firefly particles */
SK.makeFireflies = (scene, count=40, bounds={x:6, y:3.5, z:2}) => {
  const geom = new THREE.BufferGeometry();
  const pos = new Float32Array(count*3);
  const phase = new Float32Array(count);
  for(let i=0;i<count;i++){
    pos[i*3]   = (Math.random()-.5) * bounds.x * 2;
    pos[i*3+1] = (Math.random()-.5) * bounds.y * 2;
    pos[i*3+2] = (Math.random()-.5) * bounds.z * 2;
    phase[i] = Math.random()*Math.PI*2;
  }
  geom.setAttribute('position', new THREE.BufferAttribute(pos, 3));
  const tex = SK.makeGlow('#f0d060', 64);
  const mat = new THREE.PointsMaterial({
    map: tex, color: 0xf0d060,
    size: 0.22, transparent: true, depthWrite: false,
    blending: THREE.AdditiveBlending,
    sizeAttenuation: true,
  });
  const pts = new THREE.Points(geom, mat);
  scene.add(pts);
  return { pts, phase, bounds, count,
    tick(t){
      const p = geom.attributes.position.array;
      for(let i=0;i<count;i++){
        p[i*3]   += Math.sin(t*.0006 + phase[i]) * .004;
        p[i*3+1] += Math.cos(t*.0008 + phase[i]*1.3) * .004;
        p[i*3+2] += Math.sin(t*.0005 + phase[i]*.7) * .003;
      }
      geom.attributes.position.needsUpdate = true;
      mat.opacity = .6 + Math.sin(t*.002)*.2;
    }
  };
};

/* floating petal / leaf particles */
SK.makePetals = (scene, count=20, color='#e8b8a0') => {
  const petals = [];
  const tex = SK.mkTex(32, 32, (c,w,h)=>{
    c.clearRect(0,0,w,h);
    c.fillStyle = color;
    c.beginPath();
    c.ellipse(w/2, h/2, w*.4, h*.22, 0, 0, Math.PI*2);
    c.fill();
    c.fillStyle = 'rgba(255,255,255,.3)';
    c.beginPath();
    c.ellipse(w/2-4, h/2-2, w*.15, h*.08, 0, 0, Math.PI*2);
    c.fill();
  });
  for(let i=0;i<count;i++){
    const m = new THREE.Sprite(new THREE.SpriteMaterial({ map: tex, transparent: true, depthWrite: false }));
    m.scale.set(.25, .25, 1);
    m.position.set((Math.random()-.5)*12, Math.random()*6+1, (Math.random()-.5)*3);
    const speed = .003 + Math.random()*.005;
    const swayAmp = .5 + Math.random()*.8;
    const rot = Math.random()*Math.PI;
    scene.add(m);
    petals.push({m, speed, swayAmp, rot, phase: Math.random()*Math.PI*2});
  }
  return {
    tick(t){
      for(const p of petals){
        p.m.position.y -= p.speed;
        p.m.position.x += Math.sin(t*.001 + p.phase) * .003 * p.swayAmp;
        p.m.material.rotation += .005;
        if(p.m.position.y < -3.5){
          p.m.position.y = 6;
          p.m.position.x = (Math.random()-.5)*12;
        }
      }
    }
  };
};

/* render-loop factory */
SK.loop = (renderer, scene, cam, hooks={}) => {
  let t = 0, raf;
  const run = () => {
    t += 16;
    if(hooks.tick) hooks.tick(t);
    renderer.render(scene, cam);
    raf = requestAnimationFrame(run);
  };
  run();
  return {
    stop(){ cancelAnimationFrame(raf); },
  };
};

/* create renderer+camera+scene sized to wrapper el */
SK.mkScene = (wrap, opts={}) => {
  const W = wrap.clientWidth || 800;
  const H = wrap.clientHeight || 540;
  const r = new THREE.WebGLRenderer({ antialias:true, alpha:true });
  r.setPixelRatio(Math.min(window.devicePixelRatio, 2));
  r.setSize(W, H);
  r.setClearColor(0x000000, 0);
  wrap.appendChild(r.domElement);
  const cam = new THREE.PerspectiveCamera(opts.fov||45, W/H, .1, 200);
  cam.position.set(0, 0, opts.z || 7);
  const scene = new THREE.Scene();
  scene.add(new THREE.AmbientLight(0xffffff, 1));
  // resize observer
  const ro = new ResizeObserver(()=>{
    const w = wrap.clientWidth, h = wrap.clientHeight;
    if(w && h){
      r.setSize(w, h);
      cam.aspect = w/h;
      cam.updateProjectionMatrix();
    }
  });
  ro.observe(wrap);
  return { renderer: r, scene, cam, W, H };
};

/* paint a full backdrop plane sized for a given fov camera */
SK.addBackdrop = (scene, cam, painter, distance=-30) => {
  const TEX = SK.mkTex(1200, 800, painter);
  const h = 2 * distance * Math.tan(cam.fov * Math.PI/360);
  const w = h * cam.aspect;
  const m = SK.mkPlane(w*-3, h*-3, TEX);  // large enough
  m.position.z = distance;
  scene.add(m);
  return m;
};

window.SK = SK;
