## Constants

const OPEN = -1

struct Polyhedron{Dp,Tp,Td}
  vertices::Vector{Point{Dp,Tp}}
  edge_vertex_graph::Vector{Vector{Int32}}
  isopen::Bool
  data::Td
end

struct PolyhedronData
  vertex_to_planes::Vector{Vector{Int32}}
  vertex_to_original_faces::Vector{Vector{Int32}}
  vertex_to_parent_vertex::Vector{Int32}
  vertex_to_parent_edge::Vector{Tuple{Int32,Int32}}
  plane_to_vertex_to_distances::Vector{Vector{Float64}}
  plane_to_ref_plane::Vector{Int32}
  plane_to_ids::Vector{Int32}
end

# Constructors

function Polyhedron(stl::STL)
  𝓖 = compute_graph(stl)
  X = get_vertex_coordinates(stl)
  isopen = is_open_surface(stl)
  p = Polyhedron(X,𝓖;isopen)
  set_original_faces!(p,stl)
  p
end

function Polyhedron(
  vertices::AbstractVector{<:Point},
  graph::Vector{<:Vector},
  isopen,
  data)

  Polyhedron(collect(vertices),graph,isopen,data)
end

function Polyhedron(
  vertices::AbstractVector{<:Point},
  graph::Vector{<:Vector}
  ;isopen=false::Bool)

  Polyhedron(vertices,graph,isopen,polyhedron_data(length(vertices)))
end

function Polyhedron(
  vertices::AbstractVector{<:Point},
  graph::Vector{<:Vector},
  data)

  isopen = false
  Polyhedron(vertices,graph,isopen,data)
end

function Polyhedron(p::Polytope{2},vertices::AbstractVector{<:Point})
  if p == TRI
    e_v_graph = [[2,3],[3,1],[1,2]]
  elseif p == QUAD
    e_v_graph = [[2, 3],[4, 1],[1, 4],[3, 2]]
  else
    @unreachable
  end
  e_v_graph = map(i->Int32.(i),e_v_graph)
  data = polyhedron_data(p)
  Polyhedron(vertices,e_v_graph,data)
end

function Polyhedron(p::Polytope{3},vertices::AbstractVector{<:Point})
  if p == TET
    e_v_graph = [[2,4,3],[3,4,1],[1,4,2],[1,2,3]]
  elseif p == HEX
    e_v_graph = [
      [5, 2, 3],
      [6, 4, 1],
      [7, 1, 4],
      [8, 3, 2],
      [1, 7, 6],
      [2, 5, 8],
      [3, 8, 5],
      [4, 6, 7] ]
  else
    @unreachable
  end
  e_v_graph = map(i->Int32.(i),e_v_graph)
  data = polyhedron_data(p)
  Polyhedron(vertices,e_v_graph,data)
end

function Polyhedron(p::Polytope)
  Polyhedron(p,get_vertex_coordinates(p))
end

# Operations

function clip(poly::Polyhedron,Π;inside=true,inout=trues(length(Π)),boundary=nothing)
  p = poly
  for (i,Πi) in enumerate(Π)
    side = inside == inout[i] ? :left : :right
    p = clip(p,Πi,side;boundary)
    !isnothing(p) || break
  end
  p
end

function split_overlapping(p::Polyhedron,Π)
  p⁻ = clip(p,Π,:left,boundary=true)
  p⁺ = clip(p,Π,:right,boundary=true)
  p⁻,p⁺
end

function split_gapping(p::Polyhedron,Π)
  p⁻ = clip(p,Π,:left,boundary=false)
  p⁺ = clip(p,Π,:right,boundary=false)
  p⁻,p⁺
end

function clip(p::Polyhedron,Π,side;boundary=nothing::Union{Nothing,Bool},invert=false)
  @assert side ∈ (:right,:left)
  if boundary === nothing
    p⁻,p⁺ = split(p,Π,side;invert)
    p  = side == :left ? p⁻ : p⁺
  else
    invert = false
    if side == :right && !boundary
      invert = true
      side = :left
    elseif side == :left && boundary
      invert = true
      side = :right
    end
    p = clip(p,Π,side;invert)
  end
  p
end

function split(p::Polyhedron,Π,side=:both;invert=false)
  @assert side ∈ (:both,:left,:right)
  distances = get_plane_distances(get_data(p),Π)
  _sign = invert ? (-) : (+)
  distances = lazy_map(_sign,distances)
  smin = Inf
  smax = -Inf
  for v in 1:num_vertices(p)
    isactive(p,v) || continue
    smin = min(smin,distances[v])
    smax = max(smax,distances[v])
  end
  if smin ≥ 0
    return nothing,p
  end
  if smax < 0
    if has_coplanars(get_data(p),Π)
      add_plane!(get_data(p),Π)
    end
    return p,nothing
  end
  new_vertices = empty( get_vertex_coordinates(p) )
  in_graph = _copy(get_graph(p))
  if side == :both
    out_graph = _copy(get_graph(p))
  end
  ≶ = side ∈ (:both,:left) ? (≥) : (<)
  data = copy( get_data(p) )
  D = num_dims(p)
  for v in 1:num_vertices(p)
    isactive(p,v) || continue
    distances[v] ≶ 0 && continue
    for (i,vneig) in enumerate( get_graph(p)[v] )
      vneig ∉ (UNSET,OPEN) || continue
      distances[vneig] ≶ 0 || continue
      vertex = compute_intersection(p[v],distances[v],p[vneig],distances[vneig])
      push!( new_vertices, vertex )
      add_vertex!(data,v,vneig,Π)
      push!( in_graph, fill(UNSET,D) )
      in_graph[v][i] = num_vertices(p) + length(new_vertices)
      in_graph[end][1] = v
      if side == :both
        push!( out_graph, fill(UNSET,D) )
        ineig = findfirst( isequal(v), get_graph(p)[vneig] )
        out_graph[vneig][ineig] = num_vertices(p) + length(new_vertices)
        out_graph[end][1] = vneig
      end
    end
  end
  if side == :both
    p_out = split_postprocess!(out_graph,copy(data),new_vertices,p,distances,(<))
  else
    p_out = nothing
  end
  p_in = split_postprocess!(in_graph,data,new_vertices,p,distances,(≶))
  p⁻ = side ≠ :right ? p_in : p_out
  p⁺ = side ≠ :right ? p_out : p_in
  p⁻,p⁺
end

function simplexify(poly::Polyhedron{3})
  !isopen(poly) || return simplexify_surface(poly)
  vstart = fill(UNSET,num_vertices(poly))
  stack = Int32[]
  for v in 1:num_vertices(poly)
    isactive(poly,v) || continue
    vstart[v] == UNSET || continue
    vstart[v] = v
    empty!(stack)
    push!(stack,v)
    while !isempty(stack)
      vcurrent = pop!(stack)
      for vneig in get_graph(poly)[vcurrent]
        if vstart[vneig] == UNSET
          vstart[vneig] = v
          push!(stack,vneig)
        end
      end
    end
  end
  v_to_pv = get_data(poly).vertex_to_parent_vertex
  istouch = map( i -> falses(length(i)), get_graph(poly) )
  vertex_coordinates = get_vertex_coordinates(poly)
  T = Vector{Int32}[]
  X = vertex_coordinates
  for v in 1:num_vertices(poly)
    isactive(poly,v) || continue
    for i in 1:length(get_graph(poly)[v])
      !istouch[v][i] || continue
      istouch[v][i] = true
      vcurrent = v
      vnext = get_graph(poly)[v][i]
      while vnext != v
        inext = findfirst( isequal(vcurrent), get_graph(poly)[vnext] )
        !isnothing(inext) || break
        inext = ( inext % length( get_graph(poly)[vnext] ) ) + 1
        istouch[vnext][inext] = true
        vcurrent = vnext
        vnext = get_graph(poly)[vnext][inext]
        @assert vcurrent ≠ vnext
        vcurrent ≠ vnext || break
        if v ∉ (vstart[v],vcurrent,vnext)
          k = [vstart[v],v,vcurrent,vnext]
          push!(T,k)
        end
      end
    end
  end
  T,X
end

function simplexify_surface(poly::Polyhedron{3})
  stack = Int32[]
  v_to_pv = get_data(poly).vertex_to_parent_vertex
  istouch = map( i -> falses(length(i)), get_graph(poly) )
  T = Vector{Int32}[]
  for v in 1:num_vertices(poly)
    isactive(poly,v) || continue
    for i in 1:length(get_graph(poly)[v])
      !istouch[v][i] || continue
      istouch[v][i] = true
      vcurrent = v
      vnext = get_graph(poly)[v][i]
      vnext ∉ (OPEN,UNSET) || continue
      while vnext != v
        inext = findfirst( isequal(vcurrent), get_graph(poly)[vnext] )
        inext = ( inext % length( get_graph(poly)[vnext] ) ) + 1
        istouch[vnext][inext] = true
        vcurrent = vnext
        vnext = get_graph(poly)[vnext][inext]
        vnext ∉ (OPEN,UNSET) || break
        if v ∉ (vcurrent,vnext)
          k = [v,vcurrent,vnext]
          push!(T,k)
        end
      end
    end
  end
  T,get_vertex_coordinates(poly)
end

function simplexify_boundary(poly::Polyhedron{3},stl::STL)
  D = 3
  stack = Int32[]
  v_to_pv = get_data(poly).vertex_to_parent_vertex
  if isopen(poly)
    v_to_Π = get_data(poly).vertex_to_original_faces
  else
    v_to_Π = get_data(poly).vertex_to_planes
  end
  facedims = get_facedims(stl)
  facet_list = Int32[]
  istouch = map( i -> falses(length(i)), get_graph(poly) )
  vertex_coordinates = get_vertex_coordinates(poly)
  T = Vector{Int32}[]
  X = empty(vertex_coordinates)
  f_to_stlf = Int32[]
  for v in 1:num_vertices(poly)
    isactive(poly,v) || continue
    for i in 1:length(get_graph(poly)[v])
      !istouch[v][i] || continue
      istouch[v][i] = true
      vcurrent = v
      vnext = get_graph(poly)[v][i]
      vnext ∉ (OPEN,UNSET) || continue
      empty!(facet_list)
      append!( facet_list, v_to_Π[v] )
      filter!( i -> i > 0, facet_list)
      filter!( i -> facedims[i] == num_dims(stl), facet_list)
      filter!( i -> i ∈ v_to_Π[vnext], facet_list )
      while vnext != v
        inext = findfirst( isequal(vcurrent), get_graph(poly)[vnext] )
        inext = ( inext % length( get_graph(poly)[vnext] ) ) + 1
        istouch[vnext][inext] = true
        vcurrent = vnext
        vnext = get_graph(poly)[vnext][inext]
        vnext ∉ (OPEN,UNSET) || break
        filter!( i -> i ∈ v_to_Π[vnext], facet_list )
        !isempty(facet_list) || break
        if v ∉ (vcurrent,vnext)
          kx = (v,vcurrent,vnext)
          k = (1:D) .+ length(X)
          for x in kx
            push!(X,vertex_coordinates[x])
          end
          push!(T,k)
          push!(f_to_stlf,first(facet_list))
        end
      end
    end
  end
  T,X,f_to_stlf
end

function simplexify_boundary(::Nothing,args...)
  nothing,nothing,nothing
end

function simplexify(polys::AbstractVector{<:Polyhedron{Dp,Tp}}) where {Dp,Tp}
  T = Vector{Int32}[]
  X = Point{Dp,Tp}[]
  for poly in polys
    Ti,Xi = simplexify(poly)
    append!(T, map(i->i.+length(X),Ti) )
    append!(X,Xi)
  end
  T,X
end

function simplexify_boundary(
  polys::AbstractVector{<:Polyhedron{Dp,Tp}},
  stl::STL) where {Dp,Tp}

  T = Vector{Int32}[]
  X = Point{Dp,Tp}[]
  f_to_stlf = Int32[]
  for poly in polys
    Ti,Xi,f_to_f_i = simplexify_boundary(poly,stl)
    append!(T, map(i->i.+length(X),Ti) )
    append!(X,Xi)
    append!(f_to_stlf,f_to_f_i)
  end
  T,X,f_to_stlf
end

function simplexify_boundary(::NTuple{N,Nothing},args...) where N
  nothing,nothing,nothing,nothing
end

function simplexify_boundary(p::Tuple,labels::Tuple,stl::STL)
  P = eltype(p[1])
  Dp = num_dims(P)
  Tp = point_eltype(P)
  T = Vector{Int32}[]
  X = Point{Dp,Tp}[]
  f_to_stlf = Int32[]
  f_to_ios = Int8[]
  for (p_i,ios) in zip( p, labels )
    Ti, Xi, f_to_f_i = simplexify_boundary(p_i,stl)
    if !isnothing(Ti)
      append!(T, map(i->i.+length(X),Ti) )
      append!(X,Xi)
      append!(f_to_stlf,f_to_f_i)
      append!(f_to_ios, fill(Int8(ios),length(Ti)) )
    end
  end
  T,X,f_to_stlf,f_to_ios
end

function surface(poly::Polyhedron{3})
  T,X = simplexify_surface(poly)
  p = TRI
  c = array_cache(T)
  !isempty(T) || return 0.0
  sum( i -> measure(get_cell!(c,T,X,p,i)), 1:length(T) )
end

function surface(poly::Polyhedron{3},stl::STL)
  T,X, = simplexify_boundary(poly,stl)
  p = TRI
  c = array_cache(T)
  !isempty(T) || return 0.0
  sum( i -> measure(get_cell!(c,T,X,p,i)), 1:length(T) )
end

function surface(polys::AbstractVector{<:Polyhedron},args...)
  s = 0.0
  for p in polys
    s += surface(p,args...)
  end
  s
end

function volume(poly::Polyhedron{3})
  T,X = simplexify(poly)
  p = TET
  c = array_cache(T)
  sum( i -> measure(get_cell!(c,T,X,p,i)), 1:length(T) )
end

function volume(polys::AbstractVector{<:Polyhedron},args...)
  v = 0.0
  for p in polys
    v += volume(p,args...)
  end
  v
end

function restrict(poly::Polyhedron,stl::STL,stl_facets)
  cell_to_nodes = get_cell_vertices(stl)
  c = array_cache(cell_to_nodes)
  nodes = Int32[]
  for f in stl_facets
    for n in getindex!(c,cell_to_nodes,f)
      if n ∉ nodes
        push!(nodes,n)
      end
    end
  end
  sort!(nodes)
  restrict(poly,nodes)
end

function restrict(p::Polyhedron,nodes)
  graph = get_graph(p)[nodes]
  f = i -> Int32( i ∈ nodes ? findfirst(isequal(i),nodes) : OPEN )
  graph = map(i->map(f,i),graph)
  data = restrict(get_data(p),nodes)
  vertices = get_vertex_coordinates(p)[nodes]
  isopen = true
  Polyhedron(vertices,graph,isopen,data)
end

function restrict(data::PolyhedronData,nodes)
  v_to_Π = data.vertex_to_planes[nodes]
  v_to_of = data.vertex_to_original_faces[nodes]
  v_to_v = collect(1:length(nodes))
  v_to_e = fill((UNSET,UNSET),length(nodes))
  Π_to_rΠ = copy(data.plane_to_ref_plane)
  Π_to_id = copy(data.plane_to_ids)
  _Π_to_v_to_d = data.plane_to_vertex_to_distances
  Π_to_v_to_d = [ _Π_to_v_to_d[i][nodes] for i in 1:length(Π_to_id) ]
  args = v_to_Π, v_to_of, v_to_v, v_to_e, Π_to_v_to_d, Π_to_rΠ, Π_to_id
  PolyhedronData(args...)
end

# Printers

function plot(p::Polyhedron,filename=nothing)
  vertices = Int32[]
  for i in 1:num_vertices(p)
    if isactive(p,i)
      push!(vertices,i)
    end
  end
  vertex_to_node = Dict( zip( vertices, 1:length(vertices) ) )
  g = empty( get_graph(p) )
  for (i,vneigs) in enumerate( get_graph(p) )
    isactive(p,i) || continue
    nodes = Int32[]
    for (j,vneig) in enumerate(vneigs)
      if vneig ∈ (UNSET,OPEN)
        push!(vertices,UNSET)
        n = length(vertices)
      else
        n = vertex_to_node[vneig]
      end
      push!(nodes,n)
    end
    push!(g,nodes)
  end
  v_to_pv = p.data.vertex_to_parent_vertex
  names = String[]
  for v in vertices
    if v == UNSET
      name = ""
    else
      name =  v_to_pv[v] == v ? "$v" : "$v($(v_to_pv[v]))"
    end
    push!(names,name)
  end
  kwargs = (fontsize=10,node_size=0,line=(:dot,0.5,1),thickness_scaling=0.5)
  GraphRecipes.graphplot(g,names=names,curves=false;kwargs...)
  isnothing(filename) || Plots.savefig(filename)
  Plots.plot!()
end

function writevtk(p::Polyhedron,filename;kwargs...)
  writevtk(edge_mesh(p),filename;kwargs...)
end

function writevtk(ps::Array{<:Polyhedron},filename)
  for (i,p) in enumerate(ps)
    writevtk(p,filename*"$i")
  end
end

# Getters

num_dims(::T) where T<:Polyhedron = num_dims(T)

num_dims(::Type{<:Polyhedron{D}}) where D = D

point_eltype(::T) where T<:Polyhedron = point_eltype(T)

point_eltype(::Type{<:Polyhedron{D,T}}) where {D,T} = T

num_vertices(a::Polyhedron) = length(a.vertices)

get_vertex_coordinates(a::Polyhedron) = a.vertices

Base.getindex(a::Polyhedron,i::Integer) = a.vertices[i]

@inline get_graph(a::Polyhedron) = a.edge_vertex_graph

get_data(a::Polyhedron) = a.data

Base.isopen(a::Polyhedron) = a.isopen

get_vertex_to_planes(a::PolyhedronData) = a.vertex_to_planes

function isactive(p::Polyhedron,vertex::Integer)
  !isempty( get_graph(p)[vertex] )
end

function get_original_reflex_faces(p::Polyhedron{D},stl::STL;empty=true) where D
  get_original_faces(p,stl,Val{D-2}();empty)
end

function get_original_facets(p::Polyhedron{D},stl::STL;empty=false) where D
  get_original_faces(p,stl,Val{D-1}();empty)
end

function get_original_faces(p::Polyhedron,stl::STL,::Val{d};empty) where d
  faces = collect_original_faces(p,stl,d)
  filter!(f->has_original_face(p,f,Val{d}();empty),faces)
  faces
end

function collect_original_faces(p::Polyhedron,stl::STL,d::Integer)
  v_to_f = get_data(p).vertex_to_original_faces
  facedims = get_facedims(stl)
  faces = Int32[]
  for v in 1:num_vertices(p)
    if isactive(p,v)
      for f ∈ v_to_f[v]
        if facedims[f] == d && f ∉ faces
          push!(faces,f)
        end
      end
    end
  end
  faces
end

function has_original_reflex_face(p::Polyhedron{D},face::Integer;empty=true) where D
  has_original_face(p,face,Val{D-2}();empty)
end

function has_original_facet(p::Polyhedron{D},face::Integer;empty=false) where D
  has_original_face(p,face,Val{D-1}();empty)
end

function has_original_face(p::Polyhedron,face::Integer,::Val{0};empty=false)
  v_to_f = get_data(p).vertex_to_original_faces
  for v in 1:num_vertices(p)
    if isactive(p,v) && face ∈ v_to_f[v]
      return true
    end
  end
  false
end

function has_original_face(p::Polyhedron,face::Integer,::Val{1};empty=true)
  v_to_f = get_data(p).vertex_to_original_faces
  v_to_v = get_data(p).vertex_to_parent_vertex
  for v in 1:num_vertices(p)
    if isactive(p,v) && face ∈ v_to_f[v]
      for vneig in get_graph(p)[v]
        vneig ∉ (OPEN,UNSET) || continue
        if face ∈ v_to_f[vneig] && ( empty || v_to_v[v] ≠ v_to_v[vneig] )
          return true
        end
      end
    end
  end
  false
end

function has_original_face(p::Polyhedron,face::Integer,::Val{2};empty=false)
  d = 2
  v_to_f = get_data(p).vertex_to_original_faces
  v_to_v = get_data(p).vertex_to_parent_vertex
  for v in 1:num_vertices(p)
    if isactive(p,v) && face ∈ v_to_f[v]
      for vneig in get_graph(p)[v]
        vneig ∉ (OPEN,UNSET) || continue
        if face ∈ v_to_f[vneig]
          num_v = 1
          vcurrent = v
          vnext = vneig
          while vnext ≠ v
            if v_to_v[vnext] ∉ (v_to_v[v],v_to_v[vcurrent]) || empty
              num_v += 1
            end
            vcurrent,vnext = vnext,next_vertex(p,vcurrent,vnext)
            vnext ∉ (UNSET,OPEN) || break
            face ∈ v_to_f[vnext] || break
          end
          if vnext == v && num_v ≥ d+1
            return true
          end
        end
      end
    end
  end
  false
end

function has_faces(p::Polyhedron,::Val{0})
  for v in 1:num_vertices(p)
    isactive(p,v) || continue
    return true
  end
  false
end

function has_faces(p::Polyhedron,::Val{1})
  for v in 1:num_vertices(p)
    isactive(p,v) || continue
    for vneig in get_graph(p)[v]
      @assert isactive(p,vneig)
      vneig ∉ (OPEN,UNSET) || continue
      v_to_v[v] ≠ v_to_v[vneig] || continue
      return true
    end
    @unreachable
  end
  false
end

function has_faces(p::Polyhedron,::Val{2})
  d = 2
  v_to_v = get_data(p).vertex_to_parent_vertex
  for v in 1:num_vertices(p)
    isactive(p,v) || continue
    for vneig in get_graph(p)[v]
      vneig ∉ (OPEN,UNSET) || continue
      num_v = 1
      vcurrent = v
      vnext = vneig
      while vnext ≠ v
        if v_to_v[vnext] ∉ (v_to_v[v],v_to_v[vcurrent])
          num_v += 1
        end
        vcurrent,vnext = vnext,next_vertex(p,vcurrent,vnext)
        vnext ∉ (UNSET,OPEN) || break
        if vnext == v && num_v ≥ d+1
          return true
        end
      end
    end
  end
  false
end

has_vertices(p::Polyhedron) = has_faces(p,Val{0}())

has_edges(p::Polyhedron) = has_faces(p,Val{1}())

has_facets(p::Polyhedron{D}) where D = has_faces(p,Val{D-1}())

function get_plane_distances(data::PolyhedronData)
  data.plane_to_vertex_to_distances
end

function get_plane_distances(data::PolyhedronData,Π)
  i = findfirst(isequal(Π),get_plane_ids(data))
  get_plane_distances(data)[i]
end

function get_plane_ids(data::PolyhedronData)
  data.plane_to_ids
end

function has_plane(data::PolyhedronData,Π)
  Π ∈ get_plane_ids(data)
end

## Helpers

function check_graph(p::Polyhedron)
  check_graph(get_graph(p))
end

function check_graph(graph::AbstractVector{<:AbstractVector})
  for v in 1:length(graph)
    !isempty(graph[v]) || continue
    for vneig in graph[v]
      vneig ∉ (OPEN,UNSET) || continue
      v ∈ graph[vneig] || return false
    end
  end
  true
end

function compact!(p::Polyhedron)
  ids = findall(i->!isactive(p,i),1:num_vertices(p))
  old_to_new = fill(UNSET,num_vertices(p))
  new = 0
  for i in 1:num_vertices(p)
    isactive(p,i) || continue
    new += 1
    old = i
    old_to_new[old] = new
  end
  vertices = get_vertex_coordinates(p)
  graph = get_graph(p)
  deleteat!(vertices,ids)
  deleteat!(graph,ids)
  f = i -> i ∈ (OPEN,UNSET) ? i : old_to_new[i]
  map!(i->map!(f,i,i),graph,graph)
  compact!(p.data,ids,old_to_new)
  p
end

function compact!(data::PolyhedronData,ids,old_to_new)
  v_to_Π = data.vertex_to_planes
  v_to_of = data.vertex_to_original_faces
  v_to_pv = data.vertex_to_parent_vertex
  v_to_pe = data.vertex_to_parent_edge
  Π_to_v_to_dist = data.plane_to_vertex_to_distances
  Π_to_ref_Π = data.plane_to_ref_plane
  Π_to_id = data.plane_to_ids

  deleteat!(v_to_Π,ids)
  deleteat!(v_to_of,ids)
  deleteat!(v_to_pv,ids)
  for (i,pv) in enumerate(v_to_pv)
    if old_to_new[pv] == UNSET
      old_to_new[pv] = i
    end
    v_to_pv[i] = old_to_new[pv]
  end
  deleteat!(v_to_pe,ids)
  f = i -> i == UNSET ? i : old_to_new[i]
  for (i,pe) in enumerate(v_to_pe)
    v_to_pe[i] = (f(pe[1]),f(pe[2]))
  end
  for v_to_dist in Π_to_v_to_dist
    deleteat!(v_to_dist,ids)
  end
  data
end

function Base.copy(poly::Polyhedron)
  vertices = get_vertex_coordinates(poly)
  graph = get_graph(poly)
  open = isopen(poly)
  data = copy(get_data(poly))
  Polyhedron(vertices,graph,open,data)
end

function Base.copy(data::PolyhedronData)
  v_to_Π = copy(data.vertex_to_planes)
  v_to_of = copy(data.vertex_to_original_faces)
  v_to_pv = copy(data.vertex_to_parent_vertex)
  v_to_pe = copy(data.vertex_to_parent_edge)
  Π_to_v_to_dist = _copy(data.plane_to_vertex_to_distances)
  Π_to_ref_Π = copy(data.plane_to_ref_plane)
  Π_to_id = copy(data.plane_to_ids)
  args = v_to_Π, v_to_of, v_to_pv, v_to_pe, Π_to_v_to_dist, Π_to_ref_Π, Π_to_id
  PolyhedronData(args...)
end

function compute_distances!(p::Polyhedron,Π,faces;atol=0)
  data = get_data(p)
  v_to_f = get_data(p).vertex_to_original_faces
  Π_to_v_to_d = get_plane_distances(data)
  Π_ids = get_plane_ids(data)
  Π_to_rΠ = data.plane_to_ref_plane
  num_Π = length(Π_ids)
  append!(Π_ids,faces)
  append!(Π_to_rΠ,fill(UNSET,length(faces)))
  append!(Π_to_v_to_d, [ zeros(num_vertices(p)) for _ in faces ] )
  for (i,(Πi,fi)) in enumerate(zip(Π,faces))
    v_to_d = Π_to_v_to_d[i+num_Π]
    for v in 1:num_vertices(p)
      if fi ∈ v_to_f[v]
        dist = 0.0
      else
        dist = signed_distance(p[v],Πi)
        if abs(dist) < atol
          dist = 0.0
        end
      end
      v_to_d[v] = dist
    end
  end
end

function edge_mesh(p::Polyhedron)
  edge_to_vertices = Vector{Int32}[]
  for v in 1:num_vertices(p)
    for vneig in get_graph(p)[v]
      if vneig > v
        push!(edge_to_vertices,[v,vneig])
      end
    end
  end
  X = get_vertex_coordinates(p)
  T = Table(edge_to_vertices)
  UnstructuredGrid(X,T,[SEG2],fill(1,length(T)))
end

function compute_graph(stl::STL{2})
  @notimplementedif get_polytope(stl) ≠ TRI
  D = 2
  v_to_e = get_faces(stl,0,1)
  v_to_f = get_faces(stl,0,D)
  f_to_v = get_faces(stl,D,0)
  ec = array_cache(v_to_e)
  fc = array_cache(v_to_f)
  vc = array_cache(f_to_v)
  graph = [ Int32[] for _ in 1:num_vertices(stl) ]
  for v in 1:num_vertices(stl)
    vfacets = getindex!(fc,v_to_f,v)
    !isempty(vfacets) || continue
    f0 = first(vfacets)
    fnext = f0
    while true
      i = findfirst(isequal(v), getindex!(vc,f_to_v,fnext) )
      inext = i == D+1 ? 1 : i+1
      vnext = getindex!(vc,f_to_v,fnext)[inext]
      push!(graph[v],vnext)
      faces = getindex!(fc,v_to_f,vnext)
      i = findfirst( f -> f ≠ fnext && v ∈ getindex!(vc,f_to_v,f), faces )
      fnext = isnothing(i) ? UNSET : faces[i]
      fnext ≠ UNSET || break
      fnext ≠ f0 || break
    end
    if fnext == UNSET
      n_edges = length( getindex!(ec,v_to_e,v) )
      if length(graph[v]) < n_edges
        v0 = first(graph[v])
        fnext = f0
        while fnext ≠ UNSET
          i = findfirst(isequal(v), getindex!(vc,f_to_v,fnext) )
          inext = i == 1 ? D+1 : i-1
          vnext = getindex!(vc,f_to_v,fnext)[inext]
          pushfirst!(graph[v],vnext)
          faces = getindex!(fc,v_to_f,vnext)
          i = findfirst( f -> f ≠ fnext && v ∈ getindex!(vc,f_to_v,f), faces )
          fnext = isnothing(i) ? UNSET : faces[i]
        end
        @assert length(graph[v]) == n_edges
      end
      push!(graph[v],OPEN)
    end
  end
  check_graph(graph) || error("Unable to build vertex-edge graph")
  graph
end

function set_original_faces!(p::Polyhedron,a...)
  set_original_faces!(get_data(p),a...)
  p
end

function polyhedron_data(num_vertices::Integer)
  v_to_Π = [ Int32[] for _ in 1:num_vertices ]
  v_to_of = [ Int32[] for _ in 1:num_vertices ]
  v_to_v = collect(1:num_vertices)
  v_to_e = fill((UNSET,UNSET),num_vertices)
  Π_to_v_to_d = Vector{Int32}[]
  Π_to_rΠ = Int32[]
  Π_to_id = Int32[]
  args = v_to_Π, v_to_of, v_to_v, v_to_e, Π_to_v_to_d, Π_to_rΠ, Π_to_id
  PolyhedronData(args...)
end

function polyhedron_data(p::Polytope)
  v_to_Π = map( i-> Int32.(i), -get_faces(p,0,num_dims(p)-1) )
  v_to_of = [ Int32[] for _ in 1:num_vertices(p) ]
  v_to_v = Int32.(collect(1:num_vertices(p)))
  v_to_e = fill((Int32(UNSET),Int32(UNSET)),num_vertices(p))
  Π_to_v_to_d = Vector{Int32}[]
  Π_to_rΠ = Int32[]
  Π_to_id = Int32[]
  args = v_to_Π, v_to_of, v_to_v, v_to_e, Π_to_v_to_d, Π_to_rΠ, Π_to_id
  PolyhedronData(args...)
end

@inline function split_postprocess!(graph,data,new_vertices,input_poly,distances,(≶))
  complete_graph!(graph,num_vertices(input_poly))
  disconnect_graph!(graph,num_vertices(input_poly),distances,(≶))
  add_open_vertices!(graph,input_poly)
  vertices = [input_poly.vertices;new_vertices]
  update_data!(data,graph,num_vertices(input_poly))
  poly = Polyhedron(vertices,graph,isopen(input_poly),data)
  compact!(poly)
end

function complete_graph!(edge_graph,num_vertices::Integer)
  for v in num_vertices+1:length(edge_graph)
    vnext = next_vertex(edge_graph,num_vertices,v)
    vnext ∉ (UNSET,OPEN) || continue
    edge_graph[v][end] = vnext
    edge_graph[vnext][2] = v
  end
end

function add_open_vertices!(graph,poly::Polyhedron)
  if isopen(poly)
    for v in num_vertices(poly)+1:length(graph)
      if UNSET ∉ graph[v]
        push!(graph[v],graph[v][end])
        graph[v][end-1] = OPEN
      end
    end
  end
end

function disconnect_graph!(edge_graph,num_vertices,distances,(≶)::Function)
  for i in 1:num_vertices
    if distances[i] ≶ 0
      edge_graph[i] = empty!( edge_graph[i] )
    end
  end
end

function update_data!(data,graph,num_vertices)
  v_to_pv = data.vertex_to_parent_vertex
  v_to_pe = data.vertex_to_parent_edge
  Π_to_v_to_d = get_plane_distances(data)
  for v in num_vertices+1:length(graph)
    v_to_pv[v] == v || continue
    for vnext in num_vertices+1:length(graph)
      v_to_pv[vnext] == vnext || continue
      if v_to_pe[v] == v_to_pe[vnext] && v_to_pe[v] ≠ (UNSET,UNSET)
        v_to_pv[vnext] = v
        for v_to_d in Π_to_v_to_d
          v_to_d[v] = v_to_d[v_to_pv[v]]
        end
      end
    end
  end
end

function next_vertex(edge_graph::Vector,num_vertices::Integer,vstart::Integer)
  vcurrent = vstart
  vnext = first( edge_graph[vcurrent] )
  while vnext ≤ num_vertices && vnext ∉ (UNSET,OPEN)
    i = findfirst( isequal(vcurrent), edge_graph[vnext] )
    vcurrent = vnext
    inext = ( i % length( edge_graph[vcurrent] ) ) + 1
    vnext = edge_graph[vcurrent][ inext ]
  end
  vnext
end

function compute_intersection(p1::Point,d1::Real,p2::Point,d2::Real)
  ( p1*(abs(d2))+p2*(abs(d1)) ) / ( abs(d1) + abs(d2) )
end

add_vertex!(data::Nothing,a...) = data

function _intersect(a::AbstractVector{T},b::AbstractVector{T}) where T
  n = count(in(a),b)
  r = Vector{T}(undef,n)
  n = 0
  for i in a
    if i ∈ b
      n += 1
      r[n] = i
    end
  end
  r
end

function _copy(a::AbstractVector{<:AbstractVector})
  [ copy(i) for i in a ]
end

function add_vertex!(data::PolyhedronData,v1::Integer,v2::Integer,Πid::Integer)
  v_to_Π = data.vertex_to_planes
  v_to_f = data.vertex_to_original_faces
  v_to_pv = data.vertex_to_parent_vertex
  v_to_pe = data.vertex_to_parent_edge
  Πs = _intersect( v_to_Π[v1], v_to_Π[v2] )
  fs = _intersect( v_to_f[v1], v_to_f[v2] )
  pe = (v_to_pv[v1],v_to_pv[v2])
  push!( Πs, Πid )
  push!( v_to_Π, Πs )
  push!( v_to_f, fs )
  push!( v_to_pe, pe)
  Π_to_v_to_d = get_plane_distances(data)
  Π_to_id = get_plane_ids(data)
  i = findfirst(isequal(Πid),Π_to_id)
  d1 = Π_to_v_to_d[i][v1]
  d2 = Π_to_v_to_d[i][v2]
  pv = iszero(d1) ? v_to_pv[v1] : length(v_to_pv)+1
  pv = iszero(d2) ? v_to_pv[v2] : pv
  push!(v_to_pv,pv)
  @assert d1 ≠ d2
  ref_i = data.plane_to_ref_plane[i]
  for i in 1:length(Π_to_v_to_d)
    v_to_d = Π_to_v_to_d[i]
    if pv ≠ length(v_to_pv)
      dist = v_to_d[pv]
    elseif ref_i == UNSET || data.plane_to_ref_plane[i] ∈ (UNSET,i)
      if Π_to_id[i] == Πid
        dist = 0.0
      else
        dist = (d1*v_to_d[v2]-d2*v_to_d[v1])/(d1-d2)
      end
    else
      j = data.plane_to_ref_plane[i]
      @assert abs(j) < i
      dist = sign(j) * Π_to_v_to_d[abs(j)][end]
    end
    push!(v_to_d,dist)
  end
  data
end

function set_original_faces!(data::PolyhedronData,stl::STL)
  Dc = num_dims(stl)
  v_to_f = data.vertex_to_original_faces
  for d in 0:Dc
    offset = get_offset(stl,d)
    _v_to_f = get_faces(stl,0,d)
    c = array_cache(_v_to_f)
    for v in 1:length(v_to_f)
      append!( v_to_f[v], map( i -> i.+offset, getindex!(c,_v_to_f,v) ) )
    end
  end
  data
end

function next_vertex(p::Polyhedron,vprevious::Integer,vcurrent::Integer)
  i = findfirst( isequal(vprevious), get_graph(p)[vcurrent] )
  i = ( i % length( get_graph(p)[vcurrent] ) ) + 1
  get_graph(p)[vcurrent][ i ]
end

## Kd Tree stuff

function refine_by_vertices(Γ::Polyhedron,K::Polyhedron,atol=0)
  refine_by_vertices(Γ,K,get_vertex_coordinates(Γ),atol)
end

function refine_by_vertices(Γ,K,vertices,atol)
  i = findfirst( v -> has_intersection(K,v), vertices )
  if isnothing(i)
    R = [Γ],[K]
  else
    v = vertices[i]
    d = get_direction(v,K)
    Π,Π⁻,Π⁺ = get_planes(v,d,atol)
    Πid,Πid⁻,Πid⁺ = get_new_plane_ids(Γ)
    compute_distances!(Γ,[Π⁻,Π⁺],[Πid⁻,Πid⁺])
    compute_distances!(K,[Π],[Πid])
    _,γ⁻ = split(Γ,Πid⁺)
    _,γ⁺ = split(Γ,Πid⁻)
    k⁻,k⁺ = split(K,Πid)
    round_distances!(k⁻,atol/10)
    round_distances!(k⁺,atol/10)
    Γn = [γ⁻,γ⁺]
    Kn = [k⁻,k⁺]
    if length(vertices) == i
      R = Γn,Kn
    else
      Γr,Kr = empty(Γn),empty(Kn)
      vertices = view(vertices,i+1:length(vertices))
      for (γ,k) in zip(Γn,Kn)
        Γi,Ki = refine_by_vertices(γ,k,vertices,atol)
        append!(Γr,Γi)
        append!(Kr,Ki)
      end
      R = Γr,Kr
    end
  end
  R
end

function get_bounding_box(cell::Polyhedron)
  i = findfirst(i->isactive(cell,i),1:num_vertices(cell))
  pmin,pmax = cell[i],cell[i]
  for i in 1:num_vertices(cell)
    if isactive(cell,i)
      pmin = Point( min.(Tuple(cell[i]),Tuple(pmin)) )
      pmax = Point( max.(Tuple(cell[i]),Tuple(pmax)) )
    end
  end
  pmin,pmax
end

function has_intersection(cell::Polyhedron,v::Point)
  pmin,pmax = get_bounding_box(cell)
  all( Tuple(pmin) .< Tuple(v) ) || return false
  all( Tuple(pmax) .> Tuple(v) ) || return false
  true
end

function get_direction(v,cell)
  pmin,pmax = get_bounding_box(cell)
  d0 = Tuple(v) .- Tuple(pmin)
  d1 = Tuple(pmax) .- Tuple(v)
  d = min.(d0,d1)
  d_max = maximum(d)
  findfirst(isequal(d_max),d)
end

function get_planes(v,d,atol)
  δ = VectorValue(Base.setindex(Tuple(zero(v)),atol,d))
  Π = CartesianPlane(v,d,+1)
  Π⁻ = CartesianPlane(v-δ,d,+1)
  Π⁺ = CartesianPlane(v+δ,d,-1)
  Π,Π⁻,Π⁺
end

function get_new_plane_ids(poly)
  id = minimum( get_plane_ids(poly.data) )
  id-1,id-2,id-3
end

function round_distances!(poly,atol)
  Π_to_v_to_dist = poly.data.plane_to_vertex_to_distances
  for i in 1:length(Π_to_v_to_dist)
    v_to_dist = Π_to_v_to_dist[i]
    for v in 1:length(v_to_dist)
      if abs(v_to_dist[v]) < atol
        v_to_dist[v] = 0.0
      end
    end
  end
end

function delete_inactive_planes!(Γ::Polyhedron,K::Polyhedron,stl::STL)
  @assert isopen(Γ)
  planes = get_active_planes(Γ,K,stl)
  restrict_planes!(Γ,planes)
  restrict_planes!(K,planes)
end

function get_active_planes(Γ::Polyhedron,K::Polyhedron,stl::STL)
  used_planes_Γ = _get_used_planes(K)
  used_planes_K = _get_used_planes(Γ)
  used_planes = lazy_append( used_planes_Γ, used_planes_K )
  face_planes = _get_face_planes(Γ,stl)
  planes = lazy_append( used_planes, face_planes )
  _append_ref_planes( Γ, planes )
end

function restrict_planes!(poly::Polyhedron,planes)
  restrict_planes!(get_data(poly),planes)
end

function restrict_planes!(data::PolyhedronData,planes)
  Π_to_id = data.plane_to_ids
  Π_to_v_to_dist = data.plane_to_vertex_to_distances
  Π_to_ref_Π = data.plane_to_ref_plane
  is_inactive = trues(length(Π_to_id))
  is_touch = falses(length(Π_to_id))
  for Π in planes
    i = findfirst(isequal(Π),Π_to_id)
    !isnothing(i) || continue
    is_inactive[i] = false
  end
  id = 0
  for i in 1:length(Π_to_id)
    if !is_inactive[i]
      id += 1
      if !is_touch[i]
        ref_plane = abs( Π_to_ref_Π[i] )
        if ref_plane != UNSET
          @assert i == ref_plane
          for j in i:length(Π_to_id)
            if !is_inactive[j] &&
               !is_touch[j] &&
               abs( Π_to_ref_Π[j] ) == ref_plane

              _sign = sign( Π_to_ref_Π[j] )
              Π_to_ref_Π[j] = _sign * id
              is_touch[j] = true
            end
          end
        end
      end
    end
  end
  deleteat!( Π_to_v_to_dist, is_inactive)
  deleteat!( Π_to_ref_Π, is_inactive)
  deleteat!( Π_to_id, is_inactive)
end

function _get_used_planes(poly::Polyhedron)
  v_to_Π = get_data(poly).vertex_to_planes
  planes = Int32[]
  for v in 1:length(v_to_Π)
    for Π in v_to_Π[v]
      if Π ∉ planes
        push!(planes,Π)
      end
    end
  end
  planes
end

function _get_face_planes(poly::Polyhedron,stl::STL)
  rfaces = get_original_reflex_faces(poly,stl,empty=true)
  facets = get_original_facets(poly,stl,empty=true)
  lazy_append(rfaces,facets)
end

function _append_ref_planes(poly::Polyhedron,planes)
  ref_planes = Int32[]
  Π_to_ref_Π = get_data(poly).plane_to_ref_plane
  Π_to_id = get_plane_ids( get_data(poly) )
  for Π in planes
    i = findfirst( isequal(Π), Π_to_id )
    i !== nothing || continue
    iref = abs(Π_to_ref_Π[i])
    if iref ≠ UNSET
      Π_ref = Π_to_id[iref]
      if Π_ref ∉ ref_planes && Π_ref ∉ planes
        push!(ref_planes,Π_ref)
      end
    end
  end
  lazy_append(planes,ref_planes)
end


module LibSpatialIndex

    include("capi.jl")

    version() = unsafe_string(C.SIDX_Version())

    function _checkresult(result::C.RTError, message::String)
        if result == C.RT_Failure || result == C.RT_Fatal
            error(message)
        end
    end

    """
    The RTree index [guttman84] is a balanced tree structure that consists of
    index nodes, leaf nodes and data.

    Every node (leaf and index) has a fixed capacity of entries, (the node
    capacity) chosen at index creation An `RTree` abstracts the data with their
    Minimum Bounding Region (`MBR`) and clusters these MBRs according to various
    heuristics in the leaf nodes. Queries are evaluated from the root of the
    tree down the leaves. Since the index is balanced nodes can be under full.
    They cannot be empty though. A `fillfactor` specifies the minimum number of
    entries allowed in any node. The fill factor is usually close to `70%`.

    # Options

    * `ndim`: Dimensionality of the data that will be inserted.
    * `indextype`: one of `RT_RTree` (default), `RT_MVRTree`, or `RT_TPRTree`.
    * `variant`: one of `RT_Linear`, `RT_Quadratic`, or `RT_Star` (default).
    * `storage`: one of `RT_Memory` (default), `RT_Disk`, or `RT_Custom`.
    * `indexcapacity`: The index node capacity. Default is `100`.
    * `leafcapacity`: The leaf node capacity. Default is `100`.
    * `leafpoolcapacity`: Default is `100`.
    * `indexpoolcapacity`: Default is `100`.
    * `regionpoolcapacity`: Default is `1000`.
    * `pointpoolcapacity`: Default is `500`.
    * `tightMBR`: Default is `true`.
    * `nearminimumoverlapfactor`: Default is `32`.
    * `fillfactor`: The fill factor. Default is `0.7`.
    * `splitdistributionfactor`: Default is `0.4`.
    * `reinsertfactor`: Default is `0.3`.

    # Performance

    Dataset size, data density, etc. have nothing to do with capacity and page
    size. What you are trying to achieve is fast reads from the disk and take
    advantage of disk buffering and prefetching.

    Normally, you select the page size to be equal to the disk page size
    (depends on how you format the drive). Then you choose the node capacity to
    be enough to fill the whole page (including data entries if you have any).

    There is a tradeoff though in making node capacity too large. The larger the
    capacity, the longer the “for loops” for inserting, deleting, locating node
    entries take (CPU time). On the other hand, the larger the capacity the
    shorter the tree becomes, reducing the number of random IOs to reach the
    leaves. Hence, you might want to fit multiple nodes (of smaller capacity)
    inside a single page to balance I/O and CPU cost, in case your disk page
    size is too large.

    Finally, if you have enough buffer space to fit most of the index nodes in
    main memory, then large capacities do not make too much sense, because the
    height of the tree does not matter any more. Of course, the smaller the
    capacity, the larger the number of leaf nodes you will have to retrieve from
    disk for range queries (point queries and nearest neighbor queries will not
    suffer that much). So very small capacities hurt as well.

    There is another issue when trying to fit multiple nodes per page: Leftover
    space. You might have leftover space due to data entries that do not have a
    fixed size. Unfortunately, in that case, leftover space per page is lost,
    negatively impacting space usage.

    Selecting the right page size is easy; make it equal to the disk page size.
    Selecting the right node capacity is an art.

    # References
    [guttman84] “R-Trees: A Dynamic Index Structure for Spatial Searching”
        Antonin Guttman, Proc. 1984 ACM-SIGMOD Conference on Management of
        Data (1985), 47-57.
    """
    mutable struct RTree
        index::C.IndexH
        indextype::C.RTIndexType
        ndim::UInt32
        variant::C.RTIndexVariant
        storage::C.RTStorageType
        indexcapacity::UInt32
        leafcapacity::UInt32
        leafpoolcapacity::UInt32
        indexpoolcapacity::UInt32
        regionpoolcapacity::UInt32
        pointpoolcapacity::UInt32
        tightMBR::Bool
        nearminimumoverlapfactor::UInt32
        fillfactor::Float64
        splitdistributionfactor::Float64
        reinsertfactor::Float64

        function RTree(ndim::Integer;
                indextype::C.RTIndexType = C.RT_RTree,
                variant::C.RTIndexVariant = C.RT_Star,
                storage::C.RTStorageType = C.RT_Memory,
                indexcapacity::Integer = 100,
                leafcapacity::Integer = 100,
                leafpoolcapacity::Integer = 100,
                indexpoolcapacity::Integer = 100,
                regionpoolcapacity::Integer = 1000,
                pointpoolcapacity::Integer = 500,
                tightMBR::Bool = true,
                nearminimumoverlapfactor::Integer = 32,
                fillfactor::Real = 0.7,
                splitdistributionfactor::Real = 0.4,
                reinsertfactor::Real = 0.3
            )
            p = C.IndexProperty_Create()
            C.IndexProperty_SetIndexType(p, indextype)
            C.IndexProperty_SetDimension(p, UInt32(ndim))
            C.IndexProperty_SetIndexVariant(p, variant)
            C.IndexProperty_SetIndexStorage(p, storage)
            C.IndexProperty_SetIndexCapacity(p, UInt32(indexcapacity))
            C.IndexProperty_SetLeafCapacity(p, UInt32(leafcapacity))
            C.IndexProperty_SetLeafPoolCapacity(p, UInt32(leafpoolcapacity))
            C.IndexProperty_SetIndexPoolCapacity(p, UInt32(indexpoolcapacity))
            C.IndexProperty_SetRegionPoolCapacity(p, UInt32(regionpoolcapacity))
            C.IndexProperty_SetPointPoolCapacity(p, UInt32(pointpoolcapacity))
            # IndexProperty_SetBufferingCapacity(iprop::IndexPropertyH, value::UInt32)
            C.IndexProperty_SetEnsureTightMBRs(p, UInt32(tightMBR))
            # IndexProperty_SetOverwrite(iprop::IndexPropertyH, value::UInt32)
            C.IndexProperty_SetNearMinimumOverlapFactor(p, UInt32(nearminimumoverlapfactor))
            # IndexProperty_SetWriteThrough(iprop::IndexPropertyH, value::UInt32)
            C.IndexProperty_SetFillFactor(p, fillfactor)
            C.IndexProperty_SetSplitDistributionFactor(p, splitdistributionfactor)
            # IndexProperty_SetTPRHorizon(iprop::IndexPropertyH, value::Cdouble)
            C.IndexProperty_SetReinsertFactor(p, reinsertfactor)
            # IndexProperty_SetFileName(iprop::IndexPropertyH, value)
            # IndexProperty_SetFileNameExtensionDat(iprop::IndexPropertyH, value)
            # IndexProperty_SetFileNameExtensionIdx(iprop::IndexPropertyH, value)
            # IndexProperty_SetCustomStorageCallbacksSize(iprop::IndexPropertyH, value::UInt32)
            # IndexProperty_SetCustomStorageCallbacks(iprop::IndexPropertyH, value)
            # IndexProperty_SetIndexID(iprop::IndexPropertyH, value::Int64)
            index = C.Index_Create(p)
            Bool(C.Index_IsValid(index)) || error("Invalid index")
            C.IndexProperty_Destroy(p)
            rtree = new(index, indextype, ndim, variant, storage, indexcapacity,
                leafcapacity, leafpoolcapacity, indexpoolcapacity,
                regionpoolcapacity, pointpoolcapacity, tightMBR,
                nearminimumoverlapfactor, fillfactor, splitdistributionfactor,
                reinsertfactor)
            finalizer( x -> (C.Index_Destroy(x.index); x.index = C_NULL), rtree)
            rtree
        end
    end

    """
    Inserts an item into the `rtree` with given `id` and boundingbox specified
    by `minvalues` and `maxvalues`, where the item lies within the interval
    `[minvalues[i],maxvalues[i]]` for each axis `i` in 1, ..., `ndim`.
    """
    function insert!(
            rtree::RTree,
            id::Integer,
            minvalues::Vector{Float64},
            maxvalues::Vector{Float64}
        )
        C.Index_InsertData(rtree.index, Int64(id), pointer(minvalues),
            pointer(maxvalues), UInt32(length(minvalues)), Ptr{UInt8}(0), Cint(0)
        )
    end

    """
    Returns a vector of `id`s corresponding to items in `rtree` that intersects
    the box specified by `minvalues` and `maxvalues`.

    Each item intersects the interval `[minvalues[i],maxvalues[i]]` for each
    axis `i` in 1, ..., `ndim`.
    """
    function intersects(
            rtree::RTree,
            minvalues::Vector{Float64},
            maxvalues::Vector{Float64}
        )
        items = Ref{Ptr{Int64}}()
        nresults = Ref{UInt64}()
        result = C.Index_Intersects_id(rtree.index, pointer(minvalues),
            pointer(maxvalues), UInt32(length(minvalues)), items, nresults
        )
        _checkresult(result, "Index_Intersects_id: Failed to evaluate")
        unsafe_wrap(Array, items[], nresults[])
    end

    """
    Returns a vector of `id`s corresponding to items in `rtree` that intersects
    the coordinates specified by `point`.
    """
    intersects(rtree::RTree, point::Vector{Float64}) = intersects(rtree, point, point)

    """
    Returns a vector of `id`s corresponding to the `k` items in `rtree`
    that are nearest to the box specified by `minvalues` and `maxvalues`.

    Each item intersects the interval `[minvalues[i],maxvalues[i]]` for each
    axis `i` in 1, ..., `ndim`. If there are fewer than `k` items in `rtree`,
    it will return less than `k` items. On the other hand, if there are ties
    between some of the items, it might return more than `k` items.
    """
    function knn(
            rtree::RTree,
            minvalues::Vector{Float64},
            maxvalues::Vector{Float64},
            k::Integer
        )
        items = Ref{Ptr{Int64}}()
        nresults = Ref{UInt64}(k)
        result = C.Index_NearestNeighbors_id(rtree.index, pointer(minvalues),
            pointer(maxvalues), UInt32(length(minvalues)), items, nresults)
        _checkresult(result, "Index_NearestNeighbors_id: Failed to evaluate")
        unsafe_wrap(Array, items[], nresults[])
    end

    """
    Returns a vector of `id`s corresponding to the `k` items in `rtree`
    that are nearest to the box specified by `minvalues` and `maxvalues`.

    If there are fewer than `k` items in `rtree`, it will return less than `k`
    items. On the other hand, if there are ties between some of the items,
    it might return more than `k` items.
    """
    knn(rtree::RTree, point::Vector{Float64}, k::Integer) = knn(rtree, point, point, k)

end # module


#File Name: barcodeCounter.jl
#Author: Henry C. Wong (wongh@wustl.edu,henrycwong@mst.edu,cywongx@gmail.com)
#Descr:     Julia file that finds the barcodes of fastq file based on a given spacer sequence
#Dependecies:
#  Julia 1.4.2
#  FASTX
#  DelimitedFiles
#
#  Arguments: barcodeDictionary.tsv *.fastq *.info
#    1) barcode dictionary in a tsv format where the first row are barcodes and the second row are names
#    2) a fastq file
#    3) a file that contains the spacer sequence (and only the spacer sequence)

include("barcodeCounterFuncts.jl");
using DelimitedFiles
using FASTX

#read barcode (dictionary) file
grid = readBarcodeFile(ARGS[1]);
#spin off barcodes and sequence names into their own file
barcodeArray = grid[:,1];
names = grid[:,2];
#create empty array to fill counts with
counts = fill(0,length(names));
#create dictionaries to align names, barcodes, and counts
countDict = Dict(barcodeArray .=> counts);
nameDict = Dict(barcodeArray .=> names);

#fill dictionaries with data from fastq file
fillDict(ARGS[2],countDict,nameDict);
#merge both dictionaries to prepare to output data to file
contentArray = syncDictsToArray(countDict,nameDict);
#sort combined matrix by descending order
sortedContentArr = sortslices(contentArray,dims=1,by=x->x[3],rev=true);
#create file name
fileName = createOutputFileName(ARGS[2]);
#write to file
writeResultsToFile(sortedContentArr,fileName);


module SignalTemps

using CUDA, Distributed
using LinearAlgebra
using DataFrames,Query
using StatsBase, Distributions, Random
using UMAP
using LightGraphs,MetaGraphs

export getalpha, getbeta, getkappa, gettemps, safediv, getup, getupavg, groupavg, getgroups
export filterdf,filteravg,filtersum,umapexpr,sublabels,fplot,filterexpressed
export lrtManual

include("temps.jl") # calculate interactions

include("data.jl") # load in the data

include("lrtManual.jl") # manual lrt ident, uses OrthoDB ortholog (eg danio) search from PCquery given primary (eg human) pathway genes
end


# From https://discourse.julialang.org/t/aliases-for-union-t-nothing-and-union-t-missing/15402/4
const Maybe{T} = Union{T,Nothing}

# The following default values are picked from `<QE source>/Modules/read_namelists.f90`
"""
    ControlNamelist <: Namelist

Represent the `CONTROL` namelist of `cp.x`.
"""
@with_kw struct ControlNamelist <: Namelist
    calculation::String = "cp"
    title::String = "MD Simulation"
    verbosity::String = "low"
    isave::Int = 100
    restart_mode::String = "restart"
    nstep::Int = 50
    iprint::Int = 10
    tstress::Bool = false
    tprnfor::Bool = false
    dt::Float64 = 1.0
    outdir::String = "./"
    saverho::Bool = true
    prefix::String = "cp"
    ndr::Int = 50
    ndw::Int = 50
    tabps::Bool = false
    max_seconds::Float64 = 1e7
    etot_conv_thr::Float64 = 1e-4
    forc_conv_thr::Float64 = 1e-3
    ekin_conv_thr::Float64 = 1e-6
    disk_io::String = "default"
    memory::String = "default"
    pseudo_dir::String = raw"$HOME/espresso/pseudo/"
    tefield::Bool = false
    # These checks are from https://github.com/QEF/q-e/blob/4132a64/Modules/read_namelists.f90#L1282-L1369.
    @assert(
        calculation in
        ("cp", "scf", "nscf", "relax", "vc-relax", "vc-cp", "cp-wf", "vc-cp-wf")
    )
    @assert verbosity in ("high", "low", "debug", "medium", "default", "minimal")
    @assert isave >= 1
    @assert nstep >= 0
    @assert iprint >= 1
    @assert dt >= 0
    @assert ndr >= 50
    @assert ndw <= 0 || ndw >= 50
    @assert max_seconds >= 0
    @assert etot_conv_thr >= 0
    @assert forc_conv_thr >= 0
    @assert ekin_conv_thr >= 0
    @assert memory in ("small", "default", "large")
end # struct ControlNamelist

"""
    SystemNamelist <: Namelist

Represent the `SYSTEM` namelist of `cp.x`.
"""
@with_kw struct SystemNamelist <: Namelist
    ibrav::Int = -1
    celldm::Vector{Maybe{Float64}} = zeros(6)  # Must specify
    A::Float64 = 0.0
    B::Float64 = 0.0
    C::Float64 = 0.0
    cosAB::Float64 = 0.0
    cosAC::Float64 = 0.0
    cosBC::Float64 = 0.0
    nat::Int = 0
    ntyp::Int = 0
    nbnd::Int = 0
    tot_charge::Float64 = 0.0
    tot_magnetization::Float64 = -1.0
    ecutwfc::Float64 = 0.0
    ecutrho::Float64 = 0.0
    nr1::Int = 0
    nr2::Int = 0
    nr3::Int = 0
    nr1s::Int = 0
    nr2s::Int = 0
    nr3s::Int = 0
    nr1b::Int = 0
    nr2b::Int = 0
    nr3b::Int = 0
    occupations::String = "fixed"
    degauss::Float64 = 0.0
    smearing::String = "gaussian"
    nspin::Int = 1
    ecfixed::Float64 = 0.0
    qcutz::Float64 = 0.0
    q2sigma::Float64 = 0.1  # The default value in QE's source code is 0.01
    input_dft::String = "none"
    exx_fraction::Float64 = 0.25
    lda_plus_u::Bool = false
    Hubbard_U::Vector{Union{Nothing,Float64}} = []
    vdw_corr::String = "none"
    london_s6::Float64 = 0.75
    london_rcut::Float64 = 200.0
    ts_vdw::Bool = false
    ts_vdw_econv_thr::Float64 = 1e-6
    ts_vdw_isolated::Bool = false
    assume_isolated::String = "none"
    # These checks are from https://github.com/QEF/q-e/blob/4132a64/Modules/read_namelists.f90#L1378-L1499.
    @assert ibrav in union(-1:1:14, (-3, -5, -9, -12, -13))
    @assert(
        if ibrav in -1:0
            true  # Skip the check, cannot use `nothing`
        else
            celldm[1] != 0 || A != 0  # Cannot use `iszero` to compare now!
        end,
        "invalid lattice parameters (`celldm` $celldm or `A` $A)!"
    )
    @assert(
        if ibrav == 14
            length(celldm) == 6
        elseif ibrav in (5, -5, 12, 13)
            4 <= length(celldm) <= 6
        elseif ibrav in (4, 6, 7, 8, 9, -9, 10, 11)
            3 <= length(celldm) <= 6
        elseif ibrav == -13  # `-13` is new from QE 6.4
            5 <= length(celldm) <= 6
        else
            1 <= length(celldm) <= 6
        end,
        "`celldm` must have length between 1 to 6! See `ibrav`'s doc!"
    )
    @assert nat >= 0
    @assert(0 <= ntyp <= 10, "`ntyp` $ntyp is either less than zero or too large!")
    @assert nspin in (1, 2, 4)
    @assert ecutwfc >= 0
    @assert ecutrho >= 0
    @assert(iszero(degauss), "`degauss` is not used in CP!")
    @assert ecfixed >= 0
    @assert qcutz >= 0
    @assert q2sigma >= 0
end # struct SystemNamelist

"""
    ElectronsNamelist <: Namelist

Represent the `ELECTRONS` namelist of `cp.x`.
"""
@with_kw struct ElectronsNamelist <: Namelist
    electron_maxstep::Int = 100
    electron_dynamics::String = "none"
    conv_thr::Float64 = 1e-6
    niter_cg_restart::Int = 20
    efield::Float64 = 0.0
    epol::Int = 3
    emass::Float64 = 400.0
    emass_cutoff::Float64 = 2.5
    orthogonalization::String = "ortho"
    ortho_eps::Float64 = 1e-8
    ortho_max::Int = 20
    ortho_para::Int = 0
    electron_damping::Float64 = 0.1
    electron_velocities::String = "default"
    electron_temperature::String = "not_controlled"
    ekincw::Float64 = 0.001
    fnosee::Float64 = 1.0
    startingwfc::String = "random"
    tcg::Bool = false
    maxiter::Int = 100
    passop::Float64 = 0.3
    n_inner::Int = 2
    ninter_cold_restart::Int = 1
    lambda_cold::Float64 = 0.03
    grease::Float64 = 1.0
    ampre::Float64 = 0.0
    # These checks are from https://github.com/QEF/q-e/blob/4132a64/Modules/read_namelists.f90#L1508-L1543.
    @assert electron_dynamics in ("none", "sd", "damp", "verlet", "cg")  # Different from code
    @assert emass > 0
    @assert emass_cutoff > 0
    @assert orthogonalization in ("ortho", "Gram-Schmidt")
    @assert ortho_eps > 0
    @assert ortho_max >= 1
    @assert electron_velocities in ("zero", "default", "change_step")  # New in 6.4
    @assert electron_temperature in ("nose", "rescaling", "not_controlled")
    @assert ekincw > 0
    @assert fnosee > 0
    @assert startingwfc in ("atomic", "random")
end # struct ElectronsNamelist

"""
    IonsNamelist <: Namelist

Represent the `IONS` namelist of `cp.x`.

Input this namelist only if `calculation` is `"cp"`, `"relax"`, `"vc-relax"`, `"vc-cp"`, `"cp-wf"`, or `"vc-cp-wf"`.
"""
@with_kw struct IonsNamelist <: Namelist
    ion_dynamics::String = "none"
    ion_positions::String = "default"
    ion_velocities::String = "default"
    ion_damping::Float64 = 0.2
    ion_radius::Vector{Union{Nothing,Float64}} = [0.5]  # The default value in QE's source code is just one 0.5
    iesr::Int = 1
    ion_nstepe::Int = 1
    remove_rigid_rot::Bool = false
    ion_temperature::String = "not_controlled"
    tempw::Float64 = 300.0
    fnosep::Float64 = 1.0
    tolp::Float64 = 100.0
    nhpcl::Int = 1
    nhptyp::Int = 0
    nhgrp::Vector{Union{Nothing,Int}} = [0]
    fnhscl::Vector{Union{Nothing,Float64}} = zeros(1)
    ndega::Int = 0
    tranp::Vector{Union{Nothing,Bool}} = falses(1)
    amprp::Vector{Union{Nothing,Float64}} = zeros(1)
    greasp::Float64 = 1.0
    # These checks are from https://github.com/QEF/q-e/blob/4132a64/Modules/read_namelists.f90#L1552-L1585.
    @assert ion_dynamics in ("none", "sd", "cg", "damp", "verlet")
    @assert ion_positions in ("default", "from_input")
    @assert ion_velocities in ("default", "change_step", "random", "from_input", "zero")
    @assert ion_nstepe > 0
    @assert ion_temperature in ("nose", "rescaling", "not_controlled")
    @assert tempw > 0
    @assert fnosep > 0
    @assert 0 <= nhpcl <= 4
end # struct IonsNamelist

"""
    CellNamelist <: Namelist

Represent the `CELL` namelist of `cp.x`.

Input this namelist only if `calculation` is `"vc-relax"`, `"vc-cp"`, or `"vc-cp-wf"`.
"""
@with_kw struct CellNamelist <: Namelist
    cell_parameters::String = "default"
    cell_dynamics::String = "none"
    cell_velocities::String = "default"
    cell_damping::Float64 = 0.1
    press::Float64 = 0.0
    wmass::Float64 = 0.0
    cell_factor::Float64 = 1.2
    cell_temperature::String = "not_controlled"
    temph::Float64 = 0.0
    fnoseh::Float64 = 1.0
    greash::Float64 = 1.0
    cell_dofree::String = "all"
    # These checks are from https://github.com/QEF/q-e/blob/4132a64/Modules/read_namelists.f90#L1596-L1625.
    @assert cell_parameters in ("default", "from_input")
    @assert cell_dynamics in ("none", "sd", "damp-pr", "pr")
    @assert cell_velocities in ("zero", "default")
    @assert wmass >= 0
    @assert cell_temperature in ("nose", "rescaling", "not_controlled")
    @assert(
        cell_dofree in (
            "all",
            "x",
            "y",
            "z",
            "xy",
            "xz",
            "yz",
            "xyz",
            "shape",
            "2Dxy",
            "2Dshape",
            "volume",
        )
    )
end # struct CellNamelist

"""
    PressAiNamelist <: Namelist

Represent the `PRESS_AI` namelist of `cp.x`.

Input this namelist only when `tabps` is `true`.
"""
@with_kw struct PressAiNamelist <: Namelist
    abivol::Bool = false
    abisur::Bool = false
    P_ext::Float64 = 0.0
    pvar::Bool = false
    P_in::Float64 = 0.0
    P_fin::Float64 = 0.0
    Surf_t::Float64 = 0.0
    rho_thr::Float64 = 0.0
    dthr::Float64 = 0.0
end # struct PressAiNamelist

"""
    WannierNamelist <: Namelist

Represent the `WANNIER` namelist of `cp.x`.

Input this namelist only if `calculation` is `"cp-wf"` or `"vc-cp-wf"`.
"""
@with_kw struct WannierNamelist <: Namelist
    wf_efield::Bool = false
    wf_switch::Bool = false
    sw_len::Int = 1
    efx0::Float64 = 0.0
    efy0::Float64 = 0.0
    efz0::Float64 = 0.0
    efx1::Float64 = 0.0
    efy1::Float64 = 0.0
    efz1::Float64 = 0.0
    wfsd::Int = 1
    wfdt::Float64 = 5.0
    maxwfdt::Float64 = 0.3
    nit::Int = 10
    nsd::Int = 10
    wf_q::Float64 = 1500.0
    wf_friction::Float64 = 0.3
    nsteps::Int = 20
    tolw::Float64 = 1e-8
    adapt::Bool = true
    calwf::Int = 3
    nwf::Int = 0
    wffort::Int = 40
    writev::Bool = false
    exx_neigh::Int = 60
    exx_dis_cutoff::Float64 = 8.0
    exx_poisson_eps::Float64 = 1e-6
    exx_ps_rcut_self::Float64 = 6.0
    exx_ps_rcut_pair::Float64 = 5.0
    exx_me_rcut_self::Float64 = 10.0
    exx_me_rcut_pair::Float64 = 7.0
    # These checks are from https://github.com/QEF/q-e/blob/4132a64/Modules/read_namelists.f90#L1634-L1650.
    @assert 1 <= calwf <= 5
    @assert 1 <= wfsd <= 3
end # struct WannierNamelist


export should_sample,
    ALWAYS_ON,
    ALWAYS_OFF,
    DEFAULT_ON,
    DEFAULT_OFF

abstract type AbstractSampler end

@enum Decision begin
    DECISION_DROP
    DECISION_RECORD_ONLY
    DECISION_RECORD_AND_SAMPLE
end

is_sampled(d::Decision) = d === DECISION_RECORD_AND_SAMPLE
API.is_recording(d::Decision) = d === DECISION_RECORD_ONLY || d === DECISION_RECORD_AND_SAMPLE

struct SamplingResult
    decision::Decision
    attributes::Attributes
    trace_state::TraceState
end

is_sampled(r::SamplingResult) = is_sampled(r.decision)
API.is_recording(r::SamplingResult) = is_recording(r.decision)

struct StaticSampler <: AbstractSampler
    decision::Decision
end

const ALWAYS_ON = StaticSampler(DECISION_RECORD_AND_SAMPLE)
const ALWAYS_OFF = StaticSampler(DECISION_DROP)

function Base.show(io::IO, s::StaticSampler)
    if s.decision === DECISION_DROP
        print(io, "AlwaysOffSampler")
    else
        print(io, "AlwaysOnSampler")
    end
end

function should_sample(
    s::StaticSampler,
    parent_context,
    trace_id,
    name,
    kind=SPAN_KIND_INTERNAL,
    attributes=Attributes(),
    links=[],
    trace_state=TraceState(),
)
    if s.decision === DECISION_DROP
        attributes = Attributes()
    end
    SamplingResult(
        s.decision,
        attributes,
        trace_state
    )
end

struct TraceIdRatioBased <: AbstractSampler
    ratio::Float64
    bound::UInt128
    function TraceIdRatioBased(ratio)
        0 <= ratio <= 1 || throw(ArgumentError("ratio should be in range [0,1]"))
        new(ratio, round(UInt128, typemax(UInt128) * ratio))
    end
end

function should_sample(
    s::TraceIdRatioBased,
    parent_context,
    trace_id,
    name,
    kind=SPAN_KIND_INTERNAL,
    attributes=Attributes(),
    links=[],
    trace_state=TraceState(),
)
    decision = DECISION_DROP
    if trace_id < s.bound
        decision = DECISION_RECORD_AND_SAMPLE
    end
    if decision === DECISION_DROP
        attributes = Attributes()
    end
    SamplingResult(
        decision,
        attributes,
        trace_state
    )
end

Base.@kwdef struct ParentBasedSampler{R,T1,T2,T3,T4} <: AbstractSampler
    root_sampler::R
    remote_parent_sampled::T1 = ALWAYS_ON
    remote_parent_not_sampled::T2 = ALWAYS_OFF
    local_parent_sampled::T3 = ALWAYS_ON
    local_parent_not_sampled::T4 = ALWAYS_OFF
end

function should_sample(
    s::ParentBasedSampler,
    parent_context,
    trace_id,
    name,
    kind=SPAN_KIND_INTERNAL,
    attributes=Attributes(),
    links=[],
    trace_state=TraceState(),
)
    parent_span_ctx = parent_context |> current_span |> span_context
    sampler = s.root_sampler
    if parent_span_ctx !== INVALID_SPAN_CONTEXT
        if parent_span_ctx.is_remote
            if parent_span_ctx.trace_flag.sampled
                sampler = s.remote_parent_sampled
            else
                sampler = s.remote_parent_not_sampled
            end
        else
            if parent_span_ctx.trace_flag.sampled
                sampler = s.local_parent_sampled
            else
                sampler = s.local_parent_not_sampled
            end
        end
    end
    should_sample(sampler, parent_span_ctx, trace_id, name, kind, attributes, links, trace_state)
end

"""Sampler that respects its parent span's sampling decision, but otherwise never samples."""
DEFAULT_OFF = ParentBasedSampler(root_sampler=ALWAYS_OFF)

"""Sampler that respects its parent span's sampling decision, but otherwise always samples."""
DEFAULT_ON = ParentBasedSampler(root_sampler=ALWAYS_ON)


export AbstractApproximator,
    ApproximatorStyle, Q_APPROXIMATOR, QApproximator, V_APPROXIMATOR, VApproximator

"""
    (app::AbstractApproximator)(env)

An approximator is a functional object for value estimation.
It serves as a black box to provides an abstraction over different
kinds of approximate methods (for example DNN provided by Flux or Knet).
"""
abstract type AbstractApproximator end

"""
    update!(a::AbstractApproximator, correction)

Usually the `correction` is the gradient of inner parameters.
"""
function RLBase.update!(a::AbstractApproximator, correction) end

#####
# traits
#####

abstract type AbstractApproximatorStyle end

"""
Used to detect what an [`AbstractApproximator`](@ref) is approximating.
"""
function ApproximatorStyle(::AbstractApproximator) end

struct QApproximator <: AbstractApproximatorStyle end

const Q_APPROXIMATOR = QApproximator()

struct VApproximator <: AbstractApproximatorStyle end

const V_APPROXIMATOR = VApproximator()


# This file is a part of SimilaritySearch.jl

"""
struct SearchGraphCallbacks
    hints::Union{Nothing,Callback} = DisjointHints()
    hyperparameters::Union{Nothing,Callback} = OptimizeParameters(kind=ParetoRecall())
    logbase::Float32 = 1.5
    starting::Int32 = 8
end

Call insertions and indexing methods with `SearchGraphCallbacks` objects to control how the index structure is adjusted (callbacks are called when ``n > starting`` and ``\\lceil(\\log(logbase, n)\\rceil != \\lceil\\log(logbase, n+1)\\rceil``)
"""
@with_kw struct SearchGraphCallbacks
    hints::Union{Nothing,Callback} = DisjointHints()
    hyperparameters::Union{Nothing,Callback} = OptimizeParameters(kind=ParetoRecall())
    logbase::Float32 = 1.5
    starting::Int32 = 8
end

Base.copy(g::SearchGraph;
    dist=g.dist,
    db=g.db,
    links=g.links,
    locks=g.locks,
    hints=g.hints,
    search_algo=copy(g.search_algo),
    verbose=true
) = SearchGraph(; dist, db, links, locks, hints, search_algo, verbose)

"""
    SearchGraphCallbacks(kind::ErrorFunction;
        hints=DisjointHints(),
        logbase=1.5,
        starting=8,
        initialpopulation=16,
        maxpopulation=initialpopulation,
        maxiters=12,
        bsize=4,
        mutbsize=4bsize,
        crossbsize=2bsize,
        tol=-1.0,
        params=SearchParams(; maxpopulation, bsize, mutbsize, crossbsize, tol, maxiters),
        ksearch=10,
        numqueries=32,
        space::BeamSearchSpace=BeamSearchSpace()
    )

Convenient constructor function to create `SearchGraphCallbacks` structs. See [`SearchGraphCallbacks`](@ref), [`SearchParams`](@ref), and [`BeamSearchSpace`](@ref) for more details.

# Arguments

- `kind`: The kind of error function, e.g. `MinRecall(0.9)`.
- `hints`: How search hints should be computed.
- `logbase`: Controls the periodicity of callback executions.
- `starting`: Controls the minimum size of the index before execute callbacks.
- `initialpopulation`: Optimization argument that determines the initial number of configurations.
- `maxiters`: Optimization argument that determines the number of iterations.
- `bsize`: Optimization argument that determines how many top configurations are allowed to mutate and cross.
- `tol`: Optimization argument that determines the minimal tolerance improvement to stop the optimization.
- `params`: The `SearchParams` arguments (if separated optimization arguments are not enough)
- `ksearch`: The number of neighbors to be retrived by the optimization process.
- `numqueries`: The number of queries to be performed during the optimization process.
- `space`: The cofiguration search space
"""
function SearchGraphCallbacks(kind::ErrorFunction;
    hints=DisjointHints(),
    logbase=1.5,
    starting=8,
    initialpopulation=16,
    maxiters=12,
    bsize=4,
    tol=-1.0,
    params=SearchParams(; maxpopulation=initialpopulation, bsize, mutbsize=4bsize, crossbsize=2bsize, tol, maxiters),
    ksearch=10,
    numqueries=32,
    space::BeamSearchSpace=BeamSearchSpace()
)
hyperparameters = OptimizeParameters(; kind, initialpopulation, params, ksearch, numqueries, space)
SearchGraphCallbacks(; hints, hyperparameters, logbase, starting)
end

"""
    execute_callbacks(index::SearchGraph, n=length(index), m=n+1)

Process all registered callbacks
"""
function execute_callbacks(callbacks::SearchGraphCallbacks, index::SearchGraph, n=length(index), m=n+1; force=false)
    if force || (n >= callbacks.starting && ceil(Int, log(callbacks.logbase, n)) != ceil(Int, log(callbacks.logbase, m)))
        callbacks.hints !== nothing && execute_callback(callbacks.hints, index)
        callbacks.hyperparameters !== nothing && execute_callback(callbacks.hyperparameters, index)
    end
end


using CSyntax
using Dates
using GLFW
using ModernGL

const DEFAULT_R = 0.8
const FRAGMENT_SHADER_BW = "fragment_shader_bw.glsl"
const FRAGMENT_SHADER_COLORFUL = "fragment_shader_colorful.glsl"
const VERTEX_SHADER = "vertex_shader.glsl"

function glsetup()
    # set up OpenGL context version
    # it seems OpenGL 4.1 is the highest version supported by MacOS.
    @static if Sys.isapple()
        VERSION_MAJOR = 4
        VERSION_MINOR = 1
    end

    @static if Sys.isapple()
        GLFW.WindowHint(GLFW.CONTEXT_VERSION_MAJOR, VERSION_MAJOR)
        GLFW.WindowHint(GLFW.CONTEXT_VERSION_MINOR, VERSION_MINOR)
        GLFW.WindowHint(GLFW.OPENGL_PROFILE, GLFW.OPENGL_CORE_PROFILE)
        GLFW.WindowHint(GLFW.OPENGL_FORWARD_COMPAT, GL_TRUE)
    else
        GLFW.DefaultWindowHints()
    end
end

function get_radius(args)
    if length(args) == 0
        return DEFAULT_R
    end
    try
        r = parse(Float64, args[1])
        return if r > 0 r else DEFAULT_R end
    catch
        return DEFAULT_R
    end
end

function get_fragment_shader(args)
    if length(args) >= 2 && args[2] == "bw"
        return FRAGMENT_SHADER_BW
    end
    return FRAGMENT_SHADER_COLORFUL
end

function create_shader_program(fs_source, vs_source)
    vs = glCreateShader(GL_VERTEX_SHADER)
    glShaderSource(vs, 1, Ptr{GLchar}[pointer(vs_source)], C_NULL)
    glCompileShader(vs)
    fs = glCreateShader(GL_FRAGMENT_SHADER)
    glShaderSource(fs, 1, Ptr{GLchar}[pointer(fs_source)], C_NULL)
    glCompileShader(fs)
    program = glCreateProgram()
    glAttachShader(program, vs)
    glAttachShader(program, fs)
    glLinkProgram(program)
    return program
end

mutable struct Controls
    right::Bool
    up::Bool
    left::Bool
    down::Bool
    zoomin::Bool
    zoomout::Bool
    paused::Bool
    backward::Bool
    forward::Bool
    max_iter_increase::Bool
    max_iter_decrease::Bool
    print::Bool
    quit::Bool
end

Controls() = Controls(false, false, false, false, false, false, false, false,
    false, false, false, false, false)

function make_key_listener(controls)
    function key_listener(window, key, a, action, b)
        if action == GLFW.PRESS
            if key == GLFW.KEY_I
                controls.zoomin = true
            elseif key == GLFW.KEY_O
                controls.zoomout = true
            elseif key == GLFW.KEY_D
                controls.right = true
            elseif key == GLFW.KEY_W
                controls.up = true
            elseif key == GLFW.KEY_A
                controls.left = true
            elseif key == GLFW.KEY_S
                controls.down = true
            elseif key == GLFW.KEY_SPACE
                controls.paused = !controls.paused
            elseif key == GLFW.KEY_LEFT
                controls.backward = true
            elseif key == GLFW.KEY_RIGHT
                controls.forward = true
            elseif key == GLFW.KEY_UP
                controls.max_iter_increase = true
            elseif key == GLFW.KEY_DOWN
                controls.max_iter_decrease = true
            elseif key == GLFW.KEY_P
                controls.print = true
            elseif key == GLFW.KEY_Q
                controls.quit = true
            end
        elseif action == GLFW.RELEASE
            if key == GLFW.KEY_I
                controls.zoomin = false
            elseif key == GLFW.KEY_O
                controls.zoomout = false
            elseif key == GLFW.KEY_D
                controls.right = false
            elseif key == GLFW.KEY_W
                controls.up = false
            elseif key == GLFW.KEY_A
                controls.left = false
            elseif key == GLFW.KEY_S
                controls.down = false
            elseif key == GLFW.KEY_LEFT
                controls.backward = false
            elseif key == GLFW.KEY_RIGHT
                controls.forward = false
            elseif key == GLFW.KEY_UP
                controls.max_iter_increase = false
            elseif key == GLFW.KEY_DOWN
                controls.max_iter_decrease = false
            end
        end
    end
end

function main()

    glsetup()

    window = GLFW.CreateWindow(720, 720, "Julia Set Explorer")
    @assert window != C_NULL "could not open window with GLFW3."
    GLFW.MakeContextCurrent(window)

    glEnable(GL_DEPTH_TEST)
    glDepthFunc(GL_LESS)

    program = create_shader_program(read(get_fragment_shader(ARGS), String),
        read(VERTEX_SHADER, String))

    screen_rect = GLfloat[
        -1.0, -1.0, 0.0,
        1.0, -1.0, 0.0,
        1.0, 1.0, 0.0,
        -1.0, -1.0, 0.0,
        1.0, 1.0, 0.0,
        -1.0, 1.0, 0.0
    ]

    vbo = GLuint(0)
    @c glGenBuffers(1, &vbo)
    glBindBuffer(GL_ARRAY_BUFFER, vbo)
    glBufferData(GL_ARRAY_BUFFER, sizeof(screen_rect), screen_rect,
        GL_STATIC_DRAW)

    vao = GLuint(0)
    @c glGenVertexArrays(1, &vao)
    glBindVertexArray(vao)
    glBindBuffer(GL_ARRAY_BUFFER, vbo)
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, C_NULL)
    glEnableVertexAttribArray(0)

    glClearColor(0.0, 0.0, 0.0, 1.0)

    # angular frequency
    w = 0.1 * 2 * pi
    r = get_radius(ARGS)

    controls = Controls()
    GLFW.SetKeyCallback(window, make_key_listener(controls))
    t0 = now()
    tm = t0
    c = r * exp(0im)
    center = 0 + 0im
    centerdot = 2.0
    scale = 0.5
    scalebase = 3.0
    max_iter = 200.0
    max_iter_dot = 20.0
    while !GLFW.WindowShouldClose(window)

        if controls.quit
            break
        end

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

        glUseProgram(program)

        tn = now()
        t = (tn - t0).value / 1000.0
        dt = (tn - tm).value / 1000.0
        if !controls.paused && !controls.backward && !controls.forward
            c = c * exp(im * w * dt)
        end
        if controls.forward
            c = c * exp(2 * im * w * dt)
        end
        if controls.backward
            c = c * exp(-2 * im * w * dt)
        end
        if controls.zoomin
            scale += scale * log(scalebase) * dt
        end
        if controls.zoomout
            scale -= scale * log(scalebase) * dt
        end
        if controls.right
            center += centerdot * dt / scale
        end
        if controls.up
            center += im * centerdot * dt / scale
        end
        if controls.left
            center -= centerdot * dt / scale
        end
        if controls.down
            center -= im * centerdot * dt / scale
        end
        if controls.max_iter_increase
            max_iter += max_iter_dot * dt
        end
        if controls.max_iter_decrease
            max_iter -= max_iter_dot * dt
        end
        if controls.print
            println("c = $c")
            println("Showing the section between " *
                "$(round(center - (1 + 1im)/scale, digits=5)) and " *
                "$(round(center + (1 + 1im)/scale, digits=5))")
            controls.print = false
        end
        tm = tn

        loc = glGetUniformLocation(program, "max_iter")
        glUniform1i(loc, floor(max_iter))
        loc = glGetUniformLocation(program, "c")
        glUniform2f(loc, real(c), imag(c))
        loc = glGetUniformLocation(program, "center")
        glUniform2f(loc, real(center), imag(center))
        loc = glGetUniformLocation(program, "scale")
        glUniform1f(loc, scale)

        glBindVertexArray(vao)
        glDrawArrays(GL_TRIANGLES, 0, 6)

        GLFW.PollEvents()
        GLFW.SwapBuffers(window)
    end

    GLFW.DestroyWindow(window)

end

main()


#=
This file is auto-generated. Do not edit.
=#
"""
    mutable struct LCFilter <: Filter
        lf::Float64
        rf::Float64
        cf::Float64
        ext::Dict{String, Any}
        states::Vector{Symbol}
        n_states::Int
    end

Parameters of a LCL filter outside the converter

# Arguments
- `lf::Float64`: filter inductance, validation range: `(0, nothing)`
- `rf::Float64`: filter resistance, validation range: `(0, nothing)`
- `cf::Float64`: filter capacitance, validation range: `(0, nothing)`
- `ext::Dict{String, Any}`
- `states::Vector{Symbol}`: The states of the LCFilter model are:
	ir_filter: Real current out of the filter,
	ii_filter: Imaginary current out of the filter
- `n_states::Int`: LCFilter has two states
"""
mutable struct LCFilter <: Filter
    "filter inductance"
    lf::Float64
    "filter resistance"
    rf::Float64
    "filter capacitance"
    cf::Float64
    ext::Dict{String, Any}
    "The states of the LCFilter model are:
	ir_filter: Real current out of the filter,
	ii_filter: Imaginary current out of the filter"
    states::Vector{Symbol}
    "LCFilter has two states"
    n_states::Int
end

function LCFilter(lf, rf, cf, ext=Dict{String, Any}(), )
    LCFilter(lf, rf, cf, ext, [:ir_filter, :ii_filter], 2, )
end

function LCFilter(; lf, rf, cf, ext=Dict{String, Any}(), states=[:ir_filter, :ii_filter], n_states=2, )
    LCFilter(lf, rf, cf, ext, states, n_states, )
end

# Constructor for demo purposes; non-functional.
function LCFilter(::Nothing)
    LCFilter(;
        lf=0,
        rf=0,
        cf=0,
        ext=Dict{String, Any}(),
    )
end

"""Get [`LCFilter`](@ref) `lf`."""
get_lf(value::LCFilter) = value.lf
"""Get [`LCFilter`](@ref) `rf`."""
get_rf(value::LCFilter) = value.rf
"""Get [`LCFilter`](@ref) `cf`."""
get_cf(value::LCFilter) = value.cf
"""Get [`LCFilter`](@ref) `ext`."""
get_ext(value::LCFilter) = value.ext
"""Get [`LCFilter`](@ref) `states`."""
get_states(value::LCFilter) = value.states
"""Get [`LCFilter`](@ref) `n_states`."""
get_n_states(value::LCFilter) = value.n_states

"""Set [`LCFilter`](@ref) `lf`."""
set_lf!(value::LCFilter, val) = value.lf = val
"""Set [`LCFilter`](@ref) `rf`."""
set_rf!(value::LCFilter, val) = value.rf = val
"""Set [`LCFilter`](@ref) `cf`."""
set_cf!(value::LCFilter, val) = value.cf = val
"""Set [`LCFilter`](@ref) `ext`."""
set_ext!(value::LCFilter, val) = value.ext = val


struct Font
    surface::Cairo.CairoSurface
    charDict::Dict{Char, Tuple{Int, Int, Int, Int}}
    charSpacing::Int
    lineSpacing::Int

    Font(filename::String, fontString::String, charSpacing::Int=1, lineSpacing::Int=1) = new(open(Cairo.read_from_png, filename), generateFontDict(fontString), charSpacing, lineSpacing)
end

stringLineAlignOffsets = Dict{Symbol, Function}(
    :left => (widest, width) -> 0,
    :center => (widest, width) -> round(Int, (widest - width) / 2),
    :right => (widest, width) -> widest - width,
)

function getQuadTuple(row::Int, col::Int, width::Int=3, height::Int=5)
    return (8 * row - 8, 8 * col - 8, width, height)
end

# Use first occurance of a symbol as the target quad
function generateFontDict(fontString::String)
    fontMap = Dict{Char, Tuple{Int, Int, Int, Int}}()

    for (i, row) in enumerate(split(fontString, "\n"))
        for (j, c) in enumerate(row)
            get!(fontMap, c) do
                getQuadTuple(j, i, 3, 5)
            end
        end
    end

    return fontMap
end

# Multiline string
function lineWidth(font::Font, s::AbstractString, replaceMissing::Char=' ')
    return isempty(s) ? 0 : sum([get(font.charDict, c, font.charDict[replaceMissing])[3] for c in s]) + (length(s) - 1) * font.charSpacing
end

function lineHeight(font::Font, s::AbstractString, replaceMissing::Char=' ')
    return isempty(s) ? 0 : maximum([get(font.charDict, c, font.charDict[replaceMissing])[4] for c in s])
end

function Base.size(font::Font, s::AbstractString, replaceMissing::Char=' ')
    lines = split(s, "\n")

    width = maximum(lineWidth.(Ref(font), lines))
    height = sum(lineHeight.(Ref(font), lines)) + (length(lines) - 1) * font.lineSpacing

    return width, height
end

function drawString(ctx::Cairo.CairoContext, font::Font, s::AbstractString, x::Int=0, y::Int=0; replaceMissing::Char=' ', align::Symbol=:left, tint::Union{colorTupleType, Nothing}=colors.canvas_font_color)
    shouldSave = x != 0 || y != 0

    if shouldSave
        Cairo.save(ctx)
        Cairo.translate(ctx, x, y)
    end

    accX = 0
    accY = 0

    lineAllignFunc = get(stringLineAlignOffsets, align, :left)
    widest, height = size(font, s, replaceMissing)

    for line in split(s, "\n")
        width = lineWidth(font, line, replaceMissing)
        accX = lineAllignFunc(widest, width)

        for c in line
            quadX, quadY, width, height = get(font.charDict, c, font.charDict[replaceMissing])
            drawImage(ctx, font.surface, accX, accY, quadX, quadY, width, height, tint=tint)

            accX += width + font.charSpacing
        end

        accY += lineHeight(font, line, replaceMissing) + font.lineSpacing
    end

    if shouldSave
        Cairo.restore(ctx)
    end
end

const pico8FontString = raw"""

 !"#$% ´()*+,-./
0123456789:;<=>?
@abcdefghijklmno
pqrstuvwxyz[\]^_
`ABCDEFGHIJKLMNO
PQRSTUVWXYZ{|}~
"""

const pico8Font = missing


@everywhere push!(LOAD_PATH, "../")
@everywhere push!(LOAD_PATH, "./")

include("swimmerfunctions.jl")

####################################### Experiment Parameters / HyperParameters

model_file = "$(modeldir)/swimmer.xml"

# simulator parameters
T = 500
numT = 32
skip = 5
niter = 30

# solver parameters
gamma = 0.995
gae = 0.98

fullFIM = true

norm_step_size = 0.05

cg_iter= 12
cg_reg = 1e-6
cg_tol = 1e-10

#### deterministic randomization
srand(12345) # random seed, set globally

dtype = Float64

my_mjsys       = mjw.load_model(model_file, skip, "normal")

if METHOD == :NPG ######################################### NPG startup
   my_policy      = Policy.GLP{dtype}(my_mjsys.ns, my_mjsys.nu) # inputs: n, m
   #my_policy      = Policy.NN{dtype}(my_mjsys.ns, my_mjsys.nu, 32, 2) # inputs: n, m

   pgmodel        = PolicyGradModel(copy(my_policy.theta),
                                    my_mjsys,
                                    my_policy,
                                    T, numT, 10, niter)

   # NPG baseline aka value function approximation
   baseline    = Baseline.Quadratic{Float64}(1e-5, my_mjsys.ns, T, numT)
   #baseline    = Baseline.NN{Float64}(my_mjsys.ns, T, numT, 128)

   pg_specs   = NPGStrategy{dtype}(pgmodel,
                                   baseline,
                                   fullFIM,
                                   norm_step_size,
                                   gamma, gae,
                                   cg_iter, cg_reg, cg_tol)

elseif METHOD == :MPPI ######################################### MPPI startup
   mpcT = 100
   mppi = MPPIModel(eye(my_mjsys.nu)*0.1, # sigma
                    0.3,                  # lambda
                    my_mjsys,
                    mpcT, # horizon for mpc; set to T for non-MPC mode
                    T,
                    numT;
                    theta = randn(my_mjsys.nu, mpcT),
                    s0 = [my_mjsys.d.qpos; my_mjsys.d.qvel], # set initial state here
                   )

   mppi_specs = MPPIStrategy(mppi)
else
   error("WHAT YOU DOING")
end


#Testbed for synthetic spike recovery from example background
using BioBackgroundModels, BioMotifInference, Random, Distributed, Distributions, Serialization
Random.seed!(786)
#CONSTANTS
no_obs=500
obsl=100:200

const folder_path="/bench/PhD/NGS_binaries/BBM/refined_folders"

report_folders=deserialize(folder_path)

bhmm_vec=[BHMM([0.40027008799648645, 0.1997894691941005, 0.006556173444055311, 0.3381222117369811, 0.053302496489949405, 0.0019595611384158307], [0.9910385961237116 0.001055084535731191 0.0030117366072612726 0.0025443692116936516 1.875366316590218e-83 0.002350213521605879; 0.003592562092379169 0.6212087435679351 1.4862557155404555e-91 0.3738436265294334 0.001355067810251799 5.674610926019336e-174; 0.03314627345971247 0.00026886477654034587 0.9628685354878737 3.677787936178394e-5 0.00367954839651186 5.0e-324; 0.0002903769304330447 0.3737391779744011 1.584645416437449e-35 0.6225645652084915 0.002839228767872827 0.0005666511188010834; 0.0005590565306708014 0.0064195824547637215 0.004061509889975232 0.01962586251822172 0.9687912312979926 0.0005427573083774576; 0.0174697691356862 2.0648278441425858e-20 8.008498087945918e-218 0.004151185943888868 0.0013251598095032858 0.9770538851109234], [Categorical([0.2565257692018922, 0.24506145230281004, 0.2649485969649606, 0.23346418153033835]), Categorical([0.052491168994042534, 0.0987036876775239, 0.2383241923003866, 0.610480951028046]), Categorical([0.08371833024203595, 0.07204591736665274, 0.3854013598343639, 0.4588343925569475]), Categorical([0.5954709485303519, 0.2497223085607694, 0.09528006039357192, 0.0595266825153071]), Categorical([0.4744103622376032, 0.04038264172760258, 0.04588736323302444, 0.4393196328017719]), Categorical([0.41173384083242826, 0.44270375576246973, 0.030612125665857307, 0.11495027773924743])])]

bhmm_dist=Categorical([1.])

struc_sig_1=[.1 .7 .1 .1
           .1 .1 .1 .7
           .1 .7 .1 .1]
periodicity=8
struc_frac_obs=.75

tata_box=[.05 .05 .05 .85
          .85 .05 .05 .05
          .05 .05 .05 .85
          .85 .05 .05 .05
          .425 .075 .075 .425
          .85 .05 .05 .05
          .425 .075 .075 .425]
motif_frac_obs=.7
motif_recur_range=1:4

@info "Constructing synthetic sample set..."
obs1, bg_scores1, hmm_truth1, spike_truth1 = synthetic_sample(no_obs,obsl,bhmm_vec,bhmm_dist,[struc_sig_1,tata_box],[(true,(struc_frac_obs,periodicity)),(false,(motif_frac_obs,motif_recur_range))])

e1 = "BMISpikeTest"

#JOB CONSTANTS
const ensemble_size = 750
const no_sources = 4
const source_min_bases = 3
const source_max_bases = 12
const source_length_range= source_min_bases:source_max_bases
const mixing_prior = .5
const models_to_permute = ensemble_size * 3
funcvec=full_perm_funcvec
push!(funcvec, BioMotifInference.permute_source)
push!(funcvec, BioMotifInference.permute_source)
args=[Vector{Tuple{Symbol,Any}}() for i in 1:length(funcvec)]
args[end-1]=[(:weight_shift_freq,0.),(:length_change_freq,1.),(:length_perm_range,1:1)]
args[end]=[(:weight_shift_freq,.1),(:length_change_freq,0.),(:weight_shift_dist,Uniform(.00001,.01))]

instruct = Permute_Instruct(funcvec, ones(length(funcvec))./length(funcvec),models_to_permute,100; args=args)

@info "Assembling source priors..."
prior_array= Vector{Matrix{Float64}}()
source_priors = BioMotifInference.assemble_source_priors(no_sources, prior_array)

@info "Assembling ensemble..."
isfile(e1*"/ens") ? (ens1 = deserialize(e1*"/ens")) :
    (ens1 = IPM_Ensemble(e1, ensemble_size, source_priors, (falses(0,0), mixing_prior), bg_scores1, obs1, source_length_range))

@info "Converging ensemble..."
logZ1 = BioMotifInference.converge_ensemble!(ens1, instruct, backup=(true,250), wk_disp=false, tuning_disp=false, ens_disp=true, conv_plot=false, src_disp=true, lh_disp=false, liwi_disp=false)


function construct_network!(
    container::OptimizationContainer,
    sys::PSY.System,
    model::NetworkModel{CopperPlatePowerModel},
    ::ProblemTemplate,
)
    if get_use_slacks(model)
        add_variables!(container, SystemBalanceSlackUp, sys, CopperPlatePowerModel)
        add_variables!(container, SystemBalanceSlackDown, sys, CopperPlatePowerModel)
        add_to_expression!(
            container,
            ActivePowerBalance,
            SystemBalanceSlackUp,
            sys,
            model,
            CopperPlatePowerModel,
        )
        add_to_expression!(
            container,
            ActivePowerBalance,
            SystemBalanceSlackDown,
            sys,
            model,
            CopperPlatePowerModel,
        )
        objective_function!(container, PSY.System, model, CopperPlatePowerModel)
    end

    add_constraints!(
        container,
        CopperPlateBalanceConstraint,
        sys,
        model,
        CopperPlatePowerModel,
    )

    add_constraint_dual!(container, sys, model)
    return
end

function construct_network!(
    container::OptimizationContainer,
    sys::PSY.System,
    model::NetworkModel{AreaBalancePowerModel},
    ::ProblemTemplate,
)
    area_mapping = PSY.get_aggregation_topology_mapping(PSY.Area, sys)
    branches = get_available_components(PSY.Branch, sys)
    if get_use_slacks(model)
        throw(
            IS.ConflictingInputsError(
                "Slack Variables are not compatible with AreaBalancePowerModel",
            ),
        )
    end

    area_balance(
        container,
        ExpressionKey(ActivePowerBalance, PSY.Bus),
        area_mapping,
        branches,
    )
    add_constraint_dual!(container, sys, model)
    return
end

function construct_network!(
    container::OptimizationContainer,
    sys::PSY.System,
    model::NetworkModel{StandardPTDFModel},
    ::ProblemTemplate,
)
    ptdf = get_PTDF(model)

    if ptdf === nothing
        throw(ArgumentError("no PTDF matrix supplied"))
    end

    if get_use_slacks(model)
        add_variables!(container, SystemBalanceSlackUp, sys, CopperPlatePowerModel)
        add_variables!(container, SystemBalanceSlackDown, sys, CopperPlatePowerModel)
        add_to_expression!(
            container,
            ActivePowerBalance,
            SystemBalanceSlackUp,
            sys,
            model,
            StandardPTDFModel,
        )
        add_to_expression!(
            container,
            ActivePowerBalance,
            SystemBalanceSlackDown,
            sys,
            model,
            StandardPTDFModel,
        )
        objective_function!(container, PSY.System, model, StandardPTDFModel)
    end

    add_constraints!(container, CopperPlateBalanceConstraint, sys, model, StandardPTDFModel)

    add_constraint_dual!(container, sys, model)
    return
end

function construct_network!(
    container::OptimizationContainer,
    sys::PSY.System,
    model::NetworkModel{T},
    template::ProblemTemplate,
) where {T <: PTDFPowerModel}
    construct_network!(
        container,
        sys,
        model,
        template;
        instantiate_model=instantiate_nip_ptdf_expr_model,
    )

    add_pm_expr_refs!(container, T, sys)

    add_constraints!(container, CopperPlateBalanceConstraint, sys, model, PTDFPowerModel)
    add_constraint_dual!(container, sys, model)

    return
end

function construct_network!(
    container::OptimizationContainer,
    sys::PSY.System,
    model::NetworkModel{T},
    template::ProblemTemplate;
    instantiate_model=instantiate_nip_expr_model,
) where {T <: PM.AbstractActivePowerModel}
    if T in UNSUPPORTED_POWERMODELS
        throw(
            ArgumentError(
                "$(T) formulation is not currently supported in PowerSimulations",
            ),
        )
    end

    if get_use_slacks(model)
        add_variables!(container, SystemBalanceSlackUp, sys, T)
        add_variables!(container, SystemBalanceSlackDown, sys, T)
        add_to_expression!(
            container,
            ActivePowerBalance,
            SystemBalanceSlackUp,
            sys,
            model,
            T,
        )
        add_to_expression!(
            container,
            ActivePowerBalance,
            SystemBalanceSlackDown,
            sys,
            model,
            T,
        )
        objective_function!(container, PSY.Bus, model, T)
    end

    @debug "Building the $T network with $instantiate_model method" _group =
        LOG_GROUP_NETWORK_CONSTRUCTION
    powermodels_network!(container, T, sys, template, instantiate_model)
    add_pm_variable_refs!(container, T, sys)
    add_pm_constraint_refs!(container, T, sys)

    add_constraint_dual!(container, sys, model)
    return
end

function construct_network!(
    container::OptimizationContainer,
    sys::PSY.System,
    model::NetworkModel{T},
    template::ProblemTemplate;
    instantiate_model=instantiate_nip_expr_model,
) where {T <: PM.AbstractPowerModel}
    if T in UNSUPPORTED_POWERMODELS
        throw(
            ArgumentError(
                "$(T) formulation is not currently supported in PowerSimulations",
            ),
        )
    end

    if get_use_slacks(model)
        add_variables!(container, SystemBalanceSlackUp, sys, T)
        add_variables!(container, SystemBalanceSlackDown, sys, T)
        add_to_expression!(
            container,
            ActivePowerBalance,
            SystemBalanceSlackUp,
            sys,
            model,
            T,
        )
        add_to_expression!(
            container,
            ActivePowerBalance,
            SystemBalanceSlackDown,
            sys,
            model,
            T,
        )
        add_to_expression!(
            container,
            ReactivePowerBalance,
            SystemBalanceSlackUp,
            sys,
            model,
            T,
        )
        add_to_expression!(
            container,
            ReactivePowerBalance,
            SystemBalanceSlackDown,
            sys,
            model,
            T,
        )
        objective_function!(container, PSY.Bus, model, T)
    end

    @debug "Building the $T network with $instantiate_model method" _group =
        LOG_GROUP_NETWORK_CONSTRUCTION
    powermodels_network!(container, T, sys, template, instantiate_model)
    add_pm_variable_refs!(container, T, sys)
    add_pm_constraint_refs!(container, T, sys)

    add_constraint_dual!(container, sys, model)
    return
end

function construct_network!(
    container::OptimizationContainer,
    sys::PSY.System,
    model::NetworkModel{T},
    template::ProblemTemplate;
    instantiate_model=instantiate_bfp_expr_model,
) where {T <: PM.AbstractBFModel}
    if T in UNSUPPORTED_POWERMODELS
        throw(
            ArgumentError(
                "$(T) formulation is not currently supported in PowerSimulations",
            ),
        )
    end

    if get_use_slacks(model)
        add_variables!(container, SystemBalanceSlackUp, sys, T)
        add_variables!(container, SystemBalanceSlackDown, sys, T)
        add_to_expression!(
            container,
            ActivePowerBalance,
            SystemBalanceSlackUp,
            sys,
            model,
            T,
        )
        add_to_expression!(
            container,
            ActivePowerBalance,
            SystemBalanceSlackDown,
            sys,
            model,
            T,
        )
        add_to_expression!(
            container,
            ReactivePowerBalance,
            SystemBalanceSlackUp,
            sys,
            model,
            T,
        )
        add_to_expression!(
            container,
            ReactivePowerBalance,
            SystemBalanceSlackDown,
            sys,
            model,
            T,
        )
        objective_function!(container, PSY.Bus, model, T)
    end

    @debug "Building the $T network with $instantiate_model method" _group =
        LOG_GROUP_NETWORK_CONSTRUCTION
    powermodels_network!(container, T, sys, template, instantiate_model)
    add_pm_variable_refs!(container, T, sys)
    add_pm_constraint_refs!(container, T, sys)
    add_constraint_dual!(container, sys, model)
    return
end

function construct_network!(
    container::OptimizationContainer,
    sys::PSY.System,
    model::NetworkModel{T},
    template::ProblemTemplate;
    instantiate_model=instantiate_vip_expr_model,
) where {T <: PM.AbstractIVRModel}
    if T in UNSUPPORTED_POWERMODELS
        throw(
            ArgumentError(
                "$(T) formulation is not currently supported in PowerSimulations",
            ),
        )
    end

    if get_use_slacks(model)
        add_variables!(container, SystemBalanceSlackUp, sys, T)
        add_variables!(container, SystemBalanceSlackDown, sys, T)
        add_to_expression!(
            container,
            ActivePowerBalance,
            SystemBalanceSlackUp,
            sys,
            model,
            T,
        )
        add_to_expression!(
            container,
            ActivePowerBalance,
            SystemBalanceSlackDown,
            sys,
            model,
            T,
        )
        add_to_expression!(
            container,
            ReactivePowerBalance,
            SystemBalanceSlackUp,
            sys,
            model,
            T,
        )
        add_to_expression!(
            container,
            ReactivePowerBalance,
            SystemBalanceSlackDown,
            sys,
            model,
            T,
        )
        objective_function!(container, PSY.Bus, model, T)
    end

    @debug "Building the $T network with $instantiate_model method" _group =
        LOG_GROUP_NETWORK_CONSTRUCTION
    powermodels_network!(container, T, sys, template, instantiate_model)
    add_pm_variable_refs!(container, T, sys)
    add_pm_constraint_refs!(container, T, sys)
    add_constraint_dual!(container, sys, model)
    return
end