import pygame
import sys
import math
import random

pygame.init()
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("3D Minecraft - No Clipping")
clock = pygame.time.Clock()
FPS = 60

# Colors
SKY_TOP = (110, 170, 255)
SKY_BOTTOM = (200, 230, 255)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)

# Block colors
BLOCK_COLORS = {
    'grass_top': (140, 210, 100),
    'grass_side': (130, 110, 70),
    'dirt': (150, 110, 70),
    'stone': (150, 150, 150),
    'wood': (170, 130, 70),
    'leaves': (60, 150, 60),
}

BLOCK_TYPES = {
    'air': None,
    'grass': {'top': BLOCK_COLORS['grass_top'], 'side': BLOCK_COLORS['grass_side'], 'bottom': BLOCK_COLORS['dirt']},
    'dirt': {'top': BLOCK_COLORS['dirt'], 'side': BLOCK_COLORS['dirt'], 'bottom': BLOCK_COLORS['dirt']},
    'stone': {'top': BLOCK_COLORS['stone'], 'side': BLOCK_COLORS['stone'], 'bottom': BLOCK_COLORS['stone']},
    'wood': {'top': BLOCK_COLORS['wood'], 'side': BLOCK_COLORS['wood'], 'bottom': BLOCK_COLORS['wood']},
    'leaves': {'top': BLOCK_COLORS['leaves'], 'side': BLOCK_COLORS['leaves'], 'bottom': BLOCK_COLORS['leaves']},
}

# World
WORLD_WIDTH = 32
WORLD_DEPTH = 32
WORLD_HEIGHT = 12
world = [[[('air', 0) for _ in range(WORLD_HEIGHT)] for _ in range(WORLD_DEPTH)] for _ in range(WORLD_WIDTH)]

# Generate terrain
for x in range(WORLD_WIDTH):
    for z in range(WORLD_DEPTH):
        h = int(1 + 2.0 * (math.sin(x * 0.3) + math.cos(z * 0.4)))
        h = max(1, min(4, h))
        for y in range(h):
            if y == 0:
                block = 'grass'
            elif y == 1:
                block = 'dirt'
            else:
                block = 'stone'
            world[x][z][y] = (block, 0)

# Trees
for _ in range(20):
    tx = random.randint(2, WORLD_WIDTH-3)
    tz = random.randint(2, WORLD_DEPTH-3)
    height = random.randint(3, 5)
    for ty in range(1, height+1):
        world[tx][tz][ty] = ('wood', 0)
    for lx in range(tx-1, tx+2):
        for lz in range(tz-1, tz+2):
            for ly in range(height, height+2):
                if 0 <= lx < WORLD_WIDTH and 0 <= lz < WORLD_DEPTH and 0 <= ly < WORLD_HEIGHT:
                    world[lx][lz][ly] = ('leaves', 0)

# Player
player_x, player_y, player_z = 15.0, 6.0, 15.0
yaw, pitch = 0.0, 0.0
FOV = math.pi / 3
NEAR = 0.1
MOVE_SPEED = 0.08
MOUSE_SENS = 0.002
VEL_Y = 0
GRAVITY = -0.02
JUMP_SPEED = 0.25

# Player collision box
PLAYER_HALF_WIDTH = 0.3    # half width in XZ plane
PLAYER_HEIGHT = 1.8

selected_slot = 2
HOTBAR = ['grass', 'dirt', 'stone', 'wood', 'leaves']

def is_solid(block_type):
    return block_type != 'air'

def check_collision(px, py, pz):
    """Check if the player's AABB overlaps any solid block."""
    min_x = math.floor(px - PLAYER_HALF_WIDTH)
    max_x = math.floor(px + PLAYER_HALF_WIDTH)
    min_y = math.floor(py)
    max_y = math.floor(py + PLAYER_HEIGHT)
    min_z = math.floor(pz - PLAYER_HALF_WIDTH)
    max_z = math.floor(pz + PLAYER_HALF_WIDTH)

    for x in range(min_x, max_x + 1):
        for y in range(min_y, max_y + 1):
            for z in range(min_z, max_z + 1):
                if 0 <= x < WORLD_WIDTH and 0 <= y < WORLD_HEIGHT and 0 <= z < WORLD_DEPTH:
                    if world[x][z][y][0] != 'air':
                        return True
    return False

def move_with_collision(dx, dy, dz):
    """Move player by (dx, dy, dz), checking collisions per axis and subdividing steps."""
    global player_x, player_y, player_z, VEL_Y

    # Subdivide movement to avoid tunneling
    max_step = 0.02
    steps_x = max(1, int(abs(dx) / max_step) + 1)
    steps_y = max(1, int(abs(dy) / max_step) + 1)
    steps_z = max(1, int(abs(dz) / max_step) + 1)
    steps = max(steps_x, steps_y, steps_z)

    step_dx = dx / steps
    step_dy = dy / steps
    step_dz = dz / steps

    for _ in range(steps):
        # X axis
        new_x = player_x + step_dx
        if not check_collision(new_x, player_y, player_z):
            player_x = new_x
        # Z axis
        new_z = player_z + step_dz
        if not check_collision(player_x, player_y, new_z):
            player_z = new_z
        # Y axis
        new_y = player_y + step_dy
        if step_dy <= 0:  # falling
            if not check_collision(player_x, new_y, player_z):
                player_y = new_y
                # If we were falling and now on ground, reset VEL_Y
                # We'll handle grounding later
            else:
                # Adjust to top of block
                player_y = math.floor(player_y)
                VEL_Y = 0
        else:  # jumping
            if not check_collision(player_x, new_y, player_z):
                player_y = new_y
            else:
                VEL_Y = 0

def project(x, y, z):
    dx = x - player_x
    dy = y - player_y
    dz = z - player_z

    cos_y = math.cos(-yaw)
    sin_y = math.sin(-yaw)
    dx_rot = dx * cos_y - dz * sin_y
    dz_rot = dx * sin_y + dz * cos_y

    cos_p = math.cos(-pitch)
    sin_p = math.sin(-pitch)
    dy_rot = dy * cos_p - dz_rot * sin_p
    dz_rot = dy * sin_p + dz_rot * cos_p

    if dz_rot <= NEAR:
        return None

    sx = (dx_rot / dz_rot) * (WIDTH / (2 * math.tan(FOV/2))) + WIDTH/2
    sy = (-dy_rot / dz_rot) * (WIDTH / (2 * math.tan(FOV/2))) + HEIGHT/2

    if abs(sx) > 10000 or abs(sy) > 10000:
        return None
    return (int(sx), int(sy))

def draw_block(screen, bx, by, bz, btype):
    info = BLOCK_TYPES.get(btype)
    if not info:
        return

    dx = bx+0.5 - player_x
    dy = by+0.5 - player_y
    dz = bz+0.5 - player_z
    if math.sqrt(dx*dx + dy*dy + dz*dz) > 25:
        return

    faces = {
        'top':    (0,1,0, [(0,1,0),(1,1,0),(1,1,1),(0,1,1)]),
        'bottom': (0,-1,0, [(0,0,0),(1,0,0),(1,0,1),(0,0,1)]),
        'front':  (0,0,1, [(0,0,1),(1,0,1),(1,1,1),(0,1,1)]),
        'back':   (0,0,-1, [(0,0,0),(1,0,0),(1,1,0),(0,1,0)]),
        'left':   (-1,0,0, [(0,0,0),(0,0,1),(0,1,1),(0,1,0)]),
        'right':  (1,0,0, [(1,0,0),(1,0,1),(1,1,1),(1,1,0)])
    }

    for fname, (nx,ny,nz, corners) in faces.items():
        nb_x, nb_y, nb_z = bx+nx, by+ny, bz+nz
        visible = True
        if 0 <= nb_x < WORLD_WIDTH and 0 <= nb_y < WORLD_HEIGHT and 0 <= nb_z < WORLD_DEPTH:
            if world[nb_x][nb_z][nb_y][0] != 'air':
                visible = False
        if not visible:
            continue

        pts = []
        for cx,cy,cz in corners:
            p = project(bx+cx, by+cy, bz+cz)
            if p is None:
                pts = None
                break
            pts.append(p)
        if pts is None:
            continue

        if fname == 'top':
            color = info['top']; shade = 1.0
        elif fname == 'bottom':
            color = info['bottom']; shade = 0.4
        elif fname == 'front':
            color = info['side']; shade = 0.8
        elif fname == 'back':
            color = info['side']; shade = 0.6
        elif fname == 'right':
            color = info['side']; shade = 0.7
        else:
            color = info['side']; shade = 0.5

        r = int(color[0] * shade)
        g = int(color[1] * shade)
        b = int(color[2] * shade)
        pygame.draw.polygon(screen, (r,g,b), pts)

def cast_ray():
    cos_y = math.cos(yaw)
    sin_y = math.sin(yaw)
    cos_p = math.cos(pitch)
    sin_p = math.sin(pitch)

    dir_x = sin_y * cos_p
    dir_y = -sin_p
    dir_z = cos_y * cos_p

    x = player_x
    y = player_y + 0.9   # eye height
    z = player_z
    step = 0.05
    max_dist = 8.0
    for _ in range(int(max_dist/step)):
        x += dir_x*step
        y += dir_y*step
        z += dir_z*step
        bx, by, bz = int(x), int(y), int(z)
        if 0 <= bx < WORLD_WIDTH and 0 <= by < WORLD_HEIGHT and 0 <= bz < WORLD_DEPTH:
            if world[bx][bz][by][0] != 'air':
                return bx, by, bz, dir_x, dir_y, dir_z
    return None

def draw_hotbar(surface):
    panel_w = 280
    panel_h = 55
    panel_x = (WIDTH - panel_w) // 2
    panel_y = HEIGHT - panel_h - 10
    panel = pygame.Surface((panel_w, panel_h), pygame.SRCALPHA)
    panel.fill((0,0,0,180))
    surface.blit(panel, (panel_x, panel_y))

    slot_w = 50
    slot_h = 50
    gap = 5
    for i, block in enumerate(HOTBAR):
        slot_x = panel_x + 5 + i*(slot_w+gap)
        slot_y = panel_y + 2
        rect = pygame.Rect(slot_x, slot_y, slot_w, slot_h)
        if i == selected_slot:
            pygame.draw.rect(surface, (255,255,0), rect, 3)
        else:
            pygame.draw.rect(surface, (200,200,200), rect, 1)
        info = BLOCK_TYPES[block]
        col = info['top']
        pygame.draw.rect(surface, col, (slot_x+8, slot_y+8, slot_w-16, slot_h-16))
        pygame.draw.rect(surface, BLACK, (slot_x+8, slot_y+8, slot_w-16, slot_h-16), 1)
        font = pygame.font.Font(None, 18)
        text = font.render(str(i+1), True, WHITE)
        surface.blit(text, (slot_x+2, slot_y+2))

def draw_sky(surface):
    for y in range(HEIGHT):
        t = y / HEIGHT
        r = int(SKY_TOP[0] + (SKY_BOTTOM[0]-SKY_TOP[0])*t)
        g = int(SKY_TOP[1] + (SKY_BOTTOM[1]-SKY_TOP[1])*t)
        b = int(SKY_TOP[2] + (SKY_BOTTOM[2]-SKY_TOP[2])*t)
        pygame.draw.line(surface, (r,g,b), (0,y), (WIDTH,y))
    for cx, cy in [(100,80), (400,50), (650,90)]:
        pygame.draw.ellipse(surface, WHITE, (cx, cy, 80, 30))
        pygame.draw.ellipse(surface, WHITE, (cx+20, cy-10, 60, 25))

def main():
    global player_x, player_y, player_z, yaw, pitch, VEL_Y, selected_slot

    pygame.mouse.set_visible(False)
    pygame.event.set_grab(True)

    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_ESCAPE:
                    running = False
                elif event.key == pygame.K_SPACE:
                    # Only jump if on ground
                    if check_collision(player_x, player_y - 0.01, player_z):
                        VEL_Y = JUMP_SPEED
                elif pygame.K_1 <= event.key <= pygame.K_5:
                    selected_slot = event.key - pygame.K_1
            elif event.type == pygame.MOUSEBUTTONDOWN:
                if event.button == 1:
                    hit = cast_ray()
                    if hit:
                        bx,by,bz,_,_,_ = hit
                        if 0 <= bx < WORLD_WIDTH and 0 <= by < WORLD_HEIGHT and 0 <= bz < WORLD_DEPTH:
                            world[bx][bz][by] = ('air',0)
                elif event.button == 3:
                    hit = cast_ray()
                    if hit:
                        bx,by,bz,dx,dy,dz = hit
                        ax,ay,az = abs(dx),abs(dy),abs(dz)
                        nx=ny=nz=0
                        if ax >= ay and ax >= az:
                            nx = 1 if dx>0 else -1
                        elif ay >= ax and ay >= az:
                            ny = 1 if dy>0 else -1
                        else:
                            nz = 1 if dz>0 else -1
                        px,py,pz = bx+nx, by+ny, bz+nz
                        if 0 <= px < WORLD_WIDTH and 0 <= py < WORLD_HEIGHT and 0 <= pz < WORLD_DEPTH:
                            if world[px][pz][py][0] == 'air':
                                world[px][pz][py] = (HOTBAR[selected_slot], 0)

        dx,dy = pygame.mouse.get_rel()
        yaw += dx * MOUSE_SENS
        pitch += dy * MOUSE_SENS
        pitch = max(-math.pi/2+0.01, min(math.pi/2-0.01, pitch))

        keys = pygame.key.get_pressed()
        forward = 0
        strafe = 0
        if keys[pygame.K_w]: forward += 1
        if keys[pygame.K_s]: forward -= 1
        if keys[pygame.K_a]: strafe -= 1
        if keys[pygame.K_d]: strafe += 1

        move_x = (math.sin(yaw) * forward + math.cos(yaw) * strafe) * MOVE_SPEED
        move_z = (math.cos(yaw) * forward - math.sin(yaw) * strafe) * MOVE_SPEED

        # Apply gravity
        VEL_Y += GRAVITY
        if VEL_Y < -0.5: VEL_Y = -0.5   # terminal velocity

        # Move with collision
        move_with_collision(move_x, VEL_Y, move_z)

        # Clamp world boundaries
        player_x = max(0.5, min(WORLD_WIDTH-0.5, player_x))
        player_z = max(0.5, min(WORLD_DEPTH-0.5, player_z))
        player_y = max(0.0, min(WORLD_HEIGHT-0.1, player_y))

        # Draw
        draw_sky(screen)

        rd = 15
        for x in range(max(0,int(player_x)-rd), min(WORLD_WIDTH,int(player_x)+rd+1)):
            for z in range(max(0,int(player_z)-rd), min(WORLD_DEPTH,int(player_z)+rd+1)):
                for y in range(WORLD_HEIGHT):
                    btype = world[x][z][y][0]
                    if btype != 'air':
                        draw_block(screen, x, y, z, btype)

        pygame.draw.line(screen, WHITE, (WIDTH//2-10, HEIGHT//2), (WIDTH//2+10, HEIGHT//2), 2)
        pygame.draw.line(screen, WHITE, (WIDTH//2, HEIGHT//2-10), (WIDTH//2, HEIGHT//2+10), 2)

        draw_hotbar(screen)

        font = pygame.font.Font(None, 24)
        pos_text = font.render(f"Pos: ({player_x:.1f}, {player_y:.1f}, {player_z:.1f})", True, BLACK)
        screen.blit(pos_text, (10, 10))

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

    pygame.quit()
    sys.exit()

if __name__ == "__main__":
    main()