using Clapeyron, Test

@testset "database_lookup" begin
    params1 = Clapeyron.getparams(["water", "methanol"], ["SAFT/PCSAFT"],return_sites=false)
    @test haskey(params1, "sigma")

    # The rest of the test will be conducted with a custom dataset in the test_csvs directory.
    testspecies = ["sp1", "sp2", "sp3", "sp4", "sp5"]

    filepath_normal = ["test_csvs/normal_pair_test.csv",
                       "test_csvs/normal_single_test.csv",
                       "test_csvs/normal_assoc_test.csv",
                       "test_csvs/normal_single2_test.csv",
                       "test_csvs/normal_assoc2_test.csv"]

    filepath_clashingheaders = ["test_csvs/headercollision_single_test",
                                "test_csvs/headercollision_assoc_test"]

    filepath_asymmetry = ["test_csvs/asymmetry_pair_test",
                          "test_csvs/asymmetry_assoc_test"]

    filepath_multiple_identifiers = ["test_csvs/multiple_identifiers_single_test.csv",
                                     "test_csvs/multiple_identifiers_pair_test.csv",
                                     "test_csvs/multiple_identifiers_assoc_test.csv"]
    filepath_gc = ["test_csvs/group_test.csv"]
    filepath_param_gc = ["test_csvs/group_param_test.csv"]
    # Check that it detects the right sites.
    @test Clapeyron.findsitesincsvs(testspecies, filepath_normal) == [[],
                                                                     [],
                                                                     ["e", "e2", "H"],
                                                                     ["e", "H"],
                                                                     ["e", "e2", "H"]]

    # Check that it throws an error if ignore_missing_singleparams is not set to true.
    @test_throws ErrorException Clapeyron.getparams(testspecies; userlocations=filepath_normal,return_sites = false)

    params = Clapeyron.getparams(testspecies; userlocations=filepath_normal, ignore_missing_singleparams=["emptyparam","missingparam"],return_sites = false)
    sites = Clapeyron.SiteParam(params["intparam"].components)
    #Printing: SingleParam
    @test repr(params["intparam"]) == "SingleParam{Int64}(\"intparam\")[\"sp1\",\"sp2\",\"sp3\",\"sp4\",\"sp5\"]"
    @test repr("text/plain",params["intparam"]) == "SingleParam{Int64}(\"intparam\") with 5 components:\n \"sp1\" => 6\n \"sp2\" => 2\n \"sp3\" => 7\n \"sp4\" => 4\n \"sp5\" => 5"
    #Printing: PairParam
    @test repr(params["overwriteparam"]) == "PairParam{Float64}(\"overwriteparam\")[5×5 Matrix{Float64}]"
    @test repr("text/plain",params["overwriteparam"]) == "PairParam{PairParam{Float64}}[\"sp1\", \"sp2\", \"sp3\", \"sp4\", \"sp5\"]) with values:\n5×5 Matrix{Float64}:\n 1.6  4.0  0.0  0.0  0.0\n 4.0  1.2  0.0  3.0  0.0\n 0.0  0.0  1.3  2.0  0.0\n 0.0  3.0  2.0  1.4  0.0\n 0.0  0.0  0.0  0.0  1.5"
    #Printing: AssocParam
    @test repr(params["overwriteassocparam"]) == "AssocParam{String}(\"overwriteassocparam\")[\"val1\", \"val8\", \"val5\", \"val4\", \"val7\", \"val6\", \"val3\", \"42\"]"
    @test repr("text/plain",params["overwriteassocparam"]) == "AssocParam{String}[\"sp1\", \"sp2\", \"sp3\", \"sp4\", \"sp5\"]) with values:\n(\"sp3\", \"e\") >=< (\"sp3\", \"H\"): val1\n(\"sp3\", \"e2\") >=< (\"sp3\", \"H\"): val8\n(\"sp3\", \"H\") >=< (\"sp4\", \"e\"): val5\n(\"sp3\", \"H\") >=< (\"sp4\", \"H\"): val4\n(\"sp4\", \"e\") >=< (\"sp5\", \"H\"): val7\n(\"sp4\", \"H\") >=< (\"sp5\", \"e2\"): val6\n(\"sp5\", \"e\") >=< (\"sp5\", \"e\"): val3\n(\"sp5\", \"e\") >=< (\"sp5\", \"H\"): 42\n"
    #Printing: SiteParam
    @test repr(sites) == "SiteParam[\"sp1\" => [], \"sp2\" => [], \"sp3\" => [], \"sp4\" => [], \"sp5\" => []]"
    @test repr("text/plain",sites) == "SiteParam with 5 components:\n \"sp1\": (no sites)\n \"sp2\": (no sites)\n \"sp3\": (no sites)\n \"sp4\": (no sites)\n \"sp5\": (no sites)"
    # Check that all the types are correct.
    @test typeof(params["intparam"]) <: Clapeyron.SingleParam{Int}
    @test typeof(params["doubleparam"]) <: Clapeyron.SingleParam{Float64}
    @test typeof(params["boolparam"]) <: Clapeyron.SingleParam{Bool}
    @test typeof(params["stringparam"]) <: Clapeyron.SingleParam{String}
    # If column has both strings and numbers, they should all be strings.
    @test typeof(params["mixedparam"]) <: Clapeyron.SingleParam{String}
    # Contains missing values
    @test typeof(params["missingparam"]) <: Clapeyron.SingleParam{Int}
    # All missing values
    #before returned Clapeyron.SingleParam{Any}
    #now returns Clapeyron.SingleParam{Float64}
    @test typeof(params["emptyparam"]) <: Clapeyron.SingleParam{Float64}
    # Overwrite Int with Float, and also an upgrade from single to pair param
    @test typeof(params["overwriteparam"]) <: Clapeyron.PairParam{Float64}
    #test for missingness in PairParam, i got bitten by this before
    @test params["overwriteparam"].ismissingvalues ==  Bool[0 0 1 1 1;
                                                            0 0 1 0 1;
                                                            1 1 0 0 1;
                                                            1 0 0 0 1;
                                                            1 1 1 1 0]
    # Overwrite String with Int
    @test typeof(params["overwritestringparam"]) <: Clapeyron.SingleParam{String}
    # Overwrite Int with String
    @test typeof(params["overwriteassocparam"]) <: Clapeyron.AssocParam{String}
    @test repr(params["overwriteassocparam"]) == "AssocParam{String}(\"overwriteassocparam\")[\"val1\", \"val8\", \"val5\", \"val4\", \"val7\", \"val6\", \"val3\", \"42\"]"
    @test repr("text/plain",params["overwriteassocparam"]) == "AssocParam{String}[\"sp1\", \"sp2\", \"sp3\", \"sp4\", \"sp5\"]) with values:\n(\"sp3\", \"e\") >=< (\"sp3\", \"H\"): val1\n(\"sp3\", \"e2\") >=< (\"sp3\", \"H\"): val8\n(\"sp3\", \"H\") >=< (\"sp4\", \"e\"): val5\n(\"sp3\", \"H\") >=< (\"sp4\", \"H\"): val4\n(\"sp4\", \"e\") >=< (\"sp5\", \"H\"): val7\n(\"sp4\", \"H\") >=< (\"sp5\", \"e2\"): val6\n(\"sp5\", \"e\") >=< (\"sp5\", \"e\"): val3\n(\"sp5\", \"e\") >=< (\"sp5\", \"H\"): 42\n"
    # Check that values of "sp1" and "sp3" has been correctly overwritten.
    # "sp1" was overwritten from the same file
    # "sp3" was overwritten from a separate file, "normal_single2_test.csv"
    @test params["intparam"].values == [6, 2, 7, 4, 5]
    # Also check that "testsource1" and "testsource3" are not present, and in their place
    # we have "testsource6" and "testsource19".
    @test "testsource1" ∉ params["intparam"].sources
    @test "testsource3" ∉ params["intparam"].sources
    @test "testsource5" ∈ params["intparam"].sources
    @test "testsource19" ∈ params["intparam"].sources

    # Check that missing values have been correctly defaulted.
    @test params["missingparam"].values == [0, 2, 3, 0, 0]
    @test params["missingparam"].ismissingvalues == Bool[1, 0, 0, 1, 1]

    #test that Passing a single param to a pair param doesnt erase the missings
    singletopairparam = PairParam(params["missingparam"],"singletopairparam")
    diagmissingvalues = view(singletopairparam.ismissingvalues,1:6:25)
    @test all(diagmissingvalues .== params["missingparam"].ismissingvalues)
    diagmissingvalues .= true
    @test all(singletopairparam.ismissingvalues)

    # Check that the promotion form 1D to 2D array is succesful, with non-diagonal values present and symmetrical.
    @test params["overwriteparam"].values == [1.6  4.0  0.0  0.0  0.0
                                              4.0  1.2  0.0  3.0  0.0
                                              0.0  0.0  1.3  2.0  0.0
                                              0.0  3.0  2.0  1.4  0.0
                                              0.0  0.0  0.0  0.0  1.5]

    @test params["overwriteparam"].diagvalues == [1.6, 1.2, 1.3, 1.4, 1.5]

    assoc_param_values =
    [[Array{Int64}(undef,0,0)]  [Array{Int64}(undef,0,0)]  [Array{Int64}(undef,0,3)          ]  [Array{Int64}(undef,0,2)]  [Array{Int64}(undef,0,3)       ]
     [Array{Int64}(undef,0,0)]  [Array{Int64}(undef,0,0)]  [Array{Int64}(undef,0,3)          ]  [Array{Int64}(undef,0,2)]  [Array{Int64}(undef,0,3)       ]
     [Array{Int64}(undef,3,0)]  [Array{Int64}(undef,3,0)]  [[0 0 2000; 0 0 2700; 2000 2700 0]]  [[0 0; 0 0; 2400 2300]  ]  [[0 0 0; 0 0 0; 0 0 0]         ]
     [Array{Int64}(undef,2,0)]  [Array{Int64}(undef,2,0)]  [[0 0 2400; 0 0 2300]             ]  [[0 0; 0 0]             ]  [[0 0 2600; 0 2500 0]          ]
     [Array{Int64}(undef,3,0)]  [Array{Int64}(undef,3,0)]  [[0 0 0; 0 0 0; 0 0 0]            ]  [[0 0; 0 2500; 2600 0]  ]  [[2200 0 2100; 0 0 0; 2100 0 0]]]

    for i ∈ 1:5
        for j ∈ 1:5
            valij = assoc_param_values[i,j]
            s1,s2 = size(valij)
            testij = params["assocparam"].values[i,j]
            if iszero(s1*s2)
                continue # a compressed assoc matrix can hold more information that is discarded normally
            else
                for a ∈ 1:s1
                    for b ∈ 1:s2
                        @test valij[a,b] == testij[a,b]
                    end
                end
            end
        end
    end
  #  @test params["assocparam"].values[i,j] .== assoc_param_values[i,j] for (i,j) in zip()

    # Clashing headers between association and non-association parameters are not allowed
    @test_throws ErrorException Clapeyron.getparams(testspecies; userlocations=filepath_clashingheaders)

    # If parameter is not tagged as asymmetrical, having non-missing values across the diagonal will throw an error
    @test_throws ErrorException Clapeyron.getparams(testspecies; userlocations=filepath_asymmetry, ignore_missing_singleparams=["asymmetricpair"],return_sites = false)

    asymmetricparams = Clapeyron.getparams(testspecies; userlocations=filepath_asymmetry, asymmetricparams=["asymmetricpair", "asymmetricassoc"], ignore_missing_singleparams=["asymmetricpair"],return_sites = false)

    asymmetricparams["asymmetricpair"].values == [0.06  0.04  0.0  0.0   0.0
                                                  0.05  0.0   0.0  0.0   0.0
                                                  0.0   0.0   0.0  0.02  0.0
                                                  0.0   0.03  0.0  0.0   0.0
                                                  0.0   0.0   0.0  0.0   0.0]
    asymmetricparams["asymmetricassoc"].values ==
    [[Array{Int64}(undef,0,0)]  [Array{Int64}(undef,0,0)]  [Array{Int64}(undef,0,3)       ]  [Array{Int64}(undef,0,2) ]  [Array{Int64}(undef,0,3)    ]
     [Array{Int64}(undef,0,0)]  [Array{Int64}(undef,0,0)]  [Array{Int64}(undef,0,3)       ]  [Array{Int64}(undef,0,2) ]  [Array{Int64}(undef,0,3)    ]
     [Array{Int64}(undef,3,0)]  [Array{Int64}(undef,3,0)]  [[0 0 2800; 0 0 0; 2000 2700 0]]  [[0 2900; 0 0; 2400 2300]]  [[0 0 0; 0 0 0; 0 0 0]      ]
     [Array{Int64}(undef,2,0)]  [Array{Int64}(undef,2,0)]  [[0 0 0; 0 0 0]                ]  [[0 0; 0 0]              ]  [[0 0 0; 0 2500 0]          ]
     [Array{Int64}(undef,3,0)]  [Array{Int64}(undef,3,0)]  [[0 0 0; 0 0 0; 0 0 0]         ]  [[0 0; 0 0; 2600 0]      ]  [[2200 0 0; 0 0 0; 2100 0 0]]]

    # Also, since a non-missing value exists on the diagonal of "asymmetricpair",
    # and the diagonal contains missing values, it should throw an error
    # when parameter is not in ignore_missing_singleparams
    @test_throws ErrorException Clapeyron.getparams(testspecies; userlocations=filepath_asymmetry, asymmetricparams=["asymmetricpair", "asymmetricassoc"])

    # Testing for multiple identifiers
    multiple_identifiers = Clapeyron.getparams(testspecies; userlocations=filepath_multiple_identifiers, return_sites=false)
    @test multiple_identifiers["param_single"].values == [100, 200, 200, 300, 300]
    @test multiple_identifiers["param_pair"].values   == [1000  4000     0     0     0
                                                         4000  2000     0  6000     0
                                                            0     0  2000  5000     0
                                                            0  6000  5000  3000     0
                                                            0     0     0     0  3000]
    @test multiple_identifiers["param_assoc"].values[3,2][1,1] == 10000 &&
            multiple_identifiers["param_assoc"].values[5,1][1,1] == 20000

    # GC test, 3 comps, 4 groups

    components_gc = GroupParam(["test1", "test2", ("test3", ["grp1" => 2, "grp2" => 2, "grp3" => 3,"grp4" => 5])]; usergrouplocations=filepath_gc)

    #Printing: GroupParam
    @test repr(components_gc) == "GroupParam[\"test1\" => [\"grp1\" => 1, \"grp2\" => 2], \"test2\" => [\"grp2\" => 1], \"test3\" => [\"grp1\" => 2, \"grp2\" => 2, \"grp3\" => 3, \"grp4\" => 5]]"
    @test repr("text/plain",components_gc) == "GroupParam with 3 components:\n \"test1\": \"grp1\" => 1, \"grp2\" => 2\n \"test2\": \"grp2\" => 1\n \"test3\": \"grp1\" => 2, \"grp2\" => 2, \"grp3\" => 3, \"grp4\" => 5"

    @test components_gc.components == ["test1", "test2", "test3"]
    @test components_gc.groups == [["grp1","grp2"],["grp2"],["grp1","grp2","grp3","grp4"]]
    @test components_gc.n_groups == [[1,2], [1], [2,2,3,5]]

    # Check that flattening of groups is correct.
    @test components_gc.flattenedgroups == ["grp1", "grp2", "grp3","grp4"]
    @test components_gc.n_flattenedgroups == [[1,2,0,0], [0,1,0,0 ], [2,2,3,5]]
    @test components_gc.i_flattenedgroups == 1:4
    # Build param struct using the gc components above

    param_gc = getparams(components_gc; userlocations=filepath_param_gc)
    @test param_gc["param1"].values == [1, 2, 3, 4]

    #ParamTable
    file = ParamTable(:single,(species = ["sp1"],userparam = [2]))
    param_user = getparams(testspecies,userlocations = [file],ignore_missing_singleparams=["userparam"])
    @test param_user["userparam"].values[1] === 2
end


using Flux, Random

struct ImmutableChain{T<:Tuple}
    layers::T
end

ImmutableChain(x, y, xs...) = ImmutableChain((x, y, xs...))

Flux.children(c::ImmutableChain) = c.layers
Flux.mapchildren(f, c::ImmutableChain) = ImmutableChain(f.(c.layers))

applyall(::Tuple{}, x) = x
applyall(fs::Tuple, x) = applyall(Base.tail(fs), first(fs)(x))

(c::ImmutableChain)(x) = applyall(c.layers, x)

# Helper function to take a recursive data structure (Such as an ImmutableChain)
# and flatten it out into a tuple
@generated function flatten_tuple(t)
    stmts = Any[]
    tuple_args = Any[]
    function collect_args(t, sym)
        for i = 1:fieldcount(t)
            fT = fieldtype(t, i)
            if fT === Nothing
                continue
            end
            g = gensym()
            push!(stmts, :($g = getfield($sym, $i)))
            if !(fT <: AbstractArray)
                collect_args(fT, g)
            else
                push!(tuple_args, g)
            end
        end
    end
    collect_args(t, :t)
    return quote
        $(stmts...)
        tuple($(tuple_args...))
    end
end

# The inverse of flatten_tuple()
@generated function unflatten_tuple(pattern, t)
    function expand_arg(pattern, idx)
        fields = Any[]
        for i = 1:fieldcount(pattern)
            fT = fieldtype(pattern, i)
            if fT === Nothing
                push!(fields, nothing)
                continue
            end
            if fT <: Tuple || fT <: NamedTuple
                arg, idx = expand_arg(fT, idx)
                push!(fields, arg)
            else
                push!(fields, Expr(:call, :getfield, :t, idx))
                idx += 1
            end
        end
        Expr(:new, pattern, fields...), idx
    end
    res = expand_arg(pattern, 1)[1]
    return res
end

# Include our TPUBatchNorm object
include("tpu_batchnorm.jl")
export map_to_tpu, map_to_cpu, TPUBatchNorm, tpu
function map_to_tpu end
const tpu = map_to_tpu

## Mapping utilities
# Convert scalars to single-element XRTArrays with eltype Float32:
map_to_tpu(x::Real) = XRTArray(convert(Float32, x))

# Convert arrays to XRTArrays with eltype Float32
map_to_tpu(x::AbstractArray) = XRTArray(Float32.(x))

# Strip off the TrackedArray coating to get at the data underneath
map_to_tpu(x::TrackedArray) = map_to_tpu(Flux.data(x))

# Turn Chain objects into ImmutableChain objects which store the computation within their type signature
map_to_tpu(x::Chain) = ImmutableChain(tuple(map(map_to_tpu, x.layers)...))

# Convert BatchNorm layers into TPUBatchNorm layers, passing all children straight through,
# except for the "active" child, which is not used by the TPUBatchNorm
map_to_tpu(x::Flux.BatchNorm) = TPUBatchNorm(map(map_to_tpu, Flux.children(x))[1:end-1]...)

# We deal manually with recurrence, so Recur{T} values get turned into just T values
map_to_tpu(x::Flux.Recur) = Flux.mapchildren(map_to_tpu, x.cell)

# For all other objects, just map the children through `map_to_tpu`.
map_to_tpu(x) = Flux.mapchildren(map_to_tpu, x)

map_to_cpu(x) = Flux.mapchildren(map_to_cpu, x)
map_to_cpu(x::XRTArray) = convert(Array, x)
map_to_cpu(x::ImmutableChain) = Chain(map(map_to_cpu, x.layers)...)
map_to_cpu(x::TPUBatchNorm) = Flux.BatchNorm(map(map_to_cpu, Flux.children(x))..., false)
map_to_cpu(x::Flux.LSTMCell) = Flux.Recur(Flux.mapchildren(map_to_cpu, x))

## Layer reimplementations that need to know about XRTArray
import NNlib: softmax, logsoftmax, ∇softmax, ∇logsoftmax
import Flux: logitcrossentropy
export crossentropy, logitcrossentropy, softmax, ∇softmax, logsoftmax, ∇logsoftmax

# Work around inference limitations to dynamically find the number of
# batches held within a tensor.
neg_nbatch(x) = XRTArray(Float32(-size(x, ndims(x))))
Zygote.@nograd neg_nbatch

function crossentropy(ŷ::XRTArray, y::XRTArray)
    return sum(y .* log.(ŷ)) / neg_nbatch(y)
end

function logitcrossentropy(logŷ::XRTArray, y::XRTArray)
    return sum(y .* logsoftmax(logŷ)) / neg_nbatch(y)
end

# Define softmax and its gradient for XRTArrays
function softmax(xs::XRTArray)
    ys = xs .- maximum(xs, dims=1)
    zs = exp.(ys)
    return zs ./ sum(zs, dims=1)
end
function ∇softmax(Δ, xs::XRTArray)
    sf = softmax(xs)
    return sf .* (Δ .- sum(Δ .* sf, dims = 1))
end
Zygote.@adjoint softmax(xs::XRTArray) = softmax(xs), Δ -> (∇softmax(Δ, xs),)

# Define logsoftmax and its gradient for XRTArrays
function logsoftmax(xs::XRTArray)
    ys = xs .- maximum(xs, dims=1)
    zs = sum(exp.(ys), dims=1)
    return ys .- log.(zs)
end
∇logsoftmax(Δ, xs::XRTArray) = ∇softmax(Δ ./ max.(eps(eltype(xs)),softmax(xs)), xs)
Zygote.@adjoint logsoftmax(xs::XRTArray) = logsoftmax(xs), Δ -> (∇logsoftmax(Δ, xs),)

function Base.sort(x::XRTArray{T,<:Any,1}; keys=x) where {T}
    return XLA.HloSort{typeof(<)}(0, false)(<, keys, x)[2]
end

Base.rand(::Type{XRTArray{T}}, shape::Int...) where {T} = XLA.HloRng(T, shape, 1)(XRTArray(typemin(T)),XRTArray(typemax(T)))
Base.rand(::Type{XRTArray{F}}, shape::Int...) where {F <: AbstractFloat} = XLA.HloRng(F, shape, 1)(XRTArray(0.0f0),XRTArray(1.0f0))
Base.randn(::Type{XRTArray{F}}, shape::Int...) where {F <: AbstractFloat} = XLA.HloRng(F, shape, 2)(XRTArray(0.0f0),XRTArray(1.0f0))

# Shuffle by repeatedly sorting x with a random vector as the sorting key.
# see https://github.com/tensorflow/tensorflow/blob/85deaa11cae878ba2c0e5284085956f75434b5b2/tensorflow/compiler/tf2xla/kernels/random_ops.cc#L83-L143 for more
Base.@pure calc_rounds(L; exponent=3) = ceil(UInt32, exponent * log(L) / log(typemax(UInt32)))
function shuffle(x::XRTArray)
    round_idx = XRTArray(calc_rounds(length(x)))
    while round_idx > XRTArray(0x00000000)
        keys = rand(XRTArray{UInt32}, length(x))
        x = sort(x, keys=keys)
        round_idx -= XRTArray(0x00000001)
    end
    return x
end


export AddStoppingCondition, SetIterationLimit, BumpIterationLimit, SetFunctionEvaluationLimit,
       BumpFunctionEvaluationLimit, SetMinimumMeshSize, SetMinimumPollSize, RuntimeStoppingCondition,
       SetButtonLimit,SetHypervolumeLimit,HypervolumeStoppingCondition, checkKeyInterrupt

"""
    AddStoppingCondition(p::DSProblem, c::T) where T <: AbstractStoppingCondition

Add the stopping condition `c` to the problem `p`.
"""
function AddStoppingCondition(p::DSProblem, c::T) where T <: AbstractStoppingCondition
    push!(p.stoppingconditions, c)
end

_check_stoppingconditions(p::DSProblem) = _check_stoppingconditions(p, p.stoppingconditions)
function _check_stoppingconditions(p::DSProblem, c::Vector{T}) where T <: AbstractStoppingCondition
    for condition in c
        #Stopping conditions return true if optimisation should continue,
        #false if the condition is met and optimisation should stop
        if CheckStoppingCondition(p, condition) == false
            setstatus(p, condition)
            return false
        end
    end
        !checkKeyInterrupt(p) && return false

    return true
end

function setstatus(p, s::T) where T <: AbstractStoppingCondition
    p.status.optimization_status_string = StoppingConditionStatus(s)

    if s isa IterationStoppingCondition
        p.status.optimization_status = IterationLimit
    elseif s isa MeshPrecisionStoppingCondition
        p.status.optimization_status = MeshPrecisionLimit
    elseif s isa PollPrecisionStoppingCondition
        p.status.optimization_status = PollPrecisionLimit
    elseif s isa FunctionEvaluationStoppingCondition
        p.status.optimization_status = FunctionEvaluationLimit
    elseif s isa RuntimeStoppingCondition
        p.status.optimization_status = RuntimeLimit
    else
        p.status.optimization_status = OtherStoppingCondition
    end
end

StoppingConditionStatus(::T) where T <: AbstractStoppingCondition = "Unknown stopping condition status"

function _init_stoppingconditions(p::DSProblem)
    for c in p.stoppingconditions
        init_stoppingcondition(p, c)
    end
end

init_stoppingcondition(p::DSProblem, ::AbstractStoppingCondition) = nothing

_get_conditionindexes(p::DSProblem, target::Type) = _get_conditionindexes(p.stoppingconditions, target)
_get_conditionindexes(s::Vector{AbstractStoppingCondition}, target::Type) =
    [i for (i,v) in enumerate(s) if typeof(v) == target]

#===== Built-in stopping conditions =====#
# Key Interrupt

# for MADS
function checkKeyInterrupt(p::DSProblem)::Bool
    bb = bytesavailable(stdin)
    data = read(stdin, bb)
    if bb>0 && data==UInt8[0x71] #q for quit
        println("quit")
        p.status.optimization_status=KeyInterrupt
        p.status.optimization_status_string = "Key Interrupt"
        return false
    end
    return true
end

#Iteration limit
mutable struct IterationStoppingCondition <: AbstractStoppingCondition
    limit::Int64
end

StoppingConditionStatus(::IterationStoppingCondition) = "Iteration limit"

CheckStoppingCondition(p::DSProblem, s::IterationStoppingCondition) = p.status.iteration < s.limit

function init_stoppingcondition(p::DSProblem, s::IterationStoppingCondition)
    if s.limit == -1
        error("Please set a maximum number of iterations")
    end
end

"""
    SetIterationLimit(p::DSProblem, i::Int)

Set the maximum number of iterations to `i`.
"""
function SetIterationLimit(p::DSProblem, i::Int)
    if i < p.status.iteration
        error("Cannot set iteration limit to lower than the number of iterations that have run")
    else
        iteration_indexes = _get_conditionindexes(p, IterationStoppingCondition)
        for index in iteration_indexes
            p.stoppingconditions[index].limit = i
        end
    end
end

"""
    BumpIterationLimit(p::DSProblem, i::Int)

Increase the iteration limit by `i`.
"""
function BumpIterationLimit(p::DSProblem, i::Int)
    iteration_indexes = _get_conditionindexes(p, IterationStoppingCondition)
    for index in iteration_indexes
        p.stoppingconditions[index].limit += i
    end
end

#Mesh size precision limit
mutable struct MeshPrecisionStoppingCondition{T} <: AbstractStoppingCondition
    cont_min_mesh_size::T
    min_mesh_sizes::Vector{T}

    function MeshPrecisionStoppingCondition{T}(min_mesh_size::Union{T, Nothing}=nothing) where T
        c = new()

        if min_mesh_size != nothing
            c.cont_min_mesh_size = min_mesh_size
        else
            c.cont_min_mesh_size = get_min_mesh_size(T)
        end

        return c
    end
end

function init_stoppingcondition(p::DSProblem, s::MeshPrecisionStoppingCondition)
    s.min_mesh_sizes = map(δ_min -> δ_min > 0 ? δ_min : s.cont_min_mesh_size, p.config.mesh.δ_min)
end

StoppingConditionStatus(::MeshPrecisionStoppingCondition) = "Mesh Precision limit"

function CheckStoppingCondition(p::DSProblem, s::MeshPrecisionStoppingCondition)
    if p.config.mesh.only_granular
        p.config.mesh.l > -50
    else
        any(GetMeshSizeVector(p) .> s.min_mesh_sizes)
    end
end

(GetMeshSizeVector(p::DSProblem{T})::Vector{T}) where T = p.config.mesh.δ

get_min_mesh_size(::Type{Float64}) = 1.1102230246251565e-16
get_min_mesh_size(::Type{T}) where T = eps(T)/2

"""
    SetMinimumMeshSize(p::DSProblem{T}, i::T) where T

Set the minimum mesh size for continuous variables.
"""
function SetMinimumMeshSize(p::DSProblem{T}, i::T) where T
    if i <= 0
        error("Minimum mesh size must be positive.")
    else
        mesh_precision_indexes = _get_conditionindexes(p, MeshPrecisionStoppingCondition)
        for index in mesh_precision_indexes
            p.stoppingconditions[index].cont_min_mesh_size = i
        end
    end
end

#Poll size precision limit
mutable struct PollPrecisionStoppingCondition{T} <: AbstractStoppingCondition
    cont_min_poll_size::T
    min_poll_sizes::Vector{T}

    function PollPrecisionStoppingCondition{T}(min_poll_size::Union{T, Nothing}=nothing) where T
        c = new()

        if min_poll_size != nothing
            c.cont_min_poll_size = min_poll_size
        else
            c.cont_min_poll_size = get_min_poll_size(T)
        end

        return c
    end
end

function init_stoppingcondition(p::DSProblem, s::PollPrecisionStoppingCondition)
    s.min_poll_sizes = map(δ_min -> δ_min > 0 ? δ_min : s.cont_min_poll_size, p.config.mesh.δ_min)
end

StoppingConditionStatus(::PollPrecisionStoppingCondition) = "Poll Precision limit"

function CheckStoppingCondition(p::DSProblem, s::PollPrecisionStoppingCondition)
    if p.config.mesh.only_granular
        p.config.mesh.l > -50
    else
        any(GetPollSizeVector(p) .> s.min_poll_sizes)
    end
end

(GetPollSizeVector(p::DSProblem{T})::Vector{T}) where T = p.config.mesh.Δ

get_min_poll_size(::Type{Float64}) = 1.1102230246251565e-16
get_min_poll_size(::Type{T}) where T = eps(T)/2

"""
    SetMinimumPollSize(p::DSProblem{T}, i::T) where T

Set the minimum poll size for continuous variables.
"""
function SetMinimumPollSize(p::DSProblem{T}, i::T) where T
    if i <= 0
        error("Minimum poll size must be positive.")
    else
        poll_precision_indexes = _get_conditionindexes(p, PollPrecisionStoppingCondition)
        for index in poll_precision_indexes
            p.stoppingconditions[index].cont_min_poll_size = i
        end
    end
end

#Function evaluation limit
mutable struct FunctionEvaluationStoppingCondition <: AbstractStoppingCondition
    limit::Int64
end

StoppingConditionStatus(::FunctionEvaluationStoppingCondition) = "Function evaluation limit"

CheckStoppingCondition(p::DSProblem, s::FunctionEvaluationStoppingCondition) = p.status.function_evaluations < s.limit

function init_stoppingcondition(::DSProblem, s::FunctionEvaluationStoppingCondition)
    if s.limit == -1
        error("Please set a maximum number of function evaluations")
    end
end

"""
    SetFunctionEvaluationLimit(p::DSProblem, i::Int)

Set the maximum number of function evaluations to `i`.
"""
function SetFunctionEvaluationLimit(p::DSProblem, i::Int)
    if i < p.status.function_evaluations
        error("Cannot set function evaluation limit to lower than the number of function evaluations that have run")
    else
        function_evaluation_indexes = _get_conditionindexes(p, FunctionEvaluationStoppingCondition)
        for index in function_evaluation_indexes
            p.stoppingconditions[index].limit = i
        end
    end
end

"""
    BumpFunctionEvaluationLimit(p::DSProblem, i::Int)

Increase the function evaluation limit by `i`.
"""
function BumpFunctionEvaluationLimit(p::DSProblem, i::Int)
    function_evaluation_indexes = _get_conditionindexes(p, FunctionEvaluationStoppingCondition)
    for index in function_evaluation_indexes
        p.stoppingconditions[index].limit += i
    end
end

#===== Optional stopping conditions =====#

#Runtime limit
mutable struct RuntimeStoppingCondition <: AbstractStoppingCondition
    limit::Float64
end

StoppingConditionStatus(::RuntimeStoppingCondition) = "Runtime limit"

CheckStoppingCondition(p::DSProblem, s::RuntimeStoppingCondition) = (time() - p.status.start_time) < s.limit

function init_stoppingcondition(::DSProblem, s::RuntimeStoppingCondition)
    if s.limit <= 0
        error("Runtime limit must be positive.")
    end
end

"""
    SetRuntimeLimit(p::DSProblem, i::Float64)

Set the runtime limit to `i`.
"""
function SetRuntimeLimit(p::DSProblem, i::Float64)
    if i < p.status.runtime_total
        error("Cannot set runtime limit to lower than the runtime of the previous run")
    elseif i <= 0
        error("Runtime limit has to be positive.")
    else
        runtime_indexes = _get_conditionindexes(p, RuntimeStoppingCondition)
        for index in runtime_indexes
            p.stoppingconditions[index].limit = i
        end
    end
end

#Pareto front hyper-volume limit (Bimads only)
mutable struct HypervolumeStoppingCondition <: AbstractStoppingCondition
    limit::Float64
end

StoppingConditionStatus(::HypervolumeStoppingCondition) = "Hyper-Volume limit"
CheckStoppingCondition(p::DSProblem, s::HypervolumeStoppingCondition) = true

function init_stoppingcondition(::DSProblem, s::HypervolumeStoppingCondition)
    if s.limit <= 0
        error("Hypervolume limit must be positive.")
    end
end

"""
    SetHypervolumeLimit(p::DSProblem, i::Float64)

Set the Hyper-Volume limit to `i`.
"""
function SetHypervolumeLimit(p::DSProblem, i::Float64)
    if i <= 0
        error("Hyper-Volume limit has to be positive.")
    else
        runtime_indexes = _get_conditionindexes(p, HypervolumeStoppingCondition)
        for index in runtime_indexes
            p.stoppingconditions[index].limit = i
        end
    end
end


abstract type AbstractStructuralInterference{T} <: AbstractEmitter{T} end

struct ConstantDopplerStructuralInterference{
    T <: AbstractFloat,
    CS <: ConstantDopplerSatellite{<: AbstractGNSS, T}
} <: AbstractStructuralInterference{T}
    sat::CS
end

function ConstantDopplerStructuralInterference(
    sat::ConstantDopplerSatellite{S, T},
    signal_amplification::Union{Unitful.Gain{Unitful.Decibel, :?, <:Real}, Unitful.Level, DynamicCN0};
    added_carrier_doppler = NaN*Hz,
    added_carrier_phase = NaN,
    added_code_phase = NaN,
    exists::E = true,
    doa::D = SVector(0,0,1),
    added_relative_velocity = 0.0m/s,
    added_signal_path = 0.0m
) where {
    S <: AbstractGNSS,
    T <: AbstractFloat,
    D <: Union{SVector{3}, AbstractDOA},
    E <: Union{Bool, AbstractExistence}
}
    system = sat.system
    if isnan(added_carrier_doppler)
        carrier_doppler = get_carrier_doppler(sat) + added_relative_velocity /
            SPEED_OF_LIGHT * get_center_frequency(system)
    else
        carrier_doppler = get_carrier_doppler(sat) + added_carrier_doppler
    end
    if isnan(added_carrier_phase)
        carrier_phase = mod(get_carrier_phase_2pi(sat) +
            calc_carrier_phase(
                added_signal_path,
                get_center_frequency(system) + carrier_doppler
            ) + 0.5,
            1
        ) - 0.5
    else
        added_carrier_phase = added_carrier_phase / 2π
        carrier_phase = mod(get_carrier_phase_2pi(sat) + added_carrier_phase + 0.5, 1) - 0.5
    end
    if isnan(added_code_phase)
        code_doppler = carrier_doppler * get_code_center_frequency_ratio(system)
        code_phase = mod(
            get_code_phase(sat) +
            calc_code_phase(
                added_signal_path,
                get_code_frequency(system) + code_doppler, get_code_length(system)
            ),
            get_code_length(system)
        )
    else
        code_phase = mod(get_code_phase(sat) + added_code_phase, get_code_length(system))
    end
    cn0 = typeof(signal_amplification) <: Unitful.Gain ?
        float(get_carrier_to_noise_density_ratio(sat) + signal_amplification) :
        signal_amplification
    code = Vector{Int8}(undef, 0)
    signal = StructArray{Complex{T}}(undef, 0)
    sat = ConstantDopplerSatellite{S, T, D, E, typeof(cn0)}(
        system,
        get_prn(sat),
        carrier_doppler,
        carrier_phase,
        code_phase,
        cn0,
        exists,
        doa,
        code,
        signal
    )
    ConstantDopplerStructuralInterference(sat)
end

function gen_signal!(
    si::ConstantDopplerStructuralInterference,
    sample_frequency,
    intermediate_frequency,
    n0,
    num_samples::Integer,
    rng
)
    gen_signal!(si.sat, sample_frequency, intermediate_frequency, n0, num_samples, rng)
end

function propagate(
    si::ConstantDopplerStructuralInterference,
    num_samples,
    intermediate_frequency,
    sample_frequency,
    n0,
    rng
)
    ConstantDopplerStructuralInterference(
        propagate(si.sat, num_samples, intermediate_frequency, sample_frequency, n0, rng)
    )
end

@inline get_doa(si::ConstantDopplerStructuralInterference) = get_doa(si.sat)
@inline get_existence(si::ConstantDopplerStructuralInterference) = get_existence(si.sat)
@inline get_carrier_doppler(si::ConstantDopplerStructuralInterference) =
    get_carrier_doppler(si.sat)
@inline get_code_doppler(si::ConstantDopplerStructuralInterference) =
    get_code_doppler(si.sat)
@inline get_carrier_phase(si::ConstantDopplerStructuralInterference) =
    get_carrier_phase(si.sat)
@inline get_code_phase(si::ConstantDopplerStructuralInterference) = get_code_phase(si.sat)
@inline get_prn(si::ConstantDopplerStructuralInterference) = get_prn(si.sat)
@inline get_amplitude(si::ConstantDopplerStructuralInterference, n0) =
    get_amplitude(si.sat, n0)
@inline get_gnss_system(si::ConstantDopplerStructuralInterference) = get_gnss_system(si.sat)
@inline get_carrier_to_noise_density_ratio(si::ConstantDopplerStructuralInterference) =
    get_carrier_to_noise_density_ratio(si.sat)


#=
    This file handles the conversion from svg produced by `tex2svg` to Luxor commands.
    It currently misses a lot of possible commands that svg supports.
    Warnings are thrown if this happens such that one can easily grasp whether it
    should have worked or whether there is something missing
=#

"""
    float_attribute(o, name)

Get the attribute `name` of the XMLElement and parse it as a Float64
"""
float_attribute(o::LightXML.XMLElement, name) = parse(Float64, attribute(o, name))

#=
    draw_obj functions have a type which is the type of the svg element.
    i.e <rect calls the `draw_obj(::Val{:rect}, o, defs)` command or `::Val{:path}` for `<path`
=#

"""
    draw_obj(::Val{:rect}, o, defs)

Draw the rectangle defined by the object `o`.
"""
function draw_obj(::Val{:rect}, o, defs)
    width = float_attribute(o, "width")
    height = float_attribute(o, "height")
    x = float_attribute(o, "x")
    y = float_attribute(o, "y")
    # TODO: draw it anti clockwise as it gets scaled to clockwise :/
    move(Point(x + width, y))
    line(Point(x, y))
    line(Point(x, y + height))
    line(Point(x + width, y + height))
    closepath()
end

"""
    draw_obj(::Val{:g}, o, defs)

Draws a group by setting the attributes (like transformations)
and then calls `draw_obj` for all child elements.
"""
function draw_obj(::Val{:g}, o, defs)
    set_attrs(o)

    childs = collect(child_elements(o))
    for child in childs
        sym_name = Symbol(name(child))
        @layer begin
            draw_obj(Val{sym_name}(), child, defs)
        end
    end
end

"""
    draw_obj(::Val{:use}, o, defs)

Calls the command specified in `defs`.
"""
function draw_obj(::Val{:use}, o, defs)
    set_attrs(o)
    id = attribute(o, "href")[2:end]

    if haskey(defs, id)
        def = defs[id]
        sym = Symbol(name(def))
        draw_obj(Val{sym}(), def, defs)
    else
        @warn "There is no definition for $id"
    end
end

"""
    draw_obj(::Val{:path}, o, defs)

Calls the commands specified in the path data.
Currently supports only a subset of possible SVG commands.
"""
function draw_obj(::Val{:path}, o, defs)
    set_attrs(o)
    data = attribute(o, "d")
    counter = 0

    # split without loosing the command
    data_parts = split(data, r"(?=[A-Za-z])")
    # needs to keep track of the current point `c_pt` and the last point `l_pt`
    l_pt = O
    c_pt = O
    circle_pts = []
    for pi in 1:length(data_parts)
        p = data_parts[pi]
        command, args = p[1], p[2:end]
        if command != 'T'
            counter = 0
        end
        # using if else statements instead of dispatching here. Maybe it's faster :D
        if command == 'M'
            c_pt = path_move(parse.(Float64, split(args))...)
            # need to set the last control point as the current point for 'T'
            l_pt = c_pt
        elseif command == 'Q'
            l_pt, c_pt = path_quadratic(c_pt, parse.(Float64, split(args))...)
        elseif command == 'T'
            # the control point is a reflection based on the current and last point
            control_pt = l_pt + 2 * (c_pt - l_pt)
            l_pt, c_pt =
                path_quadratic(c_pt, control_pt..., parse.(Float64, split(args))...)
            push!(circle_pts, Point(parse.(Float64, split(args))...))
            counter += 1
        elseif command == 'L'
            new_pt = Point(parse.(Float64, split(args))...)
            line(new_pt)
            l_pt, c_pt = c_pt, new_pt
        elseif command == 'H'
            new_pt = Point(parse(Float64, args), c_pt.y)
            line(new_pt)
            l_pt, c_pt = c_pt, new_pt
        elseif command == 'V'
            new_pt = Point(c_pt.x, parse(Float64, args))
            line(new_pt)
            l_pt, c_pt = c_pt, new_pt
        elseif command == 'Z'
            closepath()
        else
            @warn "Couldn't parse the svg command: $command"
        end
    end
end

#=
    All kinds of commands for creating a path. The commands need to return the current point
    some of them also a previous point
=#

"""
    path_move(x,y)

Moving to the specified point
"""
function path_move(x, y)
    p = Point(x, y)
    move(p)
    p
end

"""
    path_quadratic(c_pt::Point, x,y, xe, ye)

Drawing a quadratic bezier curve by computing a cubic one as that is supported by Luxor
"""
function path_quadratic(c_pt::Point, x, y, xe, ye)
    e_pt = Point(xe, ye)
    qc = Point(x, y)
    # compute the control points of a cubic bezier curve
    c1 = c_pt + 2 / 3 * (qc - c_pt)
    c2 = e_pt + 2 / 3 * (qc - e_pt)
    curve(c1, c2, e_pt)
    return qc, e_pt
end

#=
    All kinds of setting attribute functions
=#

"""
    set_attrs(o)

Setting the attributes of the object `o` by calling `set_attr` methods.
"""
function set_attrs(o)
    for attribute in attributes(o)
        sym = Symbol(name(attribute))
        set_attr(Val{sym}(), LightXML.value(attribute))
    end
end

"""
    set_attr(::Val{:transform}, transform_strs)

Call the corresponding `set_transform` method i.e `matrix`, `scale` and `translate`
"""
function set_attr(::Val{:transform}, transform_strs)
    if transform_strs !== nothing
        transform_parts = split(transform_strs, r"(?<=[)]) ")
        for transform_str in transform_parts
            m = match(r"(.+)\((.+)\)", transform_str)
            type = Symbol(m.captures[1])
            set_transform(
                Val{type}(),
                parse.(Float64, strip.(split(m.captures[2], r"[, ]")))...,
            )
        end
    end
end

set_attr(::Val{:x}, x) = translate(parse(Float64, x), 0)
set_attr(::Val{:y}, y) = translate(0, parse(Float64, y))
set_attr(::Val{Symbol("stroke-width")}, sw) = setline(parse(Float64, sw))

set_transform(::Val{:translate}, x, y) = translate(x, y)
set_transform(::Val{:scale}, x, y = x) = scale(x, y)

"""
    set_transform(::Val{:matrix}, args...)

Multiply the new matrix with the current matrix and set it.
"""
function set_transform(::Val{:matrix}, args...)
    old = cairotojuliamatrix(getmatrix())
    update = cairotojuliamatrix(collect(args))
    new = old * update
    # only the first two rows are considered
    setmatrix(vec(new[1:2, :]))
end

#=
    General fallbacks
=#
draw_obj(::Union{Val{:title},Val{:defs}}, o, defs) = nothing
# no support for colors at this point
set_attr(t::Union{Val{:stroke},Val{:fill},Val{Symbol("aria-hidden")}}, args...) = nothing

draw_obj(t, o, defs) = @warn "Can't draw $t"
set_transform(t, args...) = @warn "Can't transform $t"
set_attr(::Val{:href}, args...) = nothing
set_attr(::Val{:d}, args...) = nothing
set_attr(::Val{:id}, args...) = nothing
set_attr(t, args...) = @warn "No attr match for $t"

"""
    svgwh(svg)

Return the width and height of svg displayed scaled to half the font size.
"""
function svgwh(svg)
    fsize = get_current_setting().fontsize
    xdoc = parse_string(svg)
    xroot = root(xdoc)
    # remove ex in the end
    ex_width = parse(Float64, attribute(xroot, "width")[1:(end - 2)])
    ex_height = parse(Float64, attribute(xroot, "height")[1:(end - 2)])
    return (fsize * (425 / 1000)) .* (ex_width, ex_height)
end

"""
    pathsvg(svg)

Convert an svg to a path using Luxor. Normally called via the `latex` command.
It handles only a subset of the full power of svg.
"""
function pathsvg(svg)
    fsize = get_current_setting().fontsize
    xdoc = parse_string(svg)
    xroot = root(xdoc)
    def_element = get_elements_by_tagname(xroot, "defs")[1]
    # create a dict for all the definitions
    defs = Dict{String,Any}()
    for def in collect(child_elements(def_element))
        defs[attribute(def, "id")] = def
    end
    x, y, width, height = parse.(Float64, split(attribute(xroot, "viewBox")))
    # remove ex in the end
    ex_width = parse(Float64, attribute(xroot, "width")[1:(end - 2)])
    ex_height = parse(Float64, attribute(xroot, "height")[1:(end - 2)])
    # everything capsulated in a layer
    @layer begin
        # unit ex: half of font size
        # such that we can scale half of a font size (size of a lower letter)
        # with the corresponding height of the svg canvas
        # and the ex_height given in it's description
        scale((fsize * (425 / 1000)) / (height / ex_height))
        translate(-x, -y)

        for child in collect(child_elements(xroot))
            sym_name = Symbol(name(child))
            @layer begin
                draw_obj(Val{sym_name}(), child, defs)
            end
        end
    end
end


using Reexport
using DataFrames
using CSV
using HTTP
using TOML
using Dates
using Statistics
using Distributions
using Optim
using Convex
using SCS
using JSON
using MathOptInterface
using StatsBase

# load my codes -
include("Types.jl")
include("Base.jl")
include("Network.jl")
include("Files.jl")
include("Compute.jl")
include("Transform.jl")
include("Factory.jl")
include("Simulations.jl")
include("Options.jl")


@testset "Construction overloading" begin
    model = load_model(StandardModel, model_paths["iJO1366.json"])

    rxn_original = model.reactions["NADH16pp"]
    nadh = model.metabolites["nadh_c"]
    h_c = model.metabolites["h_c"]
    q8 = model.metabolites["q8_c"]
    q8h2 = model.metabolites["q8h2_c"]
    nad = model.metabolites["nad_c"]
    h_p = model.metabolites["h_p"]

    rxn = nadh + 4.0 * h_c + 1.0 * q8 ⟶ 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p
    @test rxn.lb == 0.0 && rxn.ub > 0.0

    rxn = 1.0 * nadh + 4.0 * h_c + q8 ← 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p
    @test rxn.lb < 0.0 && rxn.ub == 0.0

    rxn = 1.0 * nadh + 4.0 * h_c + 1.0 * q8 ↔ q8h2 + nad + 3.0 * h_p
    @test rxn.lb < 0.0 && rxn.ub > 0.0

    rxn = 1.0 * nadh → nothing
    @test length(rxn.metabolites) == 1

    rxn = nothing → nadh
    @test length(rxn.metabolites) == 1

    rxn = nothing → 1.0nadh
    @test length(rxn.metabolites) == 1

    rxn = 1.0 * nadh + 4.0 * h_c + 1.0 * q8 ⟶ 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p
    @test prod(values(rxn.metabolites)) == -12
    @test ("q8h2_c" in [x for x in keys(rxn.metabolites)])

    rxn = nadh + 4.0 * h_c + 1.0 * q8 ⟶ 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p
    @test rxn.lb == 0.0 && rxn.ub > 0.0

    @test length(h_p + h_p) == 2
    @test length(h_p + h_p + h_p) == 3

    @test length(+([2.0q8, 1.0nad], nadh)) == 3
end


#excitation transfers - we default to regular transfers when no better candidate is found
exci_transfer_left(v,A,B=A) = transfer_left(v,A,B)
exci_transfer_right(v,A,B=A) = transfer_right(v,A,B)
exci_transfer_left(v,A,B,C) = transfer_left(v,A,B,C)
exci_transfer_right(v,A,B,C) = transfer_right(v,A,B,C)
exci_transfer_left(v,A,B,C,D) = transfer_left(v,A,B,C,D)
exci_transfer_right(v,A,B,C,D) = transfer_right(v,A,B,C,D)

#transfer, but the upper A is an excited tensor
exci_transfer_left(v::MPSBondTensor, A::MPOTensor, Ab::MPSTensor) = @tensor t[-1 -2;-3] := v[1,2]*A[2,3,-2,-3]*conj(Ab[1,3,-1])
exci_transfer_right(v::MPSBondTensor, A::MPOTensor, Ab::MPSTensor) = @tensor t[-1 -2;-3] := A[-1,3,-2,1]*v[1,2]*conj(Ab[-3,3,2])

#transfer, but there is both a utility leg and an mpo leg that is passed through
exci_transfer_left(v::MPOTensor, A::MPSTensor, Ab::MPSTensor=A) = @tensor v[-1 -2;-3 -4] := v[1,-2,-3,2]*A[2,3,-4]*conj(Ab[1,3,-1])
exci_transfer_right(v::MPOTensor, A::MPSTensor, Ab::MPSTensor=A) = @tensor v[-1 -2;-3 -4] := A[-1,3,1]*v[1,-2,-3,2]*conj(Ab[-4,3,2])

#transfer, but the upper A is an excited tensor and there is an mpo leg being passed through
exci_transfer_left(v::MPSTensor, A::MPOTensor, Ab::MPSTensor) = @tensor t[-1 -2;-3 -4] := v[1,-2,2]*A[2,3,-3,-4]*conj(Ab[1,3,-1])
exci_transfer_right(v::MPSTensor, A::MPOTensor, Ab::MPSTensor) = @tensor t[-1 -2;-3 -4] := A[-1,3,-2,1]*v[1,-3,2]*conj(Ab[-4,3,2])

#mpo transfer, but with A an excitation-tensor
exci_transfer_left(v::MPSTensor,O::MPOTensor,A::MPOTensor,Ab::MPSTensor) = @tensor t[-1 -2;-3 -4] := v[4,5,1]*A[1,3,-3,-4]*O[5,2,-2,3]*conj(Ab[4,2,-1])
exci_transfer_right(v::MPSTensor,O::MPOTensor,A::MPOTensor,Ab::MPSTensor) = @tensor t[-1 -2;-3 -4] := A[-1,1,-2,5]*O[-3,3,4,1]*conj(Ab[-4,3,2])*v[5,4,2]

#mpo transfer, with an excitation leg
exci_transfer_left(v::MPOTensor,O::MPOTensor,A::MPSTensor,Ab::MPSTensor=A) = @tensor v[-1 -2;-3 -4] := v[4,5,-3,1]*A[1,3,-4]*O[5,2,-2,3]*conj(Ab[4,2,-1])
exci_transfer_right(v::MPOTensor,O::MPOTensor,A::MPSTensor,Ab::MPSTensor=A) = @tensor v[-1 -2;-3 -4] := A[-1,1,5]*O[-3,3,4,1]*conj(Ab[-4,3,2])*v[5,-2,4,2]

#A is an excitation tensor; with an excitation leg
exci_transfer_left(vec::Array{V,1},ham::MPOHamiltonian,pos::Int,A::M,Ab::V=A) where V<:MPSTensor where M <:MPOTensor = exci_transfer_left(M,vec,ham,pos,A,Ab)
exci_transfer_right(vec::Array{V,1},ham::MPOHamiltonian,pos::Int,A::M,Ab::V=A) where V<:MPSTensor where M <:MPOTensor = exci_transfer_right(M,vec,ham,pos,A,Ab)

#v has an extra excitation leg
exci_transfer_left(vec::Array{V,1},ham::MPOHamiltonian,pos::Int,A::M,Ab::M=A) where V<:MPOTensor where M <:MPSTensor = exci_transfer_left(V,vec,ham,pos,A,Ab)
exci_transfer_right(vec::Array{V,1},ham::MPOHamiltonian,pos::Int,A::M,Ab::M=A) where V<:MPOTensor where M <:MPSTensor = exci_transfer_right(V,vec,ham,pos,A,Ab)

function exci_transfer_left(RetType,vec,ham::MPOHamiltonian,pos,A,Ab=A)
    toreturn = Array{RetType,1}(undef,length(vec));
    assigned = [false for i in 1:ham.odim]

    for (j,k) in keys(ham,pos)
        if assigned[k]
            if isscal(ham,pos,j,k)
                toreturn[k]+=ham.Os[pos,j,k]*exci_transfer_left(vec[j],A,Ab)
            else
                toreturn[k]+=exci_transfer_left(vec[j],ham[pos,j,k],A,Ab)
            end
        else
            if isscal(ham,pos,j,k)
                toreturn[k]=ham.Os[pos,j,k]*exci_transfer_left(vec[j],A,Ab)
            else
                toreturn[k]=exci_transfer_left(vec[j],ham[pos,j,k],A,Ab)
            end
            assigned[k]=true
        end
    end

    for k in 1:ham.odim
        if !assigned[k]
            #prefereably this never happens, because it's a wasted step
            #it's also avoideable with a little bit more code
            toreturn[k]=exci_transfer_left(vec[1],ham[pos,1,k],A,Ab)
        end
    end

    return toreturn
end
function exci_transfer_right(RetType,vec,ham::MPOHamiltonian,pos,A,Ab=A)
    toreturn = Array{RetType,1}(undef,length(vec));
    assigned = [false for i in 1:ham.odim]

    for (j,k) in keys(ham,pos)
        if assigned[j]
            if isscal(ham,pos,j,k)
                toreturn[j]+=ham.Os[pos,j,k]*exci_transfer_right(vec[k],A,Ab)
            else
                toreturn[j]+=exci_transfer_right(vec[k],ham[pos,j,k],A,Ab)
            end

        else
            if isscal(ham,pos,j,k)
                toreturn[j]=ham.Os[pos,j,k]*exci_transfer_right(vec[k],A,Ab)
            else
                toreturn[j]=exci_transfer_right(vec[k],ham[pos,j,k],A,Ab)
            end
            assigned[j]=true
        end
    end

    for j in 1:ham.odim
        if !assigned[j]
            toreturn[j]=exci_transfer_right(vec[1],ham[pos,j,1],A,Ab)
        end
    end

    return toreturn
end

#=
original code :
(lBsEr,convhist)=linsolve(RecursiveVec(lBs...),RecursiveVec(lBs...),GMRES()) do y
    x=collect(y.vecs)
    tor=reduce((a,b)->transfer_left(a,ham,b,mpsright.AR[b],mpsleft.AL[b])*exp(-1im*p),1:len,init=x)

    if(trivial)
        for i in ids
            @tensor tor[i][-1,-2,-3,-4]-=tor[i][1,-2,2,-4]*r_RL(mpsleft)[2,1]*l_RL(mpsleft)[-1,-3]
        end
    end

    tor=x-tor
    return  RecursiveVec(tor...)
end
lBsE=lBsEr.vecs

but this can be made faster; using the fact that the hamiltonion is upper-triangular, which is what we do here
=#

function left_excitation_transfer_system(lBs,ham,mpsleft::InfiniteMPS,mpsright::InfiniteMPS,trivial,p)
    len = ham.period
    found=zero.(lBs)
    ids = collect(Iterators.filter(x->isid(ham,x),1:ham.odim));

    for i in 1:ham.odim

        #this operation can be sped up by at least a factor 2;  found mostly consists of zeros
        start = found
        for k in 1:len
            start = exci_transfer_left(start,ham,k,mpsright.AR[k],mpsleft.AL[k])*exp(conj(1im*p))

            if trivial
                for l in ids[2:end-1]
                    @tensor start[l][-1,-2,-3,-4]-=start[l][1,-2,-3,2]*r_RL(mpsright,k)[2,1]*l_RL(mpsright,k)[-1,-4]
                end
            end
        end

        #either the element ham_ii exists; in which case we have to solve a linear system
        #otherwise it's easy and we already know found[i]
        if reduce((a,b)->contains(ham,b,i,i),1:len,init=true)
            (found[i],convhist)=linsolve(lBs[i]+start[i],lBs[i]+start[i],GMRES()) do y
                x=reduce((a,b)->exci_transfer_left(a,ham[b,i,i],mpsright.AR[b],mpsleft.AL[b])*exp(conj(1im*p)),1:len,init=y)

                if trivial && i in ids
                    @tensor x[-1,-2,-3,-4]-=x[1,-2,-3,2]*r_RL(mpsleft)[2,1]*l_RL(mpsleft)[-1,-4]
                end

                return y-x
            end
            if convhist.converged<1
                @info "left $(i) excitation inversion failed normres $(convhist.normres)"
            end
        else
            found[i]=lBs[i]+start[i]
        end
    end
    return found
end

function right_excitation_transfer_system(rBs,ham,mpsleft,mpsright::InfiniteMPS,trivial,p)
    len = ham.period
    found=zero.(rBs)
    ids = collect(Iterators.filter(x->isid(ham,x),1:ham.odim));
    for i in ham.odim:-1:1

        start = found
        for k in len:-1:1
            start = exci_transfer_right(start,ham,k,mpsleft.AL[k],mpsright.AR[k])*exp(1im*p)

            if trivial
                for l in ids[2:end-1]
                    @tensor start[l][-1,-2,-3,-4]-=start[l][1,-2,-3,2]*l_LR(mpsright,k)[2,1]*r_LR(mpsright,k)[-1,-4]
                end
            end
        end

        if reduce((a,b)->contains(ham,b,i,i),1:len,init=true)
            (found[i],convhist)=linsolve(rBs[i]+start[i],rBs[i]+start[i],GMRES()) do y
                x=reduce((a,b)->exci_transfer_right(a,ham[b,i,i],mpsleft.AL[b],mpsright.AR[b])*exp(1im*p),len:-1:1,init=y)

                if trivial && i in ids
                    @tensor x[-1,-2,-3,-4]-=x[1,-2,-3,2]*l_LR(mpsright)[2,1]*r_LR(mpsright)[-1,-4]
                end

                return y-x
            end
            if convhist.converged<1
                @info "right $(i) excitation inversion failed normres $(convhist.normres)"
            end
        else
            found[i]=rBs[i]+start[i]
        end
    end
    return found
end


using FiniteDifferences: rand_tangent, difference

function test_difference(ε::Real, x, dx)
    y = x + ε * dx
    dx_diff = difference(ε, y, x)
    # TODO: `@test isapprox(dx, dx_diff)` once `isapprox` is defined appropriately
    # see https://github.com/JuliaDiff/ChainRulesCore.jl/issues/184
    @test typeof(dx) == typeof(dx_diff)
end

@testset "difference" begin

    @testset "Primal: $(typeof(x))" for (ε, x) in [

        # Test things that don't have tangents.
        (randn(), :a),
        (randn(), 'a'),
        (randn(), "a"),
        (randn(), 0),

        # Test Numbers.
        (randn(Float32), randn(Float32)),
        (randn(Float64), randn(Float64)),
        (randn(Float32), randn(ComplexF32)),
        (randn(Float64), randn(ComplexF64)),

        # Test StridedArrays.
        (randn(), randn(5)),
        (randn(Float32), randn(ComplexF32, 5, 2)),
        (randn(), [randn(1) for _ in 1:3]),
        (randn(), [randn(5, 4), "a"]),

        # Tuples.
        (randn(), (randn(5, 2), )),
        (randn(), (randn(), 4)),
        (randn(), (4, 3, 2)),

        # NamedTuples.
        (randn(), (a=randn(5, 2),)),
        (randn(), (a=randn(), b=4)),
        (randn(), (a=4, b=3, c=2)),

        # Arbitrary structs.
        (randn(), sin),
        (randn(), cos),
        (randn(), Foo(5.0, 4, randn(5, 2))),
        (randn(), Foo(randn(), 1, Foo(randn(), 1, 1))),

        # LinearAlgebra types (also just structs).
        (randn(), UpperTriangular(randn(2, 2))),
        (randn(), Diagonal(randn(4))),
        (randn(), SVector{2, Float64}(1.0, 2.0)),
        (randn(), SMatrix{2, 2, ComplexF64}(1.0, 2.0, 3.0, 4.0)),
        (randn(), Symmetric(randn(2, 2))),
        (randn(), Hermitian(randn(ComplexF64, 1, 1))),
        (randn(), Adjoint(randn(ComplexF64, 3, 3))),
        (randn(), Transpose(randn(3))),
    ]
        test_difference(ε, x, rand_tangent(x))
    end

    # Ensure struct fallback errors for non-struct types.
    @test_throws ArgumentError invoke(difference, Tuple{Float64, T, T} where {T}, 5.0, 5.0, 5.0)
end


function add!(d::Dag, parents)  # TODO rename sum!
    # TODO parents is similar to `combine!` ... need a type info for doc
    cn = combine!(d, parents)

    # combine! return a BVal{Tuple{...}}
    # the retuned node type after add! is promote_type of all types in tuple
    CT = promote_type(eltype(eltype(cn)).parameters...)  # TODO method of BVal
    @assert promote_type(map(p -> eltype(p[2]), parents)...) == CT

    return node!(d, CT, [(cn, v -> Nullable(sum(v.val)))])
end

# TODO doc; similar to combining version, but all nodes always trigger and
#      always use Latest value of previous nodes
function add!(d::Dag, nodes...)
    return apply!(d, sum, 0, nodes...)
end

function first!(d::Dag, node::Node{T}) where {T<:Tuple}
    nth!(d, node, 1)
end

function last!(d::Dag, node::Node{T}) where {T<:Tuple}
    # Cannot call nth! directly as below
    # because `end` is not recognize as last elt index
    # nth!(d, node, end)
    nth!(d, node, length(eltype(node).types))
end

function nth!(d::Dag, node::Node{T}, n) where {T<:Tuple}
    EE = eltype(node).types[n]
    node!(d, EE, [(node, v -> Nullable(v[n]))])
end

# create a moving window node, i.e. transform input in vector of last N values
# TODO maybe have a diff output: instead of vector could also output what's
#      added and what's removed (for online stream calculation) ... maybe a
#      diff win! interface can do that
function win!(d::Dag, node::Node, n)
    EE = Vector{eltype(node)}
    # TODO Datastructures.jl CircularDeque
    vec = EE()

    function win_push!(v)
        while length(vec) >= n
            shift!(vec)
        end
        push!(vec, v)
    end

    # node!(d, EE, [(node, v -> Nullable(win_push!(v)))])
    tr!(d, node, win_push!)
end


#module NaquadahEvents
# using Cairo, Gtk, Gtk.ShortNames

export  splotch, ScrollEvent, # not sure all this needs exported (?)
        DragEvent, MouseDownEvent, ClickEvent #, Event,

# ======================================================================================
#
# ======================================================================================
function DragEvent(document, widget, event)
    pressed, released = document.event.pressed[1], document.event.pressed[end]
    splotch(widget, pressed,1.0,0.0,0.0)

    ctx = getgc(widget)
    move_to(ctx,pressed.x, pressed.y)
    line_to(ctx,released.x, released.y)
    stroke(ctx)

    splotch(widget, released,1.0,0.0,0.0)
    println("drag from X $(pressed.x), Y $(pressed.y) to X $(released.x), Y $(released.y)")
    document.event.pressed = []
end

# ======================================================================================
#
# ======================================================================================
onArea(x, y,  shape ) = onArea(x, y,  getBorderBox(shape)... ) # WAS: getContentBox

function onArea(x, y,    l, t, w, h)
        return x > l && x < l+w && y > t && y < t+h ? true : false
end
# ======================================================================================
function insideParentClip(x, y, node)
    return onArea( x, y,  lastClipParent(node).shape )
end
# ======================================================================================
function order(a,b)
    a<b ? (a,b) : (b,a)
end
# ======================================================================================
function order2(l,r,t,b)
    l, r = order(l,r)
    t, b = order(t,b)
    return (l,r,t,b)
end
# ======================================================================================
function boxesOverlap(shape, l,r,t,b)
    return onArea(l, t, shape) || onArea(l, b, shape) || onArea(r, t, shape) || onArea(r, b, shape)
end
# ======================================================================================
# Also create: function findNodesAtPoint(node, x, y)
# ======================================================================================
function findNodesInBox(node, left, top, right, bottom)
    items = []
    shape = node.shape
    box = getContentBox(shape)
    started = false

    # TODO: also include some test to exclude "unselectable" nodes.
    function looper(rows)
        for r in rows
            for n in r.nodes
                        if isdefined(n, :rows)
                            looper(n.rows)
                        else
                                s = n.shape
                                if onArea(left, top, s)         # ¡Selection Start
                                    push!(items, n)
                                    started = true
                                    if onArea(right, bottom, s)
                                        started = false
                                        return items
                                    end
                                elseif onArea(right, bottom, s) # Selection End!
                                    push!(items, n)
                                    started = false
                                    return items
                                elseif started
                                    push!(items, n)
                                end

                        end
            end
        end
        return items
    end

    looper(node.rows)

    return items
end
# ======================================================================================
#
# a,b,c,d = [1],[2],[3],[4]
# l = [a b b c d a a]
# same(list, object) = list .=== object
# ======================================================================================
function selectText(ctx, document, pressed, released)

    left, right, top, bottom = order2(pressed.x,released.x, pressed.y,released.y)
    selected = findNodesInBox(document.children[1].children[3], left, top, right, bottom)

    clipPath = getContentBox(document.children[1].children[3].shape)
    # Objective here is to remove duplicates and also repaint tainted rows
    selectedNodes = document.event.selectedNodes

    found = false
for oldnode in selectedNodes
    for node in selected
        found = false
         if node ===  oldnode
             found = true
         end
    end
    if !found
        c = inheritColor(oldnode.parent)

        setcolor( ctx, c...)
        rectangle(ctx,  oldnode.shape.left-1, oldnode.shape.top-1, oldnode.shape.width+2, oldnode.shape.height+2 )
        fill(ctx)
        DrawText(ctx, oldnode, clipPath)
    end
end

    # buffer = []
    # for shape in selected
    #     if shape.top > bottom || (shape.top+shape.height) < top # outside range and needs removed
    #         setcolor( ctx, shape.parent.shape.color...)
    #         rectangle(ctx,  shape.left, shape.top, shape.width, shape.height )
    #         fill(ctx)
    #         DrawText(ctx, shape, clipPath)
    #     else
    #         push!(buffer,shape)
    #     end
    # end
    # selected = buffer

    if pressed.y < released.y # just in case the selection is made from bottom to top
        DrawSelectedText(ctx, selected, pressed.x, released.x, pressed.y, released.y, clipPath)
    else
        DrawSelectedText(ctx, selected, released.x, pressed.x, released.y, pressed.y, clipPath)
    end
    document.event.selectedNodes = selected

end
# ======================================================================================
#
# ======================================================================================
function DrawEvent(document, widget, event)
    println("move x$(event.x) y$(event.y)")
end
# ======================================================================================
#
# ======================================================================================
function MouseDownEvent(document, widget, event)
    down = document.eventsList.mousedown
    #println("clicked: ", event.x, event.y)
    for node in down

        if onArea(event.x, event.y, node.shape ) && insideParentClip( event.x, event.y, node)
            ctx = getgc(widget)
            page = document.children[1].children[3]

            if node.shape.color[2] > 0.9
                node.temp = deepcopy(node.shape)
                node.shape.color = [0.996094, 0.5, 0.5]
            else
                node.shape = deepcopy(node.temp)
            end
            drawNode(ctx, document, node)
            reveal(widget)
        end
    end
end
#setAttribute(node, "color", "red")
#AtributesToLayout(document, node, false)
# ======================================================================================

# ======================================================================================
function setAttribute(d, key::String, attribute::String)
    println(d.DOM)
    d.DOM[key] = attribute
end
# ======================================================================================
#
# ======================================================================================
function ClickEvent(document, widget, event)
    clicked = document.eventsList.click
    splotch(widget, event, 1.0,0.0,0.0)
    document.event.pressed = []
    for node in clicked
        if onArea(event.x, event.y, node.shape ) && insideParentClip( event.x, event.y, node)
            if haskey(node.DOM, "click")
                click = node.DOM["click"]
                println(click)
                # if haskey(click, "code")
                    code = Meta.parse(click)
                    ex = :($(Expr(:toplevel, :(document = $document), :($code))))
                    eval(ex)
                    # WAS: eval(parse(click["code"]))
                # end
            end
            #"click":{"code":"println(\"This is some code!\")", "preventDefault":true}

        end
    end

end
# ======================================================================================
#
# ======================================================================================
function OnObject(document, x, y, all=true)

end
# ======================================================================================
#
# ======================================================================================
function ScrollEvent(document, widget, event)
  ctx = getgc(widget)
  node = document.children[1].children[3] # TODO: fix! ..test to see if mouse is over object.

  Unit = 70.0

  # I am scrolling(jumping) by 30px here but Opera scrolls by about 50px
  # Opera lacks smoothness too but it seems to transition-scroll by the 50px
  # ...so using the mouse wheel it is impossible to move less than that increment.
  function drawTwice()
      setcolor(ctx, node.shape.color...)
      rectangle(ctx,  node.shape.left,  node.shape.top, node.shape.width,  node.shape.height )
      fill(ctx);

      DrawViewport(ctx, document, node)
      reveal(widget)
  end

  # SCROLL UP!
  if event.direction == 0 && node.scroll.y < node.shape.top # was: 0
    diff = abs(node.scroll.y)
    if diff < Unit
      Unit = diff
    end
    node.scroll.y += (Unit*.5)
    VmoveAllChildren(node, (Unit*.5), false)
    drawTwice()
    node.scroll.y += (Unit*.5)
    VmoveAllChildren(node, (Unit*.5), false)
    drawTwice()
    # SCROLL DOWN!
elseif event.direction == 1 && (node.scroll.contentHeight + node.scroll.y + node.shape.top) > node.shape.height
      diff = (node.scroll.contentHeight + node.scroll.y + node.shape.top) - node.shape.height
      if diff < Unit
        Unit = diff
      end
      node.scroll.y -= (Unit*.5)
      VmoveAllChildren(node, -(Unit*.5), false) # -30
      drawTwice()
      node.scroll.y -= (Unit*.5)
      VmoveAllChildren(node, -(Unit*.5), false) # -30
      drawTwice()
  end

  # #set_source_rgb(ctx, 1,1,1)
  # setcolor( ctx, node.shape.color...)
  # rectangle(ctx,  node.shape.left,  node.shape.top, node.shape.width,  node.shape.height )
  # fill(ctx);
  #
  # DrawViewport(ctx, document, node)
  # reveal(widget)

end

function drawTwice()
    setcolor( ctx, node.shape.color...)
    rectangle(ctx,  node.shape.left,  node.shape.top, node.shape.width,  node.shape.height )
    fill(ctx);

    DrawViewport(ctx, document, node)
    reveal(widget)
end
# ======================================================================================
# Draw a splotch
# CALLED FROM:
# ======================================================================================
function splotch(widget,event,r,g,b)
      ctx = getgc(widget)
            deg = (pi/180.0)
        s1, e1 = 1 * deg, 100 * deg
        s2, e2 = 120 * deg, 220 * deg
        s3, e3 = 240 * deg, 340 * deg
        move_to(ctx, event.x, event.y)
        set_line_width(ctx, 2.56);
        set_antialias(ctx,4)
        set_source_rgb(ctx, r, g, b)
        arc(ctx, event.x, event.y, 2, 0, 2pi) # 0, 2pi
        stroke(ctx)

        #set_line_width(ctx, 3);
        set_source_rgba(ctx, r, g, b, 0.7)
        arc(ctx, event.x, event.y, 5, s1, e1) # 0, 2pi
        stroke(ctx)
        arc(ctx, event.x, event.y, 5, s2, e2) # 0, 2pi
        stroke(ctx)
        arc(ctx, event.x, event.y, 5, s3, e3) # 0, 2pi
        stroke(ctx)
        # arc_negative(cr, xc, yc, node.shape.radius,
        #     node.shape.angle[1] * (pi/180.0),
        #     node.shape.angle[2] * (pi/180.0));

        #set_line_width(ctx, 2);
        set_source_rgba(ctx, r, g, b, 0.5)
        arc(ctx, event.x, event.y, 8, s1, e1)
        stroke(ctx)
        arc(ctx, event.x, event.y, 8, s2, e2)
        stroke(ctx)
        arc(ctx, event.x, event.y, 8, s3, e3)
        stroke(ctx)

        #set_line_width(ctx, 1);
        set_source_rgba(ctx, r, g, b, 0.3)
        arc(ctx, event.x, event.y, 11, s1, e1)
        stroke(ctx)
        arc(ctx, event.x, event.y, 11, s2, e2)
        stroke(ctx)
        arc(ctx, event.x, event.y, 11, s3, e3)
        stroke(ctx)
        reveal(widget)
    end
#end # module


for (F,f) in ((:M,:m), (:S,:s))
    frange = Symbol(f, :range)
    _frange = Symbol(:_, f, :range)
    _fcolon_colon = Symbol(:_, f, :colon_colon)
    fcolon = Symbol(:_, f, :colon)
    _frangestyle = Symbol(:_, f, :rangestyle)
    floatfrange = Symbol(:float, f, :range)
    stepfrangelen_hp = Symbol(:step, f, :rangelen_hp)
    flinspace1 = Symbol(:_, f, :linspace1)
    flinspace = Symbol(:_, f, :linspace)

    LINR = Symbol(:Lin, F, :Range)
    SRL = Symbol(:Step, F, :RangeLen)
    SR = Symbol(:Step, F, :Range)
    UR = Symbol(:Unit, F, :Range)
    @eval begin
        function $(frange)(start; length::Union{Integer,Nothing}=nothing, stop=nothing, step=nothing)
            return $(_frange)(start, step, stop, length)
        end

        function $(frange)(start, stop; length::Union{Integer,Nothing}=nothing, step=nothing)
            return $(_frange)(start, step, stop, length)
        end

        # Range from start to stop: range(a, [step=s,] stop=b), no length
        $(_frange)(start, step,      stop, ::Nothing) = $(fcolon)(start, step, stop)
        $(_frange)(start, ::Nothing, stop, ::Nothing) = $(fcolon)(start, stop)

        $(fcolon)(a::Real, b::Real) = $(fcolon)(promote(a,b)...)

        $(fcolon)(start::T, stop::T) where {T<:Real} = $(UR){T}(start, stop)

        $(fcolon)(start::T, stop::T) where {T} = $(fcolon)(start, oftype(stop-start, 1), stop)

        $(fcolon)(a::T, b::T) where {T<:AbstractFloat} = $(fcolon)(a, T(1), b)

       function $(fcolon)(a::T, b::AbstractFloat, c::T) where {T<:Real}
            return $(fcolon)(promote(a,b,c)...)
        end
        function $(fcolon)(a::T, b::AbstractFloat, c::T) where {T<:AbstractFloat}
            return $(fcolon)(promote(a,b,c)...)
        end
        function $(fcolon)(a::T, b::Real, c::T) where {T<:AbstractFloat}
            return $(fcolon)(promote(a,b,c)...)
        end

        # promote start and stop, leaving step alone
        function $(fcolon)(start::A, step, stop::C) where {A<:Real,C<:Real}
            return $(fcolon)(convert(promote_type(A, C), start),
                             step,
                             convert(promote_type(A, C), stop)
                            )
        end
        function $(fcolon)(start::T, step, stop::T) where {T}
            return $(_fcolon_colon)(start, step, stop)
        end
        function $(fcolon)(start::T, step, stop::T) where {T<:Real}
            return $(_fcolon_colon)(start, step, stop)
        end

        function $(fcolon)(start::T, step::T, stop::T) where T<:Union{Float16,Float32,Float64}
            step == 0 && throw(ArgumentError("range step cannot be zero"))
            # see if the inputs have exact rational approximations (and if so,
            # perform all computations in terms of the rationals)
            step_n, step_d = Base.rat(step)
            if step_d != 0 && T(step_n/step_d) == step
                start_n, start_d = Base.rat(start)
                stop_n, stop_d = Base.rat(stop)
                if start_d != 0 && stop_d != 0 &&
                        T(start_n/start_d) == start && T(stop_n/stop_d) == stop
                    den = lcm(start_d, step_d) # use same denominator for start and step
                    m = maxintfloat(T, Int)
                    if den != 0 && abs(start*den) <= m && abs(step*den) <= m &&  # will round succeed?
                            rem(den, start_d) == 0 && rem(den, step_d) == 0      # check lcm overflow
                        start_n = round(Int, start*den)
                        step_n = round(Int, step*den)
                        len = max(0, div(den*stop_n - stop_d*start_n + step_n*stop_d, step_n*stop_d))
                        # Integer ops could overflow, so check that this makes sense
                        if Base.isbetween(start, start + (len-1)*step, stop + step/2) &&
                                !Base.isbetween(start, start + len*step, stop)
                            # Return a 2x precision range
                            return $(floatfrange)(T, start_n, step_n, len, den)
                        end
                    end
                end
            end
            # Fallback, taking start and step literally
            lf = (stop-start)/step
            if lf < 0
                len = 0
            elseif lf == 0
                len = 1
            else
                len = round(Int, lf) + 1
                stop′ = start + (len-1)*step
                # if we've overshot the end, subtract one:
                len -= (start < stop < stop′) + (start > stop > stop′)
            end
            $(stepfrangelen_hp)(T, start, step, 0, len, 1)
        end
        # AbstractFloat specializations
        function $(_fcolon_colon)(start::T, step, stop::T) where {T}
            T′ = typeof(start+zero(step))
            return $(SR)(convert(T′,start), step, convert(T′,stop))
        end

        function $(fcolon)(start::T, step::T, stop::T) where {T<:AbstractFloat}
            return $(_fcolon_colon)(Base.OrderStyle(T), Base.ArithmeticStyle(T), start, step, stop)
        end
        $(fcolon)(start::T, step::T, stop::T) where {T<:Real} =
            $(_fcolon_colon)(Base.OrderStyle(T), Base.ArithmeticStyle(T), start, step, stop)
        $(_fcolon_colon)(::Base.Ordered, ::Any, start::T, step, stop::T) where {T} = $(SR)(start, step, stop)
        # for T<:Union{Float16,Float32,Float64} see twiceprecision.jl
        $(_fcolon_colon)(::Base.Ordered, ::Base.ArithmeticRounds, start::T, step, stop::T) where {T} =
            $(SRL)(start, step, floor(Int, (stop-start)/step)+1)
        $(_fcolon_colon)(::Any, ::Any, start::T, step, stop::T) where {T} =
            $(SRL)(start, step, floor(Int, (stop-start)/step)+1)
        # range of a given length: range(a, [step=s,] length=l), no stop
        $(_frange)(a::Real,          ::Nothing,         ::Nothing, len::Integer) = $(UR){typeof(a)}(a, oftype(a, a+len-1))
        $(_frange)(a::AbstractFloat, ::Nothing,         ::Nothing, len::Integer) = $(_frange)(a, oftype(a, 1),   nothing, len)
        $(_frange)(a::AbstractFloat, st::AbstractFloat, ::Nothing, len::Integer) = $(_frange)(promote(a, st)..., nothing, len)
        $(_frange)(a::Real,          st::AbstractFloat, ::Nothing, len::Integer) = $(_frange)(float(a), st,      nothing, len)
        $(_frange)(a::AbstractFloat, st::Real,          ::Nothing, len::Integer) = $(_frange)(a, float(st),      nothing, len)
        $(_frange)(a,                ::Nothing,         ::Nothing, len::Integer) = $(_frange)(a, oftype(a-a, 1), nothing, len)

        function $(_frange)(a::T, step::T, ::Nothing, len::Integer) where {T <: AbstractFloat}
            return $(_frangestyle)(Base.OrderStyle(T), Base.ArithmeticStyle(T), a, step, len)
        end
        function $(_frange)(a::T, step, ::Nothing, len::Integer) where {T}
            return $(_frangestyle)(Base.OrderStyle(T), Base.ArithmeticStyle(T), a, step, len)
        end
        function $(_frangestyle)(::Base.Ordered, ::Base.ArithmeticWraps, a::T, step::S, len::Integer) where {T,S}
            return $(SR){T,S}(a, step, convert(T, a+step*(len-1)))
        end
        function $(_frangestyle)(::Any, ::Any, a::T, step::S, len::Integer) where {T,S}
            return $(SRL){typeof(a+0*step),T,S}(a, step, len)
        end

        function $(_frange)(start::T, ::Nothing, stop::T, len::Integer) where {T<:IEEEFloat}
            len < 2 && return $(flinspace1)(T, start, stop, len)
            start == stop && $(stepfrangelen_hp)(T, start, zero(T), 0, len, 1)
            # Attempt to find exact rational approximations
            start_n, start_d = rat(start)
            stop_n, stop_d = rat(stop)
            if start_d != 0 && stop_d != 0
                den = lcm(start_d, stop_d)
                m = maxintfloat(T, Int)
                if den != 0 && abs(den*start) <= m && abs(den*stop) <= m
                    start_n = round(Int, den*start)
                    stop_n = round(Int, den*stop)
                    if T(start_n/den) == start && T(stop_n/den) == stop
                        return $(flinspace)(T, start_n, stop_n, len, den)
                    end
                end
            end
            return $(flinspace)(start, stop, len)
        end

        $(_frange)(start,     step,      ::Nothing, ::Nothing) = # range(a, step=s)
            throw(ArgumentError("At least one of `length` or `stop` must be specified"))
        $(_frange)(start,     ::Nothing, ::Nothing, ::Nothing) = # range(a)
            throw(ArgumentError("At least one of `length` or `stop` must be specified"))
        $(_frange)(::Nothing, ::Nothing, ::Nothing, ::Nothing) = # range(nothing)
            throw(ArgumentError("At least one of `length` or `stop` must be specified"))
        $(_frange)(start::Real, step::Real, stop::Real, length::Integer) = # range(a, step=s, stop=b, length=l)
            throw(ArgumentError("Too many arguments specified; try passing only one of `stop` or `length`"))
        $(_frange)(::Nothing, ::Nothing, ::Nothing, ::Integer) = # range(nothing, length=l)
            throw(ArgumentError("Can't start a range at `nothing`"))

        function $(_frange)(start::T, ::Nothing, stop::S, len::Integer) where {T,S}
            a, b = promote(start, stop)
            return $(_frange)(a, nothing, b, len)
        end
        $(_frange)(start::T, ::Nothing, stop::T, len::Integer) where {T<:Real} = $(LINR){T}(start, stop, len)
        $(_frange)(start::T, ::Nothing, stop::T, len::Integer) where {T} = $(LINR){T}(start, stop, len)
        $(_frange)(start::T, ::Nothing, stop::T, len::Integer) where {T<:Integer} =
            $(flinspace)(float(T), start, stop, len)
        ## for Float16, Float32, and Float64 we hit twiceprecision.jl to lift to higher precision StepRangeLen
        # for all other types we fall back to a plain old LinRange
        $(flinspace)(::Type{T}, start::Integer, stop::Integer, len::Integer) where T = $(LINR){T}(start, stop, len)

        function $(flinspace)(::Type{T}, start::Integer, stop::Integer, len::Integer) where {T<:IEEEFloat}
            return $(flinspace)(T, start, stop, len, 1)
        end
        function $(flinspace)(
            ::Type{T},
            start_n::Integer,
            stop_n::Integer,
            len::Integer,
            den::Integer
           ) where {T<:IEEEFloat}
            len < 2 && return $(flinspace1)(T, start_n/den, stop_n/den, len)
            start_n == stop_n && return $(stepfrangelen_hp)(T, (start_n, den), (zero(start_n), den), 0, len, 1)
            tmin = -start_n/(Float64(stop_n) - Float64(start_n))
            imin = round(Int, tmin*(len-1)+1)
            imin = clamp(imin, 1, Int(len))
            ref_num = Int128(len-imin) * start_n + Int128(imin-1) * stop_n
            ref_denom = Int128(len-1) * den
            ref = (ref_num, ref_denom)
            step_full = (Int128(stop_n) - Int128(start_n), ref_denom)
            return $(stepfrangelen_hp)(T, ref, step_full,  nbitslen(T, len, imin), Int(len), imin)
        end

        function $(flinspace1)(::Type{T}, start, stop, len::Integer) where {T<:IEEEFloat}
            len >= 0 || throw(ArgumentError("range($start, stop=$stop, length=$len): negative length"))
            if len <= 1
                len == 1 && (start == stop || throw(ArgumentError("range($start, stop=$stop, length=$len): endpoints differ")))
                # Ensure that first(r)==start and last(r)==stop even for len==0
                # The output type must be consistent with steprangelen_hp
                if T<:Union{Float32,Float16}
                    return $(SRL){T}(Float64(start), Float64(start) - Float64(stop), len, 1)
                else
                    return $(SRL)(TwicePrecision(start, zero(T)), TwicePrecision(start, -stop), len, 1)
                end
            end
            throw(ArgumentError("should only be called for len < 2, got $len"))
        end
        function $(_frange)(a::T, st::T, ::Nothing, len::Integer) where T<:Union{Float16,Float32,Float64}
            start_n, start_d = Base.rat(a)
            step_n, step_d = Base.rat(st)
            if start_d != 0 && step_d != 0 &&
                    T(start_n/start_d) == a && T(step_n/step_d) == st
                den = lcm(start_d, step_d)
                m = Base.maxintfloat(T, Int)
                if abs(den*a) <= m && abs(den*st) <= m &&
                        rem(den, start_d) == 0 && rem(den, step_d) == 0
                    start_n = round(Int, den*a)
                    step_n = round(Int, den*st)
                    return $(floatfrange)(T, start_n, step_n, len, den)
                end
            end
            return $(stepfrangelen_hp)(T, a, st, 0, len, 1)
        end

        function $(stepfrangelen_hp)(
            ::Type{Float64},
            ref::Tuple{Integer,Integer},
            step::Tuple{Integer,Integer},
            nb::Integer,
            len::Integer,
            offset::Integer
           )
            $(SRL)(TwicePrecision{Float64}(ref), TwicePrecision{Float64}(step, nb), Int(len), offset)
        end

        function $(stepfrangelen_hp)(
            ::Type{T},
            ref::Tuple{Integer,Integer},
            step::Tuple{Integer,Integer},
            nb::Integer,
            len::Integer,
            offset::Integer
           ) where {T<:IEEEFloat}
            return $(SRL){T}(ref[1]/ref[2], step[1]/step[2], Int(len), offset)
        end

        function $(stepfrangelen_hp)(
            ::Type{Float64},
            ref::Base.F_or_FF,
            step::Base.F_or_FF,
            nb::Integer,
            len::Integer,
            offset::Integer
           )
            return $(SRL)(Base._TP(ref), twiceprecision(Base._TP(step), nb), Int(len), offset)
        end

        function $(stepfrangelen_hp)(
            ::Type{T}, ref::Base.F_or_FF,
            step::Base.F_or_FF, nb::Integer,
            len::Integer,
            offset::Integer
           ) where {T<:IEEEFloat}
            return $(SRL){T}(Base.asF64(ref), Base.asF64(step), Int(len), offset)
        end
        function $(floatfrange)(::Type{T}, start_n::Integer, step_n::Integer, len::Integer, den::Integer) where T
            if len < 2 || step_n == 0
                return $(stepfrangelen_hp)(T, (start_n, den), (step_n, den), 0, Int(len), 1)
            end
            # index of smallest-magnitude value
            imin = clamp(round(Int, -start_n/step_n+1), 1, Int(len))
            # Compute smallest-magnitude element to 2x precision
            ref_n = start_n+(imin-1)*step_n  # this shouldn't overflow, so don't check
            nb = nbitslen(T, len, imin)
            return $(stepfrangelen_hp)(T, (ref_n, den), (step_n, den), nb, Int(len), imin)
        end

        function $(floatfrange)(a::AbstractFloat, st::AbstractFloat, len::Real, divisor::AbstractFloat)
            T = promote_type(typeof(a), typeof(st), typeof(divisor))
            m = maxintfloat(T, Int)
            if abs(a) <= m && abs(st) <= m && abs(divisor) <= m
                ia, ist, idivisor = round(Int, a), round(Int, st), round(Int, divisor)
                if ia == a && ist == st && idivisor == divisor
                    # We can return the high-precision range
                    return $(floatfrange)(T, ia, ist, Int(len), idivisor)
                end
            end
            # Fallback (misses the opportunity to set offset different from 1,
            # but otherwise this is still high-precision)
            return $(stepfrangelen_hp)(T, (a,divisor), (st,divisor), Base.nbitslen(T, len, 1), Int(len), 1)
        end
        function $(flinspace)(start::T, stop::T, len::Integer) where {T<:IEEEFloat}
            (isfinite(start) && isfinite(stop)) || throw(ArgumentError("start and stop must be finite, got $start and $stop"))
            # Find the index that returns the smallest-magnitude element
            Δ, Δfac = stop-start, 1
            if !isfinite(Δ)   # handle overflow for large endpoints
                Δ, Δfac = stop/len - start/len, Int(len)
            end
            tmin = -(start/Δ)/Δfac            # t such that (1-t)*start + t*stop == 0
            imin = round(Int, tmin*(len-1)+1) # index approximately corresponding to t
            if 1 < imin < len
                # The smallest-magnitude element is in the interior
                t = (imin-1)/(len-1)
                ref = T((1-t)*start + t*stop)
                step = imin-1 < len-imin ? (ref-start)/(imin-1) : (stop-ref)/(len-imin)
            elseif imin <= 1
                imin = 1
                ref = start
                step = (Δ/(len-1))*Δfac
            else
                imin = Int(len)
                ref = stop
                step = (Δ/(len-1))*Δfac
            end
            if len == 2 && !isfinite(step)
                # For very large endpoints where step overflows, exploit the
                # split-representation to handle the overflow
                return $(stepfrangelen_hp)(T, start, (-start, stop), 0, 2, 1)
            end
            # 2x calculations to get high precision endpoint matching while also
            # preventing overflow in ref_hi+(i-offset)*step_hi
            m, k = prevfloat(floatmax(T)), max(imin-1, len-imin)
            step_hi_pre = clamp(step, max(-(m+ref)/k, (-m+ref)/k), min((m-ref)/k, (m+ref)/k))
            nb = nbitslen(T, len, imin)
            step_hi = truncbits(step_hi_pre, nb)
            x1_hi, x1_lo = add12((1-imin)*step_hi, ref)
            x2_hi, x2_lo = add12((len-imin)*step_hi, ref)
            a, b = (start - x1_hi) - x1_lo, (stop - x2_hi) - x2_lo
            step_lo = (b - a)/(len - 1)
            ref_lo = a - (1 - imin)*step_lo
            return $(stepfrangelen_hp)(T, (ref, ref_lo), (step_hi, step_lo), 0, Int(len), imin)
        end
    end
end


# # A fast [Trie](https://github.com/gkappler/Tries.jl)-based parser for a collection of literal Strings.
# Matching any one of many `String`s (e.g. names) is slow in PCRE with `|` alternatives.
using Random
s = [ randstring(10) for _ in 1:1000 ];
re = Regex(join(s,"|"));

using BenchmarkTools
@benchmark match(re,s[end])

# A optimized state-machine parser will perform much better.
#
# TODO: Benchmark Automa.jl here
#
# `CombinedParsers` does not use state machines, but
# provides fast `_iterate` implementation for `Either{Trie{Char}}`,
# that is constructed with `Either(::Vector{<:AbstractString})`
using CombinedParsers
pc = Either(s);
@benchmark match(pc,s[end])

# Note that `pc` does not capture the result by default with `parse`.
parse(!pc,s[end])

# For large word collections, PCRE fails
s = [ randstring(10) for _ in 1:10000 ];
re = Regex(join(s,"|"));

# `CombinedParsers` displays good time complexity.
pc = Either(s);
@benchmark Either(s);
@benchmark match(pc,s[end])

# `Either{Trie{Char}}` can be used to perform fast text search in Julia.
# ### Next steps
# - Flesh out text search example.
# - Parallelization
# - Relate to Aho-Corasick algorithm.


__precompile__()

module Colors

using FixedPointNumbers, ColorTypes, Reexport, Compat
@reexport using ColorTypes

typealias AbstractGray{T} Color{T,1}
typealias Color3{T} Color{T,3}

import Base: ==, +, -, *, /
import Base: convert, eltype, hex, isless, linspace, show, typemin, typemax

# Additional exports, not exported by ColorTypes
export weighted_color_mean,
       hex, @colorant_str,
       protanopic, deuteranopic, tritanopic,
       distinguishable_colors, whitebalance,
       colordiff, DE_2000, DE_94, DE_JPC79, DE_CMC, DE_BFD, DE_AB, DE_DIN99, DE_DIN99d, DE_DIN99o,
       MSC, sequential_palette, diverging_palette, colormap,
       colormatch, CIE1931_CMF, CIE1964_CMF, CIE1931J_CMF, CIE1931JV_CMF

# Early utilities
include("utilities.jl")

# Include other module components
include("conversions.jl")
include("algorithms.jl")
include("parse.jl")
include("differences.jl")
include("colormaps.jl")
include("display.jl")
include("colormatch.jl")

@deprecate rgba RGBA
@deprecate hsva HSVA
@deprecate hsla HSLA
@deprecate xyza XYZA
@deprecate xyYa xyYA
@deprecate laba LabA
@deprecate luva LuvA
@deprecate lchaba LCHabA
@deprecate lchuva LCHuvA
@deprecate din99a DIN99A
@deprecate din99da DIN99dA
@deprecate din99oa DIN99oA
@deprecate lmsa LMSA
@deprecate argb32 ARGB32

end # module


@testset "2D Geometry" begin

    c = Square([1,2])
    HyperRectangle(c)

end


export DecomposedDiscretePost

"""
    DecomposedDiscretePost <: DiscretePost

Textbook implementation of a discrete post operator, but with decomposed intersections.

### Fields

- `options` -- an `Options` structure that holds the algorithm-specific options

### Algorithm

The algorithm is based on [Reachability analysis of linear hybrid systems via block
decomposition](https://arxiv.org/pdf/1905.02458.pdf).
This discrete-post operator can only be used in combination with the continuous-post operator [`BFFPS19`](@ref).
"""
struct DecomposedDiscretePost <: DiscretePost
    options::Options

    function DecomposedDiscretePost(𝑂::Options)
        𝑂copy = copy(𝑂)
        # TODO: Check why it takes always default value for convex_hull
        check_aliases_and_add_default_value!(𝑂.dict, 𝑂copy.dict, [:overapproximation], Hyperrectangle)
        check_aliases_and_add_default_value!(𝑂.dict, 𝑂copy.dict, [:out_vars], Vector{Int}())

        return new(𝑂copy)
    end
end

# convenience constructor from pairs of symbols
DecomposedDiscretePost(𝑂::Pair{Symbol,<:Any}...) = DecomposedDiscretePost(Options(Dict{Symbol,Any}(𝑂)))

# default options for the DecomposedDiscretePost discrete post operator
DecomposedDiscretePost() = DecomposedDiscretePost(Options())

init(𝒫::DecomposedDiscretePost, 𝒮::AbstractSystem, 𝑂::Options) = init!(𝒫, 𝒮, copy(𝑂))

# TODO: use 𝑂 only?
function init!(𝒫::DecomposedDiscretePost, 𝒮::AbstractSystem, 𝑂::Options)
    𝑂[:n] = statedim(𝒮, 1)

    # solver-specific options (adds default values for unspecified options)
    𝑂out = validate_solver_options_and_add_default_values!(𝑂)

    return 𝑂out
end

function tube⋂inv(𝒫::DecomposedDiscretePost,
                  reach_tube::Vector{<:SparseReachSet{<:LazySet{N}}},
                  invariant,
                  start_interval
                 ) where {N}

    Rsets = Vector{AbstractReachSet{<:LazySet{N}}}()
    @inbounds for reach_set in reach_tube
        # intersection with invariant is computed inside BFFPS19 CPost operator
        push!(Rsets,
              substitute(reach_set,
                         time_start=time_start(reach_set) + start_interval[1],
                         time_end=time_end(reach_set) + start_interval[2]))
    end

    return Rsets
end

function post(𝒫::DecomposedDiscretePost,
              HS::HybridSystem,
              waiting_list::Vector{Tuple{Int, <:AbstractReachSet{LazySet{N}}, Int}},
              passed_list,
              source_loc_id,
              tube⋂inv,
              jumps,
              options
             ) where {N}
    jumps += 1
    n = options[:n]
    oa = 𝒫.options[:overapproximation]
    n_lowdim = length(𝒫.options[:temp_vars])
    source_invariant = HS.modes[source_loc_id].X
    inv_isa_Hrep, inv_isa_H_polytope = get_Hrep_info(source_invariant)
    for trans in out_transitions(HS, source_loc_id)
        info("Considering transition: $trans")
        target_loc_id = target(HS, trans)
        target_loc = HS.modes[target(HS, trans)]
        target_invariant = target_loc.X
        constrained_map = resetmap(HS, trans)
        guard = stateset(constrained_map)
        # perform jumps
        post_jump = Vector{ReachSet{LazySet{N}}}()
        sizehint!(post_jump, length(tube⋂inv))
        for reach_set in tube⋂inv
            if (dim(set(reach_set)) == n_lowdim && n_lowdim < n)
                continue
            end
            # check intersection with guard
            R⋂G = Intersection(set(reach_set), guard)
            if isempty(R⋂G)
                continue
            end
            R⋂G = overapproximate(R⋂G, CartesianProductArray, oa)

            # apply assignment
            A⌜R⋂G⌟ = apply_assignment(𝒫, constrained_map, R⋂G)
            A⌜R⋂G⌟ = overapproximate(A⌜R⋂G⌟, CartesianProductArray, oa)

            # intersect with target invariant
            A⌜R⋂G⌟⋂I = Intersection(target_invariant, A⌜R⋂G⌟)
            if isempty(A⌜R⋂G⌟⋂I)
                continue
            end

            A⌜R⋂G⌟⋂I = overapproximate(A⌜R⋂G⌟⋂I, CartesianProductArray, oa)

            # store result
            push!(post_jump, ReachSet{LazySet{N}}(A⌜R⋂G⌟⋂I,
                                                  time_start(reach_set),
                                                  time_end(reach_set)))
        end

        postprocess(𝒫, HS, post_jump, options, waiting_list, passed_list,
            target_loc_id, jumps)
    end
end


abstract type AbstractImportanceSampling end
abstract type UnweightedIS <: AbstractImportanceSampling end
abstract type WeightedIS <: AbstractImportanceSampling end

struct IS <: UnweightedIS end
struct PDIS <: UnweightedIS end
struct WIS <: WeightedIS end
struct WPDIS <: WeightedIS end
struct CWPDIS <: WeightedIS end

include("bandit_is.jl")
include("trajectory_is.jl")

function estimate_returns(ism::AbstractImportanceSampling, π, H)
    return @. estimate_return((ism,), (π,), H)
end

function estimate_returns!(G, ism::AbstractImportanceSampling, π, H)
    @. G = estimate_return((ism,), (π,), H)
end

function estimate_returns_withentropy(ism::AbstractImportanceSampling, π, H, α)
    return @. estimate_return_withentropy((ism,), (π,), H, α)
end

function estimate_returns_withentropy!(G, ism::AbstractImportanceSampling, π, H, α)
    @. G = estimate_return_withentropy((ism,), (π,), H, α)
end

function estimate_returns(ism::WIS, π, H)
    N = length(H)
    ρtot = 0.0
    rets = estimate_return.((ism,), (π,), H)
    G = [r[1] for r in rets]

    for i in 1:N
        ρi = rets[i][2]
        ρtot += ρi
    end
    G ./ ρtot
end

function estimate_returns_withentropy(ism::WIS, π, H, α)
    N = length(H)
    ρtot = 0.0
    rets = estimate_return_withentropy.((ism,), (π,), H, (α,))
    G = [r[1] for r in rets]

    for i in 1:N
        ρi = rets[i][2]
        ρtot += ρi
    end
    G / ρtot
end

function estimate_returns!(G, ism::WIS, logpdf_fn, H)
    N = length(H)
    ρtot = 0.0
    for i in 1:N
        Gi, ρi = estimate_return(ism, logpdf_fn, H[i])
        G[i] = Gi
        ρtot += ρi
    end
    @. G ./= ρtot
end

function estimate_returns_withentropy!(G, ism::WIS, π, H, α)
    N = length(H)
    ρtot = 0.0
    for i in 1:N
        Gi, ρi = estimate_return_withentropy(ism, π, H[i], α)
        G[i] = Gi
        ρtot += ρi
    end
    @. G ./= ρtot
end