"""
roids.py
========

.. figure:: ../_static/roids.jpg
    :align: center

    Test for polygons.

Asteroids style game demo using polygons.

.. note:: This example requires the following modules:

  .. hlist::
    :columns: 3

    - `st7789py`
    - `tft_config`
    - `tft_buttons`

"""

import math
import random
import utime
import tft_config
import tft_buttons as Buttons
import st7789py as st7789


tft = tft_config.config(tft_config.WIDE)
buttons = Buttons.Buttons()


def main():
    '''
    Game on!
    '''

    class Poly():
        '''
        Poly class to keep track of a polygon based sprite
        '''

        def __init__(
                self,
                # list (x,y) tuples of convex polygon, must be closed
                polygon,
                x=None,             # x location of polygon
                y=None,             # y location of polygon
                v_x=None,           # velocity in x axis
                v_y=None,           # velocity in y axis
                angle=None,         # angle in radians polygon is facing
                spin=None,          # spin in radians per frame_time
                scale=None,         # scale factor for polygon
                radius=None,        # radius of polygon for collision detection
                max_velocity=10,    # max velocity of polygon
                counter=0):

            # scale the polygon if scale was given
            self.polygon = (
                polygon if scale is None else [(int(scale*x[0]), int(scale*x[1])) for x in polygon])

            # if no location given assign a random location
            self.x = random.randint(0, width) if x is None else x
            self.y = random.randint(0, width) if y is None else y

            # set angle if given
            self.angle = float(0) if angle is None else angle

            # set random spin unless one was given
            self.spin = random.randint(-3, 3) / 16 if spin is None else spin

            # set random velocity unless one was given
            self.velocity_x = random.uniform(
                0.50, 0.99)*6-3 + 0.75 if v_x is None else v_x
            self.velocity_y = random.uniform(
                0.50, 0.99)*6-3 + 0.75 if v_y is None else v_y

            # set radius, max_velocity and radius counter
            self.radius = radius
            self.max_velocity = max_velocity
            self.counter = counter

        def rotate(self, rad):
            '''
            Rotate polygon in radians
            '''
            self.angle += rad
            if self.angle > rad_max:
                self.angle = 0
            elif self.angle < 0:
                self.angle = rad_max

        def move(self):
            '''
            Rotate and move polygon velocity distance.
            '''
            if self.spin != 0:
                self.rotate(self.spin)

            self.x += int(self.velocity_x)
            self.y += int(self.velocity_y)
            self.x %= width
            self.y %= height

        def draw(self, color):
            '''
            Draw the polygon
            '''
            tft.polygon(self.polygon, self.x, self.y, color, self.angle, 0, 0)

        def collision(self, poly):
            '''
            Detect collisions using overlapping radiuses.
            Returns True on collision.
            '''
            return abs(
                ((self.x - poly.x) * (self.x - poly.x) +
                 (self.y - poly.y) * (self.y - poly.y))
                < (self.radius + poly.radius) * (self.radius + poly.radius))

    def create_roid(size, x=None, y=None, v_x=None, v_y=None):
        '''
        Create a new roid with the given parameters.
        '''
        return Poly(
            roid_poly,
            x=x,
            y=y,
            v_x=v_x,
            v_y=v_y,
            scale=roid_scale[size],
            radius=roid_radius[size],
            counter=size)

    def update_missiles():
        '''
        Update active missiles and handle asteroid hits.
        '''
        for missile in list(missiles):                      # for each missile
            missile.draw(st7789.BLACK)                      # erase old missile
            if missile.counter > 0:                         # if counter > 0 missile is active
                missile.move()                              # update missile position

                for roid in list(roids):                    # for each roid
                    if missile.collision(roid):             # check if missile collides with roid
                        roid.draw(st7789.BLACK)             # erase the roid
                        if roid.counter > 0:                # if roid is not the smallest size
                            roids.append(                   # add first smaller roid
                                create_roid(
                                    roid.counter-1,
                                    x=roid.x,
                                    y=roid.y,
                                    v_x=roid.velocity_x,
                                    v_y=roid.velocity_y))
                            roids.append(                   # add second smaller roid
                                create_roid(
                                    roid.counter-1,
                                    x=roid.x,
                                    y=roid.y,
                                    v_x=-roid.velocity_x,
                                    v_y=-roid.velocity_y))

                        roids.remove(roid)                  # remove the roid that was hit
                        missile.counter = 0

                if missile.counter > 0:                     # if the missile has life left
                    missile.draw(st7789.WHITE)              # draw missile
                    missile.counter -= 1                    # reduce missile life
                else:

                    missiles.remove(missile)                # remove exploded missile
            else:
                missiles.remove(missile)                    # remove expired missile

    def update_ship():
        '''
        Update ship velocity and limit to max_velocity
        '''
        # apply drag to velocity of ship so it will eventually slow to a stop
        ship.velocity_x -= ship.velocity_x * ship_drag_frame
        ship.velocity_y -= ship.velocity_y * ship_drag_frame

        if ship.velocity_x > ship.max_velocity:             # Limit velocity to +/- max_velocity
            ship.velocity_x = ship.max_velocity
        elif ship.velocity_x < -ship.max_velocity:
            ship.velocity_x = -ship.max_velocity

        if ship.velocity_y > ship.max_velocity:
            ship.velocity_y = ship.max_velocity
        elif ship.velocity_y < -ship.max_velocity:
            ship.velocity_y = -ship.max_velocity

        if abs(ship.velocity_x) < 0.1:                      # if ship is moving very slowly, stop it
            ship.velocity_x = 0.0

        if abs(ship.velocity_y) < 0.1:
            ship.velocity_y = 0.0

        ship.move()                                         # move the ship and draw it
        ship.draw(st7789.WHITE)

    def update_roids():
        '''
        Update roid positions handle ship collisions
        Returns True if not hit, False if hit
        '''
        not_hit = True

        for roid in roids:                                  # for each roid, erase, move then draw
            roid.draw(st7789.BLACK)
            roid.move()
            roid.draw(st7789.WHITE)

            if roid.collision(ship):                        # check for roid/ship collision
                ship.draw(st7789.BLACK)                     # erase ship
                ship.velocity_x = 0.0                       # stop movement
                ship.velocity_y = 0.0

                not_hit = False

        return not_hit

    def explode_ship():
        '''
        Increment explosion step and alternate between drawing
        explosion poly and explosion poly rotated 45 degrees

        Returns True when explosion is finished
        '''
        ship.counter += 1
        if ship.counter % 2 == 0:
            tft.polygon(explosion_poly, ship.x, ship.y, st7789.BLACK, 0.785398)
            tft.polygon(explosion_poly, ship.x, ship.y, st7789.WHITE)
        else:
            tft.polygon(explosion_poly, ship.x, ship.y, st7789.WHITE, 0.785398)
            tft.polygon(explosion_poly, ship.x, ship.y, st7789.BLACK)

        if ship.counter > 25:
            # erase explosion, move ship to center and stop explosion
            tft.polygon(explosion_poly, ship.x, ship.y, st7789.BLACK)
            # move ship to center
            ship.x = width >> 1
            ship.y = height >> 1
            ship.counter = 0
            return True

        return False

    # enable display and clear screen
    tft.fill(st7789.BLACK)
    width = tft.width
    height = tft.height

    rad_max = 2 * math.pi               # 360' in radians

    # ship variables
    ship_alive = True
    ship_radius = 7
    ship_rad_frame = rad_max / 16       # turning rate per frame
    ship_accel_frame = 0.6              # acceleration per frame
    ship_drag_frame = 0.015             # drag factor per frame

    ship_poly = [(-7, -7), (7, 0), (-7, 7), (-3, 0), (-7, -7)]

    ship = Poly(
        ship_poly,
        x=width >> 1,
        y=height >> 1,
        v_x=0,
        v_y=0,
        radius=ship_radius,
        spin=0.0)

    explosion_poly = [(-4, -4), (-4, 4), (4, 4), (4, -4), (-4, -4)]

    # asteroid variables
    roid_radius = [5, 10, 16]
    roid_scale = [0.33, 0.66, 1.0]

    roid_poly = [
        (-5, -15), (-2, -13), (11, -14), (15, -7), (14, 0),
        (16, 5), (11, 16), (7, 16), (-7, 14), (-14, 7),
        (-13, 1), (-14, -8), (-11, -15), (-5, -15)]
    roids = []

    # missile variables
    missile_velocity = 8
    missile_max = 8
    missile_life = 20
    missile_rate = 200
    missile_last = utime.ticks_ms()
    missile_poly = [(-1, -1), (1, -1), (1, 1), (-1, 1), (-1, -1)]
    missiles = []

    frame_time = 60                         # target frame rate delay

    # game loop
    while True:
        last_frame = utime.ticks_ms()

        # add roids if there are none
        if len(roids) == 0:
            roids = [create_roid(2), create_roid(2)]

        update_missiles()

        # Erase the ship
        ship.draw(st7789.BLACK)

        if ship_alive:
            # if left button pressed
            if buttons.left and buttons.left.value() == 0:
                # rotate ship counter clockwise
                ship.rotate(-ship_rad_frame)

            # if right button pressed
            if buttons.right and buttons.right.value() == 0:
                 # rotate ship clockwise
                ship.rotate(ship_rad_frame)

            # if hyperspace button pressed move ship to random location
            if buttons.hyper and buttons.hyper.value() == 0:
                diameter = ship.radius * 2
                ship.x = random.randint(diameter, width - diameter)
                ship.y = random.randint(diameter, height - diameter)

                # stop movement
                ship.velocity_x = 0.0
                ship.velocity_y = 0.0

            # if thrust button pressed
            if buttons.thrust and buttons.thrust.value() == 0:
                # accelerate ship in the direction the ship is facing
                d_y = math.sin(ship.angle) * ship_accel_frame
                d_x = math.cos(ship.angle) * ship_accel_frame
                ship.velocity_x += d_x
                ship.velocity_y += d_y

            # if the fire button is pressed and less than missile_max active missles
            if buttons.fire and buttons.fire.value() == 0 and len(missiles) < missile_max:

                # limit missiles firing to once every missile_rate ms
                if last_frame - missile_last > missile_rate:

                    # fire missile in direction ship in facing
                    v_y = math.sin(ship.angle) * missile_velocity
                    v_x = math.cos(ship.angle) * missile_velocity

                    # create new missile
                    missile = Poly(
                        missile_poly,
                        x=ship.x,
                        y=ship.y,
                        v_x=v_x,
                        v_y=v_y,
                        angle=ship.angle,
                        radius=1,
                        spin=0.0,
                        counter=missile_life)

                    # add to to missile list and save last fire time
                    missiles.append(missile)
                    missile_last = last_frame

            update_ship()

        else:
            # explosion animation until returns True
            ship_alive = explode_ship()

        # update roids and return if ship was not hit
        not_hit = update_roids()
        if ship_alive:
            ship_alive = not_hit

        # wait until frame time expires
        while utime.ticks_ms() - last_frame < frame_time:
            pass


main()
