// output/boids-simulation/boid.js
// Boid implementation and constants. Speed is a fixed constant for all boids.
export const SPEED = 220;            // px/s — constant speed (not size-dependent)
const PERCEPTION = 55;               // neighborhood radius in px
const MAX_FORCE = 120;               // steering acceleration cap (px/s^2)
const ALIGN_WEIGHT = 1.0;
const COHESION_WEIGHT = 0.8;
const SEPARATION_WEIGHT = 1.35;

function randDir() {
  const a = Math.random() * Math.PI * 2;
  return { x: Math.cos(a), y: Math.sin(a) };
}
function mag(x, y) { return Math.hypot(x, y); }
function normalize(x, y) {
  const m = Math.hypot(x, y) || 1e-6;
  return { x: x / m, y: y / m };
}
function limit(x, y, max) {
  const m = Math.hypot(x, y);
  if (m > max) {
    const s = max / (m || 1e-6);
    return { x: x * s, y: y * s };
  }
  return { x, y };
}

export class Boid {
  constructor(x, y, hue) {
    this.x = x;
    this.y = y;
    const d = randDir();
    this.vx = d.x * SPEED;
    this.vy = d.y * SPEED;
    this.ax = 0;
    this.ay = 0;
    this.hue = hue;
    this.size = 8; // visual size (does not affect speed)
  }

  update(boids, w, h, dt) {
    // Neighborhood accumulators
    let alignX = 0, alignY = 0;
    let cohX = 0, cohY = 0;
    let sepX = 0, sepY = 0;
    let count = 0;

    for (let i = 0; i < boids.length; i++) {
      const other = boids[i];
      if (other === this) continue;
      const dx = other.x - this.x;
      const dy = other.y - this.y;
      const d2 = dx * dx + dy * dy;
      if (d2 < PERCEPTION * PERCEPTION) {
        const d = Math.sqrt(d2) || 1e-6;
        // Alignment: average of neighbor directions
        const ov = normalize(other.vx, other.vy);
        alignX += ov.x;
        alignY += ov.y;

        // Cohesion: average neighbor positions
        cohX += other.x;
        cohY += other.y;

        // Separation: steer away — weighted by inverse distance
        const aw = 1 / d;
        const away = normalize(this.x - other.x, this.y - other.y);
        sepX += away.x * aw;
        sepY += away.y * aw;

        count++;
      }
    }

    // Reset acceleration
    this.ax = 0;
    this.ay = 0;

    if (count > 0) {
      // Alignment
      alignX /= count; alignY /= count;
      let desired = normalize(alignX, alignY);
      desired.x *= SPEED; desired.y *= SPEED;
      let steer = { x: desired.x - this.vx, y: desired.y - this.vy };
      steer = limit(steer.x, steer.y, MAX_FORCE);
      this.ax += steer.x * ALIGN_WEIGHT;
      this.ay += steer.y * ALIGN_WEIGHT;

      // Cohesion
      const centerX = cohX / count, centerY = cohY / count;
      desired = normalize(centerX - this.x, centerY - this.y);
      desired.x *= SPEED; desired.y *= SPEED;
      steer = { x: desired.x - this.vx, y: desired.y - this.vy };
      steer = limit(steer.x, steer.y, MAX_FORCE);
      this.ax += steer.x * COHESION_WEIGHT;
      this.ay += steer.y * COHESION_WEIGHT;

      // Separation
      desired = normalize(sepX, sepY);
      desired.x *= SPEED; desired.y *= SPEED;
      steer = { x: desired.x - this.vx, y: desired.y - this.vy };
      steer = limit(steer.x, steer.y, MAX_FORCE * 1.2);
      this.ax += steer.x * SEPARATION_WEIGHT;
      this.ay += steer.y * SEPARATION_WEIGHT;
    }

    // Integrate
    this.vx += this.ax * dt;
    this.vy += this.ay * dt;

    // Enforce constant speed
    let v = normalize(this.vx, this.vy);
    this.vx = v.x * SPEED;
    this.vy = v.y * SPEED;

    this.x += this.vx * dt;
    this.y += this.vy * dt;

    // Wrap around edges (toroidal)
    if (this.x < 0) this.x += w;
    if (this.y < 0) this.y += h;
    if (this.x >= w) this.x -= w;
    if (this.y >= h) this.y -= h;
  }

  draw(ctx) {
    const a = Math.atan2(this.vy, this.vx);
    const len = this.size * 1.7;
    const wing = this.size * 0.9;

    const tipX = this.x + Math.cos(a) * len;
    const tipY = this.y + Math.sin(a) * len;
    const leftX = this.x + Math.cos(a + 2.5) * wing;
    const leftY = this.y + Math.sin(a + 2.5) * wing;
    const rightX = this.x + Math.cos(a - 2.5) * wing;
    const rightY = this.y + Math.sin(a - 2.5) * wing;

    ctx.fillStyle = `hsl(${this.hue}, 90%, 60%)`;
    ctx.beginPath();
    ctx.moveTo(tipX, tipY);
    ctx.lineTo(leftX, leftY);
    ctx.lineTo(rightX, rightY);
    ctx.closePath();
    ctx.fill();
  }
}
