function converttopartitionedbernoullidbm(mdbm::MultimodalDBM)
   Vector{Union{BernoulliRBM, PartitionedRBM{BernoulliRBM}}}(mdbm)
end

"""
    addlayer!(dbm, x)
Adds a pretrained layer to the BasicDBM `dbm`, given the dataset `x` as input
for the visible layer of the `dbm`.

# Optional keyword arguments:
* `isfirst`, `islast`: indicating that the new RBM should be trained as
   first (assumed if `dbm` is empty) or last layer of the DBM, respectively.
   If those flags are not set, the added layer is trained as intermediate layer.
   This information is needed to determine the factor for the weights during
   pretraining.
* `epochs`, `nhidden`, `learningrate`/`learningrates`, `pcd`, `cdsteps`,
  `monitoring`: used for fitting the weights of the last layer, see `fitrbm(x)`
"""
function addlayer!(dbm::BasicDBM, x::Matrix{Float64};
      isfirst::Bool = isempty(dbm),
      islast::Bool = false,
      nhidden::Int = size(x,2),
      epochs::Int = 10,
      learningrate::Float64 = 0.005,
      learningrates::Vector{Float64} = learningrate * ones(epochs),
      pcd::Bool = true,
      cdsteps::Int = 1,
      monitoring::Function = nomonitoring)

   warn("`addlayer!`` is deprecated. Please use the options of `fitdbm` instead.")
   # propagate input x up to last hidden layer
   hh = x
   for i = 1:length(dbm)
      hh = hiddenpotential(dbm[i], hh, 2.0) # intermediate layer, factor is 2.0
   end

   upfactor = downfactor = 2.0
   if isfirst
      downfactor = 1.0
   end
   if islast
      upfactor = 1.0
   end

   rbm = fitrbm(hh, nhidden = nhidden, epochs = epochs,
         rbmtype = BernoulliRBM,
         learningrate = learningrate,
         learningrates = learningrates,
         pcd = pcd, cdsteps = 1,
         upfactor = upfactor, downfactor = downfactor,
         monitoring = monitoring)

   push!(dbm, rbm)
   dbm
end

function defaultfinetuninglearningrates(learningrate, epochs)
   learningrate * 11.0 ./ (10.0 .+ (1:epochs))
end

"""
    fitdbm(x; ...)
Fits a (multimodal) DBM to the data set `x`.
The procedure consists of two parts:
First a stack of RBMs is pretrained in a greedy layerwise manner
(see `stackrbms(x)`). Then the weights of all layers are jointly
trained using the general Boltzmann Machine learning procedure
(see `traindbm!(dbm,x)`).

# Optional keyword arguments (ordered by importance):
* `nhiddens`: vector that defines the number of nodes in the hidden layers of
   the DBM. The default value specifies two hidden layers with the same size
   as the visible layer.
* `epochs`: number of training epochs for joint training
* `epochspretraining`: number of training epochs for pretraining,
   defaults to `epochs`
* `learningrate`/`learningrates`:
   learning rate(s) for joint training of layers (= fine tuning)
   using the learning algorithm for a general Boltzmann Machine.
   The learning rate for fine tuning is by default decaying with the number of epochs,
   starting with the given value for the `learningrate`.
   (For more details see `traindbm!`).
* `learningratepretraining`: learning rate for pretraining,
   defaults to `learningrate`
* `batchsizepretraining`: batchsize for pretraining, defaults to 1
* `nparticles`: number of particles used for sampling during joint training of
   DBM, default 100
* `pretraining`: The arguments for layerwise pretraining
   can be specified for each layer individually.
   This is done via a vector of `TrainLayer` objects.
   (For a detailed description of the possible parameters,
   see help for `TrainLayer`).
   If the number of training epochs and the learning rate are not specified
   explicitly for a layer, the values of `epochspretraining`,
   `learningratepretraining` and `batchsizepretraining` are used.
* `monitoring`: Monitoring function accepting a `dbm` and the number of epochs
   retuning nothing. Used for the monitoring of fine-tuning.
* `monitoringdatapretraining`: a `DataDict` that contains data used for
   monitoring the pretraining (see argument `monitoringdata` of `stackrbms`.)
* `optimizer`/`optimizers`: an optimizer or a vector of optimizers for each epoch
   (see `AbstractOptimizer`) used for fine-tuning.
* `optimizerpretraining`: an optimizer used for pre-training.
   Defaults to the `optimizer`.
"""
function fitdbm(x::Matrix{Float64};
      nhiddens::Vector{Int} = Vector{Int}(),
      epochs::Int = 10,
      nparticles::Int = 100,
      learningrate::Float64 = 0.005,
      learningrates::Vector{Float64} =
            defaultfinetuninglearningrates(learningrate, epochs),
      sdlearningrate::Float64 = 0.0,
      sdlearningrates::Vector{Float64} =
            defaultfinetuninglearningrates(sdlearningrate, epochs),
      learningratepretraining::Float64 = learningrate,
      epochspretraining::Int = epochs,
      batchsizepretraining::Int = 1,
      pretraining::AbstractTrainLayers = Vector{TrainLayer}(),
      monitoring::Function = nomonitoring,
      monitoringdatapretraining::DataDict = DataDict(),
      optimizer::AbstractOptimizer = NoOptimizer(),
      optimizers::Vector{<:AbstractOptimizer} = Vector{AbstractOptimizer}(),
      optimizerpretraining::AbstractOptimizer = optimizer)

   if isempty(pretraining) && isempty(nhiddens)
      # set default only if there is not any more detailed info
      nvariables = size(x,2)
      nhiddens = [nvariables; nvariables]
   end

   # Layerwise pre-training
   pretraineddbm = stackrbms(x, nhiddens = nhiddens,
         epochs = epochspretraining, predbm = true,
         batchsize = batchsizepretraining,
         learningrate = learningratepretraining,
         optimizer = optimizer,
         trainlayers = pretraining,
         monitoringdata = monitoringdatapretraining)
   # Fine-tuning using mean-field approximation in algorithm for
   # training a general Boltzmann machine
   traindbm!(pretraineddbm, x, epochs = epochs, nparticles = nparticles,
         learningrate = learningrate, learningrates = learningrates,
         sdlearningrate = sdlearningrate, sdlearningrates = sdlearningrates,
         optimizer = optimizer, optimizers = optimizers,
         monitoring = monitoring)
end

"""
    newparticleslike(particles)
Creates new and uninitialized particles of the same dimensions as the given
`particles`.
"""
function newparticleslike(particles::Particles)
   newparticles = Particles(undef, length(particles))
   for i in eachindex(particles)
      newparticles[i] = Matrix{Float64}(undef, size(particles[i]))
   end
   newparticles
end

"""
    meanfield(dbm, x)
    meanfield(dbm, x, eps)
Computes the mean-field approximation for the data set `x` and
returns a matrix of particles for the DBM.
The number of particles is equal to the number of samples in `x`.
`eps` is the convergence criterion for the fix-point iteration, default 0.001.
It is assumed that all nodes in in-between-layers are Bernoulli distributed.
"""
function meanfield(dbm::MultimodalDBM, x::Array{Float64,2}, eps::Float64 = 0.001)

   nlayers = length(dbm) + 1
   mu = Particles(undef, nlayers)

   # Initialization with single bottom-up pass using twice the weights for all
   # but the topmost layer (see [Salakhutdinov+Hinton, 2012], p. 1985)
   mu[1] = x
   for i=2:(nlayers-1)
      mu[i] = hiddenpotential(dbm[i-1], mu[i-1], 2.0)
   end
   mu[nlayers] = hiddenpotential(dbm[nlayers-1], mu[nlayers-1])

   newmu = newparticleslike(mu)
   newmu[1] = x
   input2 = newparticleslike(mu)

   # mean-field updates until convergence criterion is met
   delta = 1.0
   while delta > eps
      delta = 0.0

      for i = 2:(nlayers-1)
         # total input from layer below
         hiddeninput!(newmu[i], dbm[i-1], mu[i-1])
         # input from layer above
         visibleinput!(input2[i], dbm[i], mu[i+1])
         # combine input
         newmu[i] .+= input2[i]
         # By computing the potential,
         # the assumption is used that all nodes in in-between-layers
         # are Bernoulli-distributed:
         sigm!(newmu[i])

         delta = max(delta, maximum(abs.(mu[i] - newmu[i])))

         # swap new and old mu
         tmp = newmu[i]
         newmu[i] = mu[i]
         mu[i] = tmp
      end

      # last layer
      hiddenpotential!(newmu[end], dbm[end], mu[end-1])
      delta = max(delta, maximum(abs.(mu[end] - newmu[end])))

      # swap new and old mu
      tmp = newmu[end]
      newmu[end] = mu[end]
      mu[end] = tmp
   end

   mu
end

"""
    traindbm!(dbm, x; ...)
Trains the `dbm` (a `BasicDBM` or a more general `MultimodalDBM`) using
the learning procedure for a general Boltzmann Machine with the
training data set `x`.
A learning step consists of mean-field inference (positive phase),
stochastic approximation by Gibbs Sampling (negative phase) and the parameter
updates.

# Optional keyword arguments (ordered by importance):
* `epoch`: number of training epochs
* `learningrate`/`learningrates`: a vector of learning rates for each epoch to
   update the weights and biases. The learning rates should decrease with the
   epochs, e. g. like `a / (b + epoch)`. If only one value is given as
   `learningrate`, `a` and `b` are 11.0 and 10.0, respectively.
* `nparticles`: number of particles used for sampling, default 100
* `monitoring`: A function that is executed after each training epoch.
   It has to accept the trained DBM and the current epoch as arguments.
"""
function traindbm!(dbm::MultimodalDBM, x::Array{Float64,2};
      epochs::Int = 10,
      nparticles::Int = 100,
      learningrate::Float64 = 0.005,
      learningrates::Vector{Float64} =
            defaultfinetuninglearningrates(learningrate, epochs),
      sdlearningrate::Float64 = 0.0,
      sdlearningrates::Vector{Float64} =
            defaultfinetuninglearningrates(sdlearningrate, epochs),
      monitoring::Function = nomonitoring,
      optimizer::AbstractOptimizer = NoOptimizer(),
      optimizers::Vector{<:AbstractOptimizer} = Vector{AbstractOptimizer}())

   assert_enoughvaluesforepochs("learningrates", learningrates, epochs)

   optimizer = converttodbmoptimizer(optimizer, dbm)
   map!(opt -> converttodbmoptimizer(opt, dbm), optimizers, optimizers)
   optimizers = assertinitoptimizers(optimizer, optimizers, dbm,
         learningrates, sdlearningrates, epochs)

   particles = initparticles(dbm, nparticles)

   for epoch = 1:epochs
      traindbm!(dbm, x, particles, optimizers[epoch])

      # monitoring the learning process at the end of epoch
      monitoring(dbm, epoch)
   end

   dbm
end

"""
    traindbm!(dbm, x, particles, learningrate)
Trains the given `dbm` for one epoch.
"""
function traindbm!(dbm::MultimodalDBM, x::Array{Float64,2}, particles::Particles,
      optimizer::AbstractOptimizer)

   gibbssample!(particles, dbm)
   mu = meanfield(dbm, x)

   computegradient!(optimizer, mu, particles, dbm)
   updateparameters!(dbm, optimizer)

   dbm
end


num_machines() = static(1)
num_sockets() = static(1)

function _get_num_threads()::Int
  (Sys.CPU_THREADS)::Int >> (Sys.ARCH !== :aarch64)
end

const _get_num_cores = _get_num_threads

let nc = static(_get_num_threads())
  global num_l1cache() = nc
  global num_cores() = nc
end
let syst = static((Sys.CPU_THREADS)::Int)
  global sys_threads() = syst
  global num_threads() = syst
end
@static if Sys.ARCH === :aarch64
  num_l2cache() = static(1)
  num_l3cache() = static(0)
else
  num_l2cache() = num_l1cache()
  num_l3cache() = static(1)
end
num_l4cache() = static(0)

if Sys.ARCH === :aarch64 && Sys.isapple()
  cache_size(::Union{Val{1},StaticInt{1}}) = StaticInt{131072}()
else
  cache_size(::Union{Val{1},StaticInt{1}}) = StaticInt{32768}()
end
cache_associativity(::Union{Val{1},StaticInt{1}}) = StaticInt{0}()
cache_type(::Union{Val{1},StaticInt{1}}) = Val{:Data}()
cache_inclusive(::Union{Val{1},StaticInt{1}}) = False()

if Sys.ARCH === :aarch64 && Sys.isapple()
  cache_size(::Union{Val{2},StaticInt{2}}) = StaticInt{12582912}()
else
  cache_size(::Union{Val{2},StaticInt{2}}) = StaticInt{262144}()
end
cache_associativity(::Union{Val{2},StaticInt{2}}) = StaticInt{0}()
cache_type(::Union{Val{2},StaticInt{2}}) = Val{:Unified}()
cache_inclusive(_) = False()
@static if Sys.isapple() && Sys.ARCH === :aarch64
  cache_linesize(_) = StaticInt{128}() # assume...
else
  cache_linesize(_) = StaticInt{64}() # assume...
end
cache_size(_) = StaticInt{0}()

cache_type(::Union{Val{3},StaticInt{3}}) = Val{:Unified}()
cache_size(::Union{Val{3},StaticInt{3}}) = num_cores() * StaticInt{1441792}()

_extra_init() = nothing


module GuillotineCallback

include("./../../constraints/geometric/constants.jl")
using JuMP
using .Constants

export guillotine_callback, get_involved_boxes_flbx, find_all_guillotine_cuts_exceeding_limit

#Finds a guillotine cut that is larger than the allowed amount
function guillotine_callback(cb_data, m)

    max_cut = guillotine_ds_var * callback_value(cb_data, m[:maxHeight])

    fixed_x = []
    fixed_y = []

    #Check if there exists a cut higher than the allowed cuts
    for i in 1:length(boxes) - 1

        if length(fixed_x) > 0 || length(fixed_y) > 0
            print("Fixed X ", fixed_x, "\n")
            print("Fixed Y ", fixed_y, "\n")
        end

        print("Round ", i, "\n")

        #Find out where the cut is and pass information onwards
        if (callback_value(cb_data, m[:cut_flbx_left_i][i]) + 0.001 >= max_cut && check_if_fixed_x(cb_data, m, 1, i, fixed_x))
            print("Nr 1 \n")
            get_involved_boxes(cb_data, m, 1, i)
            x_value = callback_value(cb_data, m[:flbx_front_left_bot_x][i])
            push!(fixed_x, x_value)
        end
        if (callback_value(cb_data, m[:cut_flbx_j_plus_left_i][i]) + 0.001 >= max_cut && check_if_fixed_x(cb_data, m, 2, i, fixed_x))
            print("Nr 2 \n")
            get_involved_boxes(cb_data, m, 2, i)
            x_value = callback_value(cb_data, m[:flbx_front_left_bot_x][i])
            push!(fixed_x, x_value)
        end
        if (callback_value(cb_data, m[:cut_flbx_i_plus_right][i]) + 0.001 >= max_cut && check_if_fixed_x(cb_data, m, 3, i, fixed_x))
            print("Nr 3 \n")
            get_involved_boxes(cb_data, m, 3, i)
            x_value = callback_value(cb_data, m[:flbx_front_left_bot_x][i]) + callback_value(cb_data, m[:box_x_axis_cover][i])
            push!(fixed_x, x_value)
        end
        if (callback_value(cb_data, m[:cut_flbx_ij_plus_right][i]) + 0.001 >= max_cut && check_if_fixed_x(cb_data, m, 4, i, fixed_x))
            print("Nr 4 \n")
            get_involved_boxes(cb_data, m, 4, i)
            x_value = callback_value(cb_data, m[:flbx_front_left_bot_x][i]) + callback_value(cb_data, m[:box_x_axis_cover][i])
            push!(fixed_x, x_value)
        end
        if (callback_value(cb_data, m[:cut_flby_right][i]) + 0.001 >= max_cut && check_if_fixed_y(cb_data, m, 5, i, fixed_y))
            print("Nr 5 \n")
            get_involved_boxes(cb_data, m, 5, i)
            y_value = callback_value(cb_data, m[:flby_front_left_bot_y][i])
            push!(fixed_y, y_value)
        end
        if (callback_value(cb_data, m[:cut_flby_j_plus_right][i]) + 0.001 >= max_cut && check_if_fixed_y(cb_data, m, 6, i, fixed_y))
            print("Nr 6 \n")
            get_involved_boxes(cb_data, m, 6, i)
            y_value = callback_value(cb_data, m[:flby_front_left_bot_y][i])
            push!(fixed_y, y_value)
        end
        if (callback_value(cb_data, m[:cut_flby_i_plus_left][i]) + 0.001 >= max_cut && check_if_fixed_y(cb_data, m, 7, i, fixed_y))
            print("Nr 7 \n")
            get_involved_boxes(cb_data, m, 7, i)
            y_value = callback_value(cb_data, m[:flby_front_left_bot_y][i]) + callback_value(cb_data, m[:box_y_axis_cover][i])
            push!(fixed_y, y_value)
        end
        if (callback_value(cb_data, m[:cut_flby_ij_plus_left][i]) + 0.001 >= max_cut && check_if_fixed_y(cb_data, m, 8, i, fixed_y))
            print("Nr 8 \n")
            get_involved_boxes(cb_data, m, 8, i)
            y_value = callback_value(cb_data, m[:flby_front_left_bot_y][i]) + callback_value(cb_data, m[:box_y_axis_cover][i])
            push!(fixed_y, y_value)
        end
    end

    return
end

function check_if_fixed_x(cb_data, m, pos, i, fixed_x)
    fixed = false
    if pos == 1 || pos == 2
        for fixed_pos in 1:length(fixed_x)
            if (callback_value(cb_data, m[:flbx_front_left_bot_x][i]) - 0.0001 <=
                fixed_x[fixed_pos] <=
                callback_value(cb_data, m[:flbx_front_left_bot_x][i]) + 0.0001)
                fixed = true
            end
        end
    elseif pos == 3 || pos == 4
        for fixed_pos in 1:length(fixed_x)
            if (callback_value(cb_data, m[:flbx_front_left_bot_x][i]) + callback_value(cb_data, m[:box_x_axis_cover][i]) - 0.0001 <=
                fixed_x[fixed_pos] <=
                callback_value(cb_data, m[:flbx_front_left_bot_x][i]) + callback_value(cb_data, m[:box_x_axis_cover][i]) + 0.0001)
                fixed = true
            end
        end
    end
    return ~fixed
end

function check_if_fixed_y(cb_data, m, pos, i, fixed_y)
    fixed = false
    if pos == 5 || pos == 6
        for fixed_pos in 1:length(fixed_y)
            if (callback_value(cb_data, m[:flby_front_left_bot_y][i]) - 0.0001 <=
                fixed_y[fixed_pos] <=
                callback_value(cb_data, m[:flby_front_left_bot_y][i]) + 0.0001)
                fixed = true
            end
        end
    elseif pos == 7 || pos == 8
        for fixed_pos in 1:length(fixed_y)
            if (callback_value(cb_data, m[:flby_front_left_bot_y][i]) + callback_value(cb_data, m[:box_y_axis_cover][i]) - 0.0001 <=
                fixed_y[fixed_pos] <=
                callback_value(cb_data, m[:flby_front_left_bot_y][i]) + callback_value(cb_data, m[:box_y_axis_cover][i]) + 0.0001)
                fixed = true
            end
        end
    end
    return ~fixed
end

#Takes a box number to check what boxes are involved in guillotine
function get_involved_boxes(cb_data, m, pos, i)
    involved = []
    push!(involved, i)
    # Check the position of the cut
    for j in 1:length(boxes) - 1
        if i!=j
            if (pos == 1)
                if (callback_value(cb_data, m[:guillotine_flbx][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 2)
                if (callback_value(cb_data, m[:guillotine_flbx_j_plus][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 3)
                if (callback_value(cb_data, m[:guillotine_flbx_i_plus][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 4)
                if (callback_value(cb_data, m[:guillotine_flbx_ij_plus][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 5)
                if (callback_value(cb_data, m[:guillotine_flby][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 6)
                if (callback_value(cb_data, m[:guillotine_flby_j_plus][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 7)
                if (callback_value(cb_data, m[:guillotine_flby_i_plus][i, j]) == 1)
                    push!(involved, j)
                end
            elseif (pos == 8)
                if (callback_value(cb_data, m[:guillotine_flby_ij_plus][i, j]) == 1)
                    push!(involved, j)
                end
            end
            break
        end
    end

    if (length(involved) >= 2)
        create_constraint(cb_data, m, involved, pos)
    end
end

function create_constraint(cb_data, m, involved, pos)
    print(involved)

    max_height = 0
    max_height_box = 0
    snd_max_height = 0
    snd_max_height_box = 0

    for i in 1:length(involved)
        height_of_i = callback_value(cb_data, m[:flbz_front_left_bot_z][involved[i]]) + callback_value(cb_data, m[:hob_height_of_box][involved[i]])
        if height_of_i >= max_height
            #update second highest
            snd_max_height = max_height
            snd_max_height_box = max_height_box

            #update highest
            max_height = height_of_i
            max_height_box = involved[i]
        elseif height_of_i >= snd_max_height
            #update second highest
            snd_max_height = height_of_i
            snd_max_height_box = involved[i]
        end
    end

    for i in 1:3
        print('\n')
        print(i)
        print('\n')
        @show callback_value(cb_data, m[:flbx_front_left_bot_x][i])
        @show callback_value(cb_data, m[:flby_front_left_bot_y][i])
        @show callback_value(cb_data, m[:flbz_front_left_bot_z][i])
    end

    print('\n')
    print("First ", max_height, " ", max_height_box, "\n")
    print("Second ", snd_max_height, " ", snd_max_height_box, "\n")

    #If same size as sentinel, then the new constraints can't apply.
    if ~((callback_value(cb_data, m[:lob_length_of_box][max_height_box]) >=
        (1 - guillotine_fix_var) * callback_value(cb_data, m[:lob_length_of_box][length(boxes)])) &&
        (callback_value(cb_data, m[:wob_width_of_box][max_height_box]) >=
        (1 - guillotine_fix_var) * callback_value(cb_data, m[:wob_width_of_box][length(boxes)])))

        #check if lowest dimension still same as width of sentinel
        if ~((callback_value(cb_data, m[:lob_length_of_box][max_height_box]) >=
            (1 - guillotine_fix_var) * callback_value(cb_data, m[:lob_length_of_box][length(boxes)])) &&
            (callback_value(cb_data, m[:wob_width_of_box][max_height_box]) >=
            (1 - guillotine_fix_var) * callback_value(cb_data, m[:lob_length_of_box][length(boxes)])))
            if pos == 1
                con = @build_constraint(
                    m[:flbx_front_left_bot_x][max_height_box] -
                    m[:flbx_front_left_bot_x][snd_max_height_box] >= guillotine_fix_var
                    )
                print("added constraint 1 \n")
            elseif pos == 2
                con = @build_constraint(
                    (m[:flbx_front_left_bot_x][max_height_box] + m[:box_x_axis_cover][max_height_box]) -
                    m[:flbx_front_left_bot_x][snd_max_height_box] >= guillotine_fix_var
                    )
                print("added constraint 2 \n")
            elseif pos == 3
                con = @build_constraint(
                    (m[:flbx_front_left_bot_x][snd_max_height_box] + m[:box_x_axis_cover][snd_max_height_box]) -
                    m[:flbx_front_left_bot_x][max_height_box] >= guillotine_fix_var
                    )
                print("added constraint 3 \n")
            elseif pos == 4
                con = @build_constraint(
                    (m[:flbx_front_left_bot_x][snd_max_height_box] + m[:box_x_axis_cover][snd_max_height_box]) -
                    (m[:flbx_front_left_bot_x][max_height_box] + m[:box_x_axis_cover][max_height_box]) >= guillotine_fix_var
                    )
                print("added constraint 4 \n")
            end
            if pos == 1 || pos == 2 || pos == 3 || pos == 4
                MOI.submit(m, MOI.LazyConstraint(cb_data), con)
            end
        end
        if ~((callback_value(cb_data, m[:lob_length_of_box][max_height_box]) >=
            (1 - guillotine_fix_var) * callback_value(cb_data, m[:wob_width_of_box][length(boxes)])) &&
            (callback_value(cb_data, m[:wob_width_of_box][max_height_box]) >=
            (1 - guillotine_fix_var) * callback_value(cb_data, m[:wob_width_of_box][length(boxes)])))
            if pos == 5
                con = @build_constraint(
                    m[:flby_front_left_bot_y][max_height_box] -
                    m[:flby_front_left_bot_y][snd_max_height_box] >= guillotine_fix_var
                    )
                print("added constraint 5 \n")
            elseif pos == 6
                con = @build_constraint(
                    (m[:flby_front_left_bot_y][max_height_box] + m[:box_y_axis_cover][max_height_box]) -
                    m[:flby_front_left_bot_y][snd_max_height_box] >= guillotine_fix_var
                    )
                print("added constraint 6 \n")
            elseif pos == 7
                con = @build_constraint(
                    (m[:flby_front_left_bot_y][snd_max_height_box] + m[:box_y_axis_cover][snd_max_height_box]) -
                    m[:flby_front_left_bot_y][max_height_box] >= guillotine_fix_var
                    )
                print("added constraint 7 \n")
            elseif pos == 8
                con = @build_constraint(
                    (m[:flby_front_left_bot_y][snd_max_height_box] + m[:box_y_axis_cover][snd_max_height_box]) -
                    (m[:flby_front_left_bot_y][max_height_box] + m[:box_y_axis_cover][max_height_box]) >= guillotine_fix_var
                    )
                print("added constraint 8 \n")
            end
            if pos == 5 || pos == 6 || pos == 7 || pos == 8
                MOI.submit(m, MOI.LazyConstraint(cb_data), con)
            end
        end
    end

end

end


using Base:Integer, UInt
cd(@__DIR__)  # this sets the directory pointer to this file location
cd("..")
using Pkg  # Pkg.<command> is equivalent of `]` in command line julia
Pkg.activate(".")  # this specifies the environment being used and activates it

using Plots
# using TrackingHeaps
using Random
import Base.length
using BenchmarkTools
include("..\\utils\\ModularPlane.jl")
using Main.ModularPlane

"""
fundamentally in evolution every pattern (organism) needs a source of information (matter and energy) to succeed

self, discrete and deterministic, reasonable way of gaining information to allow reproduction
    - as soon as they have enough bits to replicate, they replicate (into a new grid)

    - or an incomplete copy of them idly travels around the next grid and is built up as the pattern finds more bits (in the form of other incomplete patterns)
        - motion: one cell is moved to a edge location which rotates counterclockwise each frame (so motion is quasi-circular)
        - or, the most recently moved cell is the starting point for counting (+x or cckmost margin), count n cckwise, where n is size of self

fractal evolution (replication is at smaller scales)? Then info is exponential
"""

seed = 633
Random.seed!(seed)

struct Pattern
    coords::Set{GridIndex}
end

length(p::Pattern) = length(p.coords)

### frame to frame updates
function updategrid!(patterns::Set{Pattern}, cells::Matrix{Int8}, tmp::Matrix{Int8})
    xdim, ydim = size(cells)
    tmp .= (cells .> 0) .+ cells
    newcells = Set{GridIndex}()
    println("sum ", sum(length.(patterns)), " ", sum(cells .> 0))
    for pattern in patterns
        pat_size = length(pattern.coords)
        if pat_size > 0
            pat_sum = sum(x -> cells[x], pattern.coords)
            newcoords = Set{GridIndex}()
            if pat_sum % 4 < 2
                extremum = argmax(x -> x.I[pat_sum % 2 + 1], pattern.coords)
            else
                extremum = argmin(x -> x.I[pat_sum % 2 + 1], pattern.coords)
            end
            start = GridIndex(extremum.I[1], extremum.I[2] + 1)
            for coord in pattern.coords
                if cells[coord] == AGE_CAP
                    prev = extremum
                    current = start
                    step = 1
                    cutoff = 4 * pat_size  # max possible surface area for squares
                    while (step <= pat_size - 1 || cells[current] >= AGE_MIN || current in newcells) && step <= cutoff
                        step += 1
                        prev, current = makestep(prev, current, pattern)
                    end
                    if step == cutoff + 1
                        current = coord  # if you can't find anywhere to grow, fill the space of the cell the just died
                    end
                    new = current
                    tmp[coord] = 0
                    push!(newcells, new)
                    tmp[new] = AGE_MIN
                    push!(newcoords, new)
                    start = new
                else
                    push!(newcoords, coord)
                end
            end
            updatecoords!(pattern, newcoords)
        else
            # println("dead pattern")
        end
    end
    patterns, tmp, cells
end

function indexof(vec::Vector{T}, val::T) where T
    for i in 1:length(vec)
        if vec[i] == val
            return i
        end
    end
    -1
end

function makestep(prev::GridIndex, current::GridIndex, pattern::Pattern)
    neighs = GridIndex.([   (current.I[1], current.I[2] + 1)
                            (current.I[1] + 1, current.I[2] + 1)
                            (current.I[1] + 1, current.I[2])
                            (current.I[1] + 1, current.I[2] - 1)
                            (current.I[1], current.I[2] - 1)
                            (current.I[1] - 1, current.I[2] - 1)
                            (current.I[1] - 1, current.I[2])
                            (current.I[1] - 1, current.I[2] + 1)])
    done = false
    started = false
    # println(current)
    # println("neighs ", neighs, "\n")
    i = indexof(neighs, prev)
    prev = current
    while i <= 16  # 2 len neighs
        i += 1
        idx = i % 8 + 1  # increment idx cyclically
        if !started
            if !(neighs[idx] in pattern.coords)  # is the location part of my body
                started = true
            end
        else
            if neighs[idx] in pattern.coords
                break  # we let current be the previous neighbor once we encounter a live cell
            end
        end
        current = neighs[idx]
    end
    prev, current
end

function updatecoords!(pattern::Pattern, coords::Set{GridIndex})
    for _ in 1:length(pattern.coords)
        pop!(pattern.coords)
    end
    for _ in 1:length(coords)
        push!(pattern.coords, pop!(coords))
    end
end

const AGE_MIN = 4
const AGE_CAP = 8
xdim, ydim = 100, 140
x = 1:xdim
y = 1:ydim

# initialize patterns
pattern_locs = [GridIndex(MInt(i, xdim), MInt(j, ydim)) for i in 0:20:xdim - 1 for j in 0:20:ydim - 1]
# pattern_locs = CartesianIndex.([(5, 10), (35, 10), (40, 25), (20, 30), (10, 55), (5, 50), (35, 40), (40, 60)])
# pattern_locs = CartesianIndex.([(5, 10)])
patterns = Set{Pattern}()
for ploc in pattern_locs
    xd = rand(1:12)
    yd = rand(1:12)
    n = rand((2xd * yd + 2) ÷ 3:xd * yd)
    possible_coords = Set{GridIndex}([GridIndex(i + ploc.I[1], j + ploc.I[2]) for i in 1:xd for j in 1:yd])
    coords = Set{GridIndex}(rand(possible_coords, n))
    push!(patterns, Pattern(coords))
end

cells = zeros(Int8, xdim, ydim)
for pattern in patterns
    for c in pattern.coords
        cells[c] = rand(AGE_MIN:AGE_CAP)
    end
end

iterations = 50
name = string("main ", xdim, "x", ydim, " seed=", seed, " iters=", iterations)
tmp = similar(cells)
bsize = 0

anim = @animate for i in 1:iterations
    # NOTE: x is vertical, y is horizontal, so swap the arguments for reasonable results
    heatmap(bsize + 1:ydim - bsize, bsize + 1:xdim - bsize, view(cells, bsize + 1:xdim - bsize, bsize + 1:ydim - bsize),
    colorbar=false, grid=false, ticks=false,
    showaxis=false, title=name, aspect_ratio=1, dpi=150)
    patterns, cells, tmp = updategrid!(patterns, cells, tmp)
end

gif(anim, string("gifs\\", name, ".gif"), fps=15)