piecesList = {}
currentSource = { pos = vec3(), index = nil }

-- Group: groups pieces or groups together

Group = {
  destroyed = false,
  spawnAllAtOnce = false,
  delay = 0,
  timeout = 0,
  velocity = vec3(),
  items = {}
}

function Group:new(p1, p2)
  if #piecesList > 1000 then error('Too much') end
  local o = {}
  setmetatable(o, self)
  self.__index = self
  fillProps(o, p1)
  fillProps(o, p2)
  o.items = {}
  o.chained = {}
  o.pos = (o.pos and o.pos or currentSource.pos) + (o.offset or vec3())
  o.velocity = o.velocity:clone()
  o.sourceIndex = currentSource.index
  self.initBase(o)
  return o
end

function Group:inheritChain(chained)
  if chained ~= nil then
    for i = 2, #chained do
      o.chained[#o.chained + 1] = chained[i]
    end
  end
end

function Group:chain(o, chained)
  Piece.chain(self, o, chained)
end

function Group:newType()
  self.__index = self
  return setmetatable({ parentClass = self }, self)
end

function Group:initBase()
  piecesList[#piecesList + 1] = self
  self:init(self.items, { pos = self.pos, velocity = self.velocity, child = true })
end

function Group:init(P, o)
end

function Group:spawn(P)
  currentSource.index = self.sourceIndex
  if self.spawnAllAtOnce then
    for i = 1, #self.chained do
      self.chained[i](self, P, {})
    end
  else
    if #self.chained > 0 then
      self.chained[1](self, P, self.chained)
    end
  end
end

function Group:destroy()
  self.destroyed = true
  for k, v in pairs(self.items) do
    v:destroy()
  end
end

function Group:updateBase(dt)
  if self.destroyed then
    return false
  end

  if self.delay > 0 then
    self.delay = self.delay - dt
  else
    local anyWorking = false
    local runs = 0
    for k, v in pairs(self.items) do
      anyWorking = v:updateBase(dt) or anyWorking
      runs = runs + 1
    end
  
    if not anyWorking or not self:update(self.items, dt) then
      self:spawn(self.ptr)
      self:destroy()
    end
  end

  return true
end

function Group:update(P, dt)
  if self.timeout ~= nil and self.timeout > 0 then
    self.timeout = self.timeout - dt
    return true
  end
  return self.timeout >= 0
end

function Group:setTimeout(timeout)
  self.timeout = timeout == 0 and 0.00001 or timeout
end

-- Piece: directly linked to a certain emitter

Piece = { 
  ptr = nil, 
  destroyed = false,
  spawnAllAtOnce = false,
  ptrAdded = false,
  delay = 0,
  timeout = 0,
  velocity = vec3() 
}

function Piece:new(p1, p2)
  if #piecesList > 1000 then error('Too much') end
  local o = {}
  setmetatable(o, self)
  self.__index = self
  fillProps(o, p1)
  fillProps(o, p2)
  o.chained = {}
  o.pos = (o.pos and o.pos or currentSource.pos) + (o.offset or vec3())
  o.velocity = o.velocity:clone()
  o.sourceIndex = currentSource.index
  self.initBase(o)
  return o
end

function Piece:inheritChain(chained)
  if chained ~= nil then
    for i = 2, #chained do
      self.chained[#self.chained + 1] = chained[i]
    end
  end
end

function Piece:chain(arg, spawnFunc, params)
  if type(arg) == 'table' then
    local typeToCreate = arg
    if type(spawnFunc) ~= 'function' then
      params = spawnFunc
      spawnFunc = nil
    end
    if spawnFunc == nil then
      spawnFunc = function (s, o, P, cb)
        cb{ pos = P.pos, velocity = P.velocity }
      end
    end
    arg = function(o, P, chained) 
      if params == nil then params = {} end
      spawnFunc(o, params, P, function (p)
        typeToCreate:new(p, params):inheritChain(chained)
      end)
    end
  end
  self.chained[#self.chained + 1] = arg
  return self
end

function Piece:newType()
  self.__index = self
  return setmetatable({ parentClass = self }, self)
end

function Piece:initBase()
  self.ptr = ac.Firework()
  self.ptr.pos = self.pos
  self.ptr.velocity = self.velocity
  if not self.child then
    piecesList[#piecesList + 1] = self
  end
  self:setSmokingColor()
  self:setColor()
  self:init(self.ptr)
end

function Piece:init(P)
  P.sparkingIntensity = 10
  P.sparkingLifespan = 1
  P.sparkingSize = 1
  P.sparkingStretch = 1
  P.sparkingSpeedMin = 1
  P.sparkingSpeedMax = 10
  P.sparkingDir = vec3(0, 1, 0)
  P.sparkingDirSpreadXZ = 10
  P.sparkingDirSpreadY = 1
  P.sparkingColorA = rgb(1, 0, 0)
  P.sparkingColorB = rgb(1, 1, 0)
end

function Piece:spawn(P)
  currentSource.index = self.sourceIndex
  if self.spawnAllAtOnce then
    for i = 1, #self.chained do
      self.chained[i](self, P, {})
    end
  else
    if #self.chained > 0 then
      self.chained[1](self, P, self.chained)
    end
  end
end

function Piece:destroy()
  self.destroyed = true
  ac.fireworks:erase(self.ptr) 
end

function Piece:updateBase(dt)
  if self.destroyed then
    return false
  end

  if self.delay > 0 then
    self.delay = self.delay - dt
  else
    if not self.audioSet then
      self.audioSet = true
      self:audio(self.ptr)
    end

    if self.nextLightColor ~= nil then
      ac.addFireworkFlash(self.sourceIndex, self.nextLightPos, self.nextLightColor, self.nextLightDuration)
      self.nextLightColor = nil
    end

    if not self.ptrAdded then 
      ac.fireworks:pushWhereFits(self.ptr)
      self.ptrAdded = true
    end
  
    if self.timeout ~= 0.00001 then
      self.ptr.pushingForce = 0
    end
    if not self:update(self.ptr, dt) then
      self:spawn(self.ptr)
      self:destroy()
    end
  end

  return true
end

function Piece:update(P, dt)
  P.pos:add(P.velocity * dt)
  if self.timeout ~= nil and self.timeout > 0 then
    self.timeout = self.timeout - dt
    return true
  end
  return self.timeout >= 0
end

function Piece:setColor()
  local colorA, colorB = GetColors(self.color)
  self.color = (colorA + colorB) / 2
  self.colorA = colorA
  self.colorB = colorB
  self:setSparkingColor(colorA, colorB)
end

function Piece:addLight(duration, color, pos)
  if self.sourceIndex == nil then return end
  
  color = color or self.color
  duration = duration or 0.03

  local chance = math.min(duration, 0.95)
  if not self.lightAdded then
    self.lightAdded = true
  elseif chance > math.random() then 
    return 
  end

  color = color / 3
  pos = pos or self.ptr.pos

  if not self.ptrAdded then
    self.nextLightColor = color
    self.nextLightPos = pos
    self.nextLightDuration = duration
  else
    ac.addFireworkFlash(self.sourceIndex, pos, color, duration)
  end
end

function Piece:setTimeout(timeout)
  self.timeout = timeout == 0 and 0.00001 or timeout
end

function Piece:audio(P)
end

function Piece:setSparking(intensity, lifespan)
  self.ptr.sparkingIntensity = intensity
  self.ptr.sparkingLifespan = lifespan
  self.ptr.sparkingSpreadFlags = 0
end

function Piece:setSmoking(intensity, lifespan, size)
  self.ptr.smokingIntensity = intensity or 1
  self.ptr.smokingLifespan = lifespan or 1
  self.ptr.smokingSize = size or 3
end

function Piece:setSmokingColor(opacity, color)
  self.ptr.smokingOpacity = opacity or 0.5
  self.ptr.smokingColor = rgb(0.1, 0.1, 0.1)
end

function Piece:setBurst(count, lifespan, mode)
  self.ptr.sparkingIntensityShot = count
  self.ptr.sparkingLifespan = lifespan or 2
  self.ptr.sparkingSpreadFlags = mode or 0
end

function Piece:setSmokingBurst(count, lifespan, size)
  self.ptr.smokingIntensityShot = count or 10
  self.ptr.smokingLifespan = lifespan or 6
  self.ptr.smokingSize = size or 6
end

function Piece:setSmokingSpeed(speed, speedSpread)
  self.ptr.smokingSpeedRandomMin = speed * (1 - speedSpread)
  self.ptr.smokingSpeedRandomMax = speed * (1 + speedSpread)
end

function Piece:setSparkingSpread(posSpread, lifespanSpread, sizeSpread)
  self.ptr.sparkingPosSpread = posSpread
  self.ptr.sparkingLifespanSpread = lifespanSpread
  self.ptr.sparkingSizeSpread = sizeSpread
end

function Piece:setSmokingSpread(posSpread)
  self.ptr.smokingPosSpread = 0
end

function Piece:setSparkingDir(dir, spreadXZ, spreadY)
  self.ptr.sparkingDir = dir
  self.ptr.sparkingDirSpreadXZ = spreadXZ
  self.ptr.sparkingDirSpreadY = spreadY
end

function Piece:setSmokingVelocity(vel)
  self.ptr.smokingVelocityOffset = vel
end

function Piece:setSparkingSpeed(speed, speedSpread)
  self.ptr.sparkingSpeedMin = speed * (1 - speedSpread)
  self.ptr.sparkingSpeedMax = speed * (1 + speedSpread)
end

function Piece:setSparkingColor(colorA, colorB, size)
  self.ptr.sparkingColorA = colorA
  self.ptr.sparkingColorB = colorB or colorA
  self.ptr.sparkingSize = size or 0.1
end