# Note: we define type aliases outside of the @muladd block to avoid Revise breaking when code
# inside the @muladd block is edited. See https://github.com/trixi-framework/Trixi.jl/issues/801
# for more details.

# `DGMulti` refers to both multiple DG types (polynomial/SBP, simplices/quads/hexes) as well as
# the use of multi-dimensional operators in the solver.
const DGMulti{NDIMS, ElemType, ApproxType, SurfaceIntegral, VolumeIntegral} =
  DG{<:RefElemData{NDIMS, ElemType, ApproxType}, Mortar, SurfaceIntegral, VolumeIntegral} where {Mortar}

# Type aliases. The first parameter is `ApproxType` since it is more commonly used for dispatch.
const DGMultiWeakForm{ApproxType, ElemType} =
  DGMulti{NDIMS, ElemType, ApproxType, <:SurfaceIntegralWeakForm, <:VolumeIntegralWeakForm} where {NDIMS}

const DGMultiFluxDiff{ApproxType, ElemType} =
  DGMulti{NDIMS, ElemType, ApproxType, <:SurfaceIntegralWeakForm, <:VolumeIntegralFluxDifferencing} where {NDIMS}

const DGMultiFluxDiffSBP{ApproxType, ElemType} =
  DGMulti{NDIMS, ElemType, ApproxType, <:SurfaceIntegralWeakForm, <:VolumeIntegralFluxDifferencing} where {NDIMS, ApproxType<:Union{SBP, AbstractDerivativeOperator}}

const DGMultiSBP{ApproxType, ElemType} =
  DGMulti{NDIMS, ElemType, ApproxType, SurfaceIntegral, VolumeIntegral} where {NDIMS, ElemType, ApproxType<:Union{SBP, AbstractDerivativeOperator}, SurfaceIntegral, VolumeIntegral}

# By default, Julia/LLVM does not use fused multiply-add operations (FMAs).
# Since these FMAs can increase the performance of many numerical algorithms,
# we need to opt-in explicitly.
# See https://ranocha.de/blog/Optimizing_EC_Trixi for further details.
@muladd begin

# these are necessary for pretty printing
polydeg(dg::DGMulti) = dg.basis.N
Base.summary(io::IO, dg::DG) where {DG <: DGMulti} = print(io, "DGMulti(polydeg=$(polydeg(dg)))")
Base.real(rd::RefElemData{NDIMS, Elem, ApproxType, Nfaces, RealT}) where {NDIMS, Elem, ApproxType, Nfaces, RealT} = RealT

"""
    DGMulti(; polydeg::Integer,
              element_type::AbstractElemShape,
              approximation_type=Polynomial(),
              surface_flux=flux_central,
              surface_integral=SurfaceIntegralWeakForm(surface_flux),
              volume_integral=VolumeIntegralWeakForm(),
              RefElemData_kwargs...)

Create a discontinuous Galerkin method which uses
- approximations of polynomial degree `polydeg`
- element type `element_type` (`Tri()`, `Quad()`, `Tet()`, and `Hex()` currently supported)

Optional:
- `approximation_type` (default is `Polynomial()`; `SBP()` also supported for `Tri()`, `Quad()`,
  and `Hex()` element types).
- `RefElemData_kwargs` are additional keyword arguments for `RefElemData`, such as `quad_rule_vol`.
  For more info, see the [StartUpDG.jl docs](https://jlchan.github.io/StartUpDG.jl/dev/).
"""
function DGMulti(; polydeg=nothing,
                   element_type::AbstractElemShape,
                   approximation_type=Polynomial(),
                   surface_flux=flux_central,
                   surface_integral=SurfaceIntegralWeakForm(surface_flux),
                   volume_integral=VolumeIntegralWeakForm(),
                   kwargs...)

  # call dispatchable constructor
  DGMulti(element_type, approximation_type, volume_integral, surface_integral;
          polydeg=polydeg, kwargs...)
end

# dispatchable constructor for DGMulti to allow for specialization
function DGMulti(element_type::AbstractElemShape,
                 approximation_type,
                 volume_integral,
                 surface_integral;
                 polydeg::Integer,
                 kwargs...)

  rd = RefElemData(element_type, approximation_type, polydeg; kwargs...)
  return DG(rd, nothing #= mortar =#, surface_integral, volume_integral)
end

# now that DGMulti is defined, we can define constructors for VertexMappedMesh which use dg::DGMulti
"""
    VertexMappedMesh(vertex_coordinates, EToV, dg::DGMulti;
                     is_on_boundary = nothing,
                     is_periodic::NTuple{NDIMS, Bool} = ntuple(_->false, NDIMS)) where {NDIMS, Tv}

Constructor which uses `dg::DGMulti` instead of `rd::RefElemData`.
"""
VertexMappedMesh(vertex_coordinates, EToV, dg::DGMulti; kwargs...) =
  VertexMappedMesh(vertex_coordinates, EToV, dg.basis; kwargs...)

"""
    VertexMappedMesh(triangulateIO, dg::DGMulti, boundary_dict::Dict{Symbol, Int})

Constructor which uses `dg::DGMulti` instead of `rd::RefElemData`.
"""
VertexMappedMesh(triangulateIO, dg::DGMulti, boundary_dict::Dict{Symbol, Int}) =
  VertexMappedMesh(triangulateIO, dg.basis, boundary_dict)

# Todo: DGMulti. Add traits for dispatch on affine/curved meshes here.

# Matrix type for lazy construction of physical differentiation matrices
# Constructs a lazy linear combination of B = ∑_i coeffs[i] * A[i]
struct LazyMatrixLinearCombo{Tcoeffs, N, Tv, TA <: AbstractMatrix{Tv}} <: AbstractMatrix{Tv}
  matrices::NTuple{N, TA}
  coeffs::NTuple{N, Tcoeffs}
  function LazyMatrixLinearCombo(matrices, coeffs)
    @assert all(matrix -> size(matrix) == size(first(matrices)), matrices)
    new{typeof(first(coeffs)), length(matrices), eltype(first(matrices)), typeof(first(matrices))}(matrices, coeffs)
  end
end
Base.eltype(A::LazyMatrixLinearCombo) = eltype(first(A.matrices))
Base.IndexStyle(A::LazyMatrixLinearCombo) = IndexCartesian()
Base.size(A::LazyMatrixLinearCombo) = size(first(A.matrices))

@inline function Base.getindex(A::LazyMatrixLinearCombo{<:Real, N}, i, j) where {N}
  val = zero(eltype(A))
  for k in Base.OneTo(N)
    val = val + A.coeffs[k] * getindex(A.matrices[k], i, j)
  end
  return val
end

# `SimpleKronecker` lazily stores a Kronecker product `kron(ntuple(A, NDIMS)...)`.
# This object also allocates some temporary storage to enable the fast computation
# of matrix-vector products.
struct SimpleKronecker{NDIMS, TA, Ttmp}
  A::TA
  tmp_storage::Ttmp # temporary array used for Kronecker multiplication
end

# constructor for SimpleKronecker which requires specifying only `NDIMS` and
# the 1D matrix `A`.
function SimpleKronecker(NDIMS, A, eltype_A=eltype(A))
  @assert size(A, 1) == size(A, 2) # check if square
  tmp_storage=[zeros(eltype_A, ntuple(_ -> size(A, 2), NDIMS)...) for _ in 1:Threads.nthreads()]
  return SimpleKronecker{NDIMS, typeof(A), typeof(tmp_storage)}(A, tmp_storage)
end

# Computes `b = kron(A, A) * x` in an optimized fashion
function LinearAlgebra.mul!(b_in, A_kronecker::SimpleKronecker{2}, x_in)

  @unpack A = A_kronecker
  tmp_storage = A_kronecker.tmp_storage[Threads.threadid()]
  n = size(A, 2)

  # copy `x_in` to `tmp_storage` to avoid mutating the input
  @assert length(tmp_storage) == length(x_in)
  for i in eachindex(tmp_storage)
    tmp_storage[i] = x_in[i]
  end
  x = reshape(tmp_storage, n, n)
  b = reshape(b_in, n, n)

  @turbo for j in 1:n, i in 1:n
    tmp = zero(eltype(x))
    for ii in 1:n
      tmp = tmp + A[i, ii] * x[ii, j]
    end
    b[i, j] = tmp
  end

  @turbo for j in 1:n, i in 1:n
    tmp = zero(eltype(x))
    for jj in 1:n
      tmp = tmp + A[j, jj] * b[i, jj]
    end
    x[i, j] = tmp
  end

  @turbo for i in eachindex(b_in)
    b_in[i] = x[i]
  end

  return nothing
end

# Computes `b = kron(A, A, A) * x` in an optimized fashion
function LinearAlgebra.mul!(b_in, A_kronecker::SimpleKronecker{3}, x_in)

  @unpack A = A_kronecker
  tmp_storage = A_kronecker.tmp_storage[Threads.threadid()]
  n = size(A, 2)

  # copy `x_in` to `tmp_storage` to avoid mutating the input
  for i in eachindex(tmp_storage)
    tmp_storage[i] = x_in[i]
  end
  x = reshape(tmp_storage, n, n, n)
  b = reshape(b_in, n, n, n)

  @turbo for k in 1:n, j in 1:n, i in 1:n
    tmp = zero(eltype(x))
    for ii in 1:n
      tmp = tmp + A[i, ii] * x[ii, j, k]
    end
    b[i, j, k] = tmp
  end

  @turbo for k in 1:n, j in 1:n, i in 1:n
    tmp = zero(eltype(x))
    for jj in 1:n
      tmp = tmp + A[j, jj] * b[i, jj, k]
    end
    x[i, j, k] = tmp
  end

  @turbo for k in 1:n, j in 1:n, i in 1:n
    tmp = zero(eltype(x))
    for kk in 1:n
      tmp = tmp + A[k, kk] * x[i, j, kk]
    end
    b[i, j, k] = tmp
  end

  return nothing
end

end # @muladd


# MIT License

# Copyright (c) Microsoft Corporation.

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE

"""
    Plane{T,N} <: ParametricSurface{T,N}

Infinite planar surface where the positive normal side is outside the surface.

By default this will not create any geometry for visualization, the optional `vishalfsizeu` and `vishalfsizev` arguments can be used to draw the plane as a rectangle for visualization **note that this does not fully represent the surface**.
In this case, the rotation of the rectangle around the normal to the plane is defined by `visvec` - `surfacenormal×visvec` is taken as the vector along the u axis.

```julia
Plane(surfacenormal::SVector{N,T}, pointonplane::SVector{N,T}; interface::NullOrFresnel{T} = nullinterface(T), vishalfsizeu::T = 0.0, vishalfsizev::T = 0.0, visvec::SVector{N,T} = [0.0, 1.0, 0.0])
Plane(nx::T, ny::T, nz::T, x::T, y::T, z::T; interface::NullOrFresnel{T} = nullinterface(T), vishalfsizeu::T = 0.0, vishalfsizev::T = 0.0, visvec::SVector{N,T} = [0.0, 1.0, 0.0])
```
"""
struct Plane{T,N} <: ParametricSurface{T,N}
    normal::SVector{N,T}
    d::T
    pointonplane::SVector{N,T}
    interface::NullOrFresnel{T}
    # below only for visualization purposes
    vishalfsizeu::T
    vishalfsizev::T
    visuvec::SVector{N,T}
    visvvec::SVector{N,T}

    function Plane(surfacenormal::AbstractArray{T,1}, pointonplane::AbstractArray{T,1}; interface::NullOrFresnel{T} = NullInterface(T), vishalfsizeu::T = zero(T), vishalfsizev::T = zero(T), visvec::AbstractArray{T,1} = [0.0, 1.0, 0.0]) where {T}
        @assert length(surfacenormal) == length(pointonplane) == length(visvec)
        N = length(surfacenormal)
        return Plane(SVector{N,T}(surfacenormal), SVector{N,T}(pointonplane), interface = interface, vishalfsizeu = vishalfsizeu, vishalfsizev = vishalfsizev, visvec = SVector{N,T}(visvec))
    end

    function Plane(surfacenormal::SVector{N,T}, pointonplane::SVector{N,T}; interface::NullOrFresnel{T} = NullInterface(T), vishalfsizeu::T = zero(T), vishalfsizev::T = zero(T), visvec::SVector{N,T} = SVector{3,T}(0.0, 1.0, 0.0)) where {T<:Real,N}
        norml = normalize(surfacenormal)
        d = dot(norml, pointonplane)
        if abs(dot(visvec, norml)) == one(T)
            visvec = SVector{3,T}(1.0, 0.0, 0.0)
        end
        uvec = normalize(cross(normalize(visvec), norml))
        vvec = normalize(cross(norml, uvec))
        return new{T,N}(norml, d, pointonplane, interface, vishalfsizeu, vishalfsizev, uvec, vvec)
    end

    function Plane(nx::T, ny::T, nz::T, x::T, y::T, z::T; interface::NullOrFresnel{T} = NullInterface(T), vishalfsizeu::T = zero(T), vishalfsizev::T = zero(T), visvec = SVector{3,T}(0.0, 1.0, 0.0)) where {T<:Real}
        return Plane(SVector{3,T}(nx, ny, nz), SVector{3,T}(x, y, z), interface = interface, vishalfsizeu = vishalfsizeu, vishalfsizev = vishalfsizev, visvec = visvec)
    end
end
export Plane

Base.show(io::IO, a::Plane{T}) where {T<:Real} = print(io, "Plane{$T}($(a.pointonplane), $(normal(a)), $(interface(a)))")

interface(a::Plane{T,N}) where {T<:Real,N} = a.interface
normal(pln::Plane{T,N}) where {T<:Real,N} = pln.normal

inside(pln::Plane{T,3}, p::SVector{3,T}) where {T<:Real} = dot(normal(pln), p) - pln.d < zero(T)

onsurface(pln::Plane{T,3}, p::SVector{3,T}) where {T<:Real} = samepoint(dot(normal(pln), p), pln.d)

distancefromplane(p::Plane{T,N}, point::SVector{N,T}) where {N,T<:Real} = dot(normal(p), point) - p.d

uvrange(::Type{Plane{T,N}}) where {T<:Real,N} = ((-one(T), one(T)), (-one(T), one(T)))
point(p::Plane{T}, u::T, v::T) where {T<:Real} = p.pointonplane + p.vishalfsizeu * u * p.visuvec + p.vishalfsizev * v * p.visvvec
normal(p::Plane{T}, ::T, ::T) where {T<:Real} = normal(p)

function surfaceintersection(pln::Plane{T,N}, r::AbstractRay{T,N}) where {T<:Real,N}
    n̂ = normal(pln)
    d = direction(r)
    o = origin(r)
    nd = dot(n̂, d)
    if samepoint(nd, zero(T))
        # ray and plane are parallel
        if inside(pln, o) || onsurface(pln, o)
            return rayorigininterval(Infinity(T))
        else
            # no intersection only if the ray is strictly outside of the palne
            return EmptyInterval(T)
        end
    end
    t = (pln.d - dot(n̂, o)) / nd
    if t < zero(T)
        if inside(pln, o)
            # if the ray starts 'inside' the surface then we want to return a ray so intersection works
            return rayorigininterval(Infinity(T))
        else
            return EmptyInterval(T) # no ray plane intersection
        end
    end
    temp = Intersection(t, point(r, t), n̂, zero(T), zero(T), interface(pln))
    if nd < zero(T)
        return positivehalfspace(temp)
    else
        return rayorigininterval(temp)
    end
end

function BoundingBox(pln::Plane{T,3}) where {T<:Real}
    # TODO! this is far from ideal, we should try and do something better for intersection with non-axis-algined planes
    # valid for axis aligned planes, otherwise we have to assume an infinite bounding box
    if normal(pln) === SVector{3,T}(0, 0, 1)
        return BoundingBox(typemin(T), typemax(T), typemin(T), typemax(T), typemin(T), pln.pointonplane[3])
    elseif normal(pln) === SVector{3,T}(0, 0, -1)
        return BoundingBox(typemin(T), typemax(T), typemin(T), typemax(T), pln.pointonplane[3], typemax(T))
    elseif normal(pln) === SVector{3,T}(0, 1, 0)
        return BoundingBox(typemin(T), typemax(T), typemin(T), pln.pointonplane[2], typemin(T), typemax(T))
    elseif normal(pln) === SVector{3,T}(0, -1, 0)
        return BoundingBox(typemin(T), typemax(T), pln.pointonplane[2], typemax(T), typemin(T), typemax(T))
    elseif normal(pln) === SVector{3,T}(1, 0, 0)
        return BoundingBox(typemin(T), pln.pointonplane[1], typemin(T), typemax(T), typemin(T), typemax(T))
    elseif normal(pln) === SVector{3,T}(-1, 0, 0)
        return BoundingBox(pln.pointonplane[1], typemax(T), typemin(T), typemax(T), typemin(T), typemax(T))
    else
        return BoundingBox(typemin(T), typemax(T), typemin(T), typemax(T), typemin(T), typemax(T))
    end
end


not_implemented_for(x) = error("Not implemented for $(x). You might want to put:  `using Makie` into your code!")

#TODO only have one?
const Theme = Attributes

Theme(x::AbstractPlot) = x.attributes

default_theme(scene, T) = Attributes()

function default_theme(scene)
    light = Vec3f0[Vec3f0(1.0,1.0,1.0), Vec3f0(0.1,0.1,0.1), Vec3f0(0.9,0.9,0.9), Vec3f0(20,20,20)]
    Theme(
        color = theme(scene, :color),
        visible = theme(scene, :visible),
        linewidth = 1,
        light = light,
        transformation = automatic,
        model = automatic,
        alpha = 1.0,
        transparency = false,
        overdraw = false,
    )
end

#this defines which attributes in a theme should be removed if another attribute is defined by the user,
#to avoid conflicts later through the pipeline

mutual_exclusive_attributes(::Type{<:AbstractPlot}) = Dict()
"""
    `image(x, y, image)` / `image(image)`

Plots an image on range `x, y` (defaults to dimensions).
"""
@recipe(Image) do scene
    Theme(;
        default_theme(scene)...,
        colormap = [RGBAf0(0,0,0,1), RGBAf0(1,1,1,1)],
        colorrange = automatic,
        fxaa = false,
    )
end

# could be implemented via image, but might be optimized specifically by the backend
"""
    `heatmap(x, y, values)` or `heatmap(values)`

Plots a heatmap as an image on `x, y` (defaults to interpretation as dimensions).
"""
@recipe(Heatmap) do scene
    Theme(;
        default_theme(scene)...,
        colormap = theme(scene, :colormap),
        colorrange = automatic,
        linewidth = 0.0,
        levels = 1,
        fxaa = true,
        interpolate = false
    )
end

"""
    `volume(volume_data)`

Plots a volume. Available algorithms are:
* `:iso` => IsoValue
* `:absorption` => Absorption
* `:mip` => MaximumIntensityProjection
* `:absorptionrgba` => AbsorptionRGBA
* `:indexedabsorption` => IndexedAbsorptionRGBA
"""
@recipe(Volume) do scene
    Theme(;
        default_theme(scene)...,
        fxaa = true,
        algorithm = :iso,
        absorption = 1f0,
        isovalue = 0.5f0,
        isorange = 0.05f0,
        colormap = theme(scene, :colormap),
        colorrange = (0, 1),
        color = nothing,
    )
end
mutual_exclusive_attributes(::Type{<:Volume}) =
    Dict(:colorrange => :color,
         :colormap   => :color,
         )

"""
    `surface(x, y, z)`

Plots a surface, where `(x, y, z)` are supposed to lie on a grid.
"""
@recipe(Surface) do scene
    Theme(;
        default_theme(scene)...,
        colormap = theme(scene, :colormap),
        colorrange = automatic,
        shading = true,
        fxaa = true,
    )
end

"""
    `lines(x, y, z)` / `lines(x, y)` / or `lines(positions)`

Creates a connected line plot for each element in `(x, y, z)`, `(x, y)` or `positions`.
"""
@recipe(Lines) do scene
    Theme(;
        default_theme(scene)...,
        linewidth = 1.0,
        colormap = theme(scene, :colormap),
        colorrange = automatic,
        linestyle = theme(scene, :linestyle),
        fxaa = false
    )
end

"""
    `linesegments(x, y, z)` / `linesegments(x, y)` / `linesegments(positions)`

Plots a line for each pair of points in `(x, y, z)`, `(x, y)`, or `positions`.

**Attributes**:
The same as for [`lines`](@ref)
"""
@recipe(LineSegments) do scene
    default_theme(scene, Lines)
end

# alternatively, mesh3d? Or having only mesh instead of poly + mesh and figure out 2d/3d via dispatch
"""
    `mesh(x, y, z)`, `mesh(mesh_object)`, `mesh(x, y, z, faces)`, or `mesh(xyz, faces)`

Plots a 3D mesh.
"""
@recipe(Mesh) do scene
    Theme(;
        default_theme(scene)...,
        fxaa = true,
        interpolate = false,
        shading = true,
        colormap = theme(scene, :colormap),
        colorrange = automatic,
    )
end

"""
    `scatter(x, y, z)` / `scatter(x, y)` / `scatter(positions)`

Plots a marker for each element in `(x, y, z)`, `(x, y)`, or `positions`.
"""
@recipe(Scatter) do scene
    Theme(;
        default_theme(scene)...,
        marker = theme(scene, :marker),
        markersize = theme(scene, :markersize),
        strokecolor = RGBA(0, 0, 0, 0),
        strokewidth = 0.0,
        glowcolor = RGBA(0, 0, 0, 0),
        glowwidth = 0.0,
        rotations = Billboard(),
        colormap = theme(scene, :colormap),
        colorrange = automatic,
        marker_offset = automatic,
        fxaa = false,
        transform_marker = false, # Applies the plots transformation to marker
        uv_offset_width = Vec4f0(0),
        distancefield = nothing,
    )
end

"""
    `meshscatter(x, y, z)` / `meshscatter(x, y)` / `meshscatter(positions)`

Plots a mesh for each element in `(x, y, z)`, `(x, y)`, or `positions` (similar to `scatter`).
`markersize` is a scaling applied to the primitive passed as `marker`
"""
@recipe(MeshScatter) do scene
    Theme(;
        default_theme(scene)...,
        marker = Sphere(Point3f0(0), 1f0),
        markersize = theme(scene, :markersize),
        rotations = Quaternionf0(0, 0, 0, 1),
        colormap = theme(scene, :colormap),
        colorrange = automatic,
        fxaa = true,
        shading = true
    )
end

"""
    `text(string)`

Plots a text.
"""
@recipe(Text) do scene
    Theme(;
        default_theme(scene)...,
        strokecolor = (:black, 0.0),
        strokewidth = 0,
        font = theme(scene, :font),
        align = (:left, :bottom),
        rotation = 0.0,
        textsize = 20,
        position = Point2f0(0),
    )
end

const atomic_function_symbols = (
        :text, :meshscatter, :scatter, :mesh, :linesegments,
        :lines, :surface, :volume, :heatmap, :image
)

const atomic_functions = getfield.(Ref(AbstractPlotting), atomic_function_symbols)
const Atomic{Arg} = Union{map(x-> Combined{x, Arg}, atomic_functions)...}

function color_and_colormap!(plot, intensity = plot[:color])
    if isa(intensity[], AbstractArray{<: Number})
        haskey(plot, :colormap) || error("Plot $T needs to have a colormap to allow the attribute color to be an array of numbers")
        replace_automatic!(plot, :colorrange) do
            lift(extrema_nan, intensity)
        end
        true
    else
        delete!(plot, :colorrange)
        false
    end
end

"""
    `calculated_attributes!(plot::AbstractPlot)`

Fill in values that can only be calculated when we have all other attributes filled
"""
calculated_attributes!(plot::T) where T = calculated_attributes!(T, plot)

"""
    `calculated_attributes!(trait::Type{<: AbstractPlot}, plot)`
trait version of calculated_attributes
"""
calculated_attributes!(trait, plot) = nothing

function calculated_attributes!(::Type{<: Mesh}, plot)
    need_cmap = color_and_colormap!(plot)
    need_cmap || delete!(plot, :colormap)
    return
end

function calculated_attributes!(::Type{<: Union{Heatmap, Image}}, plot)
    plot[:color] = plot[3]
    color_and_colormap!(plot)
end
function calculated_attributes!(::Type{<: Surface}, plot)
    colors = plot[3]
    if haskey(plot, :color)
        color = plot[:color][]
        if isa(color, AbstractMatrix{<: Number}) && !(color === to_value(colors))
            colors = plot[:color]
        end
    end
    color_and_colormap!(plot, colors)
end
function calculated_attributes!(::Type{<: MeshScatter}, plot)
    color_and_colormap!(plot)
end

function calculated_attributes!(::Type{<: Scatter}, plot)
    # calculate base case
    color_and_colormap!(plot)
    replace_automatic!(plot, :marker_offset) do
        # default to middle
        lift(x-> Vec2f0.((x .* (-0.5f0))), plot[:markersize])
    end
end

function calculated_attributes!(::Type{<: Union{Lines, LineSegments}}, plot)
    color_and_colormap!(plot)
    pos = plot[1][]
    # extend one color per linesegment to be one (the same) color per vertex
    # taken from @edljk  in PR #77
    if haskey(plot, :color) && isa(plot[:color][], AbstractVector) && iseven(length(pos)) && (length(pos) ÷ 2) == length(plot[:color][])
        plot[:color] = lift(plot[:color]) do cols
            map(i-> cols[(i + 1) ÷ 2], 1:(length(cols) * 2))
        end
    end
end

# # to allow one color per edge
# function calculated_attributes!(plot::LineSegments)
#     plot[:color] = lift(plot[:color], plot[1]) do c, p
#         if (length(p) ÷ 2) == length(c)
#             [c[k] for k in 1:length(c), l in 1:2]
#         else
#             c
#         end
#     end
# end

function (PT::Type{<: Combined})(parent, transformation, attributes, input_args, converted)
    PT(parent, transformation, attributes, input_args, converted, AbstractPlot[])
end
plotsym(::Type{<:AbstractPlot{F}}) where F = Symbol(typeof(F).name.mt.name)

"""
    used_attributes(args...) = ()

function used to indicate what keyword args one wants to get passed in `convert_arguments`.
Usage:
```example
    struct MyType end
    used_attributes(::MyType) = (:attribute,)
    function convert_arguments(x::MyType; attribute = 1)
        ...
    end
    # attribute will get passed to convert_arguments
    # without keyword_verload, this wouldn't happen
    plot(MyType, attribute = 2)
    #You can also use the convenience macro, to overload convert_arguments in one step:
    @keywords convert_argumetns(x::MyType; attribute = 1)
        ...
    end
```
"""
used_attributes(PlotType, args...) = ()

"""
apply for return type
    (args...,)
"""
function apply_convert!(P, attributes::Attributes, x::Tuple)
    return (plottype(P, x...), x)
end

"""
apply for return type PlotSpec
"""
function apply_convert!(P, attributes::Attributes, x::PlotSpec{S}) where S
    args, kwargs = x.args, x.kwargs
    # Note that kw_args in the plot spec that are not part of the target plot type
    # will end in the "global plot" kw_args (rest)
    for (k, v) in pairs(kwargs)
        attributes[k] = v
    end
    return (plottype(P, S), args)
end

function seperate_tuple(args::Node{<: NTuple{N, Any}}) where N
    ntuple(N) do i
        lift(args) do x
            if i <= length(x)
                x[i]
            else
                error("You changed the number of arguments. This isn't allowed!")
            end
        end
    end
end

function (PlotType::Type{<: AbstractPlot{Typ}})(scene::SceneLike, attributes::Attributes, args) where Typ
    input = to_node.(args)
    argnodes = lift(input...) do args...
        convert_arguments(PlotType, args...)
    end
    PlotType(scene, attributes, input, argnodes)
end

function (PlotType::Type{<: AbstractPlot{Typ}})(scene::SceneLike, attributes::Attributes, input, args) where Typ
    # The argument type of the final plot object is the assumened to stay constant after
    # argument conversion. This might not always hold, but it simplifies
    # things quite a bit
    ArgTyp = typeof(to_value(args))
    # construct the fully qualified plot type, from the possible incomplete (abstract)
    # PlotType
    FinalType = Combined{Typ, ArgTyp}
    plot_attributes, scene_attributes = merged_get!(()-> default_theme(scene, FinalType), plotsym(FinalType), scene, attributes)
    trans = get(plot_attributes, :transformation, automatic)
    transformation = if to_value(trans) == automatic
        Transformation(scene)
    elseif isa(to_value(trans), Transformation)
        to_value(trans)
    else
        t = Transformation(scene)
        transform!(t, to_value(trans))
        t
    end

    replace_automatic!(plot_attributes, :model) do
        transformation.model
    end
    # create the plot, with the full attributes, the input signals, and the final signal nodes.
    plot_obj = FinalType(scene, transformation, plot_attributes, input, seperate_tuple(args))
    calculated_attributes!(plot_obj)
    plot_obj, scene_attributes
end

"""
    `plot_type(plot_args...)`

The default plot type for any argument is `lines`.
Any custom argument combination that has only one meaningful way to be plotted should overload this.
e.g.:
```example
    # make plot(rand(5, 5, 5)) plot as a volume
    plottype(x::Array{<: AbstractFlot, 3}) = Volume
```
"""
plottype(plot_args...) = Combined{Any, Tuple{typeof.(to_value.(plot_args))...}} # default to dispatch to type recipes!
plottype(::AbstractVector, ::AbstractVector) = Lines
plottype(::AbstractMatrix) = Heatmap
# If the Combined has no plot func, calculate them
plottype(::Type{<: Combined{Any}}, argvalues...) = plottype(argvalues...)
# If it has something more concrete than Any, use it directly
plottype(P::Type{<: Combined{T}}, argvalues...) where T = P

"""
    plottype(P1::Type{<: Combined{T1}}, P2::Type{<: Combined{T2}})

Chooses the more concrete plot type
```example
function convert_arguments(P::PlotFunc, args...)
    ptype = plottype(P, Lines)
    ...
end
"""
plottype(P1::Type{<: Combined{Any}}, P2::Type{<: Combined{T}}) where T = P2
plottype(P1::Type{<: Combined{T}}, P2::Type{<: Combined}) where T = P1

"""
Returns the Combined type that represents the signature of `args`.
"""
function Plot(args::Vararg{Any, N}) where N
    Combined{Any, <: Tuple{args...}}
end
Base.@pure function Plot(::Type{T}) where T
    Combined{Any, <: Tuple{T}}
end
Base.@pure function Plot(::Type{T1}, ::Type{T2}) where {T1, T2}
    Combined{Any, <: Tuple{T1, T2}}
end

# all the plotting functions that get a plot type
const PlotFunc = Union{Type{Any}, Type{<: AbstractPlot}}

plot(P::PlotFunc, args...; kw_attributes...) = plot!(Scene(), P, Attributes(kw_attributes), args...)
plot!(P::PlotFunc, args...; kw_attributes...) = plot!(current_scene(), P, Attributes(kw_attributes), args...)
plot(scene::SceneLike, P::PlotFunc, args...; kw_attributes...) = plot!(Scene(scene), P, Attributes(kw_attributes), args...)
plot!(scene::SceneLike, P::PlotFunc, args...; kw_attributes...) = plot!(scene, P, Attributes(kw_attributes), args...)

plot(scene::SceneLike, P::PlotFunc, attributes::Attributes, args...; kw_attributes...) = plot!(Scene(scene), P, merge!(Attributes(kw_attributes), attributes), args...)
plot!(P::PlotFunc, attributes::Attributes, args...; kw_attributes...) = plot!(current_scene(), P, merge!(Attributes(kw_attributes), attributes), args...)
plot(P::PlotFunc, attributes::Attributes, args...; kw_attributes...) = plot!(Scene(), P, merge!(Attributes(kw_attributes), attributes), args...)

# Overload remaining functions
eval(default_plot_signatures(:plot, :plot!, :Any))

# plots to scene

plotfunc(::Combined{F}) where F = F

"""
Main plotting signatures that plot/plot! route to if no Plot Type is given
"""
function plot!(scene::SceneLike, P::PlotFunc, attributes::Attributes, args...; kw_attributes...)
    attributes = merge!(Attributes(kw_attributes), attributes)
    argvalues = to_value.(args)
    PreType = plottype(P, argvalues...)
    # plottype will lose the argument types, so we just extract the plot func
    # type and recreate the type with the argument type
    PreType = Combined{plotfunc(PreType), typeof(argvalues)}
    convert_keys = intersect(used_attributes(PreType, argvalues...), keys(attributes))
    kw_signal = if isempty(convert_keys) # lift(f) isn't supported so we need to catch the empty case
        Node(())
    else
        lift((args...)-> Pair.(convert_keys, args), getindex.(attributes, convert_keys)...) # make them one tuple to easier pass through
    end
    # call convert_arguments for a first time to get things started
    converted = convert_arguments(PreType, argvalues...; kw_signal[]...)
    # convert_arguments can return different things depending on the recipe type
    # apply_conversion deals with that!

    FinalType, argsconverted = apply_convert!(PreType, attributes, converted)
    converted_node = Node(argsconverted)
    input_nodes =  to_node.(args)
    onany(kw_signal, lift(tuple, input_nodes...)) do kwargs, args
        # do the argument conversion inside a lift
        result = convert_arguments(FinalType, args...; kwargs...)
        finaltype, argsconverted = apply_convert!(FinalType, attributes, result)
        if finaltype != FinalType
            error("Plot type changed from $FinalType to $finaltype after conversion.
                Changing the plot type based on values in convert_arguments is not allowed"
            )
        end
        converted_node[] = argsconverted
    end
    plot!(scene, FinalType, attributes, input_nodes, converted_node)
end

plot!(p::Combined) = _plot!(p)

_plot!(p::Atomic{T}) where T = p

function _plot!(p::Combined{Any, T}) where T
    args = (T.parameters...,)
    typed_args = join(string.("::", args), ", ")
    error("Plotting for the arguments ($typed_args) not defined. If you want to support those arguments, overload plot!(plot::Plot$((args...,)))")
end
function _plot!(p::Combined{X, T}) where {X, T}
    args = (T.parameters...,)
    typed_args = join(string.("::", args), ", ")
    error("Plotting for the arguments ($typed_args) not defined for $X. If you want to support those arguments, overload plot!(plot::$X{ <: $T})")
end

function plot!(scene::SceneLike, ::Type{PlotType}, attributes::Attributes, input::NTuple{N, Node}, args::Node) where {N, PlotType <: AbstractPlot}
    # create "empty" plot type - empty meaning containing no plots, just attributes + arguments
    plot_object, scene_attributes = PlotType(scene, attributes, input, args)

    nattributes, rest = merge_attributes!(scene_attributes, theme(scene))

    # TODO warn about rest - should be unused arguments!
    empty!(scene.attributes)
    # transfer the merged attributes from theme and user defined to the scene
    merge!(scene.attributes, nattributes)
    for (at1, at2) in mutual_exclusive_attributes(PlotType)
        #nothing here to get around defaults in GLVisualize
        haskey(attributes, at1) && haskey(attributes, at2) && error("$at1 conflicts with $at2, please specify only one.")
        if haskey(attributes, at1) && haskey(plot_object.attributes, at2)
            plot_object.attributes[at2] = nothing
        elseif haskey(attributes, at2) && haskey(plot_object.attributes, at1)
            plot_object.attributes[at1] = nothing
        end
    end
    # call user defined recipe overload to fill the plot type
    plot!(plot_object)

    push!(scene.plots, plot_object)

    scene[:raw][] || update_limits!(scene)
    (!scene[:raw][] || scene[:camera][] != automatic) && setup_camera!(scene)
    scene[:raw][] || add_axis!(scene, rest)
    # ! ∘ isaxis --> (x)-> !isaxis(x)
    # move axis to front, so that scene[end] gives back the last plot and not the axis!
    if !isempty(scene.plots) && isaxis(last(scene.plots))
        axis = pop!(scene.plots)
        pushfirst!(scene.plots, axis)
    end
    # call the assembly recipe, that also adds this to the scene
    # kw_args not consumed by PlotType will be passed forward to plot! as non_plot_kwargs
    #plot!(scene, plot_object, scene_attributes)
    scene
end

function plot!(scene::Combined, ::Type{PlotType}, attributes::Attributes, args...) where PlotType <: AbstractPlot
    # create "empty" plot type - empty meaning containing no plots, just attributes + arguments
    plot_object, scene_attributes = PlotType(scene, attributes, args)
    # call user defined recipe overload to fill the plot type
    plot!(plot_object)
    push!(scene.plots, plot_object)
    scene
end

function scale_scene!(scene)
    if is2d(scene)
        area = pixelarea(scene)[]
        lims = limits(scene)[]
        # not really sure how to scale 3D scenes in a reasonable way
        mini, maxi = minimum(lims), maximum(lims)
        l = ((mini[1], maxi[1]), (mini[2], maxi[2]))
        xyzfit = fit_ratio(area, l)
        s = to_ndim(Vec3f0, xyzfit, 1f0)
        scale!(scene, s)
    end
    return scene
end

function setup_camera!(scene::Scene)
    if scene[:camera][] == automatic
        cam = cameracontrols(scene)
        if cam == EmptyCamera()
            if is2d(scene)
                #@info("setting camera to 2D")
                cam2d!(scene)
            else
                #@info("setting camera to 3D")
                cam3d!(scene)
            end
        end
    elseif scene[:camera][] in (cam2d!, cam3d!, campixel!)
        scene[:camera][](scene)
    else
        error("Unrecogniced `camera` attribute type: $(typeof(scene[:camera][])). Use automatic, cam2d! or cam3d!")
    end
    scene
end

function find_in_plots(scene::Scene, key::Symbol)
    # TODO findfirst is a bit flaky... maybe merge multiple ranges + tick labels?!
    idx = findfirst(scene.plots) do plot
        !isaxis(plot) && haskey(plot, key) && plot[key][] !== automatic
    end
    if idx !== nothing
        scene.plots[idx][key]
    else
        automatic
    end
end

function add_axis!(scene::Scene, attributes = Attributes())
    show_axis = scene[:show_axis][]
    show_axis isa Bool || error("show_axis needs to be a bool")
    axistype = if scene[:axis_type][] == automatic
        is2d(scene) ? axis2d! : axis3d!
    elseif scene[:axis_type][] in (axis2d!, axis3d!)
        scene[:axis_type][]
    else
        error("Unrecogniced `axis_type` attribute type: $(typeof(scene[:axis_type][])). Use automatic, axis2d! or axis3d!")
    end

    if show_axis && !(any(isaxis, plots(scene)))
        key = axistype == axis2d! ? :axis2d : :axis3d
        axis_attributes = scene[key]
        ranges = get(attributes, :tickranges) do
            find_in_plots(scene, :tickranges)
        end
        labels = get(attributes, :ticklabels) do
            find_in_plots(scene, :ticklabels)
        end
        axistype(
            scene, axis_attributes, limits(scene),
            ticks = (ranges = ranges, labels = labels)
        )
    end
    scene
end

function add_labels!(scene::Scene)
    if plot_attributes[:show_legend][] && haskey(p.attributes, :colormap)
        legend_attributes = plot_attributes[:legend][]
        colorlegend(scene, p.attributes[:colormap], p.attributes[:colorrange], legend_attributes)
    end
    scene
end

update_limits!(scene::Scene) = update_limits!(scene, scene[:limits][], scene[:padding][])

function update_limits!(scene::Scene, limits::Automatic, padding)
    # for when scene is empty
    dlimits = data_limits(scene)
    tlims = (minimum(dlimits), maximum(dlimits))
    if !all(x-> all(isfinite, x), tlims)
        @warn "limits of scene contain non finite values: $(tlims[1]) .. $(tlims[2])"
        mini = map(x-> ifelse(isfinite(x), x, 0.0), tlims[1])
        maxi = Vec3f0(ntuple(3) do i
            x = tlims[2][i]
            ifelse(isfinite(x), x, tlims[1][i] + 1f0)
        end)
        tlims = (mini, maxi)
    end
    new_widths = Vec3f0(ntuple(3) do i
        a = tlims[1][i]; b = tlims[2][i]
        w = b - a
        # check for widths == 0.0... 3rd dimension is allowed to be 0 though.
        # TODO maybe we should allow any one dimension to be 0, and then use the other 2 as 2D
        with0 = (i != 3) && (w ≈ 0.0)
        with0 && @warn "Founds 0 width in scene limits: $(tlims[1]) .. $(tlims[2])"
        ifelse(with0, 1f0, w)
    end)
    update_limits!(scene, FRect3D(tlims[1], new_widths), padding)
end

function update_limits!(scene::Scene, new_limits::HyperRectangle, padding = Vec3f0(0))
    lims = FRect3D(new_limits)
    lim_w = widths(lims)
    # use the smallest widths for scaling, to have a consistently wide padding for all sides
    minw = if lim_w[3] ≈ 0.0
        m = min(lim_w[1], lim_w[2])
        Vec3f0(m, m, 0.0)
    else
        Vec3f0(minimum(lim_w))
    end
    padd_abs = minw .* to_ndim(Vec3f0, padding, 0.0)
    limits(scene)[] = FRect3D(minimum(lims) .- padd_abs, lim_w .+  2padd_abs)
    scene
end


##  Copyright 2022 Myrl Marmarelis
##
##  Licensed under the Apache License, Version 2.0 (the "License");
##  you may not use this file except in compliance with the License.
##  You may obtain a copy of the License at
##
##    http://www.apache.org/licenses/LICENSE-2.0
##
##  Unless required by applicable law or agreed to in writing, software
##  distributed under the License is distributed on an "AS IS" BASIS,
##  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
##  See the License for the specific language governing permissions and
##  limitations under the License.

using StaticArrays, Setfield

"""
  `input` features should be scaled between zero and one,
  preferably with a mapping defined a priori (not an empirical
  CDF or standardization,) so that new `Instance`s may come
  online without having to alter the old ones.
  Note: should I store it log-transformed, then?
  Some features need not be scaled like that if they will
  never be treated.
""" # if they're already Z-scored, should I apply a normal CDF
# or an empirical CDF that guarantees the histogram to be uniform?
struct Instance{ In, Out, T<:Real }
  id :: Int
  input :: SVector{In, T}
  output :: SVector{Out, T}
end

# some differential operator on `input` must match `output`,
# the latter of which takes whatever dimensionality of the operator.
# `output` basically can be used to store intermediate computations
struct ConstraintInstance{ In, Out, T<:Real }
  input :: SVector{In, T}
  output :: SVector{Out, T}
end

wrap_treatment_index(index, In) = index < 0 ? (index + In+1) : index

# may not be the route we always follow in structuring treatment-effect models
function treat_instance(instance::Instance{In,Out,T},
    index::Int, treatment::T; perturb::Bool=false
    )::Instance{In,Out,T} where {In,Out,T}
  @assert index != 0
  index = wrap_treatment_index(index, In)
  if perturb
    @set instance.input[index] += treatment
  else
    @set instance.input[index] = treatment
  end
end

function make_propensity_instance(instance::Instance{In,Out,T},
    index::Int)::Instance{In-1,1,T} where {In,Out,T}
  index = wrap_treatment_index(index, In)
  propensity_input = SVector{In-1}(
    instance.input[i < index ? i : (i+1)] for i in 1:(In-1) )
  propensity_output = @SVector [ instance.input[index] ]
  Instance(instance.id, propensity_input, propensity_output)
end


module MusicTransformer

using Flux
using Transformers

include("layers.jl")
include("models.jl")
include("musicencoders.jl")
include("generate.jl")
include("pretrain.jl")

end


abstract type AbstractNNPolicy  <: Policy end

## NN Policy interface

"""
    getnetwork(policy)
    return the  value network of the policy
"""
function getnetwork end

"""
    resetstate!(policy)
reset the hidden states of a policy
"""
function resetstate! end

struct NNPolicy{P, Q, A} <: AbstractNNPolicy
    problem::P
    qnetwork::Q
    action_map::Vector{A}
    n_input_dims::Int64
end

NNPolicy(problem::MDPCommonRLEnv, qnetwork, action_map::Vector, n_input_dims::Int) = NNPolicy(convert(MDP, problem), qnetwork, action_map, n_input_dims)
NNPolicy(problem::POMDPCommonRLEnv, qnetwork, action_map::Vector, n_input_dims::Int) = NNPolicy(convert(POMDP, problem), qnetwork, action_map, n_input_dims)

function getnetwork(policy::NNPolicy)
    return policy.qnetwork
end

function resetstate!(policy::NNPolicy)
    Flux.reset!(policy.qnetwork)
end

actionmap(p::NNPolicy) = p.action_map

function _action(policy::NNPolicy, o)
    if ndims(o) == policy.n_input_dims
        obatch = reshape(o, (size(o)...,1))
        vals = policy.qnetwork(obatch)
        return policy.action_map[argmax(vals)]
    else
        throw("NNPolicyError: was expecting an array with $(policy.n_input_dims) dimensions, got $(ndims(o))")
    end
end

function _actionvalues(policy::NNPolicy{P,Q,A}, o::AbstractArray{T,N}) where {P,Q,A,T<:Real,N}
    if ndims(o) == policy.n_input_dims
        obatch = reshape(o, (size(o)...,1))
        return dropdims(policy.qnetwork(obatch), dims=2)
    else
        throw("NNPolicyError: was expecting an array with $(policy.n_input_dims) dimensions, got $(ndims(o))")
    end
end

function _value(policy::NNPolicy{P}, o::AbstractArray{T,N}) where {P,T<:Real,N}
    if ndims(o) == policy.n_input_dims
        obatch = reshape(o, (size(o)...,1))
        return maximum(policy.qnetwork(obatch))
    else
        throw("NNPolicyError: was expecting an array with $(policy.n_input_dims) dimensions, got $(ndims(o))")
    end
end

POMDPs.action(policy::NNPolicy, o) = _action(policy, o)
POMDPs.action(policy::NNPolicy{P}, s) where {P <: MDP} = _action(policy, POMDPs.convert_s(Vector{Float32}, s, policy.problem))
POMDPs.action(policy::NNPolicy{P}, a, o) where {P <: POMDP} = _action(policy, vcat(vec(POMDPs.convert_a(Vector{Float32}, a, policy.problem)), vec(POMDPs.convert_o(Vector{Float32}, o, policy.problem))))

POMDPPolicies.actionvalues(policy::NNPolicy, o) = _actionvalues(policy, o)
POMDPPolicies.actionvalues(policy::NNPolicy{P}, s) where {P<:MDP} = _actionvalues(policy, POMDPs.convert_s(Vector{Float32}, s, policy.problem))
POMDPPolicies.actionvalues(policy::NNPolicy{P}, a, o) where {P<:POMDP} = _actionvalues(policy, vcat(vec(POMDPs.convert_a(Vector{Float32}, a, policy.problem)), vec(POMDPs.convert_o(Vector{Float32}, o, policy.problem))))

POMDPs.value(policy::NNPolicy, o) = _value(policy, o)
POMDPs.value(policy::NNPolicy{P}, s) where {P <: MDP} = _value(policy, POMDPs.convert_s(Vector{Float32}, s, policy.problem))
POMDPs.value(policy::NNPolicy{P}, a, o) where {P <: POMDP} = _value(policy, vcat(vec(POMDPs.convert_a(Vector{Float32}, a, policy.problem)), vec(POMDPs.convert_o(Vector{Float32}, o, policy.problem))))


using Knet
libknet8handle = Libdl.dlopen(Knet.libknet8)

SIZE1 = 1000
SIZE2 = 100
ITER = 100000
mat32b = rand(Float32,SIZE1,SIZE2)
mat32 = KnetArray(mat32b)
col32b = rand(Float32,SIZE1)
col32 = KnetArray(col32b)
row32b = rand(Float32,1,SIZE2)
row32 = KnetArray(row32b)
mat64b = rand(Float64,SIZE1,SIZE2)
mat64 = KnetArray(mat64b)
col64b = rand(Float64,SIZE1)
col64 = KnetArray(col64b)
row64b = rand(Float64,1,SIZE2)
row64 = KnetArray(row64b)

f32 = f64 = nothing

function cuda21test(fname, jname=fname, o...)
    println(fname)
    fcpu = eval(parse(jname))

    global f32 = Libdl.dlsym(libknet8handle, fname*"_32_21")
    print("gpu col32 ")
    @time cuda21rep(f32,mat32,col32)
    print("cpu col32 ")
    @time out32 = fcpu21rep(fcpu,mat32b,col32b)
    isapprox(out32,Array(col32)) || warn("$fname 32 mat col")
    print("gpu row32 ")
    @time cuda21rep(f32,mat32,row32)
    print("cpu row32 ")
    @time out32 = fcpu21rep(fcpu,mat32b,row32b)
    isapprox(out32,Array(row32)) || warn("$fname 32 mat row")

    global f64 = Libdl.dlsym(libknet8handle, fname*"_64_21")
    print("gpu col64 ")
    @time cuda21rep(f64,mat64,col64)
    print("cpu col64 ")
    @time out64 = fcpu21rep(fcpu,mat64b,col64b)
    isapprox(out64,Array(col64)) || warn("$fname 64 mat col")
    print("gpu row64 ")
    @time cuda21rep(f64,mat64,row64)
    print("cpu row64 ")
    @time out64 = fcpu21rep(fcpu,mat64b,row64b)
    isapprox(out64,Array(row64)) || warn("$fname 64 mat row")
end

function cuda21rep{T}(f,x::KnetArray{T},y::KnetArray{T})
    nx = length(x); ny = length(y)
    if size(y,1)==1; sy=size(x,1); else; sy=1; end
    for i=1:ITER
        ccall(f,Void,(Cint,Ptr{T},Cint,Cint,Ptr{T}),nx,x,sy,ny,y)
    end
    Knet.@cuda(cudart,cudaDeviceSynchronize,())
    Knet.@cuda(cudart,cudaGetLastError,())
end

function fcpu21rep{T}(f,x::Array{T},y::Array{T})
    if size(y,1)==1; region=1; else; region=2; end
    out = nothing
    for i=1:1 # ITER
        out = f(x,region)
    end
    return out
end

for f in Knet.cuda21
    isa(f,Tuple) || (f=(f,))
    cuda21test(f...)
end


function nutrients_init(arch, g)
    fields = (Field(arch, g), Field(arch, g),
              Field(arch, g), Field(arch, g),
              Field(arch, g), Field(arch, g),
              Field(arch, g), Field(arch, g),
              Field(arch, g), Field(arch, g))

    nut = NamedTuple{nut_names}(fields)
    return nut
end

"""
    default_nut_init()
Generate defalut nutrient initial conditions.
"""
function default_nut_init()
    init = (DIC=20.0, NH4=0.2, NO3=0.5, PO4=0.03, DOC=1.0, DON=0.1, DOP=0.05, POC=0.0, PON=0.0, POP=0.0)
    rand_noise = (DIC=0.1, NH4=0.1, NO3=0.1, PO4=0.1, DOC=0.1, DON=0.1, DOP=0.1, POC=0.1, PON=0.1, POP=0.1)
    return (initial_condition = init, rand_noise = rand_noise)
end

"""
    generate_nutrients(arch, grid, source; mask = nothing)
Set up initial nutrient fields according to `grid`.

Keyword Arguments
=================
- `arch`: `CPU()` or `GPU()`. The computer architecture used to time-step `model`.
- `grid`: The resolution and discrete geometry on which nutrient fields are solved.
- `source`: A `NamedTuple` containing 10 numbers each of which is the uniform initial condition of one tracer,
            or a `Dict` containing the file paths pointing to the files of nutrient initial conditions.
- `mask` (optional): Mask out the tracers generated out of the domain, a 3D array with size `(Nx, Ny, Nz)`.
"""
function generate_nutrients(arch, g, source::Union{Dict,NamedTuple}; mask=nothing)
    total_size = (g.Nx+g.Hx*2, g.Ny+g.Hy*2, g.Nz+g.Hz*2)
    nut = nutrients_init(arch, g)
    pathkeys = collect(keys(source))

    if typeof(source) <: NamedTuple
        pathkeys = collect(keys(source.initial_condition))
    end

    for name in nut_names
        if length(findall(x->x==name, pathkeys)) == 0
            throw(ArgumentError("NUT_INIT: nutrient not found $(name)"))
        else
            if typeof(source) <: Dict # file paths
                tmp = deserialize(source[name]) |> array_type(arch)
                if sum(tmp .< 0.0) > 0
                    throw(ArgumentError("NUT_INIT:  The initial condition should be none-negetive."))
                end

                if size(tmp) == (g.Nx, g.Ny, g.Nz)
                    @views @. nut[name].data[g.Hx+1:g.Hx+g.Nx, g.Hy+1:g.Hy+g.Ny, g.Hz+1:g.Hz+g.Nz] = tmp[:,:,:]
                else
                    throw(ArgumentError("NUT_INIT:  grid mismatch"))
                end
            elseif typeof(source) <: NamedTuple # value
                if source.initial_condition[name] < 0.0
                    throw(ArgumentError("NUT_INIT:  The initial condition should be none-negetive."))
                end
                lower = 1.0 - source.rand_noise[name]
                upper = 1.0 + source.rand_noise[name]
                nut[name].data .= fill(source.initial_condition[name],total_size) .* rand(lower:1e-4:upper, total_size) |> array_type(arch)
            end
        end

        if mask ≠ nothing
            if size(mask) == (g.Nx, g.Ny, g.Nz)
                @views @. nut[name].data[g.Hx+1:g.Hx+g.Nx, g.Hy+1:g.Hy+g.Ny, g.Hz+1:g.Hz+g.Nz] *= mask
            else
                throw(ArgumentError("nut_mask: grid mismatch, size(mask) must equal to (grid.Nx, grid.Ny, grid.Nz)."))
            end
        end
    end

    fill_halo_nut!(nut,g)

    return nut
end


module BitSAD

using DataStructures
using UUIDs, PearsonHash
using Random: MersenneTwister
using MacroTools
using Reexport

export AbstractBit, AbstractBitstream
export SBit, SBitstream
export DBit, DBitstream
export float, pos, neg
export zero, one
export +, -, *, /, ÷, sqrt, decorrelate, norm
export generate, generate!, estimate!
export push!, pop!, observe, length
export SDM

include("types/bitstream.jl")
include("types/sbitstream.jl")
include("types/dbitstream.jl")
include("modules/sdm.jl")

include("hardware/HW.jl")

@reexport using .HW

end # module


@testset "Events/Miscellaneous" begin
    # These test sets are a grab bag of tests that don't fit anywhere else.
    #
    evaluate(s) = collect(E.events(L.State(IOBuffer(s))))
    tokenize(s) = L.tokens(L.State(IOBuffer(s)))

    E = Events
    L = Events.Lexical

    ME = E.MarkupError

    illegal_characters = Set(vcat('\u00':'\u08', '\u0e':'\u1f', '\ud800':'\udfff', [ '\u0b', '\u0c', '\ufffe', '\uffff' ]))

    name_start_chars = [ Char(':'), Char('A'):Char('Z'), Char('a'):Char('z'),
                         0xc0:0xd6, 0xd8:0xf6, 0xf8:0x2ff, 0x370:0x37d, 0x37f:0x1fff,
                         0x200c:0x200d, 0x2070:0x218f, 0x2c00:0x2fef, 0x3001:0xd7ff,
                         0xf900:0xfdcf, 0xfdf0:0xfffd, 0x10000:0xeffff ]

    # This is only the "other" characters that are in NameChar ... to get a full set, we will need to include
    # NameStartChar.
    #
    name_chars = [ Char('-'), Char('.'), Char('0'):Char('9'), 0xb7, 0x300:0x36f, 0x203f:0x2040 ]

    # These are some characters that are not part of either of the two above sets.
    #
    other_chars = [ 0xb8:0xbf, 0xd7, 0xf7, 0x37e, 0x200e:0x203e ]

    function select_characters(character_range)
        if length(character_range) == 1
            return [ String([ Char(character_range) ]) ]

        else
            result = [ String([ Char(character_range.start) ]), String([ Char(character_range.stop) ]) ]

            if length(character_range) > 2
                other_character = rand(character_range[2:length(character_range) - 1])

                push!(result, String([ Char(other_character) ]))
            end

            return result
        end
    end

    function build_values(character_range)
        return select_characters(character_range)
    end

    function build_values(first_character_range, second_character_range)
        first_set  = select_characters(first_character_range)
        second_set = select_characters(second_character_range)

        return vec([ f * s for f ∈ first_set, s ∈ second_set ])
    end

    function build_values(first_character_range, second_character_range, third_character_range)
        first_set  = select_characters(first_character_range)
        second_set = select_characters(second_character_range)
        third_set  = select_characters(third_character_range)

        return vec([ f * s * t for f ∈ first_set, s ∈ second_set, t ∈ third_set ])
    end

    @testset "Events/Miscellaneous, is_name() (Positive)" begin
        # First some trivial cases ... basically trigger each branch. If it's a range, trigger it at each extreme and
        # somewhere in the middle (selected randomly)
        #
        for name_start_char ∈ name_start_chars
            for name ∈ build_values(name_start_char)
                tokens = tokenize(name)
                @test E.is_name(tokens)
                @test length(collect(tokens)) == 1
            end
        end

        # Now some two-character cases, where both characters come from NameStartChar.
        #
        for name_start_char ∈ name_start_chars
            for name_char ∈ name_start_chars
                for name ∈ build_values(name_start_char, name_char)
                    tokens = tokenize(name)
                    @test E.is_name(tokens)
                    @test length(collect(tokens)) == 1
                end
            end
        end

        # And now some two character-cases, where the second character comes from NameChar.
        #
        for name_start_char ∈ name_start_chars
            for name_char ∈ name_chars
                for name ∈ build_values(name_start_char, name_char)
                    tokens = tokenize(name)
                    @test E.is_name(tokens)
                    @test length(collect(tokens)) == 1
                end
            end
        end

        # Finally, some three character-cases. Trying combinations like we did earlier could take way too much time. So
        # we'll randomly construct one and test it.
        #
        all_name_chars = vcat(name_start_chars, name_chars)

        name_start_char  = rand(name_start_chars)
        second_name_char = rand(all_name_chars)
        third_name_char  = rand(all_name_chars)

        name = rand(build_values(name_start_char, second_name_char, third_name_char))

        tokens = tokenize(name)
        @test E.is_name(tokens)
        @test length(collect(tokens)) == 1
    end

    @testset "Events/Miscellaneous, is_nmtoken() (Positive)" begin
        # First some trivial cases ... basically trigger each branch. If it's a range, trigger it at each extreme and
        # somewhere in the middle (selected randomly)
        #
        for name_char ∈ name_chars
            for nmtoken ∈ build_values(name_char)
                tokens = tokenize(nmtoken)
                @test E.is_nmtoken(tokens)
                @test length(collect(tokens)) == 1
            end
        end

        # Now some two-character cases.
        #
        for first_name_char ∈ name_chars
            for second_name_char ∈ name_chars
                for nmtoken ∈ build_values(first_name_char, second_name_char)
                    tokens = tokenize(nmtoken)
                    @test E.is_nmtoken(tokens)
                    @test length(collect(tokens)) == 1
                end
            end
        end

        # Finally, some three character-cases. Trying combinations like we did earlier could take way too much time. So
        # we'll randomly construct one and test it.
        #
        all_name_chars = vcat(name_start_chars, name_chars)

        first_name_char  = rand(all_name_chars)
        second_name_char = rand(all_name_chars)
        third_name_char  = rand(all_name_chars)

        nmtoken = rand(build_values(first_name_char, second_name_char, third_name_char))

        tokens = tokenize(nmtoken)
        @test E.is_nmtoken(tokens)
        @test length(collect(tokens)) == 1
    end

    @testset "Events/Miscellaneous, is_name() (Negative)" begin
        # Negative tests for single-character names are easy enough ... just choose a character from NameChar.
        #
        for name_start_char ∈ name_chars
            for name ∈ build_values(name_start_char)
                tokens = tokenize(name)
                @test !E.is_name(tokens)
            end
        end

        # For two character-cases, uses a "safe" character for the first position, and pull the second character from
        # other_chars, defined above.
        #
        for first_char ∈ [ 'a' ]
            for second_char ∈ other_chars
                for name ∈ build_values(first_char, second_char)
                    tokens = tokenize(name)
                    @test !E.is_name(tokens)
                    @test length(collect(tokens)) == 1
                end
            end
        end

        # Finally, make sure that is_name() doesn't try accepting tokens that aren't of type text to begin with.
        #
        for input ∈ [ "+", "<", "<!", " ", "<?", "?>", ">", "" ]
            tokens = tokenize(input)
            @test !E.is_name(tokens)
        end
    end

    @testset "Events/Miscellaneous, is_nmtoken() (Negative)" begin
        # Negative tests for single-character nmtokens are easy enough ... just choose a character from other_chars,
        # defined above.
        #
        for name_char ∈ other_chars
            for nmtoken ∈ build_values(name_char)
                tokens = tokenize(nmtoken)
                @test !E.is_nmtoken(tokens)
            end
        end

        # For two character-cases, uses a "safe" character for the first position, and pull the second character from
        # other_chars, defined above.
        #
        for first_char ∈ [ 'a' ]
            for second_char ∈ other_chars
                for nmtoken ∈ build_values(first_char, second_char)
                    tokens = tokenize(nmtoken)
                    @test !E.is_nmtoken(tokens)
                    @test length(collect(tokens)) == 1
                end
            end
        end

        # Finally, make sure that is_name() doesn't try accepting tokens that aren't of type text to begin with.
        #
        for input ∈ [ "+", "<", "<!", " ", "<?", "?>", ">", "" ]
            tokens = tokenize(input)
            @test !E.is_nmtoken(tokens)
        end
    end

    @testset "Events/Miscellaneous, is_nmtoken() (Negative ... illegal characters)" begin
        for character ∈ illegal_characters
            events = evaluate(string(character))
            @test events == [ ME("ERROR: Encountered the illegal character \'"
                                 * "#x" * string(Int(character), base = 16) * "\'." * " It will be discarded.",
                                 L.Location("a buffer", -1)) ]
        end
    end
end


using Oracle.Timestamps

OraTimestamp(date::Date) = OraTimestamp(Dates.year(date), Dates.month(date), Dates.day(date), 0, 0, 0, 0, 0, 0)
OraTimestamp(datetime::DateTime) = OraTimestamp(Dates.year(datetime), Dates.month(datetime), Dates.day(datetime), Dates.hour(datetime), Dates.minute(datetime), Dates.second(datetime), Int64(Dates.millisecond(datetime)*1E6), 0, 0)

function OraTimestamp(ts::Timestamp)
    yy = Int16(Dates.year(ts))
    mm = UInt8(Dates.month(ts))
    dd = Dates.day(ts)
    hh = Dates.hour(ts)
    mi = Dates.minute(ts)
    ss = Dates.second(ts)
    fsecond = Dates.millisecond(ts) * 1_000_000 + Dates.microsecond(ts) * 1_000 + Dates.nanosecond(ts)
    return OraTimestamp(yy, mm, dd, hh, mi, ss, fsecond, 0, 0)
end

function OraTimestamp(ts::TimestampTZ)
    yy = Int16(Dates.year(ts))
    mm = UInt8(Dates.month(ts))
    dd = Dates.day(ts)
    hh = Dates.hour(ts)
    mi = Dates.minute(ts)
    ss = Dates.second(ts)
    fsecond = Dates.millisecond(ts) * 1_000_000 + Dates.microsecond(ts) * 1_000 + Dates.nanosecond(ts)
    return OraTimestamp(yy, mm, dd, hh, mi, ss, fsecond, ts.tz_offset.hour, ts.tz_offset.minute)
end

function parse_timestamp(oracle_type::OraOracleTypeNum, oracle_timestamp::OraTimestamp) :: Union{Dates.DateTime, Timestamp, TimestampTZ}
    if oracle_type == ORA_ORACLE_TYPE_DATE
        return parse_datetime(oracle_timestamp)
    elseif oracle_type == ORA_ORACLE_TYPE_TIMESTAMP
        return parse_timestamp(oracle_timestamp)
    elseif oracle_type == ORA_ORACLE_TYPE_TIMESTAMP_TZ
        return parse_timestamp_tz(oracle_timestamp)
    elseif oracle_type == ORA_ORACLE_TYPE_TIMESTAMP_LTZ
        return parse_timestamp_ltz(oracle_timestamp)
    else
        error("Unexpected oracle type for timestamp: $oracle_type.")
    end
end

function parse_datetime(ts::OraTimestamp) :: Dates.DateTime
    @assert ts.fsecond == 0
    @assert ts.tzHourOffset == 0
    @assert ts.tzMinuteOffset == 0
    return DateTime(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second)
end

function parse_timestamp(ts::OraTimestamp) :: Timestamp
    @assert ts.tzHourOffset == 0
    @assert ts.tzMinuteOffset == 0
    return Timestamp(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second, ts.fsecond)
end

function parse_timestamp_tz(ts::OraTimestamp) :: TimestampTZ
    return TimestampTZ(false, ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second, ts.fsecond, ts.tzHourOffset, ts.tzMinuteOffset)
end

function parse_timestamp_ltz(ts::OraTimestamp) :: TimestampTZ
    return TimestampTZ(true, ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second, ts.fsecond, ts.tzHourOffset, ts.tzMinuteOffset)
end


##############################################################################
##
## LSMR MultiCores
##
##############################################################################

struct FixedEffectSolverLSMRCores{T} <: AbstractFixedEffectSolver{T}
    fes::Vector{<:FixedEffect}
    weights::AbstractWeights
end

function AbstractFixedEffectSolver{T}(fes::Vector{<:FixedEffect}, weights::AbstractWeights,
                ::Type{Val{:lsmr_cores}}) where {T}
    FixedEffectSolverLSMRCores{T}(fes, weights)
end

function AbstractFixedEffectSolver{T}(fes::Vector{<:FixedEffect}, weights::AbstractVector,
                ::Type{Val{:lsmr_parallel}}) where {T}
    info(":lsmr_parallel is deprecated. Use :lsmr_cores")
    AbstractFixedEffectSolver{T}(fes, weights, Val{:lsmr_cores})
end

function solve_residuals!(r::AbstractVector, feM::FixedEffectSolverLSMRCores{T}; kwargs...) where {T}
    solve_residuals!(r, AbstractFixedEffectSolver{T}(feM.fes, feM.weights, Val{:lsmr}); kwargs...)
end

function solve_residuals!(X::AbstractMatrix, feM::FixedEffectSolverLSMRCores; kwargs...)
    iterations = zeros(Int, size(X, 2))
    convergeds = zeros(Bool, size(X, 2))
    result = pmap(x -> solve_residuals!(x, feM; kwargs...), [X[:, j] for j in 1:size(X, 2)])
    for j in 1:size(X, 2)
        X[:, j] = result[j][1]
        iterations[j] =  result[j][2]
        convergeds[j] = result[j][3]
    end
    return X, iterations, convergeds
end

function solve_coefficients!(r::AbstractVector, feM::FixedEffectSolverLSMRCores{T}, ; kwargs...) where {T}
    solve_coefficients!(r, AbstractFixedEffectSolver{T}(feM.fes, feM.weights, Val{:lsmr}); kwargs...)
end

##############################################################################
##
## LSMR MultiThreads
##
## One needs to construct a new fe matrix / fe vectirs for each LHS/RHS
## This is because each thread need to have its own cache / u / w, etc
##
##############################################################################

struct FixedEffectSolverLSMRThreads{T} <: AbstractFixedEffectSolver{T}
    x::Vector{FixedEffectSolverLSMR{T}}
end

function AbstractFixedEffectSolver{T}(fes::Vector{<:FixedEffect}, weights::AbstractWeights, ::Type{Val{:lsmr_threads}}) where {T}
    FixedEffectSolverLSMRThreads([AbstractFixedEffectSolver{T}(fes, weights, Val{:lsmr}) for _ in 1:Threads.nthreads()])
end

function solve_residuals!(r::AbstractVector, feM::FixedEffectSolverLSMRThreads; kwargs...)
    solve_residuals!(r, feM.x[1]; kwargs...)
end

function solve_residuals!(X::AbstractMatrix, feM::FixedEffectSolverLSMRThreads; kwargs...)
   iterations = zeros(Int, size(X, 2))
   convergeds = zeros(Bool, size(X, 2))
   Threads.@threads for j in 1:size(X, 2)
        _, iteration, converged = solve_residuals!(view(X, :, j), feM.x[Threads.threadid()]; kwargs...)
        iterations[j] = iteration
        convergeds[j] = converged
   end
   return X, iterations, convergeds
end

function solve_coefficients!(r::AbstractVector, feM::FixedEffectSolverLSMRThreads; kwargs...)
    solve_coefficients!(r, feM.x[1]; kwargs...)
end


# See https://udel.edu/~arthij/MD.pdf for information on forces
# See https://arxiv.org/pdf/1401.1181.pdf for applying forces to atoms
# See OpenMM documentation and Gromacs manual for other aspects of forces

export
    mass,
    ustripvec,
    force,
    accelerations,
    LennardJones,
    SoftSphere,
    Mie,
    Coulomb,
    CoulombReactionField,
    Gravity,
    HarmonicBond,
    HarmonicAngle,
    PeriodicTorsion,
    RBTorsion

"""
    mass(atom)

The mass of an atom.
"""
mass(atom::Atom) = atom.mass

"""
    ustripvec(x)

Broadcasted form of `ustrip` from Unitful.jl, allowing e.g. `ustripvec.(coords)`.
"""
ustripvec(x) = ustrip.(x)

function checkforcetype(fdr, force_type)
    if unit(first(fdr)) != force_type
        error("Simulation force type is ", force_type,
                " but encountered force type ", unit(first(fdr)))
    end
end

"""
    force(inter, coord_i, coord_j, atom_i, atom_j, box_size)

Calculate the force between a pair of atoms due to a given interation type.
Custom interaction types should implement this function.
"""
function force end

# Allow 2D broadcasting whilst eliminating the diagonal corresponding to self interaction
@inline @inbounds function force(inter, coord_i, coord_j, atom_i, atom_j, box_size, self_interaction, force_type)
    if isone(self_interaction)
        return ustrip.(zero(coord_i))
    else
        fdr = force(inter, coord_i, coord_j, atom_i, atom_j, box_size)
        checkforcetype(fdr, force_type)
        return ustrip.(fdr)
    end
end

@inline @inbounds function force!(forces, inter, s::Simulation, i::Integer, j::Integer, force_type, weight_14::Bool=false)
    fdr = force(inter, s.coords[i], s.coords[j], s.atoms[i], s.atoms[j], s.box_size)
    checkforcetype(fdr, force_type)
    if weight_14
        fdr_ustrip = ustrip.(fdr) * inter.weight_14
    else
        fdr_ustrip = ustrip.(fdr)
    end
    forces[i] -= fdr_ustrip
    forces[j] += fdr_ustrip
    return nothing
end

"""
    accelerations(simulation; parallel=true)

Calculate the accelerations of all atoms using the general and specific
interactions and Newton's second law.
"""
function accelerations(s::Simulation; parallel::Bool=true)
    n_atoms = length(s.coords)

    if parallel && nthreads() > 1 && n_atoms >= 100
        forces_threads = [ustripvec.(zero(s.coords)) for i in 1:nthreads()]

        # Loop over interactions and calculate the acceleration due to each
        for inter in values(s.general_inters)
            if inter.nl_only
                neighbors = s.neighbors
                @threads for ni in 1:length(neighbors)
                    i, j, w = neighbors[ni]
                    force!(forces_threads[threadid()], inter, s, i, j, s.force_unit, w)
                end
            else
                @threads for i in 1:n_atoms
                    for j in 1:(i - 1)
                        force!(forces_threads[threadid()], inter, s, i, j, s.force_unit)
                    end
                end
            end
        end

        forces = sum(forces_threads)
    else
        forces = ustripvec.(zero(s.coords))

        for inter in values(s.general_inters)
            if inter.nl_only
                neighbors = s.neighbors
                for ni in 1:length(neighbors)
                    i, j, w = neighbors[ni]
                    force!(forces, inter, s, i, j, s.force_unit, w)
                end
            else
                for i in 1:n_atoms
                    for j in 1:(i - 1)
                        force!(forces, inter, s, i, j, s.force_unit)
                    end
                end
            end
        end
    end

    for inter_list in values(s.specific_inter_lists)
        sparse_forces = force.(inter_list, (s.coords,), (s,))
        ge1, ge2 = getindex.(sparse_forces, 1), getindex.(sparse_forces, 2)
        checkforcetype(first(first(ge2)), s.force_unit)
        sparse_vec = sparsevec(reduce(vcat, ge1), reduce(vcat, ge2), n_atoms)
        forces += ustripvec.(Array(sparse_vec))
    end

    return (forces * s.force_unit) ./ mass.(s.atoms)
end

function accelerations(s::Simulation, coords, coords_is, coords_js, atoms_is, atoms_js, self_interactions)
    n_atoms = length(coords)
    forces = ustripvec.(zero(coords))

    for inter in values(s.general_inters)
        # Currently the neighbor list is not used for this implementation
        forces -= reshape(sum(force.((inter,), coords_is, coords_js, atoms_is, atoms_js,
                                        (s.box_size,), self_interactions, s.force_unit);
                                        dims=2), n_atoms)
    end

    for inter_list in values(s.specific_inter_lists)
        # Take coords off the GPU if they are on there
        coords_cpu = Array(coords)
        sparse_forces = force.(inter_list, (coords_cpu,), (s,))
        ge1, ge2 = getindex.(sparse_forces, 1), getindex.(sparse_forces, 2)
        checkforcetype(first(first(ge2)), s.force_unit)
        sparse_vec = sparsevec(reduce(vcat, ge1), reduce(vcat, ge2), n_atoms)
        # Move back to GPU if required
        forces += convert(typeof(forces), ustripvec.(Array(sparse_vec)))
    end

    return (forces * s.force_unit) ./ mass.(s.atoms)
end

include("interactions/lennard_jones.jl")
include("interactions/soft_sphere.jl")
include("interactions/mie.jl")
include("interactions/coulomb.jl")
include("interactions/coulomb_reaction_field.jl")
include("interactions/gravity.jl")
include("interactions/harmonic_bond.jl")
include("interactions/harmonic_angle.jl")
include("interactions/periodic_torsion.jl")
include("interactions/rb_torsion.jl")


mutable struct SparseSolver{dim} <: KKTSolver{SqrScaling{dim}}
	k0::Vector{Float64}
	k1::Vector{Float64}
	k2::Vector{Float64}

	n0::Vector{Float64}
	n1::Vector{Float64}

	m0::Vector{Float64}
	AA::SparseMatrixCSC{Float64, Int}
	Gt::SparseMatrixCSC{Float64, Int}
	GiW::SuiteSparse.CHOLMOD.Sparse{Float64}
	AtS::SuiteSparse.CHOLMOD.Sparse{Float64}
	Gint::SparseMatrixCSC{Float64, Int}
	Gfact::SuiteSparse.CHOLMOD.Factor{Float64}
	Afact::SuiteSparse.CHOLMOD.Factor{Float64}

	ws1::LdivWorkspace{Float64}
	dv1::SuiteSparse.CHOLMOD.Dense{Float64}
	dv2::SuiteSparse.CHOLMOD.Dense{Float64}

	ws2::LdivWorkspace{Float64}
	dv3::SuiteSparse.CHOLMOD.Dense{Float64}
	dv4::SuiteSparse.CHOLMOD.Dense{Float64}
	function SparseSolver(pr::Problem{C,n,m,k,sing}) where {C, n, m, k, sing}
		k0, k1, k2 = zeros(k), zeros(k), zeros(k)
		n0, n1 = zeros(n), zeros(n)
		m0 = zeros(m)

		ws1 = LdivWorkspace(Float64)
		dv1, dv2 = SuiteSparse.CHOLMOD.Dense(zeros(pr.n)), SuiteSparse.CHOLMOD.Dense(zeros(pr.n))
		ws2 = LdivWorkspace(Float64)
		dv3, dv4 = SuiteSparse.CHOLMOD.Dense(zeros(pr.m)), SuiteSparse.CHOLMOD.Dense(zeros(pr.m))

		AA = pr.A'*pr.A
		if !sing
			Gt = sparse(pr.G')
			desired_stype = 0
		else
			Gt = sparse(pr.G'*pr.G + pr.A'*pr.A)
			desired_stype = -1
		end
		cm = SuiteSparse.CHOLMOD.defaults(SuiteSparse.CHOLMOD.common_struct[Threads.threadid()])
		GtS = SuiteSparse.CHOLMOD.Sparse(Gt, desired_stype)
		AtCSC = sparse(pr.A')
		AtS = SuiteSparse.CHOLMOD.Sparse(AtCSC)
		#note that we're setting up both Gfact and Afact to be unsymmetric, so CHOLMOD will factorize AA'
		Gfact = SuiteSparse.CHOLMOD.analyze(GtS, cm)
		cholesky!(Gfact, GtS) # get the sparsity pattern right
		examplemat = SuiteSparse.CHOLMOD.Sparse(sparse((Gfact.UP\AtCSC)'), 0)
		Afact = SuiteSparse.CHOLMOD.analyze(examplemat, cm) # factorize W^t W, where L W = A or W = L^-1 A; thus W^t = A^t L^-t
		Gint = spzeros(pr.k, pr.n)
		return new{n}(k0, k1, k2, n0, n1, m0,
			AA, Gt, GtS, AtS, Gint, Gfact, Afact,
			ws1, dv1, dv2,
			ws2, dv3, dv4)
	end
end

function setup_iter(ss::SparseSolver, pr::Problem{C,n,m,k,sing}, s::State, scaling::SqrScaling) where {C, n, m, k, sing}
	if !sing
		copyto!(ss.GiW, ss.Gt)
		lmul!(scaling.iW, ss.GiW)
		cholesky!(ss.Gfact, ss.GiW)
		# do the low rank updates
		modify_factors!(pr.cones, ss.Gfact, scaling, pr.G)
	else
		Gint = ss.Gint
		copyto!(Gint, pr.G)
		lmul!(scaling.iWiW, Gint)
		i1 = pr.G' * Gint :: SparseMatrixCSC{Float64, Int}
		i1 .+= ss.AA
		cholesky!(ss.Gfact, i1)
		# do the low rank updates
		modify_factors!(pr.cones, ss.Gfact, scaling, pr.G)
	end
	# convert the factorization to an LLt one (is LDLt if we had to modify the factor)
	SuiteSparse.CHOLMOD.change_factor!(ss.Gfact, true, false, false, false)
	# solve L C^t = A^t giving C^t = L^-1 A^t, then transpose to get C = A L^-T
	i2 = SuiteSparse.CHOLMOD.spsolve(SuiteSparse.CHOLMOD.CHOLMOD_P, ss.Gfact, ss.AtS)
	Ct = SuiteSparse.CHOLMOD.spsolve(SuiteSparse.CHOLMOD.CHOLMOD_L, ss.Gfact, i2)
	Ctt = SuiteSparse.CHOLMOD.transpose_(Ct, 1)
	cholesky!(ss.Afact, Ctt)
end

function solve_kkt(ss::SparseSolver, pr::Problem{C,n,m,k,sing}, s::State, scaling::SqrScaling,
				   dx::Vector{Float64}, dy::Vector{Float64}, dz::Vector{Float64}, ds::Vector{Float64},
				   cx::Vector{Float64}, cy::Vector{Float64}, cz::Vector{Float64}, cs::Vector{Float64}) where {C, n, m, k, sing}
	iprod!(pr.cones, ss.k0, scaling.l, ds)
	scale!(pr.cones, scaling, ss.k0, ss.k1)
	bx = dx
	by = dy
	ss.k2 .= dz .- ss.k1
	iscale!(pr.cones, scaling, ss.k2, ss.k1)
	iscale!(pr.cones, scaling, ss.k1, ss.k1)
	mul!(ss.n0, pr.G', ss.k1) #ss.n0 = G'*W^-1 * W^-T*(dz - ss.k1)
	ss.n0 .+= bx
	At = pr.A' :: Adjoint{Float64, SparseMatrixCSC{Float64, Int}}
	if sing
		ss.n0 .+= At*by
	end

	# ss.m0 = A L-TL-1 ss.n0
	copyto!(ss.dv1, ss.n0)
	div!(ss.Gfact, ss.dv2, ss.dv1, ss.ws1)
	copyto!(ss.n1, ss.dv2)
	mul!(ss.m0, pr.A, ss.n1)
	ss.m0 .-= by
	copyto!(ss.dv3, ss.m0)
	div!(ss.Afact, ss.dv4, ss.dv3, ss.ws2)
	copyto!(cy, ss.dv4)
	if sing
		ss.m0 .= by .- cy
	else
		ss.m0 .= .-cy
	end
	mul!(ss.n1, At, ss.m0)
	ss.n0 .+= ss.n1
	# cx = L-TL-1 ss.n0
	copyto!(ss.dv1, ss.n0)
	div!(ss.Gfact, ss.dv2, ss.dv1, ss.ws1)
	copyto!(cx, ss.dv2)
	mul!(ss.k1, pr.G, cx)
	ss.k1 .-= ss.k2
	iscale!(pr.cones, scaling, ss.k1, cz)
	iscale!(pr.cones, scaling, cz, cz)
	scale!(pr.cones, scaling, cz, ss.k1)
	ss.k0 .-= ss.k1
	scale!(pr.cones, scaling, ss.k0, cs)
end


module InventoryModels
#using DataStructures : reset! as dsreset!
using Distributions, MacroTools, Base.Iterators, Reexport

@reexport using ReinforcementLearningBase
using Requires

const NumDist = Union{Number, Distribution}
const State = Union{NumDist, AbstractVector{<:NumDist}}
#const RLBase = ReinforcementLearningBase

abstract type AbstractItem end

include("elementary blocks/inventory.jl")
export Inventory, LinearHoldingCost
include("elementary blocks/leadtime.jl")
export LeadTime
include("elementary blocks/policies.jl")
export sSPolicy, RQPolicy, QPolicy, BQPolicy
include("elementary blocks/supplier.jl")
export Supplier, LinearOrderCost, FixedLinearOrderCost
include("elementary blocks/market.jl")
export Market, LinearStockoutCost
include("elementary blocks/assembly.jl")
export Assembly
include("items/endproduct.jl")
export EndProduct
include("items/item.jl")
export Item
include("cvnormal.jl")
export CVNormal, cv
include("constraints.jl")
export RessourceConstraint
include("inventory_system.jl")
export InventorySystem, state_size, action_size, print_state, print_action
include("utils.jl")
export test_policy
include("zoo.jl")
export sl_sip
include("Scarf/Scarf.jl")
export Scarf
include("Scarf/interface.jl")
export ISLogger
include("logger.jl")

#state less objects are unchanged when reseted
reset!(::Any) = nothing

#=export dashboard, draw_graph
include("dashboard/dashboard.jl")=#
end


using MetaICVI
# using ScikitLearn: fit!, score, @sk_import
# using ScikitLearn: fit!, score
using ScikitLearn.CrossValidation: train_test_split
using ScikitLearn
# using ProgressMeter
using PyCallJLD
# @sk_import linear_model:RidgeClassifier
# learner = RidgeClassifier()

# Include local utils
# include("../test/test_utils.jl")

# Identify the rocket file
# rocket_file = "data/models/rocket.jld2"
# classifier_file = "data/models/classifier.jld"

# # Point to data
# data_dir(args...) = joinpath("data/training", args...)
data_dir = "data/training"

# Create the options
opts = MetaICVIOpts(
    # rocket_file = rocket_file,
    # classifier_file = classifier_file,
    n_rocket = 20
)

# Create the metaicvi object
metaicvi = MetaICVIModule(opts)

(features_data, features_targets) = get_training_features(metaicvi, data_dir)

# Create a split for training/testing the learner, correct for how scikitlearn expects features
x_train, x_test, y_train, y_test = train_test_split(transpose(features_data), features_targets, test_size=0.4)

# Train and test the learner
# fit!(metaicvi.classifier, x_train, y_train)
train_and_save(metaicvi, x_train, y_train)
performance = score(metaicvi.classifier, x_test, y_test)

@info "Performance is: $performance"


## ExoplanetsSysSim/src/target.jl
## (c) 2015 Eric B. Ford

#using Distributions

struct KeplerTarget
  #sys::PlanetarySystem   # Make array for planetary systems aroud multiple stars in one target?
  sys::Vector{PlanetarySystemAbstract}
  cdpp::Array{Float64,2} # fractional, not ppm; 2D to allow for multiple time scales, months, quarters or seasons/spacecraft rotation angles
                           # QUERY: Do we want a separate CDPP for SC?  Or will CDPP's in different months be for LC/SC depending on this variable?
                            # QUERY: Should this be moved to KeplerTargetObs?
                            # QUERY: Should we not add this to target and use the star id to lookup CDPP from the stellar table?
  contam::Float64     # QUERY: Do we want/need this, since we're able to generate multiple stars in a single target?
  data_span::Float64
  duty_cycle::Float64
  window_function_id::Int64 # Points to the id of the window function for this target
  #channel::Int64         # E.g., if we cared which Kepler channel the target fell on
  #has_sc::Vector{Bool}   # TODO OPT: Make Immutable Vector or BitArray for speed?  QUERY: Should this go in KeplerTargetObs?
  #                       # QUERY: Do we want a separate CDPP for SC?  Or will CDPP's in different months be for LC/SC depending on this variable?
  #ra::Float64           # E.g., if we cared about position on sky     QUERY:  Should we replace with galactic longitude and latitute?
  #dec::Floa64           #
end
num_planets(t::KeplerTarget) = sum( num_planets, t.sys)
flux(t::KeplerTarget) = sum(flux,t.sys)+t.contam

function draw_asymmetric_normal(mu::Real, sig_plus::Real, sig_minus::Real)
  @assert sig_minus >= zero(sig_minus)
  stdn = randn()
  mu + ( (stdn>=zero(stdn)) ? sig_plus*stdn : sig_minus*stdn )
  #mu + ( (stdn>=zero(stdn)) ? sig_plus*stdn : abs(sig_minus)*stdn )
end

function make_cdpp_array(star_id::Integer)
  star_table(id::Integer,sym::Symbol) = StellarTable.star_table(id,sym)::Float64
  cdpp_arr = (1.0e-6*sqrt(1.0/24.0/LC_duration)) .* Float64[star_table(star_id, :rrmscdpp01p5)*sqrt(1.5), star_table(star_id, :rrmscdpp02p0)*sqrt(2.), star_table(star_id,:rrmscdpp02p5)*sqrt(2.5), star_table(star_id,:rrmscdpp03p0)*sqrt(3.), star_table(star_id,:rrmscdpp03p5)*sqrt(3.5), star_table(star_id,:rrmscdpp04p5)*sqrt(4.5), star_table(star_id,:rrmscdpp05p0)*sqrt(5.), star_table(star_id,:rrmscdpp06p0)*sqrt(6.), star_table(star_id,:rrmscdpp07p5)*sqrt(7.5), star_table(star_id,:rrmscdpp09p0)*sqrt(9.), star_table(star_id,:rrmscdpp10p5)*sqrt(10.5), star_table(star_id,:rrmscdpp12p0)*sqrt(12.), star_table(star_id,:rrmscdpp12p5)*sqrt(12.5), star_table(star_id,:rrmscdpp15p0)*sqrt(15.)]
end

function generate_kepler_target_from_table(sim_param::SimParam)
  # generate_star = get_function(sim_param,"generate_star")
  generate_planetary_system = get_function(sim_param,"generate_planetary_system")
  ar_table_sigmas = true
  max_draws_star_properties = 20
  min_star_radius = 0.5
  min_star_mass = 0.5
  max_star_radius = 2.0
  max_star_mass = 2.0
  max_star_density = 1000.0
  max_star_id = StellarTable.num_usable_in_star_table()

  star_table(id::Integer,sym::Symbol) = StellarTable.star_table(id,sym)

  @assert(1<=max_star_id)
  star_id = rand(1:max_star_id)
  mass = 0.0
  dens = 0.0
  radius = 0.0
    #if use_star_table_sigmas
    if get(sim_param,"use_star_table_sigmas",false)
        attmpt_num = 0
        while (!(min_star_radius<radius<max_star_radius))# || (!(min_star_mass<mass<max_star_mass)) || (!(0.0<dens<max_star_density))
            if attmpt_num >= max_draws_star_properties
                star_id = rand(1:max_star_id)
                attmpt_num = 0
            end
            radius = draw_asymmetric_normal( star_table(star_id,:radius), star_table(star_id,:radius_err1), abs(star_table(star_id,:radius_err2)) )
            #mass = draw_asymmetric_normal( star_table(star_id,:mass), star_table(star_id,:mass_err1), abs(star_table(star_id,:mass_err2)) )
            #dens = draw_asymmetric_normal( star_table(star_id,:dens), star_table(star_id,:dens_err1), abs(star_table(star_id,:dens_err2)) )
            attmpt_num += 1
        end

        # ZAMS mass-radius relation taken from 15.1.1 of Allen's Astrophysical Quantities (2002)
        if radius > 1.227
            mass = 10^((log10(radius)-0.011)/0.64)
        else
            mass = 10^((log10(radius)+0.02)/0.917)
        end
        dens   = (mass*sun_mass_in_kg_IAU2010*1000.)/(4//3*pi*(radius*sun_radius_in_m_IAU2015*100.)^3)  # Self-consistent density (gm/cm^3)
  else
    radius = star_table(star_id,:radius)
    mass   = star_table(star_id,:mass)
    #dens   = star_table(star_id,:dens)
    dens   = (mass*sun_mass_in_kg_IAU2010*1000.)/(4//3*pi*(radius*sun_radius_in_m_IAU2015*100.)^3)  # Self-consistent density (gm/cm^3)
  end
  star = SingleStar(radius,mass,1.0, star_id)     # TODO SCI: Allow for blends, binaries, etc.
  cdpp_arr = make_cdpp_array(star_id)
  contam = 0.0 # rand(LogNormal(1.0e-3,1.0))      # TODO SCI: Come up with better description of Kepler targets, maybe draw from real contaminations
  data_span = star_table(star_id, :dataspan)
  duty_cycle = star_table(star_id, :dutycycle)
  if StellarTable.star_table_has_key(:wf_id)
     wf_id = star_table(star_id,:wf_id)
  else
     wf_id = WindowFunction.get_window_function_id(star_table(star_id,:kepid))
  end
  # ch = rand(DiscreteUniform(1,84))              # Removed channel in favor of window function id
  ps = generate_planetary_system(star, sim_param)
  return KeplerTarget([ps],repeat(cdpp_arr, outer=[1,1]),contam,data_span,duty_cycle,wf_id)
end

function generate_kepler_target_simple(sim_param::SimParam)
   generate_star = get_function(sim_param,"generate_star")
   generate_planetary_system = get_function(sim_param,"generate_planetary_system")
   star::StarAbstract = generate_star(sim_param)

   mean_log_cdpp = 4.9759601617565465   # mean frmo star table
   stddev_log_cdpp = 0.6704860437536709 # std dev from star table
  rrmscdpp_5hr = exp(mean_log_cdpp+stddev_log_cdpp*randn())
  cdpp_5hr = 1.0e-6 * rrmscdpp_5hr * sqrt(5.0/24.0 / LC_duration )
  contam = 0.0 # rand(LogNormal(1.0e-3,1.0))   # TODO SCI: Come up with better description of Kepler targets, maybe draw from real contaminations
  wf_id = 0
  # ch = rand(DiscreteUniform(1,84))              # Removed channel in favor of window function id
  ps = generate_planetary_system(star, sim_param)
  return KeplerTarget([ps],fill(cdpp_5hr,num_cdpp_timescales,num_quarters),contam,mission_data_span,mission_duty_cycle,0)
end

function test_target(sim_param::SimParam)
  generate_kepler_target_simple(sim_param)
  StellarTable.setup_star_table(sim_param)
  generate_kepler_target_from_table(sim_param)
end


# ----------------------------------------------------
# --- Global functions
# ----------------------------------------------------
FLAG    = false
UPDATE  = false
CURRENT = true
MAX_HOLD = false
FLAG_ERROR = 0
ERROR_STR = ""
nbSegMean = 24
radio = nothing

WINDOW_SIZE = (800,700); # Change it by @eval AbstractSDRsSpectrum WINDOW_SIZE=(400,200)
PLOT_SIZE = (700,500);
MAX_BANDWIDTH = 16;

# ----------------------------------------------------
# --- Main GUI core processing function
# ----------------------------------------------------
function mainGUI(sdr,carrierFreq,samplingRate,gain=20,nbSegMeanInit=24,p=nothing,maxHold=false;kwargs...)
    # --- Simulation parameters
    global ERROR_STR
    global FLAG_ERROR
    global FLAG;
    global UPDATE;
    global nbSegMean = nbSegMeanInit;
    global newCarrierFreq	  = carrierFreq;		# --- Starting frequency
    # --- Duration and buffer size
    duration                  = 0.5;
    nbSamples                 = Int( duration * samplingRate);
    # --- Update radio configuration
    try
        global radio			= openSDR(sdr,newCarrierFreq,samplingRate,gain;kwargs...);
    catch exception
        FLAG_ERROR = -1
        ERROR_STR = exception
    end
    # --- Create FIR filter
    nFFT      = 1024;
    nbSamples = 1024;
    buffer    = zeros(Complex{Cfloat},nbSamples);
    sig       = zeros(Complex{Cfloat},nFFT);
    sfM         = zeros(Cfloat,nFFT);
    maxS        = -120*ones(Cfloat,nFFT);
    y= zeros(Cfloat,nFFT);
    sF        = zeros(Cfloat,nFFT);
    α         = 1/ nbSegMean;
    x         = ((0:nFFT-1)/nFFT .- 0.5) .* samplingRate ./1e6;
    cnt = 0;
    FLAG = true;
    # --- Audio rendering
    try
        while(true)
            if UPDATE
                # --- We have update something, need to redo sme stuff
                nbSegMean == 0 ? α = 1 :  α         = 1/ nbSegMean;
                sfM .= 0;
                x         = ((0:nFFT-1)/nFFT .- 0.5) .* getSamplingRate(radio) ./1e6;
                UPDATE = false;
                maxS .= -120;
                maxHold = MAX_HOLD;
                deletetraces!(p,1);
            end
            # --- We get buffer
            # --- Getting signal
           cnt +=1
            recv!(buffer,radio);
            if nbSamples < nFFT
                sig[1:nbSamples] .= buffer;
            else
                sig[1:nFFT] .= buffer[1:nFFT];
            end
            global tmp = sig;
            # --- Apply | FFT(.) | ^2
            sF  .= abs2.(fftshift(fft(sig)));
            # --- Averaging
            sfM .= (1-α) .* sfM .+ α .* sF;
            y .= 10*log10.(sfM);
            maxS .= max.(maxS,y);
            # --- Plot
            if mod(cnt,100) == 0
                deletetraces!(p,0);
                if maxHold == true  && cnt > 1
                    deletetraces!(p,1);
                end
                pl1	  = PlotlyJS.scatter(; x,y, name=" ");
                addtraces!(p,pl1);
                if maxHold == true
                    maxS .= max.(maxS,y);
                    pl2	  = PlotlyJS.scatter(; x,y=maxS, name=" ");
                    addtraces!(p,pl2);
                    sleep(0.1);
                else
                    sleep(0.05);
                end
            end
            yield();
            # Exit
            if FLAG == false;
                break;
            end
        end
    catch exception;
        close(radio);
        FLAG_ERROR  = -2
        ERROR_STR = exception
        rethrow(exception)
    end
    close(radio);
    return 0
end

# ----------------------------------------------------
# --- GUI
# ----------------------------------------------------
function gui(;kw1...)
    global FLAG = false;
    global UPDATE = false;
    # ----------------------------------------------------
    # --- Define widgets
    # ----------------------------------------------------
    # --- Creating widget for FM frequencies
    # frequencies = widget(100:4:5000, label="FM frequencies (MHz)");
    widgetFrequencies = textbox(hint=""; value="800",label="Frequency (MHz)  ")
    # widgetFrequencies= widget(100:10:5000, label="Radio Frequencies (MHz)");
    widgetGain        = widget(0:50, label="Radio Gain");
    widgetAverage     = widget(0:512, label="Averaging window");
    widgetBandwidth   = widget(0:1:MAX_BANDWIDTH, label="Bandwidth (MHz)");
    # --- Creating some stuff related to args
    widgetArgs = textbox(hint=""; value="arg=\"addr=192.168.10.13\"");
    widgetMaxHold = checkbox(false,label="Max Hold");
    # --- Widget for radio configuration
    sdrList = AbstractSDRs.getSupportedSDRs();
    # options = Observable([:e310,:uhd,:radiosim])
    options = Observable(sdrList);
    wdg = dropdown(options);
    # --- Start button
    startButton = button("Start !"; value=0)
    configButton= button("Radio Config"; value=0,style = Dict("backgroundColor" => "#636EFA") );
    stopButton = button(" ! Stop"; value=0,style = Dict("backgroundColor" => "#EF553B") );
    # ---
    (wW,hW) = WINDOW_SIZE;
    # w = Window(Blink.@d(:width=>wW, :height=>hW));
    w = Window();
    title(w,"Spectrum viewer in Julia");
    # --- Horizontal axis
    layout = hbox(pad(1em,wdg),pad(1em,widgetArgs));
    layout = vbox(layout,widgetGain);
    layout = vbox(layout,widgetAverage);
    layout = vbox(layout,widgetFrequencies);
    layout = vbox(layout,widgetBandwidth);
    layout = hbox(layout,pad(1em,vbox(startButton,configButton,stopButton,widgetMaxHold)));
    #
    (wW,hW) = PLOT_SIZE;
    plotLayout = Layout(;
                        width = wW,
                        height = hW,
                        title=" Spectrum ",
                        xaxis_title=" Frequency MHz ",
                        yaxis_title=" Power ",
                        xaxis_showgrid=true, yaxis_showgrid=true,
                        yaxis_range =[-80,0]
                        )
    N = 100;
    x = 0:N-1;
    y = zeros(N);
    pl1	  = PlotlyJS.scatter(; x,y, name=" ");
    p = PlotlyJS.plot(pl1,plotLayout)
    layout = vbox(layout,p);
    body!(w,layout);
    # ----------------------------------------------------
    # ---
    # ----------------------------------------------------
    # --- Update Frequency
    on(updateC,widgetFrequencies);
    # --- Update Gain
    on(updateG,widgetGain);
    # --- Update average
    on(updateM,widgetAverage);
    on(updateB,widgetBandwidth);
    v = on(configButton) do click
        c = Dates.format(now(), "HH:MM:SS")
        println("--------\n $c New config radio");
        print(radio);
        if FLAG_ERROR < 0
            println("")
            println("An error has occured during AbstractSDRsSpectrum.gui()")
            println("$ERROR_STR")
        end
    end
    m = on(widgetMaxHold) do mm
        global MAX_HOLD = mm;
        UPDATE = true;
    end
    s = on(stopButton) do click
        if FLAG == true
            startButton["is-loading"][]=false;
            global FLAG = false;
            # close(w);
        end
    end
    # --- Starting routine
    task = Any
    h = on(startButton) do click
        startButton["is-loading"][]=true
        #@show click
        #if click == 1
        #    map!(fromStartToStop,startButton,12,16)
        #end
        sdr =  wdg[];
        carrierFreq = parse(Float64,widgetFrequencies[])*1e6
        # carrierFreq = widgetFrequencies[]*1e6;
        gain        = widgetGain[];
        nbSegMean   = widgetAverage[];
        samplingRate = widgetBandwidth[] *1e6;
        maxHold = widgetMaxHold[];
        # kwargs = (;args=widgetArgs[]);
        # --- Creating a dict with all keywords, given as a string
        str = widgetArgs[]
        argS = split(str,",")
        kwargs = Dict()
        for s in argS
            isempty(s) && break
            (k,p_) = split(s,"=")
            p_ = replace(p_,"\""=>"")
            push!(kwargs,Symbol(k)=>p_)
        end
        # kwargs = (;args=widgetArgs[]);
        @async task = mainGUI(sdr,carrierFreq,samplingRate,gain,nbSegMean,p,maxHold;kwargs...,kw1...);
    end
    success(w.shell.proc)
    global FLAG=false;
    close(radio);
    # We get the hand back and can close the radio
    return nothing
end

# ----------------------------------------------------
# --- Callbacks
# ----------------------------------------------------
function updateC(val)
    global FLAG, UPDATE, CURRENT
    if FLAG == true
        if val != ""
            carrierFreq = parse(Float64,val)*1e6;
            if CURRENT == true
                # When we write new value it can lead to several calls to updateCarrierFreq, not necessary well handle on the SDR side... We implement a soft lock here to be sure we do not call sevearl times updateCarrierFreq
                updateCarrierFreq!(radio,carrierFreq);
                UPDATE = true
                CURRENT = false
            end
            CURRENT = true
        end
    end
end
function updateG(val)
    global FLAG, UPDATE;
    if FLAG == true
        gain = val;
        updateGain!(radio,gain);
        UPDATE = true;
    end
end

function updateM(val)
    global nbSegMean = val;
    global UPDATE = true;
end
function updateB(val)
    global FLAG, UPDATE;
    if FLAG == true
        samplingRate = val*1e6;
        # carrierFreq = val*1e6;
        updateSamplingRate!(radio,samplingRate);
        UPDATE = true;
    end
end


module ElasticFDA

## Requirements
using Interpolations
using Dierckx
using NLopt
using ProgressMeter
using Distributions

export
    smooth_data!,
    smooth_data,
    f_to_srsf,
    srsf_to_f,
    optimum_reparam,
    warp_q_gamma,
    warp_f_gamma,
    vert_fPCA,
    horiz_fPCA,
    gauss_model,
    group_warping_bayes,
    pair_warping_bayes,
    elastic_distance,
    elastic_regression,
    elastic_logistic,
    elastic_mlogistic,
    elastic_prediction,
    srsf_align,
    align_fPCA,
    trapz,
    rgam,
    curve_karcher_mean,
    curve_srvf_align,
    curve_principal_directions,
    curve_karcher_cov,
    sample_shapes,
    curve_to_q,
    q_to_curve,
    calc_shape_dist,
    curve_pair_align,
    curve_geodesic,
    resamplecurve,
    reparam_image,
    pair_align_image,
    bs,
    project_curve,
    @cpp

# load fdasrsf library
if (is_unix())
    path = "../deps/src/fdasrsf/fdasrsf"
else
    path = "..\\deps\\fdasrsf"
end
const libfdasrsf = joinpath(dirname(@__FILE__), path)

# load gropt library
if (is_unix())
    path1 = "../deps/src/gropt/gropt"
else
    path1 = "..\\deps\\gropt"
end
const libgropt = joinpath(dirname(@__FILE__), path1)

# load fdaqmap library
if (is_unix())
    path2 = "../deps/src/fdaqmap/fdaqmap"
else
    path2 = "..\\deps\\fdaqmap"
end
const libfdaqmap = joinpath(dirname(@__FILE__), path2)

# Ensure library is available.
function __init__()
    if (Libdl.dlopen_e(libfdasrsf) == C_NULL)
        error("libfdasrsf not properly installed. Run Pkg.build(\"ElasticFDA\")")
    end

    if (Libdl.dlopen_e(libgropt) == C_NULL)
        error("libgropt not properly installed. Run Pkg.build(\"ElasticFDA\")")
    end

    if (Libdl.dlopen_e(libfdaqmap) == C_NULL)
        error("libfdaqmap not properly installed. Run Pkg.build(\"ElasticFDA\")")
    end

end

# CPP.jl workaround not compiling on windows
# the following macro was copied from https://github.com/timholy/Cpp.jl with the
# following modifications:
# 1) Update const for new julia v0.4 warnings
# 2) Formating
# the licensce for the code is included in the package directory
# Useful references:
# http://www.agner.org/optimize/calling_conventions.pdf
# http://mentorembedded.github.io/cxx-abi/abi.html#mangling

const SUBSTITUTION = [""; map(string, collect(0:9)); map(string, collect('A':'Z'))];

# Allow calling of functions in C++ shared libraries.
# Usage: a = @cpp ccall((:mysymbol,mylib),...)
macro cpp(ex)
    # If you get "undefined symbol" errors, use nm or readelf to view
    # the names of the symbols in the library. Then use the resulting
    # information to help improve this macro!
    # Note: for global objects without class or namespace qualifiers
    # (e.g., global consts), you do not need to use this macro (it
    # will work with a plain ccall, even though it is a C++ library)
    msg = "@cpp requires a ccall((:mysymbol, mylib),...)  expression"
    if !isa(ex,Expr) || ex.head != :ccall
        error(msg)
    end

    # Parse the library symbol's name
    exlib = ex.args[1]
    if !(isa(exlib,Expr) && exlib.head == :tuple)
        error(msg)
    end
    sym = exlib.args[1]
    fstr = string(eval(sym))
    #GNU3-4 ABI
    fstr = string("_Z",length(fstr),fstr)

    # Parse the arguments to ccall and construct the parameter type string
    exargtypes = ex.args[3]
    if exargtypes.head != :tuple
        error(msg)
    end
    exargs = exargtypes.args
    pstr = ""
    symtable = (:Void,:Bool,:Cchar,:Char,:String,:Int,:Int8,:Uint8,:Int16,:Uint16,:Int32,:Cint,:Uint32,:Int64,:Uint64,:Float32,:Float64)
    # GNU3-4 ABI v.3 and v.4
    ptable =   ('v', 'b', 'c', 'w', "Pc", 'i', 'a', 'h', 's', 't', 'i', 'i', 'j', 'l', 'm', 'f', 'd')
    msub = String[]
    for iarg = 1:length(exargs)
        thisarg = exargs[iarg]
        thisargm = ""
        while isa(thisarg,Expr) && thisarg.head == :curly && thisarg.args[1] == :Ptr
            thisargm = string(thisargm,'P')
            thisarg = thisarg.args[2]
        end
        matched = false
        for isym = 1:length(symtable)
            if thisarg == symtable[isym]
                matched = true
                thisargm = string(thisargm, ptable[isym])
                # Cchar is a special notation just for name mangling,
                # convert back to :Int8
                if thisarg == :Cchar
                    ex.args[3].args[iarg] = :Int8
                end
                break
            end
        end
        if matched
            if length(thisargm) > 1
                # Use substitution
                idx = indexin([thisargm], msub)[1]
                if idx != 0
                    pstr = string(pstr, "S"*SUBSTITUTION[idx]*"_")
                else
                    push!(msub, thisargm)
                    pstr = string(pstr, thisargm)
                end
            else
                pstr = string(pstr, thisargm)
            end
        else
            println(thisarg)
            error("@cpp: argument not recognized")
        end
    end
    ex.args[1].args[1] = Expr(:quote, Symbol(string(fstr,pstr)))
    ex.args[1].args[2] = esc(ex.args[1].args[2])
    for ival = 4:length(ex.args)
        ex.args[ival] = esc(ex.args[ival])
    end
    ex
end

### source files
include("curve_funcs.jl")
include("curve_stats.jl")
include("srsf_funcs.jl")
include("misc_funcs.jl")
include("fPCA.jl")
include("bspline.jl")
include("dp_bayes.jl")
include("mcmc_align.jl")
include("gauss_model.jl")
include("group_warping_bayes.jl")
include("pair_warping_bayes.jl")
include("regression.jl")
include("regression_funcs.jl")
include("time_warping.jl")
include("image_funcs.jl")
include("image_stats.jl")

end # module