### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
###
### This file is responsible for managing the animal modules and contains functions
### and types integrating the nature submodel with the rest of Persefone.
###


## An enum used to assign a sex to each animal
@enum Sex hermaphrodite male female
#XXX hermaphrodite probably is not needed and its implementation is iffy

"""
    Animal

This is the generic agent type for all animals. Individual species are created
using the [`@species`](@ref) macro. In addition to user-defined, species-specific
fields, all species contain the following fields:

- `id` An integer unique identifier for this individual.
- `sex` male, female, or hermaphrodite.
- `parents` The IDs of the individual's parents.
- `pos` An (x, y) coordinate tuple.
- `age` The age of the individual in days.
- `phase` The update function to be called during the individual's current life phase.
- `energy` A [DEBparameters](@ref) struct for calculating energy budgets.
- `offspring` A vector containing the IDs of an individual's children.
"""
abstract type Animal <: ModelAgent end

"""
    speciesof(animal)

Return the species name of this animal as a string.
"""
function speciesof(a::Union{Animal,Type})
    # strip out the module name if necessary (`Persefone.<species>`)
    (a isa Animal) && (a = typeof(a))
    spstrings = split(string(a), ".")
    length(spstrings) == 1 ? spstrings[1] : spstrings[2]
end

"""
    speciestype(name)

Return the Type of this species.
"""
function speciestype(name::String)
    # get the species Type from its namestring by looking in the module namespace
    speciestype = nothing
    for var in names(Persefone, all=true)
        if string(var) == name && getfield(Persefone, var) isa Type
            speciestype = getfield(Persefone, var)
            break
        end
    end
    isnothing(speciestype) && @error("Species $name is not defined.")
    speciestype
end
    
"""
    animalid(animal)

A small utility function to return a string with the species name and ID of an animal.
"""
function animalid(a::Animal)
    return "$(speciesof(a)) $(a.id)"
end

"""
    create!(animal, model)

The `create!` function is called for every individual at birth or at model initialisation.
Species must use [`@create`](@ref) to define a species-specific method. This is the fall-
back method, in case none is implemented for a species.
"""
function create!(a::Animal, model::SimulationModel)
    @warn "Species $(speciesof(a)) has no create!() method. Use `@create` to add one."
end

"""
    stepagent!(animal, model)

Update an animal by one day, executing it's currently active phase function.
"""
function stepagent!(animal::Animal, model::SimulationModel)
    animal.age += 1
    animal.phase(animal, model)
end

"""
    initnature!(model)

Initialise the model with all simulated animal populations.
"""
function initnature!(model::SimulationModel)
    # The config file determines which species are simulated in this run
    for speciesname in @param(nature.targetspecies)
        # Call each species' initialisation function
        initpopulation!(speciesname, model)
    end
    # Initialise the data output
    initecologicaldata(model)
end

"""
    updatenature!(model)

Run processes that affect all animals.
"""
function updatenature!(model::SimulationModel)
    # Update all animals. Dead animals are replaced by `nothing`, we can skip these.
    #XXX I assume that keeping all those `nothing`s in the animal vector won't lead
    # to memory bloat, but I will have to check that.
    for a in model.animals
        !isnothing(a) && stepagent!(a, model)
    end
    # The migrant pool is sorted by date of return, so we can simply look at the top
    # of the stack to check whether any animals are returning today.
    while !isempty(model.migrants) && model.migrants[1].second <= model.date
        returnee = model.migrants[1].first
        model.animals[returnee.id] = returnee
        @debug "$(animalid(returnee)) has returned."
        deleteat!(model.migrants, 1)
    end
end

"""
    killallanimals!(model)

Remove all animal individuals from the simulation.
"""
function killallanimals!(model)
    for a in model.animals
        kill!(a, model)
    end
    model.migrants = Vector{Pair{Animal, Date}}()
    model.animals = Vector{Union{Animal,Nothing}}()
    return
end