if !haskey(ENV, "BUILDKITE")
    import Pkg
    Pkg.develop(Pkg.PackageSpec(; path = dirname(@__DIR__)))
end
using Test

using KernelAbstractions
const KA = KernelAbstractions

import CUDAKernels
const CK = CUDAKernels

import Thermodynamics
const TD = Thermodynamics

import UnPack

using Random
using LinearAlgebra

import RootSolvers
const RS = RootSolvers

import CLIMAParameters
const CP = CLIMAParameters

struct EarthParameterSet <: CP.AbstractEarthParameterSet end
const param_set = EarthParameterSet()

if get(ARGS, 1, "Array") == "CuArray"
    import CUDA
    ArrayType = CUDA.CuArray
    CUDA.allowscalar(false)
    device(::Type{T}) where {T <: CUDA.CuArray} = CK.CUDADevice()
else
    ArrayType = Array
    device(::Type{T}) where {T <: Array} = CK.CPU()
end

@show ArrayType

@kernel function test_thermo_kernel!(
    param_set,
    dst::AbstractArray{FT},
    e_int,
    ρ,
    p,
    q_tot,
) where {FT}
    i = @index(Group, Linear)
    @inbounds begin

        ts = TD.PhaseEquil_ρeq(param_set, FT(ρ[i]), FT(e_int[i]), FT(q_tot[i]))
        dst[1, i] = TD.air_temperature(param_set, ts)

        ts_ρpq = TD.PhaseEquil_ρpq(
            param_set,
            FT(ρ[i]),
            FT(p[i]),
            FT(q_tot[i]),
            true,
            100,
            RS.RegulaFalsiMethod,
        )
        dst[2, i] = TD.air_temperature(param_set, ts_ρpq)
    end
end

# Since we use `rand` to generate the ProfileSet,
# just initialize on the CPU, and provide convert
# function to move arrays to the GPU.
convert_profile_set(ps::TD.TestedProfiles.ProfileSet, ArrayType, slice) =
    TD.TestedProfiles.ProfileSet(
        ArrayType(ps.z[slice]),
        ArrayType(ps.T[slice]),
        ArrayType(ps.p[slice]),
        ArrayType(ps.RS[slice]),
        ArrayType(ps.e_int[slice]),
        ArrayType(ps.h[slice]),
        ArrayType(ps.ρ[slice]),
        ArrayType(ps.θ_liq_ice[slice]),
        ArrayType(ps.q_tot[slice]),
        ArrayType(ps.q_liq[slice]),
        ArrayType(ps.q_ice[slice]),
        TD.PhasePartition.(ps.q_tot[slice], ps.q_liq[slice], ps.q_ice[slice]),
        ArrayType(ps.RH[slice]),
        ArrayType(ps.e_pot[slice]),
        ArrayType(ps.u[slice]),
        ArrayType(ps.v[slice]),
        ArrayType(ps.w[slice]),
        ArrayType(ps.e_kin[slice]),
        ps.phase_type,
    )

@testset "Thermodynamics - kernels" begin
    FT = Float32
    dev = device(ArrayType)
    profiles = TD.TestedProfiles.PhaseEquilProfiles(param_set, Array)
    slice = Colon()
    profiles = convert_profile_set(profiles, ArrayType, slice)

    n_profiles = length(profiles.z)
    n_vars = length(propertynames(profiles))
    d_dst = ArrayType(Array{FT}(undef, 2, n_profiles))
    fill!(d_dst, 0)

    UnPack.@unpack e_int, ρ, p, q_tot = profiles

    work_groups = (1,)
    ndrange = (n_profiles,)
    kernel! = test_thermo_kernel!(dev, work_groups)
    event = kernel!(param_set, d_dst, e_int, ρ, p, q_tot, ndrange = ndrange)
    wait(dev, event)

    ts_correct =
        TD.PhaseEquil_ρeq.(param_set, Array(ρ), Array(e_int), Array(q_tot))
    @test all(Array(d_dst)[1, :] .≈ TD.air_temperature.(param_set, ts_correct))

    ts_correct =
        TD.PhaseEquil_ρpq.(
            param_set,
            Array(ρ),
            Array(p),
            Array(q_tot),
            true,
            100,
            RS.RegulaFalsiMethod,
        )
    @test all(Array(d_dst)[2, :] .≈ TD.air_temperature.(param_set, ts_correct))

end


## WARNING: this is an attempt to wrap cuDNNs dropout implementation
##          it doesn't work and should be considered work in progress

## Dropout descriptor

mutable struct DOD
    ptr::Ptr{Void}
    states::CuArray{UInt8,1}
end

# TODO: what should be passed for states? what size should it have?
function DOD(dropout::Float32; handle=cudnnhandle(), states=C_NULL,
             seed=UInt64(42))
    d = Ref{Cptr}(0)
    @cuda(cudnn, cudnnCreateDropoutDescriptor, (Ref{Cptr},), d)

    statesSizeInBytes = Ref{Csize_t}(0)
    @cuda(cudnn, cudnnDropoutGetStatesSize, (Cptr, Ref{Csize_t}), handle, statesSizeInBytes)

    states = CuArray{UInt8,1}(statesSizeInBytes[])
    # TODO: states will be garbage collected and thus its memory will be reclained
    # should we return it in DOD?
    @cuda(cudnn, cudnnSetDropoutDescriptor,
          (Ref{Cptr}, Cptr, Cfloat, Cptr, Cuint, Culong),
          d, handle, dropout, states.ptr, statesSizeInBytes[], seed)
    dod = DOD(d[], states)
    finalizer(dod, x->@cuda(cudnn, cudnnDestroyDropoutDescriptor, (Cptr,), x.ptr))
    # finalizer(dod, x -> cudnnDestroyDropoutDescriptor(x.ptr))
    return dod
end

# doesn't work on my machine
function get_dropout_desc(dod::DOD)
    dropout = Ref{Cfloat}(0)
    states = Ref{Cptr}(0)
    seed = Ref{Culonglong}(0)
    # TODO: cudnnGetDropoutDescriptor cannot be found. Wrong library version?
    @cuda(cudnn, cudnnGetDropoutDescriptor,
          (Cptr, Cptr, Ptr{Cfloat}, Ptr{Cptr}, Ptr{Culong}),
          d, handle, dropout, states, seed)
end

function  get_reserve_space_size(x::CuArray{T}) where T
    td = TD(x)
    sz = Ref{Csize_t}(0)
    @cuda(cudnn, cudnnDropoutGetReserveSpaceSize, (Cptr, Ref{Csize_t}), td.ptr, sz)
    return sz[]
end

function  dropout_get_states_size(handle)
    sz = Ref{Csize_t}(0)
    @cuda(cudnn, cudnnDropoutGetStatesSize, (Cptr, Ref{Csize_t}), handle, sz)
    return sz[]
end

function dropout_forward!(y::CuArray{T}, x::CuArray{T}, dropout::Float64;
                          handle=cudnnhandle(), rs=nothing) where T
    dod = DOD(Float32(dropout))
    xtd = TD(x)
    ytd = TD(y)
    if rs == nothing
        rs_sz = get_reserve_space_size(x)
        rs = CuArray(zeros(UInt8, rs_sz))
        # rs = Ptr{Void}(0)
    else
        rs_sz = length(rs)
    end
    @cuda(cudnn, cudnnDropoutForward,
          #  (Cptr, Cptr, Cptr, Cptr, Cptr, Cptr, Cptr, Csize_t),
          (Cptr, Cptr, Cptr, Cptr, Cptr, Cptr, Cptr, Csize_t),
          handle, dod.ptr, xtd.ptr, x.ptr, ytd.ptr, y.ptr, rs.ptr, rs_sz)

end

function main()
    x = CuArray(randn(Float32, 5, 4, 3, 2))
    y = similar(x)
    dropout = 0.5
    handle = cudnnhandle()
    rs = nothing

    dropoutDesc = DOD(Float32(dropout))
    xdesc = TD(x)
    ydesc = TD(y)
    x = x
    y = y

    reserveSpace = C_NULL
    reserveSizeInBytes = Cint(get_reserve_space_size(x))

    cudnnDropoutForward(handle, dropoutDesc.ptr, xdesc, x.ptr, ydesc, y.ptr, reserveSpace,
                        reserveSizeInBytes)
end


## CooperativeIDM from AutonomousMerging developed by Maxime originally

"""
    CooperativeIDM <: DriverModel{LaneFollowingAccel}
The cooperative IDM (c-IDM) is a rule based driver model for merging scenarios.
It controls the longitudinal actions of vehicles on the main lane.
A cooperation level `c` controls how the vehicles reacts to the merging vehicle.
When `c=0` the vehicle ignores the merging vehicle. When `c=1` the vehicle considers the merging
vehicle as its front car when TTC_ego > TTC_mergin_vehicle. When it is not considering the merging vehicle,
the car follows the IntelligentDriverModel.

# Fields
    - `env::MergingEnvironment = MergingEnvironment(main_lane_angle = 0.0, merge_lane_angle = pi/6)` the merging environment
    - `idm::IntelligentDriverModel = IntelligentDriverModel(v_des = env.main_lane_vmax, d_cmf = 2.0, d_max=2.0, T = 1.5, s_min = 2.0, a_max = 2.0)` the default IDM
    - `c::Float64 = 0.0` the cooperation level
"""
@with_kw mutable struct CooperativeIDM <: DriverModel{LaneFollowingAccel}
    env = MergingEnvironment(merge_point = VecSE2(1064.5227,959.1559,-2.8938))
    idm::IntelligentDriverModel = IntelligentDriverModel(v_des = 15., d_cmf = 2.0, d_max=2.0,
                                                         T = 1.5,s_min = 2.0,a_max = 2.0)
    c::Float64 = 0.0 # cooperation level
    # internals
    a::Float64 = 0.0
    a_merge::Float64 = 0.0
    a_idm::Float64 = 0.0
    other_acc::Float64 = 0.0
    s_des::Float64 = idm.s_min
    dist_at_merge::Float64 = 0.0
    ego_ttm::Float64 = 0.0
    veh_ttm::Float64 = 0.0
    front_car::Bool = false
    consider_merge::Bool = false
end

function AutomotiveSimulator.reset_hidden_state!(model::CooperativeIDM)
    reset_hidden_state!(model.idm)
    model.a = 0.0
    model.a_merge = 0.0
    model.a_idm = 0.0
    model.other_acc = 0.0
    model.s_des = model.idm.s_min
    model.dist_at_merge = 0.0
end

function AutomotiveSimulator.set_desired_speed!(model::CooperativeIDM, vdes::Float64)
    set_desired_speed!(model.idm, vdes)
end

function AutomotiveSimulator.observe!(model::CooperativeIDM, scene::Scene, roadway::Roadway, egoid::Int64)
    ego_ind = findfirst(egoid, scene)
    #print("---ego_id = $(egoid) \n")
    ego = scene[ego_ind]
    # fore = get_neighbor_fore_along_lane(scene, ego_ind, roadway, VehicleTargetPointFront(), VehicleTargetPointRear(), VehicleTargetPointFront())
    # if fore.ind == nothing # uses the first vehicle on the lain as neighbor

    #     vehmin, vehind = findfirst_lane(scene, main_lane(model.env))
    #     headway = get_end(main_lane(model.env)) - ego.state.posF.s + vehmin.state.posF.s
    #     track_longitudinal!(model.idm, ego.state.v, vehmin.state.v, headway)
    #     print("Nobody in front\n")
    # else
    #     # @printf("ID: %d | neighbor %d \n", egoid, scene[fore.ind].id)
    #     print("Front veh id = $(scene[fore.ind].id)\n")
    #     observe!(model.idm, scene, roadway, egoid)
    # end

    observe!(model.idm,scene,roadway,egoid)
    a_idm = model.idm.a
    model.a_idm = a_idm
    veh = find_merge_vehicle(model.env, scene,ego)
    if veh == nothing
        #println("No merge vehicle")
        model.a = model.a_idm
    else
        #print("Ahaaa: There is a merge veh bros. merge veh id =$(veh.id)\n")
        model.other_acc = 0.0
        model.a = 0.0
        model.a
        ego_ttm = time_to_merge(model.env, ego, model.a)
        veh_ttm = time_to_merge(model.env, veh, model.other_acc)
        model.ego_ttm = ego_ttm
        model.veh_ttm = veh_ttm
        if ( ego_ttm < 0.0 || ego_ttm < veh_ttm || veh_ttm == Inf)
            #print("Ego TTM < Merge TTM, ignoring\n")
            ego_ttm < veh_ttm
            model.a = model.a_idm
            model.consider_merge = false
            model.front_car = false
        else
            model.consider_merge = true
            if veh_ttm < model.c*ego_ttm
                model.front_car = true
                headway = distance_projection(model.env, veh) - distance_projection(model.env, ego)
                headway -= veh.def.length
                v_oth = veh.state.v
                v_ego = ego.state.v
                # @show "tracking front car"
                track_longitudinal!(model.idm, v_ego, v_oth, headway)
                model.a_merge = model.idm.a
                model.a = min(model.a_merge, model.a_idm)
            else
                model.a = model.a_idm
                model.front_car = false
            end
        end
    end
    #print("cidm observe says: I'm done observing\n")
    return model
end

Base.rand(rng::AbstractRNG,model::CooperativeIDM) = LaneFollowingAccel(model.a)

function find_merge_vehicle(env,scene::Scene,ego_veh)
    ego_lane = get_lane(env.roadway,ego_veh)
    ego_lane_tag = ego_lane.tag # Let's use tag as I think it'll be faster to compare equality than entire lane
    ego_ttm = time_to_merge(env,ego_veh,0.)

    # Based on upper vs lower environment, decide the specific lanes to use
    merge_lane_tag = LaneTag(0,0)
    if ego_lane_tag == main_lane_tag(env)
        merge_lane_tag = merging_lane_tag(env)
    elseif ego_lane_tag == merging_lane_tag(env)
        merge_lane_tag = main_lane_tag(env)
    end

    # merge_lane_tag = LaneTag(0,0)
    #     # Depending on the ego vehicle lane, decide whether merge lane is a or b1
    # if ego_lane_tag == LaneTag(1,1) # Check if ego veh is on lane a
    #     merge_lane_tag = LaneTag(1,2)
    # elseif ego_lane_tag == LaneTag(1,2)
    #     merge_lane_tag = LaneTag(1,1)
    # end

    diff_ttm = 10000
    merge_veh = nothing
    for veh in scene
        lane = get_lane(env.roadway,veh)
        if lane.tag == merge_lane_tag
            veh_ttm = time_to_merge(env,veh,0.)
            diff_ttm_temp = abs(veh_ttm-ego_ttm)
            #print("merger vehicle id = $(veh.id)\n")
            if diff_ttm_temp < diff_ttm
                diff_ttm = diff_ttm_temp
                merge_veh = veh
            end
        end
    end
    return merge_veh
end

"""
    findfirst_lane(scene::Scene, lane::Lane)
find the first vehicle on the lane (in terms of longitudinal position)
"""
function findfirst_lane(scene::Scene, lane::Lane)
    s_min = Inf
    vehmin = nothing
    vehind = nothing
    for (i, veh) in enumerate(scene)
        if veh.state.posF.roadind.tag == lane.tag && veh.state.posF.s < s_min
            s_min = veh.state.posF.s
            vehmin = veh
            vehind = i
        end
    end
    return vehmin, vehind
end

"""
    function get_end(lane::Lane)
- Gets the distance along lane of the last curve point on the lane
"""
function get_end(lane::Lane)
    return lane.curve[end].s
end

"""
    time_to_merge(env::MergingEnvironment, veh::Vehicle, a::Float64 = 0.0)
return the time to reach the merge point using constant acceleration prediction.
If the acceleration, `a` is not specified, it performs a constant velocity prediction.
"""
function time_to_merge(env, veh::Entity, a::Float64 = 0.0)
    d = -dist_to_merge(env, veh)
    v = veh.state.v
    t = Inf
    if isapprox(a, 0)
        t =  d/veh.state.v
    else
        delta = v^2 + 2.0*a*d
        if delta < 0.0
            t = Inf
        else
            t = (-v + sqrt(delta)) / a
        end
        if t < 0.0
            t = Inf
        end
    end
    return t
end

"""
    dist_to_merge(env::MergingEnvironment, veh::Vehicle)
returns the distance to the merge point.
"""
function dist_to_merge(env, veh::Entity)
    lane = get_lane(env.roadway, veh)
    dist = veh.state.posF.s - get_end(lane)
    return dist
end

"""
    distance_projection(env::MergingEnvironment, veh::Vehicle)
Performs a projection of `veh` onto the main lane. It returns the longitudinal position of the projection of `veh` on the main lane.
The projection is computing by conserving the distance to the merge point.
"""
function distance_projection(env, veh::Entity)
    if get_lane(env.roadway, veh) == main_lane(env)
        return veh.state.posF.s
    else
        dm = -dist_to_merge(env, veh)
        return env.roadway[env.merge_index].s - dm
    end
end


let
    S = CBSEnv.State
    A = CBSEnv.Action
    P = PathNode{S,A}
    vtx_grid = initialize_dense_vtx_grid(4,4)
    # 1  5   9  13
    # 2  6  10  14
    # 3  7  11  15
    # 4  8  12  16
    G = initialize_grid_graph_from_vtx_grid(vtx_grid)
    starts = [CBSEnv.State(1,0),CBSEnv.State(2,0)]
    goals = [CBSEnv.State(vtx=5),CBSEnv.State(vtx=6)]
    heuristic = PerfectHeuristic(G,map(s->s.vtx,starts),map(s->s.vtx,goals))
    env = CBSEnv.LowLevelEnv(graph=G,heuristic=heuristic)
    mapf = MAPF(env,starts,goals)
    let
        root_node = initialize_root_node(mapf)
        paths = [
            Path([P(S(1,1),A(Edge(1,2),2),S(2,3))]),
            Path([P(S(2,1),A(Edge(2,1),2),S(1,3))]),
        ]
        table = detect_conflicts(paths)
        @test CRCBS.is_action_conflict(get_next_conflict(table))
        set_solution_path!(root_node.solution,paths[1],1)
        set_solution_path!(root_node.solution,paths[2],2)
        detect_conflicts!(root_node)
        @test count_conflicts(root_node) == 1
        @test count_conflicts(root_node,1,2) == count_conflicts(root_node,2,1)
        @test count_conflicts(root_node,[1,2],[1,2]) == 1
        c = get_next_conflict(root_node)
        constraints = generate_constraints_from_conflict(c)
        @test length(constraints) == 2
        for c in constraints
            @test is_action_constraint(c)
        end
        add_constraint!(env,root_node,constraints[1])
    end
    let
        root_node = initialize_root_node(mapf)
        paths = [
            Path([P(S(1,0),A(Edge(1,2),0),S(2,1))]),
            Path([P(S(2,0),A(Edge(2,2),0),S(2,1))]),
        ]
        set_solution_path!(root_node.solution,paths[1],1)
        set_solution_path!(root_node.solution,paths[2],2)
        child1 = initialize_child_search_node(root_node)
        child2 = initialize_child_search_node(root_node)
        detect_conflicts!(child1,[1])
        @test count_conflicts(child1) == 1
        @test count_conflicts(child1,1,2) == 1
        @test count_conflicts(child1,2,1) == 1
        @test count_conflicts(child1,[1,2],[1,2]) == 1

        c = get_next_conflict(child1)
        constraints = generate_constraints_from_conflict(c)
        @test length(constraints) == 2
        for c in constraints
            @test is_state_constraint(c)
        end
        add_constraint!(env,child1,constraints[1])

        @test count_conflicts(child2) == 0
        @test count_conflicts(root_node) == 0

        @test convert_to_vertex_lists(
            get_paths(child1.solution)[1]
            ) == convert_to_vertex_lists(
            get_paths(root_node.solution)[1]
            )

        set_solution_path!(child1.solution,Path([P()]),1)
        @test convert_to_vertex_lists(
            get_paths(child1.solution)[1]
            ) != convert_to_vertex_lists(
            get_paths(root_node.solution)[1]
            )

        detect_conflicts!(child1,[1])
        @test count_conflicts(child1) == 0
    end
end
let
    S = CBSEnv.State
    A = CBSEnv.Action
    P = PathNode{S,A}

    vtx_grid = initialize_dense_vtx_grid(4,4)
    # 1  5   9  13
    # 2  6  10  14
    # 3  7  11  15
    # 4  8  12  16
    G = initialize_grid_graph_from_vtx_grid(vtx_grid)
    env = CBSEnv.LowLevelEnv(graph=G,agent_idx=1)
    isa(state_space_trait(env),DiscreteSpace)
    let
        for v in vertices(G)
            s = S(v,1)
            idx,t = serialize(env,s,s.t)
            sp, _ = deserialize(env,S(),idx,t)
            @test get_vtx(s) == get_vtx(sp)
        end
        for e in edges(G)
            a = A(e,1)
            idx,t = serialize(env,a,1)
            ap, _ = deserialize(env,A(),idx,t)
            @test get_e(a) == get_e(ap)
        end
    end
    let
        for constraints in [
            discrete_constraint_table(env,1),
            # ConstraintTable{P}(agent_id = 1)
            ]
            # add a constraint whose agent id does not match (should throw an error)
            s = S(1,1)
            a = A(Edge(1,2),1)
            sp = S(2,2)
            c = state_constraint(get_agent_id(constraints)+1,P(),2)
            @test_throws AssertionError add_constraint!(env,constraints,c) # wrong agent id
            @test_throws AssertionError CRCBS.has_constraint(env,constraints,c) # wrong agent id
            # add a constraint whose agent id DOES match
            c = state_constraint(get_agent_id(constraints),P(s,a,sp),2)
            add_constraint!(env,constraints,c)
            @test CRCBS.has_constraint(env,constraints,c)
            remove_constraint!(env,constraints,c)
            @test !CRCBS.has_constraint(env,constraints,c)
            add_constraint!(env,constraints,c)
            sc,ac = CRCBS.search_constraints(env,constraints,get_path_node(c))
            @test length(sc) == 1
            @test length(ac) == 0
            # # add a constraint whose agent id DOES match
            c = action_constraint(get_agent_id(constraints),P(s,a,sp),2)
            add_constraint!(env,constraints,c)
            @test CRCBS.has_constraint(env,constraints,c)
            remove_constraint!(env,constraints,c)
            @test !CRCBS.has_constraint(env,constraints,c)
            add_constraint!(env,constraints,c)
            sc,ac = CRCBS.search_constraints(env,constraints,get_path_node(c))
            @test length(sc) == 1
            @test length(ac) == 1
            # # query sorted constraints
            c = sorted_state_constraints(env,constraints)[1]
            @test get_vtx(get_sp(c)) == get_vtx(sp)
            c = sorted_action_constraints(env,constraints)[1]
            @test get_e(get_a(c)) == get_e(a)
            # Test interval constraints
            c = state_constraint(get_agent_id(constraints),P(s,a,sp),3,4)
            add_constraint!(env,constraints,c)
            for t in 3:4
                @test CRCBS.has_constraint(env,constraints,state_constraint(get_agent_id(constraints),P(s,a,sp),t))
            end
            remove_constraint!(env,constraints,c)
            for t in 3:4
                @test !CRCBS.has_constraint(env,constraints,state_constraint(get_agent_id(constraints),P(s,a,sp),t))
            end

        end
    end
end


export iauAtciqn
"""
Quick ICRS, epoch J2000.0, to CIRS transformation, given precomputed
star-independent astrometry parameters plus a list of light-
deflecting bodies.

Use of this function is appropriate when efficiency is important and
where many star positions are to be transformed for one date.  The
star-independent parameters can be obtained by calling one of the
functions iauApci[13], iauApcg[13], iauApco[13] or iauApcs[13].

If the only light-deflecting body to be taken into account is the
Sun, the iauAtciq function can be used instead.  If in addition the
parallax and proper motions are zero, the iauAtciqz function can be
used.

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

Status:  support function.

Given:
   rc,dc  double       ICRS RA,Dec at J2000.0 (radians)
   pr     double       RA proper motion (radians/year; Note 3)
   pd     double       Dec proper motion (radians/year)
   px     double       parallax (arcsec)
   rv     double       radial velocity (km/s, +ve if receding)
   astrom iauASTROM*   star-independent astrometry parameters:
      pmt    double       PM time interval (SSB, Julian years)
      eb     double[3]    SSB to observer (vector, au)
      eh     double[3]    Sun to observer (unit vector)
      em     double       distance from Sun to observer (au)
      v      double[3]    barycentric observer velocity (vector, c)
      bm1    double       sqrt(1-|v|^2): reciprocal of Lorenz factor
      bpn    double[3][3] bias-precession-nutation matrix
      along  double       longitude + s' (radians)
      xpl    double       polar motion xp wrt local meridian (radians)
      ypl    double       polar motion yp wrt local meridian (radians)
      sphi   double       sine of geodetic latitude
      cphi   double       cosine of geodetic latitude
      diurab double       magnitude of diurnal aberration vector
      eral   double       "local" Earth rotation angle (radians)
      refa   double       refraction constant A (radians)
      refb   double       refraction constant B (radians)
   n     int           number of bodies (Note 3)
   b     iauLDBODY[n] data for each of the n bodies (Notes 3,4):
      bm    double        mass of the body (solar masses, Note 5)
      dl    double        deflection limiter (Note 6)
      pv    [2][3]        barycentric PV of the body (au, au/day)

Returned:
   ri,di   double    CIRS RA,Dec (radians)

Notes:

   1. Star data for an epoch other than J2000.0 (for example from the
   Hipparcos catalog, which has an epoch of J1991.25) will require a
   preliminary call to iauPmsafe before use.

   2. The proper motion in RA is dRA/dt rather than cos(Dec)*dRA/dt.

   3. The struct b contains n entries, one for each body to be
   considered.  If n = 0, no gravitational light deflection will be
   applied, not even for the Sun.

   4. The struct b should include an entry for the Sun as well as for
   any planet or other body to be taken into account.  The entries
   should be in the order in which the light passes the body.

   5. In the entry in the b struct for body i, the mass parameter
   b[i].bm can, as required, be adjusted in order to allow for such
   effects as quadrupole field.

   6. The deflection limiter parameter b[i].dl is phi^2/2, where phi is
   the angular separation (in radians) between star and body at
   which limiting is applied.  As phi shrinks below the chosen
   threshold, the deflection is artificially reduced, reaching zero
   for phi = 0.   Example values suitable for a terrestrial
   observer, together with masses, are as follows:

      body i     b[i].bm        b[i].dl

      Sun        1.0            6e-6
      Jupiter    0.00095435     3e-9
      Saturn     0.00028574     3e-10

   7. For efficiency, validation of the contents of the b array is
   omitted.  The supplied masses must be greater than zero, the
   position and velocity vectors must be right, and the deflection
   limiter greater than zero.

Called:
   iauPmpx      proper motion and parallax
   iauLdn       light deflection by n bodies
   iauAb        stellar aberration
   iauRxp       product of r-matrix and pv-vector
   iauC2s       p-vector to spherical
   iauAnp       normalize angle into range 0 to 2pi

This revision:   2013 October 9

SOFA release 2018-01-30

Copyright (C) 2018 IAU SOFA Board.  See notes at end.
"""
# void iauAtciqn(double rc, double dc, double pr, double pd,
#                double px, double rv, iauASTROM *astrom,
#                int n, iauLDBODY b[], double *ri, double *di)

function iauAtciqn(rc::Real, dc::Real, pr::Real, pd::Real,
                   px::Real, rv::Real, astrom::iauASTROM,
                   n::Int, b::Array{iauLDBODY, 1})

   # Allocate return value
   ref_astrom = Ref{iauASTROM}(astrom)
   ref_ri     = Ref{Float64}(0.0)
   ref_di     = Ref{Float64}(0.0)
   ref_b      = Ref{iauLDBODY}(b[1])

   # for ld in b
   #    # ld.pv = ld.pv'
   #    ld.pv = ((ld.pv[1]))
   # end

   ccall((:iauAtciqn, libsofa_c), Cvoid,
            (Cdouble, Cdouble,
            Cdouble, Cdouble,
            Cdouble, Cdouble,
            Ref{iauASTROM}, Cint, Ref{iauLDBODY},
            Ref{Cdouble}, Ref{Cdouble}),
            convert(Float64, rc), convert(Float64, dc),
            convert(Float64, pr), convert(Float64, pd),
            convert(Float64, px), convert(Float64, rv),
            ref_astrom, convert(Int32, n), ref_b, ref_ri, ref_di)

   # for ld in b
   #    ld.pv = ld.pv'
   # end

   return ref_ri[], ref_di[]
end


export ForceField, System

# TODO list:
# 1. Implement Dissulfide bond assignment
# 2. Check why graph canonicalization isn't working for nucleic acids with CHARMM

import LightXML
import Chemfiles
using DataFrames
using OrderedCollections

struct ResidueTemplate
    atoms::Vector{Chemfiles.Atom}
    adjacency::BitArray{2}

    function ResidueTemplate(residue, type_masses)
        natoms = length(residue.atoms)
        atom_index = LittleDict(Chemfiles.name.(residue.atoms) .=> 1:natoms)
        adjmat = falses(natoms, natoms)
        for atom_pair in residue.bonds
            i, j = [atom_index[atom] for atom in atom_pair]
            adjmat[i, j] = adjmat[j, i] = true
        end
        atom_masses = [type_masses[Chemfiles.type(atom)] for atom in residue.atoms]
        order, adjmat = canonical_form(adjmat, atom_masses)
        return new(residue.atoms[order], adjmat)
    end
end

struct ForceField
    atom_types::DataFrame
    bond_types::DataFrame
    angle_types::DataFrame
    dihedral_types::DataFrame
    improper_types::DataFrame
    nonbonded::DataFrame
    templates::OrderedDict{String,ResidueTemplate}
    lj₁₋₄::Float64
    coulomb₁₋₄::Float64
end

const ATOM_TYPE = LittleDict(
    :name => String, :class => String, :element => String, :mass => Float64,
)

const HARMONIC_BOND = LittleDict(
    :type1  => String, :type2  => String,
    :class1 => String, :class2 => String,
    :length => Float64,
    :k => Float64,
)

const HARMONIC_ANGLE = LittleDict(
    :type1  => String, :type2  => String, :type3  => String,
    :class1 => String, :class2 => String, :class3 => String,
    :angle => Float64,
    :k => Float64,
)

const PERIODIC_TORSION = LittleDict(
    :type1  => String, :type2  => String, :type3  => String, :type4  => String,
    :class1 => String, :class2 => String, :class3 => String, :class4 => String,
    :periodicity1 => Int, :phase1 => Float64, :k1 => Float64,
    :periodicity2 => Int, :phase2 => Float64, :k2 => Float64,
    :periodicity3 => Int, :phase3 => Float64, :k3 => Float64,
    :periodicity4 => Int, :phase4 => Float64, :k4 => Float64,
    :periodicity5 => Int, :phase5 => Float64, :k5 => Float64,
    :periodicity6 => Int, :phase6 => Float64, :k6 => Float64,
)

const NONBONDED = LittleDict(
    :type => String, :charge => Float64, :sigma => Float64, :epsilon => Float64,
)

mutable struct Residue
    atoms
    bonds
    external_bonds
    Residue() = new([], [], [])
    Residue(atoms, bonds, external_bonds) = new(atoms, bonds, external_bonds)
end

Base.copy(r::Residue) = Residue(copy(r.atoms), copy(r.bonds), copy(r.external_bonds))

sanitized(str) = replace(replace(replace(str, "-" => "_"), "'" => "p"), "*" => "a")

function AddAtom!(residue, attributes)
    atom = Chemfiles.Atom(sanitized(attributes["name"]))
    charge = convert(Float64, get(attributes, "charge", 0))
    Chemfiles.set_charge!(atom, charge)
    Chemfiles.set_type!(atom, attributes["type"])
    push!(residue.atoms, atom)
end

function AddBond!(residue, attributes)
    push!(residue.bonds, Set(sanitized.(values(attributes))))
end

function AddExternalBond!(residue, attributes)
    push!(residue.external_bonds, sanitized(attributes["atomName"]))
end

function ChangeAtom!(residue, attributes)
    name = sanitized(attributes["name"])
    for atom in residue.atoms
        if Chemfiles.name(atom) == name
            charge = convert(Float64, get(attributes, "charge", 0))
            Chemfiles.set_charge!(atom, charge)
            Chemfiles.set_type!(atom, attributes["type"])
            return nothing
        end
    end
end

function RemoveAtom!(residue, attributes)
    atom = sanitized(attributes["name"])
    residue.atoms = filter(x -> Chemfiles.name(x) ≠ atom, residue.atoms)
end

function RemoveBond!(residue, attributes)
    bond = Set(sanitized.(attributes[a] for a in ["atomName1", "atomName2"]))
    residue.bonds = filter(x -> x ≠ bond, residue.bonds)
end

function RemoveExternalBond!(residue, attributes)
    atom = sanitized(attributes["atomName"])
    residue.external_bonds = filter(x -> x ≠ atom, residue.external_bonds)
end

Base.convert(::Type{T}, x::S) where {T<:Number,S<:AbstractString} = parse(T, x)

Base.zero(::Type{String}) = ""

function DataFrame(category, element_list, key)
    df = DataFrame((collect ∘ values)(category), (collect ∘ keys)(category))
    zeros = LittleDict(string(key) => zero(value) for (key, value) in category)
    attribute_dicts(list, key) = [LightXML.attributes_dict(a) for e in list for a in e[key]]
    for dict in attribute_dicts(element_list, key)
        append!(df, merge(zeros, dict))
    end
    # TODO: remove all empty columns
    return df
end

function ForceField(xml_file)
    xroot = LightXML.root(LightXML.parse_file(xml_file))
    patches = LittleDict(
        LightXML.attribute(item, "name") => [
            Symbol(LightXML.name(child)*"!") => LightXML.attributes_dict(child)
            for child in LightXML.child_elements(item)
        ]
        for elem in xroot["Patches"] for item in elem["Patch"]
    )
    atom_types = DataFrame(ATOM_TYPE, xroot["AtomTypes"], "Type")
    type_index = LittleDict(atom_types.name .=> collect(1:nrow(atom_types)))
    type_masses = LittleDict(atom_types.name .=> atom_types.mass)
    templates = OrderedDict{String,ResidueTemplate}()
    for elem in xroot["Residues"]
        for (residue_index, residue_item) in enumerate(elem["Residue"])
            residue = Residue()
            names = []
            for (index, atom_item) in enumerate(residue_item["Atom"])
                attributes = LightXML.attributes_dict(atom_item)
                push!(names, attributes["name"])
                AddAtom!(residue, attributes)
            end
            for bond in residue_item["Bond"]
                atom_names = [
                    key ∈ ["to", "from"] ? names[parse(Int, value)+1] : value
                    for (key, value) in LightXML.attributes_dict(bond)
                ]
                AddBond!(residue, Dict(["atomName1", "atomName2"] .=> atom_names))
            end
            for bond in residue_item["ExternalBond"]
                attributes = LightXML.attributes_dict(bond)
                if haskey(attributes, "from")
                    attributes["atomName"] = names[parse(Int, attributes["from"])+1]
                end
                AddExternalBond!(residue, attributes)
            end
            residue_name = LightXML.attribute(residue_item, "name")
            templates[residue_name] = ResidueTemplate(residue, type_masses)
            for item in residue_item["AllowPatch"]
                patch = LightXML.attribute(item, "name")
                patched_residue = copy(residue)
                for (action, attributes) in patches[patch]
                    getfield(@__MODULE__, action)(patched_residue, attributes)
                end
                templates["$residue_name($patch)"] = ResidueTemplate(patched_residue, type_masses)
            end
        end
    end
    bonds = DataFrame(HARMONIC_BOND, xroot["HarmonicBondForce"], "Bond")
    angles = DataFrame(HARMONIC_ANGLE, xroot["HarmonicAngleForce"], "Angle")
    dihedrals = DataFrame(PERIODIC_TORSION, xroot["PeriodicTorsionForce"], "Proper")
    impropers = DataFrame(PERIODIC_TORSION, xroot["PeriodicTorsionForce"], "Improper")
    nonbonded = DataFrame(NONBONDED, xroot["NonbondedForce"], "Atom")
    scaling_factors = LightXML.attributes_dict(xroot["NonbondedForce"][1])
    lj₁₋₄ = get(scaling_factors, "lj14scale", 1.0)
    coulomb₁₋₄ = get(scaling_factors, "coulomb14scale", 1.0)
    return ForceField(atom_types, bonds, angles, dihedrals, impropers,
                      nonbonded, templates, lj₁₋₄, coulomb₁₋₄)
end

function pdb_aliases(pdb_aliases_file)
    name(element) = LightXML.attribute(element, "name")
    mass(element) = parse(Float64, LightXML.attribute(element, "mass"))
    regex(item) = Regex(LightXML.attribute(item, "code"))
    ids(item) = parse.(Int, values(LightXML.attributes_dict(item)))
    xroot = LightXML.root(LightXML.parse_file(pdb_aliases_file))
    masses = LittleDict(name(e) => mass(e) for e in xroot["Elements"][1]["Element"])
    regex_codes = regex.(xroot["RegularExpressions"][1]["Regex"])
    std_bonds = LittleDict(name(e) => ids.(e["Bond"]) for e in xroot["Residue"])
    return masses, regex_codes, std_bonds
end

const PDB_MASSES, PDB_REGEX_CODES, PDB_STD_BONDS =
    pdb_aliases(joinpath(@__DIR__, "data", "pdb_aliases.xml"))

if isdefined(Chemfiles, :atoms)
    Chemfiles_atoms(residue::Chemfiles.Residue) = Chemfiles.atoms(residue)
else
    function Chemfiles_atoms(residue::Chemfiles.Residue)
        count = size(residue)
        result = Array{UInt64}(undef, count)
        Chemfiles.__check(Chemfiles.lib.chfl_residue_atoms(
            Chemfiles.__const_ptr(residue),
            pointer(result),
            count,
        ))
        return result
    end
end

function System(file, force_field; disambiguation=Dict())
    trajectory = Chemfiles.Trajectory(file)
    frame = read(trajectory)
    unit_cell = Chemfiles.UnitCell(frame)
    # cell_lenghts = Chemfiles.lengths(unit_cell)
    # cell_angles = Chemfiles.angles(unit_cell)
    positions = Chemfiles.positions(frame)
    has_velocities = Chemfiles.has_velocities(frame)
    velocities = has_velocities ? Chemfiles.velocities(frame) : zeros(size(positions))
    topology = Chemfiles.Topology(frame)

    num_atoms = size(topology)
    num_residues = Chemfiles.count_residues(topology)
    atoms = [Chemfiles.Atom(topology, index) for index = 0:num_atoms-1]
    Chemfiles.set_name!.(atoms, (sanitized∘Chemfiles.name).(atoms))
    residues = [Chemfiles.Residue(topology, index) for index = 0:num_residues-1]
    residue_atoms = map((x -> x .+ 1) ∘ Chemfiles_atoms, residues)
    atom_residues = Vector{Int}(undef, num_atoms)
    internal_map = Vector{Int}(undef, num_atoms)
    for (index, atoms_list) in enumerate(residue_atoms)
        atom_residues[atoms_list] .= index
        internal_map[atoms_list] .= collect(1:length(atoms_list))
    end

    is_std_pdb = Chemfiles.property.(residues, "is_standard_pdb")
    for (atom, residue_index) in zip(atoms, atom_residues)
        if is_std_pdb[residue_index]
            element = match(r"[HCNOPS]", Chemfiles.type(atom)).match
            Chemfiles.set_mass!(atom, PDB_MASSES[element])
        end
    end

    bonds = [
        collect(bond)
        for bond in eachcol(Chemfiles.bonds(topology) .+ 1)
        if !all(is_std_pdb[atom_residues[bond]])
    ]
    chain_id = ""
    previous_indices = previous_names = []
    for index = 1:num_residues
        residue = residues[index]
        atom_indices = residue_atoms[index]
        atom_names = Chemfiles.name.(atoms[atom_indices])
        residue_name = Chemfiles.name(residue)
        if is_std_pdb[index]
            new_chain = Chemfiles.property(residue, "chainid") != chain_id
            if new_chain
                chain_id = Chemfiles.property(residue, "chainid")
                previous_indices = previous_names = []
            end
            combined_indices = vcat(previous_indices, atom_indices)
            combined_names = vcat(previous_names, atom_names)
            for (atom_1, atom_2) in PDB_STD_BONDS[residue_name]
                from = broadcast(occursin, PDB_REGEX_CODES[atom_1], combined_names) |> findfirst
                to = broadcast(occursin, PDB_REGEX_CODES[atom_2], combined_names) |> findfirst
                from === nothing || to === nothing || push!(bonds, combined_indices[[from, to]])
            end
            previous_indices = atom_indices
            previous_names = "_" .* atom_names
        end
    end

    adjacency_matrices = map(n -> falses(n, n), length.(residue_atoms))
    for (atom_1, atom_2) in bonds
        index = atom_residues[atom_1]
        if atom_residues[atom_2] == index
            i, j = internal_map[[atom_1, atom_2]]
            adjacency_matrices[index][i, j] = adjacency_matrices[index][j, i] = true
        end
    end

    for resid = 1:num_residues
        residue = residues[resid]
        indices = residue_atoms[resid]
        matrix = adjacency_matrices[resid]
        atom_masses = Chemfiles.mass.(atoms[indices])
        canonical_order, canonical_matrix = canonical_form(matrix, atom_masses)
        matches = [n for (n, t) in force_field.templates if t.adjacency == canonical_matrix]
        name = Chemfiles.name(residue)
        length(matches) == 0 &&
            error("No force field templates matched residue $resid ($(name))")
        if length(matches) > 1
            haskey(disambiguation, resid) ||
                error("Multiple force field templates $(matches) matched residue $resid ($(name))")
            disambiguation[resid] in matches ||
                error("Provided disambiguation for residue $resid ($(name)) is not in $(matches)")
            matches = [disambiguation[resid]]
        end
        template_atoms = force_field.templates[matches[1]].atoms
        for (i, a) in zip(indices[canonical_order], template_atoms)
            Chemfiles.set_property!(atoms[i], "ff.type", Chemfiles.type(a))
            Chemfiles.set_property!(atoms[i], "ff.charge", Chemfiles.charge(a))
        end
    end

    system = Chemfiles.Frame()
    Chemfiles.set_cell!(system, unit_cell)
    has_velocities && Chemfiles.add_velocities!(system)
    location = Vector{Int}(undef, num_atoms)
    atom_index = 0
    for (index, residue) in enumerate(residues)
        new_residue = Chemfiles.Residue(Chemfiles.name(residue), index)
        for i in residue_atoms[index]
            Chemfiles.add_atom!(system, atoms[i], positions[:, i], velocities[:, i])
            Chemfiles.add_atom!(new_residue, atom_index)
            location[i] = atom_index
            atom_index += 1
        end
        Chemfiles.add_residue!(system, new_residue)
    end
    for bond in bonds
        Chemfiles.add_bond!(system, location[bond]...)
    end
    return system
end


# LPS25H pressure/temperature sensor
# See http://www.st.com/resource/en/datasheet/lps25h.pdf

"""
    LPS25H_pressure()

The atmospheric pressure (in millibars) from the LPS25H sensor.
"""
function LPS25H_pressure()
    setaddr(LPS25H_ADDRESS)
    CTRL_REG1 = 0x20
    CTRL_REG2 = 0x21

    smbus_write(CTRL_REG1, 0x00) # power down
    smbus_write(CTRL_REG1, 0x84) # power on, block update
    smbus_write(CTRL_REG2, 0x01) # one-shot aquisition

    while true
        sleep(0.025)
        smbus_read(CTRL_REG2) == 0 && break # check if finished
    end

    pressure = (UInt64.(smbus_read(0x2a)) << 16 |
                UInt64.(smbus_read(0x29)) << 8 |
                UInt64.(smbus_read(0x28)))/4096.0

    smbus_write(CTRL_REG1, 0x00) # power down
    return pressure
end

"""
    LPS25H_temperature()

The temperature (in °C) from the LPS25H sensor.
"""
function LPS25H_temperature()
    setaddr(LPS25H_ADDRESS)
    CTRL_REG1 = 0x20
    CTRL_REG2 = 0x21

    smbus_write(CTRL_REG1, 0x00) # power down
    smbus_write(CTRL_REG1, 0x84) # power on, block update
    smbus_write(CTRL_REG2, 0x01) # one-shot aquisition

    while true
        sleep(0.025)
        smbus_read(CTRL_REG2) == 0 && break # check if finished
    end

    temp = 42.5 + (Int16(smbus_read(0x2c)) << 8 | smbus_read(0x2b)) / 480

    smbus_write(CTRL_REG1, 0x00) # power down
    return temp
end


function LineAxis(parent::Scene; kwargs...)

    attrs = merge!(Attributes(kwargs), default_attributes(LineAxis))

    decorations = Dict{Symbol, Any}()

    @extract attrs (endpoints, limits, flipped, ticksize, tickwidth,
        tickcolor, tickalign, ticks, ticklabelalign, ticklabelrotation, ticksvisible,
        ticklabelspace, ticklabelpad, labelpadding,
        ticklabelsize, ticklabelsvisible, spinewidth, spinecolor, label, labelsize, labelcolor,
        labelfont, ticklabelfont,
        labelvisible, spinevisible, trimspine, flip_vertical_label)

    pos_extents_horizontal = lift(endpoints) do endpoints
        if endpoints[1][2] == endpoints[2][2]
            horizontal = true
            extents = (endpoints[1][1], endpoints[2][1])
            position = endpoints[1][2]
            return (position, extents, horizontal)
        elseif endpoints[1][1] == endpoints[2][1]
            horizontal = false
            extents = (endpoints[1][2], endpoints[2][2])
            position = endpoints[1][1]
            return (position, extents, horizontal)
        else
            error("Axis endpoints $(endpoints[1]) and $(endpoints[2]) are neither on a horizontal nor vertical line")
        end
    end

    ticksnode = Node(Point2f0[])
    ticklines = linesegments!(
        parent, ticksnode, linewidth = tickwidth, color = tickcolor,
        show_axis = false, visible = ticksvisible
    )[end]
    decorations[:ticklines] = ticklines

    ticklabelannosnode = Node{Vector{Tuple{String, Point2f0}}}([("temp", Point2f0(0, 0))])
    ticklabels = annotations!(
        parent,
        ticklabelannosnode,
        align = ticklabelalign,
        rotation = ticklabelrotation,
        textsize = ticklabelsize,
        font = ticklabelfont,
        show_axis = false,
        visible = ticklabelsvisible)[end]

    ticklabel_ideal_space = lift(ticklabelannosnode, ticklabelalign, ticklabelrotation, ticklabelfont, ticklabelsvisible, typ=Float32) do args...
        maxwidth = if pos_extents_horizontal[][3]
                # height
                ticklabelsvisible[] ? height(FRect2D(boundingbox(ticklabels))) : 0f0
            else
                # width
                ticklabelsvisible[] ? width(FRect2D(boundingbox(ticklabels))) : 0f0
        end
    end

    attrs[:actual_ticklabelspace] = 0f0
    actual_ticklabelspace = attrs[:actual_ticklabelspace]

    onany(ticklabel_ideal_space, ticklabelspace) do idealspace, space
        s = if space == AbstractPlotting.automatic
            idealspace
        else
            space
        end
        if s != actual_ticklabelspace[]
            actual_ticklabelspace[] = s
        end
    end

    decorations[:ticklabels] = ticklabels

    tickspace = lift(ticksvisible, ticksize, tickalign) do ticksvisible,
            ticksize, tickalign

        ticksvisible ? max(0f0, ticksize * (1f0 - tickalign)) : 0f0
    end

    labelgap = lift(spinewidth, tickspace, ticklabelsvisible, actual_ticklabelspace,
        ticklabelpad, labelpadding) do spinewidth, tickspace, ticklabelsvisible,
            actual_ticklabelspace, ticklabelpad, labelpadding

        spinewidth + tickspace +
            (ticklabelsvisible ? actual_ticklabelspace + ticklabelpad : 0f0) +
            labelpadding
    end

    labelpos = lift(pos_extents_horizontal, flipped, labelgap) do (position, extents, horizontal), flipped, labelgap

        # fullgap = tickspace[] + labelgap

        middle = extents[1] + 0.5f0 * (extents[2] - extents[1])

        x_or_y = flipped ? position + labelgap : position - labelgap

        if horizontal
            Point2(middle, x_or_y)
        else
            Point2(x_or_y, middle)
        end
    end

    labelalign = lift(pos_extents_horizontal, flipped, flip_vertical_label) do (position, extents, horizontal), flipped, flip_vertical_label
        if horizontal
            (:center, flipped ? :bottom : :top)
        else
            (:center, if flipped
                    flip_vertical_label ? :bottom : :top
                else
                    flip_vertical_label ? :top : :bottom
                end
            )
        end
    end

    labelrotation = lift(pos_extents_horizontal, flip_vertical_label) do (position, extents, horizontal), flip_vertical_label
        if horizontal
            0f0
        else
            if flip_vertical_label
                Float32(-0.5pi)
            else
                Float32(0.5pi)
            end
        end
    end

    labeltext = text!(
        parent, label, textsize = labelsize, color = labelcolor,
        position = labelpos, show_axis = false, visible = labelvisible,
        align = labelalign, rotation = labelrotation, font = labelfont,
    )[end]

    decorations[:labeltext] = labeltext

    tickvalues = Node(Float32[])

    tickvalues = lift(pos_extents_horizontal, limits, ticks) do (position, extents, horizontal),
            limits, ticks
        px_width = extents[2] - extents[1]
        compute_tick_values(ticks, limits..., px_width)
    end

    tickpositions = Node(Point2f0[])
    tickstrings = Node(String[])

    onany(tickvalues) do tickvalues
        # limoy = limits[].origin[2]
        # limh = limits[].widths[2]
        # px_ox = pixelarea(scene)[].origin[1]
        # px_oy = pixelarea(scene)[].origin[2]
        # px_w = pixelarea(scene)[].widths[1]
        # px_h = pixelarea(scene)[].widths[2]

        position, extents, horizontal = pos_extents_horizontal[]

        px_o = extents[1]
        px_width = extents[2] - extents[1]

        lim_o = limits[][1]
        lim_w = limits[][2] - limits[][1]

        tick_fractions = (tickvalues .- lim_o) ./ lim_w
        tick_scenecoords = px_o .+ px_width .* tick_fractions

        tickpos = if horizontal
            [Point(x, position) for x in tick_scenecoords]
        else
            [Point(position, y) for y in tick_scenecoords]
        end

        # now trigger updates
        tickpositions[] = tickpos

        tickstrings[] = get_tick_labels(ticks[], tickvalues)
    end

    onany(tickstrings, labelgap, flipped) do tickstrings, labelgap, flipped
        # tickspace is always updated before labelgap
        # tickpositions are always updated before tickstrings
        # so we don't need to lift those

        position, extents, horizontal = pos_extents_horizontal[]

        nticks = length(tickvalues[])

        ticklabelgap = spinewidth[] + tickspace[] + ticklabelpad[]

        shift = if horizontal
            Point2f0(0f0, flipped ? ticklabelgap : -ticklabelgap)
        else
            Point2f0(flipped ? ticklabelgap : -ticklabelgap, 0f0)
        end

        ticklabelpositions = tickpositions[] .+ Ref(shift)
        ticklabelannosnode[] = collect(zip(tickstrings, ticklabelpositions))
    end

    onany(tickpositions, tickalign, ticksize, spinewidth) do tickpositions,
            tickalign, ticksize, spinewidth

        position, extents, horizontal = pos_extents_horizontal[]

        if horizontal
            tickstarts = [tp + (flipped[] ? -1f0 : 1f0) * Point2f0(0f0, tickalign * ticksize - 0.5f0 * spinewidth) for tp in tickpositions]
            tickends = [t + (flipped[] ? -1f0 : 1f0) * Point2f0(0f0, -ticksize) for t in tickstarts]
            ticksnode[] = interleave_vectors(tickstarts, tickends)
        else
            tickstarts = [tp + (flipped[] ? -1f0 : 1f0) * Point2f0(tickalign * ticksize - 0.5f0 * spinewidth, 0f0) for tp in tickpositions]
            tickends = [t + (flipped[] ? -1f0 : 1f0) * Point2f0(-ticksize, 0f0) for t in tickstarts]
            ticksnode[] = interleave_vectors(tickstarts, tickends)
        end
    end

    linepoints = lift(pos_extents_horizontal, flipped, spinewidth, trimspine, tickpositions, tickwidth) do (position, extents, horizontal),
            flipped, sw, trimspine, tickpositions, tickwidth

        if !trimspine
            if horizontal
                y = position + (flipped ? 1f0 : -1f0) * 0.5f0 * sw
                p1 = Point2f0(extents[1] - sw, y)
                p2 = Point2(extents[2] + sw, y)
                [p1, p2]
            else
                x = position + (flipped ? 1f0 : -1f0) * 0.5f0 * sw
                p1 = Point2f0(x, extents[1] - sw)
                p2 = Point2f0(x, extents[2] + sw)
                [p1, p2]
            end
        else
            [tickpositions[1], tickpositions[end]] .+ [
                (horizontal ? Point2f0(-0.5f0 * tickwidth, 0) : Point2f0(0, -0.5f0 * tickwidth)),
                (horizontal ? Point2f0(0.5f0 * tickwidth, 0) : Point2f0(0, 0.5f0 * tickwidth)),
            ]
        end
    end

    decorations[:axisline] = lines!(parent, linepoints, linewidth = spinewidth, visible = spinevisible,
        color = spinecolor, raw = true)[end]

    protrusion = lift(ticksvisible, label, labelvisible, labelpadding, labelsize, tickalign, spinewidth,
            tickspace, ticklabelsvisible, actual_ticklabelspace, ticklabelpad, labelfont, ticklabelfont) do ticksvisible,
            label, labelvisible, labelpadding, labelsize, tickalign, spinewidth, tickspace, ticklabelsvisible,
            actual_ticklabelspace, ticklabelpad, labelfont, ticklabelfont

        position, extents, horizontal = pos_extents_horizontal[]

        real_labelsize = if iswhitespace(label)
            0f0
        else
            horizontal ? boundingbox(labeltext).widths[2] : boundingbox(labeltext).widths[1]
        end

        labelspace = (labelvisible && !iswhitespace(label)) ? real_labelsize + labelpadding : 0f0
        spinespace = spinewidth
        # tickspace = ticksvisible ? max(0f0, xticksize * (1f0 - xtickalign)) : 0f0
        ticklabelgap = ticklabelsvisible ? actual_ticklabelspace + ticklabelpad : 0f0

        together = spinespace + tickspace + ticklabelgap + labelspace
    end

    # extents[] = extents[] # trigger

    LineAxis(parent, protrusion, attrs, decorations, tickpositions, tickvalues, tickstrings)
end

function tight_ticklabel_spacing!(la::LineAxis)

    horizontal = if la.attributes.endpoints[][1][2] == la.attributes.endpoints[][2][2]
        true
    elseif la.attributes.endpoints[][1][1] == la.attributes.endpoints[][2][1]
        false
    else
        error("endpoints not on a horizontal or vertical line")
    end

    tls = la.decorations[:ticklabels]
    maxwidth = if horizontal
            # height
            tls.visible[] ? height(FRect2D(boundingbox(tls))) : 0f0
        else
            # width
            tls.visible[] ? width(FRect2D(boundingbox(tls))) : 0f0
    end
    la.attributes.ticklabelspace = maxwidth
end

function iswhitespace(str)
    match(r"^\s+$", str) !== nothing
end

compute_tick_values(ct::CustomTicks, args...) = ct.f_tickvalues(args...)
get_tick_labels(ct::CustomTicks, values) = ct.f_ticklabels(values)


# FIXME: upstream https://github.com/JuliaGraphics/ColorVectorSpace.jl/issues/75
@inline _nan(::Type{HSV{Float16}}) = HSV{Float16}(NaN16,NaN16,NaN16)
@inline _nan(::Type{HSV{Float32}}) = HSV{Float32}(NaN32,NaN32,NaN32)
@inline _nan(::Type{HSV{Float64}}) = HSV{Float64}(NaN,NaN,NaN)
@inline _nan(::Type{T}) where {T} = nan(T)

# The default values used by extrapolation for off-domain points
const FillType = Union{Number,Colorant,Flat,Periodic,Reflect}
const FloatLike{T<:AbstractFloat} = Union{T,AbstractGray{T}}
const FloatColorant{T<:AbstractFloat} = Colorant{T}
@inline _default_fill(::Type{T}) where {T<:FloatLike} = convert(T, NaN)
@inline _default_fill(::Type{T}) where {T<:FloatColorant} = _nan(T)
@inline _default_fill(::Type{T}) where {T} = zero(T)

@inline _make_compatible(T, fill) = fill
@inline _make_compatible(::Type{T}, fill::Number) where {T} = T(fill)

box_extrapolation(etp::AbstractExtrapolation) = etp

function box_extrapolation(itp::AbstractInterpolation{T}, fill::FillType = _default_fill(T)) where T
    etp = extrapolate(itp, _make_compatible(T, fill))
    box_extrapolation(etp)
end

function box_extrapolation(parent::AbstractArray{T,N}, degree::D = Linear(), args...) where {T,N,D<:Union{Linear,Constant}}
    axs = axes(parent)
    itp = Interpolations.BSplineInterpolation{T,N,typeof(parent),BSpline{D},typeof(axs)}(parent, axs, BSpline(degree))
    box_extrapolation(itp, args...)
end

function box_extrapolation(parent::AbstractArray{T,N}, degree::Interpolations.Degree, args...) where {T,N}
    itp = interpolate(parent, BSpline(degree))
    box_extrapolation(itp, args...)
end

function box_extrapolation(parent::AbstractArray, fill::FillType)
    box_extrapolation(parent, Linear(), fill)
end

function box_extrapolation(itp::AbstractInterpolation, degree::Union{Linear,Constant}, args...)
    throw(ArgumentError("Boxing an interpolation in another interpolation is discouraged. Did you specify the parameter \"$degree\" on purpose?"))
end

function box_extrapolation(itp::AbstractInterpolation, degree::Interpolations.Degree, args...)
    throw(ArgumentError("Boxing an interpolation in another interpolation is discouraged. Did you specify the parameter \"$degree\" on purpose?"))
end

function box_extrapolation(itp::AbstractExtrapolation, fill::FillType)
    throw(ArgumentError("Boxing an extrapolation in another extrapolation is discouraged. Did you specify the parameter \"$fill\" on purpose?"))
end

# This is type-piracy, but necessary if we want Interpolations to be
# independent of OffsetArrays.
function AxisAlgorithms.A_ldiv_B_md!(dest::OffsetArray, F, src::OffsetArray, dim::Integer, b::AbstractVector)
    indsdim = axes(parent(src), dim)
    indsF = axes(F)[2]
    if indsF == indsdim
        AxisAlgorithms.A_ldiv_B_md!(parent(dest), F, parent(src), dim, b)
        return dest
    end
    throw(DimensionMismatch("indices $(axes(parent(src))) do not match $(axes(F))"))
end


# Try to open at least all downloaded datasets
@testset "Reading rasters" begin
    for f in remotefiles
        @testset "Reading $f" begin
            ga = GeoArrays.read(f)
        end
    end
end

@testset "Writing rasters" begin

    @testset "Simplest version" begin
        ga = GeoArray(rand(100,200,3))
        GeoArrays.write!("test.tif", ga)
    end
    @testset "Nodata" begin
        ga = GeoArray(Array{Union{Missing, Int32}}(rand(1:10,100,200,3)))
        GeoArrays.write!("test_nodata.tif", ga, 1)
    end
end


import LinearAlgebra

export ucase,
    ucrss,
    uddf,
    union,
    unitim,
    unload,
    unorm,
    unormg,
    utc2et

function _ucase(in)
    n = length(in) + 1
    out = Array{SpiceChar}(undef, n)
    ccall((:ucase_c, libcspice), Cvoid, (Cstring, SpiceInt, Ref{SpiceChar}),
          in, n, out)
    chararray_to_string(out)
end

"""
    ucase(in)

!!! warning "Deprecated"
    Use `uppercase(in)` instead.
"""
ucase

@deprecate ucase uppercase

function _ucrss(v1, v2)
    vout = Array{SpiceDouble}(undef, 3)
    ccall((:ucrss_c ,libcspice), Cvoid,
          (Ref{SpiceDouble}, Ref{SpiceDouble}, Ref{SpiceDouble}),
          v1, v2, vout)
    vout
end

"""
    ucrss(v1, v2)

!!! warning "Deprecated"
    Use `LinearAlgebra.normalize(LinearAlgebra.cross(v1, v2))` instead.
"""
ucrss

@deprecate ucrss(v1, v2) LinearAlgebra.normalize(LinearAlgebra.cross(v1, v2))

"""
    uddf(udfunc, x, dx)

Routine to calculate the first derivative of a caller-specified function using
a three-point estimation.

### Arguments ###

- `udfunc`: A callable that computes the scalar value of interest,
    e.g. `f(x::Float64) -> Float64`
- `x`: Independent variable of `udfunc`
- `dx`: Interval from `x` for derivative calculation

### Output ###

Returns the approximate derivative of `udfunc` at `x`.

### References ###

- [NAIF Documentation](https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/uddf_c.html)
"""
function uddf(udfunc, x, dx)
    function _udfunc(et::SpiceDouble, value::Ptr{SpiceDouble})
        value_ = unsafe_wrap(Array{SpiceDouble}, value, 1)
        value_[1] = udfunc(et)
        nothing
    end
    func = @cfunction($_udfunc, Cvoid, (SpiceDouble, Ptr{SpiceDouble}))
    deriv = Ref{SpiceDouble}()
    ccall((:uddf_c, libcspice), Cvoid,
          (Ref{Cvoid}, SpiceDouble, SpiceDouble, Ref{SpiceDouble}),
          func, x, dx, deriv)
    deriv[]
end

"""
    union(a::T, b::T) where T <: SpiceCell

Compute the union of two sets of any data type to form a third set.

### Arguments ###

- `a`: First input set
- `b`: Second input set

### Output ###

Returns a cell containing the union of `a` and `b`.

### References ###

- [NAIF Documentation](https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/union_c.html)
"""
function Base.union(a::SpiceCell{T}, b::SpiceCell{T}) where T
    size = max(a.cell.size, b.cell.size)
    if T <: SpiceChar
        length = max(a.cell.length, b.cell.length)
        out = SpiceCell{T}(size, length)
    else
        out = SpiceCell{T}(size)
    end
    a_data = a.data
    b_data = b.data
    out_data = out.data
    GC.@preserve a_data b_data out_data begin
        ccall((:union_c, libcspice), Cvoid, (Ref{Cell{T}}, Ref{Cell{T}}, Ref{Cell{T}}),
              a.cell, b.cell, out.cell)
    end
    handleerror()
    out
end

"""
    unitim(epoch, insys, outsys)

Transform time from one uniform scale to another.

### Arguments ###

- `epoch`: An epoch to be converted
- `insys`: The time scale associated with the input epoch
- `outsys`: The time scale associated with the function value

The uniform time scales are:

- `:TAI`
- `:TDT`
- `:TDB`
- `:ET`
- `:JED`
- `:JDTDB`
- `:JDTDT`

### Output ###

Returns the time in the system specified by `outsys` that is equivalent to the
`epoch` in the `insys` time scale.

### References ###

- [NAIF Documentation](https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/unitim_c.html)
"""
function unitim(epoch, insys, outsys)
    out = ccall((:unitim_c, libcspice), SpiceDouble,
                (SpiceDouble, Cstring, Cstring),
                epoch, string(insys), string(outsys))
    handleerror()
    out
end

"""
    unload(file)

Unload a SPICE kernel.

### Arguments ###

- `file`: The file name of a kernel to unload

### References ###

- [NAIF Documentation](https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/unload_c.html)
"""
function unload(file)
    ccall((:unload_c, libcspice), Cvoid, (Cstring,), file)
    handleerror()
end

function _unorm(v1)
    vout = Array{SpiceDouble}(undef, 3)
    vmag = Ref{SpiceDouble}()
    ccall((:unorm_c, libcspice), Cvoid,
          (Ref{SpiceDouble}, Ref{SpiceDouble}, Ref{SpiceDouble}),
          v1, vout, vmag)
    vout, vmag[]
end

"""
    unorm(v1)

!!! warning "Deprecated"
    Use `(LinearAlgebra.normalize(v1), LinearAlgebra.norm(v1))` instead.
"""
unorm

@deprecate unorm(v) (normalize(v), norm(v))

function _unormg(v1)
    ndim = length(v1)
    vout = Array{SpiceDouble}(undef, ndim)
    vmag = Ref{SpiceDouble}()
    ccall((:unormg_c, libcspice), Cvoid,
          (Ref{SpiceDouble}, SpiceInt, Ref{SpiceDouble}, Ref{SpiceDouble}),
          v1, ndim, vout, vmag)
    vout, vmag[]
end

"""
    unormg(v1)

!!! warning "Deprecated"
    Use `(LinearAlgebra.normalize(v1), LinearAlgebra.norm(v1))` instead.
"""
unormg

@deprecate unormg(v) (normalize(v), norm(v))

"""
    utc2et(utcstr)

Convert an input time from Calendar or Julian Date format, UTC, to ephemeris
seconds past J2000.

### Arguments ###

- `utcstr`: Input time string, UTC

### Output ###

Returns the equivalent of utcstr, expressed in ephemeris seconds past J2000.

### References ###

- [NAIF Documentation](https://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/cspice/utc2et_c.html)
"""
function utc2et(utcstr)
    et = Ref{SpiceDouble}()
    ccall((:utc2et_c, libcspice), Cvoid, (Cstring, Ref{SpiceDouble}),
          string(utcstr), et)
    handleerror()
    et[]
end


# UnitCommitment.jl: Optimization Package for Security-Constrained Unit Commitment
# Copyright (C) 2020, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.

using UnitCommitment, JSON, GZip, DataStructures

basedir = @__DIR__

function parse_case14()
    return JSON.parse(
        GZip.gzopen("$basedir/../../instances/test/case14.json.gz"),
        dicttype = () -> DefaultOrderedDict(nothing),
    )
end

@testset "repair!" begin
    @testset "Cost curve should be convex" begin
        json = parse_case14()
        json["Generators"]["g1"]["Production cost curve (MW)"] = [100, 150, 200]
        json["Generators"]["g1"]["Production cost curve (\$)"] = [10, 25, 30]
        instance = UnitCommitment._from_json(json, repair = false)
        @test UnitCommitment.repair!(instance) == 4
    end

    @testset "Startup limit must be greater than Pmin" begin
        json = parse_case14()
        json["Generators"]["g1"]["Production cost curve (MW)"] = [100, 150]
        json["Generators"]["g1"]["Production cost curve (\$)"] = [100, 150]
        json["Generators"]["g1"]["Startup limit (MW)"] = 80
        instance = UnitCommitment._from_json(json, repair = false)
        @test UnitCommitment.repair!(instance) == 1
    end

    @testset "Startup costs and delays must be increasing" begin
        json = parse_case14()
        json["Generators"]["g1"]["Startup costs (\$)"] = [300, 200, 100]
        json["Generators"]["g1"]["Startup delays (h)"] = [8, 4, 2]
        instance = UnitCommitment._from_json(json, repair = false)
        @test UnitCommitment.repair!(instance) == 4
    end
end


#=
Root file for the routing related files.
=#

module Routing

using ..Mapper2.Helper
Helper.@SetupDocStringTemplates

using ..Mapper2.MapperCore
using ..Mapper2.MapperGraphs

using DataStructures

using Logging
using Random

export route!,
    ChannelIndex,
    PortVertices,
    RoutingStruct,
    RoutingLink,
    BasicRoutingLink,
    RoutingChannel,
    BasicRoutingChannel,
    routing_link_type,
    routing_channel,
    annotate

abstract type AbstractRoutingAlgorithm end

"""
Representation of routing resources in an architecture.

API
---
* [`channels`](@ref)
* [`cost`](@ref)
* [`capacity`](@ref)
* [`occupancy`](@ref)
* [`addchannel`](@ref)
* [`remchannel`](@ref)

Implementations
---------------
* [`BasicRoutingLink`](@ref) - Reference this type for what methods of the API
    come for free when using various fields of the basic type.
"""
abstract type RoutingLink end

"""
Representation of channels in the taskgraph for routing.

API
---
* [`start_vertices`](@ref)
* [`stop_vertices`](@ref)
* [`isless(a::RoutingChannel, b::RoutingChannel)`](@ref)

Implementations
---------------
* [`BasicChannel`](@ref)
"""
abstract type RoutingChannel end

################################################################################
include("Types.jl")
include("Graph.jl")
include("Links.jl")
include("Channels.jl")
include("Struct.jl")

# Algorithms
include("Pathfinder/Pathfinder.jl")

# Routing dispatch
"""
    route!(map::Map; meta_prefix = "")

Run pathfinder routing directly on `map`. Keyword argument `meta_prefix` allows
controlling the prefix of the `map.metadata` keys generated below.

Records the following metrics into `map.metadata`:

* `routing_struct_time` - Time it took to build the [`RoutingStruct`](@ref)

* `routing_struct_bytes` - Memory allocation to build [`RoutingStruct`](@ref)

* `routing_passed :: Bool` - `true` if routing passes [`check_routing`](@ref),
    otherwise `false`.

* `routing_error :: Bool` - `true` if routing experienced a connectivity error.

The following are also included if `routing_error == false`.

* `routing_time` - Time to run routing.

* `routing_bytes` - Memory allocation during routing.

* `routing_global_links` - The number of global links used in the final routing.
"""
function route!(m::Map; meta_prefix = "")
    # Build the routing structure
    routing_struct, struct_time, struct_bytes, _, _ = @timed RoutingStruct(m)
    # Default to Pathfinder
    algorithm = routing_algorithm(m, routing_struct)
    # Run the routing algorithm.
    routing_error = false
    local route_time
    local route_bytes
    try
        _, route_time, route_bytes, _, _ = @timed route!(algorithm, routing_struct)
    catch err
        @error err
        routing_error = true
    end

    # Record the final results.
    record(m, routing_struct)
    routing_passed = check_routing(m)

    # Save all of this to metadata.
    m.metadata["$(meta_prefix)routing_struct_time"] = struct_time
    m.metadata["$(meta_prefix)routing_struct_bytes"] = struct_bytes
    m.metadata["$(meta_prefix)routing_passed"] = routing_passed
    m.metadata["$(meta_prefix)routing_error"] = routing_error
    if !routing_error
        m.metadata["$(meta_prefix)routing_time"] = route_time
        m.metadata["$(meta_prefix)routing_bytes"] = route_bytes
        m.metadata["$(meta_prefix)routing_global_links"] = MapperCore.total_global_links(m)
    end

    return nothing
end

routing_algorithm(m::Map, rs) = Pathfinder(m, rs)

################################################################################
# REQUIRED METHODS
################################################################################
"""
    annotate(ruleset::RuleSet, item::Union{Port,Link,Component})

Return some [`<:RoutingLink`](@ref RoutingLink) for `item`.  If
`item <: Component`, it is a primitive. If not other primitives have been
defined, it will be a `mux`.

See: [`BasicRoutingLink`](@ref)

Default: `BasicRoutingLink(capacity = getcapacity(ruleset, item))
"""
function annotate(ruleset::RuleSet, item::Union{Port,Link,Component})
    return BasicRoutingLink(; capacity = getcapacity(ruleset, item))
end

"""
    canuse(ruleset::RuleSet, link::RoutingLink, channel::RoutingChannel)::Bool

Return `true` if `channel` can be routed using `link`.

See: [`RoutingLink`](@ref), [`RoutingChannel`](@ref)

Default: `true`
"""
MapperCore.canuse(::RuleSet, link::RoutingLink, channel::RoutingChannel) = true

end


@static if is_windows()
  const windows_time_freq = begin
    x = Array{Int64}()
    if ccall(:QueryPerformanceFrequency,Bool,(Ptr{Int64},),pointer(x))
      x[]
    else
      error("Unsupported operating system. Use Windows XP or later.")
    end
  end
end

const approx_timer_resolution = @static if is_windows()
  1/windows_time_freq
else
  1e-6
end

@static if is_windows()
  function precise_time()
    x = Array{Int64}()
    if !ccall(:QueryPerformanceCounter,Bool,(Ptr{Int64},),pointer(x))
      error("Unsupported operating system. Use Windows XP or later.")
    else
      x[] / windows_time_freq
    end
  end
else
  const precise_time = time
end


using BeamformerRecipes
using YAML

"""
    cal_solution(redis, arrayname, unixtime) -> nants, nchan, ants, cals

Fetch the most recent telstate calibration solution for subarray `arrayname`
from `redis` as of `unixtime`.  `nants` and `nchan` are integers used in the
dimensioning of the data arrays, `ants` is a Vector of antenna names (in F
engine order), and `cals` is a `Dict{Symbol, Any}` suitable for splatting into
the keyword arguments of the `CalInfo` constructor.
"""
function cal_solution(redis, arrayname, unixtime)
    # zrevrangebyscore returns an OrderedSet
    rkeys = zrevrangebyscore(redis, "$arrayname:cal_solutions:index", unixtime, 0, "limit", 0, 1)
    if length(rkeys) == 0
        @warn "no cal solutions found on or before $unixtime, using fallback hack" _module=nothing _file=nothing
        rkeys = ["array_1:cal_solutions:20220117T035342Z"]
    end
    length(rkeys) <= 0 && error("no cal solutions found")
    length(rkeys)  > 1 && error("too many cal solutions found")
    @info "using cal solution $(rkeys[1])"
    strcals = hgetall(redis, rkeys[1])

    # Parse integers
    nants = parse(Int, strcals["nants"])
    nchan = parse(Int, strcals["nchan"])
    ants = YAML.load(strcals["antenna_list"])
    # TODO Fix upstream and in redis
    # Workaround for nants bug
    if nants > 64
        nants ÷= 8
    end

    # Create Arrays and copy data into them
    # TODO Fix cal_K type upstream and in redis
    if sizeof(strcals["cal_K"]) == nants * 2 #=npols=# * sizeof(Float64) * 2 #=complex=#
        cal_K = Array{ComplexF64}(undef, nants, 2)
    else
        cal_K = Array{Float64}(undef, nants, 2)
    end
    @assert sizeof(cal_K) == sizeof(strcals["cal_K"])
    unsafe_copyto!(pointer(cal_K), Ptr{eltype(cal_K)}(pointer(strcals["cal_K"])), length(cal_K))

    cal_B = Array{ComplexF64}(undef, nants, 2, nchan)
    @assert sizeof(cal_B) == sizeof(strcals["cal_B"])
    unsafe_copyto!(pointer(cal_B), Ptr{ComplexF64}(pointer(strcals["cal_B"])), length(cal_B))

    cal_G = Array{ComplexF64}(undef, nants, 2)
    @assert sizeof(cal_G) == sizeof(strcals["cal_G"])
    unsafe_copyto!(pointer(cal_G), Ptr{ComplexF64}(pointer(strcals["cal_G"])), length(cal_G))

    cal_all = Array{ComplexF64}(undef, nants, 2, nchan)
    @assert sizeof(cal_all) == sizeof(strcals["cal_all"])
    unsafe_copyto!(pointer(cal_all), Ptr{ComplexF64}(pointer(strcals["cal_all"])), length(cal_all))

    # Replace all NaN values with 0
    cal_K[isnan.(cal_K)] .= 0
    cal_B[isnan.(cal_B)] .= 0
    cal_G[isnan.(cal_G)] .= 0
    cal_all[isnan.(cal_all)] .= 0

    cals = Dict{Symbol, Any}()
    cals[:cal_K] = real(cal_K) * 1e9 # seconds to nanoseconds
    cals[:cal_B] = cal_B
    cals[:cal_G] = cal_G
    cals[:cal_all] = cal_all
    cals[:refant] = strcals["refant"]

    # (Un-)conjugate cal_all antenna data upto refant
    refant_idx = findfirst(==(cals[:refant]), ants)
    @views conj!(cals[:cal_all][1:refant_idx-1, :, :])

    nants, nchan, ants, cals
end


################################################################################
#
#	ABSTRACT TYPE
#
#   AbstractBandstructure{path}
#   --> path is the path in k-space
#
#   FILE CONTAINS
#       - abstract type definition
#       - interface definition
#
#
################################################################################

################################################################################
#
#   ABSTRACT TYPE DEFINITION
#
################################################################################
abstract type AbstractBandstructure{
    P<:AbstractReciprocalPath{RP} where {RP},
    H<:AbstractHamiltonian{L,UC,HB} where {L,UC,HB}
} end

# export the type
export AbstractBandstructure

################################################################################
#
#	INTERFACING / ACCESSING BANDSTRUCTURES
#	(functions have to be overwritten by concrete types)
#
################################################################################

# obtaining the hamiltonian
function hamiltonian(
            bs :: BS
        ) :: H where {RP, P<:AbstractReciprocalPath{RP}, L,UC,HB,H<:AbstractHamiltonian{L,UC,HB}, BS <: AbstractBandstructure{P,H}}

    # print an error because implementation for concrete type is missing
    error("not implemented interface function 'hamiltonian' for bandstructure type " * string(typeof(bs)))
end

# export the function
export hamiltonian

# obtaining the path
function path(
            bs :: BS
        ) :: P where {RP, P<:AbstractReciprocalPath{RP}, L,UC,HB,H<:AbstractHamiltonian{L,UC,HB}, BS <: AbstractBandstructure{P,H}}

    # print an error because implementation for concrete type is missing
    error("not implemented interface function 'path' for bandstructure type " * string(typeof(bs)))
end

# export the function
export path

# obtaining the energy values
# bands for each segment
# bands[i] gives all bands of segment i
# bands[i][j] gives all energy values for band j of segment i
# bands[i][j][k] gives the energy value at kpoint index k of band j in segment i
function energies(
            bs :: BS
        ) :: Vector{<:Vector{<:Vector{<:Real}}} where {RP, P<:AbstractReciprocalPath{RP}, L,UC,HB,H<:AbstractHamiltonian{L,UC,HB}, BS <: AbstractBandstructure{P,H}}

    # print an error because implementation for concrete type is missing
    error("not implemented interface function 'energies' for bandstructure type " * string(typeof(bs)))
end

# export the function
export energies

# recalculate the band structure (energy values)
function recalculate!(
            bs :: BS
            ;
            resolution :: Union{Int64, Vector{Int64}} = 100
        ) where {RP, P<:AbstractReciprocalPath{RP}, L,UC,HB,H<:AbstractHamiltonian{L,UC,HB}, BS <: AbstractBandstructure{P,H}}

    # print an error because implementation for concrete type is missing
    error("not implemented interface function 'recalculate!' for bandstructure type " * string(typeof(bs)))
end

# export the function
export recalculate!

function saveBandstructure(
        bs :: BS,
        fn :: AbstractString,
        group :: AbstractString = "bandstructure"
        ;
        append :: Bool = false
    ) where {RP, P<:AbstractReciprocalPath{RP}, L,UC,HB,H<:AbstractHamiltonian{L,UC,HB}, BS<:AbstractBandstructure{P,H}}

    # print an error because implementation for concrete type is missing
    error("not implemented interface function 'recalculate!' for bandstructure type " * string(BS))
end

function loadBandstructure(
        ::Type{BS},
        fn :: AbstractString,
        group :: AbstractString = "bandstructure"
    ) where {RP, P<:AbstractReciprocalPath{RP}, L,UC,HB,H<:AbstractHamiltonian{L,UC,HB}, BS<:AbstractBandstructure{P,H}}

    # print an error because implementation for concrete type is missing
    error("not implemented interface function 'recalculate!' for bandstructure type " * string(BS))
end

export saveBandstructure, loadBandstructure


struct ObjectData
    objects::Vector{AbstractWorldObj}
    collidable_centers::Vector{Vector{Float32}}
    collidable_corners::Vector{Array{Float32, 2}}
    collidable_norms::Vector{Array{Float32, 2}}
    collidable_safety_radii::Vector{Float32}
end

function ObjectData(map_data::Dict, road_tile_size::Float32, domain_rand::Bool, grid::Vector{Union{Missing, Dict{String,Any}}},
                    grid_width::Int, grid_height::Int)
    # Create the objects array
    objects = Vector{AbstractWorldObj}()

    # The corners for every object, regardless if collidable or not
    #object_corners = []

    # Arrays for checking collisions with N static objects
    # (Dynamic objects done separately)
    # (N x 2): Object position used in calculating reward
    collidable_centers = Vector{Vector{Float32}}()

    # (N x 2 x 4): 4 corners - (x, z) - for object's boundbox
    collidable_corners = Vector{Matrix{Float32}}()

    # (N x 2 x 2): two 2D norms for each object (1 per axis of boundbox)
    collidable_norms = Vector{Matrix{Float32}}()

    # (N): Safety radius for object used in calculating reward
    collidable_safety_radii = Vector{Float32}()

    # For each object
    for (obj_idx, desc) in enumerate(get(map_data, "objects", []))
        kind = desc["kind"]

        pos = Float32.(desc["pos"])
        x, z = pos[1:2]
        y = length(pos) == 3 ? pos[3] : 0f0

        rotate = Float32(desc["rotate"])
        optional = get(desc, "optional", false)

        pos = road_tile_size * [x, y, z]

        # Load the mesh
        #TODO
        mesh = ObjMesh.get(kind)

        if "height" in keys(desc)
            scale = Float32(desc["height"]) / mesh.max_coords[2]
        else
            scale = Float32(desc["scale"])
        end

        @assert !("height" ∈ keys(desc) && "scale" ∈ keys(desc)) "cannot specify both height and scale"

        static = get(desc, "static", true)

        obj_desc = Dict([
            "kind"=> kind,
            "mesh"=> mesh,
            "pos"=> pos,
            "scale"=> scale,
            "y_rot"=> rotate,
            "optional"=> optional,
            "static"=> static
        ])

        # obj = nothing

        if static
            if kind == "trafficlight"
                obj = TrafficLightObj(obj_desc, domain_rand, SAFETY_RAD_MULT)
            else
                obj = WorldObj(obj_desc, domain_rand, SAFETY_RAD_MULT)
            end
        else
            if kind == "duckiebot"
                obj = DuckiebotObj(obj_desc, domain_rand, SAFETY_RAD_MULT, WHEEL_DIST,
                                   ROBOT_WIDTH, ROBOT_LENGTH)
            elseif kind == "duckie"
                obj = DuckieObj(obj_desc, domain_rand, SAFETY_RAD_MULT, road_tile_size)
            else
                msg = "I do not know what object this is: $kind"
                error(msg)
            end
        end

        objects = vcat(objects, [obj])

        # Compute collision detection information

        # angle = rotate * (math.pi / 180)

	# Find drivable tiles object could intersect with
        possible_tiles = find_candidate_tiles(_obj_corners(obj), road_tile_size)

        # If the object intersects with a drivable tile
        if static && kind != "trafficlight" && _collidable_object(
                grid, grid_width, grid_height, _obj_corners(obj), _obj_norm(obj), possible_tiles, road_tile_size)
            collidable_centers = vcat(collidable_centers, [pos])
            collidable_corners = vcat(collidable_corners, [permutedims(_obj_corners(obj))])
            collidable_norms = vcat(collidable_norms, [_obj_norm(obj)])
            collidable_safety_radii = vcat(collidable_safety_radii, [_safety_radius(obj)])
        end
    end

    ObjectData(objects, collidable_centers, collidable_corners,
               collidable_norms, collidable_safety_radii)
end

function _get_curve(grid::Vector{Union{Missing,Dict{String,Any}}}, i::Int, j::Int,
                    width::Int, height::Int, road_tile_size::Float32)
    ##
    #    Get the Bezier curve control points for a given tile
    ##
    tile = _get_tile(grid, i, j, width, height)
    @assert !isnothing(tile)

    kind = tile["kind"]
    angle = tile["angle"]

    # Each tile will have a unique set of control points,
    # Corresponding to each of its possible turns

    if startswith(kind, "straight")
        pts = [
                [-0.2f0 0f0 -0.50f0;
                 -0.2f0 0f0 -0.25f0;
                 -0.2f0 0f0  0.25f0;
                 -0.2f0 0f0  0.50f0],

                [0.2f0 0f0  0.50f0;
                 0.2f0 0f0  0.25f0;
                 0.2f0 0f0 -0.25f0;
                 0.2f0 0f0 -0.50f0],
              ] .* road_tile_size

    elseif kind == "curve_left"
        pts = [
                [-0.2f0 0f0 -0.5f0;
                 -0.2f0 0f0  0.0f0;
                  0.0f0 0f0  0.2f0;
                  0.5f0 0f0  0.2f0],

                [0.5f0 0f0 -0.2f0;
                 0.3f0 0f0 -0.2f0;
                 0.2f0 0f0 -0.3f0;
                 0.2f0 0f0 -0.5f0],
              ] .* road_tile_size

    elseif kind == "curve_right"
        pts = [
                [-0.2f0 0f0 -0.5f0;
                 -0.2f0 0f0 -0.2f0;
                 -0.3f0 0f0 -0.2f0;
                 -0.5f0 0f0 -0.2f0],

                [-0.5f0 0f0  0.2f0;
                 -0.3f0 0f0  0.2f0;
                  0.3f0 0f0  0.0f0;
                  0.2f0 0f0 -0.5f0],
              ] .* road_tile_size

    # Hardcoded all curves for 3way intersection
    elseif startswith(kind, "3way")
        pts = [
                [-0.2f0 0f0 -0.50f0;
                 -0.2f0 0f0 -0.25f0;
                 -0.2f0 0f0  0.25f0;
                 -0.2f0 0f0  0.50f0],

                [-0.2f0 0f0 -0.5f0;
                 -0.2f0 0f0  0.0f0;
                  0.0f0 0f0  0.2f0;
                  0.5f0 0f0  0.2f0],

                [0.2f0 0f0  0.50f0;
                 0.2f0 0f0  0.25f0;
                 0.2f0 0f0 -0.25f0;
                 0.2f0 0f0 -0.50f0],

                [0.5f0 0f0 -0.2f0;
                 0.3f0 0f0 -0.2f0;
                 0.2f0 0f0 -0.2f0;
                 0.2f0 0f0 -0.5f0],

                [0.2f0 0f0 0.5f0;
                 0.2f0 0f0 0.2f0;
                 0.3f0 0f0 0.2f0;
                 0.5f0 0f0 0.2f0],

                [ 0.5f0 0f0 -0.2f0;
                  0.3f0 0f0 -0.2f0;
                 -0.2f0 0f0  0.0f0;
                 -0.2f0 0f0  0.5f0],
              ] .* road_tile_size

    # Template for each side of 4way intersection
    elseif startswith(kind, "4way")
        pts = [
                [-0.2f0 0f0 -0.5f0;
                 -0.2f0 0f0  0.0f0;
                  0.0f0 0f0  0.2f0;
                  0.5f0 0f0  0.2f0],

                [-0.2f0 0f0 -0.50f0;
                 -0.2f0 0f0 -0.25f0;
                 -0.2f0 0f0  0.25f0;
                 -0.2f0 0f0  0.50f0],

                [-0.2f0 0f0 -0.5f0;
                 -0.2f0 0f0 -0.2f0;
                 -0.3f0 0f0 -0.2f0;
                 -0.5f0 0f0 -0.2f0],
              ] .* road_tile_size
    else
        @assert false kind
    end

    # Rotate and align each curve with its place in global frame
    if startswith(kind, "4way")

        # Generate all four sides' curves,
        # with 3-points template above
	function size_gen(rot::Int)
	    mat = gen_rot_matrix([0f0, 1f0, 0f0], rot * Float32(π) / 2f0)
      	    pts_new = map(x -> x * mat, pts)
            add_vec = [(i-0.5f0) 0f0 (j-0.5f0);] * road_tile_size
       	    return hcat(map(x -> x .+ add_vec, pts_new)...)
	end

	fourway_pts = hcat(map(rot->size_gen(rot), 0:3)...)
        return reshape(fourway_pts, 4, 3, :)

    # Hardcoded each curve; just rotate and shift
    elseif startswith(kind, "3way")
        mat = gen_rot_matrix([0f0, 1f0, 0f0], angle * Float32(π) / 2f0)
        #NOTE: pts is 3D matrix, find a work around if * does not work
        pts_new = map(x -> x * mat, pts)
        add_vec = [(i-0.5f0) 0f0 (j-0.5f0);] * road_tile_size
        pts_new = map(x -> x .+ add_vec, pts_new)
        threeway_pts = hcat(pts_new...)

        return reshape(threeway_pts, 4, 3, :)
    else
        mat = gen_rot_matrix([0f0, 1f0, 0f0], angle * Float32(π) / 2f0)
        pts = map(x -> x * mat, pts)
        add_vec = [(i-0.5f0) 0f0 (j - 0.5f0);] * road_tile_size
        pts = map(x -> x .+ add_vec, pts)
    end

    pts = hcat(pts...)
    return reshape(pts, 4, 3, :)
end
#=
struct tile
    coords::Vector{Float32}
    texture
    color
    kind::String
    angle::Float32
    drivable::Bool
    curves
end
=#
struct Grid
    road_tile_size::Float32
    grid_width::Int
    grid_height::Int
    _grid::Vector
    obj_data::ObjectData
    road_vlist::Matrix{Float32}
    road_tlist::Matrix{Float32}
    ground_vlist::Matrix{Float32}
    drivable_tiles::Vector{Dict}
    mesh::ObjectMesh
    start_tile::Union{Nothing,Dict}
end

function _set_tile!(grid::Vector{Union{Missing,Dict{String,Any}}}, i::Int, j::Int,
                    width::Int, height::Int, tile::Dict{String,Any})
    @assert 1 ≤ i ≤ width
    @assert 1 ≤ j ≤ height
    grid[(j-1)*width + i] = tile
end

_get_tile(grid::Grid, i, j) = _get_tile(grid._grid, i, j, grid.grid_width, grid.grid_height)

function _get_tile(grid::Vector{Union{Missing,Dict{String,Any}}}, i::Int, j::Int, width::Int, height::Int)
    ##
    #Returns nothing if the duckiebot is not in a tile.
    ##
    if 1 ≤ i ≤ width && 1 ≤ j ≤ height
        return grid[(j-1)*width + i]
    end

    return nothing
end

function _init_vlists(road_tile_size)
    # Create the vertex list for our road quad
    # Note: the vertices are centered around the origin so we can easily
    # rotate the tiles about their center
    half_size = road_tile_size / 2f0
    verts = [
        -half_size 0f0 -half_size;
         half_size 0f0 -half_size;
         half_size 0f0  half_size;
        -half_size 0f0  half_size;
    ]
    texCoords = [
        1f0 0f0;
        0f0 0f0;
        0f0 1f0;
        1f0 1f0
    ]

    road_vlist = verts
    road_tlist = texCoords

    # Create the vertex list for the ground quad
    verts = [
        -1 -0.8f0  1;
        -1 -0.8f0 -1;
         1 -0.8f0 -1;
         1 -0.8f0  1
    ]

    ground_vlist = verts

    return road_vlist, road_tlist, ground_vlist
end

function Grid(map_data::Dict, domain_rand)
    if "tile_size" ∉ keys(map_data)
        msg = "Must now include explicit tile_size in the map data."
        throw(KeyError(msg))
    end

    road_tile_size = Float32(map_data["tile_size"])

    road_vlist, road_tlist, ground_vlist = _init_vlists(road_tile_size)

    tiles = map_data["tiles"]
    @assert length(tiles) > 0
    @assert length(tiles[1]) > 0

    # Create the grid
    grid_height = length(tiles)
    grid_width = length(tiles[1])
    _grid = Vector{Union{Missing, Dict{String,Any}}}(missing, grid_width * grid_height)

    # We keep a separate list of drivable tiles
    drivable_tiles = []

    # For each row in the grid
    for (j, row) ∈ enumerate(tiles)
        msg = "each row of tiles must have the same length"
        if length(row) != grid_width
            error(msg)
        end

        # For each tile in this row
        for (i, tile) in enumerate(row)
            tile = strip(tile)

            tile == "empty" && continue

            if '/' ∈ tile
                kind, orient = split(tile, '/')
                kind = strip(kind, ' ')
                orient = strip(orient, ' ')
                angle = findfirst(isequal(orient), ["S", "E", "N", "W"]) - 1
                drivable = true
            elseif '4' ∈ tile
                kind = "4way"
                angle = 2
                drivable = true
            else
                kind = tile
                angle = 0
                drivable = false
            end

            tile = Dict([
                "coords"=> [i, j],
                "kind"=> kind * "",
                "angle"=> angle,
                "drivable"=> drivable
            ])

            _set_tile!(_grid, i, j, grid_width, grid_height, tile)

            if drivable
                tile["curves"] = _get_curve(_grid, i, j, grid_width,
                                            grid_height, road_tile_size)
                push!(drivable_tiles, tile)
            end
        end
    end
    #TODO
    mesh = ObjMesh.get("duckiebot")
    obj_data = ObjectData(map_data, road_tile_size, domain_rand, _grid, grid_width, grid_height)

    # Get the starting tile from the map, if specified
    start_tile = nothing
    if "start_tile" ∈ keys(map_data)
        coords = map_data["start_tile"]
        coords .+= 1
        start_tile = _get_tile(_grid, coords..., grid_width, grid_height)
    end

    Grid(road_tile_size, grid_width, grid_height, _grid, obj_data, road_vlist,
         road_tlist, ground_vlist, drivable_tiles, mesh, start_tile)
end

_get_curve(grid::Grid, i, j) = _get_curve(grid._grid, i, j, grid.grid_width,
                                          grid.grid_height, grid.road_tile_size)

mutable struct Map
    map_name::String
    map_file_path::String
    map_data::Dict       # The parsed content of the map_file
    _grid::Grid
end

function Map(map_name::String, domain_rand::Bool)
    ##
    #Load the map layout from a YAML file
    ##
    map_file_path = get_file_path("src/maps", map_name, "yaml")
    #logger.debug('loading map file "%s"' % self.map_file_path)

    map_data = nothing
    open(map_file_path, "r") do f
        map_data = YAML.load(f)
    end

    _grid = Grid(map_data, domain_rand)

    Map(map_name, map_file_path, map_data, _grid)
end


@testset "AnalyticVI" begin
    seed!(42)
    L = 3
    D = 10
    N = 20
    b = 5
    i = AnalyticVI()
    @test i isa AnalyticVI{Float64}
    @test AnalyticVI(; ϵ=0.0001f0) isa AnalyticVI{Float32}
    @test AnalyticSVI(10; ϵ=0.0001f0) isa AnalyticVI{Float32}
    @test repr(i) == "Analytic Variational Inference"
    @test AGP.ρ(i) == 1.0
    @test AGP.is_stochastic(i) == false

    i = AnalyticSVI(b)
    @test i isa AnalyticVI{Float64}
    AGP.set_ρ!(i, N / b)
    @test AGP.ρ(i) == N / b
    @test AGP.is_stochastic(i) == true
end


const ToT = UInt8
const ChannelID = UInt8
const DOMID = UInt32
const Floor = UInt8
const DU = UInt8
const HitTime = Float64
const TriggerMask = Int64

struct OMKey
    du::DU
    floor::Floor
end

struct Position{T} <: FieldVector{3, T}
    x::T
    y::T
    z::T

    Position(x::T, y::T, z::T) where {T} = new{T}(x, y, z)
end
Position(x, y, z) = Position(promote(x, y, z)...)

struct Direction{T} <: FieldVector{3, T}
    x::T
    y::T
    z::T
end
Direction(x, y, z) = Direction(promote(x, y, z)...)
Direction(ϕ, θ) = Direction(cos(ϕ)*cos(θ), sin(ϕ)*cos(θ), sin(θ))

struct MCEventInfo
    event_id::Int64
    group_id::Int64
    mc_id::Int64
    mc_time::Float64
    nanoseconds::Int64
    run_id::Int64
    timestamp::Int64
    weight_w1::Float64
    weight_w2::Float64
    weight_w3::Float64
    weight_w4::Float64
end

Base.show(io::IO, e::MCEventInfo) = begin
    print(io, "MCEventInfo: id($(e.event_id)), mc_id($(e.mc_id)), " *
          "mc_time($(e.mc_time) ($(e.nanoseconds)), ts($(e.timestamp)), run_id($(e.run_id))")
end

struct DAQEventInfo
    det_id::Int64
    event_id::Int64
    frame_index::Int64
    group_id::Int64
    mc_run_id::Int64
    mc_time::Float64
    nanoseconds::Int64
    overlays::Int64
    run_id::Int64
    timestamp::Int64
    trigger_counter::Int64
    trigger_mask::Int64
    weight_w1::Float64
    weight_w2::Float64
    weight_w3::Float64
    weight_w4::Float64
end

Base.show(io::IO, e::DAQEventInfo) = begin
    print(io, "DAQEventInfo: id($(e.event_id)), det_id($(e.det_id)), " *
          "timestamp($(e.timestamp) ($(e.nanoseconds)), run_id($(e.run_id))")
end

struct TimesliceInfo
    frame_index::UInt32
    slice_id::UInt32
    timestamp::UInt32
    nanoseconds::UInt32
    n_frames::UInt32
    group_id::UInt32
end

struct Track
    dir::Direction
    pos::Position
    time
end

# Fit
abstract type AbstractRecoTrack end

struct RecoTrack<:AbstractRecoTrack
    dir::Direction
    pos::Position
    time::Float64
end

struct NoRecoTrack<:AbstractRecoTrack end

# MC
struct MCTrack
    bjorken_y::Float64
    dir_x::Float64
    dir_y::Float64
    dir_z::Float64
    E::Float64
    group_id::Int64
    id::Int64
    interaction_channel::Int64
    is_cc::Bool
    length::Float64
    pos_x::Float64
    pos_y::Float64
    pos_z::Float64
    t::Float64
    particle_type::Int64
end

Track(t::MCTrack) = Track([t.dir_x, t.dir_y, t.dir_z], [t.pos_x, t.pos_y, t.pos_z], t.t)

Base.show(io::IO, t::MCTrack) = begin
    E = Printf.@sprintf "%0.1f" t.E
    bjorken_y = Printf.@sprintf "%0.2f" t.bjorken_y
    print(io, "MCTrack: bjorken_y($(bjorken_y)), t($(t.t)), " *
              "pos($(t.pos_x), $(t.pos_y), $(t.pos_z)), " *
              "dir($(t.dir_x), $(t.dir_y), $(t.dir_z)), " *
              "E($(E)), type($(t.particle_type))")
end

# Hardware
struct PMT
    channel_id::ChannelID
    pos::Position
    dir::Direction
end

struct DOM
    id::UInt32
    floor::Floor
    du::DU
    pmts::Vector{PMT}
end

struct Calibration
    det_id::Int32
    pos::Dict{Int32,Vector{NeRCA.Position}}
    dir::Dict{Int32,Vector{NeRCA.Direction}}
    t0::Dict{Int32,Vector{Float64}}
    du::Dict{Int32,DU}
    floor::Dict{Int32,Floor}
    omkeys::Dict{OMKey,Int32}  # maps OMKey to Module ID
    pmts::Dict{Int32,Tuple{Int32,UInt8}}
    max_z
    n_dus
end

Base.show(io::IO, c::Calibration) = begin
    print(io, "Calibration data for detector '$(c.det_id)' " *
              "with $(length(c.pos)) modules.")
end

# Signal
abstract type AbstractHit end
abstract type AbstractDAQHit<:AbstractHit end
abstract type AbstractMCHit<:AbstractHit end

Base.isless(lhs::AbstractHit, rhs::AbstractHit) = lhs.t < rhs.t

struct Hit <: AbstractDAQHit
    channel_id::ChannelID
    dom_id::DOMID
    t::HitTime
    tot::ToT
    trigger_mask::TriggerMask
end

struct SnapshotHit <: AbstractDAQHit
    channel_id::ChannelID
    dom_id::DOMID
    t::HitTime
    tot::ToT
end

struct MCHit <: AbstractHit
    a::Float32
    origin::UInt32
    pmt_id::UInt32
    t::HitTime
end

mutable struct Multiplicity
    count::Int32
    id::Int64
end

struct CalibratedHit <: AbstractDAQHit
    channel_id::ChannelID
    dom_id::UInt32
    du::DU
    floor::Floor
    t::HitTime
    tot::ToT
    pos::Position
    dir::Direction
    t0::HitTime
    trigger_mask::TriggerMask
    multiplicity::Multiplicity
end

struct TimesliceHit <: AbstractDAQHit
    channel_id::Int8
    dom_id::UInt32
    t::Int32
    tot::Int16
end

struct TriggeredHit <: AbstractDAQHit
    dom_id::Int32
    channel_id::UInt8
    t::Int32
    tot::UInt8
    trigger_mask::TriggerMask
end

struct DAQEvent
    det_id::Int32
    run_id::Int32
    timeslice_id::Int32
    timestamp::Int32
    ticks::Int32
    trigger_counter::Int64
    trigger_mask::Int64
    overlays::Int32
    n_triggered_hits::Int32
    triggered_hits::Vector{TriggeredHit}
    n_hits::Int32
    hits::Vector{Hit}
end

Base.show(io::IO, d::DAQEvent) = begin
    print(io, "DAQEvent: $(d.n_triggered_hits) triggered hits, " *
              "$(d.n_hits) snapshot hits")
end


module InteractiveFixedEffectModels
##############################################################################
##
## Dependencies
##
##############################################################################

using Base
using LinearAlgebra
using Statistics
using Printf

using StatsBase
using StatsModels
using Tables
using DataFrames
using LeastSquaresOptim
using FillArrays
using Distributions
using Reexport
using FixedEffects
@reexport using FixedEffectModels

if !isdefined(FixedEffectModels, :ModelTerm)
    ModelTerm = Model
end
##############################################################################
##
## Exported methods and types
##
##############################################################################

export InteractiveFixedEffectTerm,
InteractiveFixedEffectModel,
ife,
regife
##############################################################################
##
## Load files
##
##############################################################################
include("types.jl")
include("methods/gauss_seidel.jl")
include("methods/ls.jl")
include("fit.jl")
include("deprecated.jl")
end


abstract type BoundaryComponent{N} end

"""
    get_positions(boundary_component::BoundaryComponent{N})
        -> Vector{SVectorF{N}}

Return the positions of the boundary particles.
"""
function get_positions end

"""
    get_velocities(boundary_component::BoundaryComponent{N})
        -> Vector{SVectorF{N}}

Return the velocities of the boundary particles.
"""
function get_velocities end

"""
    get_velocity(boundary_component::BoundaryComponent{N}, i::Unsigned)
        -> SVectorF{N}

Return the velocity of the boundary particle with the given index.
"""
function get_velocity(boundary_component::BoundaryComponent, i::Unsigned)
    velocities = get_boundary_velocities(boundary_component)
    velocities[i]
end

"""
    get_pressures(boundary_component::BoundaryComponent)
        -> Vector{Float}

Return the virtual pressures of the boundary particles.
"""
function get_pressures end

"""
    get_pressure(boundary_component::BoundaryComponent, i::Unsigned)
        -> Float

Return the virtual pressure of the boundary particle with the given index.
"""
function get_pressure(boundary_component::BoundaryComponent, i::Unsigned)
    pressures = get_boundary_pressures(boundary_component)
    pressures[i]
end

"""
    compute_boundary_mass_density(
        boundary_component::BoundaryComponent,
        energy_component::EnergyComponent,
        eos::EquationOfState,
        idx_fluid::Unsigned,
        idx_boundary::Unsigned,
    )

Compute the virtual mass density of the boundary particle with index `idx_boundary`
due to its interaction with the fluid particle with index `idx_fluid`.
"""
function compute_mass_density end

"""
    updateboundaries!(
        boundary_component::BoundaryComponent{N},
        positions::AbstractVector{SVectorF{N}},
        velocities::Velocities{N},
        mass_component::MassComponent{MD,<:Kernel{N}},
        pressures::Pressures,
    )

Update the state of the boundary particles according to the given fluid particles.
"""
function updateboundaries! end

"""
    evolveboundaries!(boundary_component::BoundaryComponent, Δt::Number)

Evolves the boundary particles with the given time step.
"""
function evolveboundaries! end

include("boundary/none.jl")
include("boundary/wall.jl")