"""
Example using Pymunk physics engine

@author: Paul Vincent Craven
@author: Robert Duvall (added classes, simplified code)

Artwork from https://kenney.nl
"""
import arcade
import pymunk
import random
import math

# Choose game window size and a name for your game to appear in the title bar
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 800
GAME_NAME = 'Pymunk Pegboard Example'
# Choose number and placement of bumpers in the game
BUMPER_ROWS = 5
BUMPER_COLUMNS = 5
BUMPER_SIZE = 20
BUMPER_SPACING = 150
BUMPER_IMAGE = ':resources:images/pinball/bumper.png'
# Choose placement and look of floor at the bottom of the game
FLOOR_OFFSET_Y = 10
FLOOR_INCLINE_X = 50
FLOOR_INCLINE_Y = 30
FLOOR_WIDTH = 2
# Choose ball size, image, and physics mass
BALL_SIZE = 15
BALL_MASS = 0.5
BALL_IMAGE = ':resources:images/items/gold_1.png'
# Choose physics parameters based on (very scientific) pixels per time step
PHYSICS_TIME_STEP = 1 / 60.0
GRAVITY = -900
FRICTION = 0.5


class PymunkSprite(arcade.Sprite):
    """
    This class represents a Sprite that works with the Pymunk physics system by keeping the Sprite's values in sync with
    the physics Shape's values.
    """
    def __init__(self, filename, pymunk_shape, x, y):
        super().__init__(filename)
        # set Pymunk specific attributes so it works properly
        self.pymunk_shape = pymunk_shape
        self.pymunk_shape.body.position = [x, y]
        self.pymunk_shape.friction = FRICTION
        # set Arcade Sprite specific attributes to correspond to physics values
        self.width = pymunk_shape.radius * 2
        self.height = pymunk_shape.radius * 2
        self.center_x = x
        self.center_y = y

    def update(self):
        """
        Update Sprite's position and angle based on those calculated by Pymunk rather than our own (dx, dy)
        """
        self.center_x = self.pymunk_shape.body.position.x
        self.center_y = self.pymunk_shape.body.position.y
        self.angle = math.degrees(self.pymunk_shape.body.angle)


class StaticFloor:
    """
    This class represents a shape that cannot be moved and is drawn as a simple line (not a default Sprite).
    """
    def __init__(self, start, end, color):
        # create an unmoving physics object
        self.body = pymunk.Body(body_type=pymunk.Body.STATIC)
        self.shape = pymunk.Segment(self.body, start, end, 0.0)
        self.shape.friction = FRICTION
        # set color to be used when drawing
        self.color = color

    def draw(self):
        """
        Draw as a colored Line that represents the physics shape.
        """
        pv1 = self.body.position + self.shape.a.rotated(self.body.angle)
        pv2 = self.body.position + self.shape.b.rotated(self.body.angle)
        arcade.draw_line(pv1.x, pv1.y, pv2.x, pv2.y, self.color, FLOOR_WIDTH)


class Game(arcade.Window):
    """
    This class represents the entire "game" (there is no interaction, just resetting):
    - setting up any objects to appear in game (in the constructor)
    - updating their values (in method on_update)
    - drawing them (in method on_draw)
    - responding to key input (in method on_key_press)
    """
    def __init__(self, width, height, title):
        super().__init__(width, height, title)
        self.peg_list = arcade.SpriteList()
        self.ball_list = arcade.SpriteList()
        self.floor_list = []
        # setup Pymunk
        self.space = pymunk.Space()
        self.space.gravity = (0.0, GRAVITY)

    def setup(self):
        """

        """
        self.space = pymunk.Space()
        self.space.gravity = (0.0, GRAVITY)
        self.peg_list.clear()
        self.ball_list.clear()

        self.floor_list = [
            StaticFloor([0, FLOOR_OFFSET_Y], [SCREEN_WIDTH, FLOOR_OFFSET_Y], arcade.color.WHITE),
            StaticFloor([SCREEN_WIDTH - FLOOR_INCLINE_X, FLOOR_OFFSET_Y], [SCREEN_WIDTH, FLOOR_INCLINE_Y], arcade.color.WHITE),
            StaticFloor([FLOOR_INCLINE_X, FLOOR_OFFSET_Y], [0, FLOOR_INCLINE_Y], arcade.color.WHITE)
        ]
        for f in self.floor_list:
            self.space.add(f.shape, f.body)

        for row in range(BUMPER_ROWS):
            for col in range(BUMPER_COLUMNS):
                self.make_bumper(BUMPER_SIZE,
                                 col * BUMPER_SPACING + (BUMPER_SPACING // 2 * (row % 2 + 1)),
                                 row * BUMPER_SPACING + BUMPER_SPACING // 2)

        arcade.schedule(self.make_ball_schedule, 0.4)
        arcade.set_background_color(arcade.color.DARK_SLATE_GRAY)

    def make_ball(self, mass, radius, x, y):
        """

        """
        inertia = pymunk.moment_for_circle(mass, 0, radius, [0, 0])
        body = pymunk.Body(mass, inertia)
        shape = pymunk.Circle(body, radius)
        self.ball_list.append(PymunkSprite(BALL_IMAGE, shape, x, y))
        self.space.add(body, shape)

    def make_bumper(self, radius, x, y):
        """

        """
        body = pymunk.Body(body_type=pymunk.Body.STATIC)
        shape = pymunk.Circle(body, radius)
        self.peg_list.append(PymunkSprite(BUMPER_IMAGE, shape, x, y))
        self.space.add(body, shape)

    def make_ball_schedule(self, dt):
        """

        """
        self.make_ball(BALL_MASS, BALL_SIZE, random.randint(0, SCREEN_WIDTH), SCREEN_HEIGHT)

    def on_key_press(self, key, modifiers):
        """
        Called whenever a key is pressed.
        """
        if key == arcade.key.R:
            arcade.unschedule(self.make_ball_schedule)
            self.setup()

    def on_draw(self):
        """
        Draw all the game objects on the screen.
        """
        # DO NOT CHANGE -- always clear the screen as the FIRST step
        self.clear()
        # now draw the game objects
        self.peg_list.draw()
        self.ball_list.draw()
        for f in self.floor_list:
            f.draw()

    def on_update(self, dt):
        """

        """
        # update physics, using a constant time step instead of varying dt
        self.space.step(PHYSICS_TIME_STEP)
        # update moving physics objects
        for ball in self.ball_list:
            ball.update()
            # check if ball has fallen off the screen
            if ball.center_y < 0:
                # remove balls from drawing list AND physics space
                ball.remove_from_sprite_lists()
                self.space.remove(ball.pymunk_shape, ball.pymunk_shape.body)


# Create the game and set initial scene
game = Game(SCREEN_WIDTH, SCREEN_HEIGHT, GAME_NAME)
game.setup()
# Play the game forever
arcade.run()
