local particles = {}
local precision = 1

-- base particle class
class("Particle").extends()
function Particle:init(x, y)
    self.x = x or 0
    self.y = y or 0
    self.size = {1,1}
    self.spread = {0,359}
    self.speed = {1,1}
    self.acceleration = {0, 0}
    self.thickness = {0,0}
    self.lifespan = {1,1}
    self.decay = 1
    self.particles = {}
    self.colour = playdate.graphics.kColorBlack
    self.bounds = {0,0,0,0}
    self.mode = 0
    if self.type == 2 then -- polys
        self.points={3,3}
        self.angular={0,0}
        self.rotation={0,359}
    elseif self.type == 3 then -- images
        self.angular={0,0}
        self.image = playdate.graphics.image.new(1,1)
        self.table = nil
        self.rotation = {0,359}
    end

    particles[#particles+1] = self

end

function Particle:remove()
    for i = 1, #particles, 1 do
        if particles[i] == self then
            table.remove(particles, i)
            break
        end
    end
end

-- [[ SETTERS AND GETTERS ]] --

-- movement
function Particle:moveTo(x,y)
    self.x = x
    self.y = y
end

function Particle:moveBy(x,y)
    self.x += x
    self.y += y
end

function Particle:getPos()
    return self.x, self.y
end

-- size
function Particle:setSize(min, max)
    self.size = {min, max or min}
end

function Particle:getSize()
    return self.size[1], self.size[2]
end

-- mode
function Particle:setMode(mode)
    self.mode = mode
end

function Particle:getMode()
    return self.mode
end

-- spread
function Particle:setSpread(min,max)
    self.spread = {min, max or min}
end

function Particle:getSpread()
    return self.spread[1], self.spread[2]
end

-- speed
function Particle:setSpeed(min,max)
    self.speed = {min, max or min}
end

function Particle:getSpeed()
    return self.speed[1], self.speed[2]
end

-- acceleration
function Particle:setAcceleration(min,max)
    self.acceleration = {min, max or min}
end

function Particle:getAcceleration()
    return self.acceleration[1], self.acceleration[2]
end

-- thickness
function Particle:setThickness(min, max)
    self.thickness = {min, max or min}
end

function Particle:getThickness()
    return self.thickness[1], self.thickness[2]
end

-- lifespan
function Particle:setLifespan(min,max)
    self.lifespan = {min, max or min}
end

function Particle:getLifespan()
    return self.lifespan[1], self.lifespan[2]
end

-- colour
function Particle:setColor(colour)
    self.colour = colour
end

function Particle:getColor()
    return self.colour
end

function Particle:setColour(colour)
    self.colour = colour
end

function Particle:getColour()
    return self.colour
end

-- bounds
function Particle:setBounds(x1, y1, x2, y2)
    self.bounds = {x1,y1,x2,y2}
end

function Particle:getBounds()
    return self.bounds[1],self.bounds[2],self.bounds[3],self.bounds[4]
end

-- decay
function Particle:setDecay(decay)
    self.decay = decay
end

function Particle:getDecay()
    return self.decay
end

-- particles
function Particle:getParticles()
    return self.particles
end

function Particle:clearParticles()
    self.particles = {}
end

function Particle:update()
end

-- [[ PARTICLE MODES ]] --

local function decay(partlist, decay)
    for part = 1, #partlist, 1 do
        local particle = partlist[part]
        particle.size -= decay

        if particle.size <= 0 then
            particle.size = 0
        end

        partlist[part] = particle
    end

    for part = #partlist, 1, -1 do
        local particle = partlist[part]
        if particle.size <= 0 then
            table.remove(partlist,part)
        end
    end

    return partlist
end

local function disappear(partlist)
    for part = 1, #partlist, 1 do
        local particle = partlist[part]
        particle.lifespan -= .1
    end
    for part = #partlist, 1, -1 do
        local particle = partlist[part]
        if particle.lifespan <= 0 then
            table.remove(partlist,part)
        end
    end

    return partlist
end

local function loop(partlist, bounds)
    if bounds[3] > bounds[1] and bounds[4] > bounds[2] then
        local xDif , yDif = bounds[3] - bounds[1], bounds[4] - bounds[2]
        for part = 1, #partlist, 1 do
            local particle = partlist[part]
            if particle.x > bounds[3] then particle.x -= xDif
            elseif particle.x < bounds[1] then particle.x += xDif end
            if particle.y > bounds[4] then particle.y -= yDif
            elseif particle.y < bounds[2] then particle.y += yDif end
        end
    end

    return partlist
end

local function stay(partlist, bounds)
    if bounds[3] > bounds[1] and bounds[4] > bounds[2] then
        local xDif , yDif = bounds[3] - bounds[1], bounds[4] - bounds[2]
        for part = #partlist, 1, -1 do
            local particle = partlist[part]
            if particle.x > bounds[3] then table.remove(partlist,part)
            elseif particle.x < bounds[1] then table.remove(partlist,part)
            elseif particle.y > bounds[4] then table.remove(partlist,part)
            elseif particle.y < bounds[2] then table.remove(partlist,part) end
        end
    end

    return partlist
end

class("ParticleCircle", {type = 1}).extends(Particle)

function ParticleCircle:create(amount)
    for i = 1, amount, 1 do
        local part = {
            x = self.x,
            y = self.y,
            dir = math.random(self.spread[1],self.spread[2]),
            size = math.random(self.size[1],self.size[2]) * precision,
            speed = math.random(self.speed[1],self.speed[2]) * precision,
            acceleration = math.random(self.acceleration[1],self.acceleration[2]) * precision,
            lifespan = math.random(self.lifespan[1],self.lifespan[2]) * precision,
            thickness = math.random(self.thickness[1],self.thickness[2]),
            decay = self.decay
        }

        self.particles[#self.particles+1] = part
    end
end

function ParticleCircle:add(amount)
    self:create(amount)
end

function ParticleCircle:update()
    local w = playdate.graphics.getLineWidth()
    local c = playdate.graphics.getColor()
    playdate.graphics.setColor(self.colour)
    for part = 1, #self.particles, 1 do
        local circ = self.particles[part]
        if circ.thickness < 1 then
            playdate.graphics.fillCircleAtPoint(circ.x,circ.y,circ.size)
        else
            playdate.graphics.setLineWidth(circ.thickness)
            playdate.graphics.drawCircleAtPoint(circ.x, circ.y, circ.size)
        end

        circ.x += math.sin(math.rad(circ.dir)) * circ.speed
        circ.y -= math.cos(math.rad(circ.dir)) * circ.speed
        
        circ.speed += circ.acceleration / 100

        self.particles[part] = circ
    end
    playdate.graphics.setLineWidth(w)
    playdate.graphics.setColor(c)
    if self.mode == 1 then
        decay(self.particles, self.decay)
        
    elseif self.mode == 0 then
        disappear(self.particles)
        
    elseif self.mode == 2 then
        loop(self.particles, self.bounds)
        
    else
        stay(self.particles, self.bounds)
        
    end
end

class("ParticlePoly", {type = 2}).extends(Particle)

function ParticlePoly:getPoints()
    return self.points[1], self.points[2]
end

function ParticlePoly:setPoints(min,max)
    self.points = {min, max or min}
end

-- angular
function ParticlePoly:getAngular()
    return self.angular[1], self.angular[2]
end

function ParticlePoly:setAngular(min,max)
    self.angular = {min, max or min}
end

function ParticlePoly:getRotation()
    return self.rotation[1], self.rotation[2]
end

function ParticlePoly:setRotation(min,max)
    self.rotation = {min, max or min}
end

function ParticlePoly:create(amount)
    for i = 1, amount, 1 do
        local part = {
            x = self.x,
            y = self.y,
            dir = math.random(self.spread[1],self.spread[2]),
            size = math.random(self.size[1],self.size[2]) * precision,
            speed = math.random(self.speed[1],self.speed[2]) * precision,
            acceleration = math.random(self.acceleration[1],self.acceleration[2]) * precision,
            lifespan = math.random(self.lifespan[1],self.lifespan[2]) * precision,
            thickness = math.random(self.thickness[1],self.thickness[2]) * precision,
            angular = math.random(self.angular[1],self.angular[2]) * precision,
            points = math.random(self.points[1], self.points[2]),
            decay = self.decay,
            rotation = math.random(self.rotation[1],self.rotation[2])
        }

        self.particles[#self.particles+1] = part
    end
end

function ParticlePoly:add(amount)
    self:create(amount)
end

function ParticlePoly:update()
    local w = playdate.graphics.getLineWidth()
    local c = playdate.graphics.getColor()
    playdate.graphics.setColor(self.colour)
    for part = 1, #self.particles, 1 do
        local poly = self.particles[part]
        local polygon = {}
        local degrees = 360 / poly.points
        for point = 1, poly.points, 1 do
            polygon[#polygon+1] = poly.x + math.sin(math.rad(degrees * point + poly.rotation)) * poly.size
            polygon[#polygon+1] = poly.y - math.cos(math.rad(degrees * point + poly.rotation)) * poly.size
        end
        if poly.thickness < 1 then
            playdate.graphics.fillPolygon(table.unpack(polygon))
        else
            playdate.graphics.setLineWidth(poly.thickness)
            playdate.graphics.drawPolygon(table.unpack(polygon))
        end

        poly.x += math.sin(math.rad(poly.dir)) * poly.speed
        poly.y = poly.y - math.cos(math.rad(poly.dir)) * poly.speed

        poly.rotation += poly.angular
        
        poly.speed += poly.acceleration / 100

        self.particles[part] = poly
    end
    playdate.graphics.setLineWidth(w)
    playdate.graphics.setColor(c)

    if self.mode == 1 then
        decay(self.particles, self.decay)
        
    elseif self.mode == 0 then
        disappear(self.particles)
        
    elseif self.mode == 2 then
        loop(self.particles, self.bounds)
        
    else
        stay(self.particles, self.bounds)
        
    end
end

class("ParticleImage", {type = 3}).extends(Particle)

function ParticleImage:getAngular()
    return self.angular[1], self.angular[2]
end

function ParticleImage:setAngular(min,max)
    self.angular = {min, max or min}
end

function ParticleImage:getRotation()
    return self.rotation[1], self.rotation[2]
end

function ParticleImage:setRotation(min,max)
    self.rotation = {min, max or min}
end

function ParticleImage:setImage(image)
    self.image = image
    self.table = nil
end

function ParticleImage:setImageTable(image)
    self.image = nil
    self.table = image
end

function ParticleImage:getImage()
    return self.image
end

function ParticleImage:getImageTable()
    return self.table
end

function ParticleImage:create(amount)
    if self.image ~= nil then
        for i = 1, amount, 1 do
            local part = {
                x = self.x,
                y = self.y,
                dir = math.random(self.spread[1],self.spread[2]),
                size = math.random(self.size[1],self.size[2]) * precision,
                speed = math.random(self.speed[1],self.speed[2]) * precision,
                acceleration = math.random(self.acceleration[1],self.acceleration[2]) * precision,
                lifespan = math.random(self.lifespan[1],self.lifespan[2]) * precision,
                thickness = math.random(self.thickness[1],self.thickness[2]),
                angular = math.random(self.angular[1],self.angular[2]) * precision,
                decay = self.decay,
                image = self.image,
                rotation = math.random(self.rotation[1],self.rotation[2])
            }

            self.particles[#self.particles+1] = part
        end
    else
        for i = 1, amount, 1 do
            local part = {
                x = self.x,
                y = self.y,
                dir = math.random(self.spread[1],self.spread[2]),
                size = math.random(self.size[1],self.size[2]) * precision,
                speed = math.random(self.speed[1],self.speed[2]) * precision,
                acceleration = math.random(self.acceleration[1],self.acceleration[2]) * precision,
                lifespan = math.random(self.lifespan[1],self.lifespan[2]) * precision,
                thickness = math.random(self.thickness[1],self.thickness[2]),
                angular = math.random(self.angular[1],self.angular[2]) * precision,
                decay = self.decay,
                image = self.table[math.random(#self.table)],
                rotation = math.random(self.rotation[1],self.rotation[2])
            }

            self.particles[#self.particles+1] = part
        end
    end
end

function ParticleImage:add(amount)
    self:create(amount)
end

function ParticleImage:update()
    for part = 1, #self.particles, 1 do
        local img = self.particles[part]

        img.image:drawRotated(img.x,img.y,img.rotation,img.size)

        img.rotation += img.angular

        img.x += math.sin(math.rad(img.dir)) * img.speed
        img.y = img.y - math.cos(math.rad(img.dir)) * img.speed
        
        img.speed += img.acceleration / 100

        self.particles[part] = img
    end

    if self.mode == 1 then
        decay(self.particles, self.decay)
        
    elseif self.mode == 0 then
        disappear(self.particles)
        
    elseif self.mode == 2 then
        loop(self.particles, self.bounds)
        
    else
        stay(self.particles, self.bounds)
        
    end
end

class("ParticleImageBasic", {type = 3}).extends(ParticleImage)

function ParticleImageBasic:update()
    for part = 1, #self.particles, 1 do
        local img = self.particles[part]

        img.image:drawScaled(img.x,img.y,img.size)

        img.rotation += img.angular

        img.x += math.sin(math.rad(img.dir)) * img.speed
        img.y = img.y - math.cos(math.rad(img.dir)) * img.speed

        img.speed += img.acceleration / 100

        self.particles[part] = img
    end

    if self.mode == 1 then
        decay(self.particles, self.decay)
        
    elseif self.mode == 0 then
        disappear(self.particles)
        
    elseif self.mode == 2 then
        loop(self.particles, self.bounds)
        
    else
        stay(self.particles, self.bounds)
        
    end
end

class("ParticlePixel").extends(Particle)

function ParticlePixel:create(amount)
    for i = 1, amount, 1 do
        local part = {
            x = self.x,
            y = self.y,
            dir = math.random(self.spread[1],self.spread[2]),
            speed = math.random(self.speed[1],self.speed[2]) * precision,
            acceleration = math.random(self.acceleration[1],self.acceleration[2]) * precision,
            lifespan = math.random(self.lifespan[1],self.lifespan[2])  * precision,
        }

        self.particles[#self.particles+1] = part
    end
end

function ParticlePixel:add(amount)
    self:create(amount)
end

function ParticlePixel:update()
    local c = playdate.graphics.getColor()
    playdate.graphics.setColor(self.colour)
    for part = 1, #self.particles, 1 do
        local pix = self.particles[part]
        
        playdate.graphics.drawPixel(pix.x,pix.y)

        pix.x += math.sin(math.rad(pix.dir)) * pix.speed
        pix.y -= math.cos(math.rad(pix.dir)) * pix.speed
        
        pix.speed += pix.acceleration / 100

        self.particles[part] = pix
    end
    playdate.graphics.setColor(c)

    if self.mode == 0 then
        disappear(self.particles)
        
    elseif self.mode == 2 then
        loop(self.particles, self.bounds)
        
    else
        stay(self.particles, self.bounds)
        
    end
end

-- [[ GLOBAL PARTICLE STUFF ]] --

class("Particles").extends()

Particles.modes = {DISAPPEAR = 0, DECAY = 1, LOOP = 2, STAY = 3}

function Particles:update()
    for particle = 1, #particles, 1 do
        particles[particle]:update()
    end
end

function Particles:clearAll()
    for part = 1, #particles, 1 do
        particles[part]:clearParticles()
    end
end

function Particles:removeAll()
    local lb = #particles
    for part = 1, #particles, 1 do
        particles[part] = nil
    end
end

function Particles:setPrecision(prec)
    precision = prec
end

function Particles:getPrecision()
    return precision
end