import * as THREE from "three";

export type VisualizerState = "idle" | "listening" | "thinking" | "speaking";

export interface AgentVisualizerHandle {
  setAudioLevel(level: number): void;
  setState(state: VisualizerState): void;
  setConnected(connected: boolean): void;
  setConnecting(connecting: boolean): void;
  destroy(): void;
}

// ---------------------------------------------------------------------------
// Geometry
// ---------------------------------------------------------------------------

function createParaboloidRing(
  radius = 1.1,
  segments = 320,
  tubeRadius = 0.007,
): THREE.BufferGeometry {
  const points: THREE.Vector3[] = [];
  for (let i = 0; i <= segments; i++) {
    const theta = (i / segments) * Math.PI * 2;
    points.push(new THREE.Vector3(radius * Math.cos(theta), 0, radius * Math.sin(theta)));
  }
  const curve = new THREE.CatmullRomCurve3(points, true);
  const geo = new THREE.TubeGeometry(curve, segments, tubeRadius, 12, true);

  const posCount = geo.attributes.position.count;
  const tAttr = new Float32Array(posCount);
  const tubularSegments = segments;
  const radialSegments = 12;
  for (let tube = 0; tube <= tubularSegments; tube++) {
    const tVal = tube / tubularSegments;
    for (let rad = 0; rad <= radialSegments; rad++) {
      tAttr[tube * (radialSegments + 1) + rad] = tVal;
    }
  }
  geo.setAttribute("aRingT", new THREE.BufferAttribute(tAttr, 1));
  return geo;
}

// ---------------------------------------------------------------------------
// Shaders
// ---------------------------------------------------------------------------

const ringVertexShader = `
  attribute float aRingT;
  uniform float uPhase;
  uniform float uAmplitude;
  varying float vWorldX;
  void main() {
    float theta = aRingT * 6.28318530718;
    vec3 pos = position;
    pos.y += uAmplitude * cos(5.0 * theta + uPhase);
    vec4 wp = modelMatrix * vec4(pos, 1.0);
    vWorldX = wp.x;
    gl_Position = projectionMatrix * viewMatrix * wp;
  }
`;

const ringFragmentShader = `
  uniform vec3 uColor;
  uniform float uFade;
  uniform float uFadeOffset;
  varying float vWorldX;
  void main() {
    float alpha = 1.0 - uFade * smoothstep(0.0, 1.0, (-vWorldX + uFadeOffset * 1.1) / 1.1 * 0.5 + 0.5);
    gl_FragColor = vec4(uColor, alpha);
  }
`;

// ---------------------------------------------------------------------------
// Frame update helpers
// ---------------------------------------------------------------------------

interface FrameState {
  currentAudioLevel: number;
  smoothAudioLevel: number;
  smoothAudioLevel2: number;
  deformScale: number;
  ringScale: number;
  spinSpeed: number;
  cardColorT: number;
  connectedT: number;
  prevCardRGB: string;
  targetConnected: number;
  isConnecting: boolean;
  currentState: VisualizerState;
}

interface RingPair {
  mat1: THREE.ShaderMaterial;
  mat2: THREE.ShaderMaterial;
}

const CARD_GRAY_RGB = [232, 228, 224];
const CARD_IDLE_RGB = [212, 85, 63];
const CARD_LISTEN_RGB = [120, 40, 28];

function updateAudio(s: FrameState, t: number): void {
  const lerpAudio = 1 - 0.85 ** t;
  const lerpAudio2 = 1 - 0.94 ** t;
  s.smoothAudioLevel += (s.currentAudioLevel - s.smoothAudioLevel) * lerpAudio;
  s.smoothAudioLevel2 += (s.currentAudioLevel - s.smoothAudioLevel2) * lerpAudio2;
}

interface SceneRefs {
  group: THREE.Group;
  camera: THREE.OrthographicCamera;
  speakingSideView: THREE.Vector3;
  topView: THREE.Vector3;
  lookAt: THREE.Vector3;
  applyFrustum: () => void;
  orthoScaleRef: { value: number };
}

function updateGeometry(s: FrameState, refs: SceneRefs, t: number, now: number): void {
  const speaking = s.currentState === "speaking";
  const { group, camera, speakingSideView, topView, lookAt, applyFrustum, orthoScaleRef } = refs;

  let targetDeformScale = 1.0 + s.smoothAudioLevel * 1.1;
  if (speaking) {
    targetDeformScale = 2.05 + s.smoothAudioLevel * 2.9;
  } else if (s.currentState === "thinking") {
    targetDeformScale = 0.55 + s.smoothAudioLevel * 0.35;
  }
  s.deformScale += (targetDeformScale - s.deformScale) * (1 - 0.88 ** t);
  group.scale.y = s.deformScale;

  const targetRingScale =
    s.currentState === "thinking"
      ? 1.0 + 0.18 * (0.5 + 0.5 * Math.sin((now * (Math.PI * 2)) / 1000))
      : 1.0;
  s.ringScale += (targetRingScale - s.ringScale) * (1 - 0.9 ** t);
  group.scale.x = s.ringScale;
  group.scale.z = s.ringScale;

  const targetSpinSpeed = speaking
    ? 0.012 + s.smoothAudioLevel * 0.105
    : s.currentState === "thinking"
      ? 0.006
      : 0.0022;
  s.spinSpeed += (targetSpinSpeed - s.spinSpeed) * (1 - 0.86 ** t);
  group.rotation.y += s.spinSpeed * t;

  if (speaking || camera.position.distanceToSquared(topView) > 0.0001) {
    const lerpCamera = 1 - 0.96 ** t;
    const targetPos = speaking ? speakingSideView : topView;
    camera.position.lerp(targetPos, lerpCamera);
    camera.lookAt(lookAt);
  }

  const camLen = camera.position.length();
  const sideT = camLen > 0.001 ? Math.abs(camera.position.x) / camLen : 0;
  const targetOrthoScale = 1.0 - sideT * 0.3;
  if (Math.abs(targetOrthoScale - orthoScaleRef.value) > 0.0001) {
    orthoScaleRef.value += (targetOrthoScale - orthoScaleRef.value) * (1 - 0.88 ** t);
    applyFrustum();
  }
}

function updateShaderUniforms(
  s: FrameState,
  rings: RingPair,
  camera: THREE.OrthographicCamera,
  t: number,
  now: number,
): void {
  const orthoCamera = camera;
  const camLen = orthoCamera.position.length();
  const sideT = camLen > 0.001 ? Math.abs(orthoCamera.position.x) / camLen : 0;
  const lerpSide = 1 - 0.88 ** t;
  const lerpAmp = 1 - 0.88 ** t;
  const baseAmp = 0.06 * 1.1;
  const speaking = s.currentState === "speaking";

  rings.mat1.uniforms.uFade.value += (sideT - rings.mat1.uniforms.uFade.value) * lerpSide;
  rings.mat1.uniforms.uFadeOffset.value +=
    (sideT * 0.8 - rings.mat1.uniforms.uFadeOffset.value) * lerpSide;
  rings.mat2.uniforms.uFade.value += (sideT - rings.mat2.uniforms.uFade.value) * lerpSide;
  rings.mat2.uniforms.uFadeOffset.value +=
    (sideT * 0.8 - rings.mat2.uniforms.uFadeOffset.value) * lerpSide;

  if (speaking) {
    const breathe = Math.sin(now * 0.0018);
    const base = 1.8 + s.smoothAudioLevel * 4.0;
    rings.mat1.uniforms.uPhase.value += (base + breathe * 0.6) * (t / 60);
    rings.mat2.uniforms.uPhase.value +=
      (base - breathe * 1.4 + s.smoothAudioLevel * 2.0) * (t / 60);
    const targetAmp1 = baseAmp * (1.0 + s.smoothAudioLevel * 3.5);
    const targetAmp2 = baseAmp * (1.0 + s.smoothAudioLevel2 * 3.5);
    rings.mat1.uniforms.uAmplitude.value +=
      (targetAmp1 - rings.mat1.uniforms.uAmplitude.value) * lerpAmp;
    rings.mat2.uniforms.uAmplitude.value +=
      (targetAmp2 - rings.mat2.uniforms.uAmplitude.value) * lerpAmp;
  } else {
    rings.mat2.uniforms.uPhase.value +=
      (Math.PI - rings.mat2.uniforms.uPhase.value) * (1 - 0.92 ** t);
    rings.mat1.uniforms.uAmplitude.value +=
      (baseAmp - rings.mat1.uniforms.uAmplitude.value) * lerpAmp;
    rings.mat2.uniforms.uAmplitude.value +=
      (baseAmp - rings.mat2.uniforms.uAmplitude.value) * lerpAmp;
  }
}

function updateCardColor(
  s: FrameState,
  renderer: THREE.WebGLRenderer,
  t: number,
  now: number,
): void {
  s.connectedT += (s.targetConnected - s.connectedT) * (1 - 0.88 ** t);
  const throb =
    s.isConnecting && s.targetConnected === 0
      ? 0.22 * (0.5 - 0.5 * Math.sin((now * (Math.PI * 2)) / 1000))
      : 0.0;

  const baseR = Math.round(
    CARD_GRAY_RGB[0] +
      (CARD_IDLE_RGB[0] - CARD_GRAY_RGB[0]) * s.connectedT -
      throb * CARD_GRAY_RGB[0],
  );
  const baseG = Math.round(
    CARD_GRAY_RGB[1] +
      (CARD_IDLE_RGB[1] - CARD_GRAY_RGB[1]) * s.connectedT -
      throb * CARD_GRAY_RGB[1],
  );
  const baseB = Math.round(
    CARD_GRAY_RGB[2] +
      (CARD_IDLE_RGB[2] - CARD_GRAY_RGB[2]) * s.connectedT -
      throb * CARD_GRAY_RGB[2],
  );

  const targetCardT = s.currentState === "listening" ? 1.0 : 0.0;
  const cardBase = targetCardT > s.cardColorT ? 0.05 : 0.7;
  s.cardColorT += (targetCardT - s.cardColorT) * (1 - cardBase ** t);

  const r = Math.min(255, Math.round(baseR + (CARD_LISTEN_RGB[0] - baseR) * s.cardColorT));
  const g = Math.min(255, Math.round(baseG + (CARD_LISTEN_RGB[1] - baseG) * s.cardColorT));
  const b = Math.min(255, Math.round(baseB + (CARD_LISTEN_RGB[2] - baseB) * s.cardColorT));
  const cardRGB = `${r},${g},${b}`;
  if (cardRGB !== s.prevCardRGB) {
    renderer.setClearColor((r << 16) | (g << 8) | b, 1);
    s.prevCardRGB = cardRGB;
  }
}

// ---------------------------------------------------------------------------
// Public factory
// ---------------------------------------------------------------------------

interface SceneSetup {
  scene: THREE.Scene;
  camera: THREE.OrthographicCamera;
  group: THREE.Group;
  rings: RingPair;
  lookAt: THREE.Vector3;
  speakingSideView: THREE.Vector3;
  topView: THREE.Vector3;
}

function buildScene(): SceneSetup {
  const scene = new THREE.Scene();
  const orthoSize = 2.0;
  const camera = new THREE.OrthographicCamera(
    -orthoSize,
    orthoSize,
    orthoSize,
    -orthoSize,
    0.1,
    40,
  );
  const lookAt = new THREE.Vector3(0, 0, 0);
  const speakingSideView = new THREE.Vector3(3.45, 0, 0);
  const topView = new THREE.Vector3(0, 3.25, 0.001);
  camera.position.copy(topView);
  camera.lookAt(lookAt);
  scene.add(new THREE.AmbientLight(0xffffff, 1.0));

  const makeRingMaterial = (phase: number) =>
    new THREE.ShaderMaterial({
      uniforms: {
        uColor: { value: new THREE.Color(0xfff5eb) },
        uFade: { value: 0.0 },
        uFadeOffset: { value: 0.0 },
        uPhase: { value: phase },
        uAmplitude: { value: 0.06 * 1.1 },
      },
      vertexShader: ringVertexShader,
      fragmentShader: ringFragmentShader,
      transparent: true,
      side: THREE.DoubleSide,
      depthWrite: false,
    });

  const mat1 = makeRingMaterial(0.0);
  const mat2 = makeRingMaterial(Math.PI);
  const rings: RingPair = { mat1, mat2 };

  const group = new THREE.Group();
  group.add(new THREE.Mesh(createParaboloidRing(), mat1));
  group.add(new THREE.Mesh(createParaboloidRing(), mat2));
  group.rotation.y = Math.PI * 0.18;
  scene.add(group);

  return { scene, camera, group, rings, lookAt, speakingSideView, topView };
}

// ---------------------------------------------------------------------------
// Frame state factory
// ---------------------------------------------------------------------------

function makeFrameState(): FrameState {
  return {
    currentAudioLevel: 0,
    smoothAudioLevel: 0,
    smoothAudioLevel2: 0,
    deformScale: 1.0,
    ringScale: 1.0,
    spinSpeed: 0.0,
    cardColorT: 0.0,
    connectedT: 0.0,
    prevCardRGB: "",
    targetConnected: 0.0,
    isConnecting: false,
    currentState: "idle",
  };
}

// ---------------------------------------------------------------------------
// Render loop
// ---------------------------------------------------------------------------

interface RenderLoopOpts {
  renderer: THREE.WebGLRenderer;
  scene: THREE.Scene;
  camera: THREE.OrthographicCamera;
  frameState: FrameState;
  sceneRefs: SceneRefs;
  rings: RingPair;
}

function startRenderLoop(opts: RenderLoopOpts): () => void {
  const { renderer, scene, camera, frameState, sceneRefs, rings } = opts;
  let lastNow = 0;
  let rafId = 0;
  let destroyed = false;

  const renderFrame = (now = 0) => {
    if (destroyed) return;
    const dt = Math.min((now - lastNow) / 1000, 0.1);
    lastNow = now;
    const t = dt * 60;
    updateAudio(frameState, t);
    updateGeometry(frameState, sceneRefs, t, now);
    updateShaderUniforms(frameState, rings, camera, t, now);
    updateCardColor(frameState, renderer, t, now);
    renderer.render(scene, camera);
    rafId = requestAnimationFrame(renderFrame);
  };
  rafId = requestAnimationFrame(renderFrame);

  return () => {
    destroyed = true;
    cancelAnimationFrame(rafId);
  };
}

// ---------------------------------------------------------------------------
// Public factory
// ---------------------------------------------------------------------------

export function createAgentVisualizer(container: HTMLElement): AgentVisualizerHandle {
  const renderer = new THREE.WebGLRenderer({
    antialias: true,
    alpha: false,
    powerPreference: "high-performance",
  });
  renderer.setPixelRatio(1);
  renderer.setClearColor(0xe8e4e0, 1);
  renderer.domElement.dataset.ptt = "1";
  container.innerHTML = "";
  container.appendChild(renderer.domElement);

  const { scene, camera, group, rings, lookAt, speakingSideView, topView } = buildScene();
  const orthoSize = 2.0;
  const orthoScaleRef = { value: 1.0 };

  const applyFrustum = () => {
    const width = Math.max(2, container.clientWidth);
    const height = Math.max(2, container.clientHeight);
    const aspect = width / height;
    const s = orthoSize * orthoScaleRef.value;
    if (aspect >= 1) {
      camera.left = -s * aspect;
      camera.right = s * aspect;
      camera.top = s;
      camera.bottom = -s;
    } else {
      camera.left = -s;
      camera.right = s;
      camera.top = s / aspect;
      camera.bottom = -s / aspect;
    }
    camera.updateProjectionMatrix();
  };

  const resize = () => {
    renderer.setSize(
      Math.max(2, container.clientWidth),
      Math.max(2, container.clientHeight),
      false,
    );
    applyFrustum();
  };
  resize();
  window.addEventListener("resize", resize);

  const sceneRefs: SceneRefs = {
    group,
    camera,
    speakingSideView,
    topView,
    lookAt,
    applyFrustum,
    orthoScaleRef,
  };
  const frameState = makeFrameState();
  const stopLoop = startRenderLoop({ renderer, scene, camera, frameState, sceneRefs, rings });

  return {
    setAudioLevel(level: number) {
      frameState.currentAudioLevel = Math.max(0, Math.min(1, Number(level) || 0));
    },
    setState(state: VisualizerState) {
      frameState.currentState = state;
    },
    setConnected(connected: boolean) {
      frameState.targetConnected = connected ? 1.0 : 0.0;
      if (connected) frameState.isConnecting = false;
    },
    setConnecting(connecting: boolean) {
      frameState.isConnecting = !!connecting;
    },
    destroy() {
      stopLoop();
      window.removeEventListener("resize", resize);
      renderer.dispose();
    },
  };
}
