import { Board, BoardState, Move, Neighbor, PointState, SimpleBoard } from "../Types";

import { GoColor, GoOpponent, GoValidity } from "@enums";
import { bitverseBoardShape } from "../Constants";
import { getExpansionMoveArray } from "../boardAnalysis/goAI";
import {
  boardFromSimpleBoard,
  boardStringFromBoard,
  clearAllPointHighlights,
  evaluateIfMoveIsValid,
  findAllCapturedChains,
  findLibertiesForChain,
  getAllChains,
  updatedBoardFromSimpleBoard,
} from "../boardAnalysis/boardAnalysis";
import { endGoGame } from "../boardAnalysis/scoring";
import { addObstacles, resetCoordinates, rotate90Degrees } from "./offlineNodes";

/** Generates a new BoardState object with the given opponent and size. Optionally use an existing board. */
export function getNewBoardState(
  boardSize: number,
  ai = GoOpponent.Netburners,
  applyObstacles = false,
  boardToCopy?: Board,
): BoardState {
  if (ai === GoOpponent.w0r1d_d43m0n) {
    boardToCopy = resetCoordinates(rotate90Degrees(boardFromSimpleBoard(bitverseBoardShape)));
    boardSize = 19;
    applyObstacles = false;
  }

  const newBoardState: BoardState = {
    previousBoards: [],
    previousPlayer: GoColor.white,
    ai: ai,
    passCount: 0,
    cheatCount: 0,
    cheatCountForWhite: 0,
    komiOverride: null,
    highlightedPoints: Array.from({ length: boardSize }, () => Array.from({ length: boardSize }, () => null)),
    board: Array.from({ length: boardSize }, (_, x) =>
      Array.from({ length: boardSize }, (_, y) =>
        !boardToCopy || boardToCopy?.[x]?.[y]
          ? {
              color: boardToCopy?.[x]?.[y]?.color ?? GoColor.empty,
              chain: "",
              liberties: null,
              x,
              y,
            }
          : null,
      ),
    ),
  };

  if (applyObstacles) {
    addObstacles(newBoardState);
  }

  const handicap = getHandicap(newBoardState.board[0].length, ai);
  if (handicap) {
    applyHandicap(newBoardState.board, handicap);
  }
  return newBoardState;
}

/**
 * Generates a new BoardState object from a given SimpleBoard string array, and an optional prior move board state
 */
export function getNewBoardStateFromSimpleBoard(
  simpleBoard: SimpleBoard,
  priorSimpleBoard?: SimpleBoard,
  ai: GoOpponent = GoOpponent.Netburners,
  priorColor: GoColor | undefined = undefined,
): BoardState {
  const newState = getNewBoardState(simpleBoard.length, ai, false, updatedBoardFromSimpleBoard(simpleBoard));
  if (priorSimpleBoard) {
    newState.previousBoards.push(priorSimpleBoard.join(""));
  }

  if (priorColor) {
    newState.previousPlayer = priorColor;
  } else if (priorSimpleBoard) {
    // Identify the previous player based on the difference in pieces
    const priorWhitePieces = priorSimpleBoard.join("").match(/O/g)?.length ?? 0;
    const priorBlackPieces = priorSimpleBoard.join("").match(/X/g)?.length ?? 0;
    const currentWhitePieces = simpleBoard.join("").match(/O/g)?.length ?? 0;
    const currentBlackPieces = simpleBoard.join("").match(/X/g)?.length ?? 0;
    if (priorWhitePieces - priorBlackPieces > currentWhitePieces - currentBlackPieces) {
      newState.previousPlayer = GoColor.black;
    }
  }
  updateChains(newState.board);
  return newState;
}

/**
 * Determines how many starting pieces the opponent has on the board
 */
export function getHandicap(boardSize: number, opponent: GoOpponent) {
  // Illuminati and WD get a few starting routers
  if (opponent === GoOpponent.Illuminati || opponent === GoOpponent.w0r1d_d43m0n) {
    return {
      [5]: 1,
      [7]: 3,
      [9]: 4,
      [13]: 5,
      [19]: 7,
    }[boardSize];
  }
  return 0;
}

/**
 * Make a new move on the given board, and update the board state accordingly
 * Modifies the board state in place
 * @returns a boolean representing whether the move was successful
 */
export function makeMove(boardState: BoardState, x: number, y: number, player: GoColor) {
  // Do not update on invalid moves
  const validity = evaluateIfMoveIsValid(boardState, x, y, player, false);
  if (validity !== GoValidity.valid || !boardState.board[x][y]?.color) {
    //console.debug(`Invalid move attempted! ${x} ${y} ${player} : ${validity}`);
    return false;
  }

  const point = boardState.board[x][y];
  if (!point) return false;

  // Add move to board history
  boardState.previousBoards.unshift(boardStringFromBoard(boardState.board));
  clearAllPointHighlights(boardState);

  point.color = player;
  boardState.previousPlayer = player;
  boardState.passCount = 0;

  updateCaptures(boardState.board, player);
  return true;
}

/**
 * Pass the current player's turn without making a move.
 * Ends the game if this is the second pass in a row.
 */
export function passTurn(boardState: BoardState, player: GoColor, allowEndGame = true) {
  if (boardState.previousPlayer === null || boardState.previousPlayer === player) {
    return;
  }
  clearAllPointHighlights(boardState);

  boardState.previousPlayer = boardState.previousPlayer === GoColor.black ? GoColor.white : GoColor.black;
  boardState.passCount++;

  if (boardState.passCount >= 2 && allowEndGame) {
    endGoGame(boardState);
  }
}

/**
 * Makes a number of random moves on the board before the game starts, to give one player an edge.
 * Modifies the board in place.
 */
export function applyHandicap(board: Board, handicap: number): void {
  const availableMoves = [];
  for (const column of board) {
    for (const point of column) {
      if (point) {
        if (point.color !== GoColor.empty) {
          // Game is in progress, don't apply handicap
          return;
        }
        availableMoves.push(point);
      }
    }
  }
  const handicapMoveOptions = getExpansionMoveArray(board, availableMoves);
  const handicapMoves: Move[] = [];

  // Special handling for 5x5: extra weight on handicap piece in the center of the board
  if (availableMoves.length < 26 && board[2][2] && Math.random() < 0.2) {
    board[2][2].color = GoColor.white;
    updateChains(board);
    return;
  }

  // select random distinct moves from the move options list up to the specified handicap amount
  for (let i = 0; i < handicap && i < handicapMoveOptions.length; i++) {
    const index = Math.floor(Math.random() * handicapMoveOptions.length);
    handicapMoves.push(handicapMoveOptions[index]);
    handicapMoveOptions.splice(index, 1);
  }

  handicapMoves.forEach((move: Move) => {
    const point = board[move.point.x][move.point.y];
    return move.point && point && (point.color = GoColor.white);
  });
  updateChains(board);
}

/**
 * Finds all groups of connected stones on the board, and updates the points in them with their
 * chain information and liberties.
 * Updates a board in-place.
 */
export function updateChains(board: Board, resetChains = true): void {
  resetChains && clearChains(board);

  for (let x = 0; x < board.length; x++) {
    for (let y = 0; y < board[x].length; y++) {
      const point = board[x][y];
      // If the current point is already analyzed, skip it
      if (!point || point.chain !== "") continue;

      const chainMembers = findAdjacentPointsInChain(board, x, y);
      const libertiesForChain = findLibertiesForChain(board, chainMembers);
      const id = `${point.x},${point.y}`;

      chainMembers.forEach((member) => {
        member.chain = id;
        member.liberties = libertiesForChain;
      });
    }
  }
}

/**
 * Assign each point on the board a chain ID, and link its list of 'liberties' (which are empty spaces
 * adjacent to some point on the chain including the current point).
 *
 * Then, remove any chains with no liberties.
 * Modifies the board in place.
 */
export function updateCaptures(board: Board, playerWhoMoved: GoColor, resetChains = true): void {
  updateChains(board, resetChains);
  const chains = getAllChains(board);

  const chainsToCapture = findAllCapturedChains(chains, playerWhoMoved);
  if (!chainsToCapture?.length) {
    return;
  }

  chainsToCapture?.forEach((chain) => captureChain(chain));
  updateChains(board);
}

/**
 * Removes a chain from the board, after being captured
 */
function captureChain(chain: PointState[]) {
  chain.forEach((point) => {
    point.color = GoColor.empty;
    point.chain = "";
    point.liberties = [];
  });
}

/**
 * Removes the chain data from all points on a board, in preparation for being recalculated later
 * Updates the board in-place
 */
function clearChains(board: Board): void {
  for (const column of board) {
    for (const point of column) {
      if (!point) continue;
      point.chain = "";
      point.liberties = null;
    }
  }
}

/**
 * Finds all the pieces in the current continuous group, or 'chain'
 *
 * Iteratively traverse the adjacent pieces of the same color to find all the pieces in the same chain,
 * which are the pieces connected directly via a path consisting only of only up/down/left/right
 */
export function findAdjacentPointsInChain(board: Board, x: number, y: number) {
  const point = board[x][y];
  if (!point) {
    return [];
  }
  const checkedPoints: PointState[] = [];
  const adjacentPoints: PointState[] = [point];
  const pointsToCheckNeighbors: PointState[] = [point];

  while (pointsToCheckNeighbors.length) {
    const currentPoint = pointsToCheckNeighbors.pop();
    if (!currentPoint) {
      break;
    }

    checkedPoints.push(currentPoint);
    const neighbors = findNeighbors(board, currentPoint.x, currentPoint.y);

    [neighbors.north, neighbors.east, neighbors.south, neighbors.west].filter(isNotNullish).forEach((neighbor) => {
      if (neighbor && neighbor.color === currentPoint.color && !contains(checkedPoints, neighbor)) {
        adjacentPoints.push(neighbor);
        pointsToCheckNeighbors.push(neighbor);
      }
      checkedPoints.push(neighbor);
    });
  }

  return adjacentPoints;
}

/**
 * Finds all empty spaces on the board.
 */
export function getEmptySpaces(board: Board): PointState[] {
  const emptySpaces: PointState[] = [];

  board.forEach((column) => {
    column.forEach((point) => {
      if (point && point.color === GoColor.empty) {
        emptySpaces.push(point);
      }
    });
  });

  return emptySpaces;
}

/**
 * Makes a deep copy of the given board state
 */
export function getStateCopy(initialState: BoardState) {
  const boardState = structuredClone(initialState);

  boardState.previousBoards = initialState.previousBoards ?? [];
  boardState.previousPlayer = initialState.previousPlayer;
  boardState.ai = initialState.ai;
  boardState.passCount = initialState.passCount;

  return boardState;
}

/** Make a deep copy of a board */
export function getBoardCopy(board: Board): Board {
  return structuredClone(board);
}

export function contains(arr: PointState[], point: PointState) {
  return !!arr.find((p) => p && p.x === point.x && p.y === point.y);
}

export function findNeighbors(board: Board, x: number, y: number): Neighbor {
  return {
    north: board[x]?.[y + 1],
    east: board[x + 1]?.[y],
    south: board[x]?.[y - 1],
    west: board[x - 1]?.[y],
  };
}

export function getArrayFromNeighbor(neighborObject: Neighbor): PointState[] {
  return [neighborObject.north, neighborObject.east, neighborObject.south, neighborObject.west].filter(isNotNullish);
}

export function isNotNullish<T>(argument: T | undefined | null): argument is T {
  return argument != null;
}
