### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
###
### This file holds various utility types and functions, especially for dealing with
### units and dates.
###

### UNITS AND DIMENSIONS

## Import and define units and dimensions
import Unitful: cm, m, km, ha, mg, g, kg, Length, Area, Mass
const m² = m^2
const km² = km^2

# enable division with different length/area unit types
Base.:(/)(x::S,y::T) where {S<:Length, T<:Length} = (upreferred(x)/m) / (upreferred(y)/m)
Base.:(/)(x::S,y::T) where {S<:Area, T<:Area} = (upreferred(x)/m²) / (upreferred(y)/m²)
Base.:(/)(x::S,y::T) where {S<:Mass, T<:Mass} = (upreferred(x)/g) / (upreferred(y)/g)

## RECURRING DATES

"""
    AnnualDate

A type to handle recurring dates (e.g. migration, harvest).
Stores a month and a day, and can be compared against normal dates.
To save typing, a Tuple{Int64,Int64} is automatically converted to an
AnnualDate, allowing this syntax: `nestingend::AnnualDate = (August, 15)`.
"""
mutable struct AnnualDate
    month::Int64
    day::Int64

    # inner constructor to make sure inputs are in range
    AnnualDate(month::Int64, day::Int64) =
        if !(0 < month <= 12)
            Base.error("AnnualDate: month $month is out of range.") #TODO replace with exception
        elseif !(0 < day <= 31)
            # not strictly accurate (AnnualDate(February, 30) is possible), but good enough
            Base.error("AnnualDate: day $day is out of range.") #TODO replace with exception
        else
            new(month, day)
        end
end

# (automatically) convert integer tuples to AnnualDate.
# Allows writing `(August, 2)` instead of `AnnualDate(August, 2)` where an AnnualDate is expected.
AnnualDate(ad::Tuple{Int64,Int64}) = AnnualDate(ad...)
Base.convert(::Type{AnnualDate}, ad::Tuple{Int64,Int64}) = AnnualDate(ad)
Base.convert(::Type{Tuple{Int64,Int64}}, ad::AnnualDate) = (ad.month, ad.day)

# allow creating AnnualDates from a string of the format "8 August"
AnnualDate(ad::String) = AnnualDate(Date(ad, dateformat"d U"))
Base.convert(::Type{AnnualDate}, ad::String) = AnnualDate(ad)
Base.tryparse(::Type{AnnualDate}, ad::String) = AnnualDate(ad)

# Interface with Dates
AnnualDate(date::Date) = AnnualDate(month(date), day(date))
Base.convert(::Type{AnnualDate}, ad::Date) = AnnualDate(ad)
Dates.month(ad::AnnualDate) = ad.month
Dates.day(ad::AnnualDate) = ad.day
Dates.monthday(ad::AnnualDate) = (ad.month, ad.day)
Date(year::Int64, ad::AnnualDate) = Date(year, ad.month, ad.day)

# Comparison between AnnualDates and with Dates
Base.:(==)(ad1::AnnualDate, ad2::AnnualDate) = (month(ad1) == month(ad2) && day(ad1) == day(ad2))
Base.:(==)(ad::AnnualDate, d::Date) = (month(d) == month(ad) && day(d) == day(ad))
Base.:(==)(d::Date, ad::AnnualDate) = (ad == d)
Base.:(<)(ad1::AnnualDate, ad2::AnnualDate) = (month(ad1) < month(ad2) ||
                                               (month(ad1) == month(ad2) && day(ad1) < day(ad2)))
Base.:(<)(ad::AnnualDate, d::Date) = ad < AnnualDate(d)
Base.:(<)(d::Date, ad::AnnualDate) = AnnualDate(d) < ad

# Addition and subtraction of date periods
Base.:(+)(ad::AnnualDate, time::DatePeriod) = AnnualDate(Date(2022, ad) + time)
Base.:(-)(ad::AnnualDate, time::DatePeriod) = AnnualDate(Date(2022, ad) - time)
Base.:(-)(ad1::AnnualDate, ad2::AnnualDate) = ad1 > ad2 ?
    Date(2022, ad1) - Date(2022, ad2) :
    Date(2022, ad1) - Date(2021, ad2)

# Taking ranges
Base.:(:)(start::AnnualDate, stop::AnnualDate) =
    if start < stop # normal case, e.g. Easter:Christmas
        AnnualDate.(Date(2022, start):Date(2022, stop))
    else # handle wrap-around, e.g. Christmas:Easter #XXX ignores leap years
        AnnualDate.(Date(2021, start):Date(2022, stop))
    end

# Instantiate a recurring date for a given year
"""
    thisyear(annualdate, model)
    nextyear(annualdate, model)
    lastyear(annualdate, model)

Convert an AnnualDate to a Date, using the current/next/previous year of the simulation run.
"""
thisyear(ad::AnnualDate, model::SimulationModel) = Date(year(model.date), ad)
nextyear(ad::AnnualDate, model::SimulationModel) = Date(year(model.date)+1, ad)
lastyear(ad::AnnualDate, model::SimulationModel) = Date(year(model.date)-1, ad)

### RANDOM NUMBERS

"""
    randn(vector)

Return a random element from the given vector, following a (mostly) normal distribution based on
index values (i.e. elements in the middle of the vector will be returned most frequently).
"""
function Base.randn(v::AbstractVector, rng=Random.default_rng())
    r = bounds(randn(rng) + 4, min = 1, max=7) # normal distribution with mean 4, values of [1,7]
    step = 7 / length(v)
    i = Int(round(r / step))
    v[i]
end

"""
    @randn(vector)

Return a normally-distributed random number or element from the sample, using the model RNG.
This is a utility wrapper that can only be used a context where the
`model` object is available.
"""
macro randn(v)
    :($(esc(:randn))($(esc(v)), $(esc(:model)).rng))
end

"""
    @rand(args...)

Return a random number or element from the sample, using the model RNG.
This is a utility wrapper that can only be used a context where the
`model` object is available.
"""
macro rand(args...)
    :($(esc(:rand))($(esc(:model)).rng, $(map(esc, args)...)))
end

"""
    @shuffle!(collection)

Shuffle the given collection in place, using the model RNG.
This is a utility wrapper that can only be used a context where the
`model` object is available.
"""
macro shuffle!(collection)
    :($(esc(:shuffle!))($(esc(:model)).rng, $(esc(collection))))
end

"""
    @chance(odds)

Return true if a random number is less than the odds (0.0 <= `odds` <= 1.0),
using the model RNG. This is a utility wrapper that can only be used a context
where the `model` object is available.
"""
macro chance(odds)
    :($(esc(:rand))($(esc(:model)).rng) < $(esc(odds)))
end

### MISCELLANEOUS

"""
    bounds(x; max=Inf, min=0)

A utility function to make sure that a number is within a given set of bounds.
Returns `max`/`min` if `x` is greater/less than this.
"""
function bounds(x::Number; max::Number=Inf, min::Number=0)
    if unit(x) != NoUnits
        max = max*unit(x)
        min = min*unit(x)
    end
    x > max ? max :
        x < min ? min :
        x
end

"""
    cycle!(vector, n=1)

Move the first element of the vector to the end, repeat n times.
"""
function cycle!(v::AbstractVector, n::Int64=1)
    for i in 1:n
        push!(v, v[1])
        deleteat!(v, 1)
    end
    v
end

"""
    @areaof(npixels)

Calculate the area of a given number of landscape pixels, knowing the resolution of the
world map (requires the `model` object to be available).
"""
macro areaof(npixels)
    :($(esc(npixels)) * $(esc(:model)).settings["world.mapresolution"]^2)
end