@inline static(::Val{N}) where {N} = StaticInt{N}()
@inline static(::Nothing) = nothing
@generated function static_sizeof(::Type{T}) where {T}
    st = Base.allocatedinline(T) ? sizeof(T) : sizeof(Int)
    Expr(:block, Expr(:meta, :inline), Expr(:call, Expr(:curly, :StaticInt, st)))
end

# These have versions that may allow for more optimizations, so we override base methods with a single `StaticInt` argument.
for (f,ff) ∈ [
  # (:(Base.:+),:vadd_fast), (:(Base.:-),:vsub_fast), (:(Base.:*),:vmul_fast),
  (:(Base.:+),:vadd_nsw), (:(Base.:-),:vsub_nsw), (:(Base.:*),:vmul_nsw),
  (:(Base.:+),:vadd_nuw), (:(Base.:-),:vsub_nuw), (:(Base.:*),:vmul_nuw),
  (:(Base.:+),:vadd_nw), (:(Base.:-),:vsub_nw), (:(Base.:*),:vmul_nw),
  # (:(Base.:<<),:vshl), (:(Base.:÷),:(Base.div)), (:(Base.:%), :(Base.rem)), (:(Base.:>>>),:(Base.:>>>))
]
  @eval begin
    # @inline $f(::StaticInt{M}, ::StaticInt{N}) where {M, N} = StaticInt{$f(M, N)}()
    # If `M` and `N` are known at compile time, there's no need to add nsw/nuw flags.
    @inline $ff(::StaticInt{M}, ::StaticInt{N}) where {M, N} = $f(StaticInt{M}(),StaticInt{N}())
    # @inline $f(::StaticInt{M}, x) where {M} = $ff(M, x)
    # @inline $f(x, ::StaticInt{M}) where {M} = $ff(x, M)
    @inline $ff(::StaticInt{M}, x::T) where {M,T<:IntegerTypesHW} = $ff(M%T, x)
    @inline $ff(x::T, ::StaticInt{M}) where {M,T<:IntegerTypesHW} = $ff(x, M%T)
    @inline $ff(::StaticInt{M}, x) where {M} = $ff(M, x)
    @inline $ff(x, ::StaticInt{M}) where {M} = $ff(x, M)
  end
end
for f ∈ [:vsub_nsw, :vsub_nuw, :vsub_nw]
  @eval begin
    @inline $f(::Zero, m::Number) = -m
    @inline $f(::Zero, m::IntegerTypesHW) = -m
    @inline $f(m::Number, ::Zero) =  m
    @inline $f(m::IntegerTypesHW, ::Zero) =  m
    @inline $f(::Zero, ::Zero) = Zero()
    @inline $f(::Zero, ::StaticInt{N}) where {N} = -StaticInt{N}()
    @inline $f(::StaticInt{N}, ::Zero) where {N} = StaticInt{N}()
  end
end
for f ∈ [:vadd_nsw, :vadd_nuw, :vadd_nw]
  @eval begin
    @inline $f(::StaticInt{N}, ::Zero) where {N} = StaticInt{N}()
    @inline $f(::Zero, ::StaticInt{N}) where {N} = StaticInt{N}()
    @inline $f(::Zero, ::Zero) = Zero()
    @inline $f(a::Number, ::Zero) = a
    @inline $f(a::IntegerTypesHW, ::Zero) = a
    @inline $f(::Zero, a::Number) = a
    @inline $f(::Zero, a::IntegerTypesHW) = a
  end
end

@inline Base.:(+)(m::AbstractMask{W}, ::StaticInt{N}) where {N,W} = m + vbroadcast(Val{W}(), N)
@inline Base.:(+)(::StaticInt{N}, m::AbstractMask{W}) where {N,W} = vbroadcast(Val{W}(), N) + m

for V ∈ [:AbstractSIMD, :MM, :VecUnroll, :AbstractMask]
  for (mod,m,a,s) ∈ ((:FastMath, :mul_fast, :add_fast, :sub_fast),(:Base,:(*),:(+),:(-)))
    @eval begin
      @inline $mod.$m(::Zero, x::$V) = Zero()
      @inline $mod.$m(::One, x::$V) = x
      @inline $mod.$m(x::$V, ::Zero) = Zero()
      @inline $mod.$m(x::$V, ::One) = x

      @inline $mod.$a(::Zero, x::$V) = x
      @inline $mod.$a(x::$V, ::Zero) = x

      @inline $mod.$s(::Zero, x::$V) = -x
      @inline $mod.$s(x::$V, ::Zero) =  x
    end
  end
end

@inline vload(::StaticInt{N}, args...) where {N} = StaticInt{N}()
@inline stridedpointer(::StaticInt{N}) where {N} = StaticInt{N}()
@inline zero_offsets(::StaticInt{N}) where {N} = StaticInt{N}()


include("BUDE.jl")
using StaticArrays
using Enzyme
using FiniteDifferences
import Base.Experimental: @aliasscope

# Enzyme.API.printperf!(true)
# Enzyme.API.printall!(true)

const Device = (undef, "CPU", "Threaded")

function devices()
  return [Device]
end

const Float32Max = typemax(Float32)

function run(params::Params, deck::Deck, _::DeviceWithRepr)
  println("Using max $(Threads.nthreads()) threads")
  if params.ppwi != DefaultPPWI
    @warn "Threaded implementation only uses wgsize, the PPWI argument is ignored"
  end

  poses = size(deck.poses)[2]
  etotals = Array{Float32}(undef, poses)

  d_protein = fill!(similar(deck.protein), Atom(0f0, 0f0, 0f0, 0))
  d_etotals = fill!(similar(etotals), 0)
  # warmup
  if params.enzyme
     d_etotals[1] = 1
     @time autodiff(fasten_main, Const,
        Val(convert(Int, params.wgsize)),
        Duplicated(deck.protein, d_protein),
        Const(deck.ligand),
        Const(deck.forcefield),
        Const(deck.poses),
        Duplicated(etotals, d_etotals)
      )
      if params.verify

        etotals0 = Array{Float32}(undef, poses)
	fasten_main(
	      Val(convert(Int, params.wgsize)),
	      deck.protein,
	      deck.ligand,
	      deck.forcefield,
	      deck.poses,
	      etotals0,
       )
        function fwdwrap(x)
           tmp = copy(deck.protein)
	   tmp[13] = Atom(x, tmp[13].y, tmp[13].z, tmp[13].type)
	   fetotals = Array{Float32}(undef, poses)
           fasten_main(Val(convert(Int, params.wgsize)), tmp, deck.ligand, deck.forcefield, deck.poses, fetotals)
           fetotals[1]
        end
	fd = FiniteDifferences.central_fdm(5, 1)(fwdwrap, deck.protein[13].x)
	@show d_protein[13], fd
      end
  else
    @time fasten_main(
      Val(convert(Int, params.wgsize)),
      deck.protein,
      deck.ligand,
      deck.forcefield,
      deck.poses,
      etotals,
    )
  end

  if params.enzyme
    elapsed = @elapsed for _ = 1:params.iterations
      autodiff(fasten_main, Const,
        Val(convert(Int, params.wgsize)),
        Duplicated(deck.protein, d_protein),
        Const(deck.ligand),
        Const(deck.forcefield),
        Const(deck.poses),
        Duplicated(etotals, d_etotals)
      )
    end
  else
    elapsed = @elapsed for _ = 1:params.iterations
      fasten_main(
        Val(convert(Int, params.wgsize)),
        deck.protein,
        deck.ligand,
        deck.forcefield,
        deck.poses,
        etotals,
      )
    end
  end

  (etotals, elapsed, params.wgsize)
end

@eval @fastmath function fasten_main(
  ::Val{WGSIZE},
  protein::Array{Atom},
  ligand::Array{Atom},
  forcefield::Array{FFParams},
  poses::Array{Float32,2},
  etotals::Array{Float32}, # Only output
) where {WGSIZE}
  nposes::Int = size(poses)[2]
  numGroups::Int = nposes ÷ WGSIZE

  Enzyme.@parallel protein ligand forcefield poses etotals for group = 1:numGroups
    protein = Base.Experimental.Const(protein)
    ligand = Base.Experimental.Const(ligand)
    forcefield = Base.Experimental.Const(forcefield)
    poses = Base.Experimental.Const(poses)
    nligand::Int = length(ligand)
    nprotein::Int = length(protein)
    etot = MArray{Tuple{WGSIZE}, Float32}(undef)
    transform = MArray{Tuple{WGSIZE, 3, 4},Float32}(undef)

    @aliasscope @simd ivdep for i = 1:WGSIZE
    # for i = 1:WGSIZE
      ix = (group - 1) * (WGSIZE) + i
      @inbounds sx::Float32 = sin(poses[1, ix])
      @inbounds cx::Float32 = cos(poses[1, ix])
      @inbounds sy::Float32 = sin(poses[2, ix])
      @inbounds cy::Float32 = cos(poses[2, ix])
      @inbounds sz::Float32 = sin(poses[3, ix])
      @inbounds cz::Float32 = cos(poses[3, ix])
      @inbounds transform[i, 1, 1] = cy * cz
      @inbounds transform[i, 1, 2] = sx * sy * cz - cx * sz
      @inbounds transform[i, 1, 3] = cx * sy * cz + sx * sz
      @inbounds transform[i, 1, 4] = poses[4, ix]
      @inbounds transform[i, 2, 1] = cy * sz
      @inbounds transform[i, 2, 2] = sx * sy * sz + cx * cz
      @inbounds transform[i, 2, 3] = cx * sy * sz - sx * cz
      @inbounds transform[i, 2, 4] = poses[5, ix]
      @inbounds transform[i, 3, 1] = -sy
      @inbounds transform[i, 3, 2] = sx * cy
      @inbounds transform[i, 3, 3] = cx * cy
      @inbounds transform[i, 3, 4] = poses[6, ix]
      @inbounds etot[i] = Zero
      # $(Expr(:loopinfo, Symbol("julia.simdloop"), (Symbol("llvm.loop.unroll.full"),)))
    end

    for il = 1:nligand
      @inbounds l_atom::Atom = ligand[il]

      @inbounds l_params::FFParams = forcefield[l_atom.type+1]
      lhphb_ltz::Bool = l_params.hphb < Zero
      lhphb_gtz::Bool = l_params.hphb > Zero

      lpos = MArray{Tuple{WGSIZE, 3}, Float32}(undef)

      @simd ivdep for i = 1:WGSIZE
      # for i = 1:WGSIZE
        @inbounds lpos[i, 1] = (
          transform[i, 1, 4] +
          l_atom.x * transform[i, 1, 1] +
          l_atom.y * transform[i, 1, 2] +
          l_atom.z * transform[i, 1, 3]
        )
        @inbounds lpos[i, 2] = (
          transform[i, 2, 4] +
          l_atom.x * transform[i, 2, 1] +
          l_atom.y * transform[i, 2, 2] +
          l_atom.z * transform[i, 2, 3]
        )
        @inbounds lpos[i, 3] = (
          transform[i, 3, 4] +
          l_atom.x * transform[i, 3, 1] +
          l_atom.y * transform[i, 3, 2] +
          l_atom.z * transform[i, 3, 3]
        )
        # $(Expr(:loopinfo, Symbol("julia.simdloop"), (Symbol("llvm.loop.unroll.full"),)))
      end

      @aliasscope for ip = 1:nprotein
        @inbounds p_atom = protein[ip]
        @inbounds p_params::FFParams = forcefield[p_atom.type+1]

        radij::Float32 = p_params.radius + l_params.radius
        r_radij::Float32 = One / radij

        elcdst::Float32 = (p_params.hbtype == HbtypeF && l_params.hbtype == HbtypeF) ? Four : Two
        elcdst1::Float32 =
          (p_params.hbtype == HbtypeF && l_params.hbtype == HbtypeF) ? Quarter : Half
        type_E::Bool = ((p_params.hbtype == HbtypeE || l_params.hbtype == HbtypeE))

        phphb_ltz::Bool = p_params.hphb < Zero
        phphb_gtz::Bool = p_params.hphb > Zero
        phphb_nz::Bool = p_params.hphb != Zero
        p_hphb::Float32 = p_params.hphb * (phphb_ltz && lhphb_gtz ? -One : One)
        l_hphb::Float32 = l_params.hphb * (phphb_gtz && lhphb_ltz ? -One : One)
        distdslv::Float32 =
          (phphb_ltz ? (lhphb_ltz ? Npnpdist : Nppdist) : (lhphb_ltz ? Nppdist : -Float32Max))
        r_distdslv::Float32 = One / distdslv

        chrg_init::Float32 = l_params.elsc * p_params.elsc
        dslv_init::Float32 = p_hphb + l_hphb

        @simd ivdep for i = 1:WGSIZE
        # for i = 1:WGSIZE
          @inbounds x::Float32 = lpos[i, 1] - p_atom.x
          @inbounds y::Float32 = lpos[i, 2] - p_atom.y
          @inbounds z::Float32 = lpos[i, 3] - p_atom.z

          distij::Float32 = sqrt(x ^ 2 + y ^ 2+ z ^ 2)

          # Calculate the sum of the sphere radii
          distbb::Float32 = distij - radij
          zone1::Bool = (distbb < Zero)

          # Calculate steric energy
          @inbounds etot[i] += (One - (distij * r_radij)) * (zone1 ? Two * Hardness : Zero)

          # Calculate formal and dipole charge interactions
          chrg_e::Float32 =
            chrg_init * ((zone1 ? One : (One - distbb * elcdst1)) * (distbb < elcdst ? One : Zero))
          neg_chrg_e::Float32 = -abs(chrg_e)
          chrg_e = type_E ? neg_chrg_e : chrg_e
          @inbounds etot[i] += chrg_e * Cnstnt

          # Calculate the two cases for Nonpolar-Polar repulsive interactions
          coeff::Float32 = (One - (distbb * r_distdslv))
          dslv_e::Float32 = dslv_init * ((distbb < distdslv && phphb_nz) ? One : Zero)
          dslv_e *= (zone1 ? One : coeff)
          @inbounds etot[i] += dslv_e
          # $(Expr(:loopinfo, Symbol("julia.simdloop"), (Symbol("llvm.loop.unroll.full"),)))
        end
      end
    end
    @simd ivdep for i = 1:WGSIZE
    # for i = 1:WGSIZE
      ix = (group - 1) * WGSIZE + i
      @inbounds etotals[ix] = etot[i] * Half
      # $(Expr(:loopinfo, Symbol("julia.simdloop"), (Symbol("llvm.loop.unroll.full"),)))
    end
  end
end

main()


# An IntervalCollection is an efficiently stored and indexed set of annotated genomic intervals.
# It looks something like this.
#
#                                      ┌─────┐
#                                      │trees│
#                                      └─────┘
#                                         │
#                              ┌──────────┴──────────┬────────────┐
#                              ▼                     ▼            │
#  Each sequence has       ┌──────┐              ┌──────┐         ▼
#    an associated         │ chr1 │              │ chr2 │
#    IntervalTree     ┌────┴──────┴────┐    ┌────┴──────┴────┐    ...
#                     │ IntervalTree 1 │    │ IntervalTree 2 │
#                     └────────────────┘    └────────────────┘
#                              │
#                    ┌─────────┴─────────────────────────┬────────────────────────┐
#                    │                                   │                        │
#                    ▼                                   ▼                        ▼
#   ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓  ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
#   ┃ Interval{T}(10000, 20000, ...) ┃  ┃ Interval{T}(35000, 40000, ...) ┃      ...
#   ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛  ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
#
#
#
#
#                               ┌────────────────┐
#       ordered_trees holds     │ IntervalTree 1 │
#         an array of the       ├────────────────┤
#       some IntervalTrees,     │ IntervalTree 2 │
#           ordered by          └────────────────┘
#       chromosome for fast
#       ordered iteration.             ...
#

# Aliases for types of IntervalTrees.jl (IC: Interval Collection).
const ICTree{T}                               = IntervalTrees.IntervalBTree{Int64,Interval{T},64}
const ICTreeIteratorState{T}                  = IntervalTrees.IntervalBTreeIteratorState{Int64,Interval{T},64}
const ICTreeIntersection{T}                   = IntervalTrees.Intersection{Int64,Interval{T},64}
const ICTreeIntersectionIterator{F,S,T}       = IntervalTrees.IntersectionIterator{F,Int64,Interval{S},64,Interval{T},64}
const ICTreeIntervalIntersectionIterator{F,T} = IntervalTrees.IntervalIntersectionIterator{F, Int64,Interval{T},64}

"An IntervalCollection is an efficiently stored and indexed set of annotated genomic intervals."
mutable struct IntervalCollection{T}
    # Sequence name mapped to IntervalTree, which in turn maps intervals to a list of metadata.
    trees::Dict{String,ICTree{T}}

    # Keep track of the number of stored intervals.
    length::Int

    # A vector of values(trees) sorted on sequence name.
    # This is used to iterate intervals as efficiently as possible, but is only updated as needed, indicated by the ordered_trees_outdated flag.
    ordered_trees::Vector{ICTree{T}}
    ordered_trees_outdated::Bool

    "Empty initaialzation."
    function IntervalCollection{T}() where T
        return new{T}(Dict{String,ICTree{T}}(), 0, ICTree{T}[], false)
    end

    "Bulk insertion."
    function IntervalCollection{T}(intervals::AbstractVector{Interval{T}}, sort::Bool=false) where T
        if sort
            sort!(intervals)
        else
            if !issorted(intervals)
                error("Intervals must be sorted, or `sort=true` set, to construct an IntervalCollection")
            end
        end

        n = length(intervals)
        trees = Dict{String,ICTree{T}}()
        i = 1
        while i <= n
            j = i
            while j <= n && seqname(intervals[i]) == seqname(intervals[j])
                j += 1
            end
            trees[seqname(intervals[i])] = ICTree{T}(view(intervals, i:j-1))
            i = j
        end
        return new{T}(trees, n, ICTree{T}[], true)
    end
end

"""
    IntervalCollection(intervals::AbstractVector{Interval{T}}, sort::Bool=false) where T
Shorthand constructor.
"""
function IntervalCollection(intervals::AbstractVector{Interval{T}}, sort::Bool=false) where T
    return IntervalCollection{T}(intervals, sort)
end

"""
    IntervalCollection{T}(data, sort::Bool=false) where T
Constructor that offers conversion through collection.
"""
function IntervalCollection{T}(data, sort::Bool=false) where T
    return IntervalCollection(collect(Interval{T}, data), sort)
end

"""
    IntervalCollection(data, sort::Bool=false)
Constructor that guesses metadatatype, and offers conversion through collection.
"""
function IntervalCollection(data, sort::Bool=false)
    return IntervalCollection(collect(Interval{metadatatype(data)}, data), sort)
end

function update_ordered_trees!(ic::IntervalCollection{T}) where T
    if ic.ordered_trees_outdated
        ic.ordered_trees = collect(ICTree{T}, values(ic.trees))
        p = sortperm(collect(AbstractString, keys(ic.trees)), lt = isless)
        ic.ordered_trees = ic.ordered_trees[p]
        ic.ordered_trees_outdated = false
    end
end

function Base.push!(ic::IntervalCollection{T}, i::Interval{T}) where T
    tree = get!(ic.trees, seqname(i)) do
        # Setup empty tree for new seqname key.
        ic.ordered_trees_outdated = true
        return ICTree{T}()
    end
    push!(tree, i)
    ic.length += 1
    return ic
end

function Base.show(io::IO, ic::IntervalCollection{T}) where T
    n_entries = length(ic)
    println(io, "IntervalCollection{$(T)} with $(n_entries) intervals:")
    if n_entries > 0
        for (k, i) in enumerate(ic)
            if k > 8
                break
            end
            println(IOContext(io, :compact=>true), "  ", i)
        end
        if n_entries > 8
            print(io, "  ⋮")
        end
    end
end

function Base.length(ic::IntervalCollection)
    return ic.length
end

function Base.eltype(::Type{IntervalCollection{T}}) where T
    return Interval{T}
end

function Base.:(==)(a::IntervalCollection, b::IntervalCollection)
    if length(a) != length(b)
        return false
    end

    # Return false if any of the intervals are not equivalent.
    return all(zip(a,b)) do (ia,ib)
        ia == ib
    end
end

# Iterators
# ---------

#=
mutable struct IntervalCollectionIteratorState{T}
    i::Int # index into ordered_trees
    tree_state::ICTreeIteratorState{T}

    function IntervalCollectionIteratorState{T}(i::Int) where T
        return new{T}(i)
    end

    function IntervalCollectionIteratorState{T}(i::Int, tree_state) where T
        return new{T}(i, tree_state)
    end
end

function iterinitstate(ic::IntervalCollection{T}) where T
    update_ordered_trees!(ic)
    i = 1
    while i <= length(ic.ordered_trees)
        tree_state = IntervalTrees.iterinitstate(ic.ordered_trees[i])
        if !(tree_state.leaf === nothing || isempty(tree_state.leaf))
            return IntervalCollectionIteratorState{T}(i, tree_state)
        end
        i += 1
    end
    return IntervalCollectionIteratorState{T}(i)
end

function Base.start(ic::IntervalCollection{T}) where T
    update_ordered_trees!(ic)
    i = 1
    while i <= length(ic.ordered_trees)
        tree_state = start(ic.ordered_trees[i])
        if !done(ic.ordered_trees[i], tree_state)
            return IntervalCollectionIteratorState{T}(i, tree_state)
        end
        i += 1
    end
    return IntervalCollectionIteratorState{T}(i)
end
=#

#=
function Base.iterate(ic::IntervalCollection, state=iterinitstate(ic))
    i = state.i
    treeit = iterate(ic.ordered_trees[i], state.tree_state)
    if treeit === nothing
        i += 1
        while i <= length(ic.ordered_trees)
            tree_state = IntervalTrees.iterinitstate(ic.ordered_trees[i])
            if !(tree_state.leaf === nothing || isempty(tree_state.leaf))
                break
            end
            i += 1
        end
    end
    state.i, state.tree_state = i, tree_state
    return value, state
end
=#

function iterprep(ic::IntervalCollection)
    update_ordered_trees!(ic)
    return ()
end

# State is a tuple:
# (ot iteration state, current ot element, current ot element state)
# "ot" is shorthand for "ordered_trees"
# This iterate method basically works like the Iterators.Flatten iterator.
@propagate_inbounds function Base.iterate(ic::IntervalCollection, state = iterprep(ic))
    if state !== ()
        # Iterate over each interval in current ordered tree.
        tree_it = iterate(Base.tail(state)...)
        tree_it !== nothing && return (tree_it[1], (state[1], state[2], tree_it[2]))
    end
    # Iterate over ordered_trees.
    ot_it = (state === () ? iterate(ic.ordered_trees) : iterate(ic.ordered_trees, state[1]))
    ot_it === nothing && return nothing
    iterate(ic, (ot_it[2], ot_it[1]))
end

#=
function Base.next(ic::IntervalCollection, state)
    i = state.i
    value, tree_state = next(ic.ordered_trees[i], state.tree_state)
    if done(ic.ordered_trees[i], tree_state)
        i += 1
        while i <= length(ic.ordered_trees)
            tree_state = start(ic.ordered_trees[i])
            if !done(ic.ordered_trees[i], tree_state)
                break
            end
            i += 1
        end
    end
    state.i, state.tree_state = i, tree_state
    return value, state
end

function Base.done(ic::IntervalCollection, state)
    return state.i > length(ic.ordered_trees)
end
=#

# Filter predicates
# -----------------

true_cmp(a, b) = true

eachoverlap_equal_filter(a, b) = first(a) == first(b) && last(a) == last(b)

# TODO: other prebaked filter predicates

"""
Find a the first interval with matching start and end points.

Returns that interval, or 'nothing' if no interval was found.
"""
function Base.findfirst(a::IntervalCollection, b::Interval; filter=true_cmp)
    if !haskey(a.trees, seqname(b))
        return nothing
    end

    return findfirst(a.trees[seqname(b)], b, filter)
end

# Overlaps
# --------

function eachoverlap(a::IntervalCollection{T}, query::Interval; filter::F = true_cmp) where {F,T}
    if haskey(a.trees, seqname(query))
        return ICTreeIntervalIntersectionIterator{F,T}(filter, ICTreeIntersection{T}(), a.trees[seqname(query)], query)
    end

    return ICTreeIntervalIntersectionIterator{F,T}(filter, ICTreeIntersection{T}(), ICTree{T}(), query)
end

function eachoverlap(a::IntervalCollection, b::IntervalCollection; filter = true_cmp)
    seqnames = collect(AbstractString, keys(a.trees) ∩ keys(b.trees))
    sort!(seqnames, lt = isless)
    a_trees = [a.trees[seqname] for seqname in seqnames]
    b_trees = [b.trees[seqname] for seqname in seqnames]
    return IntersectIterator(filter, a_trees, b_trees)
end

struct IntersectIterator{F, S, T}
    filter::F
    a_trees::Vector{ICTree{S}}
    b_trees::Vector{ICTree{T}}
end

#=
mutable struct IntersectIteratorState{F,S,T}
    i::Int  # index into a_trees/b_trees.
    intersect_iterator::ICTreeIntersectionIterator{F,S,T}

    function IntersectIteratorState{F,S,T}(i) where {F,S,T}
        return new{F,S,T}(i)
    end

    function IntersectIteratorState{F,S,T}(i, iter) where {F,S,T}
        return new{F,S,T}(i, iter)
    end
end
=#

function Base.eltype(::Type{IntersectIterator{F,S,T}}) where {F,S,T}
    return Tuple{Interval{S},Interval{T}}
end

function Base.IteratorSize(::Type{IntersectIterator{F,S,T}}) where {F,S,T}
    return Base.SizeUnknown()
end

#=
function Base.start(it::IntersectIterator{F,S,T}) where {F,S,T}
    i = 1
    while i <= length(it.a_trees)
        intersect_iterator = intersect(it.a_trees[i], it.b_trees[i], it.filter)
        intersect_iterator_state = start(intersect_iterator)
        if !done(intersect_iterator, intersect_iterator_state)
            return IntersectIteratorState{F,S,T}(i, intersect_iterator)
        end
        i += 1
    end
    return IntersectIteratorState{F,S,T}(i)
end
=#

# State is a tuple:
# (tree pair iteration state, current intersect iterator, current intersect iterator state)
function Base.iterate(it::IntersectIterator{F, S, T}, state = ()) where {F,S,T}
    if state !== ()
        # Iterate over each intersection between two trees.
        isect = iterate(Base.tail(state)...)
        isect !== nothing && return (isect[1], (state[1], state[2], isect[2]))
    end
    # Iterate over tree pairs.
    treeA = (state === () ? iterate(it.a_trees) : iterate(it.a_trees, state[1]))
    treeB = (state === () ? iterate(it.b_trees) : iterate(it.b_trees, state[1]))
    treeA === nothing && return nothing
    intersect_iterator = intersect(treeA[1], treeB[1], it.filter)
    iterate(it, (treeA[2], intersect_iterator))
end

#=
function Base.next(it::IntersectIterator{F, S, T}, state) where {F,S,T}
    i, intersect_iterator = state.i, state.intersect_iterator
    value, intersect_iterator_state = next(intersect_iterator, nothing)
    if done(intersect_iterator, intersect_iterator_state)
        i += 1
        while i <= length(it.a_trees)
            intersect_iterator = intersect(it.a_trees[i], it.b_trees[i],
                                           it.filter)
            intersect_iterator_state = start(intersect_iterator)
            if !done(intersect_iterator, intersect_iterator_state)
                break
            end
            i += 1
        end
    end
    state.i, state.intersect_iterator = i, intersect_iterator
    return value, state
end

function Base.done(it::IntersectIterator{F, S, T}, state) where {F, S, T}
    return state.i > length(it.a_trees)
end
=#

function eachoverlap(a, b::IntervalCollection; filter=true_cmp)
    return IntervalCollectionStreamIterator(filter, a, b)
end

struct IntervalCollectionStreamIterator{F,S,T}
    filter::F
    stream::S
    collection::IntervalCollection{T}
end

function Base.eltype(::Type{IntervalCollectionStreamIterator{F,S,T}}) where {F,S,T}
    return Tuple{Interval{metadatatype(S)},Interval{T}}
end

function Base.IteratorSize(::Type{IntervalCollectionStreamIterator{F,S,T}}) where {F,S,T}
    return Base.SizeUnknown()
end

#= ### The old iteration protocol prior to version 0.7 / 1.0 of julia.

mutable struct IntervalCollectionStreamIteratorState{F,Ta,Tb,U}
    intersection::ICTreeIntersection{Tb}
    stream_value::Interval{Ta}
    stream_state::U

    function IntervalCollectionStreamIteratorState{F,Ta,Tb,U}(intersection, stream_value, stream_state) where {F,Ta,Tb,U}
        return new{F,Ta,Tb,U}(intersection, stream_value, stream_state)
    end

    function IntervalCollectionStreamIteratorState{F,Ta,Tb,U}() where {F,Ta,Tb,U}
        return new{F,Ta,Tb,U}(ICTreeIntersection{Tb}())
    end
end

# This mostly follows from SuccessiveTreeIntersectionIterator in IntervalTrees
function Base.start(it::IntervalCollectionStreamIterator{F,S,T}) where {F,S,T}
    stream_state = start(it.stream)
    intersection = ICTreeIntersection{T}()
    while !done(it.stream, stream_state)
        stream_value, stream_state = next(it.stream, stream_state)
        if haskey(it.collection.trees, seqname(stream_value))
            tree = it.collection.trees[seqname(stream_value)]
            IntervalTrees.firstintersection!(tree, stream_value, nothing, intersection, it.filter)
            if intersection.index != 0
                return IntervalCollectionStreamIteratorState{F,T,metadatatype(it.stream),typeof(stream_state)}(intersection, stream_value, stream_state)
            end
        end
    end
    return IntervalCollectionStreamIteratorState{S,metadatatype(it.stream),typeof(stream_state)}()
end

function Base.next(it::IntervalCollectionStreamIterator{F,S,T}, state) where {F,S,T}
    intersection = state.intersection
    entry = intersection.node.entries[intersection.index]
    return_value = (state.stream_value, entry)
    IntervalTrees.nextintersection!(intersection.node, intersection.index, state.stream_value, intersection, it.filter)
    while intersection.index == 0 && !done(it.stream, state.stream_state)
        state.stream_value, state.stream_state = next(it.stream, state.stream_state)
        if haskey(it.b.trees, seqname(state.stream_value))
            tree = it.b.trees[seqname(state.stream_value)]
            IntervalTrees.firstintersection!(tree, state.stream_value, nothing, intersection, it.filter)
        end
    end
    return return_value, state
end

function Base.done(it::IntervalCollectionStreamIterator, state)
    return state.intersection.index == 0
end
=#

# New julia 0.7 / 1.0 iteration protocol for collection stream iterator.
# State is a tuple:
# (current_query, stream_state, intersection_object)
function Base.iterate(it::IntervalCollectionStreamIterator{F,S,T}, state = ()) where {F,S,T}
    # If first iteration, make empty intersection, otherwise get it from the state.
    intersection = (state !== () ? state[3] : ICTreeIntersection{T}())

    # If this is not the first iteration, and there is an available intersection for the current query, return it and search for the next intersection.
    if state !== () && intersection.index != 0
        entry = intersection.node.entries[intersection.index]
        return_value = (state[1], entry)
        IntervalTrees.nextintersection!(intersection.node, intersection.index, state[1], intersection, it.filter)
        return return_value, (state[1], state[2], intersection)
    end

    # If code reaches this point, there is no valid intersection to return for the current query, so we get the next query and start looking for intersections.
    while intersection.index == 0
        # Get a new query from the stream, and its first intersection in the collection.
        stream_it = (state === () ? iterate(it.stream) : iterate(it.stream, state[2]))
        stream_it === nothing && return nothing # You have reached the end of the stream, stop iterating.
        stream_value = stream_it[1]
        if haskey(it.collection.trees, seqname(stream_value))
            tree = it.collection.trees[seqname(stream_value)]
            IntervalTrees.firstintersection!(tree, stream_value, nothing, intersection, it.filter)
            iterate_result = iterate(it, (stream_value, stream_it[2], intersection))
            if iterate_result !== nothing
                return iterate_result
            end
        end
    end
end


using Chess
function testMove(b::Board, m::Move)
        negamax(b, 3, true; evalFunc = LichessBot.reallySimpleEval) == m
end
function testMove(fen::AbstractString, m::Move)
        testMove(fromfen(fen), m)
end
function testMove(b, m::AbstractString)
        testMove(b, movefromstring(m))
end
boards = [
("5k2/pR4R1/3p3p/6p1/4N3/8/PPr2PPP/7K b - - 2 26", "c2c1"),
("rnbk4/pppp2b1/4pr2/P4p1Q/1q3P2/2N5/3R2P1/2B1KB1R b - - 2 28", "b4c3"),
("rnbk1b1Q/pppp4/4pr2/P4p2/5P2/2q5/3R2P1/2B1KB1R w - - 2 30", "c1b2"),
("rnb1kbr1/pppp1ppp/4p3/8/1qP4Q/1P6/P2PNPPP/RNB1KB1R w KQq - 3 8", "h4h7"),
("1r1kr3/1pp4p/7b/p2b1pp1/3B1PP1/PPnP2Q1/R2N1K1P/5BNR b - - 7 26", "c3d1")
]
@testset "Evaluation" begin
        for (bs, ms) in boards
                b = fromfen(bs)
                @test LichessBot.negamax(b, 3, true) == movefromstring(ms)
        end
        #=begin
                local b = startboard()
                @test !isnothing(LichessBot.oldChooseMove(b))
        end
        begin
                local b = fromfen(
                        "5k2/pR4R1/3p3p/6p1/4N3/8/PPr2PPP/7K b - - 2 26",
                )
                @test LichessBot.negamax(b, 3, true) == Move(SQ_C2, SQ_C1)
        end
        begin
                local b = fromfen(
                        "rnbk4/pppp2b1/4pr2/P4p1Q/1q3P2/2N5/3R2P1/2B1KB1R b - - 2 28",
                )
                @test negamax(b, 3, true) == Move(SQ_B4, SQ_C3)
        end
        begin
                local b = fromfen(
                        "rnbk1b1Q/pppp4/4pr2/P4p2/5P2/2q5/3R2P1/2B1KB1R w - - 2 30",
                )
                @test negamax(
                        b,
                        3,
                        true;
                        evalFunc = LichessBot.reallySimpleEval,
                ) == movefromstring("c1b2")
        end
        begin
                local b = fromfen(
                        "rnb1kbr1/pppp1ppp/4p3/8/1qP4Q/1P6/P2PNPPP/RNB1KB1R w KQq - 3 8",
                )
                @test negamax(
                        b,
                        3,
                        true;
                        evalFunc = LichessBot.reallySimpleEval,
                ) == movefromstring("h4h7")
        end
        begin
                local b = fromfen("1r1kr3/1pp4p/7b/p2b1pp1/3B1PP1/PPnP2Q1/R2N1K1P/5BNR b - - 7 26")
                @test negamax(
                b, 1, true
                ) == movefromstring("c3d1")
        end=#
        for (bs, ms) in boards
                b = fromfen(bs)
                @test negamax(b, 3, true) == negamax(b, 3, true; AB=false)
                @test negamax(b, 3) == negamax(b, 3; AB=false)
        end
end


"""
Holidays for Canada
"""
struct CanadaSettlement <: HolidayCalendar end
const Canada = CanadaSettlement

"""
Holidays for Toronto Stock Exchange
"""
struct CanadaTSX <: HolidayCalendar end

function isholiday(::CanadaSettlement, dt::Dates.Date)

    yy = Dates.year(dt)
    mm = Dates.month(dt)
    dd = Dates.day(dt)
    ww = Dates.dayofweek(dt)

    # Bisection
    if mm >= 8
        # Fixed holidays
        if (
                # first Monday of August (Provincial Holiday)
                findweekday(Dates.Monday, yy, 8, 1, true)  == dt
                # first Monday of September (Labor Day)
                || findweekday(Dates.Monday, yy, 9, 1, true)  == dt
                # second Monday of October (Thanksgiving Day)
                || findweekday(Dates.Monday, yy, 10, 2, true) == dt
                # November 11th (possibly moved to Monday)
                || adjustweekendholidayPost(Dates.Date(yy, 11, 11)) == dt
                # Christmas
                || adjustweekendholidayPost( Dates.Date(yy, 12, 25) ) == dt
                # Boxing
                || adjustweekendholidayPost(adjustweekendholidayPost( Dates.Date(yy, 12, 25) ) + Dates.Day(1)) == dt
            )
            return true
        end
    else
        # mm < 8
        # Fixed holidays
        if (
                # New Year's Day
                adjustweekendholidayPost(Dates.Date(yy, 01, 01); adjust_saturdays=false) == dt
                # Family Day (third Monday in February, since 2008)
                || ( yy >= 2008 && findweekday(Dates.Monday, yy, 2, 3, true) == dt )
                # The Monday on or preceding 24 May (Victoria Day)
                || (dd > 17 && dd <= 24 && ww == Dates.Monday && mm == Dates.May)
                # July 1st, possibly moved to Monday (Canada Day)
                || adjustweekendholidayPost(Dates.Date(yy, 07, 01)) == dt
          )
            return true
        end

        # Easter occurs up to April, which is before August (mm < 8). See test/easter-min-max.jl .
        # Holidays based on easter date
        dt_rata::Int = Dates.days(dt)
        e_rata::Int = easter_rata(Dates.Year(yy))

        if (
                # Good Friday
                ( dt_rata == ( e_rata - 2 ) )
            )
            return true
        end
    end

    return false
end

function isholiday(::CanadaTSX, dt::Dates.Date)

    yy = Dates.year(dt)
    mm = Dates.month(dt)
    dd = Dates.day(dt)
    ww = Dates.dayofweek(dt)

    # Bisection
    if mm >= 8
        # Fixed holidays
        if (
                # first Monday of August (Provincial Holiday)
                findweekday(Dates.Monday, yy, 8, 1, true)  == dt
                # first Monday of September (Labor Day)
                || findweekday(Dates.Monday, yy, 9, 1, true)  == dt
                # second Monday of October (Thanksgiving Day)
                || findweekday(Dates.Monday, yy, 10, 2, true) == dt
                # Christmas
                || adjustweekendholidayPost( Dates.Date(yy, 12, 25) ) == dt
                # Boxing
                || adjustweekendholidayPost(adjustweekendholidayPost( Dates.Date(yy, 12, 25) ) + Dates.Day(1)) == dt
            )
            return true
        end
    else
        # mm < 8
        # Fixed holidays
        if (
                # New Year's Day
                adjustweekendholidayPost(Dates.Date(yy, 01, 01); adjust_saturdays=false) == dt
                # Family Day (third Monday in February, since 2008)
                || ( yy >= 2008 && findweekday(Dates.Monday, yy, 2, 3, true) == dt )
                # The Monday on or preceding 24 May (Victoria Day)
                || (dd > 17 && dd <= 24 && ww == Dates.Monday && mm == Dates.May)
                # July 1st, possibly moved to Monday (Canada Day)
                || adjustweekendholidayPost(Dates.Date(yy, 07, 01)) == dt

          )
            return true
        end

        # Easter occurs up to April, which is before August (mm < 8). See test/easter-min-max.jl .
        # Holidays based on easter date
        dt_rata::Int = Dates.days(dt)
        e_rata::Int = easter_rata(Dates.Year(yy))

        if (
                # Good Friday
                ( dt_rata == ( e_rata - 2 ) )
            )
            return true
        end
    end

    return false
end


# Run TMscore from command line - see https://zhanglab.ccmb.med.umich.edu/TM-score

export
    tmscore,
    tmscorepathvalid,
    fractionbetweeninputs

"""
TM-score of model and reference, given as filepaths.
Uses external command line program - see https://zhanglab.ccmb.med.umich.edu/TM-score.
"""
function tmscore(model::AbstractString,
                    reference::AbstractString;
                    command::AbstractString=defaults["tmscore_path"])
    @assert isfile(model) "Model filepath not valid: \"$model\""
    @assert isfile(reference) "Reference filepath not valid: \"$reference\""
    tmscore_output = read(pipeline(`$command $model $reference`), String)
    # This is easier using grep and sed but they are system-specific
    reg_match = match(r"\nTM-score    = \d\.\d\d\d\d", tmscore_output)
    @assert reg_match != nothing "TMscore failed or the result could not be read"
    return parse(Float64, reg_match.match[16:end])
end

"Check a command runs TMscore."
function tmscorepathvalid(command::AbstractString)
    try
        read(pipeline(`$command`), String)
    catch
        return false
    end
    return true
end

"""
Fraction of structures in an ensemble that are closer to both input structures
than the input structures are to each other.
"""
function fractionbetweeninputs(ens_prefix::AbstractString,
                    n_strucs::Integer,
                    i1::AbstractString,
                    i2::AbstractString;
                    command::AbstractString=defaults["tmscore_path"])
    tmscores_ens_i1 = [tmscore("$ens_prefix.$i", i1; command=command) for i in 1:n_strucs]
    tmscores_ens_i2 = [tmscore("$ens_prefix.$i", i2; command=command) for i in 1:n_strucs]
    tmscore_i1_i2 = tmscore(i1, i2; command=command)
    tmscore_i2_i1 = tmscore(i2, i1; command=command)
    counter = 0
    for i in 1:n_strucs
        if tmscores_ens_i1[i] > tmscore_i2_i1 && tmscores_ens_i2[i] > tmscore_i1_i2
            counter += 1
        end
    end
    return counter / n_strucs
end


abstract type Aircraft end

get_mass_props(ac::Aircraft) = ac.mass_props
get_pfm(ac::Aircraft) = ac.pfm
get_aerodynamics(ac::Aircraft) = ac.aerodynamics
get_propulsion(ac::Aircraft) = ac.propulsion

get_name(ac::Aircraft) = "Generic Aricraft"
get_wing_area(ac::Aircraft) = 0.0  # m²
get_wing_span(ac::Aircraft) = 0.0  # m
get_chord(ac::Aircraft) = 0.0  # m
get_arp(ac::Aircraft) = [0, 0, 0]  # m

get_empty_mass_props(ac::Aircraft) = RigidSolid(0.0, zeros(3), zeros(3, 3))
get_payload_mass_props(ac::Aircraft) = RigidSolid(0.0, zeros(3), zeros(3, 3))

function calculate_aircraft(ac::Aircraft, fcs::FCS, aerostate::AeroState,
                            state::State, grav::Gravity; consume_fuel=false)
    aero = get_aerodynamics(ac)
    prop = get_propulsion(ac)
    mass_props = get_mass_props(ac)

    # Calculate propulsion
    prop = calculate_propulsion(prop, fcs, aerostate, state; consume_fuel=consume_fuel)
    # Calculate aerodynamics
    aero = calculate_aerodynamics(ac, aero, fcs, aerostate, state)
    # Calculate gravity forces
    grav_force = get_gravity_body(grav, get_attitude(state)) * mass_props.mass
    grav_pfm = PointForcesMoments(mass_props.cg, grav_force, [0, 0, 0])

    β = get_beta(aerostate)
    α = get_alpha(aerostate)
    pfm = grav_pfm + get_pfm(prop) + get_pfm(aero)

    # TODO: where to discount fuel consumption?
    # Here and produce an aircraft with a mass and grav forces "not coherent"
    # or before calculating grav forces?
    # Calculate mass properties
    mass_props = (
        get_fuel_mass_props(prop) +
        get_empty_mass_props(ac) +
        get_payload_mass_props(ac)
        )
    # Return aircraft object
    typeof(ac)(mass_props, pfm, aero, prop)
end


@enum MatchEvent noevents matchset roundinit roundwin roundtie matchend tiledraw tiledrop meldcall riichicall doraflip playerdc playerin

    function GO(str::AbstractString, pst::PlayState)

        function getbits(code)
            digits(parse(Int, code); base = 2, pad = 8)
        end

        bits = map(Bool, parsekey(getbits, "type", str))

        (   !bits[8] && !bits[6]    ) && ( lobby = 一般 )
        (    bits[8] && !bits[6]    ) && ( lobby = 上級 )
        (   !bits[8] &&  bits[6]    ) && ( lobby = 特上 )
        (    bits[8] &&  bits[6]    ) && ( lobby = 鳳凰 )

        pst.rules = Rules(!bits[1], !bits[2], !bits[3],
                bits[4], bits[5], bits[7], lobby)

        return noevents
    end

    function UN(str::AbstractString, pst::PlayState)

        namaes = String[]
        for namekey in ("n0", "n1", "n2", "n3")
            name = parsekey((s)->decodeuri(s), namekey, str)
            if !isnothing(name)
                push!(namaes, name)
            end
        end

        if length(namaes) == 1
            nameindex = findfirst(isequal(namaes[1]), pst.table.namaes) - 1
            seatindex = findfirst(isequal(Seat(nameindex)), pst.dced)
            if !isnothing(seatindex) deleteat!(pst.dced, seatindex)
            else push!(pst.dced, Seat(nameindex)) end
            return playerin
        end

        playercount = length(namaes)
        vecranks = Vector{Dan}(undef, playercount)
        vecrates = Vector{Float32}(undef, playercount)
        vecsexes = Vector{Char}(undef, playercount)

        pst.table = Table(namaes,
            splitkey((s)->Dan(parse(Int,s)), "dan", str, vecranks),
            splitkey((s)->parse(Float32,s), "rate", str, vecrates),
            splitkey((s)->(s[1]), "sx", str, vecsexes))
        pst.dced = Seat[]

        pst.hands = [Tiles(undef, 14) for i in 1:playercount]
        pst.melds = [Melds(undef, 12) for i in 1:playercount]
        pst.discard = [Tiles(undef, 32) for i in 1:playercount]
        pst.tedashi = [Tiles(undef, 32) for i in 1:playercount]
        pst.rchtile = [Tiles(undef, 1) for i in 1:playercount]

        pst.scores = Vector{Int32}(undef, playercount)
        pst.status = Vector{State}(undef, playercount)

        return matchset
    end

    function INIT(str::AbstractString, pst::PlayState)

        roundseed = splitkey((s)->(s), "seed", str,
                Vector{String}(undef, 6))
        number, pst.honba, pst.rbets, dice01, dice02 =
                map((s)-> s[1] - '0', roundseed)
        pst.doraid = [Wall[roundseed[end]]]

        pst.turn = 0
        pst.cycle = Round(number)
        pst.rolls = Dice(dice01), Dice(dice02)

        pst.dealer = Seat(parsekey((s)->parse(Int,s), "oya", str))
        pst.player = pst.dealer
        splitkey((s)->parse(Int32,s), "ten", str, pst.scores)

        playercount = length(pst.table.namaes)
        haikey = ("hai0", "hai1", "hai2", "hai3")

        for i in 1:playercount
            fill!(pst.hands[i], missing)
            fill!(pst.melds[i], missing)
            fill!(pst.discard[i], missing)
            fill!(pst.tedashi[i], missing)
            fill!(pst.rchtile[i], missing)
            splitkey(
                (s)->Wall[s], haikey[i],
                    str, pst.hands[i]
            )
        end

        fill!(pst.status, CLOSED)
        pst.result = nothing

        return roundinit
    end

    function AGA(str::AbstractString, pst::PlayState)

        fu, pt, lh = splitkey((s)->parse(Int,s), "ten", str,
                Vector{Int}(undef, 3))

        yaku = Tuple{Yaku,Int}[]

        if occursin("yakuman", str)
            ykm::Vector{Int} = splitkey((s)->parse(Int,s), "yakuman", str)
            for index in range(1, length(ykm); step = 1)
                push!(yaku, (Yaku(ykm[index]), 1))
            end
        else
            yku::Vector{Int} = splitkey((s)->parse(Int,s), "yaku", str)
            for index in range(1, length(yku); step = 2)
                push!(yaku, (Yaku(yku[index]), yku[index + 1]))
            end
        end

        han = mapreduce(x->x[2], +, yaku)

        dora = Tile[]; splitkey((s)->Wall[s], "doraHai", str, dora)
        ura = Tile[]; splitkey((s)->Wall[s], "doraHaiUra", str, ura)

        pst.result = RoundWin(pt, (han, fu), Limit(lh), yaku, dora, ura,
            Seat(parsekey((s)->parse(Int,s), "who", str)),
            Seat(parsekey((s)->parse(Int,s), "fromWho", str))
        )

        sc = splitkey((s)->parse(Int,s), "sc", str, Vector{Int}(undef, 8))
        for i in range(1, Int(length(sc) / 2); step = 1)
            pst.scores[i] = pst.scores[i] + sc[2*i]
        end

        return roundwin
    end

    function RYU(str::AbstractString, pst::PlayState)

        tierule::Ryuukyoku = tsuujou
        reveal = Seat[]

        if occursin("type", str)
            gettierule(s) = Ryuukyoku(findfirst(isequal(s),
                ("yao9", "reach4", "ron3", "kan4", "kaze4", "nm")
            ))
            tierule = parsekey(gettierule, "type", str)
        end

        occursin("hai0", str) && push!(reveal, Seat(0))
        occursin("hai1", str) && push!(reveal, Seat(1))
        occursin("hai2", str) && push!(reveal, Seat(2))
        occursin("hai3", str) && push!(reveal, Seat(3))

        pst.result = RoundTie(tierule, reveal)

        sc = splitkey((s)->parse(Int,s), "sc", str, Vector{Int}(undef, 8))
        for i in range(1, Int(length(sc) / 2); step = 1)
            pst.scores[i] = pst.scores[i] + sc[2*i]
        end

        return roundtie
    end

    function DRAW(str::AbstractString, pst::PlayState, who::Int)

        pst.turn = pst.turn + 1
        pst.player = Seat(who - 1)
        pst.hands[who][end] = Wall[str]

        return tiledraw
    end

    function DROP(str::AbstractString, pst::PlayState, who::Int)

        droppedtile = Wall[str]

        dropindex = findfirst(isequal(missing), pst.discard[who])
        pst.discard[who][dropindex] = droppedtile

        if !isequal(pst.hands[who][end], droppedtile)

            dropindex = findfirst(isequal(missing), pst.tedashi[who])
            pst.tedashi[who][dropindex] = droppedtile

            tileindex = findfirst(isequal(droppedtile), pst.hands[who])
            pst.hands[who][tileindex] = pst.hands[who][end]
        end

        pst.hands[who][end] = missing

        return tiledrop
    end

    function MELD(str::AbstractString, pst::PlayState)

        who = parsekey((s)->parse(Int,s), "who", str) + 1
        code = parsekey((s)->parse(Int,s), "m", str)
        from = mod(who + code & 0x3, 1:4)

        if      (code & 0x04 != 0)  meld = chi(code, who, from)
        elseif  (code & 0x08 != 0)  meld = pon(code, who, from)
        elseif  (code & 0x10 != 0)  meld = kkn(code, who, from)
        elseif  (code & 0x20 != 0)  meld = pei(code, who, from)
        else    #= code unknown =#  meld = kan(code, who, from)
        end

        meldindex = findfirst(isequal(missing), pst.melds[who])
        pst.player = Seat(who - 1)
        pst.melds[who][meldindex] = meld

        for tile in meld.with
            tileindex = findfirst(isequal(tile), pst.hands[who])
            pst.hands[who][tileindex] = missing
        end

        if !ismissing(pst.hands[who][end])
            tileindex = findfirst(isequal(meld.tile), pst.hands[who])
            pst.hands[who][tileindex] = pst.hands[who][end]
            pst.hands[who][end] = missing
        end

        if (meld.play != 暗槓)
            pst.status[who] = OPENED
        end

        return meldcall
    end

    function RCH(str::AbstractString, pst::PlayState)

        who = parsekey((s)->parse(Int,s), "who", str) + 1

        if parsekey((s)->parse(Int,s), "step", str) == 1
            pst.status[who] = RIICHI
        else
            pst.scores[who] -= 10
            tileindex = findfirst(isequal(missing), pst.discard[who])
            pst.rchtile[who][begin] = pst.discard[who][tileindex-1]
        end

        return riichicall
    end

    function DORA(str::AbstractString, pst::PlayState)

        push!(pst.doraid, parsekey((s)->Wall[s], "hai", str))

        return doraflip
    end

    function BYE(str::AbstractString, pst::PlayState)

        seat = Seat(parsekey((s)->parse(Int,s), "who", str))
        dcedindex = findfirst(isequal(seat), pst.dced)
        if isnothing(dcedindex) push!(pst.dced, seat)
        else deleteat!(pst.dced, dcedindex) end

        return playerdc
    end

    function SKIP(str::AbstractString, pst::PlayState)

        return noevents
    end

    function END(str::AbstractString, pst::PlayState)

        return matchend
    end


using Pkg;Pkg.activate(@__DIR__)
using Comrade
using PyCall
using Plots

using ComradeAHMC

load_ehtim()
@pyimport eht_dmc as ed

# To download the data visit https://doi.org/10.25739/g85n-f134
obs = ehtim.obsdata.load_uvfits(joinpath(@__DIR__, "SR1_M87_2017_096_hi_hops_netcal_StokesI.uvfits"))
obs.add_scans()

obsavg = obs.avg_coherent(0.0, scan_avg=true)

img = ehtim.image.make_empty(512, μas2rad(250.0), obs.ra, obs.dec, obs.rf, obs.source)
img = img.add_gauss(1.0, [μas2rad(40.0), μas2rad(30.0), π/3, 0.0, 0.0])

obslist = obsavg.split_obs()

gainoff = 0.0
gainp = Dict("AA"=> 0.01,
             "AP" => 0.1,
             "AZ" => 0.1,
             "JC" => 0.1,
             "LM" => 0.2,
             "PV" => 0.1,
             "SM" => 0.1,
             "SR" => 0.1)
obsim = img.observe_same(ehtim.obsdata.merge_obs(obslist), ttype="fast",  gainp=gainp, gain_offset=gainoff, ampcal=false, phasecal=false, stabilize_scan_phase=true, stabilize_scan_amp=true)
# now get cal table
ctable = ehtim.calibrating.self_cal.self_cal(obsim, img, caltable=true)
ehtim.caltable.save_caltable(ctable, obsim, datadir=joinpath(@__DIR__ ,"CaltableTest"))

dvis = extract_vis(obsim)
dlcamp = extract_lcamp(obsim)
dcphase = extract_cphase(obsim)
damp = extract_amp(obsim)
st = scantable(dvis)

gcache = Comrade.GainCache(st)

struct Test{G}
    gcache::G
end

function Test(st::Comrade.ScanTable)
    gcache = Comrade.GainCache(st)
    return Test{typeof(gcache)}(gcache)
end

function (mod::Test)(θ)
    (;f, σ, τ, ξ,  gamp, gphase) = θ
    g = exp.(gamp).*cis.(gphase)
    m = f*rotated(stretched(Gaussian(), σ*τ, σ), ξ)
    return GainModel(mod.gcache, g, m)
end

# define the priors
distamp = (AA = Normal(0.0, 0.1),
           AP = Normal(0.0, 0.1),
           LM = Normal(0.0, 0.2),
           AZ = Normal(0.0, 0.1),
           JC = Normal(0.0, 0.1),
           PV = Normal(0.0, 0.1),
           #SM = LogNormal(0.0, 0.1)
         )
distphase = (AA = Normal(0.0, 1e-4),
             AP = Normal(0.0, π),
             LM = Normal(0.0, π),
             AZ = Normal(0.0, 1.0*π),
             JC = Normal(0.0, 1.0*π),
             PV = Normal(0.0, π),
             #SM = Normal(0.0, 1.0*π),
             )
prior = (
          f = Uniform(0.9, 1.1),
          σ = Uniform(μas2rad(10.0), μas2rad(30.0)),
          τ = Uniform(0.1, 1.0),
          ξ = Uniform(-π/2, π/2),
          gamp = Comrade.GainPrior(distamp, st),
          gphase = Comrade.GainPrior(distphase, st)
        )
# Now form the posterior

mms  = Test(st)

cllklhd = RadioLikelihood(dlcamp, dcphase)
clpost = Posterior(cllklhd, prior, mms)

lklhd = RadioLikelihood(dvis)#damp, dcphase)
post = Posterior(lklhd, prior, mms)
tpost = asflat(post)
# We will use HMC to sample the posterior.
# First to reduce burn in we use pathfinder

ndim = dimension(tpost)
f = OptimizationFunction(tpost, GalacticOptim.AutoForwardDiff())
x0 = Comrade.HypercubeTransform.inverse(tpost, rand(post.prior))

prob = OptimizationProblem(f, x0, nothing, lb=fill(-5.0, ndim), ub=fill(5.0, ndim))
sol = solve(prob, BBO_adaptive_de_rand_1_bin_radiuslimited(); maxiters=1_000_000)

prob = GalacticOptim.OptimizationProblem(f, 0.1*randn(ndim), nothing)
sol = solve(prob, LBFGS(); g_tol=1e-2, maxiters=2_000)

@info sol.minimum

xopt = transform(tpost, sol)

residual(mms(xopt), dvis)

metric = DenseEuclideanMetric(ndim)
chain, stats = sample(post, AHMC(;metric), 7_000; nadapts=5_000, init_params=xopt)


# November, 2020
# Kevin Dorma
# module for common hydraulic calculations
# rev 0

# I need to make some decisions about how to interace with these functions
# I assume that I use dataframes to store information about sizing lines and general hydraulics
# then I will execute a function and it will return a result, but not change the raw information
# size lines
    # Input dataframe for describing the lines
    # Output line descriptor, pipe schedule, size criteria DP/100 and C, IDmm for each criteria and the chosen IDmm
# pick NPS will return the next size larger for NPS.This will be the chosen size.
# we will have utility functions for increasing or decreasing the NPS by 1.

module Hydraulics

using DataFrames
using CSV
#using ExcelFiles

export calcMoodyF
export addPipeProperties, addFluidProperties, getReynolds, getSegmentDP
export getLineSize, getListLargerNPS, getLargerNPS

# these are the reference data.
# and this needs to be loaded correctly when we use the code in a module
include("npsList.jl")
include("schedList.jl")
include("pipeRoughnessList.jl")
include("fitting3K.jl")
include("pipeIDlist.jl")

#schedList = CSV.File("schedList.csv") |> DataFrame
#pipeRoughness = CSV.File("pipeRoughness.csv") |> DataFrame
#fitting3K = CSV.File("fitting3K.csv") |> DataFrame
#IDmm = CSV.File("pipeIDlist.csv") |> DataFrame
## not used
#pipeTable = CSV.read("pipeIDtable.csv");

function packageInfo()
    # return information about the package as a string
    return("Hydraulics package, Kevin Dorma. Written in Julia, December 2020.")
end

# need function to append piping data
# later

function calcMoodyF(Reynolds,eD)
    # Moody friction factor, this is Halaand
    # eD is e/D relative roughness
    # Reynolds is Reynolds number
    invSqrtF = -1.8 .* log10.((eD ./ 3.70) .^ 1.11 + 6.9 ./ Reynolds)
    moodyF = (1 ./ (invSqrtF .^2) )
    return (moodyF)
end

function checkFittingList(fittingList)
    # fittingList is our list of fittings, fittingReference is the generic data
    # go through the list of fittings (fittingList) and compare with the reference list (possibly our fitting3K global list)
    # return items that are not found
    errorList = DataFrame(message = String[], entry=Int64[], Segment=String[], fittingType=String[])
    for i = 1:(size(fittingList)[1])
        thisFitting = fittingList[i,:fittingType]
        match = fitting3K[fitting3K.fittingType .== thisFitting,:]
        if ((size(match)[1]) == 0)
            push!(errorList, ("Fitting not found in row", i, fittingList[i,:Segment], fittingList[i,:fittingType] ))
        end
    end
    return (errorList)
end

function checkLineList(lines,fluidList)
    # go through our list of lines and compare with our reference lists
    # return the line items that are not found
    errorList = DataFrame(message = String[], entry=Int64[], Segment=String[], NPS=Float64[], Schedule=String[], material=String[], fluidName=String[])
    for i = 1:(size(lines)[1])
        # NPS
        thisItem = lines[i,:NPS]
        match = npsList[npsList.NPS .== thisItem,:]
        if ((size(match)[1]) == 0)
            push!(errorList, ("NPS not found in row", i, lines[i,:Segment], lines[i,:NPS], lines[i,:Schedule], lines[i,:Material], lines[i,:fluidName] ))
        end
        # schedule
        thisItem = lines[i,:Schedule]
        match = schedList[schedList.Schedule .== thisItem,:]
        if ((size(match)[1]) == 0)
            push!(errorList, ("Schedule not found in row", i, lines[i,:Segment], lines[i,:NPS], lines[i,:Schedule], lines[i,:Material], lines[i,:fluidName] ))
        end
        # roughness material
        thisItem = lines[i,:Material]
        match = pipeRoughness[pipeRoughness.Material .== thisItem,:]
        if ((size(match)[1]) == 0)
            push!(errorList, ("Material not found in row", i, lines[i,:Segment], lines[i,:NPS], lines[i,:Schedule], lines[i,:Material], lines[i,:fluidName] ))
        end
        # fluid
        thisItem = lines[i,:fluidName]
        match = fluidList[fluidList.fluidName .== thisItem,:]
        if ((size(match)[1]) == 0)
            push!(errorList, ("FluidName not found in row", i, lines[i,:Segment], lines[i,:NPS], lines[i,:Schedule], lines[i,:Material], lines[i,:fluidName] ))
        end
    end
    return (errorList)
end

function addPipeProperties(df)
    # roughness, IDmm, eD ratio
    # this appends columns to df
    # used either line sizing or hydraulic calculations

    df[:,:roughnessMM] .= 0.0
    df[:,:IDmm] .= 0.0

    for i = 1:(size(df)[1])
        df[i,:roughnessMM]=pipeRoughness[df[i,:Material] .== pipeRoughness[:,:Material],:roughnessMM][1]
        df[i,:IDmm]= IDmm[(df[i,:NPS] .== IDmm[:,:NPS]) .& (df[i,:Schedule] .== IDmm[:,:Schedule]),:Idmm][1]
    end

    return (0.0)
end

function addFluidProperties(df,fluidList)
    # extract the density, viscosity and whatever else is needed from the fluidList
    # Add this to the df
    # used either line sizing or hydraulic calculations

    df[:,:rho_kgm3] .= 0.0
    df[:,:mu_mPas] .= 0.0
    df[:,:roughnessMM] .= 0.0

    for i = 1:(size(df)[1])
        df[i,:rho_kgm3] = fluidList[df[i,:fluidName] .== fluidList[:,:fluidName], :rho_kgm3][1]
        df[i,:mu_mPas]  = fluidList[df[i,:fluidName] .== fluidList[:,:fluidName], :mu_mPas][1]
        df[i,:roughnessMM]  = pipeRoughness[df[i,:Material] .== pipeRoughness[:,:Material], :roughnessMM][1]
    end

    return (0.0)
end

function lineSizeDP(df)
    # this is for sizing lines based on pressure drop kPa per 100 m, return the ID in mm
    # very convenient form straight out of Perrys handbook
    # uses friction factor correlation from Chen
    # Do not modify the original data in df

    g = 9.80665
    Sf = df.kPaPer100m*1000 ./ (df.rho_kgm3*g*100)
    q = df.massFlow .* df.margin ./(df.rho_kgm3*3600)
    eps = (df.roughnessMM/1000)
    kinVisc = (df.mu_mPas/1000) ./ df.rho_kgm3
    termA = ((eps.^5)*g .* Sf ./ (q.^2)).^0.25
    termB = ((kinVisc .^ 5)./((q.^3) .* Sf * g )).^0.2
    termC = 0.125*(termA .+ termB).^0.2
    returnValue = (1000*(termC .* (q.^2) ./ (g*Sf)).^0.2)
    return (returnValue)
end

function lineSizeErosion(df)
    # df is the dataframe with hydraulic information
    # this is the erosion C factor method
    # C = v/sqrt(rho), with v in m/s and rho in kg/m3
    # for most service, C = 120, this is similar to C = 100 for imperial units
    # return the ID in mm

    g = 9.80665
    maxVeloc = df.frictionCsi ./ sqrt.(df.rho_kgm3)
    q = df.massFlow .* df.margin ./(df.rho_kgm3*3600)
    area = q ./ maxVeloc
    returnValue = (sqrt.(4*area ./ pi )*1000.0)
    return (returnValue)
end

function getLineSize(df,fluidList)
    # from the calculated line size for the two different methods, determine the required line size

    addFluidProperties(df,fluidList)

    theSchedule = df[:,:Schedule]

    theSegment = df[:,:Segment]

    tempDP100 = lineSizeDP(df)
    tempErosion = lineSizeErosion(df)
    tempNeeded = tempErosion[:] + tempDP100[:]
    for j in 1:size(tempDP100)[1]
        tempNeeded[j] =max(tempDP100[j], tempErosion[j])
    end
    returnValue = DataFrame(Segment = theSegment, mmDP100 = tempDP100, mmErosion = tempErosion, mmNeeded = tempNeeded, Schedule = theSchedule)
    return (returnValue)
end

# I need a function to pick the next larger line size given the pipe schedule

function getLargerNPS(ourIDmm, ourSchedule)
    # from the common list of line sizes, pick the pipe size that matches the required schedule
    # and is the next larger that the required ID
    # this works for a single line size
    ourScheduleList = IDmm[IDmm[:,:Schedule] .== ourSchedule, :];
    refinedList = ourScheduleList[(ourScheduleList[:,:Idmm] .- ourIDmm) .> 0.0,:];
    theMinID = minimum(refinedList[:,:Idmm])
    ourNPS = refinedList[(refinedList[:,:Idmm] .== theMinID), :NPS]

    return (ourNPS)
end

function getListLargerNPS(lineSizeDF)
    # we will use the required line size and schedule and return the NPS and actual ID
    diamList = DataFrame(Segment=String[], NPS=Float64[], Schedule=String[], IDmm=Float64[])
    for i = 1:(size(lineSizeDF)[1])
        ourSchedule = lineSizeDF[i,:Schedule];
        ourIDmm = lineSizeDF[i,:mmNeeded]
        ourScheduleList = IDmm[IDmm[:,:Schedule] .== ourSchedule, :];
        refinedList = ourScheduleList[(ourScheduleList[:,:Idmm] .- ourIDmm) .> 0.0,:];
        theMinID = minimum(refinedList[:,:Idmm])
        ourNPS = refinedList[(refinedList[:,:Idmm] .== theMinID), :NPS][1]

        push!(diamList, (lineSizeDF[i,:Segment], ourNPS, ourSchedule, theMinID))
    end
    return (diamList)
end

function sizing2hydraulics(sizingDF, chosenLineSizeDF)
    # given the line sizing DF, copy the info into hydraulics dataframe
    # chosenSizeDF contains the NPS and Schedule
    # return the hydraulics DF
    # we also need a simple fittingDF, but this is done with a separate function
    hydraulicsDF = DataFrame(Segment=String[], Description=String[], LineTag=String[], PnID=String[], NPS=Float64[], Schedule=String[],
		Material=String[],	fluidName=[],	inletP_kPaa=[],	massFlow=Float64[],	margin=Float64[])
    for i = 1:(size(sizingDF)[1])
        push!(hydraulicsDF, (sizingDF[i,:Segment], sizingDF[i,:Description],
                sizingDF[i,:LineTag], sizingDF[i,:PnID], chosenLineSizeDF[i,:NPS],
                chosenLineSizeDF[i,:Schedule], sizingDF[i,:Material], sizingDF[i,:fluidName],
                100.0, sizingDF[i,:massFlow], sizingDF[i,:margin]))
    end
    return(hydraulicsDF)
end

function sizing2fittingList(sizingDF, chosenLineSizeDF)
    # given the line sizing DF, copy the info into the fitting list
    # chosenSizeDF contains the NPS and Schedule
    # return the pre-populsted fittign list DF
    fittingListDF = DataFrame(Segment=String[], fittingType=String[], num_length_m=Float64[], comment=String[], revision=String[])

    for i = 1:(size(sizingDF)[1])
        push!(fittingListDF, (sizingDF[i,:Segment], "PIPE", 100.0, "from line sizing", "A"))
    end
    return(fittingListDF)
end

function incrementLineSize(rowNum, lineSizeDF, increment)
    # increment the specified row number needed line size by +1 or -1
    # not the cleanest function, but it works

    neededDF = DataFrame(Segment=String[], mmNeeded=Float64[], Schedule=String[])

    i = rowNum
    push!(neededDF, (lineSizeDF[i,:Segment], lineSizeDF[i,:IDmm] + increment, lineSizeDF[i,:Schedule]))

    incrementedLines = getListLargerNPS(neededDF)

    lineSizeDF[i,:NPS] = incrementedLines[1,:NPS]
    lineSizeDF[i,:IDmm] = incrementedLines[1,:IDmm]

    return (3.14)
end

function getReynolds(df)
    # preliminary calculations for pipe hydraulics
    # return the velocity, Reynolds, friction factor, density, diameterMM, diameterInch

    theSegment = df.Segment
    diamMM = df.IDmm
    diamInch = diamMM / 25.4
    rho = df.rho_kgm3
    volFlow = df.massFlow .* df.margin ./ df.rho_kgm3
    area = 0.25 * pi * (df.IDmm / 1000.0).^2
    velocity = volFlow ./ area / 3600
    Reynolds = df.rho_kgm3 .* velocity .* (df.IDmm / 1000.0) ./ (df.mu_mPas/1000.0)
    eD = df.roughnessMM ./ df.IDmm
    moodyF = calcMoodyF(Reynolds, eD)
    returnValue = DataFrame(Segment = theSegment, velocity_ms = velocity, rho_kgm3 = rho, Re = Reynolds, frictF = moodyF, IDmm = diamMM, IDinch = diamInch)
    return (returnValue)
end

function elementDP(lines, fittingList)
    # given the hydraulic elements in df (long list of pipe and fittings)
    # and the preliminary values in prelim
    # calculate the DP in each element
    # dp = rho.f. (L/D) v2/2
    # dp = rho K v2/2
    # where I need to calculate K = K1/Re + Kinf*(1 + Kd/Dinch^0.3)
    # can I get all of the pipe segments and use K = Kp * f L/D, where Kp = 1 for pipe
    # return the value for Kp (1 for pipe, 0 for fitting) and the pressure drop in the item
    returnValue = DataFrame(Segment = String[], Kp = Float64[], fittingK=Float64[], pipeK=Float64[], elementK = Float64[], DPkpa = Float64[])

    # start with getting Reynolds and other important things
    prelim = getReynolds(lines)

    for i = 1:(size(fittingList)[1])
        theSegment = fittingList[i,:Segment]

        # first we get all of the K values
        valK1 = fitting3K[fitting3K[:,:fittingType] .== fittingList[i,:fittingType],:K1][1]
        valKinf = fitting3K[fitting3K[:,:fittingType] .== fittingList[i,:fittingType],:Kinf][1]
        valKd = fitting3K[fitting3K[:,:fittingType] .== fittingList[i,:fittingType],:Kd][1]
        valKp = fitting3K[fitting3K[:,:fittingType] .== fittingList[i,:fittingType],:Kp][1]

        # now we get the hydraulic properties
        ff = prelim[prelim[:,:Segment] .== fittingList[i,:Segment],:frictF][1]
        rho = prelim[prelim[:,:Segment] .== fittingList[i,:Segment],:rho_kgm3][1]
        idInch = prelim[prelim[:,:Segment] .== fittingList[i,:Segment],:IDinch][1]
        idMM = prelim[prelim[:,:Segment] .== fittingList[i,:Segment],:IDmm][1]
        veloc = prelim[prelim[:,:Segment] .== fittingList[i,:Segment],:velocity_ms][1]
        Re = prelim[prelim[:,:Segment] .== fittingList[i,:Segment],:Re][1]

        Kfitting = valK1/Re + valKinf*(1.0 + valKd/(idInch^0.3))

        # pressure drop in kPa
        thisKfitting = fittingList[i,:num_length_m] * Kfitting
        thisKpipe = valKp * ff * fittingList[i,:num_length_m] /(idMM/1000.0)
        thisK = thisKfitting + thisKpipe
        thisDP = thisK * (0.001 * rho * 0.5 * (veloc)^ 2)

        push!(returnValue, (theSegment, valKp, thisKfitting, thisKpipe, thisK, thisDP))
    end
    return (returnValue)
end

function compileDP(lines, fittingDP)
    # for each entry in lines, sum all of the fittingDP
    returnValue = DataFrame(Segment = String[], segmentK = Float64[], DPkpa = Float64[], inletP = Float64[], outletP = Float64[])

    for i = 1:(size(lines)[1])
        theSegment = lines[i,:Segment]
        theSumK = sum(fittingDP[fittingDP[:,:Segment] .== theSegment,:elementK])
        theSumDP = sum(fittingDP[fittingDP[:,:Segment] .== theSegment,:DPkpa])
        theInletP = lines[i,:inletP_kPaa]
        theOutletP = theInletP - theSumDP
        push!(returnValue, (theSegment, theSumK, theSumDP, theInletP, theOutletP))
    end
    return (returnValue)
end

function getSegmentDP(lines, fittingList, fluidList)
    # lines describes the hydraulics, fittingList has the piping details, fluidList is the fluid

    addFluidProperties(lines,fluidList)
    addPipeProperties(lines)
    fittingDP = elementDP(lines, fittingList)
    returnValue = compileDP(lines, fittingDP)
    return (returnValue)
end

end # module


#Parent type of all simulations
abstract type @par(AbstractSimulation) end
#=
Simulation will encapsule different structs for:
  Time-stepping, PassiveScalar, DensityStratification, LESModel, Forcing Scheme
=#

#Traits
@par haspassivescalar(s::Type{T}) where {T<:@par(AbstractSimulation)} =
    PassiveScalarType !== NoPassiveScalar
haspassivescalar(s::AbstractSimulation) = haspassivescalar(typeof(s))

@par hasdensity(s::Type{T}) where {T<:@par(AbstractSimulation)}  =
    DensityStratificationType !== NoDensityStratification
hasdensity(s::AbstractSimulation) = hasdensity(typeof(s))

@par hasles(s::Type{T}) where {T<:@par(AbstractSimulation)}  =
    LESModelType !== NoLESModel
hasles(s::AbstractSimulation) = hasles(typeof(s))

@par hasdensityles(s::Type{T}) where {T<:@par(AbstractSimulation)}  =
    (hasdensity(T) && hasdensityles(DensityStratificationType))
hasdensityles(s::AbstractSimulation) = hasdensityles(typeof(s))

@par haspassivescalarles(s::Type{T}) where {T<:@par(AbstractSimulation)}  =
    (haspassivescalar(T) && haspassivescalarles(PassiveScalarType))
haspassivescalarles(s::AbstractSimulation) = haspassivescalarles(typeof(s))

@par hasforcing(s::Type{T}) where {T<:@par(AbstractSimulation)}  =
    ForcingType !== NoForcing
hasforcing(s::AbstractSimulation) = hasforcing(typeof(s))

@par hashyperviscosity(s::Type{T}) where {T<:@par(AbstractSimulation)} =
    HyperViscosityType !== NoHyperViscosity
hashyperviscosity(s::AbstractSimulation) = hashyperviscosity(typeof(s))

@par is_vorticityEquation(s::Type{T}) where {T<:@par(AbstractSimulation)} =
    EquationType === VorticityEquation
is_vorticityEquation(s::AbstractSimulation) = is_vorticityEquation(typeof(s))

@par need_vorticity(s::Type{T}) where {T<:@par(AbstractSimulation)} =
    is_vorticityEquation(T) | hasles(T)
need_vorticity(s::AbstractSimulation) = need_vorticity(typeof(s))

struct @par(Simulation) <: @par(AbstractSimulation)
    u::VectorField{Float64,3,2,false}
    rhs::VectorField{Float64,3,2,false}
    reductionh::Vector{Float64}
    reductionv::Vector{Float64}
    reductionh_fil::Vector{Float64}
    reductionv_fil::Vector{Float64}
    xspec::PaddedArray{Float64,1,0,true}
    yspec::PaddedArray{Float64,1,0,true}
    equation::EquationType
    timestep::VelocityTimeStepType
    passivescalar::PassiveScalarType
    densitystratification::DensityStratificationType
    lesmodel::LESModelType
    forcing::ForcingType
    hyperviscosity::HyperViscosityType
    iteration::Base.RefValue{Int}
    time::Base.RefValue{Float64}
    dtoutput::Int
    dtstats::Int
    dtspec::Int
    hspec::Array{Float64,3}
    vspec::Array{Float64,3}
    specH::Array{Float64,1}
    tspecH::Array{Float64,1}
    specV::Array{Float64,1}
    kspec::ScalarField{Float64,3,2,false}
    nlstats::Base.RefValue{Tuple{Float64,Float64,Float64,Float64}}
    pressstats::Base.RefValue{Tuple{Float64,Float64,Float64,Float64}}

    @par function @par(Simulation)(u::VectorField,equation,timestep,passivescalar,densitystratification,lesmodel,forcing,hv,iteration,time,dtout,dtstats,dtspecs)

        rhs = similar(u)
        kspec = ScalarField{Float64}(size(u.r),(LX,LY,LZ))

        reductionh = zeros(THR ? Threads.nthreads() : 1)
        reductionv = zeros(THR ? Threads.nthreads() : 1)

        reductionh_fil = !STAT_FIL ? zeros(1) : zeros(THR ? Threads.nthreads() : 1)
        reductionv_fil = !STAT_FIL ? zeros(1) : zeros(THR ? Threads.nthreads() : 1)

        xspec = PaddedArray(NRX)
        yspec = PaddedArray(NY)

        hspec = zeros(Float64,size(u))
        vspec = zeros(Float64,size(u))
        specH = zeros(Float64,length(KH))
        tspecH = zeros(Float64,length(KH))
        specV = zeros(Float64,length(KRZ))
        nlstats = Ref((1.0,1.0,1.0,1.0))
        pressstats = Ref((1.0,1.0,1.0,1.0))

        return @par(new)(u,rhs,reductionh,reductionv,reductionh_fil,reductionv_fil,xspec,yspec,equation,timestep,passivescalar,densitystratification,lesmodel,forcing,hv,iteration,time,dtout,dtstats,dtspecs,hspec,vspec,specH,tspecH,specV,kspec,nlstats,pressstats)
    end

end

is_output_time(s) = (s.timestep.x.iteration[] != 0) & (mod(s.iteration[],s.dtoutput) == 0)
is_stats_time(s) = (mod(s.iteration[],s.dtstats) == 0)
is_spec_time(s) = (mod(s.iteration[],s.dtspec) == 0)

##################################### Equation type #########################################

struct VorticityEquation end

struct NavierStokesEquation{T}
    uu::SymTrTenField{T,3,2,false}
end

msg(::VorticityEquation) = "Navier-Stokes in rotational form ((∇×u)×u)"
msg(::NavierStokesEquation) = "Navier-Stokes in conservation form (∇⋅(uu))"

##################################### Equation type #########################################

@inline @par nuh(s::Type{T}) where {T<:@par(AbstractSimulation)} = nuh(HyperViscosityType)
@inline nuh(s::AbstractSimulation) = nuh(typeof(s))

@inline @par get_hyperviscosity_exponent(s::Type{T}) where {T<:@par(AbstractSimulation)} = get_hyperviscosity_exponent(HyperViscosityType)
@inline get_hyperviscosity_exponent(s::AbstractSimulation) = get_hyperviscosity_exponent(typeof(s))

@par function Base.show(io::IO,s::@par(Simulation))
smsg = """
Fluid Flow Simulation
$(msg(s.equation))

nx: $(NRX)
ny: $NY
nz: $NZ
x domain size: $(LX)*2π
y domain size: $(LY)*2π
z domain size: $(LZ)*2π

Kinematic Viscosity: $(ν)

Velocity time-stepping method: $(typeof(s.timestep.x))
Dealias cutoff: $(DEALIAS_TYPE)
Number of Threads: $NT
"""
smsg = join((smsg,msg.(getfield.(Ref(s),sim_fields))...))#msg(s.passivescalar),
#  msg(s.densitystratification),
#  msg(s.lesmodel),
#  msg(s.forcing)))

print(io,smsg)
end

#LES models
include("LESmodels/les_types.jl")

# Simulaiton with Scalar fields ===================================================================================================================================================

abstract type AbstractPassiveScalar{L,TT,α,dρdz,Gdirec} end

    haspassivescalarles(a::Type{T}) where {L,TT,α,dρdz,Gdirec,T<:AbstractPassiveScalar{L,TT,α,dρdz,Gdirec}} =
        L === NoLESScalar ? false : true
    @inline haspassivescalarles(a::AbstractPassiveScalar) = haspassivescalarles(typeof(a))

    has_les_scalar_vorticity_model(a::Type{T}) where {L,TT,α,dρdz,Gdirec,T<:AbstractPassiveScalar{L,TT,α,dρdz,Gdirec}} =
        is_vorticity_model(L)
    @inline has_les_scalar_vorticity_model(a::AbstractPassiveScalar) = has_les_scalar_vorticity_model(typeof(a))
    @inline @par has_les_scalar_vorticity_model(s::Type{T}) where {T<:@par(AbstractSimulation)} = has_les_scalar_vorticity_model(PassiveScalarType)

    diffusivity(a::Type{T}) where {L,TT,α,dρdz,Gdirec,T<:AbstractPassiveScalar{L,TT,α,dρdz,Gdirec}} =
        α
    @inline diffusivity(a::AbstractPassiveScalar) = diffusivity(typeof(a))

    meangradient(a::Type{T}) where {L,TT,α,dρdz,Gdirec,T<:AbstractPassiveScalar{L,TT,α,dρdz,Gdirec}} =
        dρdz
    @inline meangradient(a::AbstractPassiveScalar{L,TT,α,dρdz,Gdirec}) where {L,TT,α,dρdz,Gdirec} = meangradient(typeof(a))

    graddir(a::Type{T}) where {L,TT,α,dρdz,Gdirec,T<:AbstractPassiveScalar{L,TT,α,dρdz,Gdirec}} =
        Gdirec
    @inline graddir(a::AbstractPassiveScalar{L,TT,α,dρdz,Gdirec}) where {L,TT,α,dρdz,Gdirec} = graddir(typeof(a))

    initialize!(a::AbstractPassiveScalar,s::AbstractSimulation) = initialize!(a.timestep,parent(real(a.rhs)),diffusivity(a),s)

    statsheader(a::AbstractPassiveScalar) = "scalar,scalarp2,dscalardxp2,dscalardyp2,dscalardzp2,scalardiss"

    stats(a::AbstractPassiveScalar,s::AbstractSimulation) = scalar_stats(a.φ,a,s)

struct NoPassiveScalar <: AbstractPassiveScalar{NoLESScalar,nothing,nothing,nothing,nothing} end

    initialize!(a::NoPassiveScalar,s::AbstractSimulation) = nothing

    statsheader(a::NoPassiveScalar) = ""

    stats(a::NoPassiveScalar,s::AbstractSimulation) = (),()

    msg(a::NoPassiveScalar) = "\nPassive Scalar: No passive scalar\n"

struct PassiveScalar{L,TTimeStep, α #=Difusitivity = ν/Pr =#,
                  dρdz #=Linear mean profile=#, Gdirec #=Axis of mean profile =#} <: AbstractPassiveScalar{L,TTimeStep,α,dρdz,Gdirec}
    φ::ScalarField{Float64,3,2,false}
    rhs::ScalarField{Float64,3,2,false}
    flux::VectorField{Float64,3,2,false}
    lesmodel::L
    timestep::TTimeStep
    reduction::Vector{Float64}

    function PassiveScalar{TT,α,dρdz,Gdirec}(ρ,les,timestep) where {TT,α,dρdz,Gdirec}
        ρrhs = similar(ρ)
        flux = VectorField(size(ρ.field.r),(ρ.kx.l,ρ.ky.l,ρ.kz.l))
        reduction = zeros(THR ? Threads.nthreads() : 1)
        return new{typeof(les),TT,α,dρdz,Gdirec}(ρ,ρrhs,flux,les,timestep,reduction)
    end
end

msg(a::PassiveScalar{L,TT,α,dρdz,Gdirec}) where {L,TT,α,dρdz,Gdirec} = """

Passive Scalar: true
Scalar diffusivity: $(α)
Scalar mean gradient: $(dρdz)
Scalar mean gradient direction: $(Gdirec)
Scalar time-stepping method: $(TT)
Scalar LES model: $(L)

"""
# ==========================================================================================================
# Hyper viscosity Type

include("Hyperviscosity/hyperviscosity_types.jl")

# ==========================================================================================================
# Density Stratification Types

include("density_stratification_types.jl")

# ==========================================================================================
# Forcing Scheme

include("Forcing_methods/forcing_types.jl")

# Initializan function =========================================================================================================================================================================================================

function parameters(d::Dict)

    nx = parse(Int,d[:nx])
    ny = parse(Int,d[:ny])
    nz = parse(Int,d[:nz])

    ncx = div(nx,2)+1
    lcs = ncx*ny*nz
    lcv = 3*lcs
    lrs = 2*lcs
    lrv = 2*lcv

    lx = Float64(eval(Meta.parse(d[:xDomainSize])))
    ly = Float64(eval(Meta.parse(d[:yDomainSize])))
    lz = Float64(eval(Meta.parse(d[:zDomainSize])))
    ν = Float64(eval(Meta.parse(d[:kinematicViscosity])))

    start = haskey(d,:start) ? parse(Int,d[:start]) : 0
    starttime = haskey(d,:startTime) ? parse(Float64,d[:startTime]) : 0.0
    dtstat = parse(Int,d[:dtStat])
    dtspecs = parse(Int,d[:dtSpec])
    dtout = parse(Int,d[:writeTime])

    if haskey(d,:equation)
        if d[:equation] == "NS"
            equation = NavierStokesEquation{Float64}(SymTrTenField((nx,ny,nz),(lx,ly,lz)))
        else
            equation = VorticityEquation()
        end
    else
        equation = VorticityEquation()
    end

    @info("Reading initial velocity field u1.$start u2.$start u3.$start")
    u = VectorField{Float64}("u1.$start","u2.$start","u3.$start")

    kxp = reshape(rfftfreq(nx,lx),(ncx,1,1))
    kyp = reshape(fftfreq(ny,ly),(1,ny,1))
    kzp = reshape(fftfreq(nz,lz),(1,1,nz))

    haskey(d,:threaded) ? (tr = parse(Bool,d[:threaded])) : (tr = true)

    tr || FFTW.set_num_threads(1)
    nt = tr ? Threads.nthreads() : 1

    b = Globals.splitrange(lrs, nt)

    haskey(d,:cutoff) ? (cutoffr = Float64(eval(Meta.parse(d[:cutoff])))) : (cutoffr = 2/3)

    cutoff = (cutoffr*kxp[end])^2

    if haskey(d,:hyperViscosity)
        if d[:hyperViscosity] == "spectralBarrier"
            initkp = haskey(d,:initk) ? parse(Float64,d[:initk]) : cutoffr*2KX[end] / 3
            endK = haskey(d,:endk) ? parse(Float64,d[:endk]) : sqrt(cutoffr*3)*KX[end]
            hyperviscositytype = SpectralBarrier(initkp,endK)
        else
            νh = parse(Float64,d[:hyperViscosityCoefficient])
            m = haskey(d,:hyperViscosityM) ? parse(Int,d[:hyperViscosityM]) : 2
            hyperviscositytype = HyperViscosity{νh,m}()
        end
    else
        hyperviscositytype = NoHyperViscosity()
    end

    integrator = haskey(d,:velocityTimeStep) ? Symbol(d[:velocityTimeStep]) : :ETD3rdO
    variableTimeStep = haskey(d,:variableTimestepFlag) ? parse(Bool,d[:variableTimestepFlag]) : true
    cfl = haskey(d,:cfl) ? parse(Float64,d[:cfl]) : 0.18
    idt = haskey(d,:dt) ? Float64(eval(Meta.parse(d[:dt]))) : 0.0
    vtimestep = if integrator === :Euller
        VectorTimeStep{cfl}(Euller{variableTimeStep,idt}(Ref(idt)),Euller{variableTimeStep,idt}(Ref(idt)),Euller{variableTimeStep,idt}(Ref(idt)))
    elseif integrator === :Adams_Bashforth3rdO
        VectorTimeStep{cfl}(Adams_Bashforth3rdO{variableTimeStep,idt}(),Adams_Bashforth3rdO{variableTimeStep,idt}(),Adams_Bashforth3rdO{variableTimeStep,idt}())
    else
        VectorTimeStep{cfl}(ETD3rdO(variableTimeStep,idt,hyperviscositytype,ν),ETD3rdO(variableTimeStep,idt,hyperviscositytype,ν),ETD3rdO(variableTimeStep,idt,hyperviscositytype,ν))
    end

    if haskey(d,:passiveScalar)
        α = ν/Float64(eval(Meta.parse(d[:scalarPr])))
        dρdz = Float64(eval(Meta.parse(d[:scalarGradient])))
        @info("Reading initial scalar field scalar.$start")
        rho = isfile("scalar.$start") ? ScalarField{Float64}("scalar.$start") : ScalarField{Float64}((nx,ny,nz),(lx,ly,lz))
        scalardir = haskey(d,:scalarDirection) ? Symbol(d[:scalarDirection]) : :z
        scalartimestep = if integrator === :Euller
            Euller{variableTimeStep,idt}(Ref(idt))
        elseif integrator === :Adams_Bashforth3rdO
            Adams_Bashforth3rdO{variableTimeStep,idt}()
        else
            ETD3rdO(variableTimeStep,idt,hyperviscositytype,α)
        end

        lestypescalar = if haskey(d,:lesModel)
            if haskey(d,:scalarLesModel)
                if d[:scalarLesModel] == "vorticityDiffusion"
                    c = haskey(d,:scalarVorticityCoefficient) ? parse(Float64,d[:scalarVorticityCoefficient]) : 1.0
                    VorticityDiffusion(c)
                else
                    EddyDiffusion()
                end
            else
                EddyDiffusion()
            end
        else
            NoLESScalar()
        end

        scalartype = PassiveScalar{typeof(scalartimestep),α,dρdz,scalardir}(rho,lestypescalar,scalartimestep)
    else
        scalartype = NoPassiveScalar()
    end

    densitytype = stratificationtype(d,start,integrator,nx,ny,nz,lx,ly,lz,variableTimeStep,idt)

    lestype = les_types(d,nx,ny,nz,lx,ly,lz)

    forcingtype = forcing_model(d,nx,ny,nz,ncx,lx)

    s = Simulation{typeof(equation),typeof(vtimestep),
        typeof(scalartype),typeof(densitytype),typeof(lestype),typeof(forcingtype),
        typeof(hyperviscositytype)}(u,equation,vtimestep,scalartype,densitytype,lestype,forcingtype,hyperviscositytype,Ref(start),Ref(starttime),dtout,dtstat,dtspecs)
  #
    return s
end

parameters() = parameters(readglobal())


# Sundials.jl algorithms

# Abstract Types
abstract type SundialsODEAlgorithm{Method,LinearSolver} <: AbstractODEAlgorithm end
abstract type SundialsDAEAlgorithm{LinearSolver} <: AbstractDAEAlgorithm end

# ODE Algorithms
immutable CVODE_BDF{Method,LinearSolver} <: SundialsODEAlgorithm{Method,LinearSolver}
    jac_upper::Int
    jac_lower::Int
    stored_upper::Int
    krylov_dim::Int
    stability_limit_detect::Bool
    max_hnil_warns::Int
    max_order::Int
    max_error_test_failures::Int
    max_nonlinear_iters::Int
    max_convergence_failures::Int
end
Base.@pure function CVODE_BDF(;method=:Newton,linear_solver=:Dense,
                    jac_upper=0,jac_lower=0,stored_upper = jac_upper+jac_lower, non_zero=0,krylov_dim=0,
                    stability_limit_detect=false,
                    max_hnil_warns = 10,
                    max_order = 5,
                    max_error_test_failures = 7,
                    max_nonlinear_iters = 3,
                    max_convergence_failures = 10)
    if linear_solver == :Band && (jac_upper==0 || jac_lower==0)
        error("Banded solver must set the jac_upper and jac_lower")
    end
    if linear_solver != :None && linear_solver != :Diagonal && linear_solver != :Dense && linear_solver != :Band && linear_solver != :BCG && linear_solver != :GMRES && linear_solver != :FGMRES && linear_solver != :PCG && linear_solver != :TFQMR
        error("Linear solver choice not accepted.")
    end
    CVODE_BDF{method,linear_solver}(jac_upper,jac_lower,stored_upper,krylov_dim,
                                    stability_limit_detect,
                                    max_hnil_warns,
                                    max_order,
                                    max_error_test_failures,
                                    max_nonlinear_iters,
                                    max_convergence_failures)
end

immutable CVODE_Adams{Method,LinearSolver} <: SundialsODEAlgorithm{Method,LinearSolver}
    jac_upper::Int
    jac_lower::Int
    stored_upper::Int
    krylov_dim::Int
    stability_limit_detect::Bool
    max_hnil_warns::Int
    max_order::Int
    max_error_test_failures::Int
    max_nonlinear_iters::Int
    max_convergence_failures::Int
end
Base.@pure function CVODE_Adams(;method=:Functional,linear_solver=:None,
                      jac_upper=0,jac_lower=0,
                      stored_upper = jac_upper+jac_lower,
                      krylov_dim=0,
                      stability_limit_detect=false,
                      max_hnil_warns = 10,
                      max_order = 12,
                      max_error_test_failures = 7,
                      max_nonlinear_iters = 3,
                      max_convergence_failures = 10
                      )
    if linear_solver == :Band && (jac_upper==0 || jac_lower==0)
        error("Banded solver must set the jac_upper and jac_lower")
    end
    if linear_solver != :None && linear_solver != :Diagonal && linear_solver != :Dense && linear_solver != :Band && linear_solver != :BCG && linear_solver != :GMRES && linear_solver != :FGMRES && linear_solver != :PCG && linear_solver != :TFQMR
        error("Linear solver choice not accepted.")
    end
    CVODE_Adams{method,linear_solver}(jac_upper,jac_lower,stored_upper,krylov_dim,
                                      stability_limit_detect,
                                      max_hnil_warns,
                                      max_order,
                                      max_error_test_failures,
                                      max_nonlinear_iters,
                                      max_convergence_failures)
end

immutable ARKODE{Method,LinearSolver,T,T1,T2} <: SundialsODEAlgorithm{Method,LinearSolver}
    stiffness::T
    jac_upper::Int
    jac_lower::Int
    stored_upper::Int
    krylov_dim::Int
    max_hnil_warns::Int
    max_error_test_failures::Int
    max_nonlinear_iters::Int
    max_convergence_failures::Int
    predictor_method::Int
    nonlinear_convergence_coefficient::Float64
    dense_order::Int
    order::Int
    set_optimal_params::Bool
    crdown::Float64
    dgmax::Float64
    rdiv::Float64
    msbp::Int
    itable::T1
    etable::T2
end

Base.@pure function ARKODE(stiffness=Implicit();method=:Newton,linear_solver=:Dense,
                    jac_upper=0,jac_lower=0,stored_upper = jac_upper+jac_lower, non_zero=0,krylov_dim=0,
                    max_hnil_warns = 10,
                    max_error_test_failures = 7,
                    max_nonlinear_iters = 3,
                    max_convergence_failures = 10,
                    predictor_method = 0,
                    nonlinear_convergence_coefficient = 0.1,
                    dense_order = 3,
                    order = 4,
                    set_optimal_params = false,
                    crdown = 0.3,
                    dgmax = 0.2,
                    rdiv = 2.3,
                    msbp = 20,
                    adaptivity_method = 0,
                    itable = nothing,
                    etable = nothing
                    )
    if linear_solver == :Band && (jac_upper==0 || jac_lower==0)
        error("Banded solver must set the jac_upper and jac_lower")
    end
    if linear_solver != :None && linear_solver != :Dense && linear_solver != :Band && linear_solver != :BCG && linear_solver != :GMRES && linear_solver != :FGMRES && linear_solver != :PCG && linear_solver != :TFQMR
        error("Linear solver choice not accepted.")
    end
    ARKODE{method,linear_solver,typeof(stiffness),
                    typeof(itable),typeof(etable)}(
                                    stiffness,
                                    jac_upper,jac_lower,
                                    stored_upper,krylov_dim,
                                    max_hnil_warns,
                                    max_error_test_failures,
                                    max_nonlinear_iters,
                                    max_convergence_failures,
                                    predictor_method,
                                    nonlinear_convergence_coefficient,
                                    dense_order,
                                    order,
                                    set_optimal_params,
                                    crdown,
                                    dgmax,
                                    rdiv,
                                    msbp,
                                    itable,
                                    etable)
end

# DAE Algorithms
immutable IDA{LinearSolver} <: SundialsDAEAlgorithm{LinearSolver}
  jac_upper::Int
  jac_lower::Int
  stored_upper::Int
  krylov_dim::Int
  max_order::Int
  max_error_test_failures::Int
  nonlinear_convergence_coefficient::Float64
  max_nonlinear_iters::Int
  max_convergence_failures::Int
  nonlinear_convergence_coefficient_ic::Float64
  max_num_steps_ic::Int
  max_num_jacs_ic::Int
  max_num_iters_ic::Int
  max_num_backs_ic::Int
  use_linesearch_ic::Bool
end
Base.@pure function IDA(;linear_solver=:Dense,jac_upper=0,jac_lower=0,
                        stored_upper = jac_upper+jac_lower,
                        krylov_dim=0,
                        max_order = 5,
                        max_error_test_failures = 7,
                        max_nonlinear_iters = 3,
                        nonlinear_convergence_coefficient = 0.33,
                        nonlinear_convergence_coefficient_ic = 0.0033,
                        max_num_steps_ic = 5,
                        max_num_jacs_ic = 4,
                        max_num_iters_ic = 10,
                        max_num_backs_ic = 100,
                        use_linesearch_ic = true,
                        max_convergence_failures = 10)
  if linear_solver == :Band && (jac_upper==0 || jac_lower==0)
      error("Banded solver must set the jac_upper and jac_lower")
  end
  if linear_solver != :Dense && linear_solver != :Band && linear_solver != :BCG && linear_solver != :GMRES && linear_solver != :FGMRES && linear_solver != :PCG && linear_solver != :TFQMR
      error("Linear solver choice not accepted.")
  end
  IDA{linear_solver}(jac_upper,jac_lower,stored_upper,krylov_dim,
                      max_order,
                      max_error_test_failures,
                      nonlinear_convergence_coefficient,
                      max_nonlinear_iters,
                      max_convergence_failures,
                      nonlinear_convergence_coefficient_ic,
                      max_num_steps_ic,
                      max_num_jacs_ic,
                      max_num_iters_ic,
                      max_num_backs_ic,
                      use_linesearch_ic)
end

method_choice(alg::SundialsODEAlgorithm{Method}) where Method = Method
method_choice(alg::SundialsDAEAlgorithm) = :Newton
linear_solver(alg::SundialsODEAlgorithm{Method,LinearSolver}) where {Method,LinearSolver}= LinearSolver
linear_solver(alg::SundialsDAEAlgorithm{LinearSolver}) where LinearSolver = LinearSolver