export UpdateSampleIterate

"""
    UpdateSampleIterate <: UpdateRule

 Update using by iterating over another recursive rule, while sampling from the input

## Parameters
* `rule::UpdateRule`: rule to use at each iteration (default is Elo() )
* `batch_size::Int`: the size of batches to use (default is 1)
* `n_samples::Int`: the number of random samples to take
* ``

## Not implemented yet
* `batching_rule::` ??? how to select batches ???

```
"""
struct UpdateSampleIterate <: UpdateRule
    rule::UpdateRule
    batch_size::Int
    n_samples::Int
    function UpdateSampleIterate(rule::UpdateRule, batch_size::Int, n_samples::Int)
        @check_args(UpdateSampleIterate, batch_size >= one(batch_size))
        @check_args(UpdateSampleIterate, n_samples >= one(n_samples))
        @check_args(UpdateSampleIterate, update_info(rule)[:state_model] == "recursive")
        new(rule, batch_size, n_samples)
    end
end
UpdateSampleIterate(;rule::UpdateRule=UpdateElo(), batch_size::Int=1, n_samples::Int=1) = UpdateSampleIterate( rule, batch_size, n_samples)

function update_info( rule::UpdateSampleIterate )
    info = Dict(
                :name => "SampleIterate",
                :reference => "none",
                :computation => "sequential",
                :state_model => "recursive",
                :input => update_info( rule.rule )[:input],
                :output => update_info( rule.rule )[:output],
                :model => update_info( rule.rule )[:model],
                :ties => update_info( rule.rule )[:ties],
                :factors => update_info( rule.rule )[:factors],
                :parameters => append!( ["rule(default=Elo)", "batch_size(default=1)"], update_info( rule.rule )[:parameters] ),
                :record => true
                )
    return info
end
update_info( ::Type{UpdateSampleIterate} ) =  error("Need to specify the sub-rule")

function update_ratings( rule::UpdateSampleIterate,
                         input_ratings::RatingsList,
                         input_competitions::DataFrame;
                         record::Union{Missing,RatingsTable}=missing)
    n = size(input_competitions,1)
    m = length( input_ratings.players )
    I = player_indexes( input_ratings.players )
    r = deepcopy(input_ratings)

    # select a batch, and then update using the appropriate rule
    d = input_competitions # just an abbreviation
    for (j,i) = enumerate( 1 : rule.batch_size : rule.n_samples )
        rows = d[ rand(1:n, rule.batch_size), :]
        r = update_ratings(rule.rule, r, rows)
        if !ismissing(record) && size(record,1)>=j
            record[j] = r # record results if required, and space allocated
        elseif !ismissing(record)
            println("Warn: out of space to record ratings: j=$j of $(size(record,1))")
        end
    end

    # output ratings list
    return r
end

function predict_outcome(rule::UpdateSampleIterate,
                         ratingA::Real, ratingB::Real,
                         factorA::Union{Missing,Real}, factorB::Union{Missing,Real})
    return predict_outcome(rule.rule, ratingA, ratingB, factorA, factorB)
end


VERSION >= v"0.6.0" && __precompile__(true)

module Iscalc

# Make sure you do
# using DataFrames
# using SQLite
# in that order in a fresh Julia session to ensure the conversion methods get defined

using Base.Dates, Requests, JuliaDB, CSV, FileIO, DataFrames, TextParse, HTTP, DataStreams, Missings, Roots, LaTeXStrings
using BusinessDays

#using IJulia, SQLite
#using Compat

include("GetData.jl")
include("OT.jl")
include("API.jl")

export obter, JCorridos, FluxosCaixa, fspot, fdesconto
export euronextBonds, factsheet, catalogo
export load, save, table, column, join, merge, stack, unstack, select, reduce   #JUliaDB
export latexstring # Latexstrings
println("\nISCALC V0.3.4 <2018-04-25>")

end # module


export sparse_coding

import ..SparseCoding

using mlpack._Internal.io

import mlpack_jll
const sparse_codingLibrary = mlpack_jll.libmlpack_julia_sparse_coding

# Call the C binding of the mlpack sparse_coding binding.
function sparse_coding_mlpackMain()
  success = ccall((:sparse_coding, sparse_codingLibrary), Bool, ())
  if !success
    # Throw an exception---false means there was a C++ exception.
    throw(ErrorException("mlpack binding error; see output"))
  end
end

" Internal module to hold utility functions. "
module sparse_coding_internal
  import ..sparse_codingLibrary

import ...SparseCoding

# Get the value of a model pointer parameter of type SparseCoding.
function IOGetParamSparseCoding(paramName::String)::SparseCoding
  SparseCoding(ccall((:IO_GetParamSparseCodingPtr, sparse_codingLibrary), Ptr{Nothing}, (Cstring,), paramName))
end

# Set the value of a model pointer parameter of type SparseCoding.
function IOSetParamSparseCoding(paramName::String, model::SparseCoding)
  ccall((:IO_SetParamSparseCodingPtr, sparse_codingLibrary), Nothing, (Cstring, Ptr{Nothing}), paramName, model.ptr)
end

# Serialize a model to the given stream.
function serializeSparseCoding(stream::IO, model::SparseCoding)
  buf_len = UInt[0]
  buf_ptr = ccall((:SerializeSparseCodingPtr, sparse_codingLibrary), Ptr{UInt8}, (Ptr{Nothing}, Ptr{UInt}), model.ptr, Base.pointer(buf_len))
  buf = Base.unsafe_wrap(Vector{UInt8}, buf_ptr, buf_len[1]; own=true)
  write(stream, buf)
end
# Deserialize a model from the given stream.
function deserializeSparseCoding(stream::IO)::SparseCoding
  buffer = read(stream)
  SparseCoding(ccall((:DeserializeSparseCodingPtr, sparse_codingLibrary), Ptr{Nothing}, (Ptr{UInt8}, UInt), Base.pointer(buffer), length(buffer)))
end
end # module

"""
    sparse_coding(; [atoms, initial_dictionary, input_model, lambda1, lambda2, max_iterations, newton_tolerance, normalize, objective_tolerance, seed, test, training, verbose])

An implementation of Sparse Coding with Dictionary Learning, which achieves
sparsity via an l1-norm regularizer on the codes (LASSO) or an (l1+l2)-norm
regularizer on the codes (the Elastic Net).  Given a dense data matrix X with d
dimensions and n points, sparse coding seeks to find a dense dictionary matrix D
with k atoms in d dimensions, and a sparse coding matrix Z with n points in k
dimensions.

The original data matrix X can then be reconstructed as Z * D.  Therefore, this
program finds a representation of each point in X as a sparse linear combination
of atoms in the dictionary D.

The sparse coding is found with an algorithm which alternates between a
dictionary step, which updates the dictionary D, and a sparse coding step, which
updates the sparse coding matrix.

Once a dictionary D is found, the sparse coding model may be used to encode
other matrices, and saved for future usage.

To run this program, either an input matrix or an already-saved sparse coding
model must be specified.  An input matrix may be specified with the `training`
option, along with the number of atoms in the dictionary (specified with the
`atoms` parameter).  It is also possible to specify an initial dictionary for
the optimization, with the `initial_dictionary` parameter.  An input model may
be specified with the `input_model` parameter.

As an example, to build a sparse coding model on the dataset `data` using 200
atoms and an l1-regularization parameter of 0.1, saving the model into `model`,
use

```julia
julia> using CSV
julia> data = CSV.read("data.csv")
julia> _, _, model = sparse_coding(atoms=200, lambda1=0.1,
            training=data)
```

Then, this model could be used to encode a new matrix, `otherdata`, and save the
output codes to `codes`:

```julia
julia> using CSV
julia> otherdata = CSV.read("otherdata.csv")
julia> codes, _, _ = sparse_coding(input_model=model,
            test=otherdata)
```

# Arguments

 - `atoms::Int`: Number of atoms in the dictionary.  Default value `15`.

 - `initial_dictionary::Array{Float64, 2}`: Optional initial dictionary
      matrix.
 - `input_model::SparseCoding`: File containing input sparse coding
      model.
 - `lambda1::Float64`: Sparse coding l1-norm regularization parameter.
      Default value `0`.

 - `lambda2::Float64`: Sparse coding l2-norm regularization parameter.
      Default value `0`.

 - `max_iterations::Int`: Maximum number of iterations for sparse coding
      (0 indicates no limit).  Default value `0`.

 - `newton_tolerance::Float64`: Tolerance for convergence of Newton
      method.  Default value `1e-06`.

 - `normalize::Bool`: If set, the input data matrix will be normalized
      before coding.  Default value `false`.

 - `objective_tolerance::Float64`: Tolerance for convergence of the
      objective function.  Default value `0.01`.

 - `seed::Int`: Random seed.  If 0, 'std::time(NULL)' is used.  Default
      value `0`.

 - `test::Array{Float64, 2}`: Optional matrix to be encoded by trained
      model.
 - `training::Array{Float64, 2}`: Matrix of training data (X).
 - `verbose::Bool`: Display informational messages and the full list of
      parameters and timers at the end of execution.  Default value `false`.

# Return values

 - `codes::Array{Float64, 2}`: Matrix to save the output sparse codes of
      the test matrix (--test_file) to.
 - `dictionary::Array{Float64, 2}`: Matrix to save the output dictionary
      to.
 - `output_model::SparseCoding`: File to save trained sparse coding model
      to.

"""
function sparse_coding(;
                       atoms::Union{Int, Missing} = missing,
                       initial_dictionary = missing,
                       input_model::Union{SparseCoding, Missing} = missing,
                       lambda1::Union{Float64, Missing} = missing,
                       lambda2::Union{Float64, Missing} = missing,
                       max_iterations::Union{Int, Missing} = missing,
                       newton_tolerance::Union{Float64, Missing} = missing,
                       normalize::Union{Bool, Missing} = missing,
                       objective_tolerance::Union{Float64, Missing} = missing,
                       seed::Union{Int, Missing} = missing,
                       test = missing,
                       training = missing,
                       verbose::Union{Bool, Missing} = missing,
                       points_are_rows::Bool = true)
  # Force the symbols to load.
  ccall((:loadSymbols, sparse_codingLibrary), Nothing, ());

  IORestoreSettings("Sparse Coding")

  # Process each input argument before calling mlpackMain().
  if !ismissing(atoms)
    IOSetParam("atoms", convert(Int, atoms))
  end
  if !ismissing(initial_dictionary)
    IOSetParamMat("initial_dictionary", initial_dictionary, points_are_rows)
  end
  if !ismissing(input_model)
    sparse_coding_internal.IOSetParamSparseCoding("input_model", convert(SparseCoding, input_model))
  end
  if !ismissing(lambda1)
    IOSetParam("lambda1", convert(Float64, lambda1))
  end
  if !ismissing(lambda2)
    IOSetParam("lambda2", convert(Float64, lambda2))
  end
  if !ismissing(max_iterations)
    IOSetParam("max_iterations", convert(Int, max_iterations))
  end
  if !ismissing(newton_tolerance)
    IOSetParam("newton_tolerance", convert(Float64, newton_tolerance))
  end
  if !ismissing(normalize)
    IOSetParam("normalize", convert(Bool, normalize))
  end
  if !ismissing(objective_tolerance)
    IOSetParam("objective_tolerance", convert(Float64, objective_tolerance))
  end
  if !ismissing(seed)
    IOSetParam("seed", convert(Int, seed))
  end
  if !ismissing(test)
    IOSetParamMat("test", test, points_are_rows)
  end
  if !ismissing(training)
    IOSetParamMat("training", training, points_are_rows)
  end
  if verbose !== nothing && verbose === true
    IOEnableVerbose()
  else
    IODisableVerbose()
  end

  IOSetPassed("codes")
  IOSetPassed("dictionary")
  IOSetPassed("output_model")
  # Call the program.
  sparse_coding_mlpackMain()

  return IOGetParamMat("codes", points_are_rows),
         IOGetParamMat("dictionary", points_are_rows),
         sparse_coding_internal.IOGetParamSparseCoding("output_model")
end


using ParserCombinator

mutable struct Direction
    current::Char
    next::Array{Direction}
end

function unwrap_any(a)
    if typeof(a) == Array{Any,1}
        return unwrap_any(a[1])
    end
    return a
end

function Direction(d::Array)::Direction
    return Direction(d[1][1], [])
end

function Joiner(a)
    if length(a) == 1
        return a
    elseif length(a) == 2
        a2 = collect(Iterators.flatten(a[2]))
        for n in a2
            push!(a[1].next, n[1][1])
        end
        return a2
    else
        println("LEngth more than 2")
        println(typeof(a))
        println(unwrap_any(a[2]))
    end
end

Dir = p"[NSEW]"|> Direction
All = Delayed()
Normal = Delayed()
Branch = Delayed()

Normal.matcher = Dir + Star(All) |> Joiner
Branch.matcher = E"(" + All + Star(E"|" + All) + E")" |> identity
All.matcher = Normal | Branch |> identity
Parser = All + Eos()

parse_one("N(S)W", Parser)


# *********************************************************************************
# REopt, Copyright (c) 2019-2020, Alliance for Sustainable Energy, LLC.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without modification,
# are permitted provided that the following conditions are met:
#
# Redistributions of source code must retain the above copyright notice, this list
# of conditions and the following disclaimer.
#
# Redistributions in binary form must reproduce the above copyright notice, this
# list of conditions and the following disclaimer in the documentation and/or other
# materials provided with the distribution.
#
# Neither the name of the copyright holder nor the names of its contributors may be
# used to endorse or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
# OF THE POSSIBILITY OF SUCH DAMAGE.
# *********************************************************************************
Base.@kwdef struct ElecStorage <: AbstractStorage
    min_kw::Float64 = 0.0
    max_kw::Float64 = 1.0e4
    min_kwh::Float64 = 0.0
    max_kwh::Float64 = 1.0e6
    internal_efficiency_pct::Float64 = 0.975
    inverter_efficiency_pct::Float64 = 0.96
    rectifier_efficiency_pct::Float64 = 0.96
    soc_min_pct::Float64 = 0.2
    soc_init_pct::Float64 = 0.5
    can_grid_charge::Bool = true
    installed_cost_per_kw::Float64 = 840.0
    cost_per_kwh::Float64 = 420.0
    replace_cost_per_kw::Float64 = 410.0
    replace_cost_per_kwh::Float64 = 200.0
    inverter_replacement_year::Int = 10
    battery_replacement_year::Int = 10
    macrs_option_years::Int = 7
    macrs_bonus_pct::Float64 = 1.0
    macrs_itc_reduction::Float64 = 0.5
    total_itc_pct::Float64 = 0.0
    total_rebate_per_kw::Float64 = 0.0
end

# TODO change all DenseAxisArray's into Dicts
struct Storage <: AbstractStorage
    types::Array{Symbol,1}
    min_kw::DenseAxisArray{Float64,1}
    max_kw::DenseAxisArray{Float64,1}
    min_kwh::DenseAxisArray{Float64,1}
    max_kwh::DenseAxisArray{Float64,1}
    charge_efficiency::DenseAxisArray{Float64,1}
    discharge_efficiency::DenseAxisArray{Float64,1}
    soc_min_pct::DenseAxisArray{Float64,1}
    soc_init_pct::DenseAxisArray{Float64,1}
    installed_cost_per_kw::DenseAxisArray{Float64,1}
    cost_per_kwh::DenseAxisArray{Float64,1}
    can_grid_charge::Array{Symbol,1}
    grid_charge_efficiency::Dict{Symbol, Float64}
end

"""
    function Storage(d::Dict{Symbol,Dict}, f::Financial)

Construct Storage struct from Dict with keys for each storage type (eg. :elec) and values with
    input dicts for each storage type. Note that the REopt inputs are indexed on the storage type.
"""
function Storage(d::Dict, f::Financial)  # nested dict
    types = Symbol[]
    can_grid_charge = Symbol[]
    raw_vals = Dict(zip(fieldnames(Storage), [Float64[] for _ in range(1, stop=fieldcount(Storage))]))
    # raw_vals has keys = fieldnames(Storage) and values = arrays of values for each type in types

    for (storage_type, input_dict) in d

        push!(types, storage_type)
        struct_name = string(titlecase(string(storage_type)) * "Storage")  # eg. ElecStorage
        storage_instance = eval(Meta.parse(struct_name * "(;$input_dict...)"))

        if storage_instance.can_grid_charge
            push!(can_grid_charge, storage_type)
        end
        fill_storage_vals!(raw_vals, storage_instance, storage_type, f)
    end

    storage_args = Dict(
        :types => types,
        :can_grid_charge => can_grid_charge,
    )
    d2 = Dict()  # Julia won't let me use storage_args: "unable to check bounds for indices of type Symbol"
    for k in keys(raw_vals)
        d2[k] = DenseAxisArray(raw_vals[k], types)
    end

    grid_charge_efficiency = Dict(:elec => convert(Float64, d2[:charge_efficiency][:elec]))

    return Storage(
        storage_args[:types],
        d2[:min_kw],
        d2[:max_kw],
        d2[:min_kwh],
        d2[:max_kwh],
        d2[:charge_efficiency],
        d2[:discharge_efficiency],
        d2[:soc_min_pct],
        d2[:soc_init_pct],
        d2[:installed_cost_per_kw],
        d2[:cost_per_kwh],
        storage_args[:can_grid_charge],
        grid_charge_efficiency
    )
    # TODO expand for smart thermostat
end

function fill_storage_vals!(d::Dict{Symbol, Array{Float64,1}}, s::AbstractStorage, t::Symbol, f::Financial)
    push!(d[:min_kw], s.min_kw)
    push!(d[:max_kw], s.max_kw)
    push!(d[:min_kwh], s.min_kwh)
    push!(d[:max_kwh], s.max_kwh)
    push!(d[:soc_min_pct], s.soc_min_pct)
    push!(d[:soc_init_pct], s.soc_init_pct)

    push!(d[:charge_efficiency], s.rectifier_efficiency_pct * s.internal_efficiency_pct^0.5)
    push!(d[:discharge_efficiency], s.inverter_efficiency_pct * s.internal_efficiency_pct^0.5)

    push!(d[:installed_cost_per_kw], effective_cost(;
        itc_basis=s.installed_cost_per_kw,
        replacement_cost=s.replace_cost_per_kw,
        replacement_year=s.inverter_replacement_year,
        discount_rate=f.owner_discount_pct,
        tax_rate=f.owner_tax_pct,
        itc=s.total_itc_pct,
        macrs_schedule = s.macrs_option_years == 7 ? f.macrs_seven_year : f.macrs_five_year,
        macrs_bonus_pct=s.macrs_bonus_pct,
        macrs_itc_reduction = s.macrs_itc_reduction,
        rebate_per_kw = s.total_rebate_per_kw
    ))
    push!(d[:cost_per_kwh], effective_cost(;
        itc_basis=s.cost_per_kwh,
        replacement_cost=s.replace_cost_per_kwh,
        replacement_year=s.inverter_replacement_year,
        discount_rate=f.owner_discount_pct,
        tax_rate=f.owner_tax_pct,
        itc=s.total_itc_pct,
        macrs_schedule=s.macrs_option_years == 7 ? f.macrs_seven_year : f.macrs_five_year,
        macrs_bonus_pct=s.macrs_bonus_pct,
        macrs_itc_reduction = s.macrs_itc_reduction
    ))
end


module HydrostaticBoussinesq

export HydrostaticBoussinesqModel, AbstractHydrostaticBoussinesqProblem, OceanDGModel,
       LinearHBModel, calculate_dt

using StaticArrays
using LinearAlgebra: I, dot, Diagonal
using ..VariableTemplates
using ..MPIStateArrays
using ..DGmethods: init_ode_state
using ..PlanetParameters: grav
using ..Mesh.Filters: CutoffFilter, apply!, ExponentialFilter
using ..Mesh.Grids: polynomialorder, VerticalDirection, HorizontalDirection, min_node_distance

using ..DGmethods.NumericalFluxes: Rusanov, CentralNumericalFluxGradient,
                                   CentralNumericalFluxDiffusive,
                                   CentralNumericalFluxNonDiffusive

import ..DGmethods.NumericalFluxes: update_penalty!, numerical_flux_diffusive!,
                                    NumericalFluxNonDiffusive

import ..DGmethods: BalanceLaw, vars_aux, vars_state, vars_gradient,
                    vars_diffusive, flux_nondiffusive!,
                    flux_diffusive!, source!, wavespeed,
                    boundary_state!, update_aux!, update_aux_diffusive!,
                    gradvariables!, init_aux!, init_state!,
                    LocalGeometry, DGModel, nodal_update_aux!, diffusive!,
                    copy_stack_field_down!, create_state, calculate_dt,
                    vars_integrals, vars_reverse_integrals,
                    indefinite_stack_integral!,
                    reverse_indefinite_stack_integral!,
                    integral_load_aux!, integral_set_aux!,
                    reverse_integral_load_aux!,
                    reverse_integral_set_aux!

×(a::SVector, b::SVector) = StaticArrays.cross(a, b)
∘(a::SVector, b::SVector) = StaticArrays.dot(a, b)

abstract type AbstractHydrostaticBoussinesqProblem end

"""
    HydrostaticBoussinesqModel <: BalanceLaw

A `BalanceLaw` for ocean modeling.

write out the equations here

# Usage

    HydrostaticBoussinesqModel(problem)

"""
struct HydrostaticBoussinesqModel{P,T} <: BalanceLaw
  problem::P
  cʰ::T
  cᶻ::T
  αᵀ::T
  νʰ::T
  νᶻ::T
  κʰ::T
  κᶻ::T
  function HydrostaticBoussinesqModel{FT}(problem;
                                      cʰ = FT(0),     # m/s
                                      cᶻ = FT(0),     # m/s
                                      αᵀ = FT(2e-4),  # (m/s)^2 / K
                                      νʰ = FT(5e3),   # m^2 / s
                                      νᶻ = FT(5e-3),  # m^2 / s
                                      κʰ = FT(1e3),   # m^2 / s
                                      κᶻ = FT(1e-4),  # m^2 / s
                                      ) where {FT <: AbstractFloat}
    return new{typeof(problem),FT}(problem, cʰ, cᶻ, αᵀ, νʰ, νᶻ, κʰ, κᶻ)
  end
end
HBModel = HydrostaticBoussinesqModel

"""
    calculate_dt(grid, model::HBModel)

calculates the time step based on grid spacing and model parameters
takes minimum of gravity wave, diffusive, and viscous CFL
factor of 1000 in the diffusive CFL is to handle the convective adjustment

"""
function calculate_dt(grid, model::HBModel, Courant_number)
    minΔx = min_node_distance(grid, HorizontalDirection())
    minΔz = min_node_distance(grid, VerticalDirection())

    CFL_gravity = minΔx / model.cʰ
    CFL_diffusive = minΔz^2 / (1000 * model.κᶻ)
    CFL_viscous = minΔz^2 / model.νᶻ

    dt = 1//2 * minimum([CFL_gravity, CFL_diffusive, CFL_viscous])

    return dt
end

"""
    OceanDGModel()

helper function to add required filtering
not used in the Driver+Config setup
"""
function OceanDGModel(bl::HBModel, grid, numfluxnondiff, numfluxdiff,
                      gradnumflux; kwargs...)
  vert_filter = CutoffFilter(grid, polynomialorder(grid)-1)
  exp_filter  = ExponentialFilter(grid, 1, 8)

  modeldata = (vert_filter = vert_filter, exp_filter=exp_filter)

  return DGModel(bl, grid, numfluxnondiff, numfluxdiff, gradnumflux;
                 kwargs..., modeldata=modeldata)
end

"""
    vars_state(::HBModel)

prognostic variables evolved forward in time

u = (u,v) = (zonal velocity, meridional velocity)
η = sea surface height
θ = temperature
"""
# If this order is changed check the filter usage!
function vars_state(m::HBModel, T)
  @vars begin
    u::SVector{2, T}
    η::T # real a 2-D variable TODO: should be 2D
    θ::T
  end
end

"""
    init_state!(::HBModel)

sets the initial value for state variables
dispatches to ocean_init_state! which is defined in a problem file such as SimpleBoxProblem.jl
"""
function ocean_init_state! end
function init_state!(m::HBModel, Q::Vars, A::Vars, coords, t)
  return ocean_init_state!(m.problem, Q, A, coords, t)
end

"""
    vars_aux(::HBModel)
helper variables for computation

first half is because there is no dedicated integral kernels
these variables are used to compute vertical integrals
w = vertical velocity
w_reverse = w but integrated in the opposite direction
wz0 = w at z = 0
pkin = bulk hydrostatic pressure contribution
pkin_reverse = pkin but integrated in the opposite direction

second half of these are fields that are used for computation
θʳ = relaxation value of the sea surface temperature
f = coriolis force
τ = wind stress
ν = vector of viscosities (zonal, meridional, vertical)
κ = vector of diffusivities (zonal, meridional, vertical)
"""
# If this order is changed check update_aux!
function vars_aux(m::HBModel, T)
  @vars begin
    w::T
    pkin::T         # ∫(-αᵀ θ)
    wz0::T          # w at z=0
    θʳ::T           # SST given    # TODO: Should be 2D
    f::T            # coriolis
    τ::T            # wind stress  # TODO: Should be 2D
    ν::SVector{3, T}
    κ::SVector{3, T}
  end
end

"""
    init_aux!(::HBModel)

sets the initial value for auxiliary variables (those that aren't related to vertical integrals)
dispatches to ocean_init_aux! which is defined in a problem file such as SimpleBoxProblem.jl
"""
function ocean_init_aux! end
function init_aux!(m::HBModel, A::Vars, geom::LocalGeometry)
  return ocean_init_aux!(m, m.problem, A, geom)
end

"""
    vars_gradient(::HBModel)

variables that you want to take a gradient of
these are just copies in our model
"""
function vars_gradient(m::HBModel, T)
  @vars begin
    u::SVector{2, T}
    θ::T
  end
end

"""
    gradvariables!(::HBModel)

copy u and θ to var_gradient
this computation is done pointwise at each nodal point

# arguments:
- `m`: model in this case HBModel
- `G`: array of gradient variables
- `Q`: array of state variables
- `A`: array of aux variables
- `t`: time, not used
"""
@inline function gradvariables!(m::HBModel, G::Vars, Q::Vars, A, t)
  G.u = Q.u
  G.θ = Q.θ

  return nothing
end

"""
    vars_diffusive(::HBModel)

the output of the gradient computations
once again just copies, we don't do any transforms or reductions
"""
function vars_diffusive(m::HBModel, T)
  @vars begin
    ∇u::SMatrix{3, 2, T, 6}
    ∇θ::SVector{3, T}
  end
end

"""
    diffusive!(::HBModel)

copy ∇u and ∇θ to var_diffusive
this computation is done pointwise at each nodal point

# arguments:
- `m`: model in this case HBModel
- `D`: array of diffusive variables
- `G`: array of gradient variables
- `Q`: array of state variables
- `A`: array of aux variables
- `t`: time, not used
"""
@inline function diffusive!(m::HBModel, D::Vars, G::Grad, Q::Vars,
                            A::Vars, t)
  D.∇u = G.u
  D.∇θ = G.θ

  return nothing
end

"""
    vars_integral(::HBModel)

location to store integrands for bottom up integrals
∇hu = the horizontal divegence of u, e.g. dw/dz
"""
function vars_integrals(m::HBModel, T)
  @vars begin
    ∇hu::T
  end
end

"""
    integral_load_aux!(::HBModel)

copy w to var_integral
this computation is done pointwise at each nodal point

arguments:
m -> model in this case HBModel
I -> array of integrand variables
Q -> array of state variables
A -> array of aux variables
"""
@inline function integral_load_aux!(m::HBModel, I::Vars, Q::Vars, A::Vars)
  I.∇hu = A.w # borrow the w value from A...

  return nothing
end

"""
    integral_set_aux!(::HBModel)

copy integral results back out to aux
this computation is done pointwise at each nodal point

arguments:
m -> model in this case HBModel
A -> array of aux variables
I -> array of integrand variables
"""
@inline function integral_set_aux!(m::HBModel, A::Vars, I::Vars)
  A.w = I.∇hu

  return nothing
end

"""
    vars_reverse_integral(::HBModel)

location to store integrands for top down integrals
αᵀθ = density perturbation
"""
function vars_reverse_integrals(m::HBModel, T)
  @vars begin
    αᵀθ::T
  end
end

"""
    reverse_integral_load_aux!(::HBModel)

copy αᵀθ to var_reverse_integral
this computation is done pointwise at each nodal point

arguments:
m -> model in this case HBModel
I -> array of integrand variables
A -> array of aux variables
"""
@inline function reverse_integral_load_aux!(m::HBModel, I::Vars, Q::Vars, A::Vars)
  I.αᵀθ = A.pkin

  return nothing
end

"""
    reverse_integral_set_aux!(::HBModel)

copy reverse integral results back out to aux
this computation is done pointwise at each nodal point

arguments:
m -> model in this case HBModel
A -> array of aux variables
I -> array of integrand variables
"""
@inline function reverse_integral_set_aux!(m::HBModel, A::Vars, I::Vars)
  A.pkin = I.αᵀθ

  return nothing
end

"""
    flux_nondiffusive!(::HBModel)

calculates the hyperbolic flux contribution to state variables
this computation is done pointwise at each nodal point

# arguments:
m -> model in this case HBModel
F -> array of fluxes for each state variable
Q -> array of state variables
A -> array of aux variables
t -> time, not used

# computations
∂ᵗu = ∇∘(g*η + g∫αᵀθdz + v∘u)
∂ᵗθ = ∇∘(vθ) where v = (u,v,w)
"""
@inline function flux_nondiffusive!(m::HBModel, F::Grad, Q::Vars,
                                    A::Vars, t::Real)
  @inbounds begin
    u = Q.u # Horizontal components of velocity
    η = Q.η
    θ = Q.θ
    w = A.w   # vertical velocity
    pkin = A.pkin

    v = @SVector [u[1], u[2], w]
    Ih = @SMatrix [ 1 -0;
                   -0  1;
                   -0 -0]

    # ∇h • (g η)
    F.u += grav * η * Ih

    # ∇h • (- ∫(αᵀ θ))
    F.u += grav * pkin * Ih

    # ∇h • (v ⊗ u)
    # F.u += v * u'

    # ∇ • (u θ)
    F.θ += v * θ
  end

  return nothing
end

"""
    flux_diffusive!(::HBModel)

calculates the parabolic flux contribution to state variables
this computation is done pointwise at each nodal point

# arguments:
- `m`: model in this case HBModel
- `F`: array of fluxes for each state variable
- `Q`: array of state variables
- `D`: array of diff variables
- `A`: array of aux variables
- `t`: time, not used

# computations
∂ᵗu = -∇∘(ν∇u)
∂ᵗθ = -∇∘(κ∇θ)
"""
@inline function flux_diffusive!(m::HBModel, F::Grad, Q::Vars, D::Vars,
                                 A::Vars, t::Real)
  F.u -= Diagonal(A.ν) * D.∇u
  F.θ -= Diagonal(A.κ) * D.∇θ

  return nothing
end

"""
    source!(::HBModel)
    calculates the source term contribution to state variables
    this computation is done pointwise at each nodal point

    arguments:
    m -> model in this case HBModel
    F -> array of fluxes for each state variable
    Q -> array of state variables
    A -> array of aux variables
    t -> time, not used

    computations
    ∂ᵗu = -f×u
    ∂ᵗη = w|(z=0)
"""
@inline function source!(m::HBModel{P}, source::Vars, Q::Vars,
                         diffusive::Vars, A::Vars, t::Real) where P
  @inbounds begin
    u,v = Q.u # Horizontal components of velocity
    f = A.f
    wz0 = A.wz0

    # f × u
    source.u -= @SVector [-f * v, f * u]

    source.η += wz0
  end

  return nothing
end

"""
    wavespeed(::HBModel)

calculates the wavespeed for rusanov flux
"""
@inline wavespeed(m::HBModel, n⁻, _...) = abs(SVector(m.cʰ, m.cʰ, m.cᶻ)' * n⁻)

"""
    update_penalty(::HBModel)
    set Δη = 0 when computing numerical fluxes
"""
# We want not have jump penalties on η (since not a flux variable)
function update_penalty!(::Rusanov, ::HBModel, n⁻, λ, ΔQ::Vars,
                         Q⁻, A⁻, Q⁺, A⁺, t)
  ΔQ.η = -0

  return nothing
end

"""
    update_aux!(::HBModel)

    applies the vertical filter to the zonal and meridional velocities to preserve numerical incompressibility
    applies an exponential filter to θ to anti-alias the non-linear advective term

    doesn't actually touch the aux variables any more, but we need a better filter interface than this anyways
"""
function update_aux!(dg::DGModel, m::HBModel, Q::MPIStateArray, t::Real)
  MD = dg.modeldata

  # required to ensure that after integration velocity field is divergence free
  vert_filter = MD.vert_filter
  # Q[1] = u[1] = u, Q[2] = u[2] = v
  apply!(Q, (1, 2), dg.grid, vert_filter, VerticalDirection())

  exp_filter = MD.exp_filter
  # Q[4] = θ
  apply!(Q, (4,), dg.grid, exp_filter, VerticalDirection())

  return true
end

"""
    update_aux_diffusive!(::HBModel)

    ∇hu to w for integration
    performs integration for w and pkin (should be moved to its own integral kernels)
    copies down w and wz0 because we don't have 2D structures

    now for actual update aux stuff
    implements convective adjustment by bumping the vertical diffusivity up by a factor of 1000 if dθdz < 0
"""
function update_aux_diffusive!(dg::DGModel, m::HBModel, Q::MPIStateArray, t::Real)
  A  = dg.auxstate

  # store ∇ʰu as integrand for w
  # update vertical diffusivity for convective adjustment
  function f!(m::HBModel, Q, A, D, t)
    @inbounds begin
      A.w = -(D.∇u[1,1] + D.∇u[2,2])
      A.pkin = -m.αᵀ * Q.θ

      D.∇θ[3] < 0 ? A.κ = (m.κʰ, m.κʰ, 1000 * m.κᶻ) : A.κ = (m.κʰ, m.κʰ, m.κᶻ)
    end

    return nothing
  end
  nodal_update_aux!(f!, dg, m, Q, t; diffusive=true)

  # compute integrals for w and pkin
  indefinite_stack_integral!(dg, m, Q, A, t) # bottom -> top
  reverse_indefinite_stack_integral!(dg, m, Q, A, t) # top -> bottom

  # project w(z=0) down the stack
  # Need to be consistent with vars_aux
  # A[1] = w, A[3] = wz0
  copy_stack_field_down!(dg, m, A, 1, 3)

  return true
end

"""
    boundary_state!(nf, ::HBModel, Q⁺, A⁺, Q⁻, A⁻, bctype)

applies boundary conditions for the hyperbolic fluxes
dispatches to a function in OceanBoundaryConditions.jl based on bytype defined by a problem such as SimpleBoxProblem.jl
"""
@inline function boundary_state!(nf, m::HBModel, Q⁺::Vars, A⁺::Vars, n⁻,
                                 Q⁻::Vars, A⁻::Vars, bctype, t, _...)
  return ocean_boundary_state!(m, m.problem, bctype, nf, Q⁺, A⁺, n⁻, Q⁻, A⁻, t)
end

"""
    boundary_state!(nf, ::HBModel, Q⁺, D⁺, A⁺, Q⁻, D⁻, A⁻, bctype)

applies boundary conditions for the parabolic fluxes
dispatches to a function in OceanBoundaryConditions.jl based on bytype defined by a problem such as SimpleBoxProblem.jl
"""
@inline function boundary_state!(nf, m::HBModel,
                                 Q⁺::Vars, D⁺::Vars, A⁺::Vars,
                                 n⁻,
                                 Q⁻::Vars, D⁻::Vars, A⁻::Vars,
                                 bctype, t, _...)
  return ocean_boundary_state!(m, m.problem, bctype, nf, Q⁺, D⁺, A⁺, n⁻, Q⁻, D⁻, A⁻, t)
end

include("OceanBoundaryConditions.jl")
include("LinearHBModel.jl")

end


using YAML
using Unitful

function convertchemkin2yml(chemkinpath;spcdictpath="",output="chem.rms")
    if spcdictpath != ""
        spcs,rxns = chemkin.load_chemkin_file(chemkinpath,dictionary_path=spcdictpath)
    else
        spcs,rxns = chemkin.load_chemkin_file(chemkinpath)
    end
    D = getmechdictfromchemkin(spcs,rxns)
    writeyml(D;path=output)
end

function writeyml(D;path="chem.rms")
    YAML.write_file(path,D)
end

function getmechdictfromchemkin(spcs,rxns)
    names = [x.label for x in spcs]
    for (i,name) in enumerate(names)
        if count(names.==name) > 1
            names[i] = string(name,"-",count(names.==name))
        end
    end
    D = Dict([])
    D["Units"] = Dict([])
    D["Reactions"] = []
    D["Phases"] = [Dict([])]
    D["Phases"][1]["name"] = "phase"
    D["Phases"][1]["Species"] = [obj2dict(x,spcs,names) for x in spcs]
    D["Reactions"] = [obj2dict(x,spcs,names) for x in rxns]
    return D
end

function convertcanterayml2rmsyml(path;output="chem.rms")
    canterayml = YAML.load_file(path)
    D = getmechdictfromcantera(canterayml)
    writeyml(D;path=output)
end

function getmechdictfromcantera(canterayml)
    unsupportedphases = [
        "binary-solution-tabulated",
        "compound-lattice",
        "constant-density",
        "Debye-Huckel",
        "edge",
        "electron-cloud",
        "fixed-stoichiometry",
        "HMW-electrolyte",
        # "ideal-gas",
        "ideal-molal-solution",
        "ideal-condensed",
        "ideal-solution-VPSS",
        # "ideal-surface",
        "ions-from-neutral-molecule",
        "lattice",
        "liquid-water-IAPWS95",
        "Margules",
        "Maskell-solid-solution",
        "Peng-Robinson",
        "plasma",
        "pure-fluid",
        "Redlich-Kister",
        "Redlich-Kwong",
    ]

    spcs = canterayml["species"]
    rxns = canterayml["reactions"]
    phases = canterayml["phases"]
    units = canterayml["units"] #user specified units
    units = Dict(key=>uparse(value) for (key,value) in units) #convert to Unitful units

    #prevent duplicate names
    spc_names = [spc["name"] for spc in spcs]
    for (i,name) in enumerate(spc_names)
        if count(spc_names.==name) > 1
            k = 2
            new_name = name
            while new_name in spc_names
                new_name = string(name,"-",k)
            end
            spc_names[i] = new_name
        end
    end

    D = Dict([])
    D["Units"] = Dict([]) #will be converting all values to SI
    D["Phases"] = []
    for phase in phases
        if phase["thermo"] in unsupportedphases
            @error "Currently not supporting $(phase["thermo"])"
        end
        phasedict = Dict()
        phasedict["name"] = phase["name"]
        phasedict["Species"] = [canteradict2rmsdict(spc,spcs,spc_names,units,:species) for spc in spcs if spc["name"] in phase["species"]]
        push!(D["Phases"],phasedict)
    end
    D["Reactions"] = [canteradict2rmsdict(rxn,spcs,spc_names,units,:reaction) for rxn in rxns]
    return D
end

function getradicals(obj::T) where {T}
    sm = obj.molecule[1].to_smiles()
    if sm == "[O][O]"
        return 0
    else
        return obj.molecule[1].multiplicity-1
    end
end

function obj2dict(obj,spcs,names;label="solvent")
    D = Dict([])
    if pybuiltin("isinstance")(obj,species.Species)
        D["name"] = names[findall(x->x==obj,spcs)[1]]
        D["type"] = "Species"
        if length(obj.molecule) == 0
            println(obj)
            println(obj.label)
        end
        D["smiles"] = obj.molecule[1].to_smiles()
        D["thermo"] = obj2dict(obj.thermo,spcs,names)
        if D["smiles"] != "[O][O]"
            D["radicalelectrons"] = obj.molecule[1].multiplicity-1
        else
            D["radicalelectrons"] = 0
        end
    elseif pybuiltin("isinstance")(obj,nasa.NASA)
        D["polys"] = [obj2dict(k,spcs,names) for k in obj.polynomials]
        D["type"] = "NASA"
    elseif pybuiltin("isinstance")(obj,nasa.NASAPolynomial)
        D["type"] = "NASApolynomial"
        D["coefs"] = PyVector(obj.coeffs)
        D["Tmax"] = obj.Tmax.value_si
        D["Tmin"] = obj.Tmin.value_si
    elseif pybuiltin("isinstance")(obj,reaction.Reaction)
        D["reactants"] = [names[findall(y->y==x,spcs)[1]]  for x in obj.reactants]
        D["products"] = [names[findall(y->y==x,spcs)[1]] for x in obj.products]
        D["kinetics"] = obj2dict(obj.kinetics,spcs,names)
        D["type"] = "ElementaryReaction"
        D["radicalchange"] = sum([getradicals(x) for x in obj.products])-sum([getradicals(x) for x in obj.reactants])
        D["reversible"] = obj.reversible
    elseif pybuiltin("isinstance")(obj,arrhenius.Arrhenius)
        D["type"] = "Arrhenius"
        D["A"] = obj.A.value_si
        D["Ea"] = obj.Ea.value_si
        D["n"] = obj.n.value_si
    elseif pybuiltin("isinstance")(obj,arrhenius.PDepArrhenius)
        D["type"] = "PdepArrhenius"
        D["Ps"] = PyVector(obj.pressures.value_si)
        D["arrs"] = [obj2dict(x,spcs,names) for x in obj.arrhenius]
    elseif pybuiltin("isinstance")(obj,arrhenius.MultiArrhenius)
        D["type"] = "MultiArrhenius"
        D["arrs"] = [obj2dict(x,spcs,names) for x in obj.arrhenius]
    elseif pybuiltin("isinstance")(obj,arrhenius.MultiPDepArrhenius)
        D["type"] = "MultiPdepArrhenius"
        D["parrs"] = [obj2dict(x,spcs,names) for x in obj.arrhenius]
    elseif pybuiltin("isinstance")(obj,falloff.ThirdBody)
        D["type"] = "ThirdBody"
        D["arr"] = obj2dict(obj.arrheniusLow,spcs,names)
        D["efficiencies"] = Dict([spcs[i].label=>float(val) for (i,val) in enumerate(obj.get_effective_collider_efficiencies(spcs)) if val != 1])
    elseif pybuiltin("isinstance")(obj,falloff.Lindemann)
        D["type"] = "Lindemann"
        D["arrhigh"] = obj2dict(obj.arrheniusHigh,spcs,names)
        D["arrlow"] = obj2dict(obj.arrheniusLow,spcs,names)
        D["efficiencies"] = Dict([spcs[i].label=>float(val) for (i,val) in enumerate(obj.get_effective_collider_efficiencies(spcs)) if val != 1])
    elseif pybuiltin("isinstance")(obj,falloff.Troe)
        D["type"] = "Troe"
        D["arrhigh"] = obj2dict(obj.arrheniusHigh,spcs,names)
        D["arrlow"] = obj2dict(obj.arrheniusLow,spcs,names)
        D["efficiencies"] = Dict([spcs[i].label=>float(val) for (i,val) in enumerate(obj.get_effective_collider_efficiencies(spcs)) if val != 1])
        D["a"] = obj.alpha
        D["T1"] = obj.T1.value_si
        if !isa(obj.T2,Nothing)
            D["T2"] = obj.T2.value_si
        else
            D["T2"] = 0.0
        end
        D["T3"] = obj.T3.value_si
    elseif pybuiltin("isinstance")(obj,chebyshev.Chebyshev)
        D["type"] = "Chebyshev"
        n,m = size(obj.coeffs.value_si)
        D["coefs"] = PyVector([PyVector(obj.coeffs.value_si[i,:]) for i in 1:n])
        D["Tmin"] = obj.Tmin.value_si
        D["Tmax"] = obj.Tmax.value_si
        D["Pmin"] = obj.Pmin.value_si
        D["Pmax"] = obj.Pmax.value_si
    elseif pybuiltin("isinstance")(obj,wilhoit.Wilhoit)
        D["type"] = "Wilhoit"
        D["coefs"] = [obj.a0,obj.a1,obj.a2,obj.a3]
        D["Cp0"] = obj.Cp0.value_si
        D["Cpinf"] = obj.CpInf.value_si
        D["H0"] = obj.H0.value_si
        D["S0"] = obj.S0.value_si
        D["B"] = obj.B.value_si
    elseif pybuiltin("isinstance")(obj,solvation.SolventData)
        D["type"] = "Solvent"
        D["name"] = label
        dsub = dict()
        dsub["type"] = "RiedelViscosity"
        dsub["A"] = obj.A.value_si
        dsub["B"] = obj.B.value_si
        dsub["C"] = obj.C.value_si
        dsub["D"] = obj.D.value_si
        dsub["E"] = obj.E.value_si
        D["mu"] = dsub
    elseif pybuiltin("is")(obj,pybuiltin("None"))
        return pybuiltin("None")
    else
        throw(error("object $obj not understood"))
    end
    return D
end

function canteradict2rmsdict(canteradict,spcs,names,units,dict_type;numreactants=nothing,polyindex=nothing)
    D = Dict([])
    if dict_type == :species #species
        D["name"] = names[findall(x->x==canteradict,spcs)[1]]
        D["type"] = "Species"
        D["smiles"] = ""
        D["thermo"] = canteradict2rmsdict(canteradict["thermo"],spcs,names,units,:thermo)
        if haskey(canteradict,"equation-of-state")
            model = canteradict["equation-of-state"]["model"]
            @error "Currently not supporting $(model) thermo model"
        end
    elseif dict_type == :thermo
        if canteradict["model"] == "NASA7"
            D["type"] = "NASA"
            D["polys"] = [canteradict2rmsdict(canteradict,spcs,names,units,:NASApolynomial;polyindex=i) for i in 1:(length(canteradict["temperature-ranges"])-1)]
        else
             @error "Currently only support NASA7 thermo model from Cantera"
        end
    elseif dict_type == :NASApolynomial
        D["type"] = "NASApolynomial"
        D["Tmax"] = canteradict["temperature-ranges"][polyindex+1]
        D["Tmin"] = canteradict["temperature-ranges"][polyindex]
        D["coefs"] = canteradict["data"][polyindex,:][1]
    elseif dict_type == :reaction #reaction
        equation = canteradict["equation"]
        reversible = true
        if occursin(" <=> ",equation) #reversible
            reactants, products = split(canteradict["equation"]," <=> ")
        elseif occursin(" => ",equation) #irreversible
            reactants, products = split(canteradict["equation"]," => ")
            reversible = false
        end
        reactants = _interpretstoichstring(reactants,names)
        products = _interpretstoichstring(products,names)
        kinetics = canteradict2rmsdict(canteradict,spcs,names,units,:kinetics,numreactants=length(reactants))
        D["reactants"] = reactants
        D["products"] = products
        D["kinetics"] = kinetics
        D["reversible"] = reversible
        D["type"] = "ElementaryReaction"
    elseif dict_type == :kinetics
        if haskey(canteradict,"type")
            kinetics_type = canteradict["type"]
            if kinetics_type == "three-body"
                D["type"] = "ThirdBody"
                D["arr"] = canteradict2rmsdict(canteradict["rate-constant"],spcs,names,units,:arrhenius,numreactants=numreactants+1)
                D["efficiencies"] = get(canteradict,"efficiencies",Dict([]))
            elseif kinetics_type == "falloff"
                if haskey(canteradict,"Troe")
                    D["type"] = "Troe"
                    D["arrhigh"] = canteradict2rmsdict(canteradict["high-P-rate-constant"],spcs,names,units,:arrhenius,numreactants=numreactants)
                    D["arrlow"] = canteradict2rmsdict(canteradict["low-P-rate-constant"],spcs,names,units,:arrhenius,numreactants=numreactants+1)
                    D["efficiencies"] = get(canteradict,"efficiencies",Dict([]))
                    D["a"] = canteradict["Troe"]["A"]
                    D["T1"] = canteradict["Troe"]["T1"]
                    if haskey(canteradict["Troe"],"T2")
                        D["T2"] = canteradict["Troe"]["T2"]
                    else
                        D["T2"] = 0.0
                    end
                    D["T3"] = canteradict["Troe"]["T3"]
                else #Lindemann
                    D["type"] = "Lindemann"
                    D["arrhigh"] = canteradict2rmsdict(canteradict["high-P-rate-constant"],spcs,names,units,:arrhenius,numreactants=numreactants)
                    D["arrlow"] = canteradict2rmsdict(canteradict["low-P-rate-constant"],spcs,names,units,:arrhenius,numreactants=numreactants+1)
                    D["efficiencies"] = get(canteradict,"efficiencies",Dict([]))
                end
            elseif kinetics_type == "pressure-dependent-Arrhenius"
                D["type"] = "PdepArrhenius"
                D["Ps"] = [kinetics["P"] for kinetics in canteradict["rate-constants"]]
                D["arrs"] = [canteradict2rmsdict(kinetics,spcs,names,units,:arrhenius,numreactants=numreactants) for kinetics in canteradict["rate-constants"]]
            elseif kinetics_type == "Chebyshev"
                D["type"] = "Chebyshev"
                D["coefs"] = [canteradict["data"][:,i] for i in 1:size(canteradict["data"],2)]
                D["Tmin"] = tosivalue(canteradict["temperature-range"][1],units=units,value_type=:temperature)
                D["Tmax"] = tosivalue(canteradict["temperature-range"][end],units=units,value_type=:temperature)
                D["Pmin"] = tosivalue(canteradict["pressure-range"][1],units=units,value_type=:pressure)
                D["Pmax"] = tosivalue(canteradict["pressure-range"][end],units=units,value_type=:pressure)
            else
                @error "Currently not supporting $(kinetics_type)"
            end
        elseif haskey(canteradict,"sticking-coefficient")
            D = canteradict2rmsdict(canteradict["sticking-coefficient"],spcs,names,units,:arrhenius,numreactants=0)
            D["type"] = "StickingCoefficient"
        elseif haskey(canteradict,"rate-constant") #arrhenius
            D = canteradict2rmsdict(canteradict["rate-constant"],spcs,names,units,:arrhenius,numreactants=numreactants)
        else
            @error "Currently not supporting $(canteradict) type kinetics"
        end
        if haskey(canteradict,"coverage-dependencies")
            @error "Currently not supporting coverage dependencies"
        end
    elseif dict_type == :arrhenius
        D["type"] = "Arrhenius"
        D["A"] = tosivalue(canteradict["A"],units=units,value_type=:Afactor,numreactants=numreactants)
        D["Ea"] = tosivalue(canteradict["Ea"],units=units,value_type=:activationenergy)
        D["n"] = canteradict["b"]
    end
    return D
end

function _interpretstoichstring(spcs,names)
    spc_names = Vector{String}()

    spcs = split(spcs," ")
    for (ind,item) in enumerate(spcs)
        stoich = tryparse(Int64,item)
        if stoich != nothing
            spc = spcs[ind+1]
            append!(spc_names,[spc for i in 1:(stoich-1)])
        else
            if item in names
                spc = item
                push!(spc_names,spc)
            end
        end
    end
    return spc_names
end

function tosivalue(value;units=nothing,value_type=nothing,numreactants=nothing)

    if value isa String #unit specified for this individual value
        value,unit = split(value," ")
        value = parse(Float64,value)
        unit = uparse(unit)
    else
        if value_type == :temperature
            unit = get(units,"temperature",Unitful.K)
        elseif value_type == :pressure
            unit = get(units,"pressure",Unitful.Pa)
        elseif value_type == :activationenergy
            unit = get(units,"activation-energy",nothing)
            if unit == nothing
                quantity_unit = get(units,"quantity",Unitful.mol)
                energy_unit = get(units,"energy",Unitful.J)
                unit = (energy_unit)/(quantity_unit)
            end
        elseif value_type == :Afactor
            time_unit = get(units,"time",Unitful.s)
            length_unit = get(units,"length",Unitful.m)
            quantity_unit = get(units,"quantity",Unitful.mol)
            if numreactants == 1
                unit = time_unit^(-1)
            elseif numreactants == 2
                unit = (length_unit)^3/(quantity_unit*time_unit)
            elseif numreactants == 3
                unit = (length_unit)^6/((quantity_unit)^2*(time_unit))
            elseif numreactants == 0
                return value
            end
        end
    end
    return upreferred(value*unit).val
end


using UPPE
using Test

using PhysicalConstants.CODATA2018
const C0 = SpeedOfLightInVacuum.val

cd(@__DIR__)

@testset "UPPE.jl" begin
    include("test_t.jl")
    include("test_rt.jl")
end

rm("results", recursive=true)


# Based on ChainRules.jl

# TODO: Is it fine to re-use the executor for the primal? Should it use more
# "uniform" execution parameter?

function ChainRulesCore.rrule(
    ::typeof(Folds.sum),
    ::typeof(identity),
    xs::AbstractArray,
    ex::Executor,
)
    project = ProjectTo(xs)
    shape = size(xs)
    y = Folds.sum(xs, ex)
    function sum_pullback(dy_raw)
        if dy_raw isa NoTangent
            return (NoTangent(), NoTangent(), NoTangent(), NoTangent())
        end
        dy = unthunk(dy_raw)
        xs_thunk = InplaceableThunk(
            dx -> unsum!(dx, dy, ex),
            @thunk(project(unsum(shape, dy, ex))),
        )
        return (NoTangent(), NoTangent(), xs_thunk, NoTangent())
    end
    return y, sum_pullback
end

function unsum!(dx, dy, ex)
    Folds.foreach(referenceable(dx), ex) do r
        r[] += dy
    end
    dx
end

function unsum(shape, dy, ex)
    xs = Folds.map(_ -> dy, 1:prod(shape; init = 1), ex)
    return reshape(xs, shape)
end

function ChainRulesCore.rrule(
    config::RuleConfig{>:HasReverseMode},
    ::typeof(Folds.sum),
    f,
    xs::AbstractArray,
    ex::Executor,
)
    # TODO: Is it fine to re-use the executor for the primal? Should it use more
    # "uniform" execution parameter?

    # TODO: don't allocate an "intermediate" array for `f(x)`s?
    fx_and_pullbacks = Folds.map(x -> rrule_via_ad(config, f, x), xs, ex)
    y = Folds.sum(first, fx_and_pullbacks, ex)

    pullbacks = Folds.map(last, fx_and_pullbacks, ex)

    project = ProjectTo(xs)

    function sum_pullback(ȳ)
        f̄_and_x̄s = Folds.map(f -> f(ȳ), pullbacks, ex)
        # no point thunking as most of work is in f̄_and_x̄s which we need to compute for both
        f̄ = if fieldcount(typeof(f)) === 0 # Then don't eed to worry about derivative wrt f
            NoTangent()
        else
            Folds.sum(first, f̄_and_x̄s, ex)
        end
        x̄s = Folds.map(unthunk ∘ last, f̄_and_x̄s, ex) # project does not support receiving InplaceableThunks
        return NoTangent(), f̄, project(x̄s), NoTangent()
    end
    return y, sum_pullback
end


srate = 44100
# convenience function to calculate the mean-squared error
mse(x,y) = norm(x-y)^2/length(x)

# Generates a 100-sample 2-channel signal
function gen_reference(srate)
    t = collect(0:99) / srate
    phase = [2pi*440t 2pi*880t]
    0.5sin.(phase)
end

# reference file generated with Audacity. Careful to turn dithering off
# on export for deterministic output!
reference_wav = joinpath("data", "440left_880right_0.5amp.wav")
reference_wav_float = joinpath("data", "440left_880right_0.5amp_float.wav")
reference_wav_double = joinpath("data","440left_880right_0.5amp_double.wav")
reference_wav_pcm24 = joinpath("data", "440left_880right_0.5amp_pcm24.wav")
reference_ogg = joinpath("data", "440left_880right_0.5amp.ogg")
reference_flac = joinpath("data", "440left_880right_0.5amp.flac")
reference_buf = gen_reference(srate)

# define some loaders and savers that bypass FileIO's detection machinery, of
# the form:
# load_wav(io::String, args...) = LibSndFile.load(File(format"WAV", io), args...)
# load_wav(io::IO, args...) = LibSndFile.load(Stream(format"WAV", io), args...)
# also create do-compatible methods of the form:
# function loadstreaming_wav(dofunc::Function, io::IO, args...)
#     str = LibSndFile.load(dofunc, Stream(format"WAV", io), args...)
#     try
#         dofunc(str)
#     finally
#         close(str)
#     end
# end

for f in (:load, :save, :loadstreaming, :savestreaming)
    for io in ((String, File), (IO, Stream))
        for fmt in formats
            @eval $(Symbol(f, fmt[1]))(io::$(io[1]), args...) =
            LibSndFile.$f($(io[2]){$(fmt[2])}( io), args...)
            if f in (:loadstreaming, :savestreaming)
                @eval function $(Symbol(f, fmt[1]))(dofunc::Function, io::$(io[1]), args...)
                  str = LibSndFile.$f($(io[2]){$(fmt[2])}( io), args...)
                    try
                        dofunc(str)
                    finally
                        close(str)
                    end
                end
            end
        end
    end
end


module TestExamplesThreaded

using Test
using Trixi

include("test_trixi.jl")

# Start with a clean environment: remove Trixi output directory if it exists
outdir = "out"
Trixi.mpi_isroot() && isdir(outdir) && rm(outdir, recursive=true)

@testset "Threaded tests" begin
  @testset "TreeMesh" begin
    @trixi_testset "elixir_advection_restart.jl" begin
      @test_trixi_include(joinpath(examples_dir(), "tree_2d_dgsem", "elixir_advection_restart.jl"),
        l2   = [1.2148032444677485e-5],
        linf = [6.495644794757283e-5])
    end

    @trixi_testset "elixir_advection_amr_refine_twice.jl" begin
      @test_trixi_include(joinpath(examples_dir(), "tree_2d_dgsem", "elixir_advection_amr_refine_twice.jl"),
        l2   = [0.00019847333806230843],
        linf = [0.005591345460895569])
    end

    @trixi_testset "elixir_advection_amr_coarsen_twice.jl" begin
      @test_trixi_include(joinpath(examples_dir(), "tree_2d_dgsem", "elixir_advection_amr_coarsen_twice.jl"),
        l2   = [0.00519897841357112],
        linf = [0.06272325552264647])
    end

    @trixi_testset "elixir_euler_source_terms_nonperiodic.jl" begin
      @test_trixi_include(joinpath(examples_dir(), "tree_2d_dgsem", "elixir_euler_source_terms_nonperiodic.jl"),
        l2   = [2.3652137675654753e-6, 2.1386731303685556e-6, 2.138673130413185e-6, 6.009920290578574e-6],
        linf = [1.4080448659026246e-5, 1.7581818010814487e-5, 1.758181801525538e-5, 5.9568540361709665e-5],
        rtol = 0.001)
    end

    @trixi_testset "elixir_euler_ec.jl" begin
      @test_trixi_include(joinpath(examples_dir(), "tree_2d_dgsem", "elixir_euler_ec.jl"),
        l2   = [0.061751715597716854, 0.05018223615408711, 0.05018989446443463, 0.225871559730513],
        linf = [0.29347582879608825, 0.31081249232844693, 0.3107380389947736, 1.0540358049885143])
    end
  end

  @testset "StructuredMesh" begin
    @trixi_testset "elixir_advection_restart.jl with waving flag mesh" begin
      @test_trixi_include(joinpath(examples_dir(), "structured_2d_dgsem", "elixir_advection_restart.jl"),
        l2   = [0.00017274040834067234],
        linf = [0.0015435741643734513],
        elixir_file="elixir_advection_waving_flag.jl",
        restart_file="restart_000041.h5")
    end

    @trixi_testset "elixir_mhd_ec.jl" begin
      @test_trixi_include(joinpath(examples_dir(), "structured_2d_dgsem", "elixir_mhd_ec.jl"),
        l2   = [0.04937480811868297, 0.06117033019988596, 0.060998028674664716, 0.03155145889799417,
                0.2319175391388658, 0.02476283192966346, 0.024483244374818587, 0.035439957899127385,
                0.0016022148194667542],
        linf = [0.24749024430983746, 0.2990608279625713, 0.3966937932860247, 0.22265033744519683,
                0.9757376320946505, 0.12123736788315098, 0.12837436699267113, 0.17793825293524734,
                0.03460761690059514],
        tspan = (0.0, 0.3))
    end
  end

  @testset "UnstructuredMesh" begin
    @trixi_testset "elixir_ape_gauss_wall.jl" begin
      @test_trixi_include(joinpath(examples_dir(), "unstructured_2d_dgsem", "elixir_ape_gauss_wall.jl"),
        l2   = [0.029331247985489625, 0.02934616721732521, 0.03803253571320854, 0.0,
                7.465932985352019e-16, 1.4931865970704038e-15, 1.4931865970704038e-15],
        linf = [0.3626825396196784, 0.3684490307932018, 0.8477478712580901, 0.0,
                8.881784197001252e-16, 1.7763568394002505e-15, 1.7763568394002505e-15],
        tspan = (0.0, 5.0))
    end
  end

  @testset "P4estMesh" begin
    @trixi_testset "elixir_euler_source_terms_nonperiodic_unstructured.jl" begin
      @test_trixi_include(joinpath(examples_dir(), "p4est_2d_dgsem", "elixir_euler_source_terms_nonperiodic_unstructured.jl"),
        l2   = [0.005689496253354319, 0.004522481295923261, 0.004522481295922983, 0.009971628336802528],
        linf = [0.05125433503504517, 0.05343803272241532, 0.053438032722404216, 0.09032097668196482])
    end

    @trixi_testset "elixir_eulergravity_convergence.jl" begin
      @test_trixi_include(joinpath(examples_dir(), "p4est_2d_dgsem", "elixir_eulergravity_convergence.jl"),
        l2   = [0.00024871265138964204, 0.0003370077102132591, 0.0003370077102131964, 0.0007231525513793697],
        linf = [0.0015813032944647087, 0.0020494288423820173, 0.0020494288423824614, 0.004793821195083758],
        tspan = (0.0, 0.1))
    end
  end

  @testset "DGMulti" begin
    @trixi_testset "elixir_euler_weakform.jl (SBP, EC)" begin
      @test_trixi_include(joinpath(examples_dir(), "dgmulti_2d", "elixir_euler_weakform.jl"),
        cells_per_dimension = (4, 4),
        volume_integral = VolumeIntegralFluxDifferencing(flux_ranocha),
        surface_integral = SurfaceIntegralWeakForm(flux_ranocha),
        approximation_type = SBP(),
        l2 = [0.01285864081726596, 0.010650165503847099, 0.01065016550381281, 0.026286162111579015],
        linf = [0.037333313274372504, 0.05308320130762212, 0.05308320130841948, 0.13378665881805185],
      )
    end

    @trixi_testset "elixir_euler_triangulate_pkg_mesh.jl" begin
      @test_trixi_include(joinpath(examples_dir(), "dgmulti_2d", "elixir_euler_triangulate_pkg_mesh.jl"),
        l2 = [4.664661209491976e-6, 3.7033509525940745e-6, 4.794877426562555e-6, 1.2682723101532175e-5],
        linf = [2.5099852761334418e-5, 2.2683684021362893e-5, 2.6180448559287584e-5, 5.5752932611508044e-5]
      )
    end
  end
end

# Clean up afterwards: delete Trixi output directory
Trixi.mpi_isroot() && isdir(outdir) && @test_nowarn rm(outdir, recursive=true)

end # module


#= Custom calibration configuration file. =#

using Distributions
using StatsBase
using LinearAlgebra
using CalibrateEDMF
using CalibrateEDMF.ModelTypes
using CalibrateEDMF.LESUtils
using CalibrateEDMF.TurbulenceConvectionUtils
# Import prior constraints
using EnsembleKalmanProcesses.ParameterDistributions
using JLD2

# Cases defined as structs for quick access to default configs
struct ObsCampaigns end
struct ObsCampaignsVal end
# struct MyAwesomeSetup end

function get_config()
    config = Dict()
    # Flags for saving output data
    config["output"] = get_output_config()
    # Define regularization of inverse problem
    config["regularization"] = get_regularization_config()
    # Define reference used in the inverse problem
    config["reference"] = get_reference_config(ObsCampaigns())
    # Define reference used for validation
    config["validation"] = get_reference_config(ObsCampaignsVal())
    # Define the parameter priors
    config["prior"] = get_prior_config()
    # Define the kalman process
    config["process"] = get_process_config()
    # Define the SCM static configuration
    config["scm"] = get_scm_config()
    return config
end

function get_output_config()
    config = Dict()
    config["outdir_root"] = pwd()
    config["overwrite_scm_file"] = false # Flag for overwritting SCM input file
    return config
end

function get_regularization_config()
    config = Dict()
    # Regularization of observations: mean and covariance
    config["perform_PCA"] = true # Performs PCA on data
    config["variance_loss"] = 1.0e-2 # Variance truncation level in PCA
    config["normalize"] = true  # whether to normalize data by pooled variance
    config["tikhonov_mode"] = "relative" # Tikhonov regularization
    config["tikhonov_noise"] = 1.0e-6 # Tikhonov regularization
    config["dim_scaling"] = true # Dimensional scaling of the loss

    # Parameter regularization: L2 regularization with respect to prior mean.
    #  - Set to `nothing` to use prior covariance as regularizer,
    #  - Set to a float for isotropic parameter regularization.
    #  - Pass a dictionary of lists similar to config["prior_mean"] for
    #       anisotropic regularization. The dictionary must be complete.
    #       If you want to avoid regularizing a certain parameter, set the entry
    #       to [0].
    # To turn off regularization, set config["process"]["augmented"] to false.
    config["l2_reg"] = 1.0
    return config
end

function get_process_config()
    config = Dict()
    config["N_iter"] = 6
    config["N_ens"] = 7
    config["algorithm"] = "Unscented" # "Sampler", "Unscented", "Inversion"
    config["noisy_obs"] = false # Choice of covariance in evaluation of y_{j+1} in EKI. True -> Γy, False -> 0
    # Artificial time stepper of the EKI.
    config["Δt"] = 1.0
    # Whether to augment the outputs with the parameters for regularization
    config["augmented"] = true
    config["failure_handler"] = "sample_succ_gauss"
    return config
end

function get_reference_config(::ObsCampaigns)
    config = Dict()
    config["case_name"] = ["GABLS", "DYCOMS_RF01"]
    # Flag to indicate source of data (LES or SCM) for reference data and covariance
    config["y_reference_type"] = LES()
    config["Σ_reference_type"] = LES()
    config["y_names"] = [["thetal_mean", "u_mean", "v_mean", "tke_mean"], ["thetal_mean", "ql_mean", "qt_mean"]]
    config["y_dir"] = ["/groups/esm/ilopezgo/Output.GABLS.iles128wCov", "/groups/esm/ilopezgo/Output.DYCOMS_RF01.may20"]
    # provide list of dirs if different from `y_dir`
    # config["Σ_dir"] = [...]
    config["scm_suffix"] = repeat(["000123"], length(config["case_name"]))
    config["scm_parent_dir"] = repeat(["scm_init"], length(config["case_name"]))
    config["t_start"] = [7, 2] * 3600.0
    config["t_end"] = [9, 4] * 3600.0
    # Specify averaging intervals for covariance, if different from mean vector (`t_start` & `t_end`)
    # config["Σ_t_start"] = [...]
    # config["Σ_t_end"] = [...]
    config["batch_size"] = 1
    return config
end

function get_reference_config(::ObsCampaignsVal)
    config = Dict()
    config["case_name"] = ["DYCOMS_RF01"]
    # Flag to indicate source of data (LES or SCM) for reference data and covariance
    config["y_reference_type"] = LES()
    config["Σ_reference_type"] = LES()
    config["y_names"] = [["total_flux_h", "total_flux_qt"]]
    config["y_dir"] = ["/groups/esm/ilopezgo/Output.DYCOMS_RF01.may20"]
    # provide list of dirs if different from `y_dir`
    # config["Σ_dir"] = [...]
    config["scm_suffix"] = repeat(["000123"], length(config["case_name"]))
    config["scm_parent_dir"] = repeat(["scm_init"], length(config["case_name"]))
    config["t_start"] = [2] * 3600.0
    config["t_end"] = [4] * 3600.0
    # Specify averaging intervals for covariance, if different from mean vector (`t_start` & `t_end`)
    # config["Σ_t_start"] = [...]
    # config["Σ_t_end"] = [...]
    return config
end

function get_prior_config()
    config = Dict()
    config["constraints"] = Dict(
        # diffusion parameters
        "tke_ed_coeff" => [bounded(0.01, 1.0)],
        "tke_diss_coeff" => [bounded(0.01, 1.0)],
        "static_stab_coeff" => [bounded(0.1, 1.0)],
    )
    # TC.jl prior mean
    config["prior_mean"] = Dict(
        # diffusion parameters
        "tke_ed_coeff" => [0.05],
        "tke_diss_coeff" => [0.5],
        "static_stab_coeff" => [0.9],
    )

    config["unconstrained_σ"] = 0.5
    return config
end

function get_scm_config()
    config = Dict()
    config["namelist_args"] =
        [("time_stepping", "dt_min", 1.0), ("time_stepping", "dt_max", 10.0), ("stats_io", "frequency", 60.0)]
    return config
end


# Julia wrapper for header: include/blockmat.h
# Automatically generated using Clang.jl wrap_c, version 0.0.0

# TODO detect size and use bitstype because if the DLL changes and gets
#      compiled with NOSHORTS we are screwed with the following code...
@static if Sys.iswindows()
    const csdpshort = Cushort
  else
    const csdpshort = CSDP_INT
end

# begin enum blockcat
const blockcat = Cuint
const DIAG = (blockcat)(0)
const MATRIX = (blockcat)(1)
const PACKEDMATRIX = (blockcat)(2)
# end enum blockcat

struct blockdatarec
    _blockdatarec::Ptr{Cdouble}
end

struct blockrec <: AbstractMatrix{Cdouble}
    data::blockdatarec
    blockcategory::blockcat
    blocksize::csdpshort
end

mutable struct blockmatrix <: AbstractBlockMatrix{Cdouble}
    nblocks::CSDP_INT
    blocks::Ptr{blockrec}
end
blockmatrix() = blockmatrix(CSDP_INT(0), C_NULL)

mutable struct sparseblock
    next::Ptr{sparseblock}
    nextbyblock::Ptr{sparseblock}
    entries::Ptr{Cdouble}
    iindices::Ptr{csdpshort}
    jindices::Ptr{csdpshort}
    numentries::CSDP_INT
    blocknum::csdpshort
    blocksize::csdpshort
    constraintnum::csdpshort
    issparse::csdpshort
end
function sparseblock(next, jblock, blocknum, constr)
    # See easysdp.c
    blocksize = jblock.n
    numentries = length(jblock.i)
    issparse = numentries <= blocksize / 4 || numentries <= 15 # FIXME also if category (which is in C...) is DIAG
end

# If I add mutable here I get : ReadOnlyMemoryError() in initsoln (I know it is counter-intuitive)
struct constraintmatrix
    blocks::Ptr{sparseblock}
end

# Skipping MacroDefinition: ijtok ( iiii , jjjj , lda ) ( ( jjjj - 1 ) * lda + iiii - 1 )
# Skipping MacroDefinition: ijtokp ( iii , jjj , lda ) ( ( iii + jjj * ( jjj - 1 ) / 2 ) - 1 )
# Skipping MacroDefinition: ktoi ( k , lda ) ( ( k % lda ) + 1 )
# Skipping MacroDefinition: ktoj ( k , lda ) ( ( k / lda ) + 1 )

mutable struct paramstruc
    axtol::Cdouble
    atytol::Cdouble
    objtol::Cdouble
    pinftol::Cdouble
    dinftol::Cdouble
    maxiter::CSDP_INT
    minstepfrac::Cdouble
    maxstepfrac::Cdouble
    minstepp::Cdouble
    minstepd::Cdouble
    usexzgap::CSDP_INT
    tweakgap::CSDP_INT
    affine::CSDP_INT
    perturbobj::Cdouble
    fastmode::CSDP_INT
end

function paramstruc(options::Dict)
    paramstruc(get(options, :axtol      , 1.0e-8),
               get(options, :atytol     , 1.0e-8),
               get(options, :objtol     , 1.0e-8),
               get(options, :pinftol    , 1.0e8),
               get(options, :dinftol    , 1.0e8),
               get(options, :maxiter    , 100),
               get(options, :minstepfrac, 0.90),
               get(options, :maxstepfrac, 0.97),
               get(options, :minstepp   , 1.0e-8),
               get(options, :minstepd   , 1.0e-8),
               get(options, :usexzgap   , 1),
               get(options, :tweakgap   , 0),
               get(options, :affine     , 0),
               get(options, :perturbobj , 1),
               get(options, :fastmode   , 0))
end


export check_camera_alignment, master_parse

function read_digital(io::IOStream)
    seekend(io)
    myend=position(io)
    println(myend)
    seek(io,1024)

    #Each event is 16 bytes

    myevents= [zeros(Int64,0) for i=1:8]

    while position(io)<(myend-16)
        t = read(io,Int64) #Timestamp
        read(io,Int16) #Sample position within buffer
        e_type = read(io,UInt8) #uint8 event type (all the events that are saved have type TTL = 3 ; Network Event = 5)
        read(io,UInt8) #uint8 processor ID (the processor this event originated from)
        e_id = read(io,UInt8) #uint8 event ID (code associated with this event, usually 1 or 0)
        channel = read(io,UInt8) #uint8 event channel (the channel this event is associated with)
        read(io,UInt16) #One uint16 recording number (version 0.2 and higher)
        if e_type == 3
            push!(myevents[channel+1],t)
        end
    end
    myevents
end

#Change open ephys files into a different MAT file for each unit with
#1) Analog trace, video ttls, and laser ttls
function master_parse(folder_path,video_chan,laser_chan)

    event_path = string(folder_path, "all_channels.events")
    io_event = open(event_path,"r");

    spike_path = get_spike_path(folder_path,1)
    io_spike = open(spike_path,"r")
    spikes = SampleArray(Float32,io_spike);
    spikes = Array(spikes)

    close(io_spike)

    io_spike = open(spike_path,"r")
    times=TimeArray(Int64,io_spike);

    digital=read_digital(io_event)

    video_ts=digital[video_chan][1:2:end] - times[1]
    laser_ts=digital[laser_chan][1:2:end] - times[1]

    close(io_spike)
    close(io_event)

    file=matopen(string(folder_path,"spikes.mat"),"w")
    write(file,"spikes",spikes)
    write(file,"video_ts",video_ts)
    write(file,"laser_ts",laser_ts)
    close(file)
    nothing
end

function parse_ttl(io,ind,t1,t2)
    digital=read_digital(io)

    #Array of bools that is true during a TTL event, and false when no TTL is present
    digital_events=falses(t2);

    #Array where each index corresponds to the total number of TTL digital_events
    #Up to that point.
    digital_time=zeros(Int64,t2)

    states=falses
    states_t=digital[ind][1]
    states_i=1
    states_totals=0

    states_array=zeros(Int64,0)

    current_time = t1
    for i=2:t2

        #If this is when a digital event occurs
        if states_t == current_time

            #Change the state
            digital_events[i] = !digital_events[i-1]

            #If we went high
            if digital_events[i]
                states_totals+=1
                push!(states_array,current_time)
            end

            if length(digital[ind])<(states_i+1)

            else
                states_i=states_i+1

                #new time to look for is
                states_t=digital[ind][states_i]
            end

        else
            digital_events[i] = digital_events[i-1]
        end

        digital_time[i]=states_totals
        current_time += 1
    end
    (states_array,digital_events,digital_time)
end

function check_camera_alignment(gui)

    xx=read(`mediainfo --Output="Video;%FrameCount%" $(gui.vid_path)`)

    video_frames=parse(Int64,convert(String,xx[1:(end-1)]))
    if gui.video_ts[end]+1 == video_frames
        println("Exposure event totals match frames in video file")
    else
        println("ERROR: frame counts do not match")
        println(string("File frame count: ",gui.video_ts[end]+1))
        println(string("TTL frame count: ", video_frames))
    end
    nothing
end

function get_backup_timestamps(backup_path)

	xx=open(backup_path,"r")

	ephys_times=zeros(Int64,0)
	camera_frames=zeros(Int32,0)
	seek(xx,0)
	while (!eof(xx))
    		push!(ephys_times,read(xx, Int64))
    		push!(camera_frames,read(xx, Int32))
	end
	close(xx)
	(ephys_times,camera_frames)
end


function parentind(ind::Symbol, op::Operation)
    for (id,opp) ∈ enumerate(parents(op))
        name(opp) === ind && return id
    end
    -1
end
function symbolind(ind::Symbol, op::Operation, td::UnrollArgs)
    id = parentind(ind, op)
    id == -1 && return ind
    @unpack u₁, u₁loopsym, u₂loopsym, suffix = td
    parent = parents(op)[id]
    pvar = if u₂loopsym ∈ loopdependencies(parent)
        variable_name(parent, suffix)
    else
        mangledvar(parent)
    end
    ex = u₁loopsym ∈ loopdependencies(parent) ? Symbol(pvar, u₁) : pvar
    Expr(:call, lv(:staticm1), ex)
end

staticexpr(x::Integer) = Expr(:call, Expr(:curly, lv(:Static), convert(Int, x)))
staticexpr(x) = Expr(:call, lv(:Static), x)
maybestatic(x::Integer) = staticexpr(x)
maybestatic(x) = x
_MMind(ind) = Expr(:call, lv(:MM), VECTORWIDTHSYMBOL, ind)
_MMind(ind::Integer) = Expr(:call, lv(:MM), VECTORWIDTHSYMBOL, staticexpr(ind))
function addoffset!(ret::Expr, ex, offset::Integer, _mm::Bool = false)
    if iszero(offset)
        if _mm
            ind = _MMind(ex)
        else
            push!(ret.args, ex)
            return
        end
    elseif _mm
        ind = _MMind(Expr(:call, lv(:vadd_fast), ex, staticexpr(offset)))
    else
        ind = Expr(:call, lv(:vadd_fast), ex, staticexpr(offset))
    end
    push!(ret.args, ind)
    nothing
end
function addoffset!(ret::Expr, offset::Int, _mm::Bool = false)
    push!(ret.args, _mm ? _MMind(offset) : staticexpr(offset))
    nothing
end

"""
unrolled loads are calculated as offsets with respect to an initial gesp. This has proven important to helping LLVM generate efficient code in some cases.
Therefore, unrolled === true results in inds being ignored.
_mm means to insert `mm`s.
"""
function mem_offset(op::Operation, td::UnrollArgs, inds_calc_by_ptr_offset::Vector{Bool}, _mm::Bool = true)
    # @assert accesses_memory(op) "Computing memory offset only makes sense for operations that access memory."
    ret = Expr(:tuple)
    indices = getindicesonly(op)
    offsets = getoffsets(op)
    loopedindex = op.ref.loopedindex
    # inds_calc_by_ptr_offset = indices_calculated_by_pointer_offsets(ls, op.ref)
    @unpack vectorized = td
    for (n,ind) ∈ enumerate(indices)
        offset = offsets[n] % Int
        # if ind isa Int # impossible
            # push!(ret.args, ind + offset)
        # else
        if loopedindex[n]
            if inds_calc_by_ptr_offset[n]
                addoffset!(ret, offset, _mm & (ind === vectorized))
            else
                addoffset!(ret, ind, offset, _mm & (ind === vectorized))
            end
        else
            addoffset!(ret, symbolind(ind, op, td), offset)
        end
    end
    ret
end

function add_vectorized_offset!(ret::Expr, ind, offset, incr)
    if isone(incr)
        if iszero(offset)
            push!(ret.args, _MMind(Expr(:call, lv(:vadd_fast), VECTORWIDTHSYMBOL, maybestatic(ind))))
        else
            push!(ret.args, _MMind(Expr(:call, lv(:vadd_fast), ind, Expr(:call, lv(:vadd_fast), VECTORWIDTHSYMBOL, staticexpr(offset)))))
        end
    elseif iszero(incr)
        if iszero(offset)
            push!(ret.args, _MMind(ind))
        else
            addoffset!(ret, ind, offset, true)
        end
    elseif iszero(offset)
        push!(ret.args, _MMind(Expr(:call, lv(:vadd_fast), Expr(:call, lv(:vmul_fast), VECTORWIDTHSYMBOL, maybestatic(incr)), maybestatic(ind))))
    else
        push!(ret.args, _MMind(Expr(:call, lv(:vadd_fast), ind, Expr(:call, lv(:vadd_fast), Expr(:call, lv(:vmul_fast), VECTORWIDTHSYMBOL, maybestatic(incr)), staticexpr(offset)))))
    end
end
function add_vectorized_offset_unrolled!(ret::Expr, offset, incr)
    if isone(incr)
        if iszero(offset)
            push!(ret.args, _MMind(Expr(:call, lv(:Static), VECTORWIDTHSYMBOL)))
        else
            push!(ret.args, _MMind(Expr(:call, lv(:vadd_fast), VECTORWIDTHSYMBOL, staticexpr(offset))))
        end
    elseif iszero(incr)
        if iszero(offset)
            push!(ret.args, _MMind(Expr(:call, lv(:Zero))))
        else
            push!(ret.args, _MMind(staticexpr(offset)))
        end
    elseif iszero(offset)
        push!(ret.args, _MMind(Expr(:call, lv(:*), VECTORWIDTHSYMBOL, maybestatic(incr))))
    else
        push!(ret.args, _MMind(Expr(:call, lv(:vadd_fast), Expr(:call, lv(:vmul_fast), VECTORWIDTHSYMBOL, maybestatic(incr)), staticexpr(offset))))
    end
end
function add_vectorized_offset!(ret::Expr, ind, offset, incr, unrolled)
    if unrolled
        add_vectorized_offset_unrolled!(ret, offset, incr)
    else
        add_vectorized_offset!(ret, ind, offset, incr)
    end
end
function mem_offset_u(op::Operation, td::UnrollArgs, inds_calc_by_ptr_offset::Vector{Bool})
    @assert accesses_memory(op) "Computing memory offset only makes sense for operations that access memory."
    @unpack u₁, u₁loopsym, u₂loopsym, vectorized, suffix = td
    incr₁ = u₁
    incr₂ = isnothing(suffix) ? 0 : suffix
    ret = Expr(:tuple)
    indices = getindicesonly(op)
    offsets = getoffsets(op)
    # allbasezero = all(inds_calc_by_ptr_offset) && all(iszero, offsets)
    loopedindex = op.ref.loopedindex
    if iszero(incr₁) & iszero(incr₂)
        return mem_offset(op, td, inds_calc_by_ptr_offset)
        # append_inds!(ret, indices, loopedindex)
    else
        for (n,ind) ∈ enumerate(indices)
            ind_by_offset = inds_calc_by_ptr_offset[n]
            offset = convert(Int, offsets[n])
            indvectorized = ind === vectorized
            # if n == zero_offset
                # addoffset!(ret, 0)
            # elseif ind === u₁loopsym
            if ind === u₁loopsym
                if indvectorized
                    add_vectorized_offset!(ret, ind, offset, incr₁, ind_by_offset)
                elseif ind_by_offset
                    addoffset!(ret, incr₁ + offset)
                else
                    addoffset!(ret, ind, incr₁ + offset)
                end
            elseif ind === u₂loopsym
                if indvectorized
                    add_vectorized_offset!(ret, ind, offset, incr₂, ind_by_offset)
                elseif ind_by_offset
                    addoffset!(ret, incr₂ + offset)
                else
                    addoffset!(ret, ind, incr₂ + offset)
                end
            elseif loopedindex[n]
                if ind_by_offset
                    addoffset!(ret, offset, indvectorized)
                else
                    addoffset!(ret, ind, offset, indvectorized)
                end
            else
                addoffset!(ret, symbolind(ind, op, td), offset)
            end
        end
    end
    ret
end

function varassignname(var::Symbol, u::Int, isunrolled::Bool)
    isunrolled ? Symbol(var, u) : var
end
# name_memoffset only gets called when vectorized
function name_memoffset(var::Symbol, op::Operation, td::UnrollArgs, u₁unrolled::Bool, inds_calc_by_ptr_offset::Vector{Bool})
    @unpack u₁, u₁loopsym, u₂loopsym, suffix = td
    if isnothing(suffix) && u₁ < 0 # sentinel value meaning not unrolled
        name = var
        mo = mem_offset(op, td, inds_calc_by_ptr_offset)
    else
        name = u₁unrolled ? Symbol(var, u₁) : var
        mo = mem_offset_u(op, td, inds_calc_by_ptr_offset)
    end
    name, mo
end

function condvarname_and_unroll(cond, u₁loop, u₂loop, suffix, opu₂)
    if isnothing(suffix) || !opu₂
        condvar, condu₁, condu₂ = variable_name_and_unrolled(cond, u₁loop, u₂loop, nothing)
    else
        condvar, condu₁, condu₂ = variable_name_and_unrolled(cond, u₁loop, u₂loop, suffix)
    end
    condvar, condu₁
end


#=
The tree structure used for all the ClaDS functions
=#

"""
A phylogeny object for the module ClaDS. It is represented as a branch with a given length and optional attributes, and its daughter trees.

- `offsprings::Array{Tree,1}`: the two daughter trees.
- `branch_length::Float64`: the length of the branch
- `attributes::Array{T,1} where {T<:Number}`: soem attributes of the branch. Used to store the speciation rates.
- `n_nodes::Int64`: the number of nodes in the tree (internal nodes + tips)
- `extant::Bool`: does the tree have any extant species?
- `label::String`: if the tree is a tip (ie n_nodes == 1), contains the name of the corresponding species.
"""
struct Tree
    offsprings::Array{Tree,1}
    branch_length::Float64
    attributes::Array{T,1} where {T<:Number}
    n_nodes::Int64
    extant::Bool
    label::String
end

#=
Constructors (12 methods with various default options)
=#

# specify all
function Tree(offsprings::Array{Tree,1}, branch_length::Float64, attributes::T, n_nodes::Int64, extant::Bool, label::String) where {T<:Number}
    Tree(offsprings, branch_length, [attributes], n_nodes, extant, label)
end

# all except tree labels, set to empty strings
function Tree(offsprings::Array{Tree,1}, branch_length::Float64, attributes::T, n_nodes::Int64, extant::Bool) where {T<:Number}
    Tree(offsprings, branch_length, attributes, n_nodes, extant, "")
end

function Tree(offsprings::Array{Tree,1}, branch_length::Float64, attributes::Array{T,1}, n_nodes::Int64, extant::Bool) where {T<:Number}
    Tree(offsprings, branch_length, attributes, n_nodes, extant, "")
end

# without extant information (set to true)
function Tree(offsprings::Array{Tree,1}, branch_length::Float64, attributes::Array{T,1}, n_nodes::Int64, label::String) where {T<:Number}
    Tree(offsprings, branch_length, attributes, n_nodes, true, label)
end

function Tree(offsprings::Array{Tree,1}, branch_length::Float64, attributes::T, n_nodes::Int64, label::String) where {T<:Number}
    Tree(offsprings, branch_length, [attributes], n_nodes, true, label)
end

# without tip labels (set to empty strings) nor extant information (set to true)
function Tree(offsprings::Array{Tree,1}, branch_length::Float64, attributes::Array{T,1}, n_nodes::Int64) where {T<:Number}
    Tree(offsprings, branch_length, attributes, n_nodes, true)
end

function Tree(offsprings::Array{Tree,1}, branch_length::Float64, attributes::T, n_nodes::Int64) where {T<:Number}
    Tree(offsprings, branch_length, [attributes], n_nodes, true)
end

# whithout specification of the node number (set to the sum of the node number of the offspring +1)
function Tree(offsprings::Array{Tree,1}, branch_length::Float64, attributes)
    n_nodes = length(branch_length)
    extant = false
    for i in offsprings
        n_nodes += i.n_nodes
        extant = extant || i.extant
    end
    extant = extant || n_nodes == 1
    Tree(offsprings, branch_length, attributes, n_nodes, extant)
end

function Tree(offsprings::Array{Tree,1}, branch_length::Float64, attributes, label::String)
    n_nodes = length(branch_length)
    extant = false
    for i in offsprings
        n_nodes += i.n_nodes
        extant = extant || i.extant
    end
    extant = extant || n_nodes == 1
    Tree(offsprings, branch_length, attributes, n_nodes, extant, label)
end

function Tree(offsprings::Array{Tree,1}, branch_length::Float64, attributes, extant::Bool)
    n_nodes = length(branch_length)
    for i in offsprings
        n_nodes += i.n_nodes
    end
    Tree(offsprings, branch_length, attributes, n_nodes, extant)
end

function Tree(offsprings::Array{Tree,1}, branch_length::Float64, attributes, extant::Bool, label::String)
    n_nodes = length(branch_length)
    for i in offsprings
        n_nodes += i.n_nodes
    end
    Tree(offsprings, branch_length, attributes, n_nodes, extant, label)
end

# empty tree
function Tree()
    Tree(Array{Tree,1}(undef,0), 0., Array{Float64,1}(undef,0), 0, false)
end


mutable struct FieldHandler
    fields::Vector{Field}
    cellset::Set{Int}
    bc_values::Vector{BCValues} # for boundary conditions
end
"""
    FieldHandler(fields::Vector{Field}, cellset)

Construct a `FieldHandler` based on an array of `Field`s and assigns it a set of cells.
"""
function FieldHandler(fields::Vector{Field}, cellset)
    # TODO for now, only accept isoparamtric mapping
    bc_values = [BCValues(field.interpolation, field.interpolation) for field in fields]
    FieldHandler(fields, cellset, bc_values)
end

struct CellVector{T}
    values::Vector{T}
    offset::Vector{Int}
    length::Vector{Int}
end

function Base.getindex(elvec::CellVector, el::Int)
    offset = elvec.offset[el]
    return elvec.values[offset:offset + elvec.length[el]-1]
 end

struct MixedDofHandler{dim,C,T} <: JuAFEM.AbstractDofHandler
    fieldhandlers::Vector{FieldHandler}
    cell_dofs::CellVector{Int}
    cell_nodes::CellVector{Int}
    cell_coords::CellVector{Vec{dim,T}}
    closed::ScalarWrapper{Bool}
    grid::Grid{dim,C,T}
    ndofs::ScalarWrapper{Int}
end

function MixedDofHandler(grid::Grid{dim,C,T}) where {dim,C,T}
    MixedDofHandler{dim,C,T}(FieldHandler[], CellVector(Int[],Int[],Int[]), CellVector(Int[],Int[],Int[]), CellVector(Vec{dim,T}[],Int[],Int[]), JuAFEM.ScalarWrapper(false), grid, JuAFEM.ScalarWrapper(-1))
end

getfieldnames(fh::FieldHandler) = [field.name for field in fh.fields]
getfielddims(fh::FieldHandler) = [field.dim for field in fh.fields]
getfieldinterpolations(fh::FieldHandler) = [field.interpolation for field in fh.fields]
ndofs_per_cell(dh::MixedDofHandler, cell::Int=1) = dh.cell_dofs.length[cell]
nnodes_per_cell(dh::MixedDofHandler, cell::Int=1) = dh.cell_nodes.length[cell]

function celldofs!(global_dofs::Vector{Int}, dh::MixedDofHandler, i::Int)
    @assert isclosed(dh)
    @assert length(global_dofs) == ndofs_per_cell(dh, i)
    unsafe_copyto!(global_dofs, 1, dh.cell_dofs.values, dh.cell_dofs.offset[i], length(global_dofs))
    return global_dofs
end

function celldofs(dh::MixedDofHandler, i::Int)
    @assert isclosed(dh)
    return dh.cell_dofs[i]
end

function cellcoords!(global_coords::Vector{Vec{dim,T}}, dh::MixedDofHandler, i::Int) where {dim,T}
    @assert isclosed(dh)
    @assert length(global_coords) == nnodes_per_cell(dh, i)
    unsafe_copyto!(global_coords, 1, dh.cell_coords.values, dh.cell_coords.offset[i], length(global_coords))
    return global_coords
end

function cellnodes!(global_nodes::Vector{Int}, dh::MixedDofHandler, i::Int) where {dim,T}
    @assert isclosed(dh)
    @assert length(global_nodes) == nnodes_per_cell(dh, i)
    unsafe_copyto!(global_nodes, 1, dh.cell_nodes.values, dh.cell_nodes.offset[i], length(global_nodes))
    return global_nodes
end

"""
    getfieldnames(dh::MixedDofHandler)

Returns the union of all the fields. Can be used as an iterable over all the fields in the problem.
"""
function getfieldnames(dh::MixedDofHandler)
    fieldnames = Vector{Symbol}()
    for fh in dh.fieldhandlers
        for name in getfieldnames(fh)
            push!(fieldnames, name)
        end
    end
    return unique!(fieldnames)
end

"""
    getfielddim(dh::MixedDofHandler, name::Symbol)

Returns the dimension of a specific field, given by name. Note that it will return the dimension of the first field found among the `FieldHandler`s.
"""
function getfielddim(dh::MixedDofHandler, name::Symbol)

    for fh in dh.fieldhandlers
        field_pos = findfirst(i->i == name, getfieldnames(fh))
        if field_pos > 0
            return fh.fields[field_pos].dim
        end
    end
    error("did not find field $name")
end

"""
    nfields(dh::MixedDofHandler)

Returns the number of unique fields defined.
"""
nfields(dh::MixedDofHandler) = length(getfieldnames(dh))

function Base.push!(dh::MixedDofHandler, fh::FieldHandler)
    @assert !isclosed(dh)
    push!(dh.fieldhandlers, fh)
    return dh
end

function Base.push!(dh::MixedDofHandler, name::Symbol, dim::Int)
    celltype = getcelltype(dh.grid)
    isconcretetype(celltype) || error("If you have more than one celltype in Grid, you must use push!(dh::MixedDofHandler, fh::FieldHandler)")
    push!(dh, name, dim, default_interpolation(celltype))
end

function Base.push!(dh::MixedDofHandler, name::Symbol, dim::Int, ip::Interpolation)
    @assert !isclosed(dh)

    if length(dh.fieldhandlers) == 0
        cellset = Set(1:getncells(dh.grid))
        push!(dh.fieldhandlers, FieldHandler(Field[],cellset,BCValues[]))
    elseif length(dh.fieldhandlers) > 1
        error("If you have more than one FieldHandler, you must specify field")
    end
    fh = first(dh.fieldhandlers)

    field = Field(name,ip,dim)
    bc_value = BCValues(field.interpolation, field.interpolation)

    push!(fh.fields, field)
    push!(fh.bc_values, bc_value)

    return dh
end

"""
    close!(dh::MixedDofHandler)

Closes the dofhandler and creates degrees of freedom for each cell.
Dofs are created in the following order: Go through each FieldHandler in the order they were added. For each field in the FieldHandler, create dofs for the cell. This means that dofs on a particular cell will be numbered according to the fields; first dofs for field 1, then field 2, etc.
"""
function close!(dh::MixedDofHandler{dim}) where {dim}

    @assert !JuAFEM.isclosed(dh)
    field_names = JuAFEM.getfieldnames(dh)  # all the fields in the problem
    numfields =  length(field_names)

    # Create dicts that stores created dofs
    # Each key should uniquely identify the given type
    vertexdicts = [Dict{Int, Array{Int}}() for _ in 1:numfields]
    edgedicts = [Dict{Tuple{Int,Int}, Array{Int}}() for _ in 1:numfields]
    facedicts = [Dict{Tuple{Int,Int}, Array{Int}}() for _ in 1:numfields]
    celldicts = [Dict{Int, Array{Int}}() for _ in 1:numfields]

    # Set initial values
    nextdof = 1  # next free dof to distribute

    append!(dh.cell_dofs.offset, zeros(getncells(dh.grid)))
    append!(dh.cell_dofs.length, zeros(getncells(dh.grid)))

    @debug "\n\nCreating dofs\n"
    for fh in dh.fieldhandlers
        # sort the cellset since we want to loop through the cells in a fixed order
        cellnumbers = sort(collect(fh.cellset))
        nextdof = _close!(
            dh,
            cellnumbers,
            JuAFEM.getfieldnames(fh),
            JuAFEM.getfielddims(fh),
            JuAFEM.getfieldinterpolations(fh),
            nextdof,
            vertexdicts,
            edgedicts,
            facedicts,
            celldicts)
    end
    dh.ndofs[] = maximum(dh.cell_dofs.values)
    dh.closed[] = true

    #Create cell_nodes and cell_coords (similar to cell_dofs)
    push!(dh.cell_nodes.offset, 1)
    push!(dh.cell_coords.offset, 1)
    for cell in dh.grid.cells
        for nodeid in cell.nodes
            push!(dh.cell_nodes.values, nodeid)
            push!(dh.cell_coords.values, dh.grid.nodes[nodeid].x)
        end
        push!(dh.cell_nodes.offset, length(dh.cell_nodes.values)+1)
        push!(dh.cell_coords.offset, length(dh.cell_coords.values)+1)
        push!(dh.cell_nodes.length, length(cell.nodes))
        push!(dh.cell_coords.length, length(cell.nodes))
    end

    return dh
end

function _close!(dh::MixedDofHandler{dim}, cellnumbers, field_names, field_dims, field_interpolations, nextdof, vertexdicts, edgedicts, facedicts, celldicts) where {dim}

    ip_infos = JuAFEM.InterpolationInfo[]
    for interpolation in field_interpolations
        ip_info = JuAFEM.InterpolationInfo(interpolation)
        # these are not implemented yet (or have not been tested)
        @assert(ip_info.nvertexdofs <= 1)
        @assert(ip_info.nedgedofs <= 1)
        @assert(ip_info.nfacedofs <= 1)
        @assert(ip_info.ncelldofs <= 1)  # not tested but probably works
        push!(ip_infos, ip_info)
    end

    # loop over all the cells, and distribute dofs for all the fields
    for ci in cellnumbers
        dh.cell_dofs.offset[ci] = length(dh.cell_dofs.values)+1

        cell = dh.grid.cells[ci]
        cell_dofs = Int[]  # list of global dofs for each cell
        @debug "Creating dofs for cell #$ci"

        for fi in 1:length(field_names)
            @debug "\tfield: $(field_names[fi])"
            ip_info = ip_infos[fi]

            if ip_info.nvertexdofs > 0
                nextdof = add_vertex_dofs(cell_dofs, cell, vertexdicts[fi], field_dims[fi], ip_info.nvertexdofs, nextdof)
            end

            if ip_info.nfacedofs > 0
                nextdof = add_face_dofs(cell_dofs, cell, facedicts[fi], field_dims[fi], ip_info.nfacedofs, nextdof)
            end

            if ip_info.nedgedofs > 0
                nextdof = add_edge_dofs(cell_dofs, cell, edgedicts[fi], field_dims[fi], ip_info.nedgedofs, nextdof)
            end

            if ip_info.ncelldofs > 0
                nextdof = add_cell_dofs(cell_dofs, ci, celldicts[fi], field_dims[fi], ip_info.ncelldofs, nextdof)
            end

        end
        # after done creating dofs for the cell, push them to the global list
        push!(dh.cell_dofs.values, cell_dofs...)
        dh.cell_dofs.length[ci] = length(cell_dofs)

        @debug "Dofs for cell #$ci:\n\t$cell_dofs"
    end # cell loop
    return nextdof
end

"""
Returns the next global dof number and an array of dofs.
If dofs have already been created for the object (vertex, face) then simply return those, otherwise create new dofs.
"""
function get_or_create_dofs!(nextdof, field_dim; dict, key)

    token = Base.ht_keyindex2!(dict, key)
    if token > 0  # vertex, face etc. visited before
        # reuse stored dofs (TODO unless field is discontinuous)
        @debug "\t\tkey: $key dofs: $(dict[key])  (reused dofs)"
        return nextdof, dict[key]

    else  # create new dofs
        dofs = collect(nextdof : nextdof + field_dim-1)
        @debug "\t\tkey: $key dofs: $dofs"
        Base._setindex!(dict, dofs, key, -token) #
        nextdof += field_dim
        return nextdof, dofs
    end
end

function add_vertex_dofs(cell_dofs, cell, vertexdict, field_dim, nvertexdofs, nextdof)
    for vertex in JuAFEM.vertices(cell)
        @debug "\tvertex #$vertex"
        nextdof, dofs = get_or_create_dofs!(nextdof, field_dim, dict=vertexdict, key=vertex)
        push!(cell_dofs, dofs...)
    end
    return nextdof
end

function add_face_dofs(cell_dofs, cell, facedict, field_dim, nfacedofs, nextdof)
    @assert nfacedofs == 1 "Currently only supports interpolations with nfacedofs = 1"

    for face in JuAFEM.faces(cell)
        sface = JuAFEM.sortface(face)
        @debug "\tface #$sface"
        nextdof, dofs = get_or_create_dofs!(nextdof, field_dim, dict=facedict, key=sface)
        push!(cell_dofs, dofs...)
    end
    return nextdof
end

function add_edge_dofs(cell_dofs, cell, edgedict, field_dim, nedgedofs, nextdof)
    @assert nedgedofs == 1 "Currently only supports interpolations with nedgedofs = 1"
    for edge in JuAFEM.edges(cell)
        sedge, dir = JuAFEM.sortedge(edge)
        @debug "\tedge #$sedge"
        nextdof, dofs = get_or_create_dofs!(nextdof, field_dim, dict=edgedict, key=sedge)
        push!(cell_dofs, dofs...)
    end
    return nextdof
end

function add_cell_dofs(cell_dofs, cell, celldict, field_dim, ncelldofs, nextdof)
    for celldof in 1:ncelldofs
        @debug "\tcell #$cell"
        nextdof, dofs = get_or_create_dofs!(nextdof, field_dim, dict=celldict, key=cell)
        push!(cell_dofs, dofs...)
    end
    return nextdof
end

# TODO if not too slow it can replace the "Grid-version"
function _create_sparsity_pattern(dh::MixedDofHandler, sym::Bool)
    ncells = getncells(dh.grid)
    N::Int = 0
    for element_id = 1:ncells  # TODO check for correctness
        n = ndofs_per_cell(dh, element_id)
        N += sym ? div(n*(n+1), 2) : n^2
    end
    N += ndofs(dh) # always add the diagonal elements
    I = Int[]; resize!(I, N)
    J = Int[]; resize!(J, N)

    cnt = 0
    for element_id in 1:ncells
        n = ndofs_per_cell(dh, element_id)
        global_dofs = zeros(Int, n)
        celldofs!(global_dofs, dh, element_id)
        @inbounds for j in 1:n, i in 1:n
            dofi = global_dofs[i]
            dofj = global_dofs[j]
            sym && (dofi > dofj && continue)
            cnt += 1
            if cnt > length(J)
                resize!(I, trunc(Int, length(I) * 1.5))
                resize!(J, trunc(Int, length(J) * 1.5))
            end
            I[cnt] = dofi
            J[cnt] = dofj
        end
    end
    @inbounds for d in 1:ndofs(dh)
        cnt += 1
        if cnt > length(J)
            resize!(I, trunc(Int, length(I) + ndofs(dh)))
            resize!(J, trunc(Int, length(J) + ndofs(dh)))
        end
        I[cnt] = d
        J[cnt] = d
    end
    resize!(I, cnt)
    resize!(J, cnt)
    V = zeros(length(I))
    K = sparse(I, J, V)
    return K

end

@inline create_sparsity_pattern(dh::MixedDofHandler) = _create_sparsity_pattern(dh, false)

function find_field(fh::FieldHandler, field_name::Symbol)
    j = findfirst(i->i == field_name, getfieldnames(fh))
    j == 0 && error("did not find field $field_name")
    return j
end

# Calculate the offset to the first local dof of a field
function field_offset(fh::FieldHandler, field_name::Symbol)
    offset = 0
    for i in 1:find_field(fh, field_name)-1
        offset += getnbasefunctions(getfieldinterpolations(fh)[i])::Int * getfielddims(fh)[i]
    end
    return offset
end

find_field(dh::MixedDofHandler, field_name::Symbol) = find_field(first(dh.fieldhandlers), field_name)
field_offset(dh::MixedDofHandler, field_name::Symbol) = field_offset(first(dh.fieldhandlers), field_name)
getfieldinterpolation(dh::MixedDofHandler, field_idx::Int) = dh.fieldhandlers[1].fields[field_idx].interpolation
getfielddim(dh::MixedDofHandler, field_idx::Int) = dh.fieldhandlers[1].fields[field_idx].dim
getbcvalue(dh::MixedDofHandler, field_idx::Int) =  dh.fieldhandlers[1].bc_values[field_idx]


using RealSense

include("rs-utils.jl")

err = Ref{Ptr{rs2_error}}(C_NULL)

ctx = rs2_create_context(RS2_API_VERSION, err)
checkerror(err)

device = get_a_realsense_device(ctx)
print_device_information(device)

sensor = get_a_sensor_from_a_device(device)

profile = choose_a_streaming_profile(sensor)

is_extendable = rs2_stream_profile_is(profile, RS2_EXTENSION_VIDEO_PROFILE, err)
checkerror(err)
if is_extendable > 0
    intrinsicsRef = Ref{rs2_intrinsics}(rs2_intrinsics(0,0,0,0,0,0,0,(-1,-1,-1,-1,-1)))
    rs2_get_video_stream_intrinsics(profile, intrinsicsRef, err)
    checkerror(err)
    intrinsics = intrinsicsRef[]
    @info "Principal Point: $(intrinsics.ppx), $(intrinsics.ppy)"
    @info "Focal Length: $(intrinsics.fx), $(intrinsics.fy)"
    @info "Distortion Model: $(intrinsics.model)"
    @info "Distortion Coefficients: " intrinsics.coeffs
end

is_extendable = rs2_stream_profile_is(profile, RS2_EXTENSION_MOTION_PROFILE, err)
checkerror(err)
if is_extendable > 0
    intrinsicsRef = Ref{rs2_motion_device_intrinsic}(rs2_motion_device_intrinsic(ntuple(x->ntuple(x->0,Val{4}),Val{3}), ntuple(x->0,Val{3}), ntuple(x->0,Val{3})))
    rs2_get_motion_intrinsics(profile, intrinsicsRef, err)
    checkerror(err)
    intrinsics = intrinsicsRef[]
    println(" Scale X      cross axis      cross axis  Bias X")
    println(" cross axis    Scale Y        cross axis  Bias Y")
    println(" cross axis    cross axis     Scale Z     Bias Z")
    for i in intrinsics.data
        for j in i
            print("  $j    ")
        end
        print("\n")
    end
    println("Variance of noise for X, Y, Z axis:")
    @info intrinsics.noise_variances
    println("Variance of bias for X, Y, Z axis:")
    @info intrinsics.bias_variances
end

rs2_delete_sensor(sensor)
rs2_delete_device(device)
rs2_delete_context(ctx)