export simulatemodel

function modify_constraint(m::JuMP.AbstractModel, consname::Symbol, data::Array{Float64,2})

    !(size(m[consmane]) == size(data)) ? @error("The data and the constraint are size inconsistent") : true

    for (n, c) in enumerate(IndexCartesian(), data)

        JuMP.setRHS(m[consname], data[n])

    end

    return m

end

function run_simulations(simulation::PowerSimulationsModel{S}, solver, ps_dict::Dict; args...) where {S<:AbstractOperationsModel}
    # TODO: refactor system to be mutable and remove ps_dict from arguments

    # CheckPowerModel(m::PowerSimulationsModel{T}) where T<:AbstractPowerSimulationType
    # AssignSolver(m::PowerSimulationsModel{T}) where T<:AbstractPowerSimulationType
    # WarmUpModel(m::PowerSimulationsModel{T}) where T<:AbstractPowerSimulationType

    # Precalculate PTDF and initialize generator statuses
    sys = simulation.model.system

    if simulation.model.transmission <: StandardPTDF
        PTDF,  A = PowerSystems.buildptdf(sys.branches, sys.buses)
    else
        PTDF = nothing
    end

    name_index = [gen.name for gen in sys.generators.thermal];

    initialpowerdict = :initialpowerdict in keys(args) ? args[:initialpowerdict] : Dict(zip(name_index,[sys.generators.thermal[i].tech.activepower for i in 1:length(name_index)]))
    initialstatusdict = :initialstatusdict in keys(args) ? args[:initialstatusdict] : Dict(zip(name_index,ones(length(name_index))));
    initialondurationdict = :initialondurationdict in keys(args) ? args[:initialondurationdict] : Dict(zip(name_index,ones(length(name_index))*100));
    initialoffdurationdict = :initialoffdurationdict in keys(args) ? args[:initialoffdurationdict] : Dict(zip(name_index,zeros(length(name_index))));

    simulation_results = Dict();

    # run a PCM
    for (step, step_ts) in sort(simulation.timeseries)
        # assign TS to ps_dict
        ps_dict = PowerSystems.assign_ts_data(ps_dict,step_ts);

        # build sys
        sys = PowerSystems.PowerSystem(ps_dict; args...);

        # make model
        tmp_model = PS.PowerOperationModel(simulation.model.psmodel,
            simulation.model.generation,
            simulation.model.demand,
            simulation.model.storage,
            simulation.model.branches,
            simulation.model.transmission,
            simulation.model.services,
            sys,
            Model(), false, PTDF)

        # build model
        println( "Building model for $step ...")
        buildmodel!(sys, tmp_model; PTDF = PTDF,
            initialpower = initialpowerdict,
            initialstatus = initialstatusdict,
            initialonduration = initialondurationdict,
            initialoffduration = initialoffdurationdict)

        # solve model
        println("Solving model for $step ...")
        optimize!(tmp_model.model,with_optimizer(solver))

        status = :termination_status in fieldnames(typeof(tmp_model.model.moi_backend.model.optimizer)) ? tmp_model.model.moi_backend.model.optimizer.termination_status : nothing

        if status == JuMP.MOI.Success
            println( "Problem solved successfully...")
        elseif status == nothing
            @info "$solver does not provide a termination status"
        else
            @warn "Problem solve unsuccessful, solver returned with status: $status"
        end

        # TODO: Subset model results when a lookahead is provided

        # extract results
        res = PS.get_model_result(tmp_model)

        simulation_results[step] = copy(res)

        # update initial...
        initialpowerdict = :p_th in keys(res) ? PS.get_previous_value(res[:p_th]) : nothing
        initialstatusdict = :on_th in keys(res) ? PS.get_previous_value(res[:on_th]) : nothing
        initialondurationdict = :start_th in keys(res) ? PS.commitment_duration(res,initialondurationdict,:start_th) : nothing
        initialoffdurationdict = :stop_th in keys(res) ? PS.commitment_duration(res,initialoffdurationdict,:stop_th) : nothing

    end

    return simulation_results

end;


function nthetas(lshaped::AbstractLShapedSolver, ::AbstractExecution)
    return nthetas(lshaped.nscenarios, lshaped.aggregation, scenarioproblems(lshaped.stochasticprogram))
end

function timestamp(lshaped::AbstractLShapedSolver, ::AbstractExecution)
    return lshaped.data.iterations
end

function current_decision(lshaped::AbstractLShapedSolver, ::AbstractExecution)
    return lshaped.x
end

function incumbent_decision(::AbstractLShapedSolver, ::Integer, regularization::AbstractRegularization, ::AbstractExecution)
    return regularization.ξ
end

function incumbent_objective(::AbstractLShapedSolver, ::Integer, regularization::AbstractRegularization, ::AbstractExecution)
    return regularization.data.Q̃
end

function incumbent_trustregion(::AbstractLShapedSolver, ::Integer, rd::RegularizedDecomposition, ::AbstractExecution)
    return rd.data.σ
end

function incumbent_trustregion(::AbstractLShapedSolver, ::Integer, tr::TrustRegion, ::AbstractExecution)
    return tr.data.Δ
end

function start_workers!(::AbstractLShapedSolver, ::AbstractExecution)
    return nothing
end

function close_workers!(::AbstractLShapedSolver, ::AbstractExecution)
    return nothing
end

function readd_cuts!(lshaped::AbstractLShapedSolver, consolidation::Consolidation, ::AbstractExecution)
    for i in eachindex(consolidation.cuts)
        for cut in consolidation.cuts[i]
            add_cut!(lshaped, cut; consider_consolidation = false, check = false)
        end
        for cut in consolidation.feasibility_cuts[i]
            add_cut!(lshaped, cut; consider_consolidation = false, check = false)
        end
    end
    return nothing
end

function subobjectives(lshaped::AbstractLShapedSolver, execution::AbstractExecution)
    return execution.subobjectives
end

function set_subobjectives(lshaped::AbstractLShapedSolver, Qs::AbstractVector, execution::AbstractExecution)
    execution.subobjectives .= Qs
    return nothing
end

function model_objectives(lshaped::AbstractLShapedSolver, execution::AbstractExecution)
    return execution.model_objectives
end

function set_model_objectives(lshaped::AbstractLShapedSolver, θs::AbstractVector, execution::AbstractExecution)
    ids = active_model_objectives(lshaped)
    execution.model_objectives[ids] .= θs[ids]
    return nothing
end

function solve_master!(lshaped::AbstractLShapedSolver, ::AbstractExecution)
    try
        solve_regularized_master!(lshaped, lshaped.regularization)
    catch
        # Master problem could not be solved for some reason.
        @unpack Q,θ = lshaped.data
        gap = abs(θ-Q)/(abs(Q)+1e-10)
        # Always print this warning
        @warn "Master problem could not be solved, solver returned status $(status(lshaped.mastersolver)). The following relative tolerance was reached: $(@sprintf("%.1e",gap)). Aborting procedure."
        return :StoppedPrematurely
    end
    if status(lshaped.mastersolver) == :Infeasible
        @warn "Master is infeasible. Aborting procedure."
        return :Infeasible
    end
    if status(lshaped.mastersolver) == :Unbounded
        @warn "Master is unbounded. Aborting procedure."
        return :Unbounded
    end
    return :Optimal
end

function iterate!(lshaped::AbstractLShapedSolver, ::AbstractExecution)
    # Resolve all subproblems at the current optimal solution
    Q, added = resolve_subproblems!(lshaped)
    if Q == Inf && !handle_feasibility(lshaped.feasibility)
        @warn "Stochastic program is not second-stage feasible at the current decision. Rerun procedure with feasibility_cuts = true to use feasibility cuts."
        return :Infeasible
    end
    if Q == -Inf
        return :Unbounded
    end
    lshaped.data.Q = Q
    # Update incumbent (if applicable)
    take_step!(lshaped)
    # Solve master problem
    status = solve_master!(lshaped)
    if status != :Optimal
        return status
    end
    # Update master solution
    update_solution!(lshaped, lshaped.mastersolver)
    lshaped.data.θ = calculate_estimate(lshaped)
    # Log progress
    log!(lshaped)
    # Check optimality
    if check_optimality(lshaped) || (lshaped.regularization isa NoRegularization && !added)
        # Optimal
        lshaped.data.Q = calculate_objective_value(lshaped)
        # Final log
        log!(lshaped; optimal = true)
        return :Optimal
    end
    # Project (if applicable)
    project!(lshaped)
    # Consolidate (if applicable)
    consolidate!(lshaped, lshaped.consolidation)
    # Just return a valid status for this iteration
    return :Valid
end


# PREAMBLE

# PKG_SETUP

# ## Setup

using RoboDojo

# ## Initial conditions
q1 = nominal_configuration(quadruped)
v1 = zeros(quadruped.nq)

# ## Time
h = 0.01
T = 100

# ## Simulator
s = Simulator(quadruped, T, h=h)

# ## Simulate
simulate!(s, q1, v1)

# ## Visualizer
vis = Visualizer()
render(vis)

# ## Visualize
visualize!(vis, s)


"""
Tools for model edge analysis for automatic mechanism generation
"""

using Logging
using Sundials
using SparseArrays
using DiffEqBase: build_solution
using Base.Iterators: flatten

abstract type AbstractTerminationCriterion end

struct TerminationTime <: AbstractTerminationCriterion
    time::Float64
end
struct TerminationConversion <: AbstractTerminationCriterion
    species::Species
    conversion::Float64
end
struct TerminationRateRatio <: AbstractTerminationCriterion
    ratio::Float64
end

export TerminationTime
export TerminationConversion
export TerminationRateRatio

"""
Generate appropriate core Simulation/SystemSimulation object
"""
function tosim(sol,domains,inters,p)
    return Simulation(sol,domains,inters,p)
end

"""
Generate appropriate core Simulation/SystemSimulation object
"""
function tosim(sol,domains::Tuple,inters,p)
   return SystemSimulation(sol,domains,inters,p)
end

"""
Generate appropriate edge Simulation/SystemSimulation object
"""
function getsim(inte,react,coreedgedomain,inters,p,coretoedgespcmap)
    ycoreedge = getycoreedge(inte.u,coretoedgespcmap,coreedgedomain.indexes[end])
    sol = build_solution(react.ode,inte.alg,[0.0,inte.t],[ycoreedge,ycoreedge])
    sim = Simulation(sol,coreedgedomain,inters,p)
    return sim
end

"""
Generate appropriate edge Simulation/SystemSimulation object
"""
function getsim(inte,react,coreedgedomains::Tuple,inters,p,coretoedgespcmap)
    ycoreedge = getycoreedge(inte.u,coretoedgespcmap,coreedgedomains[end].indexes[end])
    sol = build_solution(react.ode,inte.alg,[0.0,inte.t],[ycoreedge,ycoreedge])
    ssys = SystemSimulation(sol,coreedgedomains,inters,p)
    return ssys
end

"""
Generate a state vector appropriate for both the edge and core from the core state vector
"""
function getycoreedge(y,coretoedgespcmap,numedgespc)
    ycoreedge = zeros(numedgespc)
    for (coreind,edgeind) in coretoedgespcmap
        ycoreedge[edgeind] = y[coreind]
    end
    return ycoreedge
end

"""
Calculate key flux and concentration related quantities for edge analysis
"""
@inline function calcfluxes(sim::Simulation)
    t = sim.sol.t[end]
    dydt = zeros(sim.domain.indexes[end])
    ns,cs,T,P,V,C,N,mu,kfs,krevs,Hs,Us,Gs,diffs,Cvave,cpdivR,phi = calcthermo(sim.domain,sim.sol.u[end],sim.sol.t[end],DiffEqBase.NullParameters())
    rts,frts,rrts = addreactionratecontributionsforwardreverse!(dydt,sim.domain.rxnarray,cs,kfs,krevs,V)
    calcdomainderivatives!(sim.domain,dydt,[];t=t,T=T,P=P,Us=Us,Hs=Hs,V=V,C=C,ns=ns,N=N,Cvave=Cvave)
    return dydt,rts,frts,rrts,cs
end

"""
Calculate key flux and concentration related quantities for edge analysis
"""
@inline function calcfluxes(ssys::SystemSimulation)
    cstot = zeros(sum([length(sim.domain.phase.species) for sim in ssys.sims]))
    dydt = zeros(length(ssys.sol.u[end]))
    y = ssys.sol.u[end]
    t = ssys.sol.t[end]
    p = ssys.p
    domains = [sim.domain for sim in ssys.sims]
    interfaces = ssys.interfaces
    domain = domains[1]
    ns,cs,T,P,V,C,N,mu,kfs,krevs,Hs,Us,Gs,diffs,Cvave,cpdivR,phi = calcthermo(domain,y,t,DiffEqBase.NullParameters())
    vns = Array{Any,1}(undef,length(domains))
    vns[1] = ns
    vcs = Array{Any,1}(undef,length(domains))
    vcs[1] = cs
    cstot[domain.indexes[1]:domain.indexes[2]] = cs
    vT = Array{Any,1}(undef,length(domains))
    vT[1] = T
    vP = Array{Any,1}(undef,length(domains))
    vP[1] = P
    vV = Array{Any,1}(undef,length(domains))
    vV[1] = V
    vC = Array{Any,1}(undef,length(domains))
    vC[1] = C
    vN = Array{Any,1}(undef,length(domains))
    vN[1] = N
    vmu = Array{Any,1}(undef,length(domains))
    vmu[1] = mu
    vkfs = Array{Any,1}(undef,length(domains))
    vkfs[1] = kfs
    vkrevs = Array{Any,1}(undef,length(domains))
    vkrevs[1] = krevs
    vHs = Array{Any,1}(undef,length(domains))
    vHs[1] = Hs
    vUs = Array{Any,1}(undef,length(domains))
    vUs[1] = Us
    vGs = Array{Any,1}(undef,length(domains))
    vGs[1] = Gs
    vdiffs = Array{Any,1}(undef,length(domains))
    vdiffs[1] = diffs
    vCvave = Array{Any,1}(undef,length(domains))
    vCvave[1] = Cvave
    vcpdivR = Array{Any,1}(undef,length(domains))
    vcpdivR[1] = cpdivR
    vphi = Array{Any,1}(undef,length(domains))
    vphi[1] = phi
    rts,frts,rrts = addreactionratecontributionsforwardreverse!(dydt,domain.rxnarray,cstot,kfs,krevs,V)
    rtsall = [rts]
    frtsall = [frts]
    rrtsall = [rrts]
    for (i,domain) in enumerate(@views domains[2:end])
        k = i + 1
        vns[k],vcs[k],vT[k],vP[k],vV[k],vC[k],vN[k],vmu[k],vkfs[k],vkrevs[k],vHs[k],vUs[k],vGs[k],vdiffs[k],vCvave[k],vcpdivR[k],vphi[k] = calcthermo(domain,y,t,DiffEqBase.NullParameters())
        cstot[domain.indexes[1]:domain.indexes[2]] .= vcs[k]
        rts,frts,rrts = addreactionratecontributionsforwardreverse!(dydt,domain.rxnarray,cstot,vkfs[k],vkrevs[k],vV[k])
        push!(rtsall,rts)
        push!(frtsall,frts)
        push!(rrtsall,rrts)
    end
    for (i,inter) in enumerate(interfaces)
        if isa(inter,ReactiveInternalInterface)
            kfs,krevs = getkfskrevs(inter,vT[inter.domaininds[1]],vT[inter.domaininds[2]],vphi[inter.domaininds[1]],vphi[inter.domaininds[2]],vGs[inter.domaininds[1]],vGs[inter.domaininds[2]],cstot)
            rts,frts,rrts = addreactionratecontributionsforwardreverse!(dydt,inter.rxnarray,cstot,kfs.*p[inter.parameterindexes[1]:inter.parameterindexes[2]],krevs.*p[inter.parameterindexes[1]:inter.parameterindexes[2]],inter.A)
            push!(rtsall,rts)
            push!(frtsall,frts)
            push!(rrtsall,rrts)
        elseif isa(inter,ReactiveInternalInterfaceConstantTPhi)
            rts,frts,rrts = addreactionratecontributionsforwardreverse!(dydt,inter.rxnarray,cstot,inter.kfs,inter.krevs,inter.A)
            push!(rtsall,rts)
            push!(frtsall,frts)
            push!(rrtsall,rrts)
        elseif isa(inter,AbstractReactiveInternalInterface)
            typ = typeof(inter)
            error("No handling available for AbstractReactiveInternalInterface: $typ")
        end
    end
    for (i,domain) in enumerate(domains)
        calcdomainderivatives!(domain,dydt,interfaces;t=t,T=vT[i],P=vP[i],Us=vUs[i],Hs=vHs[i],V=vV[i],C=vC[i],ns=vns[i],N=vN[i],Cvave=vCvave[i])
    end
    return dydt,collect(flatten(rtsall)),collect(flatten(frtsall)),collect(flatten(rrtsall)),cstot
end
export calcfluxes

"""
Precalculate important indices and maps for use in edge analysis
"""
function getkeyselectioninds(coreedgedomains,coreedgeinters,domains,inters)
    corespcsinds = collect(flatten([coreedgedomains[i].indexes[1]:coreedgedomains[i].indexes[1]+domains[i].indexes[2]-domains[i].indexes[1] for i = 1:length(domains)]))
    edgespcsinds = collect(flatten([coreedgedomains[i].indexes[1]+domains[i].indexes[2]-domains[i].indexes[1]+1:coreedgedomains[i].indexes[2] for i = 1:length(domains)]))
    corerxninds = Array{Int64,1}()
    edgerxninds = Array{Int64,1}()
    Nrxns = sum(length(d.phase.reactions) for d in coreedgedomains)
    reactantindices = zeros(Int64,(3,Nrxns))
    productindices  = zeros(Int64,(3,Nrxns))
    coretoedgespcmap = Dict{Int64,Int64}()
    coretoedgerxnmap = Dict{Int64,Int64}()
    spcindexcore = 0
    spcindexedge = 0
    rxnindexcore = 0
    rxnindexedge = 0
    ind = 1
    for i = 1:length(domains)
        for (j,spc) in enumerate(domains[i].phase.species)
            edgeind = findfirst(isequal(spc),coreedgedomains[i].phase.species)
            coretoedgespcmap[j+spcindexcore] = edgeind+spcindexedge
        end
        for j = 3:length(domains[i].indexes)
            coretoedgespcmap[domains[i].indexes[j]] = coreedgedomains[i].indexes[j]
        end
        for (j,rxn) in enumerate(coreedgedomains[i].phase.reactions)
            coreind = findfirst(isequal(rxn),domains[i].phase.reactions)
            if coreind === nothing
                push!(edgerxninds,j+rxnindexedge)
            else
                coretoedgerxnmap[coreind+rxnindexcore] = j+rxnindexedge
                push!(corerxninds,j+rxnindexedge)
            end
        end
        spcindexcore += length(domains[i].phase.species)
        spcindexedge += length(coreedgedomains[i].phase.species)
        rxnindexcore += length(domains[i].phase.reactions)
        rxnindexedge += length(coreedgedomains[i].phase.reactions)

        indend = length(domains[i].phase.reactions)
        reactantindices[:,ind:ind+indend-1] = domains[i].rxnarray[1:3,:]
        productindices[:,ind:ind+indend-1] = domains[i].rxnarray[4:6,:]
        ind += indend
    end

    for i = 1:length(inters)
        if isa(inters[i],ReactiveInternalInterface)
            push!(corerxnrangearray,index:index+length(inters[i].reactions))
            push!(edgerxnrangearray,index+length(inters[i].reactions):index+length(coreedgeinters[i].reactions))
            index += length(coreedgeinters[i].phase.reactions)

            indend = length(inters[i].reactions)
            reactantindices[:,ind:ind+indend] = inters[i].rxnarray[1:3,:]
            productindices[:,ind:ind+indend] = inters[i].rxnarray[4:6,:]
            ind += indend
        end
    end
    return corespcsinds,collect(flatten(corerxninds)),edgespcsinds,collect(flatten(edgerxninds)),reactantindices,productindices,coretoedgespcmap,coretoedgerxnmap
end

"""
Precalculate important indices and maps for use in edge analysis
"""
function getkeyselectioninds(coreedgedomain::AbstractDomain,coreedgeinters,domain,inters)
    corespcsinds = 1:length(domain.phase.species)
    edgespcsinds = length(domain.phase.species)+1:length(coreedgedomain.phase.species)
    corerxninds = 1:length(domain.phase.reactions)
    edgerxninds = length(domain.phase.reactions)+1:length(coreedgedomain.phase.reactions)
    reactantindices = coreedgedomain.rxnarray[1:3,:]
    productindices = coreedgedomain.rxnarray[4:6,:]
    coretoedgespcmap = Dict([i=>findfirst(isequal(spc),coreedgedomain.phase.species) for (i,spc) in enumerate(domain.phase.species)])
    coretoedgerxnmap = Dict([i=>findfirst(isequal(rxn),coreedgedomain.phase.reactions) for (i,rxn) in enumerate(domain.phase.reactions)])
    for j = 3:length(domain.indexes)
        coretoedgespcmap[domain.indexes[j]] = coreedgedomain.indexes[j]
    end
    return corespcsinds,corerxninds,edgespcsinds,edgerxninds,reactantindices,productindices,coretoedgespcmap,coretoedgerxnmap
end

"""
Process flux information into useful quantities for edge analysis
"""
function processfluxes(sim::SystemSimulation,
        corespcsinds,corerxninds,edgespcsinds,edgerxninds)

    dydt,rts,frts,rrts,cs = calcfluxes(sim)

    corespeciesrates = abs.(dydt[corespcsinds])
    charrate = sqrt(dot(corespeciesrates,corespeciesrates))
    edgespeciesrates = abs.(dydt[edgespcsinds])
    edgereactionrates = rts[edgerxninds]
    corespeciesrateratios = corespeciesrates./charrate
    edgespeciesrateratios = edgespeciesrates./charrate
    corereactionrates = rts[corerxninds]
    corespeciesconcentrations = cs[corespcsinds]
    corespeciesconsumptionrates = zeros(length(corespeciesconcentrations))
    corespeciesproductionrates = zeros(length(corespeciesconcentrations))

    #process core species consumption and production rates
    index = 1
    for d in getfield.(sim.sims,:domain)
        for i = 1:size(d.rxnarray)[2]
            if any(d.rxnarray[:,i].>length(corespeciesconcentrations))
                continue
            end
            for j = 1:3
                if d.rxnarray[j,i] != 0
                    corespeciesconsumptionrates[d.rxnarray[j,i]] += frts[i+index]
                    corespeciesproductionrates[d.rxnarray[j,i]] += rrts[i+index]
                else
                    break
                end
            end
            for j = 4:6
                if d.rxnarray[j,i] != 0
                    corespeciesproductionrates[d.rxnarray[j,i]] += frts[i+index]
                    corespeciesconsumptionrates[d.rxnarray[j,i]] += rrts[i+index]
                else
                    break
                end
            end
        end
        index += size(d.rxnarray)[2]
    end
    for d in sim.interfaces
        if hasproperty(d,:rxnarray)
            for i = 1:size(d.rxnarray)[2]
                if any(d.rxnarray[:,i].>length(corespeciesconcentrations))
                    continue
                end
                for j = 1:3
                    if d.rxnarray[j,i] != 0
                        corespeciesconsumptionrates[d.rxnarray[j,i]] += frts[i+index]
                        corespeciesproductionrates[d.rxnarray[j,i]] += rrts[i+index]
                    else
                        break
                    end
                end
                for j = 4:6
                    if d.rxnarray[j,i] != 0
                        corespeciesproductionrates[d.rxnarray[j,i]] += frts[i+index]
                        corespeciesconsumptionrates[d.rxnarray[j,i]] += rrts[i+index]
                    else
                        break
                    end
                end
            end
            index += size(d.rxnarray)[2]
        end
    end

    return dydt,rts,frts,rrts,cs,corespeciesrates,charrate,edgespeciesrates,edgereactionrates,corespeciesrateratios,edgespeciesrateratios,corereactionrates,corespeciesconcentrations,corespeciesproductionrates,corespeciesconsumptionrates
end

"""
Process flux information into useful quantities for edge analysis
"""
function processfluxes(sim::Simulation,corespcsinds,corerxninds,edgespcsinds,edgerxninds)

    dydt,rts,frts,rrts,cs = calcfluxes(sim)

    corespeciesrates = abs.(dydt[corespcsinds])
    charrate = sqrt(dot(corespeciesrates,corespeciesrates))
    edgespeciesrates = abs.(dydt[edgespcsinds])
    edgereactionrates = rts[edgerxninds]
    corespeciesrateratios = corespeciesrates./charrate
    edgespeciesrateratios = edgespeciesrates./charrate
    corereactionrates = rts[corerxninds]
    corespeciesconcentrations = cs[corespcsinds]
    corespeciesconsumptionrates = zeros(length(corespeciesconcentrations))
    corespeciesproductionrates = zeros(length(corespeciesconcentrations))

    #process core species consumption and production rates
    d = sim.domain
    for i = 1:size(d.rxnarray)[2]
        if any(d.rxnarray[:,i].>length(corespeciesconcentrations))
            continue
        end
        for j = 1:3
            if d.rxnarray[j,i] != 0
                corespeciesconsumptionrates[d.rxnarray[j,i]] += frts[i]
                corespeciesproductionrates[d.rxnarray[j,i]] += rrts[i]
            else
                break
            end
        end
        for j = 4:6
            if d.rxnarray[j,i] != 0
                corespeciesproductionrates[d.rxnarray[j,i]] += frts[i]
                corespeciesconsumptionrates[d.rxnarray[j,i]] += rrts[i]
            else
                break
            end
        end
    end

    return dydt,rts,frts,rrts,cs,corespeciesrates,charrate,edgespeciesrates,edgereactionrates,corespeciesrateratios,edgespeciesrateratios,corereactionrates,corespeciesconcentrations,corespeciesproductionrates,corespeciesconsumptionrates
end

export processfluxes

"""
Calculate branching numbers for appropriate reactions for use in evaluating
the branching criterion: 1.0 < branchfactor * max(branchingratio,branchingratiomax) * rateratio^branchingindex
"""
function calcbranchingnumbers(sim,reactantinds,productinds,corespcsinds,corerxninds,edgereactionrates,corespeciesrateratios,
        corespeciesconsumptionrates,branchfactor,branchingratiomax,branchingindex)
    branchingnums = zeros(length(edgereactionrates))
    for index in 1:length(edgereactionrates)
        reactionrate = edgereactionrates[index]

        if reactionrate > 0
            reactantside = reactantinds[:,index+length(corerxninds)]
            productside = productinds[:,index+length(corerxninds)]
        else
            reactantside = productinds[:,index+length(corerxninds)]
            productside = reactantinds[:,index+length(corerxninds)]
        end

        rade = [sim.species[i].radicalelectrons for i in productside if i != 0]

        if maximum(rade) > 1
            continue
        end

        for spcindex in reactantside
            if spcindex == 0
                continue
            elseif spcindex < length(corespcsinds)
                if sim.species[spcindex].radicalelectrons != 1
                    continue
                end
                consumption = corespeciesconsumptionrates[spcindex]
                if consumption != 0
                    br = reactionrate / consumption
                    rr = corespeciesrateratios[spcindex]
                    if br > branchingratiomax
                        br = branchingratiomax
                    end

                    bnum = branchfactor * br * rr^branchingindex

                    if bnum > branchingnums[index]
                        branchingnums[index] = bnum
                    end
                end
            end
        end
    end
   return branchingnums
end

export calcbranchingnumbers

"""
determine species pairings that are concentrated enough that they should be reacted
"""
function updatefilterthresholds!(sim,corespcsinds,corespeciesconcentrations,charrate,
        unimolecularthreshold,bimolecularthreshold,trimolecularthreshold,tolmovetocore,
        filterthreshold)
    unimolecularthresholdrateconstant,bimolecularthresholdrateconstant,trimolecularthresholdrateconstant = getthresholdrateconstants(sim,sim.domain.phase,filterthreshold)

    unimolecularthresholdval = tolmovetocore * charrate / unimolecularthresholdrateconstant
    bimolecularthresholdval = tolmovetocore * charrate / bimolecularthresholdrateconstant
    trimolecularthresholdval = tolmovetocore * charrate / trimolecularthresholdrateconstant

    for i in 1:length(corespcsinds)
        if !unimolecularthreshold[i]
            if corespeciesconcentrations[i] > unimolecularthresholdval
                unimolecularthreshold[i] = true
            end
        end
    end
    for i in 1:length(corespcsinds)
        for j in i:length(corespcsinds)
            if  !bimolecularthreshold[i,j]
                if corespeciesconcentrations[i] * corespeciesconcentrations[j] > bimolecularthresholdval
                    bimolecularthreshold[i,j] = true
                end
            end
        end
    end
    for i in 1:length(corespcsinds)
        for j in i:length(corespcsinds)
            for k in j:length(corespcsinds)
                if !trimolecularthreshold[i,j,k]
                    if corespeciesconcentrations[i] * corespeciesconcentrations[j] * corespeciesconcentrations[k] > trimolecularthresholdval
                        trimolecularthreshold[i,j,k] = true
                    end
                end
            end
        end
    end
end

export updatefilterthresholds!

"""
Determine species/reactions that should be added to the model core, react thresholding and
whether the simulation should be interrupted or terminated
"""
function identifyobjects!(sim,corespcsinds,corerxninds,edgespcsinds,
        edgerxninds,reactantinds,productinds,unimolecularthreshold,bimolecularthreshold,
        trimolecularthreshold,maxedgespeciesrateratios,tolmovetocore,tolinterruptsimulation,
        ignoreoverallfluxcriterion,filterreactions,maxnumobjsperiter,branchfactor,branchingratiomax,
        branchingindex,terminateatmaxobjects,termination,y0,invalidobjects,firsttime,
        filterthreshold)

    rxnarray = vcat()
    t = sim.sol.t[end]
    y = sim.sol.u[end]
    breakflag = false
    numcorespc = length(corespcsinds)
    numcorerxns = length(corerxninds)
    invalidobjectsprintboolean = true
    terminated = false
    conversion = 0.0

    (dydt,rts,frts,rrts,cs,corespeciesratse,charrate,edgespeciesrates,
    edgereactionrates,corespeciesrateratios,edgespeciesrateratios,
    corereactionrates,corespeciesconcentrations,corespeciesproductionrates,
    corespeciesconsumptionrates) = processfluxes(sim,corespcsinds,corerxninds,edgespcsinds,edgerxninds)

    for i = 1:length(edgespeciesrateratios)
        if edgespeciesrateratios[i] > maxedgespeciesrateratios[i]
            maxedgespeciesrateratios[i] = edgespeciesrateratios[i]
        end
    end

    if charrate == 0 && length(edgereactionrates) > 0
        maxspeciesindex = argmax(edgespeciesrates)
        maxspeciesrate = edgespeciesrates[maxspeciesindex]
        name = sim.names[maxspeciesindex]
        @info "at time $t s, species $name was added to model core to avoid singularity"
        push!(invalidobjects,sim.species[maxspeciesindex])
        return (false,true)
    end

    if branchfactor != 0.0 && !firsttime
        branchingnums = calcbranchingnumbers(sim,reactantinds,productinds,corespcsinds,corerxninds,edgereactionrates,
            corespeciesrateratios,corespeciesconsumptionrates,branchfactor,branchingratiomax,branchingindex)
    end

    if filterreactions
        updatefilterthresholds!(sim,corespcsinds,corespeciesconcentrations,charrate,
            unimolecularthreshold,bimolecularthreshold,trimolecularthreshold,tolmovetocore,
            filterthreshold)
    end

    newobjectinds = Array{Int64,1}()
    newobjects = []
    newobjectvals = Array{Float64,1}()
    newobjecttype = []

    tempnewobjects = []
    tempnewobjectinds = Array{Int64,1}()
    tempnewobjectvals = Array{Float64,1}()
    tempnewobjecttype = []

    interrupt = false

    #movement of species to core based on rate ratios

    if !ignoreoverallfluxcriterion
        for (i,ind) in enumerate(edgespcsinds)
            rr = edgespeciesrateratios[i]
            obj = sim.species[ind]
            name = obj.name
            if rr > tolmovetocore
                if !(obj in newobjects || obj in invalidobjects)
                    push!(tempnewobjects,obj)
                    push!(tempnewobjectinds,ind)
                    push!(tempnewobjectvals,rr)
                    push!(tempnewobjecttype,"rr")
                end
            end
            if rr > tolinterruptsimulation
                @info "at time $t sec, species $name at $rr exceeded the minimum rate for simulation interruption of $tolinterruptsimulation"
                interrupt = true
            end
        end

        sortedinds = reverse(sortperm(tempnewobjectvals))

        for q in sortedinds
            push!(newobjects,tempnewobjects[q])
            push!(newobjectinds,tempnewobjectinds[q])
            push!(newobjectvals,tempnewobjectvals[q])
            push!(newobjecttype,tempnewobjecttype[q])
        end

        tempnewobjects = []
        tempnewobjectinds = Array{Int64,1}()
        tempnewobjectvals = Array{Float64,1}()
        tempnewobjecttype = []
    end

    if branchfactor != 0.0 && !firsttime
        for (i,ind) in enumerate(edgerxninds)
            bnum = branchingnums[i]
            if bnum > 1
                obj = sim.reactions[ind+numcorerxns]
                if !(obj in newobjects || obj in invalidobjects)
                    push!(tempnewobjects,obj)
                    push!(tempnewobjectinds,ind)
                    push!(tempnewobjectvals,bnum)
                    push!(tempnewobjecttype,"branching")
                end
            end
        end
        sortedinds = reverse(sortperm(tempnewobjectvals))

        for q in sortedinds
            push!(newobjects,tempnewobjects[q])
            push!(newobjectinds,tempnewobjectinds[q])
            push!(newobjectvals,tempnewobjectvals[q])
            push!(newobjecttype,tempnewobjecttype[q])
        end

        tempnewobjects = []
        tempnewobjectinds = Array{Int64,1}()
        tempnewobjectvals = Array{Float64,1}()
        tempnewobjecttype = []
    end

    if length(invalidobjects) + length(newobjects) > maxnumobjsperiter
        if invalidobjectsprintboolean
            @info "Exceeded max number of objects...removing excess objects"
            invalidobjectsprintboolean = false
        end
        num = maxnumobjsperiter - length(invalidobjects)
        newobjects = newobjects[1:num]
        newobjectinds = newobjectinds[1:num]
        newobjectvals = newobjectvals[1:num]
        newobjecttype = newobjecttype[1:num]
    end

    if terminateatmaxobjects && length(invalidobjects) + length(newobjects) >= maxnumobjsperiter
        @info "Reached max number of objects...preparing to terminate"
        interrupt = true
    end

    if length(newobjects) > 0
        for (i,obj) in enumerate(newobjects)
            val = newobjectvals[i]
            ind = newobjectinds[i]
            if isa(obj, Species)
                name = obj.name
                @info "At time $t sec, species $name at rate ratio $val exceeded the minimum rate for moving to model core of $tolmovetocore"
            elseif isa(obj,ElementaryReaction)
                rstr = getrxnstr(obj)
                @info "at time $t sec, reaction $rstr at a branching number of $val exceeded the threshold of 1 for moving to model core"
            end
        end

        append!(invalidobjects,newobjects)
    end

    if interrupt
        @info "Terminating simulation due to interrupt"
    end

    for term in termination
        if isa(term, TerminationTime)
            if t > term.time
                terminated = true
                @info "at time $t sec, reached target termination time"
            end
        elseif isa(term, TerminationRateRatio)
            if maxcharrate != 0.0 && charrate / maxcharrate < term.ratio
                terminated = true
                ratio = term.ratio
                @info "At time $t sec, reached target termination rate ratio $ratio"
            end
        else isa(term, TerminationConversion)
            index = findfirst(isequal(term.species.name),sim.names)
            conversion = 1 - (y[index] / y0[index])
            name = sim.species[index].name
            if conversion >= term.conversion
                terminated = true
                @info "At time $t sec, reeached target termination conversion $conversion of $name"
            end
        end
    end

    if terminated
        for term in termination
            if isa(term, TerminationConversion)
                index = findfirst(isequal(term.species.name),sim.names)
                conversion = 1 - (y[index] / y0[index])
                name = sim.species[index].name
                @info "$name conversion: $conversion"
            end
        end
    end

    return (terminated,interrupt,conversion)
end

export identifyobjects!

"""
run edge analysis to determine objects (species/reactions) that should be added to model core
"""
function selectobjects(react,coreedgedomains,coreedgeinters,domains,inters,
                corep,coreedgep,tolmovetocore,tolinterruptsimulation,ignoreoverallfluxcriterion,filterreactions,
                maxnumobjsperiter,tolbranchrxntocore,branchingratiomax,
                branchingindex,terminateatmaxobjects,termination,
                filterthreshold;
                atol=1e-20,rtol=1e-6,solver=CVODE_BDF())

    (corespcsinds,corerxninds,edgespcsinds,edgerxninds,reactantindices,
                productindices,coretoedgespcmap,coretoedgerxnmap) = getkeyselectioninds(coreedgedomains,coreedgeinters,domains,inters)

    unimolecularthreshold = falses(length(corespcsinds))
    bimolecularthreshold = falses((length(corespcsinds),length(corespcsinds)))
    trimolecularthreshold = falses((length(corespcsinds),length(corespcsinds),length(corespcsinds)))
    maxedgespeciesrateratios = zeros(length(edgespcsinds))
    invalidobjects = []
    terminated = false
    conversion = 0.0
    code = :Success

    if tolbranchrxntocore != 0.0
        branchfactor = 1.0/tolbranchrxntocore
    else
        branchfactor = 0.0
    end

    tf = react.ode.tspan[2]
    inte = init(react.ode,solver,abstol=atol,reltol=rtol);

    t = inte.t
    sim = getsim(inte,react,coreedgedomains,coreedgeinters,corep,coretoedgespcmap)

    y0 = sim.sol[end]
    spcsaddindices = Array{Int64,1}()
    firsttime = true

    n = 1
    while t < tf && code == :Success
        for i = 1:n
            step!(inte)
        end
        code = check_error(inte)
        t = inte.t
        sim = getsim(inte,react,coreedgedomains,coreedgeinters,coreedgep,coretoedgespcmap)
        terminated,interrupt,conversion = identifyobjects!(sim,corespcsinds,corerxninds,edgespcsinds,
            edgerxninds,reactantindices,productindices,unimolecularthreshold,bimolecularthreshold,
                trimolecularthreshold,maxedgespeciesrateratios,tolmovetocore,tolinterruptsimulation,ignoreoverallfluxcriterion,filterreactions,
                maxnumobjsperiter,branchfactor,branchingratiomax,
                branchingindex,terminateatmaxobjects,termination,y0,invalidobjects,firsttime,
                filterthreshold)
        if firsttime
            firsttime = false
        end
        if terminated || interrupt
            break
        end
    end

    if code == :Success
        return (terminated,false,invalidobjects,unimolecularthreshold,
            bimolecularthreshold,trimolecularthreshold,maxedgespeciesrateratios,t,conversion)
    else
        @error "Solver failed with code $code resurrecting job"
        dydt,rts,frts,rrts,cs,corespeciesrates,charrate,edgespeciesrates,edgereactionrates,
        corespeciesrateratios,edgespeciesrateratios,corereactionrates,corespeciesconcentrations,
        corespeciesproductionrates,corespeciesconsumptionrates = processfluxes(sim,corespcsinds,corerxninds,edgespcsinds,edgerxninds)
        ind = edgespcsinds[argmax(edgespeciesrates)]
        invalidobjects = [sim.species[ind]]
        return (terminated,true,invalidobjects,unimolecularthreshold,
            bimolecularthreshold,trimolecularthreshold,maxedgespeciesrateratios,t,conversion)
    end

end

export selectobjects

"""
calculate threshold rate constants for different numbers of reactants
(to some extent the filter assumes rate constants can't be faster than these thresholds)
"""
function getthresholdrateconstants(sim::Simulation,phase,filterthreshold)
    return 2.08366122e10*getT(sim,sim.sol.t[end]),filterthreshold,filterthreshold/1.0e3
end

"""
calculate threshold rate constants for different numbers of reactants
(to some extent the filter assumes rate constants can't be faster than these thresholds)
"""
function getthresholdrateconstants(sim::Simulation,phase::IdealDiluteSolution,filterthreshold)
    T = getT(sim,sim.sol.t[end])
    mu = phase.solvent.mu(T)
    return 2.08366122e10*T,22.2*T/mu,0.11*T/mu
end

export getthresholdrateconstants


module ThreePlayer

import Cribbage: CribbageGame, GameState, Player, nextplayer, UnexpectedPlayerException
import Cribbage.TwoPlayer: incrementscore, numplayers, cardsdealt, cardstocrib, players, reportscore, cardstocrib, isgameover, rotatedealer

export incrementscore, isgameover, players, reportscore, rotatedealer, ThreePlayerGame, ThreePlayerGameState

struct ThreePlayerGame <: CribbageGame
    p₁ :: Player
    p₂ :: Player
    p₃ :: Player
    winningscore :: Int8
    function ThreePlayerGame(
        p₁::Player, p₂::Player, p₃::Player, winningscore::Integer=121)
        @assert(winningscore == 121 || winningscore == 61, "winningscore is either 61 or 121")
        @assert(p₁ ≢ p₂)
        @assert(p₁ ≢ p₃)
        @assert(p₂ ≢ p₃)
        new(p₁, p₂, p₃, winningscore)
    end
end

struct ThreePlayerGameState <: GameState
    game :: ThreePlayerGame
    dealer :: Player
    s₁ :: Int8
    s₂ :: Int8
    s₃ :: Int8
    function ThreePlayerGameState(
            game::ThreePlayerGame,
            dealer::Player,
            s₁::Integer=0,
            s₂::Integer=0,
            s₃::Integer=0
        )
        @assert(dealer ∈ players(game), "dealer is not one of the players of the game")
        @assert(0 ≤ s₁ ≤ game.winningscore, "s₁ is an invalid score")
        @assert(0 ≤ s₂ ≤ game.winningscore, "s₂ is an invalid score")
        @assert(0 ≤ s₃ ≤ game.winningscore, "s₃ is an invalid score")
        new(game, dealer, s₁, s₂, s₃)
    end
end

function numplayers(game::ThreePlayerGame)::Int8
    return 3
end

function cardsdealt(game::ThreePlayerGame)::Int8
    return 5
end

function cardstocrib(game::ThreePlayerGame)::Int8
    return 1
end

function players(game::ThreePlayerGame)::Array{Player,1}
    return [ game.p₁, game.p₂, game.p₃ ]
end

function incrementscore(gamestate::ThreePlayerGameState, player::Player, amount::Integer)
    @assert(amount ≥ 0)
    if player === gamestate.game.p₁
        return ThreePlayerGameState(
            gamestate.game,
            gamestate.dealer,
            min(gamestate.s₁ + amount, gamestate.game.winningscore),
            gamestate.s₂,
            gamestate.s₃
        )
    elseif player === gamestate.game.p₂
        return ThreePlayerGameState(
            gamestate.game,
            gamestate.dealer,
            gamestate.s₁,
            min(gamestate.s₂ + amount, gamestate.game.winningscore),
            gamestate.s₃
        )
    elseif player === gamestate.game.p₃
        return ThreePlayerGameState(
            gamestate.game,
            gamestate.dealer,
            gamestate.s₁,
            gamestate.s₂,
            min(gamestate.s₃ + amount, gamestate.game.winningscore)
        )
    else
        throw(UnexpectedPlayerException("$(player.name) is not one of the players in this game"))
    end
end

function isgameover(gamestate::ThreePlayerGameState)::Bool
    return (
        gamestate.s₁ == gamestate.game.winningscore ||
        gamestate.s₂ == gamestate.game.winningscore ||
        gamestate.s₃ == gamestate.game.winningscore
    )
end

function reportscore(gamestate::ThreePlayerGameState)::String
    return "$(gamestate.s₁) - $(gamestate.s₂) - $(gamestate.s₃)"
end

function rotatedealer(gamestate::ThreePlayerGameState)::ThreePlayerGameState
    return ThreePlayerGameState(
        gamestate.game,
        nextplayer(players(gamestate.game), gamestate.dealer),
        gamestate.s₁,
        gamestate.s₂,
        gamestate.s₃
    )
end

end  # module ThreePlayer


# Once this stabilizes, migrate to a Graphics layer? Only if that supports text, which seems unlikely.
using Colors
using Graphics, Cairo

function annotate!(guidict::Dict, ann; anchored::Bool=true)
    c, zr = guidict["gui"]["canvas"], guidict["roi"]["zoomregion"]
    if anchored
        annf = AnchoredAnnotation((_)->canvasbb(c), (_)->zoombb(zr), ann)
    else
        annf = FloatingAnnotation((_)->canvasbb(c), ann)
    end
    h = hash(annf)
    add_annotation!(guidict["annotations"], h=>annf)
    AnnotationHandle(annf, h)
end

function add_annotation!(anns::Signal{Dict{UInt,Any}}, p::Pair)
    a = value(anns)
    push!(a, p)
    push!(anns, a)
end

struct AnnotationHandle{T}
    ann::T
    hash::UInt
end

function Base.delete!(anns::Signal{Dict{UInt,Any}}, anh::AnnotationHandle)
    delete!(value(anns), anh.hash)
    push!(anns, value(anns))
end
Base.delete!(guidict::Dict, anh::AnnotationHandle) = delete!(guidict["annotations"], anh)

function draw_annotations(canvas, anns)
    for (h, ann) in anns
        draw(canvas, ann)
    end
    nothing
end

function canvasbb(c)
    sz = size(widget(c))
    BoundingBox(1, sz[1], 1, sz[2])
end
function zoombb(zr)
    inds = axes(value(zr))
    BoundingBox(first(inds[2]), last(inds[2]), first(inds[1]), last(inds[1]))
end

abstract type AbstractAnnotation end

# Use this type when you want your annotation to be linked to particular data-coordinates
# (for example, to highlight a particular data point)
# devicebb(data) returns a BoundingBox in device coordinates, userbb(data) returns one in user coordinates.
# It's up to the draw() function to decide how to exploit these (most commonly, with set_coordinates())
mutable struct AnchoredAnnotation{T} <: AbstractAnnotation
    devicebb::Function
    userbb::Function
    data::T
    valid::Bool
end
AnchoredAnnotation(devicebb::Function, userbb::Function, data::T) where {T} =
    AnchoredAnnotation{T}(devicebb, userbb, data, true)

# Use this type when you want your annotation to appear at a specific point on the screen, regardless
# of zoom/resize state (e.g., a scale bar)
mutable struct FloatingAnnotation{T} <: AbstractAnnotation
    devicebb::Function
    data::T
end
# FloatingAnnotation{T}(devicebb::Function, data::T) = AnchoredAnnotation{T}(devicebb, data)

mutable struct AnnotationScalebarFixed{T}
    width::T   # Probably has units
    height::T
    getsize::Function   # syntax w,h = getsize(width,height)
    centerx::Float64
    centery::Float64
    color::Color
end

AnnotationScalebarFixed(width::T, height::T, imsl::Signal{M},
                        centerx::Real, centery::Real,
                        color::Color = RGB(1,1,1)) where {M<:AbstractMatrix,T} =
    AnnotationScalebarFixed{T}(width, height, (wp,hp) -> normalized_lengths(imsl,wp,hp),
                               Float64(centerx), Float64(centery), color)

## Text annotations

mutable struct AnnotationText
    x::Float64
    y::Float64
    z::Float64
    t::Float64
    string::AbstractString
    color::Color
    fontfamily::String
    fontoptions::String
    fontsize::Integer
    fontdesc::String
    angle::Float64
    halign::String
    valign::String
    markup::Bool
    scale::Bool
end

function AnnotationText(x::Real, y::Real, str::AbstractString;
                        z = NaN, t = NaN,
                        color = RGB(0,0,0), angle = 0.0, fontfamily = "sans", fontsize = 10,
                        fontoptions = "",  halign = "center", valign = "center",
                        markup = false, scale=true)
    AnnotationText(Float64(x), Float64(y), Float64(z),
                   Float64(t), str, color, fontfamily, fontoptions,
                   fontsize, fontdescription(fontfamily, fontoptions, fontsize),
                   Float64(angle), halign, valign, markup, scale)
end

fontdescription(fontfamily, fontoptions, fontsize) =
    string(fontfamily, " ", fontoptions, " ", fontsize)

## Point annotations

mutable struct AnnotationPoints{T}
    pts::T
    z::Float64
    t::Float64
    size::Float64
    shape::Char
    color::Color
    linewidth::Float64
    linecolor::Color
    scale::Bool
end

function AnnotationPoints(xys::Vector{Tuple{R,R}}=Tuple{Float64,Float64}[];
                          z = NaN, t = NaN, size=10.0, shape::Char='x', color = RGB(1,1,1),
                          linewidth=1.0, linecolor=color, scale::Bool=false) where R<:Real
    AnnotationPoints{typeof(xys)}(xys, z, t, float(size), shape, to_colorant(color),
                                  float(linewidth), to_colorant(linecolor), scale)
end

function AnnotationPoints(xys::Matrix{R}; z = NaN, t = NaN, size=10.0,
                          shape::Char='x', color = RGB(1,1,1), linewidth=1.0,
                          linecolor=color, scale::Bool=false) where R<:Real
    AnnotationPoints{Matrix{R}}(xys, z, t, float(size),
                                shape, to_colorant(color), float(linewidth),
                                to_colorant(linecolor), scale)
end

function AnnotationPoint(xy::Tuple{Real,Real}; z = NaN, t = NaN, size=10.0,
                         shape::Char='x', color = RGB(1,1,1), linewidth=1.0,
                         linecolor=color, scale::Bool=false)
    AnnotationPoints{Tuple{Float64,Float64}}((Float64(xy[1]),Float64(xy[2])),
                                             z, t, float(size), shape,
                                             to_colorant(color), float(linewidth),
                                             to_colorant(linecolor), scale)
end

AnnotationPoint(x::Real, y::Real; args...) =
    AnnotationPoint((Float64(x), Float64(y)); args...)

## Line annotations

mutable struct AnnotationLines{R<:Union{Real, Tuple{Real, Real}}, T}
    lines::T
    z::Float64
    t::Float64
    linecolor::Color
    linewidth::Float64
    coordinate_order::Vector{Int}

    function AnnotationLines{R,T}(lines::T, z, t, linecolor, linewidth, coord_order_str) where {R,T}
        ord = sortperm(codeunits(coord_order_str))
        @assert coord_order_str[ord] == "xxyy"
        new{R,T}(lines, z, t, linecolor, linewidth, ord)
    end
end

function AnnotationLines(lines::Vector{Tuple{Tuple{R, R},Tuple{R, R}}}=
                         Tuple{Tuple{Float64, Float64},Tuple{Float64, Float64}}[];
                         z = NaN, t = NaN, color=RGB(1,1,1),
                         linewidth=1.0, coord_order="xyxy") where R<:Real
    AnnotationLines{R,Vector{Tuple{Tuple{R,R},Tuple{R, R}}}}(lines, z, t, color,
                                                             linewidth, coord_order)
end

function AnnotationLines(lines::Matrix{R}; z = NaN, t = NaN, color=RGB(1,1,1),
                         linewidth=1.0, coord_order="xyxy") where R<:Real
    AnnotationLines{R, Matrix{R}}(lines, z, t, color, linewidth, coord_order)
end

function AnnotationLine(line::Tuple{Tuple{R,R},Tuple{R,R}};
                        z = NaN, t = NaN, color=RGB(1,1,1), linewidth=1.0) where R<:Real
    AnnotationLines{R,Tuple{Tuple{R,R},Tuple{R,R}}}(line, z, t, color, linewidth, "xyxy")
end

AnnotationLine(pt1::Tuple{Real,Real}, pt2::Tuple{Real,Real}; args...) =
    AnnotationLine((pt1, pt2); args...)

function AnnotationLine(c1::Real, c2::Real, c3::Real, c4::Real; coord_order="xyxy", args...)
    ord = sortperm(codeunits(coord_order))
    @assert coord_order[ord] == "xxyy"
    (x1,x2,y1,y2) = [c1,c2,c3,c4][ord]
    AnnotationLine((Float64(x1), Float64(y1)),
                   (Float64(x2), Float64(y2)); args...)
end

## Box annotations

mutable struct AnnotationBox
    left::Float64
    top::Float64
    right::Float64
    bottom::Float64
    z::Float64
    t::Float64
    linecolor::Color
    linewidth::Float64
end

function AnnotationBox(c1::Real, c2::Real, c3::Real, c4::Real; z = NaN, t = NaN,
                       color=RGB(1,1,1), linewidth=1.0, coord_order="xyxy")
    ord = sortperm(codeunits(coord_order))
    @assert coord_order[ord] == "xxyy"
    (x1, x2, y1, y2) = [c1, c2, c3, c4][ord]
    (x1, x2) = minmax(x1, x2)
    (y1, y2) = minmax(y1, y2)
    AnnotationBox(x1, y1, x2, y2, z, t, color, linewidth)
end

function AnnotationBox(pt1::Tuple{Real, Real}, pt2::Tuple{Real, Real};
                       coord_order="xyxy", args...)
    AnnotationBox(pt1..., pt2...; coord_order=coord_order, args...)
end

AnnotationBox(bb::BoundingBox; args...) = AnnotationBox(bb.xmin, bb.ymin, bb.xmax, bb.ymax; args...)

"""
    wnorm, hnorm = normalized_lengths(img, width, height)

Convert absolute `width` and `height` into normalized
variants. Depends on the [`pixelspacing`](@ref) of `img`.
"""
function normalized_lengths(imsl::AbstractMatrix, width, height)
    ps = pixelspacing(imsl)
    inds = axes(imsl)
    w = width/(ps[2]*length(inds[2]))
    h = height/(ps[1]*length(inds[1]))
    w, h
end
normalized_lengths(imsl::Signal, width, height) =
    normalized_lengths(value(imsl), width, height)

"""
    scalebar(guidict::Dict, length; x = 0.8, y = 0.1, color = RGB(1,1,1))

Add a scale bar annotation to the image display controlled by
`guidict` (returned by [`imshow`](@ref)). If the
[`pixelspacing`](@ref) of the image is set using Unitful quantities,
`length` should also be expressed in physical units.

`x` and `y` control the placement of the scalebar, and `color` its rendered color.
"""
function scalebar(guidict::Dict, length; x = 0.8, y = 0.1, color = RGB(1,1,1))
    imsl = guidict["roi"]["image roi"]
    ann = AnnotationScalebarFixed(length/1, length/10, imsl, x, y, color)
    annotate!(guidict, ann, anchored=false)
end

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

setvalid!(ann::AnchoredAnnotation, z, t) = (ann.valid = annotation_isvalid(ann.data, z, t))

function annotation_isvalid(dat::Union{AnnotationText,
                                       AnnotationPoints,
                                       AnnotationLines,
                                       AnnotationBox}, z, t)
    (isnan(dat.z) || round(dat.z) == z) && (isnan(dat.t) || round(dat.t) == t)
end

annotation_isvalid(x, z, t) = true

function setvalid!(ann::FloatingAnnotation, z, t)
end

function Gtk.draw(c::Gtk.GtkCanvas, ann::AnchoredAnnotation)
    if ann.valid
        ctx = getgc(c)
        Graphics.save(ctx)
        data = ann.data
        set_coordinates(ctx, ann.devicebb(data), ann.userbb(data))
        scale_x = width(ann.userbb(data))/width(ann.devicebb(data))
        scale_y = height(ann.userbb(data))/height(ann.devicebb(data))
        draw_anchored(ctx, data, scale_x, scale_y)
        restore(ctx)
    end
end

function Gtk.draw(c::Gtk.GtkCanvas, ann::FloatingAnnotation{AnnotationScalebarFixed{T}}) where T
    ctx = getgc(c)
    Graphics.save(ctx)
    data = ann.data
    set_coordinates(ctx, ann.devicebb(data), BoundingBox(0,1,0,1))
    set_source(ctx, data.color)
    w, h = data.getsize(data.width, data.height)
    bb = BoundingBox(-w/2+data.centerx, w/2+data.centerx, -h/2+data.centery, h/2+data.centery)
    rectangle(ctx, bb)
    fill(ctx)
    restore(ctx)
end

function draw_anchored(ctx::GraphicsContext, data::AnnotationText, scale_x, scale_y)
    set_source(ctx, data.color)
    if data.scale && scale_x != 1
        fontdesc = fontdescription(data.fontfamily, data.fontoptions, round(Int,data.fontsize/scale_x))
    else
        fontdesc = data.fontdesc
    end
    Cairo.set_font_face(ctx, fontdesc)
    Cairo.text(ctx, data.x-0.5, data.y-0.5, data.string, halign = data.halign, valign = data.valign,
               angle = data.angle, markup = data.markup)
end

function draw_anchored(ctx::GraphicsContext, data::AnnotationPoints, scale_x, scale_y)
    set_line_width(ctx, data.linewidth)
    set_source(ctx, data.linecolor)
    if data.scale
        sz_x = sz_y = data.size
    else
        sz_x =  scale_x * data.size
        sz_y =  scale_y * data.size
    end
    draw_pts(ctx, data.pts, sz_x, sz_y, data.shape, data.color, data.linecolor)
end

draw_pts(ctx::GraphicsContext, pt::NTuple{2}, args...) = draw_pt(ctx, pt, args...)

function draw_pts(ctx::GraphicsContext, pts::Vector{Tuple{R,R}}, args...) where R<:Real
    for pt in pts
        draw_pt(ctx, pt, args...)
    end
end

function draw_pts(ctx::GraphicsContext, pts::Matrix, args...)
    @assert size(pts,1) == 2
    for i in 1:size(pts,2)
        pt = pts[:,i]
        draw_pt(ctx, pt, args...)
    end
end

function draw_pt(ctx::GraphicsContext, pt, sz_x, sz_y, shape::Char, color::Color, linecolor::Color)
    x::Float64,y::Float64 = pt
    hsz_x = sz_x/2
    hsz_y = sz_y/2

    if (shape == '.') | (shape == 'o')
        move_to(ctx, x, y)
        if sz_x == sz_y
            new_sub_path(ctx)
            circle(ctx, x, y, sz_x)
        else
            # draw an ellipse
            Graphics.save(ctx)
            translate(ctx, x, y)
            scale(ctx, sz_x, sz_y)
            new_sub_path(ctx)
            circle(ctx, 0, 0, 1);
            restore(ctx)
        end
        if shape == '.'
            set_source(ctx, color)
            fill_preserve(ctx)
            set_source(ctx, linecolor)
        end
    elseif (shape == 'x') | (shape == '*') | (shape == '+')
        if (shape == 'x') | (shape == '*')
            move_to(ctx, x-hsz_x, y-hsz_y)
            line_to(ctx, x+hsz_x, y+hsz_y)
            move_to(ctx, x-hsz_x, y+hsz_y)
            line_to(ctx, x+hsz_x, y-hsz_y)
        end
        if (shape == '+') | (shape == '*')
            move_to(ctx, x-hsz_x, y)
            line_to(ctx, x+hsz_x, y)
            move_to(ctx, x, y-hsz_y)
            line_to(ctx, x, y+hsz_y)
        end
    end

    stroke(ctx)
end

function draw_anchored(ctx::GraphicsContext, data::AnnotationLines, args...)
    set_line_width(ctx, data.linewidth)
    set_source(ctx, data.linecolor)
    draw_lines(ctx, data.lines, data.coordinate_order)
end

draw_lines(ctx::GraphicsContext, line::Tuple{Tuple{Real, Real},Tuple{Real, Real}}, _) =
    draw_line(ctx, line)

function draw_lines(ctx::GraphicsContext,
                    lines::Vector{Tuple{Tuple{R, R},Tuple{R, R}}}, _) where R<:Real
    for line in lines
        draw_line(ctx, line)
    end
end

function draw_lines(ctx::GraphicsContext, lines::Matrix{R}, coordinate_order) where R<:Real
    for i in 1:size(lines, 2)
        pt = tuple(lines[coordinate_order,i]...)
        draw_line(ctx, pt)
    end
end

function draw_lines(ctx::GraphicsContext, lines::Matrix{R}, _) where R<:Tuple{Real,Real}
    for i in 1:size(lines, 2)
        pt = tuple(lines[:,i]...)
        draw_line(ctx, pt)
    end
end

function draw_line(ctx::GraphicsContext, line::Tuple{Tuple{Real, Real},Tuple{Real, Real}})
    (x1, y1), (x2, y2) = line
    move_to(ctx, x1, y1)
    line_to(ctx, x2, y2)
    stroke(ctx)
end

function draw_line(ctx::GraphicsContext, line::Tuple{Real, Real, Real, Real})
    x1, y1, x2, y2 = line
    move_to(ctx, x1, y1)
    line_to(ctx, x2, y2)
    stroke(ctx)
end

## Box

function draw_anchored(ctx::GraphicsContext, data::AnnotationBox, args...)
    set_line_width(ctx, data.linewidth)
    set_source(ctx, data.linecolor)
    draw_box(ctx, data.top, data.bottom, data.left, data.right)
end

function draw_box(ctx::GraphicsContext, top, bottom, left, right)
    move_to(ctx, left, top)
    line_to(ctx, right, top)
    line_to(ctx, right, bottom)
    line_to(ctx, left, bottom)
    line_to(ctx, left, top)
    stroke(ctx)
end

to_colorant(c::Colorant) = c
to_colorant(str::AbstractString) = parse(Colorant, str)


# Residuals utilities

##################################################
# Residuals definition
"Add residuals for least squares optimization."
function KN_add_rsds(m::Model, ncons::Integer)
    ptr_cons = zeros(Cint, ncons)
    ret = @kn_ccall(add_rsds, Cint, (Ptr{Cvoid}, Cint, Ptr{Cint}), m.env, ncons, ptr_cons)
    _checkraise(ret)
    return ptr_cons
end

function KN_add_rsd(m::Model)
    ptr_cons = Cint[0]
    ret = @kn_ccall(add_rsd, Cint, (Ptr{Cvoid}, Ptr{Cint}), m.env, ptr_cons)
    _checkraise(ret)
    return ptr_cons[1]
end

##################################################
# Residuals structure
##################################################

# add structure of linear constraint
"""
Add linear structure to the residual functions.
Each component i of arrays indexRsds, indexVars and coefs adds a linear
term:
   coefs[i]*x[indexVars[i]]
to residual r[indexRsds[i]].

"""
function KN_add_rsd_linear_struct(m::Model,
                                  indexRsds::Vector{Cint},
                                  indexVars::Vector{Cint},
                                  coefs::Vector{Float64})
    # get number of constraints
    nnz = length(indexRsds)
    @assert nnz == length(indexVars) == length(coefs)
    ret = @kn_ccall(add_rsd_linear_struct,
                    Cint,
                    (Ptr{Cvoid}, KNLONG, Ptr{Cint}, Ptr{Cint}, Ptr{Cdouble}),
                    m.env,
                    nnz,
                    indexRsds,
                    indexVars,
                    coefs)
    _checkraise(ret)
end

function KN_add_rsd_linear_struct(m::Model,
                                  indexRsd::Integer,
                                  indexVar::Vector{Cint},
                                  coefs::Vector{Float64})
    # get number of constraints
    nnz = length(indexVar)
    @assert nnz == length(coefs)
    ret = @kn_ccall(add_rsd_linear_struct_one,
                    Cint,
                    (Ptr{Cvoid}, KNLONG, Cint, Ptr{Cint}, Ptr{Cdouble}),
                    m.env,
                    nnz,
                    indexRsd,
                    indexVar,
                    coefs)
    _checkraise(ret)
end
KN_add_rsd_linear_struct(m::Model, indexRsd::Integer, indexVar::Integer, coef::Float64) =
    KN_add_rsd_linear_struct(m, indexRsd, Int32[indexVar], [coef])

##################################################
# Residuals getters
##################################################
function KN_get_rsd_values(m::Model)
    nc = KN_get_number_cons(m)
    rsdvals = zeros(Cdouble, nc)
    ret = @kn_ccall(get_rsd_values_all, Cint, (Ptr{Cvoid}, Ptr{Cdouble}),
                    m.env, rsdvals)
    _checkraise(ret)
    return rsdvals
end

function KN_get_rsd_values(m::Model, indexRsds::Vector{Cint})
    nc = length(indexRsds)
    rsdvals = zeros(Cdouble, nc)
    ret = @kn_ccall(get_rsd_values, Cint,
                    (Ptr{Cvoid}, Cint, Ptr{Cint}, Ptr{Cdouble}),
                    m.env, nc, indexRsds, rsdvals)
    _checkraise(ret)
    return rsdvals
end

function KN_get_rsd_values(m::Model, indexRsd::Integer)
    nc = 1
    rsdvals = zeros(Cdouble, nc)
    ret = @kn_ccall(get_rsd_value, Cint, (Ptr{Cvoid}, Cint, Ptr{Cdouble}),
                    m.env, indexRsd, rsdvals)
    _checkraise(ret)
    return rsdvals
end

##################################################
# Residuals constants
##################################################
function KN_add_rsd_constants(m::Model, indexRsds::Vector{Cint}, constants::Vector{Cdouble})
    nnc = length(constants)
    @assert length(indexRsds) == length(constant)
    ret = @kn_ccall(add_rsd_constants, Cint,
                    (Ptr{Cvoid}, Cint, Ptr{Cint}, Ptr{Cdouble}),
                    m.env,
                    nnc,
                    indexRsds,
                    constants)
    _checkraise(ret)
end

function KN_add_rsd_constants(m::Model, constants::Vector{Cdouble})
    nnc = length(constants)
    ret = @kn_ccall(add_rsd_constants_all, Cint,
                    (Ptr{Cvoid}, Ptr{Cdouble}),
                    m.env, constants)
    _checkraise(ret)
end

function KN_add_rsd_constant(m::Model, indexRsd::Integer, constant::Cdouble)
    ret = @kn_ccall(add_rsd_constant, Cint,
                    (Ptr{Cvoid}, Cint, Cdouble),
                    m.env, indexRsd, constant)
    _checkraise(ret)
end


export
    VectorTextNode,
    set_text!

import ..Nodes.draw

using ..Rendering

mutable struct VectorTextNode <: Ranger.AbstractNode
    base::Nodes.NodeData
    transform::Nodes.TransformProperties{Float64}

    text::String

    color::Palette

    # Holds font vector lines
    mesh::Geometry.Mesh

    function VectorTextNode(world::Ranger.World, name::String, parent::Ranger.AbstractNode)
        o = new()

        o.base = Nodes.NodeData(name, parent, world)
        o.transform = Nodes.TransformProperties{Float64}()
        o.color = Rendering.White()
        o.mesh = Geometry.Mesh()

        o
    end
end

function set_text!(node::VectorTextNode, text::String)
    node.text = text
    Nodes.set_dirty!(node, true);
end

function rebuild!(node::VectorTextNode, context::Rendering.RenderContext)
    # Use glyph properties to adjust char location.
    xpos = 0.0

    for c in node.text
        glyph = Rendering.get_glyph(context.vector_font, c)

        for vertex in glyph.vectors
            Geometry.add_vertex!(node.mesh, vertex.x + xpos, vertex.y)
        end

        xpos += context.vector_font.horizontal_offset
    end

    Geometry.build!(node.mesh)
end

function Nodes.draw(node::VectorTextNode, context::Rendering.RenderContext)
    # Transform this node's vertices using the context
    if Nodes.is_dirty(node)
        rebuild!(node, context)
        Rendering.transform!(context, node.mesh)
        Nodes.set_dirty!(node, false)
    end

    Rendering.set_draw_color(context, node.color)
    Rendering.render_lines(context, node.mesh);
end


module GenerationWrapper

using JavaCall

const AbsoluteDate = @jimport org.orekit.time.AbsoluteDate
const AbstractMeasurementBuilder = @jimport org.orekit.estimation.measurements.generation.AbstractMeasurementBuilder
const AbstractScheduler = @jimport org.orekit.estimation.measurements.generation.AbstractScheduler
const AngularAzEl = @jimport org.orekit.estimation.measurements.AngularAzEl
const AngularAzElBuilder = @jimport org.orekit.estimation.measurements.generation.AngularAzElBuilder
const AngularRaDec = @jimport org.orekit.estimation.measurements.AngularRaDec
const AngularRaDecBuilder = @jimport org.orekit.estimation.measurements.generation.AngularRaDecBuilder
const Class = @jimport java.lang.Class
const ContinuousScheduler = @jimport org.orekit.estimation.measurements.generation.ContinuousScheduler
const CorrelatedRandomVectorGenerator = @jimport org.hipparchus.random.CorrelatedRandomVectorGenerator
const DatesSelector = @jimport org.orekit.time.DatesSelector
const Enum = @jimport java.lang.Enum
const EstimationModifier = @jimport org.orekit.estimation.measurements.EstimationModifier
const EventBasedScheduler = @jimport org.orekit.estimation.measurements.generation.EventBasedScheduler
const EventDetector = @jimport org.orekit.propagation.events.EventDetector
const Frame = @jimport org.orekit.frames.Frame
const Generator = @jimport org.orekit.estimation.measurements.generation.Generator
const GroundStation = @jimport org.orekit.estimation.measurements.GroundStation
const InterSatellitesRange = @jimport org.orekit.estimation.measurements.InterSatellitesRange
const InterSatellitesRangeBuilder = @jimport org.orekit.estimation.measurements.generation.InterSatellitesRangeBuilder
const JString = @jimport java.lang.String
const List = @jimport java.util.List
const MeasurementBuilder = @jimport org.orekit.estimation.measurements.generation.MeasurementBuilder
const Object = @jimport java.lang.Object
const ObservableSatellite = @jimport org.orekit.estimation.measurements.ObservableSatellite
const ObservedMeasurement = @jimport org.orekit.estimation.measurements.ObservedMeasurement
const Optional = @jimport java.util.Optional
const PV = @jimport org.orekit.estimation.measurements.PV
const PVBuilder = @jimport org.orekit.estimation.measurements.generation.PVBuilder
const Position = @jimport org.orekit.estimation.measurements.Position
const PositionBuilder = @jimport org.orekit.estimation.measurements.generation.PositionBuilder
const Propagator = @jimport org.orekit.propagation.Propagator
const Range = @jimport org.orekit.estimation.measurements.Range
const RangeBuilder = @jimport org.orekit.estimation.measurements.generation.RangeBuilder
const RangeRate = @jimport org.orekit.estimation.measurements.RangeRate
const RangeRateBuilder = @jimport org.orekit.estimation.measurements.generation.RangeRateBuilder
const Scheduler = @jimport org.orekit.estimation.measurements.generation.Scheduler
const SignSemantic = @jimport org.orekit.estimation.measurements.generation.SignSemantic
const SortedSet = @jimport java.util.SortedSet
const SpacecraftState = @jimport org.orekit.propagation.SpacecraftState
const TurnAroundRange = @jimport org.orekit.estimation.measurements.TurnAroundRange
const TurnAroundRangeBuilder = @jimport org.orekit.estimation.measurements.generation.TurnAroundRangeBuilder

export AbstractMeasurementBuilder
export AbstractScheduler
export AngularAzElBuilder
export AngularRaDecBuilder
export ContinuousScheduler
export EventBasedScheduler
export Generator
export InterSatellitesRangeBuilder
export MeasurementBuilder
export PVBuilder
export PositionBuilder
export RangeBuilder
export RangeRateBuilder
export Scheduler
export SignSemantic
export TurnAroundRangeBuilder
export add_modifier
export add_propagator
export add_scheduler
export build
export compare_to
export describe_constable
export equals
export generate
export get_builder
export get_class
export get_declaring_class
export get_modifiers
export get_propagator
export get_selector
export hash_code
export init
export measurement_is_feasible
export name
export notify
export notify_all
export ordinal
export to_string
export value_of
export values
export wait

include("abstract_measurement_builder.jl")
include("abstract_scheduler.jl")
include("angular_az_el_builder.jl")
include("angular_ra_dec_builder.jl")
include("continuous_scheduler.jl")
include("event_based_scheduler.jl")
include("generator.jl")
include("inter_satellites_range_builder.jl")
include("measurement_builder.jl")
include("position_builder.jl")
include("pv_builder.jl")
include("range_builder.jl")
include("range_rate_builder.jl")
include("scheduler.jl")
include("sign_semantic.jl")
include("turn_around_range_builder.jl")

end


module SkyRoom

export main

using Dates, WGLMakie, JSServe, ImageCore, FilePathsBase, CSV, Pkg.TOML, Tar, FileIO, ImageMagick, Observables, PyCall, LibSerialPort, Missings#, MemoryHunter
using FilePathsBase: /
using JSServe.DOM
using JSServe: @js_str

# function print_sizes()
#     mem = Pair{String, Int}[]
#     for m in values(Base.loaded_modules), vs in names(m, all = true)
#         if isdefined(m, vs)
#             v = getfield(m, vs)
#             x = Base.summarysize(v)
#             if x > 10^6
#                 push!(mem, string(m, "; ", vs, ": ") => x)
#             end
#         end
#     end
#     sort!(mem, by = last, rev = true)
#     n = length(mem)
#     if n > 30
#         deleteat!(mem, 31:n)
#     end
#     println("Memory usage:")
#     for (txt, i) in mem
#         println(txt, Base.format_bytes(i))
#     end
#     println("")
# end

const t₀ = Ref{DateTime}()
const datadir = Ref{PosixPath}()
const tempdatadir = Ref{PosixPath}()
const nicolas = Ref{Bool}()
const baudrate = Ref{Int}()
const top_rpm = Ref{Int}()

function __init__()
    t₀[] = now()
    datadir[] = p"/home/pi/mnt/data"
    isdir(datadir[]) || mkpath(datadir[])
    tempdatadir[] = p"/home/pi/mnt/tmp"
    isdir(tempdatadir[]) || mkpath(tempdatadir[])
    nicolas[] = Base.Libc.gethostname() == "nicolas"
    baudrate[] = 9600
    top_rpm[] = 12650
    py"""
    import picamera
    import io

    def PC():
        return picamera.PiCamera()

    def PIO():
        return io.BytesIO()
    """

end

include("cobs.jl")
include("abstractarduinos.jl")
include("leds.jl")
# if nicolas[]
include("winds.jl")
# end

get_rpms(::Nothing) = nothing
record(::Nothing, _) = nothing
Base.isopen(::Nothing) = true
Base.close(::Nothing) = nothing

include("camera.jl")

function initialize()
    fan_ports = ["/dev/serial/by-id/usb-Arduino__www.arduino.cc__0043_957353530323510141D0-if00", "/dev/serial/by-id/usb-Arduino__www.arduino.cc__0043_95635333930351917172-if00", "/dev/serial/by-id/usb-Arduino__www.arduino.cc__0043_95735353032351010260-if00", "/dev/serial/by-id/usb-Arduino__www.arduino.cc__0043_55838323435351213041-if00", "/dev/serial/by-id/usb-Arduino__www.arduino.cc__0043_957353530323514121D0-if00"]
    allwind = nicolas[] ? AllWind([FanArduino(id, port) for (id, port) in enumerate(fan_ports) if isconnected(port)], 1)  : nothing
    led_arduino = LEDArduino()
    framerate = 30
    camera = PiCamera(framerate, 67, 67, 4)

    data = Observable((frame = snap(camera), trpms = get_rpms(allwind)))
    task = @async while isopen(allwind) && isopen(camera)
        # try
            data[] = (; frame = snap(camera), trpms = get_rpms(allwind))
            sleep(0.01)
        #=catch e
            @show now()
            MemoryHunter.print_sizes()
            @show e
            throw(e)
        end=#
    end
    return allwind, led_arduino, camera, data, task
end

function plotrpm(trpms)
    if isnothing(trpms[])
        return nothing
    end
    rpms = map(trpms) do (_, x)
        collect(Missings.replace(Iterators.flatten(x), NaN))
    end
    nports = length(last(trpms[]))
    rpmplt_cont = (colors = repeat(1:nports, inner = [3]), x = vcat(((i - 1)*(nports - 1) + 1 : (i - 1)*(nports - 1) + 3 for i in 1:nports)...), y = top_rpm[]*ones(3nports), resolution = (540, round(Int, 3nports + 3*540/(3nports+4))))
    rpmplot = Scene(show_axis = false, resolution = rpmplt_cont.resolution)
    barplot!(rpmplot, rpmplt_cont.x, rpmplt_cont.y, color = :white, strokecolor = :black, strokewidth = 1)
    barplot!(rpmplot, rpmplt_cont.x, rpms, color = rpmplt_cont.colors, strokecolor = :transparent, strokewidth = 0)
    disconnect!(AbstractPlotting.camera(rpmplot))
    return rpmplot
end

parse2wind(windrow) = [Wind(id, v) for (id, v) in enumerate(windrow)]
parse2stars(starsrow) = [Star(v...) for v in Iterators.partition(starsrow, 4) if !any(ismissing, v)]

function parse2one(setup_file)
    rs = NamedTuple{(:label, :stars),Tuple{String,Array{Star,1}}}[]
    for nt in CSV.Rows(setup_file, header = 1:2)
        if !ismissing(nt.setup_label)
            t = Tuple(nt)
            stars = parse2stars(t[2:end])
            push!(rs, (label = t[1], stars = stars))
        end
    end
    rs
end

function parse2both(setup_file)
    rs = NamedTuple{(:label, :fans, :stars),Tuple{String,Array{Wind,1},Array{Star,1}}}[]
    for nt in CSV.Rows(setup_file, header = 1:2)
        if !ismissing(nt.setup_label)
            t = Tuple(nt)
            fans = parse2wind(t[2:6])
            stars = parse2stars(t[7:end])
            push!(rs, (label = t[1], fans = fans, stars = stars))
        end
    end
    rs
end

function get_setups()
    setupsurl = nicolas[] ? "https://docs.google.com/spreadsheets/d/e/2PACX-1vQNLWhLfp_iuW68j7SM6Px8ysTmbrfmrP_7ipXK9BkfzBgfqn3Mj7ra177mZyHlY5NLA3SDtfYNTROv/pub?gid=0&single=true&output=csv" : "https://docs.google.com/spreadsheets/d/e/2PACX-1vSfv92ymTJjwdU-ft9dgglOOnxPVWwtk6gFIVSocHM3jSfHkjYk-mtEXl3g96-735Atbk1LBRt-8lAY/pub?gid=0&single=true&output=csv"
    setup_file = download(setupsurl)
    nicolas[] ? parse2both(setup_file) : parse2one(setup_file)
end

_fun(::Nothing, _) = nothing
_fun(allwind, setup) = for a in allwind.arduinos
    a.pwm[] = setup.fans[a.id].pwm
end

function button(allwind, led_arduino, setup, setuplog)
    button_class = "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
    b = JSServe.Button(string(setup.label), class = button_class)
    on(b) do _
        _fun(allwind, setup)
        led_arduino.pwm[] = parse2arduino(setup.stars)
        push!(setuplog, now() => todict(setup))
    end
    return b
end

typedict(x::T) where {T} = Dict(fn=>getfield(x, fn) for fn ∈ fieldnames(T))

function todict(setup)
    x = Dict{Symbol, Any}(pairs(setup))
    if haskey(x, :fans)
        x[:fans] = typedict.(x[:fans])
    end
    x[:stars] = typedict.(x[:stars])
    return x
end

stop_record(::Nothing) = nothing
stop_record(allwind) = close(allwind.io)

function record(tf, allwind, camera, timestamp, setuplog)
    if tf
        timestamp[] = string(now())
        folder = datadir[] / timestamp[]
        !isdir(folder) && mkdir(folder)
        camera.cam.recording && camera.cam.stop_recording()
        camera.cam.start_recording(string(folder / "video.h264"))
        record(allwind, folder)
        deleteat!(setuplog, 1:length(setuplog) - 1)
    else
        camera.cam.stop_recording()
        stop_record(allwind)
    end
end

function save(timestamp, beetleid, comment, setuplog, left2backup, msg)
    folder = datadir[] / timestamp[]
    if !isdir(folder)
        msg[] = "You haven't recorded a video yet, there is nothing to save"
        return nothing
    end
    msg[] = "Saving"
    md = Dict()
    md["timestamp"] = timestamp[]
    md["beetleid"] = beetleid[]
    md["comment"] = comment[]
    md["setuplog"] = Dict(string(t) => v for (t,v) in setuplog)
    open(folder / "metadata.toml", "w") do io
        TOML.print(io, md)
    end
    left2backup[] += 1
    comment[] = ""
    beetleid[] = ""
    timestamp[] = "garbage"
    msg[] = "Saved"
    return nothing
end

function backup(left2backup, msg)
    msg[] = "Started backing up..."
    for folder in readpath(datadir[])
        name = basename(folder)
        tmp = tempdatadir[] / name * ".tar"
        Tar.create(string(folder), string(tmp))
        rm(folder, recursive = true)
        bucket = nicolas[] ? "nicolas-cage-skyroom" : "top-floor-skyroom2"
        run(`aws s3 mv $tmp s3://$bucket/$name.tar --quiet`)
        left2backup[] = length(readdir(datadir[]))
    end
    msg[] = "Finished backing up!"
end

function handler(allwind, led_arduino, camera, data, session, request)

    data2 = Observable(data[])
    on(session, data) do x
        data2[] = x
    end

    msg = Observable("")
    on(msg) do x
        if !isempty(x)
            @async begin
                sleep(3)
                msg[] = ""
            end
        end
    end

    frame = map(data2) do x
        x.frame
    end
    trpms = map(data2) do x
        x.trpms
    end

    rpmplot = plotrpm(trpms)
    frameplot = image(frame, scale_plot = false, show_axis = false)
    disconnect!(WGLMakie.camera(frameplot))

    setuplog = []
    timestamp = Ref("")
    recording = JSServe.Checkbox(false)
    on(x -> record(x, allwind, camera, timestamp, setuplog), recording)

    text_class = "border py-2 px-3 text-grey-darkest"
    comment = JSServe.TextField("", class = text_class)
    beetleid = JSServe.TextField("", class = text_class)

    button_class = "bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
    saving = JSServe.Button("Save", class = button_class)
    on(saving) do _
        if recording[]
            recording[] = false
            sleep(0.1)
        end
        save(timestamp, beetleid, comment, setuplog, left2backup, msg)
    end

    backingup = JSServe.Button("Backup", class = button_class)
    left2backup = Observable(length(readdir(datadir[])))
    on(_ -> backup(left2backup, msg), backingup)

    setups = get_setups()
    buttons = [button(allwind, led_arduino, setup, setuplog) for setup in setups]

    # print_sizes()

    grid_class = "grid auto-cols-max grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4"
    return DOM.div(JSServe.TailwindCSS,
                   DOM.div(rpmplot),
                   DOM.div(frameplot),
                   DOM.div(buttons..., class = grid_class),
                   DOM.div("Record ", recording),
                   DOM.div("Beetle ID ", beetleid),
    DOM.div("Comment ", comment),
    DOM.div(saving),
    DOM.div(backingup, left2backup, " runs left to backup"),
    DOM.div(msg),
    class = "grid grid-cols-1 gap-4"
   )
end

function main()
    allwind, led_arduino, camera, data, task = initialize()
    JSServe.Server((session, request) -> handler(allwind, led_arduino, camera, data, session, request), "0.0.0.0", 8082)
end

end # module

# PyCall, Dates, WGLMakie, JSServe, ImageCore, FilePathsBase, CSV, DataFrames, HTTP, Pkg.TOML, Tar, FileIO, ImageMagick, LibSerialPort, Observables


# This file is a part of Julia. License is MIT: https://julialang.org/license

import LinearAlgebra: BlasInt
@static if isdefined(LinearAlgebra, :ARPACKException)
    import LinearAlgebra: ARPACKException
else
    struct ARPACKException <: Exception
        info::BlasInt
    end

    function Base.showerror(io::IO, ex::ARPACKException)
        print(io, "ARPACKException: ")
        if ex.info == -8
            print(io, "error return from calculation of a real Schur form.")
        elseif ex.info == -9
            print(io, "error return from calculation of eigenvectors.")
        elseif ex.info == -14
            print(io, string("did not find any eigenvalues to sufficient accuracy. ",
                "Try with a different starting vector or more Lanczos vectors ",
                "by increasing the value of ncv."))
        else
            print(io, "unspecified ARPACK error: $(ex.info)")
        end
    end
end

## aupd and eupd wrappers

function aupd_wrapper(T, matvecA!::Function, matvecB::Function, solveSI::Function, n::Integer,
                      sym::Bool, cmplx::Bool, bmat::String,
                      nev::Integer, ncv::Integer, which::String,
                      tol::Real, maxiter::Integer, mode::Integer, v0::Vector)
    lworkl = cmplx ? ncv * (3*ncv + 5) : (sym ? ncv * (ncv + 8) :  ncv * (3*ncv + 6) )
    TR = cmplx ? T.types[1] : T
    TOL = Ref{TR}(tol)

    v     = Matrix{T}(undef, n, ncv)
    workd = Vector{T}(undef, 3*n)
    workl = Vector{T}(undef, lworkl)
    rwork = cmplx ? Vector{TR}(undef, ncv) : Vector{TR}()

    if isempty(v0)
        resid = Vector{T}(undef, n)
        info  = Ref{BlasInt}(0)
    else
        resid = deepcopy(v0)
        info  = Ref{BlasInt}(1)
    end
    iparam = zeros(BlasInt, 11)
    ipntr  = zeros(BlasInt, (sym && !cmplx) ? 11 : 14)
    ido    = Ref{BlasInt}(0)

    iparam[1] = BlasInt(1)       # ishifts
    iparam[3] = BlasInt(maxiter) # maxiter
    iparam[7] = BlasInt(mode)    # mode

    zernm1 = 0:(n-1)

    while true
        if cmplx
            naupd(ido, bmat, n, which, nev, TOL, resid, ncv, v, n,
                  iparam, ipntr, workd, workl, lworkl, rwork, info)
        elseif sym
            saupd(ido, bmat, n, which, nev, TOL, resid, ncv, v, n,
                  iparam, ipntr, workd, workl, lworkl, info)
        else
            naupd(ido, bmat, n, which, nev, TOL, resid, ncv, v, n,
                  iparam, ipntr, workd, workl, lworkl, info)
        end
        if info[] != 0
            throw(ARPACKException(info[]))
        end

        x = view(workd, ipntr[1] .+ zernm1)
        y = view(workd, ipntr[2] .+ zernm1)
        if mode == 1  # corresponds to dsdrv1, dndrv1 or zndrv1
            if ido[] == 1
                matvecA!(y, x)
            elseif ido[] == 99
                break
            else
                throw(ARPACKException("unexpected behavior"))
            end
        elseif mode == 3 && bmat == "I" # corresponds to dsdrv2, dndrv2 or zndrv2
            if ido[] == -1 || ido[] == 1
                y[:] = solveSI(x)
            elseif ido[] == 99
                break
            else
                throw(ARPACKException("unexpected behavior"))
            end
        elseif mode == 2 # corresponds to dsdrv3, dndrv3 or zndrv3
            if ido[] == -1 || ido[] == 1
                matvecA!(y, x)
                if sym
                    x[:] = y    # overwrite as per Remark 5 in dsaupd.f
                end
                y[:] = solveSI(y)
            elseif ido[] == 2
                y[:] = matvecB(x)
            elseif ido[] == 99
                break
            else
                throw(ARPACKException("unexpected behavior"))
            end
        elseif mode == 3 && bmat == "G" # corresponds to dsdrv4, dndrv4 or zndrv4
            if ido[] == -1
                y[:] = solveSI(matvecB(x))
            elseif  ido[] == 1
                y[:] = solveSI(view(workd,ipntr[3] .+ zernm1))
            elseif ido[] == 2
                y[:] = matvecB(x)
            elseif ido[] == 99
                break
            else
                throw(ARPACKException("unexpected behavior"))
            end
        else
            throw(ArgumentError("ARPACK mode ($mode) not yet supported"))
        end
    end

    return (resid, v, n, iparam, ipntr, workd, workl, lworkl, rwork, TOL)
end

function eupd_wrapper(T, n::Integer, sym::Bool, cmplx::Bool, bmat::String,
                      nev::Integer, which::String, ritzvec::Bool,
                      TOL::Ref, resid, ncv::Integer, v, ldv, sigma, iparam, ipntr,
                      workd, workl, lworkl, rwork)
    howmny = "A"
    select = Vector{BlasInt}(undef, ncv)
    info   = Ref{BlasInt}(0)

    dmap = x -> abs.(x)
    if iparam[7] == 3 # shift-and-invert
        dmap = x -> abs.(1 ./ (x .- sigma))
    elseif which == "LR" || which == "LA" || which == "BE"
        dmap = real
    elseif which == "SR" || which == "SA"
        dmap = x -> -real(x)
    elseif which == "LI"
        dmap = imag
    elseif which == "SI"
        dmap = x -> -imag(x)
    end

    if cmplx
        d = Vector{T}(undef, nev+1)
        sigmar = Ref{T}(sigma)
        workev = Vector{T}(undef, 2ncv)
        neupd(ritzvec, howmny, select, d, v, ldv, sigmar, workev,
              bmat, n, which, nev, TOL, resid, ncv, v, ldv,
              iparam, ipntr, workd, workl, lworkl, rwork, info)
        if info[] != 0
            throw(ARPACKException(info[]))
        end

        p = sortperm(dmap(d[1:nev]), rev=true)
        return ritzvec ? (d[p], v[1:n, p],iparam[5],iparam[3],iparam[9],resid) : (d[p],iparam[5],iparam[3],iparam[9],resid)
    elseif sym
        d = Vector{T}(undef, nev)
        sigmar = Ref{T}(sigma)
        seupd(ritzvec, howmny, select, d, v, ldv, sigmar,
              bmat, n, which, nev, TOL, resid, ncv, v, ldv,
              iparam, ipntr, workd, workl, lworkl, info)
        if info[] != 0
            throw(ARPACKException(info[]))
        end

        p = sortperm(dmap(d), rev=true)
        return ritzvec ? (d[p], v[1:n, p],iparam[5],iparam[3],iparam[9],resid) : (d[p],iparam[5],iparam[3],iparam[9],resid)
    else
        dr = Vector{T}(undef, nev+1)
        di = Vector{T}(undef, nev+1)
        fill!(dr,NaN)
        fill!(di,NaN)
        sigmar = Ref{T}(real(sigma))
        sigmai = Ref{T}(imag(sigma))
        workev = Vector{T}(undef, 3*ncv)
        neupd(ritzvec, howmny, select, dr, di, v, ldv, sigmar, sigmai,
              workev, bmat, n, which, nev, TOL, resid, ncv, v, ldv,
              iparam, ipntr, workd, workl, lworkl, info)
        if info[] != 0
            throw(ARPACKException(info[]))
        end
        evec = complex.(Matrix{T}(undef, n, nev+1), Matrix{T}(undef, n, nev+1))

        j = 1
        while j <= nev
            if di[j] == 0
                evec[:,j] = v[:,j]
            else # For complex conjugate pairs
                evec[:,j]   = v[:,j] + im*v[:,j+1]
                evec[:,j+1] = v[:,j] - im*v[:,j+1]
                j += 1
            end
            j += 1
        end
        if j == nev+1 && !isnan(di[j])
            if di[j] == 0
                evec[:,j] = v[:,j]
                j += 1
            else
                throw(ARPACKException("unexpected behavior"))
            end
        end

        d = complex.(dr, di)

        if j == nev+1
            p = sortperm(dmap(d[1:nev]), rev=true)
        else
            p = sortperm(dmap(d), rev=true)
            p = p[1:nev]
        end

        return ritzvec ? (d[p], evec[1:n, p],iparam[5],iparam[3],iparam[9],resid) : (d[p],iparam[5],iparam[3],iparam[9],resid)
    end
end

for (T, saupd_name, seupd_name, naupd_name, neupd_name) in
    ((:Float64, :dsaupd_, :dseupd_, :dnaupd_, :dneupd_),
     (:Float32, :ssaupd_, :sseupd_, :snaupd_, :sneupd_))
    @eval begin
        function naupd(ido, bmat, n, evtype, nev, TOL::Ref{$T}, resid::Vector{$T}, ncv, v::Matrix{$T}, ldv,
                       iparam, ipntr, workd::Vector{$T}, workl::Vector{$T}, lworkl, info)
            ccall(($(string(naupd_name)), libarpack), Cvoid,
                  (Ref{BlasInt}, Ptr{UInt8}, Ref{BlasInt}, Ptr{UInt8}, Ref{BlasInt},
                   Ptr{$T}, Ptr{$T}, Ref{BlasInt}, Ptr{$T}, Ref{BlasInt},
                   Ptr{BlasInt}, Ptr{BlasInt}, Ptr{$T}, Ptr{$T}, Ref{BlasInt}, Ref{BlasInt}, Clong, Clong),
                  ido, bmat, n, evtype, nev,
                  TOL, resid, ncv, v, ldv,
                  iparam, ipntr, workd, workl, lworkl, info, sizeof(bmat), sizeof(evtype))
        end

        function neupd(rvec, howmny, select, dr, di, z, ldz, sigmar, sigmai,
                  workev::Vector{$T}, bmat, n, evtype, nev, TOL::Ref{$T}, resid::Vector{$T}, ncv, v, ldv,
                  iparam, ipntr, workd::Vector{$T}, workl::Vector{$T}, lworkl, info)
            ccall(($(string(neupd_name)), libarpack), Cvoid,
                  (Ref{BlasInt}, Ptr{UInt8}, Ptr{BlasInt}, Ptr{$T}, Ptr{$T}, Ptr{$T}, Ref{BlasInt},
                   Ref{$T}, Ref{$T}, Ptr{$T}, Ptr{UInt8}, Ref{BlasInt}, Ptr{UInt8}, Ref{BlasInt},
                   Ptr{$T}, Ptr{$T}, Ref{BlasInt}, Ptr{$T}, Ref{BlasInt},
                   Ptr{BlasInt}, Ptr{BlasInt}, Ptr{$T}, Ptr{$T}, Ref{BlasInt}, Ref{BlasInt}, Clong, Clong, Clong),
                  rvec, howmny, select, dr, di, z, ldz,
                  sigmar, sigmai, workev, bmat, n, evtype, nev,
                  TOL, resid, ncv, v, ldv,
                  iparam, ipntr, workd, workl, lworkl, info, sizeof(howmny), sizeof(bmat), sizeof(evtype))
        end

        function saupd(ido, bmat, n, which, nev, TOL::Ref{$T}, resid::Vector{$T}, ncv, v::Matrix{$T}, ldv,
                       iparam, ipntr, workd::Vector{$T}, workl::Vector{$T}, lworkl, info)
            ccall(($(string(saupd_name)), libarpack), Cvoid,
                  (Ref{BlasInt}, Ptr{UInt8}, Ref{BlasInt}, Ptr{UInt8}, Ref{BlasInt},
                   Ptr{$T}, Ptr{$T}, Ref{BlasInt}, Ptr{$T}, Ref{BlasInt},
                   Ptr{BlasInt}, Ptr{BlasInt}, Ptr{$T}, Ptr{$T}, Ref{BlasInt}, Ref{BlasInt}, Clong, Clong),
                  ido, bmat, n, which, nev,
                  TOL, resid, ncv, v, ldv,
                  iparam, ipntr, workd, workl, lworkl, info, sizeof(bmat), sizeof(which))
        end

        function seupd(rvec, howmny, select, d, z, ldz, sigma,
                       bmat, n, evtype, nev, TOL::Ref{$T}, resid::Vector{$T}, ncv, v::Matrix{$T}, ldv,
                       iparam, ipntr, workd::Vector{$T}, workl::Vector{$T}, lworkl, info)
            ccall(($(string(seupd_name)), libarpack), Cvoid,
                  (Ref{BlasInt}, Ptr{UInt8}, Ptr{BlasInt}, Ptr{$T}, Ptr{$T}, Ref{BlasInt},
                   Ptr{$T}, Ptr{UInt8}, Ref{BlasInt}, Ptr{UInt8}, Ref{BlasInt},
                   Ptr{$T}, Ptr{$T}, Ref{BlasInt}, Ptr{$T}, Ref{BlasInt},
                   Ptr{BlasInt}, Ptr{BlasInt}, Ptr{$T}, Ptr{$T}, Ref{BlasInt}, Ref{BlasInt}, Clong, Clong, Clong),
                  rvec, howmny, select, d, z, ldz,
                  sigma, bmat, n, evtype, nev,
                  TOL, resid, ncv, v, ldv,
                  iparam, ipntr, workd, workl, lworkl, info, sizeof(howmny), sizeof(bmat), sizeof(evtype))
        end
    end
end

for (T, TR, naupd_name, neupd_name) in
    ((:ComplexF64, :Float64, :znaupd_, :zneupd_),
     (:ComplexF32, :Float32, :cnaupd_, :cneupd_))
    @eval begin
        function naupd(ido, bmat, n, evtype, nev, TOL::Ref{$TR}, resid::Vector{$T}, ncv, v::Matrix{$T}, ldv,
                       iparam, ipntr, workd::Vector{$T}, workl::Vector{$T}, lworkl,
                       rwork::Vector{$TR}, info)
            ccall(($(string(naupd_name)), libarpack), Cvoid,
                  (Ref{BlasInt}, Ptr{UInt8}, Ref{BlasInt}, Ptr{UInt8}, Ref{BlasInt},
                   Ptr{$TR}, Ptr{$T}, Ref{BlasInt}, Ptr{$T}, Ref{BlasInt},
                   Ptr{BlasInt}, Ptr{BlasInt}, Ptr{$T}, Ptr{$T}, Ref{BlasInt}, Ptr{$TR}, Ref{BlasInt}),
                  ido, bmat, n, evtype, nev,
                  TOL, resid, ncv, v, ldv,
                  iparam, ipntr, workd, workl, lworkl, rwork, info)
        end

        function neupd(rvec, howmny, select, d, z, ldz, sigma, workev::Vector{$T},
                       bmat, n, evtype, nev, TOL::Ref{$TR}, resid::Vector{$T}, ncv, v::Matrix{$T}, ldv,
                       iparam, ipntr, workd::Vector{$T}, workl::Vector{$T}, lworkl,
                       rwork::Vector{$TR}, info)
            ccall(($(string(neupd_name)), libarpack), Cvoid,
                  (Ref{BlasInt}, Ptr{UInt8}, Ptr{BlasInt}, Ptr{$T}, Ptr{$T}, Ref{BlasInt},
                   Ptr{$T}, Ptr{$T}, Ptr{UInt8}, Ref{BlasInt}, Ptr{UInt8}, Ref{BlasInt},
                   Ptr{$TR}, Ptr{$T}, Ref{BlasInt}, Ptr{$T}, Ref{BlasInt},
                   Ptr{BlasInt}, Ptr{BlasInt}, Ptr{$T}, Ptr{$T}, Ref{BlasInt}, Ptr{$TR}, Ref{BlasInt}),
                  rvec, howmny, select, d, z, ldz,
                  sigma, workev, bmat, n, evtype, nev,
                  TOL, resid, ncv, v, ldv,
                  iparam, ipntr, workd, workl, lworkl, rwork, info)
        end
    end
end


module EchelleTools
using Reexport

include("constants.jl")

include("abstract_types.jl")

include("instruments.jl")

include("util.jl")
#=
export default_telluric_template, default_stellar_template
export make_model_wavelength_range
export ManifestFormatEntry
=#

include("psf.jl")

include("blaze/blaze.jl")

include("obs_data.jl")
export EchelleObservation, EchelleObservationSet
export make_average_observation, z_bc

include("model.jl")
export SpectralOrderModel, SpectralModel
export get_order

export ObservationModel, ObsSeriesModel
export eval_model, eval_model_baseline

#=
include("analyze.jl")
export mean_spectrum, median_spectrum
export lambda_ssb_from_obs, log_lambda_ssb_from_obs
export make_blaze_and_template_1d, make_blaze_and_template_2d
export make_stellar_basis, default_telluric_model
export make_template_1d
export make_spectral_order_model, make_spectral_model
export make_observations_model
# export model_spectrum_1d, model_spectrum_2d

include("pca.jl")
export interp_one_obs_order_for_pca_1d, fit_pca_obs_order_interpolated
# =#

#=
include("toy_data.jl")
export spectral_line, make_toy_data
=#

# Instrument specific files
include("HARPSN/HARPSN.jl")
#using HARPSN
#=
read_harpsn_manifest_file = HARPSN.read_harpsn_manifest_file
read_harpsn_manifest_files = HARPSN.read_harpsn_manifest_files
get_harpsn_data_filenames = HARPSN.get_harpsn_data_filenames
get_harpsn_data_filenames = HARPSN.get_harpsn_data_filenames
read_harpsn_data = HARPSN.read_harpsn_data
read_harpsn_blaze_list = HARPSN.read_blaze_list
export read_harpsn_manifest_file, read_harpsn_manifest_files, get_harpsn_data_filenames, read_harpsn_data
export get_harpsn_data_filenames_and_bsrv
export read_harpsn_blaze_list
=#

#=
include("HPF/HPF.jl")
read_hpf_manifest_file = HPF.read_hpf_manifest_file
read_hpf_manifest_files = HPF.read_hpf_manifest_files
get_hpf_data_filenames = HPF.get_hpf_data_filenames
read_hpf_data = HPF.read_hpf_data
export read_hpf_manifest_file, read_hpf_manifest_files, get_hpf_data_filenames, read_hpf_data
=#

end # module EchelleTools


"""
    mutable struct Event{T <: SSDFloat}

Collection struct for individual events. This (mutable) struct is ment to be used to look at individual events,
not to process a hugh amount of events.
"""
mutable struct Event{T <: SSDFloat}
    locations::Union{Vector{<:AbstractCoordinatePoint{T}}, Missing}
    energies::Union{Vector{T}, Missing}
    drift_paths::Union{Vector{EHDriftPath{T}}, Missing}
    waveforms::Union{Vector{<:Any}, Missing}
    Event{T}() where {T <: SSDFloat} = new{T}(missing, missing, missing, missing)
end

function Event(locations::Vector{<:AbstractCoordinatePoint{T}}, energies::Vector{<:RealQuantity{T}} = ones(T, length(locations)))::Event{T} where {T <: SSDFloat}
    evt = Event{T}()
    evt.locations = locations
    evt.energies = to_internal_units(internal_energy_unit, energies)
    evt.waveforms = missing
    return evt
end

function Event(evt::NamedTuple{(:evtno, :detno, :thit, :edep, :pos),
         <:Tuple{
            Union{Integer, AbstractVector{<:Integer}},
            Union{Integer, AbstractVector{<:Integer}},
            AbstractVector{<:RealQuantity},
            AbstractVector{<:RealQuantity},
            AbstractVector{<:AbstractVector{<:RealQuantity}}
        }}, T = missing)
    if ismissing(T) T = eltype(to_internal_units(internal_energy_unit, evt[:edep][:])) end

    event = Event(
        CartesianPoint{T}.(to_internal_units.(internal_length_unit, evt[:pos][:])),
        T.(to_internal_units.(internal_energy_unit, evt[:edep][:]))
    )

    return event
end

in(evt::Event, detector::SolidStateDetector) = all( pt -> pt in detector, evt.locations)
in(evt::Event, simulation::Simulation) = all( pt -> pt in simulation.detector, evt.locations)

function drift_charges!(event::Event{T}, sim::Simulation{T}; max_nsteps::Int = 1000, Δt::RealQuantity = 5u"ns", verbose::Bool = true)::Nothing where {T <: SSDFloat}
    event.drift_paths = drift_charges(sim, CartesianPoint.(event.locations), Δt = Δt, max_nsteps = max_nsteps, verbose = verbose)
    nothing
end
function get_signal!(event::Event{T}, sim::Simulation{T}, contact_id::Int; Δt::RealQuantity = 5u"ns")::Nothing where {T <: SSDFloat}
    @assert !ismissing(event.drift_paths) "No drit path for this event. Use `drift_charges(event::Event, sim::Simulation` first."
    @assert !ismissing(sim.weighting_potentials[contact_id]) "No weighting potential for contact $(contact_id). Use `calculate_weighting_potential!(sim, contact_id)` first."
    if ismissing(event.waveforms)
        event.waveforms = Union{Missing, RadiationDetectorSignals.RDWaveform}[missing for i in eachindex(sim.detector.contacts)]
    end
    event.waveforms[contact_id] = get_signal(sim, event.drift_paths, event.energies, contact_id)
    nothing
end
function get_signals!(event::Event{T}, sim::Simulation{T}; Δt::RealQuantity = 5u"ns")::Nothing where {T <: SSDFloat}
    @assert !ismissing(event.drift_paths) "No drit path for this event. Use `drift_charges(event::Event, sim::Simulation` first."
    if ismissing(event.waveforms)
        event.waveforms = Union{Missing, RadiationDetectorSignals.RDWaveform}[missing for i in eachindex(sim.detector.contacts)]
    end
    for contact in sim.detector.contacts
        @assert !ismissing(sim.weighting_potentials[contact.id]) "No weighting potential for contact $(contact.id). Use `calculate_weighting_potential!(sim, contact_id)` first."
        event.waveforms[contact.id] = get_signal(sim, event.drift_paths, event.energies, contact.id, Δt = Δt)
    end
    nothing
end

@recipe function f(wvs::Vector{Union{Missing, RadiationDetectorSignals.RDWaveform}})
    @series begin
        RadiationDetectorSignals.RDWaveform[wv for wv in skipmissing(wvs)]
    end
end

function simulate!(event::Event{T}, sim::Simulation{T}; max_nsteps::Int = 1000, Δt::RealQuantity = 5u"ns", verbose::Bool = true)::Nothing where {T <: SSDFloat}
    drift_charges!(event, sim, max_nsteps = max_nsteps, Δt = Δt, verbose = verbose)
    get_signals!(event, sim, Δt = Δt)
    nothing
end

function add_baseline_and_extend_tail(wv::RadiationDetectorSignals.RDWaveform{T,U,TV,UV}, n_baseline_samples::Int, total_waveform_length::Int) where {T,U,TV,UV}
    new_signal::Vector{eltype(UV)} = Vector{eltype(UV)}(undef, total_waveform_length)
    new_signal[1:n_baseline_samples] .= zero(eltype(UV))
    if length(wv.value) <= total_waveform_length - n_baseline_samples
        new_signal[n_baseline_samples+1:n_baseline_samples+length(wv.value)] = wv.value
        new_signal[n_baseline_samples+length(wv.value)+1:end] .= wv.value[end]
    else
        new_signal[n_baseline_samples+1:end] = wv.value[1:total_waveform_length - n_baseline_samples]
    end
    new_times = if TV <: AbstractRange
        range( zero(first(wv.time)), step = step(wv.time), length = total_waveform_length )
    else
        error("Not yet definted for timestamps of type `$(TV)`")
    end
    return RDWaveform( new_times, new_signal )
end