module FEIterators

using StaticArrays
using MeshCore
using MeshCore: nshapes, indextype, nrelations, nentities, IncRel, shapedesc
using MeshSteward: Mesh, baseincrel, increl
using ..RefShapes: manifdim, manifdimv
using ..FElements: refshape, nfeatofdim
import ..FElements: jacjac
using ..FESpaces.FEFields: FEField, ndofsperterm
using ..FESpaces: FESpace, doftype
import ..FESpaces: ndofsperel
using ..QPIterators: QPIterator, bfungradpar

#= TODO is it more natural to have access to the geometry from the font element space or from the iterator? =#

"""
    FEIterator{FES, IR, G, IT, T, V, IR0, IR1, IR2, IR3, F0, F1, F2, F3}

Type of finite element iterator. Parameterized with the types of
- `FES`: finite element space,
- `IR`: base incidence relation of the mesh,
- `G`: type of the geometry attribute,
- `IT`: type of integer indices, such as the  numbers of nodes and degrees of freedom,
- `T`: type of the degree of freedom value (real double, complex float, ... ),
- `V`: `Val` representation of the manifold dimension of the base relation elements,
- `IR0`, `IR1`, `IR2`, `IR3`: types of incidence relations with which degrees
  of freedom are associated in the finite element space, for each of the
  manifolds dimensions 0, 1, 2, 3,
- `F0`, `F1`, `F2`, `F3`: types of fields with which degrees
  of freedom are associated in the finite element space, for each of the
  manifolds dimensions 0, 1, 2, 3.
"""
struct FEIterator{FES, IR, G, IT, T, V, IR0, IR1, IR2, IR3, F0, F1, F2, F3}
    fesp::FES
    _bir::IR
    _geom::G
    _dofs::Vector{IT}
    _dofvals::Vector{T}
    _entmdim::Vector{IT}
    _dofcomp::Vector{IT}
    _nodes::Vector{IT}
    _irs0::Union{Nothing, IR0}
    _irs1::Union{Nothing, IR1}
    _irs2::Union{Nothing, IR2}
    _irs3::Union{Nothing, IR3}
    _fld0::Union{Nothing, F0}
    _fld1::Union{Nothing, F1}
    _fld2::Union{Nothing, F2}
    _fld3::Union{Nothing, F3}
    _manifdimv::V
    _state::Vector{IT}

    function FEIterator(fesp::FES) where {FES}
        _bir = baseincrel(fesp.mesh)
        _geom = MeshCore.attribute(_bir.right, "geom")
        _dofs = zeros(Int64, ndofsperel(fesp))
        _dofvals = zeros(doftype(fesp), ndofsperel(fesp))
        _entmdim = zeros(Int64, ndofsperel(fesp))
        _dofcomp = zeros(Int64, ndofsperel(fesp))
        _nodes = zeros(Int64, nfeatofdim(fesp.fe, 0))
        _fld0 = nothing
        _fld1 = nothing
        _fld2 = nothing
        _fld3 = nothing
        _irs0 = nothing
        _irs1 = nothing
        _irs2 = nothing
        _irs3 = nothing
        0 in keys(fesp._irsfields) && (_irs0 = fesp._irsfields[0][1]; _fld0 = fesp._irsfields[0][2])
        1 in keys(fesp._irsfields) && (_irs1 = fesp._irsfields[1][1]; _fld1 = fesp._irsfields[1][2])
        2 in keys(fesp._irsfields) && (_irs2 = fesp._irsfields[2][1]; _fld2 = fesp._irsfields[2][2])
        3 in keys(fesp._irsfields) && (_irs3 = fesp._irsfields[3][1]; _fld3 = fesp._irsfields[3][2])
        _manifdimv = Val(manifdim(refshape(fesp.fe)))
        p = 1
        _irs0 != nothing && (p = _init_e_d!(_entmdim, _dofcomp, p, _irs0, _fld0))
        _irs1 != nothing && (p = _init_e_d!(_entmdim, _dofcomp, p, _irs1, _fld1))
        _irs2 != nothing && (p = _init_e_d!(_entmdim, _dofcomp, p, _irs2, _fld2))
        _irs3 != nothing && (p = _init_e_d!(_entmdim, _dofcomp, p, _irs3, _fld3))
        return new{FES, typeof(_bir), typeof(_geom), eltype(_dofs), doftype(fesp), typeof(_manifdimv), typeof(_irs0), typeof(_irs1), typeof(_irs2), typeof(_irs3), typeof(_fld0), typeof(_fld1), typeof(_fld2), typeof(_fld3)}(fesp, _bir, _geom, _dofs, _dofvals, _entmdim, _dofcomp, _nodes, _irs0, _irs1, _irs2, _irs3, _fld0, _fld1, _fld2, _fld3, _manifdimv, [0])
    end
end

"""
    Base.iterate(it::FEIterator, state = 1)

Advance the iterator to the next entity.

The nodes of the finite element are cached, as is a vector of all the degrees
of freedom represented on the element.
"""
function Base.iterate(it::FEIterator, state = 1)
    if state > nrelations(it._bir)
        return nothing
    else
        return (_update!(it, state), state+1)
    end
end

"""
    Base.length(it::FEIterator)

Number of elements represented by this iterator.
"""
Base.length(it::FEIterator)  = nrelations(it._bir)

"""
    ndofsperel(it::FEIterator)

Retrieve the number of degrees of freedom per element.
"""
ndofsperel(it::FEIterator) = ndofsperel(it.fesp)

"""
    eldofs(it::FEIterator)

Retrieve the vector of the element degrees of freedom
"""
eldofs(it::FEIterator) = it._dofs

"""
    elnodes(it::FEIterator)

Retrieve the vector of the nodes of the element.
"""
elnodes(it::FEIterator) = it._nodes

"""
    eldofentmdims(it::FEIterator)

Retrieve the vector of the entity dimensions for each element degree of freedom.

Each degree of freedom is associated with some entity of the finite element:
vertices, edges, faces, and so on. This vector records the dimension of the
manifold entity with which each degree of freedom is associated.
"""
eldofentmdims(it::FEIterator) = it._entmdim

"""
    eldofcomps(it::FEIterator)

Retrieve the vector of the component numbers for each element degree of freedom.

If multiple copies of the finite element are referenced in the finite element
space, each copy is referred to as component.
"""
eldofcomps(it::FEIterator) = it._dofcomp

function _init_e_d!(mdim, dofcomp, p, ir, fl)
    ndpt = ndofsperterm(fl)
    for k in 1:nentities(ir, 1)
        for i in 1:ndpt
            mdim[p] = MeshCore.manifdim(shapedesc(ir.right))
            dofcomp[p] = i
            p = p + 1
        end
    end
    return p
end

function _storedofs!(d, p, e, ir, fl)
    ndpt = ndofsperterm(fl)
    for k in 1:nentities(ir, e)
        gk = ir[e, k]
        for i in 1:ndpt
            d[p] = fl.dofnums[gk][i]
            p = p + 1
        end
    end
    return p
end

function _storedofvals!(d, p, e, ir, fl)
    ndpt = ndofsperterm(fl)
    for k in 1:nentities(ir, e)
        gk = ir[e, k]
        for i in 1:ndpt
            d[p] = fl.dofvals[gk][i]
            p = p + 1
        end
    end
    return p
end

function _update!(it::FEIterator, state)
    it._state[1] = state
    copyto!(it._nodes, it._bir[state])
    p = 1
    it._irs0 != nothing && (p = _storedofs!(it._dofs, p, state, it._irs0, it._fld0))
    it._irs1 != nothing && (p = _storedofs!(it._dofs, p, state, it._irs1, it._fld1))
    it._irs2 != nothing && (p = _storedofs!(it._dofs, p, state, it._irs2, it._fld2))
    it._irs3 != nothing && (p = _storedofs!(it._dofs, p, state, it._irs3, it._fld3))
    return it
end

"""
    eldofvals(it::FEIterator)

Provide access to vector of element degrees of freedom.
"""
function eldofvals(it::FEIterator)
    p = 1
    it._irs0 != nothing && (p = _storedofvals!(it._dofvals, p, it._state[1], it._irs0, it._fld0))
    it._irs1 != nothing && (p = _storedofvals!(it._dofvals, p, it._state[1], it._irs1, it._fld1))
    it._irs2 != nothing && (p = _storedofvals!(it._dofvals, p, it._state[1], it._irs2, it._fld2))
    it._irs3 != nothing && (p = _storedofvals!(it._dofvals, p, it._state[1], it._irs3, it._fld3))
    return it._dofvals
end

"""
    jacjac(it::FEIterator, qpit::QPIterator)

Compute the Jacobian matrix and the Jacobian determinant.

The finite element iterator cooperates with the quadrature point iterator here
to compute the Jacobian at the current integration point.
"""
function jacjac(it::FEIterator, qpit::QPIterator)
    return jacjac(it.fesp.fe, it._geom, it._nodes, qpit._geomscalbfungrad_ps[qpit._pt])
end

"""
    location(it::FEIterator, qpit::QPIterator)

Calculate the location of the quadrature point.
"""
function location(it::FEIterator, qpit::QPIterator)
    n = it._nodes[1]
    loc = it._geom[n] * qpit._geomscalbfuns[qpit._pt][1]
    for i in 2:length(it._nodes)
        n = it._nodes[i]
        loc += it._geom[n] * qpit._geomscalbfuns[qpit._pt][i]
    end
    return loc
end

end


__precompile__()

module Intan

using HDF5, DistributedArrays, SpikeSorting, Gtk.ShortNames, Cairo, MAT,JLD, DSP

using SharedArrays, Libdl, Distributed, Dates

import SpikeSorting.Vec2, SpikeSorting.RubberBand, SpikeSorting.rb_set, SpikeSorting.rb_draw, SpikeSorting.identity_matrix, SpikeSorting.select_color

if VERSION > v"0.7-"
    const Void = Nothing
    const is_linux() = Sys.islinux()
    const is_apple() = Sys.isapple()
    const is_windows() = Sys.iswindows()
    const is_unix() = Sys.isunix()
    const setproperty! = set_gtk_property!
    const getproperty = get_gtk_property
    const linspace(x,y,z) = range(x,stop=y,length=z)
    const indmin = argmin
    const find = findall
    const indmax = argmax
    Base.round(x::Number, digits) = round(x; digits=digits)
end

export makeRHD, RHD2164, RHD2132,  makegui, Debug, Intan_GUI

#const MyFilter = DSP.Filters.DF2TFilter{DSP.Filters.SecondOrderSections{Float64,Float64},Array{Float64,2}}
const MyFilter = DSP.DF2TFilter{SecondOrderSections{:z, Float64, Float64}, Matrix{Float64}}

include("types.jl")
include("constants.jl")
include("gtk_helpers.jl")
include("gui/single_channel_canvas.jl")
include("gui/cluster_treeview.jl")
include("gui/multi_canvas_selection.jl")
include("gui/soft_scope.jl")
include("gui/event_display.jl")
include("gui/spectrogram.jl")
include("gui/multi_channel_display.jl")
include("gui/reference_channel.jl")
include("gui/parameter_table.jl")
include("gui/filters.jl")
include("gui/thresholds.jl")
include("gui/save_pref.jl")
include("gui/clock.jl")
include("gui/digital_out_gui.jl")

#functions needed for evaluation board from Intan
include("ICs/Opal_Kelly_FPGA/opalkelly_gui.jl")
include("ICs/Opal_Kelly_FPGA/c_wrapper.jl")
include("ICs/Opal_Kelly_FPGA/dacs.jl")
include("ICs/Opal_Kelly_FPGA/ttls.jl")
include("ICs/Opal_Kelly_FPGA/intan_expansion_board.jl")
include("ICs/Opal_Kelly_FPGA/cable_delay.jl")
include("ICs/Opal_Kelly_FPGA/headstage_cmd.jl")

include("rhd2000evalboard.jl")
include("rhd2000registers.jl")
include("gui.jl")
include("digital_output.jl")
include("plotting.jl")
include("offline.jl")
include("task_list.jl")
include("save_load.jl")
include("benchmark.jl")

#Spike Sorting
include("sorting/windows.jl")
include("sorting/templates.jl")

#Graphics
include("graphics_help/cairo.jl")

end


"""
Basic structure keeping the parameters of all the agents in the current simulation of a model.

# Elements

 - **t**::AbstractFloat Time of the community
 - **N**::Int Number of particles in the community
 - **declaredSymb**::Dict{String,Array{Symbol}} Dictionary storing the names of all the parameters declared in model according to the respective fields where they have been declared.
 - **var**::Array{<:AbstractFloat,2} 2D Array with all the agents in rows and all the corresponding values of the variables in columns.
 - **inter**::Array{<:AbstractFloat,2} 2D Array with all the agents in rows and all the corresponding values of the interaction parameters in columns.
 - **loc**::Array{<:AbstractFloat,2} 2D Array with all the agents in rows and all the corresponding values of the local parameters in columns.
 - **locInter**::Array{<:AbstractFloat,2} 2D Array with all the agents in rows and all the corresponding values of the local interaction parameters in columns.
 - **glob**::Array{<:AbstractFloat,1} 1D Array with all the corresponding values of the global parameters in rows.
 - **globArray**::Array{Array{AbstractFloat},1} 1D Array with all the corresponding global arrays in rows.
 - **ids**::Array{Int,2} 2D Array with all the agents in rows and all the corresponding values of the identities in columns

# Constructors

    function Community(abm::Model; N::Int=1, t::AbstractFloat=0.)

### Arguments
 - **abm** (Model) Agent Model structure

### Additional keyword arguments
 - **N::Int** Number of Agent with wich start the model. N=1 by default.
 - **t::AbstractFloat** Time of the community at creation. t=1. by default
 - **mediumN::Array{Int,1}** Size of the medium grid. (Default [])

### Example
```@julia
 julia> model = Model();
 julia> addLocal!(model,:x);
 julia> community = Community(model, N=2, t=0.5)
 Community(0.5, 2, Dict{String,Array{Symbol,N} where N}("glob" => [],"ids" => [],"locInter" => [],"loc" => [:x],"inter" => [],"var" => []), Array{AbstractFloat}(undef,2,0), Array{AbstractFloat}(undef,2,0), AbstractFloat[0.0; 0.0], AbstractFloat[0.0; 0.0], AbstractFloat[], Array{Int64}(undef,2,0))
 julia> community.N
 2
 julia> community.t
 0.5
 julia> community.declaredSymb
 Dict{String,Array{Symbol,N} where N} with 6 entries:
   "glob"     => Symbol[]
   "ids"      => Symbol[]
   "locInter" => Symbol[]
   "loc"      => [:x]
   "inter"    => Symbol[]
   "var"      => Symbol[]
```

# Base extended methods

In addition for directly accessing the elements of the structure, the following methods from the Base Package has been extended to access the elements of the Community.

    function Base.getindex(a::Community,var::Symbol)

Returns an array with all the values of the declared symbol for all the agents.

Example
```julia
 julia> model.N
 2
 julia> community.declaredSymbols["loc"]
 1-element Array{Symbol,1}:
 :x
 julia> community[:x]
 2-element Array{<:AbstractFloat,1}:
  0.0
  0.0
```

    function Base.setindex!(a::Community,v::Array{<:AbstractFloat},var::Symbol)

Sets the values of a declared symbol to the values of the array v. The array has to be the same length as N.

Example
```@julia
 julia> community[:x] = [1.,2.];
 julia> community[:x]
 2-element Array{Float64,1}:
  1.0
  2.0
```

    function Base.setindex!(a::Community,v::Number,var::Symbol)

Sets the values of a declared symbol to the given value v.

Example
```@julia
julia> community[:x] = 2.;
julia> community[:x]
2-element Array{Float64,1}:
 2.0
 2.0
```
"""
mutable struct Community
    dims::Int
    t::AbstractFloat
    N::Int
    mediumN::Array{Int,1}
    simulationBox::Array{<:AbstractFloat,2}
    radiusInteraction::Union{Real,Array{<:AbstractFloat,1}}
    declaredSymbols_::Dict{String,Array{Symbol}}
    local_::Array{<:AbstractFloat,2}
    identity_::Array{Int,2}
    global_::Array{<:AbstractFloat,1}
    globalArray_::Array{Array{Float64},1}
    medium_::Union{Array{<:AbstractFloat,1},Array{<:AbstractFloat,2},Array{<:AbstractFloat,3},Array{<:AbstractFloat,4}}
end

function Community(abm::Model; N::Int=1, t::AbstractFloat=0., mediumN::Array{Int,1}=Array{Int,1}([]))

    if !isempty(abm.agent.declaredSymbols["Medium"])
        if length(mediumN) != abm.agent.dims
            error("mediumN has to be an array with the same length as Model dimensions specifing the number of points in the grid.")
        end
    end

    dims = abm.agent.dims
    mediumN = mediumN
    simulationBox = zeros(FLOAT,dims,2)
    radiusInteraction = 0.
    loc = zeros(FLOAT,N,length(abm.agent.declaredSymbols["Local"]))
    ids = ones(INT,N,length(abm.agent.declaredSymbols["Identity"]))
    ids[:,1] .= 1:N
    glob = zeros(FLOAT,length(abm.agent.declaredSymbols["Global"]))
    globArray = []
    medium = zeros(FLOAT,mediumN...,length(abm.agent.declaredSymbols["Medium"]))
    for i in abm.agent.declaredSymbols["GlobalArray"]
        push!(globArray,[])
    end

    declaredSymbols = abm.agent.declaredSymbols

    return Community(dims,t,N,mediumN,simulationBox,radiusInteraction,declaredSymbols,loc,ids,glob,globArray,medium)
end

function Base.show(io::IO,com::Community)
    print("PARAMETERS\n")
    for i in keys(com.declaredSymbols_)
        if ! isempty(com.declaredSymbols_[i])
            print(i,"\n\t")
            for j in com.declaredSymbols_[i]
                println(" ",j)
                println(" ",getproperty(com,j))
            end
            print("\n")
        end
    end
end

"""
Structure that basically stores an array of Coomunities at different time points.

# Elements

 - **com** (Array{Community}) Array where the communities are stored

# Constructors

    function CommunityInTime()

Instantiates an empty CommunityInTime folder.

# Base extended methods

    function Base.push!(a::CommunityInTime,c::Community)

Adds one Community element to the CommunityInTime object.

    function Base.length(a::CommunityInTime)

Returns the number of time points of the Community in time.

    function Base.getindex(a::CommunityInTime,var::Int)
    function Base.firstindex(a::CommunityInTime,var::Int)
    function Base.lastindex(a::CommunityInTime,var::Int)

Returns the Community of the corresponding entry.

    function Base.getindex(a::CommunityInTime,var::Symbol)

Returns a 2D array with rows being the agents and the rows the timepoints. If the agent did not existed for certain time point, the extry is filled with a NaN value.
"""
mutable struct CommunityInTime
    com::Array{Community,1}

    function CommunityInTime()

        return new(Community[])
    end
end


abstract type Simulation{T,Dim} end

"""
    FrequencySimulation([particles::AbstractParticles=[],]
                        source::AbstractSource)

Build a FrequencySimulation. If particles are not provided, an empty array is used.

After building, you can [`run`](@ref) the simulation to get a [`FrequencySimulationResult`](@ref).
"""
mutable struct FrequencySimulation{T<:AbstractFloat,Dim,P<:PhysicalMedium} <: Simulation{T,Dim}
    "Vector of particles, can be of different types."
    particles::AbstractParticles
    "Source wave, where source.medium is the background medium of the simulation."
    source::AbstractSource{T,P}
end

# Constructor which infers parametric types from input arguments, note that we  don't need to do much type checking as the struct will error is inconsistent
function FrequencySimulation(particles::AbstractParticles{T,Dim}, source::AbstractSource{T,P}) where {Dim,T,P<:PhysicalMedium{T,Dim}}
    FrequencySimulation{T,Dim,P}(particles, source)
end

# A simulation with just sources is perfectly reasonable
function FrequencySimulation(source::AbstractSource{T,P}) where {Dim,T,P<:PhysicalMedium{T,Dim}}
    FrequencySimulation{T,Dim,P}(Vector{AbstractParticle{T,Dim}}(undef,0), source)
end

import Base.show
function show(io::IO, mime::MIME"text/plain", s::FrequencySimulation{T}) where {T}
    # text/plain is repl type
    # FrequencySimulation paramaters can be determined entirely from the medium and shape so we do not need to print them
    println(io, "FrequencySimulation{$T}")
    print(io,   "medium    = ")
    show(io, s.source.medium)
    println(io)
    println(io, "particles = ")
    for particle in s.particles
        show(io, particle)
    end
    return
end

import Base.run

run(particles::AbstractParticles, source::AbstractSource, x::Union{Shape,AbstractVector}, ω; kws...) = run(FrequencySimulation(particles,source), x, ω; kws...)

run(source::AbstractSource, x::Union{Shape,Vector}, ω; kws...) = run(FrequencySimulation(source), x, ω; kws...)

function run(sim::FrequencySimulation{T,Dim,P}, x::AbstractVector{T}, ωs::AbstractVector{T}=T[];
        kws...)::(SimulationResult{T,Dim,FieldDim} where FieldDim) where {Dim,P,T}
    run(sim,[x],ωs; kws...)
end

function run(sim::FrequencySimulation{T,Dim,P}, x::AbstractVector{T}, ω::T;
        kws...)::(SimulationResult{T,Dim,FieldDim} where FieldDim) where {Dim,P,T}
    run(sim,[x],[ω]; kws...)
end

# Main run function, all other run functions use this
function run(sim::FrequencySimulation{T,Dim,P}, x_vec::Union{Vector{Vector{T}},Vector{SVector{Dim,T}}}, ω::T;
        basis_order::Int = 5,
        only_scattered_waves::Bool = false, kws...) where {Dim,FieldDim,T,P<:PhysicalMedium{T,Dim,FieldDim}}

    x_vec = [SVector{Dim,T}(x...) for x in x_vec]

    # return just the source if there are no particles
    if isempty(sim.particles)
        f = field(sim.source)
        field_vec = f.(x_vec,ω)
    else
        # Calculate the outgoing basis coefficients around each particle, this is where most of the maths happens
        a_vec = basis_coefficients(sim, ω; basis_order=basis_order)

        if only_scattered_waves # remove source wave
            sim = FrequencySimulation(sim.particles,
                constant_source(sim.source.medium, zero(T)*im)
            )
        end

        # Evaluate the total field at the requested x positions
        field_vec = field(sim, ω, x_vec, a_vec)
    end

    # Construct results object
    field_vec = reshape(map(f->SVector{FieldDim,Complex{T}}(f), field_vec), :, 1)
    return FrequencySimulationResult{T,Dim,FieldDim}(field_vec, x_vec, Vector([ω]))

end

function run(sim::FrequencySimulation{T,Dim,P}, x_vec::Union{Vector{Vector{T}},Vector{SVector{Dim,T}}}, ωs::AbstractArray{T}=T[];
        ts::AbstractArray{T} = T[], result_in_time = !isempty(ts),
        basis_order::Int = 5,
        min_basis_order::Int = basis_order,
        basis_order_vec::AbstractVector{Int} = [-1],
        kws...)::(SimulationResult{T,Dim,FieldDim} where FieldDim)  where {Dim,P,T}

    if isempty(ωs) ωs = t_to_ω(ts) end
    x_vec = [SVector{Dim,T}(x...) for x in x_vec]

    # Considering basis_order to be the maximum basis order, then to avoid too much truncation error we use smaller basis orders on the smaller frequencies.
    if basis_order_vec == [-1]
        max_basis_order = max(basis_order,min_basis_order)
        basis_order_vec = Int.(round.(
            LinRange(min_basis_order, max_basis_order, length(ωs))
        ))
        basis_order_vec = basis_order_vec[sortperm(ωs)]
    end

    # if user asks for ω = 0, then we provide
    if first(ωs) == zero(T)
        # Compute for each angular frequency, then join up all the results
        fields = mapreduce(
            i -> run(sim,x_vec,ωs[i]; basis_order = basis_order_vec[i], kws...).field,
        hcat, eachindex(ωs)[2:end])

        # extrapolate field at ω = 0, which should be real when the time signal is real
        zeroresponse = real(ωs[3].*fields[:,1] - ωs[2].*fields[:,2]) ./ (ωs[3]-ωs[2])
        fields = reshape([zeroresponse; fields[:]], length(zeroresponse), size(fields,2)+1)
    else
        fields = mapreduce(
            i -> run(sim,x_vec,ωs[i]; basis_order = basis_order_vec[i], kws...).field,
        hcat, eachindex(ωs))
    end

    if !result_in_time
        FrequencySimulationResult(fields,x_vec,Vector(ωs))
    else
        @error "Using `result_in_time=true` in `run` is depricated and will be removed in later versions. Please instead use `frequency_to_time` on the results of `run`."
        # if isempty(ts) ts = ω_to_t(ωs) end
        #
        # # better to use the defaults of TimeSimulationResult's Constructor.
        # frequency_to_time(FrequencySimulationResult(fields,x_vec,Vector(ωs)); t_vec = reshape(ts,length(ts)), time_kws...)
    end
end

# Add docstring to run functions
"""
    run(sim::FrequencySimulation, x, ω; basis_order=5)

Run the simulation `sim` for the position `x` and angular frequency `ω`.

Position can be an SVector or Vector{SVector} and frequency can be a float or
vector of floats.
"""
function run(s::FrequencySimulation) throw(MethodError(run, (s,))) end

"""
    run(sim::FrequencySimulation, region::Shape;
        res=20, xres=res, yres=res, basis_order=5)

Run the simulation `sim` for a grid of positions in region and for angular frequency `ω`.

Frequency can be a float or vector of floats. The resolution of the grid points is defined
by xres and yres.
"""
function run(sim::FrequencySimulation{T,Dim}, region::Shape, ω_vec::AbstractVector;
            kws...) where {T,Dim}

    x_vec, inds = points_in_shape(region; kws...)
    # x_vec is a square grid of points and x_vec[inds] are the points in the region.

    result = run(sim, x_vec[inds], ω_vec; kws...)

    field_mat = zeros(typeof(result.field[1]), length(x_vec), length(ω_vec))
    field_mat[inds,:] = result.field

    return FrequencySimulationResult(field_mat, x_vec, ω_vec)
end

"""
    basis_coefficients(sim::FrequencySimulation, ω::AbstractFloat; basis_order::Int=5)::Matrix{Complex}

Return coefficients for bases around each particle for a given simulation and angular frequency (ω).
"""
function basis_coefficients(sim::FrequencySimulation{T,Dim,P}, ω::T; basis_order::Int = 5) where {Dim,P,T}

    # Precompute T-matrices for these particles
    t_matrices = get_t_matrices(sim.source.medium, sim.particles, ω, basis_order)

    # Compute scattering matrix for all particles
    S = scattering_matrix(sim.source.medium, sim.particles, t_matrices, ω, basis_order)

    # Get forcing vector from source, forms the right hand side of matrix equation to find basis_coefficients
    source_coefficient = regular_spherical_coefficients(sim.source)
    forcing = reduce(vcat, [source_coefficient(basis_order,origin(p),ω) for p in sim.particles])

    # Find scattering coefficients by solving this forcing
    a = (S + I) \ forcing

    # reshape and multiply by t-matrix to get the scattering coefficients
    a = reshape(a,basisorder_to_basislength(P,basis_order),length(sim.particles))
    for i in axes(a,2)
        a[:,i] = t_matrices[i] * a[:,i]
    end
    return a
end

function field(sim::FrequencySimulation{T,Dim,P}, ω::T, x_vec::Vector{v}, a_vec) where {Dim,P,T,v <: AbstractArray{T}}

    N = basislength_to_basisorder(P,size(a_vec,1))
    num_particles = length(sim.particles)
    basis = outgoing_basis_function(sim.source.medium, ω)

    function sum_basis(x)
        sum(eachindex(sim.particles)) do i
            p = sim.particles[i]
            sum(a_vec[:,i] .* basis(N, x-origin(p)))
        end
    end
    map(x_vec) do x
        j = findfirst(p -> x ∈ p, sim.particles)
        if typeof(j) === Nothing
            field(sim.source)(x,ω) + sum_basis(x)
        else
            p = sim.particles[j]
            internal_field(x, p, sim.source, ω, collect(a_vec[:,j]))
        end
    end
end


#gnum-division.jl
#division algorithms between unums, ubounds and gnums.

#=

doc"""
  `div!(::Unum, ::Gnum)` divides a unum BY a gnum.  the gnum will be altered as
  a result.  This function may also alter the unum by appending g-flags into the
  flags section.  Returns a reference to the gnum to allow chaining.
"""
function div!{ESS,FSS}(a::Unum{ESS,FSS}, b::Gnum{ESS,FSS})
  #make sure our ignore_sides parameters are set.
  clear_ignore_sides!(b)
  #go ahead and set "g-flags" on the value a.
  set_g_flags!(a)

  #ascertain the result sign.  Note that all division algorithms are
  #sign-agnostic and return positive-definite results.
  result_sign::UInt16 = @signof(a) $ @signof(b.lower)

  #perform hard override checks on the multiply.
  __check_div_hard_override!(a, b)

  #flip sides?

  #test sidedness and trampoline to the appropriate procedure.
  #is_onesided(b) ? div_onesided!(a, b) : div_twosided!(a, b)

  (result_sign != 0) && additive_inverse!(b)
end
=#
#=
doc"""
  `mul!(::Ubound, ::Gnum)` multiplies a ubound INTO a gnum.
  the gnum will be altered as a result.  This function may also alter the ubound
  by appending g-flags into the flags section of the constituent unums.  Returns
  a reference to the gnum to allow chaining.
"""
function mul!{ESS,FSS}(a::Ubound{ESS,FSS}, b::Gnum{ESS,FSS})
  throw(ArgumentError("not implemented yet."))
end

doc"""
  `mul!(a::Gnum, b::Gnum)` multiplies a gnum `a` INTO a gnum `b`.
  the *second* gnum will be altered as a result.  Following at&t assembly syntax,
  the result is on the right hand side (this is the equivalent of b \*= a ).
  Returns a reference to the gnum to allow chaining.
"""
function mul!{ESS,FSS}(a::Gnum{ESS,FSS}, b::Gnum{ESS,FSS})
  throw(ArgumentError("not implemented yet."))
end

################################################################################
## onesided/twosided multiplies.

doc"""
  `mul_onesided!(::Unum, ::Gnum)` performs a multiplication of a unum a into
  a unum b, where the starting gnum is one-sided.  This function does not check
  that the passed parameters match this description.
"""
function mul_onesided!{ESS,FSS}(a::Unum{ESS,FSS}, b::Gnum{ESS,FSS})
  #all procedures in onesided multiply presume that a is positive.

  if is_zero(b, LOWER_UNUM)
    ignore_side!(b, LOWER_UNUM)
  elseif is_inf(b, LOWER_UNUM)
    b.lower.flags &= ~UNUM_SIGN_MASK
    ignore_side!(b, LOWER_UNUM)
  elseif should_calculate(b, LOWER_UNUM) && is_unit(b.lower)
    copy_unum!(a, b.lower)
    #overwrite the sign; this will be re-xored later.
    b.lower.flags &= ~UNUM_SIGN_MASK
    ignore_side!(b, LOWER_UNUM)
  elseif is_mmr(a)
    should_calculate(b, LOWER_UNUM) && mmr_onesided_mult!(b)
  elseif is_sss(a)
    should_calculate(b, LOWER_UNUM) && sss_onesided_mult!(b)
  end

  #check for the gnum value being mmr or sss.
  if should_calculate(b, LOWER_UNUM)
    if is_mmr(b, LOWER_UNUM)
      copy_unum!(a, b.lower)
      mmr_onesided_mult!(b)
    elseif is_sss(b, LOWER_UNUM)
      copy_unum!(a, b.lower)
      sss_onesided_mult!(b)
    end
  end

  #we have exhausted all of the special cases, so just do the multiplication.
  should_calculate(b, LOWER_UNUM) && (__multiply!(a, b, LOWER_UNUM, UPPER_UNUM))
  #swap parity if a was negative.

  #return the gnum.
  b
end

doc"""
  `mul_twosided!(::Unum, ::Gnum)` performs a multiplication of a unum a into
  a unum b, where the starting gnum is one-sided.  This function does not check
  that the passed parameters match this description.
"""
function mul_twosided!{ESS,FSS}(a::Unum{ESS,FSS}, b::Gnum{ESS,FSS})
  throw(ArgumentError("twosided mults not implemented yet"))
end
=#
#=
doc"""
  `__transits_zero(::Gnum)` checks to see if a Gnum has a negative-definite left
  and a positive-definite right.  This is useful for division because such a gnum
  will throw an error.
"""
function __transits_zero{ESS,FSS}(b::Gnum{ESS,FSS})
  #onesided gnums cannot transit zero
  return is_twosided(b) & is_negative(b.lower) & is_positive(b.upper) & !(is_zero(b, LOWER_UNUM)) & !(is_zero(b, UPPER_UNUM))
end

################################################################################
## shortcut division, for special values.

doc"""
  `__check_div_hard_override!(::Unum, ::Gnum)` checks to see if the terms in
  the divisor or the dividend trigger a "hard override".  These values are,
  in cascading precedence:

  nan (dividend, divisor) -> outputs nan.
  value that transits zero (divisor) -> outputs nan.
"""
function __check_div_hard_override!{ESS,FSS}(a::Utype{ESS,FSS}, b::Gnum{ESS,FSS})
  ############################################
  # analysis of division with NaNs.
  # if our dividend is nan, then set the product to nan.
  is_nan(a) && (@scratch_this_operation!(b); return)
  is_nan(b) && (ignore_both_sides!(b); return)

  ############################################
  # check for values that transit zero
  __transits_zero(b) && (@scratch_this_operation!(b); return)
end

#checking for values in the dividend which trigger override.
function __check_div_dividend_override!{ESS,FSS}(a::Unum{ESS,FSS}, b::Gnum{ESS,FSS})
  ############################################
  # analysis of division with infs
  # inf / anything becomes inf, except inf, which becomes NaN.

  if (is_g_inf(a))
    #infinity can't multiply times just zero, either
    should_calculate(b, LOWER_UNUM) && is_inf(b, LOWER_UNUM) && (@scratch_this_operation!(b))
    should_calculate(b, UPPER_UNUM) && is_inf(b, UPPER_UNUM) && (@scratch_this_operation!(b))
    #set the lower side to infinity, then ignore further calculations, and make it onesided
    inf!(b, z16, LOWER_UNUM); ignore_side!(b, LOWER_UNUM); set_onesided!(b)
  end

  ############################################
  # analysis of division with zeros
  # zero divided by anything becomes zero.
  # n.b. this does not check twosided unums, because that has already been checked.
  if is_g_zero(a)
    zero!(b, LOWER_UNUM)
    set_onesided!(b)
    ignore_side!(b, LOWER_UNUM)
  end
end

function __check_div_divisor_override!{ESS,FSS}(a::Unum{ESS,FSS}, b::Gnum{ESS,FSS})
  #anything / inf is zero, but make sure the zero gets placed in the lower unum.
  if should_calculate(b, LOWER_UNUM) && is_inf(b, LOWER_UNUM)
    zero!(b, LOWER_UNUM)
    ignore_side!(b, LOWER_UNUM); set_onesided!(b)
  end
  if should_calculate(b, UPPER_UNUM) && is_inf(b, UPPER_UNUM)
    #move the value of the lower unum into the upper segment and then do the calculation.
    #note tthat using this implies that the upper unum is active, s o we don't need to
    #set this to onesided.
    copy_unum_with_gflags!(b.lower, b.upper)
    zero!(b, LOWER_UNUM)
    ignore_side!(b, LOWER_UNUM)
  end
end

doc"""
  `sss_onesided_mult!(::Gnum)` multiplies a onesided Gnum times an sss.
  the lower value of the Gnum contains the value to be multiplied.  The upper
  value will contain the result value.
"""
function sss_onesided_mult!{ESS,FSS}(b::Gnum{ESS,FSS})
  #if the magnitude is more than one, then we multiply the value of b times
  #smallsubnormal.
  if is_magnitude_less_than_one(b.lower)
    sss!(b, z16, LOWER_UNUM)
    ignore_side!(b, LOWER_UNUM)
  else
    #we're going to have a two-valued solution, so alias the inner and outer values.
    set_twosided!(b)
    #first, make the upper part small_exact.
    small_exact!(b.upper, z16)
    __outward_exact!(b.lower)
    #do the multiplication
    __multiply!(b.lower, b, UPPER_UNUM, DISCARD_SECONDARY)
    #check to see if we need to retrace.
    is_exact(b.upper) && __inward_ulp!(b.upper)
    #set the lower unum to be small subnormal.
    sss!(b, z16, LOWER_UNUM)
    #finished with calculations.
    ignore_both_sides!(b)
  end
end

doc""" `mmr_onesided_mult!(::Gnum)` multiplies a onesided Gnum times an mmr."""
function mmr_onesided_mult!{ESS,FSS}(b::Gnum{ESS,FSS})
  if is_magnitude_less_than_one(b.lower)
    #we're going to have a two-valued solution, so alias the inner and outer values.
    set_twosided!(b)
    #first, make the buffer big_exact.
    big_exact!(b.buffer, z16)
    #multiply the buffer times the value in the lower unum, overwriting it.
    __multiply!(b.buffer, b, LOWER_UNUM, DISCARD_SECONDARY)
    #check to see if we need to retrace.
    is_exact(b.lower) && make_ulp!(b.lower)
    #make the upper side mmr.
    mmr!(b, z16, UPPER_UNUM)
    ignore_both_sides!(b)
  else
    #then everything stays mmr.
    mmr!(b, z16, LOWER_UNUM)
    ignore_side!(b, LOWER_UNUM)
  end
end
=#
#=
################################################################################
## actual algorithmic multiplication

doc"""
  `Unums.__multiply!(::Unum, ::Gnum, ::Type{Val}, ::Type{Val})`
  performs the algorithmic multiplication of an unum into one side of the gnum.
  Because a unum muliplication times another unum must always be on the same
  side of zero, this procedure ignores sign, always returning a positive value.

  The *primary result* is the result of the exact multiplication between the two
  values; the *secondary result* is the result of multiplying the ulp values.

  The first value parameter contains the side that contains the multiplicand:
  this may be UPPER_GNUM, LOWER_GNUM, or BUFFER.

  The second value parameter contains a directive that determines what to do
  when the value is inexact.  If this is UPPER_GNUM, LOWER_GNUM, or BUFFER, then
  the secondary result is stored in the respective destination.  This destination
  cannot be identical to the first value parameter.

  You may also specify DISCARD_PRIMARY or DISCARD_SECONDARY as the directives,
  which will discard the primary or secondary results.
"""
@gen_code function __multiply!{ESS,FSS,side,directive}(a::Unum{ESS,FSS}, b::Gnum{ESS,FSS}, ::Type{Val{side}}, ::Type{Val{directive}})
  max_exp = max_exponent(ESS)
  sml_exp = min_exponent(ESS, FSS)
  min_exp = min_exponent(ESS)
  mesize = max_esize(ESS)
  mfsize = max_fsize(FSS)

  (side == directive) && throw(ArgumentError("multiply algorithm cannot send an ulp result to the same side as source"))

  @code quote
    a_exp::Int64 = decode_exp(a)
    b_exp::Int64 = decode_exp(b.$side)
    a_dev::UInt64 = is_exp_zero(a) * o64
    b_dev::UInt64 = is_exp_zero(b.$side) * o64

    #make the result positive
    b.scratchpad.flags &= ~UNUM_SIGN_MASK

    scratchpad_exp::Int64 = a_exp + b_exp + a_dev + b_dev

    #preliminary comparisons that will stop us from performing unnecessary steps.
    (scratchpad_exp > $max_exp + 1) && (@preserve_sflags b mmr!(b, z16, Val{side}); return)
    (scratchpad_exp < $sml_exp - 2) && (@preserve_sflags b sss!(b, z16, Val{side}); return)
  end

  if FSS < 6
    #fss < 6 can perform a multiplication within the existing 64-bit integer width.
    @code :(b.scratchpad.fraction = __chunk_mult_small(a.fraction, b.$side.fraction))
  elseif FSS == 6
    #fss = 6 requires access to the scratchpad array.
    @code :(nan!(b); return)
  else
    @code :(nan!(b); return)
  end

  @code quote

    #fraction multiplication:  where va is "virtual bit" that is 1 for normals or 0 for subnormals.
    #  va + a
    #  vb + b
    #  va*vb + vb * a + va * b + ab
    #
    carry::UInt64 = o64 - (a_dev | b_dev) #if either a or b are subnormal, the carry is zero.
    #note that adding the fractions back in has a cross-multiplied effect.
    (b_dev == 0) && (carry = __carried_add_frac!(carry, a, b.scratchpad))
    (a_dev == 0) && (carry = __carried_add_frac!(carry, b.$side, b.scratchpad))

    #next, do carry analysis. carry can be 0, 1, 2, or 3 at the most.
    if (carry == 0)
      #shift over the fraction as far as we need to.
      shift::Int64 = leading_zeros(b.scratchpad.fraction) + 1
      #check if we're zero.
      is_frac_zero(b.scratchpad) && (sss!(b, z16, Val{side}); return)
      __leftshift_frac!(b.scratchpad, shift)
      #adjust the exponent.
      scratchpad_exp -= shift
      (scratchpad_exp < $sml_exp) && (sss!(b, z16, Val{side}); return)
      if (scratchpad_exp < $min_exp)
        #calculate the leftshift.
        shift = $min_exp - scratchpad_exp
        __rightshift_frac_with_underflow_check!(b.scratchpad, shift)
        set_frac_bit!(b.scratchpad, shift)
        b.scratchpad.esize = $mesize
        b.scratchpad.exponent = 0
      else
        (b.scratchpad.esize, b.scratchpad.exponent) = encode_exp(scratchpad_exp)
        carry = 1
      end
    elseif (carry == 1)
      if scratchpad_exp > $max_exp
        @preserve_sflags b (mmr!(b, z16, Val{side}); return)
      end
      (b.scratchpad.esize, b.scratchpad.exponent) = encode_exp(scratchpad_exp)
    else
      (scratchpad_exp += 1)
      if scratchpad_exp > $max_exp
        @preserve_sflags b (mmr!(b, z16, Val{side}); return)
      else
        #shift things over by one since we went up in size.
        __rightshift_frac_with_underflow_check!(b.scratchpad, 1)
        #carry from the carry over into the fraction.
        (carry == 3) && set_frac_top!(b.scratchpad)
        #re-encode the exponent.
        (b.scratchpad.esize, b.scratchpad.exponent) = encode_exp(scratchpad_exp)
      end
      #set the carry to one
      carry = 1
    end
  end

  #if we're instructed to discard the secondary value, then simply copy over
  #the values, and we're done.
  (directive == :discard_secondary) && (@code :(copy_unum_with_gflags!(b.scratchpad, b.$side); return); @goto __mult_end)
  #assign the destination for the secondary varibale.
  sdest = (directive == :discard_primary) ? side : directive
  #this code ge
  retrace_code1 = (directive == :discard_primary) ? :(nothing) : :(carry = __tandem_copy_add_frac!(carry, b.$side, b.scratchpad, a.fsize))
  retrace_code2 = (directive == :discard_primary) ? :(nothing) : :(copy_unum!(b.scratchpad, b.$side))
  needs_twosided = (sdest == :upper) ? (:(set_twosided!(b))) : :(nothing)
  #if we're writing to the lower side, then be prepared to collapse an mmr into a onesided.
  mmr_collapse = (side == :lower) && !(directive == :discard_primary) ? :(set_onesided!(b)) : :(nothing)

  @code quote
    #if both sides were exact, the result is fairly simple.  Just copy the scratchpad results
    #on over to the destination side, we don't have to do anything further.

    is_exact(a) && is_exact(b.$side) && (copy_unum_with_gflags!(b.scratchpad, b.$side); return)
    $needs_twosided

    #the scratchpad needs to be exact, but as a temporary shim we need to make
    #it an ulp to assess if the result is mmr.
    make_ulp!(b.scratchpad)
    is_mmr(b.scratchpad) && ($mmr_collapse; mmr!(b, z16, Val{side}); return)
    make_exact!(b.scratchpad)

#    println("a: ", bits(a))
#    println("1) scr: ", bits(b.scratchpad), " carry: ", carry)

    #from here on out we engage the heuristic algorithm.  The 'direct' algorithm
    #would be to do two multiplications, but it is possible to save on that
    #calculation.  The algorithm is as follows:
    # we're going to do the multiplication X1 * X2
    # X1 = (2^P1)(F1) (+) (2^(P1-U1))
    # X2 = (2^P2)(F2) (+) (2^(P2-U2))
    # X1 * X2 = 2^(P1P2)(F1F2 (+) (2^-U1)F2 + (2^-U2)F1 + 2^(-U1-U2))
    # where x (+) a ≡ x + (0, a)

    #copy the scratchpad back, and add into the scratchpad b.$side.fraction >> a.fsize
    (is_ulp(a)) ? $retrace_code1 : $retrace_code2

#    println("2) scr: ", bits(b.scratchpad), " carry: ", carry)
    #add into the scratchpad a.fraction >> b.$side.fsize
    (is_ulp(b.$side)) && (carry = __add_frac_with_shift!(carry, a, b.scratchpad, b.$side.fsize))
    #check to make sure the far value isn't exact; move it inward if so.

    #tentatively make the fsize the maximum
    b.$side.fsize = $mfsize

    #resolve the scratchpad's carry
    __carry_resolve!(carry, b.scratchpad, Val{true})

    is_exact(b.scratchpad) && __inward_ulp!(b.scratchpad)

    #move the scratchpad back to the destination
    copy_unum!(b.scratchpad, b.$sdest)
  end

  @label __mult_end
end
=#


abstract type _AbstractFunction end

"""
    _Operation

An enum representing different node types in a symbolic expression graph.

 * `_OP_COEFFICIENT`: a numerical `Float64`
 * `_OP_VARIABLE`: a decision variable
 * `_OP_OPERATION`: a function call
 * `_OP_INTEGER_EXPONENT`: a special case for handling integer polynomials such
   as `x^2`, since the `2.0` is usually structural rather than a coefficient
   that differs between expressions.
"""
@enum(
    _Operation,
    _OP_COEFFICIENT,
    _OP_VARIABLE,
    _OP_OPERATION,
    _OP_INTEGER_EXPONENT,
)

"""
    _Node

A type to represent a node in the symbolic expression graph.

!!! warning
    `_Node`s can only be used in conjunction with `_Function`.

`_Node` has the following fields:

 * `.head::_Operation`: this describes the node type
 * `.hash::UInt64`: a hash of the expression tree rooted at this node. See the
   Fast hashing section for more details.
 * `.index::Int` : interpretation depends on `.head`. If head is:
   * `_OP_COEFFICIENT`, the index into `f.data`
   * `_OP_VARIABLE`, the index into `f.ordered_variables`
   * `_OP_INTEGER_EXPONENT`, the integer exponent `children[1]^index`
 * `.operation::Symbol` : the symbolic name of the function call, if `.head` is
   `_OP_OPERATION`, otherwise this can be ignored.
 * `.children::Union{Nothing,Vector{_Node}}` : a vector of children of the node,
   if `.head` is `_OP_OPERATION`, otherwise `nothing`.

## Fast hashing

The expression graph is structured as a Merkle tree (a.k.a. a hash tree). That
is, each node contains a field `.hash`, which is the hash of the node's
operation with the hash of its children. Since children are also nodes, hashing
a child is an O(1) lookup of `child.hash`. Moreover, two expressions are
structurally identical if the have the same hash when converted into `_Node`
form.

## Other considerations

Through experience, the representation of the symbolic expression graph is not
the bottleneck in a nonlinear optimization model. Therefore, we choose the
somewhat inefficient storage rpresentation of
`children::Union{Nothing,Vector{_Node}}` for the list of children. This results
in a GC tracked object for each node that has children. If memory issues are
identified in future, changing how we represent the expression graph is one
promising avenue of investigation.
"""
struct _Node
    head::_Operation
    hash::UInt
    index::Int
    operation::Symbol
    children::Union{Nothing,Vector{_Node}}

    function _Node(f::_AbstractFunction, coefficient::Float64)
        # Store the coefficient
        push!(f.data, coefficient)
        # Compute the hash of the node. Because all coefficients are alike, this
        # is just the hash of `_OP_COEFFICIENT`.
        h = hash(_OP_COEFFICIENT)
        # Index is the length of `f.data` when we need to look it up again.
        index = length(f.data)
        return new(_OP_COEFFICIENT, h, index, :NONE, nothing)
    end
    function _Node(f::_AbstractFunction, variable::MOI.VariableIndex)
        # We need to convert the variable to the order in which it will appear
        # in .ordered_variables. Currently, the order is stored in
        # `f.variables`...
        lookup = f.variables::Dict{MOI.VariableIndex,Int}
        # And if the variable hasn't been seen yet, add a new index
        index = get!(lookup, variable, length(lookup) + 1)
        # The hash of this node depends on the variable and it's position in
        # ordered_variables.
        h = hash(_OP_VARIABLE, hash(index))
        return new(_OP_VARIABLE, h, index, :NONE, nothing)
    end
    function _Node(::_AbstractFunction, operation::Symbol, children::_Node...)
        # Okay, a function call. The hash depends on the operation and the
        # children.
        h = hash(operation, _hash(children...))
        return new(_OP_OPERATION, h, 0, operation, collect(children))
    end
    # Special cased performance optimization: x^N where N::Int is a common
    # operation that would otherwise get re-written to x[i]^p[j]. Doing so
    # disables a lot of potential optimizations, so it's worth special-casing
    # this. Other operations aren't as important.
    function _Node(::_AbstractFunction, ::typeof(^), x::_Node, N::Int)
        # The hash depends on ^, as well as the child node `x` and the
        # exponent `N`
        h = hash(_OP_INTEGER_EXPONENT, hash(_hash(x), hash(N)))
        return new(_OP_INTEGER_EXPONENT, h, N, :^, [x])
    end
end

# The hash of a child is an O(1) lookup
_hash(child::_Node) = child.hash

# Recursively has nodes together
_hash(a::_Node, args::_Node...) = hash(a.hash, _hash(args...))

"""
    _Function(expr::Expr)

This function parses the Julia expression `expr` into a `_Function`.

`_Function` has the following fields:

 * `ordered_variables`: a vector containing the `.value` indices of each
   variable in the order that they are discovered in the expression tree.
 * `coefficients::Vector{Float64}`: a vector to store the numerical input values
   of `x`.
 * `data::Vector{Float64}`: the list of numerical coefficients that appear in
   `expr`
 * `expr::_Node`: the symbolic representation of `expr`.

For more information on how to interpret the fields, see `_Node`.
"""
mutable struct _Function <: _AbstractFunction
    ordered_variables::Vector{Int}
    coefficients::Vector{Float64}
    data::Vector{Float64}
    expr::_Node
    # A field that is only used when constructing the _Function
    variables::Union{Nothing,Dict{MOI.VariableIndex,Int}}
    function _Function(expr::Expr)
        f = new()
        f.variables = Dict{MOI.VariableIndex,Int}()
        f.data = Float64[]
        f.expr = _Node(f, expr)
        f.coefficients = zeros(length(f.variables))
        f.ordered_variables = zeros(Int, length(f.variables))
        for (k, v) in f.variables
            f.ordered_variables[v] = k.value
        end
        f.variables = nothing
        return f
    end
end

_is_integer(::Any) = false
_is_integer(x::Real) = Base.isinteger(x)

"""
    _Node(f::_Function, expr::Expr)

Parse `expr` into `f` and return a `_Node`.

!!! warning
    This function gets called recursively.
"""
function _Node(f::_Function, expr::Expr)
    if isexpr(expr, :call)
        # Performance optimization: most calls will be unary or binary
        # operators. Therefore, we can specialize an if-statement to handle the
        # common cases without needing to splat.
        if length(expr.args) == 2
            return _Node(f, expr.args[1], _Node(f, expr.args[2]))
        elseif length(expr.args) == 3
            # Special cased performance optimization for _OP_INTEGER_EXPONENT
            if expr.args[1] == :^ && _is_integer(expr.args[3])
                return _Node(f, ^, _Node(f, expr.args[2]), Int(expr.args[3]))
            end
            return _Node(
                f,
                expr.args[1],
                _Node(f, expr.args[2]),
                _Node(f, expr.args[3]),
            )
        else
            nodes = (_Node(f, expr.args[i]) for i in 2:length(expr.args))
            return _Node(f, expr.args[1], nodes...)
        end
    else
        @assert isexpr(expr, :ref)
        @assert expr.args[1] == :x
        return _Node(f, expr.args[2])
    end
end

struct _SymbolicsFunction{F,G,H}
    f::F
    ∇f::G
    ∇²f::H
    g::Vector{Float64}
    H::Vector{Float64}
    H_structure::Vector{Tuple{Int,Int}}
end

"""
    _expr_to_symbolics(expr::_Node, p, x)

Convert a `_Node` into a `Symbolics.jl` expression via operator overloading.
"""
function _expr_to_symbolics(expr::_Node, p, x)
    if expr.head == _OP_COEFFICIENT
        return p[expr.index]
    elseif expr.head == _OP_VARIABLE
        return x[expr.index]
    elseif expr.head == _OP_OPERATION
        # TODO(odow): improve this lookup of expr.operation to function.
        f = getfield(Base, expr.operation)
        return f((_expr_to_symbolics(c, p, x) for c in expr.children)...)
    else
        @assert expr.head == _OP_INTEGER_EXPONENT
        return _expr_to_symbolics(expr.children[1], p, x)^expr.index
    end
end

function _SymbolicsFunction(f::_Function, features::Vector{Symbol})
    d, n = length(f.data), length(f.ordered_variables)
    Symbolics.@variables(p[1:d], x[1:n])
    p, x = collect(p), collect(x)
    f_expr = _expr_to_symbolics(f.expr, p, x)
    f = Symbolics.build_function(f_expr, p, x; expression = Val{false})
    if :Jac in features || :Grad in features
        ∇f_expr = Symbolics.gradient(f_expr, x)
        _, g! = Symbolics.build_function(∇f_expr, p, x; expression = Val{false})
        g_cache = zeros(length(x))
    else
        g!, g_cache = nothing, Float64[]
    end
    if :Hess in features
        ∇²f_expr_square = Symbolics.sparsejacobian(∇f_expr, x)
        # ∇²f_expr_square is sparse but it is also symmetrical. MathOptInterface
        # needs only the lower-triangular (technically, it doesn't matter which
        # triangle, but let's choose the lower-triangular for simplicity).
        I, J, V = SparseArrays.findnz(∇²f_expr_square)
        # Rather than using a SparseArray as storage, convert to MOI's
        # ((i, j), v) datastructure. This simplifies
        # `hessian_lagrangian_structure` calls later.
        ∇²f_expr = [V[i] for i in 1:length(I) if I[i] >= J[i]]
        ∇²f_structure = [(I[i], J[i]) for i in 1:length(I) if I[i] >= J[i]]
        _, h! =
            Symbolics.build_function(∇²f_expr, p, x; expression = Val{false})
        h_cache = zeros(length(∇²f_structure))
    else
        h!, h_cache, ∇²f_structure = nothing, Float64[], Tuple{Int,Int}[]
    end
    return _SymbolicsFunction(f, g!, h!, g_cache, h_cache, ∇²f_structure)
end

"""
    AbstractNonlinearOracleBackend

An abstract type to help dispatch on different implementations of the function
evaluations.
"""
abstract type AbstractNonlinearOracleBackend end

"""
    DefaultBackend() <: AbstractNonlinearOracleBackend

A simple implementation that loops over the different constraints.
"""
struct DefaultBackend <: AbstractNonlinearOracleBackend end

mutable struct _NonlinearOracle{B} <: MOI.AbstractNLPEvaluator
    backend::B
    objective::Union{Nothing,_Function}
    constraints::Vector{_Function}
    symbolic_functions::Dict{UInt,_SymbolicsFunction}
    hessian_sparsity_map::Vector{Int}
    eval_objective_timer::Float64
    eval_objective_gradient_timer::Float64
    eval_constraint_timer::Float64
    eval_constraint_jacobian_timer::Float64
    eval_hessian_lagrangian_timer::Float64
    function _NonlinearOracle(backend::B, objective, constraints) where {B}
        return new{B}(
            backend,
            objective,
            constraints,
            Dict{UInt,_SymbolicsFunction}(),
            Int[],
            0.0,
            0.0,
            0.0,
            0.0,
            0.0,
        )
    end
end

function MOI.initialize(
    ::_NonlinearOracle,
    ::AbstractNonlinearOracleBackend,
    ::Vector{Symbol},
)
    return
end

function MOI.initialize(oracle::_NonlinearOracle, features::Vector{Symbol})
    oracle.eval_constraint_timer = 0.0
    oracle.eval_constraint_jacobian_timer = 0.0
    oracle.eval_hessian_lagrangian_timer = 0.0
    hashes = map(f -> f.expr.hash, oracle.constraints)
    for h in unique(hashes)
        f = oracle.constraints[findfirst(isequal(h), hashes)]
        oracle.symbolic_functions[h] = _SymbolicsFunction(f, features)
    end
    if oracle.objective !== nothing
        f = oracle.objective
        h = f.expr.hash
        if !haskey(oracle.symbolic_functions, h)
            oracle.symbolic_functions[h] = _SymbolicsFunction(f, features)
        end
    end
    MOI.initialize(oracle, oracle.backend, features)
    return
end

MOI.features_available(::_NonlinearOracle) = [:Grad, :Jac, :Hess]

function _update_coefficients(f::_Function, x::AbstractVector{Float64})
    for (i, v) in enumerate(f.ordered_variables)
        @inbounds f.coefficients[i] = x[v]
    end
    return
end

function _eval_function(
    oracle::_NonlinearOracle,
    func::_Function,
    x::AbstractVector{Float64},
)
    _update_coefficients(func, x)
    @inbounds f = oracle.symbolic_functions[func.expr.hash]
    return f.f(func.data, func.coefficients)::Float64
end

function _eval_gradient(
    oracle::_NonlinearOracle,
    func::_Function,
    x::AbstractVector{Float64},
)
    _update_coefficients(func, x)
    @inbounds f = oracle.symbolic_functions[func.expr.hash]
    f.∇f(f.g, func.data, func.coefficients)
    return f.g
end

function _eval_hessian(
    oracle::_NonlinearOracle,
    func::_Function,
    x::AbstractVector{Float64},
)
    _update_coefficients(func, x)
    @inbounds f = oracle.symbolic_functions[func.expr.hash]
    f.∇²f(f.H, func.data, func.coefficients)
    return f.H
end

function MOI.eval_objective(
    oracle::_NonlinearOracle,
    x::AbstractVector{Float64},
)
    start = time()
    @assert oracle.objective !== nothing
    objective_value = _eval_function(oracle, oracle.objective, x)
    oracle.eval_objective_timer += time() - start
    return objective_value
end

function MOI.eval_objective_gradient(
    oracle::_NonlinearOracle,
    g::AbstractVector{Float64},
    x::AbstractVector{Float64},
)
    start = time()
    @assert oracle.objective !== nothing
    ∇f = _eval_gradient(oracle, oracle.objective, x)
    fill!(g, 0.0)
    for (i, v) in enumerate(oracle.objective.ordered_variables)
        @inbounds g[v] = ∇f[i]
    end
    oracle.eval_objective_gradient_timer += time() - start
    return
end

function MOI.eval_constraint(
    oracle::_NonlinearOracle,
    g::AbstractVector{Float64},
    x::AbstractVector{Float64},
)
    start = time()
    for (row, c) in enumerate(oracle.constraints)
        g[row] = _eval_function(oracle, c, x)
    end
    oracle.eval_constraint_timer += time() - start
    return
end

function MOI.jacobian_structure(oracle::_NonlinearOracle)
    structure = Tuple{Int,Int}[]
    for (row, c) in enumerate(oracle.constraints)
        for col in c.ordered_variables
            push!(structure, (row, col))
        end
    end
    return structure
end

function MOI.eval_constraint_jacobian(
    oracle::_NonlinearOracle,
    J::AbstractVector{Float64},
    x::AbstractVector{Float64},
)
    start = time()
    k = 1
    for c in oracle.constraints
        g = _eval_gradient(oracle, c, x)
        for gi in g
            J[k] = gi
            k += 1
        end
    end
    oracle.eval_constraint_jacobian_timer += time() - start
    return
end

function _hessian_lagrangian_structure(
    ::Vector{Tuple{Int,Int}},
    ::_NonlinearOracle,
    ::Any,
    ::Nothing,
)
    return
end

function _hessian_lagrangian_structure(
    structure::Vector{Tuple{Int,Int}},
    oracle::_NonlinearOracle,
    map::Dict{Tuple{Int,Int},Int},
    c::_Function,
)
    f = oracle.symbolic_functions[c.expr.hash]
    for (i, j) in f.H_structure
        row, col = c.ordered_variables[i], c.ordered_variables[j]
        row_col = row >= col ? (row, col) : (col, row)
        index = get(map, row_col, nothing)
        if index === nothing
            push!(structure, row_col)
            map[row_col] = length(structure)
            push!(oracle.hessian_sparsity_map, length(structure))
        else
            push!(oracle.hessian_sparsity_map, index::Int)
        end
    end
    return
end

function MOI.hessian_lagrangian_structure(oracle::_NonlinearOracle)
    structure = Tuple{Int,Int}[]
    map = Dict{Tuple{Int,Int},Int}()
    _hessian_lagrangian_structure(structure, oracle, map, oracle.objective)
    for c in oracle.constraints
        _hessian_lagrangian_structure(structure, oracle, map, c)
    end
    return structure
end

function MOI.eval_hessian_lagrangian(
    oracle::_NonlinearOracle,
    H::AbstractVector{Float64},
    x::AbstractVector{Float64},
    σ::Float64,
    μ::AbstractVector{Float64},
)
    fill!(H, 0.0)
    start = time()
    k = 1
    if oracle.objective !== nothing && !iszero(σ)
        h = _eval_hessian(oracle, oracle.objective, x)
        for (i, hi) in enumerate(h)
            @inbounds H[oracle.hessian_sparsity_map[i]] += σ * hi
        end
        k += length(h)
    end
    for (μi, constraint) in zip(μ, oracle.constraints)
        if iszero(μi)
            continue
        end
        h = _eval_hessian(oracle, constraint, x)
        for nzval in h
            @inbounds H[oracle.hessian_sparsity_map[k]] += μi * nzval
            k += 1
        end
    end
    oracle.eval_hessian_lagrangian_timer += time() - start
    return
end

function _nonlinear_constraint(expr::Expr)
    lower, upper, body = -Inf, Inf, :()
    if isexpr(expr, :call, 3)
        if expr.args[1] == :(>=)
            lower, upper, body = expr.args[1], expr.args[5], expr.args[3]
        elseif expr.args[1] == :(<=)
            lower, upper, body = -Inf, expr.args[3], expr.args[2]
        else
            @assert expr.args[1] == :(==)
            lower, upper, body = expr.args[3], expr.args[3], expr.args[2]
        end
    else
        @assert isexpr(expr, :comparison, 5)
        lower, upper, body = expr.args[1], expr.args[5], expr.args[3]
    end
    return _Function(body), MOI.NLPBoundsPair(lower, upper)
end

"""
    _nlp_block_data(
        d::MOI.AbstractNLPEvaluator;
        backend::AbstractNonlinearOracleBackend,
    )

Convert an AbstractNLPEvaluator into a SymbolicAD instance of MOI.NLPBlockData.
"""
function _nlp_block_data(
    d::MOI.AbstractNLPEvaluator;
    backend::AbstractNonlinearOracleBackend,
)
    MOI.initialize(d, [:ExprGraph])
    objective = d.has_nlobj ? _Function(MOI.objective_expr(d)) : nothing
    n = length(d.constraints)
    functions = Vector{_Function}(undef, n)
    bounds = Vector{MOI.NLPBoundsPair}(undef, n)
    for i in 1:n
        f, bound = _nonlinear_constraint(MOI.constraint_expr(d, i))
        functions[i], bounds[i] = f, bound
    end
    oracle = _NonlinearOracle(backend, objective, functions)
    return MOI.NLPBlockData(bounds, oracle, d.has_nlobj)
end

###
### ThreadedBackend
###

struct _ConstraintOffset
    index::Int
    ∇f_offset::Int
    ∇²f_offset::Int
end

struct ThreadedBackend <: AbstractNonlinearOracleBackend
    offsets::Vector{Vector{_ConstraintOffset}}
    ThreadedBackend() = new(Vector{_ConstraintOffset}[])
end

function MOI.initialize(
    oracle::_NonlinearOracle,
    backend::ThreadedBackend,
    ::Vector{Symbol},
)
    obj_hess_offset = 0
    if oracle.objective !== nothing
        h = oracle.objective.expr.hash
        obj_hess_offset = length(oracle.symbolic_functions[h].H)
    end
    offsets = Dict{UInt,Vector{_ConstraintOffset}}(
        h => _ConstraintOffset[] for h in keys(oracle.symbolic_functions)
    )
    index, ∇f_offset, ∇²f_offset = 1, 1, obj_hess_offset + 1
    for f in oracle.constraints
        push!(
            offsets[f.expr.hash],
            _ConstraintOffset(index, ∇f_offset, ∇²f_offset),
        )
        index += 1
        symbolic_function = oracle.symbolic_functions[f.expr.hash]
        ∇f_offset += length(symbolic_function.g)
        ∇²f_offset += length(symbolic_function.H)
    end
    for v in values(offsets)
        push!(backend.offsets, v)
    end
    return
end

function MOI.eval_constraint(
    oracle::_NonlinearOracle{ThreadedBackend},
    g::AbstractVector{Float64},
    x::AbstractVector{Float64},
)
    start = time()
    Threads.@threads for offsets in oracle.backend.offsets
        for c in offsets
            func = oracle.constraints[c.index]
            @inbounds g[c.index] = _eval_function(oracle, func, x)
        end
    end
    oracle.eval_constraint_timer += time() - start
    return
end

function MOI.eval_constraint_jacobian(
    oracle::_NonlinearOracle{ThreadedBackend},
    J::AbstractVector{Float64},
    x::AbstractVector{Float64},
)
    start = time()
    Threads.@threads for offset in oracle.backend.offsets
        for c in offset
            func = oracle.constraints[c.index]
            g = _eval_gradient(oracle, func, x)
            for i in 1:length(g)
                @inbounds J[c.∇f_offset+i-1] = g[i]
            end
        end
    end
    oracle.eval_constraint_jacobian_timer += time() - start
    return
end

function _hessian_lagrangian_structure(
    structure::Vector{Tuple{Int,Int}},
    oracle::_NonlinearOracle{ThreadedBackend},
    ::Dict{Tuple{Int,Int},Int},
    c::_Function,
)
    f = oracle.symbolic_functions[c.expr.hash]
    for (i, j) in f.H_structure
        row, col = c.ordered_variables[i], c.ordered_variables[j]
        push!(structure, row >= col ? (row, col) : (col, row))
    end
    return
end

function MOI.eval_hessian_lagrangian(
    oracle::_NonlinearOracle{ThreadedBackend},
    H::AbstractVector{Float64},
    x::AbstractVector{Float64},
    σ::Float64,
    μ::AbstractVector{Float64},
)
    fill!(H, 0.0)
    start = time()
    hessian_offset = 0
    if oracle.objective !== nothing && !iszero(σ)
        h = _eval_hessian(oracle, oracle.objective, x)
        for i in 1:length(h)
            H[i] = σ * h[i]
        end
        hessian_offset += length(h)
    end
    Threads.@threads for offset in oracle.backend.offsets
        for c in offset
            if iszero(μ[c.index])
                continue
            end
            func = oracle.constraints[c.index]
            h = _eval_hessian(oracle, func, x)
            for i in 1:length(h)
                @inbounds H[c.∇²f_offset+i-1] = μ[c.index] * h[i]
            end
        end
    end
    oracle.eval_hessian_lagrangian_timer += time() - start
    return
end

###
### JuMP integration
###

function _to_expr(
    nlp_data::JuMP._NLPData,
    data::JuMP._NonlinearExprData,
    subexpressions::Vector{Expr},
)
    tree = Any[]
    for node in data.nd
        expr = if node.nodetype == JuMP._Derivatives.CALL
            Expr(:call, JuMP._Derivatives.operators[node.index])
        elseif node.nodetype == JuMP._Derivatives.CALLUNIVAR
            Expr(:call, JuMP._Derivatives.univariate_operators[node.index])
        elseif node.nodetype == JuMP._Derivatives.SUBEXPRESSION
            subexpressions[node.index]
        elseif node.nodetype == JuMP._Derivatives.MOIVARIABLE
            MOI.VariableIndex(node.index)
        elseif node.nodetype == JuMP._Derivatives.PARAMETER
            nlp_data.nlparamvalues[node.index]
        else
            @assert node.nodetype == JuMP._Derivatives.VALUE
            data.const_values[node.index]
        end
        if 1 <= node.parent <= length(tree)
            push!(tree[node.parent].args, expr)
        end
        push!(tree, expr)
    end
    return tree[1]
end

_to_expr(::JuMP._NLPData, ::Nothing, ::Vector{Expr}) = nothing

function _nlp_block_data(
    model::JuMP.Model;
    backend::AbstractNonlinearOracleBackend,
)
    return _nlp_block_data(model.nlp_data; backend = backend)
end

function _nlp_block_data(
    nlp_data::JuMP._NLPData;
    backend::AbstractNonlinearOracleBackend,
)
    subexpressions = map(nlp_data.nlexpr) do expr
        return _to_expr(nlp_data, expr, Expr[])::Expr
    end
    nlobj = _to_expr(nlp_data, nlp_data.nlobj, subexpressions)
    objective = nlobj === nothing ? nothing : _Function(nlobj)
    functions = map(nlp_data.nlconstr) do c
        return _Function(_to_expr(nlp_data, c.terms, subexpressions))
    end
    return MOI.NLPBlockData(
        [MOI.NLPBoundsPair(c.lb, c.ub) for c in nlp_data.nlconstr],
        _NonlinearOracle(backend, objective, functions),
        objective !== nothing,
    )
end

function optimize_hook(
    model::JuMP.Model;
    backend::AbstractNonlinearOracleBackend = DefaultBackend(),
)
    nlp_block = _nlp_block_data(model; backend = backend)
    nlp_data = model.nlp_data
    model.nlp_data = nothing
    MOI.set(model, MOI.NLPBlock(), nlp_block)
    JuMP.optimize!(model; ignore_optimize_hook = true)
    model.nlp_data = nlp_data
    return
end


# SPDX-License-Identifier: MIT

using ArgParse
using HDF5
using SpeechFeatures
using TOML
using WAV

#######################################################################
# Default values

const FEATYPES = Dict(
    "MFCC" => (MFCC,
               ("removedc", "dithering", "srate", "frameduration",
                "framestep", "preemphasis", "windowfn", "windowpower",
                "nfilters", "lofreq", "hifreq", "addenergy", "nceps",
                "liftering")
              ),
    "FBANK" => (LogMelSpectrum,
                ("removedc", "dithering", "srate", "frameduration",
                 "framestep", "preemphasis", "windowfn", "windowpower",
                 "nfilters", "lofreq", "hifreq")
               ),
    "MAGSPEC" => (LogMagnitudeSpectrum,
                  ("removedc", "dithering", "srate", "frameduration",
                   "framestep", "preemphasis", "windowfn", "windowpower")
                 )
)

const WINDOWS = Dict(
    "hann" => SpeechFeatures.HannWindow,
    "hamming" => SpeechFeatures.HammingWindow,
    "rectangular" => SpeechFeatures.RectangularWindow
)

const DEFAULTS = Dict(
    "removedc" => true,
    "dithering" => 0,
    "srate" => 16000,
    "frameduration" => 0.025,
    "framestep" => 0.01,
    "preemphasis" => 0.97,
    "windowfn" => "hann",
    "windowpower" => 0.85,
    "nfilters" => 26,
    "lofreq" => 80,
    "hifreq" => 7600,
    "addenergy" => true,
    "nceps" => 12,
    "liftering" => 22
)

const DEFAULTS_DELTA = Dict(
    "order" => 2,
    "deltawin" => 2
)

const DEFAULTS_NORM = Dict(
    "mean_norm" => true,
)

#######################################################################

function parse_commandline()
    s = ArgParseSettings()
    @add_arg_table s begin
        "--compression-level", "-c"
            arg_type = Int
            default = 0
            help = "compression level of the HDF5 archive"
        "configfile"
            required = true
            help = "features configuration file (TOML format)"
        "scp"
            required = true
            help = "SCP file to extract the features"
        "out"
            required = true
            help = "output HDF5 archive"
    end
    parse_args(s)
end

loadpath(path) = wavread(path, format = "double")

loadpipe(pipe) = channels, srate = wavread(IOBuffer(read(cmd)))
function loadpipe(pipe)
    subcmds = [`$(split(subcmd))` for subcmd in split(pipe[1:end-1], "|")]
    cmd = pipeline(subcmds...)
    wavread(IOBuffer(read(cmd)))
end

load(path_or_pipe) =
    endswith(path_or_pipe, "|") ? loadpipe(path_or_pipe) : loadpath(path_or_pipe)

function load_scpentry(line)
    tokens = split(strip(line))
    uttid = tokens[1]
    path_or_pipe = join(tokens[2:end], " ")
    String(uttid), path_or_pipe
end

function buildextractor(conf)
    featype, options = FEATYPES[conf["featype"]]
    feaopts = Dict()
    for opt in options
        if opt == "windowfn"
            feaopts[Symbol(opt)] = WINDOWS[conf[opt]]
        else
            feaopts[Symbol(opt)] = conf[opt]
        end
    end
    feaextractor = featype(;feaopts...)

    if "DeltaCoeffs" in keys(conf)
        deltaopts = merge(DEFAULTS_DELTA, conf["DeltaCoeffs"])
        Δ_extractor = DeltaCoeffs(order = deltaopts["order"],
                                  deltawin = deltaopts["deltawin"])
        feaextractor = Δ_extractor ∘ feaextractor
    end

    if "Normalization" in keys(conf)
        normopts = merge(DEFAULTS_NORM, conf["Normalization"])
        if normopts["mean_norm"]
            feaextractor = MeanNorm() ∘ feaextractor
        end
    end

    feaextractor
end

function main(args)
    conf = merge(DEFAULTS, TOML.parsefile(args["configfile"]))
    feaextractor = buildextractor(conf)

    @debug "extracting features to $(args["out"])"

    h5open(args["out"], "w") do f

        for (key, val) in conf
            if typeof(val) <: Dict
                for (skey, sval) in val
                    attributes(f)["$key.$skey"] = sval
                end
            else
                attributes(f)[key] = val
            end
        end

        for line in readlines(args["scp"])
            @debug "processing $line"
            uttid, path_or_pipe = load_scpentry(line)
            channels, srate = load(path_or_pipe)

            @assert srate == conf["srate"]
            @assert size(channels, 2) == 1

            fea = channels[:,1] |> feaextractor
            fea = convert(Array{Float32}, fea)

            f[uttid, compress = args["compression-level"]] = fea
        end
    end
end

run(`hostname`)
println(ARGS)
args = parse_commandline()
main(args)


# custom extension of CuArray in CUDArt for sparse vectors/matrices
# using CSC format for interop with Julia's native sparse functionality

export CuSparseMatrixCSC, CuSparseMatrixCSR, CuSparseMatrixBSR, CuSparseMatrixCOO,
       CuSparseMatrix, AbstractCuSparseMatrix,
       CuSparseVector

using LinearAlgebra: BlasFloat
using SparseArrays: nonzeroinds, dimlub

abstract type AbstractCuSparseArray{Tv, Ti, N} <: AbstractSparseArray{Tv, Ti, N} end
const AbstractCuSparseVector{Tv, Ti} = AbstractCuSparseArray{Tv, Ti, 1}
const AbstractCuSparseMatrix{Tv, Ti} = AbstractCuSparseArray{Tv, Ti, 2}

Base.convert(T::Type{<:AbstractCuSparseArray}, m::AbstractArray) = m isa T ? m : T(m)

mutable struct CuSparseVector{Tv, Ti} <: AbstractCuSparseVector{Tv, Ti}
    iPtr::CuVector{Ti}
    nzVal::CuVector{Tv}
    dims::NTuple{2,Int}
    nnz::Ti

    function CuSparseVector{Tv, Ti}(iPtr::CuVector{<:Integer}, nzVal::CuVector,
                                dims::Integer) where {Tv, Ti <: Integer}
        new{Tv, Ti}(iPtr, nzVal, (dims,1), length(nzVal))
    end
end

function CUDA.unsafe_free!(xs::CuSparseVector)
    unsafe_free!(nonzeroinds(xs))
    unsafe_free!(nonzeros(xs))
    return
end

mutable struct CuSparseMatrixCSC{Tv, Ti} <: AbstractCuSparseMatrix{Tv, Ti}
    colPtr::CuVector{Ti}
    rowVal::CuVector{Ti}
    nzVal::CuVector{Tv}
    dims::NTuple{2,Int}
    nnz::Ti

    function CuSparseMatrixCSC{Tv, Ti}(colPtr::CuVector{<:Integer}, rowVal::CuVector{<:Integer},
                                   nzVal::CuVector, dims::NTuple{2,<:Integer}) where {Tv, Ti <: Integer}
        new{Tv, Ti}(colPtr, rowVal, nzVal, dims, length(nzVal))
    end
end

CuSparseMatrixCSC(A::CuSparseMatrixCSC) = A

function CUDA.unsafe_free!(xs::CuSparseMatrixCSC)
    unsafe_free!(xs.colPtr)
    unsafe_free!(rowvals(xs))
    unsafe_free!(nonzeros(xs))
    return
end

"""
    CuSparseMatrixCSR{Tv, Ti} <: AbstractCuSparseMatrix{Tv, Ti}

Container to hold sparse matrices in compressed sparse row (CSR) format on the
GPU.

!!! note
    Most CUSPARSE operations work with CSR formatted matrices, rather
    than CSC.

!!! compat "CUDA 11"
    Support of indices type rather than `Cint` (`Int32`) requires at least CUDA 11.
"""
mutable struct CuSparseMatrixCSR{Tv, Ti} <: AbstractCuSparseMatrix{Tv, Ti}
    rowPtr::CuVector{Ti}
    colVal::CuVector{Ti}
    nzVal::CuVector{Tv}
    dims::NTuple{2,Int}
    nnz::Ti

    function CuSparseMatrixCSR{Tv, Ti}(rowPtr::CuVector{<:Integer}, colVal::CuVector{<:Integer},
                                   nzVal::CuVector, dims::NTuple{2,Int}) where {Tv, Ti<:Integer}
        new{Tv, Ti}(rowPtr, colVal, nzVal, dims, length(nzVal))
    end
end

CuSparseMatrixCSR(A::CuSparseMatrixCSR) = A

function CUDA.unsafe_free!(xs::CuSparseMatrixCSR)
    unsafe_free!(xs.rowPtr)
    unsafe_free!(xs.colVal)
    unsafe_free!(nonzeros(xs))
    return
end

"""
Container to hold sparse matrices in block compressed sparse row (BSR) format on
the GPU. BSR format is also used in Intel MKL, and is suited to matrices that are
"block" sparse - rare blocks of non-sparse regions.
"""
mutable struct CuSparseMatrixBSR{Tv, Ti} <: AbstractCuSparseMatrix{Tv, Ti}
    rowPtr::CuVector{Ti}
    colVal::CuVector{Ti}
    nzVal::CuVector{Tv}
    dims::NTuple{2,Int}
    blockDim::Ti
    dir::SparseChar
    nnz::Ti

    function CuSparseMatrixBSR{Tv, Ti}(rowPtr::CuVector{<:Integer}, colVal::CuVector{<:Integer},
                                   nzVal::CuVector, dims::NTuple{2,<:Integer},
                                   blockDim::Integer, dir::SparseChar, nnz::Integer) where {Tv, Ti<:Integer}
        new{Tv, Ti}(rowPtr, colVal, nzVal, dims, blockDim, dir, nnz)
    end
end

CuSparseMatrixBSR(A::CuSparseMatrixBSR) = A

function CUDA.unsafe_free!(xs::CuSparseMatrixBSR)
    unsafe_free!(xs.rowPtr)
    unsafe_free!(xs.colVal)
    unsafe_free!(nonzeros(xs))
    return
end

"""
Container to hold sparse matrices in coordinate (COO) format on the GPU. COO
format is mainly useful to initially construct sparse matrices, afterwards
switch to [`CuSparseMatrixCSR`](@ref) for more functionality.
"""
mutable struct CuSparseMatrixCOO{Tv, Ti} <: AbstractCuSparseMatrix{Tv, Ti}
    rowInd::CuVector{Ti}
    colInd::CuVector{Ti}
    nzVal::CuVector{Tv}
    dims::NTuple{2,Int}
    nnz::Ti

    function CuSparseMatrixCOO{Tv, Ti}(rowInd::CuVector{<:Integer}, colInd::CuVector{<:Integer},
                                   nzVal::CuVector, dims::NTuple{2,Int}=(dimlub(rowInd),dimlub(colInd)),
                                   nnz::Integer=length(nzVal)) where {Tv, Ti}
        new{Tv, Ti}(rowInd,colInd,nzVal,dims,nnz)
    end
end

CuSparseMatrixCOO(A::CuSparseMatrixCOO) = A

"""
Utility union type of [`CuSparseMatrixCSC`](@ref), [`CuSparseMatrixCSR`](@ref),
[`CuSparseMatrixBSR`](@ref), [`CuSparseMatrixCOO`](@ref).
"""
const CuSparseMatrix{Tv, Ti} = Union{
    CuSparseMatrixCSC{Tv, Ti},
    CuSparseMatrixCSR{Tv, Ti},
    CuSparseMatrixBSR{Tv, Ti},
    CuSparseMatrixCOO{Tv, Ti}
}

# NOTE: we use Cint as default Ti on CUDA instead of Int to provide
# maximum compatiblity to old CUSPARSE APIs
function CuSparseVector{Tv}(iPtr::CuVector{<:Integer}, nzVal::CuVector, dims::Integer) where {Tv}
    CuSparseVector{Tv, Cint}(convert(CuVector{Cint}, iPtr), nzVal, dims)
end

function CuSparseMatrixCSC{Tv}(colPtr::CuVector{<:Integer}, rowVal::CuVector{<:Integer},
                                   nzVal::CuVector, dims::NTuple{2,<:Integer}) where {Tv}
    CuSparseMatrixCSC{Tv, Cint}(colPtr, rowVal, nzVal, dims)
end

function CuSparseMatrixCSR{Tv}(rowPtr::CuVector{<:Integer}, colVal::CuVector{<:Integer},
                                   nzVal::CuVector, dims::NTuple{2,Int}) where {Tv}
    CuSparseMatrixCSR{Tv, Cint}(rowPtr, colVal, nzVal, dims)
end

function CuSparseMatrixBSR{Tv}(rowPtr::CuVector{<:Integer}, colVal::CuVector{<:Integer},
                                   nzVal::CuVector, dims::NTuple{2,<:Integer},
                                   blockDim::Integer, dir::SparseChar, nnz::Integer) where {Tv}
    CuSparseMatrixBSR{Tv, Cint}(rowPtr, colVal, nzVal, dims, blockDim, dir, nnz)
end

function CuSparseMatrixCOO{Tv}(rowInd::CuVector{<:Integer}, colInd::CuVector{<:Integer},
                                   nzVal::CuVector, dims::NTuple{2,Int}=(dimlub(rowInd),dimlub(colInd)),
                                   nnz::Integer=length(nzVal)) where {Tv}
    CuSparseMatrixCOO{Tv, Cint}(rowInd,colInd,nzVal,dims,nnz)
end

## convenience constructors
CuSparseVector(iPtr::DenseCuArray{<:Integer}, nzVal::DenseCuArray{T}, dims::Int) where {T} =
    CuSparseVector{T}(iPtr, nzVal, dims)

CuSparseMatrixCSC(colPtr::DenseCuArray{<:Integer}, rowVal::DenseCuArray{<:Integer},
                  nzVal::DenseCuArray{T}, dims::NTuple{2,Int}) where {T} =
    CuSparseMatrixCSC{T}(colPtr, rowVal, nzVal, dims)

CuSparseMatrixCSR(rowPtr::DenseCuArray, colVal::DenseCuArray, nzVal::DenseCuArray{T}, dims::NTuple{2,Int}) where T =
    CuSparseMatrixCSR{T}(rowPtr, colVal, nzVal, dims)

CuSparseMatrixBSR(rowPtr::DenseCuArray, colVal::DenseCuArray, nzVal::DenseCuArray{T}, blockDim, dir, nnz,
                  dims::NTuple{2,Int}) where T =
    CuSparseMatrixBSR{T}(rowPtr, colVal, nzVal, dims, blockDim, dir, nnz)

Base.similar(Vec::CuSparseVector) = CuSparseVector(copy(nonzeroinds(Vec)), similar(nonzeros(Vec)), Vec.dims[1])
Base.similar(Mat::CuSparseMatrixCSC) = CuSparseMatrixCSC(copy(Mat.colPtr), copy(rowvals(Mat)), similar(nonzeros(Mat)), Mat.dims)
Base.similar(Mat::CuSparseMatrixCSR) = CuSparseMatrixCSR(copy(Mat.rowPtr), copy(Mat.colVal), similar(nonzeros(Mat)), Mat.dims)
Base.similar(Mat::CuSparseMatrixBSR) = CuSparseMatrixBSR(copy(Mat.rowPtr), copy(Mat.colVal), similar(nonzeros(Mat)), Mat.blockDim, Mat.dir, nnz(Mat), Mat.dims)

## array interface

Base.length(g::CuSparseVector) = prod(g.dims)
Base.size(g::CuSparseVector) = g.dims
Base.ndims(g::CuSparseVector) = 1

Base.length(g::CuSparseMatrix) = prod(g.dims)
Base.size(g::CuSparseMatrix) = g.dims
Base.ndims(g::CuSparseMatrix) = 2

function Base.size(g::CuSparseVector, d::Integer)
    if d == 1
        return g.dims[d]
    elseif d > 1
        return 1
    else
        throw(ArgumentError("dimension must be ≥ 1, got $d"))
    end
end

function Base.size(g::CuSparseMatrix, d::Integer)
    if d in [1, 2]
        return g.dims[d]
    elseif d > 1
        return 1
    else
        throw(ArgumentError("dimension must be ≥ 1, got $d"))
    end
end

Base.eltype(g::CuSparseMatrix{T}) where T = T

## sparse array interface

SparseArrays.nnz(g::AbstractCuSparseArray) = g.nnz
SparseArrays.nonzeros(g::AbstractCuSparseArray) = g.nzVal

SparseArrays.nonzeroinds(g::AbstractCuSparseVector) = g.iPtr

SparseArrays.rowvals(g::CuSparseMatrixCSC) = g.rowVal

LinearAlgebra.issymmetric(M::Union{CuSparseMatrixCSC,CuSparseMatrixCSR}) = false
LinearAlgebra.ishermitian(M::Union{CuSparseMatrixCSC,CuSparseMatrixCSR}) = false
LinearAlgebra.issymmetric(M::Symmetric{CuSparseMatrixCSC}) = true
LinearAlgebra.ishermitian(M::Hermitian{CuSparseMatrixCSC}) = true

LinearAlgebra.istriu(M::UpperTriangular{T,S}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix} = true
LinearAlgebra.istril(M::UpperTriangular{T,S}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix} = false
LinearAlgebra.istriu(M::LowerTriangular{T,S}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix} = false
LinearAlgebra.istril(M::LowerTriangular{T,S}) where {T<:BlasFloat, S<:AbstractCuSparseMatrix} = true

Hermitian{T}(Mat::CuSparseMatrix{T}) where T = Hermitian{T,typeof(Mat)}(Mat,'U')

## indexing

# translations
Base.getindex(A::AbstractCuSparseVector, ::Colon)          = copy(A)
Base.getindex(A::AbstractCuSparseMatrix, ::Colon, ::Colon) = copy(A)
Base.getindex(A::AbstractCuSparseMatrix, i, ::Colon)       = getindex(A, i, 1:size(A, 2))
Base.getindex(A::AbstractCuSparseMatrix, ::Colon, i)       = getindex(A, 1:size(A, 1), i)
Base.getindex(A::AbstractCuSparseMatrix, I::Tuple{Integer,Integer}) = getindex(A, I[1], I[2])

# column slices
function Base.getindex(x::CuSparseMatrixCSC, ::Colon, j::Integer)
    checkbounds(x, :, j)
    r1 = convert(Int, x.colPtr[j])
    r2 = convert(Int, x.colPtr[j+1]) - 1
    CuSparseVector(rowvals(x)[r1:r2], nonzeros(x)[r1:r2], size(x, 1))
end

function Base.getindex(x::CuSparseMatrixCSR, i::Integer, ::Colon)
    checkbounds(x, i, :)
    c1 = convert(Int, x.rowPtr[i])
    c2 = convert(Int, x.rowPtr[i+1]) - 1
    CuSparseVector(x.colVal[c1:c2], nonzeros(x)[c1:c2], size(x, 2))
end

# row slices
Base.getindex(A::CuSparseMatrixCSC, i::Integer, ::Colon) = CuSparseVector(sparse(A[i, 1:end]))  # TODO: optimize
Base.getindex(A::CuSparseMatrixCSR, ::Colon, j::Integer) = CuSparseVector(sparse(A[1:end, j]))  # TODO: optimize

function Base.getindex(A::CuSparseMatrixCSC{T}, i0::Integer, i1::Integer) where T
    m, n = size(A)
    if !(1 <= i0 <= m && 1 <= i1 <= n)
        throw(BoundsError())
    end
    r1 = Int(A.colPtr[i1])
    r2 = Int(A.colPtr[i1+1]-1)
    (r1 > r2) && return zero(T)
    r1 = searchsortedfirst(rowvals(A), i0, r1, r2, Base.Order.Forward)
    ((r1 > r2) || (rowvals(A)[r1] != i0)) ? zero(T) : nonzeros(A)[r1]
end

function Base.getindex(A::CuSparseMatrixCSR{T}, i0::Integer, i1::Integer) where T
    m, n = size(A)
    if !(1 <= i0 <= m && 1 <= i1 <= n)
        throw(BoundsError())
    end
    c1 = Int(A.rowPtr[i0])
    c2 = Int(A.rowPtr[i0+1]-1)
    (c1 > c2) && return zero(T)
    c1 = searchsortedfirst(A.colVal, i1, c1, c2, Base.Order.Forward)
    ((c1 > c2) || (A.colVal[c1] != i1)) ? zero(T) : nonzeros(A)[c1]
end

function SparseArrays._spgetindex(m::Integer, nzind::CuVector{Ti}, nzval::CuVector{Tv},
                                  i::Integer) where {Tv,Ti}
    ii = searchsortedfirst(nzind, convert(Ti, i))
    (ii <= m && nzind[ii] == i) ? nzval[ii] : zero(Tv)
end

## interop with sparse CPU arrays

# cpu to gpu
# NOTE: we eagerly convert the indices to Cint here to avoid additional conversion later on
CuSparseVector{T}(Vec::SparseVector) where {T} =
    CuSparseVector(CuVector{Cint}(Vec.nzind), CuVector{T}(Vec.nzval), length(Vec))
CuSparseVector{T}(Mat::SparseMatrixCSC) where {T} =
    size(Mat,2) == 1 ?
        CuSparseVector(CuVector{Cint}(Mat.rowval), CuVector{T}(Mat.nzval), size(Mat)[1]) :
        throw(ArgumentError("The input argument must have a single column"))
CuSparseMatrixCSC{T}(Vec::SparseVector) where {T} =
    CuSparseMatrixCSC{T}(CuVector{Cint}([1]), CuVector{Cint}(Vec.nzind),
                         CuVector{T}(Vec.nzval), size(Vec))
CuSparseMatrixCSC{T}(Mat::SparseMatrixCSC) where {T} =
    CuSparseMatrixCSC{T}(CuVector{Cint}(Mat.colptr), CuVector{Cint}(Mat.rowval),
                         CuVector{T}(Mat.nzval), size(Mat))
CuSparseMatrixCSR{T}(Mat::Transpose{Tv, <:SparseMatrixCSC}) where {T, Tv} =
    CuSparseMatrixCSR{T}(CuVector{Cint}(parent(Mat).colptr), CuVector{Cint}(parent(Mat).rowval),
                         CuVector{T}(parent(Mat).nzval), size(Mat))
CuSparseMatrixCSR{T}(Mat::Adjoint{Tv, <:SparseMatrixCSC}) where {T, Tv} =
    CuSparseMatrixCSR{T}(CuVector{Cint}(parent(Mat).colptr), CuVector{Cint}(parent(Mat).rowval),
                         CuVector{T}(conj.(parent(Mat).nzval)), size(Mat))
CuSparseMatrixCSR{T}(Mat::SparseMatrixCSC) where {T} = CuSparseMatrixCSR(CuSparseMatrixCSC{T}(Mat))
CuSparseMatrixBSR{T}(Mat::SparseMatrixCSC, blockdim) where {T} = CuSparseMatrixBSR(CuSparseMatrixCSR{T}(Mat), blockdim)
CuSparseMatrixCOO{T}(Mat::SparseMatrixCSC) where {T} = CuSparseMatrixCOO(CuSparseMatrixCSR{T}(Mat))

# untyped variants
CuSparseVector(x::AbstractSparseArray{T}) where {T} = CuSparseVector{T}(x)
CuSparseMatrixCSC(x::AbstractSparseArray{T}) where {T} = CuSparseMatrixCSC{T}(x)
CuSparseMatrixCSR(x::AbstractSparseArray{T}) where {T} = CuSparseMatrixCSR{T}(x)
CuSparseMatrixBSR(x::AbstractSparseArray{T}, blockdim) where {T} = CuSparseMatrixBSR{T}(x, blockdim)
CuSparseMatrixCOO(x::AbstractSparseArray{T}) where {T} = CuSparseMatrixCOO{T}(x)
CuSparseMatrixCSR(x::Transpose{T}) where {T} = CuSparseMatrixCSR{T}(x)
CuSparseMatrixCSR(x::Adjoint{T}) where {T} = CuSparseMatrixCSR{T}(x)
CuSparseMatrixCSC(x::Transpose{T}) where {T} = CuSparseMatrixCSC{T}(x)
CuSparseMatrixCSC(x::Adjoint{T}) where {T} = CuSparseMatrixCSC{T}(x)

# gpu to cpu
SparseVector(x::CuSparseVector) = SparseVector(length(x), Array(nonzeroinds(x)), Array(nonzeros(x)))
SparseMatrixCSC(x::CuSparseMatrixCSC) = SparseMatrixCSC(size(x)..., Array(x.colPtr), Array(rowvals(x)), Array(nonzeros(x)))
SparseMatrixCSC(x::CuSparseMatrixCSR) = SparseMatrixCSC(CuSparseMatrixCSC(x))  # no direct conversion
SparseMatrixCSC(x::CuSparseMatrixBSR) = SparseMatrixCSC(CuSparseMatrixCSR(x))  # no direct conversion
SparseMatrixCSC(x::CuSparseMatrixCOO) = SparseMatrixCSC(CuSparseMatrixCSR(x))  # no direct conversion

# collect to Array
Base.collect(x::CuSparseVector) = collect(SparseVector(x))
Base.collect(x::CuSparseMatrixCSC) = collect(SparseMatrixCSC(x))
Base.collect(x::CuSparseMatrixCSR) = collect(SparseMatrixCSC(x))
Base.collect(x::CuSparseMatrixBSR) = collect(CuSparseMatrixCSR(x))  # no direct conversion
Base.collect(x::CuSparseMatrixCOO) = collect(CuSparseMatrixCSR(x))  # no direct conversion

Adapt.adapt_storage(::Type{CuArray}, xs::SparseVector) = CuSparseVector(xs)
Adapt.adapt_storage(::Type{CuArray}, xs::SparseMatrixCSC) = CuSparseMatrixCSC(xs)
Adapt.adapt_storage(::Type{CuArray{T}}, xs::SparseVector) where {T} = CuSparseVector{T}(xs)
Adapt.adapt_storage(::Type{CuArray{T}}, xs::SparseMatrixCSC) where {T} = CuSparseMatrixCSC{T}(xs)

Adapt.adapt_storage(::CUDA.CuArrayAdaptor, xs::AbstractSparseArray) =
  adapt(CuArray, xs)
Adapt.adapt_storage(::CUDA.CuArrayAdaptor, xs::AbstractSparseArray{<:AbstractFloat}) =
  adapt(CuArray{Float32}, xs)

Adapt.adapt_storage(::Type{Array}, xs::CuSparseVector) = SparseVector(xs)
Adapt.adapt_storage(::Type{Array}, xs::CuSparseMatrixCSC) = SparseMatrixCSC(xs)

## copying between sparse GPU arrays

function Base.copyto!(dst::CuSparseVector, src::CuSparseVector)
    if dst.dims != src.dims
        throw(ArgumentError("Inconsistent Sparse Vector size"))
    end
    copyto!(nonzeroinds(dst), nonzeroinds(src))
    copyto!(nonzeros(dst), nonzeros(src))
    dst.nnz = nnz(src)
    dst
end

function Base.copyto!(dst::CuSparseMatrixCSC, src::CuSparseMatrixCSC)
    if dst.dims != src.dims
        throw(ArgumentError("Inconsistent Sparse Matrix size"))
    end
    copyto!(dst.colPtr, src.colPtr)
    copyto!(rowvals(dst), rowvals(src))
    copyto!(nonzeros(dst), nonzeros(src))
    dst.nnz = nnz(src)
    dst
end

function Base.copyto!(dst::CuSparseMatrixCSR, src::CuSparseMatrixCSR)
    if dst.dims != src.dims
        throw(ArgumentError("Inconsistent Sparse Matrix size"))
    end
    copyto!(dst.rowPtr, src.rowPtr)
    copyto!(dst.colVal, src.colVal)
    copyto!(nonzeros(dst), nonzeros(src))
    dst.nnz = nnz(src)
    dst
end

function Base.copyto!(dst::CuSparseMatrixBSR, src::CuSparseMatrixBSR)
    if dst.dims != src.dims
        throw(ArgumentError("Inconsistent Sparse Matrix size"))
    end
    copyto!(dst.rowPtr, src.rowPtr)
    copyto!(dst.colVal, src.colVal)
    copyto!(nonzeros(dst), nonzeros(src))
    dst.dir = src.dir
    dst.nnz = nnz(src)
    dst
end

function Base.copyto!(dst::CuSparseMatrixCOO, src::CuSparseMatrixCOO)
    if dst.dims != src.dims
        throw(ArgumentError("Inconsistent Sparse Matrix size"))
    end
    copyto!(dst.rowInd, src.rowInd)
    copyto!(dst.colInd, src.colInd)
    copyto!(nonzeros(dst), nonzeros(src))
    dst.nnz = nnz(src)
    dst
end

Base.copy(Vec::CuSparseVector) = copyto!(similar(Vec), Vec)
Base.copy(Mat::CuSparseMatrixCSC) = copyto!(similar(Mat), Mat)
Base.copy(Mat::CuSparseMatrixCSR) = copyto!(similar(Mat), Mat)
Base.copy(Mat::CuSparseMatrixBSR) = copyto!(similar(Mat), Mat)
Base.copy(Mat::CuSparseMatrixCOO) = copyto!(similar(Mat), Mat)

# input/output

for (gpu, cpu) in [CuSparseVector => SparseVector,
                   CuSparseMatrixCSC => SparseMatrixCSC,
                   CuSparseMatrixCSR => SparseMatrixCSC,
                   CuSparseMatrixBSR => SparseMatrixCSC,
                   CuSparseMatrixCOO => SparseMatrixCSC]
    @eval Base.show(io::IOContext, x::$gpu) =
        show(io, $cpu(x))

    @eval function Base.show(io::IO, mime::MIME"text/plain", S::$gpu)
        xnnz = nnz(S)
        m, n = size(S)
        print(io, m, "×", n, " ", typeof(S), " with ", xnnz, " stored ",
                  xnnz == 1 ? "entry" : "entries")
        if !(m == 0 || n == 0)
            println(io, ":")
            io = IOContext(io, :typeinfo => eltype(S))
            if ndims(S) == 1
                show(io, $cpu(S))
            else
                # so that we get the nice Braille pattern
                Base.print_array(io, $cpu(S))
            end
        end
    end
end

# interop with device arrays

function Adapt.adapt_structure(to::CUDA.Adaptor, x::CuSparseVector)
    return CuSparseDeviceVector(
        adapt(to, x.iPtr),
        adapt(to, x.nzVal),
        x.dims, x.nnz
    )
end

function Adapt.adapt_structure(to::CUDA.Adaptor, x::CuSparseMatrixCSR)
    return CuSparseDeviceMatrixCSR(
        adapt(to, x.rowPtr),
        adapt(to, x.colVal),
        adapt(to, x.nzVal),
        x.dims, x.nnz
    )
end

function Adapt.adapt_structure(to::CUDA.Adaptor, x::CuSparseMatrixCSC)
    return CuSparseDeviceMatrixCSC(
        adapt(to, x.colPtr),
        adapt(to, x.rowVal),
        adapt(to, x.nzVal),
        x.dims, x.nnz
    )
end

function Adapt.adapt_structure(to::CUDA.Adaptor, x::CuSparseMatrixBSR)
    return CuSparseDeviceMatrixBSR(
        adapt(to, x.rowPtr),
        adapt(to, x.colVal),
        adapt(to, x.nzVal),
        x.dims, x.blockDim,
        x.dir, x.nnz
    )
end

function Adapt.adapt_structure(to::CUDA.Adaptor, x::CuSparseMatrixCOO)
    return CuSparseDeviceMatrixCOO(
        adapt(to, x.rowInd),
        adapt(to, x.colInd),
        adapt(to, x.nzVal),
        x.dims, x.nnz
    )
end


parse_float(x) = parse(Float64, x)

function run_lblrtm(work_dir::String, ν_s::Float64, ν_e::Float64)::Tuple{Array{Float64,1},Array{Float64,1}}
    ν_s -= 25
    ν_e += 25

    @info "Entering $(pwd()) ..."
    cd(work_dir)
    @info "Entered."

    @info "Checking system links..."
    requirements = ["lblrtm", "TAPE3"]
    for link in requirements
        @assert(islink(link) || isfile(link), "$link does not exist.")
    end
    @info "System links OK."

    @info "Removing last run's files..."
    rm("TAPE5", force=true)
    rm("TAPE6", force=true)
    rm("TAPE7", force=true)
    rm("TAPE8", force=true)
    rm("TAPE10", force=true)
    @info "Removed."

    IHIRAC = 1
    ILBLF4 = 2
    ICNTNM = 1
    IAERSL = 1
    IEMIT = 1
    ISCAN = 0
    IFILTR = 0
    IPLOT = 0
    ITEST = 0
    IATM = 1
    IMRG = 0
    ILAS = 0
    IOD = 1
    IXSECT = 0
    NPTS = 80
    ISOTPL = 0
    IBRD = 0

    SAMPLE = 4
    DVSET = 0.0 # Spectral resolution for calculation
    ALFAL0 = 0.04
    AVMASS = 36.0
    DPTMIN = 0.0002
    DPTFAC = 0.001
    IFNFLG = 0
    DVOUT = 0.1 # Spectral resolution for output

    TBOUND = 300.0 # Boundary temperature
    SREMIS = 1.0 # Emissivity coefficient - Rand 0

    MODEL = 6 # U.S.Standard
    ITYPE = 2
    IBMAX = 0
    ZERO = 0
    NOPRNT = 0

    H1 = 100.0
    H2 = 0.0
    ANGLE = 180.0
    @info "Writing TAPE5..."
    open("TAPE5", "w") do tape5
        write(tape5, "\$ LBLRTM INPUT\n") # Record 1.1
        write(tape5, " HI=$IHIRAC F4=$ILBLF4 CN=$ICNTNM AE=$IAERSL EM=$IEMIT SC=$ISCAN FI=$IFILTR PL=$IPLOT TS=$ITEST AM=$IATM MG=$IMRG LA=$ILAS OD=$IOD XS=$IXSECT   00   00\n") # Record 1.2
        write(tape5, @sprintf("%10.3f%10.3f%10.3f%10.3f%10.3f%10.3f%10.3f%10.3f%5d%10.3f\n", ν_s, ν_e, SAMPLE, DVSET, ALFAL0, AVMASS, DPTMIN, DPTFAC, IFNFLG, DVOUT)) # Record 1.3
        write(tape5, @sprintf("%10.3f%10.3f\n", TBOUND, SREMIS)) # Record 1.4
        write(tape5, @sprintf("%5d%5d%5d%5d%5d\n", MODEL, ITYPE, IBMAX, ZERO, NOPRNT)) # Record 3.1
        write(tape5, @sprintf("%10.3f%10.3f%10.3f\n", H1, H2, ANGLE)) # Record 3.2
        write(tape5, "\n") # Record 3.3A
        write(tape5, "\n") # Record 4.1
        write(tape5, "-1.\n")
        write(tape5, @sprintf(
"""
\$ Transfer to ASCII plotting data
 HI=0 F4=0 CN=0 AE=0 EM=0 SC=0 FI=0 PL=1 TS=0 AM=0 MG=0 LA=0 MS=0 XS=0    0    0
# Plot title not used
%10.3f%10.3f   10.2000  100.0000    5    0   12    0     1.000 0  0    0
    0.0000    1.2000    7.0200    0.2000    4    0    1    1    0    0 0    3 27
%10.3f%10.3f   10.2000  100.0000    5    0   12    0     1.000 0  0    0
    0.0000    1.2000    7.0200    0.2000    4    0    1    1    1    0 0    3 28
-1.
""", ν_s, ν_e, ν_s, ν_e))
    end
    @info "TAPE5 OK."

    # Run LBLRTM
    @info "Running LBLRTM..."
    @time run(`./lblrtm`)
    @info "LBLRTM completed."

    # Collect result
    @info "Collecting result..."
    wavenumber = []
    brightness_temperature = []
    open("TAPE28") do io
        see_header = false
        see_data = false
        for line in eachline(io)
            if !see_header
                if findfirst("WAVENUMBER", line) ≠ nothing
                    see_header = true
                end
            elseif !see_data
                see_data = true
            else
                wb, bt = map(parse_float, split(strip(line), r"\s+"))
                append!(wavenumber, wb)
                append!(brightness_temperature, bt)
            end
        end
    end
    @info "Results collected."

    return (wavenumber, brightness_temperature)
end