import { HeadlessState } from './state.js';
import {
  pos2key,
  key2pos,
  opposite,
  distanceSq,
  allPos,
  computeSquareCenter,
  roleOf,
  dropOrigOf,
  changeNumber,
  isKey,
  isSame,
} from './util.js';
import { queen, knight, janggiElephant } from './premove.js';
import * as cg from './types.js';

export function callUserFunction<T extends (...args: any[]) => void>(f: T | undefined, ...args: Parameters<T>): void {
  if (f) setTimeout(() => f(...args), 1);
}

export function toggleOrientation(state: HeadlessState): void {
  state.orientation = opposite(state.orientation);
  state.animation.current = state.draggable.current = state.selectable.selected = undefined;
}

export function reset(state: HeadlessState): void {
  state.lastMove = undefined;
  unselect(state);
  unsetPremove(state);
}

export function setPieces(state: HeadlessState, pieces: cg.PiecesDiff): void {
  for (const [key, piece] of pieces) {
    if (piece) state.boardState.pieces.set(key, piece);
    else state.boardState.pieces.delete(key);
  }
}

export function setCheck(state: HeadlessState, arg: cg.Color | boolean | cg.Key[]): void {
  if (Array.isArray(arg)) state.check = arg;
  else {
    const color = arg === true ? state.turnColor : arg;
    state.check = [];
    if (color)
      for (const [k, p] of state.boardState.pieces)
        if (state.kingRoles.includes(p.role) && p.color === color)
          state.check.push(k);
  }
}

function setPremove(state: HeadlessState, orig: cg.Orig, dest: cg.Key, meta: cg.SetPremoveMetadata): void {
  state.premovable.current = [orig, dest];
  callUserFunction(state.premovable.events.set, orig, dest, meta);
}

export function unsetPremove(state: HeadlessState): void {
  if (state.premovable.current) {
    state.premovable.current = undefined;
    callUserFunction(state.premovable.events.unset);
  }
}

function tryAutoCastle(state: HeadlessState, orig: cg.Key, dest: cg.Key): boolean {
  if (!state.autoCastle) return false;

  const king = state.boardState.pieces.get(orig);
  if (!king || king.role !== 'k-piece') return false;

  // Because state has no variant info, we assume Capablanca (king moves three squares) for 10x8 boards
  const capa = state.dimensions.width === 10 && state.dimensions.height === 8;

  const origPos = key2pos(orig);
  const destPos = key2pos(dest);
  if ((origPos[1] !== 0 && origPos[1] !== 7) || origPos[1] !== destPos[1]) return false;
  if (origPos[0] === (capa ? 5 : 4) && !state.boardState.pieces.has(dest)) {
    if (destPos[0] === (capa ? 8 : 6)) dest = pos2key([(capa ? 9 : 7), destPos[1]]);
    else if (destPos[0] === 2) dest = pos2key([0, destPos[1]]);
  }
  const rook = state.boardState.pieces.get(dest);
  if (!rook || rook.color !== king.color || rook.role !== 'r-piece') return false;

  state.boardState.pieces.delete(orig);
  state.boardState.pieces.delete(dest);

  if (origPos[0] < destPos[0]) {
    state.boardState.pieces.set(pos2key([capa ? 8 : 6, destPos[1]]), king);
    state.boardState.pieces.set(pos2key([capa ? 7 : 5, destPos[1]]), rook);
  } else {
    state.boardState.pieces.set(pos2key([2, destPos[1]]), king);
    state.boardState.pieces.set(pos2key([3, destPos[1]]), rook);
  }
  return true;
}

export function baseMove(state: HeadlessState, orig: cg.Key, dest: cg.Key): cg.Piece | boolean {
  const origPiece = state.boardState.pieces.get(orig),
    destPiece = state.boardState.pieces.get(dest);
  if (orig === dest || !origPiece) return false;
  const captured = destPiece && destPiece.color !== origPiece.color ? destPiece : undefined;
  if (dest === state.selectable.selected) unselect(state);
  callUserFunction(state.events.move, orig, dest, captured);
  if (!tryAutoCastle(state, orig, dest)) {
    state.boardState.pieces.set(dest, origPiece);
    state.boardState.pieces.delete(orig);
  }
  state.lastMove = [orig, dest];
  state.check = undefined;
  callUserFunction(state.events.change);
  return captured || true;
}

export function baseNewPiece(
  state: HeadlessState,
  piece: cg.Piece,
  dest: cg.Key,
  fromPocket: boolean,
  force?: boolean
): boolean {
  if (state.boardState.pieces.has(dest)) {
    if (force) state.boardState.pieces.delete(dest);
    else return false;
  }
  callUserFunction(state.events.dropNewPiece, piece, dest);
  state.boardState.pieces.set(dest, piece);
  if (fromPocket) changeNumber(state.boardState.pockets![piece.color], piece.role, -1);
  state.lastMove = [dropOrigOf(piece.role), dest];
  state.check = undefined;
  callUserFunction(state.events.change);
  state.movable.dests = undefined;
  state.turnColor = opposite(state.turnColor);
  return true;
}

function baseUserMove(
  state: HeadlessState,
  orig: cg.Selectable,
  dest: cg.Key,
  fromPocket: boolean,
  force?: boolean
): cg.Piece | boolean {
  const result = isKey(orig) ? baseMove(state, orig, dest) : baseNewPiece(state, orig, dest, fromPocket, force);
  if (result) {
    state.movable.dests = undefined;
    state.turnColor = opposite(state.turnColor);
    state.animation.current = undefined;
  }
  return result;
}

export function userMove(
  state: HeadlessState,
  orig: cg.Selectable,
  dest: cg.Key,
  fromPocket: boolean,
  force?: boolean
): boolean {
  if (canMove(state, orig, dest, fromPocket) || force) {
    const result = baseUserMove(state, orig, dest, fromPocket, force);
    if (result) {
      const holdTime = state.hold.stop();
      unselect(state);
      const metadata: cg.MoveMetadata = {
        premove: false,
        ctrlKey: state.stats.ctrlKey,
        holdTime,
      };
      if (result !== true) metadata.captured = result;
      if (isKey(orig)) callUserFunction(state.movable.events.after, orig, dest, metadata);
      else callUserFunction(state.movable.events.afterNewPiece, orig, dest, metadata);
      return true;
    }
  } else if (canPremove(state, orig, dest, fromPocket)) {
    setPremove(state, isKey(orig) ? orig : dropOrigOf(orig.role), dest, {
      ctrlKey: state.stats.ctrlKey,
    });
    unselect(state);
    return true;
  }
  unselect(state);
  return false;
}

export function select(state: HeadlessState, selected: cg.Selectable, force?: boolean): void {
  if (isKey(selected)) callUserFunction(state.events.select, selected);
  else callUserFunction(state.events.selectPocket, selected);
  if (state.selectable.selected) {
    if (isSame(state.selectable.selected, selected) && !state.draggable.enabled) {
      unselect(state);
      state.hold.cancel();
      return;
    } else if ((state.selectable.enabled || force) && isKey(selected) && state.selectable.selected !== selected) {
      if (userMove(state, state.selectable.selected, selected, !!state.selectable.fromPocket)) {
        state.stats.dragged = false;
        return;
      }
    }
  }
  if (
    (state.selectable.enabled || state.draggable.enabled) &&
    (isMovable(state, selected, true) || isPremovable(state, selected, true))
  ) {
    setSelected(state, selected, true);
    state.hold.start();
  }
}

export function setSelected(state: HeadlessState, selected: cg.Selectable, fromPocket?: boolean): void {
  if (isKey(selected)) setSelectedKey(state, selected);
  else setDropMode(state, selected, !!fromPocket);
}

export function setSelectedKey(state: HeadlessState, key: cg.Key): void {
  state.selectable.selected = key;
  state.selectable.fromPocket = false;
  if (isPremovable(state, key, false)) {
    state.premovable.dests = state.premovable.premoveFunc(state.boardState, key, state.premovable.castle);
  } else {
    state.premovable.dests = undefined;
  }
}

export function setDropMode(state: HeadlessState, piece: cg.Piece, fromPocket: boolean): void {
  state.selectable.selected = piece;
  state.selectable.fromPocket = fromPocket;
  if (isPremovable(state, piece, fromPocket)) {
    state.premovable.dests = state.premovable.predropFunc(state.boardState, piece);
  } else {
    state.premovable.dests = undefined;
  }
}

export function unselect(state: HeadlessState): void {
  state.selectable.selected = undefined;
  state.premovable.dests = undefined;
  state.hold.cancel();
}

export function pieceAvailability(
  state: HeadlessState,
  orig: cg.Selectable,
  fromPocket: boolean
): [cg.Piece | undefined, boolean] {
  let piece: cg.Piece | undefined;
  let available = false;
  if (isKey(orig)) {
    piece = state.boardState.pieces.get(orig);
    available = !!piece;
  } else {
    piece = orig;
    const num = state.boardState.pockets?.[piece.color].get(piece.role) ?? 0;
    available = !fromPocket || num > 0;
  }
  return [piece, available];
}

function isMovable(state: HeadlessState, orig: cg.Selectable, fromPocket: boolean): boolean {
  const [piece, available] = pieceAvailability(state, orig, fromPocket);
  return (
    available &&
    (state.movable.color === 'both' || (state.movable.color === piece!.color && state.turnColor === piece!.color))
  );
}

export const canMove = (state: HeadlessState, orig: cg.Selectable, dest: cg.Key, fromPocket: boolean): boolean =>
  orig !== dest &&
  isMovable(state, orig, fromPocket) &&
  (state.movable.free || !!state.movable.dests?.get(isKey(orig) ? orig : dropOrigOf(orig.role))?.includes(dest));

function isPremovable(state: HeadlessState, orig: cg.Selectable, fromPocket: boolean): boolean {
  const [piece, available] = pieceAvailability(state, orig, fromPocket);
  return (
    available && state.premovable.enabled && state.movable.color === piece!.color && state.turnColor !== piece!.color
  );
}

const canPremove = (state: HeadlessState, orig: cg.Selectable, dest: cg.Key, fromPocket: boolean): boolean =>
  orig !== dest &&
  isPremovable(state, orig, fromPocket) &&
  (isKey(orig)
    ? state.premovable.premoveFunc(state.boardState, orig, state.premovable.castle).includes(dest)
    : state.premovable.predropFunc(state.boardState, orig).includes(dest));

export function isDraggable(state: HeadlessState, orig: cg.Selectable, fromPocket: boolean): boolean {
  const [piece, available] = pieceAvailability(state, orig, fromPocket);
  return (
    available &&
    state.draggable.enabled &&
    (state.movable.color === 'both' ||
      (state.movable.color === piece!.color && (state.turnColor === piece!.color || state.premovable.enabled)))
  );
}

export function playPremove(state: HeadlessState): boolean {
  const move = state.premovable.current;
  if (!move) return false;
  const orig = isKey(move[0]) ? move[0] : { role: roleOf(move[0]), color: state.turnColor };
  const dest = move[1];
  let success = false;
  if (canMove(state, orig, dest, true)) {
    const result = baseUserMove(state, orig, dest, true);
    if (result) {
      const metadata: cg.MoveMetadata = { premove: true };
      if (result !== true) metadata.captured = result;
      if (isKey(orig)) callUserFunction(state.movable.events.after, orig, dest, metadata);
      else callUserFunction(state.movable.events.afterNewPiece, orig, dest, metadata);
      success = true;
    }
  }
  unsetPremove(state);
  return success;
}

export function cancelMove(state: HeadlessState): void {
  unsetPremove(state);
  unselect(state);
}

export function stop(state: HeadlessState): void {
  state.movable.color = state.movable.dests = state.animation.current = undefined;
  cancelMove(state);
}

export function getKeyAtDomPos(
  pos: cg.NumberPair,
  asWhite: boolean,
  bounds: ClientRect,
  bd: cg.BoardDimensions
): cg.Key | undefined {
  let file = Math.floor((bd.width * (pos[0] - bounds.left)) / bounds.width);
  if (!asWhite) file = bd.width - 1 - file;
  let rank = bd.height - 1 - Math.floor((bd.height * (pos[1] - bounds.top)) / bounds.height);
  if (!asWhite) rank = bd.height - 1 - rank;
  return file >= 0 && file < bd.width && rank >= 0 && rank < bd.height ? pos2key([file, rank]) : undefined;
}

export function getSnappedKeyAtDomPos(
  orig: cg.Key,
  pos: cg.NumberPair,
  asWhite: boolean,
  bounds: ClientRect,
  bd: cg.BoardDimensions
): cg.Key | undefined {
  const origPos = key2pos(orig);
  const validSnapPos = allPos(bd).filter(pos2 => {
    return (
      queen(origPos[0], origPos[1], pos2[0], pos2[1]) ||
      knight(origPos[0], origPos[1], pos2[0], pos2[1]) ||
      // Only apply this to 9x10 board to avoid interfering with other variants beside Janggi
      (bd.width === 9 && bd.height === 10 && janggiElephant(origPos[0], origPos[1], pos2[0], pos2[1]))
    );
  });
  const validSnapCenters = validSnapPos.map(pos2 => computeSquareCenter(pos2key(pos2), asWhite, bounds, bd));
  const validSnapDistances = validSnapCenters.map(pos2 => distanceSq(pos, pos2));
  const [, closestSnapIndex] = validSnapDistances.reduce(
    (a, b, index) => (a[0] < b ? a : [b, index]),
    [validSnapDistances[0], 0]
  );
  return pos2key(validSnapPos[closestSnapIndex]);
}

export const whitePov = (s: HeadlessState): boolean => s.orientation === 'white';
