"""
`module  JAC.CoulombIonization`
    ... a submodel of JAC that contains all methods for computing Coulomb ionization cross sections and alignment parameters
        for the excitation of target or projectile electrons by fast ion impact.
"""
module CoulombIonization

    using ..Basics, ..ManyElectron, ..Radial

    """
    `struct  CoulombIonization.Settings`  ... defines a type for the details and parameters of computing Coulomb-ionization lines.

        + multipoles              ::Array{EmMultipole}           ... Specifies the multipoles of the radiation field that are to be included.
        + gauges                  ::Array{UseGauge}              ... Specifies the gauges to be included into the computations.
        + energies                ::Array{Float64,1}             ... List of ... energies.
        + calcAlignment           ::Bool                         ... True, if alignment parameters to be calculated and false otherwise.
        + printBefore  ::Bool                         ... True, if all energies and lines are printed before their evaluation.
        + selectLines             ::Bool                         ... True, if lines are selected individually for the computations.
        + selectedLines           ::Array{Tuple{Int64,Int64},1}  ... List of lines, given by tupels (inital-level, final-level).
    """
    struct Settings
        multipoles                ::Array{EmMultipole}
        gauges                    ::Array{UseGauge}
        photonEnergies            ::Array{Float64,1}
        calcAlignment             ::Bool
        printBefore    ::Bool
        selectLines               ::Bool
        selectedLines             ::Array{Tuple{Int64,Int64},1}
    end

    """
    `CoulombIonization.Settings()`  ... constructor for the default values of Coulomb-excitation line computations.
    """
    function Settings()
        Settings(EmMultipole[], UseGauge[], Float64[], false, false, false, Tuple{Int64,Int64}[])
    end

    # `Base.show(io::IO, settings::CoulombIonization.Settings)`  ... prepares a proper printout of the variable settings::CoulombIonization.Settings.
    function Base.show(io::IO, settings::CoulombIonization.Settings)
        println(io, "multipoles:               $(settings.multipoles)  ")
        println(io, "gauges:                   $(settings.gauges)  ")
        println(io, "photonEnergies:           $(settings.photonEnergies)  ")
        println(io, "calcAlignment:            $(settings.calcAlignment)  ")
        println(io, "printBefore:   $(settings.printBefore)  ")
        println(io, "selectLines:              $(settings.selectLines)  ")
        println(io, "selectedLines:            $(settings.selectedLines)  ")
    end

    """
    `struct  CoulombIonization.Channel`
        ... defines a type for a Coulomb-excitation channel to help characterize a single magnetic substate. !!! This need to adapted !!!

        + multipole      ::EmMultipole          ... Multipole of the photon absorption.
        + gauge          ::EmGauge              ... Gauge for dealing with the (coupled) radiation field.
        + kappa          ::Int64                ... partial-wave of the free electron
        + symmetry       ::LevelSymmetry        ... total angular momentum and parity of the scattering state
        + phase          ::Float64              ... phase of the partial wave
        + amplitude      ::Complex{Float64}     ... Photoionization amplitude associated with the given channel.
   """
    struct  Channel
        multipole        ::EmMultipole
        gauge            ::EmGauge
        kappa            ::Int64
        symmetry         ::LevelSymmetry
        phase            ::Float64
        amplitude        ::Complex{Float64}
    end

    """
    `struct  Line`  ... defines a type for a Coulomb-excitation line that may include the definition of channels.

        + initialLevel   ::Level                  ... initial-(state) level
        + finalLevel     ::Level                  ... final-(state) level
        + crossSection   ::EmProperty             ... Cross section for this photoionization.
        + alignmentA2    ::EmProperty             ... Alignment A_2 parameter.
        + hasChannels    ::Bool                   ... Determines whether the individual (sub-) channels are defined in terms of their
                                                      magnetic substates, etc., or not.
        + channels       ::Array{CoulombIonization.Channel,1}  ... List of CoulombIonization.Channels of this line.
    """
    struct  Line
        initialLevel     ::Level
        finalLevel       ::Level
        crossSection     ::EmProperty
        alignmentA2      ::EmProperty
        hasChannels      ::Bool
        channels         ::Array{CoulombIonization.Channel,1}
    end

    # `Base.show(io::IO, line::CoulombIonization.Line)`  ... prepares a proper printout of the variable line::CoulombIonization.Line.
    function Base.show(io::IO, line::CoulombIonization.Line)
        println(io, "initialLevel:      $(line.initialLevel)  ")
        println(io, "finalLevel:        $(line.finalLevel)  ")
        println(io, "crossSection:      $(line.crossSection)  ")
    end

end # module


import CUDA.CUDNN: cudnnActivationForwardAD
using CUDA.CUDNN: cudnnActivationBackward, scalingParameter, handle
using AutoGrad: AutoGrad, @primitive1, value

@primitive1(
    (cudnnActivationForwardAD(x; activationDesc, alpha, xDesc, beta, yDesc, y),
     _dy, _y),
    ((x, y, dy, dx) = (value(x), value(_y), value(_dy), similar(x));
     if alpha[] != 1; y = y ./ alpha[]; end;
     cudnnActivationBackward(handle(), activationDesc, alpha, yDesc, y, yDesc, dy, xDesc, x, beta, xDesc, dx);
     dx))


colorsets=[[(1,0,0),(0,1,0),(0,0,1),(1/2,0,1/2),(1/4,1/2,1/4),(1,1,1)],[(1,0,0),(0,1,0),(0,0,1),(1/2,1/2,0),(1/2,0,1/2),(0,1/2,1/2),(0.15,0.7,0.15)]]
setcolorset=(game,set::Int=2)->game.colors=colorsets[set] #modify game.color to set a temporary custom color

units=Dict("standard"=>Dict(:name=>"standard", :ir=>2, :pl=>[2])) #change :pl to [1,2,3] to place anywhere

units["tunnel"]=Dict(:name=>"tunnel", :ir=>0, :pl=>[1]) #these by default cannot be placed under/over surrounded territory without permission, such permissions are not hard coded but may rather result in a rejected/modified merge request
units["bridge"]=Dict(:name=>"bridge", :ir=>0, :pl=>[3])
units["queen"]=Dict(:name=>"queen", :canspawn=>true, :graphic=>[(0.5,0.5),(-0.5,0.5),(0,-0.5)])
units["vein"]=Dict(:name=>"vein", :ir=>0, :pl=>[2])
#units["universal"]=Dict(:name=>"universal", :canspawn=>true, :ir=>3, :pl=>[1,2,3],:graphic=>[(0.5,-0.5),(-0.5,-0.5),(0,0.5)]) #this one has some weird scoring issues, sometimes adds score then subtracts it the next move. Anyways not really meant to be generally used since it overrides the need to connect (like playing with only (super)queens), more like a GM unit
#units[:storage] :canstore=>true, :storemax=> #for storing (duh!)

#white unit is a spawn for all colors whereas any unit with color (1,1,1) "is" every color, so a white spawn is a spawn for all and "white" as a unit spec can be removed. White superpowers are being reduced, "units" facilitating multiple color connections can be built with tunnel and bridge units. White now has metaphysical superpowers, perfect storage
#units["white"]=Dict(:harvest=>white_harvest, :name=>"white", :inclusive=>true, :passover=>true, :baselife=>18, :canspawn=>true)
#build white unitsets, like molecules with colored branches
function standard_costfun(game,unit) #costs have been removed, our hive will dispatch all units proposed that create agreeable formations
	distance=nearestwhite(game,unit.loc,unit.loc[3]==2)
	cost=distance*unitparams[2]
	cost*=sum(unitparams[1]) #only if sum>1? How do we create such units? Are smaller units good? Do they take less space?
	return cost
end
function standard_live!(game,unit,lifemap)
	spreadlife!(game,unit,lifemap)
end
function standard_harvest(game,unit,ledger)
	unitharvest(game,unit,ledger)
end
standard_harvest! =standard_harvest #for savefile backwardscompat


import Base: isfinite, isnan, precision, iszero, eps,
    typemin, typemax, floatmin, floatmax,
    sign_mask, exponent_mask, exponent_one, exponent_half,
    significand_mask, round, Int32, Int64,
    +, -, *, /, ^, ==, <, <=, >=, >, !=, inv,
    abs, sqrt, exp, log, log2, log10, sin, cos, tan, asin,
    acos, atan, sinh, cosh, tanh, asinh, acosh, atan,
    exponent,
    bitstring

primitive type BFloat16 <: AbstractFloat 16 end

# Floating point property queries
for f in (:sign_mask, :exponent_mask, :exponent_one,
            :exponent_half, :significand_mask)
    @eval $(f)(::Type{BFloat16}) = UInt16($(f)(Float32) >> 16)
end

iszero(x::BFloat16) = reinterpret(UInt16, x) & ~sign_mask(BFloat16) == 0x0000
isfinite(x::BFloat16) = (reinterpret(UInt16,x) & exponent_mask(BFloat16)) != exponent_mask(BFloat16)
isnan(x::BFloat16) = (reinterpret(UInt16,x) & ~sign_mask(BFloat16)) > exponent_mask(BFloat16)
precision(::Type{BFloat16}) = 8
eps(::Type{BFloat16}) = Base.bitcast(BFloat16, 0x3c00)

round(x::BFloat16, r::RoundingMode{:Up}) = BFloat16(ceil(Float32(x)))
round(x::BFloat16, r::RoundingMode{:Down}) = BFloat16(floor(Float32(x)))
round(x::BFloat16, r::RoundingMode{:Nearest}) = BFloat16(round(Float32(x)))

Int64(x::BFloat16) = Int64(Float32(x))
Int32(x::BFloat16) = Int32(Float32(x))

## floating point traits ##
"""
    InfB16
Positive infinity of type [`BFloat16`](@ref).
"""
const InfB16 = reinterpret(BFloat16, 0x7f80)

"""
    NaNB16
A not-a-number value of type [`BFloat16`](@ref).
"""
const NaNB16 = reinterpret(BFloat16, 0x7fc0)

# More floating point property queries
typemin(::Type{BFloat16}) = -InfB16
typemax(::Type{BFloat16}) = InfB16
floatmax(::Type{BFloat16}) = reinterpret(BFloat16, 0x7f7f)
floatmin(::Type{BFloat16}) = reinterpret(BFloat16, 0x0080)

# Truncation from Float32
Base.uinttype(::Type{BFloat16}) = UInt16
Base.trunc(::Type{BFloat16}, x::Float32) = reinterpret(BFloat16,
        (reinterpret(UInt32, x) >> 16) % UInt16
    )

# Conversion from Float32
function BFloat16(x::Float32)
    isnan(x) && return NaNB16
    # Round to nearest even (matches TensorFlow and our convention for
    # rounding to lower precision floating point types).
    h = reinterpret(UInt32, x)
    h += 0x7fff + ((h >> 16) & 1)
    return reinterpret(BFloat16, (h >> 16) % UInt16)
end

# Conversion from Float64
function BFloat16(x::Float64)
	BFloat16(Float32(x))
end

# Conversion from Integer
function BFloat16(x::Integer)
	convert(BFloat16, convert(Float32, x))
end

# Expansion to Float32
function Base.Float32(x::BFloat16)
    reinterpret(Float32, UInt32(reinterpret(UInt16, x)) << 16)
end

# Expansion to Float64
function Base.Float64(x::BFloat16)
    Float64(Float32(x))
end

# Truncation to integer types
Base.unsafe_trunc(T::Type{<:Integer}, x::BFloat16) = unsafe_trunc(T, Float32(x))

# Basic arithmetic
for f in (:+, :-, :*, :/, :^)
    @eval ($f)(x::BFloat16, y::BFloat16) = BFloat16($(f)(Float32(x), Float32(y)))
end
-(x::BFloat16) = reinterpret(BFloat16, reinterpret(UInt16, x) ⊻ sign_mask(BFloat16))
^(x::BFloat16, y::Integer) = BFloat16(^(Float32(x), y))

for F in (:abs, :sqrt, :exp, :log, :log2, :log10,
          :sin, :cos, :tan, :asin, :acos, :atan,
          :sinh, :cosh, :tanh, :asinh, :acosh, :atanh)
  @eval begin
    $F(x::BFloat16) = BFloat16($F(Float32(x)))
  end
end

const ZeroBFloat16 = BFloat16(0.0f0)
const OneBFloat16 = BFloat16(1.0f0)
Base.zero(::Type{BFloat16}) = ZeroBFloat16
Base.one(::Type{BFloat16}) = OneBFloat16

inv(x::BFloat16) = one(BFloat16) / x

# Floating point comparison
function ==(x::BFloat16, y::BFloat16)
    ix = reinterpret(UInt16, x)
    iy = reinterpret(UInt16, y)
    # NaNs (isnan(x) || isnan(y))
    if (ix|iy)&~sign_mask(BFloat16) > exponent_mask(BFloat16)
        return false
    end
    # Signed zeros
    if (ix|iy)&~sign_mask(BFloat16) == 0
        return true
    end
    return ix == iy
end

for op in (:<, :<=, :>, :>=, :!=)
    @eval ($op)(a::BFloat16, b::BFloat16) = ($op)(Float32(a), Float32(b))
end

Base.widen(::Type{BFloat16}) = Float32
Base.promote_rule(::Type{Float32}, ::Type{BFloat16}) = Float32
Base.promote_rule(::Type{Float64}, ::Type{BFloat16}) = Float64
for t in (Int8, Int16, Int32, Int64, Int128, UInt8, UInt16, UInt32, UInt64, UInt128)
    @eval Base.promote_rule(::Type{BFloat16}, ::Type{$t}) = BFloat16
end

# Wide multiplication
Base.widemul(x::BFloat16, y::BFloat16) = Float32(x) * Float32(y)

# Showing
function Base.show(io::IO, x::BFloat16)
    hastypeinfo = BFloat16 === get(io, :typeinfo, Any)
    if isinf(x)
        print(io, x < 0 ? "-InfB16" : "InfB16")
    elseif isnan(x)
        print(io, "NaNB16")
    else
        hastypeinfo || print(io, "BFloat16(")
        show(IOContext(io, :typeinfo=>Float32), Float32(x))
        hastypeinfo || print(io, ")")
    end
end

# Random
import Random: rand, randn, randexp, AbstractRNG, Sampler
rand(rng::AbstractRNG, ::Sampler{BFloat16}) = convert(BFloat16, rand(rng))
randn(rng::AbstractRNG, ::Type{BFloat16}) = convert(BFloat16, randn(rng))
randexp(rng::AbstractRNG, ::Type{BFloat16}) = convert(BFloat16, randexp(rng))

# Exponent
exponent(x::BFloat16) = exponent(Float32(x))

# Bitstring
bitstring(x::BFloat16) = bitstring(reinterpret(UInt16, x))


### BasicContMuvParameter

type BasicContMuvParameter <: Parameter{Continuous, Multivariate}
  key::Symbol
  index::Integer
  pdf::Union{ContinuousMultivariateDistribution, Void}
  prior::Union{ContinuousMultivariateDistribution, Void}
  setpdf::Union{Function, Void}
  setprior::Union{Function, Void}
  loglikelihood!::Union{Function, Void}
  logprior!::Union{Function, Void}
  logtarget!::Union{Function, Void}
  gradloglikelihood!::Union{Function, Void}
  gradlogprior!::Union{Function, Void}
  gradlogtarget!::Union{Function, Void}
  tensorloglikelihood!::Union{Function, Void}
  tensorlogprior!::Union{Function, Void}
  tensorlogtarget!::Union{Function, Void}
  dtensorloglikelihood!::Union{Function, Void}
  dtensorlogprior!::Union{Function, Void}
  dtensorlogtarget!::Union{Function, Void}
  uptogradlogtarget!::Union{Function, Void}
  uptotensorlogtarget!::Union{Function, Void}
  uptodtensorlogtarget!::Union{Function, Void}
  states::VariableStateVector

  function BasicContMuvParameter(
    key::Symbol,
    index::Integer,
    pdf::Union{ContinuousMultivariateDistribution, Void},
    prior::Union{ContinuousMultivariateDistribution, Void},
    setpdf::Union{Function, Void},
    setprior::Union{Function, Void},
    ll::Union{Function, Void},
    lp::Union{Function, Void},
    lt::Union{Function, Void},
    gll::Union{Function, Void},
    glp::Union{Function, Void},
    glt::Union{Function, Void},
    tll::Union{Function, Void},
    tlp::Union{Function, Void},
    tlt::Union{Function, Void},
    dtll::Union{Function, Void},
    dtlp::Union{Function, Void},
    dtlt::Union{Function, Void},
    uptoglt::Union{Function, Void},
    uptotlt::Union{Function, Void},
    uptodtlt::Union{Function, Void},
    states::VariableStateVector
  )
    args = (setpdf, setprior, ll, lp, lt, gll, glp, glt, tll, tlp, tlt, dtll, dtlp, dtlt, uptoglt, uptotlt, uptodtlt)
    fnames = fieldnames(BasicContMuvParameter)[5:21]

    # Check that all generic functions have correct signature
    for i in 1:17
      if isa(args[i], Function) &&
        isgeneric(args[i]) &&
        !method_exists(args[i], (BasicContMuvParameterState, VariableStateVector))
        error("$(fnames[i]) has wrong signature")
      end
    end

    new(
      key,
      index,
      pdf,
      prior,
      setpdf,
      setprior,
      ll,
      lp,
      lt,
      gll,
      glp,
      glt,
      tll,
      tlp,
      tlt,
      dtll,
      dtlp,
      dtlt,
      uptoglt,
      uptotlt,
      uptodtlt,
      states
    )
  end
end

function BasicContMuvParameter!(
  parameter::BasicContMuvParameter,
  setpdf::Union{Function, Void},
  setprior::Union{Function, Void},
  ll::Union{Function, Void},
  lp::Union{Function, Void},
  lt::Union{Function, Void},
  gll::Union{Function, Void},
  glp::Union{Function, Void},
  glt::Union{Function, Void},
  tll::Union{Function, Void},
  tlp::Union{Function, Void},
  tlt::Union{Function, Void},
  dtll::Union{Function, Void},
  dtlp::Union{Function, Void},
  dtlt::Union{Function, Void},
  uptoglt::Union{Function, Void},
  uptotlt::Union{Function, Void},
  uptodtlt::Union{Function, Void}
)
  args = (setpdf, setprior, ll, lp, lt, gll, glp, glt, tll, tlp, tlt, dtll, dtlp, dtlt, uptoglt, uptotlt, uptodtlt)

  # Define setpdf (i = 1) and setprior (i = 2)
  for (i, setter, distribution) in ((1, :setpdf, :pdf), (2, :setprior, :prior))
    setfield!(
      parameter,
      setter,
      if isa(args[i], Function)
        eval(codegen_setfield(parameter, distribution, args[i]))
      else
        nothing
      end
    )
  end

  # Define loglikelihood! (i = 3) and gradloglikelihood! (i = 6)
  # plfield stands for parameter likelihood-related field respectively
  for (i, plfield) in ((3, :loglikelihood!), (6, :gradloglikelihood!))
    setfield!(
      parameter,
      plfield,
      if isa(args[i], Function)
        eval(codegen_closure(parameter, args[i]))
      else
        nothing
      end
    )
  end

  # Define logprior! (i = 4) and gradlogprior! (i = 7)
  # ppfield and spfield stand for parameter prior-related field and state prior-related field repsectively
  for (i , ppfield, spfield, f) in ((4, :logprior!, :logprior, logpdf), (7, :gradlogprior!, :gradlogprior, gradlogpdf))
    setfield!(
    parameter,
      ppfield,
      if isa(args[i], Function)
        eval(codegen_closure(parameter, args[i]))
      else
        if (
            isa(parameter.prior, ContinuousMultivariateDistribution) &&
            method_exists(f, (typeof(parameter.prior), Vector{eltype(parameter.prior)}))
          ) ||
          isa(args[2], Function)
          eval(codegen_target_closure_via_distribution(parameter, :prior, f, spfield))
        else
          nothing
        end
      end
    )
  end

  # Define logtarget! (i = 5) and gradlogtarget! (i = 8)
  # ptfield, plfield and ppfield stand for parameter target, likelihood and prior-related field respectively
  # stfield, slfield and spfield stand for state target, likelihood and prior-related field respectively
  for (i , ptfield, plfield, ppfield, stfield, slfield, spfield, f) in (
    (5, :logtarget!, :loglikelihood!, :logprior!, :logtarget, :loglikelihood, :logprior, logpdf),
    (8, :gradlogtarget!, :gradloglikelihood!, :gradlogprior!, :gradlogtarget, :gradloglikelihood, :gradlogprior, gradlogpdf)
  )
    setfield!(
      parameter,
      ptfield,
      if isa(args[i], Function)
        eval(codegen_closure(parameter, args[i]))
      else
        if isa(args[i-2], Function) && isa(getfield(parameter, ppfield), Function)
          eval(codegen_sumtarget_closure(parameter, plfield, ppfield, stfield, slfield, spfield))
        elseif (
            isa(parameter.pdf, ContinuousMultivariateDistribution) &&
            method_exists(f, (typeof(parameter.pdf), Vector{eltype(parameter.pdf)}))
          ) ||
          isa(args[1], Function)
          eval(codegen_target_closure_via_distribution(parameter, :pdf, f, stfield))
        else
          nothing
        end
      end
    )
  end

  # Define tensorloglikelihood! (i = 9) and dtensorloglikelihood! (i = 12)
  # plfield stands for parameter likelihood-related field respectively
  for (i, plfield) in ((9, :tensorloglikelihood!), (12, :dtensorloglikelihood!))
    setfield!(
      parameter,
      plfield,
      if isa(args[i], Function)
        eval(codegen_closure(parameter, args[i]))
      else
        nothing
      end
    )
  end

  # Define tensorlogprior! (i = 10) and dtensorlogprior! (i = 13)
  # ppfield stands for parameter prior-related field respectively
  for (i, ppfield) in ((10, :tensorlogprior!), (13, :dtensorlogprior!))
    setfield!(
      parameter,
      ppfield,
      if isa(args[i], Function)
        eval(codegen_closure(parameter, args[i]))
      else
        nothing
      end
    )
  end

  # Define tensorlogtarget! (i = 11) and dtensorlogtarget! (i = 14)
  for (i , ptfield, plfield, ppfield, stfield, slfield, spfield) in (
    (
      11,
      :tensorlogtarget!, :tensorloglikelihood!, :tensorlogprior!,
      :tensorlogtarget, :tensorloglikelihood, :tensorlogprior
    ),
    (
      14,
      :dtensorlogtarget!, :dtensorloglikelihood!, :dtensorlogprior!,
      :dtensorlogtarget, :dtensorloglikelihood, :dtensorlogprior
    )
  )
    setfield!(
      parameter,
      ptfield,
      if isa(args[i], Function)
        eval(codegen_closure(parameter, args[i]))
      else
        if isa(args[i-2], Function) && isa(args[i-1], Function)
          eval(codegen_sumtarget_closure(parameter, plfield, ppfield, stfield, slfield, spfield))
        else
          nothing
        end
      end
    )
  end

  # Define uptogradlogtarget!
  setfield!(
    parameter,
    :uptogradlogtarget!,
    if isa(args[15], Function)
      eval(codegen_closure(parameter, args[15]))
    else
      if isa(parameter.logtarget!, Function) && isa(parameter.gradlogtarget!, Function)
        eval(codegen_uptotarget_closures(parameter, [:logtarget!, :gradlogtarget!]))
      else
        nothing
      end
    end
  )

  # Define uptotensorlogtarget!
  setfield!(
    parameter,
    :uptotensorlogtarget!,
    if isa(args[16], Function)
      eval(codegen_closure(parameter, args[16]))
    else
      if isa(parameter.logtarget!, Function) &&
        isa(parameter.gradlogtarget!, Function) &&
        isa(parameter.tensorlogtarget!, Function)
        eval(codegen_uptotarget_closures(parameter, [:logtarget!, :gradlogtarget!, :tensorlogtarget!]))
      else
        nothing
      end
    end
  )

  # Define uptodtensorlogtarget!
  setfield!(
    parameter,
    :uptodtensorlogtarget!,
    if isa(args[17], Function)
      eval(codegen_closure(parameter, args[17]))
    else
      if isa(parameter.logtarget!, Function) &&
        isa(parameter.gradlogtarget!, Function) &&
        isa(parameter.tensorlogtarget!, Function) &&
        isa(parameter.dtensorlogtarget!, Function)
        eval(codegen_uptotarget_closures(parameter, [:logtarget!, :gradlogtarget!, :tensorlogtarget!, :dtensorlogtarget!]))
      else
        nothing
      end
    end
  )
end

BasicContMuvParameter(key::Symbol, index::Integer=0; signature::Symbol=:high, args...) =
  BasicContMuvParameter(key, Val{signature}, index; args...)

function BasicContMuvParameter(
  key::Symbol,
  ::Type{Val{:low}},
  index::Integer=0;
  pdf::Union{ContinuousMultivariateDistribution, Void}=nothing,
  prior::Union{ContinuousMultivariateDistribution, Void}=nothing,
  setpdf::Union{Function, Void}=nothing,
  setprior::Union{Function, Void}=nothing,
  loglikelihood::Union{Function, Void}=nothing,
  logprior::Union{Function, Void}=nothing,
  logtarget::Union{Function, Void}=nothing,
  gradloglikelihood::Union{Function, Void}=nothing,
  gradlogprior::Union{Function, Void}=nothing,
  gradlogtarget::Union{Function, Void}=nothing,
  tensorloglikelihood::Union{Function, Void}=nothing,
  tensorlogprior::Union{Function, Void}=nothing,
  tensorlogtarget::Union{Function, Void}=nothing,
  dtensorloglikelihood::Union{Function, Void}=nothing,
  dtensorlogprior::Union{Function, Void}=nothing,
  dtensorlogtarget::Union{Function, Void}=nothing,
  uptogradlogtarget::Union{Function, Void}=nothing,
  uptotensorlogtarget::Union{Function, Void}=nothing,
  uptodtensorlogtarget::Union{Function, Void}=nothing,
  states::VariableStateVector=VariableState[]
)
  parameter = BasicContMuvParameter(key, index, pdf, prior, fill(nothing, 17)..., states)

  BasicContMuvParameter!(
    parameter,
    setpdf,
    setprior,
    loglikelihood,
    logprior,
    logtarget,
    gradloglikelihood,
    gradlogprior,
    gradlogtarget,
    tensorloglikelihood,
    tensorlogprior,
    tensorlogtarget,
    dtensorloglikelihood,
    dtensorlogprior,
    dtensorlogtarget,
    uptogradlogtarget,
    uptotensorlogtarget,
    uptodtensorlogtarget
  )

  parameter
end

function BasicContMuvParameter(
  key::Symbol,
  ::Type{Val{:high}},
  index::Integer=0;
  pdf::Union{ContinuousMultivariateDistribution, Void}=nothing,
  prior::Union{ContinuousMultivariateDistribution, Void}=nothing,
  setpdf::Union{Function, Void}=nothing,
  setprior::Union{Function, Void}=nothing,
  loglikelihood::Union{Function, Expr, Void}=nothing,
  logprior::Union{Function, Expr, Void}=nothing,
  logtarget::Union{Function, Expr, Void}=nothing,
  gradloglikelihood::Union{Function, Void}=nothing,
  gradlogprior::Union{Function, Void}=nothing,
  gradlogtarget::Union{Function, Void}=nothing,
  tensorloglikelihood::Union{Function, Void}=nothing,
  tensorlogprior::Union{Function, Void}=nothing,
  tensorlogtarget::Union{Function, Void}=nothing,
  dtensorloglikelihood::Union{Function, Void}=nothing,
  dtensorlogprior::Union{Function, Void}=nothing,
  dtensorlogtarget::Union{Function, Void}=nothing,
  uptogradlogtarget::Union{Function, Void}=nothing,
  uptotensorlogtarget::Union{Function, Void}=nothing,
  uptodtensorlogtarget::Union{Function, Void}=nothing,
  states::VariableStateVector=VariableState[],
  nkeys::Integer=0,
  vfarg::Bool=false,
  autodiff::Symbol=:none,
  order::Integer=1,
  chunksize::Integer=0,
  init::Vector=fill(Any[], 3)
)
  inargs = (
    setpdf,
    setprior,
    loglikelihood,
    logprior,
    logtarget,
    gradloglikelihood,
    gradlogprior,
    gradlogtarget,
    tensorloglikelihood,
    tensorlogprior,
    tensorlogtarget,
    dtensorloglikelihood,
    dtensorlogprior,
    dtensorlogtarget,
    uptogradlogtarget,
    uptotensorlogtarget,
    uptodtensorlogtarget
  )

  fnames = Array(Any, 17)
  fnames[1:2] = fill(Symbol[], 2)
  fnames[3:14] = [Symbol[f] for f in fieldnames(BasicContMuvParameterState)[2:13]]
  for i in 1:3
    fnames[14+i] = Symbol[fnames[j][1] for j in 5:3:(5+i*3)]
  end

  for i in 3:5
    if isa(inargs[i], Expr) && autodiff != :reverse
      error("The only case $(fnames[i][1]) can be an expression is when used in conjunction with reverse mode autodiff")
    end
  end

  if nkeys > 0
    if (autodiff == :forward || autodiff == :reverse) && vfarg
      error("In the case of autodiff, if nkeys is not 0, then vfarg must be false")
    end
  elseif nkeys < 0
    "nkeys must be non-negative, got $nkeys"
  end

  if !in(autodiff, (:none, :forward, :reverse))
    error("autodiff must be :nore or :forward or :reverse, got $autodiff")
  end

  if order < 0 || order > 2
    error("Derivative order must be 0, 1 or 2, got $order")
  elseif autodiff != :reverse && order == 0
    error("Zero order can be used only with reverse mode autodiff")
  end

  @assert chunksize >= 0 "chunksize must be non-negative, got $chunksize"

  initarg = Array(Any, 3)
  initlen = length(init)

  if initlen == 1 || initlen == 2
    if autodiff != :reverse
      @assert all(isempty, init) "init option is used only for reverse mode autodiff"
    end

    for i in 1:3
      initarg[i] = (inargs[i+2] != nothing) ? init : Any[]
    end
  elseif initlen == 3
    if autodiff != :reverse
      @assert all(isempty, init) "init option is used only for reverse mode autodiff"
    end

    initarg = init
  else
    error("init must be a vector of length 1, 2 or 3, got vector of length $initlen")
  end

  parameter = BasicContMuvParameter(key, index, pdf, prior, fill(nothing, 17)..., states)

  outargs = Union{Function, Void}[nothing for i in 1:17]

  for i in 1:17
    if isa(inargs[i], Function)
      outargs[i] = eval(
        codegen_lowlevel_variable_method(inargs[i], :BasicContMuvParameterState, true, fnames[i], nkeys, vfarg)
      )
    end
  end

  if autodiff == :forward
    fadclosure = Array(Union{Function, Void}, 3)
    for i in 3:5
      fadclosure[i-2] =
        if isa(inargs[i], Function)
          nkeys == 0 ? inargs[i] : eval(codegen_internal_autodiff_closure(parameter, inargs[i], nkeys))
        else
          nothing
        end
    end

    for i in 6:8
      if !isa(inargs[i], Function) && isa(inargs[i-3], Function)
        outargs[i] = eval(codegen_lowlevel_variable_method(
          eval(codegen_forward_autodiff_function(Val{:gradient}, fadclosure[i-5], chunksize)),
          :BasicContMuvParameterState,
          true,
          fnames[i],
          0
        ))
      end
    end

    if !isa(inargs[15], Function) && isa(inargs[5], Function)
      outargs[15] = eval(codegen_lowlevel_variable_method(
        eval(codegen_forward_autodiff_uptofunction(Val{:gradient}, fadclosure[3], chunksize)),
        :BasicContMuvParameterState,
        true,
        fnames[15],
        0
      ))
    end

    if order >= 2
      for i in 9:11
        if !isa(inargs[i], Function) && isa(inargs[i-6], Function)
          outargs[i] = eval(codegen_lowlevel_variable_method(
            eval(codegen_forward_autodiff_target(:hessian, fadclosure[i-8], chunksize)),
            :BasicContMuvParameterState,
            true,
            fnames[i],
            0
          ))
        end
      end

      if !isa(inargs[16], Function) && isa(inargs[5], Function)
        outargs[16] = eval(codegen_lowlevel_variable_method(
          eval(codegen_forward_autodiff_uptotarget(:hessian, fadclosure[3], chunksize)),
          :BasicContMuvParameterState,
          true,
          fnames[16],
          0
        ))
      end
    end
  elseif autodiff == :reverse
    local f::Function

    for i in 3:5
      if isa(inargs[i], Expr)
        if nkeys == 0
          f = eval(codegen_reverse_autodiff_function(inargs[i], :Vector, initarg[i-2][1], 0, false))
        else
          f = eval(codegen_reverse_autodiff_function(inargs[i], :Vector, initarg[i-2], 0, false))
          f = eval(codegen_internal_autodiff_closure(parameter, f, nkeys))
        end

        outargs[i] = eval(codegen_lowlevel_variable_method(f, :BasicContMuvParameterState, true, fnames[i], 0))
      end
    end

    for i in 6:8
      if !isa(inargs[i], Function)
        if isa(inargs[i-3], Function)
          if nkeys == 0
            f = ReverseDiffSource.rdiff(inargs[i-3], (initarg[i-5][1][2],), order=1, allorders=false)
          else
            f = ReverseDiffSource.rdiff(
              inargs[i-3], (initarg[i-5][1][2], initarg[i-5][2][2]), ignore=[initarg[i-5][2][1]], order=1, allorders=false
            )
            f = eval(codegen_internal_autodiff_closure(parameter, f, nkeys))
          end

          outargs[i] = eval(codegen_lowlevel_variable_method(f, :BasicContMuvParameterState, true, fnames[i], 0))
        elseif isa(inargs[i-3], Expr)
          if nkeys == 0
            f = eval(codegen_reverse_autodiff_function(inargs[i-3], :Vector, initarg[i-5][1], 1, false))
          else
            f = eval(codegen_reverse_autodiff_function(inargs[i-3], :Vector, initarg[i-5], 1, false))
            f = eval(codegen_internal_autodiff_closure(parameter, f, nkeys))
          end

          outargs[i] = eval(codegen_lowlevel_variable_method(f, :BasicContMuvParameterState, true, fnames[i], 0))
        end
      end
    end

    if !isa(inargs[15], Function)
      if isa(inargs[5], Function)
        if nkeys == 0
          f = ReverseDiffSource.rdiff(inargs[5], (initarg[3][1][2],), order=1, allorders=true)
        else
          f = ReverseDiffSource.rdiff(
            inargs[5], (initarg[3][1][2], initarg[3][2][2]), ignore=[initarg[3][2][1]], order=1, allorders=true
          )
          f = eval(codegen_internal_autodiff_closure(parameter, f, nkeys))
        end

        outargs[15] = eval(codegen_lowlevel_variable_method(f, :BasicContMuvParameterState, true, fnames[15], 0))
      elseif isa(inargs[5], Expr)
        if nkeys == 0
          f = eval(codegen_reverse_autodiff_function(inargs[5], :Vector, initarg[3][1], 1, true))
        else
          f = eval(codegen_reverse_autodiff_function(inargs[5], :Vector, initarg[3], 1, true))
          f = eval(codegen_internal_autodiff_closure(parameter, f, nkeys))
        end

        outargs[15] = eval(codegen_lowlevel_variable_method(f, :BasicContMuvParameterState, true, fnames[15], 0))
      end
    end

    if order >= 2
      for i in 9:11
        if !isa(inargs[i], Function)
          if isa(inargs[i-6], Function) || isa(inargs[i-6], Expr)
            if nkeys == 0
              f = eval(codegen_reverse_autodiff_target(:hessian, inargs[i-6], :Vector, initarg[i-8][1]))
            else
              f = eval(codegen_reverse_autodiff_target(:hessian, inargs[i-6], :Vector, initarg[i-8]))
              f = eval(codegen_internal_autodiff_closure(parameter, f, nkeys))
            end

            outargs[i] = eval(codegen_lowlevel_variable_method(f, :BasicContMuvParameterState, true, fnames[i], 0))
          end
        end
      end

      if !isa(inargs[16], Function)
        if isa(inargs[5], Function) || isa(inargs[5], Expr)
          if nkeys == 0
            f = eval(codegen_reverse_autodiff_uptotarget(:hessian, inargs[5], :Vector, initarg[3][1]))
          else
            f = eval(codegen_reverse_autodiff_uptotarget(:hessian, inargs[5], :Vector, initarg[3]))
            f = eval(codegen_internal_autodiff_closure(parameter, f, nkeys))
          end

          outargs[16] = eval(codegen_lowlevel_variable_method(f, :BasicContMuvParameterState, true, fnames[16], 0))
        end
      end
    end
  end

  BasicContMuvParameter!(parameter, outargs...)

  parameter
end

function codegen_internal_autodiff_closure(parameter::BasicContMuvParameter, f::Function, nkeys::Integer)
  fstatesarg = [Expr(:ref, :Any, [:($(parameter).states[$i].value) for i in 1:nkeys]...)]

  @gensym internal_forward_autodiff_closure

  quote
    function $internal_forward_autodiff_closure(_x::Vector)
      $(f)(_x, $(fstatesarg...))
    end
  end
end

value_support(::Type{BasicContMuvParameter}) = Continuous
value_support(::BasicContMuvParameter) = Continuous

variate_form(::Type{BasicContMuvParameter}) = Multivariate
variate_form(::BasicContMuvParameter) = Multivariate

default_state_type(::BasicContMuvParameter) = BasicContMuvParameterState

default_state{N<:Real}(variable::BasicContMuvParameter, value::Vector{N}, outopts::Dict) =
  BasicContMuvParameterState(
    value,
    [getfield(variable, fieldnames(BasicContMuvParameter)[i]) == nothing ? false : true for i in 10:18],
    (haskey(outopts, :diagnostics) && in(:accept, outopts[:diagnostics])) ? [:accept] : Symbol[]
  )

Base.show(io::IO, ::Type{BasicContMuvParameter}) = print(io, "BasicContMuvParameter")
Base.writemime(io::IO, ::MIME"text/plain", t::Type{BasicContMuvParameter}) = show(io, t)


using LinearAlgebra.BLAS
using OpenBLASHighCoreCount
using OpenBLASHighCoreCount_jll
using Test

@testset "OpenBLASHighCoreCount test suite" begin
    libs = BLAS.get_config().loaded_libs
    idx = findfirst(l -> l.libname == OpenBLASHighCoreCount_jll.libopenblas_path, libs)
    @test idx !== nothing
    @test libs[idx].interface === :ilp64
    BLAS.set_num_threads(128)
    @test BLAS.get_num_threads() == 128
end


"""
KhTrack(args...)  -> PyPlot Plot

Return a plot Normalized Flow Capacity Kh logs in a single track.

 The next table show the list of variables allowed:

 The next table show the list of variables allowed:
|PropertyName|Args|Default|Input|Description
|---|---|---|---|---|
|Depth|Mandatory| -- | Array{Number,1}| 1D Array of Depths|
|Kh|Mandatory| -- | Array{Number,1}|1D Array of Kh Values|
|DepthFrom|Optional| minimum(Depth) | Number |Set the Depth upper limit to plot|
|DepthTo|Optional| maximum(Depth) | Number |  Set the Depth lower limit to plot|
|Dtick|Optional|true|Dtick=Bool|Set the Depth ticks On/Off|
"""
function KhTrack(Depth,Kh;DepthFrom=minimum(Depth),DepthTo=maximum(Depth),Dtick=false)
    khax=gca()
    khax.plot(Kh,Depth,linewidth=2,color="black")
    khax.set_xlim([0,1])
    khax.set_ylim([DepthTo,DepthFrom])
    khax.set_xlabel("Norm. Flow Capacity")
    khax.xaxis.tick_top()
    khax.xaxis.set_label_position("top")
    setp(khax.get_yticklabels(),visible=Dtick)
    khax.set_yticks(range(DepthFrom, stop=DepthTo, length=51),minor=true)
    khax.set_yticks(range(DepthFrom, stop=DepthTo, length=11))
end


#=
This file contains functions taken and modified from Julia/base/show.jl
license of show.jl:
Copyright (c) 2009-2016: Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors:
https://github.com/JuliaLang/julia/contributors
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
=#

import Base: indent_width, uni_ops, expr_infix_wide
import Base: all_ops, expr_calls, show_block
import Base: show_list, show_enclosed_list, operator_precedence
import Base: is_quoted, is_expr, expr_infix_any
import Base: show_call, show_unquoted, show

import Sugar: ASTIO, LazyMethod, typename, functionname, _typename, show_name
import Sugar: supports_overloading, show_type, show_function, is_native_type, isintrinsic

@compat abstract type CIO <: ASTIO end
immutable EmptyCIO <: CIO
end

immutable EmptyStruct
    # Emtpy structs are not supported in OpenCL, which is why we emit a struct
    # with one floating point field
    x::Int32
    EmptyStruct() = new()
end

# helper function to fake a return type to type inference for intrinsic function stabs
@noinline function ret{T}(::Type{T})::T
    unsafe_load(Ptr{T}(C_NULL))
end
# Number types
# Abstract types
# for now we use Int, more accurate would be Int32. But to make things simpler
# we rewrite Int to Int32 implicitely like this!
const int = Int32
# same goes for float
const float = Float32
const uint = UInt32
const uchar = UInt8

const ints = (UInt64, UInt32, UInt8, Int64, Int32, Int8)
const floats = (Float32, Float64)
const numbers = (ints..., floats..., Bool)

const Ints = Union{ints...}
const Floats = Union{floats...}
const Numbers = Union{numbers...}

const vector_lengths = (2, 3, 4, 8, 16)
# Maybe we should remove this and leave it to `is_fixedsize_array`, which is extensible
_vecs = []
for i in vector_lengths, T in numbers
    T == Bool && continue
    push!(_vecs, NTuple{i, T})
    push!(_vecs, SVector{i, T})
end
const vecs = (_vecs...)
const Vecs = Union{vecs...}

##################################################
# Intrinsics that don't have a julia counterpart:

pow{T <: Numbers}(a::T, b::T) = a ^ b
cl_pow{T1, T2}(a::T1, b::T2) = pow(promote(a, b)...)
cl_pow{T <: Numbers}(a::T, b::T) = pow(a, b)

"""
smoothstep performs smooth Hermite interpolation between 0 and 1 when edge0 < x < edge1. This is useful in cases where a threshold function with a smooth transition is desired. smoothstep is equivalent to:
```
    t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0);
    return t * t * (3.0 - 2.0 * t);
```
Results are undefined if edge0 ≥ edge1.
"""
function smoothstep{T}(edge0, edge1, x::T)
    t = clamp.((x .- edge0) ./ (edge1 .- edge0), T(0), T(1))
    return t * t * (T(3) - T(2) * t)
end

"""
mix performs a linear interpolation between x and y using a to weight between them. The return value is computed as
`x .* (T(1) .- a) .+ y .* a`
"""
mix{T}(x, y, a::T) = x .* (T(1) .- a) .+ y .* a

fract(x) = x - floor(x)
fabs(x::AbstractFloat) = abs(x)

#######################################
# shared common intrinsic functions

const functions = (
    +, -, *, /, ^, <=, .<=, !, <, >, ==, !=, |, &, %,
    <<, >>,
    sqrt, fract, log,
    round, floor, ceil, trunc,
    sin, sinpi, sinh, asin, asinh,
    cos, cospi, cosh, acos, acosh,
    tan, tanh, atan, atanh, atan2,# atanpi, atan2pi, <- julia doesnt have those?!
    max, min,
    abs, pow, normalize, cross, dot, smoothstep, mix,
    exp2, exp10, expm1,
    log, log2, log10, log1p,
    length, clamp, fma, fabs, isinf, isnan, sign,
    cbrt, copysign, signbit
    # fast math
)

function fixed_array_length(T)
    N = if T <: Tuple
        length(T.parameters)
    else
        length(T)
    end
end
is_ntuple(x) = false
is_ntuple{N, T}(x::Type{NTuple{N, T}}) = true

const vector_lengths = (2, 3, 4, 8, 16)

# This IO thing gets annoying! TODO, remove IO as dispatch object and use exclusively LazyMethods

is_fixedsize_array{T}(::Type{T}) = is_fixedsize_array(nothing, T)
function is_fixedsize_array{T}(m, ::Type{T})
    (T <: StaticVector || is_ntuple(T)) &&
    fixed_array_length(T) in vector_lengths &&
    eltype(T) <: Numbers &&
    eltype(T) != Bool
end

# Functionality to remove unsupported characters from the source
global replace_unsupported, empty_replace_cache!
let _unsupported_id = 0
    const unsupported_replace_dict = Dict{Char, String}()
    function empty_replace_cache!()
        _unsupported_id = 0
        empty!(unsupported_replace_dict)
        return
    end
    """
    Creates a unique replacement for some character
    """
    function replace_unsupported(char::Char)
        get!(unsupported_replace_dict, char) do
            _unsupported_id += 1
            string(_unsupported_id)
        end
    end
end

is_supported_char(io::IO, char) = true
function is_supported_char(io::CIO, char)
    # Lets just assume for simplicity, that only ascii non operators are supported
    # in a name
    isascii(char) &&
    !Base.isoperator(Symbol(char)) &&
    !(char in ('.', '#', '(', ')', ',', '{', '}', ' '))  # some ascii codes are not allowed
end

"""
Makes sure a symbol is well formed in target source
"""
function symbol_hygiene(io::IO, sym)
    # TODO figure out what other things are not allowed
    # TODO startswith gl_, but allow variables that are actually valid inbuilds
    res_io = IOBuffer()
    for (i, char) in enumerate(string(sym))
        res = if is_supported_char(io, char)
            print(res_io, char)
        else
            i == 1 && print(res_io, 'x') # can't start with number
            print(res_io, replace_unsupported(char)) # get a
        end
    end
    String(take!(res_io))
end

typename(io::CIO, x) = Symbol(symbol_hygiene(io, _typename(io, x)))

_typename(io::IO, T::QuoteNode) = _typename(io, T.value)
julia_name(x::Type{Type{T}}) where T = string(T)

function _typename(io::IO, x)
    str = if isa(x, Expr) && x.head == :curly
        string(x, "_", join(x.args, "_"))
    elseif isa(x, Symbol)
        string(x)
    elseif isa(x, UnionAll)
        string(x)
    elseif isa(x, DataType)
        T = x
        if T <: Tuple{X} where X <: Numbers
            # We map Tuple{1, X} to the intrinsic types of it for better performance (usually this would need to be a struct)
            # TODO benchmark if this terrible idea is worth it
            typename(io, eltype(T))
        elseif T <: Type
            # make names unique when it was a type of Type{X}
            string(T)
        elseif is_fixedsize_array(io, T)
            Sugar.vecname(io, T)
        elseif T <: Tuple
            str = if isempty(T.parameters)
                "EmptyTuple_"
            elseif T == Tuple
                "EmptyTuple"
            else
                str = "Tuple_"
                tstr = map(T.parameters) do t
                    if t <: Tuple{X} where X
                        string("Tupl_", typename(io, t))
                    else
                        typename(io, t)
                    end
                end
                str *= join(tstr, "_")
            end
            str
        else
            str = string(T.name.name)
            if !isempty(T.parameters)
                tstr = map(T.parameters) do t
                    if isa(t, DataType)
                        if t <: Tuple{X} where X
                            string("Tupl_", typename(io, t))
                        else
                            typename(io, t)
                        end
                    else
                        string(t)
                    end
                end
                str *= string("_", join(tstr, "_"))
            end
            str
        end
    else
        error("Not transpilable: $x")
    end
    return str
end

_typename(io::CIO, x::Union{AbstractString, Symbol}) = x

_typename{T <: Numbers}(io::IO, x::Type{Tuple{T}}) = _typename(io, T)
_typename(io::IO, x::Type{Float64}) = "double"
_typename(io::IO, x::Type{Float32}) = "float"
_typename(io::IO, x::Type{Int64}) = "long"
_typename(io::IO, x::Type{Int32}) = "int"
_typename(io::IO, x::Type{UInt32}) = "uint"
_typename(io::IO, x::Type{UInt64}) = "ulong"
_typename(io::IO, x::Type{UInt8}) = "uchar"
_typename(io::IO, x::Type{Bool}) = "JLBool"
_typename{T}(io::IO, x::Type{Ptr{T}}) = "$(typename(io, T)) *"

_typename{F <: Function}(io::CIO, f::F) = _typename(io, F.name.mt.name)
_typename{F <: Function}(io::CIO, f::Type{F}) = string(F)

global signature_hash, empty_hash_dict!
let hash_dict = Dict{Any, Int}(), counter = 0
    function empty_hash_dict!()
        empty!(hash_dict)
        counter = 0
        return
    end
    """
    Returns a unique ID for a type signature, which is as small as possible!
    """
    function signature_hash(types)
        get!(hash_dict, Sugar.to_tuple(types)) do
            counter += 1
            counter
        end
    end
end

function Sugar.functionname(io::CIO, method::Type{T}) where T
    string('(', typename(io, T), ')')
end
function Sugar.functionname(io::CIO, method::LazyMethod)
    func = if istype(method)
        # This should only happen, if the function is actually a type constructor
        method.signature
    else
        Sugar.getfunction(method)
    end
    if func == %
        # % seems to be an operator that is printed as rem (?!)
        # TODO, are there more? This is only important for operators that are also intrinsics
        return :(%)
    end
    if func == cli.intrinsic_select
        return :select
    end
    f_sym = if isa(func, Type)
        functionname(io, func)
    else
        Base.function_name(func)
    end
    if Sugar.isintrinsic(method)
        return f_sym # intrinsic operators don't need hygiene!
    end
    str = if isfunction(method) && !supports_overloading(io)
        string(f_sym, '_', signature_hash(method.signature[2]))
    elseif istype(method)
        return string(f_sym)
    end
    if isa(io, Sugar.ASTIO)
        symbol_hygiene(io, str)
    else
        str
    end
end

function show_name(io::CIO, x)
    print(io, symbol_hygiene(io, x))
end
function show_name(io::CIO, x::Union{Slot, SSAValue})
    name = Sugar.slotname(io.method, x)
    show_name(io, name)
end

function show_type(io::CIO, x)
    print(io, typename(io, x))
end

function Base.show_unquoted(io::CIO, slot::Slot, ::Int, ::Int)
    show_name(io, slot)
end

function c_fieldname(m::LazyMethod, T, i::Integer)
    str = if isleaftype(T)
        name = if is_fixedsize_array(m, T)
            fixed_size_array_fieldname(m, T, i)
        else
            Base.fieldname(T, i)
        end
        if isa(name, Integer) # for types without fieldnames (Tuple)
            "field$name"
        else
            symbol_hygiene(EmptyCIO(), name)
        end
    else
        error("Found abstract type: $T")
    end
    Symbol(str)
end

function typed_type_fields(T)
    nf = nfields(T)
    fields = []
    if nf == 0 # structs can't be empty
        # we use bool as a short placeholder type.
        # TODO, are there cases where bool is no good?
        push!(fields, :(emtpy::Float32))
    else
        for i in 1:nf
            FT = fieldtype(T, i)
            tname = typename(EmptyCIO(), FT)
            fname = c_fieldname(T, i)
            push!(fields, :($fname::$tname))
        end
    end
    fields
end
Sugar.show_comment(io::CIO, comment) = println(io, "// ", comment)

show_linenumber(io::CIO, line) = show_comment(io, " line $line:")
show_linenumber(io::CIO, line, file) = show_comment(io, "$file $line $line")

function show_unquoted(io::CIO, x::Type, ::Int, ::Int)
    print(io, "TYP_INST_", typename(io, Type{x}))
end
function show_unquoted(io::CIO, sym::Symbol, ::Int, ::Int)
    print(io, Symbol(symbol_hygiene(io, sym)))
end
function show_unquoted(io::CIO, ssa::SSAValue, ::Int, ::Int)
    print(io, Sugar.ssavalue_name(ssa))
end
function show_unquoted(io::CIO, f::F, ::Int, ::Int) where F <: Sugar.AllFuncs
    print(io, "FUNC_INST_", typename(io, F))
end

const expr_calls_extended = merge(expr_calls, Dict(:constructor => ('{', '}')))
# show a normal (non-operator) function call, e.g. f(x,y) or A[z]
function show_call(io::CIO, head, func, func_args, indent)
    if head == :curly && Sugar.expr_type(func) <: Type# typeconstructors
        print(io, "TYP_INST_") # use the global constant instance of the type
        show_unquoted(io, func, indent)
        return
    end
    op, cl = expr_calls_extended[head]
    # print(io, '(')
    if head == :ref
        FT = Sugar.expr_type(func)
        if is_fixedsize_array(FT) && length(func_args) == 1
            # Special case for small vectors an an integer variable as index
            IDXT = Sugar.expr_type(func_args[1])
            if IDXT <: Integer && !isa(func_args[1], Integer)
                ET = eltype(FT)
                print(io, "(($(typename(io, ET))*)&") # convert to pointer
                show_unquoted(io, func, indent)
                print(io, ")[")
                show_unquoted(io, func_args[1])
                print(io, "]")
                return
            end
        end
        print(io, '(')
        show_unquoted(io, func, indent)
        print(io, ')')
        if FT <: Tuple{T} where T <: cli.Numbers
            # we Tuple{<: Numbers} is treated as scalar, so we don't print the index expression
            return
        end
    else
        print(io, functionname(io, func))
    end
    if head == :(.)
        print(io, '.')
    end
    addition_tracked_object = []
    m = io.method
    for elem in func_args
        T = Sugar.expr_type(m, elem)
        contains, fields = Sugar.contains_tracked_type(m, T)
        if contains && haskey(m.cache, :tracked_types)
            fields = m.cache[:tracked_types][elem]
            for field in fields
                push!(addition_tracked_object, last(field))
            end
        end
    end
    show_enclosed_list(io, op, vcat(func_args, addition_tracked_object), ", ", cl, indent)
    return
end

# show a block, e g if/for/etc
function show_block(io::CIO, head, args::Vector, body, indent::Int)
    if isempty(args) && isa(body, Expr) && isempty(body.args)
        return # everything empty, let's not make a fool of ourselves and print something
    end
    if isempty(args)
        print(io, head, '{')
    else
        print(io, head, '(')
        show_list(io, args, ", ", indent)
        print(io, "){")
    end
    ind = head === :module || head === :baremodule ? indent : indent + indent_width
    exs = (is_expr(body, :block) || is_expr(body, :body)) ? body.args : Any[body]
    for (i, ex) in enumerate(exs)
        sep = i == 1 ? "" : ";"
        print(io, sep, '\n', " "^ind)
        show_unquoted(io, ex, ind, -1)
    end
    print(io, ";\n", " "^indent)
end

function show_unquoted(io::CIO, ex::Expr, indent::Int, prec::Int)
    line_number = 0 # TODO include line numbers
    head, args, nargs = ex.head, ex.args, length(ex.args)
    # dot (i.e. "x.y"), but not compact broadcast exps
    if head == :(.) && !is_expr(args[2], :tuple)
        show_unquoted(io, args[1], indent + indent_width)
        print(io, '.')
        if is_quoted(args[2])
            show_unquoted(io, unquoted(args[2]), indent + indent_width)
        else
            print(io, '(')
            show_unquoted(io, args[2], indent + indent_width)
            print(io, ')')
        end

    # variable declaration TODO might this occure in normal code?
    elseif head == :(::) && nargs == 2
        show_type(io, args[2])
        print(io, ' ')
        show_name(io, args[1])
    # infix (i.e. "x<:y" or "x = y")

    elseif (head in expr_infix_any && (nargs == 2) || (head == :(:)) && nargs == 3)
        func_prec = operator_precedence(head)
        head_ = head in expr_infix_wide ? " $head " : head
        if func_prec <= prec
            show_enclosed_list(io, '(', args, head_, ')', indent, func_prec, true)
        else
            show_list(io, args, head_, indent, func_prec, true)
        end

    # function call
    elseif head === :call && nargs >= 1
        f = first(args)
        func_args = args[2:end]
        fname = Symbol(functionname(io, f))
        if fname == :getfield && nargs == 3
            accessed, fieldname = args[2], args[3]
            m = io.method
            tt = Sugar.expr_type(ex)
            show_unquoted(io, accessed, indent) # type to be accessed
            print(io, '.')
            fieldname = isa(fieldname, QuoteNode) ? fieldname.value : fieldname
            print(io, fieldname)
        else
            func_prec = operator_precedence(fname)
            # scalar multiplication (i.e. "100x")
            if (
                    fname === :* &&
                    length(func_args) == 2 && isa(func_args[1], Real) &&
                    isa(func_args[2], Symbol)
                )
                if func_prec <= prec
                    show_enclosed_list(io, '(', func_args, "", ')', indent, func_prec)
                else
                    show_list(io, func_args, "", indent, func_prec)
                end

            # unary operator (i.e. "!z")
            elseif isa(fname, Symbol) && fname in uni_ops && length(func_args) == 1
                print(io, fname)
                if isa(func_args[1], Expr) || func_args[1] in all_ops
                    show_enclosed_list(io, '(', func_args, ",", ')', indent, func_prec)
                else
                    show_unquoted(io, func_args[1])
                end

            # binary operator (i.e. "x + y")
            elseif func_prec > 0 # is a binary operator
                na = length(func_args)
                if (na == 2 || (na > 2 && fname in (:+, :++, :*))) && all(!isa(a, Expr) || a.head !== :... for a in func_args)
                    sep = " $fname "
                    if func_prec <= prec
                        show_enclosed_list(io, '(', func_args, sep, ')', indent, func_prec, true)
                    else
                        show_list(io, func_args, sep, indent, func_prec, true)
                    end
                elseif na == 1
                    # 1-argument call to normally-binary operator
                    op, cl = expr_calls[head]
                    show_unquoted(io, f, indent)
                    show_enclosed_list(io, op, func_args, ",", cl, indent)
                else
                    show_call(io, head, f, func_args, indent)
                end

            # normal function (i.e. "f(x,y)")
            else
                show_call(io, head, f, func_args, indent)
            end
        end
    # other call-like expressions ("A[1,2]", "T{X,Y}", "f.(X,Y)")
    elseif haskey(expr_calls, head) && nargs >= 1  # :ref/:curly/:calldecl/:(.)
        funcargslike = head == :(.) ? ex.args[2].args : ex.args[2:end]
        show_call(io, head, ex.args[1], funcargslike, indent)
    # comparison (i.e. "x < y < z")
    elseif (head == :comparison) && nargs >= 3 && (nargs & 1==1)
        comp_prec = minimum(operator_precedence, args[2:2:end])
        if comp_prec <= prec
            show_enclosed_list(io, '(', args, " ", ')', indent, comp_prec)
        else
            show_list(io, args, " ", indent, comp_prec)
        end
    # block with argument
    elseif head in (:while, :function, :if) && nargs == 2
        show_block(io, head, args[1], args[2], indent)
        print(io, "}")
    elseif head == :for
        forheader = args[1]
        forheader.head == :(=) || error("Unsupported for: $ex")
        i, range = forheader.args
        range.head == :(:) || error("Unsupported for: $ex")
        from, to = range.args
        print(io, "for(")
        show_unquoted(io, i)
        print(io, " = ")
        show_unquoted(io, from)
        print(io, "; ")
        show_unquoted(io, i)
        print(io, " <= ")
        show_unquoted(io, to)
        print(io, "; ")
        show_unquoted(io, i)
        print(io, "++)")
        show_block(io, "", [], args[2], indent)
        print(io, "}")
    elseif (head == :module) && nargs==3 && isa(args[1],Bool)
        show_block(io, args[1] ? :module : :baremodule, args[2], args[3], indent)
        print(io, "}")

    # type annotation (i.e. "::Int")
    elseif head == :(::) && nargs == 1
        print(io, ' ')
        print(io, typename(io, args[1]))

    elseif (nargs == 0 && head in (:break, :continue))
        print(io, head)

    elseif (nargs == 1 && head in (:abstract, :const)) ||
                          head in (:local,  :global, :export)
        print(io, head, ' ')
        show_list(io, args, ", ", indent)

    elseif (head === :macrocall) && nargs >= 1
        # expand macros
        show_unquoted(io, expand(ex), indent)

    elseif (head === :line) && 1 <= nargs <= 2
        show_linenumber(io, args...)

    elseif (head === :if) && nargs == 3     # if/else
        show_block(io, "if",   args[1], args[2], indent)
        show_block(io, "} else", args[3], indent)
        print(io, "}")

    elseif (head === :block) || (head === :body)
        show_block(io, "", ex, indent); print(io, "}")

    elseif (head === :new)
        constr_args = args[2:end]
        if isempty(constr_args)
            push!(constr_args, 0f0)
        end
        show_call(io, :constructor, args[1], constr_args, indent)
    elseif head === :return
        if length(args) == 1
            if args[1] != nothing
                if !(isa(args[1], Expr) && (args[1].typ == Void))
                    # if returns void, we need to omit the return statement
                    print(io, "return ")
                end
                show_unquoted(io, args[1])
            end
        else
            # ignore if empty, otherwise, LOL? What's a return with multiple args?
            if isempty(args)
                print(io, "return")
            else
                error("Unknown return Expr: $ex")
            end
        end
    elseif (head in (:meta, :inbounds, :boundscheck))
        # TODO, just ignore this? Log this? We definitely don't need it in GLSL
    elseif head == :local_memory_init
        # e.g. __local float test[100]
        T, N, name = args
        print(io, "__local ")
        show_type(io, T)
        print(io, ' ')
        show_name(io, name)
        print(io, "[$N]")
    elseif head == :local_memory
        # take the above initialized name and returns a pointer (local_memory_init & local_memory
        # are guaranteed to be emitted together)
        # and now just return &test;
        T, name = args
        print(io, "( __local ")
        show_type(io, T)
        print(io, " *)(&")
        show_name(io, name)
        print(io, ')')
    else
        println(ex)
        unsupported_expr(string(ex), line_number)
    end
    nothing
end

function show_typed_list(io, list, seperator, intent)
    for (i, elem) in enumerate(list)
        i != 1 && print(io, seperator)
        name, T = elem.args
        print(io, "    "^(intent))
        show_type(io, T)
        print(io, ' ')
        show_name(io, name)
    end
end

function show_returntype(io, method)
    T = Sugar.returntype(method)
    # void can only be used for return types in C, but in Julia it's used for all sorts of things
    # so we special case the return type and otherwise treat Void not as an inbuild.
    if T == Void
        print(io, "void")
    else
        show_type(io, T)
    end
    return
end
function Sugar.getfuncheader!(x::Union{LazyMethod{:CL}, LazyMethod{:GL}})
    if !isdefined(x, :funcheader)
        x.funcheader = if Sugar.isfunction(x)
            prototype = sprint() do io
                args = Sugar.getfuncargs(x)
                glio = CIO(io, x)
                show_returntype(glio, x)
                print(glio, ' ')
                show_unquoted(glio, x)
                print(glio, '(')
                for (i, elem) in enumerate(args)
                    if i != 1
                        print(glio, ", ")
                    end
                    name, T = elem.args
                    show_type(glio, T)
                    print(glio, ' ')
                    show_name(glio, name)
                end
                print(glio, ')')
            end
            # first is the function prototype!
            header = prototype
            prototype * ";\n" * header
        else
            ""
        end
    end
    x.funcheader
end

function Sugar.getfuncsource(x::Union{LazyMethod{:CL}, LazyMethod{:GL}})
    # TODO make this lazy as well?
    sprint() do io
        show_unquoted(CIO(io, x), Sugar.getast!(x), 0, 0)
    end
end

function Sugar.getfuncargs(x::Union{LazyMethod{:CL}, LazyMethod{:GL}})
    functype = x.signature[1]
    calltypes, slots = Sugar.to_tuple(x.signature[2]), Sugar.getslots!(x)
    n = Sugar.method_nargs(x)
    start = ifelse(Sugar.isclosure(functype), 1, 2)
    unpacked_pointers = []
    args = map(start:n) do i
        argtype, name = slots[i]
        # Slot types might be less specific, e.g. when the variable is unused it might end up as Any.
        # but generally the slot type is the correct one, especially in the context of varargs.
        if !isleaftype(argtype) && length(calltypes) <= i
            argtype = calltypes[i - 1]
        end
        if Sugar.contains_tracked_type(x, argtype)[1] && haskey(x.cache, :tracked_types)
            pointers = map(x.cache[:tracked_types][TypedSlot(i, argtype)]) do field
                ptr_typ = Sugar.get_fields_type(argtype, field[1:end-1])
                :($(last(field))::$ptr_typ)
            end
            append!(unpacked_pointers, pointers)
        end
        expr = :($(name)::$(argtype))
        expr.typ = argtype
        expr
    end
    vcat(args, unpacked_pointers)
end

include("rewriting.jl")


const sense_alias = Dict(
"max"      => :Max,
"maximize" => :Max,
"maximise" => :Max,
"maximum"  => :Max,
"min"      => :Min,
"minimize" => :Min,
"minimise" => :Min,
"minimum"  => :Min
)

const subject_to_alias = ["subject to", "such that", "st", "s.t."]

# a list of section keywords in lower-case
const KEYWORDS = Dict(
    "max"      => Val{:obj},
    "maximize" => Val{:obj},
    "maximise" => Val{:obj},
    "maximum"  => Val{:obj},
    "min"      => Val{:obj},
    "minimize" => Val{:obj},
    "minimise" => Val{:obj},
    "minimum"  => Val{:obj},

    "subject to" => Val{:constraints},
    "such that"  => Val{:constraints},
    "st"         => Val{:constraints},
    "s.t."       => Val{:constraints},

    "bounds" => Val{:bounds},
    "bound"  => Val{:bounds},

    "gen"      => Val{:integer},
    "general"  => Val{:integer},
    "generals" => Val{:integer},

    "bin"      => Val{:binary},
    "binary"   => Val{:binary},
    "binaries" => Val{:binary},

    "end"      => Val{:quit}
)

const COMMENT_REG = r"(.*?)\\(.*)"
function stripcomment(line::String)
    if contains(line, "\\")
        m = match(COMMENT_REG, line)
        return strip(String(m[1]))
    else
        return strip(line)
    end
end

immutable TempSparseMatrix
    i::Vector{Int}
    j::Vector{Int}
    v::Vector{Float64}
end
TempSparseMatrix() = TempSparseMatrix(Int[], Int[], Float64[])
Base.sparse(m::TempSparseMatrix, nr::Int, nc::Int) = sparse(m.i, m.j, m.v, nr, nc)

newdatastore() = Dict(
    :A => TempSparseMatrix(),
    :collb => Float64[],
    :colub => Float64[],
    :c => Float64[],
    :rowlb => Float64[],
    :rowub => Float64[],
    :sense => :Min,
    :colcat => Symbol[],
    :sos => SOS[],
    :Q => TempSparseMatrix(),
    :modelname => "",
    :colnames => String[],
    :rownames => String[],
    :open_constraint => false
)
setsense!(T, data, line) = nothing
function setsense!(::Type{Val{:obj}}, data, line)
    data[:sense] = sense_alias[lowercase(line)]
end
function read(filename::String)
    data = newdatastore()
    open(filename, "r") do io
        section = :none
        while !eof(io)
            line = stripcomment(readline(io))
            if line == "" # skip blank lines
                continue
            end
            if haskey(KEYWORDS, lowercase(line)) # section has changed
                section = KEYWORDS[lowercase(line)]
                setsense!(section, data, line)
                continue
            end
            parsesection!(section, data, line)
        end
    end
    return sparse(data[:A], length(data[:rownames]), length(data[:colnames])),
        data[:collb],
        data[:colub],
        data[:c],
        data[:rowlb],
        data[:rowub],
        data[:sense],
        data[:colcat],
        data[:sos],
        sparse(data[:Q], length(data[:rownames]), length(data[:colnames])),
        data[:modelname],
        data[:colnames],
        data[:rownames]
end

parsesection!(::Type{Val{:quit}}, data, line) = error("Corrupted LP File. You have the lne $(line) after an end.")

function parsesection!(::Type{Val{:obj}}, data, line)
    # okay so line should be the start of the objective
    if contains(line, ":")
        # throw away name
        m = match(r"(.*?)\:(.*)", line)
        line = String(m[2])
    end
    tokens = tokenize(line)
    if length(tokens) == 0 # no objective
        return
    end
    # tokens should be in order (+/-) (numeric) (variable) ...
    while length(tokens) > 0
        variable = String(pop!(tokens))
        idx = getvariableindex!(data, variable) # catch in here for malformed variables
        coef_token = pop!(tokens)
        try
            coefficient = parse(Float64, coef_token)
        catch
            error("Unable to parse objective. Expected numeric coefficient. Got $(coef_token)")
        end
        if length(tokens) > 0
            _sign = pop!(tokens)
            if _sign == "-"
                coefficient *= -1
            elseif _sign == "+"
            else
                error("Unable to parse objective due to bad operator: $(_sign) $(line)")
            end
        end
        data[:c][idx] = coefficient
    end
end

const constraintsense = Dict(
    "<"  => :le,
    "<=" => :le,
    "="  => :eq,
    "==" => :eq,
    ">"  => :ge,
    ">=" => :ge,
)

function parseconstraintcoefficients!(data, line, tokens, rowidx)
    # tokens should be in order (+/-) (numeric) (variable) ...
    while length(tokens) > 0
        variable = String(pop!(tokens))
        idx = getvariableindex!(data, variable) # catch in here for malformed variables
        coef_token = pop!(tokens)
        try
            coefficient = parse(Float64, coef_token)
        catch
            error("Unable to parse constraint $(line). Expected numeric coefficient. Got $(coef_token)")
        end
        if length(tokens) > 0
            _sign = pop!(tokens)
            if _sign == "-"
                coefficient *= -1
            elseif _sign == "+"
            else
                error("Unable to parse objective due to bad operator: $(_sign) $(line)")
            end
        end
        push!(data[:A].i, rowidx)
        push!(data[:A].j, idx)
        push!(data[:A].v, coefficient)
    end
end

function parsesos!(data, line)
    tokens = tokenize(line)
    if length(tokens) < 3
        error(string("Malformed SOS constraint: ", line))
    end
    if tokens[2] == "S1::"
        order = 1
    elseif tokens[2] == "S2::"
        order = 2
    else
        error("SOS of type $(tokens[2]) not recognised")
    end
    names   = String[]
    weights = Float64[]
    for token in tokens[3:end]
        items = split(token, ":")
        if length(items) != 2
            error(string("Invalid sequence: ", token))
        end
        push!(names, String(items[1]))
        push!(weights, parsefloat(String(items[2])))
    end
    indices = Int[]
    for name in names
        idx = findfirst(data[:colnames], name)
        if idx == 0
            push!(data[:colnames], name)
            push!(indices, length(data[:colnames]))
        else
            push!(indices, idx)
        end
    end
    push!(data[:sos], (order, indices, weights))
end

function parsesection!(::Type{Val{:constraints}}, data, line)
    if match(r" S([0-9]):: ", line) != nothing
        # it's an SOS constraint
        parsesos!(data, line)
        return
    end
    if data[:open_constraint] == false
        push!(data[:rownames], "R$(length(data[:rownames]) + 1)")
        push!(data[:rowlb], -Inf)
        push!(data[:rowub], Inf)
    end
    if contains(line, ":")
        if data[:open_constraint] == true
            error("Malformed constraint $(line). Is the previous one valid?")
        end
        # throw away name
        m = match(r"(.*?)\:(.*)", line)
        data[:rownames][end] = String(m[1])
        line = String(m[2])
    end
    data[:open_constraint] = true

    tokens = tokenize(line)
    if length(tokens) == 0 # no entries
        return
    elseif length(tokens) >= 2 && haskey(constraintsense, tokens[end-1])# test if constraint ends this line
        rhs = parsefloat(pop!(tokens))
        sym = pop!(tokens)
        if constraintsense[sym] == :le
            data[:rowub][end] = rhs
        elseif constraintsense[sym] == :ge
            data[:rowlb][end] = rhs
        elseif constraintsense[sym] == :eq
            data[:rowlb][end] = rhs
            data[:rowub][end] = rhs
        end
        data[:open_constraint] = false # finished
    end
    parseconstraintcoefficients!(data, line, tokens, length(data[:rownames]))
end

function getvariableindex!(data, v)
    i = findfirst(data[:colnames], v)
    if i == 0
        if !verifyname(v)
            error("Invalid variable name $v")
        end
        addnewvariable!(data, v)
        return length(data[:colnames])
    end
    return i
end

function addnewvariable!(data, name)
    push!(data[:collb], -Inf)
    push!(data[:colub],  Inf)
    push!(data[:c], 0)
    push!(data[:colcat], :Cont)
    push!(data[:colnames], name)
end

function parsevariabletype!(data, line, cat)
    items = tokenize(line)
    for v in items
        i = getvariableindex!(data, v)
        data[:colcat][i] = cat
    end
end
parsesection!(::Type{Val{:integer}}, data, line) = parsevariabletype!(data, line, :Int)
parsesection!(::Type{Val{:binary}}, data, line)  = parsevariabletype!(data, line, :Bin)

function parsefloat(val::String)
    if lowercase(val) == "-inf" || lowercase(val) == "-infinity"
        return -Inf
    elseif lowercase(val) == "+inf" || lowercase(val) == "+infinity"
        return Inf
    else
        return parse(Float64, val)
    end
end
function tokenize(line)
    items = String.(split(line, " "))
    items[items .!= ""]
end
bounderror(line) = error("Unable to parse bound: $(line)")
function parsesection!(::Type{Val{:bounds}}, data, line)
    items = tokenize(line)
    v = ""
    lb = -Inf
    ub = Inf
    if length(items) == 5 # ranged bound
        v = items[3]
        if (items[2] == "<=" || items[2] == "<") &&  (items[4] == "<=" || items[4] == "<") # le
            lb = parsefloat(items[1])
            ub = parsefloat(items[5])
        elseif (items[2] == ">=" || items[2] == ">") &&  (items[4] == ">=" || items[4] == ">") # ge
            lb = parsefloat(items[5])
            ub = parsefloat(items[1])
        else
            bounderror(line)
        end
    elseif length(items) == 3 # one sided
        v = items[1]
        if items[2] == "<=" || items[2] == "<" # le
            ub = parsefloat(items[3])
        elseif items[2] == ">=" || items[2] == ">" # ge
            lb = parsefloat(items[3])
        elseif items[2] == "==" || items[2] == "=" # eq
            lb = ub = parsefloat(items[3])
        else
            bounderror(line)
        end
    elseif length(items) == 2 # free
        if items[2] != "free"
            bounderror(line)
        end
        v = items[1]
    else
        bounderror(line)
    end
    i = getvariableindex!(data, v)
    data[:collb][i] = lb
    data[:colub][i] = ub

end


# when overloading new functions, don't forget to import them first

# *
for (N1, N2) in [(1, 2), (2, 1), (2, 2)]
    @eval *(x::TArray{T1,$N1}, y::TArray{T2,$N2}) where {T1,T2} =
        record!(x.tape, Call, *, (x, y))
    @eval *(x::TArray{T1, $N1}, y::AbstractArray{T2, $N2}) where {T1, T2} =
        record!(x.tape, Call, *, (x, constant(x.tape, y)))
    @eval *(x::AbstractArray{T1, $N1}, y::TArray{T2, $N2}) where {T1, T2} =
        record!(y.tape, Call, *, (constant(y.tape, x), y))
end

# +
+(x::TArray{T,N}, y::TArray{T,N}) where {T,N} = record!(x.tape, Call, +, (x, y))
+(x::TArray{T1, N}, y::AbstractArray{T2, N}) where {T1, T2, N} =
    record!(x.tape, Call, +, (x, constant(x.tape, y)))
+(x::AbstractArray{T1, N}, y::TArray{T2, N}) where {T1, T2, N} =
    record!(y.tape, Call, +, (constant(y.tape, x), y))

# -
-(x::TArray{T,N}, y::TArray{T,N}) where {T,N} = record!(x.tape, Call, -, (x, y))
-(x::TArray{T1, N}, y::AbstractArray{T2, N}) where {T1, T2, N} =
    record!(x.tape, Call, -, (x, constant(x.tape, y)))
-(x::AbstractArray{T1, N}, y::TArray{T2, N}) where {T1, T2, N} =
    record!(y.tape, Call, -, (constant(y.tape, x), y))

sum(x::TArray, dims) = record!(x.tape, Call, sum, (x,); kwargs=Dict{Symbol,Any}(:dims=>dims))
sum(x::TArray; dims=nothing) = (dims == nothing ? record!(x.tape, Call, sum, (x,)) :
                                record!(x.tape, Call, sum, (x,);
                                        kwargs=Dict{Symbol,Any}(:dims=>dims)))
mean(x::TArray, dims) = record!(x.tape, Call, mean, (x,); kwargs=Dict{Symbol,Any}(:dims=>dims))
mean(x::TArray; dims=nothing) = (dims == nothing ? record!(x.tape, Call, mean, (x,)) :
                                record!(x.tape, Call, mean, (x,);
                                        kwargs=Dict{Symbol,Any}(:dims=>dims)))
dropdims(x::TArray; dims) = record!(x.tape, Call, dropdims, (x,); kwargs=Dict{Symbol,Any}(:dims=>dims))
transpose(x::TArray) = record!(x.tape, Call, transpose, (x,))
# is it correct to replace x' with transpose(x)? please ping me if you believe it's wrong
adjoint(x::TArray) = record!(x.tape, Call, transpose, (x,))
minimum(x::TArray) = record!(x.tape, Call, minimum, (x,))
maximum(x::TArray) = record!(x.tape, Call, maximum, (x,))
getindex(x::TArray, i...) = record!(x.tape, Call, getindex, (x, i...))
# I'm not really sure we should write setindex! to the tape since it isn't proper op
# and can be removed as unused op
# setindex!(x::TArray, v::Real, I::Vararg{<:Real}) =
#     record!(x.tape, Call, setindex!, (x, constant(x.tape, v), [constant(x.tape, i) for i in I]...))
ungetindex(dy::TAny, x::TArray, i::Real) =
    record!(x.tape, Call, ungetindex, (dy, x, constant(x.tape, i)))
ungetindex(dy::TAny, x::TArray, i::Real, j::Real) =
    record!(x.tape, Call, ungetindex, (dy, x, constant(x.tape, i), constant(x.tape, j)))
reshape(x::TArray, dims::Vararg{Int64,N}) where N = record!(x.tape, Call, reshape, (x, dims))

include("linalg.jl")

for fn in (+, -)
    @eval Broadcast.broadcasted(::typeof($fn), x::TArray, y::TArray) =
        record!(x.tape, Bcast, $fn, (x, y))
    @eval Broadcast.broadcasted(::typeof($fn), x::TArray, y::Real) =
        record!(x.tape, Bcast, $fn, (x, constant(x.tape, y)))
    @eval Broadcast.broadcasted(::typeof($fn), x::Real, y::TArray) =
        record!(y.tape, Bcast, $fn, (constant(y.tape, x), y))
    # we may also easily handle constant arrays,
    # but let's see if there's actually a use case for this
end

for fn in (*, /)
    @eval Broadcast.broadcasted(::typeof($fn), x::TArray, y::TArray) =
        record!(x.tape, Bcast, $fn, (x, y))
    # Julia automatically translates `a * x`, where a::Real and x::AbstractArray, into `a .* x`
    # but if write it as Bcast, it breaks Transpose and Adjoint
    @eval Broadcast.broadcasted(::typeof($fn), x::TArray, y::Real) =
        record!(x.tape, Call, $fn, (x, constant(x.tape, y)))
    @eval Broadcast.broadcasted(::typeof($fn), x::Real, y::TArray) =
        record!(y.tape, Call, $fn, (constant(y.tape, x), y))
    # we may also easily handle constant arrays,
    # but let's see if there's actually a use case for this
end

for fn in (sin, cos, abs, abs2)
    @eval Broadcast.broadcasted(::typeof($fn), x::TArray) =
        record!(x.tape, Bcast, $fn, (x,))
end

# functions that require special treatment with CuArrays
for fn in (Base.log, Base.exp, Base.sqrt)
    @eval function Broadcast.broadcasted(::typeof($fn), x::TArray)
        record!(x.tape, Bcast, device_op(x.tape.device, $fn), (x,))
    end
    # @eval function Broadcast.broadcasted(::typeof($fn), x::TArray)
    #     if is_cuarray(x.val)
    #         record!(x.tape, Bcast, cuda_op($fn), (x,))
    #     else
    #         record!(x.tape, Bcast, $fn, (x,))
    #     end
    # end
end

# another CuArrays special case: ^
function Broadcast.broadcasted(::typeof(^), x::TArray, y::TArray)
    op = device_op(x.tape.devide, ^)
    record!(x.tape, Bcast, op, (x, y))
end
function Broadcast.broadcasted(::typeof(^), x::TArray, y::Real)
    op = device_op(x.tape.device, ^)
    record!(x.tape, Bcast, op, (x, constant(x.tape, y)))
end
function Broadcast.broadcasted(::typeof(^), x::Real, y::TArray)
    op = device_op(x.tape.device, ^)
    record!(x.tape, Bcast, op, (constant(y.tape, x), y))
end

# catchall - might replace previous definitions

Broadcast.broadcasted(fn::Fn, x::TArray{T,N}) where {Fn,T,N} = record!(x.tape, Bcast, fn, (x,))


using Oceananigans: Flat, Bounded, RegularRectilinearGrid

ENGAGING_LESBRARY_DIR = "https://engaging-web.mit.edu/~alir/lesbrary/neural_free_convection_training_data"

SIMULATION_IDS = 1:9

LESBRARY_DATA_DEPS = [
    DataDep("free_convection_$id",
            "LESbrary.jl free convection simulation $id",
            joinpath(ENGAGING_LESBRARY_DIR, "free_convection_$id", "instantaneous_statistics_with_halos.jld2")
    ) for id in SIMULATION_IDS
]

function validate_simulation_ids(ids_train, ids_test)
    @info "Simulation IDs: $(collect(SIMULATION_IDS))"
    @info "Training simulations: $(collect(ids_train))"
    @info "Testing simulations: $(collect(ids_test))"

    ids_not_used = setdiff(SIMULATION_IDS, ids_train, ids_test)
    if !isempty(ids_not_used)
        @warn "Simulations not used: $(collect(ids_not_used))"
    end

    ids_intersection = intersect(ids_train, ids_test)
    if !isempty(ids_intersection)
        @warn "Simulations used for both training and testing: $(collect(ids_intersection))"
    end

    return nothing
end

function load_data(ids_train, ids_test, Nz)

    @info "Constructing FieldTimeSeries..."

    datasets = Dict{Int, FieldDataset}(
        id => FieldDataset(@datadep_str "free_convection_$id/instantaneous_statistics_with_halos.jld2"; ArrayType=Array{Float32}, metadata_paths=["parameters"])
        for id in SIMULATION_IDS
    )

    @info "Injecting surface fluxes..."

    for id in SIMULATION_IDS
        add_surface_fluxes!(datasets[id])
    end

    @info "Coarsening grid..."

    les_grid = datasets[1]["T"].grid

    topo = (Flat, Flat, Bounded)
    domain = (les_grid.zF[1], les_grid.zF[les_grid.Nz+1])
    coarse_grid = RegularRectilinearGrid(Float32, topology=topo, size=Nz, z=domain)

    @info "Coarse graining data..."

    coarse_datasets = Dict{Int, FieldDataset}(
        id => coarse_grain(ds, coarse_grid)
        for (id, ds) in datasets
    )

    @info "Partitioning data into training and testing datasets..."

    training_datasets = Dict{Int, FieldDataset}(id => datasets[id] for id in ids_train)
    testing_datasets = Dict{Int, FieldDataset}(id => datasets[id] for id in ids_test)

    coarse_training_datasets = Dict{Int, FieldDataset}(id => coarse_datasets[id] for id in ids_train)
    coarse_testing_datasets = Dict{Int, FieldDataset}(id => coarse_datasets[id] for id in ids_test)

    return (; datasets, coarse_datasets, training_datasets, testing_datasets, coarse_training_datasets, coarse_testing_datasets)
end


#===============================================================================
    Definitions of abstract types reflecting conceptual inheritence structure
===============================================================================#

"""
Supertype of all MCMC updates
"""
abstract type MCMCUpdate end

"""
    MCMCParamUpdate <: MCMCUpdate

Supertype of all updates that make changes to the main MCMC parameter called
`state` in the `GlobalWorkspace`.
"""
abstract type MCMCParamUpdate <: MCMCUpdate end

"""
    MCMCGradientBasedUpdate <: MCMCParamUpdate

Supertype of all updates that require local gradient information to perform
update of `state`.
"""
abstract type MCMCGradientBasedUpdate <: MCMCParamUpdate end

"""
    MCMCConjugateUpdate <: MCMCUpdate

Supertype of all conjugate updates, for which sampling can be done directly,
without resorting to a Metropolis-Hastings algorithm
"""
abstract type MCMCConjugateUpdate <: MCMCUpdate end

"""
    MCMCConjugateParamUpdate <: MCMCParamUpdate

Supertype of all conjugate updates that update parameters and for which sampling
can be done directly, without resorting to a Metropolis-Hastings algorithm
"""
abstract type MCMCConjugateParamUpdate <: MCMCParamUpdate end

"""
    MCMCImputation <: MCMCUpdate

Supertype of all updates that do not make any changes to the main parameter
`state` in `GlobalWorkspace`, but instead, perform sampling on any auxiliary
variables that are not of direct interest to the MCMC chain.
"""
abstract type MCMCImputation <: MCMCUpdate end

"""
Supertype of all decorators to update schemes. In this context, we use the word
`decorator` to refer to any additional information that needs to be conveyed to
the MCMC sampler that is not directly extractable from the update-objects
themselves (for instance, a change in the delimitation of blocks that is made
in-between updates).
"""
abstract type MCMCUpdateDecorator end

"""
    isdecorator(u)

Returns true if `u` is a subtype of decorators.
"""
isdecorator(u) = (typeof(u) <: MCMCUpdateDecorator)

"""
Supertype of all workspaces—i.e. of structs that gather in one place various
objects that the MCMC sampler operates on.
"""
abstract type Workspace end

"""
    GlobalWorkspace{T} <: Workspace

Supertype of all global workspaces. Each MCMC sampler must have a unique global
workspace, which contains `state`, `state_history`, `state_proposal_history`,
`acceptance_history` and `data`. `state` is the paramater vector that the MCMC
sampling is done for (other names being self-explanatory).
"""
abstract type GlobalWorkspace{T} <: Workspace end

"""
    LocalWorkspace{T} <: Workspace

Supertype of all local workspaces. Local workspace should contain any additional
gathering of objects that are needed by specific updates, but are not are not
already in a global workspace. Each MCMC update has its own `LocalWorkspace`.
"""
abstract type LocalWorkspace{T} <: Workspace end

"""
Supertype of all transition kernels that perform updates on the main `state`
of the global workspace. Conceptually, these are used by subtypes of
`MCMCParamUpdate` to perform actual sampling and instances of types inheriting
from `TransitionKernel` are usually member objects of the instances inheriting
from `MCMCParamUpdate`.
"""
abstract type TransitionKernel end

"""
Supertype for all adaptation schemes.
"""
abstract type Adaptation end

"""
Supertype of all backends for the MCMC sampler.
"""
abstract type MCMCBackend end

"""
    GenericMCMCBackend <: MCMCBackend

A flag that no specific backend is passed.
"""
struct GenericMCMCBackend <: MCMCBackend end

abstract type ChainStats end

struct Previous end
struct Proposal end
struct PreMCMCStep end
struct PostMCMCStep end

#NOTE also appears in DiffusionDefinition.jl, but not used there.
"""
    remove_curly(::Type{K}) where K

Utility function that removes all type-specifiers listed in the curly brackets.

# Examples
```julia-repl
julia> remove_curly(Array{Float64,1})
Array
```
"""
@generated function remove_curly(::Type{K}) where K
    name_without_curly = Meta.parse(string(K)).args[1]
    :( $name_without_curly )
end


module Experiments

export Experiment

using ..AlphaZero

using Base: @kwdef

"""
    Experiment

A structure that contains the information necessary to replicate a training session.

# Constructor

    Experiment(gspec, params, mknet, netparams, benchmarks)

- `gspec` is the specification of the game to be played
- `params` has type [`Params`](@ref)
- `mknet` is a neural network constructor taking arguments `(netparams, gspec)`
- `netparams` are the neural network hyperparameters
- `benchmark` is a vector of [`Benchmark.Duel`](@ref) to be used as a benchmark to track
  training progress.

"""
@kwdef struct Experiment
  name :: String
  gspec :: AbstractGameSpec
  params :: Params
  mknet :: Any
  netparams :: Any
  benchmark :: Vector{<:Benchmark.Evaluation}
end

end


module TestInterface

using Phylo
using DataFrames
using Compat.Test

@testset "Build and tear down trees" begin
    @testset "For $TreeType" for TreeType in
        [NamedTree,
         BinaryTree{DataFrame, Vector{Float64}},
         BinaryTree{DataFrame, Vector{String}}]

        species = ["Dog", "Cat", "Human", "Potato", "Apple"]
        tree = TreeType(species)
        othernodes = ["Some 1", "Some 2"]
        @test othernodes == addnodes!(tree, othernodes)
        extra = addnodes!(tree, 1)
        @test isa(extra[1], String)
        append!(othernodes, extra)
        also = addnode!(tree)
        @test also ∉ othernodes
        @test_throws ErrorException addnode!(tree, also)
        @test_throws ErrorException addnodes!(tree, [also])
        @test_throws ErrorException deletenode!(tree, "really not")
        @test also == deletenode!(tree, also)
        also = addnode!(tree)
        push!(othernodes, also)
        @test Set(othernodes) ⊆ Set(nodenameiter(tree))
        innodes = append!(copy(species), othernodes)
        allnodes = reverse(innodes)
        pop!(innodes)
        branches =
            map(innodes) do node
                itr = Iterators.filter(name ->
                                       hasoutboundspace(tree, name) &&
                                       name != node, allnodes)
                addbranch!(tree, first(itr), node)
            end
        @test Set(branches) == Set(branchnameiter(tree))
        @test validate(tree)
        @test_throws ErrorException addbranch!(tree, allnodes[1], allnodes[2])
        who = getparent(tree, species[1])
        b = getinbound(tree, species[1])
        node1 = getnode(tree, species[1])
        @test !isunattached(node1) &&
            hasoutboundspace(node1) && !hasinboundspace(node1) &&
            outdegree(node1) == 0 && indegree(node1) == 1
        @test hasbranch(tree, b)
        @test dst(getbranch(tree, b)) == species[1]
        @test src(getbranch(tree, b)) == who
        @test b == deletebranch!(tree, b)
        @test !hasbranch(tree, b)
        @test isunattached(tree, species[1]) &&
            hasoutboundspace(tree, species[1]) &&
            hasinboundspace(tree, species[1]) &&
            outdegree(tree, species[1]) == 0 &&
            indegree(tree, species[1]) == 0
        @test_throws ErrorException getbranch(tree, b)
        branches = collect(Iterators.filter(name -> name != b, branches))
        @test Set(branches) == Set(branchnameiter(tree))
        b3 = getinbound(tree, species[2])
        source = src(tree, b3)
        destination = dst(tree, b3)
        @test b3 == changesrc!(tree, b3, who)
        @test b3 == changedst!(tree, b3, species[1])
        b2 = addbranch!(tree, source, destination)
        push!(branches, b2)
        @test Set(branches) == Set(branchnameiter(tree))
        @test species[1] == deletenode!(tree, species[1])
        @test !hasnode(tree, species[1])
        @test species[1] == branch!(tree, who, destination=species[1])
        @test hasnode(tree, species[1])
        @test validate(tree)
        @test all(map(==, Pair(first(branchiter(tree))),
                      Tuple(tree, first(branchnameiter(tree)))))
        @test all(map(==, Pair(tree, first(branchnameiter(tree))),
                      Tuple(first(branchiter(tree)))))
        @test all(map(node -> isleaf(tree, node), species))
        @test all(map(node -> !isroot(tree, node) &&
                      !isunattached(tree, node) &&
                      !isinternal(tree, node), species))
        @test Set(map(pair -> src(pair[2]), collect(getbranches(tree)))) ∪
            Set(map(pair -> dst(pair[2]), collect(getbranches(tree)))) ==
            Set(allnodes)
    end
end

end


"""
    TransformedKernel(k::Kernel, t::Transform)

Kernel derived from `k` for which inputs are transformed via a [`Transform`](@ref) `t`.

The preferred way to create kernels with input transformations is to use the composition
operator [`∘`](@ref) or its alias `compose` instead of `TransformedKernel` directly since
this allows optimized implementations for specific kernels and transformations.

See also: [`∘`](@ref)
"""
struct TransformedKernel{Tk<:Kernel,Tr<:Transform} <: Kernel
    kernel::Tk
    transform::Tr
end

@functor TransformedKernel

(k::TransformedKernel)(x, y) = k.kernel(k.transform(x), k.transform(y))

# Optimizations for scale transforms of simple kernels to save allocations:
# Instead of a multiplying every element of the inputs before evaluating the metric,
# we perform a scalar multiplcation of the distance of the original inputs, if possible.
function (k::TransformedKernel{<:SimpleKernel,<:ScaleTransform})(
    x::AbstractVector{<:Real}, y::AbstractVector{<:Real}
)
    return kappa(k.kernel, _scale(k.transform, metric(k.kernel), x, y))
end

function _scale(t::ScaleTransform, metric::Euclidean, x, y)
    return first(t.s) * evaluate(metric, x, y)
end
function _scale(t::ScaleTransform, metric::Union{SqEuclidean,DotProduct}, x, y)
    return first(t.s)^2 * evaluate(metric, x, y)
end
_scale(t::ScaleTransform, metric, x, y) = evaluate(metric, t(x), t(y))

"""
    kernel ∘ transform
    ∘(kernel, transform)
    compose(kernel, transform)

Compose a `kernel` with a transformation `transform` of its inputs.

The prefix forms support chains of multiple transformations:
`∘(kernel, transform1, transform2) = kernel ∘ transform1 ∘ transform2`.

# Definition

For inputs ``x, x'``, the transformed kernel ``\\widetilde{k}`` derived from kernel ``k`` by
input transformation ``t`` is defined as
```math
\\widetilde{k}(x, x'; k, t) = k\\big(t(x), t(x')\\big).
```

# Examples

```jldoctest
julia> (SqExponentialKernel() ∘ ScaleTransform(0.5))(0, 2) == exp(-0.5)
true

julia> ∘(ExponentialKernel(), ScaleTransform(2), ScaleTransform(0.5))(1, 2) == exp(-1)
true
```

See also: [`TransformedKernel`](@ref)
"""
Base.:∘(k::Kernel, t::Transform) = TransformedKernel(k, t)
Base.:∘(k::TransformedKernel, t::Transform) = TransformedKernel(k.kernel, k.transform ∘ t)

# Simplify kernels with identity transformation of the inputs
Base.:∘(k::Kernel, ::IdentityTransform) = k
Base.:∘(k::TransformedKernel, ::IdentityTransform) = k

Base.show(io::IO, κ::TransformedKernel) = printshifted(io, κ, 0)

function printshifted(io::IO, κ::TransformedKernel, shift::Int)
    printshifted(io, κ.kernel, shift)
    return print(io, "\n" * ("\t"^(shift + 1)) * "- $(κ.transform)")
end

# Kernel matrix operations

function kernelmatrix_diag!(K::AbstractVector, κ::TransformedKernel, x::AbstractVector)
    return kernelmatrix_diag!(K, κ.kernel, _map(κ.transform, x))
end

function kernelmatrix_diag!(
    K::AbstractVector, κ::TransformedKernel, x::AbstractVector, y::AbstractVector
)
    return kernelmatrix_diag!(K, κ.kernel, _map(κ.transform, x), _map(κ.transform, y))
end

function kernelmatrix!(K::AbstractMatrix, κ::TransformedKernel, x::AbstractVector)
    return kernelmatrix!(K, κ.kernel, _map(κ.transform, x))
end

function kernelmatrix!(
    K::AbstractMatrix, κ::TransformedKernel, x::AbstractVector, y::AbstractVector
)
    return kernelmatrix!(K, κ.kernel, _map(κ.transform, x), _map(κ.transform, y))
end

function kernelmatrix_diag(κ::TransformedKernel, x::AbstractVector)
    return kernelmatrix_diag(κ.kernel, _map(κ.transform, x))
end

function kernelmatrix_diag(κ::TransformedKernel, x::AbstractVector, y::AbstractVector)
    return kernelmatrix_diag(κ.kernel, _map(κ.transform, x), _map(κ.transform, y))
end

function kernelmatrix(κ::TransformedKernel, x::AbstractVector)
    return kernelmatrix(κ.kernel, _map(κ.transform, x))
end

function kernelmatrix(κ::TransformedKernel, x::AbstractVector, y::AbstractVector)
    return kernelmatrix(κ.kernel, _map(κ.transform, x), _map(κ.transform, y))
end


"""
    ClimaTimeSteppers

Ordinary differential equation solvers

JuliaDiffEq terminology:

* _Function_: the right-hand side function df/dt.
  * by default, a function gets wrapped in an `ODEFunction`
  * define new `IncrementingODEFunction` to support incrementing function calls.

* _Problem_: Function, initial u, time span, parameters and options

  du/dt = f(u,p,t) = fL(u,p,t)  + fR(u,p,t)

  fR(u,p,t) == f(u.p,t) - fL(u,p,t)
  fL(u,_,_) == A*u for some `A` (matrix free)

  SplitODEProlem(fL, fR)

  * `ODEProblem` from OrdinaryDiffEq.jl
    - use `jac` option to `ODEFunction` for linear + full IMEX (https://docs.sciml.ai/latest/features/performance_overloads/#ode_explicit_jac-1)
  * `SplitODEProblem` for linear + remainder IMEX
  * `MultirateODEProblem` for true multirate

* _Algorithm_: small objects (often singleton) which indicate what algorithm + options (e.g. linear solver type)
  * define new abstract `DistributedODEAlgorithm`, algorithms in this pacakge will be subtypes of this
  * define new `Multirate` for multirate solvers

* _Integrator_: contains everything necessary to solve. Used as:

  * define new `DistributedODEIntegrator` for solvers in this package

      init(prob, alg, options...) => integrator
      step!(int) => runs single step
      solve!(int) => runs it to end
      solve(prob, alg, options...) => init + solve!

* _Solution_ (not implemented): contains the "solution" to the ODE.

"""
module ClimaTimeSteppers

using KernelAbstractions
using KernelAbstractions.Extras: @unroll
using LinearAlgebra
using StaticArrays
using CUDA
using MPI

array_device(::Union{Array, SArray, MArray}) = CPU()
array_device(::CuArray) = CUDADevice()
realview(x::Union{Array, SArray, MArray}) = x
realview(x::CuArray) = x

import DiffEqBase, SciMLBase, LinearAlgebra, DiffEqCallbacks

include("functions.jl")
include("operators.jl")

abstract type DistributedODEAlgorithm <: DiffEqBase.AbstractODEAlgorithm
end

include("integrators.jl")

# Include concrete implementations
include("solvers/multirate.jl")
include("solvers/lsrk.jl")
include("solvers/ssprk.jl")
include("solvers/ark.jl")
include("solvers/mis.jl")
include("solvers/wickerskamarock.jl")
include("solvers/rosenbrock.jl")

include("callbacks.jl")

end