import pygame
import sys
import math
import random

# -----------------------------
# Pygame and world initialization
# -----------------------------
pygame.init()
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
BLOCK_SIZE = 40
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("2D Voxel Game with Advanced Debug Visualization")
clock = pygame.time.Clock()
font = pygame.font.Font(None, 20)

# -----------------------------
# Global debug options
# -----------------------------
debug_mode = False         # Toggle full debug overlay (F3)
show_bvh = False           # Toggle drawing BVH overlay (D)
num_rays = 360 * 2             # Rays cast for lighting
max_distance = 1000        # Maximum ray distance if nothing is hit

# Global counter for ray intersection tests
ray_intersect_count = 0

# -----------------------------
# Create a simple voxel world
# -----------------------------
# Build a grid of blocks (some cells are solid, some are empty)
def generate_blocks():
    blocks = []
    cols = SCREEN_WIDTH // BLOCK_SIZE
    rows = SCREEN_HEIGHT // BLOCK_SIZE
    for i in range(cols):
        for j in range(rows):
            # For demo purposes, randomly assign some blocks as solid (20% chance)
            if random.random() < 0.2:
                blocks.append(pygame.Rect(i * BLOCK_SIZE, j * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE))
    return blocks

blocks = generate_blocks()

# -----------------------------
# BVH Data Structures and Build
# -----------------------------
# BVH Node: holds a bounding box (min_x, min_y, max_x, max_y) and either children or a solid block.
class BVHNode:
    def __init__(self, bbox, left=None, right=None, block=None):
        self.bbox = bbox      # Tuple: (min_x, min_y, max_x, max_y)
        self.left = left
        self.right = right
        self.block = block    # For leaf nodes, store the actual block (pygame.Rect)

def rect_to_bbox(rect):
    return (rect.left, rect.top, rect.right, rect.bottom)

def union_bbox(b1, b2):
    x1 = min(b1[0], b2[0])
    y1 = min(b1[1], b2[1])
    x2 = max(b1[2], b2[2])
    y2 = max(b1[3], b2[3])
    return (x1, y1, x2, y2)

def build_bvh(block_list):
    if not block_list:
        return None
    if len(block_list) == 1:
        return BVHNode(rect_to_bbox(block_list[0]), block=block_list[0])
    # Sort blocks by center x-coordinate (could also choose y or alternate)
    block_list.sort(key=lambda rect: rect.centerx)
    mid = len(block_list) // 2
    left = build_bvh(block_list[:mid])
    right = build_bvh(block_list[mid:])
    if left and right:
        bbox = union_bbox(left.bbox, right.bbox)
    elif left:
        bbox = left.bbox
    else:
        bbox = right.bbox
    return BVHNode(bbox, left, right)

bvh_root = build_bvh(blocks)

# -----------------------------
# BVH Statistics Functions
# -----------------------------
def get_bvh_stats(node):
    """Return (node_count, depth) for the given BVH."""
    if node is None:
        return (0, 0)
    if node.block is not None:
        return (1, 1)
    left_count, left_depth = get_bvh_stats(node.left)
    right_count, right_depth = get_bvh_stats(node.right)
    total = 1 + left_count + right_count
    depth = 1 + max(left_depth, right_depth)
    return (total, depth)

# -----------------------------
# Ray-AABB Intersection (Slab Method)
# -----------------------------
def ray_intersect_aabb(origin, direction, bbox):
    # Increase global counter each time this function is called.
    global ray_intersect_count
    ray_intersect_count += 1

    # bbox: (min_x, min_y, max_x, max_y)
    tmin = -math.inf
    tmax = math.inf
    ox, oy = origin
    dx, dy = direction

    # X slab
    if dx != 0:
        tx1 = (bbox[0] - ox) / dx
        tx2 = (bbox[2] - ox) / dx
        tmin = max(tmin, min(tx1, tx2))
        tmax = min(tmax, max(tx1, tx2))
    else:
        if not (bbox[0] <= ox <= bbox[2]):
            return None

    # Y slab
    if dy != 0:
        ty1 = (bbox[1] - oy) / dy
        ty2 = (bbox[3] - oy) / dy
        tmin = max(tmin, min(ty1, ty2))
        tmax = min(tmax, max(ty1, ty2))
    else:
        if not (bbox[1] <= oy <= bbox[3]):
            return None

    if tmax >= tmin and tmax >= 0:
        return tmin if tmin >= 0 else tmax
    return None

# -----------------------------
# BVH Ray Casting
# -----------------------------
def ray_cast_bvh(node, origin, direction):
    if node is None:
        return None, None
    # Test if ray intersects node's bounding box.
    t_bbox = ray_intersect_aabb(origin, direction, node.bbox)
    if t_bbox is None:
        return None, None

    # If leaf node, test collision with the block.
    if node.block is not None:
        t_hit = ray_intersect_aabb(origin, direction, rect_to_bbox(node.block))
        if t_hit is not None:
            return t_hit, node.block
        else:
            return None, None

    # Otherwise, recursively test both children.
    t_left, block_left = ray_cast_bvh(node.left, origin, direction)
    t_right, block_right = ray_cast_bvh(node.right, origin, direction)

    if t_left is not None and t_right is not None:
        if t_left < t_right:
            return t_left, block_left
        else:
            return t_right, block_right
    elif t_left is not None:
        return t_left, block_left
    elif t_right is not None:
        return t_right, block_right
    return None, None

# -----------------------------
# Ray-Based Lighting Function
# -----------------------------
# Cast many rays from the light source (controlled by the mouse) and collect hit points.
light_source = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)

def cast_light_rays(light_pos):
    points = []
    # Cast one ray per degree
    for angle in range(0, 360, num_rays // num_rays):
        rad = math.radians(angle)
        direction = (math.cos(rad), math.sin(rad))
        t, _ = ray_cast_bvh(bvh_root, light_pos, direction)
        if t is None:
            t = max_distance
        hit_x = light_pos[0] + direction[0] * t
        hit_y = light_pos[1] + direction[1] * t
        points.append((hit_x, hit_y))
    return points

# -----------------------------
# Advanced Debug Menu Drawing
# -----------------------------
def draw_debug_menu(surface, fps):
    # Gather BVH statistics.
    bvh_node_count, bvh_depth = get_bvh_stats(bvh_root)
    # Prepare debug information lines.
    debug_lines = [
        "DEBUG MODE ACTIVE",
        f"FPS: {fps:.1f}",
        f"Blocks: {len(blocks)}",
        f"BVH Nodes: {bvh_node_count}",
        f"BVH Depth: {bvh_depth}",
        f"Light Source: {light_source}",
        f"Rays Cast: {num_rays}",
        f"Ray Intersection Tests (this frame): {ray_intersect_count}",
        f"Avg Tests per Ray: {ray_intersect_count / num_rays:.2f}",
        "Toggles: F3 - Debug, D - BVH overlay, R - Regenerate World"
    ]
    # Create a semi-transparent panel background.
    panel_width = 300
    panel_height = (len(debug_lines) * 22) + 10
    panel = pygame.Surface((panel_width, panel_height))
    panel.set_alpha(180)
    panel.fill((0, 0, 0))
    # Render text lines onto the panel.
    y_offset = 5
    for line in debug_lines:
        text_surf = font.render(line, True, (255, 255, 255))
        panel.blit(text_surf, (5, y_offset))
        y_offset += 22
    # Blit the debug panel onto the main surface.
    surface.blit(panel, (10, 10))

# -----------------------------
# BVH Visualization (recursive drawing)
# -----------------------------
def draw_bvh(node, surface):
    if node is None:
        return
    x, y, x2, y2 = node.bbox
    rect = pygame.Rect(x, y, x2 - x, y2 - y)
    # Draw leaf nodes in green; internal nodes in red.
    color = (0, 255, 0) if node.block is not None else (255, 0, 0)
    pygame.draw.rect(surface, color, rect, 1)
    draw_bvh(node.left, surface)
    draw_bvh(node.right, surface)

# -----------------------------
# Main Game Loop
# -----------------------------
running = True
while running:
    # Reset ray intersection counter each frame.
    ray_intersect_count = 0

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
        elif event.type == pygame.KEYDOWN:
            # Toggle full debug mode with F3.
            if event.key == pygame.K_F3:
                debug_mode = not debug_mode
            # Toggle BVH overlay with D.
            elif event.key == pygame.K_d:
                show_bvh = not show_bvh
            # Regenerate the world and rebuild the BVH with R.
            elif event.key == pygame.K_r:
                blocks = generate_blocks()
                bvh_root = build_bvh(blocks)
        # Update light source with mouse motion.
        elif event.type == pygame.MOUSEMOTION:
            light_source = event.pos

    # Clear screen.
    screen.fill((30, 30, 30))

    # Draw voxel blocks (solid cells).
    for block in blocks:
        pygame.draw.rect(screen, (100, 100, 100), block)

    # Optionally draw the BVH overlay.
    if show_bvh and bvh_root:
        draw_bvh(bvh_root, screen)

    # Cast light rays from the light source.
    light_polygon = cast_light_rays(light_source)
    # Create a separate surface for lighting effects.
    light_surface = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
    light_surface.fill((0, 0, 0))
    pygame.draw.polygon(light_surface, (255, 255, 200), light_polygon)
    light_surface.set_alpha(180)
    screen.blit(light_surface, (0, 0), special_flags=pygame.BLEND_ADD)

    # Draw the light source.
    pygame.draw.circle(screen, (255, 255, 0), light_source, 5)

    # Draw advanced debug overlay if debug mode is enabled.
    if debug_mode:
        fps = clock.get_fps()
        draw_debug_menu(screen, fps)

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

pygame.quit()
sys.exit()
