function MOIU.map_indices(index_map::Function, change::DecisionCoefficientChange)
    return DecisionCoefficientChange(index_map(change.decision), change.new_coefficient)
end
MOIU.map_indices(index_map::Function, change::DecisionStateChange) = change
MOIU.map_indices(index_map::Function, change::KnownValuesChange) = change

function MOIU.modify_function(f::AffineDecisionFunction, change::Union{MOI.ScalarConstantChange, MOI.ScalarCoefficientChange})
    return typeof(f)(MOIU.modify_function(f.variable_part, change),
                     copy(f.decision_part))
end

function MOIU.modify_function(f::QuadraticDecisionFunction{T}, change::Union{MOI.ScalarConstantChange, MOI.ScalarCoefficientChange}) where T
    return typeof(f)(
        MOIU.modify_function(f.variable_part, change),
        copy(f.decision_part),
        copy(f.cross_terms))
end

function MOIU.modify_function(f::VectorAffineDecisionFunction, change::Union{MOI.VectorConstantChange, MOI.MultirowChange})
    return typeof(f)(MOIU.modify_function(f.variable_part, change),
                     copy(f.decision_part))
end

function MOIU.modify_function(f::AffineDecisionFunction, change::DecisionCoefficientChange)
    return typeof(f)(copy(f.variable_part),
                     MOIU.modify_function(f.decision_part,
                                          MOI.ScalarCoefficientChange(change.decision, change.new_coefficient)))
end

function MOIU.modify_function(f::QuadraticDecisionFunction{T}, change::DecisionCoefficientChange) where T
    return typeof(f)(
        copy(f.variable_part),
        MOIU.modify_function(f.decision_part,
                             MOI.ScalarCoefficientChange(change.decision, change.new_coefficient)),
        copy(f.cross_terms))
end

function MOIU.modify_function(f::VectorAffineDecisionFunction, change::DecisionMultirowChange)
    return typeof(f)(copy(f.variable_part),
                     MOIU.modify_function(f.decision_part,
                                          DecisionMultirowChange(change.decision, change.new_coefficients)))
end

function MOIU.modify_function(f::Union{SingleDecision, AffineDecisionFunction, QuadraticDecisionFunction, VectorAffineDecisionFunction}, change::Union{DecisionStateChange, KnownValuesChange})
    # Nothing to do here, handled in bridges
    return f
end

# Can rely on scalarize bridge if modifications are passed along
function MOI.modify(model::MOI.ModelLike,
                    bridge::MOIB.Constraint.ScalarizeBridge{T, AffineDecisionFunction{T}},
                    change::DecisionModification) where T
    for constraint in bridge.scalar_constraints
        MOI.modify(model, constraint, change)
    end
    return nothing
end

# Can rely on vectorize bridge if modifications are passed along
function MOI.modify(model::MOI.ModelLike,
                    bridge::MOIB.Constraint.VectorizeBridge{T, VectorAffineDecisionFunction{T}},
                    change::DecisionModification) where T
    MOI.modify(model, bridge.vector_constraint, change)
    return nothing
end

# Can rely on flipsign bridges if modifications are passed along
function MOI.modify(model::MOI.ModelLike,
                    bridge::MOIB.Constraint.FlipSignBridge,
                    change::DecisionModification)
    MOI.modify(model, bridge.constraint, change)
    return nothing
end

# Can rely on norm-bridges if modifications are passed along
function MOI.modify(model::MOI.ModelLike,
                    bridge::MOIB.Constraint.NormInfinityBridge{T, VectorAffineDecisionFunction{T}},
                    change::DecisionModification) where T
    MOI.modify(model, bridge.constraint, change)
    return nothing
end
function MOI.modify(model::MOI.ModelLike,
                    bridge::MOIB.Constraint.NormOneBridge{T, VectorAffineDecisionFunction{T}},
                    change::DecisionModification) where T
    MOI.modify(model, bridge.nn_index, change)
    return nothing
end

function MOI.add_constraint(uf::MOIU.UniversalFallback, f::SingleDecision, s::MOI.AbstractScalarSet)
    F = typeof(f)
    S = typeof(s)
    # Add constraint as usual but force index
    constraints = get!(uf.constraints, (F, S)) do
        OrderedDict{CI{F, S}, Tuple{F, S}}()
    end::OrderedDict{CI{F, S}, Tuple{F, S}}
    ci = CI{typeof(f), typeof(s)}(f.decision.value)
    constraints[ci] = (f, s)
    return ci
end

function MOI.modify(uf::MOIU.UniversalFallback, ::CI{F,S}, ::Union{DecisionStateChange,KnownValuesChange}) where {F, S}
    return nothing
end

function MOI.delete(uf::MOIU.UniversalFallback, ci::CI{F, S}) where {F <: SingleDecision, S <: MOI.AbstractScalarSet}
    decision_ci = CI{MOI.SingleVariable, SingleDecisionSet{Float64}}(ci.value)
    if !MOI.is_valid(uf, ci) && MOI.is_valid(uf, decision_ci)
        throw(MOI.InvalidIndex(ci))
    end
    # Delete constraint as usual
    if MOI.is_valid(uf, ci)
        delete!(uf.constraints[(F, S)], ci)
        delete!(uf.con_to_name, ci)
        uf.name_to_con = nothing
        for d in values(uf.conattr)
            delete!(d, ci)
        end
    end
    # Update SingleDecisionSet for printing
    if MOI.is_valid(uf, decision_ci)
        set = MOI.get(uf, MOI.ConstraintSet(), decision_ci)
        new_set = SingleDecisionSet(set.stage, set.decision, NoSpecifiedConstraint(), set.is_recourse)
        MOI.set(uf, MOI.ConstraintSet(), decision_ci, new_set)
    end
    return nothing
end

function MOI.get(model::MOIU.CachingOptimizer,
                 attr::DecisionIndex,
                 ci::CI)
    return MOI.get(model.optimizer, attr, model.model_to_optimizer_map[ci])
end

function MOI.get(b::MOIB.AbstractBridgeOptimizer,
                 attr::DecisionIndex, ci::MOI.ConstraintIndex)
    return MOIB.call_in_context(b, ci, bridge -> MOI.get(b, attr, bridge))
end


module Inference

using ..Core, ..Core.RandomVariables, ..Utilities
using ..Core.RandomVariables: Metadata, _tail
using Distributions, Libtask, Bijectors
using ProgressMeter, LinearAlgebra
using ..Turing: PROGRESS, CACHERESET, AbstractSampler
using ..Turing: Model, runmodel!, get_pvars, get_dvars,
    Sampler, SampleFromPrior, SampleFromUniform,
    Selector
using ..Turing: in_pvars, in_dvars, Turing
using StatsFuns: logsumexp
using Random: GLOBAL_RNG, AbstractRNG
import AdvancedHMC; const AHMC = AdvancedHMC

import ..Turing: getspace
import Distributions: sample
import ..Core: getchunksize, getADtype
import ..Utilities: Sample, save, resume

export  InferenceAlgorithm,
        Hamiltonian,
        AbstractGibbs,
        GibbsComponent,
        StaticHamiltonian,
        AdaptiveHamiltonian,
        SampleFromUniform,
        SampleFromPrior,
        MH,
        Gibbs,      # classic sampling
        HMC,
        SGLD,
        SGHMC,
        HMCDA,
        NUTS,       # Hamiltonian-like sampling
        DynamicNUTS,
        IS,
        SMC,
        CSMC,
        PG,
        PIMH,
        PMMH,
        IPMCMC,  # particle-based sampling
        assume,
        observe,
        step,
        WelfordVar,
        WelfordCovar,
        NaiveCovar,
        get_var,
        get_covar,
        add_sample!,
        reset!

#######################
# Sampler abstraction #
#######################
abstract type AbstractAdapter end
abstract type InferenceAlgorithm end
abstract type Hamiltonian{AD} <: InferenceAlgorithm end
abstract type StaticHamiltonian{AD} <: Hamiltonian{AD} end
abstract type AdaptiveHamiltonian{AD} <: Hamiltonian{AD} end

getchunksize(::T) where {T <: Hamiltonian} = getchunksize(T)
getchunksize(::Type{<:Hamiltonian{AD}}) where AD = getchunksize(AD)
getADtype(alg::Hamiltonian) = getADtype(typeof(alg))
getADtype(::Type{<:Hamiltonian{AD}}) where {AD} = AD

"""
    mh_accept(H::T, H_new::T, log_proposal_ratio::T) where {T<:Real}

Peform MH accept criteria with log acceptance ratio. Returns a `Bool` for acceptance.

Note: This function is only used in PMMH.
"""
function mh_accept(H::T, H_new::T, log_proposal_ratio::T) where {T<:Real}
    return log(rand()) + H_new < H + log_proposal_ratio, min(0, -(H_new - H))
end

# Concrete algorithm implementations.
include("sghmc.jl")
include("hmc.jl")
include("mh.jl")
include("is.jl")
include("AdvancedSMC.jl")
include("gibbs.jl")

getspace(::IS) = ()
getspace(::Type{<:IS}) = ()
for alg in (:SMC, :PG, :PMMH, :IPMCMC, :MH)
    @eval getspace(::$alg{space}) where {space} = space
    @eval getspace(::Type{<:$alg{space}}) where {space} = space
    @eval getspace(::Type{<:$alg{space}}) where {space} = space
end
for alg in (:HMC, :HMCDA, :NUTS, :SGLD, :SGHMC)
    @eval getspace(::$alg{<:Any, space}) where {space} = space
    @eval getspace(::Type{<:$alg{<:Any, space}}) where {space} = space
end

@inline floatof(::Type{T}) where {T <: Real} = typeof(one(T)/one(T))
@inline floatof(::Type) = Real

@inline Turing.Core.get_matching_type(spl::Turing.Sampler, vi::Turing.RandomVariables.VarInfo, ::Type{T}) where {T <: AbstractFloat} = floatof(eltype(vi, spl))
@inline Turing.Core.get_matching_type(spl::Turing.Sampler{<:Hamiltonian}, vi::Turing.RandomVariables.VarInfo, ::Type{TV}) where {T, N, TV <: Array{T, N}} = Array{Turing.Core.get_matching_type(spl, vi, T), N}
@inline Turing.Core.get_matching_type(spl::Turing.Sampler{<:Union{PG, SMC}}, vi::Turing.RandomVariables.VarInfo, ::Type{TV}) where {T, N, TV <: Array{T, N}} = TArray{T, N}

## Fallback functions

# utility funcs for querying sampler information
require_gradient(spl::Sampler) = false
require_particles(spl::Sampler) = false

assume(spl::Sampler, dist::Distribution) =
error("Turing.assume: unmanaged inference algorithm: $(typeof(spl))")

observe(spl::Sampler, weight::Float64) =
error("Turing.observe: unmanaged inference algorithm: $(typeof(spl))")

## Default definitions for assume, observe, when sampler = nothing.
function assume(spl::A,
    dist::Distribution,
    vn::VarName,
    vi::VarInfo) where {A<:Union{SampleFromPrior, SampleFromUniform}}

    if haskey(vi, vn)
        r = vi[vn]
    else
        r = isa(spl, SampleFromUniform) ? init(dist) : rand(dist)
        push!(vi, vn, r, dist, spl)
    end
    # NOTE: The importance weight is not correctly computed here because
    #       r is genereated from some uniform distribution which is different from the prior
    # acclogp!(vi, logpdf_with_trans(dist, r, istrans(vi, vn)))

    r, logpdf_with_trans(dist, r, istrans(vi, vn))

end

function assume(spl::A,
    dists::Vector{T},
    vn::VarName,
    var::Any,
    vi::VarInfo) where {T<:Distribution, A<:Union{SampleFromPrior, SampleFromUniform}}

    @assert length(dists) == 1 "Turing.assume only support vectorizing i.i.d distribution"
    dist = dists[1]
    n = size(var)[end]

    vns = map(i -> VarName(vn, "[$i]"), 1:n)

    if haskey(vi, vns[1])
        rs = vi[vns]
    else
        rs = isa(spl, SampleFromUniform) ? init(dist, n) : rand(dist, n)

        if isa(dist, UnivariateDistribution) || isa(dist, MatrixDistribution)
            for i = 1:n
                push!(vi, vns[i], rs[i], dist, spl)
            end
            @assert size(var) == size(rs) "Turing.assume: variable and random number dimension unmatched"
            var = rs
        elseif isa(dist, MultivariateDistribution)
            for i = 1:n
                push!(vi, vns[i], rs[:,i], dist, spl)
            end
            if isa(var, Vector)
                @assert length(var) == size(rs)[2] "Turing.assume: variable and random number dimension unmatched"
                for i = 1:n
                    var[i] = rs[:,i]
                end
            elseif isa(var, Matrix)
                @assert size(var) == size(rs) "Turing.assume: variable and random number dimension unmatched"
                var = rs
            else
                @error("Turing.assume: unsupported variable container"); error()
            end
        end
    end

    # acclogp!(vi, sum(logpdf_with_trans(dist, rs, istrans(vi, vns[1]))))

    var, sum(logpdf_with_trans(dist, rs, istrans(vi, vns[1])))

end

observe(::Nothing,
        dist::T,
        value::Any,
        vi::VarInfo) where T = observe(SampleFromPrior(), dist, value, vi)

function observe(spl::A,
    dist::Distribution,
    value::Any,
    vi::VarInfo) where {A<:Union{SampleFromPrior, SampleFromUniform}}

    vi.num_produce += one(vi.num_produce)
    Turing.DEBUG && @debug "dist = $dist"
    Turing.DEBUG && @debug "value = $value"

    # acclogp!(vi, logpdf(dist, value))
    logpdf(dist, value)

end

function observe(spl::A,
    dists::Vector{T},
    value::Any,
    vi::VarInfo) where {T<:Distribution, A<:Union{SampleFromPrior, SampleFromUniform}}

    @assert length(dists) == 1 "Turing.observe only support vectorizing i.i.d distribution"
    dist = dists[1]
    @assert isa(dist, UnivariateDistribution) || isa(dist, MultivariateDistribution) "Turing.observe: vectorizing matrix distribution is not supported"
    if isa(dist, UnivariateDistribution)  # only univariate distributions support broadcast operation (logpdf.) by Distributions.jl
        # acclogp!(vi, sum(logpdf.(Ref(dist), value)))
        sum(logpdf.(Ref(dist), value))
    else
        # acclogp!(vi, sum(logpdf(dist, value)))
        sum(logpdf(dist, value))
    end

end

##############
# Utilities  #
##############

# VarInfo to Sample
Sample(vi::VarInfo) = Sample(0.0, todict(vi))
function todict(vi::VarInfo)
    value = todict(vi.metadata, vi)
    value[:lp] = getlogp(vi)
    return value
end
function todict(md::Metadata, vi::VarInfo)
    value = Dict{Symbol, Any}() # value is named here because of Sample has a field called value
    for vn in keys(md.idcs)
        value[Symbol(vn)] = vi[vn]
    end
    return value
end
function todict(metadata::NamedTuple{names}, vi::VarInfo) where {names}
    length(names) === 0 && return Dict{Symbol, Any}()
    f = names[1]
    mdf = getfield(metadata, f)
    return merge(todict(mdf, vi), todict(_tail(metadata), vi))
end

# VarInfo, combined with spl.info, to Sample
function Sample(vi::AbstractVarInfo, spl::Sampler)
    s = Sample(vi)
    if haskey(spl.info, :adaptor)
        s.value[:lf_eps] = AHMC.getϵ(spl.info[:adaptor])
    end
    if haskey(spl.info, :eval_num)
        s.value[:eval_num] = spl.info[:eval_num]
    end
    return s
end

getspace(spl::Sampler) = getspace(typeof(spl))
getspace(::Type{<:Sampler{Talg}}) where {Talg} = getspace(Talg)

end # module


#
# driverdriver.jl::
#
# trivial coupled system mock up
#
# very simple top level driver that show proof of concept implementation for
# simple couplng API sufficient for v0 atmos-land-ocean DG tools.
# Components
# _A - atmos
# _L - land
# _O - ocean
# _C - coupler
#
# driverdriver multiplexes land ocean atmos and coupler components.
#
# all exist on aligned horizontal grids decomposed on ranks in aligned way.
#
# components in this example run sequentially on shared set/subset of
# underlying resources without spinwait contention.
#
# components have their own MPI communicators to support MPI rank disaggregation onto
# distinct resources or to interleave on same resources with some lightweight
# resource contention coordination.
#
# disaggregated ranks require coupler to handle asynchronous send/receive posting
#
#
abstract type CLIMAcomponent end
abstract type  DGModelComponent <: CLIMAcomponent end
struct CouplerComponent         <: CLIMAcomponent end
struct AtmosModelComponent      <: DGModelComponent end
struct LandModelComponent       <: DGModelComponent end
struct OceanModelComponent      <: DGModelComponent end

comp_config(args...; kargs...) = nothing;
#
using MPI
using ClimateMachine
using ClimateMachine.Mesh.Topologies
using ClimateMachine.Mesh.Grids
using ClimateMachine.DGMethods
using ClimateMachine.DGMethods.NumericalFluxes

 #####
 # Basic initialization
 #####
 ClimateMachine.init()
 ArrayType = ClimateMachine.array_type()
 mpicomm = MPI.COMM_WORLD
 FT = Float64

 # In general we might dup communicator and/or create intercommunicators/
 # intracommicators depending on resource use and parallelism mode. Here we
 # assume sequential, all componenta on all same ranks, aligned. So each component
 # just uses default mpicomm - (also not sure if CLiMA as written always uses
 # component communicator). We carry different names so we can make them different
 # in the future.
 mpicomm_A=mpicomm
 mpicomm_O=mpicomm
 mpicomm_L=mpicomm
 mpicomm_C=mpicomm

 #####
 # Create mesh
 #####

 # Horiz mesh
 # Same horizontal mesh size, extents and polynomical order for all components
 # We still create distinct grids so that they can use different communicators when
 # needed.
 const N = 4
 const Nˣ = 10
 const Nʸ = 10
 const Lˣ = 4e6  # m
 const Lʸ = 4e6  # m
 xrange = range(FT(0); length = Nˣ + 1, stop = Lˣ)
 yrange = range(FT(0); length = Nʸ + 1, stop = Lʸ)
 brickrange_2D = (xrange, yrange)
 topol_2D( comm ) = (
   BrickTopology( comm, brickrange_2D, periodicity = (false, false) ) );
 grid_2D( topol ) = ( DiscontinuousSpectralElementGrid(
   topol,
   FloatType = FT,
   DeviceArray = ArrayType,
   polynomialorder = N,
 )
 )
 topo_2D_A  = topol_2D( mpicomm_A )
 grid_2D_A  = grid_2D(  topo_2D_A )
 topo_2D_O  = topol_2D( mpicomm_O )
 grid_2D_O  = grid_2D(  topo_2D_O )
 topo_2D_L  = topol_2D( mpicomm_L )
 grid_2D_L  = grid_2D(  topo_2D_L )
 topo_2D_C  = topol_2D( mpicomm_C )
 grid_2D_C  = grid_2D(  topo_2D_C )

 # Vert mesh (stacked topology)
 # Different components can have different vertical extents and element counts
 const Nᶻ = 20
 # Atmos vert
 const Nᶻ_A = 20
 # Ocean vert
 const Nᶻ_O = 50
 # Land vert - lets makes this 2d as an example
 const Nᶻ_L =  0
 # Coupler vert - lets makes this 2d too for now
 const Nᶻ_C =  0

 const H_A = 100000  # m
 const H_O = 5000    # m
 zrange_A  = range(FT(0   ); length = Nᶻ_A + 1, stop = H_A)
 zrange_O  = range(FT(-H_O); length = Nᶻ_O + 1, stop = 0  )
 brickrange_3D_A = (xrange, yrange, zrange_A)
 brickrange_3D_O = (xrange, yrange, zrange_O)
 topol_3D( comm, br ) = (
  StackedBrickTopology(
    mpicomm,
    br;
    periodicity = (false, false, false),
    boundary = ((1, 1), (1, 1), (2, 3)),
  )
 );
 topol_3D_A = topol_3D( mpicomm_A, brickrange_3D_A  )
 topol_3D_O = topol_3D( mpicomm_A, brickrange_3D_O  )
 grid_3D( topol ) = (
  DiscontinuousSpectralElementGrid(
   topol,
   FloatType = FT,
   DeviceArray = ArrayType,
   polynomialorder = N,
  )
 );
 grid_3D_A=grid_3D( topol_3D_A )
 grid_3D_O=grid_3D( topol_3D_O )

 # Config and init coupler
 comp_C=comp_config( CouplerComponent(), ( (), ) )

 # Config (including mask) and init drivers of individual components
 comp_A=comp_config( AtmosModelComponent() ; grids=( (grid=grid_3D_A, label="g3da",), ) )
 comp_L=comp_config( LandModelComponent()  ; grid2D=grid_2D_L )
 comp_O=comp_config( OceanModelComponent() ; grid3D=grid_3D_O )


module Word2Vec

# using Base.Collections      # for priority queue
using Base.Cartesian        # for @nexprs
using Distances
# using NumericExtensions
using DataStructures
using StatsFuns

export LinearClassifier, train_one, WordEmbedding, train, accuracy
export save, restore
export find_nearest_words

include("utils.jl")
include("softmax_classifier.jl")
include("tree.jl")
include("word_stream.jl")
include("train.jl")
include("query.jl")

end # module


module Amphitheatre

using MeshCat
using GeometryTypes
using Colors
using PlotUtils
using Sockets: @ip_str, IPAddr, IPv4, IPv6
#
using Mongoc
using GraffSDK
using JSON2
using Base64
# using Caesar
using RoME

#
using DocStringExtensions
# using Rotations
using TransformUtils
using CoordinateTransformations, Rotations
using StaticArrays
# using FileIO
# using Base64

const CTs = CoordinateTransformations
const TUs = TransformUtils

export
	BasicFactorGraphPose,
	AbstractAmphitheatre,
	visualize,
	Visualizer,
	startMeshCatVisualizer,
	stopAmphiVis!,
	#Common visse
	visPose!,
	visPoint!,
	#camera
	CameraModel,
	#DepthImages
	cloudFromDepthImageClampZ,
	cloudFromDepthImage,
	#Amphis
	TagkTl,
	AbstractAmphitheatre,
	plDrawProp,
	BasicFactorGraphPose,
	BasicGraffPose,
	TagOnPose,
	PCloudFactorGraphPose,
	GraffTagOnPose,
	GraffCloudOnPose,
	#re-export
	RGBA,
	ColorGradient, cgrad,
	@ip_str, IPAddr, IPv4, IPv6

include("../Common/CameraModel.jl")
include("../Common/DepthImages.jl")

include("common.jl")
include("amphis.jl")
include("pointclouds.jl")
include("pointcloudAmphis.jl")
include("reprojectAmphis.jl")

global runAmphi = true

function stopAmphiVis!()
	global runAmphi
  	runAmphi = false
  	nothing
end

"""
    $(SIGNATURES)
Initialize empty visualizer window with home axis.  New browser window will be opened based on `start_browser=true`.
"""
function startMeshCatVisualizer(;host=ip"127.0.0.1",
								 start_browser::Bool=true,
                                 draworigin::Bool=true,
                                 originscale::Float64=1.0)

    viz = MeshCat.Visualizer()
    if draworigin
      setobject!( viz[:origin], Triad(originscale) )
    end

    # open a new browser tab if required
	open(viz, host=host, start_browser=start_browser)

    return viz
end

"""
   $(SIGNATURES)
High level interface to launch webserver process that draws a factor_graph_vis_type <: AbstractAmphitheatre, using Three.js and MeshCat.jl.
User factor_graph_vis_type should provide a visualize!(vis::Visualizer, factor_graph_vis_variables::T<:AbstractAmphitheatre ) function.
"""
function visualize(visdatasets::Vector{AbstractAmphitheatre};
					trans=Translation(0.0,0.0,0.0),
					quat::Rotations.Quat=Quat(1.0,0.0,0.0,0.0),
				   	host=ip"127.0.0.1",
					start_browser::Bool=true,
				    draworigin::Bool=true,
				    originscale::Float64=1.0)
    #
    global runAmphi

    runAmphi = true

    # the visualizer object itself
    vis = startMeshCatVisualizer(host=host,
								 start_browser=start_browser,
								 draworigin=draworigin,
								 originscale=originscale)
	setGlobalDrawTransform!(vis, trans=trans, quat=quat)

    # run the visualization loop #TODO add is task done to avoid multiple tasks getting lost
    runAphiTask = @async begin
		while runAmphi
			# iterate through all datasets #vir wat staan rose_fgl?
			for rose_fgl in visdatasets
				# each dataset should provide an visualize function
				visualize!(vis, rose_fgl)
			end
			# take a break and repeat
			sleep(1)
		end
		@info "visualize is finalizing."
	end

    return vis, runAphiTask
end

function restartVisualize(vis, visdatasets)
	global runAmphi
	runAmphi = true
	runAphiTask = @async begin
		while runAmphi
			# iterate through all datasets #vir wat staan rose_fgl?
			for rose_fgl in visdatasets
				# each dataset should provide an visualize function
				visualize!(vis, rose_fgl)
			end
			# take a break and repeat
			sleep(1)
		end
		@info "visualize is finalizing."
	end
	return runAphiTask
end

end


for func in [:clblasSdot, :clblasDdot,
             :clblasCdotu, :clblasZdotu,
             :clblasCdotc, :clblasZdotc]

    @eval @blasfun $func(N::Csize_t, dot_product::cl.CL_mem, offDP::Csize_t,
                              X::cl.CL_mem, offx::Csize_t, incx::Cint,
                              Y::cl.CL_mem, offy::Csize_t, incy::Cint,
                              scratch_buff::cl.CL_mem,
                              n_queues::cl.CL_uint,
                              queues::Ptr{cl.CL_command_queue},
                              n_events_in_wait_list::cl.CL_uint,
                              event_wait_list::Ptr{cl.CL_event},
                              events::Ptr{cl.CL_event})
    @eval @blasfun2 $func(N::Csize_t, dot_product::cl.CL_mem, offDP::Csize_t,
                              X::cl.CL_mem, offx::Csize_t, incx::Cint,
                              Y::cl.CL_mem, offy::Csize_t, incy::Cint,
                              scratch_buff::cl.CL_mem)
end


using CSV
using DataFrames
using FileIO
using Base.Iterators
using Random
using Flux: onehotbatch

function build_batches(metadatapath::AbstractString, melspectrogramspath::AbstractString, batchsize::Integer; eos='~', pad='_', kwargs...)
   dataset, alphabet = build_dataset(metadatapath, melspectrogramspath; eos=eos, pad=pad)
   batches = build_batches!(dataset, alphabet, batchsize, alphabet[eos], alphabet[pad], kwargs...)
   return batches, alphabet
end

build_batches(dataset::AbstractVector{<:Tuple{AbstractString,DenseMatrix{Float32}}}, alphabet::AbstractDict{Char,<:Integer}, batchsize::Integer, eosindex::Integer, padindex::Integer; kwargs...) =
   build_batches!(copy(dataset), alphabet, batchsize, eosindex, padindex; kwargs...)

function build_batches!(dataset::AbstractVector{<:Tuple{AbstractString,DenseMatrix{Float32}}},
                        alphabet::AbstractDict{Char,<:Integer},
                        batchsize::Integer,
                        eosindex::Integer,
                        padindex::Integer;
                        sorted=false, reorder=true)
   sorted || sort!(dataset; by=length∘first)
   batches = map(partition(dataset, batchsize)) do batch
      pad_batch(batch, alphabet, eosindex, padindex; sorted=true, reorder=reorder)
   end
   reorder && shuffle!(batches)
   return batches
end

function pad_batch(batch::AbstractVector{<:Tuple{String,DenseMatrix{Float32}}}, alphabet::AbstractDict{Char,<:Integer}, eosindex::Integer, padindex::Integer; sorted=false, reorder=true)
   batchsize = length(batch)
   maxlength_x = 1 + (sorted ? (length∘first∘last)(batch) : maximum(length∘first, batch))
   reorder && shuffle!(batch)

   mellengths = map(xy -> size(last(xy), 1), batch)
   maxlength_y = maximum(mellengths)
   nchannels_y = size(last(first(batch)), 2)

   stoptarget = Float32.(permutedims(onehotbatch(mellengths, 1:maxlength_y)))
   textindices = fill(padindex, maxlength_x, batchsize)
   melspectrograms = zeros(Float32, maxlength_y, nchannels_y, batchsize)

   for (n, (text, melspectrogram)) ∈ enumerate(batch)
      melspectrograms[axes(melspectrogram,1),:,n] = melspectrogram
      for (i, char) ∈ enumerate(text)
         textindices[i,n] = alphabet[char]
      end
      textindices[length(text)+1,n] = eosindex
   end
   (time, nmelfeatures, batchsize) = size(melspectrograms)
   melspectrograms⁴ = reshape(melspectrograms, (time, 1, nmelfeatures, batchsize))
   return textindices, melspectrograms⁴, stoptarget
end

function build_dataset(metadatapath::AbstractString, melspectrogramspath::AbstractString; eos='~', pad='_')
   df = DataFrame(CSV.File(metadatapath; delim='|', header=[:file_name, :text, :text_normalized], quotechar='\\', escapechar='\\'))
   ## sanity check that dataframe was parsed correctly
   # lines = readlines(metadatapath)
   # all(i -> collect(df[i,:]) == split(lines[i], '|'), eachindex(lines))
   select!(df, Not(:text))
   melspectrograms = load(melspectrogramspath, "melspectrograms")
   dataset = map(eachrow(df)) do row
      row.text_normalized, melspectrograms[row.file_name]
   end
   alphabet = build_alphabet(df.text_normalized; eos='~', pad='_')
   return dataset, alphabet
end

function build_alphabet(texts::AbstractVector{<:AbstractString}; eos::Char, pad::Char)
   alphabet = sort(unique(reduce(*, texts)))
   # padding character will be used a lot, so make its index to be 2 and make eos char index to be 1
   errormessage(char) = char -> "Cannot add the $char character to the alphabet because it already contains it"
   (eos ∈ alphabet) && error(errormessage("eos"))
   (pad ∈ alphabet) && error(errormessage("pad"))
   pushfirst!(alphabet, eos, pad)
   return Dict(v => k for (k, v) ∈ enumerate(alphabet))
end


module Plankton

export plankton_advection!
export plankton_diffusion!
export plankton_update!
export generate_individuals, individuals
export find_inds!, find_NPT!, acc_counts!, calc_par!

using CUDA
using StructArrays
using Random
using KernelAbstractions: @index, @kernel, Event, MultiEvent, wait
using KernelAbstractions.Extras.LoopInfo: @unroll

using PlanktonIndividuals.Architectures: device, Architecture, GPU, CPU, rng_type
using PlanktonIndividuals.Grids
using PlanktonIndividuals.Diagnostics

using PlanktonIndividuals: AbstractMode, CarbonMode, QuotaMode, MacroMolecularMode

#####
##### generate individuals of multiple species
#####
function generate_individuals(params::Dict, arch::Architecture, Nsp, N, maxN, g::AbstractGrid, mode::AbstractMode)
    plank_names = Symbol[]
    plank_data=[]
    for i in 1:Nsp
        name = Symbol("sp"*string(i))
        plank = construct_plankton(arch, i, params, maxN, mode::AbstractMode)
        generate_plankton!(plank, N, g, arch, mode)
        push!(plank_names, name)
        push!(plank_data, plank)
    end
    planks = NamedTuple{Tuple(plank_names)}(plank_data)
    return individuals(planks)
end

include("Advection/Advection.jl")
include("QuotaMode/QuotaMode.jl")
include("CarbonMode/CarbonMode.jl")
include("MacroMolecularMode/MacroMolecularMode.jl")
include("utils.jl")

using .Advection
import .Quota
import .Carbon
import .MacroMolecular

#####
##### some workarounds for function names
#####
construct_plankton(arch::Architecture, sp::Int64, params::Dict, maxN, mode::MacroMolecularMode) =
    MacroMolecular.construct_plankton(arch::Architecture, sp::Int64, params::Dict, maxN)

construct_plankton(arch::Architecture, sp::Int64, params::Dict, maxN, mode::QuotaMode) =
    Quota.construct_plankton(arch::Architecture, sp::Int64, params::Dict, maxN)

construct_plankton(arch::Architecture, sp::Int64, params::Dict, maxN, mode::CarbonMode) =
    Carbon.construct_plankton(arch::Architecture, sp::Int64, params::Dict, maxN)

generate_plankton!(plank, N::Int64, g::AbstractGrid, arch::Architecture, mode::MacroMolecularMode) =
    MacroMolecular.generate_plankton!(plank, N::Int64, g::AbstractGrid, arch::Architecture)

generate_plankton!(plank, N::Int64, g::AbstractGrid, arch::Architecture, mode::QuotaMode) =
    Quota.generate_plankton!(plank, N::Int64, g::AbstractGrid, arch::Architecture)

generate_plankton!(plank, N::Int64, g::AbstractGrid, arch::Architecture, mode::CarbonMode) =
    Carbon.generate_plankton!(plank, N::Int64, g::AbstractGrid, arch::Architecture)

plankton_update!(plank, nuts, proc, p, plk, diags_spcs, ΔT, t, arch::Architecture, mode::MacroMolecularMode) =
    MacroMolecular.plankton_update!(plank, nuts, proc, p, plk, diags_spcs, ΔT, t, arch::Architecture, mode::AbstractMode)
plankton_update!(plank, nuts, proc, p, plk, nothing::Nothing, ΔT, t, arch::Architecture, mode::MacroMolecularMode) =
    MacroMolecular.plankton_update!(plank, nuts, proc, p, plk, nothing::Nothing, ΔT, t, arch::Architecture, mode::AbstractMode)

plankton_update!(plank, nuts, proc, p, plk, diags_spcs, ΔT, t, arch::Architecture, mode::QuotaMode) =
    Quota.plankton_update!(plank, nuts, proc, p, plk, diags_spcs, ΔT, t, arch::Architecture, mode::AbstractMode)
plankton_update!(plank, nuts, proc, p, plk, nothing::Nothing, ΔT, t, arch::Architecture, mode::QuotaMode) =
    Quota.plankton_update!(plank, nuts, proc, p, plk, nothing::Nothing, ΔT, t, arch::Architecture, mode::AbstractMode)

plankton_update!(plank, nuts, proc, p, plk, diags_spcs, ΔT, t, arch::Architecture, mode::CarbonMode) =
    Carbon.plankton_update!(plank, nuts, proc, p, plk, diags_spcs, ΔT, t, arch::Architecture, mode::AbstractMode)
plankton_update!(plank, nuts, proc, p, plk, nothing::Nothing, ΔT, t, arch::Architecture, mode::CarbonMode) =
    Carbon.plankton_update!(plank, nuts, proc, p, plk, nothing::Nothing, ΔT, t, arch::Architecture, mode::AbstractMode)

end


export Export
export sourceMap, targetMap, distributor, isLocallyComplete
export permuteToLIDs, permuteFromLIDs, exportLIDs, remoteLIDs, remotePIDs, numSameIDs

"""
Communication plan for data rekg.ribution from a (possibly) multiple-owned to a uniquely owned distribution
"""
struct Export{GID <: Integer, PID <:Integer, LID <: Integer}
    exportData::ImportExportData{GID, PID, LID}
end

#TODO document

## Constructors ##

function Export(source::BlockMap{GID, PID, LID}, target::BlockMap{GID, PID, LID}, remotePIDs::Union{AbstractArray{PID}, Nothing}=nothing; plist...) where {GID <: Integer, PID <: Integer, LID <: Integer}
    Export(source, target,
        Dict(Tuple{Symbol, Any}[pair for pair in plist]))
end

function Export(source::BlockMap{GID, PID, LID}, target::BlockMap{GID, PID, LID},
        plist::Dict{Symbol}) where {GID <: Integer, PID <: Integer, LID <: Integer}
    Export(source, target, nothing, plist)
end

function Export(source::BlockMap{GID, PID, LID}, target::BlockMap{GID, PID, LID},
        remotePIDs::Union{AbstractArray{PID}, Nothing}, plist::Dict{Symbol}) where {
            GID <: Integer, PID <: Integer, LID <: Integer}

    expor = Export(ImportExportData(source, target))

    exportGIDs = setupSamePermuteExport(expor)

    if distributedGlobal(source)
        setupRemote(expor, exportGIDs)
    end

    expor
end

## internal construction methods ##

function setupSamePermuteExport(expor::Export{GID, PID, LID})::AbstractArray{GID} where {GID <: Integer, PID <: Integer, LID <: Integer}

    data = expor.exportData

    source = sourceMap(expor)
    target = targetMap(expor)

    sourceGIDs = myGlobalElements(source)
    targetGIDs = myGlobalElements(target)

    numSrcGIDs = length(sourceGIDs)
    numTgtGIDs = length(targetGIDs)
    numGIDs = min(numSrcGIDs, numTgtGIDs)

    numSameGIDs = 1
    while numSameGIDs <= numGIDs && sourceGIDs[numSameGIDs] == targetGIDs[numSameGIDs]
        numSameGIDs += 1
    end
    numSameGIDs -= 1
    numSameIDs(data, numSameGIDs)

    exportGIDs = Array{GID, 1}(undef, 0)
    permuteToLIDs = JuliaPetra.permuteToLIDs(data)
    permuteFromLIDs = JuliaPetra.permuteFromLIDs(data)
    exportLIDs = JuliaPetra.exportLIDs(data)

    for srcLID = (numSameGIDs+1):numSrcGIDs
        curSrcGID = sourceGIDs[srcLID]
        tgtLID = lid(target, curSrcGID)
        if tgtLID != 0
            push!(permuteToLIDs, tgtLID)
            push!(permuteFromLIDs, srcLID)
        else
            push!(exportGIDs, curSrcGID)
            push!(exportLIDs, srcLID)
        end
    end

    if length(exportLIDs) != 0 && !distributedGlobal(source)
        isLocallyComplete(data, false)
        warn("Source has export LIDs but source not distributed globally.  " *
            "Exporting to a submap of the target map.")
    end

    if distributedGlobal(source)
        #resize!(JuliaPetra.exportPIDs(data), length(exportGIDs))

        (exportPIDs, exportLIDs) = remoteIDList(target, exportGIDs)
        JuliaPetra.exportPIDs(data, exportPIDs)
        missingGIDs = 0
        for i = 1:length(exportPIDs)
            if exportPIDs[i] == 0
                missingGIDs += 1
            end
        end

        if missingGIDs != 0
            warn("The source Map has GIDs not found in the target Map")

            isLocallyComplete(data, false)
            numInvalidExports = missingGIDs
            totalNumExports = length(exportPIDs)

            if numInvalidExports == totalNumExports
                # all exports invalid, can delete all exports
                resize!(exportGIDs, 0)
                resize!(exportLIDs, 0)
                resize!(exportPIDs, 0)
            else
                #some exports are valid, need to keep the valid exports
                numValidExports = 1
                for e = 1:totalNumExports
                    if exportPIDs[e] != 0
                        exportGIDs[numValidExports] = exportGIDs[e]
                        exportLIDs[numValidExports] = exportLIDs[e]
                        exportPIDs[numValidExports] = exportPIDs[e]
                        numValidExports += 1
                    end
                end
                numValidExports -= 1

                resize!(exportGIDs, numValidExports)
                resize!(exportLIDs, numValidExports)
                resize!(exportPIDs, numValidExports)
            end
        end
    end
    exportGIDs
end

function setupRemote(expor::Export{GID, PID, LID}, exportGIDs::AbstractArray{GID, 1}) where {GID <: Integer, PID <: Integer, LID <: Integer}

    data = expor.exportData

    target = targetMap(data)

    exportPIDs = JuliaPetra.exportPIDs(data)

    order = sortperm(exportPIDs)
    permute!(exportPIDs, order)
    permute!(exportLIDs(data), order)
    permute!(exportGIDs, order)

    numRemoteIDs = createFromSends(distributor(data), exportPIDs)

    remoteGIDs = resolve(distributor(data), exportGIDs)

    remoteLIDs = JuliaPetra.remoteLIDs(data)

    resize!(remoteLIDs, numRemoteIDs)

    for i in 1:length(remoteGIDs)
        remoteLIDs[i] = lid(target, remoteGIDs[i])
    end

end
## Getters ##

"""
Get the source map for the given ImportExportData
"""
function sourceMap(impor::Export{GID, PID, LID})::BlockMap{GID, PID, LID} where GID <: Integer where PID <:Integer where LID <: Integer
    impor.exportData.source
end

"""
Get the target map for the given ImportExportData
"""
function targetMap(impor::Export{GID, PID, LID})::BlockMap{GID, PID, LID} where GID <: Integer where PID <:Integer where LID <: Integer
    impor.exportData.target
end

"""
List of elements in the target map that are permuted.
"""
function permuteToLIDs(impor::Export{GID, PID, LID})::Array{LID} where GID <: Integer where PID <:Integer where LID <: Integer
    impor.exportData.permuteToLIDs
end

"""
List of elements in the source map that are permuted.
"""
function permuteFromLIDs(impor::Export{GID, PID, LID})::Array{LID} where GID <: Integer where PID <:Integer where LID <: Integer
    impor.exportData.permuteFromLIDs
end

"""
List of elements in the target map that are coming from other processors
"""
function remoteLIDs(impor::Export{GID, PID, LID})::Array{LID} where GID <: Integer where PID <: Integer where LID <: Integer
    impor.exportData.remoteLIDs
end

"""
List of elements that will be sent to other processors
"""
function exportLIDs(impor::Export{GID, PID, LID})::Array{LID} where GID <: Integer where PID <: Integer where LID <: Integer
    impor.exportData.exportLIDs
end

"""
List of processors to which elements will be sent `exportLID[i]` will be sent to processor `exportPIDs[i]`
"""
function exportPIDs(impor::Export{GID, PID, LID})::Array{PID} where GID <: Integer where PID <: Integer where LID <: Integer
    impor.exportData.exportPIDs
end

"""
Returns the number of elements that are identical between the source and target maps, up to the first different ID
"""
function numSameIDs(impor::Export{GID, PID, LID})::LID where GID <: Integer where PID <: Integer where LID <: Integer
    impor.exportData.numSameIDs
end

"""
Returns the distributor being used
"""
function distributor(impor::Export{GID, PID, LID})::Distributor{GID, PID, LID} where GID <: Integer where PID <: Integer where LID <: Integer
    impor.exportData.distributor
end

"""
Returns whether the import or export is locally complete
"""
function isLocallyComplete(impor::Export{GID, PID, LID})::Bool where GID <: Integer where PID <: Integer where LID <: Integer
    impor.exportData.isLocallyComplete
end


module Halite

@enum DockedStatus UNDOCKED=0 DOCKING=1 DOCKED=2 UNDOCKING=3

module Constants
    #: Max number of units of distance a ship can travel in a turn
    const MAX_SPEED = 7
    #: Radius of a ship
    const SHIP_RADIUS = 0.5
    #: Starting health of ship, also its max
    const MAX_SHIP_HEALTH = 255
    #: Starting health of ship, also its max
    const BASE_SHIP_HEALTH = 255
    #: Weapon cooldown period
    const WEAPON_COOLDOWN = 1
    #: Weapon damage radius
    const WEAPON_RADIUS = 5.0
    #: Weapon damage
    const WEAPON_DAMAGE = 64
    #: Radius in which explosions affect other entities
    const EXPLOSION_RADIUS = 10.0
    #: Distance from the edge of the planet at which ships can try to dock
    const DOCK_RADIUS = 4.0
    #: Number of turns it takes to dock a ship
    const DOCK_TURNS = 5
    #: Number of production units per turn contributed by each docked ship
    const BASE_PRODUCTIVITY = 6
    #: Distance from the planets edge at which new ships are created
    const SPAWN_RADIUS = 2.0
end

include("entity.jl")
include("player.jl")
include("game_map.jl")
include("game.jl")

export Game, Constants, DockedStatus
export start_game, thrust, dock, undock
export update_map, all_players, all_planets, all_ships, all_docked_ships, get_player, get_me, get_planet, get_docked_ship
export isdocked, isowned, isfull, can_dock
export navigate, send_command_queue, closest_point_to, nearest_unoccupied_planet

end


function update_rewiring_parameters(parameters::Dict{Symbol,Any}, biomass, t)
  S = size(parameters[:A], 1)

  if parameters[:rewire_method] == :ADBM
    # Is there any extinction and/or rewiring happening?
    A = parameters[:A] #interaction matrix
    Anew = ADBM(S,parameters,biomass) #potentially new interaction  matrix
    if A != Anew #different matrices => rewired
      append!(parameters[:tmpA], [parameters[:A]]) #store the old matrix
      if parameters[:adbm_trigger] == :interval
        append!(parameters[:rewiretime], t) #store the time t whichrewiring happened
      end
    end
    #Is there any extinction?
    extinct = findall(biomass .< 100*eps())
    for i in extinct
      if i ∉ parameters[:extinctions]
        append!(parameters[:extinctions], i) ;
        append!(parameters[:extinctionstime], [(t, i)])
      end
    end
    sort!(parameters[:extinctions])

    #assign new array
    parameters[:A] = Anew

    #update the parameters
    get_herbivores(parameters) #
    getW_preference(parameters) #
    get_efficiency(parameters)

  elseif parameters[:rewire_method] ∈ [:Gilljam, :DS]
    #add extinction
    extinct = findall(biomass .< 100*eps())
    for i in extinct
      if i ∉ parameters[:extinctions]
        append!(parameters[:extinctions], i) ;
        append!(parameters[:extinctionstime], [(t, i)])
        append!(parameters[:tmpA], [parameters[:A]])
      end
    end
    sort!(parameters[:extinctions])

    #assign new array and update costs
    parameters[:A] , parameters = Gilljam(S,parameters,biomass)

    #update rewiring parameters
    if parameters[:preferenceMethod] == :specialist
      parameters = update_specialist_preference(parameters,S)
    end

    #update parameters
    get_herbivores(parameters)
    getW_preference(parameters)
    parameters[:w][(LinearIndices(parameters[:w]))[findall(x -> x .== Inf, parameters[:w])]] .= 1
    get_efficiency(parameters)

  elseif parameters[:rewire_method] ∈ [:stan, :DO]
    #add extinction
    extinct = findall(biomass .< 100*eps())
    for i in extinct
      if i ∉ parameters[:extinctions]
        append!(parameters[:extinctions], i) ;
        append!(parameters[:extinctionstime], [(t, i)])
        append!(parameters[:tmpA], [parameters[:A]])
      end
    end
    sort!(parameters[:extinctions])

    parameters[:A] = Staniczenko_rewire(parameters)
    get_herbivores(parameters)
    getW_preference(parameters)
    get_efficiency(parameters)

  else
    #add extinction
    extinct = findall(biomass .< 100*eps())
    for i in extinct
      if i ∉ parameters[:extinctions]
        append!(parameters[:extinctions], i) ;
        append!(parameters[:extinctionstime], [(t, i)])
        append!(parameters[:tmpA], [parameters[:A]])
      end
    end
    sort!(parameters[:extinctions])
  end

  return parameters
end

function update_specialist_preference(parameters::Dict{Symbol,Any})
  S = size(parameters[:A], 1)
  #find those that have lost their specialistPref and are not extinct
  lost = falses(S)
  for i = 1:S
    if !parameters[:is_producer][i] && !in(i,parameters[:extinctions])
      if parameters[:A][i,parameters[:specialistPref][i]] == 0
        #assign new random species from diet
        parameters[:specialistPref][i] = sample(findall(!iszero, parameters[:A][i,:]))
      end
    else
      parameters[:specialistPref][i] = 0
    end
  end
  #return(p_r)
end


"""
    GBDTs

Grammar-Based Decision Tree (GBDT) is a machine learning model that can be used for the interpretable classification and categorization of heterogeneous multivariate time series data.
"""
module GBDTs

export
        GBDTNode,
        GBDT,
        induce_tree,
        partition,
        members_by_bool,
        classify,
        node_members,
        children_id,
        id,
        label,
        gbes_result,
        isleaf,
        children,
        gini,
        gini_loss

using Discretizers
using Reexport
using StatsBase
using TikzGraphs, LightGraphs
@reexport using AbstractTrees
@reexport using ExprRules
@reexport using ExprOptimization

"""
    GBDTNode

Node object of a GBDT.
"""
struct GBDTNode
    id::Int
    label::Int
    gbes_result::Union{Nothing,ExprOptResult}
    children::Vector{GBDTNode}
end
function GBDTNode(id::Int, label::Int)
    GBDTNode(id, label, nothing, GBDTNode[])
end

"""
    GBDT

GBDT model produced by induce_tree.
"""
struct GBDT
    tree::GBDTNode
    catdisc::Union{Nothing,CategoricalDiscretizer}
end

"""
    Counter

Mutable int counter
"""
mutable struct Counter
    i::Int
end

"""
    id(node::GBDTNode)

Returns the node id.
"""
id(node::GBDTNode) = node.id
"""
    label(node::GBDTNode)

Returns the node label.
"""
label(node::GBDTNode) = node.label
"""
    gbes_result(node::GBDTNode)

Returns the result from GBES.
"""
gbes_result(node::GBDTNode) = node.gbes_result
"""
    isleaf(node::GBDTNode)

Returns true if node is a leaf.
"""
isleaf(node::GBDTNode) = isempty(node.children)

#AbstractTrees interface
AbstractTrees.children(node::GBDTNode) = node.children
function AbstractTrees.printnode(io::IO, node::GBDTNode)
    print(io, "$(node.id): label=$(node.label)")
    r = node.gbes_result
    if r != nothing
        print(io, ", loss=$(round(r.loss;digits=2)), $(r.expr)")
    end
end

"""
    ishomogeneous(v::AbstractVector{T}) where T

Returns true if all elements in v are the same.
"""
ishomogeneous(v::AbstractVector{T}) where T = length(unique(v)) == 1

"""
    gini_loss(node::RuleNode, grammar::Grammar, X::AbstractVector{T}, y_truth::AbstractVector{Int},
                     members::AbstractVector{Int}, eval_module::Module;
                     w1::Float64=100.0,
                     w2::Float64=0.1) where T

Default loss function based on gini impurity and number of nodes in the derivation tree.
See Lee et al. "Interpretable categorization of heterogeneous time series data"
"""
function gini_loss(node::RuleNode, grammar::Grammar, X::AbstractVector{T}, y_truth::AbstractVector{Int},
                     members::AbstractVector{Int}, eval_module::Module;
                     w1::Float64=100.0,
                     w2::Float64=0.1) where T
    ex = get_executable(node, grammar)
    y_bool = partition(X, members, ex, eval_module)
    members_true, members_false = members_by_bool(members, y_bool)
    return w1*gini(y_truth[members_true], y_truth[members_false]) + w2*length(node)
end

"""
    induce_tree(grammar::Grammar, typ::Symbol, p::ExprOptAlgorithm, X::AbstractVector{XT},
                        y::AbstractVector{YT}, max_depth::Int, loss::Function=gini_loss,
                        eval_module::Module=Main) where {XT,YT}

Learn a GBDT from labeled data.  Categorical labels are converted to integers.
# Arguments:
- `grammar::Grammar`: grammar
- `typ::Symbol`: start symbol
- `p::ExprOptAlgorithm`: Parameters for ExprOptimization algorithm
- `X::AbstractVector{XT}`: Input data features, e.g., a MultivariateTimeSeries
- `y::AbstractVector{YT}`: Input (class) labels.
- `max_depth::Int`: Maximum depth of GBDT.
- `loss::Function`: Loss function.  See gini_loss() for function signature.
- `eval_module::Module`: Module in which expressions are evaluated.
"""
function induce_tree(grammar::Grammar, typ::Symbol, p::ExprOptAlgorithm, X::AbstractVector{XT},
                        y::AbstractVector{YT}, max_depth::Int, loss::Function=gini_loss,
                        eval_module::Module=Main; kwargs...) where {XT,YT}
    catdisc = CategoricalDiscretizer(y)
    y_truth = encode(catdisc, y)
    induce_tree(grammar, typ, p, X, y_truth, max_depth, loss, eval_module;
                catdisc=(catdisc), kwargs...)
end
"""
    induce_tree(grammar::Grammar, typ::Symbol, p::ExprOptAlgorithm, X::AbstractVector{T},
                        y_truth::AbstractVector{Int}, max_depth::Int, loss::Function=gini_loss,
                        eval_module::Module=Main;
                        catdisc::Union{Nothing,CategoricalDiscretizer}=nothing),
                        verbose::Bool=false) where T

Learn a GBDT from labeled data.
# Arguments:
- `grammar::Grammar`: grammar
- `typ::Symbol`: start symbol
- `p::ExprOptAlgorithm`: Parameters for ExprOptimization algorithm
- `X::AbstractVector{XT}`: Input data features, e.g., a MultivariateTimeSeries
- `y_truth::AbstractVector{Int}`: Input (class) labels.
- `max_depth::Int`: Maximum depth of GBDT.
- `loss::Function`: Loss function.  See gini_loss() for function signature.
- `eval_module::Module`: Module in which expressions are evaluated.
- `catdisc::Union{Nothing,CategoricalDiscretizer}`: Discretizer used for converting the labels.
- `min_members_per_branch::Int`: Minimum number of members for a valid branch.
- `prevent_same_label::Bool`: Prevent split if both branches have the same dominant label
- `verbose::Bool`: Verbose outputs
"""
function induce_tree(grammar::Grammar, typ::Symbol, p::ExprOptAlgorithm, X::AbstractVector{T},
                        y_truth::AbstractVector{Int}, max_depth::Int, loss::Function=gini_loss,
                        eval_module::Module=Main;
                        catdisc::Union{Nothing,CategoricalDiscretizer}=nothing,
                        min_members_per_branch::Int=0,
                        prevent_same_label::Bool=true,
                        verbose::Bool=false) where T
    verbose && println("Starting...")
    @assert length(X) == length(y_truth)
    members = collect(1:length(y_truth))
    node_count = Counter(0)
    node = _split(node_count, grammar, typ, p, X, y_truth, members, max_depth, loss, eval_module,
                 min_members_per_branch=min_members_per_branch,
                 prevent_same_label=prevent_same_label, verbose=verbose)
    return GBDT(node, catdisc)
end
function _split(node_count::Counter, grammar::Grammar, typ::Symbol, p::ExprOptAlgorithm,
                       X::AbstractVector{T}, y_truth::AbstractVector{Int},
                       members::AbstractVector{Int},
                       d::Int, loss::Function, eval_module::Module;
                       min_members_per_branch::Int=0,
                       prevent_same_label::Bool=true,
                       verbose::Bool=false) where T
    id = node_count.i += 1  #assign ids in preorder
    if d == 0 || ishomogeneous(y_truth[members])
        return GBDTNode(id, mode(y_truth[members]))
    end

    #gbes
    gbes_result = optimize(p, grammar, typ, (node,grammar)->loss(node, grammar, X, y_truth,
        members, eval_module); verbose=verbose)
    y_bool = partition(X, members, gbes_result.expr, eval_module)
    members_true, members_false = members_by_bool(members, y_bool)

    #don't create split if split doesn't result in two valid groups
    if length(members_true) <= min_members_per_branch || length(members_false) <= min_members_per_branch
        return GBDTNode(id, mode(y_truth[members]))
    end

    #don't create split if both sides of the split have the same dominant label
    if prevent_same_label && (mode(y_truth[members_true]) == mode(y_truth[members_false]))
        return GBDTNode(id, mode(y_truth[members]))
    end

    child_true = _split(node_count, grammar, typ, p, X, y_truth, members_true, d-1,
        loss, eval_module;
        min_members_per_branch=min_members_per_branch,
        prevent_same_label=prevent_same_label,
        verbose=verbose)
    child_false = _split(node_count, grammar, typ, p, X, y_truth, members_false, d-1,
        loss, eval_module;
        min_members_per_branch=min_members_per_branch,
        prevent_same_label=prevent_same_label,
        verbose=verbose)

    return GBDTNode(id, mode(y_truth[members]), gbes_result, [child_true, child_false])
end

"""
    partition(X::AbstractVector{T}, members::AbstractVector{Int}, expr, eval_module::Module) where T

Returns a Boolean vector of length members containing the results of evaluating expr on each member.  Expressions are evaluated in eval_module.
"""
function partition(X::AbstractVector{T}, members::AbstractVector{Int}, expr, eval_module::Module) where T
    y_bool = Vector{Bool}(undef, length(members))
    for i in eachindex(members)
        @eval eval_module x = $(X[members[i]])
        y_bool[i] = Core.eval(eval_module, expr) #use x in expression
    end
    y_bool
end

"""
    members_by_bool(members::AbstractVector{Int}, y_bool::AbstractVector{Bool})

Returns a tuple containing the results of splitting members by the Boolean values in y_bool.
"""
function members_by_bool(members::AbstractVector{Int}, y_bool::AbstractVector{Bool})
    @assert length(y_bool) == length(members)
    return members[findall(y_bool)], members[findall(!,y_bool)]
end

"""
    gini(v1::AbstractVector{T}, v2::AbstractVector{T}) where T

Returns the gini impurity of v1 and v2 weighted by number of elements.
"""
function gini(v1::AbstractVector{T}, v2::AbstractVector{T}) where T
    N1, N2 = length(v1), length(v2)
    return (N1*gini(v1) + N2*gini(v2)) / (N1+N2)
end
"""
    gini(v::AbstractVector{T}) where T

Returns the Gini impurity of v.  Returns 0.0 if empty.
"""
function gini(v::AbstractVector{T}) where T
    isempty(v) && return 0.0
    return 1.0 - sum(abs2, proportions(v))
end

"""
    Base.length(model::GBDT)

Returns the number of vertices in the GBDT.
"""
Base.length(model::GBDT) = length(model.tree)
"""
    Base.length(root::GBDTNode)

Returns the number of vertices in the tree rooted at root.
"""
function Base.length(root::GBDTNode)
    retval = 1
    for c in root.children
        retval += length(c)
    end
    return retval
end

Base.show(io::IO, model::GBDT) = Base.show(io::IO, model.tree)
Base.show(io::IO, tree::GBDTNode) = print_tree(io, tree)

"""
    Base.display(model::GBDT; edgelabels::Bool=false)

Returns a TikzGraphs plot of the tree.  Turn off edgelabels for cleaner plot.  Left branch is true, right branch is false.
"""
function Base.display(model::GBDT; kwargs...)
    display(model.tree, model.catdisc; kwargs...)
end
"""
    Base.display(root::GBDTNode, catdisc::Uniont{Nothing,CategoricalDiscretizer}=nothing;
                     edgelabels::Bool=false)

Returns a TikzGraphs plot of the tree.  Turn off edgelabels for cleaner plot.  Left branch is true, right branch is false.
If catdisc is supplied, use it to decode the labels.
"""
function Base.display(root::GBDTNode, catdisc::Union{Nothing,CategoricalDiscretizer}=nothing;
                     edgelabels::Bool=false)
    n_nodes = length(root)
    g = DiGraph(n_nodes)
    text_labels, edge_labels = Vector{String}(undef,n_nodes), Dict{Tuple{Int,Int},String}()
    for node in PreOrderDFS(root)
        if node.gbes_result != nothing
            r = node.gbes_result
            text_labels[node.id] = string("$(node.id): $(verbatim(string(r.expr)))")
        else
            label = catdisc != nothing ?  decode(catdisc, node.label) : node.label
            text_labels[node.id] = string("$(node.id): label=$(label)")
        end
        for (i, ch) in enumerate(node.children)
            add_edge!(g, node.id, ch.id)
            edge_labels[(node.id, ch.id)] = i==1 ? "True" : "False"
        end
    end
    if edgelabels
        return TikzGraphs.plot(g, text_labels; edge_labels=edge_labels)
    else
        return TikzGraphs.plot(g, text_labels)
    end
end
#Stay in text mode, escape some latex characters
function verbatim(s::String)
    s = replace(s, "_"=>"\\_")
end

"""
    classify(model::GBDT, X::AbstractVector{T}, members::AbstractVector{Int},
                     eval_module::Module=Main) where T

Predict classification label of each member using GBDT model.  Evaluate expressions in eval_module.
"""
function classify(model::GBDT, X::AbstractVector{T},
    members::AbstractVector{Int}=collect(1:length(X)),
    eval_module::Module=Main) where T

    classify(model.tree, X, members, eval_module; catdisc=model.catdisc)
end
"""
    classify(node::GBDTNode, X::AbstractVector{T}, members::AbstractVector{Int}, eval_module::Module=Main;
                     catdisc::Union{Nothing,CategoricalDiscretizer}=nothing) where T

Predict classification label of each member using GBDT tree.  Evaluate expressions in eval_module.  If catdisc is available, use discretizer to decode labels.
"""
function classify(node::GBDTNode, X::AbstractVector{T},
    members::AbstractVector{Int}=collect(1:length(X)),
    eval_module::Module=Main;
    catdisc::Union{Nothing,CategoricalDiscretizer}=nothing) where T

    y_pred = Vector{Int}(undef,length(members))
    for i in eachindex(members)
        @eval eval_module x=$(X[i])
        y_pred[i] = _classify(node, eval_module)
    end
    if catdisc == nothing
        return y_pred
    else
        return decode(catdisc, y_pred)
    end
end
function _classify(node::GBDTNode, eval_module::Module)
    isleaf(node) && return node.label

    ex = get_expr(node.gbes_result)
    ch =  Core.eval(eval_module, ex) ? node.children[1] : node.children[2]
    return _classify(ch, eval_module)
end

"""
    node_members(model::GBDT, X::AbstractVector{T}, members::AbstractVector{Int},
                      eval_module::Module=Main) where T

Returns the members of each node in the tree.
"""
function node_members(model::GBDT, X::AbstractVector{T}, members::AbstractVector{Int},
                      eval_module::Module=Main) where T
    node_members(model.tree, X, members, eval_module)
end
"""
    node_members(node::gbdtnode, x::abstractvector{t}, members::abstractvector{int},
                      eval_module::module=main) where T

returns the members of each node in the tree.
"""
function node_members(node::GBDTNode, X::AbstractVector{T}, members::AbstractVector{Int},
                      eval_module::Module=Main) where T
    mvec = Vector{Vector{Int}}(undef,length(node))
    _node_members!(mvec, node, X, members, eval_module)
    mvec
end
function _node_members!(mvec::Vector{Vector{Int}}, node::GBDTNode, X::AbstractVector{T},
                        members::AbstractVector{Int}, eval_module::Module) where T
    mvec[node.id] = deepcopy(members)
    isleaf(node) && return

    ex = get_expr(node.gbes_result)
    y_bool = partition(X, members, ex, eval_module)
    members_true, members_false = members_by_bool(members, y_bool)
    _node_members!(mvec, node.children[1], X, members_true, eval_module)
    _node_members!(mvec, node.children[2], X, members_false, eval_module)
end

"""
    Base.getindex(model::GBDT, id::Int)

returns node with id
"""
function Base.getindex(model::GBDT, id::Int)
    for node in PreOrderDFS(model.tree)
        if node.id == id
            return node
        end
    end
    error("node id not found")
end

"""
    children_id(node::GBDTNode)

returns a vector that contains the node ids of the children of node
"""
children_id(node::GBDTNode) = Int[c.id for c in children(node)]

"""
   get_expr(node::GBDTNode)

returns the expression of the node
"""
ExprOptimization.get_expr(node::GBDTNode) = get_expr(node.gbes_result)

end # module


#ДАНО: Робот - в произвольной клетке ограниченного прямоугольного поля, на котором могут находиться также внутренние
#прямоугольные перегородки (все перегородки изолированы друг от друга, прямоугольники могут вырождаться в отрезки)
#РЕЗУЛЬТАТ: Робот - в исходном положении, и в 4-х приграничных клетках, две из которых имеют ту же широту,
#а две - ту же долготу, что и Робот, стоят маркеры.
function move_to_startplace!(r::Robot)
    x = 0
    y = 0
    while !(isborder(r, HorizonSide(2)) && isborder(r, HorizonSide(3)))
        while !isborder(r, HorizonSide(2))
            move!(r, HorizonSide(2))
            y+=1
        end
        while !isborder(r, HorizonSide(3))
            move!(r, HorizonSide(3))
            x+=1
        end
    end
    return (x, y)
end

function move_to_beginplace!(r::Robot, X, Y)
    x = X
    y = Y

    while x != 0 || y != 0
        while x != 0
            if x > 0
                if isborder(r, HorizonSide(1))
                    move!(r, HorizonSide(0))
                    y-=1
                end
                if !isborder(r, HorizonSide(1))
                    move!(r, HorizonSide(1))
                    x -= 1
                end
            elseif x < 0
                if isborder(r, HorizonSide(3))
                    move!(r, HorizonSide(0))
                    y-=1
                end
                if !isborder(r, HorizonSide(3))
                    move!(r, HorizonSide(3))
                    x += 1
                end
            end
        end
        while y != 0

            if y > 0
                if isborder(r, HorizonSide(0))
                    move!(r, HorizonSide(1))
                    x-=1
                end
                if !isborder(r, HorizonSide(0))
                    move!(r, HorizonSide(0))
                    y -= 1
                end
            elseif y < 0
                if isborder(r, HorizonSide(2))
                    move!(r, HorizonSide(1))
                    x-=1
                end
                if !isborder(r, HorizonSide(2))
                    move!(r, HorizonSide(2))
                    y += 1
                end
            end
        end
    end
end

function move_and_put!(r::Robot, X, Y)
    x = X
    y = Y
    n = 0 # n(x)
    m = 0 # m(y)

    for side in 0:3
        if side == 0
            while !isborder(r, HorizonSide(side))
                while y > 0
                    move!(r, HorizonSide(side))
                    y-=1
                end
                if y == 0
                    putmarker!(r)
                end
                if isborder(r, HorizonSide(side))
                    break
                end
                move!(r, HorizonSide(side))
                y-=1
            end
        end
        if side == 1
            while !isborder(r, HorizonSide(side))
                while x > 0
                    move!(r, HorizonSide(side))
                    x-=1
                end
                if x == 0
                    putmarker!(r)
                end
                if isborder(r, HorizonSide(side))
                    break
                end
                move!(r, HorizonSide(side))
                x-=1
            end
        end
        if side == 2
            while !isborder(r, HorizonSide(side))
                while y < 0
                    move!(r, HorizonSide(side))
                    y += 1
                end
                if y == 0
                    putmarker!(r)
                end
                if isborder(r, HorizonSide(side))
                    break
                end
                y+=1
                move!(r, HorizonSide(side))
            end
        end
        if side == 3
            while !isborder(r, HorizonSide(side))
                while x < 0
                    move!(r, HorizonSide(side))
                    x+=1
                end
                if x == 0
                    putmarker!(r)
                end
                if isborder(r, HorizonSide(side))
                    break
                end
                x+=1
                move!(r, HorizonSide(side))
            end
        end
    end
end

function naprotiv(r::Robot)
    x, y = move_to_startplace!(r)
    move_and_put!(r, x, y)
    move_to_startplace!(r)
    move_to_beginplace!(r, x, y)
end


module DeltaSugar
    using Base, Flux
    using Zygote: @adjoint

    abstract type SugarLayer; end
    abstract type GradientHook; end

    include("GradientModifiers.jl")
    export composer
    export GradientHook, IdentityHook,
            L1Hook, L2Hook,
            ReverseGradientHook, GradientClamp

    include.( [ "DenseLayer.jl", "TenseLayer.jl" ] )
    export DenseLayer, show,
            TenseLayer#, show

    include("LayerSugar.jl")
    export L1, L2, RevGrad, ClampHook,
            L1!, L2!, RevGrad!, ClampHook!

    include("SweeterChains.jl")
    export PlainChain, TChain

    include("Train.jl")
    export train_light!, scalar_yeild, log2_yeild, smart_yield, train_loop!#, early_stop_loop!

    #debauchery
    include("BaseOperators.jl")
    export lessapprox, gtapprox, ≲, ⪅, ≳, ⪆, ⊥

    include("Memoize.jl")
    export memoize

    include("AlphaIndices.jl")
    export second,third,fourth,fifth,sixth,seventh,eigth,
            ninth,tenth,eleventh,twelfth,thirteenth,
            fourteenth,fifteenth,sixteenth,seventeenth,
            eighteenth,nineteenth,twentieth
end


@testset "Table Joins for Several Pairs" begin
    import TexTables: IndexedTable, TableCol, join_table
    # Tests for issue in PR #28 (makes sure that the recursive table join
    # implementation on pairs works properly)
    n = 5
    keys = ["x$i" for i in 1:5]
    cols = Vector{IndexedTable{1,1}}(undef, 4)
    for j in 1:4
        cols[j] = hcat(TableCol("mean", keys, rand(n)),
                       TableCol("std", keys, rand(n)))
    end

    # Test that the construction doesn't error out with 4 pairs
    for k in 1:4
        @test begin
            tab = join_table(collect("($j)" => cols[j] for j in 1:k)...)
            true
        end
    end
end


using RandomNumbers.MersenneTwisters
if !@isdefined RandomNumbers
    include("../common.jl")
end

@info "Testing MersenneTwisters"
stdout_ = stdout
pwd_ = pwd()
cd(dirname(@__FILE__))
rm("./actual"; force=true, recursive=true)
mkpath("./actual")

@test seed_type(MT19937) == NTuple{RandomNumbers.MersenneTwisters.N, UInt32}

for mt_name in (:MT19937, )

    outfile = open(string(
        "./actual/check-$(lowercase("$mt_name")).out"
    ), "w")
    redirect_stdout(outfile)

    @eval $mt_name()
    x = @eval $mt_name(123)
    @test copyto!(copy(x), x) == x

    for i in 1:100
        @printf "%.9f\n" rand(x)
    end

    close(outfile)
end
redirect_stdout(stdout_)

compare_dirs("expected", "actual")
cd(pwd_)


#!/usr/bin/env julia

using Luxor, Test

function drawcircles(Δα)
    @layer begin
        setline(8)
        for r = 0:20:1500
            α = r / 100  + Δα
            circle(O + sqrt(r ) .* (cos(α), sin(α)), r, :stroke)
            settext("<span font = '16' foreground = 'darkblue' > $(string(r)) </span>",
                 O + (0, 8 - r ); markup = true)
        end
    end
end

fna(i, suf) = "test_snap_$i.$suf"

function testsnapshot(fname; cb = missing, scalefactor = missing, kwargs...)
    if ismissing(cb)
        sn = if ismissing(scalefactor)
            snapshot(;fname = fname, kwargs...)
        else
            snapshot(;fname = fname, scalefactor = scalefactor, kwargs...)
        end
        @test typeof(sn) == Drawing
    else
        if ismissing(scalefactor)
            sn = snapshot(;fname = fname, cb = cb, kwargs...)
            @test sn.width == boxwidth(cb) && sn.height == boxheight(cb)
        else
            sn = snapshot(;fname = fname, cb = cb, scalefactor = scalefactor, kwargs...)
            @test sn.width == round(boxwidth(cb) * scalefactor)
            @test sn.height == round(boxheight(cb) * scalefactor)
        end
    end
    println("...finished test: output in $fname")
    sn
end
function circles_then_snapshot(fno, suf; kwargs...)
    drawcircles(0.5 * fno)
    testsnapshot(fna(fno, suf); kwargs...)
end

function frame_then_snapshot(fno, suf, cb; kwargs...)
    @layer begin
        setline(2)
        sethue("red")
        box(cb, :stroke)
    end
    testsnapshot(fna(fno, suf); cb = cb, kwargs...)
end

function ripples_then_snapshot(fno, suf; kwargs...)
    ctm = cairotojuliamatrix(getmatrix())
    # User space coordinates for device top
    midtopx, midtopy, _ = ctm^-1 * [-ctm[1, 3] + currentdrawing().width / 2, -ctm[2, 3], 0.0]
    colos = Luxor.Colors.colormap("blues")
    for ulr in range(0, 1, length = 3 * 577)
        r = 2 * 577 * ulr^1.5
        # phase
        ϕ = 20 * sin(40ulr)
        sethue(colos[80 + Int(round(ϕ))])
        p = Point(midtopx, midtopy + 2ϕ)
        circle(p, r, :stroke)
        # Add some rays for showing shear transformation
        sethue("yellow")
        [circle(p + (r * cos(α), -r * sin(α)), 2, :stroke) for α in [0.25, 0.75, 1.25, 1.75]π]
    end
    testsnapshot(fna(fno, suf); kwargs...)
end

# 1-4: Full drawing snapshot from bounded recording surface
Drawing(640, 480, :rec)
origin(400, 280)
sethue("gold4")
circles_then_snapshot(1, "png")
sethue("gold3")
circles_then_snapshot(2, "pdf")
sethue("gold2")
circles_then_snapshot(3, "svg")
sethue("gold1")
circles_then_snapshot(4, "png", scalefactor = 0.5)

# 5-6: Crop snapshots from bounded recording surface
ptul = -Point(getmatrix()[5:6]...) # origin is not at center
ptbr = ptul + (currentdrawing().width, currentdrawing().height)
cb = BoundingBox(ptul, ptbr) - 50
frame_then_snapshot(5, "png", cb)

cb = BoundingBox(box(Point(10, -115), Point(42, -91), :stroke))
frame_then_snapshot(6, "png", cb)

# 7: Crop and scale up
frame_then_snapshot(7, "png", cb, scalefactor = 20)

# 8: Shrink full image
origin()
cb = BoundingBox()
frame_then_snapshot(8, "png", cb, scalefactor = 0.5)

# 9-10: Crop snapshot from free extents recording surface

# It seems that the svg device called by Cairo fails to find
# a size reference from unbounded recording surfaces. Botched up svgs
# include "-1.#QNAN", or very long strings. This occurs with and
# without 'Pro api' text on the drawing.
# However, it seems that if an svg has previously been rendered from a surface
# with bounds, the svg renderer works normally with unbounded surfaces.

Drawing(NaN, NaN, :rec)
@test snapshot() == false
origin(400, 280)
sethue("gold4")
cb = BoundingBox(Point(-128, -96), Point(128, 96))
circles_then_snapshot(9, :png; cb = cb)

# Large image, more effective storage as vector type svg
cb = BoundingBox(Point(-1280, -960), Point(1280, 960))
circles_then_snapshot(10, "svg"; cb = cb)

# 11-13: Full drawing snapshot from transformed recording surface
Drawing(640, 480, :rec)
origin()
scale(0.5)
ripples_then_snapshot(11, "png")
scale(2)
ripples_then_snapshot(12, "png")
origin()
scale(1, 0.5)
ctm = getmatrix()
ctm[3] = 0.1     # shear y
setmatrix(ctm)
rotate(π / 15)
ripples_then_snapshot(13, "png")

# 14-16: Crop and scale snapshot from transformed recording surface
pul = Point(-240, -380)
fontsize(36)
text("ul", pul, valign = :top)
pur = Point(80, -380)
text("ur", pur, valign = :top)
pbr = Point(80, -140)
text("br", pbr, valign = :top)
pbl = Point(-240, -140)
text("Bottom left", pbl, valign = :bottom)
cb = BoundingBox(pul, pbr)
frame_then_snapshot(14, "png", cb)
frame_then_snapshot(15, "png", cb, scalefactor = 2)
testsnapshot(fna(16, "png"))
finish()


#
# Copyright (c) 2021 Tobias Thummerer, Lars Mikelsons, Josef Kircher
# Licensed under the MIT license. See LICENSE file in the project root for details.
#

using FMI

pathToFMU = joinpath(dirname(@__FILE__), "../model/SpringFrictionPendulum1D.fmu")

myFMU = fmiLoad(pathToFMU)

display("Event Indicators: $(myFMU.modelDescription.numberOfEventIndicators)")

fmiInstantiate!(myFMU; loggingOn=true)

fmiSetupExperiment(myFMU, 0.0)

fmiEnterInitializationMode(myFMU)
fmiExitInitializationMode(myFMU)

dt = 0.01
t_start = 0.0
t_stop = 8.0

ts = t_start:dt:t_stop
ss = []

solution = fmiSimulateME(myFMU, dt, t_start, t_stop)
fmiPlot(myFMU, solution)

fmiUnload(myFMU)


abstract type AbstractPool end

struct EmptyPool end

@enum PoolStatus empty_pool=0 halfway_pool unsat_pool mixed_pool full_pool

mutable struct _Pool{T} <: AbstractPool
    best::Int
    configurations::Vector{Configuration{T}}
    status::PoolStatus
    value::Float64
end

const Pool = Union{EmptyPool, _Pool}

pool() = EmptyPool()
function pool(config::Configuration)
    best = 1
    configs = [deepcopy(config)]
    status = full_pool
    value = get_value(config)
    return _Pool(best, configs, status, value)
end

is_empty(::EmptyPool) = true
is_empty(pool) = isempty(pool.configurations)
best_config(pool) = pool.configurations[pool.best]
best_value(pool) = pool.value
best_values(pool) = get_values(best_config(pool))

has_solution(::EmptyPool) = false
has_solution(pool::_Pool) = is_solution(best_config(pool))


using BenchmarkTools
using DataFrames
using Statistics
using Transducers
using VegaLite

resultpath = joinpath(@__DIR__, "result.json")
result, = BenchmarkTools.load(resultpath)

df_raw =
    BenchmarkTools.leaves(result) |>
    Map() do ((nworks, ex), trial)
        (nworks = parse(Int, nworks), executor = ex, trial = trial)
    end |>
    DataFrame
#-

begin
    df_tmp = select(df_raw, Not(:trial))
    df_tmp[!, :minimum] = map(trial -> minimum(trial).time, df_raw.trial)
    df_tmp[!, :median] = map(trial -> median(trial).time, df_raw.trial)
    df_tmp[!, :memory] = map(trial -> trial.memory, df_raw.trial)
    df_stats = stack(
        df_tmp,
        [:minimum, :median],
        variable_name = :time_stat,
        value_name = :time_ns,
    )
end
#-

df = combine(groupby(df_stats, [:nworks, :time_stat])) do group
    d = Dict(zip(group.executor, group.time_ns))
    (speedup = d["ThreadedEx"] / d["WorkStealingEx"],)
end
#-

plt1 = @vlplot(
    layer = [
        {
            mark = {type = :line, point = true},
            encoding = {
                x = {field = :nworks},
                y = {field = :speedup, axis = {title = "Speedup (T_default / T_WS)"}},
                color = {field = :time_stat},
            },
        },
        {
            mark = :rule,
            encoding = {y = {datum = 1}},
        },
    ],
    data = df,
    width = 400,
    height = 200,
)
#-

plt2 = @vlplot(
    mark = {type = :line, point = true},
    x = :nworks,
    y = {field = :time_ns, axis = {title = "Time [ns]"}},
    color = {field = :time_stat},
    column = :executor,
    data = df_stats,
)
#-


function viewshape(img::Image, shape::Shape)
    imgc, img2 = ImageView.view(img)
    for i=1:size(shape, 1)
        annotate!(imgc, img2, AnnotationPoint(shape[i, 2], shape[i, 1], shape='.',
                                              size=4, color=RGB(1, 0, 0)))
    end
    imgc, img2
end
viewshape{N}(mat::Array{Float64, N}, shape::Shape) = viewshape(convert(Image, mat), shape)


module Metatheory

using Base.Meta
using Reexport
using TermInterface

macro log(args...)
  quote
    haskey(ENV, "MT_DEBUG") && @info($(args...))
  end |> esc
end

@inline alwaystrue(x) = true

include("docstrings.jl")
include("utils.jl")
export @timer
export @iftimer
export @timerewrite
export @matchable

include("Patterns.jl")
@reexport using .Patterns

include("ematch_compiler.jl")
@reexport using .EMatchCompiler

include("matchers.jl")
include("Rules.jl")
@reexport using .Rules

include("Syntax.jl")
@reexport using .Syntax
include("EGraphs/EGraphs.jl")
@reexport using .EGraphs

include("Library.jl")
export Library

include("Rewriters.jl")
using .Rewriters
export Rewriters

function rewrite(expr, theory; order = :outer)
  if order == :inner
    Fixpoint(Prewalk(Fixpoint(Chain(theory))))(expr)
  elseif order == :outer
    Fixpoint(Postwalk(Fixpoint(Chain(theory))))(expr)
  end
end
export rewrite

end # module


#= ######################################################
model2D - plotting functions
###################################################### =#

function plot_model(fig, ax, m::Model; shownvec = false, shownodes = false, showcom = false, showleg = true, colors = to_colormap(:rainbow, m.npar), linewidth = 3.0, legpos = "bottom")
    # plot model
    elem_lines = Vector(undef,m.npar)
    plt_elem = Vector(undef,m.npar)
    plt_nodes = Vector(undef,m.npar)
    plt_com = Vector(undef,m.npar)
    plt_nvec = Vector(undef,m.npar)
    # colors = [:red, :green, :blue, :orange]
    # colors = to_colormap(:rainbow, m.npar)
    for p = 1:m.npar
        erange = m.elem2par[p].first:m.elem2par[p].last
        # plot elements
        elem_lines[p] = [Point2f0(m.nodes[m.elem[i].node1].x, m.nodes[m.elem[i].node1].y) =>
                    Point2f0(m.nodes[m.elem[i].node2].x, m.nodes[m.elem[i].node2].y) for i in erange]
        plt_elem[p] = linesegments!(ax, elem_lines[p], color = colors[p], linewidth = linewidth)
        # plot nodes
        if shownodes == true
            local nodes_points = [Point2f0(m.nodes[m.elem[i].node1].x, m.nodes[m.elem[i].node1].y) for i in erange]
            append!(nodes_points, [Point2f0(m.nodes[m.elem[i].node2].x, m.nodes[m.elem[i].node2].y) for i in erange])
            plt_nodes[p] = scatter!(ax, nodes_points, color = colors[p], markersize = 10)
        end
        # plot com
        if showcom == true
            local com_points = [Point2f0(m.elem[i].com.x, m.elem[i].com.y) for i in erange]
            plt_com[p] = scatter!(ax, com_points, color = :grey, markersize = 10)
        end
        # plot nvec as linesegments
        if shownvec == true
            local com_points = [Point2f0(m.elem[i].com.x, m.elem[i].com.y) for i in erange]
            # local nvec_lines = [Point2f0(m.elem[i].com.x, m.elem[i].com.y) =>
            #             Point2f0(m.elem[i].com.x + m.elem[i].nvec.x, m.elem[i].com.y + m.elem[i].nvec.y) for i in erange]
            # plt_nvec[p] = linesegments!(ax, nvec_lines, color = colors[p], linewidth = linewidth)
            # plot nvec as arrows
            scale = 0.1 # default 0.3
            arrowsize = 0.01 # default 0.05
            local nvec_directions = [Point2f0(m.elem[i].nvec.x * scale, m.elem[i].nvec.y * scale) for i in erange]
            lw_arrow = linewidth * 0.5
            arrows!(ax, com_points, nvec_directions, arrowcolor = colors[p], arrowsize = arrowsize, linecolor = colors[p], linewidth = lw_arrow)
        end
    end
    # legend
    if showleg == true
        leg_string = ["part $i" for i = 1:m.npar]
        if legpos == "right"
            leg = fig[1, end+1] = Legend(fig, plt_elem, leg_string)
        elseif legpos == "bottom"
            leg = fig[end+1, 1] = Legend(fig, plt_elem, leg_string, orientation = :horizontal, tellwidth = false, tellheight = true)
        end
        # leg.padding = (5.0f0, 5.0f0, 4.0f0, 4.0f0)
        # leg.linewidth = 1.0
        # leg.patchsize = (5, 3)
        # leg.rowgap = 0.1
        # leg.colgap = 8
        # axislegend(ax, plt_elem, leg_string, position = :rt, orientation = :horizontal)
    end
end

function plot_model_elements(fig, ax, m::Model, erange; shownvec = false, shownodes = false, showcom = false, color = :cyan, linewidth = 3.0, linestyle = nothing)
    # plot only given elements
    # color = :cyan
    # plot elements
    elem_lines = [Point2f0(m.nodes[m.elem[i].node1].x, m.nodes[m.elem[i].node1].y) =>
                Point2f0(m.nodes[m.elem[i].node2].x, m.nodes[m.elem[i].node2].y) for i in erange]
    plt_elem = linesegments!(ax, elem_lines, color = color, linewidth = linewidth, linestyle = linestyle)
    # plot nodes
    if shownodes == true
        local nodes_points = [Point2f0(m.nodes[m.elem[i].node1].x, m.nodes[m.elem[i].node1].y) for i in erange]
        append!(nodes_points, [Point2f0(m.nodes[m.elem[i].node2].x, m.nodes[m.elem[i].node2].y) for i in erange])
        plt_nodes = scatter!(ax, nodes_points, color = color, markersize = 10)
    end
    # plot com
    if showcom == true
        local com_points = [Point2f0(m.elem[i].com.x, m.elem[i].com.y) for i in erange]
        plt_com = scatter!(ax, com_points, color = :grey, markersize = 10)
    end
    # plot nvec as linesegments
    if shownvec == true
        local com_points = [Point2f0(m.elem[i].com.x, m.elem[i].com.y) for i in erange]
        # plot nvec as arrows
        scale = 0.1
        arrowsize = 0.01 # default 0.05
        local nvec_directions = [Point2f0(m.elem[i].nvec.x * scale, m.elem[i].nvec.y * scale) for i in erange]
        lw_arrow = linewidth * 0.5
        arrows!(ax, com_points, nvec_directions, arrowcolor = color, arrowsize = arrowsize, linecolor = color, linewidth = lw_arrow)
    end
end

#= ######################################################
raycast2D - plotting functions
###################################################### =#

function plot_empty_tiles(fig, ax, dx::T1, dy::T1, n::T2; linewidth = 3.0) where {T1<:AbstractFloat, T2<:Integer}
    # plot empty tiles
    # plot x lines
    t_lines = [Point2f0(i*dx, 0) => Point2f0(i*dx, n*dy) for i = 0:1:n]
    plt_elem = linesegments!(ax, t_lines, color = :black, linewidth = linewidth)
    # plot y lines
    t_lines = [Point2f0(0, i*dy) => Point2f0(n*dx, i*dy) for i = 0:1:n]
    linesegments!(ax, t_lines, color = :black, linewidth = linewidth)
end

function plot_occupied_tiles(fig, ax, dx::T1, dy::T1, n::T2, t_occ::Matrix{Union{Vector{T2},Missing}}) where {T1<:AbstractFloat, T2<:Integer}
    # plot occ tiles
    for i = 1:n, j = 1:n
        if !ismissing(t_occ[i,j])
            poly!(ax, Rect((i-1)*dx, (j-1)*dy, dx, dy), color = (:red, 0.3))
        end
    end
end

function plot_points_and_connection(fig, ax, p1::Point2D{T}, p2::Point2D{T}) where T<:AbstractFloat
    linesegments!(ax, [Point2f0(p1.x, p1.y) => Point2f0(p2.x, p2.y)], color = :blue, linewidth = 3)
    scatter!(ax, Point2f0(p1.x, p1.y), color = :green, markersize = 15)
    scatter!(ax, Point2f0(p2.x, p2.y), color = :red, markersize = 15)
end

#= ######################################################
view2D - plotting functions
###################################################### =#

function plot_existing(fig, ax, m::Model, e::T, mat; color = :yellow, linewidth = 2) where T<:Integer
    # plot a single element e with all its existing connections
    exist = Vector{T}()
    for i = 1:m.nelem
        if mat[e,i] != 0
            push!(exist, i)
        end
    end
    # plot rays
    # color = :yellow
    ray_lines = [Point2f0(m.elem[e].com.x, m.elem[e].com.y) =>
                Point2f0(m.elem[i].com.x, m.elem[i].com.y) for i in exist]
    plt_ray = linesegments!(ax, ray_lines, color = color, linewidth = linewidth)
end

function plot_model_shadow_1to1(fig, ax, m::Model, mat, source, target)
    # plot specified parts of model with their values in global colormap
    # plot source
    color_source = :yellow
    ns = (m.elem2par[source].last - m.elem2par[source].first + 1)
    source_lines = Vector{Pair{Point{2,Float32},Point{2,Float32}}}(undef,ns)
    erange_source = m.elem2par[source].first:m.elem2par[source].last
    source_lines = [Point2f0(m.nodes[m.elem[i].node1].x, m.nodes[m.elem[i].node1].y) =>
                Point2f0(m.nodes[m.elem[i].node2].x, m.nodes[m.elem[i].node2].y) for i=erange_source]
    # plot target
    nt = (m.elem2par[target].last - m.elem2par[target].first + 1)
    target_lines = Vector{Pair{Point{2,Float32},Point{2,Float32}}}(undef,nt)
    erange_target = m.elem2par[target].first:m.elem2par[target].last
    target_lines = [Point2f0(m.nodes[m.elem[i].node1].x, m.nodes[m.elem[i].node1].y) =>
                Point2f0(m.nodes[m.elem[i].node2].x, m.nodes[m.elem[i].node2].y) for i=erange_target]
    hitcount = zeros(Int64,nt)
    for i = 1:nt
        etarget = m.elem2par[target].first - 1 + i
        hitcount[i] = sum(mat[erange_source,etarget])
    end
    # all parts except source/target
    rest = collect(1:m.npar)
    filter!(x->x != target, rest)
    filter!(x->x != source, rest)
    color_rest = :blue
    nr = 0
    for p in rest
        nr += (m.elem2par[p].last - m.elem2par[p].first + 1)
    end
    rest_lines = Vector{Pair{Point{2,Float32},Point{2,Float32}}}(undef,nr)
    s1 = 0
    s2 = 0
    for p in rest
        s1 = (s2 + 1)
        s2 = (s1 + (m.elem2par[p].last - m.elem2par[p].first))
        erange_rest = m.elem2par[p].first:m.elem2par[p].last
        rest_lines[s1:s2] = [Point2f0(m.nodes[m.elem[i].node1].x, m.nodes[m.elem[i].node1].y) =>
                Point2f0(m.nodes[m.elem[i].node2].x, m.nodes[m.elem[i].node2].y) for i=erange_rest]
    end
    # plot it
    plt_source = linesegments!(ax, source_lines, color = color_source, linewidth = 3)
    plt_target = linesegments!(ax, target_lines, color = hitcount, colormap = Reverse(:Greys_3), linewidth = 3)
    plt_rest = linesegments!(ax, rest_lines, color = color_rest, linewidth = 3)
end

function plot_model_shadow_1toAll(fig, ax, m::Model, mat, source)
    # plot specified parts of model with their values in global colormap
    # plot source
    color_source = :yellow
    ns = (m.elem2par[source].last - m.elem2par[source].first + 1)
    source_lines = Vector{Pair{Point{2,Float32},Point{2,Float32}}}(undef,ns)
    erange_source = m.elem2par[source].first:m.elem2par[source].last
    source_lines = [Point2f0(m.nodes[m.elem[i].node1].x, m.nodes[m.elem[i].node1].y) =>
                Point2f0(m.nodes[m.elem[i].node2].x, m.nodes[m.elem[i].node2].y) for i=erange_source]
    # plot targets
    nt = m.nelem - ns
    target = collect(1:m.npar)
    filter!(x->x != source, target)
    target_lines = Vector{Pair{Point{2,Float32},Point{2,Float32}}}(undef,nt)
    hitcount = zeros(Int64,nt)
    s1 = 0
    s2 = 0
    for p in target
        s1 = (s2 + 1)
        s2 = (s1 + (m.elem2par[p].last - m.elem2par[p].first))
        erange_target = m.elem2par[p].first:m.elem2par[p].last
        target_lines[s1:s2] = [Point2f0(m.nodes[m.elem[i].node1].x, m.nodes[m.elem[i].node1].y) =>
                Point2f0(m.nodes[m.elem[i].node2].x, m.nodes[m.elem[i].node2].y) for i=erange_target]
        k = 0
        for j = s1:s2
            k += 1
            etarget = erange_target[k]
            hitcount[j] = sum(mat[erange_source,etarget])
        end
    end
    # plot it
    plt_source = linesegments!(ax, source_lines, color = color_source, linewidth = 5)
    plt_target = linesegments!(ax, target_lines, color = hitcount, colormap = :coolwarm, linewidth = 5)
    # colorbar
    cbar = fig[1, end+1] = Colorbar(fig, plt_target, label = "hit-intensity")
    cbar.width = 30
    cbar.height = Relative(2/3)
    valmin = minimum(hitcount)
    valmax = maximum(hitcount)
    valdelta = (valmax - valmin) / 10
    cbar.ticks = valmin:valdelta:valmax
end

#= ######################################################
therm2D - plotting functions
###################################################### =#

function plot_model_with_value(fig, ax, m::Model, val::Vector{T}, val_label; parts = 1:m.npar, cmap = :jet, linewidth = 5.0, showcbar = true, cbarpos = "right") where T<:AbstractFloat
    # plot specified parts of model with their values in global colormap
    n = 0
    for p in parts
        n += (m.elem2par[p].last - m.elem2par[p].first + 1)
    end
    pval = Vector{T}(undef,n)
    elem_lines = Vector{Pair{Point{2,Float32},Point{2,Float32}}}(undef,n)
    s1 = 0
    s2 = 0
    for p in parts
        s1 = (s2 + 1)
        s2 = (s1 + (m.elem2par[p].last - m.elem2par[p].first))
        # println(s1, " --> ", s2)
        erange = m.elem2par[p].first:m.elem2par[p].last
        pval[s1:s2] = val[erange]
        elem_lines[s1:s2] = [Point2f0(m.nodes[m.elem[i].node1].x, m.nodes[m.elem[i].node1].y) =>
                Point2f0(m.nodes[m.elem[i].node2].x, m.nodes[m.elem[i].node2].y) for i=erange]
    end
    plt_elem = linesegments!(ax, elem_lines, color = pval, colormap = cmap, linewidth = linewidth)
    # all at once
    # elem_lines = [Point2f0(m.nodes[m.elem[i].node1].x, m.nodes[m.elem[i].node1].y) =>
    #             Point2f0(m.nodes[m.elem[i].node2].x, m.nodes[m.elem[i].node2].y) for i=1:m.nelem]
    # plt_elem = linesegments!(ax, elem_lines, color = val, colormap = :viridis, linewidth = 3)
    # colorbar
    if showcbar == true
        if cbarpos == "right"
            cbar = fig[1, end+1] = Colorbar(fig, plt_elem, label = val_label)
        elseif cbarpos == "bottom"
            cbar = fig[end+1,1] = Colorbar(fig, plt_elem, label = val_label)
            cbar.vertical = false
        end
        # cbar.width = 12
        # cbar.height = Relative(2/3)
        # cbar.labelpadding = 1.0 # default 5.0
        # cbar.ticklabelpad = 1.0 # default 3.0
        # cbar.ticksize = 2.0 # default 6.0
    else
        cbar = 0
    end
    valmin = minimum(pval)
    valmax = maximum(pval)
    valdelta = (valmax - valmin) / 10
    # cbar.ticks = valmin:valdelta:valmax
    println("values between: ", valmin, " and ", valmax)
    return cbar
end

#= ######################################################
others - plotting functions
###################################################### =#

function setup_axis(ax, linewidth)
    # for springer heat and mass transfer
    ax.spinewidth = linewidth
    ax.xtickwidth = linewidth
    ax.ytickwidth = linewidth
    ax.xgridwidth = linewidth
    ax.ygridwidth = linewidth
    ax.xlabelpadding = 0.0 # default 3.0
    ax.ylabelpadding = 0.0
    ax.xticklabelpad = 0.0 # default 2.0
    ax.yticklabelpad = 1.5
    ax.xticksize = 2.0 # default 6.0
    ax.yticksize = 2.0
end


const SFACT = Symbol("@fact")
const SFACT_THROWS = Symbol("@fact_throws")

const COMPARISON_DICT = Dict(
:exactly               => Symbol("==="),
:less_than             => Symbol("<"),
:less_than_or_equal    => Symbol("<="),
:greater_than          => Symbol(">"),
:greater_than_or_equal => Symbol(">="),
)

const PROPERTIES = [:isnan, :isinf, :isfinite, :isodd, :iseven, :isempty]

"""
    fc2ts(x)

Convert expression like object x from FactCheck to TestSet.
"""
fc2ts(x) = x
fc2ts(x::Expr) = fc2ts(x, extrait(x))

function fc2ts(ex::Expr, ::MacroCall{SFACT})
     ex |> fact2testcore |> testex
end

function fc2ts(ex::Expr, ::Any)
    args = map(fc2ts, ex.args)
    return Expr(ex.head, args...)
end

function fc2ts(ex::Expr, ::MacroCall{SFACT_THROWS})
    args = ex.args[2:end]
    if length(args) == 1
        except = :Exception
        code = args[1]
    else
        @assert length(args) == 2
        except, code = args
    end
    :(@test_throws $except $code)
end

function fc2ts(ex::Expr, ::Call{:context})
    name, arg, body = name_arg_body(ex)
    body = fc2ts(body)
    :(@testset $arg $body)
end

function fc2ts(ex::Expr, ::Call{:facts})
    name, arg, body = name_arg_body(ex)
    body = fc2ts(body)
    :(@testset $arg $body)
end

function name_arg_body(doblock::Expr)
    @match doblock begin
        (name_)(arg_) do
            body_
        end => (name, arg, body)
    end
end

fact2testcore(ex) = lhs_rhs2testcore(lhs_rhs(ex)...)

function lhs_rhs(ex)
    @match ex begin
        @fact(lhs_ --> rhs_) => (lhs, rhs)
    end
end

testex(args...) = Expr(:macrocall, Symbol("@test"), args...)

lhs_rhs2testcore(lhs, rhs) = lhs_rhs2testcore(lhs, rhs, extrait(rhs))
lhs_rhs2testcore(lhs, rhs, rtrait) = :($lhs == $rhs)
lhs_rhs2testcore(lhs, rhs::Bool, ::NoExpr) = rhs ? lhs : :(!$lhs)

function lhs_rhs2testcore(lhs, rhs::Symbol, ::NoExpr)
    if rhs in PROPERTIES
        return :($rhs($lhs))
    else
        return :($lhs == $rhs)
    end
end

function lhs_rhs2testcore(lhs, rhs::Expr, ::Call{:not})
    @assert rhs.args[1] == :not
    rhs_new = callarg_unique(rhs)
    ex = lhs_rhs2testcore(lhs, rhs_new)
    :(!$ex)
end

atol2kw(x) = Expr(:kw, :atol, x)
function atol2kw(x::Expr)
    x.head == :kw && return x
    return Expr(:kw, :atol, x)
end

function lhs_rhs2testcore(lhs, rhs::Expr, ::Call{:roughly})
    args = callargs(rhs)
    arg = args[1]
    kw = map(atol2kw, args[2:end])
    if length(args) == 1
        return :($lhs ≈ $arg)
    else
        return Expr(:call, :isapprox, lhs, arg, kw...)
    end
end

function lhs_rhs2testcore{f}(lhs, rhs::Expr, ::Call{f})
    if f in keys(COMPARISON_DICT)
        s = COMPARISON_DICT[f]
        rhs_new = callarg_unique(rhs)
        return Expr(:call, s, lhs, rhs_new)
    else
        return :($lhs == $rhs)
    end
end

function lhs_rhs2testcore(lhs, rhs::Expr, ::Lambda)
    Expr(:call, rhs, lhs)
end


module PatternFolds

# Reexporr
using Reexport

# Import
@reexport using Intervals
using Lazy
using Random

# exports
export IntervalsFold
export IVectorFold
export VectorFold

export check_pattern
export fold
export folds
export gap
export length # TODO: rename span
export make_vector_fold
export pattern
export unfold

# includes
include("intervals.jl")
include("common.jl")
include("immutable_vector.jl")
include("vector.jl")

end