import pygame
import sys
import math

# -------------------------------
# Pygame and Global Setup
# -------------------------------
pygame.init()
screen_width, screen_height = 1000, 600
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption("Mars Rover Suspension Simulation")

clock = pygame.time.Clock()

# -------------------------------
# Colors and Constants
# -------------------------------
WHITE     = (255, 255, 255)
BLACK     = (0, 0, 0)
GRAY      = (200, 200, 200)
DARKGRAY  = (50, 50, 50)
GREEN     = (80, 200, 80)
BLUE      = (100, 100, 255)

# Ground control point spacing and scrolling speed.
SEGMENT_SPACING = 1   # Horizontal spacing between control points
SCROLL_SPEED = 1        # Pixels per frame

# -------------------------------
# Ground: Dynamic "Plot"
# -------------------------------
# We keep a list of control points in screen coordinates.
ground_points = []
# Initialize control points so the ground covers a bit more than the screen width.
num_points = screen_width // SEGMENT_SPACING + 3
start_x = -SEGMENT_SPACING
for i in range(num_points):
    # For the initial ground, set a default height (for example, 450)
    ground_points.append([start_x + i * SEGMENT_SPACING, 450])

def update_ground(mouse_y):
    """
    Update ground control points:
      - Shift all points left by SCROLL_SPEED.
      - If the leftmost point is off-screen, remove it.
      - Append a new point at the right end with x determined by
        last point + SEGMENT_SPACING and y given by current mouse_y.
    """
    # Shift every point to the left.
    for pt in ground_points:
        pt[0] -= SCROLL_SPEED

    # Remove points that have moved off-screen.
    if ground_points[0][0] < -SEGMENT_SPACING:
        ground_points.pop(0)

    # Add a new control point at the right if needed.
    if ground_points[-1][0] < screen_width + SEGMENT_SPACING:
        new_x = ground_points[-1][0] + SEGMENT_SPACING
        # Use the mouse's y (clamped to a reasonable range) for the new control point.
        new_y = max(300, min(mouse_y, screen_height - 50))
        ground_points.append([new_x, new_y])

def get_ground_y(x):
    """
    Given an x coordinate (in screen space), interpolate the ground's
    y value using the control points.
    """
    # Find the segment that contains x.
    for i in range(len(ground_points) - 1):
        x0, y0 = ground_points[i]
        x1, y1 = ground_points[i+1]
        if x0 <= x <= x1:
            t = (x - x0) / (x1 - x0)
            return y0 * (1 - t) + y1 * t
    # Fallback: if x is beyond our control points.
    return ground_points[-1][1]

def draw_ground(surface):
    """Draw the ground as a polyline connecting all control points,
    then fill below."""
    pts = [(pt[0], pt[1]) for pt in ground_points if -SEGMENT_SPACING <= pt[0] <= screen_width + SEGMENT_SPACING]
    if len(pts) >= 2:
        pygame.draw.lines(surface, GREEN, False, pts, 4)
        # Fill ground below the line.
        fill_points = pts.copy()
        fill_points.append((pts[-1][0], screen_height))
        fill_points.append((pts[0][0], screen_height))
        pygame.draw.polygon(surface, (200,255,200), fill_points)

# -------------------------------
# Rover Suspension Simulation
# -------------------------------
# The rover is fixed horizontally at the center.
rover_x = screen_width // 2
# Wheel horizontal offsets relative to rover center.
wheel_offsets = { 'left': -50, 'center': 0, 'right': 50 }
wheel_radius = 20

# Suspension parameters (vertical springs).
L_rest = 60         # Resting (ideal) suspension length.
k = 0.08            # Spring constant.
damping = 0.1       # Damping coefficient.
# For each wheel, we simulate the vertical position of the chassis attachment (the top of the suspension).
# We store for each wheel its current attachment y and vertical velocity.
suspension = {
    'left': 450 - L_rest,
    'center': 450 - L_rest,
    'right': 450 - L_rest
}
suspension_vel = {'left': 0, 'center': 0, 'right': 0}

def update_suspension():
    """
    Update suspension for each wheel.
    For a given wheel, the desired chassis attachment y is:
       ground_y(wheel_x) - L_rest
    We then update the current attachment using a spring–damper simulation.
    """
    for key, offset in wheel_offsets.items():
        # World x coordinate for the wheel.
        x_pos = rover_x + offset
        # The ground contact y at the wheel's x.
        ground_contact = get_ground_y(x_pos)
        desired_attach = ground_contact - L_rest
        # Current state.
        attach_y = suspension[key]
        vel = suspension_vel[key]
        # Compute spring force.
        force = k * (desired_attach - attach_y)
        # Acceleration (assume mass = 1 for simplicity).
        acc = force - damping * vel
        # Update velocity and position.
        vel += acc
        attach_y += vel
        suspension[key] = attach_y
        suspension_vel[key] = vel

def draw_spring(surface, start, end, coils=6, amplitude=8):
    """
    Draws a zigzag spring between start and end.
    """
    sx, sy = start
    ex, ey = end
    total_dist = math.hypot(ex - sx, ey - sy)
    # Define number of segments.
    num_points = coils * 2 + 1
    points = []
    for i in range(num_points):
        t = i / (num_points - 1)
        # Linear interpolation for y.
        x = sx + t * (ex - sx)
        y = sy + t * (ey - sy)
        if i != 0 and i != num_points - 1:
            offset = amplitude if i % 2 == 0 else -amplitude
            # Offset perpendicular to the line (for vertical line, horizontal offset; here assume nearly vertical)
            x += offset
        points.append((x, y))
    pygame.draw.lines(surface, DARKGRAY, False, points, 3)

def draw_rover(surface):
    """
    Draw the rover.
      - Wheels: circles drawn at their contact points on the ground.
      - Suspension: springs from wheel contact to chassis attachment.
      - Chassis: drawn as a rectangle whose top edge connects the left and right attachments.
    """
    # Determine wheel positions.
    wheels = {}
    for key, offset in wheel_offsets.items():
        world_x = rover_x + offset
        world_y = get_ground_y(world_x)
        wheels[key] = (world_x, world_y)
        # Draw wheel.
        pygame.draw.circle(surface, DARKGRAY, (int(world_x), int(world_y)), wheel_radius)
        pygame.draw.circle(surface, BLACK, (int(world_x), int(world_y)), wheel_radius, 2)
    # Update suspension so that each wheel has its chassis attachment position.
    update_suspension()
    attachments = {}
    for key, offset in wheel_offsets.items():
        attach_x = rover_x + offset
        attach_y = suspension[key]
        attachments[key] = (attach_x, attach_y)
        # Draw spring from wheel contact to attachment.
        draw_spring(surface, wheels[key], attachments[key], coils=8, amplitude=6)
        # Draw the attachment point.
        pygame.draw.circle(surface, BLACK, (int(attach_x), int(attach_y)), 4)

    # Draw chassis.
    # For simplicity, use the left and right attachment points to define the top edge.
    left_attach = attachments['left']
    right_attach = attachments['right']
    # Compute the chassis center as the average of the three attachments.
    center_attach = (
        (attachments['left'][0] + attachments['center'][0] + attachments['right'][0]) / 3,
        (attachments['left'][1] + attachments['center'][1] + attachments['right'][1]) / 3,
    )
    # Chassis properties.
    chassis_height = 20
    # The top edge will be the line from left_attach to right_attach.
    # For a rough rectangle, offset the top edge downward by chassis_height to form the body.
    top_edge = [left_attach, right_attach]
    bottom_edge = [(left_attach[0], left_attach[1] + chassis_height),
                   (right_attach[0], right_attach[1] + chassis_height)]
    body_points = [left_attach, right_attach, bottom_edge[1], bottom_edge[0]]
    pygame.draw.polygon(surface, BLUE, body_points)
    pygame.draw.polygon(surface, BLACK, body_points, 2)

# -------------------------------
# Main Simulation Loop
# -------------------------------
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # The mouse vertical position controls the next ground segment's height.
    mouse_x, mouse_y = pygame.mouse.get_pos()

    # Update ground control points.
    update_ground(mouse_y)

    # Clear the screen.
    screen.fill(WHITE)
    
    # Draw the ground.
    draw_ground(screen)
    
    # Draw the rover (suspension, wheels, chassis).
    draw_rover(screen)
    
    pygame.display.flip()
    clock.tick(60)

pygame.quit()
sys.exit()