/* core/rules.js — complete chess rules engine (0x88 board), ES module.
   Legal move generation incl. castling, en passant, promotion,
   check/checkmate/stalemate, threefold repetition, 50-move rule,
   insufficient material, SAN, FEN import/export, PGN export.
   Move generation validated against published perft counts. */
'use strict';

export const P = 1, N = 2, B = 3, R = 4, Q = 5, K = 6;
export const WHITE = 1, BLACK = -1;
export const START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';

const KNIGHT_D = [-33, -31, -18, -14, 14, 18, 31, 33];
const BISHOP_D = [-17, -15, 15, 17];
const ROOK_D   = [-16, -1, 1, 16];
const KING_D   = [-17, -16, -15, -1, 1, 15, 16, 17];

const CR_WK = 1, CR_WQ = 2, CR_BK = 4, CR_BQ = 8;

export function sq(file, rank) { return rank * 16 + file; }        // 0..7, 0..7 -> 0x88
export function fileOf(s) { return s & 7; }
export function rankOf(s) { return s >> 4; }
export function onBoard(s) { return (s & 0x88) === 0; }
export function algebraic(s) { return 'abcdefgh'[fileOf(s)] + (rankOf(s) + 1); }
export function parseSquare(str) { return sq(str.charCodeAt(0) - 97, str.charCodeAt(1) - 49); }

export const PIECE_LETTER = { [P]: '', [N]: 'N', [B]: 'B', [R]: 'R', [Q]: 'Q', [K]: 'K' };
export const FEN_PIECE = { p: P, n: N, b: B, r: R, q: Q, k: K };
export const PIECE_FEN = { [P]: 'p', [N]: 'n', [B]: 'b', [R]: 'r', [Q]: 'q', [K]: 'k' };

export class Chess {
  constructor(fen) {
    this.load(fen || 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1');
  }

  load(fen) {
    this.board = new Int8Array(128);
    const parts = fen.trim().split(/\s+/);
    let s = sq(0, 7);
    for (const ch of parts[0]) {
      if (ch === '/') { s = sq(0, rankOf(s - 16)); }
      else if (ch >= '1' && ch <= '8') { s += parseInt(ch, 10); }
      else {
        const lower = ch.toLowerCase();
        this.board[s] = FEN_PIECE[lower] * (ch === lower ? BLACK : WHITE);
        s++;
      }
    }
    this.turn = parts[1] === 'b' ? BLACK : WHITE;
    this.castling = 0;
    if (parts[2] && parts[2] !== '-') {
      if (parts[2].includes('K')) this.castling |= CR_WK;
      if (parts[2].includes('Q')) this.castling |= CR_WQ;
      if (parts[2].includes('k')) this.castling |= CR_BK;
      if (parts[2].includes('q')) this.castling |= CR_BQ;
    }
    this.ep = (parts[3] && parts[3] !== '-') ? parseSquare(parts[3]) : -1;
    this.halfmove = parts[4] ? parseInt(parts[4], 10) : 0;
    this.fullmove = parts[5] ? parseInt(parts[5], 10) : 1;
    this.history = [];        // undo stack (internal state)
    this.moveLog = [];        // {move, san, fenBefore} for UI / PGN
    this.startFen = this.fen();
    this.positions = new Map([[this.positionKey(), 1]]);
  }

  fen() {
    let out = '';
    for (let r = 7; r >= 0; r--) {
      let empty = 0;
      for (let f = 0; f < 8; f++) {
        const pc = this.board[sq(f, r)];
        if (!pc) { empty++; continue; }
        if (empty) { out += empty; empty = 0; }
        const letter = PIECE_FEN[Math.abs(pc)];
        out += pc > 0 ? letter.toUpperCase() : letter;
      }
      if (empty) out += empty;
      if (r) out += '/';
    }
    out += this.turn === WHITE ? ' w ' : ' b ';
    let cr = '';
    if (this.castling & CR_WK) cr += 'K';
    if (this.castling & CR_WQ) cr += 'Q';
    if (this.castling & CR_BK) cr += 'k';
    if (this.castling & CR_BQ) cr += 'q';
    out += (cr || '-') + ' ';
    out += (this.ep >= 0 ? algebraic(this.ep) : '-');
    out += ' ' + this.halfmove + ' ' + this.fullmove;
    return out;
  }

  positionKey() {
    // board + turn + castling + usable ep square
    let key = '';
    for (let r = 0; r < 8; r++)
      for (let f = 0; f < 8; f++) key += this.board[sq(f, r)] + ',';
    return key + this.turn + '|' + this.castling + '|' + (this.epUsable() ? this.ep : -1);
  }

  epUsable() {
    if (this.ep < 0) return false;
    const us = this.turn;
    for (const d of [-1, 1]) {
      const from = this.ep - us * 16 + d;
      if (onBoard(from) && this.board[from] === P * us) return true;
    }
    return false;
  }

  kingSquare(side) {
    for (let s = 0; s < 128; s++)
      if (onBoard(s) && this.board[s] === K * side) return s;
    return -1;
  }

  attacked(target, bySide) {
    const b = this.board;
    // pawns
    for (const d of [15, 17]) {
      const from = target - bySide * d;
      if (onBoard(from) && b[from] === P * bySide) return true;
    }
    for (const d of KNIGHT_D) {
      const from = target + d;
      if (onBoard(from) && b[from] === N * bySide) return true;
    }
    for (const d of KING_D) {
      const from = target + d;
      if (onBoard(from) && b[from] === K * bySide) return true;
    }
    for (const d of BISHOP_D) {
      let from = target + d;
      while (onBoard(from)) {
        const pc = b[from];
        if (pc) {
          if ((pc === B * bySide || pc === Q * bySide)) return true;
          break;
        }
        from += d;
      }
    }
    for (const d of ROOK_D) {
      let from = target + d;
      while (onBoard(from)) {
        const pc = b[from];
        if (pc) {
          if ((pc === R * bySide || pc === Q * bySide)) return true;
          break;
        }
        from += d;
      }
    }
    return false;
  }

  inCheck(side) {
    side = side || this.turn;
    return this.attacked(this.kingSquare(side), -side);
  }

  pseudoMoves() {
    const moves = [];
    const b = this.board, us = this.turn;
    const push = (from, to, flags, promo) => {
      moves.push({ from, to, piece: Math.abs(b[from]), captured: Math.abs(b[to]) || (flags === 'ep' ? P : 0), flags: flags || '', promo: promo || 0 });
    };
    for (let s = 0; s < 128; s++) {
      if (!onBoard(s)) continue;
      const pc = b[s];
      if (!pc || Math.sign(pc) !== us) continue;
      const type = Math.abs(pc);
      if (type === P) {
        const fwd = s + 16 * us;
        const promoRank = us === WHITE ? 7 : 0;
        if (onBoard(fwd) && !b[fwd]) {
          if (rankOf(fwd) === promoRank) for (const pr of [Q, R, B, N]) push(s, fwd, 'promo', pr);
          else {
            push(s, fwd);
            const startRank = us === WHITE ? 1 : 6;
            const fwd2 = s + 32 * us;
            if (rankOf(s) === startRank && !b[fwd2]) push(s, fwd2, 'double');
          }
        }
        for (const d of [15, 17]) {
          const to = s + d * us;
          if (!onBoard(to)) continue;
          if (b[to] && Math.sign(b[to]) === -us) {
            if (rankOf(to) === promoRank) for (const pr of [Q, R, B, N]) push(s, to, 'promo', pr);
            else push(s, to);
          } else if (to === this.ep && this.ep >= 0) {
            push(s, to, 'ep');
          }
        }
      } else if (type === N || type === K) {
        for (const d of (type === N ? KNIGHT_D : KING_D)) {
          const to = s + d;
          if (onBoard(to) && (!b[to] || Math.sign(b[to]) === -us)) push(s, to);
        }
        if (type === K) {
          const home = us === WHITE ? sq(4, 0) : sq(4, 7);
          if (s === home && !this.inCheck(us)) {
            const [kFlag, qFlag] = us === WHITE ? [CR_WK, CR_WQ] : [CR_BK, CR_BQ];
            if ((this.castling & kFlag) && !b[s + 1] && !b[s + 2] &&
                !this.attacked(s + 1, -us) && !this.attacked(s + 2, -us))
              push(s, s + 2, 'castle-k');
            if ((this.castling & qFlag) && !b[s - 1] && !b[s - 2] && !b[s - 3] &&
                !this.attacked(s - 1, -us) && !this.attacked(s - 2, -us))
              push(s, s - 2, 'castle-q');
          }
        }
      } else {
        const dirs = type === B ? BISHOP_D : type === R ? ROOK_D : KING_D; // Q uses KING_D rays
        for (const d of dirs) {
          let to = s + d;
          while (onBoard(to)) {
            if (!b[to]) { push(s, to); }
            else { if (Math.sign(b[to]) === -us) push(s, to); break; }
            to += d;
          }
        }
      }
    }
    return moves;
  }

  makeMove(m) {
    const b = this.board, us = this.turn;
    this.history.push({
      m, castling: this.castling, ep: this.ep,
      halfmove: this.halfmove, fullmove: this.fullmove,
      capturedPiece: b[m.to],
    });
    b[m.to] = m.promo ? m.promo * us : b[m.from];
    b[m.from] = 0;
    if (m.flags === 'ep') b[m.to - 16 * us] = 0;
    if (m.flags === 'castle-k') { b[m.to - 1] = b[m.to + 1]; b[m.to + 1] = 0; }
    if (m.flags === 'castle-q') { b[m.to + 1] = b[m.to - 2]; b[m.to - 2] = 0; }

    // castling rights
    if (m.piece === K) this.castling &= us === WHITE ? ~(CR_WK | CR_WQ) : ~(CR_BK | CR_BQ);
    const updateRookRights = (s) => {
      if (s === sq(0, 0)) this.castling &= ~CR_WQ;
      else if (s === sq(7, 0)) this.castling &= ~CR_WK;
      else if (s === sq(0, 7)) this.castling &= ~CR_BQ;
      else if (s === sq(7, 7)) this.castling &= ~CR_BK;
    };
    updateRookRights(m.from);
    updateRookRights(m.to);

    this.ep = m.flags === 'double' ? m.from + 16 * us : -1;
    this.halfmove = (m.piece === P || m.captured) ? 0 : this.halfmove + 1;
    if (us === BLACK) this.fullmove++;
    this.turn = -us;
  }

  unmakeMove() {
    const h = this.history.pop();
    if (!h) return;
    const m = h.m, us = -this.turn;
    this.turn = us;
    this.castling = h.castling; this.ep = h.ep;
    this.halfmove = h.halfmove; this.fullmove = h.fullmove;
    const b = this.board;
    b[m.from] = m.promo ? P * us : b[m.to];
    b[m.to] = h.capturedPiece;
    if (m.flags === 'ep') { b[m.to] = 0; b[m.to - 16 * us] = P * -us; }
    if (m.flags === 'castle-k') { b[m.to + 1] = b[m.to - 1]; b[m.to - 1] = 0; }
    if (m.flags === 'castle-q') { b[m.to - 2] = b[m.to + 1]; b[m.to + 1] = 0; }
  }

  legalMoves() {
    const out = [];
    for (const m of this.pseudoMoves()) {
      this.makeMove(m);
      if (!this.inCheck(-this.turn)) out.push(m);
      this.unmakeMove();
    }
    return out;
  }

  legalMovesFrom(from) {
    return this.legalMoves().filter(m => m.from === from);
  }

  san(m, legal) {
    legal = legal || this.legalMoves();
    let out;
    if (m.flags === 'castle-k') out = 'O-O';
    else if (m.flags === 'castle-q') out = 'O-O-O';
    else {
      out = PIECE_LETTER[m.piece];
      if (m.piece !== P) {
        // disambiguation
        const others = legal.filter(x => x.piece === m.piece && x.to === m.to && x.from !== m.from);
        if (others.length) {
          const sameFile = others.some(x => fileOf(x.from) === fileOf(m.from));
          const sameRank = others.some(x => rankOf(x.from) === rankOf(m.from));
          if (!sameFile) out += 'abcdefgh'[fileOf(m.from)];
          else if (!sameRank) out += (rankOf(m.from) + 1);
          else out += algebraic(m.from);
        }
      }
      if (m.captured) {
        if (m.piece === P) out += 'abcdefgh'[fileOf(m.from)];
        out += 'x';
      }
      out += algebraic(m.to);
      if (m.promo) out += '=' + PIECE_LETTER[m.promo];
    }
    this.makeMove(m);
    if (this.inCheck(this.turn)) out += this.legalMoves().length === 0 ? '#' : '+';
    this.unmakeMove();
    return out;
  }

  // Play a move permanently (records SAN + repetition).
  play(m) {
    const san = this.san(m);
    const fenBefore = this.fen();
    this.makeMove(m);
    const key = this.positionKey();
    this.positions.set(key, (this.positions.get(key) || 0) + 1);
    this.moveLog.push({ move: m, san, fenBefore });
    return san;
  }

  undoPlay() {
    if (!this.moveLog.length) return null;
    const key = this.positionKey();
    const cnt = this.positions.get(key);
    if (cnt > 1) this.positions.set(key, cnt - 1); else this.positions.delete(key);
    this.unmakeMove();
    return this.moveLog.pop();
  }

  uci(m) {
    return algebraic(m.from) + algebraic(m.to) + (m.promo ? PIECE_FEN[m.promo] : '');
  }

  findUci(uciStr) {
    return this.legalMoves().find(m => this.uci(m) === uciStr) || null;
  }

  insufficientMaterial() {
    const minors = [];
    for (let s = 0; s < 128; s++) {
      if (!onBoard(s) || !this.board[s]) continue;
      const t = Math.abs(this.board[s]);
      if (t === P || t === R || t === Q) return false;
      if (t === B || t === N) minors.push({ t, color: (fileOf(s) + rankOf(s)) & 1, side: Math.sign(this.board[s]) });
    }
    if (minors.length <= 1) return true; // K vs K, K+minor vs K
    if (minors.length === 2 && minors[0].t === B && minors[1].t === B &&
        minors[0].side !== minors[1].side && minors[0].color === minors[1].color) return true;
    return false;
  }

  result() {
    // returns null if game continues, else {result:'1-0'|'0-1'|'1/2-1/2', reason}
    if (this.legalMoves().length === 0) {
      if (this.inCheck(this.turn))
        return { result: this.turn === WHITE ? '0-1' : '1-0', reason: 'checkmate' };
      return { result: '1/2-1/2', reason: 'stalemate' };
    }
    if (this.halfmove >= 100) return { result: '1/2-1/2', reason: 'fifty-move rule' };
    if ((this.positions.get(this.positionKey()) || 0) >= 3)
      return { result: '1/2-1/2', reason: 'threefold repetition' };
    if (this.insufficientMaterial()) return { result: '1/2-1/2', reason: 'insufficient material' };
    return null;
  }

  pgn(headers) {
    const h = Object.assign({
      Event: 'RushChess Casual Game', Site: 'RushChess GUI', Date: new Date().toISOString().slice(0, 10).replace(/-/g, '.'),
      Round: '-', White: 'White', Black: 'Black', Result: '*',
    }, headers || {});
    let out = '';
    for (const [k, v] of Object.entries(h)) out += `[${k} "${v}"]\n`;
    if (this.startFen !== 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1')
      out += `[SetUp "1"]\n[FEN "${this.startFen}"]\n`;
    out += '\n';
    const startTurn = this.startFen.split(' ')[1] === 'b' ? BLACK : WHITE;
    let moveNo = parseInt(this.startFen.split(' ')[5] || '1', 10);
    let line = [];
    this.moveLog.forEach((entry, i) => {
      const side = (i % 2 === 0) ? startTurn : -startTurn;
      if (side === WHITE) line.push(moveNo + '. ' + entry.san);
      else { if (i === 0) line.push(moveNo + '... ' + entry.san); else line[line.length - 1] += ' ' + entry.san; moveNo++; }
    });
    out += line.join(' ') + (line.length ? ' ' : '') + (h.Result || '*') + '\n';
    return out;
  }

  // Board as array of {square:'e4', piece:'P'|'p'|...} for rendering
  pieces() {
    const out = [];
    for (let r = 0; r < 8; r++) for (let f = 0; f < 8; f++) {
      const pc = this.board[sq(f, r)];
      if (!pc) continue;
      const letter = PIECE_FEN[Math.abs(pc)];
      out.push({ square: algebraic(sq(f, r)), file: f, rank: r, piece: pc > 0 ? letter.toUpperCase() : letter });
    }
    return out;
  }

  movesPlayed() { return this.moveLog.map(e => ({ san: e.san, uci: this.uci(e.move) })); }
}

/** perft — node counter used by the self-test. */
export function perft(game, depth) {
  if (depth === 0) return 1;
  let n = 0;
  for (const m of game.pseudoMoves()) {
    game.makeMove(m);
    if (!game.inCheck(-game.turn)) n += perft(game, depth - 1);
    game.unmakeMove();
  }
  return n;
}
