// Pure game logic functions extracted for testability

const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 20;
const GARBAGE_COLOR = '#666666';

// Tetromino definitions
const TETROMINOS = {
  I: { shape: [[0,0,0,0], [1,1,1,1], [0,0,0,0], [0,0,0,0]], color: '#00ffff' },
  O: { shape: [[1,1], [1,1]], color: '#ffff00' },
  T: { shape: [[0,1,0], [1,1,1], [0,0,0]], color: '#800080' },
  S: { shape: [[0,1,1], [1,1,0], [0,0,0]], color: '#00ff00' },
  Z: { shape: [[1,1,0], [0,1,1], [0,0,0]], color: '#ff0000' },
  J: { shape: [[1,0,0], [1,1,1], [0,0,0]], color: '#0000ff' },
  L: { shape: [[0,0,1], [1,1,1], [0,0,0]], color: '#ffa500' }
};

const TETROMINO_KEYS = Object.keys(TETROMINOS);

/**
 * Create an empty board
 * @returns {number[][]} Empty 20x10 board
 */
function createEmptyBoard() {
  return Array(BOARD_HEIGHT).fill(null).map(() => Array(BOARD_WIDTH).fill(0));
}

/**
 * Get a piece from its type
 * @param {string} pieceType - Type of tetromino (I, O, T, S, Z, J, L)
 * @returns {object} Piece object with shape and color
 */
function getPieceFromType(pieceType) {
  const tetromino = TETROMINOS[pieceType];
  return {
    type: pieceType,
    shape: JSON.parse(JSON.stringify(tetromino.shape)),
    color: tetromino.color,
    x: Math.floor(BOARD_WIDTH / 2) - Math.floor(tetromino.shape[0].length / 2),
    y: 0
  };
}

/**
 * Check if a position is valid for a piece
 * @param {object} piece - The piece to check
 * @param {number} x - X position
 * @param {number} y - Y position
 * @param {number[][]} board - The game board
 * @param {number[][]} shape - Optional custom shape (for rotation)
 * @returns {boolean} True if position is valid
 *
 * Note: Negative Y is allowed (overflow/spawn area above visible board).
 * This allows pieces to be pushed up by garbage without immediate elimination.
 */
function isValidPosition(piece, x, y, board, shape = null) {
  const pieceShape = shape || piece.shape;
  for (let row = 0; row < pieceShape.length; row++) {
    for (let col = 0; col < pieceShape[row].length; col++) {
      if (pieceShape[row][col]) {
        const newX = x + col;
        const newY = y + row;

        // Check bounds
        // Note: Only checks if piece goes BELOW board (newY >= 20)
        // Negative Y is allowed - pieces can exist above visible board
        if (newX < 0 || newX >= BOARD_WIDTH || newY >= BOARD_HEIGHT) {
          return false;
        }

        // Check collision with locked pieces (only if on board)
        // Note: Only checks collision if piece is in visible area (newY >= 0)
        if (newY >= 0 && board[newY][newX]) {
          return false;
        }
      }
    }
  }
  return true;
}

/**
 * Calculate ghost piece Y position
 * @param {object} piece - The current piece
 * @param {number[][]} board - The game board
 * @returns {number|null} Ghost Y position or null if no piece
 */
function getGhostY(piece, board) {
  if (!piece) return null;

  let ghostY = piece.y;
  while (isValidPosition(piece, piece.x, ghostY + 1, board)) {
    ghostY++;
  }
  return ghostY;
}

/**
 * Check if board has overflowed (blocks in top 2 rows)
 * @param {number[][]} board - The game board
 * @returns {boolean} True if board has overflowed
 *
 * Checks rows 0 AND 1 because:
 * - I piece at y=0 only places blocks in row 1 (unique shape)
 * - T piece at y=0 also only fills row 1
 * - If we only checked row 0, these pieces would NOT trigger elimination
 *
 * This is called when a piece LOCKS, not when garbage is received.
 * Player is eliminated only when they lock a piece that overflows.
 */
function checkBoardOverflow(board) {
  for (let row = 0; row < 2; row++) {
    for (let col = 0; col < BOARD_WIDTH; col++) {
      if (board[row][col] !== 0) {
        return true;
      }
    }
  }
  return false;
}

/**
 * Add garbage row to board
 * @param {number[][]} board - The game board
 * @param {object} currentPiece - Current falling piece (may be null)
 * @returns {object} Object with new board, updated piece, and gap position
 *
 * When garbage is added:
 * - All board rows shift up by 1 (top row removed)
 * - New garbage row added at bottom with one random gap
 * - Current piece y position decreases by 1 (pushed up)
 *
 * Note: Piece y can become negative (above visible board). This is intentional
 * - Player is NOT eliminated when piece goes negative
 * - Piece will drop naturally back into visible area
 * - Elimination only occurs when piece LOCKS with blocks in rows 0-1
 */
function addGarbageRow(board, currentPiece) {
  // Remove top row (all rows shift up)
  const newBoard = board.slice(1);

  // Add garbage row with random gap at bottom
  const garbageRow = Array(BOARD_WIDTH).fill(GARBAGE_COLOR);
  const gap = Math.floor(Math.random() * BOARD_WIDTH);
  garbageRow[gap] = 0;
  newBoard.push(garbageRow);

  // Push current piece up by 1 row
  // Note: y can become negative (above visible board) - this is intentional
  // Player survives and piece will drop naturally back into view
  let updatedPiece = currentPiece;
  if (currentPiece) {
    updatedPiece = {
      ...currentPiece,
      y: currentPiece.y - 1
    };
  }

  return { board: newBoard, currentPiece: updatedPiece, gap };
}

/**
 * Lock a piece to the board
 * @param {object} piece - The piece to lock
 * @param {number[][]} board - The game board
 * @returns {number[][]} New board with piece locked
 *
 * Only locks blocks that are within visible board bounds [0, 20).
 * Blocks at negative Y (above board) are clipped and not locked.
 *
 * Example: I piece at y=-2 has blocks at rows -2,-1,0,1
 * - Only blocks at rows 0 and 1 get locked to board
 * - Blocks at rows -2 and -1 are clipped (discarded)
 */
function lockPieceToBoard(piece, board) {
  const newBoard = board.map(row => [...row]);

  for (let row = 0; row < piece.shape.length; row++) {
    for (let col = 0; col < piece.shape[row].length; col++) {
      if (piece.shape[row][col]) {
        const boardY = piece.y + row;
        const boardX = piece.x + col;

        // Only lock blocks within visible board bounds
        // Blocks above board (negative Y) are clipped
        if (boardY >= 0 && boardY < BOARD_HEIGHT && boardX >= 0 && boardX < BOARD_WIDTH) {
          newBoard[boardY][boardX] = piece.color;
        }
      }
    }
  }

  return newBoard;
}

/**
 * Clear completed rows from board
 * @param {number[][]} board - The game board
 * @returns {object} Object with new board and rows cleared count
 */
function clearRows(board) {
  let rowsCleared = 0;
  const newBoard = board.map(row => [...row]);

  for (let row = BOARD_HEIGHT - 1; row >= 0; row--) {
    if (newBoard[row].every(cell => cell !== 0)) {
      newBoard.splice(row, 1);
      newBoard.unshift(Array(BOARD_WIDTH).fill(0));
      rowsCleared++;
      row++; // Check same row again
    }
  }

  return { board: newBoard, rowsCleared };
}

/**
 * Generate a shuffled bag of 7 tetrominoes
 * @returns {string[]} Array of 7 piece types
 */
function generatePieceBag() {
  const bag = [...TETROMINO_KEYS];
  for (let i = bag.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [bag[i], bag[j]] = [bag[j], bag[i]];
  }
  return bag;
}

/**
 * Create a piece queue with multiple bags
 * @param {number} bagCount - Number of bags to generate
 * @returns {string[]} Array of piece types
 */
function createPieceQueue(bagCount = 14) {
  const queue = [];
  for (let i = 0; i < bagCount; i++) {
    queue.push(...generatePieceBag());
  }
  return queue;
}

module.exports = {
  BOARD_WIDTH,
  BOARD_HEIGHT,
  GARBAGE_COLOR,
  TETROMINOS,
  TETROMINO_KEYS,
  createEmptyBoard,
  getPieceFromType,
  isValidPosition,
  getGhostY,
  checkBoardOverflow,
  addGarbageRow,
  lockPieceToBoard,
  clearRows,
  generatePieceBag,
  createPieceQueue
};
