import init, { SimulationWasm } from './pkg/flocking.js';

const BOIDS_PER_1920X1080 = 250;
const MIN_INITIAL_BOIDS = 100;
const MAX_INITIAL_BOIDS = 1000;
const BOID_STEP = 10;

let wasm = null;
let sim = null;
let canvas = null;
let ctx = null;
let controls = null;
let controlsToggle = null;
let controlsResizeObserver = null;
let frameCount = 0;
let lastFpsUpdate = Date.now();
let displayWidth = 1;
let displayHeight = 1;
let resizeAnimationFrame = 0;

function clamp(value, min, max) {
  return Math.min(Math.max(value, min), max);
}

function getInitialBoidCount(width, height) {
  const areaScale = (width * height) / (1920 * 1080);
  const targetCount = Math.round(BOIDS_PER_1920X1080 * areaScale);
  const clampedCount = clamp(targetCount, MIN_INITIAL_BOIDS, MAX_INITIAL_BOIDS);
  return Math.round(clampedCount / BOID_STEP) * BOID_STEP;
}

function syncBoidCountToViewport(width, height) {
  const targetCount = getInitialBoidCount(width, height);
  let currentCount = sim.get_boid_count();

  while (currentCount < targetCount) {
    sim.add_boid();
    currentCount = sim.get_boid_count();
  }

  while (currentCount > targetCount) {
    sim.remove_boid();
    currentCount = sim.get_boid_count();
  }

  updateBoidCount();
}

function updateControlsCollapsedState(isCollapsed) {
  if (controls === null || controlsToggle === null) {
    return;
  }

  controls.classList.toggle('collapsed', isCollapsed);
  controls.setAttribute('aria-expanded', String(!isCollapsed));
  controlsToggle.setAttribute('aria-expanded', String(!isCollapsed));
  controlsToggle.setAttribute(
    'aria-label',
    isCollapsed ? 'Expand controls' : 'Collapse controls'
  );
  controlsToggle.textContent = isCollapsed ? '‹' : '›';
}

function sizeCanvasToViewport() {
  const dpr = window.devicePixelRatio || 1;
  const nextDisplayWidth = Math.max(1, Math.floor(canvas.clientWidth));
  const nextDisplayHeight = Math.max(1, Math.floor(canvas.clientHeight));
  const nextBufferWidth = Math.max(1, Math.round(nextDisplayWidth * dpr));
  const nextBufferHeight = Math.max(1, Math.round(nextDisplayHeight * dpr));

  if (canvas.width !== nextBufferWidth || canvas.height !== nextBufferHeight) {
    canvas.width = nextBufferWidth;
    canvas.height = nextBufferHeight;
  }

  displayWidth = nextDisplayWidth;
  displayHeight = nextDisplayHeight;
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  document.getElementById('canvasSize').textContent =
    `${displayWidth}x${displayHeight}`;

  return { width: displayWidth, height: displayHeight };
}

function scheduleResize() {
  if (resizeAnimationFrame !== 0) {
    return;
  }

  resizeAnimationFrame = window.requestAnimationFrame(() => {
    resizeAnimationFrame = 0;
    const { width, height } = sizeCanvasToViewport();
    if (sim !== null) {
      sim.resize(width, height);
      syncBoidCountToViewport(width, height);
    }
  });
}

function setupResizeObserver() {
  controlsResizeObserver = new ResizeObserver(() => {
    scheduleResize();
  });
  controlsResizeObserver.observe(document.getElementById('container'));
  controlsResizeObserver.observe(controls);
}

async function run() {
  wasm = await init();

  canvas = document.getElementById('canvas');
  ctx = canvas.getContext('2d');
  controls = document.getElementById('controls');
  controlsToggle = document.getElementById('controlsToggle');
  updateControlsCollapsedState(false);

  const { width, height } = sizeCanvasToViewport();
  sim = new SimulationWasm(width, height, getInitialBoidCount(width, height));

  setupControls();
  setupResizeObserver();
  updateBoidCount();
  window.addEventListener('resize', scheduleResize);

  animate();
}

function setupControls() {
  const alignSlider = document.getElementById('alignSlider');
  const cohesionSlider = document.getElementById('cohesionSlider');
  const separationSlider = document.getElementById('separationSlider');
  const addBtn = document.getElementById('addBtn');
  const removeBtn = document.getElementById('removeBtn');

  controlsToggle.addEventListener('click', () => {
    const isCollapsed = !controls.classList.contains('collapsed');
    updateControlsCollapsedState(isCollapsed);
    scheduleResize();
  });

  alignSlider.addEventListener('input', (e) => {
    const value = Number.parseFloat(e.target.value);
    sim.set_align_mult(value);
    document.getElementById('alignValue').textContent = value.toFixed(2);
  });

  cohesionSlider.addEventListener('input', (e) => {
    const value = Number.parseFloat(e.target.value);
    sim.set_cohesion_mult(value);
    document.getElementById('cohesionValue').textContent = value.toFixed(2);
  });

  separationSlider.addEventListener('input', (e) => {
    const value = Number.parseFloat(e.target.value);
    sim.set_separation_mult(value);
    document.getElementById('separationValue').textContent = value.toFixed(2);
  });

  addBtn.addEventListener('click', () => {
    sim.add_boid();
    updateBoidCount();
  });

  removeBtn.addEventListener('click', () => {
    sim.remove_boid();
    updateBoidCount();
  });
}

function updateBoidCount() {
  document.getElementById('boidCount').textContent = sim.get_boid_count();
}

function getBoidView() {
  const ptr = sim.boid_buffer_ptr();
  const len = sim.boid_buffer_len();
  return new Float32Array(wasm.memory.buffer, ptr, len);
}

function animate() {
  ctx.fillStyle = '#121212';
  ctx.fillRect(0, 0, displayWidth, displayHeight);

  sim.step();
  const boidData = getBoidView();
  const stride = sim.boid_buffer_stride();

  for (let i = 0; i < boidData.length; i += stride) {
    const x = boidData[i];
    const y = boidData[i + 1];
    const vx = boidData[i + 2];
    const vy = boidData[i + 3];
    const r = boidData[i + 4];
    const g = boidData[i + 5];
    const b = boidData[i + 6];
    const a = boidData[i + 7];

    const speedSq = vx * vx + vy * vy;
    let dx;
    let dy;
    if (speedSq > 1e-6) {
      const invSpeed = 1 / Math.sqrt(speedSq);
      dx = vx * invSpeed;
      dy = vy * invSpeed;
    } else {
      dx = 0;
      dy = -1;
    }

    const size = 5;
    const headMult = 3;
    const px = -dy;
    const py = dx;

    const tipX = x + dx * size * headMult;
    const tipY = y + dy * size * headMult;
    const baseX = x - dx * size;
    const baseY = y - dy * size;
    const leftX = baseX + px * size;
    const leftY = baseY + py * size;
    const rightX = baseX - px * size;
    const rightY = baseY - py * size;

    ctx.strokeStyle = `rgba(${r},${g},${b},${a / 255})`;
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.moveTo(tipX, tipY);
    ctx.lineTo(leftX, leftY);
    ctx.lineTo(rightX, rightY);
    ctx.closePath();
    ctx.stroke();
  }

  frameCount += 1;
  const now = Date.now();
  if (now - lastFpsUpdate >= 1000) {
    document.getElementById('fps').textContent = frameCount;
    frameCount = 0;
    lastFpsUpdate = now;
  }

  requestAnimationFrame(animate);
}

await run();
