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

# Note: Most (if not all) of these types are temporary, to be replaced later
# on by generic types from RadiationDetectorSignals.jl (when the latter is
# mature enough).

# Internal units should be SI units
const internal_length_unit  = ConstructiveSolidGeometry.internal_length_unit
const internal_angle_unit   = ConstructiveSolidGeometry.internal_angle_unit
const internal_time_unit    = u"s"
const internal_voltage_unit = u"V"
const internal_energy_unit  = u"eV"
const internal_efield_unit  = internal_voltage_unit / internal_length_unit
const internal_charge_unit  = u"C"
const external_charge_unit  = u"e_au" # elementary charge - from UnitfulAtomic.jl

to_internal_units(x::Quantity{<:Real, dimension(internal_time_unit)})    = ustrip(uconvert(internal_time_unit,    x))
to_internal_units(x::Quantity{<:Real, dimension(internal_voltage_unit)}) = ustrip(uconvert(internal_voltage_unit, x))
to_internal_units(x::Quantity{<:Real, dimension(internal_efield_unit)})   = ustrip(uconvert(internal_efield_unit,  x))
to_internal_units(x::Quantity{<:Real, dimension(internal_energy_unit)})  = ustrip(uconvert(internal_energy_unit,  x))
to_internal_units(x::Quantity{<:Real, dimension(internal_charge_unit)})  = ustrip(uconvert(internal_charge_unit,  x))

from_internal_units(x::Real, unit::Unitful.Units{<:Any, dimension(internal_time_unit)})    = uconvert(unit, x * internal_time_unit)
from_internal_units(x::Real, unit::Unitful.Units{<:Any, dimension(internal_voltage_unit)}) = uconvert(unit, x * internal_voltage_unit)
from_internal_units(x::Real, unit::Unitful.Units{<:Any, dimension(internal_efield_unit)})  = uconvert(unit, x * internal_efield_unit)
from_internal_units(x::Real, unit::Unitful.Units{<:Any, dimension(internal_energy_unit)})  = uconvert(unit, x * internal_energy_unit)
from_internal_units(x::Real, unit::Unitful.Units{<:Any, dimension(internal_charge_unit)})  = uconvert(unit, x * internal_charge_unit)

# Internal function for now: We should also fano noise here (optionally)
_convert_internal_energy_to_external_charge(material) = inv(to_internal_units(material.E_ionisation)) * external_charge_unit

unit_conversion = Dict{String, Unitful.Units}(
    "nm" => u"nm", "um" => u"μm", "mm" => u"mm", "cm" => u"cm", "m" => u"m", #length
    "deg" => u"°","rad" => u"rad", #angle
    "V" => u"V", "kV" => u"kV", #potential
    "K" => u"K", "Kelvin" => u"K", "C" => u"°C", "Celsius" => u"°C", #temperature
)

const UnitTuple = NamedTuple{(:length, :angle, :potential, :temperature), Tuple{
                    Unitful.FreeUnits{<:Any, Unitful.𝐋},
                    Unitful.FreeUnits{<:Any, NoDims},
                    Unitful.FreeUnits{<:Any, Unitful.𝐋^2 * Unitful.𝐌 * Unitful.𝐈^-1 * Unitful.𝐓^-3},
                    Unitful.FreeUnits{<:Any, Unitful.𝚯}}}

function default_unit_tuple()::UnitTuple
    return (
        length = u"m",
        angle = u"°",
        potential = u"V",
        temperature = u"K"
    )
end

function construct_unit(ustring::String)::Unitful.Units
    try
        parsed_unit = uparse(ustring)
        @assert parsed_unit isa Unitful.Units
        return parsed_unit
    catch
        @assert ustring in keys(unit_conversion) "Unit string $(ustring) cannot be interpreted..."
        return unit_conversion[ustring]
    end
end

function construct_units(config_file_dict::AbstractDict)::UnitTuple
    dunits::NamedTuple = default_unit_tuple()
    if haskey(config_file_dict, "units")
        d = config_file_dict["units"]
        dunits = (
            length = haskey(d, "length") ? construct_unit(d["length"]) : dunits.length,
            angle  = haskey(d, "angle") ? construct_unit(d["angle"]) : dunits.angle,
            potential = haskey(d, "potential") ? construct_unit(d["potential"]) : dunits.potential,
            temperature = haskey(d, "temperature") ? construct_unit(d["temperature"]) : dunits.temperature
        )
    end
    matching_units::Bool = dimension.(values(dunits)) == (Unitful.𝐋, Unitful.NoDims, Unitful.𝐋^2 * Unitful.𝐌 * Unitful.𝐈^-1 * Unitful.𝐓^-3, Unitful.𝚯)
    @assert matching_units "Some of the units are not given in the right format:"*
        (dimension(dunits.length)      == Unitful.𝐋       ? "" : "\n  - units.length ($(dunits.length)) does not describe a length.")*
        (dimension(dunits.angle)       == Unitful.NoDims  ? "" : "\n  - units.angle ($(dunits.angle)) does not describe an angle.")*
        (dimension(dunits.potential)   == Unitful.𝐋^2 * Unitful.𝐌 * Unitful.𝐈^-1 * Unitful.𝐓^-3 ? "" : "\n  - units.potential ($(dunits.potential)) does not describe a potential.")*
        (dimension(dunits.temperature) == Unitful.𝚯       ? "" : "\n  - units.temperature ($(dunits.temperature)) does not describe a temperature.")

    return UnitTuple(dunits)
end

const MaybeWithUnits{T} = Union{T, Quantity{<:T}}
const RealQuantity = MaybeWithUnits{<:Real}

DetectorHitEvents = TypedTables.Table{
    <:NamedTuple{
        (:evtno, :detno, :thit, :edep, :pos),
        <:Tuple{
            Union{Integer, AbstractVector{<:Integer}},
            Union{Integer, AbstractVector{<:Integer}},
            AbstractVector{<:RealQuantity},
            AbstractVector{<:RealQuantity},
            AbstractVector{<:AbstractVector{<:RealQuantity}}
        }
    }
}


"""
    Support for indicator constraints with a set constraint as the right hand side
"""
function JuMP._build_indicator_constraint(
    _error::Function,
    variable::JuMP.AbstractVariableRef,
    jump_constraint::JuMP.VectorConstraint,
    ::Type{MOI.IndicatorSet{A}},
) where {A}
    S = typeof(jump_constraint.set)
    set = CS.IndicatorSet{A,S}(jump_constraint.set, 1 + length(jump_constraint.func))
    if jump_constraint.func isa Vector{VariableRef}
        vov = JuMP.VariableRef[variable]
    else
        vov = JuMP.AffExpr[variable]
    end
    append!(vov, jump_constraint.func)
    return JuMP.VectorConstraint(vov, set)
end


module TurbulenceClosureParameters

using Oceananigans.Architectures: CPU, arch_array, architecture
using Oceananigans.TurbulenceClosures: AbstractTurbulenceClosure, AbstractTimeDiscretization, ExplicitTimeDiscretization
using Printf

#####
##### Free parameters
#####

struct FreeParameters{N, P}
     names :: N
    priors :: P
end

"""
    FreeParameters(priors; names = Symbol.(keys(priors)))

Return named `FreeParameters` with priors.
Free parameter `names` are inferred from the keys of `priors` if not provided.

Example
=======

```jldoctest
julia> using Distributions, OceanTurbulenceParameterEstimation

julia> priors = (ν = Normal(1e-4, 1e-5), κ = Normal(1e-3, 1e-5))
(ν = Normal{Float64}(μ=0.0001, σ=1.0e-5), κ = Normal{Float64}(μ=0.001, σ=1.0e-5))

julia> free_parameters = FreeParameters(priors)
FreeParameters with 2 parameters
├── names: (:ν, :κ)
└── priors: Dict{Symbol, Any}
    ├── ν => Normal{Float64}(μ=0.0001, σ=1.0e-5)
    └── κ => Normal{Float64}(μ=0.001, σ=1.0e-5)
```
"""
function FreeParameters(priors; names = Symbol.(keys(priors)))
    priors = NamedTuple(name => priors[name] for name in names)
    return FreeParameters(Tuple(names), priors)
end

free_parameters_str(fp::FreeParameters) = "$(fp.names)"

function prior_show(io, priors, name, prefix, width)
    print(io, @sprintf("%s %s => ", prefix, lpad(name, width, " ")))
    show(io, priors[name])
    return nothing
end

function Base.show(io::IO, p::FreeParameters)
    Np = length(p)
    print(io, "FreeParameters with $Np parameters", '\n',
              "├── names: $(p.names)", '\n',
              "└── priors: Dict{Symbol, Any}")

    maximum_name_length = maximum([length(string(name)) for name in p.names])

    for (i, name) in enumerate(p.names)
        prefix = i == length(p.names) ? "    └──" : "    ├──"
        print(io, '\n')
        prior_show(io, p.priors, name, prefix, maximum_name_length)
    end

    return nothing
end

Base.length(p::FreeParameters) = length(p.names)

#####
##### Setting parameters
#####

const ParameterValue = Union{Number, AbstractArray}

dict_properties(d::ParameterValue) = d

function dict_properties(d)
    p = Dict{Symbol, Any}(n => dict_properties(getproperty(d, n)) for n in propertynames(d))
    p[:type] = typeof(d).name.wrapper
    return p
end

construct_object(d::ParameterValue, parameters; name=nothing) = name ∈ keys(parameters) ? getproperty(parameters, name) : d

function construct_object(specification_dict, parameters; name=nothing, type_parameter=nothing)
    type = Constructor = specification_dict[:type]
    kwargs_vector = [construct_object(specification_dict[name], parameters; name) for name in fieldnames(type) if name != :type]
    return isnothing(type_parameter) ? Constructor(kwargs_vector...) : Constructor{type_parameter}(kwargs_vector...)
end

"""
    closure_with_parameters(closure, parameters)

Returns a new object where for each (`parameter_name`, `parameter_value`) pair
in `parameters`, the value corresponding to the key in `object` that matches
`parameter_name` is replaced with `parameter_value`.

Example
=======

```jldoctest
julia> using OceanTurbulenceParameterEstimation.TurbulenceClosureParameters: closure_with_parameters

julia> struct ClosureSubModel; a; b end

julia> struct Closure; test; c end

julia> closure = Closure(ClosureSubModel(1, 2), 3)
Closure(ClosureSubModel(1, 2), 3)

julia> parameters = (a = 12, d = 7)
(a = 12, d = 7)

julia> closure_with_parameters(closure, parameters)
Closure(ClosureSubModel(12, 2), 3)
```
"""
closure_with_parameters(closure, parameters) = construct_object(dict_properties(closure), parameters)

closure_with_parameters(closure::AbstractTurbulenceClosure{ExplicitTimeDiscretization}, parameters) =
    construct_object(dict_properties(closure), parameters, type_parameter=nothing)

closure_with_parameters(closure::AbstractTurbulenceClosure{TD}, parameters) where {TD <: AbstractTimeDiscretization} =
    construct_object(dict_properties(closure), parameters; type_parameter=TD)

closure_with_parameters(closures::Tuple, parameters) =
    Tuple(closure_with_parameters(closure, parameters) for closure in closures)

"""
    update_closure_ensemble_member!(closures, p_ensemble, parameters)

Use `parameters` to update closure from `closures` that corresponds to ensemble member `p_ensemble`.
"""
update_closure_ensemble_member!(closures::AbstractVector, p_ensemble, parameters) =
    closures[p_ensemble] = closure_with_parameters(closures[p_ensemble], parameters)

function update_closure_ensemble_member!(closures::AbstractMatrix, p_ensemble, parameters)
    for j in 1:size(closures, 2) # Assume that ensemble varies along first dimension
        closures[p_ensemble, j] = closure_with_parameters(closures[p_ensemble, j], parameters)
    end

    return nothing
end

function new_closure_ensemble(closures, θ)
    arch = architecture(closures)
    cpu_closures = arch_array(CPU(), closures)

    for (p, θp) in enumerate(θ)
        update_closure_ensemble_member!(cpu_closures, p, θp)
    end

    return arch_array(arch, cpu_closures)
end

end # module


### I/O functions for the CompressedStack structure
import Base.print, Base.string

## Convert functions : string ##
# Access and convert to Sting the content of a Nullable object
function string{T}(elt::Nullable{T})
  if !isnull(elt)
    str = string(get(elt))
  else
    str = "∅"
  end
  return str
end
# Convert a Signature into a string
function string{T}(sign::Signature{T})
  return "($(sign.first),$(sign.last)) ← $(sign.context)"
end
function string{T}(block::Block{T})
  str = "["
  for (id,sign) in enumerate(block)
    if id == 1
      str *= "$(string(sign))"
    else
      str *= ",$(string(sign))"
    end
  end
  str *= "]"
  return str
end
# Convert a Data{T} into a string
function string{D}(elt::Data{D})
  return "($(elt.index):=$(elt.data))"
end
# Convert a vector of partially compressed blocks into a string
function string{T}(lvl::Levels{T})
  str = "[\t\t\t\t"
  for (id,block) in enumerate(lvl)
      str *= "\n\t\t\tLevel $id\t\t$(string(block))"
  end
  str *= "\n\t\t\t ]"
  return str
end
# Convert a vector of explicit integral values into a string
function string{D}(explicit::Vector{Data{D}})
  str = "["
  for (id,p) in enumerate(explicit)
    if id == 1
      str *= string(p)
    else
      str *= ",$(string(p))"
    end
  end
  str *= "]"
  return str
end
# Convert a vector of D into a string (used for NormalStack)
function string{D}(values::Vector{D})
  str = "["
  for (id,p) in enumerate(values)
    if id == 1
      str *= string(p)
    else
      str *= ",$(string(p))"
    end
  end
  str *= "]"
  return str
end

## Print functions ##
# Print a CompressedStack in the console
function print(stack::CompressedStack)
  println("Compressed Stack with n=$(stack.size), p=$(stack.space),",
    " and h=$(stack.depth)")
  println("\t First:")
  println("\t\t (first) partially compressed ->")
  println("\t\t\t $(string(stack.first_partial))")
  println("\t\t (first) explicit ->")
  println("\t\t\tLevel $(stack.depth)\t\t$(string(stack.first_explicit))")
  println("\t Second:")
  println("\t\t (second) partially compressed ->")
  println("\t\t\t $(string(stack.second_partial))")
  println("\t\t (second) explicit ->")
  println("\t\t\tLevel $(stack.depth)\t\t$(string(stack.second_explicit))")
  println("\t Compressed tail:")
  println("\t\t\t\t\t$(string(stack.compressed))\n")
  println("\t Context:")
  println("\t\t\t\t\t$(string(stack.context))\n")
end

# Print NormalStack in the console
function print(stack::NormalStack)
  println("Normal Stack (used for comparison with a CompressedStack above)")
  println("\t values : $(string(stack.data))")
  println("\t context: $(string(stack.context))")
end

## Read from input file
# Get the settings (size, space, ...) of the CompressedStack
function get_settings(f::IOStream)
  i = 0
  size = 0
  space = 0

  while i < 2
    line = readline(f)
    if line[1] != '#'
      i += 1
      if i == 1
        aux = split(line)
        size = parse(Int,aux[1])
        space = parse(Int,aux[2])
      end
    end
  end
  return size,space
end

## Read the input: Get next element (before a push)
# For Compressed Stack
function readinput{T,D}(stack::CompressedStack{T,D})
  try
    stack.pos = position(stack.input)
    stack.index += 1
    line = readline(stack.input)
    aux = split(line)
    # println("Input: line=$line → aux=$aux")
    return parse(Int,aux[1])
  catch
    #println("Input: line=$line → aux=$aux")
    return 101
  end
end
function readinput(stack::NormalStack)
  stack.index += 1
  line = readline(stack.input)
  aux = split(line)
  return parse(Int,aux[1])
end


# struct WiderFactor{S<:Unsigned, T<:Unsigned} <: AbstractFactor{T}
#     basefactor::AbstractFactor{S}
# end

# Base.length(factor::WiderFactor{S, T}) where {S<:Unsigned} where {T<:Unsigned} = length(factor.basefactor)

# getlevels(factor::WiderFactor{S, T}) where {S<:Unsigned} where {T<:Unsigned} = getlevels(factor.basefactor)

# getname(factor::WiderFactor{S, T}) where {S<:Unsigned} where {T<:Unsigned} = getname(factor.basefactor)

# function slice(factor::WiderFactor{S, T}, fromobs::Integer, toobs::Integer, slicelength::Integer) where {S<:Unsigned} where {T<:Unsigned}
#     slicelength = verifyslicelength(fromobs, toobs, slicelength)
#     if S == T
#         slice(factor.basefactor, fromobs, toobs, slicelength)
#     else
#         f = x -> convert(T, x)
#         mapslice(f, slice(factor.basefactor, fromobs, toobs, slicelength), slicelength, T)
#     end
# end

# function Base.convert(::Type{WiderFactor{S, T}}, x::AbstractFactor{S}) where {S<:Unsigned} where {T<:Unsigned}
#     WiderFactor{S, T}(x)
# end

# function Base.map(factor::WiderFactor, dataframe::AbstractDataFrame)
#     map(factor.basefactor, dataframe)
# end

# function isordinal(factor::WiderFactor)
#     isordinal(factor.basefactor)
# end


using ImageQuilting
using GeoStatsBase
using GeoStatsImages
using Statistics
using Plots, VisualRegressionTests
using Test, Pkg, Random

# workaround GR warnings
ENV["GKSwstype"] = "100"

# environment settings
islinux = Sys.islinux()
istravis = "TRAVIS" ∈ keys(ENV)
datadir = joinpath(@__DIR__,"data")
visualtests = !istravis || (istravis && islinux)
if !istravis
  Pkg.add("Gtk")
  using Gtk
end

@testset "ImageQuilting.jl" begin
  @testset "Basic checks" begin
    # the output of a homogeneous image is also homogeneous
    TI = ones(20,20,20)
    reals = iqsim(TI, (10,10,10), size(TI))
    @test reals[1] == TI

    # categories are obtained from training image only
    ncateg = 3; TI = rand(0:ncateg, 20, 20, 20)
    reals = iqsim(TI, (10,10,10), size(TI))
    @test Set(reals[1]) ⊆ Set(TI)
  end

  @testset "Soft data" begin
    # trends with soft data
    TI = [zeros(10,20,1); ones(10,20,1)]
    trend = [zeros(20,10,1) ones(20,10,1)]
    reals = iqsim(TI, (10,10,1), size(TI), soft=[(trend,TI)], tol=1)
    @test mean(reals[1][:,1:10,:]) ≤ mean(reals[1][:,11:20,:])

    # no side effects with soft data
    TI = ones(20,20,20)
    TI[:,5,:] .= NaN
    aux = fill(1.0, size(TI))
    iqsim(TI, (10,10,10), size(TI), soft=[(aux,aux)])
    @test aux == fill(1.0, size(TI))

    # auxiliary variable with integer type
    TI = ones(20,20,20)
    aux = [i for i in 1:20, j in 1:20, k in 1:20]
    iqsim(TI, (10,10,10), size(TI), soft=[(aux,aux)])
    @test aux == [i for i in 1:20, j in 1:20, k in 1:20]
  end

  @testset "Hard data" begin
    # hard data is honored everywhere
    TI = ones(20,20,20)
    obs = rand(size(TI)...)
    data = Dict(CartesianIndex(i,j,k)=>obs[i,j,k] for i=1:20, j=1:20, k=1:20)
    reals = iqsim(TI, (10,10,10), size(TI), hard=data)
    @test reals[1] == obs

    # multiple realizations with hard data
    TI = ones(20,20,20)
    data = Dict(CartesianIndex(20,20,20)=>10)
    reals = iqsim(TI, (10,10,10), size(TI), hard=data, nreal=3)
    for real in reals
      @test real[20,20,20] == 10
    end
  end

  @testset "Masked grids" begin
    # masked simulation domain
    TI = ones(20,20,20)
    shape = Dict{CartesianIndex{3},Real}()
    active = trues(size(TI))
    for i=1:20, j=1:20, k=1:20
      if (i-10)^2 + (j-10)^2 + (k-10)^2 < 25
        push!(shape, CartesianIndex(i,j,k)=>NaN)
        active[i,j,k] = false
      end
    end
    reals = iqsim(TI, (10,10,10), size(TI), hard=shape)
    @test all(isnan.(reals[1][.!active]))
    @test all(.!isnan.(reals[1][active]))

    # masked training image
    TI = ones(20,20,20)
    TI[:,5,:] .= NaN
    reals = iqsim(TI, (10,10,10), size(TI))
    @test reals[1] == fill(1.0, size(TI))
    TI[1,5,:] .= 0
    reals = iqsim(TI, (10,10,10), size(TI))
    @test reals[1] == fill(1.0, size(TI))

    # masked domain and masked training image
    TI = ones(20,20,20)
    TI[:,5,:] .= NaN
    aux = fill(1.0, size(TI))
    shape = Dict(CartesianIndex(i,j,k)=>NaN for i=1:20, j=5, k=1:20)
    reals = iqsim(TI, (10,10,10), size(TI), hard=shape)
    @test all(isnan.(reals[1][:,5,:]))
    @test all(reals[1][:,1:4,:] .== 1)
    @test all(reals[1][:,6:20,:] .== 1)
    reals = iqsim(TI, (10,10,10), size(TI), hard=shape, soft=[(aux,aux)])
    @test all(isnan.(reals[1][:,5,:]))
    @test all(reals[1][:,1:4,:] .== 1)
    @test all(reals[1][:,6:20,:] .== 1)
  end

  @testset "Minimum error cut" begin
    # 3D cut
    TI = ones(20,20,20)
    _, _, voxs = iqsim(TI, (10,10,10), overlap=(1/3,1/3,1/3), debug=true)
    @test 0 ≤ voxs[1] ≤ 1
  end

  @testset "Simulation paths" begin
    # different simulation paths
    for kind in [:raster,:dilation,:random]
      path = ImageQuilting.genpath((10,10,10), kind, Int[])
      @test length(path) == 1000
    end

    # data is visited first if present
    path = ImageQuilting.genpath((10,10,10), :data, [1,1000])
    @test path[1:2] == [1,1000] || path[1:2] == [1000,1]
  end

  @testset "Voxel reuse" begin
    # mean voxel reuse is in range [0,1]
    TI = rand(20,20,20)
    μ, σ = voxelreuse(TI, (10,10,10), nreal=1)
    @test 0 ≤ μ ≤ 1
  end

  if visualtests
    for TIname in ["Strebelle","StoneWall"]
      Random.seed!(2017)
      TI = training_image(TIname)[1:50,1:50,:]
      reals = iqsim(TI, (30,30,1), size(TI), nreal=4)
      ps = [heatmap(real[:,:,1]) for real in reals]
      @plottest plot(ps...) joinpath(datadir,"Reals"*TIname*".png") !istravis
    end
    for TIname in ["StoneWall","WalkerLake"]
      Random.seed!(2017)
      TI = training_image(TIname)[1:20,1:20,:]
      @plottest voxelreuseplot(TI) joinpath(datadir,"Voxel"*TIname*".png") !istravis
    end
  end

  @testset "GeoStats.jl API" begin
    geodata = PointSetData(Dict(:variable => [1.,0.,1.]), [25. 50. 75.; 25. 75. 50.])
    grid    = RegularGrid{Float64}(100,100)
    problem = SimulationProblem(geodata, grid, :variable, 3)

    TI = training_image("Strebelle")
    inactive = [CartesianIndex(i,j,1) for i in 1:30 for j in 1:30]
    solver = ImgQuilt(:variable => (TI=TI, tilesize=(30,30,1), inactive=inactive))

    Random.seed!(2017)
    solution = solve(problem, solver)
    @test keys(solution.realizations) ⊆ [:variable]

    incomplete_solver = ImgQuilt()
    @test_throws AssertionError solve(problem, incomplete_solver)

    if visualtests
      @plottest plot(solution,size=(1000,300)) joinpath(datadir,"GeoStatsAPI.png") !istravis
    end
  end

  # if ImageQuilting.cl ≠ nothing && ImageQuilting.clfft ≠ nothing
    # @testset "GPU support" begin
      # # CPU and GPU give same results
      # TI = ones(20,20,20)
      # TI[10:end,:,:] = 2
      # Random.seed!(0); realscpu = iqsim(TI, (10,10,10), size(TI), gpu=false)
      # Random.seed!(0); realsgpu = iqsim(TI, (10,10,10), size(TI), gpu=true)
      # @test realscpu[1] == realsgpu[1]
    # end
  # end
end


struct RandCovariate <: AbstractCovariate{Float32}
    name::String
    length::Integer
end

function RandCovariate(length::Integer)
    RandCovariate("", length)
end

Base.length(covariate::RandCovariate) = covariate.length

function slice(covariate::RandCovariate, fromobs::Integer, toobs::Integer, slicelength::Integer)
    if fromobs > toobs
        EmptySeq{SubArray{Float32,1,Array{Float32,1},Tuple{UnitRange{Int64}},true}}()
    else
        fromobs = max(1, fromobs)
        toobs = min(toobs, length(covariate))
        slicelength = verifyslicelength(fromobs, toobs, slicelength)
        buffer = Vector{Float32}(undef, slicelength)
        map(Seq(Tuple{Int64, Int64}, (fromobs, toobs, slicelength), nextslice), SubArray{Float32,1,Array{Float32,1},Tuple{UnitRange{Int64}},true}) do rng
            rand!(buffer)
            view(buffer, 1:(rng[2] - rng[1] + 1))
        end
    end
end


@testset "EmbedOnehot" begin
    multi(x, n) = collect(Iterators.repeated(x, n))

    after_enc = [2,2,4,5,6,1,1]
    multi_after_enc = hcat(multi(after_enc, 5)...)

    e = Embed(10, 6)

    @test e(after_enc) == hcat(map(i->e.embedding[:,i], after_enc)...)
    @test eltype(e(after_enc, 0.5)) == Float32

    @test size(e) == (10, 6)
    @test size(e, 1) == 10
    @test size(e, 2) == 6
end


q=parse;r=reduce;v,b=0,join(reverse(digits(q(BigInt,"1"*readline(),base=16),base=2))[2:end]);function p(i);t(c)=q(Int,b[i[1]:((i[1]+=c)-1)],base=2);global v+=t(3);d,c=t(3),1;d==4&&return r((a,b)->16a+b,[t(4) for _=1:99 if c==1&&(c=t(1))<2]);a(v)=r([+,*,min,max,i,>,<,==][d+1],v);(t(1)==0&&(g=t(15)+i[1])!=0) ? (return[p(i) for _=1:99 if i[1]<g]|>a) : return[p(i) for _=1:t(11)]|>a;end;p([1])|>b->println("$v\n$b")


# MIT License

# Copyright (c) Microsoft Corporation.

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE

"""
    Rectangle{T} <: Surface{T}

Rectangular surface, not a valid CSG object.
The rotation of the rectangle around its normal is defined by `rotationvec`.
`rotationvec×surfacenormal` is taken as the vector along the u axis.

**Can be used as a detector in [`OpticalSystem`](@ref)s.**

```julia
Rectangle(halfsizeu::T, halfsizev::T, [surfacenormal::SVector{3,T}, centrepoint::SVector{3,T}]; rotationvec::SVector{3,T} = [0.0, 1.0, 0.0], interface::NullOrFresnel{T} = nullinterface(T))
```

The minimal case returns a rectangle centered at the origin with `surfacenormal = [0, 0, 1]`.
"""
struct Rectangle{T} <: Surface{T}
    plane::Plane{T,3}
    halfsizeu::T
    halfsizev::T
    uvec::SVector{3,T}
    vvec::SVector{3,T}

    function Rectangle(halfsizex::T, halfsizey::T; interface::NullOrFresnel{T} = NullInterface(T)) where {T<:Real}
        @assert halfsizex > zero(T) && halfsizey > zero(T)
        new{T}(Plane(zero(T), zero(T), one(T), zero(T), zero(T), zero(T), interface = interface), halfsizex, halfsizey, SVector{3,T}(1.0, 0.0, 0.0), SVector{3,T}(0.0, 1.0, 0.0))
    end

    function Rectangle(halfsizeu::T, halfsizev::T, surfacenormal::AbstractArray{T,1}, centrepoint::AbstractArray{T,1}; interface::NullOrFresnel{T} = NullInterface(T), rotationvec::AbstractArray{T,1} = SVector{3,T}(0.0, 1.0, 0.0)) where {T<:Real}
        @assert length(surfacenormal) == 3 && length(centrepoint) == 3
        return Rectangle(halfsizeu, halfsizev, SVector{3,T}(surfacenormal), SVector{3,T}(centrepoint), interface = interface, rotationvec = SVector{3,T}(rotationvec))
    end

    function Rectangle(halfsizeu::T, halfsizev::T, surfacenormal::SVector{3,T}, centrepoint::SVector{3,T}; interface::NullOrFresnel{T} = NullInterface(T), rotationvec::SVector{3,T} = SVector{3,T}(0.0, 1.0, 0.0)) where {T<:Real}
        @assert halfsizeu > zero(T) && halfsizev > zero(T)
        n̂ = normalize(surfacenormal)
        if abs(dot(rotationvec, n̂)) == one(T)
            rotationvec = SVector{3,T}(1.0, 0.0, 0.0)
        end
        uvec = normalize(cross(normalize(rotationvec), n̂))
        vvec = normalize(cross(n̂, uvec))
        new{T}(Plane(n̂, centrepoint, interface = interface), halfsizeu, halfsizev, uvec, vvec)
    end

    function Rectangle(plane::Plane{T,3}, halfsizeu::T, halfsizev::T, uvec::SVector{3,T}, vvec::SVector{3,T}) where {T<:Real}
        new{T}(plane, halfsizeu, halfsizev, uvec, vvec)
    end
end
export Rectangle

Base.show(io::IO, a::Rectangle{T}) where {T<:Real} = print(io, "Rectangle{$T}($(centroid(a)), $(normal(a)), $(a.halfsizeu), $(a.halfsizev), $(interface(a)))")

centroid(r::Rectangle{T}) where {T<:Real} = r.plane.pointonplane
interface(a::Rectangle{T}) where {T<:Real} = interface(a.plane)

uvrange(::Type{Rectangle{T}}) where {T<:Real} = ((-one(T), one(T)), (-one(T), one(T)))

normal(r::Rectangle{T}) where {T<:Real} = normal(r.plane)
normal(r::Rectangle{T}, ::T, ::T) where {T<:Real} = normal(r)

point(r::Rectangle{T}, u::T, v::T) where {T<:Real} = centroid(r) + (r.halfsizeu * u * r.uvec) + (r.halfsizev * v * r.vvec)
partials(r::Rectangle{T}, ::T, ::T) where {T<:Real} = r.halfsizeu * r.uvec, r.halfsizev * r.vvec

uv(r::Rectangle{T}, p::SVector{3,T}) where {T<:Real} = SVector{2,T}(dot(p - centroid(r), r.uvec) / r.halfsizeu, dot(p - centroid(r), r.vvec) / r.halfsizev)

onsurface(a::Rectangle{T}, point::SVector{3,T}) where {T<:Real} = onsurface(a.plane, point) && abs(uv(a, point)[1]) <= one(T) && abs(uv(a, point)[2]) <= one(T)

"""
    uvtopix(surf::Surface{T}, uv::SVector{2,T}, imsize::Tuple{Int,Int}) -> Tuple{Int,Int}

Converts a uvcoordinate on `surf` to an integer index to a pixel in an image of size `imsize`.
Not implemented on all `Surface` objects.
Used to determine where in the detector image a ray has hit when in intersects the detector surface of an [`OpticalSystem`](@ref).
"""
function uvtopix(::Rectangle{T}, uv::SVector{2,T}, imsize::Tuple{Int,Int}) where {T<:Real}
    u, v = uv
    h, w = imsize
    pixu = Int(floor((w - 1) * ((u + 1) / 2))) + 1
    pixv = h - Int(floor((h - 1) * ((v + 1) / 2)))
    return pixu, pixv
end

function surfaceintersection(rect::Rectangle{T}, r::AbstractRay{T,3}) where {T<:Real}
    interval = surfaceintersection(rect.plane, r)
    if interval isa EmptyInterval{T} || isinfiniteinterval(interval)
        return EmptyInterval(T) # no ray plane intersection or inside plane but no hit
    else
        intersect = halfspaceintersection(interval)
        p = point(intersect)
        if abs(dot(p - centroid(rect), rect.uvec)) > rect.halfsizeu || abs(dot(p - centroid(rect), rect.vvec)) > rect.halfsizev
            return EmptyInterval(T) # point outside rect
        else
            u, v = uv(rect, p)
            intuv = Intersection(α(intersect), p, normal(rect), u, v, interface(rect))
            if dot(normal(rect), direction(r)) < zero(T)
                return positivehalfspace(intuv)
            else
                return rayorigininterval(intuv)
            end
        end
    end
end

function makemesh(r::Rectangle{T}, ::Int = 0) where {T<:Real}
    p00 = point(r, -one(T), -one(T))
    p01 = point(r, -one(T), one(T))
    p10 = point(r, one(T), -one(T))
    p11 = point(r, one(T), one(T))
    if validtri(p00, p11, p01) && validtri(p00, p10, p11)
        return TriangleMesh([Triangle(p00, p11, p01), Triangle(p00, p10, p11)])
    else
        return nothing
    end
end


using ICapeThermoUniversalConstants,ICapeThermoCompounds,ECapeExceptions,PhysicalPropertyCalculator
import CapeOpen: MaterialObject, PropertyPackage
import ICapeThermoCompounds.TempPropData

#ICapeThermoUniversalConstants
universalconstantlist=getuniversalconstantlist()
println("universalconstantlist=",universalconstantlist)
@test_throws ECapeInvalidArgument (getuniversalconstant("avogadroConstan"))
@test getuniversalconstant("avogadroConstant") == 6.0221419947e23

#ICapeThermoCompounds
constproplist=getconstproplist()
@test length(constproplist) == 17
println("constproplist=",constproplist)
tdependentproplist=gettdependentproplist()
@test length(tdependentproplist) == 11
println("tdependentproplist=",tdependentproplist)
pdependentproplist=getpdependentproplist()
@test length(pdependentproplist) == 1
@test getnumcompounds() == 345
compIds,formulae,names,boilTemps,molwts,casnos=getcompoundlist();
comps=UInt16[findfirst(names,name) for name in ["Air","Water","Nitrogen"]];
propvals=Vector{Union{ASCIIString,Float64}}();
@test_throws ECapeThrmPropertyNotAvailable getcompoundconstant!(["casRegistryNumber","chemicalFormula","criticalPressure","liquidDensityAt25C"],comps,propvals)
allconstants=Vector{Union{ASCIIString,Float64}}()
@test_throws ECapeThrmPropertyNotAvailable getcompoundconstant!(constproplist,compIds,allconstants)
@test length(allconstants) == length(constproplist)*length(compIds)
propvals=Vector{Float64}()
getpdependentproperty!(["boilingPointTemperature"],80000.,comps,propvals)
propvals=Vector{Float64}()
@test_throws ECapeThrmPropertyNotAvailable gettdependentproperty!(tdependentproplist,300.,compIds,propvals)


module Utils

using StaticArrays
import JuLIP
import HDF5, JSON

function get_data(filenames, cutfunc, get_env)
    data = []
    for f in filenames
        SKdata = h5read_SK(f[1]; get_HS=true,
                                 get_atoms=true,
                                 get_metadata=true,
                                 get_energies=true)

        kk = size(SKdata[1],2)
        ijmat = Int.(SKdata[1][1:2,1:kk])
        Rijmat = SKdata[1][3:5,1:kk]
        sel_ijR = [1,3,4,5]
        ijRijmat = SKdata[1][sel_ijR,1:kk]
        Bondint = SKdata[1][6:end,1:kk]
        at = SKdata[6]

        if f[2] > 0
           select_i = Int(f[2])
           Rijmat = Rijmat[:, ijmat[1,:] .== select_i]
           Bondint = Bondint[:, ijmat[1,:] .== select_i]
           ijmat = ijmat[:, ijmat[1,:] .== select_i]
        end

        for i in 1:size(Bondint,2)
            R0 = SVector(Rijmat[:,i]...)
            ii = ijmat[1,i]
            Renv = get_env(at, R0, ii, cutfunc)
            E = Bondint[:,i]
            push!(data,[R0,Renv,E])
        end
    end
    return data
end

function read_json(fname)
  return JSON.parsefile(fname)
end

function write_json(fname, update_dict)
  file_dict = JSON.parse(join(readlines(fname)))
  merge!(file_dict, update_dict)
  open(fname, "w") do f
     write(f, JSON.json(file_dict,4))
  end
end

function h5read_SK(fname; get_HS=false, get_atoms=false, get_metadata=false, get_energies=false)
    data = []
    HDF5.h5open(fname, "r") do fd
        groupnames = []
        for obj in fd
           push!(groupnames, HDF5.name(obj))
        end
        group_name = groupnames[1]
        dataset_names = []
        for obj in fd[group_name]
           push!(dataset_names, HDF5.name(obj))
        end
        SK_HS = HDF5.read(fd, string(group_name,"/SKHS"))
        push!(data, SK_HS)
        if get_HS
            HS_data = []
            H_str = string(group_name,"/H")
            S_str = string(group_name,"/S")
            if haskey(fd, H_str)
                H = HDF5.read(fd, string(group_name,"/H"))
                push!(HS_data, H)
            end
            if haskey(fd, S_str)
                S = HDF5.read(fd, string(group_name,"/S"))
                push!(HS_data, S)
            end
            push!(data, HS_data)
        end
        if get_energies
            energy = HDF5.read(fd, string(group_name,"/energy"))
            freeenergy = HDF5.read(fd, string(group_name,"/freeenergy"))
            push!(data, [energy, freeenergy])
        end
        if get_atoms
            positions = HDF5.read(fd, string(group_name,"/positions"))
            unitcell = HDF5.read(fd, string(group_name,"/unitcell"))
            species = HDF5.read(fd, string(group_name,"/species"))
            push!(data, [unitcell, species, positions])
        end
        if get_metadata
            metadata_str = HDF5.read(fd, string(group_name,"/metadata"))
            metadata = JSON.parse(metadata_str)
            push!(data, metadata)
        end
        push!(data, JuLIP.Atoms(; X = positions, Z = species,
                                  cell = unitcell,
                                  pbc = [true, true, true]))
    end
    return data
end

end # End of Module


#### IMPLEMENTATION OF JEFF SKILLINGS' NESTED SAMPLING ALGORITHM ####
function nested_step!(e::Bayes_IPM_ensemble, param_set, permute_limit::Integer)
    N = length(e.models) #number of sample models/particles on the posterior surface
    i = length(e.log_Li) #iterate number, index for last values
    j = i+1 #index for newly pushed values

    ll_contour, least_likely_idx = findmin([model.log_Li for model in e.models])

    #REMOVE OLD LEAST LIKELY MODEL
    Li_model = e.models[least_likely_idx]
    deleteat!(e.models, least_likely_idx)

    e.sample_posterior ? push!(e.retained_posterior_samples, Li_model) : rm(Li_model.path) #if sampling posterior, push the model record to the ensemble's posterior samples vector, otherwise delete the serialised model pointed to by the model record

    #SELECT NEW MODEL, SAVE TO ENSEMBLE DIRECTORY, CREATE RECORD AND PUSH TO ENSEMBLE
    model_selected=false; step_report=0
    while !model_selected
        candidate,step_report=run_permutation_routine(e, param_set..., permute_limit, ll_contour)
        if !(candidate===nothing)
            model_selected=true
            new_model_record = Model_Record(string(e.path,'/',e.model_counter), candidate.log_Li);
            push!(e.models, new_model_record);
            final_model=ICA_PWM_model(string(e.model_counter), candidate.sources, candidate.informed_sources, candidate.source_length_limits, candidate.mix_matrix, candidate.log_Li, candidate.flags)
            serialize(new_model_record.path, final_model)
            e.model_counter +=1
        else
            push!(e.models, Li_model)
            return 1, nothing
        end
    end

    #UPDATE ENSEMBLE QUANTITIES
    push!(e.log_Li, minimum([model.log_Li for model in e.models])) #log likelihood of the least likely model - the current ensemble ll contour at Xi
    push!(e.log_Xi, -i/N) #log Xi - crude estimate of the iterate's enclosed prior mass
    push!(e.log_wi, log(exp(e.log_Xi[i]) - exp(e.log_Xi[j]))) #log width of prior mass spanned by the last step
    push!(e.log_Liwi, lps(e.log_Li[j],e.log_wi[j])) #log likelihood + log width = increment of evidence spanned by iterate
    push!(e.log_Zi, logaddexp(e.log_Zi[i],e.log_Liwi[j]))    #log evidence
    #information- dimensionless quantity
    push!(e.Hi, lps(
            (exp(lps(e.log_Liwi[j],-e.log_Zi[j])) * e.log_Li[j]), #term1
            (exp(lps(e.log_Zi[i],-e.log_Zi[j])) * lps(e.Hi[i],e.log_Zi[i])), #term2
            -e.log_Zi[j])) #term3

    return 0, step_report
end

function nested_step!(e::Bayes_IPM_ensemble, model_chan::RemoteChannel, worker_persistence::BitVector)
    N = length(e.models) #number of sample models/particles on the posterior surface
    i = length(e.log_Li) #iterate number, index for last values
    j = i+1 #index for newly pushed values

    #REMOVE OLD LEAST LIKELY MODEL
    ll_contour, least_likely_idx = findmin([model.log_Li for model in e.models])

    Li_model = e.models[least_likely_idx]
    deleteat!(e.models, least_likely_idx)

    e.sample_posterior ? push!(e.retained_posterior_samples, Li_model) : rm(Li_model.path) #if sampling posterior, push the model record to the ensemble's posterior samples vector, otherwise delete the serialised model pointed to by the model record

    #SELECT NEW MODEL, SAVE TO ENSEMBLE DIRECTORY, CREATE RECORD AND PUSH TO ENSEMBLE
    model_selected=false;wk=0;step_report=0
    while !model_selected
        @async wait(model_chan)
        model_tuple = take!(model_chan)
        if !(model_tuple===nothing)
            candidate,wk,step_report=model_tuple
            if candidate.log_Li > ll_contour
                model_selected=true
                new_model_record = Model_Record(string(e.path,'/',e.model_counter), candidate.log_Li);
                push!(e.models, new_model_record);
                final_model=ICA_PWM_model(string(e.model_counter), candidate.sources, candidate.informed_sources, candidate.source_length_limits, candidate.mix_matrix, candidate.log_Li, candidate.flags)
                serialize(new_model_record.path, final_model)
                e.model_counter +=1
            end
        else
            @warn "Worker failed to find new model in contour $ll_contour in iterate $i prior to reaching convergence criterion";
            worker_persistence[findfirst(worker_persistence)]=false
            !any(worker_persistence) && ((push!(e.models, Li_model)); return (1,0,0))
        end
    end

    #UPDATE ENSEMBLE QUANTITIES
    push!(e.log_Li, minimum([model.log_Li for model in e.models])) #log likelihood of the least likely model - the current ensemble ll contour at Xi
    push!(e.log_Xi, -i/N) #log Xi - crude estimate of the iterate's enclosed prior mass
    push!(e.log_wi, log(exp(e.log_Xi[i]) - exp(e.log_Xi[j]))) #log width of prior mass spanned by the last step
    push!(e.log_Liwi, lps(e.log_Li[j],e.log_wi[j])) #log likelihood + log width = increment of evidence spanned by iterate
    push!(e.log_Zi, logaddexp(e.log_Zi[i],e.log_Liwi[j]))    #log evidence

    #information- dimensionless quantity
    push!(e.Hi, lps(
            (exp(lps(e.log_Liwi[j],-e.log_Zi[j])) * e.log_Li[j]), #term1
            (exp(lps(e.log_Zi[i],-e.log_Zi[j])) * lps(e.Hi[i],e.log_Zi[i])), #term2
            -e.log_Zi[j])) #term3

    return 0, wk, step_report
end

function ns_converge!(e::Bayes_IPM_ensemble, param_set, permute_limit::Integer, evidence_fraction::AbstractFloat=.001; model_display=1, backup::Tuple{Bool,Integer}=(false,0), verbose::Bool=false)
    N = length(e.models)
    log_frac=log(evidence_fraction)

    iterate = length(e.log_Li) #get the iterate from the enemble
    meter = ProgressNS(e.naive_lh, typemax(typeof(e.naive_lh)), [1], "Nested Sampling::", 0, iterate, no_displayed_srcs=model_display)

    while lps(findmax([model.log_Li for model in e.models])[1],  e.log_Xi[end]) >= lps(log_frac,e.log_Zi[end])
        iterate = length(e.log_Li) #get the iterate from the enemble
        warn,step_report = nested_step!(e, param_set, permute_limit) #step the ensemble
        warn == 1 && #"1" passed for warn code means no workers persist; all have hit the permute limit
                (@error "All workers failed to find new models, aborting at current iterate."; return e) #if there is a warning, iust return the ensemble and print info
        iterate += 1

        backup[1] && iterate%backup[2] == 0 && serialize(string(e.path,'/',"ens"), e) #every backup interval, serialise the ensemble

        e_update_progress(e,meter,log_frac,1,step_report,model_display)
    end

    final_logZ = logsumexp([model.log_Li for model in e.models]) +  e.log_Xi[length(e.log_Li)] - log(1/length(e.models))

    @info "Job done, sampled to convergence. Final logZ $final_logZ"

    return final_logZ
end

function ns_converge!(e::Bayes_IPM_ensemble, param_sets, permute_limit::Integer, writer::Integer, worker_pool::Vector{Integer}, evidence_fraction::AbstractFloat=.001; model_display::Integer=1, backup::Tuple{Bool,Integer}=(false,0), verbose::Bool=false)
    N = length(e.models)
    log_frac=log(evidence_fraction)

    model_chan= RemoteChannel(()->Channel{Union{Tuple{ICA_PWM_model,Integer, Tuple},Nothing}}(length(worker_pool))) #channel to take EM iterates off of
    job_chan = RemoteChannel(()->Channel{Union{Vector{Model_Record},Nothing}}(1))
    put!(job_chan,e.models)

    worker_persistence=trues(length(worker_pool))

    @assert length(param_sets)==length(worker_pool) "Each worker must have a parameter set!"

    for (x,worker) in enumerate(worker_pool)
        remote_do(worker_permute, worker, e, job_chan, model_chan, param_sets[x]..., permute_limit)
    end

    iterate = length(e.log_Li) #get the iterate from the ensemble
    meter = ProgressNS(e.naive_lh, typemax(AbstractFloat), worker_pool, "Nested Sampling::", 0, iterate, no_displayed_srcs=model_display)

    while lps(findmax([model.log_Li for model in e.models])[1],  e.log_Xi[end]) >= lps(log_frac,e.log_Zi[end])
        iterate = length(e.log_Li) #get the iterate from the ensemble
        warn, wk, step_report = nested_step!(e, model_chan, worker_persistence) #step the ensemble
        warn == 1 && #"1" passed for warn code means no workers persist; all have hit the permute limit
                (@error "All workers failed to find new models, aborting at current iterate."; return e) #if there is a warning, iust return the ensemble and print info
        iterate += 1

        take!(job_chan); put!(job_chan,e.models)

        backup[1] && iterate%backup[2] == 0 && serialize(string(e.path,'/',"ens"), e) #every backup interval, serialise the ensemble

        remote_do(e_update_progress, writer, e, meter, log_frac, wk, step_report, model_display)
    end

    take!(job_chan); put!(job_chan, nothing)

    final_logZ = logsumexp([model.log_Li for model in e.models]) +  e.log_Xi[length(e.log_Li)] - log(1/length(e.models))

    @info "Job done, sampled to convergence. Final logZ $final_logZ"

    return final_logZ
end

                function e_update_progress(e,meter,log_frac,wk,step_report,model_display)
                    Li_vec=[model.log_Li for model in e.models]
                    if model_display>0
                        max_Li,max_idx=findmax(Li_vec)
                        max_model=deserialize(e.models[max_idx].path)
                        update!(meter, e.log_Li[end], max_Li, lps(max_Li,  e.log_Xi[end]), lps(log_frac,e.log_Zi[end]), e.Hi[end], Li_vec, wk, step_report...,sources=max_model.sources,bitmatrix=max_model.mix_matrix)
                    else
                        update!(meter, e.log_Li[end], max_Li, lps(max_Li,  e.log_Xi[end]), lps(log_frac,e.log_Zi[end]), e.Hi[end], Li_vec, wk, step_report...)
                    end
                end


import Base.length
import Base.reshape
import AxisArrays.AxisArray
import JuMP.value
import LinearAlgebra: transpose
import MutableArithmetics
using AxisArrays
using JuMP
using Printf

function string_dictkeys_tosymbols(d::Dict)
    d2 = Dict()
    for (k, v) in d
        d2[Symbol(k)] = v
    end
    return d2
end

function filter_dict_to_match_struct_field_names(d::Dict, s::DataType)
    f = fieldnames(s)
    d2 = Dict()
    for k in f
        if haskey(d, k)
            d2[k] = d[k]
        else
            @sprintf "\nWARNING: dict is missing struct field %s !\n" k
        end
    end
    return d2
end

Base.@kwdef struct Parameter
	 ###  Sets  ###
	 Storage::Array{String,1}      # Set B in math; new; B = "Elec","HotThermal","ColdThermal"
     TechClass::Array{String,1}    # Set C
	 DemandBin::UnitRange{Int64}   # Set E
	 FuelType::Array{String,1}     # Set F; new; F = {"NaturalGas"} for CHP and whatever fuel source is used for Boiler
     TimeStep::UnitRange{Int64}    # Set H
     TimeStepBat::UnitRange{Int64} # Set H union {0}
	 Subdivision::Array{String,1}	# Set K; new; elements = { "CapCost", "FuelBurn" }
	 Month::UnitRange{Int64} 	    # Set M
	 DemandMonthsBin::UnitRange{Int64}	# Set N
	 Ratchets::UnitRange{Int64}	   # Set R
     Seg::UnitRange{Int64}	       # Set S
	 Tech::Array{String,1}         # Set T in math
	 FuelBin::UnitRange{Int64}	   # To be removed
	 PricingTier::UnitRange{Int64}  # Set U: Pricing Tiers (proposed revision) #new
	 NMILRegime::Array{String,1}	# Set V: Net-metering Regimes

	 ###  Subsets and Indexed Sets  ####
	 ElecStorage::Array{String,1}  # B^{e} \subset B: Electrical energy storage systems
	 #HotTES::Array{String,1}  # B^{h} \subset B: Hot thermal energy storage systems (IGNORE)
	 #ColdTES::Array{String,1}  # B^{c} \subset B: Cold thermal energy storage systems (IGNORE)
	 #ThermalStorage::Array{String,1}  # B^{th} \subset B: Thermal energy storage systems (IGNORE)
	 TimeStepRatchetsMonth::Array{Array{Int64,1},1}   #  H_m: Time steps in month m
	 TimeStepRatchets::Array{Array{Int64,1},1}   #  H_r: Time steps in ratchet r
     TimeStepsWithGrid::Array{Int64,1}  # H_g: Time steps with grid connection
     TimeStepsWithoutGrid::Array{Int64,1}	 # H \setminus H_g: Time steps without grid connection
	 DemandLookbackMonths::Array{Any,1}   # M^{lb}: fixed Look back months considered for peak pricing
	 DemandLookbackRange::Int   # number of Look back months considered for peak pricing
	 SegByTechSubdivision::AxisArray # S_{kt}: System size segments from segmentation k applied to technology t
	 TechsInClass::AxisArray # T_c \subset T: Technologies that are in class c
	 TechsByFuelType::AxisArray # T_f \subset T: Technologies that burn fuel type f
	 TechsByNMILRegime::AxisArray # T_v \subset T: Technologies that may acess net-meterng regime v
	 #AbsorptionChillers::Array{String,1}  # T^{ac} \subset T: Absorption Chillers (IGNORE)
	 #CHPTechs::Array{String,1}  # T^{CHP} \subset T: CHP technologies (IGNORE)
	 #CoolingTechs::Array{String,1}  # T^{cl} \subset T: Cooling technologies (IGNORE)
	 ElectricTechs::Array{String,1}  # T^{e} \subset T: Electricity-producing technologies
	 #ElectricChillers::Array{String,1}  # T^{ec} \subset T: Electric chillers  (IGNORE)
	 FuelBurningTechs::Array{String,1}  # T^{f} \subset T: Fuel-burning technologies
	 #HeatingTechs::Array{String,1}  # T^{ht} \subset T: Heating technologies (IGNORE)
	 TechsNoTurndown::Array{String,1}  # T^{ac} \subset T: Technologies that cannot turn down, i.e., PV and wind

	 ###  Parameters and Tables supporting Indexed Sets ###
	 TechToNMILMapping::AxisArray  # Defines set T_v: Technologies that may be access net-metering regime v

	 ###  Scaling Parameters ###
	 TimeStepScaling::Float64  # \Delta: Time step scaling [h]

	 ###  Parameters for Costs and their Functional Forms ###
     AnnualMinCharge::Float64    # c^{amc}: Utility annual minimum charge
     MonthlyMinCharge::Float64    # c^{mmc}: Utility monthly minimum charge  (not in math; will use this in min charge calculation)
	 FixedMonthlyCharge::Float64  # c^{fmc}: Utility monthly fixed charge
	 FuelCost::AxisArray # c^{u}_{f}: Unit cost of fuel type f [$/MMBTU]  in math  (NEW)
	 ElecRate::Array{Float64, 2}  #   c^{g}_{uh}: Grid energy cost in energy demand tier u during time step h  (NEW)
	 OMperUnitSize::AxisArray # c^{om}_{t}: Operation and maintenance cost of technology t per unit of system size [$/kW]
     OMcostPerUnitProd::AxisArray

	 GridExportRates::Array{Float64, 2}  # c^{e}_{uh}: Export rate for energy in energy pricing tier u in time step h   (NEW)
	 CapCostSlope::AxisArray   # c^{cm}_{ts}: Slope of capital cost curve for technology t in segment s
     CapCostYInt::AxisArray  # c^{cb}_{ts}: Y-Intercept of capital cost curve for technology t in segment s
     CapCostX::AxisArray    # X-value of inflection point (will be changed)
	 #For the replacement of CapCostX, see new parameters SegmentLB and SegmentUB in section "System size and fuel limit parameters"
	 DemandRates::Array{Float64, 2}  # c^{r}_{re}: Cost per unit peak demand in tier e during ratchet r
	 DemandRatesMonth::Array{Float64, 2}   # c^{rm}_{mn}: Cost per unit peak demand in tier n during month m

	 ###  Demand Parameters ###
	 ElecLoad::Array{Float64,1}  # \delta^{d}_{h}: Electrical load in time step h   [kW]
	 # HeatingLoad::Array{Float64,1}  # \delta^{bo}_{h}: Heating load in time step h   [MMBTU/hr]
	 # CoolingLoad::Array{Float64,1}  # \delta^{c}_{h}: Cooling load in time step h   [kW]
     DemandLookbackPercent::Float64    # \delta^{lp}: Demand Lookback proportion [fraction]
     MaxDemandInTier::Array{Float64,1}  # \delta^{t}_{e}: Maximum power demand in ratchet e
     MaxDemandMonthsInTier::Array{Float64,1}   # \delta^{mt}_{n}: Maximum monthly power demand in tier n
	 MaxGridSales::Array{<:Real, 1}   # \delta^{gs}_{u}: Maximum allowable energy sales in tier u in math; equal to sum of LoadProfile["1R",ts] on set TimeStep for tier 1 (analogous "1W") and unlimited for "1X"
     MaxUsageInTier::Array{Float64,1}   # \delta^{tu}_{u}: Maximum monthly energy demand in tier u

	 ###  Incentive Parameters ###
	 NMILLimits::AxisArray   # i^{n}_{v}: Net metering and interconnect limits in net metering regime v [kW]
     MaxProdIncent::AxisArray      # \bar{i}_t: Upper incentive limit for technology t [$]
	 ProductionIncentiveRate::AxisArray  # i^{r}_{t}: Incentive rate for technology t [$/kWh] (NEW)
	 MaxSizeForProdIncent::AxisArray  # \bar{i}^{\sigma}_t: Maximum system size to obtain production incentive for technology t [kW]

	 ###  Technology-specific Time-series Factor Parameters ###
	 ProductionFactor::AxisArray    #f^{p}_{th}  Production factor of technology t and time step h  [unitless]  (NEW)
     # f^{fa}_{th}: Fuel burn ambient correction factor of technology t at time step h [unitless]
	 # f^{ha}_{th}: Hot water ambient correction factor of technology t at time step h [unitless]
	 # f^{ht}_{th}: Hot water thermal grade correction factor t correction factor of technology t at time step h [unitless]
	 # f^{ed}_{th}: Fuel burn ambient correction factor of technology t at time step h [unitless]

	 ###  Technology-specific Factor Parameters ###
	 TurbineDerate::AxisArray  # f^{d}_{t}: Derate factor for turbine technologyt [unitless]
     MinTurndown::AxisArray    # f^{td}_{t}:  Minimum turn down for technology t [unitless]
     pwf_prod_incent::AxisArray   # f^{pi}_t: Present worth factor for incentives for technology t [unitless]
	 LevelizationFactor::AxisArray    # f^{l}_{t}: Levelization factor of technology t [unitless]

	 ###  Generic Factor Parameters ###
	 pwf_om::Float64  # f^{om}: Operations and maintenance present worth factor [unitless]
     pwf_e::Float64   # f^{e}: Energy present worth factor [unitless]
	 r_tax_owner::Float64      # f^{tow}: Tax rate factor for owner [fraction]
     r_tax_offtaker::Float64   # f^{tot}: Tax rate factor for offtaker [fraction]

	 ###  System Size and Fuel Limit Parameters ###
	 TechClassMinSize::AxisArray   #  \ubar{b}^{\sigma}_{c}: Minimum system size for technology class c [kW]
	 MaxSize::AxisArray    #  \bar{b}^{\sigma}_{t}: Maximum system size for technology t [kW]
	 SegmentMinSize::AxisArray # \ubar{b}^{\sigma s}_{tks}: Minimum system size for technology t, subdivision k, segments
	 SegmentMaxSize::AxisArray  # \bar{b}^{\sigma s}_{tks}: Maximum system size for technology t, subdivision k, segments
	 FuelLimit::AxisArray # b^{fa}_{f}: Amount of available fuel for type f [MMBTU]   (NEW)

	 ###  Efficiency Parameters ###
	 ChargeEfficiency::AxisArray  # \eta^{esi}_{bt}: Efficiency of charging storage system b using technology t  [fraction] (NEW)
	 GridChargeEfficiency::Float64   # \eta^{esig}: Efficiency of charging electrical storage using grid power [fraction] (NEW)
     DischargeEfficiency::AxisArray  # \eta^{eso}_{b}: Efficiency of discharging storage system b [fraction] (NEW)
	 # \eta^{bo}: Boiler efficiency [fraction]
	 # \eta^{ecop}: Electric chiller efficiency [fraction]
	 # \eta^{acop}: Absorption chiller efficiency [fraction]

	 ###  Storage Parameters ###   # \ubar{w}^{bkW}_{b}: Minimum power capacity of storage system b (needs to be indexed on b )
     StorageMinChargePcent::Float64     #  \ubar{w}^{mcp}_{b}: Minimum state of charge of storage system b
     InitSOC::Float64    # w^{i}_{b} Initial percent state of charge for storage system b
     StorageMinSizeEnergy::AxisArray     # \bar{w}^{bkWh}_{b}: Maximum energy capacity of storage system b [kWh]
     StorageMaxSizeEnergy::AxisArray     # \ubar{w}^{bkWh}_{b}: Minimum energy capacity of storage system b [kWh]
     StorageMinSizePower::AxisArray     # \bar{w}^{bkW}_{b}: Maximum power capacity of storage system b [kW]
     StorageMaxSizePower::AxisArray     # \ubar{w}^{bkW}_{b}: Minimum power capacity of storage system b [kW]
     StorageMinSOC::AxisArray     #  \ubar{w}^{mcp}_{b}: Minimum state of charge of storage system b [fraction]
     StorageInitSOC::AxisArray  #Initial state of charge of storage system b [fraction]
     StorageCanGridCharge::Bool  # Boolean for storage system [fraction]

	 ###  Fuel Burn Parameters ###
	 FuelBurnSlope::AxisArray # m^\text{fm}_{t}: Fuel burn rate slope parameter for technology t
	 FuelBurnYInt::AxisArray # m^\text{fb}_{t}: Fuel burn rate slope parameter for technology t

	 ### Not used or used for calculation of other parameters ###
	 two_party_factor::Float64
     analysis_years::Int64     # Used to calculate present worth factors maybe?
     AnnualElecLoad::Float64   # Not used anymore (can just sum LoadProfile["1R",h] for all h in TimeStep
     CapCostSegCount::Int64    # Size of set S
     FuelBinCount::Int64       # Size of set F
     DemandBinCount ::Int64    # Size of set E
     DemandMonthsBinCount::Int64   # Size of set N
     TimeStepCount::Int64          # Size of set H
     Points::UnitRange{Int64}      # CapCostSegCount+1; this is going to be the size of set S^{c} now
     PricingTierCount::Int64    # Size of set U

     # new parameters for reformulation
     StorageCostPerKW::AxisArray
     StorageCostPerKWH::AxisArray
	 ElectricDerate::AxisArray

     # New Sets
     SalesTiers::UnitRange
     StorageSalesTiers::Array{Int, 1}
     NonStorageSalesTiers::Array{Int, 1}
	 SalesTiersByTech::AxisArray
	 TechsBySalesTier::AxisArray
	 CurtailmentTiers::Array{Int, 1}

    # Feature Additions
     TechToLocation::AxisArray
     MaxSizesLocation::Array{Float64, 1}
     Location::UnitRange
end

function Parameter(d::Dict)
    can_be_empty = (
        "MaxSize",
        "OMperUnitSize",
        "OMcostPerUnitProd",
        "MaxProdIncent",
        "MaxSizeForProdIncent",
        "TurbineDerate",
        "MinTurndown",
        "pwf_prod_incent",
        "LevelizationFactor",
        "NMILLimits",
        "TechClassMinSize",
		"TechsBySalesTier",
		"SalesTiersByTech",
		"NMILRegime",
		"TechsByNMILRegime"
     )
    if typeof(d["Tech"]) === Array{Any, 1}  # came from Python as empty array
        d["Tech"] = convert(Array{String, 1}, d["Tech"])
    end
    for x in can_be_empty
        if typeof(x) === Array{Any, 1}  # came from Python as empty array
            d[x] = convert(Array{Float64, 1}, d[x])
        end
    end
    d = convert(Dict{Union{String, Symbol}, Any}, d)

    d[:Seg] = 1:d["CapCostSegCount"]
    d[:Points] = 0:d["CapCostSegCount"]
    d[:Month] = 1:12
    d[:Ratchets] = 1:d["NumRatchets"]
    d[:FuelBin] = 1:d["FuelBinCount"]
	d[:PricingTier] = 1:d["PricingTierCount"]
    d[:DemandBin] = 1:d["DemandBinCount"]
    d[:DemandMonthsBin] = 1:d["DemandMonthsBinCount"]
    d[:TimeStep] = 1:d["TimeStepCount"]
    d[:TimeStepBat] = 0:d["TimeStepCount"]
	d[:SalesTiers] = 1:d["SalesTierCount"]
    n_location = length(d["MaxSizesLocation"])
    d[:Location] = 1:n_location

    # the following array manipulation may have to adapt once length(d["Subdivision"]) > 1
    seg_min_size_array = reshape(transpose(reshape(d["SegmentMinSize"], length(d[:Seg]), length(d["Tech"]))),
                                 length(d["Tech"]), length(d["Subdivision"]), length(d[:Seg]))
    seg_max_size_array = reshape(transpose(reshape(d["SegmentMaxSize"], length(d[:Seg]), length(d["Tech"]))),
                                length(d["Tech"]), length(d["Subdivision"]), length(d[:Seg]))

    # convert vectors to AxisArray's with axes for REopt JuMP model
    d["TurbineDerate"] = AxisArray(d["TurbineDerate"], d["Tech"])
    d["TechToLocation"] = vector_to_axisarray(d["TechToLocation"], d["Tech"], d[:Location])
    d["pwf_prod_incent"] = AxisArray(d["pwf_prod_incent"], d["Tech"])
    d["LevelizationFactor"] = AxisArray(d["LevelizationFactor"], d["Tech"])
    d["OMperUnitSize"] = AxisArray(d["OMperUnitSize"], d["Tech"])
    d["CapCostSlope"] = vector_to_axisarray(d["CapCostSlope"], d["Tech"], d[:Seg])
    d["CapCostYInt"] = vector_to_axisarray(d["CapCostYInt"], d["Tech"], d[:Seg])
    d["CapCostX"] = vector_to_axisarray(d["CapCostX"], d["Tech"], d[:Points])
    d["MaxProdIncent"] = AxisArray(d["MaxProdIncent"], d["Tech"])
    d["MaxSizeForProdIncent"] = AxisArray(d["MaxSizeForProdIncent"], d["Tech"])
    d["MaxSize"] = AxisArray(d["MaxSize"], d["Tech"])
    d["TechClassMinSize"] = AxisArray(d["TechClassMinSize"], d["TechClass"])
    d["MinTurndown"] = AxisArray(d["MinTurndown"], d["Tech"])
    d["DemandRates"] = transpose(reshape(d["DemandRates"], d["DemandBinCount"], d["NumRatchets"]))
    d["DemandRatesMonth"] = transpose(reshape(d["DemandRatesMonth"], d["DemandMonthsBinCount"], 12))
    d["NMILLimits"] = AxisArray(d["NMILLimits"], d["NMILRegime"])
    d["TechToNMILMapping"] = vector_to_axisarray(d["TechToNMILMapping"], d["Tech"], d["NMILRegime"])
    d["OMcostPerUnitProd"] = AxisArray(d["OMcostPerUnitProd"], d["Tech"])

    # Reformulation additions
    d["StorageCostPerKW"] = AxisArray([d["StorageCostPerKW"]], d["Storage"])
    d["StorageCostPerKWH"] = AxisArray([d["StorageCostPerKWH"]], d["Storage"])
    d["FuelCost"] = AxisArray(d["FuelCost"], d["FuelType"])
    d["ElecRate"] = transpose(reshape(d["ElecRate"], d["TimeStepCount"], d["PricingTierCount"]))
    d["GridExportRates"] = transpose(reshape(d["GridExportRates"], d["TimeStepCount"], d["SalesTierCount"]))
    d["FuelBurnSlope"] = AxisArray(d["FuelBurnSlope"], d["FuelBurningTechs"])
    d["FuelBurnYInt"] = AxisArray(d["FuelBurnYInt"], d["FuelBurningTechs"])
    d["ProductionFactor"] = vector_to_axisarray(d["ProductionFactor"], d["Tech"], d[:TimeStep])
    d["ProductionIncentiveRate"] = AxisArray(d["ProductionIncentiveRate"], d["Tech"])
    d["FuelLimit"] = AxisArray(d["FuelLimit"], d["FuelType"])
    d["ChargeEfficiency"] = vector_to_axisarray(d["ChargeEfficiency"], d["Tech"], d["Storage"])
    d["DischargeEfficiency"] = AxisArray(d["DischargeEfficiency"], d["Storage"])
    d["StorageMinSizeEnergy"] = AxisArray([d["StorageMinSizeEnergy"]], d["Storage"])
    d["StorageMaxSizeEnergy"] = AxisArray([d["StorageMaxSizeEnergy"]], d["Storage"])
    d["StorageMinSizePower"] = AxisArray([d["StorageMinSizePower"]], d["Storage"])
    d["StorageMaxSizePower"] = AxisArray([d["StorageMaxSizePower"]], d["Storage"])
    d["StorageMinSOC"] = AxisArray([d["StorageMinSOC"]], d["Storage"])
    d["StorageInitSOC"] = AxisArray([d["StorageInitSOC"]], d["Storage"])
    d["SegmentMinSize"] = AxisArray(seg_min_size_array, d["Tech"], d["Subdivision"], d[:Seg])
    d["SegmentMaxSize"] = AxisArray(seg_max_size_array, d["Tech"], d["Subdivision"], d[:Seg])
	d["ElectricDerate"] = vector_to_axisarray(d["ElectricDerate"], d["Tech"], d[:TimeStep])
    d["MaxGridSales"] = [d["MaxGridSales"]]

    # Indexed Sets
    if isempty(d["FuelType"])
        d["TechsByFuelType"] = []  # array of arrays is not empty, but needs to be for AxisArray conversion
    end
    d["SegByTechSubdivision"] = vector_to_axisarray(d["SegByTechSubdivision"], d["Subdivision"], d["Tech"])
    d["TechsByFuelType"] = AxisArray(d["TechsByFuelType"], d["FuelType"])
    d["TechsInClass"] = AxisArray(d["TechsInClass"], d["TechClass"])
	d["SalesTiersByTech"] = AxisArray(d["SalesTiersByTech"], d["Tech"])
	d["TechsBySalesTier"] = AxisArray(d["TechsBySalesTier"], d[:SalesTiers])
	d["TechsByNMILRegime"] = AxisArray(d["TechsByNMILRegime"], d["NMILRegime"])

    d = string_dictkeys_tosymbols(d)
    d = filter_dict_to_match_struct_field_names(d, Parameter)
    param = Parameter(;d...)
end

# Additional dispatches to make things easier
function length(::Symbol)
    return 1
end

JuMP.value(::Val{false}) = 0

JuMP.value(x::Number) = x  # for @expression(REopt, ExportBenefitYr1, 0.0) and similar

JuMP.value(::MutableArithmetics.Zero) = 0

function vector_to_axisarray(v::Array{<:Any, 1}, ax1::Array{<:Any, 1}, ax2::Union{UnitRange, Array{<:Any, 1}})
    l1 = length(ax1)
    l2 = length(ax2)
    a = transpose(reshape(v, l2, l1))
    return AxisArray(a, ax1, ax2)
end


"""
```
splitNode!(network::DislocationNetwork, splitNode, splitConnect, midCoord, midVel)
```
Splits node `splitNode` along connection `splitConnect`, puts it at coordinate `midCoord` with velocity `midVel`. If it is called from within [`refineNetwork!`](@ref), `midCoord` and `midVel` are the coordinate between `splitNode` and the node connected to it via `splitConnect` and gives it the mean velocity of the two nodes.
"""
function splitNode!(network::DislocationNetwork, splitNode, splitConnect, midCoord, midVel)
    elemT = eltype(network.bVec)
    # newNode gets inserted between splitNode and the node it is connected to via the connection splitConnect. We want to take this connection and remove it from splitNode. We then connect splitNode to newNode. Then we assign splitConnect to newNode so that it can connect to the node splitNode used to be connected to, this way we close the loop and the connections move from splitNode -> other, to splitNode -> newNode -> other. We copy the connection to a temporary variable, guaranteeing the data isn't modified by removeConnection!().
    tmpConnect = network.connectivity[:, splitNode]
    # Remove connection from node in preparation of adding the new node.
    removeConnection!(network, splitNode, splitConnect)

    # New node created at the very end.
    network.numNode[1] += 1
    newNode = network.numNode[1]

    # Allocate memory.
    if newNode > length(network.label)
        # A good heuristic for memory allocation is to allocate an extra N log₂(N) entries.
        numNewEntries = Int(round(newNode * log2(newNode)))
        network = DislocationNetwork(;
            links = network.links,
            slipPlane = network.slipPlane,
            bVec = network.bVec,
            coord = hcat(
                network.coord,
                zeros(elemT, size(network.coord, 1), numNewEntries),
            ),
            label = vcat(network.label, zeros(nodeTypeDln, numNewEntries)),
            nodeVel = hcat(
                network.nodeVel,
                zeros(elemT, size(network.nodeVel, 1), numNewEntries),
            ),
            nodeForce = hcat(
                network.nodeForce,
                zeros(elemT, size(network.nodeForce, 1), numNewEntries),
            ),
            numNode = network.numNode,
            numSeg = network.numSeg,
            maxConnect = network.maxConnect,
            connectivity = hcat(
                network.connectivity,
                zeros(Int, size(network.connectivity, 1), numNewEntries),
            ),
            linksConnect = network.linksConnect,
            extSeg = network.extSeg,
            segIdx = network.segIdx,
            segForce = network.segForce,
        )
    end

    links = network.links
    bVec = network.bVec
    network.coord[:, newNode] = midCoord
    network.label[newNode] = 1
    network.nodeVel[:, newNode] = midVel
    connectivity = network.connectivity
    linksConnect = network.linksConnect

    # Create connectivity for the new node and update links and linksConnect.
    # This only does the first connection. The rest are made after in case they are needed.
    connectivity[1, newNode] = 1
    connectivity[2, newNode] = tmpConnect[(2 * splitConnect)]
    connectivity[3, newNode] = tmpConnect[(2 * splitConnect + 1)]

    link = connectivity[2, newNode]
    colLink = connectivity[3, newNode]
    links[colLink, link] = newNode
    linksConnect[colLink, link] = 1

    # Check if we need a new link between newNode and splitNode for Burgers vector conservation.
    b = SVector{3, elemT}(bVec[1, link], bVec[2, link], bVec[3, link])
    # If burgers vector is conserved return.
    b ⋅ b == 0 && return network

    # New link created at the end.
    network.numSeg[1] += 1
    newSeg = network.numSeg[1]

    # Allocate memory.
    if newSeg > size(network.links, 2)
        # A good heuristic for memory allocation is to allocate an extra N log₂(N) entries.
        numNewEntries = Int(round(newSeg * log2(newSeg)))

        network = DislocationNetwork(;
            links = hcat(network.links, zeros(Int, size(network.links, 1), numNewEntries)),
            slipPlane = hcat(
                network.slipPlane,
                zeros(elemT, size(network.slipPlane, 1), numNewEntries),
            ),
            bVec = hcat(network.bVec, zeros(elemT, size(network.bVec, 1), numNewEntries)),
            coord = network.coord,
            label = network.label,
            nodeVel = network.nodeVel,
            nodeForce = network.nodeForce,
            numNode = network.numNode,
            numSeg = network.numSeg,
            maxConnect = network.maxConnect,
            connectivity = network.connectivity,
            linksConnect = hcat(
                network.linksConnect,
                zeros(Int, size(network.linksConnect, 1), numNewEntries),
            ),
            extSeg = vcat(network.extSeg, zeros(Bool, numNewEntries)),
            segIdx = vcat(network.segIdx, zeros(Int, numNewEntries, 3)),
            segForce = cat(
                network.segForce,
                zeros(elemT, size(network.segForce, 1), 2, numNewEntries),
                dims = 3,
            ),
        )
    end

    bVec = network.bVec
    links = network.links
    slipPlane = network.slipPlane
    connectivity = network.connectivity
    linksConnect = network.linksConnect

    # Add new link to network.
    bVec[:, newSeg] = b
    links[colLink, newSeg] = splitNode  # First node in the link is the node that was split.
    colOppLink = 3 - colLink            # Other column in links.
    links[colOppLink, newSeg] = newNode # Second node of the link is the new node.

    # Update connectivity of splitNode.
    newConnect1 = connectivity[1, splitNode] + 1
    connectivity[1, splitNode] = newConnect1
    connectivity[(2 * newConnect1):(2 * newConnect1 + 1), splitNode] .= (newSeg, colLink)

    # Update connectivity of newNode.
    newConnect2 = connectivity[1, newNode] + 1
    connectivity[1, newNode] = newConnect2
    connectivity[(2 * newConnect2):(2 * newConnect2 + 1), newNode] .= (newSeg, colOppLink)

    # Update linksConnect.
    linksConnect[colLink, newSeg] = newConnect1
    linksConnect[colOppLink, newSeg] = newConnect2

    # Fix slip plane.
    # Line direction and velocity of the resultant dislocation.
    coord = network.coord
    nodeVel = network.nodeVel

    # WARNING This calculation's dodgy. Try using the cross product of the adjacent segments.
    t = SVector{3, elemT}(
        coord[1, splitNode] - coord[1, newNode],
        coord[2, splitNode] - coord[2, newNode],
        coord[3, splitNode] - coord[3, newNode],
    )

    v = SVector{3, elemT}(
        nodeVel[1, splitNode] + nodeVel[1, newNode],
        nodeVel[2, splitNode] + nodeVel[2, newNode],
        nodeVel[3, splitNode] + nodeVel[3, newNode],
    )

    # Potential new slip plane.
    n1 = t × b  # For non-screw segments.
    n2 = t × v  # For screw segments.
    if n1 ⋅ n1 > eps(elemT) # non-screw
        slipPlane[:, newSeg] = n1 / norm(n1)
    elseif n2 ⋅ n2 > eps(elemT) # screw
        slipPlane[:, newSeg] = n2 / norm(n2)
    end

    return network
end

"""
```
refineNetwork!(
    dlnParams::DislocationParameters,
    matParams::MaterialParameters,
    mesh::AbstractMesh,
    forceDisplacement::ForceDisplacement,
    network::DislocationNetwork,
)
```
Refines a dislocation network to ensure all the segments are shorter than the maximum allowable length and so no two links form a triangle with an area over the maximum allowed.
"""
function refineNetwork!(
    dlnParams::DislocationParameters,
    matParams::MaterialParameters,
    mesh::AbstractMesh,
    forceDisplacement::ForceDisplacement,
    network::DislocationNetwork,
)
    maxAreaSq = dlnParams.maxAreaSq
    maxSegLen = dlnParams.maxSegLen
    twoMinSegLen = dlnParams.twoMinSegLen

    links = network.links
    coord = network.coord
    label = network.label
    numNode = network.numNode[1]
    nodeVel = network.nodeVel
    connectivity = network.connectivity

    elemT = eltype(network.coord)

    @inbounds for i in 1:numNode
        if connectivity[1, i] == 2 && label[i] == intMobDln
            link1, missing, link1_nodeOppI = findConnectedNode(network, i, 1) # First connection.
            link2, missing, link2_nodeOppI = findConnectedNode(network, i, 2) # Second connection.

            # Create triangle formed by the node and its two links.
            iCoord = SVector{3, elemT}(coord[1, i], coord[2, i], coord[3, i])
            # Side 1
            coordVec1 =
                SVector{3, elemT}(
                    coord[1, link1_nodeOppI],
                    coord[2, link1_nodeOppI],
                    coord[3, link1_nodeOppI],
                ) - iCoord
            # Side 2
            coordVec2 =
                SVector{3, elemT}(
                    coord[1, link2_nodeOppI],
                    coord[2, link2_nodeOppI],
                    coord[3, link2_nodeOppI],
                ) - iCoord
            # Side 3 (close the triangle)
            coordVec3 = coordVec2 - coordVec1
            # Side lengths.
            r1 = norm(coordVec1)
            r2 = norm(coordVec2)
            r3 = norm(coordVec3)

            # Half the triangle's perimeter.
            r0 = (r1 + r2 + r3) / 2
            # Heron's formula for the area of a triangle when we know its sides.
            areaSq = r0 * (r0 - r1) * (r0 - r2) * (r0 - r3)

            # Check if we have to split the second link.
            # Split node the area is greater than the maximum area and if by splitting we would not create two segments smaller than the minimum allowed and if the node is in the simulation. Or if r2 is longer than the maximum length allowed.
            if (areaSq > maxAreaSq && r2 >= twoMinSegLen && link2_nodeOppI <= numNode) ||
               r2 > maxSegLen
                midCoord =
                    SVector{3, elemT}(
                        coord[1, i] + coord[1, link2_nodeOppI],
                        coord[2, i] + coord[2, link2_nodeOppI],
                        coord[3, i] + coord[3, link2_nodeOppI],
                    ) / 2

                midVel =
                    SVector{3, elemT}(
                        nodeVel[1, i] + nodeVel[1, link2_nodeOppI],
                        nodeVel[2, i] + nodeVel[2, link2_nodeOppI],
                        nodeVel[3, i] + nodeVel[3, link2_nodeOppI],
                    ) / 2

                network = splitNode!(network, i, 2, midCoord, midVel)
                getSegmentIdx!(network)
                links = network.links
                slipPlane = network.slipPlane
                coord = network.coord
                label = network.label
                nodeVel = network.nodeVel
                connectivity = network.connectivity

                newNode = network.numNode[1]
                newLink = network.numSeg[1]

                equalSlipPlane = let
                    flag = true
                    @inbounds @simd for j in 1:3
                        flag = flag && slipPlane[j, link2] ≈ slipPlane[j, link1]
                    end
                    flag
                end
                if equalSlipPlane
                    @inbounds @simd for j in 1:3
                        slipPlane[j, newLink] = slipPlane[j, link2]
                    end
                end

                # Calculate force and mobility for the new node's connectivity.
                j = 1:connectivity[1, newNode]
                link, missing, oldNode = findConnectedNode(network, newNode, j) # j connections.

                # Calculate segment force for segment link.
                calcSegForce!(dlnParams, matParams, mesh, forceDisplacement, network, link)

                # Calculate old node velocity.
                dlnMobility!(dlnParams, matParams, network, oldNode)
                nodeVel = network.nodeVel

                # Calculate new node velocity.
                dlnMobility!(dlnParams, matParams, network, newNode)
                nodeVel = network.nodeVel
            end

            # Check if we have to split the first link.
            # Split node the area is greater than the maximum area and if by splitting we would not create two segments smaller than the minimum allowed and if the node is in the simulation. Or if r1 is longer than the maximum length allowed.
            if (areaSq > maxAreaSq && r1 >= twoMinSegLen && link1_nodeOppI <= numNode) ||
               r1 > maxSegLen
                midCoord =
                    SVector{3, elemT}(
                        coord[1, i] + coord[1, link1_nodeOppI],
                        coord[2, i] + coord[2, link1_nodeOppI],
                        coord[3, i] + coord[3, link1_nodeOppI],
                    ) / 2

                midVel =
                    SVector{3, elemT}(
                        nodeVel[1, i] + nodeVel[1, link1_nodeOppI],
                        nodeVel[2, i] + nodeVel[2, link1_nodeOppI],
                        nodeVel[3, i] + nodeVel[3, link1_nodeOppI],
                    ) / 2

                network = splitNode!(network, i, 1, midCoord, midVel)
                getSegmentIdx!(network)
                links = network.links
                slipPlane = network.slipPlane
                coord = network.coord
                label = network.label
                nodeVel = network.nodeVel
                connectivity = network.connectivity

                newNode = network.numNode[1]
                newLink = network.numSeg[1]

                equalSlipPlane = let
                    flag = true
                    @inbounds @simd for j in 1:3
                        flag = flag && slipPlane[j, link1] ≈ slipPlane[j, link2]
                    end
                    flag
                end
                if equalSlipPlane
                    @inbounds @simd for j in 1:3
                        slipPlane[j, newLink] = slipPlane[j, link1]
                    end
                end

                # Calculate force and mobility for the new node's connectivity.
                j = 1:connectivity[1, newNode]
                link, oldNode = findConnectedNode(network, newNode, j) # j connections.

                # Calculate segment force for segment link.
                calcSegForce!(dlnParams, matParams, mesh, forceDisplacement, network, link)

                # Calculate old node velocity.
                dlnMobility!(dlnParams, matParams, network, oldNode)
                nodeVel = network.nodeVel

                # Calculate new node velocity.
                dlnMobility!(dlnParams, matParams, network, newNode)
                nodeVel = network.nodeVel
            end

        elseif connectivity[1, i] > 2 && label[i] == intMobDln
            # Loop through the connections of node i.
            for j in 1:connectivity[1, i]
                link, missing, link_nodeOpp = findConnectedNode(network, i, j) # j connection.

                t = SVector{3, elemT}(
                    coord[1, link_nodeOpp] - coord[1, i],
                    coord[2, link_nodeOpp] - coord[2, i],
                    coord[3, link_nodeOpp] - coord[3, i],
                )
                r1 = norm(t)

                # If the link is smaller than the maximum segment length we skip to the next iteration.
                r1 < maxSegLen && continue

                midCoord =
                    SVector{3, elemT}(
                        coord[1, i] + coord[1, link_nodeOpp],
                        coord[2, i] + coord[2, link_nodeOpp],
                        coord[3, i] + coord[3, link_nodeOpp],
                    ) / 2

                midVel =
                    SVector{3, elemT}(
                        nodeVel[1, i] + nodeVel[1, link_nodeOpp],
                        nodeVel[2, i] + nodeVel[2, link_nodeOpp],
                        nodeVel[3, i] + nodeVel[3, link_nodeOpp],
                    ) / 2

                network = splitNode!(network, i, j, midCoord, midVel)
                getSegmentIdx!(network)
                links = network.links
                slipPlane = network.slipPlane
                coord = network.coord
                label = network.label
                nodeVel = network.nodeVel
                connectivity = network.connectivity

                newNode = network.numNode[1]
                newLink = network.numSeg[1]

                @inbounds @simd for k in 1:3
                    slipPlane[k, newLink] = slipPlane[k, link]
                end

                # Calculate force and mobility for the new node's connectivity.
                k = 1:connectivity[1, newNode]
                link, missing, oldNode = findConnectedNode(network, newNode, k) # k connections.

                # Calculate segment force for segment link.
                calcSegForce!(dlnParams, matParams, mesh, forceDisplacement, network, link)
                # Calculate old node velocity.
                dlnMobility!(dlnParams, matParams, network, oldNode)
                nodeVel = network.nodeVel

                # Calculate new node velocity.
                dlnMobility!(dlnParams, matParams, network, newNode)
                nodeVel = network.nodeVel
            end
        end
    end
    return network
end


import GR

j = [0.19454, 0.1194, 0.19454, 0.41129, 0.10807, 0.3875, 0.10807, 0.07711, 0.10807, 0.06075, 0.10681, 0.04973, 0.1043, 0.04407, 0.10178, 0.0384, 0.09813, 0.03557, 0.09335, 0.03557, 0.09108, 0.03557, 0.08856, 0.03645, 0.08579, 0.03822, 0.08303, 0.03998, 0.07963, 0.0435, 0.0756, 0.04879, 0.07207, 0.05357, 0.06761, 0.05842, 0.06219, 0.06333, 0.05678, 0.06824, 0.04954, 0.07069, 0.04048, 0.07069, 0.0284, 0.07069, 0.01877, 0.06767, 0.0116, 0.06163, 0.00442, 0.05559, 0.00083, 0.04816, 0.00083, 0.03935, 0.00083, 0.02878, 0.00725, 0.01971, 0.02009, 0.01216, 0.03293, 0.00461, 0.05143, 0.00083, 0.0756, 0.00083, 0.09347, 0.00083, 0.10971, 0.00228, 0.12431, 0.00518, 0.13891, 0.00807, 0.15143, 0.0138, 0.16188, 0.02236, 0.17233, 0.03092, 0.18038, 0.04294, 0.18605, 0.05842, 0.19171, 0.0739, 0.19454, 0.09423, 0.19454, 0.1194]

u = [0.31464, 0.40298, 0.22855, 0.40298, 0.22855, 0.20323, 0.22855, 0.1909, 0.23113, 0.17932, 0.23629, 0.16849, 0.24145, 0.15767, 0.24856, 0.14823, 0.25763, 0.14017, 0.26669, 0.13212, 0.27726, 0.12576, 0.28935, 0.1211, 0.30143, 0.11644, 0.31452, 0.11412, 0.32862, 0.11412, 0.3407, 0.11412, 0.35316, 0.11701, 0.366, 0.1228, 0.37884, 0.12859, 0.39092, 0.13627, 0.40225, 0.14583, 0.40225, 0.1194, 0.48834, 0.1194, 0.48834, 0.40298, 0.40225, 0.40298, 0.40225, 0.19832, 0.3957, 0.18825, 0.38803, 0.17988, 0.37922, 0.17321, 0.3704, 0.16654, 0.36273, 0.1632, 0.35618, 0.1632, 0.35039, 0.1632, 0.34498, 0.16427, 0.33994, 0.16641, 0.33491, 0.16855, 0.3305, 0.17139, 0.32673, 0.17491, 0.32295, 0.17843, 0.31999, 0.18265, 0.31785, 0.18756, 0.31571, 0.19247, 0.31464, 0.19769, 0.31464, 0.20323]

l = [0.60797, 0.1194, 0.60797, 0.54081, 0.52225, 0.51702, 0.52225, 0.1194]

i = [0.64226, 0.3875, 0.64226, 0.1194, 0.72835, 0.1194, 0.72835, 0.41129]

a = [0.91001, 0.26591, 0.91001, 0.18397, 0.9012, 0.17743, 0.89321, 0.17201, 0.88604, 0.16773, 0.87886, 0.16345, 0.87175, 0.16132, 0.8647, 0.16132, 0.86118, 0.16132, 0.8579, 0.16238, 0.85488, 0.16452, 0.85186, 0.16666, 0.84909, 0.1695, 0.84658, 0.17302, 0.84406, 0.17655, 0.84211, 0.18076, 0.84072, 0.18567, 0.83934, 0.19058, 0.83865, 0.19568, 0.83865, 0.20096, 0.83865, 0.20776, 0.84079, 0.21431, 0.84507, 0.2206, 0.84934, 0.22689, 0.85495, 0.23281, 0.86187, 0.23835, 0.86879, 0.24388, 0.87647, 0.24898, 0.8849, 0.25364, 0.89334, 0.2583, 0.90171, 0.26239, 0.91001, 0.26591, 0.99648, 0.1194, 0.99648, 0.33199, 0.99648, 0.34382, 0.99422, 0.35452, 0.98969, 0.36409, 0.98516, 0.37365, 0.97811, 0.38184, 0.96854, 0.38863, 0.95898, 0.39543, 0.94683, 0.40065, 0.9321, 0.4043, 0.91738, 0.40795, 0.89982, 0.40978, 0.87943, 0.40978, 0.86281, 0.40978, 0.84708, 0.40802, 0.83223, 0.40449, 0.81737, 0.40097, 0.80428, 0.39618, 0.79296, 0.39014, 0.78163, 0.3841, 0.77263, 0.37686, 0.76596, 0.36843, 0.75929, 0.36, 0.75595, 0.35087, 0.75595, 0.34105, 0.75595, 0.33048, 0.75973, 0.32173, 0.76728, 0.31481, 0.77483, 0.30789, 0.78465, 0.30443, 0.79673, 0.30443, 0.80454, 0.30443, 0.81102, 0.30556, 0.81618, 0.30783, 0.82134, 0.31009, 0.8253, 0.31311, 0.82807, 0.31689, 0.83084, 0.32066, 0.83279, 0.32507, 0.83393, 0.3301, 0.83506, 0.33514, 0.83563, 0.3403, 0.83563, 0.34559, 0.83563, 0.3549, 0.83827, 0.36283, 0.84356, 0.36937, 0.84884, 0.37592, 0.85828, 0.37919, 0.87188, 0.37919, 0.88346, 0.37919, 0.89271, 0.37542, 0.89963, 0.36786, 0.90655, 0.36031, 0.91001, 0.34747, 0.91001, 0.32935, 0.91001, 0.30141, 0.90057, 0.29914, 0.88899, 0.29562, 0.87729, 0.2919, 0.86546, 0.288, 0.85362, 0.2841, 0.84223, 0.27982, 0.83128, 0.27516, 0.82033, 0.27051, 0.81007, 0.26541, 0.80051, 0.25987, 0.79094, 0.25433, 0.78257, 0.24804, 0.7754, 0.24099, 0.76822, 0.23394, 0.76256, 0.22601, 0.75841, 0.2172, 0.75425, 0.20839, 0.75217, 0.19857, 0.75217, 0.18775, 0.75217, 0.17717, 0.75413, 0.16742, 0.75803, 0.15848, 0.76193, 0.14955, 0.76753, 0.14181, 0.77483, 0.13526, 0.78213, 0.12872, 0.791, 0.12355, 0.80145, 0.11978, 0.8119, 0.116, 0.82367, 0.11411, 0.83676, 0.11411, 0.84632, 0.11411, 0.85463, 0.11481, 0.86168, 0.11619, 0.86873, 0.11758, 0.87508, 0.11953, 0.88075, 0.12204, 0.88641, 0.12456, 0.89151, 0.12752, 0.89604, 0.13092, 0.90057, 0.13432, 0.90523, 0.13803, 0.91001, 0.14206, 0.91001, 0.1194]

function draw_path(p, codes, fill, border)
  GR.setfillcolorind(fill)
  GR.setbordercolorind(border)
  op = border == 0 ? 'f' : 'F'
  GR.path(p[1:2:end], p[2:2:end], codes)
end

function circle(xmin, ymin, xmax, ymax, fill, border)
  GR.setfillintstyle(4)
  GR.setfillcolorind(fill)
  GR.setbordercolorind(border)
  GR.fillarc(xmin, xmax, ymin, ymax, 0, 360)
end

GR.setcolorrep(1, 0.14, 0.14, 0.14)
GR.setcolorrep(2, 0.251, 0.388, 0.847) # darker blue
GR.setcolorrep(3, 0.796, 0.235, 0.2)   # darker red
GR.setcolorrep(4, 0.584, 0.345, 0.698) # darker purple
GR.setcolorrep(5, 0.22, 0.596, 0.149)  # darker green
GR.setcolorrep(6, 0.4, 0.51, 0.878)    # lighter blue
GR.setcolorrep(7, 0.835, 0.388, 0.361) # lighter red
GR.setcolorrep(8, 0.667, 0.475, 0.757) # lighter purple
GR.setcolorrep(9, 0.376, 0.678, 0.318) # lighter green

GR.setviewport(0, 1, 0, 1)
GR.setwindow(0, 1, 0, 1)
GR.updatews()

for s in 0.1:0.01:0.5
  GR.clearws()
  GR.setviewport(0.5 - s, 0.5 + s, 0.5 - s, 0.5 + s)
  GR.setborderwidth(s * 8)

  draw_path(j, "MLLLCCCCCCCCCCCCCCf", 1, 0)
  draw_path(u, "MLLCCCCCCLLLLLCCCCCCf", 1, 0)
  draw_path(l, "MLLLf", 1, 0)
  draw_path(i, "MLLLf", 1, 0)
  draw_path(a, "MLCCCCCCCCCCZMLCCCCCCCCCCCCCCCCCCLLCCCCCCCCCCCCCCLf", 1, 0)

  circle(0.09698, 0.43274, 0.20364, 0.5394, 2, 6)
  circle(0.63794, 0.43274, 0.7446, 0.5394, 3, 7)
  circle(0.70794, 0.55274, 0.8146, 0.6594, 4, 8)
  circle(0.77794, 0.43274, 0.8846, 0.5394, 5, 9)

  GR.updatews()

  sleep(0.01)
end


module Brushes

using ..Ahorn, Maple

displayName = "Brushes"
group = "Brushes"

drawingLayers = Ahorn.Layer[]

toolsLayer = nothing
targetLayer = nothing
material = '0'

const brushes = Ahorn.Brush[
    Ahorn.Brush(
        "Pencil",
        hcat(1)
    ),

    Ahorn.Brush(
        "Dither",
        [
            1 0;
            0 1
        ]
    ),
    Ahorn.Brush(
        "Ahorn",
        [
            0 0 1 0 0 0 0;
            0 1 0 1 0 0 0;
            0 0 0 1 1 0 1;
            0 0 1 1 1 0 0;
            0 1 1 1 1 1 0;
            0 1 1 1 1 1 0;
            1 1 1 1 1 1 0;
            1 1 1 1 1 0 0;
            0 1 1 1 0 0 0;
        ]
    )
]

selectedBrush = brushes[1]

hoveringBrush = nothing
const phantomBrushes = Dict{Tuple{Integer, Integer}, Ahorn.Brush}()

lastX, lastY = nothing, nothing

function applyPhantomBrushes()
    if !isempty(phantomBrushes)
        Ahorn.History.addSnapshot!(Ahorn.History.RoomSnapshot("Brush ($(selectedBrush.name), $material)", Ahorn.loadedState.room))
    end

    for (pos, brush) in phantomBrushes
        x, y = pos

        Maple.updateTileSize!(Ahorn.loadedState.room, '0', '0')
        roomTiles = Ahorn.roomTiles(targetLayer, Ahorn.loadedState.room)
        Ahorn.applyBrush!(brush, roomTiles, material, x, y)
    end

    if !isempty(phantomBrushes)
        empty!(phantomBrushes)

        Ahorn.redrawLayer!(targetLayer)
        Ahorn.redrawLayer!(toolsLayer)
    end
end

function drawBrushes(layer::Ahorn.Layer, room::Ahorn.DrawableRoom, camera::Ahorn.Camera)
    if !isa(hoveringBrush, Nothing)
        x, y, brush = hoveringBrush

        Ahorn.drawBrush(brush, layer, x, y)
    end

    # Make sure we should actually render the phantom brushes
    if Ahorn.mouseButtonHeld(0x1)
        for (pos, brush) in phantomBrushes
            x, y = pos

            Ahorn.drawBrush(brush, layer, x, y)
        end
    end
end

function addPhantomBrush(x::Int, y::Int, selectedBrush::Ahorn.Brush)
    if !haskey(phantomBrushes, (x, y))
        phantomBrushes[(x, y)] = deepcopy(selectedBrush)

        return true
    end

    return false
end

function addPhantomBrush(points::Array{Tuple{Int, Int}, 1}, selectedBrush::Ahorn.Brush)
    res = false

    for (x, y) in points
        res |= addPhantomBrush(x, y, selectedBrush)
    end

    return res
end

function addBrushOffset(bx, bw, ox, box, by, bh, oy, boy)
    return div(bx, bw) * bw + ox - box + 1, div(by, bh) * bh + oy - boy + 1
end

function cleanup()
    global hoveringBrush = nothing
    empty!(phantomBrushes)

    Ahorn.redrawLayer!(toolsLayer)
end

function setMaterials!(layer::Ahorn.Layer)
    tileNames = Ahorn.tileNames(layer)
    displayNames = Ahorn.getBrushMaterialsNames(layer, get(Ahorn.config, "sort_brush_tiles_by_name", true))

    Ahorn.setMaterialList!(displayNames, row -> row[1] == get(tileNames, material, nothing))
end

function mouseMotion(x::Number, y::Number)
    if hoveringBrush === nothing || x != hoveringBrush[1] || y != hoveringBrush[2]
        global hoveringBrush = (x, y, deepcopy(selectedBrush))

        Ahorn.redrawLayer!(toolsLayer)
    end
end

function middleClick(x::Number, y::Number)
    tiles = Ahorn.roomTiles(targetLayer, Ahorn.loadedState.room)
    tileNames = Ahorn.tileNames(targetLayer)
    target = get(tiles.data, (y, x), '0')

    global material = target
    layerName = Ahorn.layerName(targetLayer)
    Ahorn.persistence["brushes_material_$(layerName)"] = material
    Ahorn.selectMaterialList!(tileNames[target])
end

function leftClick(x::Number, y::Number)
    layer = Ahorn.layerName(targetLayer)
    Ahorn.History.addSnapshot!(Ahorn.History.RoomSnapshot("Brush $(selectedBrush.name)", Ahorn.loadedState.room))

    Maple.updateTileSize!(Ahorn.loadedState.room, '0', '0')
    roomTiles = Ahorn.roomTiles(targetLayer, Ahorn.loadedState.room)
    Ahorn.applyBrush!(selectedBrush, roomTiles, material, x, y)

    Ahorn.redrawLayer!(targetLayer)
end

function selectionMotion(x1::Number, y1::Number, x2::Number, y2::Number)
    box, boy = selectedBrush.offset

    startX = x1
    startY = y1

    pixels = rotr90(selectedBrush.pixels, selectedBrush.rotation)

    bh, bw = size(pixels)
    ox, oy = mod(startX, bw), mod(startY, bh)

    bx, by = addBrushOffset(x2, bw, ox, box, y2, bw, oy, boy)

    smoothWithLines = get(Ahorn.config, "tools_brushes_smoother_brushes", true)
    redraw = false

    if smoothWithLines && lastX !== nothing && lastY !== nothing
        pointsRaw = Ahorn.pointsOnLine(Ahorn.Line(x2, y2, lastX, lastY))
        points = Tuple{Int, Int}[]

        for (x, y) in pointsRaw
            push!(points, addBrushOffset(x, bw, ox, box, y, bw, oy, boy))
        end

        redraw = addPhantomBrush(points, selectedBrush)

    else
        redraw = addPhantomBrush(bx, by, selectedBrush)
    end

    if redraw
        Ahorn.redrawLayer!(toolsLayer)
    end

    global lastX, lastY = x2, y2
end

function selectionFinish(rect::Ahorn.Rectangle)
    global lastX, lastY = nothing, nothing

    applyPhantomBrushes()
end

function toolSelected(subTools::Ahorn.ListContainer, layers::Ahorn.ListContainer, materials::Ahorn.ListContainer)
    layerName = Ahorn.layerName(targetLayer)
    tileNames = Ahorn.tileNames(targetLayer)
    global material = get(Ahorn.persistence, "brushes_material_$(layerName)", tileNames["Air"])[1]

    wantedBrush = get(Ahorn.persistence, "brushes_brushes_brush", brushes[1].name)
    Ahorn.updateTreeView!(subTools, [brush.name for brush in brushes], row -> row[1] == wantedBrush)

    wantedLayer = get(Ahorn.persistence, "brushes_layer", "fgTiles")
    Ahorn.updateLayerList!(["fgTiles", "bgTiles"], row -> row[1] == wantedLayer)

    Ahorn.redrawingFuncs["tools"] = drawBrushes
    Ahorn.redrawLayer!(toolsLayer)
end

function layerSelected(list::Ahorn.ListContainer, materials::Ahorn.ListContainer, selected::String)
    global targetLayer = Ahorn.getLayerByName(drawingLayers, selected)
    Ahorn.persistence["brushes_layer"] = selected

    tileNames = Ahorn.tileNames(targetLayer)
    layerName = Ahorn.layerName(targetLayer)
    global material = get(Ahorn.persistence, "brushes_material_$(layerName)", tileNames["Air"])[1]
    setMaterials!(targetLayer)
end

function materialSelected(list::Ahorn.ListContainer, selected::String)
    tileNames = Ahorn.tileNames(targetLayer)
    layerName = Ahorn.layerName(targetLayer)
    Ahorn.persistence["brushes_material_$(layerName)"] = tileNames[selected]
    global material = tileNames[selected]
end

function materialFiltered(list::Ahorn.ListContainer)
    tileNames = Ahorn.tileNames(targetLayer)
    Ahorn.selectRow!(list, row -> row[1] == get(tileNames, material, '0'))
end

function getFavorites()
    if targetLayer !== nothing
        key = "favorites_brushes_$(targetLayer.name)"

        return Ahorn.getFavorites(Ahorn.persistence, key)
    end

    return []
end

function materialDoubleClicked(material::String)
    if targetLayer !== nothing
        key = "favorites_brushes_$(targetLayer.name)"
        tileNames = Ahorn.tileNames(targetLayer)

        Ahorn.toggleFavorite(Ahorn.persistence, key, material)
        setMaterials!(targetLayer)
        Ahorn.updateMaterialFilter!(targetLayer.name)
    end
end

function layersChanged(layers::Array{Ahorn.Layer, 1})
    wantedLayer = get(Ahorn.persistence, "brushes_layer", "fgTiles")

    global drawingLayers = layers
    global toolsLayer = Ahorn.getLayerByName(layers, "tools")
    global targetLayer = Ahorn.selectLayer!(layers, wantedLayer, "fgTiles")
end

function subToolSelected(list::Ahorn.ListContainer, selected::String)
    for brush in brushes
        if brush.name == selected
            Ahorn.persistence["brushes_brushes_brush"] = selected
            global selectedBrush = brush
        end
    end
end

function keyboard(event::Ahorn.eventKey)
    shouldRedraw = false

    if event.keyval == Ahorn.keyval("l")
        selectedBrush.rotation = mod(selectedBrush.rotation - 1, 4)

        shouldRedraw |= true

    elseif event.keyval == Ahorn.keyval("r")
        selectedBrush.rotation = mod(selectedBrush.rotation + 1, 4)

        shouldRedraw |= true
    end

    if shouldRedraw && hoveringBrush !== nothing
        global hoveringBrush = (hoveringBrush[1], hoveringBrush[2], deepcopy(selectedBrush))

        Ahorn.redrawLayer!(toolsLayer)
    end
end

end