import Base:in, +, -, *, ==, convert, isempty, isequal, length, push!, sizeof, append!
import SeisIO: GeoLoc, PZResp
export RawData

const rawfields = [:name, :loc, :fs, :gain, :freqmin, :freqmax, :cc_step,
                   :cc_len, :time_norm, :resp]

"""
   RawData

A structure for raw ambient noise data.

## Fields: RawData
| **Field** | **Description** |
|:--------------------|:--------------------|
| :name       | Freeform channel names |
| :id         | Channel ids. use NET.STA.LOC.CHAN format when possible. |
| :loc        | Location (position) object |
| :fs         | Sampling frequency in Hz. |
| :gain       | Scalar gain; divide data by the gain to convert to units  |
| :freqmin    | Minimum frequency from instrument response.  |
| :freqmax    | Maximum frequency from instrument response. |
| :cc_len     | Raw data window length in seconds. |
| :cc_step    | Spacing between windows in seconds. |
| :time_norm  | Apply one-bit whitening with "one_bit". |
| :resp       | Instrument response object, format [zeros poles] |
| :misc       | Dictionary for non-critical information. |
| :notes      | Timestamped notes; includes automatically-logged acquisition and |
|             | processing information. |
| :t          | Starttime of each window. |
| :x          | Raw data stored in columns. |
"""
mutable struct RawData
  name::String                                # name [Net.Sta.Loc.Chan]
  id::String                                  # id [Y-mm-dd] this is date of raw
  loc::GeoLoc                                 # loc
  fs::Float64                                 # sampling rate [Hz]
  gain::Float64                               # gain
  freqmin::Float64                            # minumum frequency [Hz]
  freqmax::Float64                            # maximum frequency [Hz]
  cc_len::Int                                 # window_length [s]
  cc_step::Int                                # step between windows [s]
  whitened::Bool                              # whitening applied
  time_norm::Union{Bool,String}               # time normaliation
  resp::PZResp                                # response poles/zeros
  misc::Dict{String,Any}                      # misc
  notes::Array{String,1}                      # notes
  t::Array{Float64,1}                         # time
  x::Array{<:Union{Float32,Float64},2}  # raw data

  function RawData(
      name     ::String,
      id       ::String,
      loc      ::GeoLoc,
      fs       ::Float64,
      gain     ::Float64,
      freqmin  ::Float64,
      freqmax  ::Float64,
      cc_len   ::Int,
      cc_step  ::Int,
      whitened ::Bool,
      time_norm::Union{Bool,String},
      resp     ::PZResp,
      misc     ::Dict{String,Any},
      notes    ::Array{String,1},
      t        ::Array{Float64,1},
      x        ::Array{<:Union{Float32,Float64},2}
      )

      return new(name, id, loc, fs, gain, freqmin, freqmax, cc_len, cc_step,
                 whitened, time_norm, resp, misc, notes, t, x)
    end

    function RawData(S::SeisData,cc_len::Int,cc_step::Int)
      merge!(S)
      # subset by time
      starttime, endtime = u2d.(nearest_start_end(S[1],cc_len, cc_step))
        # check if waveform length is < cc_len
      if Int(floor((endtime - starttime).value / 1000)) < cc_len
          return nothing
      end
      # sync start and end times
      x, starts = slide(sync(S[1],s=starttime,t=endtime) , cc_len, cc_step)
      return new(S[1].id,Dates.format(u2d(S[1].t[1,2]*1e-6),"Y-mm-dd"),S[1].loc,
                 S[1].fs,S[1].gain,1/cc_len,S[1].fs/2,cc_len,cc_step,false,
                 false,S[1].resp,S[1].misc,S[1].notes,starts,x)
    end

    function RawData(C::SeisChannel,cc_len::Int,cc_step::Int)
      # subset by time
      starttime, endtime = u2d.(nearest_start_end(C,cc_len, cc_step))
        # check if waveform length is < cc_len
      if Int(floor((endtime - starttime).value / 1000)) < cc_len
          return nothing
      end
      # sync start and end times
      x, starts = slide(sync(C,s=starttime,t=endtime), cc_len, cc_step)
      return new(C.id,Dates.format(u2d(C.t[1,2]*1e-6),"Y-mm-dd"),C.loc,C.fs,
                 C.gain,1/cc_len,C.fs/2,cc_len,cc_step,false,false, C.resp,
                 C.misc,C.notes,starts,x)
    end
end

RawData(;
          name     ::String                    = "",
          id       ::String                    = "",
          loc      ::GeoLoc                    = GeoLoc(),
          fs       ::Float64                   = zero(Float64),
          gain     ::Float64                   = one(Float64),
          freqmin  ::Float64                   = zero(Float64),
          freqmax  ::Float64                   = zero(Float64),
          cc_len   ::Int                       = zero(Int),
          cc_step  ::Int                       = zero(Int),
          whitened ::Bool                      = false,
          time_norm::Union{Bool,String}        = false,
          resp     ::PZResp                    = PZResp(),
          misc     ::Dict{String,Any}          = Dict{String,Any}(),
          notes    ::Array{String,1}           = Array{String,1}(undef, 0),
          t        ::Array{Float64,1}          = Array{Float64,1}(undef, 0),
          x      ::Array{<:Union{Float32,Float64},2} =
                     Array{Float32,2}(undef, 0, 2)
          ) = RawData(name, id, loc, fs, gain, freqmin, freqmax, cc_len, cc_step,
                      whitened, time_norm, resp, misc, notes, t, x)

in(s::String, R::RawData) = R.id==s

isempty(R::RawData) = min(R.name=="",R.id == "",isempty(R.loc),
                          R.fs == zero(typeof(F.fs)),
                          R.gain == one(typeof(F.gain)),
                          R.freqmin == zero(typeof(F.freqmin)),
                          R.freqmax == zero(typeof(F.freqmax)),
                          R.cc_len == zero(typeof(F.cc_len)),
                          R.cc_step == zero(typeof(F.cc_step)),
                          F.time_norm == false,
                          isempty(R.resp), isempty(R.misc),isempty(R.notes),
                          isempty(R.t),isempty(R.raw))

isequal(R::RawData, U::RawData) = minimum([hash(getfield(R,i))==hash(getfield(U,i)) for i in rawfields]::Array{Bool,1})
==(F::RawData, U::RawData) = isequal(R,U)::Bool

append!(R::RawData, U::RawData)  = (if R == U;
  setfield!(R, :t, vcat(getfield(R,:t), getfield(U,:t)));
  setfield!(R, :raw, hcat(getfield(R,:raw), getfield(U,:raw)))
  elseif isempty(R);
  [setfield!(R, i, getfield(U,i)) for i in rawfields];
  [setfield!(R, i, getfield(U,i)) for i in [:t,:raw]];
  end;
  return R)
+(R::RawData, U::RawData) = (T = deepcopy(R); return append!(T, U))

function sizeof(R::RawData)
s = sum([sizeof(getfield(R,f)) for f in rawfields])
if !isempty(R.notes)
  s += sum([sizeof(i) for i in R.notes])
end
if !isempty(R.misc)
  s += sum([sizeof(i) for i in values(R.misc)])
end
return s
end


using TxKit

function run_test(registry, context, dim, image)
    new_method(registry, "debug") do method
        params = DebugParams(0.75)
        compute(context, method, image, Ref(params))

        download(image)

        map_read(image) do array
            display(array[1,:,:,1])
            println()

            display(array[1,:,:,2])
            println()

            display(array[1,:,:,3])
            println()
            @assert all(array[1,:,:,3] .== 0.0)

            display(array[1,:,:,4])
            println()
            @assert all(array[1,:,:,4] .== 0.75)
        end
    end
end

new_registry() do registry
    new_context(:gpu) do context
        dim = ImageDim(6, 3, 1, 4)

        new_image(:gpu, dim, Float32, 2, context) do image
            run_test(registry, context, dim, image)
        end
    end
end

# vim: ft=julia:ts=4:sw=4:et


# This file was generated, do not modify it. # hide
X = rand(Float32, dim, 10^5)
Q = rand(Float32, dim, 10^3)
k = 10
nothing # hide


@testset "indicator" begin
    m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => []))
    @variable(m, x, CS.Integers([-3, 1, 2, 3]))
    @variable(m, y, CS.Integers([-3, 1, 2, 3]))
    @variable(m, b, Bin)
    @constraint(m, b => {x + y + 1 == 5})
    optimize!(m)
    com = CS.get_inner_model(m)
    constraint = get_constraints_by_type(com, CS.IndicatorConstraint)[1]

    @test CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [1, 2, 2])
    @test !CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [1, 2, 3])
    @test CS.is_constraint_solved(constraint, constraint.fct, constraint.set, [0, 2, 3])

    constr_indices = constraint.indices
    @test CS.still_feasible(
        com,
        constraint,
        constraint.fct,
        constraint.set,
        constr_indices[2],
        -3,
    )
    @test CS.still_feasible(
        com,
        constraint,
        constraint.fct,
        constraint.set,
        constr_indices[3],
        1,
    )
    # not actually feasible but will not be tested fully here
    CS.fix!(com, com.search_space[constr_indices[1]], 1)
    # will be tested when setting the next
    @test !CS.still_feasible(
        com,
        constraint,
        constraint.fct,
        constraint.set,
        constr_indices[3],
        -3,
    )

    # need to create a backtrack_vec to reverse pruning
    dummy_backtrack_obj = CS.BacktrackObj(com)
    push!(com.backtrack_vec, dummy_backtrack_obj)
    # reverse previous fix
    CS.reverse_pruning!(com, 1)
    com.c_backtrack_idx = 1
    # now setting it to 1 should be feasible
    @test CS.still_feasible(
        com,
        constraint,
        constraint.fct,
        constraint.set,
        constr_indices[3],
        -3,
    )

    @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set)
    for ind in constr_indices[2:3]
        @test sort(CS.values(com.search_space[ind])) == [-3, 1, 2, 3]
    end
    @test sort(CS.values(com.search_space[1])) == [0, 1]
    # feasible but remove -3
    @test CS.fix!(com, com.search_space[constr_indices[1]], 1)
    @test CS.prune_constraint!(com, constraint, constraint.fct, constraint.set)
    for ind in constr_indices[2:3]
        @test sort(CS.values(com.search_space[ind])) == [1, 2, 3]
    end
    CS.values(com.search_space[constr_indices[1]]) == [1]
end

@testset "indicator is_constraint_violated test" begin
    m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => []))
    @variable(m, b, Bin)
    @variable(m, -5 <= x[1:5] <= 5, Int)
    @constraint(m, b => {x in CS.GeqSet()})
    optimize!(m)
    com = CS.get_inner_model(m)

    constraint = com.constraints[1]

    variables = com.search_space
    @test CS.fix!(com, variables[constraint.indices[1]], 1; check_feasibility = false)
    @test CS.remove_above!(
        com,
        variables[constraint.indices[2]],
        3;
        check_feasibility = false,
    )
    @test CS.remove_below!(
        com,
        variables[constraint.indices[3]],
        4;
        check_feasibility = false,
    )
    @test CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set)

    m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => []))
    @variable(m, b, Bin)
    @variable(m, -5 <= x[1:5] <= 5, Int)
    @constraint(m, b => {x in CS.GeqSet()})
    optimize!(m)
    com = CS.get_inner_model(m)

    constraint = com.constraints[1]

    variables = com.search_space
    @test CS.fix!(com, variables[constraint.indices[1]], 1; check_feasibility = false)
    @test CS.remove_above!(
        com,
        variables[constraint.indices[2]],
        3;
        check_feasibility = false,
    )
    @test CS.remove_below!(
        com,
        variables[constraint.indices[3]],
        3;
        check_feasibility = false,
    )
    @test !CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set)

    m = Model(optimizer_with_attributes(CS.Optimizer, "no_prune" => true, "logging" => []))
    @variable(m, b, Bin)
    @variable(m, -5 <= x[1:5] <= 5, Int)
    @constraint(m, b => {x in CS.GeqSet()})
    optimize!(m)
    com = CS.get_inner_model(m)

    constraint = com.constraints[1]

    variables = com.search_space
    @test CS.fix!(com, variables[constraint.indices[1]], 0; check_feasibility = false)
    @test CS.remove_above!(
        com,
        variables[constraint.indices[2]],
        3;
        check_feasibility = false,
    )
    @test CS.remove_below!(
        com,
        variables[constraint.indices[3]],
        4;
        check_feasibility = false,
    )
    @test !CS.is_constraint_violated(com, constraint, constraint.fct, constraint.set)
end


include("abstract.jl")

"""
Analysis data of determinant quantum Monte Carlo (DQMC) simulation
"""
@with_kw mutable struct DQMCAnalysis
    acc_rate::Float64 = 0.
    prop_local::Int = 0
    acc_local::Int = 0
    acc_rate_global::Float64 = 0.
    prop_global::Int = 0
    acc_global::Int = 0
end

"""
Parameters of determinant quantum Monte Carlo (DQMC)
"""
@with_kw struct DQMCParameters
    global_moves::Bool = false
    global_rate::Int = 5
    thermalization::Int = 100 # number of thermalization sweeps
    sweeps::Int = 100 # number of sweeps (after thermalization)

    all_checks::Bool = true # e.g. check if propagation is stable/instable
    safe_mult::Int = 10

    delta_tau::Float64 = 0.1
    beta::Float64
    slices::Int = beta / delta_tau
    @assert isinteger(beta / delta_tau) string("beta/delta_tau", "
        (= number of imaginary time slices) must be an integer but is",
        beta / delta_tau, ".")

    measure_rate::Int = 10
end

"""
Determinant quantum Monte Carlo (DQMC) simulation
"""
mutable struct DQMC{M<:Model, CB<:Checkerboard, ConfType<:Any,
        Stack<:AbstractDQMCStack} <: MonteCarloFlavor
    model::M
    conf::ConfType
    s::Stack

    p::DQMCParameters
    a::DQMCAnalysis
    thermalization_measurements::Dict{Symbol, AbstractMeasurement}
    measurements::Dict{Symbol, AbstractMeasurement}

    DQMC{M, CB, ConfType, Stack}() where {M<:Model, CB<:Checkerboard,
        ConfType<:Any, Stack<:AbstractDQMCStack} = new()
end

include("stack.jl")
include("slice_matrices.jl")

"""
    DQMC(m::M; kwargs...) where M<:Model

Create a determinant quantum Monte Carlo simulation for model `m` with
keyword parameters `kwargs`.
"""
function DQMC(m::M;
        seed::Int=-1,
        checkerboard::Bool=false,
        thermalization_measurements = Dict{Symbol, AbstractMeasurement}(),
        measurements = :default,
        kwargs...
    ) where M<:Model
    # default params
    # paramskwargs = filter(kw->kw[1] in fieldnames(DQMCParameters), kwargs)
    p = DQMCParameters(; kwargs...)

    geltype = greenseltype(DQMC, m)
    heltype = hoppingeltype(DQMC, m)
    conf = rand(DQMC, m, p.slices)
    mc = DQMC{M, checkerboard ? CheckerboardTrue : CheckerboardFalse,
        typeof(conf), DQMCStack{geltype, heltype}}()
    mc.model = m
    mc.p = p
    mc.s = DQMCStack{geltype, heltype}()

    init!(
        mc, seed = seed, conf = conf,
        thermalization_measurements = thermalization_measurements,
        measurements = measurements
    )
    return mc
end

"""
    DQMC(m::M, params::Dict)
    DQMC(m::M, params::NamedTuple)

Create a determinant quantum Monte Carlo simulation for model `m` with
(keyword) parameters as specified in the dictionary/named tuple `params`.
"""
DQMC(m::Model, params::Dict{Symbol, T}) where T = DQMC(m; params...)
DQMC(m::Model, params::NamedTuple) = DQMC(m; params...)

# convenience
@inline beta(mc::DQMC) = mc.p.beta
@inline nslices(mc::DQMC) = mc.p.slices
@inline model(mc::DQMC) = mc.model
@inline conf(mc::DQMC) = mc.conf
@inline current_slice(mc::DQMC) = mc.s.current_slice

# cosmetics
import Base.summary
import Base.show
Base.summary(mc::DQMC) = "DQMC simulation of $(summary(mc.model))"
function Base.show(io::IO, mc::DQMC)
    print(io, "Determinant quantum Monte Carlo simulation\n")
    print(io, "Model: ", mc.model, "\n")
    print(io, "Beta: ", mc.p.beta, " (T ≈ $(round(1/mc.p.beta, sigdigits=3)))\n")
    N_th_meas = length(mc.thermalization_measurements)
    N_me_meas = length(mc.measurements)
    print(io, "Measurements: ", N_th_meas + N_me_meas, " ($N_th_meas + $N_me_meas)")
end
Base.show(io::IO, m::MIME"text/plain", mc::DQMC) = print(io, mc)

"""
    init!(mc::DQMC[; seed::Real=-1])

Initialize the determinant quantum Monte Carlo simulation `mc`.
If `seed !=- 1` the random generator will be initialized with `Random.seed!(seed)`.
"""
function init!(mc::DQMC;
        seed::Real = -1,
        conf = rand(DQMC,model(mc),nslices(mc)),
        thermalization_measurements = Dict{Symbol, AbstractMeasurement}(),
        measurements = :default
    )
    seed == -1 || Random.seed!(seed)

    mc.conf = conf

    init_hopping_matrices(mc, mc.model)
    initialize_stack(mc)

    mc.a = DQMCAnalysis()

    mc.thermalization_measurements = thermalization_measurements
    if measurements isa Dict{Symbol, AbstractMeasurement}
        mc.measurements = measurements
    elseif measurements == :default
        mc.measurements = default_measurements(mc, mc.model)
    else
        @warn(
            "`measurements` should be of type Dict{Symbol, AbstractMeasurement}, but is " *
            "$(typeof(measurements)). No measurements have been set."
        )
        mc.measurements = Dict{Symbol, AbstractMeasurement}()
    end

    nothing
end

"""
    run!(mc::DQMC[; verbose::Bool=true, sweeps::Int, thermalization::Int])

Runs the given Monte Carlo simulation `mc`.
Progress will be printed to `stdout` if `verbose=true` (default).
"""
function run!(mc::DQMC; verbose::Bool=true, sweeps::Int=mc.p.sweeps,
        thermalization=mc.p.thermalization)

    do_th_measurements = !isempty(mc.thermalization_measurements)
    do_me_measurements = !isempty(mc.measurements)
    !do_me_measurements && @warn(
        "There are no measurements set up for this simulation!"
    )
    total_sweeps = sweeps + thermalization

    start_time = now()
    verbose && println("Started: ", Dates.format(start_time, "d.u yyyy HH:MM"))

    # fresh stack
    verbose && println("Preparing Green's function stack")
    initialize_stack(mc) # redundant ?!
    build_stack(mc)
    propagate(mc)

    _time = time()
    verbose && println("\n\nThermalization stage - ", thermalization)
    do_th_measurements && prepare!(mc.thermalization_measurements, mc, mc.model)
    for i in 1:total_sweeps
        verbose && (i == thermalization + 1) &&println("\n\nMeasurement stage - ", sweeps)
        for u in 1:2 * nslices(mc)
            update(mc, i)

            # For optimal performance whatever is most likely to fail should be
            # checked first.
            if current_slice(mc) == nslices(mc) && i <= thermalization && mc.s.direction == -1 &&
                    iszero(mod(i, mc.p.measure_rate)) && do_th_measurements
                measure!(mc.thermalization_measurements, mc, mc.model, i)
            end
            if (i == thermalization+1)
                do_th_measurements && finish!(mc.thermalization_measurements, mc, mc.model)
                do_me_measurements && prepare!(mc.measurements, mc, mc.model)
            end
            if current_slice(mc) == nslices(mc) && mc.s.direction == -1 && i > thermalization &&
                    iszero(mod(i, mc.p.measure_rate)) && do_me_measurements
                measure!(mc.measurements, mc, mc.model, i)
            end

        end

        if mod(i, 10) == 0
            mc.a.acc_rate = mc.a.acc_rate / (10 * 2 * nslices(mc))
            mc.a.acc_rate_global = mc.a.acc_rate_global / (10 / mc.p.global_rate)
            sweep_dur = (time() - _time)/10
            if verbose
                println("\t", i)
                @printf("\t\tsweep dur: %.3fs\n", sweep_dur)
                @printf("\t\tacc rate (local) : %.1f%%\n", mc.a.acc_rate*100)
                if mc.p.global_moves
                  @printf("\t\tacc rate (global): %.1f%%\n", mc.a.acc_rate_global*100)
                  @printf("\t\tacc rate (global, overall): %.1f%%\n",
                    mc.a.acc_global/mc.a.prop_global*100)
                end
            end

            mc.a.acc_rate = 0.0
            mc.a.acc_rate_global = 0.0
            flush(stdout)
            _time = time()
        end
    end
    do_me_measurements && finish!(mc.measurements, mc, mc.model)

    mc.a.acc_rate = mc.a.acc_local / mc.a.prop_local
    mc.a.acc_rate_global = mc.a.acc_global / mc.a.prop_global

    end_time = now()
    verbose && println("Ended: ", Dates.format(end_time, "d.u yyyy HH:MM"))
    verbose && @printf("Duration: %.2f minutes", (end_time - start_time).value/1000. /60.)

    nothing
end

"""
    update(mc::DQMC, i::Int)

Propagates the Green's function and performs local and global updates at
current imaginary time slice.
"""
function update(mc::DQMC, i::Int)
    propagate(mc)

    # global move
    # if mc.p.global_moves && (current_slice(mc) == mc.p.slices &&
    #        mc.s.direction == -1 && iszero(mod(i, mc.p.global_rate)))
    #     mc.a.prop_global += 1
    #     b = global_move(mc, mc.model, mc.conf) # not yet in DQMC_optional, i.e. unsupported
    #     mc.a.acc_global += b
    # end

    # local moves
    sweep_spatial(mc)

    nothing
end

"""
    sweep_spatial(mc::DQMC)

Performs a sweep of local moves along spatial dimension at current
imaginary time slice.
"""
function sweep_spatial(mc::DQMC)
    m = model(mc)
    N = nsites(m)

    @inbounds for i in 1:N
        detratio, ΔE_boson, Δ = propose_local(mc, m, i, current_slice(mc), conf(mc))
        mc.a.prop_local += 1

        if abs(imag(detratio)) > 1e-6
            println("Did you expect a sign problem? imag. detratio: ",
                abs(imag(detratio)))
            @printf "%.10e" abs(imag(detratio))
        end
        p = real(exp(- ΔE_boson) * detratio)

        # Metropolis
        if p > 1 || rand() < p
            accept_local!(mc, m, i, current_slice(mc), conf(mc), Δ, detratio,
                ΔE_boson)
            mc.a.acc_rate += 1/N
            mc.a.acc_local += 1
        end
    end
    nothing
end

"""
    greens(mc::DQMC)

Obtain the current equal-time Green's function.

Internally, `mc.s.greens` is an effective Green's function. This method transforms
this effective one to the actual Green's function by multiplying hopping matrix
exponentials from left and right.
"""
function greens(mc::DQMC_CBFalse)
    eThalfminus = mc.s.hopping_matrix_exp
    eThalfplus = mc.s.hopping_matrix_exp_inv

    greens = copy(mc.s.greens)
    greens .= greens * eThalfminus
    greens .= eThalfplus * greens
    return greens
end
function greens(mc::DQMC_CBTrue)
    chkr_hop_half_minus = mc.s.chkr_hop_half
    chkr_hop_half_plus = mc.s.chkr_hop_half_inv

    greens = copy(mc.s.greens)

    @inbounds @views begin
        for i in reverse(1:mc.s.n_groups)
          greens .= greens * chkr_hop_half_minus[i]
        end
        for i in reverse(1:mc.s.n_groups)
          greens .= chkr_hop_half_plus[i] * greens
        end
    end
    return greens
end

include("DQMC_mandatory.jl")
include("DQMC_optional.jl")
include("measurements.jl")


function run_multilevel_from_file(filename, outputdir)
    inputdict = JSON.parsefile(filename, dicttype=Dict{Symbol, Any})
    input = loadinput(MultilevelInput, inputdict[:input])
    seeds = inputdict[:seeds]
    output = inputdict[:output]
    if inputdict[:repeat] > 1
        for i in 1:inputdict[:repeat]
            rng, seed = seeds !== nothing ? (MersenneTwister(seeds[i]), seeds[i]) : (MersenneTwister(), nothing)
            population = multilevel_simulation(input, rng, Symbol(inputdict[:simtype]))
            saveoutput(population, output, outputdir, seed, i)
        end
    else
        rng = MersenneTwister(seeds)
        population = multilevel_simulation(input, rng, Symbol(inputdict[:simtype]))
        saveoutput(population, output, outputdir, seeds, 1)
    end
end

function run_multilevel_from_file(filename, outputdir, id)
    inputdict = JSON.parsefile(filename, dicttype=Dict{Symbol, Any})
    input = loadinput(MultilevelInput, inputdict[:input])
    seed = (inputdict[:seeds])[id]
    output = inputdict[:output]
    rng = MersenneTwister(seed)
    population = multilevel_simulation(input, rng, Symbol(inputdict[:simtype]))
    saveoutput(population, output, outputdir, seed, id)
end

"""
    multilevel_simulation(input::MultilevelInput, rng::AbstractRNG = MersenneTwister;
        <keyword arguments>)

    Simulate a growing population of modules, until it reaches `maxmodules`.

    Start with a single module comprised of a single cell that grows via a branching process
    to size `input.modulesize`, then switches to a Moran process.
    New modules are created with rate `input.branchrate`, by sampling cells from the parent
    module. Return output as a Population.

    - if `simtype == :normal`, simulation is
    implemented by a Gillespie algorithm and runs until the number of modules exceeds
    input.maxmodules or time exceeds input.maxtime.

    - if `simtype == :fixedtime`, a list of modules is created (initially of length 1)
    and each module is simulated independently until input.maxtime is reached. New modules
    are added to the end of the list. This implementation is faster, but only works if we
    end the simulation at a fixed time (not fixed size). If the list of modules is bigger
    than input.maxmodules and error is thrown.

"""
function multilevel_simulation(input::MultilevelInput, rng::AbstractRNG=Random.GLOBAL_RNG,
    simtype=:normal)

    #if the population dies out we start a new simulation
    while true
        populationtracker = initialize_population(
            input.modulesize,
            clonalmutations=0
        )
        if simtype == :normal || simtype == "normal"
            populationtracker =
                simulate!(
                    populationtracker,
                    input.maxtime,
                    input.maxmodules,
                    input.b,
                    input.d,
                    input.bdrate,
                    input.branchrate,
                    input.modulesize,
                    input.branchinitsize,
                    rng
                )
        elseif simtype == :fixedtime || simtype == "fixedtime"
            populationtracker =
                simulatefixedtime!(
                    populationtracker,
                    input.maxtime,
                    input.maxmodules,
                    input.b,
                    input.d,
                    input.bdrate,
                    input.branchrate,
                    input.modulesize,
                    input.branchinitsize,
                    rng
                )
        else
            error("incorrect value for simtype")
        end
        if length(populationtracker) != 0
            break
        end
    end
    populationtracker =
        processresults!(populationtracker, input.μ, input.clonalmutations, rng)
    return Population(input, populationtracker)
end

"""
    simulate!(populationtracker, maxtime, maxmodules, b, d, bdrate, branchrate,
        modulesize, branchinitsize, rng)

Run a single multilevel simulation using the Gillespie algorithm, with the
`populationtracker` giving the initial state. Simulation runs until the module population
size reaches `maxmodules` or the age of the population reaches `maxtime`.

"""
function simulate!(populationtracker, maxtime, maxmodules, b, d, bdrate, branchrate,
    modulesize, branchinitsize, rng)

    mutID = getmutID(populationtracker[1].cells) #get id of next mutation
    t = age(populationtracker)
    while t < maxtime && length(populationtracker) < maxmodules
        populationtracker, mutID, t =
            update_population!(populationtracker, b, d, bdrate, branchrate, modulesize,
                branchinitsize, mutID, t, maxtime, rng)
        #returns empty list of modules if population dies out
        if length(populationtracker) == 0
            return populationtracker
        end
    end
    return populationtracker
end

"""
    update_population!(populationtracker, b, d, bdrate, branchrate, modulesize,
        branchinitsize, mutID, t, maxtime, rng)

Perform a single Gillespie step to advance the simulation of a multilevel population, and
increase the time t → t + Δt.

Calculates the transition rates for a Moran, birth, death or module branching event; then
selects from these transitions with probability proportional to rate. Time is increased by
`Δt ~ Exp(sum(transitionrates))`. If new time exceeds `maxtime` do not perform any transition.
"""

function update_population!(populationtracker, b, d, bdrate, branchrate, modulesize,
    branchinitsize, mutID, t, maxtime, rng)

    transitionrates = get_transitionrates(populationtracker, b, d,
        bdrate, branchrate, modulesize)
    t += exptime(rng, sum(transitionrates))
    #only update the population if t < maxtime
    if t < maxtime
        #choose transition type: 1=moran, 2=birth, 3=death, 4=branch
        transitionid = sample(
            rng,
            1:4,
            ProbabilityWeights(transitionrates ./ sum(transitionrates))
        )
        populationtracker, mutID = transition!(
            populationtracker,
            transitionid,
            modulesize,
            branchinitsize,
            mutID,
            t,
            1,
            rng
        )
    end
    return populationtracker, mutID, t
end

"""
    transition!(populationtracker, transitionid, modulesize, branchinitsize, mutID, t,
        μ, rng)

Perform a single transition step on `populationtracker`, determined by `transitionid`.
"""
function transition!(populationtracker, transitionid, modulesize, branchinitsize, mutID, t,
    μ, rng)

    if transitionid == 1
        _, mutID = moranupdate!(populationtracker, modulesize, mutID, t, μ, rng)
    elseif transitionid == 2
        _, mutID = birthupdate!(populationtracker, modulesize, mutID, t, μ, rng)
    elseif transitionid == 3
        deathupdate!(populationtracker, modulesize, t, rng)
    elseif transitionid == 4
        modulebranchingupdate!(populationtracker, modulesize, branchinitsize, t, rng)
    end
    return populationtracker, mutID
end

"""
    moranupdate!(populationtracker, modulesize, mutID, t, μ, rng)

Selects a homeostatic module uniformly at random to undergo a single Moran update. From
that module one cell divides and one cell dies.
"""
function moranupdate!(populationtracker, modulesize, mutID, t, μ, rng)
    moduletracker, parentcell, deadcell =
        choose_homeostaticmodule_cells(populationtracker, modulesize, rng)
    _, mutID = celldivision!(moduletracker, parentcell, mutID, μ, rng)
    celldeath!(moduletracker, deadcell)
    updatemodulehistory!(moduletracker, 0, t)
    return populationtracker, mutID
end

"""
    birthupdate!(populationtracker, modulesize, mutID, t, μ, rng)

Selects a cell uniformly at random from all cells in non-homeostatic modules to divide.
"""
function birthupdate!(populationtracker, modulesize, mutID, t, μ, rng)
    moduletracker, parentcell =
        choose_growingmodule_cell(populationtracker, modulesize, rng)
    _, mutID = celldivision!(moduletracker, parentcell, mutID, μ, rng, fixedmu=true)
    updatemodulehistory!(moduletracker, 1, t)
    return populationtracker, mutID
end

"""
    deathupdate!(populationtracker, modulesize, t, rng)

Selects a cell uniformly at random from all cells in non-homeostatic modules to die. If
cell death results in an empty module, remove that module from the population.
"""
function deathupdate!(populationtracker, modulesize, t, rng)
    moduletracker, deadcell =
        choose_growingmodule_cell(populationtracker, modulesize, rng)
    celldeath!(moduletracker, deadcell)
    updatemodulehistory!(moduletracker, -1, t)
    if length(moduletracker) == 0
        moduledeath!(populationtracker, moduletracker)
    end
    return populationtracker
end

"""
    modulebranchingupdate!(populationtracker, modulesize, branchinitsize, t, rng)
Select a homeostatic module, uniformly at random, to undergo branching. Cells are sampled
(number of cells giving by `branchinitsize`) from the parent module to form a new module,
which is added to `populationtracker`
"""
function modulebranchingupdate!(populationtracker, modulesize, branchinitsize, t, rng)
    parentmodule = choose_homeostaticmodule(populationtracker, modulesize, rng)
    _, newmodule =
        sample_new_module!(parentmodule, length(populationtracker) + 1,
            branchinitsize, t, rng)
    push!(populationtracker, newmodule)
    return populationtracker
end

"""
    updatemodulehistory!(moduletracker, ΔN, newtime)
Adds new population size (old size + `ΔN`) and `newtime` to the end of `moduletracker.Nvec`
and `moduletracker.tvec`.

"""
function updatemodulehistory!(moduletracker, ΔN, newtime)
    push!(moduletracker.Nvec, moduletracker.Nvec[end] + ΔN)
    push!(moduletracker.tvec, newtime)
    return moduletracker
end

"""
    choose_homeostaticmodule(populationtracker, maxmodulesize, rng::AbstractRNG)
Select a homeostatic module, i.e. module of size `maxmodulesize`, uniformly at random.
"""
function choose_homeostaticmodule(populationtracker, maxmodulesize, rng::AbstractRNG)
    homeostatic_modules = filter(x -> length(x) == maxmodulesize, populationtracker)
    return rand(rng, homeostatic_modules)
end

"""
    choose_homeostaticmodule_cells(populationtracker, maxmodulesize, rng::AbstractRNG)
Select a homeostatic module and two cells from the module, uniformly at random.
"""
function choose_homeostaticmodule_cells(populationtracker, maxmodulesize, rng::AbstractRNG)
    chosenmodule = choose_homeostaticmodule(populationtracker, maxmodulesize, rng)
    return chosenmodule, rand(rng, 1:maxmodulesize), rand(rng, 1:maxmodulesize)
end

"""
    choose_growingmodule_cells(populationtracker, maxmodulesize, rng::AbstractRNG)
Select a cell unifor´mly at random from all cells in growing (non-homeostatic) modules, and
return the module and cell.
"""
function choose_growingmodule_cell(populationtracker, maxmodulesize, rng::AbstractRNG)
    modulesizes = map(length, populationtracker)
    modules = populationtracker[modulesizes .< maxmodulesize]
    modulesizes = modulesizes[modulesizes .< maxmodulesize]
    chosenmodule = sample(rng, modules, ProbabilityWeights(modulesizes ./ sum(modulesizes)))
    chosencell = rand(rng, 1:length(chosenmodule))
    return chosenmodule, chosencell
end

"""
    get_transitionrates(populationtracker, b, d, bdrate, branchrate, modulesize)
Compute the rates for moran update, cell division, death and module branching and return as
a Vector.
"""
function get_transitionrates(populationtracker, b, d, bdrate, branchrate, modulesize)
    rates = zeros(Float64, 4)
    number_homeostatic_modules = 0
    for (i, moduletracker) in enumerate(populationtracker)
        N = length(moduletracker)
        if N < modulesize
            rates[2] += N * b
            rates[3] += N * d
        elseif N == modulesize
            number_homeostatic_modules += 1
        else
            error("module size exceeds homeostatic size")
        end
    end
    rates[1] = number_homeostatic_modules * bdrate * modulesize
    rates[4] = number_homeostatic_modules * branchrate
    return rates
end

"""
    moduledeath!(populationtracker, moduletracker)
Remove the given `moduletracker` from `populationtracker`.
"""
function moduledeath!(populationtracker, moduletracker)
    return deleteat!(populationtracker, findall(x->x==moduletracker, populationtracker))
end

"""
    simulatefixedtime!(populationtracker, maxtime, maxmodules, b, d, bdrate, branchrate,
        modulesize, branchinitsize, rng)

Run a single multilevel simulation until `maxtime`, with the `populationtracker` giving the
initial state. Faster than `simulate!`, but cannot run to fixed population size.
"""
function simulatefixedtime!(populationtracker, maxtime, maxmodules, b, d, bdrate, branchrate,
    modulesize, branchinitsize, rng)

    for moduletracker in populationtracker
        check_module_number(populationtracker, maxmodules)
        while true
            moduletracker, newmoduletracker =
                module_simulate_to_branching!(
                    moduletracker,
                    maxtime,
                    b,
                    d,
                    bdrate,
                    branchrate,
                    modulesize,
                    branchinitsize,
                    length(populationtracker) + 1,
                    rng
                )

            if newmoduletracker !== nothing
                push!(populationtracker, newmoduletracker)
            elseif length(moduletracker) == 0
                moduledeath!(populationtracker, moduletracker)
                break
            else
                break
            end
        end
    end
    return populationtracker
end

"""
    check_module_number(populationtracker, maxmodules)
Throws an error if the number of modules in `populationtracker` exceeds `maxmodules`.
"""
function check_module_number(populationtracker, maxmodules)
    numbermodules = length(populationtracker)
    if numbermodules >= maxmodules
        error("population size exceeds maxmodules: ", numbermodules, " > ", maxmodules)
    end
end

"""
    module_simulate_to_branching(moduletracker, maxtime, b, d, bdrate, branchrate,
        modulesize, branchinitsize, newmoduleid, rng)

    Simulates moduletracker dynamics until a module branching event occurs, or maxtime is
    reached.

    If moduletracker is smaller than `modulesize` it grows according to a branching
    process, with birthrate `b` and deathrate `d`, until it reaaches `modukesize`. Once it
    reaches `modulesize` the dynamic switches to a Moran process with event rate `bdrate`.

"""

function module_simulate_to_branching!(moduletracker, maxtime, b, d, bdrate,
    branchrate, modulesize, branchinitsize, newmoduleid, rng)

    #branching process until module reaches homeostatic size
    if length(moduletracker) < modulesize
        moduletracker =
            branchingprocess!(moduletracker, b, d, modulesize, 1,
                rng, numclones=0, fixedmu=true, maxclonesize=Inf, tmax=maxtime)
        if length(moduletracker) == 0
            return moduletracker, nothing
        end
    end

    #get time of next branching event
    branchtime = 1 / branchrate .* exptime(rng) + moduletracker.tvec[end]

    #if module branching will occur before maxime is reached, simulate moran process until
    #branchtime and sample new module
    if branchtime < maxtime
        moduletracker =
            moranprocess!(moduletracker, bdrate, branchtime, 1, rng, numclones=0,
                fixedmu=true)

        moduletracker, newmoduletracker =
            sample_new_module!(moduletracker, newmoduleid, branchinitsize, branchtime, rng)

        return moduletracker, newmoduletracker

    #if module branching will occur after maxime is reached, simulate moran process until
    #maxtime
    else
        moduletracker =
            moranprocess!(moduletracker, bdrate, maxtime, 1, rng, numclones=0,
                fixedmu=true)
        return moduletracker, nothing
    end
end

"""
    sample_new_module(moduletracker, newmoduleid, branchinitsize, branchtime,
        rng::AbstractRNG)

Sample cells, uniformly at random, from `moduletracker` to form `newmoduletracker`. The
number of cells is given by `branchinitsize` and the newmodule is given initial time
`branchtime`.

"""
function sample_new_module!(moduletracker, newmoduleid, branchinitsize, branchtime,
    rng::AbstractRNG)

    sampleids = sample(rng, 1:length(moduletracker.cells), branchinitsize, replace=false)
    newmoduletracker =
        initializesim_from_cells(moduletracker.cells[sampleids], moduletracker.subclones,
            newmoduleid, moduletracker.id, inittime=branchtime)
    moduletracker = celldeath!(moduletracker, sampleids)
    push!(moduletracker.Nvec, moduletracker.Nvec[end] - branchinitsize)
    push!(moduletracker.tvec, branchtime)

    return moduletracker, newmoduletracker
end

"""
    initialize_population([modulesize=nothing]; clonalmutations=0)

Create a Vector{ModuleTracker} of length 1 that contains a single module comprising a single
cell at time t=0.
"""
function initialize_population(modulesize=nothing; clonalmutations=0)

    #population is defined by ModuleTracker vector with each entry coprresponding to
    #an individual module
    population = ModuleTracker[]

    #initialise population with a single module conisting of a single cell
    initialmodule = initializesim_branching(
        modulesize,
        clonalmutations=clonalmutations
    )
    push!(population, initialmodule)

    return population
end


function mulαβ!(C::Matrix{T}, A::Matrix{T}, adjB::Adjoint{T,<:Matrix{T}},
        α=true, β=false) where {T<:BlasFloat}
    BLAS.gemm!('N', 'C', T(α), A, adjB.parent, T(β), C)
end

function mulαβ!(C::Matrix{T}, A::Matrix{T}, adjB::Diagonal{T,S},
        α=true, β=false) where {T<:BlasFloat,S}
    # adapted from LinearAlgebra/src/diagonal.jl in 1.3
    C .= (A .* permutedims(adjB.diag)) .* α .+ C .* β
end

function mulαβ!(C::SparseMatrixCSC{T}, A::SparseMatrixCSC{T}, adjB::Diagonal{T,S},
        α=true, β=false) where {T<:BlasFloat,S}
    # adapted from LinearAlgebra/src/diagonal.jl in 1.3
    C .= (A .* permutedims(adjB.diag)) .* α .+ C .* β
end

function mulαβ!(C::Matrix{T}, A::SparseMatrixCSC{T}, adjB::Adjoint{T,<:SparseMatrixCSC{T}},
        α=true, β=false) where T <: Number
    B = adjB.parent
    B.m == size(C, 2) && A.m == size(C, 1) && A.n == B.n || throw(DimensionMismatch(""))
    anz = nonzeros(A)
    arv = rowvals(A)
    bnz = nonzeros(B)
    brv = rowvals(B)
    isone(β) || rmul!(C, β)
    @inbounds for j = 1:A.n
        for ib in nzrange(B, j)
            αbnz = α * bnz[ib]
            jj = brv[ib]
            for ia in nzrange(A, j)
                C[arv[ia], jj] += anz[ia] * αbnz
            end
        end
    end
    C
end

mulαβ!(C::Matrix{T}, A::BlockedSparse{T}, adjB::Adjoint{T,<:BlockedSparse{T}}, α=true,
       β=false) where {T} = mulαβ!(C, A.cscmat, adjB.parent.cscmat', α, β)

function mulαβ!(C::Matrix{T}, A::SparseMatrixCSC{T}, adjB::Adjoint{T,Matrix{T}},
        α=true, β=false) where {T}
    B = adjB.parent
    A.n == size(B, 2) || throw(DimensionMismatch())
    A.m == size(C, 1) || throw(DimensionMismatch())
    size(B, 1) == size(C, 2) || throw(DimensionMismatch())
    nzv = A.nzval
    rv = A.rowval
    if β != 1
        β != 0 ? rmul!(C, β) : fill!(C, zero(eltype(C)))
    end
    for k = 1:size(C, 2)
        @inbounds for col = 1:A.n
            αxj = α*B[k,col]
            for j = nzrange(A, col)
                C[rv[j], k] += nzv[j]*αxj
            end
        end
    end
    C
end

mulαβ!(C::Matrix{T}, A::BlockedSparse{T}, adjB::Adjoint{T,<:Matrix{T}}, α=true, β=false) where {T} =
    mulαβ!(C, A.cscmat, adjB, α, β)

function mulαβ!(C::SparseMatrixCSC{T}, A::SparseMatrixCSC{T}, adjB::Adjoint{T,<:SparseMatrixCSC{T}},
        α=true, β=false) where {T}
    B = adjB.parent
    C.m == A.m && C.n == B.m && A.n == B.n || throw(DimensionMismatch(""))
    Anz = nonzeros(A)
    Bnz = nonzeros(B)
    Cnz = nonzeros(C)
    isone(β) || rmul!(Cnz, β)
    Arv = rowvals(A)
    Brv = rowvals(B)
    Crv = rowvals(C)
    for j in 1:A.n
        for K in nzrange(B, j)
            k = Brv[K]
            alphabjk = α * Bnz[K]
            colkfirstr = Int(C.colptr[k])
            colklastr = Int(C.colptr[k + 1] - 1)
            for I in nzrange(A, j)
                i = Arv[I]
                searchk = searchsortedfirst(Crv, i, colkfirstr, colklastr, Base.Order.Forward)
                if searchk <= colklastr && Crv[searchk] == i
                    Cnz[searchk] += alphabjk * Anz[I]
                else
                    throw(ArgumentError("C does not have the nonzero pattern of A*B'"))
                end
            end
        end
    end
    C
end

mulαβ!(C::BlockedSparse{T}, A::BlockedSparse{T}, adjB::Adjoint{T,<:BlockedSparse{T}}, α=true, β=false) where {T} =
    mulαβ!(C.cscmat, A.cscmat, adjB.parent.cscmat', α, β)

function mulαβ!(C::StridedVecOrMat{T}, A::StridedVecOrMat{T}, adjB::Adjoint{T,<:SparseMatrixCSC{T}},
        α=true, β=false) where T
    B = adjB.parent
    m, n = size(A)
    p, q = size(B)
    r, s = size(C)
    r == m && s == p && n == q || throw(DimensionMismatch(""))
    isone(β) || rmul!(C, β)
    nz = nonzeros(B)
    rv = rowvals(B)
    @inbounds for j in 1:q, k in nzrange(B, j)
        rvk = rv[k]
        anzk = α * nz[k]
        for jj in 1:r
            C[jj, rvk] += A[jj, j] * anzk
        end
    end
    C
end

mulαβ!(C::StridedVecOrMat{T}, A::StridedVecOrMat{T}, adjB::Adjoint{T,<:BlockedSparse{T}},
    α=true, β=false) where {T} = mulαβ!(C, A, adjB.parent.cscmat', α, β)

mulαβ!(C::StridedVector{T}, adjA::Adjoint{T,<:StridedMatrix{T}}, B::StridedVector{T},
    α=true, β=false) where {T<:BlasFloat} = BLAS.gemv!('C', T(α), adjA.parent, B, T(β), C)

mulαβ!(C::StridedVector{T}, adjA::Adjoint{T,<:SparseMatrixCSC{T}}, B::StridedVector{T},
    α=true, β=false) where {T} = mul!(C, adjA, B, T(α), T(β))

mulαβ!(C::StridedVector{T}, adjA::Adjoint{T,<:BlockedSparse{T}}, B::StridedVector{T},
    α=true, β=false) where {T} = mulαβ!(C, adjA.parent.cscmat', B, α, β)

function LinearAlgebra.ldiv!(adjA::Adjoint{T,<:LowerTriangular{T,UniformBlockDiagonal{T}}},
        B::StridedVector{T}) where {T}
    A = adjA.parent
    length(B) == size(A, 2) || throw(DimensionMismatch(""))
    m, n, k = size(A.data.data)
    fv = A.data.facevec
    bb = reshape(B, (n, k))
    for j in 1:k
        ldiv!(adjoint(LowerTriangular(fv[j])), view(bb, :, j))
    end
    B
end

function LinearAlgebra.rdiv!(A::Matrix{T},
        adjB::Adjoint{T,<:LowerTriangular{T,UniformBlockDiagonal{T}}}) where T
    Bd = adjB.parent.data
    m, n, k = size(Bd.data)
    size(A, 2) == size(Bd, 1) && m == n || throw(DimensionMismatch(""))
    inds = 1:m
    for (i, f) in enumerate(Bd.facevec)
        BLAS.trsm!('R', 'L', 'T', 'N', one(T), f, view(A, :, inds .+ m * (i-1)))
    end
    A
end

function LinearAlgebra.rdiv!(A::BlockedSparse{T,S,P},
        B::Adjoint{T,<:LowerTriangular{T,UniformBlockDiagonal{T}}}) where {T,S,P}
    Bpd = B.parent.data
    j,k,l = size(Bpd.data)
    cbpt = A.colblkptr
    nzv = A.cscmat.nzval
    P == j == k && length(cbpt) == (l + 1) || throw(DimensionMismatch(""))
    for (j,f) in enumerate(Bpd.facevec)
        rdiv!(reshape(view(nzv, cbpt[j]:(cbpt[j + 1] - 1)), :, P), adjoint(LowerTriangular(f)))
    end
    A
end


type DumbFoldAssigner <: FoldAssigner end
let
    @test_throws Exception assign_folds(DumbFoldAssigner(), 5, 2)

    assignment = assign_folds(RandomFoldAssigner(), 4, 2)
    @test length(find(assignment .== 1)) == 2
    @test length(find(assignment .== 2)) == 2

    assignment = assign_folds(RandomFoldAssigner(true), 5, 2)
    @test length(find(assignment .== 1)) == 3
    @test length(find(assignment .== 2)) == 2

    assignment = assign_folds(RandomFoldAssigner(false), 5, 2)
    @test length(find(assignment .== 1)) == 2
    @test length(find(assignment .== 2)) == 2

    @test assign_folds(OrderedFoldAssigner(), 4, 2) == [1, 1, 2, 2]
    @test assign_folds(OrderedFoldAssigner(true), 4, 3) == [1, 1, 2, 3]
    @test assign_folds(OrderedFoldAssigner(true), 5, 2) == [1, 1, 1, 2, 2]
    @test assign_folds(OrderedFoldAssigner(false), 5, 2) == [1, 1, 2, 2, 0]
end


export iauCpv
"""
Copy a position/velocity vector.

This function is part of the International Astronomical Union's
SOFA (Standards Of Fundamental Astronomy) software collection.

Status:  vector/matrix support function.

Given:
   pv     double[2][3]    position/velocity vector to be copied

Returned:
   c      double[2][3]    copy

Called:
   iauCp        copy p-vector

This revision:  2013 June 18

SOFA release 2018-01-30

Copyright (C) 2018 IAU SOFA Board.  See notes at end.
"""

# void iauCpv(double pv[2][3], double c[2][3])

function iauCpv(pv::AbstractMatrix{<:Real})

   # Allocate return value
   c = zeros(Float64, 3, 2)

   ccall((:iauCpv, libsofa_c), Cvoid,
         (Ptr{Cdouble}, Ptr{Cdouble}),
         convert(Array{Float64, 2}, pv'), c)

   return SMatrix{2,3}(c')
end


export AbstractAtmosProblem, AtmosProblem

include("boundaryconditions.jl")

abstract type AbstractAtmosProblem <: AbstractProblem end

"""
    AtmosProblem

The default problem definition (initial and boundary conditions)
for `AtmosModel`.
"""
struct AtmosProblem{BCS, ISP, ISA} <: AbstractAtmosProblem
    "Boundary condition specification"
    boundaryconditions::BCS
    "Initial condition (function to assign initial values of prognostic state variables)"
    init_state_prognostic::ISP
    "Initial condition (function to assign initial values of auxiliary state variables)"
    init_state_auxiliary::ISA
end

function AtmosProblem(;
    physics = nothing,
    boundaryconditions = nothing,
    init_state_prognostic::ISP = nothing,
    init_state_auxiliary::ISA = atmos_problem_init_state_auxiliary,
) where {ISP, ISA}
    @assert init_state_prognostic ≠ nothing
    if boundaryconditions == nothing
        @assert physics ≠ nothing
        boundaryconditions = (AtmosBC(physics), AtmosBC(physics))
    end

    problem = (boundaryconditions, init_state_prognostic, init_state_auxiliary)

    return AtmosProblem{typeof.(problem)...}(problem...)
end

atmos_problem_init_state_auxiliary(
    problem::AtmosProblem,
    model::AtmosModel,
    aux::Vars,
    geom::LocalGeometry,
) = nothing


module Unicode

using ParserCombinator
using GenericRegex
using GenericRegex.IR.Graph; G = GenericRegex.IR.Graph

make_rpt(lo) = p -> G.Repeat(p, lo, typemax(Int))
make_rpt(lo, hi) = p -> G.Repeat(p, lo, hi)

Literal(s::AbstractString) = (@assert length(s) == 1; G.Range(s[1], s[1]))

function make_pattern()

    @with_names begin

        # group numbering is tricky - we need to number fomr the left and
        # avoid repetitions on backtracking.  so push entries (keyed by
        # iter) to a stack on entering the group, pop on leaving.
        group_count = Dict{Any, Int}()
        group_stack = []
        group_popped = Dict{Any, Int}()
        function pre_group(i, p)
            if !haskey(group_count, i)
                group_count[i] = length(group_count) + 1
                push!(group_stack, group_count[i])
            end
            p
        end
        function post_group(i, p)
            if !haskey(group_popped, i)
                group_popped[i] = pop!(group_stack)
            end
            G.Group{Char}(group_popped[i], p)
        end

        # need to specify {Char} here as handling Any[] from parser
        make_sequence(p) = length(p) == 1 ? p[1] : G.Sequence{Char}(p)
        make_choice(p) = length(p) == 1 ? p[1] : G.Choice{Char}(p)

        literal = p"[^[\].*+\\|(){}?]"                     > Literal
        escaped = ~Equal("\\") + Dot()                     > G.Literal
        wild = E"."                                        > () -> G.Wild(Char)
        outseq = Delayed()

        atom = literal | escaped | wild | outseq
        plus = atom + E"+"                                 > make_rpt(1)
        star = atom + E"*"                                 > make_rpt(0)
        opt = atom + E"?"                                  > make_rpt(0, 1)
        once = atom + !(E"*"|E"+"|E"?")

        inseq = Plus(plus | star | opt | once)            |> make_sequence
        choice = PlusList(inseq, E"|")                    |> make_choice

        open = ITransform(E"("+ !(e"?") ,                    pre_group)
        gchoice = IApp(open + choice + E")",                 post_group)
        nchoice = E"(?:" + choice + E")"

        outseq.matcher = Plus(gchoice | nchoice)          |> make_sequence

        return choice + Eos()

    end

end

end


__precompile__()

module NESSie

import Base: cos, sign, seek
import LinearAlgebra.BLAS: gemv!, gemv, axpy!, gemm

using Distances: euclidean
using LinearAlgebra: ⋅, ×, norm, rmul!

include("base/constants.jl")
export ε0, Option, defaultopt

include("base/model.jl")
export Element, SurfaceElement, VolumeElement, Triangle, Tetrahedron, Charge, Model

include("base/quadrature.jl")
export QuadraturePoints, QuadPts2D, QuadPts3D, quadraturepoints

include("base/util.jl")
export meshunion, obspoints_line, obspoints_plane

include("base/potentials.jl")
export PotentialType, SingleLayer, DoubleLayer, LocalityType, LocalES, NonlocalES, φmol,
    ∂ₙφmol, ∇φmol

# Submodules
include("Format.jl")
export Format

include("Radon.jl")
export Radon

include("Rjasanow.jl")
export Rjasanow

include("BEM.jl")
export BEM

include("TestModel.jl")
export TestModel

end # module


#!/usr/bin/env julia
################################################################################
#
#  Copyright (c) 2015 Wojciech Migda
#  All rights reserved
#  Distributed under the terms of the Apache 2.0 license
#
################################################################################
#
#  Filename: Worker.jl
#
#  Decription:
#       Generate MFCC feature vectors
#
#  Authors:
#       Wojciech Migda
#
################################################################################
#
#  History:
#  --------
#  Date         Who  Ticket     Description
#  ----------   ---  ---------  ------------------------------------------------
#  2015-09-20   wm              Initial version
#
################################################################################

module Worker

export reduce

using MFCC

const THIS_DIR = dirname(Base.source_path())

@windows? (
begin
    ENV["PATH"] = "$(ENV["PATH"]);$(THIS_DIR);$(THIS_DIR)/../external/bin"
    #println(ENV["PATH"])
    const LIB_MP3_DECODER = "libmp3decoder"
end
:
begin
    const LIB_MP3_DECODER = find_library(["libmp3decoder"], [THIS_DIR, "$(THIS_DIR)/../external/bin"])
end
)

type Mp3Params
    rate::Uint32
    channels::Uint32
    encoding::Uint32
end

function mp3decoder!(ifname::String, osig::AbstractVector{Int16})
    # we expect 10 secs of samples, int16, stereo at 44100 Hz
    mp3params = Mp3Params(uint32(0), uint32(0), uint32(0))

    const NREAD =
        ccall((:mp3decoder, LIB_MP3_DECODER),
            Csize_t, (Ptr{Uint8}, Ptr{Void}, Csize_t, Ptr{Mp3Params}),
            ifname, osig, size(osig, 1) * sizeof(osig[1]), &mp3params)
    @assert mp3params.encoding == 0xD0 # MPG123_ENC_SIGNED_16

    return NREAD, mp3params
end

function downsample{_iTp, _oTp<:FloatingPoint}(
    # code_native(downsample, (Int, Bool, Vector{Int}, Vector{Float64}))
    ratio::Int,
    tomono::Bool,
    ivec::AbstractVector{_iTp},
    ovec::AbstractVector{_oTp})

    const STRIDE::Int = ratio * (tomono ? 2 : 1)
    const ILEN::Int = size(ivec, 1)

    resize!(ovec, div(ILEN, STRIDE))

    opos::Int = 1
    ipos::Int = 1
    @simd for ipos = 1:STRIDE:ILEN - STRIDE + 1
        @inbounds ovec[opos] = 0
        @simd for sub = 0:STRIDE - 1
            @inbounds ovec[opos] += ivec[ipos + sub]
        end
        @inbounds ovec[opos] /= STRIDE

        opos += 1
    end
end

function gen_mfcc{_Tp}(MP3_DIR::String, TRAIN_X::AbstractVector{_Tp}, start::Int;
                       _winlen=0.025, _winstep=0.01, _numcep=13,
                       _nfilt=26, _nfft=512, _preemph=0.97, _ceplifter=22, _appendEnergy=true)

    const DATA_DIR = "$(THIS_DIR)/../../data"
    const TRAIN_MP3 = MP3_DIR

    const NSAMP = 10 * 44100
    const NCHAN = 2
    full_sig = Array(Int16, NSAMP * NCHAN)
    sig = similar(full_sig, Float64)

    vvX = Vector{Float64}[]

    for irow = 1:size(TRAIN_X, 1)
        const NREAD, MP3PARAMS = mp3decoder!("$(TRAIN_MP3)/$(TRAIN_X[irow])", full_sig)
        @inbounds full_sig[1 + NREAD / 2:end] = 0

        const DOWN_RATIO = 4
        downsample(DOWN_RATIO, true, full_sig, sig)

        mfcc_feat = mfcc(sig, samplerate=MP3PARAMS.rate / float(DOWN_RATIO),
                         winlen=_winlen, winstep=_winstep, numcep=_numcep, nfilt=_nfilt, nfft=_nfft, preemph=_preemph, ceplifter=_ceplifter, appendEnergy=_appendEnergy)

        push!(vvX, vec(mfcc_feat))
    end

    return (vvX, start)
end

function reduce(
    lhs::(Vector{Vector{Float64}}, Int),
    rhs::(Vector{Vector{Float64}}, Int)
    )
    return [lhs, rhs]
end

function reduce(
    lhs::Vector{(Vector{Vector{Float64}}, Int)},
    rhs::(Vector{Vector{Float64}}, Int)
    )
    push!(lhs, rhs)
    return lhs
end

function reduce(
    lhs::Vector{(Vector{Vector{Float64}}, Int)},
    rhs::Vector{(Vector{Vector{Float64}}, Int)}
    )
    append!(lhs, rhs)
    return lhs
end

end # module Worker


###############################################################################
# TASK ########################################################################
###### The `task` field should not be accessed outside this section. ##########

function set_primal_start(task::Mosek.MSKtask, col::ColumnIndex, value::Float64)
    xx = Float64[value]
    for sol in [MSK_SOL_BAS, MSK_SOL_ITG]
        putxxslice(task, sol, col.value, col.value + Int32(1), xx)
    end
end
function set_primal_start(m::Optimizer, vi::MOI.VariableIndex, value::Float64)
    set_primal_start(m.task, mosek_index(m, vi), value)
end
function set_primal_start(task::Mosek.MSKtask, cols::ColumnIndices,
                          values::Vector{Float64})
    for sol in [MSK_SOL_BAS, MSK_SOL_ITG]
        if solutiondef(task, sol)
            xx = getxx(task, sol)
        else
            xx = zeros(Float64, getnumvar(task))
        end
        xx[cols.values] = values
        putxx(task, sol, xx)
    end
end
function set_primal_start(m::Optimizer, vis::Vector{MOI.VariableIndex},
                          values::Vector{Float64})
    if all(vi -> is_scalar(m, vi), vis)
        set_primal_start(m.task, columns(m, vis), values)
    else
        for (vi, value) in zip(vis, values)
            set_primal_start(m.task, mosek_index(m, vi), value)
        end
    end
end

###############################################################################
## INDEXING ###################################################################
###############################################################################

function variable_primal(m::Optimizer, N, col::ColumnIndex)
    return m.solutions[N].xx[col.value]
end
function variable_primal(m::Optimizer, N, mat::MatrixIndex)
    d = m.sd_dim[mat.matrix]
    r = d - mat.column + 1
    #   #entries full Δ       #entries right Δ      #entries above in lower Δ
    k = div((d + 1) * d, 2) - div((r + 1) * r, 2) + (mat.row - mat.column + 1)
    return m.solutions[N].barxj[mat.matrix][k]
end
function variable_primal(m::Optimizer, N, vi::MOI.VariableIndex)
    return variable_primal(m, N, mosek_index(m, vi))
end

###############################################################################
# MOI #########################################################################
###############################################################################

#### objective
function MOI.get(m::Optimizer, attr::MOI.ObjectiveValue)
    MOI.check_result_index_bounds(m, attr)
    return getprimalobj(m.task, m.solutions[attr.result_index].whichsol)
end
function MOI.get(m::Optimizer, attr::MOI.DualObjectiveValue)
    MOI.check_result_index_bounds(m, attr)
    return getdualobj(m.task, m.solutions[attr.result_index].whichsol)
end

MOI.get(m::Optimizer,attr::MOI.ObjectiveBound) = getdouinf(m.task,MSK_DINF_MIO_OBJ_BOUND)

MOI.get(m::Optimizer,attr::MOI.RelativeGap) = getdouinf(m.task,MSK_DINF_MIO_OBJ_REL_GAP)

MOI.get(m::Optimizer,attr::MOI.SolveTime) = getdouinf(m.task,MSK_DINF_OPTIMIZER_TIME)

# NOTE: The MOSEK interface currently only supports Min and Max
function MOI.get(model::Optimizer, ::MOI.ObjectiveSense)
    if model.feasibility
        return MOI.FEASIBILITY_SENSE
    else
        sense = getobjsense(model.task)
        if sense == MSK_OBJECTIVE_SENSE_MINIMIZE
            MOI.MIN_SENSE
        else
            MOI.MAX_SENSE
        end
    end
end

function MOI.set(model::Optimizer,
                 attr::MOI.ObjectiveSense,
                 sense::MOI.OptimizationSense)
    if sense == MOI.MIN_SENSE
        model.feasibility = false
        putobjsense(model.task,MSK_OBJECTIVE_SENSE_MINIMIZE)
    elseif sense == MOI.MAX_SENSE
        model.feasibility = false
        putobjsense(model.task,MSK_OBJECTIVE_SENSE_MAXIMIZE)
    else
        @assert sense == MOI.FEASIBILITY_SENSE
        model.feasibility = true
        putobjsense(model.task,MSK_OBJECTIVE_SENSE_MINIMIZE)
        MOI.set(model,
                MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
                MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{Float64}[], 0.0))
    end
end

#### Solver/Solution information

function MOI.get(m::Optimizer,attr::MOI.SimplexIterations)
    miosimiter = getlintinf(m.task,MSK_LIINF_MIO_SIMPLEX_ITER)
    if miosimiter > 0
        Int(miosimiter)
    else
        Int(getintinf(m.task,MSK_IINF_SIM_PRIMAL_ITER) + getintinf(m.task,MSK_IINF_SIM_DUAL_ITER))
    end
end

function MOI.get(m::Optimizer,attr::MOI.BarrierIterations)
    miosimiter = getlintinf(m.task,MSK_LIINF_MIO_INTPNT_ITER)
    if miosimiter > 0
        Int(miosimiter)
    else
        Int(getintinf(m.task,MSK_IINF_INTPNT_ITER))
    end
end

function MOI.get(m::Optimizer,attr::MOI.NodeCount)
        Int(getintinf(m.task,MSK_IINF_MIO_NUM_BRANCH))
end

MOI.get(m::Optimizer,attr::MOI.RawSolver) = m.task

MOI.get(m::Optimizer,attr::MOI.ResultCount) = length(m.solutions)

#### Problem information

function MOI.get(model::Optimizer,
                 ::MOI.NumberOfConstraints{MOI.ScalarAffineFunction{Float64},
                                           S}) where S <: ScalarLinearDomain
    F = MOI.ScalarAffineFunction{Float64}
    return count(id -> MOI.is_valid(model, MOI.ConstraintIndex{F, S}(id)),
                 allocatedlist(model.c_block))
end
function MOI.get(model::Optimizer,
                 ::MOI.ListOfConstraintIndices{MOI.ScalarAffineFunction{Float64},
                                               S}) where S <: ScalarLinearDomain
    F = MOI.ScalarAffineFunction{Float64}
    ids = filter(id -> MOI.is_valid(model, MOI.ConstraintIndex{F, S}(id)),
                 allocatedlist(model.c_block))
    return [MOI.ConstraintIndex{F, S}(id) for id in ids]
end
function MOI.get(model::Optimizer,
                 ::MOI.NumberOfConstraints{MOI.SingleVariable, S}) where S<:Union{ScalarLinearDomain,
                                                                                MOI.Integer}
    F = MOI.SingleVariable
    return count(id -> MOI.is_valid(model, MOI.ConstraintIndex{F, S}(id)),
                 allocatedlist(model.x_block))
end
function MOI.get(model::Optimizer,
                 ::MOI.ListOfConstraintIndices{MOI.SingleVariable, S}) where S<:Union{ScalarLinearDomain,
                                                                                MOI.Integer}
    F = MOI.SingleVariable
    ids = filter(id -> MOI.is_valid(model, MOI.ConstraintIndex{F, S}(id)),
                 allocatedlist(model.x_block))
    return [MOI.ConstraintIndex{F, S}(id) for id in ids]
end
function MOI.get(model::Optimizer,
                 ::MOI.NumberOfConstraints{MOI.VectorOfVariables, S}) where S<:VectorCone
    F = MOI.VectorOfVariables
    return count(i -> MOI.is_valid(model, MOI.ConstraintIndex{F, S}(i)),
                 eachindex(model.variable_to_vector_constraint_id))
end
function MOI.get(model::Optimizer,
                 ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables, S}) where S<:VectorCone
    F = MOI.VectorOfVariables
    ids = filter(i -> MOI.is_valid(model, MOI.ConstraintIndex{F, S}(i)),
                 eachindex(model.variable_to_vector_constraint_id))
    return [MOI.ConstraintIndex{F, S}(id) for id in ids]
end
function MOI.get(model::Optimizer,
                 ::MOI.NumberOfConstraints{MOI.VectorOfVariables,
                                           MOI.PositiveSemidefiniteConeTriangle})
    # TODO this only works because deletion of PSD constraints is not supported yet
    return length(model.sd_dim)
end
function MOI.get(model::Optimizer,
                 ::MOI.ListOfConstraintIndices{MOI.VectorOfVariables,
                                               MOI.PositiveSemidefiniteConeTriangle})
    # TODO this only works because deletion of PSD constraints is not supported yet
    return [MOI.ConstraintIndex{MOI.VectorOfVariables,
                                MOI.PositiveSemidefiniteConeTriangle}(id) for id in 1:length(model.sd_dim)]
end

function MOI.get(model::Optimizer,
                 ::MOI.ListOfConstraints)
    list = Tuple{DataType, DataType}[]
    F = MOI.SingleVariable
    for D in [MOI.LessThan{Float64}, MOI.GreaterThan{Float64},
              MOI.EqualTo{Float64}, MOI.Interval{Float64},
              MOI.Integer]
        if !iszero(MOI.get(model, MOI.NumberOfConstraints{F, D}()))
            push!(list, (F, D))
        end
    end
    F = MOI.ScalarAffineFunction{Float64}
    for D in [MOI.LessThan{Float64}, MOI.GreaterThan{Float64},
              MOI.EqualTo{Float64}, MOI.Interval{Float64}]
        if !iszero(MOI.get(model, MOI.NumberOfConstraints{F, D}()))
            push!(list, (F, D))
        end
    end
    F = MOI.VectorOfVariables
    for D in [MOI.SecondOrderCone, MOI.RotatedSecondOrderCone,
              # TODO reenable for Mosek 9
              #MOI.PowerCone{Float64}, MOI.DualPowerCone{Float64},
              #MOI.ExponentialCone, MOI.DualExponentialCone,
              MOI.PositiveSemidefiniteConeTriangle]
        if !iszero(MOI.get(model, MOI.NumberOfConstraints{F, D}()))
            push!(list, (F, D))
        end
    end
    return list
end

#### Warm start values

MOI.supports(::Optimizer, ::MOI.VariablePrimalStart, ::Type{MOI.VariableIndex}) = true

function MOI.set(m::Optimizer, ::MOI.VariablePrimalStart,
                 v::MOI.VariableIndex, ::Nothing)
    set_primal_start(m, v, 0.0)
end
function MOI.set(m::Optimizer, ::MOI.VariablePrimalStart,
                 v::MOI.VariableIndex, val::Float64)
    set_primal_start(m, v, val)
end

function MOI.set(m::Optimizer, ::MOI.VariablePrimalStart,
                 vis::Vector{MOI.VariableIndex}, values::Vector{Float64})
    set_primal_start(m, vis, values)
end

# function MOI.set(m::Optimizer,attr::MOI.ConstraintDualStart, vs::Vector{MOI.ConstraintIndex}, vals::Vector{Float64})
#     subj = columns(vs)

#     for sol in [ MSK_SOL_BAS, MSK_SOL_ITG ]
#         if solutiondef(m.task,sol)
#             xx = getxx(m.task,sol)
#             xx[subj] = vals
#             putxx(m.task,sol,xx)
#         else
#             xx = zeros(Float64,getnumvar(m.task))
#             xx[subj] = vals
#             putxx(m.task,sol,xx)
#         end
#     end
# end

#### Variable solution values

function MOI.get(m::Optimizer, attr::MOI.VariablePrimal, vi::MOI.VariableIndex)
    MOI.check_result_index_bounds(m, attr)
    return variable_primal(m, attr.N, vi)
end
function MOI.get!(output::Vector{Float64}, m::Optimizer,
                  attr::MOI.VariablePrimal, vs::Vector{MOI.VariableIndex})
    MOI.check_result_index_bounds(m, attr)
    @assert eachindex(output) == eachindex(vs)
    for i in eachindex(output)
        output[i] = MOI.get(m, attr, vs[i])
    end
end
function MOI.get(m::Optimizer, attr::MOI.VariablePrimal,
                 vs::Vector{MOI.VariableIndex})
    MOI.check_result_index_bounds(m, attr)
    output = Vector{Float64}(undef, length(vs))
    MOI.get!(output, m, attr, vs)
    return output
end

#### Constraint solution values

function MOI.get(
    m     ::Optimizer,
    attr  ::MOI.ConstraintPrimal,
    ci  ::MOI.ConstraintIndex{MOI.SingleVariable,D}) where D
    MOI.check_result_index_bounds(m, attr)
    col = column(m, _variable(ci))
    return m.solutions[attr.N].xx[col.value]
end

# Semidefinite domain for a variable
function MOI.get!(
    output::Vector{Float64},
    m     ::Optimizer,
    attr  ::MOI.ConstraintPrimal,
    ci  ::MOI.ConstraintIndex{MOI.VectorOfVariables,
                              MOI.PositiveSemidefiniteConeTriangle})
    MOI.check_result_index_bounds(m, attr)
    whichsol = getsolcode(m,attr.N)
    output[1:length(output)] = reorder(getbarxj(m.task, whichsol, ci.value),
                                       MOI.PositiveSemidefiniteConeTriangle)
end

# Any other domain for variable vector
function MOI.get!(
    output::Vector{Float64},
    m     ::Optimizer,
    attr  ::MOI.ConstraintPrimal,
    ci  ::MOI.ConstraintIndex{MOI.VectorOfVariables,D}) where D
    MOI.check_result_index_bounds(m, attr)
    cols = columns(m, ci)
    output[1:length(output)] = reorder(m.solutions[attr.N].xx[cols.values], D)
end

function MOI.get(m     ::Optimizer,
                 attr  ::MOI.ConstraintPrimal,
                 ci  ::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},D}) where D
    MOI.check_result_index_bounds(m, attr)
    cid = ref2id(ci)
    subi = getindex(m.c_block,cid)
    return m.solutions[attr.N].xc[subi]
end

function _variable_constraint_dual(sol::MosekSolution, col::ColumnIndex,
                                   ::Type{<:Union{MOI.Interval{Float64}, MOI.EqualTo{Float64}}})
    return sol.slx[col.value] - sol.sux[col.value]
end
function _variable_constraint_dual(sol::MosekSolution, col::ColumnIndex, ::Type{MOI.GreaterThan{Float64}})
    return sol.slx[col.value]
end
function _variable_constraint_dual(sol::MosekSolution, col::ColumnIndex, ::Type{MOI.LessThan{Float64}})
    return -sol.sux[col.value]
end
function MOI.get(m::Optimizer, attr::MOI.ConstraintDual,
                 ci::MOI.ConstraintIndex{MOI.SingleVariable, S}) where S <: ScalarLinearDomain
    MOI.check_result_index_bounds(m, attr)
    col = column(m, _variable(ci))
    dual = _variable_constraint_dual(m.solutions[attr.N], col, S)
    if getobjsense(m.task) == MSK_OBJECTIVE_SENSE_MINIMIZE
        return dual
    else
        return -dual
    end
end

getsolcode(m::Optimizer, N) = m.solutions[N].whichsol

# The dual or primal of an SDP variable block is returned in lower triangular
# form but the constraint is in upper triangular form.
function reorder(x, ::Type{MOI.PositiveSemidefiniteConeTriangle})
    n = div(isqrt(1 + 8length(x)) - 1, 2)
    @assert length(x) == MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(n))
    y = similar(x)
    k = 0
    for j in 1:n, i in j:n
        k += 1
        y[div((i - 1) * i, 2) + j] = x[k]

    end
    @assert k == length(x)
    return y
end

function reorder(x, ::Type{<:Union{MOI.ExponentialCone,
                                   MOI.DualExponentialCone}})
    return [x[3], x[2], x[1]]
end
reorder(x, ::Type{<:VectorCone}) = x

# Semidefinite domain for a variable
function MOI.get!(
    output::Vector{Float64},
    m     ::Optimizer,
    attr  ::MOI.ConstraintDual,
    ci  ::MOI.ConstraintIndex{MOI.VectorOfVariables, MOI.PositiveSemidefiniteConeTriangle})
    MOI.check_result_index_bounds(m, attr)
    whichsol = getsolcode(m,attr.N)
    # It is in fact a real constraint and cid is the id of an ordinary constraint
    dual = reorder(getbarsj(m.task, whichsol, ci.value),
                   MOI.PositiveSemidefiniteConeTriangle)
    if (getobjsense(m.task) == MSK_OBJECTIVE_SENSE_MINIMIZE)
        output[1:length(output)] .= dual
    else
        output[1:length(output)] .= -dual
    end
end

# Any other domain for variable vector
function MOI.get!(
    output::Vector{Float64},
    m     ::Optimizer,
    attr  ::MOI.ConstraintDual,
    ci  ::MOI.ConstraintIndex{MOI.VectorOfVariables,D}) where D
    MOI.check_result_index_bounds(m, attr)

    xcid = ref2id(ci)
    @assert(xcid > 0)

    cols = columns(m, ci)

    idx = reorder(1:length(output), D)

    if (getobjsense(m.task) == MSK_OBJECTIVE_SENSE_MINIMIZE)
        output[idx] = m.solutions[attr.N].snx[cols.values]
    else
        output[idx] = -m.solutions[attr.N].snx[cols.values]
    end
end

function MOI.get(m     ::Optimizer,
                 attr  ::MOI.ConstraintDual,
                 ci  ::MOI.ConstraintIndex{MOI.ScalarAffineFunction{Float64},D}) where D
    MOI.check_result_index_bounds(m, attr)

    cid = ref2id(ci)
    subi = getindex(m.c_block, cid)

    if getobjsense(m.task) == MSK_OBJECTIVE_SENSE_MINIMIZE
        m.solutions[attr.N].y[subi]
    else
        - m.solutions[attr.N].y[subi]
    end
end

solsize(m::Optimizer, ::MOI.ConstraintIndex{<:MOI.AbstractScalarFunction}) = 1
function solsize(m::Optimizer, ci::MOI.ConstraintIndex{MOI.VectorOfVariables})
    return getconeinfo(m.task, cone_id(m, ci))[3]
end
function solsize(m::Optimizer,
                 ci::MOI.ConstraintIndex{MOI.VectorOfVariables,
                                         MOI.PositiveSemidefiniteConeTriangle})
    d = m.sd_dim[ci.value]
    return MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(d))
end

function MOI.get(m::Optimizer,
                 attr::Union{MOI.ConstraintPrimal, MOI.ConstraintDual},
                 ci::MOI.ConstraintIndex{<:MOI.AbstractVectorFunction})
    MOI.check_result_index_bounds(m, attr)
    cid = ref2id(ci)
    output = Vector{Float64}(undef, solsize(m, ci))
    MOI.get!(output, m, attr, ci)
    return output
end

#### Status codes
function MOI.get(m::Optimizer, attr::MOI.RawStatusString)
    if     m.trm === nothing
        return "MOI.OPTIMIZE_NOT_CALLED"
    elseif m.trm == MSK_RES_OK
        return join([string(sol.solsta) for sol in m.solutions], ", ")
    else
        return string(m.trm)
    end

end
function MOI.get(m::Optimizer, attr::MOI.TerminationStatus)
    if     m.trm === nothing
        MOI.OPTIMIZE_NOT_CALLED
    elseif m.trm == MSK_RES_OK
        # checking `any(sol -> sol.solsta == MSK_SOL_STA_PRIM_INFEAS_CER, m.solutions)`
        # doesn't work for MIP as there is not certificate, i.e. the solutions status is
        # `UNKNOWN`, only the problem status is `INFEAS`.
        if any(sol -> sol.prosta == MSK_PRO_STA_PRIM_INFEAS, m.solutions)
            MOI.INFEASIBLE
        elseif any(sol -> sol.prosta == MSK_PRO_STA_DUAL_INFEAS, m.solutions)
            MOI.DUAL_INFEASIBLE
        else
            MOI.OPTIMAL
        end
    elseif m.trm == MSK_RES_TRM_MAX_ITERATIONS
        MOI.ITERATION_LIMIT
    elseif m.trm == MSK_RES_TRM_MAX_TIME
        MOI.TIME_LIMIT
    elseif m.trm == MSK_RES_TRM_OBJECTIVE_RANGE
        MOI.OBJECTIVE_LIMIT
    elseif m.trm == MSK_RES_TRM_MIO_NUM_RELAXS
        MOI.OTHER_LIMIT
    elseif m.trm == MSK_RES_TRM_MIO_NUM_BRANCHES
        MOI.NODE_LIMIT
    elseif m.trm == MSK_RES_TRM_NUM_MAX_NUM_INT_SOLUTIONS
        MOI.SOLUTION_LIMIT
    elseif m.trm == MSK_RES_TRM_STALL
        MOI.SLOW_PROGRESS
    elseif m.trm == MSK_RES_TRM_USER_CALLBACK
        MOI.INTERRUPTED
    elseif m.trm == MSK_RES_TRM_MAX_NUM_SETBACKS
        MOI.OTHER_LIMIT
    elseif m.trm == MSK_RES_TRM_NUMERICAL_PROBLEM
        MOI.SLOW_PROGRESS
    elseif m.trm == MSK_RES_TRM_INTERNAL
        MOI.OTHER_ERROR
    elseif m.trm == MSK_RES_TRM_INTERNAL_STOP
        MOI.OTHER_ERROR
    else
        MOI.OTHER_ERROR
    end
end

function MOI.get(m::Optimizer, attr::MOI.PrimalStatus)
    if attr.N > MOI.get(m, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    solsta = m.solutions[attr.N].solsta
    if     solsta == MSK_SOL_STA_UNKNOWN
        MOI.UNKNOWN_RESULT_STATUS
    elseif solsta == MSK_SOL_STA_OPTIMAL
        MOI.FEASIBLE_POINT
    elseif solsta == MSK_SOL_STA_PRIM_FEAS
        MOI.FEASIBLE_POINT
    elseif solsta == MSK_SOL_STA_DUAL_FEAS
        MOI.UNKNOWN_RESULT_STATUS
    elseif solsta == MSK_SOL_STA_PRIM_AND_DUAL_FEAS
        MOI.FEASIBLE_POINT
    elseif solsta == MSK_SOL_STA_PRIM_INFEAS_CER
        MOI.NO_SOLUTION
    elseif solsta == MSK_SOL_STA_DUAL_INFEAS_CER
        MOI.INFEASIBILITY_CERTIFICATE
    elseif solsta == MSK_SOL_STA_PRIM_ILLPOSED_CER
        MOI.NO_SOLUTION
    elseif solsta == MSK_SOL_STA_DUAL_ILLPOSED_CER
        MOI.REDUCTION_CERTIFICATE
    elseif solsta == MSK_SOL_STA_INTEGER_OPTIMAL
        MOI.FEASIBLE_POINT
    else
        MOI.UNKNOWN_RESULT_STATUS
    end
end

function MOI.get(m::Optimizer,attr::MOI.DualStatus)
    if attr.N > MOI.get(m, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    solsta = m.solutions[attr.N].solsta
    if     solsta == MSK_SOL_STA_UNKNOWN
        MOI.UNKNOWN_RESULT_STATUS
    elseif solsta == MSK_SOL_STA_OPTIMAL
        MOI.FEASIBLE_POINT
    elseif solsta == MSK_SOL_STA_PRIM_FEAS
        MOI.UNKNOWN_RESULT_STATUS
    elseif solsta == MSK_SOL_STA_DUAL_FEAS
        MOI.FEASIBLE_POINT
    elseif solsta == MSK_SOL_STA_PRIM_AND_DUAL_FEAS
        MOI.FEASIBLE_POINT
    elseif solsta == MSK_SOL_STA_PRIM_INFEAS_CER
        MOI.INFEASIBILITY_CERTIFICATE
    elseif solsta == MSK_SOL_STA_DUAL_INFEAS_CER
        MOI.NO_SOLUTION
    elseif solsta == MSK_SOL_STA_PRIM_ILLPOSED_CER
        MOI.REDUCTION_CERTIFICATE
    elseif solsta == MSK_SOL_STA_DUAL_ILLPOSED_CER
        MOI.NO_SOLUTION
    elseif solsta == MSK_SOL_STA_INTEGER_OPTIMAL
        MOI.NO_SOLUTION
    else
        MOI.UNKNOWN_RESULT_STATUS
    end
end


@testset "whittledata" begin
    @test_throws ArgumentError WhittleLikelihoodInference.WhittleData(CorrelatedOU, ones(10,3), 1.0)
    @test_throws ArgumentError WhittleLikelihoodInference.DebiasedWhittleData(CorrelatedOU, ones(10,3), 1.0)
    @test_throws ArgumentError WhittleLikelihoodInference.WhittleData(CorrelatedOU, ones(10,2), 1.0, taper = ones(10,2))
    @test_throws ArgumentError WhittleLikelihoodInference.DebiasedWhittleData(CorrelatedOU, ones(10,2), 1.0, taper = ones(10,2))
    @test_throws ArgumentError WhittleLikelihoodInference.WhittleData(CorrelatedOU, ones(10,2), 1.0, taper = ones(ComplexF64,10))
    @test_throws ArgumentError WhittleLikelihoodInference.DebiasedWhittleData(CorrelatedOU, ones(10,2), 1.0, taper = ones(ComplexF64,10))
end


#   This file is part of Maxima.jl. It is licensed under the MIT license
#   Copyright (c) 2016 Nathan Smith

export rhs,
       lhs

function rhs(m::MExpr)
    return mcall(MExpr("rhs($m)"))
end

"""
    rhs(expr::T) where T

Right hand side of an equation.

If `expr` is not an equation than `rhs` will return `0`
"""
function rhs(m::T) where T
    return convert(T, rhs(MExpr(m)))
end

function lhs(m::MExpr)
    return mcall(MExpr("lhs($m)"))
end

"""
    lhs(expr::T) where T

Left hand side of an equation.

If `expr` is not an equation then `lhs` will return `expr`
"""
function lhs(m::T) where T
    return convert(T, lhs(MExpr(m)))
end

allroots(m::MExpr) = mcall(MExpr("rhs($m)"))
allroots(m::T) where T = convert(T, allroots(MExpr(m)))