import tkinter as tk
from time import time, sleep
from random import choice, uniform, randint, sample, triangular
from math import sin, cos, pi

GRAVITY = 30  # you can play around with this


class Particle:

    def __init__(self, cv=None, color='black', x=0, y=0,
                 vx=0, vy=0, lifespan=5):
        self.cid = None
        self.cv = cv
        self.color = color
        self.x, self.y = x, y
        self.vx, self.vy = vx, vy
        self.age = 0
        self.lifespan = lifespan

    def update(self, dt):
        self.age += dt
        if self.alive():
            self.vy += GRAVITY * dt
            self.x += self.vx * dt
            self.y += self.vy * dt
            self.cv.move(self.cid, self.vx * dt, self.vy * dt)
        elif self.cid is not None:
            cv.delete(self.cid)
            self.cid = None

    def alive(self):
        return self.age <= self.lifespan


class SquareParticle(Particle):

    def __init__(self, x, y, size=2, **kwargs):
        super().__init__(x=x, y=y, **kwargs)
        self.cid = self.cv.create_polygon(
            x - size, y - size, x + size, y - size,
            x + size, y + size, x - size, y + size,
            fill=self.color)


class TriangleParticle(Particle):

    def __init__(self, x, y, size=2, **kwargs):
        super().__init__(x=x, y=y, **kwargs)
        self.cid = self.cv.create_polygon(
            x - size, y - size, x + size,
            y - size, x, y + size,
            fill=self.color)


class CircularParticle(Particle):

    def __init__(self, x, y, size=2, **kwargs):
        super().__init__(x=x, y=y, **kwargs)
        self.cid = self.cv.create_oval(
            x - size, y - size, x + size,
            y + size, fill=self.color)


class Fireworks:

    def __init__(self, cv=None):
        self.cv = cv
        self.age = 0
        self.particles = []

    def update(self, dt):
        self.age += dt
        for p in self.particles:
            p.update(dt)
        for i in range(len(self.particles) - 1, -1, -1):
            if not self.particles[i].alive():
                del self.particles[i]


class Volcano(Fireworks):

    def __init__(self, cv, x, pps, colors):
        super().__init__(cv)
        self.cid = cv.create_polygon(
            x - 12, 530, x + 12, 530, x, 500, fill="green3")
        self.x = x
        self.pps = pps
        self.colors = colors
        self.tospawn = 0

    def update(self, dt):
        super().update(dt)
        self.tospawn += self.pps * dt
        color = self.colors[int(self.age / 3) % len(self.colors)]
        for i in range(int(self.tospawn)):
            ptype = choice(
                [SquareParticle, TriangleParticle, CircularParticle])
            angle = uniform(-0.25, 0.25)
            speed = -uniform(80.0, 120.0)
            vx = sin(angle) * speed
            vy = cos(angle) * speed
            self.particles.append(
                ptype(cv=self.cv, x=self.x, y=500, color=color, vx=vx, vy=vy))
        self.tospawn -= int(self.tospawn)


class Bullet(Fireworks):

    def __init__(self, cv, x, vx, vy, colors, **kwargs):
        super().__init__(cv)
        self.tospawn = 1000
        self.x, self.vx, self.vy = x, vx, vy
        self.body = CircularParticle(
            cv=self.cv, x=self.x, y=500, vx=vx, vy=vy, **kwargs)
        self.particlecolors = colors
        self.particles.append(self.body)

    def explode(self):
        for i in range(int(self.tospawn)):
            ptype = choice(
                [SquareParticle, TriangleParticle, CircularParticle])
            angle = uniform(-pi, pi)
            speed = -uniform(10.0, 60.0)
            vx = sin(angle) * speed
            vy = cos(angle) * speed
            ls = uniform(2.0, 3.0)
            self.particles.append(ptype(
                cv=self.cv, x=self.body.x, y=self.body.y,
                color=choice(self.particlecolors),
                vx=vx, vy=vy, lifespan=ls))

    def update(self, dt):
        super().update(dt)
        if self.body and not self.body.alive():
            self.explode()
            self.body = None

class HeartBullet(Bullet):

    def explode(self):
        speed = uniform(-40,-10)
        for i in range(int(self.tospawn)):
            ptype = choice(   
                [SquareParticle, TriangleParticle, CircularParticle])
            while True:
                x = uniform(-1.139,1.139)
                y = uniform(-1,1.3)
                upper = (0.5 * (x**2)**(1/3) + (0.25 * (x**4)**(1/3) + 1 - x**2)**(1/2))
                uppersmall = ((0.5 * ((x*2)**2)**(1/3) + (0.25 * ((x*2)**4)**(1/3) + 1 - (x*2)**2)**(1/2)))/2
                lower = (0.5 * (x**2)**(1/3) - (0.25 * (x**4)**(1/3) + 1 - x**2)**(1/2))
                lowersmall = ((0.5 * ((x*2)**2)**(1/3) - (0.25 * ((x*2)**4)**(1/3) + 1 - (x*2)**2)**(1/2)))/2
                if not (lower <= y <= upper):
                    continue
                break
                if x*2 < -1.139 or x*2 > 1.139:
                    continue
                if lowersmall <= y <= uppersmall:
                    continue
                break             
            vx = x * speed
            vy = y * speed
            ls = uniform(2.0, 3.0)
            self.particles.append(ptype(
                cv=self.cv, x=self.body.x, y=self.body.y,
                color=choice(self.particlecolors),
                vx=vx, vy=vy, lifespan=ls))


class BulletLauncher(Fireworks):

    def __init__(self, cv, x, rps, maxaleft, maxaright, colors, ncolors):
        super().__init__(cv)
        self.cid = cv.create_rectangle(x - 15, 520, x + 15, 490, fill="gold")
        self.x = x
        self.rps = rps
        self.maxaleft, self.maxaright = maxaleft, maxaright
        self.colors = colors
        self.ncolors = ncolors
        self.next = 0
        self.bullets = []

    def update(self, dt):
        super().update(dt)
        for b in self.bullets:
            b.update(dt)
        if self.age > self.next:
            angle = uniform(-self.maxaright, self.maxaleft)
            speed = -uniform(200, 250)
            vx = sin(angle) * speed
            vy = cos(angle) * speed
            ls = cos(angle)*uniform(2.0, 3.0)
            n = randint(1, self.ncolors)
            colors = sample(self.colors, n)
            self.newBullet(self.cv, self.x, vx, vy, colors, "yellow", ls, 3)
            self.next += 1 / uniform(0.25 * self.rps, 1.75 * self.rps)

    def newBullet(self, cv, x, vx, vy, colors, color, lifespan, size):
        self.bullets.append(
                Bullet(cv, x, vx, vy, colors, color=color, lifespan=lifespan, size=size))

class HeartBulletLauncher(BulletLauncher):

    def newBullet(self, cv, x, vx, vy, colors, color, lifespan, size):
        self.bullets.append(
                HeartBullet(cv, x, vx, vy, colors, color=color, lifespan=lifespan, size=size))

def mainloop(cv, objects):
    t, dt = time(), 0
    while True:
        sleep(0.01)
        for o in objects:
            o.update(dt)
        cv.update()
        tnew = time()
        t, dt = tnew, tnew - t


if __name__ == '__main__':
    root = tk.Tk()
    cv = tk.Canvas(root, height=600, width=800)
    cv.create_rectangle(0, 0, 800, 600, fill="midnight blue")
    cv.create_rectangle(0, 450, 800, 600, fill="gray11")
    cv.pack()

    v1 = Volcano(cv, 400, 100, ["red", "orange", "yellow", "chartreuse2"])
    bl1 = BulletLauncher(cv, 600, 0.7, 0.7, 0.2,
                         ["red", "orange", "green", "blue",
                          "gold", "silver", "yellow", "lime"], 3)
    bl2 = HeartBulletLauncher(cv, 200, 0.7, 0.2, 0.7,
                         ["red", "orange", "green", "blue",
                          "gold", "silver", "yellow", "lime"], 3)
    objects = [v1, bl1, bl2]

    root.after(200, mainloop, cv, objects)
    root.mainloop()
