/* @jsx React.createElement */
// ─────────────────────────────────────────────────────────────────────
// METASPEAK Cinematic Camera + Post-FX
// ─────────────────────────────────────────────────────────────────────
// Layers cinematic motion (dolly, parallax, idle breathing, speaker
// tracking, beat punches) on top of the user's static camera anchor
// from the Camera tab. The anchor remains the user's source of truth;
// cinematic just adds offsets that decay back to zero when idle.
//
// Architecture:
//
//   panel sliders (cameraOrbit/cameraTilt/cameraDistance/cameraHeight/cameraFov)
//                    │
//                    ▼
//        applyCameraFromTw()  →  baseAnchor (Vector3 + rotation)
//                    │
//                    ▼
//        CinematicCamera.update(dt, scene)
//          ├── compute targets: speaker midpoint + parallax offset
//          ├── ease toward target with critically damped springs
//          ├── add idle breathing + micro-shake
//          └── apply final transform to camera
//
// Public API:
//   - new CinematicCamera(camera, getBaseAnchor, opts)
//   - .focusSpeaker(slot, intensity)   — call when char starts talking
//   - .clearSpeaker()                  — call when both fall silent
//   - .punch(intensity)                — beat-driven impulse (e.g. on user click)
//   - .update(dt, charPositions)       — called each frame
//   - .setEnabled(bool)                — master on/off
//
// All motion is procedural — no keyframed animation, no pre-baked moves.
// The system reacts to the actual scene state (who's talking, recent
// beats, idle time).

(function() {

const _v = new THREE.Vector3();
const _v2 = new THREE.Vector3();
const _q = new THREE.Quaternion();

// Critically damped spring helper. Smoothly approaches `target` from
// `current` with a frequency `omega` (rad/s). Returns the new value.
// This avoids overshoot regardless of dt or large jumps. See:
//   https://www.gamedev.net/articles/programming/general-and-gameplay-programming/critically-damped-spring-smoothing-r4757/
function springStep(current, velocity, target, omega, dt) {
  const f = 1 + 2 * dt * omega;
  const oo = omega * omega;
  const hoo = dt * oo;
  const hhoo = dt * hoo;
  const detInv = 1 / (f + hhoo);
  const detX = f * current + dt * velocity + hhoo * target;
  const detV = velocity + hoo * (target - current);
  return {
    value: detX * detInv,
    velocity: detV * detInv,
  };
}

class CinematicCamera {
  constructor(camera, getBaseAnchor, opts = {}) {
    this.camera = camera;
    this.getBaseAnchor = getBaseAnchor;   // () → { position, lookAt, fov }
    this.enabled = true;

    // Tuneable behaviour params
    this.cfg = Object.assign({
      // Speaker focus — when a speaker is set, camera shifts a bit toward
      // them and reduces FOV slightly (cinematic close-up feel).
      // Lower omega = slower, smoother ease. 3.5 feels like a real camera op
      // following a conversation — not snappy enough to feel jerky.
      speakerOffsetMul: 0.55,    // how far camera nudges toward speaker (m on x)
      speakerFovDelta: -2.5,     // FOV change when focused (negative = zoom in)
      speakerSpring: 3.5,        // omega — lower = smoother ease, no overshoot

      // Idle breathing — slow procedural drift so it never feels static
      breathAmp: { x: 0.08, y: 0.05, z: 0.04 },
      breathFreq: { x: 0.18, y: 0.27, z: 0.21 },  // Hz, intentionally non-harmonic

      // Parallax — head-look effect during conversation. Camera leans
      // away from the active speaker just slightly (more cinematic than
      // pure follow). Counter-parallax makes both characters feel like
      // they're being framed.
      parallaxAmount: 0.18,
      parallaxSpring: 3.2,

      // Punch — impulse on dialogue beats. Decays with damped sine.
      punchAmp: 0.25,
      punchFreq: 6.5,            // Hz
      punchDecay: 4.5,           // exponential decay rate

      // Master cinematic intensity. 0 = no cinematic motion (anchor only).
      // 1 = full effect. Set via panel slider.
      intensity: 1,
    }, opts);

    // Internal state — all spring-driven offsets relative to base anchor
    this.state = {
      offsetX: { value: 0, velocity: 0 },   // speaker dolly (x)
      offsetY: { value: 0, velocity: 0 },   // breathing/parallax (y)
      offsetZ: { value: 0, velocity: 0 },   // (z)
      fovDelta: { value: 0, velocity: 0 },
      parallax: { value: 0, velocity: 0 },
    };

    // Speaker focus target — set externally via focusSpeaker()
    // null = neutral framing. 'char1' / 'char2' = focus that character.
    this.speakerSlot = null;
    this.speakerIntensity = 1;

    // Active punches (impulse animations). Each: { startTime, amp }
    this.punches = [];

    // Time tracker for breathing oscillators
    this.t = 0;

    // Cache last char positions for parallax
    this.charPositions = { char1: null, char2: null };
  }

  setEnabled(enabled) { this.enabled = enabled; }

  setIntensity(v) { this.cfg.intensity = Math.max(0, Math.min(1.5, v)); }

  /**
   * Tell the camera that a character started talking.
   * @param {'char1'|'char2'|null} slot
   * @param {number} intensity   0..1 — how strong the focus effect is
   */
  focusSpeaker(slot, intensity = 1) {
    this.speakerSlot = slot;
    this.speakerIntensity = Math.max(0, Math.min(1, intensity));
  }

  clearSpeaker() {
    this.speakerSlot = null;
  }

  /**
   * Beat-driven impulse — small camera punch on dialogue events.
   * Most useful when called from the chat handler each new beat.
   * @param {number} intensity   0..1.5
   */
  punch(intensity = 1) {
    this.punches.push({
      startTime: this.t,
      amp: Math.max(0, Math.min(1.5, intensity)) * this.cfg.punchAmp,
    });
    // Cap to avoid memory leak if user spams clicks
    if (this.punches.length > 8) this.punches.shift();
  }

  /**
   * Frame update — call each render frame.
   * @param {number} dt              seconds since last frame
   * @param {object} charPositions   { char1: Vector3, char2: Vector3 }
   */
  update(dt, charPositions) {
    this.t += dt;
    if (!this.enabled || this.cfg.intensity <= 0) {
      // Cinematic disabled — restore base anchor (user's panel settings)
      this._applyAnchorOnly();
      return;
    }

    if (charPositions) {
      this.charPositions.char1 = charPositions.char1 || this.charPositions.char1;
      this.charPositions.char2 = charPositions.char2 || this.charPositions.char2;
    }

    // ─── Speaker focus target ───────────────────────────────────────
    let targetOffsetX = 0;
    let targetFovDelta = 0;
    let targetParallax = 0;
    if (this.speakerSlot && this.charPositions[this.speakerSlot]) {
      // Compute speaker world x relative to scene center (mid of cast)
      const sp = this.charPositions[this.speakerSlot];
      const otherSlot = this.speakerSlot === 'char1' ? 'char2' : 'char1';
      const op = this.charPositions[otherSlot];
      const midX = op ? (sp.x + op.x) * 0.5 : 0;
      const dirX = sp.x - midX;
      // Push camera slightly TOWARD speaker — this puts speaker more
      // in the center of frame without losing the other character
      targetOffsetX = dirX * this.cfg.speakerOffsetMul * this.speakerIntensity;
      targetFovDelta = this.cfg.speakerFovDelta * this.speakerIntensity;
      targetParallax = -dirX * this.cfg.parallaxAmount * this.speakerIntensity;
    }

    // ─── Idle breathing oscillators ─────────────────────────────────
    // Very slow drift in 3 axes with non-harmonic frequencies so the
    // motion doesn't loop predictably. Multiplied by intensity.
    const breathX = Math.sin(this.t * this.cfg.breathFreq.x * Math.PI * 2) * this.cfg.breathAmp.x;
    const breathY = Math.sin(this.t * this.cfg.breathFreq.y * Math.PI * 2) * this.cfg.breathAmp.y;
    const breathZ = Math.sin(this.t * this.cfg.breathFreq.z * Math.PI * 2) * this.cfg.breathAmp.z;

    // ─── Step springs toward targets ────────────────────────────────
    const omega = this.cfg.speakerSpring;
    const omegaP = this.cfg.parallaxSpring;
    const sX = springStep(this.state.offsetX.value, this.state.offsetX.velocity, targetOffsetX, omega, dt);
    this.state.offsetX = sX;
    const sFov = springStep(this.state.fovDelta.value, this.state.fovDelta.velocity, targetFovDelta, omega, dt);
    this.state.fovDelta = sFov;
    const sPar = springStep(this.state.parallax.value, this.state.parallax.velocity, targetParallax, omegaP, dt);
    this.state.parallax = sPar;

    // ─── Punch impulses ─────────────────────────────────────────────
    let punchX = 0, punchY = 0;
    this.punches = this.punches.filter(p => {
      const age = this.t - p.startTime;
      if (age > 1.2) return false;   // expired
      const decay = Math.exp(-age * this.cfg.punchDecay);
      const wave = Math.sin(age * this.cfg.punchFreq * Math.PI * 2);
      punchX += p.amp * wave * decay * 0.6;
      punchY += p.amp * wave * decay;
      return true;
    });

    // ─── Compose final camera transform ─────────────────────────────
    const anchor = this.getBaseAnchor();
    if (!anchor) { this._applyAnchorOnly(); return; }

    const i = this.cfg.intensity;
    const finalOffsetX = (this.state.offsetX.value + breathX * i + punchX) * 1;
    const finalOffsetY = (breathY * i + punchY) * 1;
    const finalOffsetZ = breathZ * i;

    this.camera.position.set(
      anchor.position.x + finalOffsetX,
      anchor.position.y + finalOffsetY,
      anchor.position.z + finalOffsetZ
    );
    // Lookat with parallax — camera "looks" slightly past the speaker,
    // creating cinematic conversation framing
    const lookX = (anchor.lookAt?.x ?? 0) + this.state.parallax.value * i;
    const lookY = anchor.lookAt?.y ?? 1.55;
    const lookZ = anchor.lookAt?.z ?? 0;
    this.camera.lookAt(lookX, lookY, lookZ);
    this.camera.fov = (anchor.fov ?? 34) + this.state.fovDelta.value * i;
    this.camera.updateProjectionMatrix();
  }

  _applyAnchorOnly() {
    const anchor = this.getBaseAnchor();
    if (!anchor) return;
    this.camera.position.copy(anchor.position);
    this.camera.lookAt(
      anchor.lookAt?.x ?? 0,
      anchor.lookAt?.y ?? 1.55,
      anchor.lookAt?.z ?? 0
    );
    this.camera.fov = anchor.fov ?? 34;
    this.camera.updateProjectionMatrix();
  }
}

// ─────────────────────────────────────────────────────────────────────
// Extended Post-FX chain
// ─────────────────────────────────────────────────────────────────────
// Adds vignette + chromatic aberration + film grain on top of the
// existing UnrealBloomPass. Each effect can be toggled and tuned via
// panel sliders. We use raw three.js postprocessing passes (no
// react-postprocessing) to stay in the single-file kiosk model.

// Vignette shader — darkens edges of the frame for cinematic feel.
const VignetteShader = {
  uniforms: {
    tDiffuse: { value: null },
    offset:   { value: 1.0 },     // 0 = no falloff, 1.5 = strong
    darkness: { value: 1.0 },     // 0 = no effect, 1.5 = very dark
  },
  vertexShader: /* glsl */`
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */`
    uniform sampler2D tDiffuse;
    uniform float offset;
    uniform float darkness;
    varying vec2 vUv;
    void main() {
      vec4 texel = texture2D(tDiffuse, vUv);
      // Distance from center, normalized
      vec2 uv = vUv - 0.5;
      float dist = length(uv) * offset;
      // Smooth falloff — squaring gives a more film-like rolloff
      float vig = smoothstep(0.8, 0.2, dist * dist);
      texel.rgb = mix(texel.rgb * (1.0 - darkness * 0.7), texel.rgb, vig);
      gl_FragColor = texel;
    }
  `,
};

// Chromatic aberration — RGB channels offset radially, like cheap lenses.
// Subtle effect (offset ~0.001-0.003) adds analog photographic feel.
const ChromaticAberrationShader = {
  uniforms: {
    tDiffuse: { value: null },
    offset:   { value: 0.0015 },   // 0 = none, 0.005 = strong
  },
  vertexShader: /* glsl */`
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */`
    uniform sampler2D tDiffuse;
    uniform float offset;
    varying vec2 vUv;
    void main() {
      vec2 dir = vUv - 0.5;
      float r = texture2D(tDiffuse, vUv + dir * offset).r;
      float g = texture2D(tDiffuse, vUv).g;
      float b = texture2D(tDiffuse, vUv - dir * offset).b;
      float a = texture2D(tDiffuse, vUv).a;
      gl_FragColor = vec4(r, g, b, a);
    }
  `,
};

// Film grain — animated noise over the image. Adds analog texture.
const FilmGrainShader = {
  uniforms: {
    tDiffuse:  { value: null },
    time:      { value: 0 },
    intensity: { value: 0.05 },     // 0 = none, 0.15 = strong
  },
  vertexShader: /* glsl */`
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */`
    uniform sampler2D tDiffuse;
    uniform float time;
    uniform float intensity;
    varying vec2 vUv;
    // Hash-based noise — varies per frame and per pixel
    float hash(vec2 p) {
      return fract(sin(dot(p, vec2(12.9898, 78.233))) * 43758.5453);
    }
    void main() {
      vec4 texel = texture2D(tDiffuse, vUv);
      float grain = hash(vUv * 1024.0 + time) - 0.5;
      texel.rgb += grain * intensity;
      gl_FragColor = texel;
    }
  `,
};

// Color grading — simple LUT-free adjustment: lift/gamma/gain + saturation.
// Used to push warm tones for tropical Malaysia feel by default.
const ColorGradeShader = {
  uniforms: {
    tDiffuse:    { value: null },
    saturation:  { value: 1.05 },    // 1 = neutral
    contrast:    { value: 1.04 },    // 1 = neutral
    brightness:  { value: 0.0 },     // 0 = neutral
    tint:        { value: new THREE.Vector3(1.02, 1.0, 0.98) },  // warm
  },
  vertexShader: /* glsl */`
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */`
    uniform sampler2D tDiffuse;
    uniform float saturation;
    uniform float contrast;
    uniform float brightness;
    uniform vec3 tint;
    varying vec2 vUv;
    void main() {
      vec4 texel = texture2D(tDiffuse, vUv);
      // Brightness
      texel.rgb += brightness;
      // Contrast — pivot at 0.5
      texel.rgb = (texel.rgb - 0.5) * contrast + 0.5;
      // Saturation — mix with luma
      float luma = dot(texel.rgb, vec3(0.2126, 0.7152, 0.0722));
      texel.rgb = mix(vec3(luma), texel.rgb, saturation);
      // Tint
      texel.rgb *= tint;
      gl_FragColor = texel;
    }
  `,
};

window.MetaspeakCinematic = {
  CinematicCamera,
  VignetteShader,
  ChromaticAberrationShader,
  FilmGrainShader,
  ColorGradeShader,
};

})();
