-- Document with emmy lua: https://emmylua.github.io/

-- Create an APL trait for the APL class
---@class APLTrait
local APLTrait = {}
APLTrait.__index = APLTrait

-- Constructor
---@param cb fun():boolean
---@return APLTrait
function APLTrait:New(cb)
    local self = setmetatable({}, APLTrait)

    self.cb = cb
    self.lastcall = 0

    return self
end

-- Evaulate the APL trait
---@return boolean
function APLTrait:Evaluate()
    if GetTime() - self.lastcall > 0.1 then
        self.lastresult = self.cb()
        self.lastcall = GetTime()
        return self.lastresult
    end

    return self.lastresult
end

-- tostring
---@return string
function APLTrait:__tostring()
    return "Bastion.__APLTrait"
end

-- Create an APL actor for the APL class
---@class APLActor
local APLActor = {}
APLActor.__index = APLActor

-- Constructor
---@param actor table
function APLActor:New(actor)
    local self = setmetatable({}, APLActor)

    self.actor = actor
    self.traits = {}

    return self
end

-- Add a trait to the APL actor
---@param ... APLTrait
---@return APLActor
function APLActor:AddTraits(...)
    for _, trait in ipairs({ ... }) do
        table.insert(self.traits, trait)
    end

    return self
end

-- Get the actor
---@return table
function APLActor:GetActor()
    return self.actor
end

-- Evaulate the APL actor
---@return boolean
function APLActor:Evaluate()
    for _, trait in ipairs(self.traits) do
        if not trait:Evaluate() then
            return false
        end
    end

    return true
end

-- Execute
function APLActor:Execute()
    if self:GetActor().apl then
        if self:GetActor().condition and self:GetActor().condition() then
            -- print("Bastion: APL:Execute: Executing sub APL " .. self:GetActor().apl.name)
            self:GetActor().apl:Execute()
        end
    end
    if self:GetActor().spell then
        if self:GetActor().condition then
            -- print("Bastion: APL:Execute: Condition for spell " .. self:GetActor().spell:GetName())
            self:GetActor().spell:CastableIf(self:GetActor().castableFunc):Cast(self:GetActor().target,
                self:GetActor().condition)
        end

        -- print("Bastion: APL:Execute: No condition for spell " .. self:GetActor().spell:GetName())
        self:GetActor().spell:CastableIf(self:GetActor().castableFunc):Cast(self:GetActor().target)
    end
    if self:GetActor().item then
        if self:GetActor().condition then
            -- print("Bastion: APL:Execute: Condition for spell " .. self:GetActor().spell:GetName())
            self:GetActor().item:UsableIf(self:GetActor().usableFunc):Use(self:GetActor().target,
                self:GetActor().condition)
        end

        -- print("Bastion: APL:Execute: No condition for spell " .. self:GetActor().spell:GetName())
        self:GetActor().item:UsableIf(self:GetActor().usableFunc):Use(self:GetActor().target)
    end
    if self:GetActor().action then
        -- print("Bastion: APL:Execute: Executing action " .. self:GetActor().action)
        self:GetActor().cb(self)
    end
    if self:GetActor().variable then
        -- print("Bastion: APL:Execute: Setting variable " .. self:GetActor().variable)
        self:GetActor()._apl.variables[self:GetActor().variable] = self:GetActor().cb(self:GetActor()._apl)
    end
end

-- has traits
---@return boolean
function APLActor:HasTraits()
    return #self.traits > 0
end

-- tostring
---@return string
function APLActor:__tostring()
    return "Bastion.__APLActor"
end

-- APL (Attack priority list) class
---@class APL
local APL = {}
APL.__index = APL

-- Constructor
---@param name string
---@return APL
function APL:New(name)
    local self = setmetatable({}, APL)

    self.apl = {}
    self.variables = {}
    self.name = name

    return self
end

-- Add a variable to the APL
---@param name string
---@param value any
function APL:SetVariable(name, value)
    self.variables[name] = value
end

-- Get and evaluate a variable
---@param name string
---@return boolean
function APL:GetVariable(name)
    return self.variables[name]
end

-- Add variable
---@param name string
---@param cb fun(...):any
---@return APLActor
function APL:AddVariable(name, cb)
    local actor = APLActor:New({ variable = name, cb = cb, _apl = self })
    table.insert(self.apl, actor)
    return actor
end

-- Add a manual action to the APL
---@param action string
---@param cb fun(...):any
---@return APLActor
function APL:AddAction(action, cb)
    local actor = APLActor:New({ action = action, cb = cb })
    table.insert(self.apl, actor)
    return actor
end

-- Add a spell to the APL
---@param spell Spell
---@param condition fun(...):boolean
---@return APLActor
function APL:AddSpell(spell, condition)
    local castableFunc = spell.CastableIfFunc
    local target = spell:GetTarget()

    local actor = APLActor:New({ spell = spell, condition = condition, castableFunc = castableFunc, target = target })

    table.insert(self.apl, actor)

    return actor
end

-- Add an item to the APL
---@param item Item
---@param condition fun(...):boolean
---@return APLActor
function APL:AddItem(item, condition)
    local usableFunc = item.UsableIfFunc
    local target = item:GetTarget()

    local actor = APLActor:New({ item = item, condition = condition, usableFunc = usableFunc, target = target })

    table.insert(self.apl, actor)

    return actor
end

-- Add an APL to the APL (for sub APLs)
---@param apl APL
---@param condition fun(...):boolean
---@return APLActor
function APL:AddAPL(apl, condition)
    if not condition then
        error("Bastion: APL:AddAPL: No condition for APL " .. apl.name)
    end
    local actor = APLActor:New({ apl = apl, condition = condition })
    table.insert(self.apl, actor)
    return actor
end

-- Execute the APL
function APL:Execute()
    for _, actor in ipairs(self.apl) do
        if actor:HasTraits() and actor:Evaluate() then
            actor:Execute()
        else
            actor:Execute()
        end
    end
end

-- tostring
---@return string
function APL:__tostring()
    return "Bastion.__APL(" .. self.name .. ")"
end

return APL, APLActor, APLTrait