/**
 * GPU Particle System with Compute Shaders
 *
 * Demonstrates TSL compute shaders for particle simulation:
 * - Instanced array buffers
 * - Physics simulation on GPU
 * - Mouse interaction
 *
 * Based on Three.js webgpu_compute_particles example (MIT License)
 * https://github.com/mrdoob/three.js
 */

import * as THREE from 'three/webgpu';
import {
  Fn,
  If,
  uniform,
  float,
  vec3,
  color,
  instancedArray,
  instanceIndex,
  hash,
  time
} from 'three/tsl';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

let camera, scene, renderer, controls;
let computeInit, computeUpdate, computeHit;

// Particle count
const PARTICLE_COUNT = 100000;

// Storage buffers
const positions = instancedArray(PARTICLE_COUNT, 'vec3');
const velocities = instancedArray(PARTICLE_COUNT, 'vec3');

// Uniforms
const gravity = uniform(-9.8);
const bounce = uniform(0.7);
const friction = uniform(0.98);
const deltaTimeUniform = uniform(0);
const clickPosition = uniform(new THREE.Vector3());
const hitStrength = uniform(5.0);

async function init() {
  // Camera
  camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100);
  camera.position.set(0, 5, 15);

  // Scene
  scene = new THREE.Scene();
  scene.background = new THREE.Color(0x111122);

  // Create compute shaders
  createComputeShaders();

  // Create particle mesh
  createParticleMesh();

  // Floor
  const floorGeometry = new THREE.PlaneGeometry(30, 30);
  const floorMaterial = new THREE.MeshStandardNodeMaterial({
    color: 0x333333
  });
  const floor = new THREE.Mesh(floorGeometry, floorMaterial);
  floor.rotation.x = -Math.PI / 2;
  floor.receiveShadow = true;
  scene.add(floor);

  // Lights
  const ambientLight = new THREE.AmbientLight(0x404040);
  scene.add(ambientLight);

  const pointLight = new THREE.PointLight(0xffffff, 100);
  pointLight.position.set(5, 10, 5);
  scene.add(pointLight);

  // Renderer
  renderer = new THREE.WebGPURenderer({ antialias: true });
  renderer.setSize(window.innerWidth, window.innerHeight);
  renderer.setPixelRatio(window.devicePixelRatio);
  document.body.appendChild(renderer.domElement);
  await renderer.init();

  // Initialize particles
  await renderer.computeAsync(computeInit);

  // Controls
  controls = new OrbitControls(camera, renderer.domElement);
  controls.enableDamping = true;
  controls.target.set(0, 2, 0);

  // Events
  window.addEventListener('resize', onWindowResize);
  renderer.domElement.addEventListener('click', onClick);

  renderer.setAnimationLoop(animate);
}

function createComputeShaders() {
  // Grid dimensions for initialization
  const gridSize = Math.ceil(Math.sqrt(PARTICLE_COUNT));
  const spacing = 0.15;
  const offset = (gridSize * spacing) / 2;

  // Initialize particles in a grid
  computeInit = Fn(() => {
    const position = positions.element(instanceIndex);
    const velocity = velocities.element(instanceIndex);

    // Calculate grid position
    const x = instanceIndex.mod(gridSize);
    const z = instanceIndex.div(gridSize);

    // Set position
    position.x.assign(x.toFloat().mul(spacing).sub(offset));
    position.y.assign(float(5.0).add(hash(instanceIndex).mul(2.0)));
    position.z.assign(z.toFloat().mul(spacing).sub(offset));

    // Random initial velocity
    velocity.x.assign(hash(instanceIndex.add(1)).sub(0.5).mul(2.0));
    velocity.y.assign(hash(instanceIndex.add(2)).mul(-2.0));
    velocity.z.assign(hash(instanceIndex.add(3)).sub(0.5).mul(2.0));
  })().compute(PARTICLE_COUNT);

  // Physics update
  computeUpdate = Fn(() => {
    const position = positions.element(instanceIndex);
    const velocity = velocities.element(instanceIndex);
    const dt = deltaTimeUniform;

    // Apply gravity
    velocity.y.addAssign(gravity.mul(dt));

    // Update position
    position.addAssign(velocity.mul(dt));

    // Apply friction
    velocity.mulAssign(friction);

    // Ground collision
    If(position.y.lessThan(0), () => {
      position.y.assign(0);
      velocity.y.assign(velocity.y.abs().mul(bounce)); // Reverse and dampen

      // Extra friction on ground
      velocity.x.mulAssign(0.9);
      velocity.z.mulAssign(0.9);
    });

    // Boundary walls
    If(position.x.abs().greaterThan(15), () => {
      position.x.assign(position.x.sign().mul(15));
      velocity.x.assign(velocity.x.negate().mul(bounce));
    });

    If(position.z.abs().greaterThan(15), () => {
      position.z.assign(position.z.sign().mul(15));
      velocity.z.assign(velocity.z.negate().mul(bounce));
    });
  })().compute(PARTICLE_COUNT);

  // Hit/explosion effect
  computeHit = Fn(() => {
    const position = positions.element(instanceIndex);
    const velocity = velocities.element(instanceIndex);

    // Distance to click
    const toClick = position.sub(clickPosition);
    const distance = toClick.length();

    // Apply force within radius
    If(distance.lessThan(3.0), () => {
      const direction = toClick.normalize();
      const force = float(3.0).sub(distance).div(3.0).mul(hitStrength);

      // Add randomness
      const randomForce = force.mul(hash(instanceIndex.add(time.mul(1000))).mul(0.5).add(0.75));

      velocity.addAssign(direction.mul(randomForce));
      velocity.y.addAssign(randomForce.mul(0.5));
    });
  })().compute(PARTICLE_COUNT);
}

function createParticleMesh() {
  // Simple sphere geometry for each particle
  const geometry = new THREE.SphereGeometry(0.08, 8, 8);

  // Material using computed positions
  const material = new THREE.MeshStandardNodeMaterial();

  // Position from compute buffer
  material.positionNode = positions.element(instanceIndex);

  // Color based on velocity
  material.colorNode = Fn(() => {
    const velocity = velocities.element(instanceIndex);
    const speed = velocity.length();

    // Color gradient: blue (slow) -> orange (fast)
    const t = speed.div(10.0).saturate();
    return color(0x0066ff).mix(color(0xff6600), t);
  })();

  material.roughnessNode = float(0.5);
  material.metalnessNode = float(0.2);

  // Create instanced mesh
  const mesh = new THREE.InstancedMesh(geometry, material, PARTICLE_COUNT);
  scene.add(mesh);
}

function onClick(event) {
  // Raycast to find click position on floor
  const raycaster = new THREE.Raycaster();
  const mouse = new THREE.Vector2(
    (event.clientX / window.innerWidth) * 2 - 1,
    -(event.clientY / window.innerHeight) * 2 + 1
  );

  raycaster.setFromCamera(mouse, camera);

  // Intersect with floor plane (y = 0)
  const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
  const intersection = new THREE.Vector3();
  raycaster.ray.intersectPlane(plane, intersection);

  if (intersection) {
    // Raise click position slightly
    intersection.y = 0.5;
    clickPosition.value.copy(intersection);

    // Run hit compute shader
    renderer.compute(computeHit);
  }
}

function onWindowResize() {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
}

const clock = new THREE.Clock();

function animate() {
  // Update delta time
  deltaTimeUniform.value = Math.min(clock.getDelta(), 0.1);

  // Run physics compute
  renderer.compute(computeUpdate);

  controls.update();
  renderer.render(scene, camera);
}

init();
