local easing = require "lib.utils.easing"

--- @alias voidCallback fun(): nil
--- @alias animationRunner fun(node: AnimationNode)

--- Узел дерева анимаций.
---
--- Отслеживает завершение всех анимаций всех дочерних узлов и оповещает вышестоящий узел.
---
--- Дочерние узлы одного уровня запускают свою анимацию одновременно после завершения анимации родителя.
--- Example:
--- ```lua
---     AnimationNode {
---         function (node) residentsleeper:sleep(1000, node) end, -- must pass itself down as the parameter
---         onEnd = function() print("completed") end,
---         children = { -- children run in parallel after the parent animation is completed
---             AnimationNode {
---                 function (node) sprite:animate("attack", node) end
---             },
---             AnimationNode {
---                 function (node) other_sprite:animate("hurt", node) end
---             },
---         }
---     }:run()
--- ```
--- @class AnimationNode
--- @field count integer
--- @field run animationRunner
--- @field parent AnimationNode?
--- @field children AnimationNode[]
--- @field finish voidCallback
--- @field onEnd voidCallback?
--- @field duration number продолжительность в миллисекундах
--- @field easing ease функция смягчения
--- @field t number прогресс анимации
--- @field state "running" | "waiting" | "finished"
local animation = {}
animation.__index = animation

--- Регистрация завершения дочерней анимации
function animation:bubbleUp()
    self.count = self.count - 1
    if self.count > 0 then return end
    self.state = "finished"
    if self.onEnd then self.onEnd() end
    if self.parent then self.parent:bubbleUp() end
end

--- @param children AnimationNode[]
--- Запланировать анимации после текущей, которые запустятся одновременно друг с другом
function animation:chain(children)
    for _, child in ipairs(children) do
        child.parent = self
        table.insert(self.children, child)
        self.count = self.count + 1
    end
    return self
end

--- Возвращает текущий прогресс анимации с учетом смягчения
function animation:getValue()
    return self.easing(self.t)
end

function animation:update(dt)
    if self.state ~= "running" then return end

    if self.t < 1 then
        self.t = self.t + dt * 1000 / self.duration -- в знаменателе продолжительность анимации в секундах
    else
        self.t = 1
        self:finish()
    end
end

--- @param data {[1]: animationRunner?, onEnd?: voidCallback, duration: number?, easing: ease?, children?: AnimationNode[]}
--- @return AnimationNode
local function new(data)
    local t = setmetatable({}, animation)
    t.run = data[1] or function(self)
        self:finish()
    end
    t.onEnd = data.onEnd
    t.count = 1 -- своя анимация
    t.children = {}
    t:chain(data.children or {})
    t.duration = data.duration or 1000
    t.easing = data.easing or easing.linear
    t.t = 0
    t.state = "running"
    t.finish = function()
        t.state = "waiting"
        t:bubbleUp()
        for _, anim in ipairs(t.children) do
            anim:run()
        end
    end
    return t
end

return new
