from .board import Board
from .pieces import Color, Piece, PieceType, Position


class Rules:
    """Chinese Chess (Xiangqi) move generation and check detection.

    Free-movement mode: players are warned about check but not forced
    to resolve it.  The general can be captured to end the game.
    """

    # ── Public API ─────────────────────────────────────────────────────

    @staticmethod
    def get_valid_moves(board: Board, piece: Piece) -> list[Position]:
        """Valid moves based on piece movement rules (free mode, no check filter)."""
        return Rules._raw_moves(board, piece)

    @staticmethod
    def is_valid_move(
        board: Board, from_pos: Position, to_pos: Position, color: Color
    ) -> bool:
        piece = board.get_piece(from_pos)
        if not piece or piece.color != color:
            return False
        return to_pos in Rules.get_valid_moves(board, piece)

    @staticmethod
    def is_in_check(board: Board, color: Color) -> bool:
        """Check if the given color's general is under attack (for warnings)."""
        general = board.find_general(color)
        if not general:
            return False

        opponent = color.opponent
        for piece in board.get_pieces(opponent):
            if general.position in Rules._raw_moves(board, piece):
                return True

        opp_general = board.find_general(opponent)
        if opp_general and opp_general.position.col == general.position.col:
            min_r = min(general.position.row, opp_general.position.row)
            max_r = max(general.position.row, opp_general.position.row)
            blocked = any(
                board.get_piece(Position(general.position.col, r))
                for r in range(min_r + 1, max_r)
            )
            if not blocked:
                return True

        return False

    @staticmethod
    def has_no_moves(board: Board, color: Color) -> bool:
        for piece in board.get_pieces(color):
            if Rules._raw_moves(board, piece):
                return False
        return True

    # ── Raw move generators ────────────────────────────────────────────

    @staticmethod
    def _raw_moves(board: Board, piece: Piece) -> list[Position]:
        dispatch = {
            PieceType.GENERAL: Rules._general_moves,
            PieceType.ADVISOR: Rules._advisor_moves,
            PieceType.ELEPHANT: Rules._elephant_moves,
            PieceType.HORSE: Rules._horse_moves,
            PieceType.CHARIOT: Rules._chariot_moves,
            PieceType.CANNON: Rules._cannon_moves,
            PieceType.SOLDIER: Rules._soldier_moves,
        }
        return dispatch[piece.piece_type](board, piece)

    @staticmethod
    def _on_board(col: int, row: int) -> bool:
        return 0 <= col < 9 and 0 <= row < 10

    @staticmethod
    def _in_palace(col: int, row: int, color: Color) -> bool:
        if not (3 <= col <= 5):
            return False
        return (7 <= row <= 9) if color == Color.RED else (0 <= row <= 2)

    @staticmethod
    def _can_land(board: Board, col: int, row: int, color: Color) -> bool:
        target = board.get_piece(Position(col, row))
        return target is None or target.color != color

    @staticmethod
    def _general_moves(board: Board, piece: Piece) -> list[Position]:
        moves = []
        c, r = piece.position
        for dc, dr in ((0, 1), (0, -1), (1, 0), (-1, 0)):
            nc, nr = c + dc, r + dr
            if Rules._in_palace(nc, nr, piece.color):
                if Rules._can_land(board, nc, nr, piece.color):
                    moves.append(Position(nc, nr))
        return moves

    @staticmethod
    def _advisor_moves(board: Board, piece: Piece) -> list[Position]:
        moves = []
        c, r = piece.position
        for dc, dr in ((1, 1), (1, -1), (-1, 1), (-1, -1)):
            nc, nr = c + dc, r + dr
            if Rules._in_palace(nc, nr, piece.color):
                if Rules._can_land(board, nc, nr, piece.color):
                    moves.append(Position(nc, nr))
        return moves

    @staticmethod
    def _elephant_moves(board: Board, piece: Piece) -> list[Position]:
        moves = []
        c, r = piece.position
        for dc, dr in ((2, 2), (2, -2), (-2, 2), (-2, -2)):
            nc, nr = c + dc, r + dr
            ec, er = c + dc // 2, r + dr // 2
            if not Rules._on_board(nc, nr):
                continue
            if piece.color == Color.RED and nr < 5:
                continue
            if piece.color == Color.BLACK and nr > 4:
                continue
            if board.get_piece(Position(ec, er)):
                continue
            if Rules._can_land(board, nc, nr, piece.color):
                moves.append(Position(nc, nr))
        return moves

    @staticmethod
    def _horse_moves(board: Board, piece: Piece) -> list[Position]:
        moves = []
        c, r = piece.position
        for dc, dr, bc, br in (
            (1, 2, 0, 1),
            (1, -2, 0, -1),
            (-1, 2, 0, 1),
            (-1, -2, 0, -1),
            (2, 1, 1, 0),
            (2, -1, 1, 0),
            (-2, 1, -1, 0),
            (-2, -1, -1, 0),
        ):
            nc, nr = c + dc, r + dr
            if not Rules._on_board(nc, nr):
                continue
            if board.get_piece(Position(c + bc, r + br)):
                continue
            if Rules._can_land(board, nc, nr, piece.color):
                moves.append(Position(nc, nr))
        return moves

    @staticmethod
    def _chariot_moves(board: Board, piece: Piece) -> list[Position]:
        moves = []
        c, r = piece.position
        for dc, dr in ((0, 1), (0, -1), (1, 0), (-1, 0)):
            nc, nr = c + dc, r + dr
            while Rules._on_board(nc, nr):
                target = board.get_piece(Position(nc, nr))
                if target is None:
                    moves.append(Position(nc, nr))
                else:
                    if target.color != piece.color:
                        moves.append(Position(nc, nr))
                    break
                nc += dc
                nr += dr
        return moves

    @staticmethod
    def _cannon_moves(board: Board, piece: Piece) -> list[Position]:
        moves = []
        c, r = piece.position
        for dc, dr in ((0, 1), (0, -1), (1, 0), (-1, 0)):
            nc, nr = c + dc, r + dr
            jumped = False
            while Rules._on_board(nc, nr):
                target = board.get_piece(Position(nc, nr))
                if not jumped:
                    if target is None:
                        moves.append(Position(nc, nr))
                    else:
                        jumped = True
                else:
                    if target is not None:
                        if target.color != piece.color:
                            moves.append(Position(nc, nr))
                        break
                nc += dc
                nr += dr
        return moves

    @staticmethod
    def _soldier_moves(board: Board, piece: Piece) -> list[Position]:
        moves = []
        c, r = piece.position
        if piece.color == Color.RED:
            forward = -1
            crossed = r <= 4
        else:
            forward = 1
            crossed = r >= 5

        nr = r + forward
        if Rules._on_board(c, nr) and Rules._can_land(board, c, nr, piece.color):
            moves.append(Position(c, nr))

        if crossed:
            for dc in (-1, 1):
                nc = c + dc
                if Rules._on_board(nc, r) and Rules._can_land(board, nc, r, piece.color):
                    moves.append(Position(nc, r))
        return moves
