using DFTK
using Unitful
using UnitfulAtomic
using AtomsBase
using Test
import DFTK: compute_pspmap

@testset "DFTK -> AbstractSystem -> DFTK" begin
    Si = ElementCoulomb(:Si)
    C  = ElementPsp(:C, psp=load_psp("hgh/pbe/c-q4.hgh"))
    H  = ElementPsp(:H, psp=load_psp("hgh/lda/h-q1.hgh"))

    lattice   = randn(3, 3)
    atoms     = [Si, C, H, C]
    positions = [rand(3) for _ in 1:4]
    magnetic_moments = DFTK.normalize_magnetic_moment.(rand(4))

    system = DFTK.construct_system(lattice, atoms, positions, magnetic_moments)
    @test atomic_symbol(system) == [:Si, :C, :H, :C]
    @test bounding_box(system)  == collect(eachcol(lattice)) * u"bohr"
    @test position(system)      == [lattice * p * u"bohr" for p in positions]

    @test system[1].potential == Si
    @test system[2].potential == C
    @test system[3].potential == H
    @test system[4].potential == C
    @test !hasproperty(system[1], :pseudopotential)
    @test system[2].pseudopotential == "hgh/pbe/c-q4.hgh"
    @test system[3].pseudopotential == "hgh/lda/h-q1.hgh"
    @test system[4].pseudopotential == "hgh/pbe/c-q4.hgh"

    parsed = DFTK.parse_system(system)
    @test parsed.lattice   == lattice
    @test parsed.atoms     == atoms
    @test parsed.positions ≈ positions atol=1e-14
    @test parsed.magnetic_moments == magnetic_moments

    let system = attach_psp(system; family="hgh", functional="lda")
        for i in 1:4
            @test system[i].potential       == atoms[i]
            @test system[i].magnetic_moment == magnetic_moments[i]
        end
    end

    for constructor in (Model, model_atomic, model_LDA, model_PBE, model_SCAN)
        model = constructor(system)
        @test model.spin_polarization == :collinear
        newsys = periodic_system(model, magnetic_moments)

        @test atomic_symbol(system)       == atomic_symbol(newsys)
        @test bounding_box(system)        == bounding_box(newsys)
        @test boundary_conditions(system) == boundary_conditions(newsys)
        @test maximum(maximum, position(system) - position(newsys)) < 1e-12u"bohr"
        for (p, newp) in zip(system, newsys)
            @test p.magnetic_moment == newp.magnetic_moment
            @test p.potential       == newp.potential
        end
    end
end

@testset "AbstractSystem -> DFTK" begin
    lattice     = [12u"bohr" * rand(3) for _ in 1:3]
    atoms       = [:C => rand(3), :Si => rand(3), :H => rand(3), :C => rand(3)]
    pos_units   = last.(atoms)
    pos_lattice = austrip.(reduce(hcat, lattice))
    system      = periodic_system(atoms, lattice; fractional=true)

    let model = Model(system)
        @test model.lattice == pos_lattice
        @test model.positions ≈ pos_units atol=1e-14
        @test model.spin_polarization == :none

        @test length(model.atoms) == 4
        @test model.atoms[1] == ElementCoulomb(:C)
        @test model.atoms[2] == ElementCoulomb(:Si)
        @test model.atoms[3] == ElementCoulomb(:H)
        @test model.atoms[4] == ElementCoulomb(:C)
    end

    let system = attach_psp(system; family="hgh", functional="pbe")
        @test length(system) == 4
        @test system[1].pseudopotential == "hgh/pbe/c-q4.hgh"
        @test system[2].pseudopotential == "hgh/pbe/si-q4.hgh"
        @test system[3].pseudopotential == "hgh/pbe/h-q1.hgh"
        @test system[4].pseudopotential == "hgh/pbe/c-q4.hgh"

        parsed = DFTK.parse_system(system)
        @test parsed.lattice == pos_lattice
        @test parsed.positions ≈ pos_units atol=1e-14
        @test isempty(parsed.magnetic_moments)

        @test length(parsed.atoms) == 4
        @test parsed.atoms[1].psp.identifier == "hgh/pbe/c-q4.hgh"
        @test parsed.atoms[2].psp.identifier == "hgh/pbe/si-q4.hgh"
        @test parsed.atoms[3].psp.identifier == "hgh/pbe/h-q1.hgh"
        @test parsed.atoms[4].psp.identifier == "hgh/pbe/c-q4.hgh"
    end

    let system = attach_psp(system; family="hgh", functional="lda")
        @test length(system) == 4
        @test system[1].pseudopotential == "hgh/lda/c-q4.hgh"
        @test system[2].pseudopotential == "hgh/lda/si-q4.hgh"
        @test system[3].pseudopotential == "hgh/lda/h-q1.hgh"
        @test system[4].pseudopotential == "hgh/lda/c-q4.hgh"

        model = Model(system)
        @test model.lattice == pos_lattice
        @test model.positions ≈ pos_units atol=1e-14
        @test model.spin_polarization == :none

        @test length(model.atoms) == 4
        @test model.atoms[1].psp.identifier == "hgh/lda/c-q4.hgh"
        @test model.atoms[2].psp.identifier == "hgh/lda/si-q4.hgh"
        @test model.atoms[3].psp.identifier == "hgh/lda/h-q1.hgh"
        @test model.atoms[4].psp.identifier == "hgh/lda/c-q4.hgh"
    end
end

@testset "Check attach_psp routine selectively" begin
    symbols = [:Cu, :Au, :Ni]

    let pspmap = compute_pspmap(symbols; functional="lda", family="hgh", core=:semicore)
        @test pspmap[:Cu] == "hgh/lda/cu-q19.hgh"
        @test pspmap[:Au] == "hgh/lda/au-q19.hgh"
        @test pspmap[:Ni] == "hgh/lda/ni-q18.hgh"
    end

    let pspmap = compute_pspmap(symbols; functional="lda", family="hgh", core=:fullcore)
        @test pspmap[:Cu] == "hgh/lda/cu-q11.hgh"
        @test pspmap[:Au] == "hgh/lda/au-q11.hgh"
        @test pspmap[:Ni] == "hgh/lda/ni-q10.hgh"
    end

    let pspmap = compute_pspmap([:Cu, :Au]; functional="pbe", family="hgh", core=:semicore)
        @test pspmap[:Cu] == "hgh/pbe/cu-q19.hgh"
        @test pspmap[:Au] == "hgh/pbe/au-q19.hgh"
    end

    let pspmap = compute_pspmap(symbols; functional="pbe", family="hgh", core=:fullcore)
        @test pspmap[:Cu] == "hgh/pbe/cu-q11.hgh"
        @test pspmap[:Au] == "hgh/pbe/au-q11.hgh"
        @test pspmap[:Ni] == "hgh/pbe/ni-q18.hgh"
    end
end


using BayesNets
using Discretizers
using JLD

basepath = "./results/Test1/"
paths = [string(basepath,i) for i in 0:4]

logname = "test_log_BN.txt"
logfile = "$basepath$logname"

scorename = "test_BN_p_dists.txt"
#scorefile = "$basepath$i/$scorename"
moveCats = [1,2,3]
numNextMoves = length(moveCats)
useLaneType = true
if contains(basepath,".1")
  useLaneType = false
end
laneTypeIndex = 3
laneTypeEncodedLen = length([0,0,0,0])
numYawBins = 20

max_parents = 7

#truncate the log file
f = open(logfile, "w")
close(f)

function Distributions.fit(::Type{DiscreteBayesNet}, data::DataFrame, params::GreedyHillClimbing, indexOfOut::Int;
    ncategories::Vector{Int} = map!(i->infer_number_of_instantiations(data[i]), Array(Int, ncol(data)), 1:ncol(data)),
    )

    n = ncol(data)
    parent_list = map!(i->Int[], Array(Vector{Int}, n), 1:n)
    datamat = convert(Matrix{Int}, data)'
    score_components = bayesian_score_components(parent_list, ncategories, datamat, params.prior, params.cache)

    while true
        best_diff = 0.0
        best_parent_list = parent_list
        i = indexOfOut
        # 1) add an edge (j->i)
        if length(parent_list[i]) < params.max_n_parents
            for j in deleteat!(collect(1:n), parent_list[i])
                    if adding_edge_preserves_acyclicity(parent_list, j, i)
                        new_parents = sort!(push!(copy(parent_list[i]), j))
                        new_component_score = bayesian_score_component(i, new_parents, ncategories, datamat, params.prior, params.cache)
                    if new_component_score - score_components[i] > best_diff
                        best_diff = new_component_score - score_components[i]
                        best_parent_list = deepcopy(parent_list)
                        best_parent_list[i] = new_parents
                    end
                end
             end
        end

        # 2) remove an edge
        for (idx, j) in enumerate(parent_list[i])

            new_parents = deleteat!(copy(parent_list[i]), idx)
            new_component_score = bayesian_score_component(i, new_parents, ncategories, datamat, params.prior, params.cache)
            if new_component_score - score_components[i] > best_diff
                best_diff = new_component_score - score_components[i]
                best_parent_list = deepcopy(parent_list)
                best_parent_list[i] = new_parents
            end

            # 3) flip an edge
            new_parent_list = deepcopy(parent_list) # TODO: make this more efficient
            deleteat!(new_parent_list[i], idx)

            if adding_edge_preserves_acyclicity(new_parent_list, i, j)
                sort!(push!(new_parent_list[j], i))
                new_diff = bayesian_score_component(i, new_parent_list[i], ncategories, datamat, params.prior, params.cache) - score_components[i]
                new_diff += bayesian_score_component(j, new_parent_list[j], ncategories, datamat, params.prior, params.cache) - score_components[j]
                if new_diff > best_diff
                    best_diff = new_diff
                    best_parent_list = new_parent_list
                end
            end
        end

        if best_diff > 0.0
            parent_list = best_parent_list
            score_components = bayesian_score_components(parent_list, ncategories, datamat, params.prior, params.cache)
        else
            break
        end
    end
    # construct the BayesNet
    cpds = Array(DiscreteCPD, n)
    varnames = names(data)
    for j in 1:n
        name = varnames[j]
        parents = varnames[parent_list[j]]
        cpds[j] = fit(DiscreteCPD, data, name, parents, params.prior,
                      parental_ncategories=ncategories[parent_list[j]],
                      target_ncategories=ncategories[j])
    end
    BayesNet(cpds)
end

open(logfile, "a") do logf
  write(logf,"Done making discretizers\n")
  write(logf, string(now(),"\n"))
end
allFeatures = Dict()
allTargets = Dict()
allFids = Dict()
nFids = 0
println("Loading features/targets for BN")
open(logfile, "a") do logf
  write(logf,"Loading features/targets for BN\n")
  write(logf, string(now(),"\n"))
end
numFeatures = 0
for path in paths
    i = path[end]
    f = open("$path/featureSet")
    allFeatures[i] = readdlm(f)
    allFeatures[i] = allFeatures[i][1:500,:]
    close(f)

    f = open("$path/targetSet")
    allTargets[i] = readdlm(f)
    allTargets[i] = allTargets[i][1:500,:]
    close(f)

    if numFeatures == 0
        numFeatures = length(allFeatures[i][1,:])
    end
    f = open("$path/Fids")
    allFids[i] = readdlm(f)
    #allFids[i] = allFids[i][1:1000]
    close(f)
    nFids += length(allFids[i])
end
moveIndex = numFeatures+1

println("Done loading features/targets for BN")
println("NumFeatures == ", numFeatures)
open(logfile, "a") do logf
  write(logf,"Done loading features/targets for BN\n")
  write(logf, string(now(),"\n"))
end

typeToEdges = Dict(
    "Vy"=>[0.0,0.5,20,40,60],
    "Vx"=>[-1000,-20,-5,-0.5,0.5,5,20,1000],
    "A"=>[-5000,-20,-5,-0.5,0.5,5,20,5000],
    "dist" => [0.0, 5, 25, 100, 500, 10000],
    "hdwy" => [0.0, 5, 25, 100, 250, 10000],)
idToType = Dict(
    4=>"Vy",
    5=>"A",
    6=> "Vx",
    7=> "A",
    8=> "yaw",
    9=> "hdwy",
    10=> "dist",)
#above dict was made when lanetype was integer
indexToID = zeros(Int64, (1, numFeatures+1+laneTypeEncodedLen))   #+1 for when no lane type, shift needs buffer
coreLen = 9 + laneTypeEncodedLen
histLen = 9
numHist = 4
totHist = histLen * numHist
traffLen = 8
for index in 1:numFeatures
    ID = index
    if index <= coreLen
      ID = index
      if useLaneType
        if index >= 4
          ID = index - laneTypeEncodedLen + 1
        end
      end
    elseif index <= coreLen + totHist
      ID = ((index - coreLen) % histLen)
      if ID == 0
        ID = histLen
      end
      ID += 1
    else
      ID = ((index - (coreLen + totHist)) % traffLen)
      if ID == 0
        ID = traffLen
      elseif ID < 3
        ID = traffLen + 1
      end
      ID += 1
    end
    indexToID[index] = ID
end
println("Done making discretizer dictionaries")

#big for loop
for i in keys(allFeatures)
  #truncate score file
  f = open("$basepath$i/$scorename", "w")
  close(f)
  println("Starting feature/target formatting for CV #: $i")
  open(logfile, "a") do logf
    write(logf,"Starting feature/target formatting for CV #: $i \n")
    write(logf, string(now(),"\n"))
  end
  testlines = allFeatures[i]
  testactuals = allTargets[i]

  trainlines = zeros(Float64, (1,numFeatures))
  trainactuals = zeros(Float64, (1,numFeatures))
  first = true
  for j in keys(allFeatures)
    if j != i
      if first == true
        trainlines = allFeatures[j]
        trainactuals = allTargets[j]
        first = false
      else
        trainlines = vcat(trainlines, allFeatures[j])
        trainactuals = vcat(trainactuals, allTargets[j])
      end
    end
  end
  println("Done feature/target formatting for CV #: $i")
  println("Starting to discretize for CV #: $i")
  open(logfile, "a") do logf
    write(logf,"Done feature/target formatting for CV #: $i \n")
    write(logf,"Starting to discretize for CV #: $i \n")
    write(logf, string(now(),"\n"))
  end
  #make discretizers with all lines, outlier problems
  allLines = vcat(trainlines, testlines)
  allactuals = vcat(trainactuals, testactuals)
  discAllLines = zeros(Int64, (length(trainlines[:,1])+length(testlines[:,1]), numFeatures+1))
  discTrainlines = zeros(Int64, (length(trainlines[:,1]), numFeatures+1))
  discTestlines = zeros(Int64, (length(testlines[:,1]), numFeatures+1))
  #DiscreteAlready = [1,2,3,11,12,20,21,29,30]
  for k in 1:numFeatures
    id = indexToID[k]
    if haskey(idToType, id) #if not, already discretized
      F_type = idToType[id]
      if haskey(typeToEdges, F_type)
        Disc = LinearDiscretizer(typeToEdges[F_type])
      else
        Disc = LinearDiscretizer(binedges(DiscretizeUniformWidth(numYawBins),allLines[:,k]))
      end
      DiscretizedAll = encode(Disc, allLines[:,k])
      DiscretizedTrain = encode(Disc, trainlines[:,k])
      DiscretizedTest = encode(Disc, testlines[:,k])
    else
      DiscretizedAll = allLines[:,k]
      DiscretizedTrain = trainlines[:,k]
      DiscretizedTest = testlines[:,k]
    end
    discTrainlines[:,k] = encode(CategoricalDiscretizer(DiscretizedAll), DiscretizedTrain)
    discTestlines[:,k] = encode(CategoricalDiscretizer(DiscretizedAll), DiscretizedTest)
    discAllLines[:,k] = encode(CategoricalDiscretizer(DiscretizedAll), DiscretizedAll)
  end
  moveDiscretizer = CategoricalDiscretizer(trainactuals)
  discTrainlines[:,moveIndex] = encode(moveDiscretizer,trainactuals)
  discTestlines[:,moveIndex] = encode(moveDiscretizer,testactuals)
  discAllLines[:,moveIndex] = encode(moveDiscretizer,allactuals)
  println("Done discretizing for CV #: $i")
  open(logfile, "a") do logf
    write(logf,"Done discretizing for CV #: $i \n")
    write(logf, string(now(),"\n"))
  end
  alldata = convert(DataFrame, discAllLines) #used to find num bins
  traindata = convert(DataFrame, discTrainlines)
  testdata = convert(DataFrame, discTestlines)

  println("Starting to fit BN for CV #: $i")
  open(logfile, "a") do logf
    write(logf,"Starting to fit BN for CV #: $i \n")
    write(logf, string(now(),"\n"))
  end
  max_parents = min(numFeatures, max_parents)
  println("Max parents: ", max_parents)
  params = GreedyHillClimbing(ScoreComponentCache(traindata), max_n_parents=max_parents, prior=UniformPrior())
  num_bins_all = map!(i->infer_number_of_instantiations(alldata[i]), Array(Int, ncol(alldata)), 1:ncol(alldata))
  bnDis = @time fit(DiscreteBayesNet, traindata, params, moveIndex; ncategories=num_bins_all)
  println("Done fitting BN for CV #: $i")
  open(logfile, "a") do logf
    write(logf,"Done fitting BN for CV #: $i \n")
    write(logf, string(now(),"\n"))
    println("Starting to score BN for CV #: $i")
    write(logf,"Starting to score BN for CV #: $i\n")
    write(logf, string(now(),"\n"))
  end
  save(string("$basepath$i","/BN_model.jld"), "bnDis", bnDis)
  score = 0
  numNaN = 0
  nrows = length(testlines[:,1])
  moveCats = unique(collect(testdata[:,moveIndex]))
  numNextMoves = length(moveCats)
  for index in 1:nrows
      actualNextMove = convert(Int64,decode(moveDiscretizer,testdata[index,moveIndex]))
      p_dists = zeros(Float64, (1, numNextMoves))
      for move in moveCats
        featureline = testdata[index,:]
        featureline[moveIndex] = encode(moveDiscretizer, move)
        p_dists[move] = pdf(bnDis, featureline)
      end
      for x in p_dists
        if !(x > 1) && !(x < 1) && !(x == 1)
          p_dists = [1.0/numNextMoves for i in 1:numNextMoves]
          numNaN += 1
          break
        end
      end
      p_dists = p_dists ./ (sum(p_dists) + 0.0000000000000001)
      p_right = p_dists[actualNextMove]
      score += (1.0  - p_right)
      scoref = open("$basepath$i/$scorename", "a")
      writedlm(scoref, reshape(p_dists, (1,numNextMoves)))
      close(scoref)
  end
  println("Score:", score)
  println("Num Predicitons:", nrows)
  println("Num NaN:", numNaN)
  scoref = open("$basepath$i/$scorename", "a")
  #writedlm(scoref, score)
  #writedlm(scoref, numNaN)
  close(scoref)
  println("Done scoring BN for CV #: $i")
  open(logfile, "a") do logf
    write(logf,"Done scoring BN for CV #: $i \n")
    write(logf, string(now(),"\n"))
  end
end #end of for i in allfeatures keys
#end  #for log file


#
println("Ad) Tests towards a SCF field: B-spline primitives and one-particle spectra in a local potential.")
#

if  false
    wa = Atomic.Computation(Atomic.Computation(), name="xx", grid=JAC.Radial.Grid(true), nuclearModel=Nuclear.Model(18., "point"),
                            configs=[Configuration("[Ne] 3s^2 3p^5"), Configuration("[Ne] 3s 3p^6"), Configuration("[Ne] 3s^2 3p^4 3d")],  ##
                            asfSettings=AsfSettings(AsfSettings(), scField=Basics.DFSField())  )

    wb = perform(wa)

elseif  true
    grid = Radial.Grid(Radial.Grid(true), rnt = 2.0e-6,h = 3.0e-2, hp = 6.0e-2, NoPoints = 900)
    grid = Radial.Grid(true)
    wa = Atomic.Computation(Atomic.Computation(), name="xx", grid=grid, nuclearModel=Nuclear.Model(16., "point"),
                            configs=[Configuration("[Ne] 3s^2 3p^2")],  ## , Basics.DFSField()
                            asfSettings=AsfSettings(AsfSettings(), scField=Basics.DFSField())  )

    wb = perform(wa)

elseif false
    #
    # Test for Björn, 7. Mai 2020
    ## grid=JAC.Radial.Grid(true)
    grid = Radial.Grid(Radial.Grid(true), rnt = 2.0e-5,h = 5.0e-2, hp = 5.0e-2, NoPoints = 2000)
    wa = Atomic.Computation(Atomic.Computation(), name="xx", grid=grid, nuclearModel=Nuclear.Model(8.0, "Fermi"),
                            configs=[Configuration("1s^2 2s"), Configuration("1s 2s^2"), Configuration("1s 2s 3p"), Configuration("1s 2s 4p")],
                            ## configs=[Configuration("1s^2 2s"), Configuration("1s^2 2p"), Configuration("1s 2s 3s"), Configuration("1s 2p 3s"),
                            ##          Configuration("1s 2s 2p")],  ##
                            asfSettings=AsfSettings(true, false, Basics.DFSField(), "hydrogenic", Dict{Subshell, Orbital}(), [1],    40, 1.0e-6, JAC.Subshell[], JAC.Subshell[],
                                                    true, false, NoneQed(), LSjjSettings(false),
                                                    false, [1,2,3,4], false, JAC.LevelSymmetry[] )  )

    wb = perform(wa)

end


#=
This file is auto-generated. Do not edit.
=#
"""
    mutable struct Generator
        service_on::Bool
        pg::Float64
        pgd_ub::Float64
        bus::Int
        pgd_lb::Float64
        name::String
        max_run::Float64
        qg::Float64
        reserve_agc::Bool
        status::Int
        pg_0::Float64
        down_time::Float64
        cost_start_warm::Float64
        reserve_agc_pg_ub::Float64
        cost_pg_model::Int
        time_start_cold::Float64
        qg_ub::Float64
        reserve_agc_pg_lb::Float64
        cost_pg_parameters::Vector{Float64}
        fuel_type::Int
        qg_lb::Float64
        cost_start_hot::Float64
        pg_lb::Float64
        cost_start_cold::Float64
        index::Int
        must_run::Int
        pg_ub::Float64
        min_run::Float64
        time_start_hot::Float64
        up_time::Float64
        time_start_warm::Float64
        min_down::Float64
    end

# Arguments
- `service_on::Bool`: in service
- `pg::Float64`: active/real power generation output
- `pgd_ub::Float64`: maximum active power increase per hour
- `bus::Int`: connecting bus id
- `pgd_lb::Float64`: maximum active power increase per hour
- `name::String`: a flexible name for components, non required to be unique
- `max_run::Float64`: unit of time (hours)
- `qg::Float64`: reactive/imaginary power generation output
- `reserve_agc::Bool`: 1/0
- `status::Int`: a 0/1 value indicating if the component should be omitted or not (0 => omitted)
- `pg_0::Float64`: previous active power output
- `down_time::Float64`: time down previously
- `cost_start_warm::Float64`: cost in dollars of starting a unit
- `reserve_agc_pg_ub::Float64`: 1/0
- `cost_pg_model::Int`: cost model type (1=pwl, 2=polynomial)
- `time_start_cold::Float64`: threshold of time (est. 16h+)
- `qg_ub::Float64`: maximum reactive power generation
- `reserve_agc_pg_lb::Float64`: 1/0
- `cost_pg_parameters::Vector{Float64}`: either points for a pwl cost or polynomial coeffents (from lowest degree first)
- `fuel_type::Int`: fuel type
- `qg_lb::Float64`: minimum active power generation
- `cost_start_hot::Float64`: cost in dollars of starting a unit
- `pg_lb::Float64`: minimum active power generation
- `cost_start_cold::Float64`: cost in dollars of starting a unit
- `index::Int`: a unique identifier for components
- `must_run::Int`: enum
- `pg_ub::Float64`: maximum active power generation
- `min_run::Float64`: unit of time (hours)
- `time_start_hot::Float64`: threshold of time (est. 9h)
- `up_time::Float64`: time up previously
- `time_start_warm::Float64`: threshold of time (est. 9-16h)
- `min_down::Float64`: unit of time (hours)
"""
mutable struct Generator
    "in service"
    service_on::Bool
    "active/real power generation output"
    pg::Float64
    "maximum active power increase per hour"
    pgd_ub::Float64
    "connecting bus id"
    bus::Int
    "maximum active power increase per hour"
    pgd_lb::Float64
    "a flexible name for components, non required to be unique"
    name::String
    "unit of time (hours)"
    max_run::Float64
    "reactive/imaginary power generation output"
    qg::Float64
    "1/0"
    reserve_agc::Bool
    "a 0/1 value indicating if the component should be omitted or not (0 => omitted)"
    status::Int
    "previous active power output"
    pg_0::Float64
    "time down previously"
    down_time::Float64
    "cost in dollars of starting a unit"
    cost_start_warm::Float64
    "1/0"
    reserve_agc_pg_ub::Float64
    "cost model type (1=pwl, 2=polynomial)"
    cost_pg_model::Int
    "threshold of time (est. 16h+)"
    time_start_cold::Float64
    "maximum reactive power generation"
    qg_ub::Float64
    "1/0"
    reserve_agc_pg_lb::Float64
    "either points for a pwl cost or polynomial coeffents (from lowest degree first)"
    cost_pg_parameters::Vector{Float64}
    "fuel type"
    fuel_type::Int
    "minimum active power generation"
    qg_lb::Float64
    "cost in dollars of starting a unit"
    cost_start_hot::Float64
    "minimum active power generation"
    pg_lb::Float64
    "cost in dollars of starting a unit"
    cost_start_cold::Float64
    "a unique identifier for components"
    index::Int
    "enum"
    must_run::Int
    "maximum active power generation"
    pg_ub::Float64
    "unit of time (hours)"
    min_run::Float64
    "threshold of time (est. 9h)"
    time_start_hot::Float64
    "time up previously"
    up_time::Float64
    "threshold of time (est. 9-16h)"
    time_start_warm::Float64
    "unit of time (hours)"
    min_down::Float64
end

function Generator(; service_on, pg, pgd_ub, bus, pgd_lb, name, max_run, qg, reserve_agc, status, pg_0, down_time, cost_start_warm, reserve_agc_pg_ub, cost_pg_model, time_start_cold, qg_ub, reserve_agc_pg_lb, cost_pg_parameters, fuel_type, qg_lb, cost_start_hot, pg_lb, cost_start_cold, index, must_run, pg_ub, min_run, time_start_hot, up_time, time_start_warm, min_down, )
    Generator(service_on, pg, pgd_ub, bus, pgd_lb, name, max_run, qg, reserve_agc, status, pg_0, down_time, cost_start_warm, reserve_agc_pg_ub, cost_pg_model, time_start_cold, qg_ub, reserve_agc_pg_lb, cost_pg_parameters, fuel_type, qg_lb, cost_start_hot, pg_lb, cost_start_cold, index, must_run, pg_ub, min_run, time_start_hot, up_time, time_start_warm, min_down, )
end

# Constructor for demo purposes; non-functional.
function Generator(::Nothing)
    Generator(;
        service_on=1,
        pg=1.2,
        pgd_ub=1.1,
        bus=1,
        pgd_lb=1.1,
        name="1",
        max_run=0,
        qg=1.1,
        reserve_agc=1,
        status=1,
        pg_0=1,
        down_time=1.1,
        cost_start_warm=1,
        reserve_agc_pg_ub=0,
        cost_pg_model=1,
        time_start_cold=1,
        qg_ub=0,
        reserve_agc_pg_lb=0,
        cost_pg_parameters=Any[1.1],
        fuel_type=1,
        qg_lb=0,
        cost_start_hot=1,
        pg_lb=1,
        cost_start_cold=1,
        index=1,
        must_run=1,
        pg_ub=2,
        min_run=0,
        time_start_hot=1,
        up_time=1.1,
        time_start_warm=1,
        min_down=0,
    )
end

"""Get Generator service_on."""
get_service_on(value::Generator) = value.service_on
"""Get Generator pg."""
get_pg(value::Generator) = value.pg
"""Get Generator pgd_ub."""
get_pgd_ub(value::Generator) = value.pgd_ub
"""Get Generator bus."""
get_bus(value::Generator) = value.bus
"""Get Generator pgd_lb."""
get_pgd_lb(value::Generator) = value.pgd_lb
"""Get Generator name."""
get_name(value::Generator) = value.name
"""Get Generator max_run."""
get_max_run(value::Generator) = value.max_run
"""Get Generator qg."""
get_qg(value::Generator) = value.qg
"""Get Generator reserve_agc."""
get_reserve_agc(value::Generator) = value.reserve_agc
"""Get Generator status."""
get_status(value::Generator) = value.status
"""Get Generator pg_0."""
get_pg_0(value::Generator) = value.pg_0
"""Get Generator down_time."""
get_down_time(value::Generator) = value.down_time
"""Get Generator cost_start_warm."""
get_cost_start_warm(value::Generator) = value.cost_start_warm
"""Get Generator reserve_agc_pg_ub."""
get_reserve_agc_pg_ub(value::Generator) = value.reserve_agc_pg_ub
"""Get Generator cost_pg_model."""
get_cost_pg_model(value::Generator) = value.cost_pg_model
"""Get Generator time_start_cold."""
get_time_start_cold(value::Generator) = value.time_start_cold
"""Get Generator qg_ub."""
get_qg_ub(value::Generator) = value.qg_ub
"""Get Generator reserve_agc_pg_lb."""
get_reserve_agc_pg_lb(value::Generator) = value.reserve_agc_pg_lb
"""Get Generator cost_pg_parameters."""
get_cost_pg_parameters(value::Generator) = value.cost_pg_parameters
"""Get Generator fuel_type."""
get_fuel_type(value::Generator) = value.fuel_type
"""Get Generator qg_lb."""
get_qg_lb(value::Generator) = value.qg_lb
"""Get Generator cost_start_hot."""
get_cost_start_hot(value::Generator) = value.cost_start_hot
"""Get Generator pg_lb."""
get_pg_lb(value::Generator) = value.pg_lb
"""Get Generator cost_start_cold."""
get_cost_start_cold(value::Generator) = value.cost_start_cold
"""Get Generator index."""
get_index(value::Generator) = value.index
"""Get Generator must_run."""
get_must_run(value::Generator) = value.must_run
"""Get Generator pg_ub."""
get_pg_ub(value::Generator) = value.pg_ub
"""Get Generator min_run."""
get_min_run(value::Generator) = value.min_run
"""Get Generator time_start_hot."""
get_time_start_hot(value::Generator) = value.time_start_hot
"""Get Generator up_time."""
get_up_time(value::Generator) = value.up_time
"""Get Generator time_start_warm."""
get_time_start_warm(value::Generator) = value.time_start_warm
"""Get Generator min_down."""
get_min_down(value::Generator) = value.min_down

"""Set Generator service_on."""
set_service_on!(value::Generator, val::Bool) = value.service_on = val
"""Set Generator pg."""
set_pg!(value::Generator, val::Float64) = value.pg = val
"""Set Generator pgd_ub."""
set_pgd_ub!(value::Generator, val::Float64) = value.pgd_ub = val
"""Set Generator bus."""
set_bus!(value::Generator, val::Int) = value.bus = val
"""Set Generator pgd_lb."""
set_pgd_lb!(value::Generator, val::Float64) = value.pgd_lb = val
"""Set Generator name."""
set_name!(value::Generator, val::String) = value.name = val
"""Set Generator max_run."""
set_max_run!(value::Generator, val::Float64) = value.max_run = val
"""Set Generator qg."""
set_qg!(value::Generator, val::Float64) = value.qg = val
"""Set Generator reserve_agc."""
set_reserve_agc!(value::Generator, val::Bool) = value.reserve_agc = val
"""Set Generator status."""
set_status!(value::Generator, val::Int) = value.status = val
"""Set Generator pg_0."""
set_pg_0!(value::Generator, val::Float64) = value.pg_0 = val
"""Set Generator down_time."""
set_down_time!(value::Generator, val::Float64) = value.down_time = val
"""Set Generator cost_start_warm."""
set_cost_start_warm!(value::Generator, val::Float64) = value.cost_start_warm = val
"""Set Generator reserve_agc_pg_ub."""
set_reserve_agc_pg_ub!(value::Generator, val::Float64) = value.reserve_agc_pg_ub = val
"""Set Generator cost_pg_model."""
set_cost_pg_model!(value::Generator, val::Int) = value.cost_pg_model = val
"""Set Generator time_start_cold."""
set_time_start_cold!(value::Generator, val::Float64) = value.time_start_cold = val
"""Set Generator qg_ub."""
set_qg_ub!(value::Generator, val::Float64) = value.qg_ub = val
"""Set Generator reserve_agc_pg_lb."""
set_reserve_agc_pg_lb!(value::Generator, val::Float64) = value.reserve_agc_pg_lb = val
"""Set Generator cost_pg_parameters."""
set_cost_pg_parameters!(value::Generator, val::Vector{Float64}) = value.cost_pg_parameters = val
"""Set Generator fuel_type."""
set_fuel_type!(value::Generator, val::Int) = value.fuel_type = val
"""Set Generator qg_lb."""
set_qg_lb!(value::Generator, val::Float64) = value.qg_lb = val
"""Set Generator cost_start_hot."""
set_cost_start_hot!(value::Generator, val::Float64) = value.cost_start_hot = val
"""Set Generator pg_lb."""
set_pg_lb!(value::Generator, val::Float64) = value.pg_lb = val
"""Set Generator cost_start_cold."""
set_cost_start_cold!(value::Generator, val::Float64) = value.cost_start_cold = val
"""Set Generator index."""
set_index!(value::Generator, val::Int) = value.index = val
"""Set Generator must_run."""
set_must_run!(value::Generator, val::Int) = value.must_run = val
"""Set Generator pg_ub."""
set_pg_ub!(value::Generator, val::Float64) = value.pg_ub = val
"""Set Generator min_run."""
set_min_run!(value::Generator, val::Float64) = value.min_run = val
"""Set Generator time_start_hot."""
set_time_start_hot!(value::Generator, val::Float64) = value.time_start_hot = val
"""Set Generator up_time."""
set_up_time!(value::Generator, val::Float64) = value.up_time = val
"""Set Generator time_start_warm."""
set_time_start_warm!(value::Generator, val::Float64) = value.time_start_warm = val
"""Set Generator min_down."""
set_min_down!(value::Generator, val::Float64) = value.min_down = val


function DormandPrince54Integrator(arg0::jdouble, arg1::jdouble, arg2::Vector{jdouble}, arg3::Vector{jdouble})
    return DormandPrince54Integrator((jdouble, jdouble, Vector{jdouble}, Vector{jdouble}), arg0, arg1, arg2, arg3)
end

function DormandPrince54Integrator(arg0::jdouble, arg1::jdouble, arg2::jdouble, arg3::jdouble)
    return DormandPrince54Integrator((jdouble, jdouble, jdouble, jdouble), arg0, arg1, arg2, arg3)
end

function get_a(obj::DormandPrince54Integrator)
    return jcall(obj, "getA", Vector{Vector{jdouble}}, ())
end

function get_b(obj::DormandPrince54Integrator)
    return jcall(obj, "getB", Vector{jdouble}, ())
end

function get_c(obj::DormandPrince54Integrator)
    return jcall(obj, "getC", Vector{jdouble}, ())
end

function get_max_growth(obj::EmbeddedRungeKuttaIntegrator)
    return jcall(obj, "getMaxGrowth", jdouble, ())
end

function get_min_reduction(obj::EmbeddedRungeKuttaIntegrator)
    return jcall(obj, "getMinReduction", jdouble, ())
end

function get_order(obj::DormandPrince54Integrator)
    return jcall(obj, "getOrder", jint, ())
end

function get_safety(obj::EmbeddedRungeKuttaIntegrator)
    return jcall(obj, "getSafety", jdouble, ())
end

function integrate(obj::EmbeddedRungeKuttaIntegrator, arg0::ExpandableODE, arg1::ODEState, arg2::jdouble)
    return jcall(obj, "integrate", ODEStateAndDerivative, (ExpandableODE, ODEState, jdouble), arg0, arg1, arg2)
end

function set_max_growth(obj::EmbeddedRungeKuttaIntegrator, arg0::jdouble)
    return jcall(obj, "setMaxGrowth", void, (jdouble,), arg0)
end

function set_min_reduction(obj::EmbeddedRungeKuttaIntegrator, arg0::jdouble)
    return jcall(obj, "setMinReduction", void, (jdouble,), arg0)
end

function set_safety(obj::EmbeddedRungeKuttaIntegrator, arg0::jdouble)
    return jcall(obj, "setSafety", void, (jdouble,), arg0)
end


using Test
using MeshCat
using MeshCatMechanisms
using RigidBodyDynamics
using RigidBodyDynamics.OdeIntegrators
using CoordinateTransformations: Translation
using ValkyrieRobot
using NBInclude
using StaticArrays

vis = Visualizer()

@testset "MeshCatMechanisms" begin
    @testset "URDF mechanism" begin
        urdf = joinpath(@__DIR__, "urdf", "Acrobot.urdf")
        robot = parse_urdf(urdf, remove_fixed_tree_joints=false)
        mvis = MechanismVisualizer(robot, URDFVisuals(urdf), vis)
        if !haskey(ENV, "CI")
            open(mvis)
            wait(mvis)
        end
        set_configuration!(mvis, [0, -0.5])
        set_configuration!(mvis, findjoint(robot, "shoulder"), 1.0)
        @test configuration(mvis) == [1.0, -0.5]
        @test configuration(mvis, findjoint(robot, "shoulder")) == [1.0]
        copyto!(mvis, [0.1, -0.6, 0.0, 0.0])
        @test configuration(mvis) == [0.1, -0.6]

        setelement!(mvis, default_frame(bodies(robot)[end]))
        setelement!(mvis, Point3D(default_frame(bodies(robot)[3]), 0.2, 0.2, 0.2))

        @testset "simulation and animation" begin
            state = MechanismState(robot, randn(2), randn(2))
            t, q, v = simulate(state, 5.0);
            animate(mvis, t, q)
            animation = Animation(mvis, t, q)
            setanimation!(mvis, animation)
        end

        @testset "deprecated `setanimation!`" begin
            state = MechanismState(robot, randn(2), randn(2))
            t, q, v = simulate(state, 5.0);
            animate(mvis, t, q)
            setanimation!(mvis, t, q)
        end
    end

    @testset "URDF with fixed joints" begin
        urdf = joinpath(@__DIR__, "urdf", "Acrobot_fixed.urdf")
        robot = parse_urdf(urdf)
        delete!(vis)
        mvis = MechanismVisualizer(robot, URDFVisuals(urdf), vis)
        set_configuration!(mvis, [0.5])

        @testset "simulation and animation" begin
            state = MechanismState(robot, randn(1), randn(1))
            t, q, v = simulate(state, 5.0);
            animate(mvis, t, q)
            setanimation!(mvis, Animation(mvis, t, q))
        end
    end

    @testset "Valkyrie" begin
        val = Valkyrie();
        delete!(vis)
        mvis = MechanismVisualizer(
           val.mechanism,
           URDFVisuals(ValkyrieRobot.urdfpath(), package_path=[ValkyrieRobot.packagepath()]),
           vis[:val_urdf])
    end

    @testset "valkyrie inertias" begin
        val = Valkyrie();
        mvis = MechanismVisualizer(
           val.mechanism,
           Skeleton(),
           vis[:val_inertia])
        settransform!(vis[:val_inertia], Translation(0, 2, 0))
    end

    @testset "visualization during simulation" begin
        urdf = joinpath(@__DIR__, "urdf", "Acrobot.urdf")
        robot = parse_urdf(urdf, remove_fixed_tree_joints=false)
        mvis = MechanismVisualizer(robot, URDFVisuals(urdf), vis[:acrobot][:robot])
        settransform!(vis[:acrobot], Translation(0, -2, 0))
        result = DynamicsResult{Float64}(robot)
        function damped_dynamics!(vd::AbstractArray, sd::AbstractArray, t, state)
            damping = 2.
            τ = -damping * velocity(state)
            dynamics!(result, state, τ)
            copyto!(vd, result.v̇)
            copyto!(sd, result.ṡ)
            nothing
        end
        state = MechanismState(robot, randn(2), randn(2))
        integrator = MuntheKaasIntegrator(state, damped_dynamics!, runge_kutta_4(Float64), MeshCatSink(mvis))
        integrate(integrator, 10., 1e-3, max_realtime_rate = 1.)
    end

    @testset "position bounds resolution" begin
        j = Joint("foo", Revolute(SVector(1., 0, 0)))
        RigidBodyDynamics.position_bounds(j) .= RigidBodyDynamics.Bounds(-1.234, 0.0)
        MeshCatMechanisms.Manipulate.sliders(j)
        MeshCatMechanisms.Manipulate.widget(j)
    end

    @testset "manipulation" begin
        delete!(vis)
        mechanism = rand_chain_mechanism(Float64, [QuaternionFloating{Float64}; [Revolute{Float64} for i = 1:5]]...)
        mvis = MechanismVisualizer(mechanism)
        widget = manipulate!(mvis)
    end

    @testset "manipulation - negative bounds issue" begin
        mechanism = rand_chain_mechanism(Float64, Revolute{Float64})
        joint = first(joints(mechanism))
        joint.position_bounds .= RigidBodyDynamics.Bounds(-2.0, -1.0)
        mvis = MechanismVisualizer(mechanism)
        widget = manipulate!(mvis)
    end

    @testset "examples" begin
        dir = joinpath(@__DIR__, "..", "examples")
        for file in readdir(dir)
            base, ext = splitext(file)
            if ext == ".jl"
                if file == "manipulation_with_blink.jl"
                    # The interaction between MeshCat and Interact
                    # causes weird task deadlocks that I don't want
                    # to deal with right now.
                    @warn "Skipping $file"
                else
                    @info "Running $file"
                    include(joinpath(dir, file))
                end
            elseif ext == ".ipynb"
                @info "Running notebook $file"
                @nbinclude(joinpath(dir, file))
            end
        end
    end
end


lattice = load_lattice(joinpath(@__DIR__, "./Mn3Si.in"))
atoms   = load_atoms(  joinpath(@__DIR__, "./Mn3Si.in"))
atoms   = attach_pseudos(atoms, Mn="hgh/pbe/mn-q15.hgh",
                                Si="hgh/pbe/si-q4.hgh")

# AFM ordering, configuration 5(c) in Jiang and Yang J. Alloys Compounds (2021)
@assert atoms[1][1].symbol == :Mn
@assert atoms[2][1].symbol == :Si
magnetic_moments = [atoms[1][1] => [5.0, -5.0, 5.0], atoms[2][1] => [0.0]]

cases = standard_cases()

config[@__DIR__] = (
    directory        = @__DIR__,
    lattice          = lattice,
    atoms            = atoms,
    magnetic_moments = magnetic_moments,
    #
    kwargs_model     = (smearing=Smearing.Gaussian(), temperature=0.01),
    Ecut             = 45,
    kgrid            = [13, 13, 13],
    supersampling    = 2.0,
    potential        = nothing,
    kwargs_scf       = (; ),
    mixing           = KerkerMixing(),
    cases            = cases,
)


# This file provides an introductory tour of Onda.jl by generating, storing,
# and loading a toy Onda dataset. Run lines in the REPL to inspect output at
# each step! Tests are littered throughout to demonstrate functionality in a
# concrete manner, and so that we can ensure examples stay updated as the
# package evolves.

# NOTE: It's helpful to read https://github.com/beacon-biosignals/OndaFormat
# before and/or alongside the completion of this tour.

using Onda, Dates, Test

###############################################################################
###############################################################################
###############################################################################
# Let's start by defining some `Signal` instances to play with. A `Signal` instance
# describes a multichannel, LPCM-encodable signal as defined by the Onda format
# specification; this type corresponds directly to the signal object defined
# by the specification.

eeg_signal = Signal(;
                    channel_names=[:fp1, :f3, :c3, :p3, :f7, :t3, :t5, :o1, :fz, :cz, :pz,
                                   :fp2, :f4, :c4, :p4, :f8, :t4, :t6, :o2],
                    start_nanosecond=Nanosecond(0), stop_nanosecond=Nanosecond(Second(20)),
                    sample_unit=:microvolts, sample_resolution_in_unit=0.25,
                    sample_offset_in_unit=0.0, sample_type=Int16, sample_rate=256.0, # Hz
                    file_extension=:lpcm, file_options=nothing)

ecg_signal = signal_from_template(eeg_signal; channel_names=[:avl, :avr],
                                  file_extension=Symbol("lpcm.zst"))

spo2_signal = Signal(; channel_names=[:spo2], start_nanosecond=Nanosecond(Second(3)),
                     stop_nanosecond=Nanosecond(Second(17)), sample_unit=:percentage,
                     sample_resolution_in_unit=(100 / typemax(UInt8)),
                     sample_offset_in_unit=0.0, sample_type=UInt8, sample_rate=20.5, # Hz
                     file_extension=:lpcm, file_options=nothing)

###############################################################################
###############################################################################
###############################################################################
# Next, we'll generate some fake sample data for each of our signals. Here, we'll
# be working with the `Samples` type. This type wraps a `Signal` and a corresponding
# matrix of interleaved LPCM-encoded (or decoded) sample data. In this matrix,
# the rows correspond to channels and the columns correspond to timesteps.

# We'll use this function to generate the actual dummy data for our signals. As
# an aside: The hypothetical person from which these hypothetical signals were
# hypothetically recorded must be experiencing some pretty crazy pathologies if
# their EEG/ECG are just saw waves...
function saws(signal)
    return [(j + i) % 100 * signal.sample_resolution_in_unit
            for i in 1:channel_count(signal), j in 1:sample_count(signal)]
end

# The second argument in the `Samples` constructor is a `Bool` that specifies if
# the data is in its encoded representation. Here, we construct our signals as
# "decoded" (i.e. in actual units, though for this toy example it doesn't really
# matter) and then "encode" them according to the specified:
eeg = encode(Samples(eeg_signal, false, saws(eeg_signal)))
ecg = encode(Samples(ecg_signal, false, saws(ecg_signal)))
spo2 = encode(Samples(spo2_signal, false, saws(spo2_signal)))

# Here are some basic functions for examining `Samples` instances:
@test sample_count(eeg) == sample_count(eeg_signal) == 20 * eeg_signal.sample_rate
@test channel_count(eeg) == channel_count(eeg_signal) == 19
@test channel(eeg, :f3) == channel(eeg_signal, :f3) == 2
@test channel(eeg, 2) == channel(eeg_signal, 2) == :f3
@test duration(eeg) == duration(span(eeg_signal)) == Second(20)

# Here are some basic indexing examples using `getindex` and `view` wherein
# channel names and sample-rate-agnostic `TimeSpan`s are employed as indices:
slice_span = TimeSpan(Second(3), Second(9))
span_range = index_from_time(eeg.signal.sample_rate, slice_span)
@test eeg[:, slice_span].data == view(eeg, :, span_range).data
@test eeg[:f3, :].data == view(eeg, 2, :).data
@test eeg[:f3, 1:10].data == view(eeg, 2, 1:10).data
@test eeg[:f3, slice_span].data == view(eeg, 2, span_range).data
@test eeg[[:f3, :c3, :p3], 1:10].data == view(eeg, 2:4, 1:10).data
@test eeg[[:c3, 4, :f3], slice_span].data == view(eeg, [3, 4, 2], span_range).data

# NOTE: Keep in mind that `duration(samples.signal)` is not generally equivalent
# to `duration(samples)`; the former is the duration of the original signal in
# the context of its parent recording, whereas the latter is the actual duration
# of `samples.data` given `signal.sample_rate`. This is similarly true for the
# `sample_count` function for the same reason!
eeg_slice = eeg[:, slice_span]
@test duration(eeg_slice) == duration(slice_span)
@test duration(eeg_slice) != duration(eeg_signal)
@test sample_count(eeg_slice) == length(span_range)
@test sample_count(eeg_slice) != sample_count(eeg_signal)

# NOTE: `Samples` is not an `AbstractArray` subtype; this special indexing
# behavior is only defined for convenient data manipulation. It is thus fine
# to access the sample data matrix directly via the `data` field if you need
# to manipulate the matrix directly or pass it to downstream computations.

###############################################################################
###############################################################################
###############################################################################
# Now that we have some actual sample data for some actual signals, let's write
# it all out as an individual recording to an Onda dataset.

root = mktempdir() # this will be deleted when the Julia process exits

# Create a `Dataset` instance. This is a thin wrapper around an `example.onda`
# directory that helps us to easily interface the Onda dataset in a compliant
# manner.
dataset = Dataset(joinpath(root, "example.onda"); create=true)

# Create a `Recording` instance within `dataset`. This object corresponds
# directly to the recording MessagePack object defined by the specification.
# NOTE: Importantly, `create_recording!` adds `uuid => recording` to the
# `dataset.recordings` dictionary before returning the pair, such that the
# `recording` variable we assign here references the same `Recording` instance
# stored within `dataset`.
uuid, recording = create_recording!(dataset)

# Store our signals/samples for the recording in our `dataset`. This both serializes
# sample data to disk and adds the signal metadata to the recording stored in
# `dataset.recordings[uuid]` (which, for us, happens to be `recording`).
store!(dataset, uuid, :eeg, eeg)
store!(dataset, uuid, :ecg, ecg)
store!(dataset, uuid, :spo2, spo2)

# Add a single `Annotation` to `recording`. An `Annotation` is simply a key-value
# pair with an associated `TimeSpan`; you can basically put any strings you'd
# like in there. For example, Beacon Biosignals stores JSON snippets in
# annotations. Here, let's just go the simple route and pretend we found an
# epileptiform spike in our EEG/ECG/SpO2 recording:
spike_annotation = Annotation("epileptiform_spike", TimeSpan(Millisecond(1500), Second(2)))
annotate!(recording, spike_annotation)

# You can add as many annotations as you'd like to a recording. Just keep in mind
# that the annotation list is a `Set`, so duplicates will be ignored:
annotate!(recording, spike_annotation)
@test length(recording.annotations) == 1

# Since our hypothetical subject already has hypothetical epilepsy, let's give
# them hypothetical narcolepsy as well by annotating sleep stages over insanely
# short 2 second epochs across the entire recording:
for (i, t) in enumerate(2:2:(Second(duration(recording)).value))
    stage = rand(["awake", "nrem1", "nrem2", "nrem3", "rem"])
    ann = Annotation(stage, TimeSpan(Second(t - 2), Second(t)))
    annotate!(recording, ann)
end

# Finally, we save `dataset.recordings` to the `recordings.msgpack.zst` file
# specified by the Onda format. NOTE: If you don't call this function, your
# changes to `dataset.recordings` will not be saved!
save_recordings_file(dataset)

###############################################################################
###############################################################################
###############################################################################
# We have a dataset! At this point, let's pretend that we weren't the ones that
# wrote out this dataset; instead, we'll pretend our colleague passed it off to
# us, and we have to load it up to check for spikes.

dataset = Dataset(joinpath(root, "example.onda"))
@test length(dataset.recordings) == 1
uuid, recording = first(dataset.recordings)

# Grab the first spike annotation we see...
spike_annotation = first(ann
                         for ann in recording.annotations
                         if ann.value == "epileptiform_spike")

# ...and load that segment of the EEG from disk as a `Samples` instance!
spike_segment = load(dataset, uuid, :eeg, spike_annotation)

# The above invocation of `load` allows a segment of a signal (as specified by
# the last argument) to be read/deserialized from disk without reading the full
# signal file. It actually works with `Annotation`s because it accepts any
# `AbstractTimeSpan`, and `Annotation <: AbstractTimeSpan`:
@test TimeSpan(spike_annotation) ==
      TimeSpan(first(spike_annotation), last(spike_annotation))
@test spike_segment.data == load(dataset, uuid, :eeg)[:, spike_annotation].data
@test spike_segment.data == load(dataset, uuid, :eeg, TimeSpan(spike_annotation)).data

# NOTE: `load(..., span)` may still have to read the entire signal from disk if
# the signal's file format doesn't support seek access. For this reason - and
# simply to avoid filesystem overhead - it is often better to load the whole
# signal first if you're going to be accessing a bunch of its segments.

# Welp, looks like a spike to me! Let's leave an annotation to confirm we
# checked it. Remember - `spike_annotation isa AbstractTimeSpan`, so we can
# generally pass it wherever we'd pass a `TimeSpan` object:
annotate!(recording, Annotation("confirmed_spike_by_me", spike_annotation))

# ...and, finally, of course, let's save our annotation!
save_recordings_file(dataset)


# Utilities for taking gradients through simulations

export
    extract_parameters,
    inject_gradients

"""
    extract_parameters(system, force_field)

Form a `Dict` of all parameters in a `System`, allowing gradients
to be tracked.
"""
function extract_parameters(sys, ff)
    params_dic = Dict()

    for at_data in sys.atoms_data
        key_prefix = "atom_$(at_data.atom_type)_"
        if !haskey(params_dic, key_prefix * "mass")
            at = ff.atom_types[at_data.atom_type]
            params_dic[key_prefix * "mass"] = at.mass
            params_dic[key_prefix * "σ"   ] = at.σ
            params_dic[key_prefix * "ϵ"   ] = at.ϵ
        end
    end

    for inter in values(sys.pairwise_inters)
        if inter isa LennardJones
            key_prefix = "inter_LJ_"
            params_dic[key_prefix * "weight_14"] = inter.weight_14
            params_dic[key_prefix * "weight_solute_solvent"] = inter.weight_solute_solvent
        elseif inter isa Coulomb
            key_prefix = "inter_CO_"
            params_dic[key_prefix * "weight_14"] = inter.weight_14
            params_dic[key_prefix * "coulomb_const"] = inter.coulomb_const
        elseif inter isa CoulombReactionField
            key_prefix = "inter_CRF_"
            params_dic[key_prefix * "dist_cutoff"] = inter.dist_cutoff
            params_dic[key_prefix * "solvent_dielectric"] = inter.solvent_dielectric
            params_dic[key_prefix * "weight_14"] = inter.weight_14
            params_dic[key_prefix * "coulomb_const"] = inter.coulomb_const
        end
    end

    for inter in values(sys.specific_inter_lists)
        if Molly.interaction_type(inter) <: HarmonicBond
            for bond_type in inter.types
                key_prefix = "inter_HB_$(bond_type)_"
                if !haskey(params_dic, key_prefix * "b0")
                    bond = ff.bond_types[atom_types_to_tuple(bond_type)]
                    params_dic[key_prefix * "b0"] = bond.b0
                    params_dic[key_prefix * "kb"] = bond.kb
                end
            end
        elseif Molly.interaction_type(inter) <: HarmonicAngle
            for angle_type in inter.types
                key_prefix = "inter_HA_$(angle_type)_"
                if !haskey(params_dic, key_prefix * "th0")
                    angle = ff.angle_types[atom_types_to_tuple(angle_type)]
                    params_dic[key_prefix * "th0"] = angle.th0
                    params_dic[key_prefix * "cth"] = angle.cth
                end
            end
        elseif Molly.interaction_type(inter) <: PeriodicTorsion
            for (torsion_type, torsion_inter) in zip(inter.types, Array(inter.inters))
                if torsion_inter.proper
                    key_prefix = "inter_PT_$(torsion_type)_"
                else
                    key_prefix = "inter_IT_$(torsion_type)_"
                end
                if !haskey(params_dic, key_prefix * "phase_1")
                    torsion = ff.torsion_types[atom_types_to_tuple(torsion_type)]
                    for i in 1:length(torsion.phases)
                        params_dic[key_prefix * "phase_$i"] = torsion.phases[i]
                        params_dic[key_prefix * "k_$i"    ] = torsion.ks[i]
                    end
                end
            end
        end
    end

    return params_dic
end

"""
    inject_gradients(sys, params_dic)

Add parameters from a dictionary to a `System`.
Allows gradients for individual parameters to be tracked.
Returns atoms, pairwise interactions and specific interaction lists.
"""
function inject_gradients(sys, params_dic, gpu::Bool=isa(sys.coords, CuArray))
    if gpu
        atoms_grad = cu(inject_atom.(Array(sys.atoms), sys.atoms_data, (params_dic,)))
    else
        atoms_grad = inject_atom.(sys.atoms, sys.atoms_data, (params_dic,))
    end
    pis_grad = inject_interaction.(sys.pairwise_inters, (params_dic,))
    sis_grad = inject_interaction_list.(sys.specific_inter_lists, (params_dic,), gpu)
    return atoms_grad, pis_grad, sis_grad
end

# get function errors with AD
dict_get(dic, key, default) = haskey(dic, key) ? dic[key] : default

function inject_atom(at, at_data, params_dic)
    key_prefix = "atom_$(at_data.atom_type)_"
    Atom(
        at.index,
        at.charge, # Residue-specific
        dict_get(params_dic, key_prefix * "mass"  , at.mass  ),
        dict_get(params_dic, key_prefix * "σ"     , at.σ     ),
        dict_get(params_dic, key_prefix * "ϵ"     , at.ϵ     ),
        at.solute,
    )
end

function inject_interaction_list(inter::InteractionList2Atoms, params_dic, gpu)
    if gpu
        inters_grad = cu(inject_interaction.(Array(inter.inters), inter.types, (params_dic,)))
    else
        inters_grad = inject_interaction.(inter.inters, inter.types, (params_dic,))
    end
    InteractionList2Atoms(inter.is, inter.js, inter.types, inters_grad)
end

function inject_interaction_list(inter::InteractionList3Atoms, params_dic, gpu)
    if gpu
        inters_grad = cu(inject_interaction.(Array(inter.inters), inter.types, (params_dic,)))
    else
        inters_grad = inject_interaction.(inter.inters, inter.types, (params_dic,))
    end
    InteractionList3Atoms(inter.is, inter.js, inter.ks, inter.types, inters_grad)
end

function inject_interaction_list(inter::InteractionList4Atoms, params_dic, gpu)
    if gpu
        inters_grad = cu(inject_interaction.(Array(inter.inters), inter.types, (params_dic,)))
    else
        inters_grad = inject_interaction.(inter.inters, inter.types, (params_dic,))
    end
    InteractionList4Atoms(inter.is, inter.js, inter.ks, inter.ls,
                          inter.types, inters_grad)
end

function inject_interaction(inter::LennardJones{S, C, W, WS, F, E}, params_dic) where {S, C, W, WS, F, E}
    key_prefix = "inter_LJ_"
    LennardJones{S, C, W, WS, F, E}(
        inter.cutoff,
        inter.nl_only,
        inter.lorentz_mixing,
        dict_get(params_dic, key_prefix * "weight_14", inter.weight_14),
        dict_get(params_dic, key_prefix * "weight_solute_solvent", inter.weight_solute_solvent),
        inter.force_units,
        inter.energy_units,
    )
end

function inject_interaction(inter::Coulomb, params_dic)
    key_prefix = "inter_CO_"
    Coulomb(
        inter.cutoff,
        inter.nl_only,
        dict_get(params_dic, key_prefix * "weight_14", inter.weight_14),
        dict_get(params_dic, key_prefix * "coulomb_const", inter.coulomb_const),
        inter.force_units,
        inter.energy_units,
    )
end

function inject_interaction(inter::CoulombReactionField, params_dic)
    key_prefix = "inter_CRF_"
    CoulombReactionField(
        dict_get(params_dic, key_prefix * "dist_cutoff", inter.dist_cutoff),
        dict_get(params_dic, key_prefix * "solvent_dielectric", inter.solvent_dielectric),
        inter.nl_only,
        dict_get(params_dic, key_prefix * "weight_14", inter.weight_14),
        dict_get(params_dic, key_prefix * "coulomb_const", inter.coulomb_const),
        inter.force_units,
        inter.energy_units,
    )
end

function inject_interaction(inter::HarmonicBond, inter_type, params_dic)
    key_prefix = "inter_HB_$(inter_type)_"
    HarmonicBond(
        dict_get(params_dic, key_prefix * "b0", inter.b0),
        dict_get(params_dic, key_prefix * "kb", inter.kb),
    )
end

function inject_interaction(inter::HarmonicAngle, inter_type, params_dic)
    key_prefix = "inter_HA_$(inter_type)_"
    HarmonicAngle(
        dict_get(params_dic, key_prefix * "th0", inter.th0),
        dict_get(params_dic, key_prefix * "cth", inter.cth),
    )
end

function inject_interaction(inter::PeriodicTorsion{N, T, E}, inter_type, params_dic) where {N, T, E}
    if inter.proper
        key_prefix = "inter_PT_$(inter_type)_"
    else
        key_prefix = "inter_IT_$(inter_type)_"
    end
    PeriodicTorsion{N, T, E}(
        inter.periodicities,
        ntuple(i -> dict_get(params_dic, key_prefix * "phase_$i", inter.phases[i]), N),
        ntuple(i -> dict_get(params_dic, key_prefix * "k_$i"    , inter.ks[i]    ), N),
        inter.proper,
    )
end


type MockSolver <: AbstractMathProgSolver
  vars_decomposition
  cstrs_decomposition
  sp_mult
  sp_prio
  obj_magnitude
  obj_lb
  obj_ub
  vars_branching_priorities
  oracles
  genvarcallbacks
  gencstrcallbacks
end
export MockSolver

function MockSolver()
  MockSolver(nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing, nothing)
end

type MockMathProgModel <: AbstractMathProgModel
  solver::MockSolver
end

LinearQuadraticModel(s::MockSolver) = MockMathProgModel(s)
ConicModel(s::MockSolver) = LPQPtoConicBridge(LinearQuadraticModel(s))
supportedcones(s::MockSolver) = [:Free,:Zero,:NonNeg,:NonPos]

################################################################################
######################   BlockDecomposition.BlockSolverInterface ###############
################################################################################
set_constrs_decomposition!(s::MockSolver, cstrs_decomposition_list) =
  (s.cstrs_decomposition = cstrs_decomposition_list)

set_vars_decomposition!(s::MockSolver, vars_decomposition_list) =
  (s.vars_decomposition = vars_decomposition_list)

set_sp_mult!(s::MockSolver, sp_multiplities) =
  (s.sp_mult = sp_multiplities)

set_var_branching_prio!(s::MockSolver, var_priorities) =
  (s.vars_branching_priorities = var_priorities)

set_sp_prio!(s::MockSolver, sp_priorities) =
  (s.sp_prio = sp_priorities)

set_var_generic!(s::MockSolver, genvarsfcts) =
  (s.genvarcallbacks = genvarsfcts)

set_cstr_generic!(s::MockSolver, gencstrcbs) =
  (s.gencstrcallbacks = gencstrcbs)

function set_objective_bounds_and_magnitude!(s::MockSolver, magnitude, lb, ub)
  s.obj_magnitude = (lb > magnitude || ub < magnitude)? NaN : magnitude
  s.obj_lb = (lb > -Inf)? lb : NaN
  s.obj_ub = (ub < Inf)? ub : NaN
end

getcurrentcost(m::MockMathProgModel, varidx) = error("GetCurrentCost not implemented")

set_oracles!(s::MockSolver, oracles) = (s.oracles = oracles)

################################################################################
##################### MathProgBase.SolverInterface #############################
## Loads problem data from the given file
function loadproblem!(m::MockMathProgModel, filename::AbstractString)
  println("loadproblem!(error) not implemented")
end

## Loads the provided problem data to set up the linear programming problem
##  loadproblem!(m::MockMathProgModel, A, l, u, c, lb, ub, sense)
##  which is the following LP
##        minₓ cᵀx
##        s.c. lb ≤ Ax ≤ ub
##              l ≤  x ≤ u
## sense specifies the direction of optimization problem (:Min or :Max)
function loadproblem!(m::MockMathProgModel, A, l, u, c, lb, ub, sense)
  println("Fake optimization.")
end

## Returns a vector containing the lower bounds l on the variables.
getvarLB(m::MockMathProgModel) = println("getvarLB! not implemented")

## Sets the lower bounds on the variables.
setvarLB!(m::MockMathProgModel, l) = println("setvarLB! not implemented")

##  Returns a vector containing the upper bounds u on the variables.
getvarUB(m::MockMathProgModel) = println("getvarUB not implemented")

## Sets the upper bounds on the variables.
setvarUB!(m::MockMathProgModel, u) = println("setvarUB! not implemented")

## Returns a vector containing the lower bounds lb on the linear constraints.
getconstrLB(m::MockMathProgModel) = println("getconstrLB not implemented")

## Sets the lower bounds on the linear constraints.
setconstrLB!(m::MockMathProgModel, lb) = println("setconstrLB! not implemented")

## Returns a vector containing the upper bounds ub on the linear constraints.
getconstrUB(m::MockMathProgModel) = println("getconstrUB not implemented")

## Sets the upper bounds on the linear constraints.
setconstrUB!(m::MockMathProgModel, ub) = println("setconstrUB! not implemented")

## Returns a vector containing the linear objective coefficients c.
getobj(m::MockMathProgModel) = println("getobj not implemented")

## Sets the linear objective coefficients.
setobj!(m::MockMathProgModel, c) = println("setobj! not implemented")

## Returns the full linear constraint matrix A, typically as a SparseMatrixCSC.
getconstrmatrix(m::MockMathProgModel) = error("getconstrmatrix! not implemented")

## Adds a new variable to the model, with lower bound l (-Inf if none), upper
## bound u (Inf if none), and objective coefficient objcoef. Constraint
## coefficients for this new variable are specified in a sparse format: the
## constrcoef vector contains the nonzero coefficients, and the constridx vector
## contains the indices of the corresponding linear constraints.
function addvar!(m::MockMathProgModel, constridx, constrcoef, l, u, objcoef)
  println("addvar! not implemented")
end

## Adds a new variable to the model, with lower bound l (-Inf if none),
## upper bound u (Inf if none), and objective coefficient objcoef. This is
## equivalent to calling the above method with empty arrays for the constraint
## coefficients.
function addvar!(m::MockMathProgModel, l, u, objcoef)
  println("addvar! not implemented")
end

## Adds a new linear constraint to the model, with lower bound lb (-Inf if none)
## and upper bound ub (Inf if none). Coefficients for this new constraint are
## specified in a sparse format: the coef vector contains the nonzero
## coefficients, and the varidx vector contains the indices of the corresponding
## variables.
# function addconstr!(m::MockMathProgModel, varidx, coef, lb, ub)
#   println("addconstr! not implemented")
# end
function addconstr!(m::MockMathProgModel, varidx, coef, lb, ub)
  println("addconstr! not implemented")
end

## Returns the number of linear constraints in the model.
numlinconstr(m::MockMathProgModel) = println("numlinconstr not implemented")

## Returns a vector containing the values of the linear constraints at the
## solution. This is the vector Ax.
getconstrsolution(m::MockMathProgModel) =
  println("getconstrsolution not implemented")

## Returns the dual solution vector corresponding to the variable bounds,
## known as the reduced costs. Not available when integer variables are present.
getreducedcosts(m::MockMathProgModel) =
  println("getreducedcosts not implemented")

######################## SOLVE INTERFACE ######################################

## Returns the solution vector found by the solver.
getsolution(m::MockMathProgModel) = println("getsolution not implemented")

## Returns the objective value of the solution found by the solver. In
## particular, this may be the objective value for the best feasible solution if
## optimality is not attained or proven.
getobjval(m::MockMathProgModel) = println("getobjval not implemented")

## Solves the optimization problem.
function optimize!(m::MockMathProgModel)
  println("Fake optimize!")
end

## Returns the termination status after solving. Possible values include
## :Optimal, :Infeasible, :Unbounded, :UserLimit (iteration limit or timeout),
## and :error. Solvers may return other statuses, for example, when presolve
## indicates that the model is either infeasible or unbounded, but did not
## determine which.
function status(m::MockMathProgModel)
  println("Fake status")
  return :Optimal
end

## Returns the best known bound on the optimal objective value. This is used,
## for example, when a branch-and-bound method is stopped before finishing.
getobjbound(m::AbstractMathProgModel) = println("getobjbound not implemented")

## Returns the final relative optimality gap as optimization terminated.
getobjgap(m::AbstractMathProgModel) = println("getobjgap not implemented")

## Returns an object that may be used to access a solver-specific API for this
## model.
getrawsolver(m::AbstractMathProgModel) = println("getrawsolver not implemented")

##  Returns the total elapsed solution time as reported by the solver.
getsolvetime(m::AbstractMathProgModel) = println("getsolvetime not implemented")

## Sets the optimization sense of the model. Accepted values are :Min and :Max.
setsense!(m::MockMathProgModel, sense) = println("setsense! not implemented")

## Returns the optimization sense of the model.
getsense(m::MockMathProgModel) = println("getsense not implemented")

## Returns the number of variables in the model.
numvar(m::MockMathProgModel) = println("numvar not implemented")

## Returns the total number of constraints in the model.
numconstr(m::MockMathProgModel) = println("numconstr not implemented")

## Release any resources and memory used by the model. Note that the Julia
## garbage collector takes care of this automatically, but automatic collection
## cannot always be forced. This method is useful for more precise control of
## resources, especially in the case of commercial solvers with licensing
## restrictions on the number of concurrent runs. Users must discard the model
## object after this method is invoked.
freemodel!(m::AbstractMathProgModel) = println("freemodel! not implemented")

## Sets the types of the variables to those indicated by the vector v. Valid
## types are :Int for integer, :Cont for continuous, :Bin for binary, :SemiCont
## for semicontinuous, and :SemiInt for semi-integer.
setvartype!(m::MockMathProgModel, typ::Vector{Symbol}) =
    println("setvartype! not implemented")

## Returns a vector indicating the types of each variable, with values described
## above.
getvartype(m::MockMathProgModel) = println("getvartype not implemented")

## It is the philosophy of MathProgBase to not abstract over most solver
## parameters, because very few are universal, and we do not want to make it
## difficult for users who already familiar with the parameters of their solver
## of choice. However, in certain situations an abstraction over parameters is
## needed, for example, in meta-solvers which must adjust parameters of a
## subsolver inside a loop. Parameters set using these methods should override
## any parameters provided in a solver-dependent way. Solvers/models may chose
## to implement the following method:

## Sets solver-independent parameters via keyword arguments. Curent valid
## parameters are TimeLimit and Silent. TimeLimit (Float64): If the solve is not
## completed to optimality tolerances within the given number of seconds, the
## solver should return immediately with status :UserLimit. Silent (Bool): If
## set to true, the solver should disable any output to the console. If set to
## false, the parameter has no effect.
function setparameters!(m::Union{AbstractMathProgSolver, AbstractMathProgModel}; kwargs...)
    println("setparameters! not yet implemented")
end

## If these parameter-setting methods are called on an AbstractMathProgSolver,
## then they should apply to all new models created from the solver
## (but not existing models). If they are called on an AbstractMathProgModel,
## they should apply to that model only. Unrecognized parameters
## (those not listed above) should be ignored or trigger a warning message.


export play, scene_and_node

using Makie
using Colors

const SNAKE_GAME_SCEENS = IdDict()

function scene_and_node(game::SnakeGame)
    if haskey(SNAKE_GAME_SCEENS, game)
        SNAKE_GAME_SCEENS[game]
    else
        node = Node(game)
        scene = init_screen(node)
        get!(SNAKE_GAME_SCEENS, game, (scene, node))
    end
end

function Base.display(game::SnakeGame)
    scene, node = scene_and_node(game)
    node[] = game
    display(scene)
end

function init_screen(game::Observable{<:SnakeGame{2}}; resolution=(1000,1000))
    SNAKE_COLORS = range(HSV(60,1,1), stop=HSV(300,1,1), length=length(game[].snakes)+1)
    scene = Scene(resolution = resolution, raw = true, camera = campixel!)

    area = scene.px_area
    poly!(scene, area)

    grid_size = @lift((widths($area)[1] / size($game)[1], widths($area)[2] / size($game)[2]))

    walls = get_walls(game[])
    if length(walls) > 0
        wall_boxes = @lift([FRect2D((w.I .- (1,1)) .* $grid_size , $grid_size) for w in walls])
        poly!(scene, wall_boxes, color=:gray)
    end

    for i in 1:length(game[].snakes)
        snake_boxes = @lift([FRect2D((p.I .- (1,1)) .* $grid_size , $grid_size) for p in $game.snakes[i]])
        poly!(scene, snake_boxes, color=SNAKE_COLORS[i], strokewidth = 5, strokecolor = :black)

        snake_head_box = @lift(FRect2D(($game.snakes[i][1].I .- (1,1)) .* $grid_size , $grid_size))
        poly!(scene, snake_head_box, color=:black)
        snake_head = @lift((($game.snakes[i][1].I .- 0.5) .* $grid_size))
        scatter!(scene, snake_head, marker='◉', color=SNAKE_COLORS[i], markersize=@lift(minimum($grid_size)))
    end

    food_position = @lift([(f.I .- (0.5, 0.5)) .* $grid_size for f in $game.foods])
    scatter!(scene, food_position, color=:red, marker='♥', markersize=@lift(minimum($grid_size)))

    display(scene)
    scene
end

play() = play(SnakeGame())

function play(game::SnakeGame{2};f_name="test.gif",framerate = 2)
    @assert length(game.snakes) <= 3 "At most three players are supported in interactive mode"
    scene, game_node = scene_and_node(game)

    LEFT = CartesianIndex(-1, 0)
    RIGHT = CartesianIndex(1, 0)
    UP = CartesianIndex(0, 1)
    DOWN = CartesianIndex(0, -1)

    actions = [rand([LEFT,RIGHT,UP,DOWN]) for _ in game.snakes]
    is_exit = Ref{Bool}(false)

    on(scene.events.keyboardbuttons) do but
        if ispressed(but, Keyboard.left)
            actions[1] != -LEFT && (actions[1] = LEFT)
        elseif ispressed(but, Keyboard.up)
            actions[1] != -UP && (actions[1] = UP)
        elseif ispressed(but, Keyboard.down)
            actions[1] != -DOWN && (actions[1] = DOWN)
        elseif ispressed(but, Keyboard.right)
            actions[1] != -RIGHT && (actions[1] = RIGHT)
        elseif ispressed(but, Keyboard.a)
            actions[2] != -LEFT && (actions[2] = LEFT)
        elseif ispressed(but, Keyboard.w)
            actions[2] != -UP && (actions[2] = UP)
        elseif ispressed(but, Keyboard.s)
            actions[2] != -DOWN && (actions[2] = DOWN)
        elseif ispressed(but, Keyboard.d)
            actions[2] != -RIGHT && (actions[2] = RIGHT)
        elseif ispressed(but, Keyboard.j)
            actions[3] != -LEFT && (actions[3] = LEFT)
        elseif ispressed(but, Keyboard.i)
            actions[3] != -UP && (actions[3] = UP)
        elseif ispressed(but, Keyboard.k)
            actions[3] != -DOWN && (actions[3] = DOWN)
        elseif ispressed(but, Keyboard.l)
            actions[3] != -RIGHT && (actions[3] = RIGHT)
        elseif ispressed(but, Keyboard.q)
            is_exit[] = true
        end
    end

    record(scene, f_name; framerate=framerate) do io
        while true
            sleep(1)
            is_success = game(actions)
            game_node[] = game
            recordframe!(io)
            is_success || break
            is_exit[] && break
        end
    end
    println("game over")
end


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

"""
  function fasta2matrix(filename::AbstractString; max_gap_fraction = 1)

  Reads an aligned fasta file and returns a matrix with amino acids converted
  to numbers following the convention A --> 1, C --> 2, ... '-' --> 21.

    "filename": full path of the fasta file.
    "max_gap_fraction": to be removed.

"""

function fasta2matrix(filename::AbstractString; max_gap_fraction = 1)
    f = FastaReader(filename)

    max_gap_fraction = Float64(max_gap_fraction)

    # pass 1

    seqs = Int[]
    inds = Int[]
    fseqlen = 0

    for (name, seq) in f
        ngaps = 0
        if f.num_parsed == 1
            ls = length(seq)
            resize!(inds, ls)
            for i = 1:ls
                c = seq[i]
                if c != '.' && c == uppercase(c)
                    fseqlen += 1
                    inds[fseqlen] = i
                    c == '-' && (ngaps += 1)
                end
            end
        else
            ls = length(seq)
            ls == length(inds) || error("inputs are not aligned")
            tstfseqlen = 0
            for i = 1:ls
                c = seq[i]
                if c != '.' && c == uppercase(c)
                    tstfseqlen += 1
                    inds[tstfseqlen] == i || error("inconsistent inputs")
                    c == '-' && (ngaps += 1)
                end
            end
            tstfseqlen == fseqlen || error("inconsistent inputs")
        end
        ngaps / fseqlen <= max_gap_fraction && push!(seqs, f.num_parsed)
    end

    length(seqs) > 0 || error("Out of $(f.num_parsed) sequences, none passed the filter (max_gap_fraction=$max_gap_fraction)")

    # pass 2
    Z = Array{Int16}(undef, fseqlen, length(seqs))
    seqid = 1
    for (name, seq) in f
        seqs[end] < f.num_parsed && break
        seqs[seqid] == f.num_parsed || continue
        for i = 1:fseqlen
            c = seq[inds[i]]
            Z[i, seqid] = letter2num(c)
        end
        seqid += 1
    end
    @assert seqid == length(seqs) + 1

    close(f)

    return Z'
end

##############################################################
"""
    letter2num(c::Union{Char,UInt8})

    Takes as input a char (representing an amino acid)
    and returns its conventional number representation.
    In this case with the convention "1 2 .. 21" <==> "A C .. -"
"""

let alphabet = [ 1, 21, 2, 3, 4, 5, 6, 7, 8, 21, 9, 10, 11, 12, 21, 13, 14, 15, 16, 17, 21, 18, 19, 21, 20]
               # A, B,  C, D, E, F, G, H, I, J,  K, L,  M,  N,  O,  P,  Q,  R,  S,  T,  U,  V,  W,  X,  Y
    global letter2num
    function letter2num(c::Union{Char,UInt8})
        i = UInt8(c) - 0x40
        1 <= i <= 25 && return alphabet[i]
        return 21
     end
end

##############################################################
"""
    num2letter(i::Integer)

    Takes as input an integer (representing an amino acid)
    and returns its conventional character representation.
    In this case with the convention "1 2 .. 21" <==> "A C .. -"
"""

let alphabet = ["A", "C", "D", "E", "F", "G", "H", "I",  "K", "L",  "M",  "N", "P",  "Q",  "R",
"S",  "T", "V",  "W",  "Y"]
    global num2letter
    function num2letter(i :: Integer)
        1 <= i <= 20 && return alphabet[i]
        return "-"
    end
end

##############################################################
"""
    vec2string(v)

    Takes as input a vector of integers (representing an amino acids)
    and returns a list of characters, the corresponding amino acids.
    In this case with the convention "1 2 .. 21" <==> "A C .. -"
"""

function vec2string(v)
    s = ""
    for i in v
        s = s*num2letter(i)
    end
    return s
end

##############################################################
"""
    Takes as input a string (representing amino acids)
    and returns a vector of numbers, the corresponding number representation.
    In this case with the convention "1 2 .. 21" <==> "A C .. -"
"""

function string2vec(s)
    v = Vector{Int8}(undef, length(s))
    for (i, l) in enumerate(s)
        v[i] = letter2num(l)
    end
    return v
end


export BrickletVoltageCurrentConfiguration
struct BrickletVoltageCurrentConfiguration
    averaging::Integer
    voltage_conversion_time::Integer
    current_conversion_time::Integer
end

export BrickletVoltageCurrentCalibration
struct BrickletVoltageCurrentCalibration
    gain_multiplier::Integer
    gain_divisor::Integer
end

export BrickletVoltageCurrentCurrentCallbackThreshold
struct BrickletVoltageCurrentCurrentCallbackThreshold
    option::Char
    min::Integer
    max::Integer
end

export BrickletVoltageCurrentVoltageCallbackThreshold
struct BrickletVoltageCurrentVoltageCallbackThreshold
    option::Char
    min::Integer
    max::Integer
end

export BrickletVoltageCurrentPowerCallbackThreshold
struct BrickletVoltageCurrentPowerCallbackThreshold
    option::Char
    min::Integer
    max::Integer
end

export BrickletVoltageCurrentIdentity
struct BrickletVoltageCurrentIdentity
    uid::String
    connected_uid::String
    position::Char
    hardware_version::Vector{Integer}
    firmware_version::Vector{Integer}
    device_identifier::Integer
end

export BrickletVoltageCurrent
"""
Measures power, DC voltage and DC current up to 720W/36V/20A
"""
mutable struct BrickletVoltageCurrent <: TinkerforgeDevice
    ipcon::IPConnection
    deviceInternal::PyObject

    """
    Creates an object with the unique device ID *uid* and adds it to
    the IP Connection *ipcon*.
    """
    function BrickletVoltageCurrent(uid::String, ipcon::IPConnection)
        package = pyimport("tinkerforge.bricklet_voltage_current")
        deviceInternal = package.BrickletVoltageCurrent(uid, ipcon.ipconInternal)

        return new(ipcon, deviceInternal)
    end
end

export get_current
"""
    $(SIGNATURES)

Returns the current.

If you want to get the current periodically, it is recommended to use the
:cb:`Current` callback and set the period with
:func:`Set Current Callback Period`.
"""
function get_current(device::BrickletVoltageCurrent)
    return device.deviceInternal.get_current()
end

export get_voltage
"""
    $(SIGNATURES)

Returns the voltage.

If you want to get the voltage periodically, it is recommended to use the
:cb:`Voltage` callback and set the period with
:func:`Set Voltage Callback Period`.
"""
function get_voltage(device::BrickletVoltageCurrent)
    return device.deviceInternal.get_voltage()
end

export get_power
"""
    $(SIGNATURES)

Returns the power.

If you want to get the power periodically, it is recommended to use the
:cb:`Power` callback and set the period with
:func:`Set Power Callback Period`.
"""
function get_power(device::BrickletVoltageCurrent)
    return device.deviceInternal.get_power()
end

export set_configuration
"""
    $(SIGNATURES)

Sets the configuration of the Voltage/Current Bricklet. It is
possible to configure number of averages as well as
voltage and current conversion time.
"""
function set_configuration(device::BrickletVoltageCurrent, averaging, voltage_conversion_time, current_conversion_time)
    device.deviceInternal.set_configuration(averaging, voltage_conversion_time, current_conversion_time)
end

export get_configuration
"""
    $(SIGNATURES)

Returns the configuration as set by :func:`Set Configuration`.
"""
function get_configuration(device::BrickletVoltageCurrent)
    return device.deviceInternal.get_configuration()
end

export set_calibration
"""
    $(SIGNATURES)

Since the shunt resistor that is used to measure the current is not
perfectly precise, it needs to be calibrated by a multiplier and
divisor if a very precise reading is needed.

For example, if you are expecting a measurement of 1000mA and you
are measuring 1023mA, you can calibrate the Voltage/Current Bricklet
by setting the multiplier to 1000 and the divisor to 1023.
"""
function set_calibration(device::BrickletVoltageCurrent, gain_multiplier, gain_divisor)
    device.deviceInternal.set_calibration(gain_multiplier, gain_divisor)
end

export get_calibration
"""
    $(SIGNATURES)

Returns the calibration as set by :func:`Set Calibration`.
"""
function get_calibration(device::BrickletVoltageCurrent)
    return device.deviceInternal.get_calibration()
end

export set_current_callback_period
"""
    $(SIGNATURES)

Sets the period with which the :cb:`Current` callback is triggered
periodically. A value of 0 turns the callback off.

The :cb:`Current` callback is only triggered if the current has changed since
the last triggering.
"""
function set_current_callback_period(device::BrickletVoltageCurrent, period)
    device.deviceInternal.set_current_callback_period(period)
end

export get_current_callback_period
"""
    $(SIGNATURES)

Returns the period as set by :func:`Set Current Callback Period`.
"""
function get_current_callback_period(device::BrickletVoltageCurrent)
    return device.deviceInternal.get_current_callback_period()
end

export set_voltage_callback_period
"""
    $(SIGNATURES)

Sets the period with which the :cb:`Voltage` callback is triggered
periodically. A value of 0 turns the callback off.

The :cb:`Voltage` callback is only triggered if the voltage has changed since
the last triggering.
"""
function set_voltage_callback_period(device::BrickletVoltageCurrent, period)
    device.deviceInternal.set_voltage_callback_period(period)
end

export get_voltage_callback_period
"""
    $(SIGNATURES)

Returns the period as set by :func:`Set Voltage Callback Period`.
"""
function get_voltage_callback_period(device::BrickletVoltageCurrent)
    return device.deviceInternal.get_voltage_callback_period()
end

export set_power_callback_period
"""
    $(SIGNATURES)

Sets the period with which the :cb:`Power` callback is triggered
periodically. A value of 0 turns the callback off.

The :cb:`Power` callback is only triggered if the power has changed since the
last triggering.
"""
function set_power_callback_period(device::BrickletVoltageCurrent, period)
    device.deviceInternal.set_power_callback_period(period)
end

export get_power_callback_period
"""
    $(SIGNATURES)

Returns the period as set by :func:`Get Power Callback Period`.
"""
function get_power_callback_period(device::BrickletVoltageCurrent)
    return device.deviceInternal.get_power_callback_period()
end

export set_current_callback_threshold
"""
    $(SIGNATURES)

Sets the thresholds for the :cb:`Current Reached` callback.

The following options are possible:

.. csv-table::
 :header: "Option", "Description"
 :widths: 10, 100

 "'x'",    "Callback is turned off"
 "'o'",    "Callback is triggered when the current is *outside* the min and max values"
 "'i'",    "Callback is triggered when the current is *inside* the min and max values"
 "'<'",    "Callback is triggered when the current is smaller than the min value (max is ignored)"
 "'>'",    "Callback is triggered when the current is greater than the min value (max is ignored)"
"""
function set_current_callback_threshold(device::BrickletVoltageCurrent, option, min, max)
    device.deviceInternal.set_current_callback_threshold(option, min, max)
end

export get_current_callback_threshold
"""
    $(SIGNATURES)

Returns the threshold as set by :func:`Set Current Callback Threshold`.
"""
function get_current_callback_threshold(device::BrickletVoltageCurrent)
    return device.deviceInternal.get_current_callback_threshold()
end

export set_voltage_callback_threshold
"""
    $(SIGNATURES)

Sets the thresholds for the :cb:`Voltage Reached` callback.

The following options are possible:

.. csv-table::
 :header: "Option", "Description"
 :widths: 10, 100

 "'x'",    "Callback is turned off"
 "'o'",    "Callback is triggered when the voltage is *outside* the min and max values"
 "'i'",    "Callback is triggered when the voltage is *inside* the min and max values"
 "'<'",    "Callback is triggered when the voltage is smaller than the min value (max is ignored)"
 "'>'",    "Callback is triggered when the voltage is greater than the min value (max is ignored)"
"""
function set_voltage_callback_threshold(device::BrickletVoltageCurrent, option, min, max)
    device.deviceInternal.set_voltage_callback_threshold(option, min, max)
end

export get_voltage_callback_threshold
"""
    $(SIGNATURES)

Returns the threshold as set by :func:`Set Voltage Callback Threshold`.
"""
function get_voltage_callback_threshold(device::BrickletVoltageCurrent)
    return device.deviceInternal.get_voltage_callback_threshold()
end

export set_power_callback_threshold
"""
    $(SIGNATURES)

Sets the thresholds for the :cb:`Power Reached` callback.

The following options are possible:

.. csv-table::
 :header: "Option", "Description"
 :widths: 10, 100

 "'x'",    "Callback is turned off"
 "'o'",    "Callback is triggered when the power is *outside* the min and max values"
 "'i'",    "Callback is triggered when the power is *inside* the min and max values"
 "'<'",    "Callback is triggered when the power is smaller than the min value (max is ignored)"
 "'>'",    "Callback is triggered when the power is greater than the min value (max is ignored)"
"""
function set_power_callback_threshold(device::BrickletVoltageCurrent, option, min, max)
    device.deviceInternal.set_power_callback_threshold(option, min, max)
end

export get_power_callback_threshold
"""
    $(SIGNATURES)

Returns the threshold as set by :func:`Set Power Callback Threshold`.
"""
function get_power_callback_threshold(device::BrickletVoltageCurrent)
    return device.deviceInternal.get_power_callback_threshold()
end

export set_debounce_period
"""
    $(SIGNATURES)

Sets the period with which the threshold callbacks

* :cb:`Current Reached`,
* :cb:`Voltage Reached`,
* :cb:`Power Reached`

are triggered, if the thresholds

* :func:`Set Current Callback Threshold`,
* :func:`Set Voltage Callback Threshold`,
* :func:`Set Power Callback Threshold`

keep being reached.
"""
function set_debounce_period(device::BrickletVoltageCurrent, debounce)
    device.deviceInternal.set_debounce_period(debounce)
end

export get_debounce_period
"""
    $(SIGNATURES)

Returns the debounce period as set by :func:`Set Debounce Period`.
"""
function get_debounce_period(device::BrickletVoltageCurrent)
    return device.deviceInternal.get_debounce_period()
end

export get_identity
"""
    $(SIGNATURES)

Returns the UID, the UID where the Bricklet is connected to,
the position, the hardware and firmware version as well as the
device identifier.

The position can be 'a', 'b', 'c', 'd', 'e', 'f', 'g' or 'h' (Bricklet Port).
A Bricklet connected to an :ref:`Isolator Bricklet <isolator_bricklet>` is always at
position 'z'.

The device identifier numbers can be found :ref:`here <device_identifier>`.
|device_identifier_constant|
"""
function get_identity(device::BrickletVoltageCurrent)
    return device.deviceInternal.get_identity()
end

export register_callback
"""
Registers the given *function* with the given *callback_id*.
"""
function register_callback(device::BrickletVoltageCurrent, callback_id, function_)
    if isnothing(function_)
        device.registered_callbacks.pop(callback_id, None)
    else
        device.registered_callbacks[callback_id] = function_
    end
end