import { clamp, distance, randomBetween, type Vector2 } from "./shared/math.ts";
import { markNativeReady, required } from "./shared/shell.ts";

markNativeReady();
interface FieldConfig { count: number; pointerForce: number; linkDistance: number }
class Particle implements Vector2 {
  x = 0; y = 0; vx = 0; vy = 0; radius = 2; hue = "#55d5dc";
  constructor(width: number, height: number) { this.reset(width, height); }
  reset(width: number, height: number): void { this.x = randomBetween(0, width); this.y = randomBetween(0, height); this.vx = randomBetween(-.4, .4); this.vy = randomBetween(-.4, .4); this.radius = randomBetween(1.2, 3.2); this.hue = Math.random() > .82 ? "#ff645f" : Math.random() > .5 ? "#55d5dc" : "#b6df63"; }
  update(width: number, height: number, pointer: Vector2 | null, force: number): void {
    if (pointer) { const dx = this.x - pointer.x; const dy = this.y - pointer.y; const d = Math.max(18, Math.hypot(dx, dy)); if (d < 180) { this.vx += (dx / d) * force / d; this.vy += (dy / d) * force / d; } }
    this.vx = clamp(this.vx * .994, -2.6, 2.6); this.vy = clamp(this.vy * .994, -2.6, 2.6); this.x += this.vx; this.y += this.vy;
    if (this.x < 0) this.x = width; if (this.x > width) this.x = 0; if (this.y < 0) this.y = height; if (this.y > height) this.y = 0;
  }
}

const canvas = required<HTMLCanvasElement>("#particle-field");
const context = canvas.getContext("2d")!;
const config: FieldConfig = { count: 120, pointerForce: 1.2, linkDistance: 120 };
let particles: Particle[] = [];
let pointer: Vector2 | null = null;
let lastFrame = performance.now(); let frameSamples: number[] = [];

function sizeCanvas(): void { const ratio = window.devicePixelRatio || 1; const rect = canvas.getBoundingClientRect(); canvas.width = Math.round(rect.width * ratio); canvas.height = Math.round(rect.height * ratio); context.setTransform(ratio, 0, 0, ratio, 0, 0); particles.forEach((particle) => particle.reset(rect.width, rect.height)); }
function syncCount(): void { while (particles.length < config.count) particles.push(new Particle(canvas.clientWidth, canvas.clientHeight)); particles.length = config.count; required<HTMLElement>("[data-particle-count]").textContent = String(config.count); }
function render(now: number): void {
  const width = canvas.clientWidth; const height = canvas.clientHeight;
  context.fillStyle = "rgba(5,7,9,.24)"; context.fillRect(0, 0, width, height);
  for (let i = 0; i < particles.length; i++) { const particle = particles[i]; particle.update(width, height, pointer, config.pointerForce); for (let j = i + 1; j < particles.length; j++) { const target = particles[j]; const d = distance(particle, target); if (d < config.linkDistance) { context.globalAlpha = (1 - d / config.linkDistance) * .22; context.strokeStyle = "#55d5dc"; context.beginPath(); context.moveTo(particle.x, particle.y); context.lineTo(target.x, target.y); context.stroke(); } } context.globalAlpha = 1; context.fillStyle = particle.hue; context.beginPath(); context.arc(particle.x, particle.y, particle.radius, 0, Math.PI * 2); context.fill(); }
  const delta = now - lastFrame; lastFrame = now; frameSamples.push(1000 / Math.max(1, delta)); if (frameSamples.length > 30) frameSamples.shift(); if (Math.round(now) % 12 === 0) required<HTMLElement>("[data-fps]").textContent = Math.round(frameSamples.reduce((a, b) => a + b, 0) / frameSamples.length).toString(); requestAnimationFrame(render);
}
function setPointer(event: PointerEvent): void { const rect = canvas.getBoundingClientRect(); pointer = { x: event.clientX - rect.left, y: event.clientY - rect.top }; required<HTMLElement>("[data-pointer-mode]").textContent = event.pointerType; }
canvas.addEventListener("pointermove", setPointer); canvas.addEventListener("pointerdown", setPointer); canvas.addEventListener("pointerleave", () => { pointer = null; required<HTMLElement>("[data-pointer-mode]").textContent = "idle"; });
const bindRange = (id: string, output: string, apply: (value: number) => void): void => { const input = required<HTMLInputElement>(id); input.addEventListener("input", () => { const value = Number(input.value); apply(value); required<HTMLOutputElement>(output).value = String(value); }); };
bindRange("#density", "[data-density-output]", (value) => { config.count = value; syncCount(); }); bindRange("#force", "[data-force-output]", (value) => { config.pointerForce = value / 10; }); bindRange("#links", "[data-links-output]", (value) => { config.linkDistance = value; });
required<HTMLButtonElement>("[data-pulse]").addEventListener("click", () => particles.forEach((particle) => { const angle = Math.random() * Math.PI * 2; particle.vx += Math.cos(angle) * 2; particle.vy += Math.sin(angle) * 2; }));
required<HTMLButtonElement>("[data-reset]").addEventListener("click", () => particles.forEach((particle) => particle.reset(canvas.clientWidth, canvas.clientHeight)));
window.addEventListener("resize", sizeCanvas); sizeCanvas(); syncCount(); requestAnimationFrame(render);
