/* board/board.js — the reusable chessboard component.
   Features: drag & click moves, premoves, promotion picker, legal-move
   hints, last-move/check/custom highlights, program + user arrows and
   circles (right-drag / right-click), coordinates inside/outside/off,
   piece sets, animation, blindfold, keyboard SAN entry, touch support.
   The board is presentation + input only — game logic stays in modes. */
'use strict';

import { h, clear } from '../ui/dom.js';
import { pieceSVG } from './pieces.js';
import { settings } from '../svc/settings.js';
import { algebraic, parseSquare, fileOf, rankOf } from '../core/rules.js';

const FILES = 'abcdefgh';

export class Board {
  /**
   * opts:
   *  interactive: bool
   *  orientation: 'w'|'b'
   *  onMove(uci): called for a user move attempt (already validated vs dests)
   *  onPremove(uci|null): premove set/cleared
   *  dests(): Map<from0x88, Array<{to, promo, capture}>> — legal targets (empty when not user's turn)
   *  canPremove(): bool — user may premove now
   *  onShapes(shapes): user drew arrows/circles
   *  onSelect(sq|null)
   */
  constructor(container, opts = {}) {
    this.opts = opts;
    this.orientation = opts.orientation || 'w';
    this.container = container;
    this.pieces = new Map();          // sq(0x88) -> {piece, el}
    this.selected = -1;
    this.premove = null;              // {from, to, promo}
    this.lastMove = null;             // {from, to}
    this.checkSq = -1;
    this.programShapes = [];          // [{from,to,color}] or [{sq,color}]
    this.userShapes = [];
    this._drag = null;
    this._rightDrag = null;
    this._kbdBuffer = '';
    this._promoResolve = null;
    this._settingsUnsub = [];
    this._build();
    this._bind();
    const repaint = () => { this._paintSquares(); this._refreshPieceSvgs(); this._paintOverlays(); };
    for (const key of ['board.theme', 'board.coords', 'pieces.set', 'pieces.blindfold', 'board.lastMove', 'board.checkHighlight', 'board.legalHints'])
      this._settingsUnsub.push(settings.on('change:' + key, repaint));
  }

  /* ================= construction ================= */
  _build() {
    this.root = h('div.cb-wrap');
    this.el = h('div.cb', { tabindex: 0, role: 'grid', 'aria-label': 'Chessboard' });
    this.squaresEl = h('div.cb-squares');
    this.hlEl = h('div.cb-layer');           // highlights
    this.piecesEl = h('div.cb-layer');
    this.hintsEl = h('div.cb-layer');
    this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    this.svg.setAttribute('viewBox', '0 0 800 800');
    this.svg.classList.add('cb-arrows');
    this.coordsEl = h('div.cb-coords');
    this.kbdEl = h('div.cb-kbd', { style: { display: 'none' } });
    this.promoEl = h('div.cb-promo', { style: { display: 'none' } });
    this.el.append(this.squaresEl, this.hlEl, this.piecesEl, this.hintsEl, this.svg, this.coordsEl, this.kbdEl, this.promoEl);
    this.root.append(this.el);
    this.container.append(this.root);
    this._paintSquares();
  }

  _paintSquares() {
    clear(this.squaresEl);
    for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) {
      this.squaresEl.append(h('div.cb-sq', {
        class: (x + y) % 2 === 0 ? 'light' : 'dark',
        style: { left: x * 12.5 + '%', top: y * 12.5 + '%' },
      }));
    }
    // coordinates
    clear(this.coordsEl);
    const mode = settings.get('board.coords');
    this.el.classList.toggle('coords-outside', mode === 'outside');
    if (mode === 'off') return;
    for (let i = 0; i < 8; i++) {
      const f = this.orientation === 'w' ? i : 7 - i;
      const r = this.orientation === 'w' ? 7 - i : i;
      this.coordsEl.append(
        h('span.cb-coord.file', { class: mode, style: { left: i * 12.5 + '%' }, dataset: { shade: (i % 2 === 0) === (mode === 'inside') ? 'd' : 'l' } }, FILES[f]),
        h('span.cb-coord.rank', { class: mode, style: { top: i * 12.5 + '%' }, dataset: { shade: i % 2 === 0 ? 'l' : 'd' } }, String(r + 1)),
      );
    }
  }

  _refreshPieceSvgs() {
    const set = settings.get('pieces.set');
    const blind = settings.get('pieces.blindfold');
    this.el.classList.toggle('blindfold', !!blind);
    for (const { piece, el } of this.pieces.values()) {
      el.innerHTML = pieceSVG(set, piece);
    }
  }

  /* ================= geometry ================= */
  _toScreen(sq) {
    const f = fileOf(sq), r = rankOf(sq);
    return this.orientation === 'w' ? { x: f, y: 7 - r } : { x: 7 - f, y: r };
  }
  _fromPoint(clientX, clientY) {
    const rect = this.el.getBoundingClientRect();
    const x = Math.floor((clientX - rect.left) / rect.width * 8);
    const y = Math.floor((clientY - rect.top) / rect.height * 8);
    if (x < 0 || x > 7 || y < 0 || y > 7) return -1;
    const f = this.orientation === 'w' ? x : 7 - x;
    const r = this.orientation === 'w' ? 7 - y : y;
    return r * 16 + f;
  }
  _place(el, sq) {
    const { x, y } = this._toScreen(sq);
    el.style.transform = `translate(${x * 100}%, ${y * 100}%)`;
  }

  setOrientation(o) {
    if (o === this.orientation) return;
    this.orientation = o;
    this._paintSquares();
    for (const [sq, { el }] of this.pieces) { el.classList.add('no-anim'); this._place(el, sq); }
    requestAnimationFrame(() => requestAnimationFrame(() =>
      this.pieces.forEach(({ el }) => el.classList.remove('no-anim'))));
    this._paintOverlays();
  }
  flip() { this.setOrientation(this.orientation === 'w' ? 'b' : 'w'); }

  /* ================= position ================= */
  /** pos: Chess instance (or {pieces(): [...]}) */
  setPosition(pos, { lastMove = null, check = -1, animate = true } = {}) {
    const speed = settings.get('board.animMs');
    const doAnim = animate && speed > 0;
    const next = new Map();
    for (const p of pos.pieces()) next.set(p.rank * 16 + p.file, p.piece);

    // pass 1: keep pieces that stay put
    const stale = new Map();          // sq -> {piece, el}
    for (const [sq, cur] of this.pieces) {
      if (next.get(sq) === cur.piece) next.delete(sq);
      else stale.set(sq, cur);
    }
    for (const sq of [...this.pieces.keys()]) if (stale.has(sq)) this.pieces.delete(sq);

    // pass 2: move matching piece types (nearest first) for animation
    for (const [sq, piece] of [...next]) {
      let bestKey = null, bestDist = 1e9;
      for (const [osq, cur] of stale) {
        if (cur.piece !== piece) continue;
        const d = Math.abs(fileOf(osq) - fileOf(sq)) + Math.abs(rankOf(osq) - rankOf(sq));
        if (d < bestDist) { bestDist = d; bestKey = osq; }
      }
      if (bestKey != null) {
        const cur = stale.get(bestKey);
        stale.delete(bestKey);
        next.delete(sq);
        if (!doAnim) cur.el.classList.add('no-anim');
        cur.el.style.transitionDuration = speed + 'ms';
        this._place(cur.el, sq);
        if (!doAnim) requestAnimationFrame(() => cur.el.classList.remove('no-anim'));
        this.pieces.set(sq, cur);
      }
    }
    // pass 3: remove leftovers, add new
    for (const [, cur] of stale) {
      if (doAnim) { cur.el.style.opacity = '0'; setTimeout(() => cur.el.remove(), speed); }
      else cur.el.remove();
    }
    const set = settings.get('pieces.set');
    for (const [sq, piece] of next) {
      const el = h('div.cb-piece.no-anim', { html: pieceSVG(set, piece), dataset: { piece } });
      el.style.transitionDuration = speed + 'ms';
      this._place(el, sq);
      this.piecesEl.append(el);
      this.pieces.set(sq, { piece, el });
      requestAnimationFrame(() => requestAnimationFrame(() => el.classList.remove('no-anim')));
    }
    for (const [sq, cur] of this.pieces) cur.el.dataset.sq = algebraic(sq);

    this.lastMove = lastMove;
    this.checkSq = check;
    this.selected = -1;
    this._paintOverlays();
  }

  /* ================= overlays ================= */
  _paintOverlays() {
    clear(this.hlEl);
    const add = (sq, cls) => {
      if (sq == null || sq < 0) return;
      const { x, y } = this._toScreen(sq);
      this.hlEl.append(h('div.cb-hl.' + cls, { style: { left: x * 12.5 + '%', top: y * 12.5 + '%' } }));
    };
    if (this.lastMove && settings.get('board.lastMove')) {
      add(this.lastMove.from, 'hl-last'); add(this.lastMove.to, 'hl-last');
    }
    if (this.premove) { add(this.premove.from, 'hl-premove'); add(this.premove.to, 'hl-premove'); }
    if (this.selected >= 0) add(this.selected, 'hl-sel');
    if (this.checkSq >= 0 && settings.get('board.checkHighlight')) add(this.checkSq, 'hl-check');
    for (const s of this.userShapes) if (s.sq != null) add(s.sq, 'hl-circle-' + (s.color || 'green'));
    this._paintHints();
    this._paintArrows();
  }

  _paintHints() {
    clear(this.hintsEl);
    if (this.selected < 0 || !settings.get('board.legalHints')) return;
    const dests = this.opts.dests ? this.opts.dests() : new Map();
    for (const d of dests.get(this.selected) || []) {
      const { x, y } = this._toScreen(d.to);
      this.hintsEl.append(h('div.cb-hint', {
        class: d.capture ? 'capture' : '',
        style: { left: x * 12.5 + '%', top: y * 12.5 + '%' },
      }));
    }
  }

  _paintArrows() {
    clear(this.svg);
    const draw = (from, to, color, width = 22) => {
      const a = this._toScreen(from), b = this._toScreen(to);
      const x1 = a.x * 100 + 50, y1 = a.y * 100 + 50, x2 = b.x * 100 + 50, y2 = b.y * 100 + 50;
      const dx = x2 - x1, dy = y2 - y1, len = Math.hypot(dx, dy);
      if (len < 1) return;
      const ux = dx / len, uy = dy / len, px = -uy, py = ux;
      const sx = x1 + ux * 34, sy = y1 + uy * 34;
      const hx = x2 - ux * 34, hy = y2 - uy * 34;
      const w = width / 2, hw = width * 1.05, hl = 36;
      const pts = [
        [sx + px * w, sy + py * w], [hx + px * w, hy + py * w], [hx + px * hw, hy + py * hw],
        [x2 - ux * (34 - hl), y2 - uy * (34 - hl)],
        [hx - px * hw, hy - py * hw], [hx - px * w, hy - py * w], [sx - px * w, sy - py * w],
      ].map(p => p.map(v => v.toFixed(1)).join(',')).join(' ');
      const poly = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
      poly.setAttribute('points', pts);
      poly.setAttribute('class', 'cb-arrow arrow-' + color);
      this.svg.append(poly);
    };
    for (const s of this.programShapes) if (s.from != null && s.to != null) draw(s.from, s.to, s.color || 'engine');
    for (const s of this.userShapes) if (s.from != null && s.to != null) draw(s.from, s.to, s.color || 'green');
    if (this.premove) draw(this.premove.from, this.premove.to, 'premove', 16);
  }

  setArrows(shapes) { this.programShapes = shapes || []; this._paintArrows(); }
  clearUserShapes() {
    if (this.userShapes.length) {
      this.userShapes = [];
      this._paintOverlays();
      this.opts.onShapes && this.opts.onShapes([]);
    }
  }

  /* ================= premove ================= */
  setPremoveState(pm) {
    this.premove = pm;
    this._paintOverlays();
  }

  /* ================= promotion ================= */
  askPromotion(toSq, color) {
    return new Promise(resolve => {
      if (settings.get('input.autoQueen')) return resolve('q');
      this._promoResolve = resolve;
      clear(this.promoEl);
      const set = settings.get('pieces.set');
      const letters = color === 'w' ? ['Q', 'N', 'R', 'B'] : ['q', 'n', 'r', 'b'];
      for (const l of letters) {
        this.promoEl.append(h('div.cb-promo-opt', {
          html: pieceSVG(set, l),
          onclick: e => { e.stopPropagation(); this._finishPromo(l.toLowerCase()); },
        }));
      }
      const { x, y } = this._toScreen(toSq);
      this.promoEl.style.left = x * 12.5 + '%';
      if (y <= 3) { this.promoEl.style.top = y * 12.5 + '%'; this.promoEl.style.bottom = 'auto'; this.promoEl.style.flexDirection = 'column'; }
      else { this.promoEl.style.top = 'auto'; this.promoEl.style.bottom = (7 - y) * 12.5 + '%'; this.promoEl.style.flexDirection = 'column-reverse'; }
      this.promoEl.style.display = 'flex';
    });
  }
  _finishPromo(choice) {
    this.promoEl.style.display = 'none';
    const r = this._promoResolve;
    this._promoResolve = null;
    if (r) r(choice);
  }

  /* ================= input ================= */
  _bind() {
    this.el.addEventListener('contextmenu', e => e.preventDefault());
    this.el.addEventListener('pointerdown', e => {
      if (this._promoResolve && !this.promoEl.contains(e.target)) this._finishPromo(null);
      if (e.button === 2) return this._beginRightDrag(e);
      if (e.button !== 0) return;
      this.clearUserShapes();
      if (!this.opts.interactive) return;
      const sq = this._fromPoint(e.clientX, e.clientY);
      if (sq < 0) return;
      const dests = this.opts.dests ? this.opts.dests() : new Map();
      const mine = dests.has(sq);
      // click-move to a destination of the selection
      if (this.selected >= 0 && sq !== this.selected) {
        const cand = (dests.get(this.selected) || []).find(d => d.to === sq);
        if (cand) { this._userMove(this.selected, sq, cand); this.selected = -1; this._paintOverlays(); return; }
      }
      // premove
      if (!mine && this.opts.canPremove && this.opts.canPremove() && this.pieces.get(sq) && this._isOwnPremovePiece(sq)) {
        this.selected = sq;
        this._paintOverlays();
        this._startDrag(e, sq, true);
        return;
      }
      if (mine) {
        this.selected = sq;
        this._paintOverlays();
        this.opts.onSelect && this.opts.onSelect(sq);
        this._startDrag(e, sq, false);
      } else {
        if (this.premove) { this.premove = null; this.opts.onPremove && this.opts.onPremove(null); }
        this.selected = -1;
        this._paintOverlays();
        this.opts.onSelect && this.opts.onSelect(null);
      }
    });
    this.el.addEventListener('pointermove', e => {
      if (this._rightDrag) return;
      if (!this._drag) return;
      const d = this._drag;
      const rect = d.rect;
      if (!d.moved) { d.el.classList.add('dragging'); d.moved = true; }
      const cell = rect.width / 8;
      d.el.style.transform = `translate(${e.clientX - rect.left - cell / 2}px, ${e.clientY - rect.top - cell / 2}px)`;
    });
    this.el.addEventListener('pointerup', e => {
      if (e.button === 2 || this._rightDrag) return this._endRightDrag(e);
      if (!this._drag) return;
      const d = this._drag; this._drag = null;
      d.el.classList.remove('dragging');
      d.el.classList.add('no-anim');
      this._place(d.el, d.from);
      requestAnimationFrame(() => requestAnimationFrame(() => d.el.classList.remove('no-anim')));
      if (!d.moved) return;                      // simple click: selection stays
      const to = this._fromPoint(e.clientX, e.clientY);
      if (to < 0 || to === d.from) { this._paintOverlays(); return; }
      if (d.premove) {
        this._setUserPremove(d.from, to);
        this.selected = -1; this._paintOverlays();
        return;
      }
      const dests = this.opts.dests ? this.opts.dests() : new Map();
      const cand = (dests.get(d.from) || []).find(x => x.to === to);
      if (cand) { this._userMove(d.from, to, cand); this.selected = -1; }
      this._paintOverlays();
    });
    // keyboard SAN entry + navigation keys pass through to mode via commands
    this.el.addEventListener('keydown', e => {
      if (!settings.get('input.keyboardEntry') || !this.opts.interactive) return;
      if (e.ctrlKey || e.altKey || e.metaKey) return;
      if (/^[a-hKKQRBNOx1-8=+#-]$/.test(e.key)) {
        this._kbdBuffer += e.key;
        this._kbdTry();
        e.stopPropagation();
        e.preventDefault();
      } else if (e.key === 'Backspace') {
        this._kbdBuffer = this._kbdBuffer.slice(0, -1);
        this._kbdShow();
        e.preventDefault();
      } else if (e.key === 'Escape') {
        this._kbdBuffer = '';
        this._kbdShow();
      }
    });
  }

  _isOwnPremovePiece(sq) {
    const cur = this.pieces.get(sq);
    if (!cur) return false;
    const isWhitePiece = cur.piece === cur.piece.toUpperCase();
    return this.opts.premoveColor ? (this.opts.premoveColor() === (isWhitePiece ? 'w' : 'b')) : false;
  }

  _startDrag(e, sq, premove) {
    const cur = this.pieces.get(sq);
    if (!cur) return;
    this._drag = { el: cur.el, from: sq, rect: this.el.getBoundingClientRect(), moved: false, premove };
    try { this.el.setPointerCapture(e.pointerId); } catch {}
  }

  async _setUserPremove(from, to) {
    // premove promotion: assume queen unless target is promotion rank and user set otherwise
    const pm = { from, to, promo: null, uci: algebraic(from) + algebraic(to) };
    const piece = this.pieces.get(from);
    if (piece && piece.piece.toLowerCase() === 'p' && (rankOf(to) === 7 || rankOf(to) === 0)) pm.promo = 'q';
    this.premove = pm;
    this.opts.onPremove && this.opts.onPremove(pm);
    this._paintOverlays();
  }

  async _userMove(from, to, cand) {
    let promo = null;
    if (cand.promo) {
      const color = rankOf(to) === 7 ? 'w' : 'b';
      promo = await this.askPromotion(to, color);
      if (!promo) return;
    }
    const uci = algebraic(from) + algebraic(to) + (promo || '');
    this.opts.onMove && this.opts.onMove(uci);
  }

  /* ---- right-drag shapes ---- */
  _beginRightDrag(e) {
    const sq = this._fromPoint(e.clientX, e.clientY);
    if (sq < 0) return;
    this._rightDrag = { from: sq };
    try { this.el.setPointerCapture(e.pointerId); } catch {}
  }
  _endRightDrag(e) {
    const rd = this._rightDrag;
    this._rightDrag = null;
    if (!rd) return;
    const to = this._fromPoint(e.clientX, e.clientY);
    const color = e.shiftKey ? 'red' : e.altKey ? 'blue' : 'green';
    if (to < 0) return;
    if (to === rd.from) {
      const existing = this.userShapes.findIndex(s => s.sq === to && s.color === color);
      if (existing >= 0) this.userShapes.splice(existing, 1);
      else this.userShapes.push({ sq: to, color });
    } else {
      const existing = this.userShapes.findIndex(s => s.from === rd.from && s.to === to && s.color === color);
      if (existing >= 0) this.userShapes.splice(existing, 1);
      else this.userShapes.push({ from: rd.from, to, color });
    }
    this._paintOverlays();
    this.opts.onShapes && this.opts.onShapes(this.userShapes);
  }

  /* ---- keyboard SAN ---- */
  _kbdShow() {
    this.kbdEl.textContent = this._kbdBuffer;
    this.kbdEl.style.display = this._kbdBuffer ? 'block' : 'none';
  }
  _kbdTry() {
    this._kbdShow();
    if (!this.opts.trySan) return;
    const res = this.opts.trySan(this._kbdBuffer);
    if (res === 'played' || res === 'invalid') {
      this._kbdBuffer = '';
      this._kbdShow();
      if (res === 'invalid') {
        this.kbdEl.textContent = '✕';
        this.kbdEl.style.display = 'block';
        setTimeout(() => this._kbdShow(), 350);
      }
    }
  }

  focus() { this.el.focus(); }

  destroy() {
    this._settingsUnsub.forEach(u => u());
    this.root.remove();
  }
}
