Slime = {}
Slime.slimes = {} -- Holds common data for all slimes.
Slime.slimesList = {}

function Slime.run()

    --> Process slimes.
    for i = 1, #Slime.slimesList do

        local slime = Slime.slimesList[i]

        -- Main stuff.
        Slime.slimes.physics.process(slime)
        Slime.slimes.hunger.process(slime)

    end

    -- Visual effects.
    Slime.slimes.effects.process()

    -- Auto-kill slimes to maintain FPS
    local fps = 1 / GetTimeStep()
    if fps < regGetFloat('tool.slime.killToMaintainFPS') then
        Slime.slimes.colony.markSlimesForRemoval()
    end

    -- Auto-kill slimes to respect max population
    local maxPop = regGetFloat('tool.slime.slimesMaxPopulation')
    local slimePop = #Slime.slimesList
    if (maxPop > 0) and (slimePop > maxPop) then
        Slime.slimes.colony.markSlimesForRemoval(1 - (maxPop / slimePop))
    end

    --> Safely delete specified slimes.
    Slime.slimes.colony.removeMarkedSlimes()

end





Slime.slimes.colony = {}

--- Create a slime object and store it in the list of slimes.
function Slime.slimes.colony.create(tr)

    local slime = {

        id = #Slime.slimesList + 1,
        removeSlime = false, -- true = mark slime for removal/deletion.

        tr = tr,
        rad = 0.2,
        foodEaten = 0,

        sticky = {
            shape = nil,
            shapeVoxels = nil,
            shapeRelativePos = nil,
        },

        timers = {
            eatDelay =   {time = 9999, delay = regGetFloat('tool.slime.eatDelay')}, -- Once slime is attached, wait before eating
        }

    }

    -- Eat timer varience.
    local eatDelay = regGetFloat('tool.slime.eatDelay')
    slime.timers.eatDelay.delay = rdmf(eatDelay * 0.9, eatDelay * 1.1) -- Registry live updates.

    -- Initialize time remaining.
    TimerResetTime(slime.timers.eatDelay)

    table.insert(Slime.slimesList, slime)
    dbp('Created slime. ' .. sfnTime()..', eatDelay='..eatDelay..', rndDelay='..slime.timers.eatDelay.delay)

end

--- Spawn a slime into the world.
function Slime.slimes.colony.spawn(pos, isDuplicate)

    Slime.slimes.colony.create(Transform(pos, QuatEuler(0,0,0)))

    if not isDuplicate then
        if rdm() < 0.1 / regGetFloat('tool.pour.rate') and regGetBool('tool.soundOn') then
            sounds.play.slime(pos, 0.5)
        end

        SpawnParticle("smoke", pos, Vec(0,0,0), 0.5, 0.5, 0.5, 0.5)
    end

end

--- Mark a slime for removal to safely remove it near the end of the frame.
function Slime.slimes.colony.markSlimeForRemoval(slime)
    slime.removeSlime = true
    dbp('Slime marked for removal: ' .. slime.id .. ' ' ..sfnTime())
end

--- Mark half of the slimes for removal.
function Slime.slimes.colony.markSlimesForRemoval(percent)
    percent = percent or 0.5
    local nbSlimes = #Slime.slimesList
    local nbSlimesToDelete = math.floor(nbSlimes * percent)
    if nbSlimesToDelete > 0 then
        for i = 1, nbSlimesToDelete do
            Slime.slimes.colony.markSlimeForRemoval(Slime.slimesList[i])
        end
    end

end

--- Safely delete all slimes that are marked for removal.
function Slime.slimes.colony.removeMarkedSlimes()

    local removeIndecies = {}
    if #Slime.slimesList >= 1 then
        for i = 1, #Slime.slimesList do
            if Slime.slimesList[i].removeSlime then
                table.insert(removeIndecies, i)
            end
        end
    end

    for i = 1, #removeIndecies do
        table.remove(Slime.slimesList, removeIndecies[i])
    end
    
end






Slime.slimes.physics = {}

---Process the physics of a single slime.
function Slime.slimes.physics.process(slime)

    if slime.sticky.shape == nil then -- Right after gas projectile hits something.

        Slime.slimes.physics.sticky.dripAndStick(slime) -- Let the slime drip and find a new shape

    elseif Slime.slimes.physics.sticky.didMassDecrease(slime) then -- Slime shape broke previous frame.

        Slime.slimes.physics.sticky.dripAndStick(slime) -- QCP right after shape breaks.

    else -- Slime is stuck to a shape and did not break last frame.

        -- Place slime pos relative to shape sticky point.
        slime.tr.pos = TransformToParentPoint(GetShapeWorldTransform(slime.sticky.shape), slime.sticky.shapeRelativePos)

    end

    -- Debug.
    dbw('SLIME '.. slime.id ..' attached voxels count ', slime.sticky.shapeVoxels)

end


Slime.slimes.physics.sticky = {}

function Slime.slimes.physics.sticky.setShape(slime, shape)

    slime.sticky.shape = shape
    slime.sticky.shapeVoxels = GetShapeVoxelCount(shape)
    slime.sticky.shapeRelativePos = TransformToLocalPoint(GetShapeWorldTransform(shape), slime.tr.pos)

    -- dbp('slime shape set ' .. sfnTime())
end

function Slime.slimes.physics.sticky.resetShape(slime)
    slime.sticky.shape = nil -- Set no shape
    slime.sticky.shapeVoxels = nil
    -- dbp('slime shape reset ' .. sfnTime())
end

function Slime.slimes.physics.sticky.didMassDecrease(slime)
    local massDecreased = slime.sticky.shapeVoxels > GetShapeVoxelCount(slime.sticky.shape)
    local shapeDestroyed = slime.sticky.shapeVoxels == nil
    if massDecreased or shapeDestroyed then
        Slime.slimes.physics.sticky.resetShape(slime)
        return true
    end
    return false
end

---Check the surroundings of a slime and attach it to very close objects.
function Slime.slimes.physics.sticky.dripAndStick(slime)

    --> Check if slime is touching anything.
    local slimeRcHit, point, normal, slimeRcShape = QueryClosestPoint(slime.tr.pos, slime.rad)
    if slimeRcHit then

        -- Set shape.
        Slime.slimes.physics.sticky.setShape(slime, slimeRcShape)

    else --> No hit.

        -- Jump in the direction of the closest matter chunk
        local hit, point, normal, shape = QueryClosestPoint(slime.tr.pos, regGetFloat('tool.slime.jumpDistance'))
        if hit then
            local targetVec = VecSub(point, slime.tr.pos)
            local dist = math.min(VecLength(targetVec), 0.5)
            slime.tr.pos = VecAdd(slime.tr.pos, VecScale(VecNormalize(targetVec), dist))
        else
            -- If none found, drip down.
            slime.tr.pos = VecAdd(slime.tr.pos, Vec(0, -0.5, 0))
        end
        
        -- No shape.
        Slime.slimes.physics.sticky.resetShape(slime)
        -- Slime moved, reset eat timer
        TimerResetTime(slime.timers.eatDelay)

    end

end




Slime.slimes.hunger = {}

--- Processes the hunger of a slime.
function Slime.slimes.hunger.process(slime)

    Slime.slimes.hunger.eat(slime)

end

-- Eat when slime is sticked to something and eat timer is over.
function Slime.slimes.hunger.eat(slime)

    if slime.sticky.shape ~= nil then -- Slime is sticked to something

        TimerRunTime(slime.timers.eatDelay)

        if slime.timers.eatDelay.time <= 0 then
            -- Eat matter nearby
            local eatSuccess = false
            local hit, point, normal, shape = QueryClosestPoint(slime.tr.pos, slime.rad)
            if hit then
                -- Found something to eat, try to eat it (may be indestructible)
                dbp('SLIME '.. slime.id ..' try eating '..shape..' at '..VecToStr(point))
                local eatRadius = regGetFloat('tool.slime.eatRadius')
                MakeHole(point, eatRadius, eatRadius, eatRadius, not regGetBool('tool.soundOn'))
                local hit2, point2, normal2, shape2 = QueryClosestPoint(slime.tr.pos, slime.rad)
                -- If eat was successful (voxel we tried to eat is no longer there)
                if (not hit2) or (VecDist(point, point2) > 0) then
                    eatSuccess = true
                    slime.foodEaten = slime.foodEaten + 1
                    -- Duplicate slime if current slime ate enough food
                    local duplicationRate = math.floor(regGetFloat('tool.slime.duplicationRate'))
                    dbp('SLIME '.. slime.id ..' eating success, food='..slime.foodEaten..'/'..duplicationRate)
                    if slime.foodEaten >= duplicationRate then
                        slime.foodEaten = 0
                        local spawnPos = getRandPointOnSphere(point, eatRadius)
                        dbp('SLIME '.. slime.id ..' duplicate center='..VecToStr(point)..', spawnPos='..VecToStr(spawnPos))
                        Slime.slimes.colony.spawn(spawnPos, true)
                    end
                    -- Detach from body / reset eat timer
                    TimerResetTime(slime.timers.eatDelay)
                    dbp('SLIME '.. slime.id ..' timer reset to '..slime.timers.eatDelay.time..', delay='..slime.timers.eatDelay.delay)
                    Slime.slimes.physics.sticky.resetShape(slime)

                end

            end
            -- If eat failed, slime dies
            if not eatSuccess then
                -- If eat failed, slime dies
                Slime.slimes.colony.markSlimeForRemoval(slime)
            end

        end

    end

end



Slime.slimes.effects = {}

function Slime.slimes.effects.process()

    -- Render slimes.
    for i = 1, #Slime.slimesList do

        local slime = Slime.slimesList[i]
        
        if regGetBool('tool.slime.renderSlimeParticles') then
            Slime.slimes.effects.renderSlimeIdle(slime.tr.pos) -- Idle slime
        end

    end


    -- Render slime projectiles.
    for i = 1, #projectiles do

        local proj = projectiles[i]
        DrawDot(proj.transform.pos, 0.2,0.2, 130/255,122/255,248/255, 1) -- Slime projectile

    end

end

--- Draw ui dots instead of particles based on.
function Slime.slimes.effects.renderSlimesIdleSimple()

    do UiPush()

        local camTr = GetCameraTransform()

        for i = 1, #Slime.slimesList do

            local slime = Slime.slimesList[i]
            local dist = VecDist(camTr.pos, slime.tr.pos)
            local s = (90/dist)

            -- Check if slime is in front of the camera.
            if TransformToLocalPoint(camTr, slime.tr.pos)[3] < 0 then

                local slimePx, slimePy = UiWorldToPixel(slime.tr.pos)

                do UiPush()

                    UiColor(130/255,122/255,248/255, 0.5)
                    UiTranslate(slimePx, slimePy)
                    -- UiImage('ui/common/dot.png')
                    UiRect(s,s)

                UiPop() end

            end

        end

    UiPop() end

end

function Slime.slimes.effects.renderSlimeIdle(pos)

    ParticleReset()

    -- Idle slime particles.
    ParticleType("smoke")
    ParticleTile(4)
    ParticleColor(130/255,122/255,248/255)
    ParticleRadius(0.08)
    ParticleAlpha(0.8)
    ParticleGravity(0)
    ParticleDrag(0)
    ParticleEmissive(1)
    ParticleRotation(0)
    ParticleStretch(0)
    ParticleSticky(0)
    ParticleCollide(0)

    SpawnParticle(pos, Vec(), 0.05)

end