import pygame
import sys
import math

# Initialize Pygame
pygame.init()

# Fixed window dimensions
WINDOW_WIDTH, WINDOW_HEIGHT = 1300, 800
SIMULATION_HEIGHT = 600  # Top part for simulation
UI_HEIGHT = WINDOW_HEIGHT - SIMULATION_HEIGHT  # Bottom part for UI
PADDING = 20  # Padding inside UI boxes

# Colors
LIGHT_GRAY = (211, 211, 211)
DARK_GRAY = (169, 169, 169)
RED = (255, 0, 0)
BLUE = (0, 0, 255)
GREEN = (0, 255, 0)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)

# Screen setup
screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT))
pygame.display.set_caption("Ball Trajectory Simulation Game")

# Clock
clock = pygame.time.Clock()
FPS = 60

# Fonts
font = pygame.font.SysFont(None, 24)
large_font = pygame.font.SysFont(None, 36)

# Physics constants
GRAVITY = 650  # units per second squared

# Ball properties
BALL_RADIUS = 96.3881  # units
BALL_COLOR = RED

# Wall properties
WALL_THICKNESS = 192.7762  # Double the BALL_RADIUS for full thickness
WALL_COLOR = BLACK  # For better visibility
WALL_POSITION_Y = 1474  # units (positive y)

# Boundaries
LEFT_BOUNDARY = -3581
RIGHT_BOUNDARY = 3581
GROUND_LEVEL = 0
CEILING_LEVEL = 1820

# Basket Y-coordinate (for prediction)
BASKET_Y = 1474  # units

# Conversion factor from units to pixels (fixed scaling)
SCALE_X = (WINDOW_WIDTH - 2 * PADDING - WALL_THICKNESS) / (RIGHT_BOUNDARY - LEFT_BOUNDARY)
SCALE_Z = SIMULATION_HEIGHT / (CEILING_LEVEL - GROUND_LEVEL)
SCALE = min(SCALE_X, SCALE_Z)  # Uniform scaling for both axes

# Conversion functions
def units_to_pixels_y(y_units):
    # Maps simulation y_units to screen x_pixels
    return PADDING + WALL_THICKNESS + (y_units - LEFT_BOUNDARY) * SCALE

def units_to_pixels_z(z_units):
    # Maps simulation z_units to screen y_pixels
    return PADDING + SIMULATION_HEIGHT - (z_units - GROUND_LEVEL) * SCALE

def pixels_to_units_y(y_pixels):
    # Maps screen x_pixels back to simulation y_units
    return LEFT_BOUNDARY + (y_pixels - PADDING - WALL_THICKNESS) / SCALE

def pixels_to_units_z(z_pixels):
    # Maps screen y_pixels back to simulation z_units
    return GROUND_LEVEL + (SIMULATION_HEIGHT + PADDING - z_pixels) / SCALE

# Classes
class Slider:
    def __init__(self, x, y, w, h, min_val, max_val, default, label):
        self.rect = pygame.Rect(x, y, w, h)
        self.min_val = min_val
        self.max_val = max_val
        self.value = default
        self.grabbed = False
        self.label = label
        self.update_slider_pos()

    def update_slider_pos(self):
        # Position of the slider handle
        self.handle_x = self.rect.x + (self.value - self.min_val) / (self.max_val - self.min_val) * self.rect.width

    def draw(self, surface, launch_angle):
        # Draw slider line
        pygame.draw.line(surface, BLACK, (self.rect.x, self.rect.centery),
                         (self.rect.x + self.rect.width, self.rect.centery), 2)
        # Draw slider handle
        pygame.draw.circle(surface, BLACK, (int(self.handle_x), self.rect.centery), self.rect.height // 2)
        # Draw label with y and z components for Initial Speed
        if self.label == "Initial Speed":
            angle_rad = math.radians(launch_angle)
            vy = self.value * math.cos(angle_rad)
            vz = self.value * math.sin(angle_rad)
            label_text = f"{self.label}: {round(self.value, 2)} ({round(vy,2)}, {round(vz,2)})"
        else:
            label_text = f"{self.label}: {round(self.value, 2)}"
        label_surface = font.render(label_text, True, BLACK)
        surface.blit(label_surface, (self.rect.x, self.rect.y - 25))

    def update(self, mouse_pos):
        if self.grabbed:
            # Update handle position based on mouse
            self.handle_x = max(self.rect.x, min(mouse_pos[0], self.rect.x + self.rect.width))
            # Update value based on handle position
            self.value = self.min_val + (self.handle_x - self.rect.x) / self.rect.width * (self.max_val - self.min_val)

    def handle_event(self, event):
        if event.type == pygame.MOUSEBUTTONDOWN:
            # Check if the handle is clicked
            handle_rect = pygame.Rect(self.handle_x - self.rect.height//2, self.rect.y,
                                      self.rect.height, self.rect.height)
            if handle_rect.collidepoint(event.pos):
                self.grabbed = True
        elif event.type == pygame.MOUSEBUTTONUP:
            self.grabbed = False

class Button:
    def __init__(self, x, y, w, h, label, action):
        self.rect = pygame.Rect(x, y, w, h)
        self.label = label
        self.action = action

    def draw(self, surface):
        pygame.draw.rect(surface, DARK_GRAY, self.rect)
        label_surface = font.render(self.label, True, WHITE)
        surface.blit(label_surface, (self.rect.x + (self.rect.width - label_surface.get_width())//2,
                                     self.rect.y + (self.rect.height - label_surface.get_height())//2))

    def handle_event(self, event):
        if event.type == pygame.MOUSEBUTTONDOWN:
            if self.rect.collidepoint(event.pos):
                self.action()

class Wall:
    def __init__(self, y_position, thickness, height):
        self.y_position = y_position
        self.thickness = thickness
        self.height = height
        self.left = y_position - thickness / 2
        self.right = y_position + thickness / 2

    def update(self, height):
        self.height = height

    def draw(self, surface):
        wall_top_y = units_to_pixels_z(self.height)
        wall_left_x = units_to_pixels_y(self.left)
        wall_right_x = units_to_pixels_y(self.right)
        wall_width = wall_right_x - wall_left_x
        wall_height_pixels = SIMULATION_HEIGHT - (self.height - GROUND_LEVEL) * SCALE
        pygame.draw.rect(surface, WALL_COLOR, (wall_left_x, wall_top_y, wall_width, wall_height_pixels))

class Ball:
    def __init__(self):
        self.reset()
        self.dragging = False

    def reset(self):
        self.y = 0
        self.z = BALL_RADIUS
        self.vy = 0
        self.vz = 0
        self.in_motion = False
        self.initial_y = None
        self.initial_z = None
        self.initial_vy = 0
        self.initial_vz = 0

    def update(self, dt):
        if self.in_motion:
            # Update velocities
            self.vz -= GRAVITY * dt
            # Update positions
            self.y += self.vy * dt
            self.z += self.vz * dt

            # Check for collisions with boundaries
            # Left boundary
            if self.y - BALL_RADIUS < LEFT_BOUNDARY:
                self.y = LEFT_BOUNDARY + BALL_RADIUS
                self.vy *= -1
            # Right boundary
            if self.y + BALL_RADIUS > RIGHT_BOUNDARY:
                self.y = RIGHT_BOUNDARY - BALL_RADIUS
                self.vy *= -1
            # Ceiling collision
            if self.z + BALL_RADIUS > CEILING_LEVEL:
                self.z = CEILING_LEVEL - BALL_RADIUS
                self.vz *= -1
            # Floor collision
            if self.z - BALL_RADIUS < GROUND_LEVEL:
                self.z = GROUND_LEVEL + BALL_RADIUS
                self.in_motion = False
                self.vz = 0
                self.vy = 0
                self.initial_y = self.y  # Update initial position upon landing
                self.initial_z = self.z

            # Wall collisions (both walls)
            for wall in walls:
                if (wall.left - BALL_RADIUS < self.y < wall.right + BALL_RADIUS) and (self.z - BALL_RADIUS <= wall.height):
                    # Determine direction of collision
                    if self.vy > 0:
                        self.y = wall.left - BALL_RADIUS
                    else:
                        self.y = wall.right + BALL_RADIUS
                    self.vy *= -1

    def draw(self, surface):
        # Convert positions to pixels
        x = units_to_pixels_y(self.y)
        y = units_to_pixels_z(self.z)
        pygame.draw.circle(surface, BALL_COLOR, (int(x), int(y)), int(BALL_RADIUS * SCALE))

    def handle_event(self, event):
        if event.type == pygame.MOUSEBUTTONDOWN:
            if not self.in_motion:
                x = units_to_pixels_y(self.y)
                y = units_to_pixels_z(self.z)
                if (event.pos[0] - x)**2 + (event.pos[1] - y)**2 <= (BALL_RADIUS * SCALE)**2:
                    self.dragging = True
        elif event.type == pygame.MOUSEBUTTONUP:
            if self.dragging:
                self.dragging = False
                if self.initial_y is not None and self.initial_z is not None:
                    # Update initial velocity based on current speed
                    self.initial_vy = self.vy
                    self.initial_vz = self.vz
                update_prediction()
        elif event.type == pygame.MOUSEMOTION:
            if self.dragging:
                new_y_pixels, new_z_pixels = event.pos
                self.y = pixels_to_units_y(new_y_pixels)
                self.z = pixels_to_units_z(new_z_pixels)
                # Enforce constraints
                self.y = max(LEFT_BOUNDARY + BALL_RADIUS, min(RIGHT_BOUNDARY - BALL_RADIUS, self.y))
                self.z = max(GROUND_LEVEL + BALL_RADIUS, min(CEILING_LEVEL - BALL_RADIUS, self.z))
                # Prevent intersection with walls
                for wall in walls:
                    if (wall.left - BALL_RADIUS < self.y < wall.right + BALL_RADIUS) and (self.z - BALL_RADIUS <= wall.height):
                        if abs(self.y - wall.left) < abs(self.y - wall.right):
                            self.y = wall.left - BALL_RADIUS
                        else:
                            self.y = wall.right + BALL_RADIUS
                # Prevent intersection with floor and ceiling
                self.z = max(GROUND_LEVEL + BALL_RADIUS, min(CEILING_LEVEL - BALL_RADIUS, self.z))
                # Set initial position to current position
                self.initial_y = self.y
                self.initial_z = self.z
                # Update prediction dynamically
                update_prediction()

# Instantiate walls (positive and negative Y)
walls = []
positive_wall = Wall(WALL_POSITION_Y, WALL_THICKNESS, 400)
negative_wall = Wall(-WALL_POSITION_Y, WALL_THICKNESS, 400)
walls.append(positive_wall)
walls.append(negative_wall)

# Instantiate sliders
INITIAL_SPEED_SLIDER = Slider(WALL_THICKNESS + 3 * PADDING, SIMULATION_HEIGHT + 20, 300, 20, 0, 6000, 1000, "Initial Speed")
LAUNCH_ANGLE_SLIDER = Slider(WALL_THICKNESS + 3 * PADDING, SIMULATION_HEIGHT + 60, 300, 20, -180, 180, 45, "Launch Angle")
WALL_HEIGHT_SLIDER = Slider(WALL_THICKNESS + 3 * PADDING, SIMULATION_HEIGHT + 100, 300, 20, 0, CEILING_LEVEL, 400, "Wall Height")

# Instantiate buttons
BUTTON_WIDTH, BUTTON_HEIGHT = 120, 40
BUTTON_X_CENTER = (WINDOW_WIDTH - WALL_THICKNESS - 6 * PADDING) // 2
BUTTON_Y_START = SIMULATION_HEIGHT + 160
BUTTON_SPACING = 50

def launch_ball_action():
    global prediction_available, prediction_success
    if ball.in_motion:
        # Stop current motion
        ball.in_motion = False
    # Set initial position
    ball.initial_y = ball.y
    ball.initial_z = ball.z
    # Set initial velocities based on sliders
    angle_rad = math.radians(LAUNCH_ANGLE_SLIDER.value)
    speed = INITIAL_SPEED_SLIDER.value
    ball.initial_vy = speed * math.cos(angle_rad)
    ball.initial_vz = speed * math.sin(angle_rad)
    ball.vy = ball.initial_vy
    ball.vz = ball.initial_vz
    ball.in_motion = True
    update_prediction()

def return_ball_action():
    global prediction_available, prediction_success
    if ball.initial_y is not None:
        # Return to initial position with zero velocity
        ball.y = ball.initial_y
        ball.z = ball.initial_z
        ball.vy = 0
        ball.vz = 0
        ball.in_motion = False
        update_prediction()

def reset_ball_action():
    ball.reset()
    # Reset sliders to default values
    INITIAL_SPEED_SLIDER.value = 1000
    INITIAL_SPEED_SLIDER.update_slider_pos()
    LAUNCH_ANGLE_SLIDER.value = 45
    LAUNCH_ANGLE_SLIDER.update_slider_pos()
    # Do not reset WALL_HEIGHT_SLIDER
    positive_wall.update(WALL_HEIGHT_SLIDER.value)
    negative_wall.update(WALL_HEIGHT_SLIDER.value)
    # Update prediction based on reset
    update_prediction()

def reset_wall_action():
    WALL_HEIGHT_SLIDER.value = 400
    WALL_HEIGHT_SLIDER.update_slider_pos()
    positive_wall.update(WALL_HEIGHT_SLIDER.value)
    negative_wall.update(WALL_HEIGHT_SLIDER.value)
    update_prediction()

# Instantiate buttons
launch_button = Button(BUTTON_X_CENTER - BUTTON_WIDTH - 20, BUTTON_Y_START, BUTTON_WIDTH, BUTTON_HEIGHT, "Launch", launch_ball_action)
return_button = Button(BUTTON_X_CENTER + 20, BUTTON_Y_START, BUTTON_WIDTH, BUTTON_HEIGHT, "Return", return_ball_action)
reset_ball_button = Button(BUTTON_X_CENTER - BUTTON_WIDTH - 20, BUTTON_Y_START + BUTTON_HEIGHT + 10, BUTTON_WIDTH, BUTTON_HEIGHT, "Reset Ball", reset_ball_action)
reset_wall_button = Button(BUTTON_X_CENTER + 20, BUTTON_Y_START + BUTTON_HEIGHT + 10, BUTTON_WIDTH, BUTTON_HEIGHT, "Reset Wall", reset_wall_action)

# Instantiate ball
ball = Ball()

# Prediction variables
prediction = []
prediction_available = False
prediction_success = False

def predict_trajectory():
    global prediction, prediction_success, prediction_available
    traj = []
    temp_ball = Ball()
    # Use initial position if set
    if ball.initial_y is not None:
        temp_ball.y = ball.initial_y
        temp_ball.z = ball.initial_z
    else:
        temp_ball.y = ball.y
        temp_ball.z = ball.z
    # Set initial velocities based on sliders
    angle_rad = math.radians(LAUNCH_ANGLE_SLIDER.value)
    speed = INITIAL_SPEED_SLIDER.value
    temp_ball.vy = speed * math.cos(angle_rad)
    temp_ball.vz = speed * math.sin(angle_rad)
    dt = 0.01
    while temp_ball.z - BALL_RADIUS >= GROUND_LEVEL:
        # Update velocities
        temp_ball.vz -= GRAVITY * dt
        # Update positions
        temp_ball.y += temp_ball.vy * dt
        temp_ball.z += temp_ball.vz * dt

        # Check for collisions with boundaries
        if temp_ball.y - BALL_RADIUS < LEFT_BOUNDARY:
            temp_ball.y = LEFT_BOUNDARY + BALL_RADIUS
            temp_ball.vy *= -1
        if temp_ball.y + BALL_RADIUS > RIGHT_BOUNDARY:
            temp_ball.y = RIGHT_BOUNDARY - BALL_RADIUS
            temp_ball.vy *= -1
        # Ceiling collision
        if temp_ball.z + BALL_RADIUS > CEILING_LEVEL:
            temp_ball.z = CEILING_LEVEL - BALL_RADIUS
            temp_ball.vz *= -1
        # Wall collisions (both walls)
        for wall in walls:
            if (wall.left - BALL_RADIUS < temp_ball.y < wall.right + BALL_RADIUS) and (temp_ball.z - BALL_RADIUS <= wall.height):
                # Determine direction of collision
                if temp_ball.vy > 0:
                    temp_ball.y = wall.left - BALL_RADIUS
                else:
                    temp_ball.y = wall.right + BALL_RADIUS
                temp_ball.vy *= -1
        traj.append((temp_ball.y, temp_ball.z))

    # Determine if the ball lands in the basket
    if temp_ball.y > BASKET_Y:
        prediction_success = True
    else:
        prediction_success = False

    prediction = traj
    prediction_available = True

def update_prediction():
    predict_trajectory()

# Initial Prediction
update_prediction()

# Function to draw graph markings
def draw_graph_markings(surface):
    # Y-axis markings
    y_markings = [-3000, -2000, -1000, 0, 1000, 2000, 3000]
    for y_val in y_markings:
        x = units_to_pixels_y(y_val)
        pygame.draw.line(surface, BLACK, (x, SIMULATION_HEIGHT), (x, SIMULATION_HEIGHT - 10), 2)  # Small tick
        label = font.render(str(y_val), True, BLACK)
        surface.blit(label, (x - label.get_width() // 2, SIMULATION_HEIGHT + 5))
    # Wall Y-coordinate markings
    wall_x = units_to_pixels_y(WALL_POSITION_Y)
    pygame.draw.line(surface, BLACK, (wall_x, SIMULATION_HEIGHT), (wall_x, SIMULATION_HEIGHT - 10), 2)
    label_wall = font.render(str(WALL_POSITION_Y), True, BLACK)
    surface.blit(label_wall, (wall_x - label_wall.get_width() // 2, SIMULATION_HEIGHT + 5))
    # Mirrored Wall
    wall_x_neg = units_to_pixels_y(-WALL_POSITION_Y)
    pygame.draw.line(surface, BLACK, (wall_x_neg, SIMULATION_HEIGHT), (wall_x_neg, SIMULATION_HEIGHT - 10), 2)
    label_wall_neg = font.render(str(-WALL_POSITION_Y), True, BLACK)
    surface.blit(label_wall_neg, (wall_x_neg - label_wall_neg.get_width() // 2, SIMULATION_HEIGHT + 5))

    # Z-axis markings
    z_markings = [0, 250, 500, 750, 1000, 1250, 1500, 1750, 1820]
    for z_val in z_markings:
        y = units_to_pixels_z(z_val)
        pygame.draw.line(surface, BLACK, (PADDING - 10, y), (PADDING, y), 2)  # Small tick
        label = font.render(str(z_val), True, BLACK)
        surface.blit(label, (PADDING - label.get_width() - 15, y - label.get_height() // 2))

# Function to draw grid lines (graph pattern)
def draw_grid(surface):
    # Draw horizontal lines (z-axis)
    for z in range(GROUND_LEVEL, CEILING_LEVEL + 1, 250):
        y = units_to_pixels_z(z)
        pygame.draw.line(surface, LIGHT_GRAY, (PADDING, y), (units_to_pixels_y(RIGHT_BOUNDARY), y), 1)
    # Draw vertical lines (y-axis)
    for y in range(LEFT_BOUNDARY, RIGHT_BOUNDARY + 1, 1000):
        x = units_to_pixels_y(y)
        pygame.draw.line(surface, LIGHT_GRAY, (x, 0), (x, SIMULATION_HEIGHT), 1)

# Main loop
run = True
while run:
    dt = clock.tick(FPS) / 1000  # Delta time in seconds

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
        # Handle slider and button events
        INITIAL_SPEED_SLIDER.handle_event(event)
        LAUNCH_ANGLE_SLIDER.handle_event(event)
        WALL_HEIGHT_SLIDER.handle_event(event)
        launch_button.handle_event(event)
        return_button.handle_event(event)
        reset_ball_button.handle_event(event)
        reset_wall_button.handle_event(event)
        ball.handle_event(event)

    mouse_pos = pygame.mouse.get_pos()
    # Update sliders if grabbed
    INITIAL_SPEED_SLIDER.update(mouse_pos)
    LAUNCH_ANGLE_SLIDER.update(mouse_pos)
    WALL_HEIGHT_SLIDER.update(mouse_pos)

    # Update walls based on wall height slider
    positive_wall.update(WALL_HEIGHT_SLIDER.value)
    negative_wall.update(WALL_HEIGHT_SLIDER.value)

    # If sliders are being adjusted or ball position is changed, update initial position and prediction
    if INITIAL_SPEED_SLIDER.grabbed or LAUNCH_ANGLE_SLIDER.grabbed or ball.initial_y is not None:
        update_prediction()

    # If wall height slider changed while ball is moving, recalculated prediction
    if WALL_HEIGHT_SLIDER.grabbed and ball.in_motion:
        update_prediction()

    # Update ball physics
    ball.update(dt)

    # Drawing
    screen.fill(LIGHT_GRAY)

    # Draw simulation area background (white)
    pygame.draw.rect(screen, WHITE, (0, 0, WINDOW_WIDTH, SIMULATION_HEIGHT))

    # Draw grid lines (graph pattern)
    draw_grid(screen)

    # Draw graph markings
    draw_graph_markings(screen)

    # Draw walls
    for wall in walls:
        wall.draw(screen)

    # Draw ball
    ball.draw(screen)

    # Draw trajectory prediction
    if prediction_available and len(prediction) >= 2:
        traj_points = [(units_to_pixels_y(pos[0]), units_to_pixels_z(pos[1])) for pos in prediction]
        pygame.draw.lines(screen, BLUE, False, traj_points, 3)  # Increased thickness and changed color

    # Draw initial position marker
    if ball.initial_y is not None:
        init_x = units_to_pixels_y(ball.initial_y)
        init_z = units_to_pixels_z(ball.initial_z)
        pygame.draw.circle(screen, GREEN, (int(init_x), int(init_z)), 5)

    # Draw UI Area Background
    pygame.draw.rect(screen, LIGHT_GRAY, (0, SIMULATION_HEIGHT, WINDOW_WIDTH, UI_HEIGHT))

    # Draw UI Boxes with padding
    # Slider Boxes
    slider_box_width = 320
    slider_box_height = 60
    pygame.draw.rect(screen, WHITE, (WALL_THICKNESS + 2 * PADDING, SIMULATION_HEIGHT + 10, slider_box_width, slider_box_height))
    pygame.draw.rect(screen, WHITE, (WALL_THICKNESS + 2 * PADDING, SIMULATION_HEIGHT + 80, slider_box_width, slider_box_height))
    pygame.draw.rect(screen, WHITE, (WALL_THICKNESS + 2 * PADDING, SIMULATION_HEIGHT + 150, slider_box_width, slider_box_height))
    
    # Buttons Boxes
    button_box_width = 140
    button_box_height = 50
    pygame.draw.rect(screen, WHITE, (WINDOW_WIDTH//2 - button_box_width//2, SIMULATION_HEIGHT + 10, button_box_width, 4 * (button_box_height + 10)))
    
    # Text/UI Boxes
    text_box_width = 400
    text_box_height = 200
    pygame.draw.rect(screen, WHITE, (WINDOW_WIDTH - text_box_width - 2 * PADDING, SIMULATION_HEIGHT + 10, text_box_width, text_box_height))
    
    # Draw sliders
    INITIAL_SPEED_SLIDER.draw(screen, LAUNCH_ANGLE_SLIDER.value)
    LAUNCH_ANGLE_SLIDER.draw(screen, LAUNCH_ANGLE_SLIDER.value)
    WALL_HEIGHT_SLIDER.draw(screen, LAUNCH_ANGLE_SLIDER.value)

    # Draw buttons
    launch_button.draw(screen)
    return_button.draw(screen)
    reset_ball_button.draw(screen)
    reset_wall_button.draw(screen)

    # Draw info texts on the right
    info_x = WINDOW_WIDTH - text_box_width - PADDING + 10
    info_y = SIMULATION_HEIGHT + 20
    spacing = 30

    # Question: Will it enter the net?
    question_text = large_font.render("Will it enter the net?", True, BLACK)
    screen.blit(question_text, (info_x, info_y))

    # Prediction Result
    if prediction_available:
        result_color = GREEN if prediction_success else RED
        result_text = large_font.render("YES" if prediction_success else "NO", True, result_color)
    else:
        result_text = large_font.render("N/A", True, BLACK)
    screen.blit(result_text, (info_x, info_y + spacing))

    # Initial Position Coordinates
    if ball.initial_y is not None and ball.initial_z is not None:
        initial_position_text = font.render(f"Initial Position: (y: {round(ball.initial_y, 2)}, z: {round(ball.initial_z, 2)})", True, BLACK)
    else:
        initial_position_text = font.render(f"Initial Position: (N/A)", True, BLACK)
    screen.blit(initial_position_text, (info_x, info_y + 2*spacing))

    # Current Position Coordinates
    current_position_text = font.render(f"Current Position: (y: {round(ball.y, 2)}, z: {round(ball.z, 2)})", True, BLACK)
    screen.blit(current_position_text, (info_x, info_y + 3*spacing))

    # Current Speed (Total Velocity)
    current_speed = math.sqrt(ball.vy ** 2 + ball.vz ** 2)
    current_speed_text = font.render(f"Current Speed: {round(current_speed, 2)} units/s", True, BLACK)
    screen.blit(current_speed_text, (info_x, info_y + 4*spacing))

    # Current Velocity Components
    velocity_components = font.render(f"Vy: {round(ball.vy, 2)} units/s, Vz: {round(ball.vz, 2)} units/s", True, BLACK)
    screen.blit(velocity_components, (info_x, info_y + 5*spacing))

    # Update the display
    pygame.display.flip()

# Quit Pygame
pygame.quit()
sys.exit()

Provide the complete fixed code when you're done fixing what I tell you so I can copy and paste. Fix the UI so that all the text and sliders and buttons are above the game / simulation area. Make sure the wall boundaries, ground and ceiling are clearly drawn in black. Stack all the ui text and elements vertically so they can't intersect. The sliders can be horizontally next to each other on the same vertical level. The buttons can also be horizontally next to each other on the same vertical level. make sure there is enough gab between the ui and the game area.