### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
###
### This file is responsible for managing the crop growth modules.
###

#TODO write tests for input functions

module ALMaSS

using Persefone:
    Management,
    Length,
    cm,
    SimulationModel,
    fertiliser,
    maxtemp,
    mintemp

import Persefone:
    stepagent!,
    croptype,
    cropname,
    cropheight,
    cropcover,
    cropyield

using Dates: Date, month, monthday
using CSV: CSV

"""
    GrowthPhase

ALMaSS crop growth curves are split into five phases, triggered by
seasonal dates or agricultural events.
"""
@enum GrowthPhase janfirst sow marchfirst harvest1 harvest2

"""
    CropCurveParams

The values in this struct define one crop growth curve.
"""
struct CropCurveParams
    #TODO add Unitful
    curveID::Int
    highnutrients::Bool
    GDD::Dict{GrowthPhase,Vector{Float64}}
    LAItotal::Dict{GrowthPhase,Vector{Float64}}
    LAIgreen::Dict{GrowthPhase,Vector{Float64}}
    height::Dict{GrowthPhase,Vector{Length{Float64}}}
end

"""
    CropType

The type struct for all crops. Currently follows the crop growth model as
implemented in ALMaSS.
"""
struct CropType
    name::String
    minsowdate::Union{Missing,Date}
    maxsowdate::Union{Missing,Date}
    minharvestdate::Union{Missing,Date}
    maxharvestdate::Union{Missing,Date}
    mingrowthtemp::Union{Missing,Float64}
    highnutrientgrowth::Union{Missing,CropCurveParams}
    lownutrientgrowth::Union{Missing,CropCurveParams}
    #issowable::Union{Function,Bool}
end

cropname(ct::CropType) = ct.name

"""
    CropState

The state data for an ALMaSS vegetation point calculation.  Usually
part of a `FarmPlot`.
"""
mutable struct CropState
    #TODO add Unitful
    croptype::CropType
    phase::GrowthPhase
    growingdegreedays::Float64
    height::Length{Float64}
    LAItotal::Float64
    LAIgreen::Float64
    #biomass::Float64 #XXX I need to figure out how to calculate this
    events::Vector{Management}
end

croptype(cs::CropState) = cs.croptype
cropname(cs::CropState) = cropname(croptype(cs))
cropheight(cs::CropState) = cs.height
cropcover(cs::CropState) = 0.0  # TODO: related to LAItotal, LAIgreen?
cropyield(cs::CropState) = 0.0  # TODO: units? needs biomass?

"""
    Base.tryparse(type, str)

Extend `tryparse` to allow parsing GrowthPhase values.
(Needed to read in the CSV parameter file.)
"""
function Base.tryparse(type::Type{GrowthPhase}, str::String)
    str == "janfirst" ? janfirst :
        str == "sow" ? sow :
        str == "marchfirst" ? marchfirst :
        str == "harvest1" ? harvest1 :
        str == "harvest2" ? harvest2 :
        nothing
end

"""
    buildgrowthcurve(data)

Convert a list of rows from the crop growth data into a CropCurveParams object.
"""
function buildgrowthcurve(data::Vector{CSV.Row})
    isempty(data) && return missing
    GDD = Dict(janfirst=>Vector{Float64}(), sow=>Vector{Float64}(),
               marchfirst=>Vector{Float64}(), harvest1=>Vector{Float64}(),
               harvest2=>Vector{Float64}())
    LAItotal = Dict(janfirst=>Vector{Float64}(), sow=>Vector{Float64}(),
                    marchfirst=>Vector{Float64}(), harvest1=>Vector{Float64}(),
                    harvest2=>Vector{Float64}())
    LAIgreen = Dict(janfirst=>Vector{Float64}(), sow=>Vector{Float64}(),
                    marchfirst=>Vector{Float64}(), harvest1=>Vector{Float64}(),
                    harvest2=>Vector{Float64}())
    height = Dict(janfirst=>Vector{Length{Float64}}(), sow=>Vector{Length{Float64}}(),
                  marchfirst=>Vector{Length{Float64}}(), harvest1=>Vector{Length{Float64}}(),
                  harvest2=>Vector{Length{Float64}}())
    for e in data        
        append!(GDD[e.growth_phase], e.GDD)
        append!(LAItotal[e.growth_phase], e.LAI_total)
        append!(LAIgreen[e.growth_phase], e.LAI_green)
        append!(height[e.growth_phase], e.height * cm)  # assumes `height` is in units of `cm`
    end
    CropCurveParams(data[1].curve_id, data[1].nutrient_status=="high",
                    GDD, LAItotal, LAIgreen, height)
end

"""
    readcropparameters(generalcropfile, cropgrowthfile)

Parse a CSV file containing the required parameter values for each crop
(as produced from the original ALMaSS file by `convert_almass_data.py`).
"""
function readcropparameters(generalcropfile::String, growthfile::String)
    @debug "Reading crop parameters"
    cropdata = CSV.File(generalcropfile, missingstring="NA", dateformat="d U",
                        types=[String,Date,Date,Date,Date,Float64])
    growthdata = CSV.File(growthfile, missingstring="NA",
                          types=[Int,String,String,GrowthPhase,String,
                                 Float64,Float64,Float64,Float64])
    croptypes = Dict{String,CropType}()
    for crop in cropdata
        cropgrowthdata = growthdata |> filter(x -> !ismissing(x.crop_name) &&
                                              x.crop_name == crop.name)
        highnuts = buildgrowthcurve(cropgrowthdata |>
                                    filter(x -> x.nutrient_status=="high"))
        lownuts = buildgrowthcurve(cropgrowthdata |>
                                   filter(x -> x.nutrient_status=="low"))
        croptypes[crop.name] = CropType(crop.name, crop.minsowdate, crop.maxsowdate,
                                        crop.minharvestdate, crop.maxharvestdate,
                                        crop.mingrowthtemp, highnuts, lownuts)
    end
    croptypes
end

"""
    stepagent!(cropstate, model)

Update a farm plot by one day.
"""
function stepagent!(cs::CropState, model::SimulationModel)
    # update growing degree days
    # if no crop-specific base temperature is given, default to 5°C
    # (https://www.eea.europa.eu/publications/europes-changing-climate-hazards-1/heat-and-cold/heat-and-cold-2014-mean)
    basetemp = cs.croptype.mingrowthtemp
    ismissing(basetemp) && (basetemp = 5.0)
    gdd = (maxtemp(model)+mintemp(model))/2 - basetemp
    gdd > 0 && (cs.growingdegreedays += gdd)
    # update the phase on key dates
    monthday(model.date) == (1,1) && (cs.phase = ALMaSS.janfirst)
    monthday(model.date) == (3,1) && (cs.phase = ALMaSS.marchfirst)
    # update crop growth
    growcrop!(cs, model)
end


## CROP MANAGEMENT AND GROWTH FUNCTIONS

"""
    sow!(cropstate, model, cropname)

Change the cropstate to sow the specified crop.
"""
function sow!(cs::CropState, model::SimulationModel, cropname::String)
    #XXX test if the crop is sowable?
    cs.croptype = model.crops[cropname]
    cs.phase = ALMaSS.sow
end

"""
    harvest!(cropstate, model)

Harvest the crop of this cropstate.
"""
function harvest!(cs::CropState, model::SimulationModel)
    cs.phase in [ALMaSS.harvest1, ALMaSS.harvest2] ?
        cs.phase = ALMaSS.harvest2 :
        cs.phase = ALMaSS.harvest1
    # height & LAI will be automatically adjusted by the growth function
    #TODO calculate and return yield
end

#TODO fertilise!()
#TODO spray!()
#TODO till!()

"""
    growcrop!(cropstate, model)

Apply the relevant crop growth model to update the plants crop state
on this farm plot.  Implements the ALMaSS crop growth model by Topping
et al.
"""
function growcrop!(cs::CropState, model::SimulationModel)
    fertiliser in cs.events ?
        curve = cs.croptype.lownutrientgrowth :
        curve = cs.croptype.highnutrientgrowth
    points = curve.GDD[cs.phase]
    for p in 1:length(points)
        if points[p] == 99999
            return # the marker that there is no further growth this phase
        elseif points[p] == -1 # the marker to set all variables to specified values
            cs.height = curve.height[cs.phase][p]
            cs.LAItotal = curve.LAItotal[cs.phase][p]
            cs.LAIgreen = curve.LAIgreen[cs.phase][p]
            return
        else
            gdd = cs.growingdegreedays
            # figure out which is the correct slope value to use for growth
            if p == length(points) || gdd < points[p+1]
                cs.height += curve.height[cs.phase][p]
                cs.LAItotal += curve.LAItotal[cs.phase][p]
                cs.LAIgreen += curve.LAIgreen[cs.phase][p]
                return
            end
            #XXX To be precise, we ought to figure out if one or more inflection
            # points have been passed between yesterday and today, and calculate the
            # growth exactly up to the inflection point with the old slope, and onward
            # with the new slope. Not doing so will introduce a small amount of error,
            # although I think this is acceptable.
        end
    end
end

end # module ALMaSS