/* panels/session.js — BoardSession: board + game document wiring shared by
   all board-centric modes. Player bars (name/rating/clock/captured), eval
   bar, premove storage, keyboard SAN entry, sounds, board sizing. */
'use strict';

import { h, clear, append, fmtClockMs, Emitter } from '../ui/dom.js';
import { Board } from '../board/board.js';
import { settings } from '../svc/settings.js';
import { sound } from '../svc/sound.js';
import { evalBarPercent, fmtEval } from '../svc/engines.js';
import { FEN_PIECE } from '../core/rules.js';

const GLYPH = { p: '♟', n: '♞', b: '♝', r: '♜', q: '♛' };
const PVALS = { p: 1, n: 3, b: 3, r: 5, q: 9 };

export class BoardSession extends Emitter {
  /**
   * opts:
   *  doc: GameDoc
   *  interactive: bool
   *  playerBars: bool
   *  evalBar: bool (subject to settings)
   *  userTurn(): 'w'|'b'|'both'|null — who may move now (null = view only)
   *  onUserMove(uci): user played a move (already legal)
   *  allowPremove(): bool
   */
  constructor(opts) {
    super();
    this.opts = opts;
    this.doc = opts.doc;
    this.premove = null;

    this.el = h('div.bs');
    this.topBar = h('div.playerbar');
    this.bottomBar = h('div.playerbar');
    this.boardBox = h('div.bs-board');
    this.evalEl = h('div.evalbar', { title: 'Evaluation' },
      this.evalWhite = h('div.evalbar-white', { style: { height: '50%' } }),
      h('div.evalbar-mark'),
      this.evalLabel = h('div.evalbar-label', { style: { bottom: '2px', color: '#333' } }, ''),
    );
    this.boardRow = h('div.bs-row', {}, this.evalEl, this.boardBox);
    if (opts.playerBars !== false) this.el.append(this.topBar);
    this.el.append(this.boardRow);
    if (opts.playerBars !== false) this.el.append(this.bottomBar);

    this.board = new Board(this.boardBox, {
      interactive: opts.interactive !== false,
      orientation: opts.orientation || 'w',
      dests: () => this._dests(),
      onMove: uci => this._onMove(uci),
      canPremove: () => !!(opts.allowPremove && opts.allowPremove() && settings.get('input.premove')),
      premoveColor: () => this._premoveColor(),
      onPremove: pm => { this.premove = pm; },
      trySan: buf => this._trySan(buf),
      onShapes: shapes => this.emit('shapes', shapes),
    });

    this.players = { w: { name: 'White' }, b: { name: 'Black' } };
    this.clocks = null;
    this._unsubs = [
      settings.on('change:board.evalBar', () => this._applyEvalBarVis()),
      settings.on('change:board.zoom', () => this._applyZoom()),
    ];
    if (opts.evalBar === false) this.evalEl.style.display = 'none';
    else this._applyEvalBarVis();
    this._applyZoom();
    this._bindDoc();
    this.update(false);
  }

  _applyEvalBarVis() {
    if (this.opts.evalBar === false) return;
    this.evalEl.style.display = settings.get('board.evalBar') ? '' : 'none';
  }
  _applyZoom() {
    this.boardBox.style.setProperty('--zoom', settings.get('board.zoom') / 100);
  }

  bindDoc(doc) {
    this._docUnsubs?.forEach(u => u());
    this.doc = doc;
    this._bindDoc();
    this.update(false);
  }
  _bindDoc() {
    this._docUnsubs = [
      this.doc.on('navigate', () => this.update(true)),
      this.doc.on('change', () => this.updateBars()),
    ];
  }

  userTurn() { return this.opts.userTurn ? this.opts.userTurn() : 'both'; }

  _premoveColor() {
    const t = this.userTurn();
    if (t !== 'w' && t !== 'b') return null;
    // premove pieces belong to user's color (it's NOT their turn now)
    const stm = this.doc.position.turn === 1 ? 'w' : 'b';
    return stm === t ? null : t;
  }

  _dests() {
    const map = new Map();
    const turn = this.userTurn();
    if (!turn) return map;
    const pos = this.doc.position;
    const stm = pos.turn === 1 ? 'w' : 'b';
    if (turn !== 'both' && turn !== stm) return map;
    for (const m of pos.legalMoves()) {
      if (!map.has(m.from)) map.set(m.from, []);
      map.get(m.from).push({ to: m.to, promo: m.promo, capture: !!m.captured });
    }
    return map;
  }

  _onMove(uci) {
    this.opts.onUserMove ? this.opts.onUserMove(uci) : this.applyUserMove(uci);
  }

  /** default behavior: play into the doc */
  applyUserMove(uci) {
    const pos = this.doc.position;
    const m = pos.findUci(uci);
    if (!m) return false;
    this.playSoundFor(m, pos);
    this.doc.playMove(m);
    return true;
  }

  playSoundFor(m, posBefore) {
    const pos = posBefore || this.doc.position;
    pos.makeMove(m);
    const check = pos.inCheck(pos.turn);
    pos.unmakeMove();
    if (check) sound.check();
    else if (m.flags && m.flags.startsWith('castle')) sound.castle();
    else if (m.captured) sound.capture();
    else sound.move();
  }

  _trySan(buf) {
    if (!buf) return 'pending';
    const turn = this.userTurn();
    const pos = this.doc.position;
    const stm = pos.turn === 1 ? 'w' : 'b';
    if (turn !== 'both' && turn !== stm) return 'invalid';
    const legal = pos.legalMoves();
    const norm = s => s.replace(/[+#]/g, '').toLowerCase();
    const q = norm(buf);
    const matches = legal.filter(m => {
      const san = norm(pos.san(m, legal));
      return san === q || san.startsWith(q);
    });
    const exact = legal.find(m => norm(pos.san(m, legal)) === q);
    if (exact) { this._onMove(pos.uci(exact)); return 'played'; }
    if (!matches.length) return 'invalid';
    return 'pending';
  }

  /** Consume stored premove if legal. Returns uci or null. */
  takePremove() {
    const pm = this.premove;
    this.premove = null;
    this.board.setPremoveState(null);
    if (!pm) return null;
    const pos = this.doc.position;
    const m = pos.legalMoves().find(x =>
      x.from === pm.from && x.to === pm.to && (!x.promo || 'pnbrq'[x.promo - 1] === (pm.promo || 'q')));
    return m ? pos.uci(m) : null;
  }
  clearPremove() { this.premove = null; this.board.setPremoveState(null); }

  update(animate = true) {
    const pos = this.doc.position;
    const cur = this.doc.current;
    const lastMove = cur !== this.doc.root && cur.uci
      ? { from: parseSq(cur.uci.slice(0, 2)), to: parseSq(cur.uci.slice(2, 4)) }
      : null;
    const check = pos.inCheck(pos.turn) ? pos.kingSquare(pos.turn) : -1;
    this.board.setPosition(pos, { lastMove, check, animate });
    if (this.premove) this.board.setPremoveState(this.premove);
    this.updateBars();
    this.emit('update');
  }

  /* ================= player bars ================= */
  setPlayers(players) { Object.assign(this.players, players); this.updateBars(); }
  setClocks(clocks) { this.clocks = clocks; this.updateBars(); }

  updateBars() {
    if (this.opts.playerBars === false) return;
    const orient = this.board.orientation;
    const bottomColor = orient;
    const topColor = orient === 'w' ? 'b' : 'w';
    this._paintBar(this.topBar, topColor);
    this._paintBar(this.bottomBar, bottomColor);
  }

  _paintBar(bar, color) {
    clear(bar);
    const pos = this.doc.position;
    const stm = pos.turn === 1 ? 'w' : 'b';
    const p = this.players[color] || {};
    const { caps, diff } = this._captured(color);
    // use the dom append helper (skips null/false); native bar.append coerces null → "null"
    append(bar, [
      h('span.pb-turn', { class: stm === color && !p.hideTurn ? 'on' : '' }),
      h('span.pb-name', {}, p.name || (color === 'w' ? 'White' : 'Black')),
      p.rating ? h('span.pb-rating', {}, String(p.rating)) : null,
      h('span.pb-captured', {}, caps, diff > 0 ? h('span.pb-diff', {}, ' +' + diff) : null),
    ]);
    if (this.clocks && this.clocks[color] != null) {
      const ms = this.clocks[color];
      bar.append(h('span.pb-clock', {
        class: (this.clocks.active === color ? 'running ' : '') + (ms < 30000 && ms > 0 ? 'low' : ''),
      }, fmtClockMs(ms, true)));
    }
  }

  _captured(byColor) {
    // material captured BY byColor = start counts - current counts of opponent pieces
    const startCounts = countPieces(this.doc.startFen);
    const pos = this.doc.position;
    const now = { w: {}, b: {} };
    for (const pc of pos.pieces()) {
      const side = pc.piece === pc.piece.toUpperCase() ? 'w' : 'b';
      const t = pc.piece.toLowerCase();
      now[side][t] = (now[side][t] || 0) + 1;
    }
    const opp = byColor === 'w' ? 'b' : 'w';
    const caps = [];
    let matDiff = 0;
    for (const t of ['q', 'r', 'b', 'n', 'p']) {
      const taken = (startCounts[opp][t] || 0) - (now[opp][t] || 0);
      for (let i = 0; i < taken; i++) caps.push(GLYPH[t]);
    }
    // material diff from byColor perspective
    for (const t of ['q', 'r', 'b', 'n', 'p']) {
      matDiff += ((now[byColor === 'w' ? 'w' : 'b'][t] || 0) - (now[opp][t] || 0)) * PVALS[t];
    }
    return { caps: caps.join(''), diff: matDiff };
  }

  /* ================= eval bar ================= */
  setEval(score) {
    const pct = evalBarPercent(score);
    this.evalWhite.style.height = pct + '%';
    let label = '';
    if (score) {
      if (score.mate != null) label = 'M' + Math.abs(score.mate);
      else label = Math.abs(score.cp / 100).toFixed(1);   // side shown by bar position
    }
    this.evalLabel.textContent = label;
    this.evalEl.title = score ? 'Evaluation: ' + fmtEval(score, 'pawns') : 'Evaluation';
    // label sits inside the advantaged side's half, in contrasting color
    const whiteAhead = pct >= 50;
    this.evalLabel.style.bottom = whiteAhead ? '3px' : '';
    this.evalLabel.style.top = whiteAhead ? '' : '3px';
    this.evalLabel.style.color = whiteAhead ? '#2b2f38' : '#e8eaee';
  }

  destroy() {
    this._unsubs.forEach(u => u());
    this._docUnsubs?.forEach(u => u());
    this.board.destroy();
    this.el.remove();
  }
}

function parseSq(s) { return (s.charCodeAt(1) - 49) * 16 + (s.charCodeAt(0) - 97); }

function countPieces(fen) {
  const out = { w: {}, b: {} };
  for (const ch of fen.split(' ')[0]) {
    if (!/[pnbrqkPNBRQK]/.test(ch)) continue;
    const side = ch === ch.toUpperCase() ? 'w' : 'b';
    const t = ch.toLowerCase();
    out[side][t] = (out[side][t] || 0) + 1;
  }
  return out;
}
