@cache struct SSPRK22Cache{uType,rateType,StageLimiter,StepLimiter} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
end

struct SSPRK22ConstantCache <: OrdinaryDiffEqConstantCache end

function alg_cache(alg::SSPRK22,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  SSPRK22Cache(u,uprev,k,fsalfirst,alg.stage_limiter!,alg.step_limiter!)
end

alg_cache(alg::SSPRK22,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits} = SSPRK22ConstantCache()

@cache struct SSPRK33Cache{uType,rateType,StageLimiter,StepLimiter} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
end

struct SSPRK33ConstantCache <: OrdinaryDiffEqConstantCache end

function alg_cache(alg::SSPRK33,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  SSPRK33Cache(u,uprev,k,fsalfirst,alg.stage_limiter!,alg.step_limiter!)
end

alg_cache(alg::SSPRK33,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits} = SSPRK33ConstantCache()

@cache struct KYKSSPRK42Cache{uType,rateType,TabType} <: OrdinaryDiffEqMutableCache
   u::uType
   uprev::uType
   k::rateType
   tmp::uType
   fsalfirst::rateType
   tab::TabType
 end

struct KYKSSPRK42ConstantCache{T, T2} <: OrdinaryDiffEqConstantCache
  α20::T
  α21::T
  α30::T
  α32::T
  α40::T
  α43::T
  β10::T
  β21::T
  β30::T
  β32::T
  β40::T
  β43::T
  c1::T2
  c2::T2
  c3::T2
end

function KYKSSPRK42ConstantCache(T, T2)
  α20 = T(0.394806441339829)
  α21 = T(0.605193558660171)
  α30 = T(0.002797307087390)
  α32 = T(0.997202692912610)
  α40 = T(0.252860909354373)
  α43 = T(0.747139090645627)
  β10 = T(0.406584463657504)
  β21 = T(0.246062298456822)
  β30 = T(0.013637216641451)
  β32 = T(0.405447122055692)
  β40 = T(0.016453567333598)
  β43 = T(0.303775146447707)
  c1 = T2(0.406584463657504)
  c2 = T2(0.4921245969136438)
  c3 = T2(0.9098323119879613)
  KYKSSPRK42ConstantCache(α20, α21, α30, α32, α40, α43, β10, β21, β30, β32, β40, β43, c1, c2, c3)
end

function alg_cache(alg::KYKSSPRK42,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  tmp = zero(u)
  k = zero(rate_prototype)
  fsalfirst = zero(rate_prototype)
  tab = KYKSSPRK42ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
  KYKSSPRK42Cache(u,uprev,k,tmp, fsalfirst,tab)
end

function alg_cache(alg::KYKSSPRK42,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  KYKSSPRK42ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
end

@cache struct SSPRK53Cache{uType,rateType,StageLimiter,StepLimiter,TabType} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  tmp::uType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
  tab::TabType
end

struct SSPRK53ConstantCache{T,T2} <: OrdinaryDiffEqConstantCache
  α30::T
  α32::T
  α40::T
  α43::T
  α52::T
  α54::T
  β10::T
  β21::T
  β32::T
  β43::T
  β54::T
  c1::T2
  c2::T2
  c3::T2
  c4::T2

  function SSPRK53ConstantCache(T, T2)
    α30 = T(0.355909775063327)
    α32 = T(0.644090224936674)
    α40 = T(0.367933791638137)
    α43 = T(0.632066208361863)
    α52 = T(0.237593836598569)
    α54 = T(0.762406163401431)
    β10 = T(0.377268915331368)
    β21 = T(0.377268915331368)
    β32 = T(0.242995220537396)
    β43 = T(0.238458932846290)
    β54 = T(0.287632146308408)
    c1 = T2(0.377268915331368)
    c2 = T2(0.754537830662736)
    c3 = T2(0.728985661612188)
    c4 = T2(0.699226135931670)

    new{T,T2}(α30, α32, α40, α43, α52, α54, β10, β21, β32, β43, β54, c1, c2, c3, c4)
  end
end

function alg_cache(alg::SSPRK53,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  tmp = zero(u)
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  tab = SSPRK53ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
  SSPRK53Cache(u,uprev,k,fsalfirst,tmp,alg.stage_limiter!,alg.step_limiter!,tab)
end

function alg_cache(alg::SSPRK53,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  SSPRK53ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
end

@cache struct SHLDDRK52Cache{uType,rateType,TabType} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  tmp::uType
  fsalfirst::rateType
  tab::TabType
end

struct SHLDDRK52ConstantCache{T1,T2} <: OrdinaryDiffEqConstantCache
  α2::T1
  α3::T1
  α4::T1
  α5::T1
  β1::T1
  β2::T1
  β3::T1
  β4::T1
  β5::T1
  c2::T2
  c3::T2
  c4::T2
  c5::T2
end

function SHLDDRK52ConstantCache(T1,T2)
  α2 = T1(-0.6913065)
  α3 = T1(-2.655155)
  α4 = T1(-0.8147688)
  α5 = T1(-0.6686587)
  β1 = T1(0.1)
  β2 = T1(0.75)
  β3 = T1(0.7)
  β4 = T1(0.479313)
  β5 = T1(0.310392)
  c2 = T2(0.1)
  c3 = T2(0.3315201)
  c4 = T2(0.4577796)
  c5 = T2(0.8666528)
  SHLDDRK52ConstantCache(α2, α3, α4, α5, β1, β2, β3, β4, β5, c2, c3, c4, c5)
end

function alg_cache(alg::SHLDDRK52,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  SHLDDRK52ConstantCache(constvalue(uBottomEltypeNoUnits),constvalue(tTypeNoUnits))
end

function alg_cache(alg::SHLDDRK52,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  tmp = zero(u)
  k = zero(rate_prototype)
  fsalfirst = zero(rate_prototype)
  tab = SHLDDRK52ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
  SHLDDRK52Cache(u,uprev,k,tmp,fsalfirst,tab)
end

@cache mutable struct SHLDDRK_2NCache{uType,rateType,TabType} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  tmp::uType
  fsalfirst::rateType
  tab::TabType
  step::Int
end

mutable struct SHLDDRK_2NConstantCache{T1,T2} <: OrdinaryDiffEqConstantCache
  α21::T1
  α31::T1
  α41::T1
  α51::T1
  β11::T1
  β21::T1
  β31::T1
  β41::T1
  β51::T1
  c21::T2
  c31::T2
  c41::T2
  c51::T2

  α22::T1
  α32::T1
  α42::T1
  α52::T1
  α62::T1
  β12::T1
  β22::T1
  β32::T1
  β42::T1
  β52::T1
  β62::T1
  c22::T2
  c32::T2
  c42::T2
  c52::T2
  c62::T2

  step::Int
end

function SHLDDRK_2NConstantCache(T1,T2)
  α21 = T1(-0.6051226)
  α31 = T1(-2.0437564)
  α41 = T1(-0.7406999)
  α51 = T1(-4.4231765)
  β11 = T1(0.2687454)
  β21 = T1(0.8014706)
  β31 = T1(0.5051570)
  β41 = T1(0.5623568)
  β51 = T1(0.0590065)
  c21 = T2(0.2687454)
  c31 = T2(0.5852280)
  c41 = T2(0.6827066)
  c51 = T2(1.1646854)

  α22 = T1(-0.4412737)
  α32 = T1(-1.0739820)
  α42 = T1(-1.7063570)
  α52 = T1(-2.7979293)
  α62 = T1(-4.0913537)
  β12 = T1(0.1158488)
  β22 = T1(0.3728769)
  β32 = T1(0.7379536)
  β42 = T1(0.5798110)
  β52 = T1(1.0312849)
  β62 = T1(0.15)
  c22 = T2(0.1158485)
  c32 = T2(0.3241850)
  c42 = T2(0.6193208)
  c52 = T2(0.8034472)
  c62 = T2(0.9184166)
  SHLDDRK_2NConstantCache(α21, α31, α41, α51, β11, β21, β31, β41, β51, c21, c31, c41, c51, α22, α32, α42, α52, α62, β12, β22, β32, β42, β52, β62, c22, c32, c42, c52, c62, 1)
end

function alg_cache(alg::SHLDDRK_2N,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  SHLDDRK_2NConstantCache(constvalue(uBottomEltypeNoUnits),constvalue(tTypeNoUnits))
end

function alg_cache(alg::SHLDDRK_2N,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  tmp = zero(u)
  k = zero(rate_prototype)
  fsalfirst = zero(rate_prototype)
  tab = SHLDDRK_2NConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
  SHLDDRK_2NCache(u,uprev,k,tmp,fsalfirst,tab,1)
end

@cache struct SSPRK53_2N1Cache{uType,rateType,StageLimiter,StepLimiter,TabType} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
  tab::TabType
end

struct SSPRK53_2N1ConstantCache{T,T2} <: OrdinaryDiffEqConstantCache
  α40::T
  α43::T
  β10::T
  β21::T
  β32::T
  β43::T
  β54::T
  c1::T2
  c2::T2
  c3::T2
  c4::T2

  function SSPRK53_2N1ConstantCache(T, T2)
    α40 = T(0.571403511494104)
    α43 = T(0.428596488505896)
    β10 = T(0.443568244942995)
    β21 = T(0.291111420073766)
    β32 = T(0.270612601278217)
    β43 = T(0.110577759392786)
    β54 = T(0.458557505351052)
    c1 = T2(0.443568244942995)
    c2 = T2(0.734679665016762)
    c3 = T2(1.005292266294979)
    c4 = T2(0.541442494648948)

    new{T,T2}(α40, α43,β10, β21, β32, β43, β54, c1, c2, c3, c4)
  end
end

function alg_cache(alg::SSPRK53_2N1,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  tab = SSPRK53_2N1ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
  SSPRK53_2N1Cache(u,uprev,k,fsalfirst,alg.stage_limiter!,alg.step_limiter!,tab)
end

function alg_cache(alg::SSPRK53_2N1,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  SSPRK53_2N1ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
end

@cache struct SSPRK53_2N2Cache{uType,rateType,StageLimiter,StepLimiter,TabType} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
  tab::TabType
end

struct SSPRK53_2N2ConstantCache{T,T2} <: OrdinaryDiffEqConstantCache
  α30::T
  α32::T
  α50::T
  α54::T
  β10::T
  β21::T
  β32::T
  β43::T
  β54::T
  c1::T2
  c2::T2
  c3::T2
  c4::T2

  function SSPRK53_2N2ConstantCache(T, T2)
    α30 = T(0.682342861037239)
    α32 = T(0.317657138962761)
    α50 = T(0.045230974482400)
    α54 = T(0.954769025517600)
    β10 = T(0.465388589249323)
    β21 = T(0.465388589249323)
    β32 = T(0.124745797313998)
    β43 = T(0.465388589249323)
    β54 = T(0.154263303748666)
    c1 = T2(0.465388589249323)
    c2 = T2(0.930777178498646)
    c3 = T2(0.420413812847710)
    c4 = T2(0.885802402097033)

    new{T,T2}(α30, α32,α50,α54,β10, β21, β32, β43, β54, c1, c2, c3, c4)
  end
end

function alg_cache(alg::SSPRK53_2N2,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  tab = SSPRK53_2N2ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
  SSPRK53_2N2Cache(u,uprev,k,fsalfirst,alg.stage_limiter!,alg.step_limiter!,tab)
end

function alg_cache(alg::SSPRK53_2N2,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  SSPRK53_2N2ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
end

@cache struct SSPRK53_HCache{uType,rateType,StageLimiter,StepLimiter,TabType} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  tmp::uType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
  tab::TabType
end

struct SSPRK53_HConstantCache{T,T2} <: OrdinaryDiffEqConstantCache
  α30::T
  α32::T
  α40::T
  α41::T
  α43::T
  β10::T
  β21::T
  β32::T
  β43::T
  β54::T
  c1::T2
  c2::T2
  c3::T2
  c4::T2

  function SSPRK53_HConstantCache(T,T2)
    α30 = T(0.308684154602513)
    α32 = T(0.691315845397487)
    α40 = T(0.280514990468574)
    α41 = T(0.270513101776498)
    α43 = T(0.448971907754928)
    β10 = T(0.377268915331368)
    β21 = T(0.377268915331368)
    β32 = T(0.260811979144498)
    β43 = T(0.169383144652957)
    β54 = T(0.377268915331368)
    c1 = T2(0.377268915331368)
    c2 = T2(0.754537830662737)
    c3 = T2(0.782435937433493)
    c4 = T2(0.622731084668631)

    new{T,T2}(α30, α32, α40, α41, α43, β10, β21, β32, β43, β54, c1, c2, c3, c4)
  end
end

function alg_cache(alg::SSPRK53_H,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  tmp = zero(u)
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  tab = SSPRK53_HConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
  SSPRK53_HCache(u,uprev,k,fsalfirst,tmp,alg.stage_limiter!,alg.step_limiter!,tab)
end

function alg_cache(alg::SSPRK53_H,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  SSPRK53_HConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
end

@cache struct SSPRK63Cache{uType,rateType,StageLimiter,StepLimiter,TabType} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  tmp::uType
  u₂::uType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
  tab::TabType
end

struct SSPRK63ConstantCache{T,T2} <: OrdinaryDiffEqConstantCache
  α40::T
  α41::T
  α43::T
  α62::T
  α65::T
  β10::T
  β21::T
  β32::T
  β43::T
  β54::T
  β65::T
  c1::T2
  c2::T2
  c3::T2
  c4::T2
  c5::T2

  function SSPRK63ConstantCache(T, T2)
    α40 = T(0.476769811285196)
    α41 = T(0.098511733286064)
    α43 = T(0.424718455428740)
    α62 = T(0.155221702560091)
    α65 = T(0.844778297439909)
    β10 = T(0.284220721334261)
    β21 = T(0.284220721334261)
    β32 = T(0.284220721334261)
    β43 = T(0.120713785765930)
    β54 = T(0.284220721334261)
    β65 = T(0.240103497065900)
    c1 = T2(0.284220721334261)
    c2 = T2(0.568441442668522)
    c3 = T2(0.852662164002783)
    c4 = T2(0.510854218958172)
    c5 = T2(0.795074940292433)

    new{T,T2}(α40, α41, α43, α62, α65, β10, β21, β32, β43, β54, β65, c1, c2, c3, c4, c5)
  end
end

function alg_cache(alg::SSPRK63,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  tmp = zero(u)
  u₂ = zero(u)
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  tab = SSPRK63ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
  SSPRK63Cache(u,uprev,k,fsalfirst,tmp,u₂,alg.stage_limiter!,alg.step_limiter!,tab)
end

function alg_cache(alg::SSPRK63,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  SSPRK63ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
end

@cache struct SSPRK73Cache{uType,rateType,StageLimiter,StepLimiter,TabType} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  tmp::uType
  u₁::uType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
  tab::TabType
end

struct SSPRK73ConstantCache{T,T2} <: OrdinaryDiffEqConstantCache
  α40::T
  α43::T
  α50::T
  α51::T
  α54::T
  α73::T
  α76::T
  β10::T
  β21::T
  β32::T
  β43::T
  β54::T
  β65::T
  β76::T
  c1::T2
  c2::T2
  c3::T2
  c4::T2
  c5::T2
  c6::T2

  function SSPRK73ConstantCache(T, T2)
    α40 = T(0.184962588071072)
    α43 = T(0.815037411928928)
    α50 = T(0.180718656570380)
    α51 = T(0.314831034403793)
    α54 = T(0.504450309025826)
    α73 = T(0.120199000000000)
    α76 = T(0.879801000000000)
    β10 = T(0.233213863663009)
    β21 = T(0.233213863663009)
    β32 = T(0.233213863663009)
    β43 = T(0.190078023865845)
    β54 = T(0.117644805593912)
    β65 = T(0.233213863663009)
    β76 = T(0.205181790464579)
    c1 = T2(0.233213863663009)
    c2 = T2(0.466427727326018)
    c3 = T2(0.699641590989027)
    c4 = T2(0.760312095463379)
    c5 = T2(0.574607439040817)
    c6 = T2(0.807821302703826)

    new{T,T2}(α40, α43, α50, α51, α54, α73, α76, β10, β21, β32, β43, β54, β65, β76, c1, c2, c3, c4, c5, c6)
  end
end

function alg_cache(alg::SSPRK73,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  tmp = zero(u)
  u₁ = zero(u)
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  tab = SSPRK73ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
  SSPRK73Cache(u,uprev,k,fsalfirst,tmp,u₁,alg.stage_limiter!,alg.step_limiter!,tab)
end

function alg_cache(alg::SSPRK73,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  SSPRK73ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
end

@cache struct SSPRK83Cache{uType,rateType,StageLimiter,StepLimiter,TabType} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  tmp::uType
  u₂::uType
  u₃::uType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
  tab::TabType
end

struct SSPRK83ConstantCache{T,T2} <: OrdinaryDiffEqConstantCache
  α50::T
  α51::T
  α54::T
  α61::T
  α65::T
  α72::T
  α73::T
  α76::T
  β10::T
  β21::T
  β32::T
  β43::T
  β54::T
  β65::T
  β76::T
  β87::T
  c1::T2
  c2::T2
  c3::T2
  c4::T2
  c5::T2
  c6::T2
  c7::T2

  function SSPRK83ConstantCache(T, T2)
    α50 = T(0.421366967085359)
    α51 = T(0.005949401107575)
    α54 = T(0.572683631807067)
    α61 = T(0.004254010666365)
    α65 = T(0.995745989333635)
    α72 = T(0.104380143093325)
    α73 = T(0.243265240906726)
    α76 = T(0.652354615999950)
    β10 = T(0.195804015330143)
    β21 = T(0.195804015330143)
    β32 = T(0.195804015330143)
    β43 = T(0.195804015330143)
    β54 = T(0.112133754621673)
    β65 = T(0.194971062960412)
    β76 = T(0.127733653231944)
    β87 = T(0.195804015330143)
    c1 = T2(0.195804015330143)
    c2 = T2(0.391608030660286)
    c3 = T2(0.587412045990429)
    c4 = T2(0.783216061320572)
    c5 = T2(0.561833689734037)
    c6 = T2(0.755247658555329)
    c7 = T2(0.804195984669857)

    new{T,T2}(α50, α51, α54, α61, α65, α72, α73, α76, β10, β21, β32, β43, β54, β65, β76, β87, c1, c2, c3, c4, c5, c6, c7)
  end
end

function alg_cache(alg::SSPRK83,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  tmp = zero(u)
  u₂ = zero(u)
  u₃ = zero(u)
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  tab = SSPRK83ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
  SSPRK83Cache(u,uprev,k,fsalfirst,tmp,u₂,u₃,alg.stage_limiter!,alg.step_limiter!,tab)
end

function alg_cache(alg::SSPRK83,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  SSPRK83ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
end

@cache struct SSPRK43Cache{uType,rateType,uNoUnitsType,StageLimiter,StepLimiter,TabType} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  utilde::uType
  atmp::uNoUnitsType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
  tab::TabType
end

struct SSPRK43ConstantCache{T,T2} <: OrdinaryDiffEqConstantCache
  one_third_u::T
  two_thirds_u::T
  half_u::T
  half_t::T2

  function SSPRK43ConstantCache(T, T2)
    one_third_u = inv(T(3))
    two_thirds_u = 2 * one_third_u
    half_u = T(0.5)
    half_t = T2(0.5)

    new{T,T2}(one_third_u, two_thirds_u, half_u, half_t)
  end
end

function alg_cache(alg::SSPRK43,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  utilde = zero(u)
  atmp = similar(u,uEltypeNoUnits)
  tab = SSPRK43ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
  SSPRK43Cache(u,uprev,k,fsalfirst,utilde,atmp,alg.stage_limiter!,alg.step_limiter!,tab)
end

function alg_cache(alg::SSPRK43,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  SSPRK43ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
end

@cache struct SSPRK432Cache{uType,rateType,uNoUnitsType,StageLimiter,StepLimiter} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  utilde::uType
  atmp::uNoUnitsType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
end

struct SSPRK432ConstantCache <: OrdinaryDiffEqConstantCache end

function alg_cache(alg::SSPRK432,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  utilde = zero(u)
  atmp = similar(u,uEltypeNoUnits)
  SSPRK432Cache(u,uprev,k,fsalfirst,utilde,atmp,alg.stage_limiter!,alg.step_limiter!)
end

alg_cache(alg::SSPRK432,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits} = SSPRK432ConstantCache()

@cache mutable struct SSPRKMSVS32Cache{uType,rateType,dtArrayType,dtType,StageLimiter,StepLimiter} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  fsalfirst::rateType
  u_2::uType
  u_1::uType
  k::rateType
  tmp::uType
  dts::dtArrayType
  dtf::dtArrayType
  μ::dtType
  v_n::Float64
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
  step::Int
end

@cache mutable struct SSPRKMSVS32ConstantCache{uType,dtArrayType,dtType} <: OrdinaryDiffEqConstantCache
  u_2::uType
  u_1::uType
  dts::dtArrayType
  dtf::dtArrayType
  μ::dtType
  v_n::Float64
  step::Int
end

function alg_cache(alg::SSPRKMSVS32,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  fsalfirst = zero(rate_prototype)
  dts = fill(zero(dt),3)
  dtf = fill(zero(dt),2)
  μ = zero(dt)
  u_2 = zero(u)
  u_1 = zero(u)
  k  = zero(rate_prototype)
  tmp = zero(u)
  SSPRKMSVS32Cache(u,uprev,fsalfirst,u_2,u_1,k,tmp,dts,dtf,μ,0.5,alg.stage_limiter!,alg.step_limiter!,1)
end

function alg_cache(alg::SSPRKMSVS32,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  dts = fill(zero(dt),3)
  dtf = fill(zero(dt),2)
  μ = zero(dt)
  u_2 = u
  u_1 = u
  SSPRKMSVS32ConstantCache(u_2,u_1,dts,dtf,μ,0.5,1)
end

@cache mutable struct SSPRKMSVS43Cache{uType,rateType,StageLimiter,StepLimiter} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  fsalfirst::rateType
  u_3::uType
  u_2::uType
  u_1::uType
  k::rateType
  k1::rateType
  k2::rateType
  k3::rateType
  tmp::uType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
  step::Int
end

@cache mutable struct SSPRKMSVS43ConstantCache{uType,rateType} <: OrdinaryDiffEqConstantCache
  u_3::uType
  u_2::uType
  u_1::uType
  k1::rateType
  k2::rateType
  k3::rateType
  step::Int
end

function alg_cache(alg::SSPRKMSVS43,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  fsalfirst = zero(rate_prototype)
  u_3 = zero(u)
  u_2 = zero(u)
  u_1 = zero(u)
  k  = zero(rate_prototype)
  k1  = zero(rate_prototype)
  k2  = zero(rate_prototype)
  k3  = zero(rate_prototype)
  tmp = zero(u)
  SSPRKMSVS43Cache(u,uprev,fsalfirst,u_3,u_2,u_1,k,k1,k2,k3,tmp,alg.stage_limiter!,alg.step_limiter!,1)
end

function alg_cache(alg::SSPRKMSVS43,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  u_3 = u
  u_2 = u
  u_1 = u
  k1  = rate_prototype
  k2  = rate_prototype
  k3  = rate_prototype
  SSPRKMSVS43ConstantCache(u_3,u_2,u_1,k1,k2,k3,1)
end

@cache struct SSPRK932Cache{uType,rateType,uNoUnitsType,StageLimiter,StepLimiter} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  utilde::uType
  atmp::uNoUnitsType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
end

struct SSPRK932ConstantCache <: OrdinaryDiffEqConstantCache end

function alg_cache(alg::SSPRK932,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  utilde = zero(u)
  atmp = similar(u,uEltypeNoUnits)
  SSPRK932Cache(u,uprev,k,fsalfirst,utilde,atmp,alg.stage_limiter!,alg.step_limiter!)
end

alg_cache(alg::SSPRK932,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits} = SSPRK932ConstantCache()

@cache struct SSPRK54Cache{uType,rateType,StageLimiter,StepLimiter,TabType} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  k₃::rateType
  u₂::uType
  u₃::uType
  tmp::uType # should be u₄, but tmp is needed for callbacks
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
  tab::TabType
end

struct SSPRK54ConstantCache{T,T2} <: OrdinaryDiffEqConstantCache
  β10::T
  α20::T
  α21::T
  β21::T
  α30::T
  α32::T
  β32::T
  α40::T
  α43::T
  β43::T
  α52::T
  α53::T
  β53::T
  α54::T
  β54::T
  c1::T2
  c2::T2
  c3::T2
  c4::T2

  function SSPRK54ConstantCache(T, T2)
    β10 = T(0.391752226571890)
    α20 = T(0.444370493651235)
    α21 = T(0.555629506348765)
    β21 = T(0.368410593050371)
    α30 = T(0.620101851488403)
    α32 = T(0.379898148511597)
    β32 = T(0.251891774271694)
    α40 = T(0.178079954393132)
    α43 = T(0.821920045606868)
    β43 = T(0.544974750228521)
    α52 = T(0.517231671970585)
    α53 = T(0.096059710526147)
    β53 = T(0.063692468666290)
    α54 = T(0.386708617503269)
    β54 = T(0.226007483236906)
    c1 = T2(0.391752226571890)
    c2 = T2(0.586079689311540)
    c3 = T2(0.474542363121400)
    c4 = T2(0.935010630967653)

    new{T,T2}(β10, α20, α21, β21, α30, α32, β32, α40, α43, β43, α52, α53, β53, α54, β54, c1, c2, c3, c4)
  end
end

function alg_cache(alg::SSPRK54,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  u₂ = zero(u)
  u₃ = zero(u)
  tmp = zero(u)
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  k₃ = zero(rate_prototype)
  tab = SSPRK54ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
  SSPRK54Cache(u,uprev,k,fsalfirst,k₃,u₂,u₃,tmp,alg.stage_limiter!,alg.step_limiter!,tab)
end

function alg_cache(alg::SSPRK54,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  SSPRK54ConstantCache(constvalue(uBottomEltypeNoUnits), constvalue(tTypeNoUnits))
end

@cache struct SSPRK104Cache{uType,rateType,StageLimiter,StepLimiter} <: OrdinaryDiffEqMutableCache
  u::uType
  uprev::uType
  k::rateType
  fsalfirst::rateType
  k₄::rateType
  tmp::uType
  stage_limiter!::StageLimiter
  step_limiter!::StepLimiter
end

struct SSPRK104ConstantCache <: OrdinaryDiffEqConstantCache end

function alg_cache(alg::SSPRK104,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{true}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits}
  tmp = zero(u)
  k = zero(rate_prototype)
  if calck
    fsalfirst = similar(k)
  else
    fsalfirst = k
  end
  k₄ = zero(rate_prototype)
  SSPRK104Cache(u,uprev,k,fsalfirst,k₄,tmp,alg.stage_limiter!,alg.step_limiter!)
end

alg_cache(alg::SSPRK104,u,rate_prototype,::Type{uEltypeNoUnits},::Type{uBottomEltypeNoUnits},::Type{tTypeNoUnits},uprev,uprev2,f,t,dt,reltol,p,calck,::Val{false}) where {uEltypeNoUnits,uBottomEltypeNoUnits,tTypeNoUnits} = SSPRK104ConstantCache()


"""
`Model`

A struct representing a model.

**Fields**
- `variable_names`: Variable names (e.g. `["X", "Y", "Z"]`).
- `parameter_names`: Parameter names (e.g. `["v", "d1", "d2"]`).
- `problem`: `ODEProblem` or `SDEProblem` or `JumpProblem`.
- `solver_algorithm`: Solver used to solve the `problem`` (e.g. `SOSRI()`).
- `solver_parameters`: Parameters passed to the solver (e,g, `(saveat=0.1,)`)
- `input`: Input to the model representedas a tuple with two elements. The first
    element is a matrix representing the input events and the second element is
    a string that specifies, which parameter is manipulated by the events.
- `output`: Function that transforms `DifferentialEqualtions` solution
    into a matrix. Rows are time steps and columns are variables.

**Functions**
- `set_initial_conditions`
- `set_timespan`
- `create_model`
- `set_solver`
- `get_parameter_index`
- `get_parameter_value`
- `set_parameter_value`
- `set_input`
- `simulate_population`
"""
struct Model
    variable_names::Vector{String}
    parameter_names::Vector{String}
    problem::Union{ODEProblem, SDEProblem, JumpProblem}
    solver_algorithm::Any
    solver_parameters::NamedTuple
    input::Tuple{Matrix{Float64}, String}
    output::Function
end

"""
`_set_model_property(model::Model; kwargs...)`

Create a new copy of `model` with replaced fields as specified by the keyword
    arguments.
"""
function _set_model_property(model::Model; kwargs...)

    # Iterate all possible fields of Model struct
    fields = []
    for field in fieldnames(Model)

        # If the field is in kwargs, use it, otherwise use the original value
        if field in keys(kwargs)
            push!(fields, deepcopy(kwargs[field]))
        else
            push!(fields, deepcopy(getproperty(model, field)))
        end

    end

    # Create a model with new fields
    return Model(fields...)

end

function _get_problem_property(problem, property_name)

    # Property name must be a symbol
    property_name = Symbol(property_name)

    # For JumpProblem access problem.prob, otherwise problem directly
    if problem isa JumpProblem
        property = getproperty(problem.prob, property_name)
    else
        property = getproperty(problem, property_name)
    end

    # Return the found property
    return property

end

function _get_problem_property(model::Model, property_name)

    return _get_problem_property(model.problem, property_name)

end

function _set_problem_property(model::Model; kwargs...)

    # Remake problem with the new properties
    new_problem = remake(model.problem; kwargs...)

    # Replace the problem in the model
    new_model = _set_model_property(model, problem=new_problem)

    return new_model
end

"""
`set_initial_conditions(model::Model, u0)`

Set initial conditions for a model.
"""
function set_initial_conditions(model::Model, u0)
    return _set_problem_property(model; u0=u0)
end

"""
`set_timespan(model::Model, tspan)`

Set timespan for a model.
"""
function set_timespan(model::Model, tspan)
    return _set_problem_property(model; tspan=tspan)
end

function set_output(model::Model, output)
    return _set_model_property(model; output=output)
end

"""
`create_model`

Create a model based on the `DifferentialEqualtions` problem.

**Arguments**
- `variable_names`: Variable names (e.g. `["X", "Y", "Z"]`).
- `parameter_names`: Parameter names (e.g. `["v", "d1", "d2"]`).
- `problem`: `ODEProblem` or `SDEProblem` or `JumpProblem`.

**Returns**
- `model`: Model represented as a `Model` struct.
"""
function create_model(variable_names, parameter_names, problem)

    # Check that variable names match the length of the initial state
    u0 = _get_problem_property(problem, "u0")
    if length(variable_names) != length(u0)
        error("Incorrect number of variable names!")
    end

    # Check that parameter names match the length of the parameter vector
    p = _get_problem_property(problem, "p")
    if length(parameter_names) != length(p)
        error("Incorrect number of parameter names!")
    end

    # Set up a default algorithm based on the problem type
    if problem isa ODEProblem
        solver_algorithm = Tsit5()
    elseif problem isa SDEProblem
        solver_algorithm = SOSRI()
    elseif problem isa JumpProblem
        solver_algorithm = SSAStepper()
    else
        error("Unknown problem type!")
    end

    # Set up the other default parameters the do not depend on the problem type
    solver_parameters = (saveat=0.1,)
    input = (Matrix{Float64}(undef, 0, 0), "")
    output = sol -> Matrix(sol[:, :]')

    # Create the Model struct
    model = Model(
        variable_names,
        parameter_names,
        problem,
        solver_algorithm,
        solver_parameters,
        input,
        output
    )

    return model

end

"""
`set_solver(model::Model, algorithm=nothing; merge_kwargs=true, kwargs...)`

Set solver algorithm and its properties.

**Arguments**
- `model`: Model.

**Optional Arguments**
- `algorithm`: Solver algorithm (e.g. `Tsit5()` or `SOSRI()`).

**Keyword Arguments**
- `merge_kwargs`: If `false`, the original arguments are deleted.
- `kwargs...`: Solver parameters.

**Returns**
- `new_model`: Model with updated solver algorithm and parameters.
"""
function set_solver(model::Model, algorithm=nothing; merge_kwargs=true, kwargs...)

    if :callback in keys(kwargs)
        error("Callback is reserved for the input function!")
    end

    if isnothing(algorithm)
        # If the algorithm was not specified, use to original one
        solver_algorithm = model.solver_algorithm
    else
        # If the algorithm was specified, use the new one
        solver_algorithm = algorithm
    end

    if length(kwargs) > 0
        # If any kwargs were passed, add them to the solver_parameters

        if merge_kwargs
            # Merge kwargs with the existing parameters
            solver_parameters = merge(model.solver_parameters, NamedTuple(kwargs))
        else
            # Replace the old parameters with the new ones
            if :callback in keys(model.solver_parameters)
                # Preserve callback
                callback = model.solver_parameters[:callback]
                solver_parameters = merge(NamedTuple(kwargs), (callback=callback,))
            else
                solver_parameters = NamedTuple(kwargs)
            end
        end

    else
        # If no kwargs were passed, just copy the original ones
        solver_parameters = model.solver_parameters

    end

    # Build a new model
    new_model = _set_model_property(model; solver_algorithm=solver_algorithm,
        solver_parameters=solver_parameters)

    return new_model

end

"""
Set callback to the solver parameters.
"""
function _set_callback(model::Model, callback)

    # Merge callback with existing solver parameters
    solver_parameters = merge(model.solver_parameters, (callback=callback,))

    # Build a new model
    new_model = _set_model_property(model; solver_parameters=solver_parameters)

    return new_model

end

"""
`get_parameter_index(model::Model, parameter_name::String)`

Find index of the specified parameter.

**Arguments**
- `model`: Model.
- `parameter_name`: Name of the parameter.

**Returns**
- `index`: Index indicating the position of the parameter.
"""
function get_parameter_index(model::Model, parameter_name::String)

    parameter_names = model.parameter_names
    index = findfirst(parameter_names .== parameter_name)
    if isnothing(index)
        error("Parameter '$(parameter_name)' is not in the model!")
    end
    return index

end

"""
`get_parameter_value(model::Model, parameter_name::String)`

Get a parameter value.

**Arguments**
- `model`: Model.
- `parameter_name`: Name of the parameter.

**Returns**
- `parameter_value`: Value of the parameter.
"""
function get_parameter_value(model::Model, parameter_name::String)

    index = get_parameter_index(model, parameter_name)
    parameter_values = _get_problem_property(model, "p")
    return parameter_values[index]

end

"""
`set_parameter_value(model::Model, name::String, value::Number)`

Set a parameter value.

**Arguments**
- `model`: Model.
- `name`: Name of the parameter. Can also be in the form of "d1=d2=d3", which
    sets all parameters d1, d2, and d3 to the same value.
- `value`: New value for the parameter.

**Returns**
- `new_model`: Copy of `model` with updated parameter value.
"""
function set_parameter_value(model::Model, name::String, value::Number)

    # Get array of parameter values
    parameter_values = _get_problem_property(model, "p")

    if occursin("=", name)
        # If "=" occurs in `name`, split the string into individual parameters
        name_array = String.(split(name, "="))
        for name_split in name_array
            # Set each parameter to `value`
            index = get_parameter_index(model, name_split)
            parameter_values[index] = value
        end
    else
        # Set the desired parameter to `value`
        index = get_parameter_index(model, name)
        parameter_values[index] = value
    end

    # Rebuild the model with the new parameter value
    new_model = _set_problem_property(model, p=parameter_values)
    return new_model

end

"""
`model::Model, name_array::Array, value_array::Array`

Set parameter values.

**Arguments**
- `model`: Model.
- `name_array`: Parameter names. Can also be in the form of "d1=d2=d3", which
    sets all parameters d1, d2, and d3 to the same value.
- `value_array`: New values for the parameters.

**Returns**
- `new_model`: Copy of `model` with updated parameter values.
"""
function set_parameter_value(model::Model, name_array::Array, value_array::Array)

    # The input arrays must have the same length
    if length(name_array) != length(value_array)
        error("The array of parameter names and values must have the same length!")
    end

    # Set the parameters
    new_model = deepcopy(model)
    for (name, value) in zip(name_array, value_array)
        new_model = set_parameter_value(new_model, name, value)
    end

    return new_model

end

"""
`_create_callback(events::Matrix, i)`

Generate `PresetTimeCallback` from `events` modyfing `i`-th parameter.
"""
function _create_callback(events::Matrix, i)

    p = NaN  # holds the original value of the parameter

    # Initialization at the beginning of the integration
    initialize = function (c, u, t, integrator)

        # Protect the original parameters from overwriting
        integrator.p = copy(integrator.p)

        # Save the default parameter
        p = integrator.p[i]

        # Set the parameter to 0
        integrator.p[i] = 0.0

    end

    # Set the parameter to its default value at the beginning of the event
    tstops_on = events[:, 1]
    affect_on! = (integrator) -> integrator.p[i] = p
    callback_on = PresetTimeCallback(tstops_on, affect_on!,
        save_positions=(false, false), initialize=initialize)

    # Set the parameter to zero at the end of the event
    tstops_off = events[:, 2]
    affect_off! = (integrator) -> integrator.p[i] = 0.0
    callback_off = PresetTimeCallback(tstops_off, affect_off!,
        save_positions=(false, false))

    return CallbackSet(callback_on, callback_off)

end

"""
`_events_to_function(events::Matrix)`

Convert `events` to a function.
"""
function _events_to_function(events::Matrix)
    N = size(events, 1)
    function fun(t)
        for i = 1:N
            if events[i, 1] <= t < events[i, 2]
                return 1.0
            end
        end
        return 0.0
    end
    return fun
end

"""
`_create_discrete_callback(events::Matrix, i)`

Generate `DiscreteCallback` from `events` modyfing `i`-th parameter.
"""
function _create_discrete_callback(events::Matrix, i)

    p = NaN  # holds the original value of the parameter

    # Initialization at the beginning of the integration
    initialize = function (c, u, t, integrator)

        # Protect the original parameters from overwriting
        integrator.p = copy(integrator.p)

        # Save the default parameter
        p = integrator.p[i]

        # Set the parameter to 0
        integrator.p[i] = 0.0

    end

    # Convert the events to a function
    fun = _events_to_function(events)

    # Call the function at every step of the integration
    affect! = (integrator) -> integrator.p[i] = fun(integrator.t)*p
    condition = (u, t, integrator) -> true
    callback = DiscreteCallback(condition, affect!; initialize=initialize,
        save_positions=(false, false))

    return callback

end

"""
`set_input(model::Model, events::Matrix, parameter_name="I")`

Set input to the model.

**Arguments**
- `model`: Model.
- `events`: Matrix representing the input square function.
- `parameter_name`: Parameter that is being modified by the input.
"""
function set_input(model::Model, events::Matrix, parameter_name="I")

    parameter_index = get_parameter_index(model, parameter_name)

    # Create a callback
    if model.problem isa Union{ODEProblem, SDEProblem}
        # Use PresetTimeCallback for ODE and SDE models
        callback = _create_callback(events, parameter_index)
    elseif model.problem isa JumpProblem
        # Use DiscreteCallback for Jump models
        callback = _create_discrete_callback(events, parameter_index)
    end

    # Build a new model with the specified input
    new_model = _set_callback(model, callback)
    new_model = _set_model_property(new_model, input=(events, parameter_name))

    return new_model

end

"""
`simulate_population(model::Model, trajectories=1; save_trajectories=true,
    show_progress=false)`

Simulate a population.

**Arguments**
- `model`: Model.

**Optional Arguments**
- `trajectories`: Number of trajectories to simulate.

**Keyword Arguments**
- `save_trajectories`: If `false`, only mean is saved.
- `show_progress`: If `true`, show a progress bar in the terminal.
"""
function simulate_population(model::Model, trajectories=1;
    save_trajectories=true, show_progress=false)

    # Make sure that the original model is not modified
    model = deepcopy(model)

    # Prepare variables for PopulationSolution fields
    t = Vector{Float64}(undef, 0)  # time
    m = Matrix{Float64}(undef, 0, 0)  # mean
    U = Array{Float64, 3}(undef, 0, 0, 0)  # trajectories
    events = model.input[1]  # events

    # Initialize the porgress meter
    if show_progress
        progress_meter = ProgressMeter.Progress(trajectories; barlen=20)
    end

    # Simulate trajectories
    success_arr = fill(false, trajectories)  # success for individual trajectories
    lk = ReentrantLock()
    Threads.@threads for i = 1:trajectories

        model2 = deepcopy(model)

        # Solve the problem
        sol2 = solve(model2.problem, model2.solver_algorithm; model2.solver_parameters...)

        # Check if the simulation was successful
        if model2.problem isa Union{ODEProblem, SDEProblem}
            success_arr[i] = sol2.retcode == :Success
        elseif model2.problem isa JumpProblem
            success_arr[i] = sol2.retcode == :Default
        else
            error("Unknown model type!")
        end

        # Save the solution if the current simulation was successful
        if success_arr[i]

            lock(lk) do

                # Save time
                if isempty(t)
                    t = sol2.t
                end

                # Map solution to a matrix
                x = model2.output(sol2)

                # Add the trajectory contribution to the mean
                if isempty(m)
                    # Initialize the mean vector
                    m = (x ./ trajectories)
                else
                    # Add solution to the mean vector
                    m .+= (x ./ trajectories)
                end

                # Save the individual trajectory
                if save_trajectories
                    if isempty(U)
                        # Initialize the matrix for individual trajectories
                        nsamples = size(x, 1)
                        nvariables = size(x, 2)
                        U = fill(NaN, nsamples, nvariables, trajectories)
                    end
                    # Add the current trajectory to the trajectory list
                    U[:, :, i] = x
                end

            end

        else
            # Stop the simulation if the current simulation was unsuccessful
            break
        end

        if show_progress
            lock(lk) do
                ProgressMeter.next!(progress_meter)
            end
        end

    end

    # Build the output struct
    success = all(success_arr)
    solution = PopulationSolution(t, m, U, events, success)

    return solution

end

"""
`scan`

Scan parameters of a model.

**Arguments**
- `model`: Model.
- `parameter_names`: Parameter names to scan.
- `parameter_values`: Parameter values for each parameter to scan over.
- `summary_function`: A function that takes in a model, evaluates it, and
    returns some parameters. If the function is called without any arguments,
    it returns the names of the parameters as a vector of strings. For example
    ```
    julia> summary_function(model)
    [1.1, 2.2, 3.3]
    julia> summary_function()
    ["Period", "Amplitude", "Phase"]
    ```

**Keyword Arguments**
- `show_progress`: If `true`, show a progress bar in the terminal.

**Returns**
- `df`: Dataframe with results.
"""
function scan(model::Model, parameter_names, parameter_values,
    summary_function; show_progress=false)

    # Protect the original model from overwriting
    model = deepcopy(model)

    # Find all possible parameter combinations
    parameter_value_combinations = find_all_combinations(parameter_values)
    n = size(parameter_value_combinations, 1)

    # Initialize summary matrix
    summary_names = summary_function()
    scan_summary = fill(NaN, n, length(summary_names))

    # Initialize progress bar
    if show_progress
        progressmeter = ProgressMeter.Progress(n; barlen=20)
    end

    # Iterate all parameter value combinations
    lk = ReentrantLock()
    Threads.@threads for i = 1:n

        # Set model parameters
        pmodel = set_parameter_value(model, parameter_names, parameter_value_combinations[i, :])

        # Evaluate the sumary function
        pmodel_summary = summary_function(pmodel)

        # Save the model summary to the overall matrix
        scan_summary[i, :] = pmodel_summary

        # Update progress bar
        if show_progress
            lock(lk) do
                ProgressMeter.next!(progressmeter)
            end
        end

    end

    # Build the output dataframe
    matrix = hcat(parameter_value_combinations, scan_summary)
    names = vcat(parameter_names, summary_names)
    df = DataFrame(matrix, names)

    return df

end

"""
`estimate_binary_arnold_tongue`

Use binary search to estimate arnold tongue for a model.

**Arguments**
- `model`: Model.
- `periods`: A two-element array like `[minimal_period, maximal_period]`.
- `amplitudes`: Input amplitudes to scan.

**Keyword Arguments**
- `trajectories`: Number of trajectories in a population.
- `i_solution`: Number of the state to use to estimate the period.
- `n_lags`: Number of lags used to estimate the autocorrelation functions.
- `input_parameter_name`: Name of the parameter that control the input amplitude.
- `min_time`: Minimal time to consider (to remove the effect of initial conditions).
- `max_time`: Maximal time of the simulation.
- `show_progress`: If true, show a progress bar in the terminal.
- `period_error`: Maximal allowed output period deviation from the input period.
- `boundary_error`: Maximal allowed error to estimate the boundary between the
    entrained and unentrained regions.
- `maximal_error_attempts`: How many attempts should be given to an errored simulation.
"""
function estimate_binary_arnold_tongue(model::Model, periods, amplitudes;
    trajectories=1, i_solution=1, n_lags=1000, input_parameter_name="I",
    min_time=nothing, max_time=nothing, show_progress=false, period_error=0.01,
    boundary_error=0.01, maximal_error_attempts=0)

    # Protect the original model from overwriting
    model = deepcopy(model)

    # Set the maximal time of the simulation
    if !isnothing(max_time)
        model = set_timespan(model, (0.0, max_time))
    end

    # Get minimal and maximal amplitude and the number of periods to scan
    minimal_amplitude = amplitudes[1]
    maximal_amplitude = amplitudes[end]
    n_periods = length(periods)

    # Each element is the minimal amplitude of entrainment for the given period
    entrainment_amplitudes = fill(NaN, n_periods)

    # Initialize a progress bar
    if show_progress
        progressmeter = ProgressMeter.Progress(n_periods; barlen=20)
    end

    # Iterate all period-amplitude combinations
    lk = ReentrantLock()
    Threads.@threads for i = 1:n_periods  # Threads.@threads

        # Set the forcing (input) period
        forcing_period = periods[i]

        # Create events for the current forcing period
        if model.problem isa JumpProblem
            end_time = model.problem.prob.tspan[2]
        else
            end_time = model.problem.tspan[2]
        end
        nevents = end_time / forcing_period + 1
        events = create_events([(:DD, forcing_period*0.5),
            (:LD, nevents, forcing_period*0.5, forcing_period*0.5)])

        # Set offset (solution before this time is discarted)
        if isnothing(min_time)
            offset = end_time*0.5
        else
            offset = min_time
        end

        # pmodel = deepcopy(model)
        pmodel = set_input(model, events, input_parameter_name)

        """
        This function will be optimized using the binary boundary search. It
        simulates the model for the given forcing amplitude and returns `true`
        if the model is entrained and `false` if the model is unentrained.
        """
        function fun(forcing_amplitude)

            # Set the forcing amplitude and events to the model
            pmodel = set_parameter_value(pmodel, input_parameter_name, forcing_amplitude)

            # Simulate the population
            solution = PopulationSolution()
            success = false
            counter = 0
            while !success
                try
                    # lock(lk) do
                    solution = simulate_population(pmodel, trajectories;
                        save_trajectories=false)
                    # end
                    success = true
                catch err
                    counter += 1
                    if counter >= maximal_error_attempts
                        @warn "[LAST] Simulation threw an error: $err"
                        solution = PopulationSolution()
                        success = true
                    else
                        @warn "[$counter] Simulation threw an error: $err"
                    end
                end
            end

            # If the simulation was successful, estimate the period
            if solution.success

                # Discard the simulation start
                solution = select_time(solution, mintime=offset)
                t = solution.time
                m = solution.mean[:, i_solution]

                R_period, _ = estimate_period(t, m; n_lags=n_lags)

                return abs(R_period - forcing_period) <= period_error

            else

                return false

            end

        end

        # Perform binary search
        entrainment_amplitudes[i] = binary_boundary_search(fun,
            [minimal_amplitude, maximal_amplitude], boundary_error)

        # Update the progress bar
        if show_progress
            ProgressMeter.next!(progressmeter)
        end

    end

    # Create the output dataframe
    arnold_tongue = DataFrame(periods=periods, amplitudes=entrainment_amplitudes)

    return arnold_tongue

end


export LightSource, DistantLight, PointLight, DiskLight

################################
# Visibility Tester is a ray from point to light source

"""
    VisibilityTester(p1::Point3, p2::Point3, eps1::Float64, eps2::Float64)

Returns a ray to test intersection on the line segment from `p1` to `p2` with the given ray
epsilon values.

"""
function VisibilityTester(p1::Point3, p2::Point3, eps1::Float64, eps2::Float64)
    distance = norm(p2-p1)
    return Ray(p1, (p2-p1)/distance, eps1, distance*(1.0 - eps2), 1)
end

"""
    VisibilityTester(origin::Point3, direction::Vector3, eps::Float64)

Returns a ray to test intersection on the line segment from `origin` infinitely far in the
given direction, with a given ray epsilon.

"""
function VisibilityTester(origin::Point3, direction::Vector3, eps::Float64)
    return Ray(origin, direction, eps, Inf, 1)
end

################################
# Point light

"""
    PointLight{S<:Spectrum} <: LightSource

Point light source. It's located at a given point and radiates isotropically. The intensity
value given is interpreted as the intensity in Watts/m^2 at a distance of 1 meter.

# Fields

- `position::Point3`, the location of the light source.
- `intensity::S`, the intensity at a distance of 1 meter in Watts/m^2.
- `light_to_world::Transformation`, transformation to shift the location of the light
   source.
- `world_to_light::Transformation`, inverse transformation of the above.

"""
struct PointLight{S<:Spectrum} <: LightSource
    position::Point3
    intensity::S
    light_to_world::Transformation
    world_to_light::Transformation
end

"""
    PointLight(L::Spectrum, l2w::Transformation)

Create a new `PointLight` with the given spectrum, located at the origin but shifted by
the given `Transformation`.

"""
function PointLight(L::Spectrum, l2w::Transformation)
    PointLight(l2w(Point3(0)), L, l2w, inv(l2w))
end

"""
    sample_L(light::PointLight, p::Point3)

Get an intensity sample from the given `DistantLight` and a ray from the light towards the
given point. The sample is always the same since the light source is a uniform parallel
light.

Returns:

- Intensity `Spectrum`
- Direction `Vector3` of the light.
- A weight coefficient which is always 1.0
- A ray from the light source to the given point.

"""
function sample_L(light::PointLight, p::Point3)
    distance = norm(light.position - p)
    L = light.intensity / distance^2
    pdf = 1.0
    vis = VisibilityTester(p, light.position, 2e-5, 0.0)
    return L, pdf, vis
end

"""
    generate_ray(light::PointLight, bounds::BoundingSphere, S::Sample)

Returns a ray from the light source at the given `BoundingSphere`, parametrized by the
given random sample.

"""
function generate_ray(light::PointLight, bounds::BoundingSphere, S::Sample)
    ray = ray_from_point(bounds.radius, S.imgX, S.imgY)
    return light.light_to_world(ray)
end

################################
# Distanct light source

"""
    DistantLight{S<:Spectrum} <: LightSource

A point light source infinitely far away, i.e. parallel rays with a given intensity spectrum
interpreted as Watts / m^2.

By default the light source is "located" infinitely far away along the positive Z axis and
the light rays travel in the -Z direction.

# Fields

- `intensity::S`, the intensity of the light in Watts/m^2.
- `light_to_world::Transformation`, transformation to define the direction of the light
   source.
- `world_to_light::Transformation`, inverse transformation of the above.

"""
struct DistantLight{S<:Spectrum} <: LightSource
    intensity::S
    light_to_world::Transformation
    world_to_light::Transformation
end

"""
    function DistantLight(L::Spectrum, l2w::Transformation)

Create a new `DistantLight` given the intensity spectrum (in units of Watts / m^2) and
a `Transformation` used to rotate the direction of the light.

"""
function DistantLight(L::Spectrum, l2w::Transformation)
    DistantLight(L, l2w, inv(l2w))
end

"""
    sample_L(light::DistantLight, p::Point3)

Get an intensity sample from the given `DistantLight` and a ray from the light towards the
given point. The sample is always the same since the light source is a uniform parallel
light.

Returns:

- Intensity `Spectrum`
- A weight coefficient which is always 1.0
- A ray from the light source to the given point.

"""
function sample_L(light::DistantLight, p::Point3)
    return light.intensity, 1.0, VisibilityTester(p, light.light_to_world(NEG_Z_AXIS), 1e-9)
end

"""
    generate_ray(light::DistantLight, bounds::BoundingSphere, S::Sample)

Returns a ray from the light source at the given `BoundingSphere`, parametrized by the
given random sample.

"""
function generate_ray(light::DistantLight, bounds::BoundingSphere, S::Sample)
    ray = ray_parallel(bounds.radius, S.imgX, S.imgY)
    shift = translation(bounds.center)
    return shift(light.light_to_world(ray))
end

###### ########################
# TODO: Area light
struct AreaLight <: LightSource end

################################
# Disk Light (TODO)

"""
    DiskLight{S<:Spectrum} <: LightSource

A light source which is a uniformly illuminated disk far away. Expressed in terms of its
angular radius. By default it's in the positive Z direction, and can be rotated with
a transformation.

# Fields:

- `radius::Float64`, angular radius (in radians) of the source
- `intensity::S`,
- `light_to_world::Transformation`, transformation to adjust the direction of the source.
   Chosen to rotate the +Z axis into the desired direction of the light.
- `world_to_light::Transformation`, inverse transformation of the above

"""
struct DiskLight{S<:Spectrum} <: LightSource
    radius::Float64
    intensity::S
    light_to_world::Transformation
    world_to_light::Transformation
end

"""
    DiskLight(direction::Vector3, radius::Float64, intensity::Spectrum)

Create new `DiskLight` in the desired direction, angular radius and intensity.

"""
function DiskLight(direction::Vector3, radius::Float64, intensity::Spectrum)
    rot_z = rotate_z_to(direction)
    Ω = 2π * (1 - cos(radius))
    return DiskLight(radius, Ω.*intensity, rot_z, inv(rot_z))
end

@inline solid_angle(D::DiskLight) = 2π * (1 - cos(D.radius))

sample_L(light::DiskLight, p::Point3) = sample_L(light, p, rand(), rand())

function sample_L(light::DiskLight, p::Point3, u1::Real, u2::Real)
    direction = light.light_to_world(disk_light_sample(light.radius, u1, u2))
    return light.intensity, 1.0, VisibilityTester(p, direction, 2e-9)
end

"""
    disk_light_sample(radius::Real, u1::Real, u2::Real)

Create a vector which points from origin to a point on the surface of a disk with given
angular radius centered on the +Z axis. It is parametrized by two numbers, `u1` and `u2`,
which range from zero to one. Using uniform random numbers will result in the surface of
the disk to be uniformly sampled.

"""
function disk_light_sample(radius::Real, u1::Real, u2::Real)
    cos_th = cos(radius)
    z = cos_th + (1.0 - cos_th) * u1
    phi = 2π * u2
    r = sqrt(1.0 - z^2)
    return Vector3(r*cos(phi), r*sin(phi), z)
end

"""
    generate_ray(light::DiskLight, bounds::BoundingSphere, S::Sample)

Returns a ray from the light source at the given `BoundingSphere`, parametrized by the
given random sample.

"""
function generate_ray(light::DiskLight, bounds::BoundingSphere, S::Sample)
    # Pick a direction, i.e. a point on the (infinitely distant) disk
    # and make the transformation to rotate Z into that direction
    V = disk_light_sample(light.radius, S.lensU, S.lensV)
    rot = light.light_to_world * rotate_z_to(V)

    # Make a ray from infinitely far along the Z axis and rotate it into the direction
    # given by T. Also shift it relative to the origin of the bounding sphere.

    ray = ray_parallel(bounds.radius, S.imgX, S.imgY)
    T = translation(bounds.center) * rot
    return T(ray)
end


function gamesize(g::AirDefenseGame)
    ncity, ndp, nap, ndpdc, ndpac, ndc, nac = g.ncity, g.ndp, g.nap, g.ndpdc, g.ndpac, g.ndc, g.nac
    nIc1 = 1
    nIa2 = binomial(ndp, ndpac)
    nIa5 = nIa2 * binomial(ndp, nac) * binomial(ndp, ndc)
    nIc3 = 1
    nId4 = binomial(ndp, ndpdc) * (2 ^ ndp)
    nIa = @sprintf("%.2E", nIa2 + nIa5)
    nId = @sprintf("%.2E", nId4)
    na1c = binomial(ndp, ndpdc)
    na2c = binomial(ndp, ndpac)
    na3a = binomial(ndpac, nac)
    na4c = 2 ^ nac
    na5d = binomial(ndpdc, ndc)
    na6a = binomial(ncity, nap)
    nas = [na1c, na2c, na3a, na4c, na5d, na6a]
    leafnodes = @sprintf("%.2E", prod(nas))
    nnodes = @sprintf("%.2E", sum(cumprod(nas)))
    (nIa, nId, leafnodes, nnodes)
end

# N : player indices (nature not included)
getplayers(g::AirDefenseGame) = 1:2

# Player index of current node (note: this is distinct from which player is currently updating CFR)
function getplayer(depth::Int64)
    (depth == 6 || depth == 3) ? 2 : (depth == 5) ? 1 : 3
    # 1 = defender, 2 = attacker, 3 = nature
end

actions(depth::Int64, An::Vector{UnitRange{Int64}}) = An[depth]
actions(depth::Int64, A::Vector{Vector{Vector{Int64}}}) = 1:length(A[depth])

function numactions(I::Int64, ni_stage, na_stage)
    if I in 1:ni_stage[3]
        return na_stage[3] # na3a
    elseif I in (ni_stage[3] + 1):(ni_stage[3] + ni_stage[5])
        return na_stage[5] # na5d
    elseif I in (ni_stage[3] + ni_stage[5] + 1):(ni_stage[3] + ni_stage[5] + ni_stage[6])
        return na_stage[6] # na5a
    else
        @warn "Infoset $I not found."
        return 0
    end
end

terminal(depth::Int64) = depth == 7

function details(h::SVector, A)
    len = findfirst(x -> x == 0, h) - 1
    (len == nothing) && (len = length(h))
    df = DataFrame(Stage = Int[], Domain = String[], Player = String[], ActionNumber = Int[], Action = Array[])
    for i = 1:len
        stage = i
        domain = (i == 6) ? "Physical" : "Cyber"
        p = getplayer(h[1:i])
        pstring = p == 2 ? "Attacker" : p == 1 ? "Defender" : "Chance"
        push!(df, (stage, domain, pstring, h[i], A[i][h[i]]))
    end
    df
end

function infoset(h::SVector, depth::Int64, g::AirDefenseGame, gs::GameSet) # btime: ~150 ns
    # findfirst benchmarks faster than numerically calculating infoset (see earlier commit)
    A, ni_stage, na_stage, sensors, iadsdefenses = gs.A, gs.ni_stage, gs.na_stage, gs.sensors, gs.iadsdefenses
    cyber_attacks, cyber_defenses = A[3], A[5]
    ndp, nac = g.ndp, g.nac
    len = depth - 1
    if len == 5
        defended_iads = getindex(gs.A[1][h[1]], gs.A[5][h[5]])
        idc = findfirst(x -> x == defended_iads, iadsdefenses)
        niadsdefenses = length(gs.iadsdefenses)
        return sum(ni_stage[1:5]) + (h[2] - 1) * na_stage[3] * niadsdefenses + (h[3] - 1) * niadsdefenses + idc
    elseif len == 4
        nsensor = length(sensors) # number of combinations of sensed cyber attacks
        sensed = getindex(gs.A[2][h[2]], getindex(A[3][h[3]], A[4][h[4]] .== 2)) # ndp id's of sensed attacks
        isensor = findfirst(x -> x == sensed, sensors) # infoset id of that combination
        return sum(ni_stage[1:4]) + (h[1] - 1) * nsensor + isensor
    elseif len == 2
        return h[2]
    else
        player = getplayer(h)
        @warn "No infoset found. Chance node? h = $h, player = $player."
        return 0
    end
end

#TODO: Make these probabilities more rational?
function chance(depth::Int64, a::Int64, g::AirDefenseGame, gs::GameSet)
    len = depth - 1
    if len == 3 # nature determines defense cyber SA
        nsensed = sum(gs.A[4][a] .== 2) # number of sensed attacks
        return (g.psc ^ nsensed) * (1 - g.psc)^(g.nac - nsensed)
    elseif len == 1 # nature deals offensive capes
        return 1.0 / gs.na_stage[2]
    elseif len == 0 # nature deals defensive capes
        return 1.0 / gs.na_stage[1]
    else
        @warn "Node at depth $depth not a chance node."
        return 0.0
    end
end

function leafutility(h::SVector, player::Int64, πi::Float64, πo::Float64, g::AirDefenseGame, gs::GameSet)
    c1, c2, ac, c4, dc, ap = h
    attacked_cities = @view gs.A[6][h[6]][:]
    defended_iads = getindex(g.iads, getindex(gs.A[1][h[1]], gs.A[5][h[5]]))
    attacked_iads = getindex(g.iads, getindex(gs.A[2][h[2]], gs.A[3][h[3]]))
    leaf_value = utility(g, gs.C, defended_iads, attacked_iads, attacked_cities)
    u = player == 1 ? leaf_value * πo : -1.0 * leaf_value * πo
    return u
end

# convert behavioral strategy to realization strategy (i.e. sequence form?)
function real_strat_calc(σ, ni, ni_stage, ns1, ns2, seqI1, seqI2, nextseqI1, nextseqI2)
    length(σ) != ni && @warn "Unexpected behavioral strategy length: $(length(σ))."
    σ1 = σ[(ni_stage[3] + 1):sum(ni_stage[1:5])]
    σ2 = vcat(σ[1:ni_stage[3]], σ[(sum(ni_stage[1:5]) + 1):end])
    rp1 = zeros(Float64, ns1)
    rp2 = zeros(Float64, ns2)
    rp1[1], rp2[1] = 1.0, 1.0
    for (i, s1) in enumerate(σ1)
        rp1[nextseqI1[i]] .= rp1[seqI1[i]] .* s1
    end
    for (i, s2) in enumerate(σ2)
        rp2[nextseqI2[i]] .= rp2[seqI2[i]] .* s2
    end
    return (rp1, rp2)
end

function real_strat_struct_calc(σ, ni, ni_stage, ns1, ns2, seqI1, seqI2, nextseqI1, nextseqI2)
    length(σ) != ni && @warn "Unexpected behavioral strategy length: $(length(σ))."
    σ1 = σ[(ni_stage[3] + 1):sum(ni_stage[1:5])]
    σ2 = vcat(σ[1:ni_stage[3]], σ[(sum(ni_stage[1:5]) + 1):end])
    rp1_struct, rp2_struct = similar(σ1), similar(σ2)
    rp1, rp2 = real_strat_calc(σ, ni, ni_stage, ns1, ns2, seqI1, seqI2, nextseqI1, nextseqI2)
    for (i, s1) in enumerate(σ1)
        rp1_struct[i] = rp1[nextseqI1[i]]
    end
    for (i, s2) in enumerate(σ2)
        rp2_struct[i] = rp2[nextseqI2[i]]
    end
    return (rp1_struct, rp2_struct)
end

function real_strat_struct_combined_calc(σ, ni, ni_stage, ns1, ns2, seqI1, seqI2, nextseqI1, nextseqI2)
    σ1 = σ[(ni_stage[3] + 1):sum(ni_stage[1:5])]
    σ2 = vcat(σ[1:ni_stage[3]], σ[(sum(ni_stage[1:5]) + 1):end])
    rp1_struct, rp2_struct = similar(σ1), similar(σ2)
    rp1, rp2 = real_strat_calc(σ, ni, ni_stage, ns1, ns2, seqI1, seqI2, nextseqI1, nextseqI2)
    for (i, s1) in enumerate(σ1)
        rp1_struct[i] = rp1[nextseqI1[i]]
    end
    for (i, s2) in enumerate(σ2)
        rp2_struct[i] = rp2[nextseqI2[i]]
    end
    rp_struct = similar(σ)
    rp_struct[1:ni_stage[3]] .= rp2_struct[1:ni_stage[3]]
    rp_struct[(ni_stage[3] + 1):sum(ni_stage[1:5])] .= rp1_struct
    rp_struct[(sum(ni_stage[1:5]) + 1):end] .= rp2_struct[(ni_stage[3] + 1):end]
    return rp_struct
end

function real_strat(σ::Vector{Vector{Float64}}, gs::GameSet, gos::GameOptSet)
    ni, ni_stage, ns1, ns2 = gs.ni, gs.ni_stage, gos.ns1, gos.ns2
    seqI1, seqI2, nextseqI1, nextseqI2 = gos.seqI1, gos.seqI2, gos.nextseqI1, gos.nextseqI2
    return real_strat_calc(σ, ni, ni_stage, ns1, ns2, seqI1, seqI2, nextseqI1, nextseqI2)
end

function real_strat(σ::Vector{Vector{Float64}}, g::AirDefenseGame)
    A, _, na_stage = build_action(g)
    ni, ni1, ni2, ni_stage = build_info(g)
    ns1, ns2, ns1_stage, ns2_stage = build_nseq(g, na_stage, ni_stage)
    _, _, (seqI1, seqI2), (nextseqI1, nextseqI2) = build_Iset(g, A, na_stage, ns1_stage, ns2_stage, ni1, ni2, ns1, ns2)
    return real_strat_calc(σ, ni, ni_stage, ns1, ns2, seqI1, seqI2, nextseqI1, nextseqI2)
end

function real_strat_struct(σ::Vector{Vector{Float64}}, g::AirDefenseGame)
    A, _, na_stage = build_action(g)
    ni, ni1, ni2, ni_stage = build_info(g)
    ns1, ns2, ns1_stage, ns2_stage = build_nseq(g, na_stage, ni_stage)
    _, _, (seqI1, seqI2), (nextseqI1, nextseqI2) = build_Iset(g, A, na_stage, ns1_stage, ns2_stage, ni1, ni2, ns1, ns2)
    return real_strat_struct_calc(σ, ni, ni_stage, ns1, ns2, seqI1, seqI2, nextseqI1, nextseqI2)
end

function real_strat_struct_combined(σ::Vector{Vector{Float64}}, g::AirDefenseGame)
    A, _, na_stage = build_action(g)
    ni, ni1, ni2, ni_stage = build_info(g)
    ns1, ns2, ns1_stage, ns2_stage = build_nseq(g, na_stage, ni_stage)
    _, _, (seqI1, seqI2), (nextseqI1, nextseqI2) = build_Iset(g, A, na_stage, ns1_stage, ns2_stage, ni1, ni2, ns1, ns2)
    return real_strat_struct_combined_calc(σ, ni, ni_stage, ns1, ns2, seqI1, seqI2, nextseqI1, nextseqI2)
end

function real_strat_struct_combined(σ::Vector{Vector{Float64}}, gs::GameSet, seqvals)
    ni, ni_stage = gs.ni, gs.ni_stage
    ns1, ns2, seqI1, seqI2, nextseqI1, nextseqI2 = seqvals
    return real_strat_struct_combined_calc(σ, ni, ni_stage, ns1, ns2, seqI1, seqI2, nextseqI1, nextseqI2)
end

# build consraint set on sequence-form strategy for CCFR
function build_ccfr_constraints(g, σfix, conf::Float64, sd::Float64)
    noise = Normal(0, sd)
    hw = quantile(noise, 1 - (1 - conf) / 2)
    σlb = [max.(σi .- hw, 0.0) for σi in σfix]
    σub = [min.(σi .+ hw, 1.0) for σi in σfix]
    rp2fix = real_strat_struct(σfix, g)[2]
    rp2lb = real_strat_struct(σlb, g)[2]
    rp2ub = real_strat_struct(σub, g)[2]
    !all(rp2lb .<= rp2fix .<= rp2ub) && @warn "Sequence-form strategy out of bounds."
    constraint_coeff = [[1, -1 .* rp2lb[i][j]] for i in 1:length(rp2lb), j in 1:length(rep2lb[i])]
    constraint_coeff = vcat(hcat(ones(length(rp2fix)), -1 .* rp2lb), hcat(-1 .* ones(length(rp2fix)), rp2ub))
    @show length(constraint_coeff), length(constraint_coeff[:,1])
    grad_constraint_coeff = hcat(constraint_coeff[:,1], zeros(length(constraint_coeff[:,1])))
    return constraint_coeff, grad_constraint_coeff
end


"""
Type representing a third-order tensor
"""
struct ThirdOrderTensorValue{D1,D2,D3,T,L} <: MultiValue{Tuple{D1,D2,D3},T,3,L}
    data::NTuple{L,T}
    function ThirdOrderTensorValue{D1,D2,D3,T}(data::NTuple{L,T}) where {D1,D2,D3,T,L}
        @assert L == D1*D2*D3
        new{D1,D2,D3,T,L}(data)
    end
end

# Empty ThirdOrderTensorValue constructor

ThirdOrderTensorValue()                       = ThirdOrderTensorValue{0,0,0,Int}(NTuple{0,Int}())
ThirdOrderTensorValue{0,0,0}()                = ThirdOrderTensorValue{0,0,0,Int}(NTuple{0,Int}())
ThirdOrderTensorValue{0,0,0,T}() where{T}     = ThirdOrderTensorValue{0,0,0,T}(NTuple{0,T}())
ThirdOrderTensorValue(data::NTuple{0})        = ThirdOrderTensorValue{0,0,0,Int}(data)
ThirdOrderTensorValue{0,0,0}(data::NTuple{0}) = ThirdOrderTensorValue{0,0,0,Int}(data)

# ThirdOrderTensorValue single NTuple argument constructor

@generated function ThirdOrderTensorValue(data::NTuple{L,T}) where {L,T}
  D=Int(cbrt(L))
  quote
    ThirdOrderTensorValue{$D,$D,$D,T}(data)
  end
end
ThirdOrderTensorValue{D}(data::NTuple{L,T}) where {D,L,T}                         = ThirdOrderTensorValue{D,D,D,T}(data)
ThirdOrderTensorValue{D1,D2,D3}(data::NTuple{L,T}) where {D1,D2,D3,L,T}           = ThirdOrderTensorValue{D1,D2,D3,T}(data)
ThirdOrderTensorValue{D1,D2,D3,T1}(data::NTuple{L,T2}) where {D1,D2,D3,L,T1,T2}   = ThirdOrderTensorValue{D1,D2,D3,T1}(NTuple{L,T1}(data))
ThirdOrderTensorValue{D1,D2,D3,T1,L}(data::NTuple{L,T2}) where {D1,D2,D3,L,T1,T2} = ThirdOrderTensorValue{D1,D2,D3,T1}(NTuple{L,T1}(data))

# ThirdOrderTensorValue single Tuple argument constructor

ThirdOrderTensorValue(data::Tuple) = ThirdOrderTensorValue(promote(data...))
ThirdOrderTensorValue{D}(data::Tuple) where {D} = ThirdOrderTensorValue{D,D,D}(promote(data...))
ThirdOrderTensorValue{D1,D2,D3}(data::Tuple) where {D1,D2,D3} = ThirdOrderTensorValue{D1,D2,D3}(promote(data...))
ThirdOrderTensorValue{D1,D2,D3,T1}(data::Tuple) where {D1,D2,D3,T1}  = ThirdOrderTensorValue{D1,D2,D3,T1}(NTuple{length(data),T1}(data))
ThirdOrderTensorValue{D1,D2,D3,T1,L}(data::Tuple) where {D1,D2,D3,T1,L}  = ThirdOrderTensorValue{D1,D2,D3,T1}(NTuple{L,T1}(data))

# ThirdOrderTensorValue Vararg constructor

ThirdOrderTensorValue(data...) = ThirdOrderTensorValue(data)
ThirdOrderTensorValue{D}(data...) where {D} = ThirdOrderTensorValue{D}(data)
ThirdOrderTensorValue{D1,D2,D3}(data...) where {D1,D2,D3} = ThirdOrderTensorValue{D1,D2,D3}(data)
ThirdOrderTensorValue{D1,D2,D3,T1}(data...) where {D1,D2,D3,T1} =  ThirdOrderTensorValue{D1,D2,D3,T1}(data)
ThirdOrderTensorValue{D1,D2,D3,T1,L}(data...) where {D1,D2,D3,T1,L} =  ThirdOrderTensorValue{D1,D2,D3,T1}(data)

# ThirdOrderTensorValue single AbstractArray{3,T} argument constructor

ThirdOrderTensorValue(data::AbstractArray{T,3}) where {T} = ((D1,D2,D3)=size(data);L=length(data);ThirdOrderTensorValue{D1,D2,D3,T}(NTuple{L,T}(data)))
ThirdOrderTensorValue{D}(data::AbstractArray{T,3}) where {D,T} = (L=length(data);ThirdOrderTensorValue{D,D,D,T}(NTuple{L,T}(data)))
ThirdOrderTensorValue{D1,D2,D3}(data::AbstractArray{T,3}) where {D1,D2,D3,T} = (L=length(data);ThirdOrderTensorValue{D1,D2,D3,T}(NTuple{L,T}(data)))
ThirdOrderTensorValue{D1,D2,D3,T1}(data::AbstractArray{T2,3}) where {D1,D2,D3,T1,T2} = (L=length(data);ThirdOrderTensorValue{D1,D2,D3,T1}(NTuple{L,T1}(data)))
ThirdOrderTensorValue{D1,D2,D3,T1,L}(data::AbstractArray{T2,3}) where {D1,D2,D3,T1,T2,L} = ThirdOrderTensorValue{D1,D2,D3,T1}(NTuple{L,T1}(data))

###############################################################
# Conversions (ThirdOrderTensorValue)
###############################################################

# Direct conversion
convert(::Type{<:ThirdOrderTensorValue{D1,D2,D3,T}}, arg::AbstractArray) where {D1,D2,D3,T} = ThirdOrderTensorValue{D1,D2,D3,T}(arg)
convert(::Type{<:ThirdOrderTensorValue{D1,D2,D3,T}}, arg::Tuple) where {D1,D2,D3,T} = ThirdOrderTensorValue{D1,D2,D3,T}(arg)

# Inverse conversion
convert(::Type{<:SMatrix{D1,D2,D3,T}}, arg::ThirdOrderTensorValue) where {D1,D2,D3,T} = SMatrix{D1,D2,D3,T}(Tuple(arg))
convert(::Type{<:MMatrix{D1,D2,D3,T}}, arg::ThirdOrderTensorValue) where {D1,D2,D3,T} = MMatrix{D1,D2,D3,T}(Tuple(arg))
convert(::Type{<:NTuple{L,T1}}, arg::ThirdOrderTensorValue) where {L,T1} = NTuple{L,T1}(Tuple(arg))

# Internal conversion
convert(::Type{<:ThirdOrderTensorValue{D1,D2,D3,T}}, arg::ThirdOrderTensorValue{D1,D2,D3}) where {D1,D2,D3,T} = ThirdOrderTensorValue{D1,D2,D3,T}(Tuple(arg))
convert(::Type{<:ThirdOrderTensorValue{D1,D2,D3,T}}, arg::ThirdOrderTensorValue{D1,D2,D3,T}) where {D1,D2,D3,T} = arg

# other

change_eltype(::Type{ThirdOrderTensorValue{D1,D2,D3,T1,L}},::Type{T2}) where {D1,D2,D3,T1,T2,L} = ThirdOrderTensorValue{D1,D2,D3,T2,L}
change_eltype(::T,::Type{T2}) where {T<:ThirdOrderTensorValue,T2} = change_eltype(T,T2)

zero(::Type{<:ThirdOrderTensorValue{D1,D2,D3,T}}) where {D1,D2,D3,T} = ThirdOrderTensorValue{D1,D2,D3,T}(tfill(zero(T),Val{D1*D2*D3}()))
zero(::ThirdOrderTensorValue{D1,D2,D3,T}) where {D1,D2,D3,T} = zero(ThirdOrderTensorValue{D1,D2,D3,T})

Mutable(::Type{<:ThirdOrderTensorValue{D1,D2,D3,T}}) where {D1,D2,D3,T} = MArray{Tuple{D1,D2,D3},T}
Mutable(::ThirdOrderTensorValue{D1,D2,D3,T}) where {D1,D2,D3,T} = Mutable(ThirdOrderTensorValue{D1,D2,D3,T})
mutable(a::ThirdOrderTensorValue{D1,D2,D3}) where {D1,D2,D3} = MArray{Tuple{D1,D2,D3}}(a.data)

###############################################################
# Introspection (ThirdOrderTensorValue)
###############################################################

eltype(::Type{<:ThirdOrderTensorValue{D1,D2,D3,T}}) where {D1,D2,D3,T} = T
eltype(::ThirdOrderTensorValue{D1,D2,D3,T}) where {D1,D2,D3,T} = eltype(ThirdOrderTensorValue{D1,D2,D3,T})

size(::Type{<:ThirdOrderTensorValue{D1,D2,D3}}) where {D1,D2,D3} = (D1,D2,D3)
size(::ThirdOrderTensorValue{D1,D2,D3}) where {D1,D2,D3} = size(ThirdOrderTensorValue{D1,D2,D3})

length(::Type{<:ThirdOrderTensorValue{D1,D2,D3}}) where {D1,D2,D3} = D1*D2*D3
length(::ThirdOrderTensorValue{D1,D2,D3}) where {D1,D2,D3} = length(ThirdOrderTensorValue{D1,D2,D3})

num_components(::Type{<:ThirdOrderTensorValue{D1,D2,D3}}) where {D1,D2,D3} = length(ThirdOrderTensorValue{D1,D2,D3})
num_components(::ThirdOrderTensorValue{D1,D2,D3}) where {D1,D2,D3} = num_components(ThirdOrderTensorValue{D1,D2,D3})


module IdealGas
include("Constants.jl")
include("Utils.jl")
using LightXML

abstract type ComponentDefinition end
abstract type ThermoData end

struct Gasphase <: ComponentDefinition
    species::Array{String,1}
end

struct NASAThermo <: ThermoData
    name::String
    phase::String
    composition::Dict{String,Int64}
    molWt::Float64
    ltl::Float64
    htl::Float64
    cmt::Float64
    ltp::Array{Float64,1}
    htp::Array{Float64,1}
end

#This array will be order as per ig.species
struct SpeciesThermoObj
    molwt::Array{Float64,1}
    thermo_all::Array{NASAThermo}
end
export SpeciesThermoObj

"""
    Tuple of element weights. This is used for the calculation of molecular weights
"""
elementWeight = (    H = 1.00794e-3,  He = 4.002602e-3,  Li = 6.941e-3,  Be = 9.012182e-3,  B = 1.811e-3,
                C = 12.011e-3,  N = 14.00674e-3,  O = 15.9994e-3,  F = 18.9984032e-3,  Ne = 20.1797e-3,
                Na = 22.98977e-3, Mg = 24.3050e-3, Al = 26.98154e-3, Si = 28.0855e-3, P = 30.97376e-3,
                S = 32.066e-3, Cl = 35.4527e-3, Ar = 39.948e-3, K = 39.0983e-3, Ca = 40.078e-3,
                Sc = 44.95591e-3, Ti = 47.88e-3, V = 50.9415e-3, Cr = 51.9961e-3, Mn = 54.9381e-3,
                Fe = 55.847e-3, Co = 58.9332e-3, Ni = 58.69e-3, Cu = 63.546e-3, Zn = 65.39e-3,
                Ga = 69.723e-3, Ge = 72.61e-3, As = 74.92159e-3, Se = 78.96e-3, Br = 79.904e-3,
                Kr = 83.80e-3,  Rb = 85.4678e-3, Sr = 87.62e-3, Y = 88.90585e-3, Zr = 91.224e-3,
                Nb = 92.90638e-3, Mo = 95.94e-3, Tc = 97.9072e-3, Ru = 101.07e-3, Rh = 102.9055e-3,
                Pd = 106.42e-3, Ag = 107.8682e-3,   Cd = 112.411e-3,    In = 114.82e-3, Sn = 118.710e-3,
                Sb = 121.75e-3, Te = 127.6e-3,  I = 126.90447e-3,   Xe = 131.29e-3,   Cs = 132.90543e-3,
                Ba = 137.327e-3, La = 138.9055e-3, Ce = 140.115e-3, Pr = 140.90765e-3, Nd = 144.24e-3,
                Pm = 144.9127e-3, Sm = 150.36e-3,   Eu = 151.965e-3,    Gd = 157.25e-3, Tb = 158.92534e-3,
                Dy = 162.50e-3, Ho = 164.93032e-3,  Er = 167.26e-3, Tm = 168.93421e-3,  Yb = 173.04e-3,
                Lu = 174.967e-3,    Hf = 178.49e-3, Ta = 180.9479e-3,   W = 183.85e-3, Re = 186.207e-3,
                Pt = 195.08e-3, Au = 196.96654e-3,  Hg = 200.59e-3, Tl = 204.3833e-3, Pb = 207.2e-3,
                Bi = 208.98037e-3,  Po = 208.9824e-3,  At = 209.9871e-3,  Rn = 222.0176e-3,
                Fr = 223.0197e-3, Ra = 226.0254e-3, Ac = 227.0279e-3, Th = 232.0381e-3,
                Pa = 231.03588e-3, U = 238.0508e-3, Np = 237.0482e-3, Pu = 244.0482e-3
            )

"""
get_element_weight(el)

function to return the element weights
#   Usage:
    get_element_weight(el)
-   'el::String' : Element name
"""
get_element_weight(el::String) = elementWeight[Symbol(titlecase(strip(el)))]

"""
Function to create thermo object. The function reads the therm.dat file and
parses the content based on the ideal gas object to create the thermo data object
The function returns SpeciesThermoObj
#   Usage:
    create_thermo() or
    create_thermo(file_path)
-   'ig::Gasphase' : Gasphase object obtained using the call create_gasphase
-   'file_path': relative path to the therm.dat file
"""
function create_thermo(species::Array{T,1}, thermo_file::AbstractString ) where T <: AbstractString
    #Create an array of thermo objects which stores the thermo data of all species
    species_thermo = Dict{String,NASAThermo}()
    molecular_weights_vector = Array{Float64,1}()

    open(thermo_file) do io
        lno = 0
        local  species_name, phase, mol_wt,LT,HT,CT
        comp = Dict{String,Int64}()
        local poly_coeff = Array{Float64,1}()
        while !eof(io)
            data_string = readline(io)
            if length(data_string) != 0
                if SubString(data_string,1,3) == "END"
                    break
                else
                    sp = uppercase(strip(SubString(data_string,1,18)))
                    if sp in species
                        lno = 1
                        species_name, phase,comp, mol_wt,LT,HT,CT = parase_thermo_species_data(data_string)
                        lno += 1
                        empty!(poly_coeff)
                    elseif lno > 1 && lno < 5
                        append!(poly_coeff,parse_thermo_polynomials(lno,data_string))
                        lno += 1
                        if lno > 4
                            nasa_thermo = NASAThermo(species_name,phase,comp ,mol_wt,LT,HT,CT,poly_coeff[8:14],poly_coeff[1:7])
                            species_thermo[nasa_thermo.name] = nasa_thermo
                        end
                    end
                end
            end
        end
    end
    species_thermo_array = Array{NASAThermo,1}()
    for sp in species
        push!(species_thermo_array,species_thermo[sp])
    end
    for td in species_thermo_array
        push!(molecular_weights_vector,td.molWt)
    end

    return SpeciesThermoObj(molecular_weights_vector, species_thermo_array)
end

"""
Function for parsing the first line of thermo data.
    It returns the species name, phase, molecular weight, high
    temperature, low temperature and Common temperature limits
    Not for external calls
#   Usage:
    parase_thermo_species_data(data_string)
"""
function parase_thermo_species_data(data_string::AbstractString)
    species_name = uppercase(strip(SubString(data_string,1,18)))
    a1 = split(strip(SubString(data_string,25,29)))
    a2 = split(strip(SubString(data_string,30,34)))
    a3 = split(strip(SubString(data_string,35,39)))
    a4 = split(strip(SubString(data_string,40,44)))
    elements_data = (a1,a2,a3,a4)
    mol_wt::Float64 = 0
    composition = Dict{String,Int64}()
    for el in elements_data
        if !isempty(el)
            mol_wt += parse(Float64,el[2])*get_element_weight(String(el[1]))
            composition[String(el[1])] = parse(Int64,el[2])
        end
    end
    phase = String(SubString(data_string,45,45))
    if length(phase)==0
        throw(error("Phase not specified for species $spname"))
    end
    LTs = String(SubString(data_string,46,57))
    if length(LTs) == 0
        throw(error("Low temperature limit not specified for species $spname"))
    end
    HTs = String(SubString(data_string,58,67))
    if length(HTs) == 0
        throw(error("High temperature limit not specified for species $spname"))
    end
    CTs = String(SubString(data_string,68,75))
    if length(CTs)==0
        throw(error("Common temperature limit not specified for species $spname"))
    end
    LT = parse(Float64, LTs)
    HT = parse(Float64, HTs)
    CT = parse(Float64, CTs)
    return (species_name,phase, composition ,mol_wt,LT,HT,CT)
end

"""
This function will parse the polynomials
    Not for external calls
"""
function parse_thermo_polynomials(lno::Int64, thdata::AbstractString)
    poly_data = Array{Float64,1}()
    if lno == 2
        push!(poly_data,parse(Float64,String(SubString(thdata,1,15))))
        push!(poly_data,parse(Float64,String(SubString(thdata,16,30))))
        push!(poly_data,parse(Float64,String(SubString(thdata,31,45))))
        push!(poly_data,parse(Float64,String(SubString(thdata,46,60))))
        push!(poly_data,parse(Float64,String(SubString(thdata,61,75))))
    elseif lno == 3
        push!(poly_data,parse(Float64,String(SubString(thdata,1,15))))
        push!(poly_data,parse(Float64,String(SubString(thdata,16,30))))
        push!(poly_data,parse(Float64,String(SubString(thdata,31,45))))
        push!(poly_data,parse(Float64,String(SubString(thdata,46,60))))
        push!(poly_data,parse(Float64,String(SubString(thdata,61,75))))
    elseif lno == 4
        push!(poly_data,parse(Float64,String(SubString(thdata,1,15))))
        push!(poly_data,parse(Float64,String(SubString(thdata,16,30))))
        push!(poly_data,parse(Float64,String(SubString(thdata,31,45))))
        push!(poly_data,parse(Float64,String(SubString(thdata,46,60))))
    end
    return poly_data
end

"""
Calculate the specific heat of pure species J/mol-K
#   Usage-1:
    cp(thermo,T)
-   'thermo::NASAThermo': NASAThermo of the species
-   'T::Float64': Temperature in K at which the property is required
# Usage-2:
    cp(sp,T,thermo,ig)
-   'sp::String' : species name
-   'T::Float64' : Temperature K
-   'thermo::SpeciesThermoObj' : Structure of SpeciesThermoObj
-   'ig::Gasphase ' : Gasphase object
"""
function cp(thermo::NASAThermo, T::Float64)
    TVec = [T^i for i in 0:4]
    T < thermo.cmt ? sum(thermo.ltp[1:5] .* TVec)*R : sum(thermo.htp[1:5] .* TVec)*R
end
cp(sp::String,T::Float64,thermoObj::SpeciesThermoObj,species_list::Array{String,1}) = cp(thermoObj.thermo_all[get_index(sp,species_list)],T)

"""
Calculates the enthalpy of pure species J/mol
#   Usage-1:
    H(thermo,T)
-   'thermo::NASAThermo': NASAThermo of the species
-   'T::Float64': Temperature in K at which the property is required
# Usage-2:
    H(sp,T,thermo,ig)
-   'sp::String' : species name
-   'T::Float64' : Temperature K
-   'thermo::SpeciesThermoObj' : Structure of SpeciesThermoObj
-   'ig::Gasphase ' : Gasphase object
"""
function H(thermo::NASAThermo, T::Float64)
    TVec = [1, T/2.0, T^2/3.0, T^3/4.0, T^4/5.0, 1.0/T]
    T < thermo.cmt ? sum(thermo.ltp[1:6] .* TVec)*R*T : sum(thermo.htp[1:6] .* TVec)*R*T
end
H(sp::String,T::Float64,thermoObj::SpeciesThermoObj,species_list::Array{String,1}) = H(thermoObj.thermo_all[get_index(sp,species_list)],T)

"""
Calculates the entropy of pure species J/mol-K
#   Usage-1:
    S(thermo,T)
-   'thermo::NASAThermo': NASAThermo of the species
-   'T::Float64': Temperature in K at which the property is required
# Usage-2:
    S(sp,T,thermo,ig)
-   'sp::String' : species name
-   'T::Float64' : Temperature K
-   'thermo::SpeciesThermoObj' : Structure of SpeciesThermoObj
-   'ig::Gasphase ' : Gasphase object
"""
function S(thermo::NASAThermo, T::Float64)
    TVec = [log(T), T, T^2/2.0, T^3/3.0, T^4/4.0]
    if T < thermo.cmt
        return (sum(thermo.ltp[1:5] .* TVec) + thermo.ltp[7])*R
    else
        return (sum(thermo.htp[1:5] .* TVec) + thermo.htp[7])*R
    end
end
S(sp::String,T::Float64,thermoObj::SpeciesThermoObj,species_list::Array{String,1}) = S(thermoObj.thermo_all[get_index(sp,species_list)],T)

"""
Calculates the specific heat of all species in J/mol-K
#   Usage
    cp_all(td,T)
-   'thermoObj::SpeciesThermoObj' : Structure of SpeciesThermoObj
-   'T::Float64' : Temperature in K at which the property is rquired
"""
function cp_all(thermoObj::SpeciesThermoObj,T::Float64)
    return map(x->cp(x,T),thermoObj.thermo_all)
end

"""
Calculates the enthalpy of all species in J/mol
#   Usage
    H_all(td,T)
-   'thermoObj::SpeciesThermoObj' : Structure of SpeciesThermoObj
-   'T::Float64' : Temperature in K at which the property is rquired
"""
function H_all(thermoObj::SpeciesThermoObj,T::Float64)
    return map(x->H(x,T),thermoObj.thermo_all)
end

"""
Calculates the entropy of all species in J/mol-K
#   Usage
    S_all(td,T)
-   'thermoObj::SpeciesThermoObj' : Structure of SpeciesThermoObj
-   'T::Float64' : Temperature in K at which the property is rquired
"""
function S_all(thermoObj::SpeciesThermoObj,T::Float64)
    return map(x->S(x,T),thermoObj.thermo_all)
end

"""
Calculates the enthalpy of a mixture J/mol
#   Usage
    Hmix(td,T,mlf)
-   'thermoObj::SpeciesThermoObj' : Structure of SpeciesThermoObj
-   'T::Float64' : Temperature in K
-   'mlf::Array{Float64,1}' : species mole fractions
"""
function Hmix(thermoObj::SpeciesThermoObj,T::Float64,mlf::Array{Float64,1})
    all_species_h = H_all(thermoObj,T)
    return sum(all_species_h .* mlf)
end

"""
Calculates the specific heat of a mixture in J/mol-K
#   Usage
    cpmix(td,T,mlf)
-   'thermoObj::SpeciesThermoObj' : Structure of SpeciesThermoObj
-   'T::Float64' : Temperature in K
-   'mlf::Array{Float64,1}' : species mole fractions
"""
function cpmix(thermoObj::SpeciesThermoObj,T::Float64,mlf::Array{Float64,1})
    all_species_cp = S_all(thermoObj,T)
    return sum(all_species_cp .* mlf)
end

"""
Calculates the entropy of a muxture in J/mol-K
#   Usage
    Smix(thermoObj,T,p,mlf)
-   'thermoObj::SpeciesThermoObj' : Structure of SpeciesThermoObj
-   'T::Float64' : Temperature in K
-   'p::Float64' : total pressure Pa
-   'mlf::Array{Float64,1} ': mole fractions
"""
function Smix(thermoObj::SpeciesThermoObj,T::Float64,p::Float64,mlf::Array{Float64,1})
    all_species_s = S_all(thermoObj,T)
    xp = mlf .* (p/p_std)
    #logterm = [x <= 0 ? 0 : log(x) for x in xp]
    logterm = collect(map(x->x <= 0 ? 0 : log(x) ,xp))
    return sum((all_species_s - (logterm .* R)) .* mlf)
end

"""
Calculates the Gibbs free energy of a muxture in J/mol
#   Usage
    Gmix(thermoObj,T,p,mlf)
-   'thermoObj::SpeciesThermoObj' : Structure of SpeciesThermoObj
-   'T::Float64' : Temperature in K
-   'p::Float64' : total pressure Pa
-   'mlf::Array{Float64,1} ': mole fractions
"""
function  Gmix(thermoObj::SpeciesThermoObj, T::Float64,p::Float64,mlf::Array{Float64,1})
    hmix = Hmix(thermoObj,T,mlf)
    smix = Smix(thermoObj,T,p,mlf)
    return hmix - T*smix
end

#end of module IdealGas
end