isautodifferentiable(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = true

DiffEqBase.isdiscrete(alg::FunctionMap) = true

isfsal(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = true
isfsal(tab::DiffEqBase.ExplicitRKTableau{MType,VType,fsal}) where {MType,VType,fsal} = fsal
# isfsal(alg::CompositeAlgorithm) = isfsal(alg.algs[alg.current])
isfsal(alg::FunctionMap) = false
isfsal(alg::Rodas4) = false
isfsal(alg::Rodas42) = false
isfsal(alg::Rodas4P) = false
isfsal(alg::Vern7) = false
isfsal(alg::Vern8) = false
isfsal(alg::Vern9) = false
# Pseudo Non-FSAL
isfsal(alg::ORK256) = false
isfsal(alg::CarpenterKennedy2N54) = false
isfsal(alg::SHLDDRK64) = false
isfsal(alg::DGLDDRK73_C) = false
isfsal(alg::DGLDDRK84_C) = false
isfsal(alg::DGLDDRK84_F) = false
isfsal(alg::NDBLSRK124) = false
isfsal(alg::NDBLSRK134) = false
isfsal(alg::NDBLSRK144) = false
isfsal(alg::PDIRK44) = false
isfsal(alg::DImplicitEuler) = false
isfsal(alg::RKO65) = false
isfsal(alg::FRK65) = true
#isfsal(alg::RKM) = false

isfsal(alg::SSPRK22) = false
isfsal(alg::SSPRK33) = false
isfsal(alg::SSPRK53) = false
isfsal(alg::SSPRK53_2N1) = false
isfsal(alg::SSPRK53_2N2) = false
isfsal(alg::SSPRK53_H) = false
isfsal(alg::SSPRK63) = false
isfsal(alg::SSPRK73) = false
isfsal(alg::SSPRK83) = false
isfsal(alg::SSPRK43) = false
isfsal(alg::SSPRK432) = false
isfsal(alg::SSPRK932) = false
isfsal(alg::SSPRK54) = false
isfsal(alg::SSPRK104) = false

get_current_isfsal(alg, cache) = isfsal(alg)
get_current_isfsal(alg::CompositeAlgorithm, cache) = isfsal(alg.algs[cache.current])

issplit(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = false
issplit(alg::SplitAlgorithms) = true

@generated function _composite_beta1_default(algs::T, current, ::Type{QT}, beta2) where {T <: Tuple, QT}
  expr = Expr(:block)
  for i in 1:length(T.types)
    push!(expr.args, quote
      if current == $i
        return QT(beta1_default(algs[$i], beta2))
      end
    end)
  end
  return expr
end
@generated function _composite_beta2_default(algs::T, current, ::Type{QT}) where {T <: Tuple, QT}
  expr = Expr(:block)
  for i in 1:length(T.types)
    push!(expr.args, quote
      if current == $i
        return QT(beta2_default(algs[$i]))
      end
    end)
  end
  return expr
end

fsal_typeof(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm},rate_prototype) = typeof(rate_prototype)
fsal_typeof(alg::ETD2,rate_prototype) = ETD2Fsal{typeof(rate_prototype)}
function fsal_typeof(alg::CompositeAlgorithm,rate_prototype)
  fsal = map(x->fsal_typeof(x,rate_prototype), alg.algs)
  @assert length(unique(fsal)) == 1 "`fsal_typeof` must be consistent"
  return fsal[1]
end

isimplicit(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = false
isimplicit(alg::OrdinaryDiffEqAdaptiveImplicitAlgorithm) = true
isimplicit(alg::OrdinaryDiffEqImplicitAlgorithm) = true
isimplicit(alg::CompositeAlgorithm) = any(isimplicit.(alg.algs))

isdtchangeable(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = true
isdtchangeable(alg::CompositeAlgorithm) = all(isdtchangeable.(alg.algs))
isdtchangeable(alg::Union{LawsonEuler,NorsettEuler,LieEuler,MagnusGauss4,CayleyEuler,ETDRK2,ETDRK3,ETDRK4,HochOst4,ETD2}) = false # due to caching

ismultistep(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = false
ismultistep(alg::CompositeAlgorithm) = any(ismultistep.(alg.algs))
ismultistep(alg::ETD2) = true

isadaptive(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = false
isadaptive(alg::OrdinaryDiffEqAdaptiveAlgorithm) = true
isadaptive(alg::OrdinaryDiffEqCompositeAlgorithm) = all(isadaptive.(alg.algs))
isadaptive(alg::DImplicitEuler) = true
isadaptive(alg::DABDF2) = true

isautoswitch(alg) = false
isautoswitch(alg::CompositeAlgorithm) = alg.choice_function isa AutoSwitch

qmin_default(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = isadaptive(alg) ? 1//5 : 0
qmin_default(alg::CompositeAlgorithm) = maximum(qmin_default.(alg.algs))
qmin_default(alg::DP8) = 1//3

qmax_default(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = 10
qmax_default(alg::CompositeAlgorithm) = minimum(qmax_default.(alg.algs))
qmax_default(alg::DP8) = 6
qmax_default(alg::Union{RadauIIA3,RadauIIA5}) = 8

get_chunksize(alg::OrdinaryDiffEqAlgorithm) = error("This algorithm does not have a chunk size defined.")
get_chunksize(alg::OrdinaryDiffEqAdaptiveImplicitAlgorithm{CS,AD}) where {CS,AD} = Val(CS)
get_chunksize(alg::OrdinaryDiffEqImplicitAlgorithm{CS,AD}) where {CS,AD} = Val(CS)
get_chunksize(alg::DAEAlgorithm{CS,AD}) where {CS,AD} = Val(CS)
get_chunksize(alg::ExponentialAlgorithm) = Val(alg.chunksize)
# get_chunksize(alg::CompositeAlgorithm) = get_chunksize(alg.algs[alg.current_alg])

alg_autodiff(alg::OrdinaryDiffEqAlgorithm) = error("This algorithm does not have an autodifferentiation option defined.")
alg_autodiff(alg::OrdinaryDiffEqAdaptiveImplicitAlgorithm{CS,AD}) where {CS,AD} = AD
alg_autodiff(alg::DAEAlgorithm{CS,AD}) where {CS,AD} = AD
alg_autodiff(alg::OrdinaryDiffEqImplicitAlgorithm{CS,AD}) where {CS,AD} = AD
alg_autodiff(alg::ExponentialAlgorithm) = alg.autodiff
# alg_autodiff(alg::CompositeAlgorithm) = alg_autodiff(alg.algs[alg.current_alg])
get_current_alg_autodiff(alg, cache) = alg_autodiff(alg)
get_current_alg_autodiff(alg::CompositeAlgorithm, cache) = alg_autodiff(alg.algs[cache.current])

alg_extrapolates(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = false
alg_extrapolates(alg::CompositeAlgorithm) = any(alg_extrapolates.(alg.algs))
alg_extrapolates(alg::ImplicitEuler) = true
alg_extrapolates(alg::DImplicitEuler) = true
alg_extrapolates(alg::DABDF2) = true
alg_extrapolates(alg::Trapezoid) = true
alg_extrapolates(alg::ImplicitMidpoint) = true
alg_extrapolates(alg::TRBDF2) = true
alg_extrapolates(alg::SSPSDIRK2) = true
alg_extrapolates(alg::SDIRK2) = true
alg_extrapolates(alg::SDIRK22) = true
alg_extrapolates(alg::Kvaerno3) = true
alg_extrapolates(alg::Kvaerno4) = true
alg_extrapolates(alg::Kvaerno5) = true
alg_extrapolates(alg::ESDIRK54I8L2SA) = true
alg_extrapolates(alg::KenCarp3) = true
alg_extrapolates(alg::KenCarp4) = true
alg_extrapolates(alg::KenCarp47) = true
alg_extrapolates(alg::KenCarp5) = true
alg_extrapolates(alg::KenCarp58) = true
alg_extrapolates(alg::Cash4) = true
alg_extrapolates(alg::Hairer4) = true
alg_extrapolates(alg::Hairer42) = true
alg_extrapolates(alg::IRKN4) = true
alg_extrapolates(alg::IRKN3) = true
alg_extrapolates(alg::ABDF2) = true
alg_extrapolates(alg::SBDF) = true
alg_extrapolates(alg::MEBDF2) = true
alg_extrapolates(alg::IRKC) = true
alg_extrapolates(alg::MagnusLeapfrog) = true

alg_order(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = error("Order is not defined for this algorithm")
get_current_alg_order(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm},cache) = alg_order(alg)
get_current_alg_order(alg::CompositeAlgorithm,cache) = alg_order(alg.algs[cache.current])

get_current_alg_order(alg::OrdinaryDiffEqAdamsVarOrderVarStepAlgorithm,cache) = cache.order
get_current_adaptive_order(alg::OrdinaryDiffEqAdamsVarOrderVarStepAlgorithm,cache) = cache.order
get_current_alg_order(alg::JVODE,cache) = get_current_adaptive_order(alg,cache)
get_current_alg_order(alg::QNDF,cache) = cache.order
get_current_adaptive_order(alg::QNDF,cache) = cache.order
get_current_adaptive_order(alg::OrdinaryDiffEqExtrapolationVarOrderVarStepAlgorithm,cache) = cache.cur_order
get_current_alg_order(alg::OrdinaryDiffEqExtrapolationVarOrderVarStepAlgorithm,cache) = cache.cur_order
get_current_alg_order(alg::ExtrapolationMidpointDeuflhard,cache) = 2(cache.n_curr + 1)
get_current_alg_order(alg::ImplicitDeuflhardExtrapolation,cache) = 2(cache.n_curr + 1)
get_current_adaptive_order(alg::ExtrapolationMidpointDeuflhard,cache) = 2cache.n_curr
get_current_adaptive_order(alg::ImplicitDeuflhardExtrapolation,cache) = 2cache.n_curr
get_current_alg_order(alg::ExtrapolationMidpointHairerWanner,cache) = 2(cache.n_curr + 1)
get_current_alg_order(alg::ImplicitHairerWannerExtrapolation,cache) = 2(cache.n_curr + 1)
get_current_alg_order(alg::ImplicitEulerBarycentricExtrapolation,cache) = cache.n_curr
get_current_alg_order(alg::ImplicitEulerExtrapolation,cache) = cache.n_curr + 1
get_current_adaptive_order(alg::ExtrapolationMidpointHairerWanner,cache) = 2cache.n_curr
get_current_adaptive_order(alg::ImplicitHairerWannerExtrapolation,cache) = 2cache.n_curr
get_current_adaptive_order(alg::ImplicitEulerExtrapolation,cache) = cache.n_curr - 1
get_current_adaptive_order(alg::ImplicitEulerBarycentricExtrapolation,cache) = cache.n_curr - 2

#alg_adaptive_order(alg::OrdinaryDiffEqAdaptiveAlgorithm) = error("Algorithm is adaptive with no order")
get_current_adaptive_order(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm},cache) = alg_adaptive_order(alg)
get_current_adaptive_order(alg::CompositeAlgorithm,cache) = alg_adaptive_order(alg.algs[cache.current])

alg_order(alg::FunctionMap) = 0
alg_order(alg::Euler) = 1
alg_order(alg::AitkenNeville) = alg.init_order
alg_order(alg::Heun) = 2
alg_order(alg::Ralston) = 2
alg_order(alg::LawsonEuler) = 1
alg_order(alg::NorsettEuler) = 1
alg_order(alg::LieEuler) = 1
alg_order(alg::CayleyEuler) = 2
alg_order(alg::ETDRK2) = 2
alg_order(alg::ETDRK3) = 3
alg_order(alg::ETDRK4) = 4
alg_order(alg::HochOst4) = 4
alg_order(alg::Exp4) = 4
alg_order(alg::EPIRK4s3A) = 4
alg_order(alg::EPIRK4s3B) = 4
alg_order(alg::EPIRK5s3) = 5
alg_order(alg::EPIRK5P1) = 5
alg_order(alg::EPIRK5P2) = 5
alg_order(alg::EXPRB53s3) = 5
alg_order(alg::SplitEuler) = 1
alg_order(alg::ETD2) = 2
alg_order(alg::Exprb32) = 3
alg_order(alg::Exprb43) = 4
alg_order(alg::Anas5) = 5
alg_order(alg::RK46NL) = 4
alg_order(alg::KuttaPRK2p5) = 5
alg_order(alg::RKO65) = 5
alg_order(alg::FRK65) = 6

alg_order(alg::SymplecticEuler) = 1
alg_order(alg::VelocityVerlet) = 2
alg_order(alg::VerletLeapfrog) = 2
alg_order(alg::PseudoVerletLeapfrog) = 2
alg_order(alg::McAte2) = 2
alg_order(alg::Ruth3) = 3
alg_order(alg::McAte3) = 3
alg_order(alg::McAte4) = 4
alg_order(alg::CandyRoz4) = 4
alg_order(alg::CalvoSanz4) = 4
alg_order(alg::McAte42) = 4
alg_order(alg::McAte5) = 5
alg_order(alg::Yoshida6) = 6
alg_order(alg::KahanLi6) = 6
alg_order(alg::McAte8) = 8
alg_order(alg::KahanLi8) = 8
alg_order(alg::SofSpa10) = 10

alg_order(alg::IRKN3) = 3
alg_order(alg::Nystrom4) = 4
alg_order(alg::Nystrom4VelocityIndependent) = 4
alg_order(alg::IRKN4) = 4
alg_order(alg::Nystrom5VelocityIndependent) = 5
alg_order(alg::DPRKN6) = 6
alg_order(alg::DPRKN8) = 8
alg_order(alg::DPRKN12) = 12
alg_order(alg::ERKN4) = 4
alg_order(alg::ERKN5) = 5

alg_order(alg::Midpoint) = 2

alg_order(alg::ORK256) = 2
alg_order(alg::CarpenterKennedy2N54) = 4
alg_order(alg::SHLDDRK52) = 2
alg_order(alg::SHLDDRK_2N) = 4
alg_order(alg::SHLDDRK64) = 4
alg_order(alg::DGLDDRK73_C) = 3
alg_order(alg::DGLDDRK84_C) = 4
alg_order(alg::DGLDDRK84_F) = 4
alg_order(alg::NDBLSRK124) = 4
alg_order(alg::NDBLSRK134) = 4
alg_order(alg::NDBLSRK144) = 4
alg_order(alg::CFRLDDRK64) = 4
alg_order(alg::TSLDDRK74) = 4
alg_order(alg::CKLLSRK43_2) = 3
alg_order(alg::CKLLSRK54_3C) = 4
alg_order(alg::CKLLSRK95_4S) = 5
alg_order(alg::CKLLSRK95_4C) = 5
alg_order(alg::CKLLSRK95_4M) = 5
alg_order(alg::CKLLSRK54_3C_3R) = 4
alg_order(alg::CKLLSRK54_3M_3R) = 4
alg_order(alg::CKLLSRK54_3N_3R) = 4
alg_order(alg::CKLLSRK85_4C_3R) = 5
alg_order(alg::CKLLSRK85_4M_3R) = 5
alg_order(alg::CKLLSRK85_4P_3R) = 5
alg_order(alg::CKLLSRK54_3N_4R) = 4
alg_order(alg::CKLLSRK54_3M_4R) = 4
alg_order(alg::CKLLSRK65_4M_4R) = 5
alg_order(alg::CKLLSRK85_4FM_4R) = 5
alg_order(alg::CKLLSRK75_4M_5R) = 5
alg_order(alg::ParsaniKetchesonDeconinck3S32) = 2
alg_order(alg::ParsaniKetchesonDeconinck3S82) = 2
alg_order(alg::ParsaniKetchesonDeconinck3S53) = 3
alg_order(alg::ParsaniKetchesonDeconinck3S173) = 3
alg_order(alg::ParsaniKetchesonDeconinck3S94) = 4
alg_order(alg::ParsaniKetchesonDeconinck3S184) = 4
alg_order(alg::ParsaniKetchesonDeconinck3S105) = 5
alg_order(alg::ParsaniKetchesonDeconinck3S205) = 5
alg_order(alg::KYK2014DGSSPRK_3S2) = 2

alg_order(alg::SSPRK22) = 2
alg_order(alg::SSPRKMSVS32) = 2
alg_order(alg::SSPRK33) = 3
alg_order(alg::KYKSSPRK42) = 2
alg_order(alg::SSPRK53) = 3
alg_order(alg::SSPRK53_2N1) = 3
alg_order(alg::SSPRK53_2N2) = 3
alg_order(alg::SSPRK53_H) = 3
alg_order(alg::SSPRK63) = 3
alg_order(alg::SSPRK73) = 3
alg_order(alg::SSPRK83) = 3
alg_order(alg::SSPRK43) = 3
alg_order(alg::SSPRK432) = 3
alg_order(alg::SSPRKMSVS43) = 3
alg_order(alg::SSPRK932) = 3
alg_order(alg::SSPRK54) = 4
alg_order(alg::SSPRK104) = 4

alg_order(alg::RK4) = 4
alg_order(alg::RKM) = 4
alg_order(alg::ExplicitRK) = alg.tableau.order

alg_order(alg::BS3) = 3
alg_order(alg::BS5) = 5
alg_order(alg::OwrenZen3) = 3
alg_order(alg::OwrenZen4) = 4
alg_order(alg::OwrenZen5) = 5
alg_order(alg::DP5) = 5
alg_order(alg::Tsit5) = 5
alg_order(alg::DP8) = 8
alg_order(alg::Vern6) = 6
alg_order(alg::Vern7) = 7
alg_order(alg::Vern8) = 8
alg_order(alg::Vern9) = 9
alg_order(alg::TanYam7) = 7
alg_order(alg::TsitPap8) = 8
alg_order(alg::RadauIIA3) = 3
alg_order(alg::RadauIIA5) = 5
alg_order(alg::ImplicitEuler) = 1
alg_order(alg::RKMK2) = 2
alg_order(alg::RKMK4) = 4
alg_order(alg::LieRK4) = 4
alg_order(alg::CG2) = 2
alg_order(alg::CG3) = 3
alg_order(alg::MagnusMidpoint) = 2
alg_order(alg::MagnusGauss4) = 4
alg_order(alg::MagnusNC6) = 6
alg_order(alg::MagnusGL6) = 6
alg_order(alg::MagnusGL8) = 8
alg_order(alg::MagnusNC8) = 8
alg_order(alg::MagnusGL4) = 4
alg_order(alg::MagnusAdapt4) = 4
alg_order(alg::LinearExponential) = 1
alg_order(alg::MagnusLeapfrog) = 2
alg_order(alg::Trapezoid) = 2
alg_order(alg::ImplicitMidpoint) = 2
alg_order(alg::TRBDF2) = 2
alg_order(alg::SSPSDIRK2) = 2
alg_order(alg::SDIRK2) = 2
alg_order(alg::SDIRK22) = 2
alg_order(alg::Kvaerno3) = 3
alg_order(alg::Kvaerno4) = 4
alg_order(alg::Kvaerno5) = 5
alg_order(alg::ESDIRK54I8L2SA) = 5
alg_order(alg::KenCarp3) = 3
alg_order(alg::CFNLIRK3) = 3
alg_order(alg::KenCarp4) = 4
alg_order(alg::KenCarp47) = 4
alg_order(alg::KenCarp5) = 5
alg_order(alg::KenCarp58) = 5
alg_order(alg::Cash4) = 4
alg_order(alg::SFSDIRK4) = 4
alg_order(alg::SFSDIRK5) = 4
alg_order(alg::SFSDIRK6) = 4
alg_order(alg::SFSDIRK7) = 4
alg_order(alg::SFSDIRK8) = 4
alg_order(alg::Hairer4) = 4
alg_order(alg::Hairer42) = 4
alg_order(alg::Feagin10) = 10
alg_order(alg::Feagin12) = 12
alg_order(alg::Feagin14) = 14
alg_order(alg::PFRK87) = 8

alg_order(alg::Rosenbrock23) = 2
alg_order(alg::Rosenbrock32) = 3
alg_order(alg::ROS3P) = 3
alg_order(alg::Rodas3) = 3
alg_order(alg::ROS34PW1a) = 3
alg_order(alg::ROS34PW1b) = 3
alg_order(alg::ROS34PW2) = 3
alg_order(alg::ROS34PW3) = 4
alg_order(alg::RosShamp4) = 4
alg_order(alg::Veldd4) = 4
alg_order(alg::Velds4) = 4
alg_order(alg::GRK4T) = 4
alg_order(alg::GRK4A) = 4
alg_order(alg::Ros4LStab) = 4
alg_order(alg::RosenbrockW6S4OS) = 4
alg_order(alg::Rodas4) = 4
alg_order(alg::Rodas42) = 4
alg_order(alg::Rodas4P) = 4
alg_order(alg::Rodas5) = 5

alg_order(alg::AB3) = 3
alg_order(alg::AB4) = 4
alg_order(alg::AB5) = 5
alg_order(alg::ABM32) = 3
alg_order(alg::ABM43) = 4
alg_order(alg::ABM54) = 5

alg_order(alg::VCAB3) = 3
alg_order(alg::VCAB4) = 4
alg_order(alg::VCAB5) = 5
alg_order(alg::VCABM3) = 3
alg_order(alg::VCABM4) = 4
alg_order(alg::VCABM5) = 5

alg_order(alg::VCABM) = 1  #dummy value

alg_order(alg::CNAB2) = 2
alg_order(alg::CNLF2) = 2

alg_order(alg::AN5) = 5
alg_order(alg::JVODE) = 1  #dummy value

alg_order(alg::ABDF2) = 2
alg_order(alg::QNDF1) = 1
alg_order(alg::QNDF2) = 2

alg_order(alg::QNDF) = 1 #dummy value

alg_order(alg::SBDF) = alg.order

alg_order(alg::ROCK2) = 2
alg_order(alg::ROCK4) = 4

alg_order(alg::ESERK4) = 4
alg_order(alg::ESERK5) = 5
alg_order(alg::SERK2) = 2

alg_order(alg::RKC) = 2
alg_order(alg::IRKC) = 2

alg_order(alg::MEBDF2) = 2
alg_order(alg::PDIRK44) = 4

alg_order(alg::DImplicitEuler) = 1
alg_order(alg::DABDF2) = 2

alg_maximum_order(alg) = alg_order(alg)
alg_maximum_order(alg::CompositeAlgorithm) = maximum(alg_order(x) for x in alg.algs)
alg_maximum_order(alg::ExtrapolationMidpointDeuflhard) = 2(alg.n_max+1)
alg_maximum_order(alg::ImplicitDeuflhardExtrapolation) = 2(alg.n_max+1)
alg_maximum_order(alg::ExtrapolationMidpointHairerWanner) = 2(alg.n_max+1)
alg_maximum_order(alg::ImplicitHairerWannerExtrapolation) = 2(alg.n_max+1)
alg_maximum_order(alg::ImplicitEulerExtrapolation) = 2(alg.n_max+1)

alg_maximum_order(alg::ImplicitEulerBarycentricExtrapolation) = alg.n_max
alg_adaptive_order(alg::ExplicitRK) = alg.tableau.adaptiveorder
alg_adaptive_order(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = alg_order(alg)-1
alg_adaptive_order(alg::Feagin10) = 8
alg_adaptive_order(alg::Feagin14) = 12

alg_adaptive_order(alg::Rosenbrock23) = 3
alg_adaptive_order(alg::Rosenbrock32) = 2

alg_adaptive_order(alg::RadauIIA3) = 1
alg_adaptive_order(alg::RadauIIA5) = 3
alg_adaptive_order(alg::RKC) = 2
alg_adaptive_order(alg::IRKC) = 1

alg_adaptive_order(alg::ImplicitEuler) = 0
alg_adaptive_order(alg::Trapezoid) = 1
# this is actually incorrect and is purposefully decreased as this tends
# to track the real error much better
alg_adaptive_order(alg::ImplicitMidpoint) = 1
# this is actually incorrect and is purposefully decreased as this tends
# to track the real error much better

alg_adaptive_order(alg::Exprb32) = 2
alg_adaptive_order(alg::Exprb43) = 4
alg_adaptive_order(alg::AN5) = 5

beta2_default(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = isadaptive(alg) ? 2//(5alg_order(alg)) : 0
beta2_default(alg::FunctionMap) = 0
beta2_default(alg::DP8) = 0//1
beta2_default(alg::DP5) = 4//100
beta2_default(alg::ExtrapolationMidpointDeuflhard) = 0//1
beta2_default(alg::ImplicitDeuflhardExtrapolation) = 0//1
beta2_default(alg::ExtrapolationMidpointHairerWanner) = 0//1
beta2_default(alg::ImplicitHairerWannerExtrapolation) = 0//1
beta2_default(alg::ImplicitEulerExtrapolation) = 0//1
beta2_default(alg::ImplicitEulerBarycentricExtrapolation) = 0//1

beta1_default(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm},beta2) = isadaptive(alg) ? 7//(10alg_order(alg)) : 0
beta1_default(alg::FunctionMap,beta2) = 0
beta1_default(alg::DP8,beta2) = typeof(beta2)(1//alg_order(alg)) - beta2/5
beta1_default(alg::DP5,beta2) = typeof(beta2)(1//alg_order(alg)) - 3beta2/4
beta1_default(alg::ExtrapolationMidpointDeuflhard,beta2) =  1//(2alg.n_init+1)
beta1_default(alg::ImplicitDeuflhardExtrapolation,beta2) =  1//(2alg.n_init+1)
beta1_default(alg::ExtrapolationMidpointHairerWanner,beta2) =  1//(2alg.n_init+1)
beta1_default(alg::ImplicitHairerWannerExtrapolation,beta2) =  1//(2alg.n_init+1)
beta1_default(alg::ImplicitEulerExtrapolation,beta2) =  1//(alg.n_init+1)
beta1_default(alg::ImplicitEulerBarycentricExtrapolation,beta2) =  1//(alg.n_init - 1)

gamma_default(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = isadaptive(alg) ? 9//10 : 0
gamma_default(alg::RKC) = 8//10
gamma_default(alg::IRKC) = 8//10
gamma_default(alg::ExtrapolationMidpointDeuflhard) = (1//4)^beta1_default(alg,beta2_default(alg))
gamma_default(alg::ImplicitDeuflhardExtrapolation) = (1//4)^beta1_default(alg,beta2_default(alg))
gamma_default(alg::ExtrapolationMidpointHairerWanner) = (65//100)^beta1_default(alg,beta2_default(alg))
gamma_default(alg::ImplicitHairerWannerExtrapolation) = (65//100)^beta1_default(alg,beta2_default(alg))
gamma_default(alg::ImplicitEulerExtrapolation) = (65//100)^beta1_default(alg,beta2_default(alg))

gamma_default(alg::ImplicitEulerBarycentricExtrapolation) = (80//100)^beta1_default(alg,beta2_default(alg))

qsteady_min_default(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = 1
qsteady_max_default(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = 1
qsteady_max_default(alg::OrdinaryDiffEqAdaptiveImplicitAlgorithm) = 6//5
# But don't re-use Jacobian if not adaptive: too risky and cannot pull back
qsteady_max_default(alg::OrdinaryDiffEqImplicitAlgorithm) = isadaptive(alg) ? 1//1 : 0
qsteady_max_default(alg::AN5) = 3//2
qsteady_max_default(alg::JVODE) = 3//2
qsteady_max_default(alg::QNDF1) = 2//1
qsteady_max_default(alg::QNDF2) = 2//1
qsteady_max_default(alg::QNDF) = 2//1

FunctionMap_scale_by_time(alg::FunctionMap{scale_by_time}) where {scale_by_time} = scale_by_time

# SSP coefficients
"""
    ssp_coefficient(alg)

Return the SSP coefficient of the ODE algorithm `alg`. If one time step of size
`dt` with `alg` can be written as a convex combination of explicit Euler steps
with step sizes `cᵢ * dt`, the SSP coefficient is the minimal value of `1/cᵢ`.

# Examples
```julia-repl
julia> ssp_coefficient(SSPRK104())
6
```
"""
ssp_coefficient(alg) = error("$alg is not a strong stability preserving method.")
ssp_coefficient(alg::Euler) = 1
ssp_coefficient(alg::SSPRK22) = 1
ssp_coefficient(alg::SSPRK33) = 1
ssp_coefficient(alg::KYKSSPRK42) = 2.459
ssp_coefficient(alg::SSPRK53) = 2.65
ssp_coefficient(alg::SSPRK53_2N1) = 2.18
ssp_coefficient(alg::SSPRK53_2N2) = 2.148
ssp_coefficient(alg::SSPRK53_H) = 2.65
ssp_coefficient(alg::SSPRK63) = 3.518
ssp_coefficient(alg::SSPRK73) = 4.2879
ssp_coefficient(alg::SSPRK83) = 5.107
ssp_coefficient(alg::SSPRK43) = 2
ssp_coefficient(alg::SSPRK432) = 2
ssp_coefficient(alg::SSPRKMSVS32) = 0.5
ssp_coefficient(alg::SSPRKMSVS43) = 0.33
ssp_coefficient(alg::SSPRK932) = 6
ssp_coefficient(alg::SSPRK54) = 1.508
ssp_coefficient(alg::SSPRK104) = 6
ssp_coefficient(alg::KYK2014DGSSPRK_3S2) =  0.8417

# We shouldn't do this probably.
#ssp_coefficient(alg::ImplicitEuler) = Inf
ssp_coefficient(alg::SSPSDIRK2) = 4

# stability regions
alg_stability_size(alg::DP5) = 3.3066
alg_stability_size(alg::Tsit5) = 3.5068
alg_stability_size(alg::Vern6) = 4.8553
alg_stability_size(alg::Vern7) = 4.6400
alg_stability_size(alg::Vern8) = 5.8641
alg_stability_size(alg::Vern9) = 4.4762

alg_can_repeat_jac(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = false
alg_can_repeat_jac(alg::OrdinaryDiffEqNewtonAdaptiveAlgorithm) = true
alg_can_repeat_jac(alg::IRKC) = false

function unwrap_alg(integrator, is_stiff)
  alg = integrator.alg
  iscomp = typeof(alg) <: CompositeAlgorithm
  if !iscomp
    return alg
  elseif typeof(alg.choice_function) <: AutoSwitchCache
    num = is_stiff ? 2 : 1
    if num == 1
      return alg.algs[1]
    else
      return alg.algs[2]
    end
  else
    if integrator.cache.current == 1
      return alg.algs[1]
    elseif integrator.cache.current == 2
      return alg.algs[2]
    else
      return alg.algs[integrator.cache.current]
    end
  end
end

# Whether `uprev` is used in the algorithm directly.
uses_uprev(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}, adaptive::Bool) = true
uses_uprev(alg::ORK256, adaptive::Bool) = false
uses_uprev(alg::CarpenterKennedy2N54, adaptive::Bool) = false
uses_uprev(alg::SHLDDRK64, adaptive::Bool) = false
uses_uprev(alg::DGLDDRK73_C, adaptive::Bool) = false
uses_uprev(alg::DGLDDRK84_C, adaptive::Bool) = false
uses_uprev(alg::DGLDDRK84_F, adaptive::Bool) = false
uses_uprev(alg::NDBLSRK124, adaptive::Bool) = false
uses_uprev(alg::NDBLSRK134, adaptive::Bool) = false
uses_uprev(alg::NDBLSRK144, adaptive::Bool) = false
uses_uprev(alg::CFRLDDRK64, adaptive::Bool) = false
uses_uprev(alg::TSLDDRK74, adaptive::Bool) = false
uses_uprev(alg::OrdinaryDiffEqAdaptiveAlgorithm, adaptive::Bool) = true
uses_uprev(alg::CKLLSRK43_2, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK54_3C, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK95_4S, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK95_4C, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK95_4M, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK54_3C_3R, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK54_3M_3R, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK54_3N_3R, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK85_4C_3R, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK85_4M_3R, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK85_4P_3R, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK54_3N_4R, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK54_3M_4R, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK65_4M_4R, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK85_4FM_4R, adaptive::Bool) = adaptive
uses_uprev(alg::CKLLSRK75_4M_5R, adaptive::Bool) = adaptive

ispredictive(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = false
ispredictive(alg::Union{RKC}) = true
ispredictive(alg::Union{SERK2}) = alg.controller === :Predictive
ispredictive(alg::OrdinaryDiffEqNewtonAdaptiveAlgorithm) = alg.controller === :Predictive
isstandard(alg::OrdinaryDiffEqNewtonAdaptiveAlgorithm) = alg.controller === :Standard
isstandard(alg::VCABM) = true
isstandard(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = false
ispi(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = !(ispredictive(alg) || isstandard(alg))

isWmethod(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = false
isWmethod(alg::Rosenbrock23) = true
isWmethod(alg::Rosenbrock32) = true
isWmethod(alg::ROS34PW1a) = true
isWmethod(alg::ROS34PW1b) = true
isWmethod(alg::ROS34PW2) = true
isWmethod(alg::ROS34PW3) = true
isWmethod(alg::RosenbrockW6S4OS) = true

isesdirk(alg::TRBDF2) = true
isesdirk(alg::Union{KenCarp3, KenCarp4, KenCarp5, KenCarp58,
                    Kvaerno3, Kvaerno4, Kvaerno5,
                    ESDIRK54I8L2SA,CFNLIRK3}) = true
isesdirk(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = false

is_mass_matrix_alg(alg::Union{OrdinaryDiffEqAlgorithm,DAEAlgorithm}) = false
is_mass_matrix_alg(alg::RosenbrockAlgorithm) = true
is_mass_matrix_alg(alg::NewtonAlgorithm) = !isesdirk(alg)


function adjacency2modularity(A)

    degree_vector = sum(A,2)

    total_edges = sum(degree_vector)

    # purposeful typo to force throwing error
    B = A - degree_vector*degree_vector'/totla_edges
    return B
end


#
println("Dc) Test of the PhotoIonization module with ASF from an internally generated initial- and final-state multiplet.")
#
setDefaults("print summary: open", "zzz-PhotoIonization.sum")
setDefaults("method: continuum, Galerkin")           ## setDefaults("method: continuum, Galerkin")  "method: continuum, asymptotic Coulomb"
setDefaults("method: normalization, pure sine")      ## setDefaults("method: normalization, pure Coulomb")    setDefaults("method: normalization, pure sine")

if  false
    # Neon subshell cross sections from 10..1000 eV photon energy
    setDefaults("unit: energy", "eV") # then, give photonEnergies in eV

    photoSettings = PhotoIonization.Settings(PhotoIonization.Settings(), gauges=[UseCoulomb, UseBabushkin], photonEnergies=[1000.],
                                             calcAnisotropy=false, calcPartialCs=false, printBefore=true)

    grid = Radial.Grid(Radial.Grid(false), rnt = 4.0e-6, h = 5.0e-2, hp = 1.0e-2, rbox = 10.0)
    wa   = Atomic.Computation(Atomic.Computation(), name="Photoionization of Ne: subshell cross sections",
                              grid=grid, nuclearModel=Nuclear.Model(10.),
                              initialConfigs=[Configuration("1s^2 2s^2 2p^6")],
                              finalConfigs  =[Configuration("1s 2s^2 2p^6"), Configuration("1s^2 2s 2p^6"), Configuration("1s^2 2s^2 2p^5")],
                              process = Photo(),  processSettings=photoSettings)
    @show wa
    perform(wa)

elseif  false
    # Aluminium subshell cross sections from 10..1000 eV photon energy
    setDefaults("unit: energy", "eV") # then, give photonEnergies in eV

    photoSettings = PhotoIonization.Settings(PhotoIonization.Settings(), gauges=[UseCoulomb, UseBabushkin], photonEnergies=[12., 20.],
                                             lineSelection=LineSelection(true, [(1,0), (2,0)], Tuple{LevelSymmetry,LevelSymmetry}[]),
                                             calcAnisotropy=false, calcPartialCs=false, printBefore=true)

    grid = Radial.Grid(Radial.Grid(false), rnt = 4.0e-6, h = 5.0e-2, hp = 1.0e-2, rbox = 10.0)
    wa   = Atomic.Computation(Atomic.Computation(), name="Photoionization of Al: subshell cross sections",
                              grid=grid, nuclearModel=Nuclear.Model(13.),
                              initialConfigs=[Configuration("1s^2 2s^2 2p^6 3s^2 3p")],
                              finalConfigs  =[Configuration("1s^2 2s^2 2p^6 3s^2")],
                              ## initialConfigs=[Configuration("1s^2 2s^2 2p^6 3s^2")],
                              ## finalConfigs  =[Configuration("1s^2 2s 2p^6 3s^2"), Configuration("1s^2 2s^2 2p^5 3s^2"),
                              ##                 Configuration("1s^2 2s^2 2p^6 3s")],
                              process = Photo(),  processSettings=photoSettings)
    @show wa
    perform(wa)

elseif  true
    # Xenon 4d subshell cross sections from 70..150 eV photon energy
    setDefaults("unit: energy", "eV") # then, give photonEnergies in eV

    photoSettings = PhotoIonization.Settings(PhotoIonization.Settings(), gauges=[UseCoulomb, UseBabushkin], photonEnergies=[70., 90., 110., 130., 150.],
                                             ## lineSelection=LineSelection(true, [(1,0), (2,0)], Tuple{LevelSymmetry,LevelSymmetry}[]),
                                             calcAnisotropy=false, calcPartialCs=false, printBefore=true)

    grid = Radial.Grid(Radial.Grid(false), rnt = 4.0e-6, h = 5.0e-2, hp = 1.0e-2, rbox = 20.0)
    wa   = Atomic.Computation(Atomic.Computation(), name="Photoionization of xenon: 4d subshell cross sections",
                              grid=grid, nuclearModel=Nuclear.Model(54.),
                              initialConfigs=[Configuration("[Kr] 4d^10 5s^2 5p^6")],
                              finalConfigs  =[Configuration("[Kr] 4d^9 5s^2 5p^6")],
                              process = Photo(),  processSettings=photoSettings)
    @show wa
    perform(wa)

elseif false
    # Test case from FAC/User Guide
    defaultSettings = PhotoIonization.Settings()
    setDefaults("unit: energy", "eV")
    setDefaults("unit: cross section", "Mbarn")
    e1 = 1.01 * 75.20911382458269  * 27.21
    e2 = 1.20 * 75.20911382458269  * 27.21
    e3 = 2.00 * 75.20911382458269  * 27.21
    e4 = 3.00 * 75.20911382458269  * 27.21
    e5 = 4.00 * 75.20911382458269  * 27.21
    e6 = 6.00 * 75.20911382458269  * 27.21

    photoSettings = PhotoIonization.Settings(defaultSettings, gauges=[UseCoulomb, UseBabushkin], photonEnergies=[e1, e2, e3, e4, e5, e6], ###
                                             calcAnisotropy=false, calcPartialCs=false, printBefore=true)

    grid = Radial.Grid(Radial.Grid(false), rnt = 4.0e-6, h = 5.0e-2, hp = 1.0e-2, rbox = 10.0)
    wa   = Atomic.Computation(Atomic.Computation(), name="Photoionization of Li-like iron",
                              grid=grid, nuclearModel=Nuclear.Model(26.),
                              initialConfigs=[Configuration("1s^2 2s"), Configuration("1s^2 2p")],
                              finalConfigs  =[Configuration("1s^2")],
                              process = Photo(),  processSettings=photoSettings)
    @show wa
    perform(wa)

elseif false
    # Test case
    defaultSettings = PhotoIonization.Settings()
    setDefaults("unit: energy", "eV")
    setDefaults("unit: cross section", "Mbarn")
    e1 =  40.0
    e2 =  60.0
    e3 =  80.0
    e4 = 100.0
    e5 = 120.0

    photoSettings = PhotoIonization.Settings(defaultSettings, gauges=[UseCoulomb, UseBabushkin], photonEnergies=[e1, e2, e3, e4, e5], ### , e2, e3, e4, e5, e6
                                             calcAnisotropy=false, calcPartialCs=false, printBefore=true)

    grid = Radial.Grid(Radial.Grid(true), rnt = 2.0e-5,h = 5.0e-2, hp = 1.0e-2, NoPoints = 900)
    wa   = Atomic.Computation(Atomic.Computation(), name="Photoionization of neutral neon",
                              grid=grid, nuclearModel=Nuclear.Model(10.),
                              initialConfigs=[Configuration("1s^2 2s^2 2p^6")],
                              finalConfigs  =[Configuration("1s^2 2s^2 2p^5"), Configuration("1s^2 2s 2p^6")],
                              process = Photo(),  processSettings=photoSettings)
    @show wa
    perform(wa)
end

setDefaults("print summary: close", "")


"""
    DynamicAggregation

Functor object for using dynamic aggregation in an L-shaped algorithm. Create by supplying a [`DynamicAggregate`](@ref) object through `aggregate ` in the `LShapedSolver` factory function and then pass to a `StochasticPrograms.jl` model.

The following selection rules are available
- [`SelectUniform`](@ref)
- [`SelectDecaying`](@ref)
- [`SelectRandom`](@ref
- [`SelectClosest`](@ref)
- [`SelectClosestToReference`](@ref)

...
# Parameters
- `num_aggregates::Int`: Number of aggregates
- `rule::SelectionRule`: Rule that determines which aggregate an incoming cut should be placed in
- `lock_after::Function = (τ,n)->false`: Function that determines if the current aggregation scheme should be fixed, based on the current optimality gap `τ` and the number of iterations `n`
...
"""
struct DynamicAggregation{T <: AbstractFloat, S <: AbstractSelectionRule} <: AbstractAggregation
    aggregates::Vector{AggregatedOptimalityCut{T}}
    rule::S
    partitioning::Dict{Int,Int}
    lock::Function

    function DynamicAggregation(num_aggregates::Integer, rule::AbstractSelectionRule, lock::Function, ::Type{T}) where T <: AbstractFloat
        S = typeof(rule)
        aggregates = [zero(AggregatedOptimalityCut{T}) for _ = 1:num_aggregates]
        return new{T,S}(aggregates, rule, Dict{Int,Int}(), lock)
    end
end

function aggregate_cut!(lshaped::AbstractLShaped, ::DynamicAggregation, cut::HyperPlane)
    return add_cut!(lshaped, cut)
end

function aggregate_cut!(lshaped::AbstractLShaped, aggregation::DynamicAggregation{T}, cut::HyperPlane{OptimalityCut}) where T <: AbstractFloat
    if aggregation.lock(gap(lshaped), num_iterations(lshaped)) && haskey(aggregation.partitioning, cut.id)
        aggregation.aggregates[aggregation.partitioning[cut.id]] += cut
        return false
    end
    (idx, full) = select(aggregation.rule, aggregation.aggregates, cut)
    aggregation.aggregates[idx] += cut
    aggregation.partitioning[cut.id] = idx
    if full
        aggregate = aggregation.aggregates[idx]
        aggregation.aggregates[idx] = zero(AggregatedOptimalityCut{T})
        return add_cut!(lshaped, aggregate)
    end
    return false
end

function aggregate_cut!(cutqueue::CutQueue, aggregation::DynamicAggregation, ::MetaData, t::Integer, cut::HyperPlane, x::AbstractArray)
    put!(cutqueue, (t, cut))
    return nothing
end

function aggregate_cut!(cutqueue::CutQueue, aggregation::DynamicAggregation{T}, metadata::MetaData, t::Integer, cut::HyperPlane{OptimalityCut}, x::AbstractArray) where T <: AbstractFloat
    gap = fetch(metadata, t, :gap)
    if aggregation.lock(gap, t) && haskey(aggregation.partitioning, cut.id)
        aggregation.aggregates[aggregation.partitioning[cut.id]] += cut
        return nothing
    end
    (idx, full) = select(aggregation.rule, aggregation.aggregates, cut)
    aggregation.partitioning[cut.id] = idx
    aggregation.aggregates[idx] += cut
    if full
        aggregate = aggregation.aggregates[idx]
        put!(cutqueue, (t, aggregate))
        aggregation.aggregates[idx] = zero(AggregatedOptimalityCut{T})
    end
    return nothing
end

function num_thetas(nscenarios::Integer, ::DynamicAggregation)
    return nscenarios
end

function num_thetas(nscenarios::Integer, ::DynamicAggregation, ::AbstractScenarioProblems)
    return nscenarios
end

function flush!(lshaped::AbstractLShaped, aggregation::DynamicAggregation{T}) where T <: AbstractFloat
    added = false
    for (i,aggregate) in enumerate(aggregation.aggregates)
        if !iszero(aggregate)
            added |= add_cut!(lshaped, aggregate)
            aggregation.aggregates[i] = zero(AggregatedOptimalityCut{T})
        end
    end
    reset!(aggregation.rule)
    return added
end

function flush!(cutqueue::CutQueue, aggregation::DynamicAggregation{T}, ::MetaData, t::Integer, x::AbstractArray) where T <: AbstractFloat
    for (i,aggregate) in enumerate(aggregation.aggregates)
        if !iszero(aggregate)
            put!(cutqueue, (t, aggregate))
            aggregation.aggregates[i] = zero(AggregatedOptimalityCut{T})
        end
    end
    reset!(aggregation.rule)
    return nothing
end

# API
# ------------------------------------------------------------
"""
    DynamicAggregate(num_aggregates::Integer, rule::AbstractSelectionRule; lock_after::Function = (τ,n)->false)

Factory object for [`DynamicAggregation`](@ref). Pass to `aggregate ` in the `LShapedSolver` factory function. See ?DynamicAggregation for parameter descriptions.

"""
mutable struct DynamicAggregate <: AbstractAggregator
    num_aggregates::Int
    rule::AbstractSelectionRule
    lock::Function

    function DynamicAggregate(num_aggregates::Integer, rule::AbstractSelectionRule; lock_after = (τ,n)->false)
        return new(num_aggregates, rule, lock_after)
    end
end

struct SelectionRule <: AggregationParameter end

function MOI.get(aggregator::DynamicAggregate, ::SelectionRule)
    return aggregator.rule
end

function MOI.set(aggregator::DynamicAggregate, ::SelectionRule, rule::AbstractSelectionRule)
    return aggregator.rule = rule
end

function (aggregator::DynamicAggregate)(nscenarios::Integer, T::Type{<:AbstractFloat})
    if aggregator.rule isa SelectRandom && aggregator.rule.max == 1
        return NoAggregation()
    end
    return DynamicAggregation(aggregator.num_aggregates, aggregator.rule, aggregator.lock, T)
end

function remote_aggregator(aggregation::DynamicAggregation, ::AbstractScenarioProblems, ::Integer)
    return DynamicAggregate(length(aggregation.aggregates), aggregation.rule; lock_after = aggregation.lock)
end

function str(aggregator::DynamicAggregate)
    return "dynamic aggregation ruled by $(str(aggregator.rule))"
end


using covid19abm
const cv = covid19abm
using Test

const infectiousstates = (cv.LAT, cv.MILD, cv.MISO, cv.INF, cv.IISO, cv.HOS, cv.ICU)

@testset "parameters" begin
    ip = ModelParameters()
    mod_ip = cv.p ## get the module parameters
    ## set random parameters
    ip.β = 99
    ip.prov = :noknownlocation
    ip.τmild = 1 ## days before they self-isolate for mild cases
    ip.fmild = 0.05  ## percent of people practice self-isolation
    ip.fsevere = 0.80 # fixed at 0.80, always within 1 day.
    ip.eldq = 0.0 ## percentage quarantine of 60+ individuals, removes from chain of transmission
    ip.calibration = false
    ip.modeltime = 500
    reset_params(ip)
    for x in propertynames(cv.p)
        @test getfield(ip, x) == getfield(mod_ip, x)
    end
end

@testset "init" begin
    cv.reset_params_default()
    initialize()

    ## age groups
    inmodel = (1, 2, 3, 4, 5) ## possible age groups in model
    ags = []
    for x in cv.humans
        push!(ags, x.ag)
        @test x.ag in inmodel
        @test x.exp == 999
        @test x.health == cv.SUS
        @test x.swap == cv.UNDEF
        @test x.iso == false
        @test x.isovia == :null
        @test x.tis == 0
        @test x.exp == 999
        @test x.sickfrom == cv.UNDEF
    end
    @test length(unique(ags)) == length(inmodel) # check if all age groups are sampled

    ## insert_infected check if infected person is added in the correct age group
    for ag in inmodel
        initialize() # reset population
        insert_infected(cv.INF, 1, ag) # 1 infected in age group ag
        @test length(findall(x -> x.health == cv.INF && x.ag == ag, cv.humans)) == 1
    end

    ## check if the initial infected person is NOT IISO
    cv.p.fsevere = 0.0
    initialize() # reset population
    insert_infected(cv.INF, 1, 1) # 1 infected in age group ag
    @test length(findall(x -> x.health == cv.INF && x.swap == cv.REC, cv.humans)) == 1

    ## check if the initial infected person is REC (since simpleinf function only puts them to REC)
    cv.p.fsevere = 1.0
    initialize() # reset population
    insert_infected(cv.INF, 1, 1) # 1 infected in age group ag
    @test length(findall(x -> x.health == cv.INF && x.swap == cv.REC, cv.humans)) == 1

    ## check if durations are properly set
    initialize()
    randhumans = rand(1:10000, 100) # select 100 random humans, no need to test all 10000
    for i in randhumans
        @test minimum(humans[i].dur) != 0
    end
end

@testset "transitions" begin
    cv.reset_params_default()
    initialize()

    ## check if time in state is up by one
    cv.time_update()
    for x in cv.humans
        @test x.tis == 1
        @test x.exp == 999
        @test x.health == cv.SUS
        @test x.swap == cv.UNDEF ## shouldn't set a swap
    end

    for x in cv.humans
        x.exp = 0 ## this will trigger a swap
    end
    @test_throws ErrorException("swap expired, but no swap set.") cv.time_update()

    #check if it goes through all the move compartments and see if health/swap changes

    for h in 2:9  ## latent to ded, ignore susceptible
        initialize()
        rh = cv.HEALTH(h)
        for x in humans
            x.swap = rh
            x.exp = 0 ## to force the swap
        end
        time_update() ## since tis > exp
        for x in humans[1:5]
            @test x.health == rh
            if rh ∈ infectiousstates  ## for all states, except rec/ded there should be a swap
                @test x.swap != cv.UNDEF
            end
        end
    end

    # CHECKING LATENT
    initialize()
    x = humans[1]
    x.ag = 1 ## move to the first age group manually.
    myp = cv.ModelParameters()

    # check basic variables
    cv.reset_params(myp)
    move_to_latent(x)
    @test x.swap ∈ (cv.ASYMP, cv.PRE)
    @test x.iso == false
    @test x.doi == 0
    @test x.tis == 0
    @test x.exp == x.dur[1]

    # check split between asymp/pre
    myp.fasymp = 0.0
    cv.reset_params(myp)
    move_to_latent(x)
    @test x.swap == cv.PRE

    myp.fasymp = 1.0
    cv.reset_params(myp)
    move_to_latent(x)
    @test x.swap == cv.ASYMP

    # CHECKING PRE
    initialize()
    x = humans[1]
    cv.reset_params(myp)
    move_to_pre(x)
    @test x.health == cv.PRE
    @test x.swap ∈ (cv.MILD, cv.INF)

    ## check if individual moves through mild, miso through fmild, tmild parameters
    initialize()
    x = humans[1]
    x.ag = 1 ## move to the first age group manually.
    x.iso = false ## turn this off so we can test the effect of fmild, tmild
    myp.fmild = 0.0
    cv.reset_params(myp)
    move_to_mild(x)
    @test x.swap == cv.REC

    myp.fmild = 1.0
    myp.τmild = 1
    cv.reset_params(myp)
    move_to_mild(x)
    @test x.swap == cv.MISO
    @test x.exp == 1

    time_update()
    @test x.health == cv.MISO
    @test x.swap == cv.REC

    ## todo: check if x.iso and x.isovia property are set correctly.
    # 1) check if they are isolated through quarantine
    myp = cv.ModelParameters()
    myp.eldq = 1.0  ## turn on eldq
    myp.eldqag = 4
    cv.reset_params(myp)
    cv.initialize()
    for x in cv.humans
        if x.ag  == myp.eldqag
            @test x.iso == true
            @test x.isovia == :qu
        else
            @test x.iso == false
            @test x.isovia == :null
        end
    end
    # 2) check if they are isolated through presymptomatic capture
    # no need to test for mild/inf movement since it's a simple assignment in code
    # more important to check through `main` when all the function dynamics are happening.
    myp = cv.ModelParameters()
    myp.eldq = 0.0  ## turn off eldq
    myp.fsevere = 0.0 ## turn on fsevere
    myp.fmild = 0.0
    myp.fpreiso = 1.0
    cv.reset_params(myp)
    cv.initialize()
    for x in cv.humans
        cv.move_to_pre(x)
        @test x.iso == true && x.isovia == :pi
    end
end

@testset "tranmission" begin
    cv.reset_params_default()
    cv.initialize()
    grps = cv.get_ag_dist()

    # since beta = 0 default, and everyone sus
    totalinf = cv.dyntrans(1, grps)
    @test totalinf == 0

    # check with only a single infected person
    insert_infected(cv.INF, 1, 1) # 1 infected in age group ag
    totalinf = cv.dyntrans(1, grps)
    @test totalinf == 0 # still zero cuz beta = 0

    # now change beta
    cv.reset_params(ModelParameters(β = 1.0))
    totalinf = cv.dyntrans(1, grps)
    @test totalinf > 0 ## actually may still be zero because of stochasticity but very unliekly

    ## somehow check the transmission reduction and number of contacts
end

@testset "calibration" begin
    # to do, run the model and test total number of infections
    myp = ModelParameters()
    myp.β = 1.0
    myp.prov = :ontario
    myp.calibration = true
    myp.fmild = 0.0
    myp.fsevere = 0.0
    myp.fpreiso = 0.0
    myp.fasymp = 0.5
    myp.initialinf = 1
    cv.reset_params(myp)
    cv.initialize()
    grps = cv.get_ag_dist()
    cv.insert_infected(cv.PRE, 1, 4)
    # find the single insert_presymptomatic person
    h = findall(x -> x.health == cv.PRE, cv.humans)
    x = humans[h[1]]
    @test length(h) == 1
    @test x.ag == 4
    @test x.swap ∈ (cv.ASYMP, cv.MILD, cv.INF) ## always true for calibration
    for i = 1:20 ## run for 20 days
        cv.dyntrans(i, grps)
        cv.time_update()
    end
    @test x.health == cv.REC  ## make sure the initial guy recovered
    @test x.exp == 999
    ## everyone should really be in latent (or susceptible) except the recovered guy
    all = findall(x -> x.health ∈ (cv.PRE, cv.ASYMP, cv.MILD, cv.MISO, cv.INF, cv.IISO, cv.HOS, cv.ICU, cv.DED), cv.humans)
    @test length(all) == 0
    ## to do, make sure everyone stays latent

end

@testset "contact trace" begin
    # todo: check interaction between doi and tracestart, default values
    # myp = ModelParameters()
    # myp.β = 1.0
    # cv.reset_params(myp)
    # cv.initialize()
    # hdx = rand(1:10000, 20) ## sample 20 humans instead of all 10000
    # for i in hdx
    #     x = cv.humans[i]
    #     cv.ct_dynamics(x)
    #     @test x.tracing == false ## since fctcapture = 0.0
    #     @test x.tracinguntil == -1
    #     @test x.tracedby == 0
    #     @test x.tracedxp == 0
    # end
    # ## test the contact_tracing() function
    # cv.initialize()
    # grps = cv.get_ag_dist()
    # myp.fctcapture = 1.0
    # myp.fasymp = 0.0  # to force no asymp
    # cv.reset_params(myp)
    # cv.initialize()
    # tracer = cv.humans[1]
    # cv.move_to_pre(tracer) # newly presymptomatic
    # cv.ct_dynamics(tracer) # will turn on tracing
    # @test tracer.tracing == true
    # @test tracer.tracinguntil == 3

    # cv.dyntrans(1, grps) ## go through a single tranmission cycle
    # alltraced = findall(x -> x.tracedby > 0, cv.humans) ## since we havn't turned fctcapture > 0
    # @test length(alltraced) > 0
    # for i in alltraced
    #     y = cv.humans[i]
    #     @test y.tracedby == 1 ## since we used the first human as the tracing contact
    #     @test y.tracedxp == 14
    #     @test y.iso == false ## the first human is not in INF stage yet.. still in presymp
    #     @test y.isovia == :null
    # end
    # ## time update calls the ctdynamics function.. have to use this because otherwise tracer is stuck on PRE day 1.
    # cv.time_update()
    # #cv.ct_dynamics(tracer) # will turn on tracing
    # @test tracer.tracing == true
    # @test tracer.tracinguntil == 2
    # for i in alltraced
    #     y = cv.humans[i]
    #     @test y.tracedby == 1 ## since we used the first human as the tracing contact
    #     @test y.tracedxp == 14
    #     @test y.iso == false ## the first human is not in INF stage yet.. still in presymp
    #     @test y.isovia == :null
    # end
    # cv.time_update()
    # @test tracer.tracing == true
    # @test tracer.tracinguntil == 1
    # for i in alltraced
    #     y = cv.humans[i]
    #     @test y.tracedby == 1 ## since we used the first human as the tracing contact
    #     @test y.tracedxp == 14
    #     @test y.iso == false ## the first human is not in INF stage yet.. still in presymp
    #     @test y.isovia == :null
    # end
    # cv.time_update()
    # @test tracer.tracing == false
    # @test tracer.tracinguntil == 0
    # # for i in alltraced
    # #     y = cv.humans[i]
    # #     @test y.tracedby == 1
    # #     @test y.tracedxp == 13 ## one less day from 14
    # #     @test y.iso == true ## the first human is not in INF stage yet.. still in presymp
    # #     @test y.isovia == :ct
    # # end
    myp = cv.ModelParameters()
    myp.fctcapture = 1.0 # force contact tracing
    myp.fasymp = 0.0 # force to pre
    cv.reset_params(myp)
    cv.initialize()
    x = cv.humans[1]
    @test x.tracestart == -1
    @test x.traceend == -1

    cv.move_to_latent(x)
    cv.ct_dynamics(x) # since ct_dynamics will run when move_to_latent will run in time update func
    @test x.swap == cv.PRE
    @test x.doi == 0
    @test x.tis == 0
    @test x.exp == x.dur[1]
    @test x.tracestart > 0
    @test x.traceend > 0

    # go through entire inf period
    totalinfperiod = x.dur[1] + x.dur[3] + x.dur[4]
    totaltracedays = 0
    for i = 1:totalinfperiod
        # dyntrans
        cv.time_update()
        @test x.doi == i # should increase with the counter.
        if x.doi >= x.tracestart && x.doi < x.traceend
            @test x.tracing == true
            totaltracedays += 1
            println("trace true in health: $(x.HEALTH), tis: $(x.tis)")
        end
    end
    @test totaltracedays == cv.p.cdaysback
end

@testset "main run" begin
    ## run model with high beta
    myp = cv.ModelParameters()
    myp.β = 0.0525
    myp.prov = :newyork
    ## run empty scenario
    ## this wont return calibrated scenario since fasymp = 0
    myp.τmild = 0
    myp.fmild = 0.0
    myp.fsevere = 0.0
    myp.eldq = 0.0
    myp.fasymp = 0.5
    myp.fpre = 1.0
    myp.fpreiso = 0.0
    myp.tpreiso = 0
    myp.fctcapture = 0.0
    cv.runsim(1, myp) # warm up the functions
    println("time with contact tracing off:")
    @time results = cv.main(myp)

    myp.fctcapture = 1.0
    cv.runsim(1, myp)
    println("time with contact tracing on:")
    @time results = cv.main(myp)

#     function string_or_char(s::SubString)
#         length(s) == 1 && return first(s)
#         return String(s)
#     end
#     arr = Union{Char,String}[]
# for i in split_string
#     push!(arr, string_or_char(i))
# end

    # function addday(n)
    #     strtorep = "---" * "\u00B7"
    #     str = ""
    #     for i = 1:n
    #         str =str * strtorep
    #     end
    #     str = str[1:end-1]
    #     str = " " * str * " "
    #     return str
    # end

    # function printascii_epi(x)
    #     fs = ""
    #     fs = fs * "L" * addday(x.dur[1])

    #     fs = fs * "P" * addday(x.dur[3])
    #     fs = fs * "S" * addday(x.dur[4])
    #     return fs
    # end

end


@testset "broadcast" begin
  @test testf((x)       -> fill!(x, 1),  rand(3,3))
  @test testf((x, y)    -> map(+, x, y), rand(2, 3), rand(2, 3))
  @test testf((x)       -> sin.(x),      rand(2, 3))
  @test testf((x)       -> log.(x) .+ 1, rand(2, 3))
  @test testf((x)       -> 2x,           rand(2, 3))
  @test testf((x)       -> x .^ 0,      rand(2, 3))
  @test testf((x)       -> x .^ 1,      rand(2, 3))
  @test testf((x)       -> x .^ 2,      rand(2, 3))
  @test testf((x)       -> x .^ 3,      rand(2, 3))
  @test testf((x)       -> x .^ 5,      rand(2, 3))
  @test testf((x)       -> (z = Int32(5); x .^ z),      rand(2, 3))
  @test testf((x)       -> (z = Float64(π); x .^ z),      rand(2, 3))
  @test testf((x)       -> (z = Float32(π); x .^ z),      rand(Float32, 2, 3))
  @test testf((x, y)    -> x .+ y,       rand(2, 3), rand(1, 3))
  @test testf((z, x, y) -> z .= x .+ y,  rand(2, 3), rand(2, 3), rand(2))
  @test (CuArray{Ptr{Cvoid}}(undef, 1) .= C_NULL) == CuArray([C_NULL])
  @test CuArray([1,2,3]) .+ CuArray([1.0,2.0,3.0]) == CuArray([2,4,6])

  @eval struct Whatever{T}
      x::Int
  end
  @test Array(Whatever{Int}.(CuArray([1]))) == Whatever{Int}.([1])
end
# https://github.com/JuliaGPU/CUDA.jl/issues/223
@testset "Ref Broadcast" begin
  foobar(idx, A) = A[idx]
  @test CuArray([42]) == foobar.(CuArray([1]), Base.RefValue(CuArray([42])))
end

@testset "Broadcast Fix" begin
  @test testf(x -> log.(x), rand(3,3))
  @test testf((x,xs) -> log.(x.+xs), Ref(1), rand(3,3))
end


#
# Miscellaneous functions
#

# Redefine unsafe_load, unsafe_store!, read, and write so that they pack the type
function define_packed(ty::DataType)
    @assert isbits(ty)
    packed_offsets = cumsum([sizeof(x) for x in ty.types])
    sz = pop!(packed_offsets)
    unshift!(packed_offsets, 0)

    if sz != sizeof(ty)
        @eval begin
            function Base.unsafe_store!(p::Ptr{$ty}, x::$ty)
                $([:(unsafe_store!(convert(Ptr{$(ty.types[i])}, p+$(packed_offsets[i])), getfield(x, $i)))
                   for i = 1:length(packed_offsets)]...)
            end
            function Base.unsafe_load(p::Ptr{$ty})
                $(Expr(:new, ty, [:(unsafe_load(convert(Ptr{$(ty.types[i])}, p+$(packed_offsets[i]))))
                                   for i = 1:length(packed_offsets)]...))
            end
            Base.sizeof(::Union{$ty,Type{$ty}}) = $sz
        end
    end

    @eval begin
        @inline Base.write(io::MmapIO, x::$ty) = _write(io, x)
        @inline Base.read(io::MmapIO, x::Type{$ty}) = _read(io, x)
        function Base.read(io::IO, ::Type{$ty})
            $(Expr(:new, ty, [:(read(io, $(ty.types[i]))) for i = 1:length(packed_offsets)]...))
        end
        function Base.write(io::IO, x::$ty)
            $([:(write(io, getfield(x, $i))) for i = 1:length(packed_offsets)]...)
            nothing
        end
    end
    nothing
end

# Loads a variable-length size according to flags
# Expects that the first two bits of flags mean:
# 0   The size of the Length of Link Name field is 1 byte.
# 1   The size of the Length of Link Name field is 2 bytes.
# 2   The size of the Length of Link Name field is 4 bytes.
# 3   The size of the Length of Link Name field is 8 bytes.
# Returns the size as a UInt and a new pointer that is offset by the
# size of the size field
function read_size(io::IO, flags::UInt8)
    if (flags & 1) == 0 && (flags & 2) == 0
        Int(read(io, UInt8))
    elseif (flags & 1) == 1 && (flags & 2) == 0
        Int(read(io, UInt16))
    elseif (flags & 1) == 0 && (flags & 2) == 2
        Int(read(io, UInt32))
    else
        Int(read(io, UInt64))
    end
end

# Determine what the size flag should be
# Same rules as above
function size_flag(sz::Integer)
    if sz <= typemax(UInt8)
        UInt8(0)
    elseif sz <= typemax(UInt16)
        UInt8(1)
    elseif sz <= typemax(UInt32)
        UInt8(2)
    else
        UInt8(3)
    end
end

# Store a size
function write_size(io::IO, sz::Integer)
    if sz <= typemax(UInt8)
        write(io, UInt8(sz))
    elseif sz <= typemax(UInt16)
        write(io, UInt16(sz))
    elseif sz <= typemax(UInt32)
        write(io, UInt32(sz))
    else
        write(io, UInt64(sz))
    end
end

# Get the size of the size
function size_size(sz::Integer)
    if sz <= typemax(UInt8)
        1
    elseif sz <= typemax(UInt16)
        2
    elseif sz <= typemax(UInt32)
        4
    else
        8
    end
end


module LDScore
    include("ldsc.jl")
    include("LDScore/LDScoreRegression.jl")
    include("LDScore/Hsq.jl")
    include("LDScore/regression.jl")
    include("LDScore/sumstats.jl")
    include("LDScore/parse.jl")

    export hsq_weights
    export aggregate
    export estimate_h2
    export _read_sumstats
    export parse_sumstats
    export args
end


import POSIXClock

typeof(Pkg.installed("Gadfly"))==Void &&
  error("Please install Gadfly package to run this example.")
import Gadfly; pl=Gadfly

const interval = 200000 # 200us
function run()
  nItr = div(60000000000,interval)
  # pre-allocate samples array
  differ = Vector{Int64}(nItr+1)
  # pre-allocate timespec
  t = POSIXClock.timespec(0,0)
  actual_t = POSIXClock.timespec(0,0)

  # Lock future memory allocations, disable GC
  ccall(:mlockall, Cint, (Cint,), 2) # CAUTION: will crash Julia on future memory allocations
  gc_enable(false)

  n = 0
  POSIXClock.gettime!(t,POSIXClock.CLOCK_MONOTONIC)
  @time while n <= nItr
    POSIXClock.nanosleep!(t,interval)
    POSIXClock.gettime!(actual_t,POSIXClock.CLOCK_MONOTONIC)
    n+=1
    differ[n]=actual_t-t
  end

  # Unlock memory allocations, enable GC
  ccall(:munlockall, Cint, ())
  gc_enable(true)

  return differ
end

const d=run()
histplot=pl.plot(
  x=d/1000, pl.Geom.histogram(),
  pl.Guide.xlabel("Latency (us)"),
  pl.Guide.ylabel("Histogram counts"))


module Modalities

"""
    NeuroModality

Supertype for traits corresponding to neuroinformatics modalities.
"""
abstract type NeuroModality end

"""
    NeuroNone

Indicates the complete absence of any `NeuroModality`.
"""
struct NeuroNone <: NeuroModality end

"""
    modality(x) -> NeuroModality

Returns the underlying `NeuroModality`, if present. Otherwise returns
`NeuroNone`.
"""
modality(x::T) where {T} = NeuroNone

modality(x::NeuroModality) = x

"""
    HasTimeComponent

Trait type for whether a modality has a time component.
"""
struct HasTimeComponent{T} end

HasTimeComponent(::T) where {T} = HasTimeComponent(T)

HasTimeComponent(::Type{T}) where {T} = HasTimeComponent{false}()

"""
    Imaging
"""
abstract type Imaging <: NeuroModality end

"""
    Localizer Scan
"""
struct LocalizerScan <: Imaging end

"""
    MagneticResonanceImaging (MRI)

Magnetic resonance imaging.
"""
abstract type MagneticResonanceImaging <: Imaging end

include("smri.jl")
include("dwi.jl")
include("fmri.jl")
include("xray.jl")
include("electrophysiology.jl")

###
### Diffusion
###

abstract type DiffusionMRI <: MagneticResonanceImaging end

struct SingleShellDTI <: DiffusionMRI end

struct MultiShellDTI <: DiffusionMRI end

struct Microscopy <: Imaging end

end


struct ValidationError <: Exception
    stmt::String
    got::String
end

function Base.show(io::IO, err::ValidationError)
    print(io, "ValidationError: got ")
    printstyled(io, err.got; color=:light_green)
    print(io, " failed to validate ")
    printstyled(io, err.stmt; color=:light_cyan)
end

struct Constraint{T, F}
    x::T
end

function Base.show(io::IO, con::Constraint{T, F}) where {T, F}
    show(io, con.x)
    printstyled(io, " #=", F, "=#"; color=:light_black)
end

const Positive{T} = Constraint{T, >(0)}
const Negative{T} = Constraint{T, <(0)}

for N in [16, 32, 64]
    PF = Symbol(:PositiveF, N)
    NF = Symbol(:NegativeF, N)
    F = Symbol(:Float, N)

    @eval begin
        export $PF, $NF
        const $PF = Positive{$F}
        const $NF = Negative{$F}
    end
end

for N in [8, 16, 32, 64, 128]
    PI = Symbol(:PositiveI, N)
    NI = Symbol(:NegativeI, N)
    I = Symbol(:Int, N)

    @eval begin
        export $PI, $NI
        const $PI = Positive{$I}
        const $NI = Negative{$I}
    end
end

function Base.convert(::Type{Constraint{T, F}}, x) where {T, F}
    F(x) || throw(ValidationError("$x does not satisfy $F"))
    return Constraint{T, F}(x)
end

struct MultipleOf{T}
    n::T
end

Base.show(io::IO, f::MultipleOf) = print(io, "multiple_of(", f.n, ")")
(f::MultipleOf)(x::Integer) = multiple_of(x, f.n)

struct SecretString <: AbstractString
    content::String
end

Base.length(s::SecretString) = length(s.content)
# do not indicate the length
Base.show(io::IO, ::SecretString) = print(io, "********")
Base.print(io::IO, ::SecretString) = print(io, "********")

struct LessThan{T}
    x::T
end

struct LessEqual{T}
    x::T
end

struct GreaterThan{T}
    x::T
end

struct GreaterEqual{T}
    x::T
end

(f::LessThan)(y) = y < f.x
(f::LessEqual)(y) = y ≤ f.x
(f::GreaterThan)(y) = y > f.x
(f::GreaterEqual)(y) = y ≥ f.x

struct Interval{L <: Union{GreaterThan, GreaterEqual}, U <: Union{LessThan, LessEqual}}
    lower::L
    upper::U
end

function interval(;gt=nothing, lt=nothing, ge=nothing, le=nothing)
    @match (gt, lt, ge, le) begin
        (nothing, nothing, nothing, nothing) => throw(ArgumentError("no boundary is specified"))
        (gt, nothing, nothing, nothing) => GreaterThan(gt)
        (nothing, lt, nothing, nothing) => LessThan(lt)
        (nothing, nothing, ge, nothing) => GreaterEqual(ge)
        (nothing, nothing, nothing, le) => LessEqual(le)

        (gt, lt, nothing, nothing) => Interval(GreaterThan(gt), LessThan(lt))
        (gt, nothing, nothing, le) => Interval(GreaterThan(gt), LessEqual(le))
        (nothing, lt, ge, nothing) => Interval(GreaterEqual(ge), LessThan(lt))
        (nothing, nothing, ge, le) => Interval(GreaterEqual(ge), LessEqual(le))

        (gt, nothing, ge, nothing) => throw(ArgumentError("duplicated upperbound: gt=$gt, ge=$ge"))
        (nothing, lt, nothing, le) => throw(ArgumentError("duplicated upperbound: lt=$lt, le=$le"))
        _ => error("case not handled")
    end
end

Base.show(io::IO, x::GreaterThan) = print(io, "(", x.x, ", ∞)")
Base.show(io::IO, x::GreaterEqual) = print(io, "[", x.x, ", ∞)")
Base.show(io::IO, x::LessThan) = print(io, "(-∞, ", x.x, ")")
Base.show(io::IO, x::LessEqual) = print(io, "(-∞, ", x.x, "]")

function Base.show(io::IO, x::Interval)
    print_interval(io, x.lower)
    print(io, ", ")
    print_interval(io, x.upper)
end

print_interval(io::IO, x::GreaterEqual) = print(io, "[", x.x)
print_interval(io::IO, x::GreaterThan) = print(io, "(", x.x)
print_interval(io::IO, x::LessThan) = print(io, x.x, ")")
print_interval(io::IO, x::LessEqual) = print(io, x.x, "]")


@testset "Condition based iteration" begin
    @testset "Ambiguous nucleotide positions" begin
        function test_ns(A, seq)
            expected = Tuple{Int, eltype(A)}[]
            for i in 1:length(seq)
                if seq[i] == 'N'
                    push!(expected, (i, DNA_N))
                end
            end
            bioseq = LongSequence{A}(seq)
            @test collect(each(isambiguous, bioseq)) == expected
        end

        for len in [1, 10, 32, 1000, 10000, 100000]
            test_ns(DNAAlphabet{4}, random_dna(len))
            test_ns(RNAAlphabet{4}, random_rna(len))

            probs = [0.25, 0.25, 0.25, 0.25, 0.00]
            test_ns(DNAAlphabet{2}, random_dna(len, probs))
            test_ns(RNAAlphabet{2}, random_rna(len, probs))
        end

        dna_seq = dna"ANANANA"
        pos = [2, 4, 6]
        j = 1
        for (p, nuc) in each(isambiguous, dna_seq)
            @test p == pos[j]
            j += 1
        end

        dna_seq = dna"NATTCGRATY"
        pos = [1, 7, 10]
        j = 1
        for (p, nuc) in each(isambiguous, dna_seq)
            @test p == pos[j]
            j += 1
        end

        rna_seq = rna"ANANANA"
        pos = [2, 4, 6]
        j = 1
        for (p, nuc) in each(isambiguous, rna_seq)
            @test p == pos[j]
            j += 1
        end

        rna_seq = rna"NAUUCGRAUY"
        pos = [1, 7, 10]
        j = 1
        for (p, nuc) in each(isambiguous,rna_seq)
            @test p == pos[j]
            j += 1
        end
    end
end


function Base.rand(method::S, sdm::M) where {S<:SpatialSampler,M<:AbstractMatrix}
    _generate!(method, sdm)
end


# Modified from Shreyas Kowshik's implementation.

"""
Categorical policy, for discrete action spaces.
"""
mutable struct CategoricalPolicy
    π # Neural network for the policy
    value_net # Neural network for the value function
    solver # Reference to the TRPOSolver or PPOSolver
end

function CategoricalPolicy(solver)
    policy_net = Chain(Dense(solver.state_size, solver.hidden_layer_size, relu; initW=_random_normal, initb=constant_init),
                       Dense(solver.hidden_layer_size, solver.action_size; initW=_random_normal, initb=constant_init),
                       x -> softmax(x))

    value_net = Chain(Dense(solver.state_size, solver.hidden_layer_size ,relu; initW=_random_normal),
                      Dense(solver.hidden_layer_size, solver.hidden_layer_size, relu; initW=_random_normal),
                      Dense(solver.hidden_layer_size, 1; initW=_random_normal))

    return CategoricalPolicy(policy_net, value_net, solver)
end

"""
Diagonal Gaussian policy, for continuous action space.
"""
mutable struct DiagonalGaussianPolicy
    μ # Neural network for the mean of the Gaussian distribution
    logΣ # Neural network for the log standard deviation of the Gaussian distribution
    value_net # Neural network for the value function
    solver # Reference to the TRPOSolver or PPOSolver
end

function DiagonalGaussianPolicy(solver, log_std)
    μ = Chain(Dense(solver.state_size, solver.hidden_layer_size, tanh; initW=_random_normal, initb=constant_init),
              Dense(solver.hidden_layer_size, solver.action_size; initW=_random_normal, initb=constant_init),
              x->tanh.(x),
              x->param(2.0) .* x) # TODO. Make this vector of environment STDs (i.e. scale it to [-x, +x] action bound)

    value_net = Chain(Dense(solver.state_size, solver.hidden_layer_size, tanh; initW=_random_normal),
                      Dense(solver.hidden_layer_size, solver.hidden_layer_size, tanh; initW=_random_normal),
                      Dense(solver.hidden_layer_size, 1; initW=_random_normal))

    logΣ = param(ones(solver.action_size) * log_std)

    return DiagonalGaussianPolicy(μ, logΣ, value_net, solver)
end

"""
Takes in the policy and returns an action based on the current state.
"""
function get_action(policy::CategoricalPolicy, state)
    action_probs = policy.π(state).data
    action_probs = reshape(action_probs, policy.solver.action_size)
    return Distributions.sample(1:policy.solver.action_size, Distributions.Weights(action_probs))
end

function get_action(policy::DiagonalGaussianPolicy, state)
    # Our policy outputs the parameters of a Normal distribution
    μ = reshape(policy.μ(state), policy.solver.action_size)
    log_std = policy.logΣ

    σ² = (exp.(log_std)).^2
    Σ = diagm(0=>σ².data)
    distribution = MvNormal(μ.data, Σ)

    return rand(distribution, policy.solver.action_size)
end

test_action(policy::CategoricalPolicy, state) = get_action(policy, state)
test_action(policy::DiagonalGaussianPolicy, state) = policy.μ(state).data # Use only the mean for prediction

"""
Translate action from neural network policy to either an ASTSeedAction or an ASTSampleAction
"""
function translate_ast_action(sim::GrayBox.Simulation, action, ::Type{ASTSeedAction})
    # Convert to seed: NOTE, not every useful when using PPO---effectively random. You've been warned (try MCTS)!
    return ASTSeedAction(hash_uint32(action...))
end

function translate_ast_action(sim::GrayBox.Simulation, action, ::Type{ASTSampleAction})
    gray_environment = GrayBox.environment(sim)
    environment_sample = GrayBox.EnvironmentSample()
    for (i,k) in enumerate(keys(gray_environment))
        # log-probability from the environment's distributions (not the log_prob from the NN policy)
        logp = logpdf(gray_environment[k], action[i])
        environment_sample[k] = GrayBox.Sample(action[i], logp)
    end
    return ASTSampleAction(environment_sample)
end

"""
Return the log-probability of an action under the current policy parameters.
"""
function log_prob(policy::CategoricalPolicy, states::Array, actions::Array)
    action_probs = policy.π(states)
    actions_one_hot = zeros(policy.solver.action_size, size(action_probs)[end])
    for i in 1:size(action_probs)[end]
        actions_one_hot[actions[:,i][1],i] = 1.0
    end
    return log.(sum((action_probs .+ 1f-5) .* actions_one_hot, dims=1))
end

function log_prob(policy::DiagonalGaussianPolicy, states::Array, actions::Array)
    μ = policy.μ(states)
    σ = exp.(policy.logΣ)
    σ² = σ.^2
    log_probs = broadcast(-, ((actions .- μ).^2)./(2 .* σ²)) .- 0.5*log.(sqrt(2π)) .- log.(σ)
    return log_probs
end

"""
Return the entropy of the policy distribution.
"""
function entropy(policy::CategoricalPolicy, states::Array)
    action_probs = policy.π(states)
    return sum(action_probs .* log.(action_probs .+ 1f-10), dims=1)
end

entropy(policy::DiagonalGaussianPolicy, states::Array) = 0.5 + 0.5 * log(2π) .+ policy.logΣ

"""
kl_params:

`old_log_probs` : CategoricalPolicy

`Array([μ, logΣ])` : DiagonalGaussianPolicy
"""
function kl_divergence(policy::CategoricalPolicy, kl_params, states::Array)
    old_log_probs = hcat(cat(kl_params..., dims=1)...)

    action_probs = policy.π(states)
    log_probs = log.(action_probs)

    log_ratio = log_probs .- old_log_probs
    kl_div = (exp.(old_log_probs)) .* log_ratio

    return -1.0f0 .* sum(kl_div, dims=1)
end

function kl_divergence(policy::DiagonalGaussianPolicy, kl_params, states::Array)
    μ0 = policy.μ(states)
    logΣ0 = policy.logΣ
    μ1 = mus = hcat([kl_params[i][1] for i in 1:length(kl_params)]...)
    logΣ1 = hcat([kl_params[i][2] for i in 1:length(kl_params)]...)

    var0 = exp.(2 .* logΣ0)
    var1 = exp.(2 .* logΣ1)
    pre_sum = 0.5 .* (((μ0 .- μ1).^2 .+ var0) ./ (var1 .+ 1e-8) .- 1.0f0) .+ logΣ1 .- logΣ0
    kl = sum(pre_sum, dims=1)

    return kl
end

get_policy_params(policy::CategoricalPolicy) = Flux.params(policy.π)
get_policy_params(policy::DiagonalGaussianPolicy) = Flux.params(Flux.params(policy.μ)..., Flux.params(policy.logΣ)...)

get_policy_net(policy::CategoricalPolicy) = [policy.π]
get_policy_net(policy::DiagonalGaussianPolicy) = [policy.μ, policy.logΣ]

get_value_params(policy::Union{CategoricalPolicy, DiagonalGaussianPolicy}) = Flux.params(policy.value_net)

get_value_net(policy::Union{CategoricalPolicy, DiagonalGaussianPolicy}) = [policy.value_net]

function get_policy(solver)
    if solver.policy_type == :discrete
        return CategoricalPolicy(solver)
    elseif solver.policy_type == :continuous
        return DiagonalGaussianPolicy(solver, solver.log_std)
    else
        error("Policy type not supported, $(solver.policy_type)")
    end
end

# TODO. Generalize for discrete spaces.
function set_action_size!(solver, mdp::MDP)
    if actiontype(mdp)== ASTSeedAction
        solver.action_size = 1
    elseif actiontype(mdp) == ASTSampleAction
        solver.action_size = length(GrayBox.environment(mdp.sim))
    else
        @warn "solver.action_size not set for actiontype: $(actiontype(mdp))"
    end
    return solver
end


using FileIO, GeometryBasics
using Test
const tf = joinpath(dirname(@__FILE__), "testfiles")
using MeshIO

function test_face_indices(mesh)
    for face in faces(mesh)
        for index in face
            pass = firstindex(coordinates(mesh)) <= index <= lastindex(coordinates(mesh))
            pass || return false
        end
    end
    return true
end

@testset "MeshIO" begin
    dirlen = 1f0
    baselen = 0.02f0
    mesh = [
        FRect3D(Vec3f0(baselen), Vec3f0(dirlen, baselen, baselen)),
        FRect3D(Vec3f0(baselen), Vec3f0(baselen, dirlen, baselen)),
        FRect3D(Vec3f0(baselen), Vec3f0(baselen, baselen, dirlen))
    ]
    mesh = merge(map(triangle_mesh, mesh))

    mktempdir() do tmpdir
        for ext in ["2dm", "off"]
            @testset "load save $ext" begin
                save(joinpath(tmpdir, "test.$ext"), mesh)
                mesh_loaded = load(joinpath(tmpdir, "test.$ext"))
                @test mesh_loaded == mesh
            end
        end
        @testset "PLY ascii and binary" begin
            f = File(format"PLY_ASCII", joinpath(tmpdir, "test.ply"))
            save(f, mesh)
            mesh_loaded = load(joinpath(tmpdir, "test.ply"))
            @test mesh_loaded == mesh
            save(File(format"PLY_BINARY", joinpath(tmpdir, "test.ply")), mesh)
        end
        @testset "STL ascii and binary" begin
            save(File(format"STL_ASCII", joinpath(tmpdir, "test.stl")), mesh)
            mesh_loaded = load(joinpath(tmpdir, "test.stl"))
            @test Set(mesh.position) == Set(mesh_loaded.position)
            save(File(format"STL_BINARY", joinpath(tmpdir, "test.stl")), mesh)
            mesh_loaded = load(joinpath(tmpdir, "test.stl"))
            @test Set(mesh.position) == Set(mesh_loaded.position)
        end
    end
    @testset "Real world files" begin

        @testset "STL" begin
            msh = load(joinpath(tf, "ascii.stl"))
            @test length(faces(msh)) == 12
            @test length(coordinates(msh)) == 36
            @test length(normals(msh)) == 36
            @test test_face_indices(msh)

            msh = load(joinpath(tf, "binary.stl"))
            @test msh isa GLNormalMesh
            @test length(faces(msh)) == 828
            @test length(coordinates(msh)) == 2484
            @test length(msh.normals) == 2484
            @test test_face_indices(msh)

            mktempdir() do tmpdir
                save(File(format"STL_BINARY", joinpath(tmpdir, "test.stl")), msh)
                msh1 = load(joinpath(tmpdir, "test.stl"))
                @test msh1 isa GLNormalMesh
                @test faces(msh) == faces(msh1)
                @test coordinates(msh) == coordinates(msh1)
                @test msh.normals == msh1.normals
            end

            msh = load(joinpath(tf, "binary_stl_from_solidworks.STL"))
            @test msh isa GLNormalMesh
            @test length(faces(msh)) == 12
            @test length(coordinates(msh)) == 36
            @test test_face_indices(msh)

            # STL Import
            msh = load(joinpath(tf, "cube_binary.stl"))
            @test length(coordinates(msh)) == 36
            @test length(faces(msh)) == 12
            @test test_face_indices(msh)

            msh = load(joinpath(tf, "cube.stl"))
            @test length(coordinates(msh)) == 36
            @test length(faces(msh)) == 12
            @test test_face_indices(msh)

        end
        @testset "PLY" begin
            msh = load(joinpath(tf, "ascii.ply"))
            @test length(faces(msh)) == 36
            @test test_face_indices(msh)
            @test length(coordinates(msh)) == 72
            # msh = load(joinpath(tf, "binary.ply")) # still missing
            #@test msh isa GLNormalMesh
            msh = load(joinpath(tf, "cube.ply")) # quads
            @test length(coordinates(msh)) == 24
            @test length(faces(msh)) == 12
            @test test_face_indices(msh)
        end
        @testset "OFF" begin
            msh = load(joinpath(tf, "test.off"))
            @test length(faces(msh)) == 28
            @test length(coordinates(msh)) == 20
            @test test_face_indices(msh)

            msh = load(joinpath(tf, "test2.off"))
            @test length(faces(msh)) == 810
            @test length(coordinates(msh)) == 405
            @test test_face_indices(msh)

            msh = load(joinpath(tf, "cube.off"))
            @test length(faces(msh)) == 12
            @test length(coordinates(msh)) == 8
            @test test_face_indices(msh)
        end
        @testset "OBJ" begin
            msh = load(joinpath(tf, "test.obj"))
            @test length(faces(msh)) == 3954
            @test length(coordinates(msh)) == 2520
            @test length(normals(msh)) == 2520
            @test test_face_indices(msh)

            msh = load(joinpath(tf, "cube.obj")) # quads
            @test length(faces(msh)) == 12
            @test length(coordinates(msh)) == 8
            @test test_face_indices(msh)

            msh = load(joinpath(tf, "polygonal_face.obj"))
            @test length(faces(msh)) == 4
            @test length(coordinates(msh)) == 6
            @test test_face_indices(msh)

            msh = load(joinpath(tf, "test_face_normal.obj"))
            @test length(faces(msh)) == 1
            @test length(coordinates(msh)) == 3
            @test test_face_indices(msh)
        end
        @testset "2DM" begin
            msh = load(joinpath(tf, "test.2dm"))
            @test test_face_indices(msh)
        end
        @testset "GMSH" begin
            msh = load(joinpath(tf, "cube.msh"))
            @test length(faces(msh)) == 24
            @test length(coordinates(msh)) == 14
            @test test_face_indices(msh)
        end
    end
end


using ALGCM1D
using Test
using Dates

@testset "ALGCM1D.jl" begin
    # Write your tests here.
    stime=DateTime(0,7,1)
    ALGCM1D.run(stime)
end


function uniquearrayrefs(ls::LoopSet)
    uniquerefs = ArrayReferenceMeta[]
    # for arrayref ∈ ls.refs_aliasing_syms
    for op ∈ operations(ls)
        arrayref = op.ref
        arrayref === NOTAREFERENCE && continue
        any(ref -> sameref(arrayref, ref), uniquerefs) || push!(uniquerefs, arrayref)
    end
    uniquerefs
end

otherindexunrolled(loopsym::Symbol, ind::Symbol, loopdeps::Vector{Symbol}) = (loopsym !== ind) && (loopsym ∈ loopdeps)
function otherindexunrolled(ls::LoopSet, ind::Symbol, ref::ArrayReferenceMeta)
    us = ls.unrollspecification[]
    u₁sym = names(ls)[us.u₁loopnum]
    u₂sym = us.u₂loopnum > 0 ? names(ls)[us.u₂loopnum] : Symbol("##undefined##")
    otherindexunrolled(u₁sym, ind, loopdependencies(ref)) || otherindexunrolled(u₂sym, ind, loopdependencies(ref))
end
multiple_with_name(n::Symbol, v::Vector{ArrayReferenceMeta}) = sum(ref -> n === vptr(ref), v) > 1
# TODO: DRY between indices_calculated_by_pointer_offsets and use_loop_induct_var
function indices_calculated_by_pointer_offsets(ls::LoopSet, ar::ArrayReferenceMeta)
    indices = getindices(ar)
    ls.isbroadcast[] && return fill(false, length(indices))
    looporder = names(ls)
    offset = isdiscontiguous(ar)
    gespinds = Expr(:tuple)
    out = Vector{Bool}(undef, length(indices))
    li = ar.loopedindex
    # @show ls.vector_width[]
    for i ∈ eachindex(li)
        ii = i + offset
        ind = indices[ii]
        # j = findfirst(isequal(ind), view(indices, offset+1:ii-1))
        # if !isnothing(j)
        #     out[i] = out[j - offset]
        #     continue
        # end
        if (!li[i]) || multiple_with_name(vptr(ar), ls.lssm[].uniquearrayrefs) ||
            (iszero(ls.vector_width[]) && isstaticloop(getloop(ls, ind)))# ||
            # ((ls.align_loops[] > 0) && (first(names(ls)) == ind))
            out[i] = false
        elseif (isone(ii) && (first(looporder) === ind))
            out[i] = otherindexunrolled(ls, ind, ar)
        else
            out[i] = true
        end
    end
    out
end

"""
Returns a vector of length equal to the number of indices.
A value > 0 indicates which loop number that index corresponds to when incrementing the pointer.
A value < 0 indicates that abs(value) is the corresponding loop, and a `loopvalue` will be used.
"""
function use_loop_induct_var!(ls::LoopSet, q::Expr, ar::ArrayReferenceMeta, allarrayrefs::Vector{ArrayReferenceMeta})
    us = ls.unrollspecification[]
    li = ar.loopedindex
    looporder = reversenames(ls)
    uliv = Vector{Int}(undef, length(li))
    indices = getindices(ar)
    offset = first(indices) === Symbol("##DISCONTIGUOUSSUBARRAY##")
    if length(indices) != offset + length(li)
        println(ar)
        throw("Length of indices and length of offset do not match!")
    end
    isbroadcast = ls.isbroadcast[]
    gespinds = Expr(:tuple)
    offsetprecalc_descript = Expr(:tuple)
    use_offsetprecalc = false
    for i ∈ eachindex(li)
        ii = i + offset
        ind = indices[ii]
        # j = findfirst(isequal(ind), view(indices, offset+1:ii-1))
        # if !isnothing(j)
        #     j -= offset
        #     push!(gespinds.args, gespinds.args[j])
        #     uliv[i] = uliv[j]
        # else
        if (!li[i])
            uliv[i] = 0
            push!(gespinds.args, Expr(:call, lv(:Zero)))
            push!(offsetprecalc_descript.args, 0)
        elseif isbroadcast ||
            ((isone(ii) && (last(looporder) === ind)) && !(otherindexunrolled(ls, ind, ar)) ||
             multiple_with_name(vptr(ar), allarrayrefs)) ||
             (iszero(ls.vector_width[]) && isstaticloop(getloop(ls, ind)))# ||
             # ((ls.align_loops[] > 0) && (first(names(ls)) == ind))

            # Not doing normal offset indexing
            uliv[i] = -findfirst(isequal(ind), looporder)::Int
            push!(gespinds.args, Expr(:call, lv(:Zero)))
            push!(offsetprecalc_descript.args, 0) # not doing offset indexing, so push 0
        else
            uliv[i] = findfirst(isequal(ind), looporder)::Int
            loop = getloop(ls, ind)
            if loop.startexact
                push!(gespinds.args, Expr(:call, Expr(:curly, lv(:Static), loop.starthint - 1)))
            else
                push!(gespinds.args, Expr(:call, lv(:staticm1), loop.startsym))
            end
            if ind === names(ls)[us.vectorizedloopnum]
                push!(offsetprecalc_descript.args, 0)
            elseif (ind === names(ls)[us.u₁loopnum]) & (us.u₁ > 3)
                use_offsetprecalc = true
                push!(offsetprecalc_descript.args, us.u₁)
            elseif (ind === names(ls)[us.u₂loopnum]) & (us.u₂ > 3)
                use_offsetprecalc = true
                push!(offsetprecalc_descript.args, us.u₂)
            else
                push!(offsetprecalc_descript.args, 0)
            end
        end
    end
    if use_offsetprecalc
        push!(q.args, Expr(:(=), vptr(ar), Expr(:call, lv(:offsetprecalc), Expr(:call, lv(:gesp), vptr(ar), gespinds), Expr(:call, Expr(:curly, :Val, offsetprecalc_descript)))))
    else
        push!(q.args, Expr(:(=), vptr(ar), Expr(:call, lv(:gesp), vptr(ar), gespinds)))
    end
    uliv
end

# Plan here is that we increment every unique array
function add_loop_start_stop_manager!(ls::LoopSet)
    q = Expr(:block)
    us = ls.unrollspecification[]
    # Presence of an explicit use of a loopinducation var means we should use that, so we look for one
    # TODO: replace first with only once you add Compat as a dep or drop support for older Julia versions
    loopinductvars = map(op -> first(loopdependencies(op)), filter(isloopvalue, operations(ls)))
    # Filtered ArrayReferenceMetas, we must increment each
    arrayrefs = uniquearrayrefs(ls)
    use_livs = map(ar -> use_loop_induct_var!(ls, q, ar, arrayrefs), arrayrefs)
    # loops, sorted from outer-most to inner-most
    looporder = reversenames(ls)
    # For each loop, we need to choose an induction variable
    nloops = length(looporder)
    # loopstarts = Vector{Vector{ArrayReferenceMeta}}(undef, nloops)
    loopstarts = fill(ArrayReferenceMeta[], nloops)
    terminators = Vector{Int}(undef, nloops) # zero is standard loop induct var
    # loopincrements = Vector{Vector{ArrayReferenceMeta}}(undef, nloops) # Not needed; copy of loopstarts
    # The purpose of the reminds thing here is to pick which of these to use for the terminator
    # We want to pick the one with the fewest outer loops with respect to this one, to minimize
    # the number of redefinitions of max-pointer used for the termination comparison.
    reached_indices = zeros(Int, length(arrayrefs))
    for (i,loopsym) ∈ enumerate(looporder) # iterates from outer to inner
        loopstartᵢ = ArrayReferenceMeta[]; arⱼ = 0; minrem = typemax(Int);
        ric = Tuple{Int,Int}[]
        for j ∈ eachindex(use_livs) # j is array ref number
            for (l,k) ∈ enumerate(use_livs[j])# l is index number, k is loop number
                if k == i
                    push!(loopstartᵢ, arrayrefs[j])
                    push!(ric, ((reached_indices[j] += 1), length(loopstartᵢ)))
                end
            end
        end
        loopstarts[i] = loopstartᵢ
        terminators[i] = if (loopsym ∈ loopinductvars) || (any(r -> any(isequal(-i), r), use_livs)) || iszero(length(loopstartᵢ))
            0
        else
            # @show i, loopsym loopdependencies.(operations(ls)) operations(ls)
            # @assert !iszero(length(loopstartᵢ))
            last(ric[argmin(first.(ric))]) # index corresponds to array ref's position in loopstart
        end
    end
    ls.lssm[] = LoopStartStopManager(
        reverse!(terminators), reverse!(loopstarts), arrayrefs
    )
    q
end
maxsym(ptr, sub) = Symbol(ptr, "##MAX##", sub, "##")
function pointermax(ls::LoopSet, ar::ArrayReferenceMeta, n::Int, sub::Int, isvectorized::Bool)::Expr
    pointermax(ls, ar, n, sub, isvectorized, getloop(ls, names(ls)[n]))
end
function pointermax(ls::LoopSet, ar::ArrayReferenceMeta, n::Int, sub::Int, isvectorized::Bool, loop::Loop)::Expr
    pointermax(ls, ar, n, sub, isvectorized, looplengthexpr(loop, n))::Expr
end
function pointermax(ls::LoopSet, ar::ArrayReferenceMeta, n::Int, sub::Int, isvectorized::Bool, stophint::Int)::Expr
    # @unpack u₁loopnum, u₂loopnum, vectorizedloopnum, u₁, u₂ = us
    loopsym = names(ls)[n]
    index = Expr(:tuple)
    for i ∈ getindicesonly(ar)
        if i === loopsym
            if iszero(sub)
                push!(index.args, stophint)
            elseif isvectorized
                if isone(sub)
                    push!(index.args, Expr(:call, lv(:valsub), stophint, VECTORWIDTHSYMBOL))
                else
                    push!(index.args, Expr(:call, lv(:vsub), stophint, Expr(:call, lv(:valmul), VECTORWIDTHSYMBOL, sub)))
                end
            else
                push!(index.args, stophint - sub)
            end
            ptr = vptr(ar)
            return Expr(:call, lv(:pointerforcomparison), ptr, index)
        else
            push!(index.args, Expr(:call, lv(:Zero)))
        end
    end
    @show ar, loopsym
end
function pointermax(ls::LoopSet, ar::ArrayReferenceMeta, n::Int, sub::Int, isvectorized::Bool, stopsym)::Expr
    # @unpack u₁loopnum, u₂loopnum, vectorizedloopnum, u₁, u₂ = us
    loopsym = names(ls)[n]
    index = Expr(:tuple)
    for i ∈ getindicesonly(ar)
        if i === loopsym
            if iszero(sub)
                push!(index.args, stopsym)
            elseif isvectorized
                if isone(sub)
                    push!(index.args, Expr(:call, lv(:valsub), stopsym, VECTORWIDTHSYMBOL))
                else
                    push!(index.args, Expr(:call, lv(:vsub), stopsym, Expr(:call, lv(:valmul), VECTORWIDTHSYMBOL, sub)))
                end
            else
                push!(index.args, Expr(:call, lv(:vsub), stopsym, sub))
            end
            return Expr(:call, lv(:pointerforcomparison), vptr(ar), index)
        else
            push!(index.args, Expr(:call, lv(:Zero)))
        end
    end
    @show ar, loopsym
end

function defpointermax(ls::LoopSet, ar::ArrayReferenceMeta, n::Int, sub::Int, isvectorized::Bool)::Expr
    Expr(:(=), maxsym(vptr(ar), sub), pointermax(ls, ar, n, sub, isvectorized))
end

function maxunroll(us::UnrollSpecification, n)
    @unpack u₁loopnum, u₂loopnum, u₁, u₂ = us
    if n == u₁loopnum# && u₁ > 1
        u₁
    elseif n == u₂loopnum# && u₂ > 1
        u₂
    else
        1
    end
end

function startloop(ls::LoopSet, us::UnrollSpecification, n::Int, submax = maxunroll(us, n))
    @unpack u₁loopnum, u₂loopnum, vectorizedloopnum, u₁, u₂ = us
    lssm = ls.lssm[]
    termind = lssm.terminators[n]
    ptrdefs = lssm.incrementedptrs[n]
    loopstart = Expr(:block)
    firstloop = n == num_loops(ls)
    for ar ∈ ptrdefs
        ptr = vptr(ar)
        push!(loopstart.args, Expr(:(=), ptr, ptr))
    end
    if iszero(termind)
        loopsym = names(ls)[n]
        push!(loopstart.args, startloop(getloop(ls, loopsym), loopsym))
    else
        isvectorized = n == vectorizedloopnum
        for sub ∈ 0:submax
            push!(loopstart.args, defpointermax(ls, ptrdefs[termind], n, sub, isvectorized))
        end
    end
    loopstart
end
function offset_ptr(ar::ArrayReferenceMeta, us::UnrollSpecification, loopsym::Symbol, n::Int, UF::Int, offsetinds::Vector{Bool})
    indices = getindices(ar)
    offset = first(indices) === DISCONTIGUOUS
    gespinds = Expr(:tuple)
    li = ar.loopedindex
    for i ∈ eachindex(li)
        ii = i + offset
        ind = indices[ii]
        if !offsetinds[i]
            push!(gespinds.args, Expr(:call, lv(:Zero)))
        elseif ind == loopsym
            incrementloopcounter!(gespinds, us, n, UF)
        else
            push!(gespinds.args, Expr(:call, lv(:Zero)))
        end
        ind == loopsym && break
    end
    Expr(:(=), vptr(ar), Expr(:call, lv(:gesp), vptr(ar), gespinds))
end
function incrementloopcounter(ls::LoopSet, us::UnrollSpecification, n::Int, UF::Int)
    @unpack u₁loopnum, u₂loopnum, vectorizedloopnum, u₁, u₂ = us
    lssm = ls.lssm[]
    ptrdefs = lssm.incrementedptrs[n]
    looporder = names(ls)
    loopsym = looporder[n]
    q = Expr(:block)
    termind = lssm.terminators[n]
    if iszero(termind) # increment liv
        push!(q.args, incrementloopcounter(us, n, loopsym, UF))
    end
    for (j,ar) ∈ enumerate(ptrdefs)
        offsetinds = indices_calculated_by_pointer_offsets(ls, ar)
        push!(q.args, offset_ptr(ar, us, loopsym, n, UF, offsetinds))
    end
    q
end
function terminatecondition(ls::LoopSet, us::UnrollSpecification, n::Int, inclmask::Bool, UF::Int)
    lssm = ls.lssm[]
    termind = lssm.terminators[n]
    if iszero(termind)
        loop = getloop(ls, names(ls)[n])
        return terminatecondition(loop, us, n, loop.itersymbol, inclmask, UF)
    end

    termar = lssm.incrementedptrs[n][termind]
    ptr = vptr(termar)
    # @show UF, isvectorized(us, n)
    if inclmask && isvectorized(us, n)
        Expr(:call, :<, callpointerforcomparison(ptr), maxsym(ptr, 0))
    else
        Expr(:call, :≤, callpointerforcomparison(ptr), maxsym(ptr, UF))
    end
end


"""
"""
struct CellDof{DS} <: CellDatum
  cell_dof::AbstractArray{<:Union{Dof,AbstractArray{<:Dof}}}
  trian::Triangulation
  domain_style::DS
end

get_data(f::CellDof) = f.cell_dof
get_triangulation(f::CellDof) = f.trian
DomainStyle(::Type{CellDof{DS}}) where DS = DS()

function change_domain(a::CellDof,::ReferenceDomain,::PhysicalDomain)
  @notimplemented
end

function change_domain(a::CellDof,::PhysicalDomain,::ReferenceDomain)
  @notimplemented
end

function change_domain(a::CellDof,target_trian::Triangulation,target_domain::DomainStyle)
  @notimplemented
end

function change_domain(a::CellDof,target_trian::RestrictedTriangulation,target_domain::DomainStyle)
  @notimplementedif DomainStyle(a) != target_domain
  trian_a = get_triangulation(a)
  @notimplementedif ! have_compatible_domains(trian_a,get_background_triangulation(target_trian))
  cell_dof = get_data(a)
  tcell_to_cell = get_cell_to_bgcell(target_trian)
  tcell_dof = lazy_map(Reindex(cell_dof),tcell_to_cell)
  CellDof(tcell_dof,target_trian,DomainStyle(a))
end

# Evaluation of CellDof

(a::CellDof)(f) = evaluate(a,f)

function evaluate!(cache,s::CellDof,f::CellField)

  trian_f = get_triangulation(f)
  trian_s = get_triangulation(s)

  if trian_f !== trian_s
    @unreachable """\n
    A CellDof can only be evaluated on a CellField defined on the same Triangulation.
    """
  end

  b = change_domain(f,s.domain_style)
  lazy_map(evaluate,get_data(s),get_data(b))
end

function evaluate!(cache, ::CellField, ::CellDof)
  @unreachable """\n
  CellField (f) objects cannot be evaluated at CellDof (s) objects.
  However, CellDofs objects can be evaluated at CellField objects.
  Did you mean evaluate(f,s) instead of evaluate(s,f), i.e.
  f(s) instead of s(f)?
  """
end


# each_word_iteration.jl
# ======================
#
# An iterator over arrays of binary fetching 31 bits at a time.
#
# This file is a part of BioJulia.
# License is MIT: https://github.com/BioJulia/WAHVectors.jl/blob/master/LICENSE.md

immutable Every31Bits{T}
    data::T
end

# General methods.

Base.eltype(iter::Every31Bits)= UInt32

@inline function Base.start(iter::Every31Bits)
    return 1
end

# Specific methods.

# BitVectors.

@inline function Base.next(iter::Every31Bits{BitVector}, state::Int)
    chunk = 0x00000000
    lv = length(iter.data)
    niter = ifelse(lv >= (state + 30), 31, (lv - state) + 1)
    @inbounds for i in 1:niter
        bit_i = UInt32(iter.data[state])
        chunk |= (bit_i << (31 - i))
        state += 1
    end
    return chunk, state
end

@inline function Base.done(iter::Every31Bits{BitVector}, state::Int)
    return state > length(iter.data)
end

@inline function Base.length(iter::Every31Bits{BitVector})
    return UInt64(ceil(length(iter.data) / 31))
end

# WAHVectors

@inline function Base.start(iter::Every31Bits{WAHVector})
    return (1,nwords(iter.data.data[1]))
end

@inline function Base.next(iter::Every31Bits{WAHVector}, state)
    elem = iter.data.data[state[1]]
end

@inline function Base.length(iter::Every31Bits{WAHVector})
    return nwords(iter.data)
end


@time @everywhere include("../BioClim/src/required.jl")

# Import CSV file
sp_table = CSV.read("Canis_lupus_baileyi.csv")

# select only scientificName, decimalLatitude, decimalLongitude, and year
sp_table = sp_table[:, [:species, :infraspecificEpithet, :taxonRank,
                        :decimalLatitude, :decimalLongitude, :year]]
# create new column copying all info from species col
sp_table.new_name = copy(sp_table.species)
# replace spaces by "_"
sp_table.new_name .= replace.(sp_table.new_name, " " .=> "_")
# if subspecies, specify in new_name column
for i in 1:length(sp_table)
    if sp_table.taxonRank[i] == "SUBSPECIES"
        sp_table.new_name[i] = string(sp_table.species[i], "_", sp_table.infraspecificEpithet[i])
    end
end

# Subset with specific columns
df = sp_table[:, [:new_name, :year, :decimalLatitude, :decimalLongitude]]
# Rename coordinate names
rename!(df, :decimalLatitude => :latitude)
rename!(df, :decimalLongitude => :longitude)
rename!(df, :new_name => :species)
# Replace spaces by underscores in species names
df.species .= replace.(df.species, " " .=> "_")
# Remove entries with missing year, latitude and lontitude
dropmissing!(df, [:year, :latitude, :longitude])

# Get number of observatons per species/subspecies
newdf = by(df, :species, n = :species => length)
show(sort(newdf, order(:species)), allrows=true)

# Show result for subspecies observations
show(sp_table[sp_table.taxonRank .== "SUBSPECIES", [:species, :infraspecificEpithet, :new_name]], allrows=true)

# # select only occurences with included year date
# sp_table = sp_table[sp_table.year .!= "NA", :]
# # convert year "string" to "integer"
# sp_table.year = parse.(Int64, sp_table.year)

# Prepare csv data using fnc
# df = prepare_csvdata(sp_table)

## From Tim's BioClim/main.jl
# Get the worldclim data by their layer number
cd("/Users/kiristern/Documents/GitHub/BioClim/")

@info "Extract and crop bioclim variables"
@time wc_vars = [clip(worldclim(i), df) for i in 1:19];
# Make the prediction for each layer
@info "Predictions for each layer"
@time predictions = [bioclim(wc_vars[i], df) for i in 1:length(wc_vars)];
# Make the final prediction by taking the minimum
@info "Minimum-consensus aggregation"
@time prediction = reduce(minimum, predictions);
# Get the threshold for NaN given a percentile
@info "Threshold estimation"
@time threshold = first(quantile(prediction[df], [0.05]))
@info "5% threshold:\t$(round(threshold; digits=3))"
# Filter the predictions based on the threshold
@info "Final prediction filtering"
@time for i in eachindex(prediction.grid)
    prediction.grid[i] < threshold && (prediction.grid[i] = NaN)
end

worldmap = clip(worldshape(50), prediction)

sdm_plot = plot([0.0], lab = " ", msw = 0.0, ms = 0.0, size = (900, 450), frame=:box,
                title = first(unique(df.species)))
xaxis!(sdm_plot, (prediction.left, prediction.right), "Longitude")
yaxis!(sdm_plot, (prediction.bottom, prediction.top), "Latitude")

for p in worldmap
    sh = Shape([pp.x for pp in p.points], [pp.y for pp in p.points])
    plot!(sdm_plot, sh, c = :lightgrey, lab = " ")
end

heatmap!(
    sdm_plot,
    longitudes(prediction), latitudes(prediction), prediction.grid,
    aspectratio = 92.60/60.75, c = :BuPu,
    clim = (0.0, maximum(filter(!isnan, prediction.grid)))
    )

for p in worldmap
    xy = map(x -> (x.x, x.y), p.points)
    plot!(sdm_plot, xy, c=:grey, lab = " ", leg=false)
end

sdm_plot

savefig("../SDM/figures/sdm.png")
savefig("../SDM/figures/sdm_map-$(first(unique(sp_table.species))).pdf")