// Swarm simulation core: pure functions over immutable-ish state.
// Models patrol flight, separation, obstacle avoidance, mesh links with
// pose-broadcast flashes and opportunistic bulk sync transfers.

export interface Vec {
  x: number;
  y: number;
}

export interface Drone {
  id: number;
  name?: string; // live mode: real drone_id from the data plane
  pos: Vec;
  vel: Vec;
  heading: number; // radians
  waypoint: number;
  battery: number; // 0..100
  channel: number; // Wi-Fi channel currently in use
}

export interface Obstacle {
  id: number;
  pos: Vec;
  radius: number;
  moving: boolean;
  vel: Vec;
}

export interface LinkStats {
  key: string;
  a: number;
  b: number;
  totalUp: number; // bytes, a -> b
  totalDown: number; // bytes, b -> a
  rate: number; // current bytes/s (both directions)
  flash: number; // 0..1, decaying broadcast pulse
  bulk: number; // remaining bytes of an in-flight bulk transfer
}

export interface World {
  t: number;
  drones: Drone[];
  obstacles: Obstacle[];
  links: Map<string, LinkStats>;
  totalBytes: number;
  broadcasts: number;
}

export const AREA = 1000; // meters, vertical extent of the field
export const MAX_ASPECT = 2.2;
// Horizontal extent follows the display aspect ratio (set via makeWorld),
// so wide monitors get a genuinely wider patrol area, not empty margins.
let areaX = AREA;
export function areaWidth(): number {
  return areaX;
}
export const LINK_RANGE = 320;
const CRUISE = 28;
const SEPARATION = 55;
const POSE_BYTES = 46;
const POSE_HZ = 5;
// Steady per-link broadcast throughput (both directions), bytes/s
export const POSE_RATE_BYTES = POSE_BYTES * POSE_HZ;
const CHANNELS = [1, 6, 11, 36, 40, 44, 149, 157];

let seedState = 1234;
export function seed(v: number): void {
  seedState = v || 1;
}
function rnd(): number {
  // xorshift32 — deterministic runs
  seedState ^= seedState << 13;
  seedState ^= seedState >>> 17;
  seedState ^= seedState << 5;
  return ((seedState >>> 0) % 100000) / 100000;
}

// Two interleaved patrol routes: even drones circle the perimeter, odd
// drones fly an X through the center. Paths cross mid-field, so the mesh
// keeps bridging between the front and the back of the formation instead
// of stretching into disconnected segments along one loop. Routes are
// derived from the current field width, so they stretch on wide displays.
function perimeterRoute(): Vec[] {
  return [
    { x: 120, y: 120 },
    { x: areaX - 120, y: 150 },
    { x: areaX - 150, y: AREA - 120 },
    { x: 150, y: AREA - 150 },
  ];
}
function crossRoute(): Vec[] {
  return [
    { x: 150, y: 150 },
    { x: areaX - 150, y: AREA - 150 },
    { x: areaX - 150, y: 150 },
    { x: 150, y: AREA - 150 },
  ];
}
function routeOf(d: Drone): Vec[] {
  return d.id % 2 === 0 ? perimeterRoute() : crossRoute();
}

// Transit objects enter at one edge, cross the whole area, and respawn at
// a fresh edge once they leave — a stream of through-traffic instead of
// obstacles bouncing until they wedge into a corner.
const TRANSIT_MARGIN = 120;
function spawnTransit(id: number): Obstacle {
  const speed = 26 + rnd() * 30;
  const alongX = 120 + rnd() * (areaX - 240);
  const alongY = 120 + rnd() * (AREA - 240);
  const skew = (rnd() - 0.5) * speed * 0.9; // diagonal-ish crossings
  const edge = Math.floor(rnd() * 4);
  const table: { pos: Vec; vel: Vec }[] = [
    { pos: { x: alongX, y: -TRANSIT_MARGIN + 20 }, vel: { x: skew, y: speed } },
    { pos: { x: alongX, y: AREA + TRANSIT_MARGIN - 20 }, vel: { x: skew, y: -speed } },
    { pos: { x: -TRANSIT_MARGIN + 20, y: alongY }, vel: { x: speed, y: skew } },
    { pos: { x: areaX + TRANSIT_MARGIN - 20, y: alongY }, vel: { x: -speed, y: skew } },
  ];
  const { pos, vel } = table[edge];
  return { id, pos, radius: 22 + rnd() * 16, moving: true, vel };
}
function outOfTransit(ob: Obstacle): boolean {
  return (
    ob.pos.x < -TRANSIT_MARGIN ||
    ob.pos.x > areaX + TRANSIT_MARGIN ||
    ob.pos.y < -TRANSIT_MARGIN ||
    ob.pos.y > AREA + TRANSIT_MARGIN
  );
}

export function makeWorld(droneCount: number, aspect = 1): World {
  areaX = AREA * Math.max(1, Math.min(MAX_ASPECT, aspect));
  const drones: Drone[] = Array.from({ length: droneCount }, (_, i) => {
    const angle = (i / droneCount) * Math.PI * 2;
    return {
      id: i,
      pos: {
        x: areaX / 2 + Math.cos(angle) * (150 + rnd() * 120),
        y: AREA / 2 + Math.sin(angle) * (150 + rnd() * 120),
      },
      vel: { x: 0, y: 0 },
      heading: angle,
      waypoint: i % 4,
      battery: 90 + rnd() * 10,
      channel: CHANNELS[i % CHANNELS.length],
    };
  });
  // Static obstacles sit at fixed fractions of the field, so they spread
  // out instead of clustering left when the field widens
  const obstacles: Obstacle[] = [
    { id: 0, pos: { x: areaX * 0.34, y: 420 }, radius: 60, moving: false, vel: { x: 0, y: 0 } },
    { id: 1, pos: { x: areaX * 0.7, y: 260 }, radius: 45, moving: false, vel: { x: 0, y: 0 } },
    { id: 2, pos: { x: areaX * 0.56, y: 720 }, radius: 70, moving: false, vel: { x: 0, y: 0 } },
    spawnTransit(3),
    spawnTransit(4),
    spawnTransit(5),
  ];
  return { t: 0, drones, obstacles, links: new Map(), totalBytes: 0, broadcasts: 0 };
}

function linkKey(a: number, b: number): string {
  return a < b ? `${a}-${b}` : `${b}-${a}`;
}

function steer(d: Drone, world: World, dt: number): Drone {
  const route = routeOf(d);
  const wp = route[(d.waypoint + d.id) % route.length];
  let ax = wp.x - d.pos.x;
  let ay = wp.y - d.pos.y;
  const wpDist = Math.hypot(ax, ay);
  let waypoint = d.waypoint;
  if (wpDist < 90) waypoint = (d.waypoint + 1) % route.length;
  ax /= wpDist || 1;
  ay /= wpDist || 1;

  // Separation from peers
  for (const other of world.drones) {
    if (other.id === d.id) continue;
    const dx = d.pos.x - other.pos.x;
    const dy = d.pos.y - other.pos.y;
    const dist = Math.hypot(dx, dy);
    if (dist < SEPARATION && dist > 0.01) {
      const push = (SEPARATION - dist) / SEPARATION;
      ax += (dx / dist) * push * 2.4;
      ay += (dy / dist) * push * 2.4;
    }
  }
  // Obstacle avoidance
  for (const ob of world.obstacles) {
    const dx = d.pos.x - ob.pos.x;
    const dy = d.pos.y - ob.pos.y;
    const dist = Math.hypot(dx, dy);
    const margin = ob.radius + 55;
    if (dist < margin && dist > 0.01) {
      const push = (margin - dist) / margin;
      ax += (dx / dist) * push * 4.0;
      ay += (dy / dist) * push * 4.0;
    }
  }

  const mag = Math.hypot(ax, ay) || 1;
  const vx = d.vel.x * 0.85 + (ax / mag) * CRUISE * 0.15;
  const vy = d.vel.y * 0.85 + (ay / mag) * CRUISE * 0.15;
  const heading = Math.atan2(vy, vx);
  return {
    ...d,
    waypoint,
    vel: { x: vx, y: vy },
    heading,
    pos: {
      x: Math.max(20, Math.min(areaX - 20, d.pos.x + vx * dt)),
      y: Math.max(20, Math.min(AREA - 20, d.pos.y + vy * dt)),
    },
    battery: Math.max(0, d.battery - dt * 0.05),
    channel: rnd() < dt * 0.15 ? CHANNELS[Math.floor(rnd() * CHANNELS.length)] : d.channel,
  };
}

export function tick(world: World, dt: number): World {
  const drones = world.drones.map((d) => steer(d, world, dt));
  const obstacles = world.obstacles.map((ob) => {
    if (!ob.moving) return ob;
    const moved = {
      ...ob,
      pos: { x: ob.pos.x + ob.vel.x * dt, y: ob.pos.y + ob.vel.y * dt },
    };
    // Once a transit object leaves the area, respawn it at a random edge
    return outOfTransit(moved) ? spawnTransit(ob.id) : moved;
  });

  const links = new Map(world.links);
  let totalBytes = world.totalBytes;
  let broadcasts = world.broadcasts;

  // Decay all links; drop the ones out of range
  for (const [key, l] of links) {
    const a = drones[l.a];
    const b = drones[l.b];
    const inRange =
      a && b && Math.hypot(a.pos.x - b.pos.x, a.pos.y - b.pos.y) <= LINK_RANGE;
    if (!inRange) {
      links.delete(key);
      continue;
    }
    links.set(key, { ...l, flash: Math.max(0, l.flash - dt * 3), rate: l.rate * 0.9 });
  }

  for (let i = 0; i < drones.length; i++) {
    for (let j = i + 1; j < drones.length; j++) {
      const dist = Math.hypot(
        drones[i].pos.x - drones[j].pos.x,
        drones[i].pos.y - drones[j].pos.y,
      );
      if (dist > LINK_RANGE) continue;
      const key = linkKey(i, j);
      const link =
        links.get(key) ??
        ({ key, a: i, b: j, totalUp: 0, totalDown: 0, rate: 0, flash: 0, bulk: 0 } as LinkStats);

      // Pose broadcasts: 5 Hz each direction
      const poseBytes = POSE_BYTES * POSE_HZ * dt;
      let up = poseBytes;
      let down = poseBytes;
      if (rnd() < dt * POSE_HZ * 0.35) {
        link.flash = 1;
        broadcasts += 1;
      }
      // Occasional bulk sync (sealed partition replication)
      if (link.bulk <= 0 && rnd() < dt * 0.02) {
        link.bulk = 50_000 + rnd() * 450_000;
      }
      if (link.bulk > 0) {
        const chunk = Math.min(link.bulk, 120_000 * dt);
        link.bulk -= chunk;
        if (rnd() < 0.5) up += chunk;
        else down += chunk;
      }
      link.totalUp += up;
      link.totalDown += down;
      link.rate = link.rate * 0.9 + ((up + down) / dt) * 0.1;
      totalBytes += up + down;
      links.set(key, { ...link });
    }
  }

  return { t: world.t + dt, drones, obstacles, links, totalBytes, broadcasts };
}

export function fmtBytes(n: number): string {
  if (n < 1024) return `${n.toFixed(0)} B`;
  if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
  if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
  return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
}
