"""Showcase of a very basic 2d platformer

The red girl sprite is taken from Sithjester's RMXP Resources:
http://untamed.wild-refuge.net/rmxpresources.php?characters

.. note:: The code of this example is a bit messy. If you adapt this to your 
    own code you might want to structure it a bit differently.
"""

__docformat__ = "reStructuredText"

import math
import sys

import pygame

import pymunk
import pymunk.pygame_util
from pymunk.vec2d import Vec2d


def cpfclamp(f, min_, max_):
    """Clamp f between min and max"""
    return min(max(f, min_), max_)


def cpflerpconst(f1, f2, d):
    """Linearly interpolate from f1 to f2 by no more than d."""
    return f1 + cpfclamp(f2 - f1, -d, d)


width, height = 690, 400
fps = 60
dt = 1.0 / fps
PLAYER_VELOCITY = 100.0 * 2.0
PLAYER_GROUND_ACCEL_TIME = 0.05
PLAYER_GROUND_ACCEL = PLAYER_VELOCITY / PLAYER_GROUND_ACCEL_TIME

PLAYER_AIR_ACCEL_TIME = 0.25
PLAYER_AIR_ACCEL = PLAYER_VELOCITY / PLAYER_AIR_ACCEL_TIME

JUMP_HEIGHT = 16.0 * 3
JUMP_BOOST_HEIGHT = 24.0
JUMP_CUTOFF_VELOCITY = 100
FALL_VELOCITY = 250.0

JUMP_LENIENCY = 0.05

HEAD_FRICTION = 0.7

PLATFORM_SPEED = 1


def main():

    ### PyGame init
    pygame.init()
    screen = pygame.display.set_mode((width, height))

    clock = pygame.time.Clock()
    running = True
    font = pygame.font.SysFont("Arial", 16)
    sound = pygame.mixer.Sound("sfx.wav")
    img = pygame.image.load("xmasgirl1.png")

    ### Physics stuff
    space = pymunk.Space()
    space.gravity = Vec2d(0, -1000)
    pymunk.pygame_util.positive_y_is_up = True
    draw_options = pymunk.pygame_util.DrawOptions(screen)

    # box walls
    static = [
        pymunk.Segment(space.static_body, (10, 50), (300, 50), 3),
        pymunk.Segment(space.static_body, (300, 50), (325, 50), 3),
        pymunk.Segment(space.static_body, (325, 50), (350, 50), 3),
        pymunk.Segment(space.static_body, (350, 50), (375, 50), 3),
        pymunk.Segment(space.static_body, (375, 50), (680, 50), 3),
        pymunk.Segment(space.static_body, (680, 50), (680, 370), 3),
        pymunk.Segment(space.static_body, (680, 370), (10, 370), 3),
        pymunk.Segment(space.static_body, (10, 370), (10, 50), 3),
    ]
    static[1].color = pygame.Color("red")
    static[2].color = pygame.Color("green")
    static[3].color = pygame.Color("red")

    # rounded shape
    rounded = [
        pymunk.Segment(space.static_body, (500, 50), (520, 60), 3),
        pymunk.Segment(space.static_body, (520, 60), (540, 80), 3),
        pymunk.Segment(space.static_body, (540, 80), (550, 100), 3),
        pymunk.Segment(space.static_body, (550, 100), (550, 150), 3),
    ]

    # static platforms
    platforms = [
        pymunk.Segment(space.static_body, (170, 50), (270, 150), 3)
        # , pymunk.Segment(space.static_body, (270, 100), (300, 100), 5)
        ,
        pymunk.Segment(space.static_body, (400, 150), (450, 150), 3),
        pymunk.Segment(space.static_body, (400, 200), (450, 200), 3),
        pymunk.Segment(space.static_body, (220, 200), (300, 200), 3),
        pymunk.Segment(space.static_body, (50, 250), (200, 250), 3),
        pymunk.Segment(space.static_body, (10, 370), (50, 250), 3),
    ]

    for s in static + platforms + rounded:
        s.friction = 1.0
        s.group = 1
    space.add(*static, *platforms, *rounded)

    # moving platform
    platform_path = [(650, 100), (600, 200), (650, 300)]
    platform_path_index = 0
    platform_body = pymunk.Body(body_type=pymunk.Body.KINEMATIC)
    platform_body.position = 650, 100
    s = pymunk.Segment(platform_body, (-25, 0), (25, 0), 5)
    s.friction = 1.0
    s.group = 1
    s.color = pygame.Color("blue")
    space.add(platform_body, s)

    # pass through platform
    passthrough = pymunk.Segment(space.static_body, (270, 100), (320, 100), 5)
    passthrough.color = pygame.Color("yellow")
    passthrough.friction = 1.0
    passthrough.collision_type = 2
    passthrough.filter = pymunk.ShapeFilter(categories=0b1000)
    space.add(passthrough)

    def passthrough_handler(arbiter, space, data):
        if arbiter.shapes[0].body.velocity.y < 0:
            return True
        else:
            return False

    space.add_collision_handler(1, 2).begin = passthrough_handler

    # player
    body = pymunk.Body(5, float("inf"))
    body.position = 100, 100

    head = pymunk.Circle(body, 10, (0, 5))
    head2 = pymunk.Circle(body, 10, (0, 13))
    feet = pymunk.Circle(body, 10, (0, -5))
    # Since we use the debug draw we need to hide these circles. To make it
    # easy we just set their color to black.
    feet.color = 0, 0, 0, 0
    head.color = 0, 0, 0, 0
    head2.color = 0, 0, 0, 0
    mask = pymunk.ShapeFilter.ALL_MASKS() ^ passthrough.filter.categories
    sf = pymunk.ShapeFilter(mask=mask)
    head.filter = sf
    head2.filter = sf
    feet.collision_type = 1
    feet.ignore_draw = head.ignore_draw = head2.ignore_draw = True

    space.add(body, head, feet, head2)
    direction = 1
    remaining_jumps = 2
    landing = {"p": Vec2d.zero(), "n": 0}
    frame_number = 0

    landed_previous = False

    while running:

        grounding = {
            "normal": Vec2d.zero(),
            "penetration": Vec2d.zero(),
            "impulse": Vec2d.zero(),
            "position": Vec2d.zero(),
            "body": None,
        }
        # find out if player is standing on ground

        def f(arbiter):
            n = -arbiter.contact_point_set.normal
            if n.y > grounding["normal"].y:
                grounding["normal"] = n
                grounding["penetration"] = -arbiter.contact_point_set.points[0].distance
                grounding["body"] = arbiter.shapes[1].body
                grounding["impulse"] = arbiter.total_impulse
                grounding["position"] = arbiter.contact_point_set.points[0].point_b

        body.each_arbiter(f)

        well_grounded = False
        if (
            grounding["body"] != None
            and abs(grounding["normal"].x / grounding["normal"].y) < feet.friction
        ):
            well_grounded = True
            remaining_jumps = 2

        ground_velocity = Vec2d.zero()
        if well_grounded:
            ground_velocity = grounding["body"].velocity

        for event in pygame.event.get():
            if (
                event.type == pygame.QUIT
                or event.type == pygame.KEYDOWN
                and (event.key in [pygame.K_ESCAPE, pygame.K_q])
            ):
                running = False
            elif event.type == pygame.KEYDOWN and event.key == pygame.K_p:
                pygame.image.save(screen, "platformer.png")

            elif event.type == pygame.KEYDOWN and event.key == pygame.K_UP:
                if well_grounded or remaining_jumps > 0:
                    jump_v = math.sqrt(2.0 * JUMP_HEIGHT * abs(space.gravity.y))
                    impulse = (0, body.mass * (ground_velocity.y + jump_v))
                    body.apply_impulse_at_local_point(impulse)
                    remaining_jumps -= 1
            elif event.type == pygame.KEYUP and event.key == pygame.K_UP:
                body.velocity = body.velocity.x, min(
                    body.velocity.y, JUMP_CUTOFF_VELOCITY
                )

        # Target horizontal velocity of player
        target_vx = 0

        if body.velocity.x > 0.01:
            direction = 1
        elif body.velocity.x < -0.01:
            direction = -1

        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            direction = -1
            target_vx -= PLAYER_VELOCITY
        if keys[pygame.K_RIGHT]:
            direction = 1
            target_vx += PLAYER_VELOCITY
        if keys[pygame.K_DOWN]:
            direction = -3

        feet.surface_velocity = -target_vx, 0

        if grounding["body"] != None:
            feet.friction = -PLAYER_GROUND_ACCEL / space.gravity.y
            head.friction = HEAD_FRICTION
        else:
            feet.friction, head.friction = 0, 0

        # Air control
        if grounding["body"] == None:
            body.velocity = Vec2d(
                cpflerpconst(
                    body.velocity.x,
                    target_vx + ground_velocity.x,
                    PLAYER_AIR_ACCEL * dt,
                ),
                body.velocity.y,
            )

        body.velocity = body.velocity.x, max(
            body.velocity.y, -FALL_VELOCITY
        )  # clamp upwards as well?

        # Move the moving platform
        destination = platform_path[platform_path_index]
        current = Vec2d(*platform_body.position)
        distance = current.get_distance(destination)
        if distance < PLATFORM_SPEED:
            platform_path_index += 1
            platform_path_index = platform_path_index % len(platform_path)
            t = 1
        else:
            t = PLATFORM_SPEED / distance
        new = current.interpolate_to(destination, t)
        platform_body.position = new
        platform_body.velocity = (new - current) / dt

        ### Clear screen
        screen.fill(pygame.Color("black"))

        ### Helper lines
        for y in [50, 100, 150, 200, 250, 300]:
            color = pygame.Color("green")
            pygame.draw.line(screen, color, (10, y), (680, y), 1)

        ### Draw stuff
        space.debug_draw(draw_options)

        direction_offset = 48 + (1 * direction + 1) // 2 * 48
        if grounding["body"] != None and abs(target_vx) > 1:
            animation_offset = 32 * (frame_number // 8 % 4)
        elif grounding["body"] is None:
            animation_offset = 32 * 1
        else:
            animation_offset = 32 * 0
        position = body.position + (-16, 28)
        p = pymunk.pygame_util.to_pygame(position, screen)
        screen.blit(img, p, (animation_offset, direction_offset, 32, 48))

        # Did we land?
        if abs(grounding["impulse"].y) / body.mass > 200 and not landed_previous:
            sound.play()
            landing = {"p": grounding["position"], "n": 5}
            landed_previous = True
        else:
            landed_previous = False
        if landing["n"] > 0:
            p = pymunk.pygame_util.to_pygame(landing["p"], screen)
            pygame.draw.circle(screen, pygame.Color("yellow"), p, 5)
            landing["n"] -= 1

        # Info and flip screen
        screen.blit(
            font.render("fps: " + str(clock.get_fps()), 1, pygame.Color("white")),
            (0, 0),
        )
        screen.blit(
            font.render(
                "Move with Left/Right, jump with Up, press again to double jump",
                1,
                pygame.Color("darkgrey"),
            ),
            (5, height - 35),
        )
        screen.blit(
            font.render("Press ESC or Q to quit", 1, pygame.Color("darkgrey")),
            (5, height - 20),
        )

        pygame.display.flip()
        frame_number += 1

        ### Update physics

        space.step(dt)

        clock.tick(fps)


if __name__ == "__main__":
    sys.exit(main())
