# Persefone.jl wrapper for AquaCrop.jl

module AquaCropWrapper

const CROPFILE = "crop_data.csv"

import AquaCrop
import CSV
using Dates: Date
using DataFrames: DataFrame

using Persefone:
    AnnualDate,
    cm,
    daynumber,
    SoilType,
    SimulationModel

import Persefone:
    stepagent!,
    croptype,
    cropname,
    cropheight,
    cropcover,
    cropyield,
    sow!,
    harvest!,
    isharvestable

using Unitful: @u_str

# We can't use Length{Float64} as that is a Union type
const Tlength = typeof(1.0cm)

# TODO: read crop names directly from AquaCrop.jl
# names extracted by running this command in the AquaCrop.jl src dir:
#     grep -nir 'crop_type ==' src/ | cut -d '=' -f 3 | sed 's/$/,/' | sort | uniq
const AQUACROP_CROPNAMES = [
    "alfalfaGDD",
    "barley",
    "barleyGDD",
    "cotton",
    "cottonGDD",
    "drybean",
    "drybeanGDD",
    "maize",
    "maizeGDD",
    "oat",
    "paddyrice",
    "paddyriceGDD",
    "potato",
    "potatoGDD",
    "quinoa",
    "rapeseed",
    "sorghum",
    "sorghumGDD",
    "soybean",
    "soybeanGDD",
    "sugarbeet",
    "sugarbeetGDD",
    "sugarcane",
    "sunflower",
    "sunflowerGDD",
    "tef",
    "tomato",
    "tomatoGDD",
    "wheat",
    "wheatGDD",
]

@kwdef struct CropType
    name::String
    aquacrop_name::String
    group::String
    minsowdate::Union{Missing,AnnualDate}
    maxsowdate::Union{Missing,AnnualDate}
end

cropname(ct::CropType) = ct.name

"""
    readcropparameters(cropdirectory)

Parse a CSV file containing some parameters required to map AquaCrop
crop names to Persefone crop names as well as additional crop data
needed for Persefone (cropgroup, minsowdate, maxsowdate).
"""
function readcropparameters(cropdirectory::String)
    @debug "Reading extra crop parameters for AquaCrop crop model"
    croptypes = Dict{String,CropType}()
    df = CSV.read(joinpath(cropdirectory, CROPFILE), DataFrame;
                  missingstring="NA", types=[String, String, String, AnnualDate, AnnualDate])
    for row in eachrow(df)
        croptypes[row.persefone_name] =
            CropType(name = row.persefone_name,
                     aquacrop_name = row.aquacrop_name,
                     group = row.crop_group,
                     minsowdate = row.min_sow_date,
                     maxsowdate = row.max_sow_date)
    end
    return croptypes
end

mutable struct CropState
    croptype::CropType
    height::Tlength  # TODO: remove height field, supply from cropstate
    soiltype::SoilType
    cropstate::AquaCrop.AquaCropField

    function CropState(crop_type::CropType, soiltype::SoilType,
                       model::SimulationModel, height::Tlength=0.0cm)
        # TODO: get this mapping for soil types from a CSV file?
        soiltype_to_aquacrop = Dict(soiltype => replace(string(soiltype), "_" => " ")
                                    for soiltype in instances(SoilType))
        aquacrop_soiltype = soiltype_to_aquacrop[soiltype]

        start_date = model.date            # TODO: maybe `today(model)`
        end_date = model.weather.lastdate  # TODO: maybe `lastdate(model)`
        start_daynum = daynumber(model.weather, start_date)
        end_daynum = daynumber(model.weather, end_date)
        dayrange = start_daynum:end_daynum
        input_Tmin = @view model.weather.mintemp[dayrange]
        input_Tmax = @view model.weather.maxtemp[dayrange]
        input_ETo = @view model.weather.evapotranspiration[dayrange]
        input_Rain = @view model.weather.precipitation[dayrange]

        # Generate the keyword object for the AquaCrop simulation
        kwargs = (
            ## Necessary keywords
            # runtype
            runtype = AquaCrop.NoFileRun(),
            # project input
            Simulation_DayNr1 = start_date,
            Simulation_DayNrN = end_date,
            Crop_Day1 = start_date,    # TODO: originally start_date + Week(1)
            Crop_DayN = end_date,
            # soil
            soil_type = aquacrop_soiltype,
            # crop
            crop_type = crop_type.aquacrop_name,
            # Climate
            InitialClimDate = start_date,
            ## Optional keyworkds
            # Climate
            Tmin = input_Tmin,
            Tmax = input_Tmax,
            ETo = input_ETo,
            Rain = input_Rain,
            # change soil properties
            soil_layers = Dict("Thickness" => 5.0)
        )

        aquacrop_cropfield, all_ok = AquaCrop.start_cropfield(; kwargs...)
        if ! all_ok.logi
            @error "Failed calling AquaCrop.start_cropfield()\nAquaCrop error: $(all_ok.msg)"
        end

        return new(crop_type, height, soiltype, aquacrop_cropfield)
    end
end

croptype(cs::CropState) = cs.croptype
cropname(cs::CropState) = cropname(croptype(cs))
cropheight(cs::CropState) = cs.height  # TODO: calculate from AquaCrop state info
cropcover(cs::CropState) = AquaCrop.canopycover(cs.cropstate)
cropyield(cs::CropState) = AquaCrop.dryyield(cs.cropstate)  # TODO: there is also freshyield
isharvestable(cs::CropState) = true  # TODO: implement this correctly

"""
    stepagent!(cropstate, model)

Update a crop state by one day.
"""
function stepagent!(cs::CropState, model::SimulationModel)
    AquaCrop.dailyupdate!(cs.cropstate)
end

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

Change the cropstate to sow the specified crop.
"""
function sow!(cs::CropState, model::SimulationModel, cropname::String)
    if cropname ∉ keys(model.crops)
        @error "cropname \"$cropname\" not found"
    end
    cs.croptype = model.crops[cropname]
    cs.height = 0.0cm
    # TODO: other things needed for AquaCrop?
end

"""
    harvest!(cropstate, model)

Harvest the crop of this cropstate.
"""
function harvest!(cs::CropState, model::SimulationModel)
    # TODO: implement this
    return 0.0u"g/m^2"
end

end # module