// Room lifecycle + matchmaking. A room holds members (humans across rounds);
// each round is a fresh Match of the members present at countdown, topped up
// with bots to `size`. Timing per the spec: countdown fires as soon as 2 humans
// are in, or when the lobby wait expires with at least 1.

import { Match, PALETTE, CFG } from './sim.js';
import { BotBrain, nextBotName } from './bots.js';
import { MAPS, MODES } from '../public/maps.js';

export const GAME_MODES = Object.keys(MODES);
export const GAME_MAPS = Object.keys(MAPS);

const env = globalThis.process?.env ?? {}; // this module also runs in the browser

export const ROOM_CFG = {
  size: 12,
  minSize: 2,
  lobbyWait: Number(env.TRON_LOBBY_WAIT ?? 10),
  publicWait: Number(env.TRON_PUBLIC_WAIT ?? 3),
  countdown: Number(env.TRON_COUNTDOWN ?? 3),
  endPause: Number(env.TRON_END_PAUSE ?? 6),
  restartWait: Number(env.TRON_RESTART_WAIT ?? 5),
};

let nextId = 0;

export class Room {
  constructor(code, isPrivate, simCfg = {}, fillBots = true, mode = 'classic', map = 'empty', size = ROOM_CFG.size, roomCfg = {}) {
    this.cfg = { ...ROOM_CFG, ...roomCfg }; // the browser build shortens the waits
    this.code = code;
    this.private = isPrivate;
    this.fillBots = fillBots;
    this.mode = MODES[mode] ? mode : 'classic';
    this.map = MAPS[map] ? map : 'empty';
    // races run on the circuit and only there
    if (this.mode === 'race') this.map = 'circuit';
    else if (MAPS[this.map].raceOnly) this.map = 'empty';
    this.size = Number.isFinite(size)
      ? Math.max(this.cfg.minSize, Math.min(this.cfg.size, Math.round(size))) : this.cfg.size;
    // mode shrink profile + map dimensions ride on top of env overrides
    const m = MAPS[this.map];
    this.simCfg = {
      ...simCfg,
      ...MODES[this.mode],
      arena: m.arena,
      spawnRadius: m.spawnRadius,
      spawnRings: m.spawnRings,
      obstacles: m.obstacles || [],
      gates: m.gates,
      waypoints: m.waypoints,
      spawnGrid: m.spawnGrid,
      padCount: m.padCount,
      padSpots: m.padSpots,
      pickupSpots: m.pickupSpots,
    };
    this.state = 'lobby';        // lobby | countdown | running | ended
    this.members = new Map();    // id -> {id, name, color, shape, pattern, bot:false, wins}
    this.match = null;
    this.brains = [];
    this.bots = [];              // bot roster for the current round
    this.time = 0;               // room clock, seconds
    this.lobbyEnd = null;
    this.countdownEnd = null;
    this.endAt = null;
    this.events = [];            // room + match events to broadcast this tick
    this.lastResult = null;
  }

  humanCount() { return this.members.size; }

  freeColor() {
    const used = new Set([...this.members.values()].map(m => m.color));
    for (const b of this.bots) used.add(b.color);
    return PALETTE.find(c => !used.has(c)) || PALETTE[0];
  }

  joinable() {
    return this.humanCount() < this.size &&
      (this.private || this.state === 'lobby' || this.state === 'ended');
  }

  addHuman(name, reqColor, shape = 'blade', pattern = 'solid') {
    if (this.humanCount() >= this.size) return null;
    const used = new Set(this.roster().map(m => m.color));
    const color = reqColor && !used.has(reqColor) ? reqColor : this.freeColor();
    const m = { id: ++nextId, name, color, shape, pattern, bot: false, wins: 0 };
    this.members.set(m.id, m);
    this.events.push({ t: 'p+', p: { ...m } });
    if (this.state === 'lobby' && this.lobbyEnd === null) {
      const wait = this.private ? this.cfg.lobbyWait : this.cfg.publicWait;
      this.lobbyEnd = this.time + wait;
      this.events.push({ t: 'lobby', secs: wait });
    }
    return m;
  }

  removeHuman(id) {
    if (!this.members.delete(id)) return;
    this.events.push({ t: 'p-', id });
    if (this.match) {
      const b = this.match.bikes.get(id);
      if (b) this.match.retire(b);
    }
    if (this.humanCount() === 0) {
      this.state = 'lobby';
      this.lobbyEnd = null;
      this.match = null;
      this.brains = [];
      this.bots = [];
    }
  }

  startCountdown() {
    this.state = 'countdown';
    this.countdownEnd = this.time + this.cfg.countdown;
    // fill with bots up to size (unless the room was created with bots off)
    this.bots = [];
    while (this.fillBots && this.members.size + this.bots.length < this.size) {
      this.bots.push({
        id: ++nextId, name: nextBotName(), color: null,
        shape: 'blade', pattern: 'solid', bot: true, wins: 0,
      });
      this.bots[this.bots.length - 1].color = this.freeColor();
    }
    this.events.push({
      t: 'countdown', secs: this.cfg.countdown,
      roster: this.roster().map(memberPayload),
    });
  }

  roster() { return [...this.members.values(), ...this.bots]; }

  startRound() {
    this.state = 'running';
    this.match = new Match(this.roster(), Math.random, this.simCfg);
    this.brains = this.bots.map(b => new BotBrain(this.match, b.id));
    this.events.push({ t: 'round_start', ...this.match.fullState() });
  }

  endRound() {
    this.state = 'ended';
    this.endAt = this.time + this.cfg.endPause;
    const winnerId = this.match ? (this.match.winner ? this.match.winner.id : 0) : 0;
    const winner = this.members.get(winnerId) || this.bots.find(b => b.id === winnerId);
    if (winner) winner.wins++;
    // one-shot result payload the transport turns into profile awards
    this.lastResult = {
      consumed: false,
      winnerId,
      riders: this.match ? this.match.standings.length : 0,
      standings: this.match ? this.match.standings : [],
    };
  }

  backToLobby() {
    this.match = null;
    this.brains = [];
    this.bots = [];
    this.state = 'lobby';
    this.lobbyEnd = this.humanCount() > 0 ? this.time + this.cfg.restartWait : null;
    this.events.push({ t: 'lobby', secs: this.lobbyEnd === null ? null : this.cfg.restartWait });
  }

  tick(dt) {
    this.time += dt;
    switch (this.state) {
      case 'lobby': {
        // with bots off a solo round would end instantly, so hold for 2 humans
        const minHumans = this.fillBots ? 1 : 2;
        if (this.humanCount() >= 2 || (this.lobbyEnd !== null && this.time >= this.lobbyEnd)) {
          if (this.humanCount() >= minHumans) this.startCountdown();
        }
        break;
      }
      case 'countdown':
        if (this.time >= this.countdownEnd) this.startRound();
        break;
      case 'running': {
        this.match.step(dt);
        for (const brain of this.brains) brain.think(dt);
        this.events.push(...this.match.drainEvents());
        if (this.match.over) this.endRound();
        break;
      }
      case 'ended':
        if (this.time >= this.endAt) this.backToLobby();
        break;
    }
  }

  drainEvents() {
    const ev = this.events;
    this.events = [];
    return ev;
  }

  fullState(youId) {
    return {
      t: 'room',
      code: this.code,
      private: this.private,
      state: this.state,
      you: youId,
      cfg: {
        arena: this.simCfg.arena ?? CFG.arena, size: this.size, fillBots: this.fillBots,
        mode: this.mode, map: this.map, trailMaxArc: this.simCfg.trailMaxArc ?? CFG.trailMaxArc,
      },
      lobbySecs: this.lobbyEnd === null ? null : Math.max(0, r2(this.lobbyEnd - this.time)),
      countdownSecs: this.state === 'countdown' ? Math.max(0, r2(this.countdownEnd - this.time)) : null,
      members: this.roster().map(memberPayload),
      match: this.match ? this.match.fullState() : null,
    };
  }
}

export class RoomManager {
  constructor(simCfg = {}, roomCfg = {}) {
    this.rooms = new Map();
    this.simCfg = simCfg;
    this.roomCfg = roomCfg;
  }

  makeCode() {
    const chars = 'ABCDEFGHJKMNPQRSTVWXYZ23456789';
    let code;
    do {
      code = Array.from({ length: 4 }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
    } while (this.rooms.has(code));
    return code;
  }

  getRoom(code) { return this.rooms.get(code); }

  joinPublic(mode = 'classic', map = 'empty') {
    for (const room of this.rooms.values()) {
      if (!room.private && room.mode === mode && room.map === map && room.joinable()) return room;
    }
    const room = new Room(this.makeCode(), false, this.simCfg, true, mode, map, ROOM_CFG.size, this.roomCfg);
    this.rooms.set(room.code, room);
    return room;
  }

  joinPrivate(code, fillBots = true, mode = 'classic', map = 'empty', size = ROOM_CFG.size) {
    code = String(code).toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 8);
    if (!code) return null;
    let room = this.rooms.get(code);
    if (!room) {
      // the creator's toggles decide; later joiners can't flip them
      room = new Room(code, true, this.simCfg, fillBots, mode, map, size, this.roomCfg);
      this.rooms.set(code, room);
    }
    if (!room.private) return null; // can't sneak into a public room by code
    return room.humanCount() < room.size ? room : null;
  }

  tick(dt) {
    for (const room of this.rooms.values()) room.tick(dt);
  }

  gc() {
    for (const [code, room] of this.rooms) {
      if (room.humanCount() === 0) this.rooms.delete(code);
    }
  }
}

function r2(n) { return Math.round(n * 100) / 100; }

function memberPayload(m) {
  return {
    id: m.id, name: m.name, color: m.color, shape: m.shape, pattern: m.pattern,
    bot: m.bot, wins: m.wins,
  };
}
