#!/usr/bin/env python3
"""
PARADOXChess: Non-Singular Chess Engine with GUI
Human vs Engine play using pygame.
"""

import chess
import chess.svg
import math
import random
import time
from typing import Union, Optional, List, Dict, Any
import pygame
import sys

# ----------------------------------------------------------------------
#  Non-Singular Operators (from the theory)
# ----------------------------------------------------------------------

PLANCK_EVAL = 1e-6
MATE_PROJECTIVE = 1_000_000      # The "point at infinity" as a finite token
T_EVAP = 100                     # Plies until mate evaporates (50-move rule)
MAX_ENTROPY = 5.0                # Firewall cutoff
PLANCK_MOBILITY = 1e-6           # Minimal mobility to avoid true zero


class UncertainValue:
    """Represents a value smeared by entropy."""
    def __init__(self, mu: float, entropy: float):
        self.mu = mu
        self.H = entropy


class Oscillation:
    """Represents a 2-cycle oscillation (e.g., in zugzwang)."""
    def __init__(self, a: float, b: float):
        self.cycle = (a, b)
        self.phase = 0

    def current(self) -> float:
        val = self.cycle[self.phase % 2]
        self.phase += 1
        return val


class EventHorizonReceipt:
    """Receipt returned at horizon (depth=0)."""
    def __init__(self, fen: str, entropy: float):
        self.fen = fen
        self.entropy = entropy


# ----------------------------------------------------------------------
#  Operator implementations
# ----------------------------------------------------------------------

def singularity_shield(x: float) -> float:
    """⊗ : Planck-scale cutoff – never below PLANCK_EVAL."""
    if abs(x) < PLANCK_EVAL:
        return math.copysign(PLANCK_EVAL, x) if x != 0 else PLANCK_EVAL
    return x


def projective_divide(a: float, b: float) -> float:
    """⨯ : a/b with projective closure: a/0 -> MATE_PROJECTIVE."""
    b = singularity_shield(b)
    if abs(b) == PLANCK_EVAL:
        return MATE_PROJECTIVE if a > 0 else -MATE_PROJECTIVE
    return a / b


def hawking_radiance(mate_score: float, depth: int) -> float:
    """⨳ : mate score radiates away over depth, evaporates at T_EVAP."""
    if depth >= T_EVAP:
        return 0.0
    return mate_score / (T_EVAP - depth)


def flip_divide(a: float, b: float) -> Union[float, Oscillation]:
    """⊘ : zugzwang oscillation. Returns Oscillation if b near zero."""
    b = singularity_shield(b)
    if abs(b) == PLANCK_EVAL:
        return Oscillation(a / PLANCK_EVAL, -a / PLANCK_EVAL)
    return a / b


def uncertain_quotient(a: float, b: float, entropy: float) -> UncertainValue:
    """⊘̷ : smear with entropy when denominator small."""
    b = singularity_shield(b)
    if abs(b) == PLANCK_EVAL:
        return UncertainValue(MATE_PROJECTIVE if a > 0 else -MATE_PROJECTIVE, entropy)
    return UncertainValue(a / b, entropy)


def event_horizon_divide(fen: str, entropy: float) -> EventHorizonReceipt:
    """∘ : returns a receipt at depth 0."""
    return EventHorizonReceipt(fen, entropy)


def wormhole_quotient(local_eval: float, board: chess.Board) -> float:
    """⨂ : non‑local correlations (castling rights, connected rooks, etc.)."""
    bonus = 0.0
    # Example: castling rights create non‑local entanglement
    if board.has_kingside_castling_rights(chess.WHITE):
        bonus += 0.1
    if board.has_queenside_castling_rights(chess.WHITE):
        bonus += 0.1
    # Rook connections on same rank/file
    for color in [chess.WHITE, chess.BLACK]:
        rooks = board.pieces(chess.ROOK, color)
        for sq1 in rooks:
            for sq2 in rooks:
                if sq1 != sq2 and (chess.square_rank(sq1) == chess.square_rank(sq2) or
                                   chess.square_file(sq1) == chess.square_file(sq2)):
                    bonus += 0.05
    return local_eval + bonus


def novikov_quotient(raw_eval: float, tt_entry: Optional[Dict]) -> float:
    """⨎ : adjust evaluation if transposition table shows a paradox."""
    if tt_entry is not None and 'parity' in tt_entry:
        if tt_entry['parity'] != (raw_eval > 0):
            return raw_eval * 0.95   # gentle consistency adjustment
    return raw_eval


def firewall_divide(branch_eval: Any, branch_entropy: float) -> Any:
    """⊞ : block branches with excessive entropy instead of pruning."""
    if branch_entropy > MAX_ENTROPY:
        return "FIREWALL_BLOCKED"
    return branch_eval


# ----------------------------------------------------------------------
#  PARADOXChess Engine
# ----------------------------------------------------------------------

class PARADOXChess:
    """Non-singular chess engine based on projective evaluation and TSP collapse."""

    def __init__(self, max_depth: int = 4):
        self.max_depth = max_depth
        self.tt = {}            # transposition table (wormhole network)
        self.nodes_evaluated = 0

    # ------------------------------------------------------------------
    #  Evaluation components
    # ------------------------------------------------------------------
    def material_balance(self, board: chess.Board) -> float:
        """Simplified material count."""
        val = 0.0
        for sq in chess.SQUARES:
            piece = board.piece_at(sq)
            if piece:
                v = {chess.PAWN: 1.0, chess.KNIGHT: 3.0, chess.BISHOP: 3.0,
                     chess.ROOK: 5.0, chess.QUEEN: 9.0}.get(piece.piece_type, 0.0)
                val += v if piece.color == chess.WHITE else -v
        return val

    def positional_entropy(self, board: chess.Board) -> float:
        """H(b) : microstate count -> entropy proxy."""
        mobility = len(list(board.legal_moves))
        # tension: number of attacks on centre squares (simplified)
        tension = 0
        for sq in [chess.E4, chess.D4, chess.E5, chess.D5]:
            tension += len(board.attacks(sq))
        return math.log1p(mobility + tension + 1)

    def tactical_complexity(self, board: chess.Board) -> float:
        """Proxy for denominator in general evaluation."""
        moves = list(board.legal_moves)
        checks = sum(1 for m in moves if board.gives_check(m))
        captures = sum(1 for m in moves if board.is_capture(m))
        return 1.0 + 0.1 * (checks + captures) + 0.01 * len(moves)

    # ------------------------------------------------------------------
    #  Core evaluation function (returns a scalar or receipt)
    # ------------------------------------------------------------------
    def evaluate(self, board: chess.Board, depth: int) -> Union[float, EventHorizonReceipt]:
        """Return a non‑singular evaluation."""
        material = self.material_balance(board)
        mobility = len(list(board.legal_moves))
        entropy = self.positional_entropy(board)

        # Singularity 1: Checkmate
        if board.is_checkmate():
            raw_mate = projective_divide(material, 0.0)
            return hawking_radiance(raw_mate, depth)

        # Singularity 2: Stalemate / Zugzwang (zero mobility, not mate)
        if mobility == 0:
            osc = flip_divide(material, 0.0)
            if isinstance(osc, Oscillation):
                return osc.current()
            return osc

        # Horizon: depth 0 returns a receipt
        if depth == 0:
            return event_horizon_divide(board.fen(), entropy)

        # General evaluation: uncertain quotient smeared by entropy
        complexity = self.tactical_complexity(board)
        uq = uncertain_quotient(material, complexity, entropy)
        eval_score = uq.mu

        # Wormhole non‑local correlations
        eval_score = wormhole_quotient(eval_score, board)

        # Novikov self‑consistency (using transposition table)
        key = board.zobrist_hash()
        tt_entry = self.tt.get(key)
        eval_score = novikov_quotient(eval_score, tt_entry)

        # Bekenstein bound: cap by entropy * 1e4
        if abs(eval_score) > entropy * 1e4:
            eval_score = math.copysign(entropy * 1e4, eval_score)

        return eval_score

    # ------------------------------------------------------------------
    #  TSP-Collapse search (non‑destructive, uses Firewall)
    # ------------------------------------------------------------------
    def search(self, board: chess.Board, depth: int, alpha: float, beta: float) -> float:
        """Minimax with projective comparison and firewall."""
        self.nodes_evaluated += 1

        # Terminal or horizon
        if board.is_game_over():
            return self.evaluate(board, depth)

        if depth == 0:
            eval_obj = self.evaluate(board, 0)
            if isinstance(eval_obj, EventHorizonReceipt):
                # Receipt: convert to heuristic using its entropy
                return eval_obj.entropy * 10.0 if board.turn == chess.WHITE else -eval_obj.entropy * 10.0
            return eval_obj if isinstance(eval_obj, (int, float)) else 0.0

        # Generate moves
        moves = list(board.legal_moves)
        # Order moves (simple MVV-LVA)
        moves.sort(key=lambda m: self.move_priority(board, m), reverse=True)

        best = -float('inf')
        for move in moves:
            board.push(move)
            branch_entropy = self.positional_entropy(board)

            # Firewall: block chaotic branches, do not delete
            if branch_entropy > MAX_ENTROPY:
                board.pop()
                continue

            child_val = self.search(board, depth - 1, -beta, -alpha)
            board.pop()

            # Projective comparison: treat MATE_PROJECTIVE as larger than any finite
            if self.projective_greater(child_val, best):
                best = child_val

            alpha = max(alpha, best)
            if alpha >= beta:
                break   # beta cutoff (still acceptable, we do not delete branches)

        if best == -float('inf'):
            # No valid moves after firewall? Return evaluation.
            best = self.evaluate(board, depth)
            if isinstance(best, (EventHorizonReceipt, UncertainValue, Oscillation)):
                best = 0.0
        return best

    def move_priority(self, board: chess.Board, move: chess.Move) -> float:
        """Simple MVV-LVA ordering."""
        piece = board.piece_at(move.from_square)
        victim = board.piece_at(move.to_square)
        if victim:
            piece_value = {chess.PAWN:1, chess.KNIGHT:3, chess.BISHOP:3,
                           chess.ROOK:5, chess.QUEEN:9}.get(piece.piece_type, 0)
            victim_value = {chess.PAWN:1, chess.KNIGHT:3, chess.BISHOP:3,
                            chess.ROOK:5, chess.QUEEN:9}.get(victim.piece_type, 0)
            return victim_value - piece_value / 100.0
        return 0.0

    def projective_greater(self, a: Union[float, Any], b: float) -> bool:
        """Comparison on RP¹ – MATE_PROJECTIVE is larger than any finite."""
        if isinstance(a, (Oscillation, EventHorizonReceipt, UncertainValue)):
            return False
        if abs(a) > MATE_PROJECTIVE / 2:
            return a > b
        return a > b

    # ------------------------------------------------------------------
    #  Main move selection
    # ------------------------------------------------------------------
    def get_best_move(self, board: chess.Board, time_limit: float = 1.0) -> chess.Move:
        """Iterative deepening with time control."""
        best_move = None
        start_time = time.time()
        for depth in range(1, self.max_depth + 1):
            move = self.iterative_deepening(board, depth)
            if move:
                best_move = move
            if time.time() - start_time > time_limit:
                break
        return best_move

    def iterative_deepening(self, board: chess.Board, depth: int) -> Optional[chess.Move]:
        """Search at fixed depth and return best move."""
        best_move = None
        best_value = -float('inf')
        moves = list(board.legal_moves)
        random.shuffle(moves)

        for move in moves:
            board.push(move)
            value = self.search(board, depth - 1, -float('inf'), float('inf'))
            board.pop()

            if self.projective_greater(value, best_value):
                best_value = value
                best_move = move
        return best_move


# ----------------------------------------------------------------------
#  GUI using Pygame
# ----------------------------------------------------------------------

class ChessGUI:
    def __init__(self, engine: PARADOXChess):
        pygame.init()
        self.engine = engine
        self.board = chess.Board()
        self.square_size = 70
        self.board_size = self.square_size * 8
        self.screen = pygame.display.set_mode((self.board_size, self.board_size + 80))
        pygame.display.set_caption("PARADOXChess")
        self.font = pygame.font.SysFont("Arial", 24)
        self.big_font = pygame.font.SysFont("Arial", 36)

        # Colors
        self.light_square = (240, 217, 181)
        self.dark_square = (181, 136, 99)
        self.highlight = (255, 255, 0, 128)      # translucent yellow
        self.last_move_highlight = (0, 255, 0, 128)  # translucent green
        self.selected_square = None
        self.legal_moves_for_selected = []
        self.running = True
        self.engine_thinking = False
        self.status_text = "Your turn (White)"

        # Map piece characters to Unicode symbols
        self.piece_symbols = {
            'r': '♜', 'n': '♞', 'b': '♝', 'q': '♛', 'k': '♚', 'p': '♟',
            'R': '♖', 'N': '♘', 'B': '♗', 'Q': '♕', 'K': '♔', 'P': '♙'
        }

    def draw_board(self):
        """Draw the chessboard with pieces."""
        for row in range(8):
            for col in range(8):
                x = col * self.square_size
                y = row * self.square_size
                color = self.light_square if (row + col) % 2 == 0 else self.dark_square
                pygame.draw.rect(self.screen, color, (x, y, self.square_size, self.square_size))

        # Highlight selected square
        if self.selected_square is not None:
            col = chess.square_file(self.selected_square)
            row = 7 - chess.square_rank(self.selected_square)
            s = pygame.Surface((self.square_size, self.square_size), pygame.SRCALPHA)
            s.fill(self.highlight)
            self.screen.blit(s, (col * self.square_size, row * self.square_size))

        # Highlight legal moves
        for move in self.legal_moves_for_selected:
            col = chess.square_file(move.to_square)
            row = 7 - chess.square_rank(move.to_square)
            s = pygame.Surface((self.square_size, self.square_size), pygame.SRCALPHA)
            s.fill((100, 255, 100, 100))
            self.screen.blit(s, (col * self.square_size, row * self.square_size))

        # Highlight last move (optional)
        if len(self.board.move_stack) > 0:
            last = self.board.move_stack[-1]
            for sq in [last.from_square, last.to_square]:
                col = chess.square_file(sq)
                row = 7 - chess.square_rank(sq)
                s = pygame.Surface((self.square_size, self.square_size), pygame.SRCALPHA)
                s.fill(self.last_move_highlight)
                self.screen.blit(s, (col * self.square_size, row * self.square_size))

        # Draw pieces
        for sq in chess.SQUARES:
            piece = self.board.piece_at(sq)
            if piece:
                col = chess.square_file(sq)
                row = 7 - chess.square_rank(sq)
                symbol = self.piece_symbols[piece.symbol()]
                text = self.font.render(symbol, True, (0, 0, 0))
                x = col * self.square_size + self.square_size // 2 - text.get_width() // 2
                y = row * self.square_size + self.square_size // 2 - text.get_height() // 2
                self.screen.blit(text, (x, y))

    def draw_status(self):
        """Draw status bar and turn indicator."""
        pygame.draw.rect(self.screen, (50, 50, 50), (0, self.board_size, self.board_size, 80))
        # Status text
        text = self.font.render(self.status_text, True, (255, 255, 255))
        self.screen.blit(text, (10, self.board_size + 10))
        # Turn indicator
        turn_text = "White to move" if self.board.turn == chess.WHITE else "Black to move"
        turn_color = (255, 255, 255) if self.board.turn == chess.WHITE else (100, 100, 100)
        turn_surf = self.font.render(turn_text, True, turn_color)
        self.screen.blit(turn_surf, (self.board_size - turn_surf.get_width() - 10, self.board_size + 10))

        # If engine thinking, show message
        if self.engine_thinking:
            think_surf = self.font.render("Engine thinking...", True, (200, 200, 0))
            self.screen.blit(think_surf, (10, self.board_size + 40))

    def get_square_from_mouse(self, pos):
        """Convert mouse coordinates to square index."""
        x, y = pos
        if x < 0 or x >= self.board_size or y < 0 or y >= self.board_size:
            return None
        col = x // self.square_size
        row = y // self.square_size
        # row 0 is rank 8, row 7 is rank 1
        rank = 7 - row
        file = col
        return chess.square(file, rank)

    def handle_move(self, from_sq, to_sq):
        """Try to make a move, return True if successful."""
        move = chess.Move(from_sq, to_sq)
        # Check if move is legal
        if move in self.board.legal_moves:
            self.board.push(move)
            self.selected_square = None
            self.legal_moves_for_selected = []
            self.status_text = "Move made. Engine thinking..."
            return True
        # Check for pawn promotion (to queen by default)
        piece = self.board.piece_at(from_sq)
        if piece and piece.piece_type == chess.PAWN:
            to_rank = chess.square_rank(to_sq)
            if (piece.color == chess.WHITE and to_rank == 7) or (piece.color == chess.BLACK and to_rank == 0):
                move = chess.Move(from_sq, to_sq, promotion=chess.QUEEN)
                if move in self.board.legal_moves:
                    self.board.push(move)
                    self.selected_square = None
                    self.legal_moves_for_selected = []
                    self.status_text = "Move made. Engine thinking..."
                    return True
        return False

    def engine_move(self):
        """Let the engine make a move."""
        if self.board.is_game_over():
            return
        self.engine_thinking = True
        self.draw_board()
        self.draw_status()
        pygame.display.flip()
        start = time.time()
        move = self.engine.get_best_move(self.board, time_limit=2.0)
        elapsed = time.time() - start
        self.engine_thinking = False
        if move:
            self.board.push(move)
            self.status_text = f"Engine played {move.uci()} (searched {self.engine.nodes_evaluated} nodes in {elapsed:.2f}s)"
            self.engine.nodes_evaluated = 0
        else:
            self.status_text = "Engine has no legal moves?!"

    def run(self):
        """Main game loop."""
        clock = pygame.time.Clock()
        self.running = True

        while self.running:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    self.running = False
                elif event.type == pygame.MOUSEBUTTONDOWN and not self.engine_thinking:
                    # Human only when it's white's turn (human is white)
                    if self.board.turn != chess.WHITE:
                        # Not human's turn
                        continue
                    pos = pygame.mouse.get_pos()
                    sq = self.get_square_from_mouse(pos)
                    if sq is not None:
                        if self.selected_square is None:
                            # Select piece if it's ours
                            piece = self.board.piece_at(sq)
                            if piece and piece.color == chess.WHITE:
                                self.selected_square = sq
                                self.legal_moves_for_selected = [m for m in self.board.legal_moves if m.from_square == sq]
                        else:
                            # Try to move
                            if self.handle_move(self.selected_square, sq):
                                # Human move succeeded, now engine's turn
                                # Check if game over
                                if self.board.is_game_over():
                                    self.status_text = "Game over!"
                                    self.selected_square = None
                                    self.legal_moves_for_selected = []
                                else:
                                    # Engine move
                                    self.engine_move()
                                    # After engine move, check game over again
                                    if self.board.is_game_over():
                                        self.status_text = "Game over!"
                            else:
                                # Invalid move, clear selection
                                self.selected_square = None
                                self.legal_moves_for_selected = []
                elif event.type == pygame.KEYDOWN:
                    if event.key == pygame.K_r:
                        # Reset game
                        self.board = chess.Board()
                        self.selected_square = None
                        self.legal_moves_for_selected = []
                        self.status_text = "Game reset. Your turn (White)."
                        self.engine_thinking = False

            # Draw everything
            self.screen.fill((0, 0, 0))
            self.draw_board()
            self.draw_status()

            # Game over message
            if self.board.is_game_over():
                result_text = ""
                if self.board.is_checkmate():
                    winner = "Black" if self.board.turn == chess.WHITE else "White"
                    result_text = f"Checkmate! {winner} wins."
                elif self.board.is_stalemate():
                    result_text = "Stalemate! Game drawn."
                elif self.board.is_insufficient_material():
                    result_text = "Insufficient material. Draw."
                else:
                    result_text = "Game over."
                text_surf = self.big_font.render(result_text, True, (255, 255, 0))
                self.screen.blit(text_surf, (self.board_size//2 - text_surf.get_width()//2, self.board_size + 40))

            pygame.display.flip()
            clock.tick(30)

        pygame.quit()
        sys.exit()


# ----------------------------------------------------------------------
#  Main entry point
# ----------------------------------------------------------------------

def main():
    engine = PARADOXChess(max_depth=3)   # Depth 3 for quick response
    gui = ChessGUI(engine)
    gui.run()


if __name__ == "__main__":
    main()