import {
  PIECES,
  pieceById,
  rotatePiece,
  accepts,
  exitDir,
  channelKey,
  opp,
  DIRS,
  makeBag,
  mulberry32,
} from './pieces.js';
import { LEVELS, applyDifficulty, parScore, medalFor } from './levels.js';

function emptyCell() {
  return {
    kind: 'empty',
    pipe: null,
    fill: { ns: 0, ew: 0, main: 0 },
    entered: { ns: null, ew: null, main: null },
    locked: { ns: false, ew: false, main: false },
    placeT: 0,
    replaceFlash: 0,
  };
}

export class Game {
  constructor(settings, rngSeed = Date.now()) {
    this.settings = settings;
    this.seed = rngSeed;
    this.rng = mulberry32(rngSeed >>> 0);
    this.mode = 'campaign';
    this.levelIndex = 0;
    this.campaignScore = 0;
    this.lives = 3;
    this.listeners = new Set();
    this.resetEmpty();
  }

  on(fn) {
    this.listeners.add(fn);
    return () => this.listeners.delete(fn);
  }

  emit(type, data = {}) {
    for (const fn of this.listeners) fn(type, data, this);
  }

  resetEmpty() {
    this.phase = 'idle';
    this.board = [];
    this.cols = 0;
    this.rows = 0;
    this.queue = [];
    this.bag = [];
    this.held = null;
    this.countdown = 0;
    this.delayMax = 1;
    this.distance = 0;
    this.minDist = 8;
    this.score = 0;
    this.levelScore = 0;
    this.combo = 0;
    this.replaces = 0;
    this.unusedPenalty = 0;
    this.time = 0;
    this.heads = [];
    this.leak = null;
    this.shake = 0;
    this.flash = 0;
    this.popup = null;
    this.history = [];
    this.flowSpeed = 0.6;
    this.fast = false;
    this.reachedEnd = false;
    this.usedCrossLoop = 0;
    this.placedCount = 0;
    this.result = null;
    this.endlessDist = 0;
    this.endlessRamp = 0;
    this.warned = false;
    this.wrap = false;
    this.level = null;
    this.stats = { flowed: 0, tanks: 0, crosses: 0 };
  }

  startCampaign(fromLevel = 0) {
    this.mode = 'campaign';
    this.campaignScore = 0;
    this.lives = 3;
    this.startLevel(fromLevel);
  }

  startEndless() {
    this.mode = 'endless';
    this.campaignScore = 0;
    this.lives = 1;
    this.endlessDist = 0;
    this.endlessRamp = 0;
    const base = {
      id: 0,
      name: 'Endless Pressure',
      blurb: 'The valve never closes.',
      cols: 9,
      rows: 9,
      delay: 12,
      speed: 0.7,
      minDist: 9999,
      start: { x: 0, y: 4, dir: 'E' },
      end: null,
      wrap: true,
      walls: [
        [4, 0],
        [4, 8],
        [0, 0],
        [8, 8],
      ],
      crosses: true,
      tanks: true,
      oneWays: false,
    };
    this._load(base);
  }

  startLevel(index) {
    const raw = LEVELS[Math.max(0, Math.min(LEVELS.length - 1, index))];
    this.levelIndex = index;
    const cfg = applyDifficulty(raw, this.settings.difficulty);
    this._load(cfg);
  }

  retryLevel() {
    if (this.mode === 'endless') this.startEndless();
    else this.startLevel(this.levelIndex);
  }

  nextLevel() {
    if (this.mode !== 'campaign') return false;
    if (this.levelIndex >= LEVELS.length - 1) return false;
    this.startLevel(this.levelIndex + 1);
    return true;
  }

  _load(cfg) {
    this.resetEmpty();
    this.level = cfg;
    this.cols = cfg.cols;
    this.rows = cfg.rows;
    this.wrap = !!cfg.wrap;
    this.minDist = cfg.minDist;
    this.flowSpeed = cfg.speed;
    this.delayMax = cfg.delay;
    this.countdown = cfg.delay;
    this.board = [];
    for (let y = 0; y < this.rows; y++) {
      const row = [];
      for (let x = 0; x < this.cols; x++) row.push(emptyCell());
      this.board.push(row);
    }
    for (const [x, y] of cfg.walls || []) {
      if (this.inBounds(x, y)) this.board[y][x].kind = 'wall';
    }
    const s = cfg.start;
    const startCell = this.board[s.y][s.x];
    startCell.kind = 'start';
    startCell.pipe = {
      id: 'start',
      mask: { E: 2, W: 8, N: 1, S: 4 }[s.dir],
      name: 'Intake',
      delay: 0.85,
      independent: false,
      oneWay: s.dir,
    };
    startCell.locked.main = true;
    if (cfg.end) {
      const e = cfg.end;
      const endCell = this.board[e.y][e.x];
      endCell.kind = 'end';
      endCell.pipe = {
        id: 'end',
        mask: { E: 2, W: 8, N: 1, S: 4 }[e.dir],
        name: 'Drain',
        delay: 0.9,
        independent: false,
        oneWay: null,
        endDir: e.dir,
      };
    }
    this.bag = [];
    this.queue = [];
    for (let i = 0; i < 5; i++) this.queue.push(this._drawPiece());
    this.held = this.queue[0];
    this.phase = 'countdown';
    this.emit('level', { level: cfg });
  }

  _drawPiece() {
    if (!this.bag.length) this.bag = makeBag(this.level, this.rng);
    return this.bag.pop();
  }

  inBounds(x, y) {
    if (this.wrap) return true;
    return x >= 0 && y >= 0 && x < this.cols && y < this.rows;
  }

  wrapPos(x, y) {
    if (!this.wrap) return { x, y };
    return {
      x: ((x % this.cols) + this.cols) % this.cols,
      y: ((y % this.rows) + this.rows) % this.rows,
    };
  }

  cell(x, y) {
    const p = this.wrapPos(x, y);
    if (p.x < 0 || p.y < 0 || p.x >= this.cols || p.y >= this.rows) return null;
    return this.board[p.y][p.x];
  }

  rotateHeld(dir = 1) {
    if (!this.settings.rotatePieces) return false;
    if (this.phase !== 'countdown' && this.phase !== 'flowing') return false;
    const cur = this.queue[0];
    if (!cur) return false;
    const next = rotatePiece(cur, dir);
    this.queue[0] = next;
    this.held = next;
    this.emit('rotate');
    return true;
  }

  canPlace(x, y) {
    if (!this.inBounds(x, y) && !this.wrap) return false;
    const c = this.cell(x, y);
    if (!c) return false;
    if (c.kind === 'wall' || c.kind === 'start' || c.kind === 'end') return false;
    if (this._anyFill(c)) return false;
    return true;
  }

  _anyFill(c) {
    return c.fill.main > 0.02 || c.fill.ns > 0.02 || c.fill.ew > 0.02;
  }

  placeAt(x, y) {
    if (this.phase !== 'countdown' && this.phase !== 'flowing') return false;
    if (!this.canPlace(x, y)) {
      this.emit('invalid');
      return false;
    }
    const c = this.cell(x, y);
    const pid = this.queue[0];
    const replacing = !!c.pipe;
    const prev = c.pipe ? { id: c.pipe.id } : null;

    this.history.push({
      x,
      y,
      prevPipe: prev ? prev.id : null,
      prevKind: c.kind,
      queueHead: pid,
      queue: this.queue.slice(),
      bag: this.bag.slice(),
      score: this.levelScore,
      combo: this.combo,
      replaces: this.replaces,
      placedCount: this.placedCount,
    });
    if (this.history.length > 8) this.history.shift();

    if (replacing) {
      this.replaces++;
      this.levelScore = Math.max(0, this.levelScore - 50);
      this.combo = 0;
      c.replaceFlash = 0.35;
      this.emit('replace', { x, y });
    } else {
      this.combo++;
      if (this.combo > 1 && this.combo % 4 === 0) {
        this.levelScore += 40 * Math.floor(this.combo / 4);
        this._pop(`COMBO x${this.combo}`, '#ffc14a');
        this.emit('combo', { n: this.combo });
      }
      this.emit('place', { x, y });
    }

    c.kind = 'pipe';
    c.pipe = { ...pieceById(pid) };
    c.placeT = this.time;
    c.fill = { ns: 0, ew: 0, main: 0 };
    c.entered = { ns: null, ew: null, main: null };

    this.queue.shift();
    this.queue.push(this._drawPiece());
    this.held = this.queue[0];
    this.placedCount++;

    const link = this._isLinkedToNetwork(x, y);
    if (link && !replacing) {
      this.levelScore += 8;
    }

    this.shake = Math.max(this.shake, replacing ? 4 : 1.5);
    return true;
  }

  _isLinkedToNetwork(x, y) {
    const c = this.cell(x, y);
    if (!c?.pipe) return false;
    for (const d of ['N', 'E', 'S', 'W']) {
      const bit = { N: 1, E: 2, S: 4, W: 8 }[d];
      if (!(c.pipe.mask & bit)) continue;
      const n = this.cell(x + DIRS[d].dx, y + DIRS[d].dy);
      if (!n) continue;
      if (n.kind === 'start' || n.kind === 'end' || n.pipe) {
        const o = opp(d);
        if (n.pipe && n.pipe.mask & DIRS[o].bit) return true;
      }
    }
    return false;
  }

  undo() {
    if (!this.settings.undo) return false;
    if (this.phase !== 'countdown' && this.phase !== 'flowing') return false;
    const h = this.history.pop();
    if (!h) return false;
    const c = this.cell(h.x, h.y);
    if (!c || this._anyFill(c)) {
      this.history.push(h);
      return false;
    }
    if (h.prevPipe) {
      c.kind = 'pipe';
      c.pipe = { ...pieceById(h.prevPipe) };
    } else {
      c.kind = 'empty';
      c.pipe = null;
    }
    c.fill = { ns: 0, ew: 0, main: 0 };
    this.queue = h.queue.slice();
    this.bag = h.bag.slice();
    this.held = this.queue[0];
    this.levelScore = h.score;
    this.combo = h.combo;
    this.replaces = h.replaces;
    this.placedCount = h.placedCount;
    this.emit('undo');
    return true;
  }

  beginFlow() {
    if (this.phase !== 'countdown') return;
    const leftover = Math.max(0, this.countdown);
    this.levelScore += Math.round(leftover * 25);
    this.countdown = 0;
    this.phase = 'flowing';
    const s = this.level.start;
    const start = this.cell(s.x, s.y);
    start.fill.main = 0.001;
    start.entered.main = opp(s.dir);
    this.heads = [
      {
        x: s.x,
        y: s.y,
        enteredFrom: opp(s.dir),
        progress: 0.001,
        channel: 'main',
      },
    ];
    this.emit('valve');
    this._pop('FLOOZ RELEASED', '#3ee0ff');
  }

  toggleFast() {
    this.fast = !this.fast;
  }

  pause() {
    if (this.phase === 'countdown' || this.phase === 'flowing') {
      this._prePause = this.phase;
      this.phase = 'paused';
      this.emit('pause');
    }
  }

  resume() {
    if (this.phase === 'paused') {
      this.phase = this._prePause || 'countdown';
      this.emit('resume');
    }
  }

  update(dt) {
    this.time += dt;
    this.shake = Math.max(0, this.shake - dt * 18);
    this.flash = Math.max(0, this.flash - dt * 2.4);
    if (this.popup) {
      this.popup.t -= dt;
      if (this.popup.t <= 0) this.popup = null;
    }
    for (const row of this.board) {
      for (const c of row) {
        if (c.replaceFlash > 0) c.replaceFlash = Math.max(0, c.replaceFlash - dt);
      }
    }

    if (this.phase === 'paused' || this.phase === 'idle') return;
    if (this.phase === 'won' || this.phase === 'lost') return;

    if (this.phase === 'countdown') {
      this.countdown -= dt;
      if (!this.warned && this.countdown < 3.2) {
        this.warned = true;
        this.emit('warn');
      }
      if (this.countdown <= 0) this.beginFlow();
      return;
    }

    if (this.phase === 'flowing' || this.phase === 'finishing') {
      const mul = (this.fast || this.phase === 'finishing' ? 2.4 : 1) *
        (this.mode === 'endless' ? 1 + this.endlessRamp * 0.08 : 1);
      this._advance(dt * mul);
    }

    if (this.phase === 'leaking') {
      this.leak.t += dt;
      if (this.leak.t > 1.15) this._resolve();
    }
  }

  _advance(dt) {
    if (!this.heads.length) return;
    const nextHeads = [];
    for (const head of this.heads) {
      const cell = this.cell(head.x, head.y);
      if (!cell?.pipe) {
        this._startLeak(head, head.enteredFrom);
        return;
      }
      const delay = cell.pipe.delay || 1;
      head.progress += (dt * this.flowSpeed) / delay;
      const ch = cell.pipe.independent ? channelKey(head.enteredFrom) : 'main';
      cell.fill[ch] = Math.min(1, head.progress);
      cell.entered[ch] = head.enteredFrom;

      if (head.progress < 1) {
        nextHeads.push(head);
        continue;
      }

      cell.fill[ch] = 1;
      cell.locked[ch] = true;
      this.distance++;
      this.stats.flowed++;
      if (cell.pipe.id.startsWith('tank')) this.stats.tanks++;
      const gained = 100 + Math.min(80, this.combo * 4);
      this.levelScore += gained;
      this.emit('cell', { x: head.x, y: head.y, gained });

      if (cell.pipe.independent) {
        const other = ch === 'ns' ? 'ew' : 'ns';
        if (cell.fill[other] >= 1) {
          this.usedCrossLoop++;
          this.levelScore += 400;
          this.stats.crosses++;
          this._pop('DOUBLE CROSS +400', '#ffc14a');
        }
      }

      if (cell.kind === 'end') {
        this.reachedEnd = true;
        if (this.distance >= this.minDist || this.mode === 'endless') {
          this._win();
          return;
        }
        this._startLeak(head, null, 'short');
        return;
      }

      const ex = exitDir(cell.pipe, head.enteredFrom);
      if (!ex) {
        this._startLeak(head, head.enteredFrom);
        return;
      }
      const nx = head.x + DIRS[ex].dx;
      const ny = head.y + DIRS[ex].dy;
      const pos = this.wrapPos(nx, ny);
      if (!this.wrap && (nx < 0 || ny < 0 || nx >= this.cols || ny >= this.rows)) {
        this._startLeak({ x: head.x, y: head.y, dir: ex }, ex);
        return;
      }
      const next = this.cell(pos.x, pos.y);
      const enter = opp(ex);
      if (!next || next.kind === 'wall' || !next.pipe) {
        this._startLeak({ x: pos.x, y: pos.y, dir: ex }, ex);
        return;
      }
      if (next.kind === 'end') {
        const want = next.pipe.endDir || enter;
        if (want !== enter) {
          this._startLeak({ x: pos.x, y: pos.y, dir: ex }, ex);
          return;
        }
        nextHeads.push({
          x: pos.x,
          y: pos.y,
          enteredFrom: enter,
          progress: 0,
          channel: 'main',
        });
        continue;
      }
      if (!accepts(next.pipe, enter)) {
        this._startLeak({ x: pos.x, y: pos.y, dir: ex }, ex);
        return;
      }
      const nch = next.pipe.independent ? channelKey(enter) : 'main';
      if (next.locked[nch] || next.fill[nch] > 0.05) {
        this._startLeak({ x: pos.x, y: pos.y, dir: ex }, ex);
        return;
      }
      nextHeads.push({
        x: pos.x,
        y: pos.y,
        enteredFrom: enter,
        progress: 0,
        channel: nch,
      });
    }
    this.heads = nextHeads;

    if (this.mode === 'endless') {
      this.endlessDist = this.distance;
      this.endlessRamp = Math.floor(this.distance / 16);
    }
  }

  _startLeak(head, dir, why = 'dead') {
    if (this.phase === 'leaking' || this.phase === 'won' || this.phase === 'lost') return;
    const enough = this.distance >= this.minDist;
    const needEnd = !!this.level.end;
    const success =
      this.mode !== 'endless' && enough && (!needEnd || this.reachedEnd) && why !== 'short';
    this.phase = 'leaking';
    this.leak = {
      x: head.x,
      y: head.y,
      dir,
      t: 0,
      success,
      why,
    };
    this.shake = success ? 6 : 14;
    this.flash = success ? 0.25 : 0.55;
    this.emit(success ? 'leak-win' : 'leak-fail', this.leak);
  }

  _resolve() {
    if (!this.leak) return;
    if (this.leak.success) this._win();
    else this._lose();
  }

  _tally() {
    let unused = 0;
    for (const row of this.board) {
      for (const c of row) {
        if (c.kind === 'pipe' && !this._anyFill(c)) unused++;
      }
    }
    this.unusedPenalty = unused * 25;
    const leftoverQueue = 0;
    const distBonus = Math.max(0, this.distance - this.minDist) * 60;
    const noReplace = this.replaces === 0 ? 250 : 0;
    const clean = Math.max(0, distBonus + noReplace - this.unusedPenalty - leftoverQueue);
    this.levelScore = Math.max(0, this.levelScore + clean);
    return { unused, distBonus, noReplace };
  }

  _win() {
    if (this.phase === 'won') return;
    this._tally();
    this.phase = 'won';
    this.campaignScore += this.levelScore;
    const par = parScore(this.level);
    this.result = {
      ok: true,
      score: this.levelScore,
      campaign: this.campaignScore,
      distance: this.distance,
      medal: medalFor(this.levelScore, par),
      par,
      last: this.mode === 'campaign' && this.levelIndex >= LEVELS.length - 1,
    };
    this._pop('SECTOR CLEAR', '#5cffd4');
    this.emit('win', this.result);
  }

  _lose() {
    if (this.phase === 'lost') return;
    this.phase = 'lost';
    this.lives = Math.max(0, this.lives - 1);
    this.result = {
      ok: false,
      score: this.levelScore,
      campaign: this.campaignScore,
      distance: this.distance,
      reason: this.leak?.why === 'short' ? 'Drain reached too early.' : 'The flooz found a leak.',
      lives: this.lives,
    };
    this._pop('PRESSURE LOSS', '#ff4455');
    this.emit('lose', this.result);
  }

  _pop(text, color) {
    this.popup = { text, color, t: 1.15 };
  }

  tracePreview() {
    if (!this.level) return [];
    const path = [];
    const s = this.level.start;
    let x = s.x;
    let y = s.y;
    let entered = opp(s.dir);
    const seen = new Set();
    for (let i = 0; i < 80; i++) {
      const c = this.cell(x, y);
      if (!c?.pipe) break;
      const key = `${x},${y},${entered}`;
      if (seen.has(key)) break;
      seen.add(key);
      path.push({ x, y, enteredFrom: entered });
      if (c.kind === 'end') break;
      const ex = exitDir(c.pipe, entered);
      if (!ex) break;
      const nx = x + DIRS[ex].dx;
      const ny = y + DIRS[ex].dy;
      const pos = this.wrapPos(nx, ny);
      if (!this.wrap && (nx < 0 || ny < 0 || nx >= this.cols || ny >= this.rows)) break;
      const next = this.cell(pos.x, pos.y);
      if (!next?.pipe) break;
      const enter = opp(ex);
      if (next.kind !== 'end' && !accepts(next.pipe, enter)) break;
      if (next.kind === 'end' && next.pipe.endDir && next.pipe.endDir !== enter) break;
      x = pos.x;
      y = pos.y;
      entered = enter;
    }
    return path;
  }

  hoverValid(x, y) {
    return this.canPlace(x, y);
  }

  snapshotScore() {
    return this.mode === 'campaign' ? this.campaignScore : this.levelScore;
  }
}

export { LEVELS, PIECES };
