/* global React, THREE */
/* ============================================================
   PhenologicalWheel — 3D wheel of orchard phenological stages.
   Tilted disc, 8 segments, drag-to-spin, click-to-snap.
   Active segment ROTATES TO FRONT-CENTER and rises toward the
   camera. HTML labels are projected to 2D screen-space each
   frame so they ride the wheel.
   ============================================================ */

(function () {
  const { useEffect, useRef, useState, useImperativeHandle, forwardRef } = React;

  // --- STAGE CONTENT ----------------------------------------------
  const STAGES = [
    {
      id: 'dormancy',
      label: 'Dormancy',
      sub: 'Pruning · Soil work',
      blurb: 'The orchard is asleep — but it is when next season is decided. Soil scans, drainage interventions, structural pruning.',
      photo: 'assets/phenology/01-dormant.jpg',
    },
    {
      id: 'budbreak',
      label: 'Bud break',
      sub: 'Wake-up · Early canopy',
      blurb: 'New growth pushes. First chance to read this season\'s vigour from the canopy and adjust early N decisions.',
      photo: 'assets/phenology/02-bud-swell.jpg',
    },
    {
      id: 'flowering',
      label: 'Flowering',
      sub: 'Bloom · Pollination',
      blurb: 'Yield potential is set. Canopy uniformity matters most — uneven blocks now will be uneven all season.',
      photo: 'assets/phenology/03-bud-break.jpg',
    },
    {
      id: 'fruitset',
      label: 'Fruit set',
      sub: 'Set count · Early thinning',
      blurb: 'Crop load decisions begin. Per-tree count tells you which trees are over- and under-cropped before sizing starts.',
      photo: 'assets/phenology/04-tight-cluster.jpg',
    },
    {
      id: 'celldivision',
      label: 'Cell division',
      sub: 'Early sizing · Calibration',
      blurb: 'Cells multiply. The fruit\'s ceiling for final size is being set right now — calibration window for K, Ca, irrigation.',
      photo: 'assets/phenology/05-pink.jpg',
    },
    {
      id: 'cellexpansion',
      label: 'Cell expansion',
      sub: 'Sizing · Stress windows',
      blurb: 'Fruit grows fastest. Weekly size measurements predict packout. Canopy stress shows up as size loss within days.',
      photo: 'assets/phenology/06-bloom.jpg',
    },
    {
      id: 'veraison',
      label: 'Veraison',
      sub: 'Colour break · Maturation',
      blurb: 'Sugar accumulation, colour development, firmness shifts. Final shape of the harvest comes into focus.',
      photo: 'assets/phenology/07-fruit-set.jpg',
    },
    {
      id: 'harvest',
      label: 'Harvest',
      sub: 'Pick · Post-harvest',
      blurb: 'Zonal maps guide pick sequencing and storage allocation. After the bins leave, post-harvest N & K replenishes reserves and prevents the alternation flip next season.',
      photo: 'assets/phenology/08-fruitlet.jpg',
    },
  ];

  // Brand colours
  const ACCENT  = 0xB8DC73;
  const ACCENT_DIM = 0x6F8C42;
  const SURFACE = 0x1B2918;
  const DEEP    = 0x0A1408;

  // The angular position (around Y axis, in radians) where the
  // ACTIVE segment should land. +Z is the camera-facing direction,
  // which corresponds to angle = π/2 in the segment's local space
  // (because shapes use cos(a) for X, sin(a) for Z).
  const FRONT_ANGLE = Math.PI / 2;

  window.PhenologicalWheel = forwardRef(function PhenologicalWheel({
    onStageChange, onStageActivate, initialStage = 0,
  }, ref) {
    const containerRef = useRef(null);
    const canvasRef = useRef(null);
    const labelsLayerRef = useRef(null);
    const stateRef = useRef({
      activeIdx: initialStage,
      targetRot: 0,
      currentRot: 0,
      // Drag
      dragging: false,
      dragLastX: 0,
      dragVelocity: 0,
      lastMoveTime: 0,
      // Hover
      hoverIdx: -1,
    });

    useImperativeHandle(ref, () => ({
      snapTo: (idx) => {
        const st = stateRef.current;
        st.activeIdx = idx;
        const angleStep = (Math.PI * 2) / STAGES.length;
        // We want segment i at FRONT_ANGLE.
        // Segment i's world angle = -i*step + wheelRot.
        // So wheelRot = FRONT_ANGLE + i*step.
        st.targetRot = FRONT_ANGLE + idx * angleStep;
        // Normalize toward currentRot to avoid spinning the long way
        while (st.targetRot - st.currentRot >  Math.PI) st.targetRot -= Math.PI * 2;
        while (st.targetRot - st.currentRot < -Math.PI) st.targetRot += Math.PI * 2;
        if (onStageChange) onStageChange(STAGES[idx], idx);
      },
      activate: (idx) => {
        if (onStageActivate) onStageActivate(STAGES[idx], idx);
      },
    }), [onStageChange, onStageActivate]);

    useEffect(() => {
      const canvas = canvasRef.current;
      const container = containerRef.current;
      const labelsLayer = labelsLayerRef.current;
      if (!canvas || !container || !window.THREE) return;

      const reduced = (() => {
        try { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
        catch (e) { return false; }
      })();

      const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
      renderer.setClearColor(0x000000, 0);

      const scene = new THREE.Scene();
      const camera = new THREE.PerspectiveCamera(34, 1, 0.1, 100);
      camera.position.set(0, 9.5, 7.0);
      camera.lookAt(0, 0, 0);

      const ambient = new THREE.AmbientLight(0xffffff, 0.5);
      scene.add(ambient);
      const key = new THREE.DirectionalLight(0xffffff, 0.85);
      key.position.set(2, 6, 4); scene.add(key);
      const rim = new THREE.DirectionalLight(0xB8DC73, 0.45);
      rim.position.set(-3, 2, -2); scene.add(rim);

      // Wheel root tilted modestly back. The "look down" feeling now
      // comes from the camera being high above, looking at the wheel.
      const wheelRoot = new THREE.Group();
      wheelRoot.rotation.x = 0; // disc lies flat
      wheelRoot.scale.setScalar(0.82); // smaller ring; HTML label sizes unchanged
      scene.add(wheelRoot);

      // Outer disc — annular (hollow centre) so the photo behind the
      // canvas reads through the inner ring. Spans from just inside the
      // segments' inner radius outward, so the segments still rest on it.
      const discShape = new THREE.Shape();
      const discOuterR = 3.7;
      const discInnerR = 1.50;
      // outer circle
      discShape.absarc(0, 0, discOuterR, 0, Math.PI * 2, false);
      // inner hole
      const discHole = new THREE.Path();
      discHole.absarc(0, 0, discInnerR, 0, Math.PI * 2, true);
      discShape.holes.push(discHole);
      const discGeo = new THREE.ExtrudeGeometry(discShape, {
        depth: 0.10, bevelEnabled: false, steps: 1, curveSegments: 96,
      });
      discGeo.rotateX(-Math.PI / 2);
      const disc = new THREE.Mesh(
        discGeo,
        new THREE.MeshStandardMaterial({ color: SURFACE, metalness: 0, roughness: 0.95 })
      );
      disc.position.y = -0.20;
      wheelRoot.add(disc);

      // Inner pool — transparent (the DOM photo disc behind the canvas
      // is meant to show through here). We keep a very faint inner ring
      // for visual continuity but skip the dark fill.
      // (Inner pool fill removed intentionally; canvas alpha lets the
      // photo disc behind read clearly inside the ring.)

      // Outer ring
      const ring = new THREE.Mesh(
        new THREE.TorusGeometry(3.72, 0.025, 8, 96),
        new THREE.MeshBasicMaterial({ color: ACCENT, transparent: true, opacity: 0.55 })
      );
      ring.rotation.x = Math.PI / 2;
      ring.position.y = -0.13;
      wheelRoot.add(ring);

      const innerRing = new THREE.Mesh(
        new THREE.TorusGeometry(1.55, 0.012, 8, 64),
        new THREE.MeshBasicMaterial({ color: ACCENT, transparent: true, opacity: 0.4 })
      );
      innerRing.rotation.x = Math.PI / 2;
      innerRing.position.y = -0.06;
      wheelRoot.add(innerRing);

      // Tick marks (sub-divisions)
      for (let i = 0; i < STAGES.length * 4; i++) {
        const a = (i / (STAGES.length * 4)) * Math.PI * 2;
        const isMajor = i % 4 === 0;
        const r0 = 3.62, r1 = isMajor ? 3.45 : 3.55;
        const tickGeo = new THREE.BufferGeometry().setFromPoints([
          new THREE.Vector3(Math.cos(a) * r0, -0.13, Math.sin(a) * r0),
          new THREE.Vector3(Math.cos(a) * r1, -0.13, Math.sin(a) * r1),
        ]);
        const tickMat = new THREE.LineBasicMaterial({
          color: ACCENT, transparent: true, opacity: isMajor ? 0.6 : 0.22,
        });
        wheelRoot.add(new THREE.Line(tickGeo, tickMat));
      }

      // SEGMENTS
      const segCount = STAGES.length;
      const angleStep = (Math.PI * 2) / segCount;
      const gap = angleStep * 0.08;
      const segInner = 1.65, segOuter = 3.4;

      const segments = [];
      // Anchor point at the segment's outer-mid (used for label placement)
      const labelAnchorRadius = (segInner + segOuter) / 2;

      for (let i = 0; i < segCount; i++) {
        const startAngle = -angleStep / 2 + gap / 2;
        const endAngle   =  angleStep / 2 - gap / 2;

        const shape = new THREE.Shape();
        const steps = 24;
        for (let s = 0; s <= steps; s++) {
          const t = s / steps;
          const a = startAngle + (endAngle - startAngle) * t;
          shape[s === 0 ? 'moveTo' : 'lineTo'](
            Math.cos(a) * segOuter, Math.sin(a) * segOuter
          );
        }
        for (let s = steps; s >= 0; s--) {
          const t = s / steps;
          const a = startAngle + (endAngle - startAngle) * t;
          shape.lineTo(Math.cos(a) * segInner, Math.sin(a) * segInner);
        }
        shape.closePath();

        const segGeo = new THREE.ExtrudeGeometry(shape, {
          depth: 0.18, bevelEnabled: true,
          bevelThickness: 0.012, bevelSize: 0.014, bevelSegments: 2, steps: 1,
        });
        segGeo.rotateX(-Math.PI / 2);

        const segMat = new THREE.MeshStandardMaterial({
          color: SURFACE, metalness: 0, roughness: 0.85,
          emissive: 0x000000, emissiveIntensity: 0,
        });
        const mesh = new THREE.Mesh(segGeo, segMat);
        const edges = new THREE.EdgesGeometry(segGeo, 28);
        const edgeMat = new THREE.LineBasicMaterial({
          color: ACCENT_DIM, transparent: true, opacity: 0.55,
        });
        const edgeLines = new THREE.LineSegments(edges, edgeMat);

        const segGroup = new THREE.Group();
        segGroup.add(mesh);
        segGroup.add(edgeLines);
        // Each segment placed at angle = -i*step around Y
        // The shape was built around local angle 0, so when wheel
        // rotation is FRONT_ANGLE + i*step, segment i lands at FRONT_ANGLE.
        segGroup.rotation.y = -i * angleStep;
        wheelRoot.add(segGroup);

        // Anchor object (invisible) for label projection — sits at the
        // outer-mid of this segment, in the segment's LOCAL space.
        const anchor = new THREE.Object3D();
        anchor.position.set(labelAnchorRadius, 0, 0);
        segGroup.add(anchor);

        segments.push({ mesh, edgeLines, edgeMat, mat: segMat, group: segGroup, anchor, idx: i });
      }

      // Hub & halo removed — open centre lets the photo backdrop read clearly.

      // Front-center marker (static, in scene not in wheelRoot)
      const markerGroup = new THREE.Group();
      markerGroup.rotation.x = wheelRoot.rotation.x;
      const markerShape = new THREE.Shape();
      markerShape.moveTo(0, 0);
      markerShape.lineTo(-0.14, -0.22);
      markerShape.lineTo( 0.14, -0.22);
      markerShape.closePath();
      const markerGeo = new THREE.ShapeGeometry(markerShape);
      markerGeo.rotateX(-Math.PI / 2);
      const marker = new THREE.Mesh(markerGeo, new THREE.MeshBasicMaterial({
        color: ACCENT, transparent: true, opacity: 0.95, side: THREE.DoubleSide,
      }));
      // Position at front of wheel (+Z direction) just inside outer edge
      marker.position.set(0, 0.02, 3.95);
      markerGroup.add(marker);
      scene.add(markerGroup);

      // ---- LABELS (HTML) ---------------------------------------
      // Pre-create label DOM nodes
      const labelEls = [];
      labelsLayer.innerHTML = '';
      for (let i = 0; i < segCount; i++) {
        const el = document.createElement('button');
        el.type = 'button';
        el.className = 'phw-label';
        el.innerHTML = `
          <span class="phw-label-num">${(i + 1).toString().padStart(2, '0')}</span>
          <span class="phw-label-text">${STAGES[i].label}</span>
        `;
        el.addEventListener('click', (e) => {
          e.stopPropagation();
          const st = stateRef.current;
          if (st.activeIdx === i) {
            // Already active — fire activate (open detail)
            if (onStageActivate) onStageActivate(STAGES[i], i);
          } else {
            // Snap to it
            st.activeIdx = i;
            st.targetRot = FRONT_ANGLE + i * angleStep;
            while (st.targetRot - st.currentRot >  Math.PI) st.targetRot -= Math.PI * 2;
            while (st.targetRot - st.currentRot < -Math.PI) st.targetRot += Math.PI * 2;
            if (onStageChange) onStageChange(STAGES[i], i);
          }
        });
        labelsLayer.appendChild(el);
        labelEls.push(el);
      }

      // ---- INTERACTION (canvas) --------------------------------
      const raycaster = new THREE.Raycaster();
      const mouse = new THREE.Vector2();

      function pickSegment(clientX, clientY) {
        const rect = canvas.getBoundingClientRect();
        mouse.x = ((clientX - rect.left) / rect.width) * 2 - 1;
        mouse.y = -((clientY - rect.top) / rect.height) * 2 + 1;
        raycaster.setFromCamera(mouse, camera);
        const hits = raycaster.intersectObjects(segments.map(s => s.mesh), false);
        if (hits.length > 0) {
          return segments.findIndex(s => s.mesh === hits[0].object);
        }
        return -1;
      }

      let dragMoved = 0;
      function onPointerDown(e) {
        const st = stateRef.current;
        st.dragging = true;
        st.dragLastX = e.clientX;
        st.dragVelocity = 0;
        st.lastMoveTime = performance.now();
        dragMoved = 0;
        canvas.setPointerCapture?.(e.pointerId ?? 1);
      }
      function onPointerMove(e) {
        const st = stateRef.current;
        if (st.dragging) {
          const dx = e.clientX - st.dragLastX;
          const rotDelta = (dx / canvas.clientWidth) * Math.PI * 1.6;
          // Drag right -> wheel rotates positive (clockwise from above)
          st.targetRot += rotDelta;
          st.currentRot += rotDelta;
          st.dragLastX = e.clientX;
          dragMoved += Math.abs(dx);
          st.dragVelocity = rotDelta / Math.max(0.016, (performance.now() - st.lastMoveTime) / 1000);
          st.lastMoveTime = performance.now();
        } else {
          const idx = pickSegment(e.clientX, e.clientY);
          if (idx !== st.hoverIdx) {
            st.hoverIdx = idx;
            container.style.cursor = idx >= 0 ? 'pointer' : 'grab';
          }
        }
      }
      function onPointerUp(e) {
        const st = stateRef.current;
        if (!st.dragging) return;
        st.dragging = false;
        canvas.releasePointerCapture?.(e.pointerId ?? 1);
        // Snap to nearest segment so the active one ends at FRONT_ANGLE
        // wheelRot = FRONT_ANGLE + activeIdx*step
        // -> activeIdx = round((wheelRot - FRONT_ANGLE) / step)
        const idxRaw = (st.targetRot - FRONT_ANGLE) / angleStep;
        const nearestIdx = Math.round(idxRaw);
        const wrapped = ((nearestIdx % segCount) + segCount) % segCount;
        st.activeIdx = wrapped;
        st.targetRot = FRONT_ANGLE + nearestIdx * angleStep;
        if (onStageChange) onStageChange(STAGES[wrapped], wrapped);
      }
      function onClick(e) {
        const st = stateRef.current;
        if (dragMoved > 5) return; // suppress click after a real drag
        const idx = pickSegment(e.clientX, e.clientY);
        if (idx >= 0) {
          if (st.activeIdx === idx) {
            // Click on already-active segment → open detail
            if (onStageActivate) onStageActivate(STAGES[idx], idx);
          } else {
            // Click on a non-active segment → snap to it
            st.activeIdx = idx;
            st.targetRot = FRONT_ANGLE + idx * angleStep;
            while (st.targetRot - st.currentRot >  Math.PI) st.targetRot -= Math.PI * 2;
            while (st.targetRot - st.currentRot < -Math.PI) st.targetRot += Math.PI * 2;
            if (onStageChange) onStageChange(STAGES[idx], idx);
          }
        }
      }

      canvas.addEventListener('pointerdown', onPointerDown);
      window.addEventListener('pointermove', onPointerMove);
      window.addEventListener('pointerup', onPointerUp);
      canvas.addEventListener('click', onClick);

      function resize() {
        const w = container.clientWidth;
        const h = container.clientHeight;
        renderer.setSize(w, h, false);
        camera.aspect = w / Math.max(1, h);
        camera.updateProjectionMatrix();
      }
      resize();
      const ro = new ResizeObserver(resize);
      ro.observe(container);

      // Initial state
      stateRef.current.targetRot  = FRONT_ANGLE + initialStage * angleStep;
      stateRef.current.currentRot = stateRef.current.targetRot;
      wheelRoot.rotation.y = stateRef.current.currentRot;
      if (onStageChange) onStageChange(STAGES[initialStage], initialStage);

      // ---- ANIMATE ---------------------------------------------
      const startTime = performance.now();
      const tmpVec = new THREE.Vector3();
      let raf;
      function tick() {
        const st = stateRef.current;
        const now = performance.now();
        const elapsed = (now - startTime) / 1000;

        // Smooth toward target
        const ease = 0.10;
        st.currentRot += (st.targetRot - st.currentRot) * ease;
        wheelRoot.rotation.y = st.currentRot;

        // Per-segment animation
        const TWO_PI = Math.PI * 2;
        for (let i = 0; i < segments.length; i++) {
          const seg = segments[i];
          // Segment world angle around Y = -i*step + currentRot
          const worldAngle = (-i * angleStep + st.currentRot) % TWO_PI;
          // Distance from FRONT_ANGLE
          let d = worldAngle - FRONT_ANGLE;
          // wrap to [-π, π]
          while (d >  Math.PI) d -= TWO_PI;
          while (d < -Math.PI) d += TWO_PI;
          const dist = Math.abs(d);
          // Closeness to front: 1 at front, 0 by ~one segment away
          const closeness = Math.max(0, 1 - dist / (angleStep * 0.55));
          const isActive = i === st.activeIdx;

          // Lift toward camera: front segments lift Y AND tilt forward in Z
          // Effective lift only matters at the front; back segments stay flat.
          const targetY = isActive ? 0.32 : closeness * 0.06;
          seg.group.position.y += (targetY - seg.group.position.y) * 0.18;

          // Pull active segment slightly TOWARD camera (positive Z in wheel-local space)
          // We accomplish this by scaling its outer/inner radius in local space — but
          // simpler: nudge its position outward along the +Z direction of the wheel.
          // Actually, "front-center" already points to +Z, and the segment shape lives
          // around its local +X axis (built around angle 0). So "toward the camera" for
          // the ACTIVE segment is along the wheelRoot's +Z, which after wheel rotation
          // varies. Skip Z-translation; the Y lift + glow + scale is enough.
          const targetScale = isActive ? 1.06 : 1.0;
          const sCur = seg.group.scale.x;
          const sNew = sCur + (targetScale - sCur) * 0.18;
          seg.group.scale.set(sNew, sNew, sNew);

          // Active emissive glow — keep base color dark so photo reads clearly
          const targetEmissive = isActive ? 0.20 : (i === st.hoverIdx ? 0.10 : 0.04);
          seg.mat.emissiveIntensity += (targetEmissive - seg.mat.emissiveIntensity) * 0.18;
          seg.mat.emissive.setHex(isActive ? 0x2A3A20 : (i === st.hoverIdx ? 0x1A2415 : 0x000000));

          // Keep base color dark for both states — the photo overlay carries the
          // visual difference between active and inactive.
          seg.mat.color.lerp(new THREE.Color(SURFACE), 0.12);

          seg.edgeMat.opacity += ((isActive ? 1.0 : 0.40) - seg.edgeMat.opacity) * 0.18;
          seg.edgeMat.color.setHex(isActive ? ACCENT : ACCENT_DIM);

          // ---- Project label position to screen space -----------
          // Anchor world position
          seg.anchor.getWorldPosition(tmpVec);
          // Project to NDC
          tmpVec.project(camera);
          // To pixels
          const halfW = canvas.clientWidth / 2;
          const halfH = canvas.clientHeight / 2;
          const px = tmpVec.x * halfW + halfW;
          const py = -tmpVec.y * halfH + halfH;
          const labelEl = labelEls[i];
          // The labels are placed OUTSIDE the wheel — push them outward along
          // the radial direction from wheel center.
          // Compute wheel center in screen space too (project origin)
          // For simplicity: extend by a fixed pixel offset based on segment angle.
          // Screen-space radial direction: from canvas center to the projected anchor.
          const dx = px - halfW;
          const dy = py - halfH;
          const len = Math.hypot(dx, dy) || 1;
          const outset = 36; // px outward from outer edge
          const lx = px + (dx / len) * outset;
          const ly = py + (dy / len) * outset;
          labelEl.style.transform = `translate(-50%, -50%) translate(${lx}px, ${ly}px)`;

          // Active label styling
          if (isActive) labelEl.classList.add('is-active');
          else labelEl.classList.remove('is-active');

          // Fade labels at the back of the wheel (where dist is large)
          const labelOpacity = Math.max(0.25, 1 - dist / Math.PI * 0.7);
          labelEl.style.opacity = labelOpacity.toFixed(3);
        }

        // (Hub & halo removed)

        renderer.render(scene, camera);
        raf = requestAnimationFrame(tick);
      }
      tick();

      return () => {
        cancelAnimationFrame(raf);
        ro.disconnect();
        canvas.removeEventListener('pointerdown', onPointerDown);
        window.removeEventListener('pointermove', onPointerMove);
        window.removeEventListener('pointerup', onPointerUp);
        canvas.removeEventListener('click', onClick);
        renderer.dispose();
        segments.forEach(s => {
          s.mesh.geometry.dispose(); s.mat.dispose();
          s.edgeLines.geometry.dispose(); s.edgeMat.dispose();
        });
      };
    }, []);

    return (
      <div className="phw-wrap" ref={containerRef}>
        <canvas ref={canvasRef} className="phw-canvas" />
        <div className="phw-labels-layer" ref={labelsLayerRef} aria-hidden="false"></div>

        <div className="phw-hint">
          <span className="phw-hint-icon">↻</span>
          <span>Drag to spin · click a stage</span>
        </div>

        <div className="phw-front-marker" aria-hidden="true">
          <span className="phw-front-marker-text">Selected</span>
        </div>
      </div>
    );
  });

  window.PHENOLOGICAL_STAGES = STAGES;
})();
