### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
###
### This file contains a set of utility functions for species, including initialisation,
### reproduction, and mortality.
###

"""
    initpopulation(habitatdescriptor; popsize=-1, pairs=false, asexual=false)

Creates a function that initialises individuals at random locations across the landscape.
This can be used to create the `initialise!` variable in a species definition block.

- `habitatdescriptor` is a function that determines whether a given location is suitable
    or not (create this using [`@habitat`](@ref)).

- `phase` determines which life phase individuals will be assigned to. If this is `nothing`,
    the species' default post-natal life stage will be used (although note that this is
    probably not what you want).

- `popsize` determines the number of individuals that will be created. If this is zero or
    negative, one individual will be created in every suitable location in the landscape.
    If `popsize` is greater than the number of suitable locations, multiple individuals
    will be created in one place. (Maximum population density can be set in the habitat
    descriptor using the [`@countanimals`](@ref) macro.)

- If `pairs` is true, a male and a female individual will be created in each selected
    location, otherwise, only one individual will be created at a time.

- If `asexual` is true, all created individuals are assigned the sex `hermaphrodite`,
    otherwise, they are randomly assigned male of female. (If `pairs` is true, `asexual`
    is ignored.)
"""
function initpopulation(habitatdescriptor::Function; phase::Union{String,Nothing}=nothing,
                        popsize::Int64=-1, pairs::Bool=false, asexual::Bool=false)
    #TODO add a `popdensity` argument
    function(species::Dict{String,Any}, model::AgentBasedModel)
        n = 0
        lastn = 0
        specname = species["name"]
        (!isnothing(phase)) && (species["phase"] = phase)
        width, height = size(model.landscape)
        while n == 0 || n < popsize
            for x in @shuffle!(Vector(1:width))
                for y in @shuffle!(Vector(1:height))
                    if habitatdescriptor((x,y), model)
                        if pairs
                            add_agent!((x,y), Animal, model, deepcopy(species), female, 0)
                            add_agent!((x,y), Animal, model, deepcopy(species), male, 0)
                            n += 2
                        else
                            sex = asexual ? hermaphrodite : @rand([male, female])
                            add_agent!((x,y), Animal, model, deepcopy(species), sex, 0)
                            n += 1
                        end
                    end
                    (popsize > 0 && n >= popsize) && break
                end
                (popsize > 0 && n >= popsize) && break
            end
            if lastn == n # prevent an infinite loop - we don't have a Cray...
                @warn "There are not enough suitable locations for $(specname) in the landscape."
                break
            end
            lastn = n
        end
        @info "Initialised $(n) $(specname)s."
    end
end

"""
    initrandompopulation(popsize; kwargs...)

A simplified version of [`initpopulation`](@ref). Creates a function that initialises
`popsize` individuals, spread at random across the landscape.
"""
function initrandompopulation(popsize::Int64; kwargs...)
    #XXX How should this be called if users are supposed to use @initialise?
    initpopulation(@habitat(true); popsize=popsize, kwargs...)
end

#XXX initpopulation with dispersal from an original source?
#XXX initpopulation based on known occurences in real-life?

"""
    reproduce!(animal, model, n=1)

Produce one or more offspring for the given animal at its current location.
"""
function reproduce!(animal::Animal, model::AgentBasedModel, n::Int64=1)
    for i in 1:n
        sex = (animal.sex == hermaphrodite) ? hermaphrodite : @rand([male, female])
        # We need to generate a fresh species dict here
        species = @eval $(Symbol(animal.traits["name"]))($model)
        add_agent!(animal.pos, Animal, model, species, sex, 0)
    end
    @debug "$(animalid(animal)) has reproduced."
end

"""
    kill(animal, model, probability=1.0, cause="")

Kill this animal, optionally with a given percentage probability.
Returns true if the animal dies, false if not.
"""
function kill!(animal::Animal, model::AgentBasedModel, probability::Float64=1.0, cause::String="")
    if @rand() < probability
        postfix = isempty(cause) ? "." : " from $cause."
        @debug "$(animalid(animal)) has died$(postfix)"
        kill_agent!(animal, model)
        return true
    end
    return false
end

"""
    nearby_animals(pos, model, radius)

Return an iterator over all animals in the given radius around this position.
"""
function nearby_animals(pos::Tuple{Int64,Int64}, model::AgentBasedModel, radius::Int64)
    #TODO enable filtering by species
    neighbours = (model[id] for id in nearby_ids(pos, model, radius))
    Iterators.filter(a -> typeof(a) == Animal, neighbours)
end

"""
    nearby_animals(animal, model, radius)

Return an iterator over all animals in the given radius around this animal, excluding itself.
"""
function nearby_animals(animal::Animal, model::AgentBasedModel, radius::Int64)
    #TODO enable filtering by species
    neighbours = (model[id] for id in nearby_ids(animal.pos, model, radius))
    Iterators.filter(a -> typeof(a) == Animal && a.id != animal.id, neighbours)
end

"""
    countanimals(pos, model; species="", radius=0)

Count the number of animals in this location (optionally supplying a species name and radius).
"""
function countanimals(pos::Tuple{Int64,Int64}, model::AgentBasedModel;
                      species::String="", radius::Int64=0)
    n = 0
    #XXX can we ignore capitalisation in the spelling of `species`?
    for a in nearby_animals(pos, model, radius)
        (species == "" || a.traits["name"] == species) && (n += 1)
    end
    return n
end

##TODO add movement functions