"""
    OneHotArray(index => value, shape) -> array::AbstractArray

Create an `array` such that `isequal(array[index], value)`.  The values at
indices other than `index` are undefined.

Currently, this array type is likely only useful as the second argument to
`BangBang.Extras.broadcast_inplace!!` and as the input argument to FLoops.jl's
`@reduce`

# Examples
```jldoctest
julia> using MicroCollections, BangBang.Extras

julia> broadcast_inplace!!(+, ones(Int64, 4), OneHotVector(2 => 3, 4))
4-element Vector{Int64}:
 1
 4
 1
 1

julia> using InitialValues

julia> broadcast_inplace!!(+, InitialValue(+), OneHotVector(2 => 3, 4))
4-element Vector{Int64}:
 0
 3
 0
 0
```
"""
OneHotArray

using BangBang: BangBang, setindex!!
using InitialValues: GenericInitialValue

struct OneHotArray{T,N,Index<:Tuple,Value} <: AbstractArray{T,N}
    index::Index
    value::Value
    shape::NTuple{N,Int}

    global _OneHotArray
    @inline _OneHotArray(
        ::Type{T},
        index::Index,
        value::Value,
        shape::NTuple{N,Int},
    ) where {T,N,Index,Value} = new{T,N,Index,Value}(index, value, shape)
end

const OneHotVector{T,Index<:Tuple,Value} = OneHotArray{T,1,Index,Value}
const OneHotMatrix{T,Index<:Tuple,Value} = OneHotArray{T,2,Index,Value}

indexof(oh::OneHotArray) = oh.index
valueof(oh::OneHotArray) = oh.value

const DimsLike = Union{Tuple{Vararg{Integer}},Integer}

astuple(index) = (index,)
astuple(index::Tuple) = index

@inline function OneHotArray{T}(index_value::Pair, shape::DimsLike) where {T}
    index = astuple(first(index_value))
    value = last(index_value)
    array = _OneHotArray(T, index, value, astuple(shape))
    @boundscheck checkbounds(array, index...)
    return array
end

@inline OneHotArray(index_value::Pair, shape::DimsLike) =
    OneHotArray{typeof(last(index_value))}(index_value, shape)

@inline OneHotArray{T,N}(index_value::Pair, shape::NTuple{N,<:Integer}) where {T,N} =
    OneHotArray{T}(index_value, shape)::OneHotArray{T,N}

@inline OneHotArray{<:Any,N}(index_value::Pair{<:Any,T}, shape::DimsLike) where {N,T} =
    OneHotArray(index_value, astuple(shape))::OneHotArray{<:Any,N}

@inline OneHotVector{T}(index_value::Pair, length::Integer) where {T,N} =
    OneHotArray{T}(index_value, (length,))::OneHotArray{T,N}

Base.size(A::OneHotArray) = A.shape

@inline function Base.getindex(A::OneHotArray{<:Any,N}, I::Vararg{Int,N}) where {N}
    index::NTuple{N,Int} = to_indices(A, A.index)
    if I == index
        return A.value
    else
        throw(UndefRefError())
    end
end

commonsize(a::OneHotArray) = size(a)
Base.@propagate_inbounds function commonsize(a::OneHotArray, b::OneHotArray, rest...)
    @boundscheck if size(a) != size(b)
        error("incompatible size: size(a) = ", size(a), " size(b) = ", size(b))
    end
    commonsize(a, rest...)
    return size(a)
end

Base.@propagate_inbounds function BangBang.Extras.broadcast_inplace!!(
    f,
    inputoutput,
    oh::OneHotArray,
)
    i = indexof(oh)
    v = valueof(oh)
    return setindex!!(inputoutput, f(inputoutput[i...], v), i...)
end

Base.@propagate_inbounds function BangBang.Extras.broadcast_inplace!!(
    op::OP,
    ::GenericInitialValue{OP},
    oh::OneHotArray,
    rest::OneHotArray...,
) where {OP}
    T = mapfoldl(eltype, promote_type, rest; init = eltype(oh))
    output = fill(Base.reduce_empty(op, T), commonsize(oh, rest...))
    output[indexof(oh)...] = valueof(oh)
    for oh in rest  # TODO: unroll?
        output[indexof(oh)...] = valueof(oh)
    end
    return output
end


using JSON
Nproc=16
time_LL_grad=zeros(Nproc)
time_LL=zeros(Nproc)
time_PR=zeros(Nproc)

for i in 1:Nproc
    rmprocs(workers())
    addprocs(i)
    println("workers: ",length(workers()))
    @everywhere import Base.convert
    @everywhere using MAT
    @everywhere using ForwardDiff
    @everywhere using Optim
    @everywhere using Polynomials
    @everywhere using JSON
    @everywhere model_num="10"

    @everywhere Nstim = 80000

    @everywhere path="/home/hcli64/hcli64751/rats/fit_behaviour_julia/"
    #
    @everywhere include(path*"functions/functions_julia.jl")
    @everywhere include(path*"functions/parameter_functions.jl")
    @everywhere include(path*"functions/utils.jl")
    @everywhere rat_num = "1"
    @everywhere path_load=path*"scripts_rats/data/"
    @everywhere filename="processed_data_rat"*rat_num*".json"

    @everywhere data = Dict()
    open(path_load*filename, "r") do f
        global data
        dicttxt = readstring(f)  # file information to string
        data=JSON.parse(dicttxt)  # parse and transform data
    end

    stim_10 = data["stim_10"][1:Nstim,:]
    choices_10 = data["choices_10"][1:Nstim,:]

    @everywhere history_bias=zeros(Nstim)
    ###### check the gradient ##########

    @everywhere Nframes=10
    @everywhere Tframe=.05
    model_num="10"

    @everywhere args=[("c2",[1]),("c4",0),("st",[1,2]),("x0",0),("bias_d",0),("hbias_d",0),("sigma_a",[1])]
    @everywhere consts=[("c2",[2]),("c4",[1]),("st",[3,4]),("x0",[1,2]),("bias_d",[1]),("hbias_d",[1]),("sigma_a",0)]

    @everywhere z = [0,1,0,0,0,0,0,0]

    @everywhere lb=[-10.,0.0,0.0,0.1]
    @everywhere ub=[10.,10,10,10.]

    @everywhere model_f=Dict(:c2_f=>c2_urgency,:c4_f=>c4_const,:c6_f=>c6_zeros,:bias_d_f=>bias_d_const,
    :hbias_d_f=>history_bias_d_const,:st_f=>st_logistic_bias2,:x0_f=>initial_condition_bias_hbias,
    :ro_f=>read_out_perfect,:lambda=>0.0)

    x_ini=[ 0.6,4.72,6.38,1.52]
    LL = ComputeLL2(stim_10,choices_10,history_bias,Tframe, args, x_ini,consts,z,model_f)
    ll_t,grad_t=LL_function_grads(stim_10,choices_10,history_bias,Tframe, args, x_ini,consts,z,model_f)
    time_LL[i]=@elapsed ComputeLL2(stim_10,choices_10,history_bias,Tframe, args, x_ini,consts,z,model_f)
    time_LL_grad[i]=@elapsed ll_t,grad_t=LL_function_grads(stim_10,choices_10,history_bias,Tframe, args, x_ini,consts,z,model_f)
    param=make_dict(args,x_ini,consts,z)
    time_PR[i] = @elapsed PR=ProbRight2(stim_10,history_bias,Tframe,param,model_f)

end
@everywhere path_save="/home/hcli64/hcli64751/rats/fit_behaviour_julia/results/"
mkpath(path_save)
results = Dict("time_LL"=>time_LL,"time_LL_grad"=>time_LL_grad,"time_PR"=>time_PR,"workers"=>1:Nproc,"Nstim"=>Nstim)
# pass data as a json string (how it shall be displayed in a file)
stringdata = JSON.json(results)
# write the file with the stringdata variable information
open(path_save*"number_workers.json", "w") do f
        write(f, stringdata)
     end
println("done")


using Gtk, Graphics, Colors

function drawline(ctx, p1, p2, color, width)
    move_to(ctx, p1.x, p1.y)
    set_source(ctx, color)
    line_to(ctx, p2.x, p2.y)
    set_line_width(ctx, width)
    stroke(ctx)
end

const can = @GtkCanvas()
const win = GtkWindow(can, "Colour pinstripe/Display", 400, 400)
const colors = [colorant"black", colorant"red", colorant"green", colorant"blue",
          colorant"magenta", colorant"cyan", colorant"yellow", colorant"white"]
const numcolors = length(colors)

@guarded draw(can) do widget
    ctx = getgc(can)
    h = height(can)
    w = width(can)
    deltaw = 1.0
    for (i, x) in enumerate(0:deltaw:w)
        drawline(ctx, Point(x, 0.25*h), Point(x, 0), colors[i % numcolors + 1], deltaw)
    end
    for (i, x) in enumerate(0:deltaw*2:w)
        drawline(ctx, Point(x, 0.5*h), Point(x, 0.25*h), colors[i % numcolors + 1], deltaw*2)
    end
    for (i, x) in enumerate(0:deltaw*3:w)
        drawline(ctx, Point(x, 0.75*h), Point(x, 0.5*h), colors[i % numcolors + 1], deltaw*3)
    end
    for (i, x) in enumerate(0:deltaw*4:w)
        drawline(ctx, Point(x, h), Point(x, 0.75*h), colors[i % numcolors + 1], deltaw*4)
    end
end

show(can)
const cond = Condition()
endit(w) = notify(cond)
signal_connect(endit, win, :destroy)
wait(cond)


struct Events{T <: DiseaseStateSequence}
  exposure::Union{Nothing, Vector{Float64}}
  infection::Vector{Float64}
  removal::Union{Nothing, Vector{Float64}}

  function Events{T}(e::V, i::V, r::V) where {T <: SEIR, V <: Vector{Float64}}
    if length(unique((length.([e; i; r])))) != 1
      @error "Length of event time vectors must be equal"
    end
    return new{T}(e, i, r)
  end

  function Events{T}(n::Integer) where T <: SEIR
    return new{T}(fill(NaN, n), fill(NaN, n), fill(NaN, n))
  end

  function Events{T}(e::V, i::V) where {T <: SEI, V <: Vector{Float64}}
    if length(unique((length.([e; i])))) != 1
      @error "Length of event time vectors must be equal"
    end
    return new{T}(e, i, nothing)
  end

  function Events{T}(n::Integer) where T <: SEI
    return new{T}(fill(NaN, n), fill(NaN, n), nothing)
  end

  function Events{T}(i::V, r::V) where {T <: SIR, V <: Vector{Float64}}
    if length(unique((length.([i; r])))) != 1
      @error "Length of event time vectors must be equal"
    end
    return new{T}(nothing, i, r)
  end

  function Events{T}(n::Integer) where T <: SIR
    return new{T}(nothing, fill(NaN, n), fill(NaN, n))
  end

  function Events{T}(i::V) where {T <: SI, V <: Vector{Float64}}
    return new{T}(nothing, i, nothing)
  end

  function Events{T}(n::Integer) where T <: SI
    return new{T}(nothing, fill(NaN, n), nothing)
  end
end

function Events{T}(a::Array{Float64,2}) where T <: DiseaseStateSequence
  if size(a, 2) == 3
    return Events{T}(a[:,1], a[:,2], a[:,3])
  elseif size(a, 2) == 2
    return Events{T}(a[:,1], a[:,2])
  elseif size(a, 2) == 1
    return Events{T}(a[:,1])
  else
    @error "Invalid array size for construction of an $(Events{T}) object"
  end
end

function Events{T}(x::DiseaseStates) where T <: DiseaseStateSequence
  events = Events{T}(length(x))
  for i = 1:length(x)
    if x[i] in convert(DiseaseStates, T)
      for j = convert(DiseaseStates, T)[2:findfirst(Ref(x[i]) .== convert(DiseaseStates, T))]
        events[j][i] = -Inf
      end
    else
      @error "Invalid initial state for individual $i"
    end
  end
  return events
end

function individuals(x::Events{M}) where{
                     M <: DiseaseStateSequence}
  return length(x.infection)
end

function Base.show(io::IO, x::Events{T}) where T <: DiseaseStateSequence
  return print(io, "$T model event times (n=$(individuals(x)))")
end

function Base.getindex(x::Events{T}, new_state::DiseaseState) where T <: DiseaseStateSequence
  if new_state == State_E
    return x.exposure
  elseif new_state == State_I
    return x.infection
  elseif new_state == State_R
    return x.removal
  else
    error("Unsupported indexing disease state")
  end
end

function Base.getindex(x::Events{T}, states::DiseaseStates) where T <: DiseaseStateSequence
  y = x[states[1]]
  for i = 2:length(states)
    y = hcat(y, x[states[i]])
  end
  return y
end

function Base.convert(::Type{Array{Float64, 2}}, x::Events{T}) where T <: DiseaseStateSequence
  return x[convert(DiseaseStates, T)[2:end]]
end

function Base.convert(::Type{Vector{Float64}}, x::Events{T}) where T <: DiseaseStateSequence
  return x[convert(DiseaseStates, T)[2:end]][:]
end

function Base.convert(::Type{Array{Float64, 2}}, x::Array{Events{T}, 1}) where T <: DiseaseStateSequence
  y = convert(Vector{Float64}, x[1])'
  for i = 2:length(x)
    y = vcat(y, convert(Vector{Float64}, x[i])')
  end
  return y
end

function Base.minimum(x::Events{T}) where T <: DiseaseStateSequence
  y = convert(Array{Float64, 2}, x)
  return minimum(y[(y .!== NaN) .& (y .> -Inf)])
end

function Base.maximum(x::Events{T}) where T <: DiseaseStateSequence
  y = convert(Array{Float64, 2}, x)
  return maximum(y[(y .!== NaN) .& (y .> -Inf)])
end

function mean(x::Vector{Events{T}}) where T <: DiseaseStateSequence
  return Events{T}(mean([convert(Array{Float64, 2}, i) for i in x]))
end


"""
  module JCRHF
The module required for computation of the wave function using the *Restricted
Hartree-Fock* (RHF) method in a Self-Consistent Field (SCF) calculation. This
module will be used often, as the RHF wave function is often the zeroth-order
wave function for closed-shell systems.
"""
module JCRHF

#== RHF energy module ==#
Base.include(@__MODULE__,"energy/Energy.jl")
export Energy

#== RHF gradient module ==#
Base.include(@__MODULE__,"gradient/Gradient.jl")
export Gradient

#== RHF properties module ==#
Base.include(@__MODULE__,"properties/Properties.jl")
export Properties

end


using YaoGraphRegister
using Test

@testset "YaoGraphRegister.jl" begin
    # Write your own tests here.
end

using LightGraphs, GraphPlot, Compose

g = SimpleGraph(10, 10)
gplot(g)


"""
    ocean_boundary_state!(::NumericalFluxFirstOrder, ::Impenetrable{FreeSlip}, ::SWModel)

apply free slip boundary condition for velocity
sets reflective ghost point
"""
@inline function ocean_boundary_state!(
    ::NumericalFluxFirstOrder,
    ::Impenetrable{FreeSlip},
    ::SWModel,
    ::TurbulenceClosure,
    q⁺,
    a⁺,
    n⁻,
    q⁻,
    a⁻,
    t,
    args...,
)
    q⁺.η = q⁻.η

    V⁻ = @SVector [q⁻.U[1], q⁻.U[2], -0]
    V⁺ = V⁻ - 2 * n⁻ ⋅ V⁻ .* SVector(n⁻)
    q⁺.U = @SVector [V⁺[1], V⁺[2]]

    return nothing
end

"""
    ocean_boundary_state!(::Union{NumericalFluxGradient, NumericalFluxSecondOrder}, ::Impenetrable{FreeSlip}, ::SWModel)

no second order flux computed for linear drag
"""
ocean_boundary_state!(
    ::Union{NumericalFluxGradient, NumericalFluxSecondOrder},
    ::VelocityBC,
    ::SWModel,
    ::LinearDrag,
    _...,
) = nothing

"""
    ocean_boundary_state!(::NumericalFluxGradient, ::Impenetrable{FreeSlip}, ::SWModel)

apply free slip boundary condition for velocity
sets non-reflective ghost point
"""
function ocean_boundary_state!(
    ::NumericalFluxGradient,
    ::Impenetrable{FreeSlip},
    ::SWModel,
    ::ConstantViscosity,
    Q⁺,
    A⁺,
    n⁻,
    Q⁻,
    A⁻,
    t,
    args...,
)
    V⁻ = @SVector [Q⁻.U[1], Q⁻.U[2], -0]
    V⁺ = V⁻ - n⁻ ⋅ V⁻ .* SVector(n⁻)
    Q⁺.U = @SVector [V⁺[1], V⁺[2]]

    return nothing
end

"""
    shallow_normal_boundary_flux_second_order!(::NumericalFluxSecondOrder, ::Impenetrable{FreeSlip}, ::SWModel)

apply free slip boundary condition for velocity
apply zero numerical flux in the normal direction
"""
function ocean_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Impenetrable{FreeSlip},
    ::SWModel,
    ::ConstantViscosity,
    Q⁺,
    D⁺,
    A⁺,
    n⁻,
    Q⁻,
    D⁻,
    A⁻,
    t,
    args...,
)
    Q⁺.U = Q⁻.U
    D⁺.ν∇U = n⁻ * (@SVector [-0, -0])'

    return nothing
end

"""
    ocean_boundary_state!(::NumericalFluxFirstOrder, ::Impenetrable{NoSlip}, ::SWModel)

apply no slip boundary condition for velocity
sets reflective ghost point
"""
@inline function ocean_boundary_state!(
    ::NumericalFluxFirstOrder,
    ::Impenetrable{NoSlip},
    ::SWModel,
    ::TurbulenceClosure,
    q⁺,
    α⁺,
    n⁻,
    q⁻,
    α⁻,
    t,
    args...,
)
    q⁺.η = q⁻.η
    q⁺.U = -q⁻.U

    return nothing
end

"""
    ocean_boundary_state!(::NumericalFluxGradient, ::Impenetrable{NoSlip}, ::SWModel)

apply no slip boundary condition for velocity
set numerical flux to zero for U
"""
@inline function ocean_boundary_state!(
    ::NumericalFluxGradient,
    ::Impenetrable{NoSlip},
    ::SWModel,
    ::ConstantViscosity,
    q⁺,
    α⁺,
    n⁻,
    q⁻,
    α⁻,
    t,
    args...,
)
    FT = eltype(q⁺)
    q⁺.U = @SVector zeros(FT, 2)

    return nothing
end

"""
    ocean_boundary_state!(::NumericalFluxSecondOrder, ::Impenetrable{NoSlip}, ::SWModel)

apply no slip boundary condition for velocity
sets ghost point to have no numerical flux on the boundary for U
"""
@inline function ocean_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Impenetrable{NoSlip},
    ::SWModel,
    ::ConstantViscosity,
    q⁺,
    σ⁺,
    α⁺,
    n⁻,
    q⁻,
    σ⁻,
    α⁻,
    t,
    args...,
)
    q⁺.U = -q⁻.U
    σ⁺.ν∇U = σ⁻.ν∇U

    return nothing
end

"""
    ocean_boundary_state!(::Union{NumericalFluxFirstOrder, NumericalFluxGradient}, ::Penetrable{FreeSlip}, ::SWModel)

no mass boundary condition for penetrable
"""
ocean_boundary_state!(
    ::Union{NumericalFluxFirstOrder, NumericalFluxGradient},
    ::Penetrable{FreeSlip},
    ::SWModel,
    ::ConstantViscosity,
    _...,
) = nothing

"""
    ocean_boundary_state!(::NumericalFluxSecondOrder, ::Penetrable{FreeSlip}, ::SWModel)

apply free slip boundary condition for velocity
apply zero numerical flux in the normal direction
"""
function ocean_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Penetrable{FreeSlip},
    ::SWModel,
    ::ConstantViscosity,
    Q⁺,
    D⁺,
    A⁺,
    n⁻,
    Q⁻,
    D⁻,
    A⁻,
    t,
    args...,
)
    Q⁺.U = Q⁻.U
    D⁺.ν∇U = n⁻ * (@SVector [-0, -0])'

    return nothing
end

"""
    ocean_boundary_state!(::Union{NumericalFluxFirstOrder, NumericalFluxGradient}, ::Impenetrable{KinematicStress}, ::HBModel)

apply kinematic stress boundary condition for velocity
applies free slip conditions for first-order and gradient fluxes
"""
function ocean_boundary_state!(
    nf::Union{NumericalFluxFirstOrder, NumericalFluxGradient},
    ::Impenetrable{KinematicStress},
    shallow::SWModel,
    turb::TurbulenceClosure,
    args...,
)
    return ocean_boundary_state!(
        nf,
        Impenetrable(FreeSlip()),
        shallow,
        turb,
        args...,
    )
end

"""
    ocean_boundary_state!(::NumericalFluxSecondOrder, ::Impenetrable{KinematicStress}, ::HBModel)

apply kinematic stress boundary condition for velocity
sets ghost point to have specified flux on the boundary for ν∇u
"""
@inline function ocean_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Impenetrable{KinematicStress},
    shallow::SWModel,
    Q⁺,
    D⁺,
    A⁺,
    n⁻,
    Q⁻,
    D⁻,
    A⁻,
    t,
)
    Q⁺.U = Q⁻.U
    D⁺.ν∇U = n⁻ * kinematic_stress(shallow.problem, A⁻.y, 1000)'
    # applies windstress for now, will be fixed in a later PR

    return nothing
end

"""
    ocean_boundary_state!(::Union{NumericalFluxFirstOrder, NumericalFluxGradient}, ::Penetrable{KinematicStress}, ::HBModel)

apply kinematic stress boundary condition for velocity
applies free slip conditions for first-order and gradient fluxes
"""
function ocean_boundary_state!(
    nf::Union{NumericalFluxFirstOrder, NumericalFluxGradient},
    ::Penetrable{KinematicStress},
    shallow::SWModel,
    turb::TurbulenceClosure,
    args...,
)
    return ocean_boundary_state!(
        nf,
        Penetrable(FreeSlip()),
        shallow,
        turb,
        args...,
    )
end

"""
    ocean_boundary_state!(::NumericalFluxSecondOrder, ::Penetrable{KinematicStress}, ::HBModel)

apply kinematic stress boundary condition for velocity
sets ghost point to have specified flux on the boundary for ν∇u
"""
@inline function ocean_boundary_state!(
    ::NumericalFluxSecondOrder,
    ::Penetrable{KinematicStress},
    shallow::SWModel,
    ::TurbulenceClosure,
    Q⁺,
    D⁺,
    A⁺,
    n⁻,
    Q⁻,
    D⁻,
    A⁻,
    t,
)
    Q⁺.u = Q⁻.u
    D⁺.ν∇u = n⁻ * kinematic_stress(shallow.problem, A⁻.y, 1000)'
    # applies windstress for now, will be fixed in a later PR

    return nothing
end


export prunenode!, prunenode, graftnode!, delete_node!, delete_null_branches!, remove_internal_singletons, prunesubtree!

"""
	prunenode!(node::TreeNode)

Prune node `node` by detaching it from its ancestor. Return pruned `node` and the root of its ancestor. The whole tree is modified.
"""
function prunenode!(node::TreeNode)
	if node.isroot
		@warn "Trying to prune root: no op."
		return node, TreeNode()
	end
	anc = node.anc
	for (i,c) in enumerate(anc.child)
		if c == node
			splice!(anc.child,i)
			break
		end
	end
	if isempty(anc.child)
		anc.isleaf = true
	end
	node.anc = nothing
	node.isroot = true
	return node, node_findroot(anc)
end

"""
	prunenode(node::TreeNode)

Prune node `node` by detaching it from its ancestor. Return pruned `node` and previous root `r`. The tree defined by `node` is copied before the operation, and therefore not modified.
"""
function prunenode(node::TreeNode)
	if node.isroot
		@warn "Trying to prune root: no op."
		node_ = deepcopy(node)
		return node_, node_
	end
	node_ = deepcopy(node)
	r = node_findroot(node_)
	anc = node_.anc
	for (i,c) in enumerate(anc.child)
		if c == node_
			splice!(anc.child,i)
			break
		end
	end
	node_.anc = nothing
	node_.isroot = true
	return node_, r
end

"""
	prunenode(t::Tree, label::Vararg{String})

Prune node `t.lnodes[label]` from `t` for all `label`. Return pruned copy of `t`.
"""
function prunenode(t::Tree, label::Vararg{String} ; propagate=true)
	return prunenode(t, collect(label), propagate=propagate)
end

"""
	prunenodes(tree, labels)

Prune nodes corresponding to labels in `labels`.
"""
function prunenode(tree, labels ; propagate=true)
	out = deepcopy(tree)
	for l in labels
		propagate ? prunenode_!(out.lnodes[l]) : prunenode!(out.lnodes[l])
	end
	out = node2tree(out.root)
end

"""
"""
function prunenode_!(node)
	if length(node.anc.child) == 1
		prunenode_!(node.anc)
	else
		prunenode!(node)
	end
end

"""
	prunesubtree!(tree, labellist)

Prune and return subtree corresponding to the MRCA of labels in `labellist`, as well as its previous direct ancestor.
# Warning
`TreeNode` objects contained in `tree` are modified, but `tree` is *not* re-indexed after the pruning. It is therefore necessary to call `node2tree(tree.root)` after this.
"""
function prunesubtree!(tree, labellist)
	r = lca([tree.lnodes[x] for x in labellist])
	a = r.anc
	if !r.isroot
		subtree = node2tree(prunenode!(r)[1])
	else
		@warn "Trying to prune root"
	end
	return subtree, a
end

"""
	remove_internal_singletons!(tree)

Remove nodes with one child. Return a new tree. Root node is left as is.

## Warning
The `TreeNode` constituting `tree` are modified in the process. This means `tree` will be be modifier as well in an uncontrolled manner.
"""
function remove_internal_singletons!(tree)
	root = tree.root
	for n in values(tree.nodes)
		if !n.isleaf && !n.isroot
			if length(n.child) == 1
				delete_node!(n, ptau=true)
			end
		end
	end
	return node2tree(root)
end

"""
	graftnode!(r::TreeNode, n::TreeNode ; tau=n.data.tau)

Graft `n` on `r`.
"""
function graftnode!(r::TreeNode, n::TreeNode ; tau=n.data.tau)
	if !n.isroot || n.anc != nothing
		@error "Trying to graft non-root node."
	end
	push!(r.child, n)
	r.isleaf = false
	n.anc = r
	n.isroot = false
	n.data.tau = tau
end

"""
	delete_node!(node::TreeNode; ptau=false)

Delete `node` from the tree. If it is an internal node, its children are regrafted on `node.anc`. Returns the new `node.anc`.  If `ptau`, branch length above `node` is added to the regrafted branch. Otherwise, the regrafted branch's length is unchanged.
"""
function delete_node!(node::TreeNode; ptau=false)
	if node.isroot
		@error "Cannot delete root node"
		error()
	end
	out = node.anc
	if node.isleaf
		prunenode!(node)
	else
		base_tau = node.data.tau
		child_list = []
		for c in node.child
			push!(child_list, c)
		end
		for c in child_list
			nc = prunenode!(c)[1]
			graftnode!(node.anc, nc, tau = (base_tau*ptau + nc.data.tau))
		end
		prunenode!(node)
	end
	return out
end

"""
	delete_null_branches!(node ; threshold = 1e-10)

Delete internal node with null branch length.
- If `node` needs not be deleted, call `delete_null_branches!` on its children
- If need be, call `delete_null_branches!` on `node.anc.child`
"""
function delete_null_branches!(node; threshold = 1e-10)
	if !node.isleaf
		if !ismissing(node.data.tau) && node.data.tau < threshold && !node.isroot
			nr = delete_node!(node)
			for c in nr.child
				delete_null_branches!(c, threshold=threshold)
			end
		else
			for c in node.child
				delete_null_branches!(c,threshold=threshold)
			end
		end
	end
end

"""
	reroot!(node::TreeNode ; newroot::Union{TreeNode,Nothing}=nothing)
- If `node.isroot`,
- Else if `newroot == nothing`, reroot the tree defined by `node` at `node`. Call `reroot!(node.anc, node)`.
- Else, call `reroot!(node.anc, node)`, then change the ancestor of `node` to be `newroot`.
"""
function reroot!(node::Union{TreeNode,Nothing}; newroot::Union{TreeNode, Nothing}=nothing)
	# Breaking cases
	if node.anc == nothing || node.isroot
		if !(node.anc == nothing && node.isroot)
			@warn "There was a problem with input tree: previous root node has an ancestor."
		elseif newroot != nothing
			i = findfirst(c->c.label==newroot.label, node.child)
			splice!(node.child, i)
			node.anc = newroot
			node.data.tau = newroot.data.tau
			node.isroot = false
		end
	else # Recursion
		if newroot == nothing
			if node.isleaf
				@warn "Rooting on a leaf node..."
			end
			node.isroot = true
			reroot!(node.anc, newroot=node)
			push!(node.child, node.anc)
			node.anc = nothing
			node.data.tau = missing
		else
			i = findfirst(c->c.label==newroot.label, node.child)
			splice!(node.child, i)
			reroot!(node.anc, newroot=node)
			push!(node.child, node.anc)
			node.anc = newroot
			node.data.tau = newroot.data.tau
		end
	end
end


#########################################################################
#                                                                       #
# This file provides functions for interfacing with Matlab .m files     #
#                                                                       #
#########################################################################

# this could benefit from a much more robust parser

export parse_matlab_file, parse_matlab_string

function parse_matlab_file(file_string::String; kwargs...)
    data_string = read(open(file_string), String)
    return parse_matlab_string(data_string; kwargs...)
end

function parse_matlab_string(data_string::String; extended = false)
    data_lines = split(data_string, '\n')

    matlab_dict = Dict{String, Any}()
    struct_name = nothing
    function_name = nothing
    column_names = Dict{String, Any}()

    last_index = length(data_lines)
    index = 1
    while index <= last_index
        line = strip(data_lines[index])
        line = "$(line)"

        if length(line) <= 0 || strip(line)[1] == '%'
            index = index + 1
            continue
        end

        if occursin("function", line)
            func, value = _extract_matlab_assignment(line)
            struct_name = strip(replace(func, "function" => ""))
            function_name = value
        elseif occursin("=", line)
            if struct_name != nothing && !occursin("$(struct_name).", line)
                @warn "assignments are expected to be made to \"$(struct_name)\" but given: $(line)"
            end

            if occursin("[", line)
                matrix_dict = _parse_matlab_matrix(data_lines, index)
                matlab_dict[matrix_dict["name"]] = matrix_dict["data"]
                if haskey(matrix_dict, "column_names")
                    column_names[matrix_dict["name"]] = matrix_dict["column_names"]
                end
                index = index + matrix_dict["line_count"] - 1
            elseif occursin("{", line)
                cell_dict = _parse_matlab_cells(data_lines, index)
                matlab_dict[cell_dict["name"]] = cell_dict["data"]
                if haskey(cell_dict, "column_names")
                    column_names[cell_dict["name"]] = cell_dict["column_names"]
                end
                index = index + cell_dict["line_count"] - 1
            else
                name, value = _extract_matlab_assignment(line)
                value = _type_value(value)
                matlab_dict[name] = value
            end
        else
            @error "Matlab parser skipping line number $(index) consisting of:\n  $(line)"
        end

        index += 1
    end

    if extended
        return matlab_dict, function_name, column_names
    else
        return matlab_dict
    end
end

"breaks up matlab strings of the form 'name = value;'"
function _extract_matlab_assignment(string::AbstractString)
    statement = split(string, ';')[1]
    statement_parts = split(statement, '=')
    @assert(length(statement_parts) == 2)
    name = strip(statement_parts[1])
    value = strip(statement_parts[2])
    return name, value
end

"Attempts to determine the type of a string extracted from a matlab file"
function _type_value(value_string::AbstractString)
    value_string = strip(value_string)

    if occursin("'", value_string) # value is a string
        value = strip(value_string, '\'')
    else
        # if value is a float
        if occursin(".", value_string) || occursin("e", value_string)
            value = check_type(Float64, value_string)
        else # otherwise assume it is an int
            value = check_type(Int, value_string)
        end
    end

    return value
end

"Attempts to determine the type of an array of strings extracted from a matlab file"
function _type_array(string_array::Vector{T}) where {T <: AbstractString}
    value_string = [strip(value_string) for value_string in string_array]

    return if any(occursin("'", value_string) for value_string in string_array)
        [strip(value_string, '\'') for value_string in string_array]
    elseif any(
        occursin(".", value_string) || occursin("e", value_string)
        for value_string in string_array
    )
        [check_type(Float64, value_string) for value_string in string_array]
    else # otherwise assume it is an int
        [check_type(Int, value_string) for value_string in string_array]
    end
end

""
_parse_matlab_cells(lines, index) = _parse_matlab_data(lines, index, '{', '}')

""
_parse_matlab_matrix(lines, index) = _parse_matlab_data(lines, index, '[', ']')

""
function _parse_matlab_data(lines, index, start_char, end_char)
    last_index = length(lines)
    line_count = 0
    columns = -1

    @assert(occursin("=", lines[index + line_count]))
    matrix_assignment = split(lines[index + line_count], '%')[1]
    matrix_assignment = strip(matrix_assignment)

    @assert(occursin(".", matrix_assignment))
    matrix_assignment_parts = split(matrix_assignment, '=')
    matrix_name = strip(matrix_assignment_parts[1])

    matrix_assignment_rhs = ""
    if length(matrix_assignment_parts) > 1
        matrix_assignment_rhs = strip(matrix_assignment_parts[2])
    end

    line_count = line_count + 1
    matrix_body_lines = [matrix_assignment_rhs]
    found_close_bracket = occursin(string(end_char), matrix_assignment_rhs)

    while index + line_count < last_index && !found_close_bracket
        line = strip(lines[index + line_count])

        if length(line) == 0 || line[1] == '%'
            line_count += 1
            continue
        end

        line = strip(split(line, '%')[1])

        if occursin(string(end_char), line)
            found_close_bracket = true
        end

        push!(matrix_body_lines, line)

        line_count = line_count + 1
    end

    #print(matrix_body_lines)
    matrix_body_lines =
        [_add_line_delimiter(line, start_char, end_char) for line in matrix_body_lines]
    #print(matrix_body_lines)

    matrix_body = join(matrix_body_lines, ' ')
    matrix_body =
        strip(replace(strip(strip(matrix_body), start_char), "$(end_char);" => ""))
    matrix_body_rows = split(matrix_body, ';')
    matrix_body_rows = matrix_body_rows[1:(length(matrix_body_rows) - 1)]

    matrix = []
    for row in matrix_body_rows
        row_items = split_line(strip(row))
        #println(row_items)
        push!(matrix, row_items)
        if columns < 0
            columns = length(row_items)
        elseif columns != length(row_items)
            @error "matrix parsing error, inconsistent number of items in each row\n$(row)"
        end
    end

    rows = length(matrix)
    typed_columns = [_type_array([matrix[r][c] for r in 1:rows]) for c in 1:columns]
    for r in 1:rows
        matrix[r] = [typed_columns[c][r] for c in 1:columns]
    end

    matrix_dict = Dict("name" => matrix_name, "data" => matrix, "line_count" => line_count)

    if index > 1 && occursin("%column_names%", lines[index - 1])
        column_names_string = lines[index - 1]
        column_names_string = replace(column_names_string, "%column_names%" => "")
        column_names = split(column_names_string)
        if length(matrix[1]) != length(column_names)
            @error "column name parsing error, data rows $(length(matrix[1])), column names $(length(column_names)) \n$(column_names)"
        end
        if any([column_name == "index" for column_name in column_names])
            @error "column name parsing error, \"index\" is a reserved column name \n$(column_names)"
        end
        matrix_dict["column_names"] = column_names
    end

    return matrix_dict
end

const single_quote_expr = r"\'((\\.|[^\'])*?)\'"

""
function split_line(mp_line::AbstractString)
    if occursin(single_quote_expr, mp_line)
        # splits a string on white space while escaping text quoted with "'"
        # note that quotes will be stripped later, when data typing occurs

        #println(mp_line)
        tokens = []
        while length(mp_line) > 0 && occursin(single_quote_expr, mp_line)
            #println(mp_line)
            m = match(single_quote_expr, mp_line)

            if m.offset > 1
                push!(tokens, mp_line[1:(m.offset - 1)])
            end
            push!(tokens, replace(m.match, "\\'" => "'")) # replace escaped quotes

            mp_line = mp_line[(m.offset + length(m.match)):end]
        end
        if length(mp_line) > 0
            push!(tokens, mp_line)
        end
        #println(tokens)

        items = []
        for token in tokens
            if occursin("'", token)
                push!(items, strip(token))
            else
                for parts in split(token)
                    push!(items, strip(parts))
                end
            end
        end
        #println(items)

        #return [strip(mp_line, '\'')]
        return items
    else
        return split(mp_line)
    end
end

""
function _add_line_delimiter(mp_line::AbstractString, start_char, end_char)
    if strip(mp_line) == string(start_char)
        return mp_line
    end

    if !occursin(";", mp_line) && !occursin(string(end_char), mp_line)
        mp_line = "$(mp_line);"
    end

    if occursin(string(end_char), mp_line)
        prefix = strip(split(mp_line, end_char)[1])
        if length(prefix) > 0 && !occursin(";", prefix)
            mp_line = replace(mp_line, end_char => ";$(end_char)")
        end
    end

    return mp_line
end

"Checks if the given value is of a given type, if not tries to make it that type"
function check_type(typ, value)
    if isa(value, typ)
        return value
    elseif isa(value, String) || isa(value, SubString)
        try
            value = parse(typ, value)
            return value
        catch e
            @error "parsing error, the matlab string \"$(value)\" can not be parsed to $(typ) data"
            rethrow(e)
        end
    else
        try
            value = typ(value)
            return value
        catch e
            @error "parsing error, the matlab value $(value) of type $(typeof(value)) can not be parsed to $(typ) data"
            rethrow(e)
        end
    end
end


@everywhere struct Constants
    C::Int
    P::Int
    J::Int
    Q::Int
    H::Int
    tt::Int
    delta::Int
end

@everywhere mutable struct Timer
    period::Int
    horizon_plan::Int
    available_cranes::Array{Int, 1}
end

@everywhere struct LTask
    p::Int
    b::Int
    c::Int
    t::Int
end

@everywhere mutable struct LoadingSequence
    order::Array{NamedTuple{(:task, :start_time, :qc),Tuple{LTask,Int,Int}}, 1}
    tasks_left::Int
    len::Int
    filled_pos::Array{Int, 1}
    loaded_cont::Array{Int, 1}
end

@everywhere mutable struct QuayCrane
    q::Int
    available_bays::Array{Int, 1}
    status::String
    time_left::Int
    current_bay::Number
    next_bay::Int
    task_buffer::Array{LTask, 1}
end

@everywhere function init_timer(CTS::Constants)
    TIME = Timer(0, CTS.H, collect(1:CTS.Q))
    return(TIME)
end

@everywhere function init_ls(CTS::Constants)
    LS = LoadingSequence([], CTS.P, 0, [], [])
    return(LS)
end

@everywhere function init_bay(q::Int, CTS::Constants)
    if q == 1
        return(1)
    elseif  q == CTS.Q
        return(CTS.J)
    else
        return(floor(CTS.J/(CTS.Q-1)*(q-1)))
    end
end

@everywhere function init_qc(CTS::Constants)
    QC=Array{QuayCrane, 1}()
    for q = 1:CTS.Q
        qc_pos = subset_bay(CTS, q)
        push!(QC, QuayCrane(q, qc_pos, "idle", 0, init_bay(q, CTS), init_bay(q, CTS), Array{LTask, 1}()))
    end
    return(QC)
end

@everywhere function Base.show(io::IO, LS::LoadingSequence)
    println(io, "LOADING SEQUENCE")
    println(io, "- tasks_left: ", LS.tasks_left)
    println(io, "- filled_pos: ", LS.filled_pos)
    println(io, "- loaded_cont: ", LS.loaded_cont)
    for i = 1:length(LS.order)
        print(io, "  "*string(i)*". ")
        println(io, LS.order[i])
    end
end

function Base.show(io::IO, ::MIME"text/plain", QC::QuayCrane)
    println(io, "QUAY CRANE - "*string(QC.q))
    println(io, " - status: ", QC.status)
    println(io, " - time_left: ", QC.time_left)
    println(io, " - current_bay: ", QC.current_bay)
    println(io, " - next_bay: ", QC.next_bay)
    println(io, " - task_buffer: ", QC.task_buffer)
    println(io, " - available_bays: ", QC.available_bays)
    println("\n")
end

function Base.show(io::IO, QC::QuayCrane)
    println(io, string(QC.q)*". QUAY CRANE")
    println(io, " - status: ", QC.status)
    println(io, " - time_left: ", QC.time_left)
    println(io, " - current_bay: ", QC.current_bay)
    println(io, " - next_bay: ", QC.next_bay)
    println(io, " - task_buffer: ", QC.task_buffer)
    println(io, " - available_bays: ", QC.available_bays)
    println("\n")
end

function Base.show(io::IO, TIME::Timer)
    println("--------------")
    println(io, "TIME PERIOD: ", TIME.period)
    println(io, " - available cranes: ", TIME.available_cranes)
    println("--------------")
end


# This file is a part of BAT.jl, licensed under the MIT License (MIT).

using BAT
using Test

using ValueShapes, Distributions

@testset "variate_transform" begin
    @testset "identity_vt" begin
        @test @inferred(BAT.IdentityVT(BAT.InfiniteSpace(), ScalarShape{Real}())) isa BAT.VariateTransform{Univariate,BAT.InfiniteSpace,BAT.InfiniteSpace}
        @test @inferred(BAT.IdentityVT(BAT.UnitSpace(), ArrayShape{Int}(2))) isa BAT.VariateTransform{Multivariate,BAT.UnitSpace,BAT.UnitSpace}
        @test @inferred(BAT.IdentityVT(BAT.MixedSpace(), ArrayShape{Float32}(2, 3))) isa BAT.VariateTransform{Matrixvariate,BAT.MixedSpace,BAT.MixedSpace}

        ntshape = NamedTupleShape(a = ScalarShape{Real}(), b = ArrayShape{Int}(2))
        @test @inferred(BAT.IdentityVT(BAT.MixedSpace(), ntshape)) isa BAT.VariateTransform{<:ValueShapes.NamedTupleVariate{(:a, :b)},BAT.MixedSpace,BAT.MixedSpace}

        nttrafo = BAT.IdentityVT(BAT.MixedSpace(), ntshape)
        ntvalue = (a = 4.2, b = [5, 7])

        @test @inferred(varshape(nttrafo)) == ntshape
        @test @inferred(inv(nttrafo)) === nttrafo

        @test @inferred(BAT.apply_vartrafo(nttrafo, ntvalue, 0)).v === ntvalue
        @test BAT.apply_vartrafo(nttrafo, ntvalue, 0.79).ladj == 0.79
        @test isnan(BAT.apply_vartrafo(nttrafo, ntvalue, NaN).ladj)
        @test @inferred(BAT.apply_vartrafo(inv(nttrafo), ntvalue, 0.79)).v === ntvalue
        @test BAT.apply_vartrafo(inv(nttrafo), ntvalue, 0.79).ladj == 0.79
        @test isnan(BAT.apply_vartrafo(inv(nttrafo), ntvalue, NaN).ladj)

        @test @inferred(nttrafo ∘ nttrafo) === nttrafo
    end
end


fname = :TRKdense
c = Combi(hessian_dense,PDataK,solve_modelKTR,preprocessKTR,decreaseKTR,TparamsKTR())
include("Template.jl")


using MOCNeutronTransport

# I can't remember if the bottom left corner of the CMFD grid needs to be (0, 0, 0)
# To compensate, we do some extra math to ensure that happens.
grid_offset = 0.1 # Buffer between boundary of the model and the edge of the CMFD grid (cm)
# Bounding box for the CMFD grid
# xmin, xmax, ymin, ymax
bb = (0.0, 80.62054 + 2*grid_offset,
      0.0, 46.2645 + 2*grid_offset)

# Model setup
# ----------------------------------------------------------------------------------------
gmsh.initialize()
gmsh.option.set_number("Geometry.OCCParallel", 1)
gmsh.option.set_number("General.NumThreads", 32)
# Import CAD file
gmsh.merge("fnr_core_slice.step")
# Visualize the model
# gmsh.fltk.run()

# Shift the model to the origin of the xy-plane
# Get the dim tags of all dimension 2 entites
dim_tags = gmsh.model.get_entities(2)
# Translate it so that the corner is at the origin
gmsh.model.occ.translate(dim_tags, 407.51125 + 10*grid_offset,
                                   307.848 + 10*grid_offset,
                                   -300)
# Model is in mm. Convert to cm by shrinking everything by 1/10
gmsh.model.occ.dilate(dim_tags, 0, 0, 0, 1//10, 1//10, 0)
# Synchronize the CAD and Gmsh models
gmsh.model.occ.synchronize()
# Verify results
gmsh.fltk.run()

# Assign materials
# ------------------------------------------------------------------------------------------
all_tags = getindex.(gmsh.model.get_entities(2), 2)
# Assign the materials as physical groups
p = gmsh.model.add_physical_group(2, all_tags)
gmsh.model.set_physical_name(2, p, "MATERIAL_UO2")

# Overlay CMFD/hierarchical grid
# -------------------------------------------------------------------------------------------
nx = [10, 1]
ny = [6, 1]
grid_tags = gmsh_overlay_rectangular_grid(bb, "MATERIAL_MODERATOR", nx, ny)
# Verify results
gmsh.fltk.run()

# Mesh
# ------------------------------------------------------------------------------------------------
# Set the characteristic edge length of the mesh cells
lc = 0.3 # cm
gmsh.model.mesh.set_size(gmsh.model.get_entities(0), lc)
# Optional mesh optimization:
niter = 2 # The optimization iterations

# 2nd order triangles
gmsh.model.mesh.generate(2) # Triangles first for high order meshes.
gmsh.model.mesh.set_order(2)
for () in 1:niter
    gmsh.model.mesh.optimize("HighOrderElastic")
    gmsh.model.mesh.optimize("Relocate2D")
    gmsh.model.mesh.optimize("HighOrderElastic")
end

# gmsh.fltk.run()

# Mesh conversion
# ----------------------------------------------------------------------------------------------------
# We want to write an Abaqus file from Gmsh, read the Abaqus file into Julia, and convert that into
# a rectangularly partitioned hierarchical XDMF file, the final input to MPACT
#
# Write Abaqus file
gmsh.write("fnr_slice.inp")
gmsh.finalize() # done with Gmsh. Finalize
mesh = read_abaqus_2d("fnr_slice.inp")
# Convert mesh into Hierarchical Rectangularly Partitioned Mesh
HRPM = partition_rectangularly(mesh)
write_xdmf_2d("fnr_slice.xdmf", HRPM)
#
## Mass conservation
## ----------------------------------------------------------------------------------------------------
## See how well your mesh conserves mass. Analytic solution not available, so fine tri6 mesh used as
## reference
#fuel_area = area(mesh, "MATERIAL_UO2")
#fuel_area_ref = 6.119632103711788
#println("Fuel area (reference): $fuel_area_ref")
#println("Fuel area ( computed): $fuel_area")
#err = 100*(fuel_area - fuel_area_ref)/fuel_area_ref
#println("Error (relative): $err %")
#println("")
#
#clad_area = area(mesh, "MATERIAL_CLAD")
#clad_area_ref = 19.884367501087866
#println("Clad area (reference): $clad_area_ref")
#println("Clad area ( computed): $clad_area")
#err = 100*(clad_area - clad_area_ref)/clad_area_ref
#println("Error (relative): $err %")


struct LazyMulAdd{M,O,T<:NativeTypesV} <: Number
    data::T
    @inline LazyMulAdd{M,O,T}(data::T) where {M,O,T<:NativeTypesV} = new{M,O,T}(data)
end
# O for offset is kind of hard to read next to default of 0?
@inline LazyMulAdd{M,O}(data::T) where {M,O,T<:Union{Base.HWReal,AbstractSIMD}} = LazyMulAdd{M,O,T}(data)
@inline LazyMulAdd{M}(data::T) where {M,T<:NativeTypesV} = LazyMulAdd{M,0,T}(data)
@inline LazyMulAdd{0,O}(data::T) where {O,T<:Union{Base.HWReal,AbstractSIMD}} = @assert false#StaticInt{O}()
@inline LazyMulAdd{0}(data::T) where {T<:NativeTypesV} = @assert false#StaticInt{0}()
@inline LazyMulAdd(data::T, ::StaticInt{M}) where {M,T} = LazyMulAdd{M,0,T}(data)
@inline LazyMulAdd(data::T, ::StaticInt{M}, ::StaticInt{O}) where {M,O,T} = LazyMulAdd{M,O,T}(data)

@inline data(lm::LazyMulAdd) = data(getfield(lm, :data)) # calls data on inner for use with indexing (normally `data` only goes through one layer)

@inline _materialize(a::LazyMulAdd{M,O,I}) where {M,O,I} = vadd_nsw(vmul_nsw(StaticInt{M}(), getfield(a, :data)), StaticInt{O}())
@inline _materialize(x) = x
@inline Base.convert(::Type{T}, a::LazyMulAdd{M,O,I}) where {M,O,I,T<:Number} = convert(T, _materialize(a))
@inline Base.convert(::Type{LazyMulAdd{M,O,I}}, a::LazyMulAdd{M,O,I}) where {M,O,I} = a
# @inline Base.convert(::Type{LazyMulAdd{M,O,I}}, a::LazyMulAdd{M}) where {M,O,I} = a
# @inline Base.convert(::Type{LazyMulAdd{M,T,I}}, a::LazyMulAdd{M,StaticInt{O},I}) where {M,O,I,T} = a

Base.promote_rule(::Type{LazyMulAdd{M,O,I}}, ::Type{T}) where {M,O,I<:Number,T} = promote_type(I, T)
Base.promote_rule(::Type{LazyMulAdd{M,O,MM{W,X,I}}}, ::Type{T}) where {M,O,W,X,I,T} = promote_type(MM{W,X,I}, T)
Base.promote_rule(::Type{LazyMulAdd{M,O,Vec{W,I}}}, ::Type{T}) where {M,O,W,I,T} = promote_type(Vec{W,I}, T)

@inline lazymul(a, b) = vmul_nsw(a, b)
@inline lazymul(::StaticInt{M}, b) where {M} = LazyMulAdd{M}(b)
@inline lazymul(::StaticInt{1}, b) = b
@inline lazymul(::StaticInt{0}, b) = StaticInt{0}()
@inline lazymul(a, ::StaticInt{M}) where {M} = LazyMulAdd{M}(a)
@inline lazymul(a, ::StaticInt{1}) = a
@inline lazymul(a, ::StaticInt{0}) = StaticInt{0}()
@inline lazymul(a::LazyMulAdd, ::StaticInt{1}) = a
@inline lazymul(::StaticInt{1}, a::LazyMulAdd) = a
@inline lazymul(a::LazyMulAdd, ::StaticInt{0}) = StaticInt{0}()
@inline lazymul(::StaticInt{0}, a::LazyMulAdd) = StaticInt{0}()
# @inline lazymul(a::LazyMulAdd, ::StaticInt{0}) = StaticInt{0}()
# @inline lazymul(::StaticInt{M}, b::MM{W,X}) where {W,M,X} = LazyMulAdd{M}(MM{W}(getfield(b, :data), StaticInt{M}()*StaticInt{X}()))
# @inline lazymul(a::MM{W,X}, ::StaticInt{M}) where {W,M,X} = LazyMulAdd{M}(MM{W}(getfield(a, :data), StaticInt{M}()*StaticInt{X}()))
@inline lazymul(a::MM{W,X}, ::StaticInt{M}) where {W,M,X} = MM{W}(vmul_nsw(StaticInt{M}(), data(a)), StaticInt{X}() * StaticInt{M}())
@inline lazymul(::StaticInt{M}, b::MM{W,X}) where {W,M,X} = MM{W}(vmul_nsw(StaticInt{M}(), data(b)), StaticInt{X}() * StaticInt{M}())
@inline lazymul(a::MM{W,X}, ::StaticInt{1}) where {W,X} = a
@inline lazymul(::StaticInt{1}, a::MM{W,X}) where {W,X} = a
@inline lazymul(a::MM{W,X}, ::StaticInt{0}) where {W,X} = StaticInt{0}()
@inline lazymul(::StaticInt{0}, a::MM{W,X}) where {W,X} = StaticInt{0}()

@inline lazymul(::StaticInt{M}, ::StaticInt{N}) where {M, N} = StaticInt{M}()*StaticInt{N}()

@inline lazymul(::StaticInt{M}, ::StaticInt{1}) where {M} = StaticInt{M}()
@inline lazymul(::StaticInt,    ::StaticInt{0}) = StaticInt{0}()

@inline lazymul(::StaticInt{1}, ::StaticInt{M}) where {M} = StaticInt{M}()
@inline lazymul(::StaticInt{0}, ::StaticInt   ) = StaticInt{0}()

@inline lazymul(::StaticInt{0}, ::StaticInt{0}) = StaticInt{0}()
@inline lazymul(::StaticInt{0}, ::StaticInt{1}) = StaticInt{0}()
@inline lazymul(::StaticInt{1}, ::StaticInt{0}) = StaticInt{0}()
@inline lazymul(::StaticInt{1}, ::StaticInt{1}) = StaticInt{1}()

@inline function lazymul(a::LazyMulAdd{M,O}, ::StaticInt{N}) where {M,O,N}
    LazyMulAdd(getfield(a, :data), StaticInt{M}() * StaticInt{N}(), StaticInt{O}() * StaticInt{N}())
end
@inline function lazymul(::StaticInt{M}, b::LazyMulAdd{N,O}) where {M,O,N}
    LazyMulAdd(getfield(b, :data), StaticInt{M}() * StaticInt{N}(), StaticInt{O}() * StaticInt{M}())
end

@inline function lazymul(a::LazyMulAdd{M,<:MM{W,X}}, ::StaticInt{N}) where {M,N,W,X}
    LazyMulAdd(MM{W}(getfield(a, :data), StaticInt{M}()*StaticInt{N}()*StaticInt{X}()), StaticInt{M}()*StaticInt{N}())
end
@inline function lazymul(::StaticInt{M}, b::LazyMulAdd{N,<:MM{W,X}}) where {M,N,W,X}
    LazyMulAdd(MM{W}(getfield(b, :data), StaticInt{M}()*StaticInt{N}()*StaticInt{X}()), StaticInt{M}()*StaticInt{N}())
end

@inline lazymul(a::LazyMulAdd{M,<:MM{W,X}}, ::StaticInt{0}) where {M,W,X} = StaticInt{0}()
@inline lazymul(::StaticInt{0}, b::LazyMulAdd{N,<:MM{W,X}}) where {N,W,X} = StaticInt{0}()
@inline lazymul(a::LazyMulAdd{M,<:MM{W,X}}, ::StaticInt{1}) where {M,W,X} = a
@inline lazymul(::StaticInt{1}, b::LazyMulAdd{N,<:MM{W,X}}) where {N,W,X} = b
@inline function lazymul(a::LazyMulAdd{M}, b::LazyMulAdd{N}) where {M,N}
    LazyMulAdd(vmul_nsw(getfield(a, :data), getfield(b, :data)), StaticInt{M}()*StaticInt{N}())
end

vmul_nsw(::LazyMulAdd{M,O}, ::StaticInt{0}) where {M,O} = Zero()
vmul_nsw(::StaticInt{0}, ::LazyMulAdd{M,O}) where {M,O} = Zero()
vmul_nsw(a::LazyMulAdd{M,O}, ::StaticInt{1}) where {M,O} = a
vmul_nsw(::StaticInt{1}, a::LazyMulAdd{M,O}) where {M,O} = a
@inline function vmul_nsw(a::LazyMulAdd{M,O}, ::StaticInt{N}) where {M,N,O}
    LazyMulAdd(getfield(a, :data), StaticInt{M}() * StaticInt{N}(), StaticInt{O}() * StaticInt{N}())
end
@inline function vmul_nsw(::StaticInt{N}, a::LazyMulAdd{M,O}) where {M,N,O}
    LazyMulAdd(getfield(a, :data), StaticInt{M}() * StaticInt{N}(), StaticInt{O}() * StaticInt{N}())
end
@inline vmul_nsw(a::LazyMulAdd{M,0}, i::IntegerTypesHW) where {M} = LazyMulAdd{M,0}(vmul_nsw(getfield(a,:data), i))
@inline vmul_nsw(a::LazyMulAdd{M,O}, i::IntegerTypesHW) where {M,O} = vmul_nsw(_materialize(a), i)

@inline function Base.:(>>>)(a::LazyMulAdd{M,O}, ::StaticInt{N}) where {M,O,N}
    LazyMulAdd(getfield(a, :data), StaticInt{M}() >>> StaticInt{N}(), StaticInt{O}() >>> StaticInt{N}())
end

# The approach with `add_indices` is that we try and make `vadd_nsw` behave well
# but for `i` and `j` type combinations where it's difficult,
# we can add specific `add_indices` methods that increment the pointer.
@inline function add_indices(p::Ptr, i, j) # generic fallback
    p, vadd_nsw(i, j)
end
@inline vadd_nsw(i::LazyMulAdd, ::Zero) = i
@inline vadd_nsw(::Zero, i::LazyMulAdd) = i

# These following two definitions normally shouldn't be hit
@inline function vadd_nsw(a::LazyMulAdd{M,O,MM{W,X,I}}, b::IntegerTypesHW) where {M,O,W,X,I}
    MM{W}(vadd_nsw(vmul_nsw(StaticInt{M}(), data(a)), b), StaticInt{X}() * StaticInt{M}())
end
@inline function vadd_nsw(b::IntegerTypesHW, a::LazyMulAdd{M,O,MM{W,X,I}}) where {M,O,W,X,I}
    MM{W}(vadd_nsw(vmul_nsw(StaticInt{M}(), data(a)), b), StaticInt{X}() * StaticInt{M}())
end

@inline vadd_nsw(a::LazyMulAdd{M,O}, b) where {M,O} = vadd_nsw(_materialize(a), b)
@inline vadd_nsw(b, a::LazyMulAdd{M,O}) where {M,O} = vadd_nsw(b, _materialize(a))
@inline vsub_nsw(a::LazyMulAdd{M,O}, b) where {M,O} = vsub_nsw(_materialize(a), b)
@inline vsub_nsw(b, a::LazyMulAdd{M,O}) where {M,O} = vsub_nsw(b, _materialize(a))

@inline vadd_nsw(a::LazyMulAdd{M,O,MM{W,X,I}}, ::Zero) where {M,O,W,X,I} = a
@inline vadd_nsw(::Zero, a::LazyMulAdd{M,O,MM{W,X,I}}) where {M,O,W,X,I} = a
@inline function vsub_nsw(a::LazyMulAdd{M,O,MM{W,X,I}}, b::IntegerTypesHW) where {M,O,W,X,I}
    MM{W}(vsub_nsw(vmul_nsw(StaticInt{M}(), data(a)), b), StaticInt{X}() * StaticInt{M}())
end
@inline function vsub_nsw(b::IntegerTypesHW, a::LazyMulAdd{M,O,MM{W,X,I}}) where {M,O,W,X,I}
    MM{W}(vsub_nsw(b, vmul_nsw(StaticInt{M}(), data(a))), (StaticInt{-1}() * StaticInt{X}()) * StaticInt{M}())
end

# because we should hit this instead:
@inline add_indices(p::Ptr, b::Integer, a::LazyMulAdd{M,O}) where {M,O} = (p + b, a)
@inline add_indices(p::Ptr, a::LazyMulAdd{M,O}, b::Integer) where {M,O} = (p + b, a)
# but in the case of `VecUnroll`s, which skip the `add_indices`, it's useful to still have the former two definitions.
# However, this also forces us to write:
@inline add_indices(p::Ptr, ::StaticInt{N}, a::LazyMulAdd{M,O}) where {M,O,N} = (p, vadd_nsw(a, StaticInt{N}()))
@inline add_indices(p::Ptr, a::LazyMulAdd{M,O}, ::StaticInt{N}) where {M,O,N} = (p, vadd_nsw(a, StaticInt{N}()))

@inline function vadd_nsw(::StaticInt{N}, a::LazyMulAdd{M,O}) where {N,M,O}
    LazyMulAdd(getfield(a, :data), StaticInt{M}(), StaticInt{O}()+StaticInt{N}())
end
@inline function vadd_nsw(a::LazyMulAdd{M,O}, ::StaticInt{N}) where {N,M,O}
    LazyMulAdd(getfield(a, :data), StaticInt{M}(), StaticInt{O}()+StaticInt{N}())
end
@inline function vadd_nsw(::StaticInt{N}, a::LazyMulAdd{M,O,MM{W,X,I}}) where {N,M,O,W,X,I}
    LazyMulAdd(getfield(a, :data), StaticInt{M}(), StaticInt{O}()+StaticInt{N}())
end
@inline function vadd_nsw(a::LazyMulAdd{M,O,MM{W,X,I}}, ::StaticInt{N}) where {N,M,O,W,X,I}
    LazyMulAdd(getfield(a, :data), StaticInt{M}(), StaticInt{O}()+StaticInt{N}())
end
@inline function vadd_nsw(a::LazyMulAdd{M,O}, b::LazyMulAdd{M,A}) where {M,O,A}
    LazyMulAdd(vadd_nsw(getfield(a, :data), getfield(b, :data)), StaticInt{M}(), StaticInt{O}()+StaticInt{A}())
end

@inline function vsub_nsw(::StaticInt{N}, a::LazyMulAdd{M,O}) where {N,M,O}
    LazyMulAdd(getfield(a, :data), StaticInt{-1}()*StaticInt{M}(), StaticInt{N}()-StaticInt{O}())
end
@inline vsub_nsw(::Zero, a::LazyMulAdd{M,O}) where {M,O} = StaticInt{-1}()*a
@inline function vsub_nsw(a::LazyMulAdd{M,O}, ::StaticInt{N}) where {N,M,O}
    LazyMulAdd(getfield(a, :data), StaticInt{M}(), StaticInt{O}()-StaticInt{N}())
end
@inline vsub_nsw(a::LazyMulAdd{M,O}, ::Zero) where {M,O} = a
@inline function vsub_nsw(::StaticInt{N}, a::LazyMulAdd{M,O,MM{W,X,I}}) where {N,M,O,W,X,I}
    LazyMulAdd(getfield(a, :data), StaticInt{-1}()*StaticInt{M}(), StaticInt{N}()-StaticInt{O}())
end
@inline vsub_nsw(::Zero, a::LazyMulAdd{M,O,MM{W,X,I}}) where {M,O,W,X,I} = StaticInt{-1}()*a
@inline function vsub_nsw(a::LazyMulAdd{M,O,MM{W,X,I}}, ::StaticInt{N}) where {N,M,O,W,X,I}
    LazyMulAdd(getfield(a, :data), StaticInt{M}(), StaticInt{O}()-StaticInt{N}())
end
@inline vsub_nsw(a::LazyMulAdd{M,O,MM{W,X,I}}, ::Zero) where {M,O,W,X,I} = a
@inline function vsub_nsw(a::LazyMulAdd{M,O}, b::LazyMulAdd{M,A}) where {M,O,A}
    LazyMulAdd(vsub_nsw(getfield(a, :data), getfield(b, :data)), StaticInt{M}(), StaticInt{O}()-StaticInt{A}())
end

@inline add_indices(p::Ptr, a::LazyMulAdd{M,O,V}, b::LazyMulAdd{N,P,J}) where {M,O,V<:AbstractSIMDVector,N,P,J<:IntegerTypes} = (gep(p, b), a)
@inline add_indices(p::Ptr, b::LazyMulAdd{N,P,J}, a::LazyMulAdd{M,O,V}) where {M,O,V<:AbstractSIMDVector,N,P,J<:IntegerTypes} = (gep(p, b), a)
@inline add_indices(p::Ptr, a::LazyMulAdd{M,O,V}, b::LazyMulAdd{M,P,J}) where {M,O,V<:AbstractSIMDVector,P,J<:IntegerTypes} = (p, vadd_nsw(a, b))
@inline add_indices(p::Ptr, b::LazyMulAdd{M,P,J}, a::LazyMulAdd{M,O,V}) where {M,O,V<:AbstractSIMDVector,P,J<:IntegerTypes} = (p, vadd_nsw(a, b))

@inline add_indices(p::Ptr, a::AbstractSIMDVector, b::LazyMulAdd{M,O,I}) where {M,O,I<:IntegerTypes} = (gep(p, b), a)
@inline add_indices(p::Ptr, b::LazyMulAdd{M,O,I}, a::AbstractSIMDVector) where {M,O,I<:IntegerTypes} = (gep(p, b), a)
@inline function add_indices(p::Ptr, ::MM{W,X,StaticInt{A}}, a::LazyMulAdd{M,O,T}) where {M,O,T<:IntegerTypes,A,W,X}
    gep(p, a), MM{W,X}(StaticInt{A}())
end
@inline function add_indices(p::Ptr, a::LazyMulAdd{M,O,T}, ::MM{W,X,StaticInt{A}}) where {M,O,T<:IntegerTypes,A,W,X}
    gep(p, a), MM{W,X}(StaticInt{A}())
end

@generated function add_indices(p::Ptr, a::LazyMulAdd{M,O,MM{W,X,StaticInt{I}}}, b::LazyMulAdd{N,P,J}) where {M,O,W,X,I,N,P,J<:IntegerTypes}
    d, r = divrem(M, N)
    if iszero(r)
        quote
            $(Expr(:meta,:inline))
            p, VectorizationBase.LazyMulAdd{$N,$(I*M)}(MM{$W,$d}(getfield(b, :data)))
        end
    else
        quote
            $(Expr(:meta,:inline))
            gep(p, b), a
        end
    end
end
@inline add_indices(p::Ptr, b::LazyMulAdd{N,P,J}, a::LazyMulAdd{M,O,MM{W,X,StaticInt{I}}}) where {M,O,W,X,I,N,P,J<:IntegerTypes} = add_indices(p, a, b)
@generated function vadd_nsw(a::LazyMulAdd{M,O,MM{W,X,StaticInt{I}}}, b::LazyMulAdd{N,P,J}) where {M,O,W,X,I,N,P,J<:IntegerTypes}
    d, r = divrem(M, N)
    if iszero(r)
        quote
            $(Expr(:meta,:inline))
            VectorizationBase.LazyMulAdd{$N,$(I*M)}(MM{$W,$d}(getfield(b, :data)))
        end
    else
        quote
            $(Expr(:meta,:inline))
            vadd_nsw(a, _materialize(b))
        end
    end
end
@inline vadd_nsw(b::LazyMulAdd{N,P,J}, a::LazyMulAdd{M,O,MM{W,X,StaticInt{I}}}) where {M,O,W,X,I,N,P,J<:IntegerTypes} = vadd_nsw(a, b)
@inline vadd_nsw(a::VecUnroll, b::LazyMulAdd) = VecUnroll(fmap(vadd_nsw, getfield(a, :data), b))
@inline vadd_nsw(b::LazyMulAdd, a::VecUnroll) = VecUnroll(fmap(vadd_nsw, b, getfield(a, :data)))
@inline vadd_nsw(a::LazyMulAdd, b::LazyMulAdd) = vadd_nsw(_materialize(a), _materialize(b))
@inline vsub_nsw(a::LazyMulAdd, b::LazyMulAdd) = vsub_nsw(_materialize(a), _materialize(b))

@generated function vsub_nsw(a::LazyMulAdd{M,O,MM{W,X,StaticInt{I}}}, b::LazyMulAdd{N,P,J}) where {M,O,W,X,I,N,P,J<:IntegerTypes}
    d, r = divrem(M, N)
    if iszero(r)
        quote
            $(Expr(:meta,:inline))
            VectorizationBase.LazyMulAdd{$N,$(I*M)}(-MM{$W,$d}(getfield(b, :data)))
        end
    else
        quote
            $(Expr(:meta,:inline))
            vsub_nsw(a, _materialize(b))
        end
    end
end
@inline vsub_nsw(b::LazyMulAdd{N,P,J}, a::LazyMulAdd{M,O,MM{W,X,StaticInt{I}}}) where {M,O,W,X,I,N,P,J<:IntegerTypes} = vsub_nsw(a, b)

@inline vsub_nsw(a::VecUnroll, b::LazyMulAdd) = VecUnroll(fmap(vsub_nsw, getfield(a, :data), b))
@inline vsub_nsw(b::LazyMulAdd, a::VecUnroll) = VecUnroll(fmap(vsub_nsw, b, getfield(a, :data)))


module OMEinsumContractionOrders

using KaHyPar
using SparseArrays, Suppressor
using OMEinsum
using OMEinsum: NestedEinsum

include("kahypar.jl")

end


#=
This file is auto-generated. Do not edit.
=#
"""
    mutable struct MonitoredLine <: ACBranch
        name::String
        available::Bool
        active_power_flow::Float64
        reactive_power_flow::Float64
        arc::Arc
        r::Float64
        x::Float64
        b::NamedTuple{(:from, :to), Tuple{Float64, Float64}}
        flow_limits::NamedTuple{(:from_to, :to_from), Tuple{Float64, Float64}}
        rate::Float64
        angle_limits::NamedTuple{(:min, :max), Tuple{Float64, Float64}}
        services::Vector{Service}
        ext::Dict{String, Any}
        time_series_container::InfrastructureSystems.TimeSeriesContainer
        internal::InfrastructureSystemsInternal
    end

# Arguments
- `name::String`
- `available::Bool`
- `active_power_flow::Float64`
- `reactive_power_flow::Float64`
- `arc::Arc`
- `r::Float64`: System per-unit value, validation range: `(0, 4)`, action if invalid: `warn`
- `x::Float64`: System per-unit value, validation range: `(0, 4)`, action if invalid: `warn`
- `b::NamedTuple{(:from, :to), Tuple{Float64, Float64}}`: System per-unit value, validation range: `(0, 2)`, action if invalid: `warn`
- `flow_limits::NamedTuple{(:from_to, :to_from), Tuple{Float64, Float64}}`: TODO: throw warning above max SIL
- `rate::Float64`: TODO: compare to SIL (warn) (theoretical limit)
- `angle_limits::NamedTuple{(:min, :max), Tuple{Float64, Float64}}`, validation range: `(-1.571, 1.571)`, action if invalid: `error`
- `services::Vector{Service}`: Services that this device contributes to
- `ext::Dict{String, Any}`
- `time_series_container::InfrastructureSystems.TimeSeriesContainer`: internal time_series storage
- `internal::InfrastructureSystemsInternal`: power system internal reference, do not modify
"""
mutable struct MonitoredLine <: ACBranch
    name::String
    available::Bool
    active_power_flow::Float64
    reactive_power_flow::Float64
    arc::Arc
    "System per-unit value"
    r::Float64
    "System per-unit value"
    x::Float64
    "System per-unit value"
    b::NamedTuple{(:from, :to), Tuple{Float64, Float64}}
    "TODO: throw warning above max SIL"
    flow_limits::NamedTuple{(:from_to, :to_from), Tuple{Float64, Float64}}
    "TODO: compare to SIL (warn) (theoretical limit)"
    rate::Float64
    angle_limits::NamedTuple{(:min, :max), Tuple{Float64, Float64}}
    "Services that this device contributes to"
    services::Vector{Service}
    ext::Dict{String, Any}
    "internal time_series storage"
    time_series_container::InfrastructureSystems.TimeSeriesContainer
    "power system internal reference, do not modify"
    internal::InfrastructureSystemsInternal
end

function MonitoredLine(name, available, active_power_flow, reactive_power_flow, arc, r, x, b, flow_limits, rate, angle_limits, services=Device[], ext=Dict{String, Any}(), time_series_container=InfrastructureSystems.TimeSeriesContainer(), )
    MonitoredLine(name, available, active_power_flow, reactive_power_flow, arc, r, x, b, flow_limits, rate, angle_limits, services, ext, time_series_container, InfrastructureSystemsInternal(), )
end

function MonitoredLine(; name, available, active_power_flow, reactive_power_flow, arc, r, x, b, flow_limits, rate, angle_limits, services=Device[], ext=Dict{String, Any}(), time_series_container=InfrastructureSystems.TimeSeriesContainer(), internal=InfrastructureSystemsInternal(), )
    MonitoredLine(name, available, active_power_flow, reactive_power_flow, arc, r, x, b, flow_limits, rate, angle_limits, services, ext, time_series_container, internal, )
end

# Constructor for demo purposes; non-functional.
function MonitoredLine(::Nothing)
    MonitoredLine(;
        name="init",
        available=false,
        active_power_flow=0.0,
        reactive_power_flow=0.0,
        arc=Arc(Bus(nothing), Bus(nothing)),
        r=0.0,
        x=0.0,
        b=(from=0.0, to=0.0),
        flow_limits=(from_to=0.0, to_from=0.0),
        rate=0.0,
        angle_limits=(min=-1.571, max=1.571),
        services=Device[],
        ext=Dict{String, Any}(),
        time_series_container=InfrastructureSystems.TimeSeriesContainer(),
    )
end

"""Get [`MonitoredLine`](@ref) `name`."""
get_name(value::MonitoredLine) = value.name
"""Get [`MonitoredLine`](@ref) `available`."""
get_available(value::MonitoredLine) = value.available
"""Get [`MonitoredLine`](@ref) `active_power_flow`."""
get_active_power_flow(value::MonitoredLine) = get_value(value, value.active_power_flow)
"""Get [`MonitoredLine`](@ref) `reactive_power_flow`."""
get_reactive_power_flow(value::MonitoredLine) = get_value(value, value.reactive_power_flow)
"""Get [`MonitoredLine`](@ref) `arc`."""
get_arc(value::MonitoredLine) = value.arc
"""Get [`MonitoredLine`](@ref) `r`."""
get_r(value::MonitoredLine) = value.r
"""Get [`MonitoredLine`](@ref) `x`."""
get_x(value::MonitoredLine) = value.x
"""Get [`MonitoredLine`](@ref) `b`."""
get_b(value::MonitoredLine) = value.b
"""Get [`MonitoredLine`](@ref) `flow_limits`."""
get_flow_limits(value::MonitoredLine) = get_value(value, value.flow_limits)
"""Get [`MonitoredLine`](@ref) `rate`."""
get_rate(value::MonitoredLine) = get_value(value, value.rate)
"""Get [`MonitoredLine`](@ref) `angle_limits`."""
get_angle_limits(value::MonitoredLine) = value.angle_limits
"""Get [`MonitoredLine`](@ref) `services`."""
get_services(value::MonitoredLine) = value.services
"""Get [`MonitoredLine`](@ref) `ext`."""
get_ext(value::MonitoredLine) = value.ext
"""Get [`MonitoredLine`](@ref) `time_series_container`."""
get_time_series_container(value::MonitoredLine) = value.time_series_container
"""Get [`MonitoredLine`](@ref) `internal`."""
get_internal(value::MonitoredLine) = value.internal

"""Set [`MonitoredLine`](@ref) `name`."""
set_name!(value::MonitoredLine, val) = value.name = val
"""Set [`MonitoredLine`](@ref) `available`."""
set_available!(value::MonitoredLine, val) = value.available = val
"""Set [`MonitoredLine`](@ref) `active_power_flow`."""
set_active_power_flow!(value::MonitoredLine, val) = value.active_power_flow = set_value(value, val)
"""Set [`MonitoredLine`](@ref) `reactive_power_flow`."""
set_reactive_power_flow!(value::MonitoredLine, val) = value.reactive_power_flow = set_value(value, val)
"""Set [`MonitoredLine`](@ref) `arc`."""
set_arc!(value::MonitoredLine, val) = value.arc = val
"""Set [`MonitoredLine`](@ref) `r`."""
set_r!(value::MonitoredLine, val) = value.r = val
"""Set [`MonitoredLine`](@ref) `x`."""
set_x!(value::MonitoredLine, val) = value.x = val
"""Set [`MonitoredLine`](@ref) `b`."""
set_b!(value::MonitoredLine, val) = value.b = val
"""Set [`MonitoredLine`](@ref) `flow_limits`."""
set_flow_limits!(value::MonitoredLine, val) = value.flow_limits = set_value(value, val)
"""Set [`MonitoredLine`](@ref) `rate`."""
set_rate!(value::MonitoredLine, val) = value.rate = set_value(value, val)
"""Set [`MonitoredLine`](@ref) `angle_limits`."""
set_angle_limits!(value::MonitoredLine, val) = value.angle_limits = val
"""Set [`MonitoredLine`](@ref) `services`."""
set_services!(value::MonitoredLine, val) = value.services = val
"""Set [`MonitoredLine`](@ref) `ext`."""
set_ext!(value::MonitoredLine, val) = value.ext = val
"""Set [`MonitoredLine`](@ref) `time_series_container`."""
set_time_series_container!(value::MonitoredLine, val) = value.time_series_container = val


###
### Janzen-Connell Model
### (c) Daniel Vedder, MIT license
###

"""
A cons cell to implement a double linked list for the forest
(Boy, do I miss Lisp :-/ )
"""
mutable struct Cons
    car::Union{Tree,Nothing}   # The Tree object held by this cell
    cdr::Union{Cons,Nothing}   # A pointer to the next cell down the line
    prev::Union{Cons,Nothing}  # A pointer to the previous cell
end

# The first cons cells in the forest list, and the number of trees
const forest = Cons(nothing,nothing,nothing)
forestlen = 0 #XXX do I actually need this?

"""
Insert a new tree object at the correct position in the forest list.
(Sorted by ascending x values.)
"""
function planttree!(tree::Tree,cons::Cons=forest)
    #FIXME sometimes doesn't insert trees into the forest list
    # (at least when called from initworld()
    while cons != nothing
        if !isa(cons.car, Tree)
            # If we have an empty cons cell, plant the tree here
            cons.car = tree
            global forestlen += 1
            @debug "Planted tree $(tree.uid) @$(tree.position.x)/$(tree.position.y)"
            return
        elseif tree.position.x < cons.car.position.x
            # If the next tree in the list is further east, insert a new cons cell
            if cons.prev != nothing #inserting in the middle
                newcons = Cons(tree,cons,cons.prev)
                cons.prev.cdr = newcons
                cons.prev = newcons
            else #if we're inserting before the first cell
                newcons = Cons(cons.car, cons.cdr, cons)
                cons.car = tree
                cons.cdr = newcons
            end
            global forestlen += 1
            @debug "Planted tree $(tree.uid) @$(tree.position.x)/$(tree.position.y)"
            return
        elseif cons.cdr == nothing
            # If we're at the end of the list, append a new cons cell
            cons.cdr = Cons(nothing,nothing,cons)
        end
        cons = cons.cdr
    end
end

"""
Remove a tree from the forest list.
(Don't let Idefix see this function :D )
"""
function killtree!(tree::Tree,cons::Cons=forest,reason::String="malice aforethought")
    while cons != nothing
        if cons.car == tree
            if cons.prev != nothing #excise a cons cell from the list
                cons.prev.cdr = cons.cdr
                cons.cdr != nothing && (cons.cdr.prev = cons.prev)
                cons = nothing
            else
                #removing the first tree is a bit more tricky, because the
                #first cons cell (global variable `forest`) mustn't be deleted
                if cons.cdr != nothing
                    cons.car = cons.cdr.car
                    cons.cdr.cdr != nothing && (cons.cdr.cdr.prev = cons)
                    cons.cdr = cons.cdr.cdr
                else
                    cons.car = nothing
                end
            end
            #cleanup and decrease the tree count
            global forestlen -= 1
            @debug "Killed tree $(tree.uid) @$(tree.position.x)/$(tree.position.y) because of $(reason)"
            tree = nothing
            return
        end
        cons = cons.cdr
    end
    @warn "Attempted to remove nonexistent tree."
end

"""
Produce seeds and disperse them in the landscape, planting them where possible
"""
function disperse!(cons::Cons=forest)
    i::Int16 = 1
    while cons != nothing
        tree = cons.car
        if !tree.mature
            i += 1
            cons = cons.cdr
            continue
        end
        @debug "Reproducing tree $(tree.uid)"
        dx = tree.species.dispersal_distance
        # Each tree produces multiple seeds
        for s in 1:tree.species.seed_production
            #TODO implement a proper dispersal kernel
            # Find a random location in a circle around the tree
            sx = tree.position.x + rand(-dx:dx)
            #FIXME sqrt() was called with an argument of -32768.0?
            # (only happens in scenarios with variance?)
            dy = convert(Int16, round(sqrt(abs(dx^2-(sx-tree.position.x)^2))))
            sy = tree.position.y + rand(-dy:dy)
            if sx >= -settings["worldsize"] && sx <= settings["worldsize"] &&
                sy >= -settings["worldsize"] && sy <= settings["worldsize"]
                seed = Tree(tree.species, sx, sy)
                planttree!(seed)
            end
        end
        i += 1
        cons = cons.cdr
    end
end

"""
Test whether the trees in two cons cells intersect, and if so, kill the
smaller tree. The integer return value indicates the outcome:
0 - no intersection (no tree killed)
1 - the first tree is larger (second tree killed)
2 - the second tree is larger (first tree killed)
Needed by `compete_individual!()`
"""
function compete_pair!(cons1::Cons, cons2::Cons)::UInt8
    tree1 = cons1.car
    tree2 = cons2.car
    (tree1 == nothing || tree2 == nothing) && return
    mindist = (tree1.size + tree2.size)/2
    dx = abs(tree1.position.x - tree2.position.x)
    dy = abs(tree1.position.y - tree2.position.y)
    if  dx >= mindist || dy >= mindist || hypot(dx,dy) >= mindist
        return 0
    elseif tree1.size > tree2.size
        killtree!(tree2, cons2, "competition")
        return 1
    else
        killtree!(tree1, cons1, "competition")
        return 2
    end
end

"""
Check whether the tree in this cons cell conflicts with trees in the
vicinity and, if so, kill the smaller one.
Needed by `compete!()`
"""
function compete_individual!(cons::Cons)
    tree = cons.car
    # go right until we're sure we won't find any more conflicts
    next = cons.cdr
    while next != nothing && abs(next.car.position.x - tree.position.x) < (tree.size/2)+2^7
        next2 = next.cdr # We have to save the coming step already, in case `next` is killed
        conflict = compete_pair!(cons, next)
        if conflict == 2
            return # This tree was killed, so we can break off
        else # otherwise, keep going
            next = next2
        end
    end
    # then go left and repeat
    next = cons.prev
    while next != nothing && abs(next.car.position.x - tree.position.x) < (tree.size/2)+2^7
        next2 = next.prev
        conflict = compete_pair!(cons, next)
        if conflict == 2
            return
        else
            next = next2
        end
    end
end

"""
Go through the landscape and check each tree for space conflicts
"""
function compete!(cons::Cons=forest)
    while cons != nothing
        compete_individual!(cons)
        cons = cons.cdr
    end
end

"""
All saplings grow until they reach maturity, then eventually die of old age.
"""
function grow!(cons::Cons=forest)
    while cons != nothing
        tree = cons.car
        next = cons.cdr
        if !tree.mature
            tree.size += tree.species.growth_rate
            tree.size >= tree.species.max_size && (tree.mature = true)
        elseif tree.age >= tree.species.max_age
            killtree!(tree, cons, "old age")
            cons = next
            continue
        end
        tree.age += 1
        cons = next
        recordindividual(tree)
    end
end

"""
An infected tree in the given cons cell spreads its infection
"""
function spread_infection(cons::Cons)
    tree = cons.car
    pathogen = tree.infection
    maxdist = pathogen.infection_radius
    # go right until we're sure we're out of the infection range
    next = cons.cdr
    while next != nothing && abs(next.car.position.x - tree.position.x) < maxdist
        tree2 = next.car
        dist = hypot(abs(tree.position.x-tree2.position.x),
                     abs(tree.position.y-tree2.position.y))
        prob = (pathogen.infection_rate-tree2.species.pathogen_resistance)*((maxdist-dist)/maxdist)
        #A tree is infected if...
        if dist <= maxdist && #... it's within range
            tree2.infection == nothing && #... it isn't infected anyway
            tree2.species.id == tree.species.id && #... it belongs to the same species
            prob > rand(Float16) #... it passes a random draw dependent on its resistance and distance
            tree2.infection = Pathogen(pathogen.host)
            @debug "Infected tree $(tree.uid) @$(tree2.position.x)/$(tree2.position.y)"
        end
        next = next.cdr
    end
    # then go left and repeat
    next = cons.prev
    while next != nothing && abs(next.car.position.x - tree.position.x) < maxdist
        tree2 = next.car
        dist = hypot(abs(tree.position.x-tree2.position.x),
                     abs(tree.position.y-tree2.position.y))
        prob = (pathogen.infection_rate-tree2.species.pathogen_resistance)*((maxdist-dist)/maxdist)
        if dist <= maxdist &&
            tree2.infection == nothing &&
            tree2.species.id == tree.species.id &&
            prob > rand(Float16)
            tree2.infection = Pathogen(pathogen.host)
            @debug "Infected tree $(tree.uid) @$(tree2.position.x)/$(tree2.position.y)"
        end
        next = next.prev
    end
end

"""
Carry out the pathogen spread phase and check for mortality
"""
function infect!(cons::Cons=forest)
    while cons != nothing
        tree = cons.car
        next = cons.cdr
        if tree.infection != nothing
            pathogen = tree.infection
            # pathogens have a one-update incubation period
            pathogen.infectious ? spread_infection(cons) : pathogen.infectious = true
            if pathogen.lethality > rand(Float16)
                killtree!(tree, cons, "disease")
            end
        end
        cons = next
    end
end