local battleGeneral = require("scripts/battleGeneral")

local tilemap

local weirdGoop = {}


weirdGoop.fillColor = Color.black.. 0.875
weirdGoop.outlineColor = Color.fromHexRGB(0xFB2F92)


weirdGoop.activeParticles = {}
weirdGoop.inactiveParticles = {}

weirdGoop.formationEffects = {}
weirdGoop.destructionEffects = {}

weirdGoop.activeParticleCount = 0


weirdGoop.destructionParticleDisappearTime = 1024
weirdGoop.destructionParticleDisappearTimeUnfancy = 512

weirdGoop.destructionParticleUnfancyCountFactor = 0.5


weirdGoop.OUTLINE_COLOR = {
    PINK = 1,
    YELLOW = 2,
    CYAN = 3,
    PURPLE = 4,
}


local PARTICLE_STATE = {
    FORMATION = {
        MOVE = 0,
        STABILISE = 1,
        STABLE = 2,
    },
    DESTRUCTION = {
        WAIT = 3,
        FALL = 4,
    },
}


local particleShader = Shader.fromFile("maskCollection/resources/weirdGoop_particle.vert","maskCollection/resources/weirdGoop_particle.frag")
local finalShader = Shader.fromFile(nil,"maskCollection/resources/weirdGoop_final.frag")

local formationShader = Shader.fromFile(nil,"maskCollection/resources/weirdGoop_formation.frag")

local outlineColorLookupImage = Graphics.loadImageResolved("maskCollection/resources/weirdGoop_outline.png")


local twoPiTimesInvGoldenRatioSquared = 2*math.pi/((math.sqrt(5) + 1)*0.5)^2

local sectionBottomY


local destructionParticleDisappearTime = weirdGoop.destructionParticleDisappearTime
local destructionParticleDisintegrateTime = 80


local particleStateFuncs = {}

do
    -- Formation states
    particleStateFuncs[PARTICLE_STATE.FORMATION.MOVE] = function(particle)
        particle.speedY = particle.speedY + particle.gravity

        particle.x = particle.x + particle.speedX
        particle.y = particle.y + particle.speedY

        particle.stateTimer = particle.stateTimer + 1

        particle.disintegration = particle.formationEffect.disintegration

        if not particle.isRecycled then
            particle.radius = math.lerp(2,6,math.min(1,particle.stateTimer/particle.formationEffect.moveTime*1.1))
        end

        if particle.stateTimer >= particle.formationEffect.moveTime then
            particle.state = PARTICLE_STATE.FORMATION.STABILISE
            particle.stateTimer = 0

            particle.x = particle.goalX
            particle.y = particle.goalY
        end
    end

    particleStateFuncs[PARTICLE_STATE.FORMATION.STABILISE] = function(particle)
        particle.stateTimer = particle.stateTimer + 1

        local elasticTime = math.min(1,particle.stateTimer/particle.formationEffect.stabiliseTime)
        local elastic = math.sin(elasticTime*math.pi*5)*(1 - elasticTime)

        particle.x = particle.goalX + elastic*particle.speedX*1.5
        particle.y = particle.goalY + elastic*particle.speedY*1.5

        particle.disintegration = particle.formationEffect.disintegration

        if particle.stateTimer >= particle.formationEffect.stabiliseTime then
            particle.state = PARTICLE_STATE.FORMATION.STABLE
            particle.stateTimer = 0
        end
    end

    particleStateFuncs[PARTICLE_STATE.FORMATION.STABLE] = function(particle)
        particle.disintegration = particle.formationEffect.disintegration
    end

    -- Destruction states
    particleStateFuncs[PARTICLE_STATE.DESTRUCTION.WAIT] = function(particle)
        particle.disintegration = math.max(0,particle.disintegration - 1/particle.disintegrateTime)
        particle.stateTimer = particle.stateTimer - 1

        if particle.stateTimer <= 0 then
            particle.state = PARTICLE_STATE.DESTRUCTION.FALL
            particle.stateTimer = 0
        end
    end

    local function moveParticle(particle,speedFactor)
        local dx = particle.speedX*speedFactor
        local dy = particle.speedY*speedFactor

        local hit,hitPointX,hitPointY,hitNormalX,hitNormalY = tilemap.lineCast(particle.x,particle.y,particle.x + dx,particle.y + dy)

        if hit then
            local dot = particle.speedX*hitNormalX + particle.speedY*hitNormalY

            particle.speedX = (-2*dot*hitNormalX + particle.speedX)*0.8
            particle.speedY = (-2*dot*hitNormalY + particle.speedY)*0.8

            particle.x = hitPointX
            particle.y = hitPointY
        else
            particle.x = particle.x + dx
            particle.y = particle.y + dy
        end
    end

    particleStateFuncs[PARTICLE_STATE.DESTRUCTION.FALL] = function(particle)
        particle.disintegration = math.max(0,particle.disintegration - 1/particle.disintegrateTime)

        particle.stateTimer = particle.stateTimer + 1

        local speedFactor = math.min(1,particle.stateTimer*0.2)

        particle.speedY = particle.speedY + particle.gravity*speedFactor

        moveParticle(particle,speedFactor)

        if particle.y >= sectionBottomY then
            particle.toRemove = true
            return
        end

        if particle.stateTimer >= destructionParticleDisappearTime then
            particle.toRemove = true
        elseif particle.stateTimer >= (destructionParticleDisappearTime - destructionParticleDisintegrateTime) then
            particle.disintegration = 1 - math.min(1,(destructionParticleDisappearTime - particle.stateTimer)/destructionParticleDisintegrateTime)
        end
    end
end


local function updateFormationEffect(formationEffect)
    formationEffect.timer = math.max(0,formationEffect.timer - 1)
    formationEffect.disintegration = 1 - math.min(1,formationEffect.timer/formationEffect.disintegrateTime)

    if formationEffect.timer == 0 and not formationEffect.neverRemove then
        formationEffect.toRemove = true
    end
end

local function updateDestructionEffect(destructionEffect)
    destructionEffect.disintegration = math.max(0,destructionEffect.disintegration - 1/destructionEffect.disintegrateTime)
    destructionEffect.toRemove = (destructionEffect.disintegration == 0)
end


local function createParticle()
    local particle = table.remove(weirdGoop.inactiveParticles) or {}

    particle.toRemove = false
    particle.isRecycled = false

    table.insert(weirdGoop.activeParticles,particle)

    weirdGoop.activeParticleCount = weirdGoop.activeParticleCount + 1

    return particle
end

local function recycleParticles(x,y,radius,particleCount,outlineColor)
    if particleCount == 0 then
        return {}
    end

    local squareRadius = radius*radius
    local recycledParticles = {}

    for i = #weirdGoop.activeParticles,1,-1 do
        local particle = weirdGoop.activeParticles[i]

        if particle.state == PARTICLE_STATE.DESTRUCTION.FALL and particle.outlineColor == outlineColor and particle.disintegration == 0 then
            local distX = particle.x - x
            local distY = particle.y - y
            local squareDistance = distX*distX + distY*distY

            if squareDistance < squareRadius then
                particle.isRecycled = true

                table.insert(recycledParticles,particle)
                particleCount = particleCount - 1

                if particleCount == 0 then
                    break
                end
            end
        end
    end

    return recycledParticles
end

local function createParticlesFromClumps(clumps,countFactor,recycleRadius,outlineColor)
    local particles = {}

    for _,clump in ipairs(clumps) do
        local particleCount = math.max(1,math.floor(clump.radius*clump.radius*math.pi*countFactor + 0.5))
        local boundaryParticleCount = math.floor(math.sqrt(particleCount) + 0.5)

        local randomAngleOffset = RNG.random(0,math.pi*2)

        local recycledParticles = recycleParticles(clump.x,clump.y,recycleRadius,particleCount,outlineColor)

        for i = 1,particleCount do
            local particle = table.remove(recycledParticles) or createParticle()

            local distance = clump.radius
            if i <= (particleCount - boundaryParticleCount) then
                distance = distance*math.sqrt(i - 0.5)/math.sqrt(particleCount - (boundaryParticleCount + 1)*0.5)
            end

            local angle = i*twoPiTimesInvGoldenRatioSquared + randomAngleOffset

            local offsetX = math.cos(angle)*(1 + (clump.eccentricity or 0))*distance
            local offsetY = math.sin(angle)*(1 - (clump.eccentricity or 0))*distance

            if clump.rotation ~= nil then
                local clumpSin = math.sin(math.rad(clump.rotation))
                local clumpCos = math.cos(math.rad(clump.rotation))

                offsetX,offsetY = offsetX*clumpCos - offsetY*clumpSin,offsetX*clumpSin + offsetY*clumpCos
            end

            particle.clumpX = clump.x + offsetX
            particle.clumpY = clump.y + offsetY

            particle.clump = clump

            table.insert(particles,particle)
        end
    end

    return particles
end

function weirdGoop.createFormationEffect(args)
    local formationEffect = {}

    formationEffect.priority = args.priority or -46

    formationEffect.moveTime = args.moveTime or 32
    formationEffect.stabiliseTime = args.stabiliseTime or 32
    formationEffect.disintegrateTime = args.disintegrateTime or 24

    formationEffect.disintegration = 0

    formationEffect.timer = formationEffect.moveTime + formationEffect.stabiliseTime + formationEffect.disintegrateTime

    formationEffect.finishCallback = args.finishCallback

    formationEffect.toRemove = false

    formationEffect.neverRemove = args.neverRemove or false
    formationEffect.id = args.id or 0

    -- Set up images
    formationEffect.images = args.images

    -- Create particles
    formationEffect.particles = createParticlesFromClumps(args.clumps,args.countFactor or 0.06,args.particleRecycleRadius or 256,args.outlineColor or 1)

    for _,particle in ipairs(formationEffect.particles) do
        particle.state = PARTICLE_STATE.FORMATION.MOVE
        particle.stateTimer = 0

        particle.goalX = particle.clumpX
        particle.goalY = particle.clumpY

        if not particle.isRecycled then
            particle.x = particle.clump.x + RNG.random(-particle.clump.radius - 16,particle.clump.radius + 16)
            particle.y = (tilemap.snapDown(particle.clump.x,particle.clump.y) or particle.clump.y) + RNG.random(16,48)
        end

        particle.gravity = 0.3

        particle.speedX = (particle.goalX - particle.x)/formationEffect.moveTime
        particle.speedY = (particle.goalY - particle.y)/formationEffect.moveTime - particle.gravity*formationEffect.moveTime*0.5

        particle.formationEffect = formationEffect

        particle.radius = (particle.isRecycled and 6) or 0
        particle.disintegration = 0

        particle.outlineColor = args.outlineColor or 1
    end


    table.insert(weirdGoop.formationEffects,formationEffect)

    return formationEffect
end

function weirdGoop.createDestructionEffect(args)
    local destructionEffect = {}

    destructionEffect.priority = args.priority or -46

    destructionEffect.disintegrateTime = args.disintegrateTime or 8
    destructionEffect.disintegration = 1

    -- Set up images
    destructionEffect.images = args.images

    -- Create particles
    local speedDirection = args.speedDirection or vector(0,-1)
    local countFactor = args.countFactor or 0.05

    if battleGeneral.fancyEffectsDisabled() then
        countFactor = countFactor*weirdGoop.destructionParticleUnfancyCountFactor
    end

    destructionEffect.particles = createParticlesFromClumps(args.clumps,countFactor,0,args.outlineColor or 1)

    for _,particle in ipairs(destructionEffect.particles) do
        local hit,hitX,hitY,_,_ = tilemap.lineCast(particle.clump.x,particle.clump.y,particle.clumpX,particle.clumpY)

        if hit then
            particle.x = hitX
            particle.y = hitY
        else
            particle.x = particle.clumpX
            particle.y = particle.clumpY
        end

        if args.particleWaitTime ~= nil then
            particle.state = PARTICLE_STATE.DESTRUCTION.WAIT
            particle.stateTimer = args.particleWaitTime
        else
            particle.state = PARTICLE_STATE.DESTRUCTION.FALL
            particle.stateTimer = 0
        end

        particle.gravity = RNG.random(0.2,0.3)

        local speedAngle = RNG.random(0,math.pi*2)
        local speedRadius = math.sqrt(RNG.random())*(args.particleSpeed or 4)

        particle.speedX = (math.cos(speedAngle) + speedDirection.x)*speedRadius
        particle.speedY = (math.sin(speedAngle) + speedDirection.y)*speedRadius

        particle.disintegrateTime = destructionEffect.disintegrateTime

        particle.radius = 5
        particle.disintegration = 1

        particle.outlineColor = args.outlineColor or 1
    end


    table.insert(weirdGoop.destructionEffects,destructionEffect)

    return destructionEffect
end


function weirdGoop.onTick()
    if battleGeneral.fancyEffectsDisabled() then
        destructionParticleDisappearTime = weirdGoop.destructionParticleDisappearTimeUnfancy
    else
        destructionParticleDisappearTime = weirdGoop.destructionParticleDisappearTime
    end

    sectionBottomY = Section(maskCollection.section).origBoundary.bottom

    -- Update formation effects
    local i = 1

    while (weirdGoop.formationEffects[i] ~= nil) do
        local formationEffect = weirdGoop.formationEffects[i]

        updateFormationEffect(formationEffect)

        if formationEffect.toRemove then
            for _,particle in ipairs(formationEffect.particles) do
                particle.toRemove = true
            end

            if formationEffect.finishCallback ~= nil then
                formationEffect.finishCallback()
            end

            table.remove(weirdGoop.formationEffects,i)
        else
            i = i + 1
        end
    end

    -- Update destruction effects
    local i = 1

    while (weirdGoop.destructionEffects[i] ~= nil) do
        local destructionEffect = weirdGoop.destructionEffects[i]

        updateDestructionEffect(destructionEffect)

        if destructionEffect.toRemove then
            table.remove(weirdGoop.destructionEffects,i)
        else
            i = i + 1
        end
    end

    -- Update particles
    i = 1

    while (weirdGoop.activeParticles[i] ~= nil) do
        local particle = weirdGoop.activeParticles[i]

        if particleStateFuncs[particle.state] ~= nil then
            particleStateFuncs[particle.state](particle)
        end

        if particle.toRemove then
            table.insert(weirdGoop.inactiveParticles,particle)
            table.remove(weirdGoop.activeParticles,i)

            weirdGoop.activeParticleCount = weirdGoop.activeParticleCount - 1
        else
            i = i + 1
        end
    end
end


local particleBuffer

local particleVC = {}
local particleTC = {}
local particleDisintegation = {}
local particleOutlineColor = {}

local particleDrawArgs = {
    vertexCoords = particleVC,
    textureCoords = particleTC,
    shader = particleShader,
    uniforms = {},
    attributes = {
        disintegration = particleDisintegation,
        outlineColorIndex = particleOutlineColor,
    },
    priority = -101,
}

local function drawParticles(cam)
    if particleBuffer == nil then
        local width,height = Graphics.getMainFramebufferSize()

        particleBuffer = Graphics.CaptureBuffer(math.ceil(width*0.5) + 1,math.ceil(height*0.5) + 1)
        particleDrawArgs.target = particleBuffer
    end

    local cameraHalfWidth = math.ceil(camera.width*0.5)
    local cameraHalfHeight = math.ceil(camera.height*0.5)

    local cameraX = math.floor(cam.x)
    local cameraY = math.floor(cam.y)
    local halfCameraX = math.floor(cameraX*0.5)
    local halfCameraY = math.floor(cameraY*0.5)
    
    local vertexCounter = 0
    local attributeCounter = 0

    for _,particle in ipairs(weirdGoop.activeParticles) do
        local x1 = math.floor(particle.x*0.5 + 0.5) - halfCameraX - math.floor(particle.radius*0.5)
        local y1 = math.floor(particle.y*0.5 + 0.5) - halfCameraY - math.floor(particle.radius*0.5)
        local x2 = x1 + particle.radius
        local y2 = y1 + particle.radius

        if x2 > 0 and x1 < cameraHalfWidth and y2 > 0 and y1 < cameraHalfHeight and particle.disintegration < 1 then
            -- Vertex coords
            particleVC[vertexCounter + 1 ] = x1 -- top left
            particleVC[vertexCounter + 2 ] = y1
            particleVC[vertexCounter + 3 ] = x2 -- top right
            particleVC[vertexCounter + 4 ] = y1
            particleVC[vertexCounter + 5 ] = x1 -- bottom left
            particleVC[vertexCounter + 6 ] = y2
            particleVC[vertexCounter + 7 ] = x2 -- top right
            particleVC[vertexCounter + 8 ] = y1
            particleVC[vertexCounter + 9 ] = x1 -- bottom left
            particleVC[vertexCounter + 10] = y2
            particleVC[vertexCounter + 11] = x2 -- bottom right
            particleVC[vertexCounter + 12] = y2

            -- Texture coords
            local x1 = particle.x - particle.radius
            local y1 = particle.y - particle.radius
            local x2 = particle.x + particle.radius
            local y2 = particle.y + particle.radius

            particleTC[vertexCounter + 1 ] = 0 -- top left
            particleTC[vertexCounter + 2 ] = 0
            particleTC[vertexCounter + 3 ] = 1 -- top right
            particleTC[vertexCounter + 4 ] = 0
            particleTC[vertexCounter + 5 ] = 0 -- bottom left
            particleTC[vertexCounter + 6 ] = 1
            particleTC[vertexCounter + 7 ] = 1 -- top right
            particleTC[vertexCounter + 8 ] = 0
            particleTC[vertexCounter + 9 ] = 0 -- bottom left
            particleTC[vertexCounter + 10] = 1
            particleTC[vertexCounter + 11] = 1 -- bottom right
            particleTC[vertexCounter + 12] = 1

            -- Attributes
            local outlineColorIndex = (particle.outlineColor - 0.5)/outlineColorLookupImage.width

            for i = 1,6 do
                attributeCounter = attributeCounter + 1

                particleDisintegation[attributeCounter] = particle.disintegration
                particleOutlineColor[attributeCounter] = outlineColorIndex
            end

            vertexCounter = vertexCounter + 12
        end
    end

    if vertexCounter <= 0 then
        return
    end

    -- Clean up leftover data
    vertexCounter = vertexCounter + 1
    attributeCounter = attributeCounter + 1

    while (particleVC[vertexCounter] ~= nil) do
        particleVC[vertexCounter] = nil
        particleTC[vertexCounter] = nil

        vertexCounter = vertexCounter + 1
    end

    while (particleDisintegation[attributeCounter] ~= nil) do
        particleDisintegation[attributeCounter] = nil
        particleOutlineColor[attributeCounter] = nil

        attributeCounter = attributeCounter + 1
    end

    -- Render everything
    particleBuffer:clear(-101)

    Graphics.glDraw(particleDrawArgs)

    Graphics.drawBox{
        texture = particleBuffer,priority = -94,
        x = 0,
        y = 0,
        width = cam.width,
        height = cam.height,
        sourceWidth = cam.width*0.5,
        sourceHeight = cam.height*0.5,
        sourceX = (cameraX % 2)*0.5,
        sourceY = (cameraY % 2)*0.5,

        shader = finalShader,uniforms = {
            noiseTexture = Graphics.sprites.hardcoded["53-1"].img,
            noiseScale = vector(4/cam.width,4/cam.height),
            noiseOffset = vector(cameraX,cameraY),

            outlineColorLookup = outlineColorLookupImage,

            fillColor = weirdGoop.fillColor,

            bufferSize = vector(particleBuffer.width,particleBuffer.height),
        },
    }
end


local function drawEffect(cam,effect)
    if effect.disintegration == 0 then
        return
    end

    for _,image in ipairs(effect.images) do
        Graphics.drawBox{
            texture = image.texture,
            priority = effect.priority,
            sceneCoords = true,
            centred = true,

            x = image.x,
            y = image.y,
            sourceX = image.sourceX,
            sourceY = image.sourceY,
            sourceWidth = image.width,
            sourceHeight = image.height,

            rotation = image.rotation,

            shader = formationShader,uniforms = {
                noiseTexture = Graphics.sprites.hardcoded["53-1"].img,
                noiseScale = vector(4/cam.width,4/cam.height),
                noiseOffset = vector(cam.x,cam.y),

                disintegration = effect.disintegration,
            },
        }
    end
end


function weirdGoop.onCameraDraw(camIdx)
    local cam = Camera(camIdx)

    drawParticles(cam)

    for _,formationEffect in ipairs(weirdGoop.formationEffects) do
        drawEffect(cam,formationEffect)
    end
    
    for _,destructionEffect in ipairs(weirdGoop.destructionEffects) do
        drawEffect(cam,destructionEffect)
    end

    --Text.print(weirdGoop.activeParticleCount,32,32)
end


function weirdGoop.onInitAPI()
    registerEvent(weirdGoop,"onTick")
    registerEvent(weirdGoop,"onCameraDraw")

    maskCollection = require("maskCollection/scripts/maskCollection")
    tilemap = require("maskCollection/scripts/tilemap")
end


return weirdGoop