--- Алгоритм обработки заклинания (for dummies):
--- 1) ПОКА выделен персонаж И он находится в режиме каста, вызывать spell:update() и spell:draw() каждый кадр (это отвечает за обработку и отрисовку превьюшки каста, например, превью пути или зоны поражения; реализуется через установку spellcaster.cast, см. код в кнопке)
---     ЕСЛИ выбран тайл, ТО вызвать spell:cast() (это запрос на обработку последствий применения заклинания, например, старт анимации ходьбы, выпуск снаряда и т.д.; реализовано в selector)
---         ЕСЛИ spell:cast() ИСТИНА, ТО вызвать selector:lock() (отключить обработку выделения всего на уровне; реализовано в selector)
---
--- 2) ПОКА анимация каста НЕ завершена, ничего не делать, ИНАЧЕ вызвать behaviors.spellcaster:endCast() (вот это сейчас нужно вызывать самостоятельно, т.к. нет возможности обобщенно отследить завершение анимаций)
--- --TODO: каждый каст должен возвращать объект, который позволит отследить момент завершения анимации спелла
--- Да, это Future/Promise/await/async

local AnimationNode = require "lib.animation_node"

--- @class Spell Здесь будет много бойлерплейта, поэтому тоже понадобится спеллмейкерский фреймворк, который просто вернет готовый Spell
--- @field tag string
--- @field update fun(self: Spell, caster: Character, dt: number): nil Изменяет состояние спелла
--- @field draw fun(self: Spell): nil Рисует превью каста, ничего не должна изменять в идеальном мире
--- @field cast fun(self: Spell, caster: Character, target: Vec3): boolean Вызывается в момент каста, изменяет мир. Возвращает bool в зависимости от того, получилось ли скастовать
local spell = {}
spell.__index = spell
spell.tag = "base"

function spell:update(caster, dt) end

function spell:draw() end

function spell:cast(caster, target) return true end

local walk = setmetatable({
    --- @type Deque
    path = nil
}, spell)
walk.tag = "dev_move"

function walk:cast(caster, target)
    if not caster:try(Tree.behaviors.stats, function(stats)
            return stats.mana >= 2
        end) then
        return false
    end

    local path = self.path
    path:pop_front()
    if path:is_empty() then return false end

    for p in path:values() do print(p) end

    caster:try(Tree.behaviors.stats, function(stats)
        stats.mana = stats.mana - 2
        print(stats.mana)
    end)

    local sprite = caster:has(Tree.behaviors.sprite)
    if not sprite then return true end
    AnimationNode {
        function(node) caster:has(Tree.behaviors.map):followPath(path, node) end,
        onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,
    }:run()

    return true
end

function walk:update(caster, dt)
    local charPos = caster:has(Tree.behaviors.map).position:floor()
    --- @type Vec3
    local mpos = Tree.level.camera:toWorldPosition(Vec3 { love.mouse.getX(), love.mouse.getY() }):floor()
    self.path = require "lib.pathfinder" (charPos, mpos)
end

function walk:draw()
    if not self.path then return end
    --- Это отрисовка пути персонажа к мышке
    love.graphics.setColor(0.6, 0.75, 0.5)
    for p in self.path:values() do
        love.graphics.circle("fill", p.x + 0.45, p.y + 0.45, 0.1)
    end
    love.graphics.setColor(1, 1, 1)
end

local regenerateMana = setmetatable({}, spell)
regenerateMana.tag = "dev_mana"

function regenerateMana:cast(caster, target)
    caster:try(Tree.behaviors.stats, function(stats)
        stats.mana = 10
        stats.initiative = stats.initiative + 10
    end)
    print(caster.id, "has regenerated mana and gained initiative")
    local sprite = caster:has(Tree.behaviors.sprite)
    if not sprite then return true end
    AnimationNode {
        function(node)
            sprite:animate("hurt", node)
        end,
        onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end
    }:run()

    return true
end

local attack = setmetatable({}, spell)
attack.tag = "dev_attack"

function attack:cast(caster, target)
    if caster:try(Tree.behaviors.map, function(map)
            local dist = math.max(math.abs(map.position.x - target.x), math.abs(map.position.y - target.y))
            print("dist:", dist)
            return dist > 2
        end) then
        return false
    end

    caster:try(Tree.behaviors.stats, function(stats)
        stats.mana = stats.mana - 2
    end)

    --- @type Character
    local targetCharacterId = Tree.level.characterGrid:get(target)
    if not targetCharacterId or targetCharacterId == caster.id then return false end
    local targetCharacter = Tree.level.characters[targetCharacterId]
    targetCharacter:try(Tree.behaviors.stats, function(stats)
        stats.hp = stats.hp - 4
    end)

    local sprite = caster:has(Tree.behaviors.sprite)
    local targetSprite = targetCharacter:has(Tree.behaviors.sprite)
    if not sprite or not targetSprite then return true end

    caster:try(Tree.behaviors.map, function(map) map:lookAt(target) end)

    AnimationNode {
        onEnd = function() caster:has(Tree.behaviors.spellcaster):endCast() end,
        children = {
            AnimationNode {
                function(node)
                    sprite:animate("attack", node)
                end
            },
            AnimationNode {
                function(node)
                    targetCharacter:has(Tree.behaviors.residentsleeper):sleep(200, node)
                end,
                children = {
                    AnimationNode {
                        function(node)
                            targetSprite:animate("hurt", node)
                        end
                    }
                }
            }
        }
    }:run()

    return true
end

----------------------------------------
local spellbook = {
    walk = walk,
    regenerateMana = regenerateMana,
    attack = attack
}

--- Создает новый спеллбук с уникальными спеллами (а не ссылками на шаблоны)
--- @param list Spell[]
function spellbook.of(list)
    local spb = {}
    for i, sp in ipairs(list) do
        spb[i] = setmetatable({}, { __index = sp })
    end
    return spb
end

return spellbook
