import { Application, Graphics, Ticker } from "pixi.js";
import { Box, Circle, Line, System } from "check2d";

const PLAY_AREA = { x: 0, y: 0, width: 600, height: 600 };
const CIRCLE_RADIUS = 50;
const SQUARE_SIZE = 200;
const PLAYER_RADIUS = 16;
const PLAYER_ROTATE_SPEED = 0.05;
const PLAYER_THRUST = 0.2;
const PLAYER_DRAG = 0.98;
const PLAYER_TIP_DIST = 18;
const BULLET_SPEED = 8;
const BULLET_RADIUS = 1.5;

const COLOR_CIRCLE = 0xff3333;
const COLOR_SQUARE = 0x33ff66;
const COLOR_OVERLAP = 0xffff00;
const COLOR_PLAYER_OVERLAP = 0x880000;
const COLOR_PLAYER = 0xaaaaff;
const COLOR_BULLET = 0xffffff;

// angle=0 points up; tip and two rear corners of the triangle
const PLAYER_TIP = [0, -18] as const;
const PLAYER_REAR_L = [-11, 10] as const;
const PLAYER_REAR_R = [11, 10] as const;

interface Body {
  x: number;
  y: number;
  vx: number;
  vy: number;
  isCollided: boolean;
}

interface Player extends Body {
  angle: number; // radians, 0 = pointing up
}

interface Bullet {
  x: number;
  y: number;
  vx: number;
  vy: number;
}

interface World {
  circle: Body;
  square: Body;
  player: Player;
  bullets: Bullet[];
}

interface Colliders {
  system: System;
  circleCollider: Circle;
  squareCollider: Box;
  playerCollider: Circle;
  walls: Line[];
}

interface SceneGraphics {
  circleGfx: Graphics;
  squareGfx: Graphics;
  playerGfx: Graphics;
  bulletGfx: Graphics;
  offsetX: number;
  offsetY: number;
}

function createWorld(): World {
  return {
    circle: { x: 150, y: 200, vx: 2.5, vy: 1.8, isCollided: false },
    square: { x: 350, y: 350, vx: -1.2, vy: 2.0, isCollided: false },
    player: { x: 300, y: 300, vx: 0, vy: 0, angle: 0, isCollided: false },
    bullets: [],
  };
}

function buildColliders(world: World): Colliders {
  const system = new System();

  const circleCollider = new Circle(
    { x: world.circle.x, y: world.circle.y },
    CIRCLE_RADIUS,
  );
  const squareCollider = new Box(
    { x: world.square.x, y: world.square.y },
    SQUARE_SIZE,
    SQUARE_SIZE,
    { isCentered: true },
  );
  const playerCollider = new Circle(
    { x: world.player.x, y: world.player.y },
    PLAYER_RADIUS,
  );

  const { x, y, width, height } = PLAY_AREA;
  const walls = [
    new Line({ x, y }, { x, y: y + height }, { isStatic: true }), // left
    new Line(
      { x: x + width, y },
      { x: x + width, y: y + height },
      { isStatic: true },
    ), // right
    new Line({ x, y }, { x: x + width, y }, { isStatic: true }), // top
    new Line(
      { x, y: y + height },
      { x: x + width, y: y + height },
      { isStatic: true },
    ), // bottom
  ];

  for (const body of [
    circleCollider,
    squareCollider,
    playerCollider,
    ...walls,
  ]) {
    system.insert(body);
  }

  return { system, circleCollider, squareCollider, playerCollider, walls };
}

function resolveWallBounce(
  body: Body,
  collider: Circle | Box,
  wall: Line,
  system: System,
) {
  if (system.checkCollision(collider, wall)) {
    const r = system.response;
    body.x -= r.overlapV.x;
    body.y -= r.overlapV.y;
    collider.setPosition(body.x, body.y);
    if (Math.abs(r.overlapN.x) > Math.abs(r.overlapN.y)) {
      body.vx *= -1;
    } else {
      body.vy *= -1;
    }
  }
}

function tickPlayer(player: Player, keys: Set<string>, dt: number) {
  if (keys.has("ArrowLeft")) player.angle -= PLAYER_ROTATE_SPEED * dt;
  if (keys.has("ArrowRight")) player.angle += PLAYER_ROTATE_SPEED * dt;
  if (keys.has("ArrowUp")) {
    player.vx += Math.sin(player.angle) * PLAYER_THRUST * dt;
    player.vy -= Math.cos(player.angle) * PLAYER_THRUST * dt;
  }
  if (keys.has("ArrowDown")) {
    player.vx -= Math.sin(player.angle) * PLAYER_THRUST * dt;
    player.vy += Math.cos(player.angle) * PLAYER_THRUST * dt;
  }
  player.vx *= PLAYER_DRAG;
  player.vy *= PLAYER_DRAG;
  player.x += player.vx * dt;
  player.y += player.vy * dt;
}

function spawnBullet(world: World) {
  const { player } = world;
  world.bullets.push({
    x: player.x + Math.sin(player.angle) * PLAYER_TIP_DIST,
    y: player.y - Math.cos(player.angle) * PLAYER_TIP_DIST,
    vx: player.vx + Math.sin(player.angle) * BULLET_SPEED,
    vy: player.vy - Math.cos(player.angle) * BULLET_SPEED,
  });
}

function tickBullets(world: World, colliders: Colliders, dt: number) {
  const { system, walls } = colliders;
  const wallSet = new Set(walls);

  const hit = new Set<Bullet>();
  for (const b of world.bullets) {
    const prevX = b.x;
    const prevY = b.y;
    b.x += b.vx * dt;
    b.y += b.vy * dt;
    const result = system.raycast(
      { x: prevX, y: prevY },
      { x: b.x, y: b.y },
      (body) => wallSet.has(body as Line),
    );
    if (result) hit.add(b);
  }

  world.bullets = world.bullets.filter((b) => !hit.has(b));
}

function worldTick(
  world: World,
  colliders: Colliders,
  keys: Set<string>,
  dt: number,
) {
  const { circle, square, player } = world;
  const { circleCollider, squareCollider, playerCollider, walls, system } =
    colliders;

  circle.x += circle.vx * dt;
  circle.y += circle.vy * dt;
  square.x += square.vx * dt;
  square.y += square.vy * dt;
  tickPlayer(player, keys, dt);

  circleCollider.setPosition(circle.x, circle.y);
  squareCollider.setPosition(square.x, square.y);
  playerCollider.setPosition(player.x, player.y);

  for (const wall of walls) {
    resolveWallBounce(circle, circleCollider, wall, system);
    resolveWallBounce(square, squareCollider, wall, system);
    resolveWallBounce(player, playerCollider, wall, system);
  }

  const circleSquare = system.checkCollision(circleCollider, squareCollider);
  const circlePlayer = system.checkCollision(circleCollider, playerCollider);
  const squarePlayer = system.checkCollision(squareCollider, playerCollider);

  world.circle.isCollided = circleSquare || circlePlayer;
  world.square.isCollided = circleSquare || squarePlayer;
  world.player.isCollided = circlePlayer || squarePlayer;

  tickBullets(world, colliders, dt);
}

function buildWorldGraphics(app: Application): SceneGraphics {
  const offsetX = (app.screen.width - PLAY_AREA.width) / 2;
  const offsetY = (app.screen.height - PLAY_AREA.height) / 2;

  const playAreaGfx = new Graphics();
  playAreaGfx
    .rect(0, 0, PLAY_AREA.width, PLAY_AREA.height)
    .stroke({ color: 0xffffff, width: 2 });
  playAreaGfx.x = offsetX;
  playAreaGfx.y = offsetY;
  app.stage.addChild(playAreaGfx);

  const circleGfx = new Graphics();
  app.stage.addChild(circleGfx);

  const squareGfx = new Graphics();
  app.stage.addChild(squareGfx);

  const playerGfx = new Graphics();
  app.stage.addChild(playerGfx);

  const bulletGfx = new Graphics();
  app.stage.addChild(bulletGfx);

  return { circleGfx, squareGfx, playerGfx, bulletGfx, offsetX, offsetY };
}

function graphicsTick(world: World, gfx: SceneGraphics) {
  gfx.circleGfx.clear();
  gfx.circleGfx
    .circle(0, 0, CIRCLE_RADIUS)
    .fill(world.circle.isCollided ? COLOR_OVERLAP : COLOR_CIRCLE);
  gfx.circleGfx.x = gfx.offsetX + world.circle.x;
  gfx.circleGfx.y = gfx.offsetY + world.circle.y;

  gfx.squareGfx.clear();
  gfx.squareGfx
    .rect(-SQUARE_SIZE / 2, -SQUARE_SIZE / 2, SQUARE_SIZE, SQUARE_SIZE)
    .fill(world.square.isCollided ? COLOR_OVERLAP : COLOR_SQUARE);
  gfx.squareGfx.x = gfx.offsetX + world.square.x;
  gfx.squareGfx.y = gfx.offsetY + world.square.y;

  gfx.playerGfx.clear();
  gfx.playerGfx
    .poly([...PLAYER_TIP, ...PLAYER_REAR_L, ...PLAYER_REAR_R])
    .stroke({
      color: world.player.isCollided ? COLOR_PLAYER_OVERLAP : COLOR_PLAYER,
      width: 2,
    });
  gfx.playerGfx.x = gfx.offsetX + world.player.x;
  gfx.playerGfx.y = gfx.offsetY + world.player.y;
  gfx.playerGfx.rotation = world.player.angle;

  gfx.bulletGfx.clear();
  for (const b of world.bullets) {
    gfx.bulletGfx
      .circle(gfx.offsetX + b.x, gfx.offsetY + b.y, BULLET_RADIUS)
      .fill(COLOR_BULLET);
  }
}

(async () => {
  const app = new Application();
  await app.init({ background: "#111111", resizeTo: window, antialias: true });
  document.getElementById("pixi-container")!.appendChild(app.canvas);

  const keys = new Set<string>();
  const world = createWorld();
  const colliders = buildColliders(world);

  window.addEventListener("keydown", (e) => {
    if (e.key.startsWith("Arrow")) e.preventDefault();
    if (e.key === " ") {
      e.preventDefault();
      spawnBullet(world);
    }
    keys.add(e.key);
  });
  window.addEventListener("keyup", (e) => keys.delete(e.key));
  const gfx = buildWorldGraphics(app);

  app.ticker.add((ticker: Ticker) => {
    worldTick(world, colliders, keys, ticker.deltaTime);
    graphicsTick(world, gfx);
  });
})();