"""Represent a technique for aligning two timeseries."""
abstract type Alignment end
struct LeftAlignment <: Alignment end
struct UnionAlignment <: Alignment end
struct IntersectAlignment <: Alignment end

"""
    LEFT

For inputs `(A, B, ...)`, tick whenever `A` ticks so long as all inputs are active.
"""
const LEFT = LeftAlignment()

"""
    UNION

For inputs `(A, B, ...)`, tick whenever any input ticks so long as all inputs are active.
"""
const UNION = UnionAlignment()

"""
    INTERSECT

For inputs `(A, B, ...)`, tick whenever all inputs tick simultaneously.
"""
const INTERSECT = IntersectAlignment()

"""The default alignment for operators when not specified."""
const DEFAULT_ALIGNMENT = UNION

"""
    UnaryNodeOp{T} <: NodeOp{T}

An abstract type representing a node op with a single parent.
"""
abstract type UnaryNodeOp{T} <: NodeOp{T} end

"""
    BinaryNodeOp{T,A<:Alignment} <: NodeOp{T}

An abstract type representing a node op with two parents, and using alignment `A`.
"""
abstract type BinaryNodeOp{T,A<:Alignment} <: NodeOp{T} end

"""
    NaryNodeOp{N,T,A<:Alignment} <: NodeOp{T}

An abstract type representing a node op with `N` parents, and using alignment `A`.

This type should be avoided for `N < 3`, since in these cases it would be more appropriate
to use either [`TimeDag.UnaryNodeOp`](@ref) or [`TimeDag.BinaryNodeOp`](@ref).
"""
abstract type NaryNodeOp{N,T,A<:Alignment} <: NodeOp{T} end

# A note on the design choice here, which is motivated by performance reasons & profiling.
#
# Options considered:
#   1. Return `(value::T, should_tick::Bool)` pair always
#   2. Take `out::Ref{T}` as a parameter, return `should_tick::Bool`
#   3. Return `Maybe{T}`.
#
# Option 1 was disfavoured, because if `should_tick` is false, one still needs to invent a
#   `value` of type `T`, which is possibly hard in general.
# Option 2 was found, from benchmarking, to be moderately slower than the other choices.
#
# Option 3 was found to retain the performance of option 1 with minimal overhead.
#
# For cases when we know the node will always tick (as indicated by `always_ticks(op)`) we
# just return a raw `T`, as the extra information will not be used.
"""
    operator!(op::UnaryNodeOp{T}, (state,), (time,) x) -> T / Maybe{T}
    operator!(op::BinaryNodeOp{T}, (state,), (time,) x, y) -> T / Maybe{T}
    operator!(op::NaryNodeOp{N,T}, (state,), (time,) x, y, z...) -> T / Maybe{T}

Perform the operation for this node.

When defining a method of this for a new op, follow these rules:
- `state` should be omitted iff [`TimeDag.stateless_operator`](@ref).
- `time` should be omitted iff [`TimeDag.time_agnostic`](@ref).
- All values `x, y, z...` should be omittted iff [`TimeDag.value_agnostic`](@ref).

For stateful operations, this operator should mutate `state` as required.

The return value `out` should be of type `T` iff [`TimeDag.always_ticks`](@ref) is true,
otherwise it should be of type [`TimeDag.Maybe`](@ref)`{T}`.

If `out <: Maybe{T}`, and has `!valid(out)`, this indicates that we do not wish to emit a
knot at this time, and it will be skipped. Otherwise, `value(out)` will be used as the
output value.
"""
function operator! end

"""
    always_ticks(node) -> Bool
    always_ticks(op) -> Bool

Returns true iff the return value from `operator` can be assumed to always be valid.

Note, that for sensible performance characteristics, this should be knowable from
`typeof(op)`
"""
always_ticks(node::Node) = always_ticks(node.op)
always_ticks(::NodeOp) = false

"""
    stateless_operator(node) -> Bool
    stateless_operator(op) -> Bool

Returns true iff `operator(op, ...)` would never look at or modify the evaluation state.

If this returns true, `create_operator_evaluation_state` will not be used.

Note that if an `op` has `stateless(op)` returning true, then it necessarily should also
return true here. The default implementation is to return `stateless(op)`, meaning that if
one is creating a node that is fully stateless, one need only define `stateless`.

For optimal performance, this should be knowable from the type of `op` alone.
"""
stateless_operator(node::Node) = stateless_operator(node.op)
stateless_operator(op::NodeOp) = stateless(op)

"""
    time_agnostic(node) -> Bool
    time_agnostic(op) -> Bool

Returns true iff `op` does not care about the time of the input knot(s).

For optimal performance, this should be knowable from the type of `op` alone.
"""
time_agnostic(node::Node) = time_agnostic(node.op)
time_agnostic(::NodeOp) = false

"""
    value_agnostic(node) -> Bool
    value_agnostic(op) -> Bool

Returns true iff `op` does not care about the value(s) of the input knot(s).

For optimal performance, this should be knowable from the type of `op` alone.
"""
value_agnostic(node::Node) = value_agnostic(node.op)
value_agnostic(::NodeOp) = false

"""
    create_operator_evaluation_state(parents, op::NodeOp) -> NodeEvaluationState

Create an empty evaluation state for the given node, when starting evaluation at the
specified time.

Note that this is state that will be passed to `operator`. The overall node may additionally
wrap this state with further state, if this is necessary for e.g. alignment.
"""
function create_operator_evaluation_state end

"""
    has_initial_values(op) -> Bool

If this returns true, it indicates that initial values for the `op`'s parents are specified.

See the documentation on [Initial values](@ref) for further information.
"""
has_initial_values(::BinaryNodeOp) = false
has_initial_values(::NaryNodeOp) = false

"""
    initial_left(op::BinaryNodeOp) -> L

Specify the initial value to use for the first parent of the given `op`.

Needs to be defined if [`has_initial_values`](@ref) returns true, and alignment is
[`UNION`](@ref). For other alignments it is not required.
"""
function initial_left end

"""
    initial_right(op::BinaryNodeOp) -> R

Specify the initial value to use for the second parent of the given `op`.

Needs to be defined if `has_initial_values(op)` returns true, and alignment is
[`UNION`](@ref) or [`LEFT`](@ref). For [`INTERSECT`](@ref) alignment it is not required.
"""
function initial_right end

"""
    initial_values(op::NaryNodeOp) -> Tuple

Specify the initial values to use for all parents of the given `op`.

Needs to be defined for nary ops for which [`has_initial_values`](@ref) returns true.
"""
function initial_values end

"""Convenience method to dispatch to reduced-argument `operator!` calls."""
function _operator!(op::NodeOp, state::NodeEvaluationState, time::DateTime, values...)
    return if stateless_operator(op) && time_agnostic(op)
        value_agnostic(op) ? operator!(op) : operator!(op, values...)
    elseif stateless_operator(op) && !time_agnostic(op)
        value_agnostic(op) ? operator!(op, time) : operator!(op, time, values...)
    elseif !stateless_operator(op) && time_agnostic(op)
        value_agnostic(op) ? operator!(op, state) : operator!(op, state, values...)
    else
        error("Error! We should have dispatched to a more specialised method.")
    end
end

function _can_propagate_constant(op::Union{UnaryNodeOp,BinaryNodeOp,NaryNodeOp})
    return always_ticks(op) && stateless_operator(op) && time_agnostic(op)
end
function _propagate_constant_value(op::UnaryNodeOp, parents::Tuple{Node})
    # NB, we know that time & state is ignored (due to _can_propagate_constant).
    return operator!(op, value(@inbounds(parents[1])))
end
function _propagate_constant_value(op::BinaryNodeOp, parents::Tuple{Node,Node})
    return operator!(op, value(@inbounds(parents[1])), value(@inbounds(parents[2])))
end
function _propagate_constant_value(op::NaryNodeOp{N}, parents::NTuple{N,Node}) where {N}
    return operator!(op, map(value, parents)...)
end

"""
    _create_operator_evaluation_state(parents, op) -> NodeEvaluationState

Internal function that will look at `stateless_operator`, and iff it is false call
`create_operator_evaluation_state`. Otherwise return an empty node state.
"""
function _create_operator_evaluation_state(parents, op)
    return if stateless_operator(op)
        EMPTY_NODE_STATE
    else
        create_operator_evaluation_state(parents, op)
    end
end

# An unary node has no alignment state, so any state comes purely from the operator.
function create_evaluation_state(parents::Tuple{Node}, op::UnaryNodeOp)
    return _create_operator_evaluation_state(parents, op)
end

function run_node!(
    node_op::UnaryNodeOp{T},
    state::NodeEvaluationState,
    ::DateTime,  # time_start
    ::DateTime,  # time_end
    input::Block{L},
) where {T,L}
    n = length(input)
    values = _allocate_values(T, n)
    return if always_ticks(node_op)
        # We can ignore the validity of the return value of the operator, since we have been
        # promised that it will always tick. Hence we can use a for loop too.
        @inbounds for i in 1:n
            time = input.times[i]
            values[i] = _operator!(node_op, state, time, input.values[i])
        end
        Block(unchecked, input.times, values)
    else
        times = _allocate_times(n)
        j = 1
        @inbounds for i in 1:n
            time = input.times[i]
            out = _operator!(node_op, state, time, input.values[i])
            if valid(out)
                values[j] = unsafe_value(out)
                times[j] = input.times[i]
                j += 1
            end
        end
        _trim!(times, j - 1)
        _trim!(values, j - 1)
        Block(unchecked, times, values)
    end
end

"""Apply, assuming `input_l` and `input_r` have identical alignment."""
function _apply_fast_align_binary!(
    op::BinaryNodeOp{T}, operator_state::NodeEvaluationState, input_l::Block, input_r::Block
) where {T}
    n = length(input_l)
    values = _allocate_values(T, n)
    return if always_ticks(op)
        # We shouldn't assume that it is valid to broadcast f over the inputs, so loop
        # manually.
        @inbounds for i in 1:n
            time = input_l.times[i]
            values[i] = _operator!(
                op, operator_state, time, input_l.values[i], input_r.values[i]
            )
        end
        Block(unchecked, input_l.times, values)
    else
        # FIXME Implement this branch!
        error("Not implemented!")
    end
end

abstract type UnionAlignmentState{L,R} <: NodeEvaluationState end

mutable struct UnionWithOpState{L,R,OperatorState} <: UnionAlignmentState{L,R}
    valid_l::Bool
    valid_r::Bool
    operator_state::OperatorState
    # These fields may initially be uninitialised.
    latest_l::L
    latest_r::R

    function UnionWithOpState{L,R}(operator_state::OperatorState) where {L,R,OperatorState}
        return new{L,R,OperatorState}(false, false, operator_state)
    end

    function UnionWithOpState{L,R}(
        operator_state::OperatorState, initial_l::L, initial_r::R
    ) where {L,R,OperatorState}
        return new{L,R,OperatorState}(true, true, operator_state, initial_l, initial_r)
    end
end

mutable struct UnionWithoutOpState{L,R} <: UnionAlignmentState{L,R}
    valid_l::Bool
    valid_r::Bool
    # These fields may initially be uninitialised.
    latest_l::L
    latest_r::R

    function UnionWithoutOpState{L,R}() where {L,R}
        return new{L,R}(false, false)
    end

    function UnionWithoutOpState{L,R}(initial_l::L, initial_r::R) where {L,R}
        return new{L,R}(true, true, initial_l, initial_r)
    end
end

operator_state(state::UnionWithOpState) = state.operator_state
operator_state(::UnionWithoutOpState) = EMPTY_NODE_STATE

function _set_l!(state::UnionAlignmentState{L}, x::L) where {L}
    state.latest_l = x
    state.valid_l = true
    return state
end

function _set_r!(state::UnionAlignmentState{L,R}, x::R) where {L,R}
    state.latest_r = x
    state.valid_r = true
    return state
end

function create_evaluation_state(
    parents::Tuple{Node,Node}, op::BinaryNodeOp{T,UnionAlignment}
) where {T}
    L = value_type(parents[1])
    R = value_type(parents[2])
    return if stateless_operator(op)
        if has_initial_values(op)
            UnionWithoutOpState{L,R}(initial_left(op), initial_right(op))
        else
            UnionWithoutOpState{L,R}()
        end
    else
        operator_state = create_operator_evaluation_state(parents, op)
        if has_initial_values(op)
            UnionWithOpState{L,R}(operator_state, initial_left(op), initial_right(op))
        else
            UnionWithOpState{L,R}(operator_state)
        end
    end
end

@inline function _maybe_add_knot!(
    node_op::NodeOp,
    operator_state::NodeEvaluationState,
    out_times::AbstractVector{DateTime},
    out_values::AbstractVector{T},
    j::Int,
    time::DateTime,
    in_values...,
) where {T}
    # Find the output value. For a given op this will either be of type T, or Maybe{T}, and
    # we can (at compile time) select the correct branch below based on `always_ticks(op)`.
    out = _operator!(node_op, operator_state, time, in_values...)

    return if always_ticks(node_op)
        # Output value is raw, and should always be used.
        @inbounds out_times[j] = time
        @inbounds out_values[j] = out
        j + 1
    else
        if valid(out)
            # Add the output only if the output is valid.
            @inbounds out_times[j] = time
            @inbounds out_values[j] = unsafe_value(out)
            j + 1
        else
            j
        end
    end
end

function run_node!(
    node_op::BinaryNodeOp{T,UnionAlignment},
    state::UnionAlignmentState,
    ::DateTime,  # time_start
    ::DateTime,  # time_end
    input_l::Block,
    input_r::Block,
) where {T}
    @inbounds if isempty(input_l) && isempty(input_r)
        # Nothing to do, since neither input has ticked.
        return Block{T}()
    elseif isempty(input_l) && !state.valid_l
        # Left is inactive and won't tick, so nothing gets emitted. But make sure we update
        # the state on the right.
        _set_r!(state, last(input_r.values))
        return Block{T}()
    elseif isempty(input_r) && !state.valid_r
        # Right is inactive and won't tick, so nothing gets emitted. But make sure we update
        # the state on the left.
        _set_l!(state, last(input_l.values))
        return Block{T}()
    end

    if _equal_times(input_l, input_r)
        # Times are indistinguishable
        # Update the alignment state.
        _set_l!(state, @inbounds last(input_l.values))
        _set_r!(state, @inbounds last(input_r.values))
        return _apply_fast_align_binary!(node_op, operator_state(state), input_l, input_r)
    end

    # Create our outputs as the maximum possible size.
    nl = length(input_l)
    nr = length(input_r)
    max_size = nl + nr
    times = _allocate_times(max_size)
    values = _allocate_values(T, max_size)

    # Store indices into the inputs. The index represents the next time point for
    # consideration for each series.
    il = 1
    ir = 1

    # Index into the output.
    j = 1

    # Loop until we exhaust inputs.
    @inbounds while (il <= nl || ir <= nr)
        # Store the next available time in the series, that is being pointed to by il & ir.
        next_time_l = il <= nl ? input_l.times[il] : DateTime(0)
        next_time_r = ir <= nr ? input_r.times[ir] : DateTime(0)

        new_time = if (il <= nl && next_time_l < next_time_r) || ir > nr
            # Left ticks next
            _set_l!(state, input_l.values[il])
            il += 1
            next_time_l
        elseif (ir <= nr && next_time_r < next_time_l) || il > nl
            # Right ticks next
            _set_r!(state, input_r.values[ir])
            ir += 1
            next_time_r
        else
            # A shared time point where neither x1 nor x2 have been exhausted.
            _set_l!(state, input_l.values[il])
            _set_r!(state, input_r.values[ir])
            il += 1
            ir += 1
            next_time_l
        end

        # We must only output a knot if both inputs are active.
        if !state.valid_l || !state.valid_r
            continue
        end

        # Compute and possibly output a knot.
        j = _maybe_add_knot!(
            node_op,
            operator_state(state),
            times,
            values,
            j,
            new_time,
            state.latest_l,
            state.latest_r,
        )
    end

    # Package the outputs into a block, resizing the outputs as necessary.
    _trim!(times, j - 1)
    _trim!(values, j - 1)
    return Block(unchecked, times, values)
end

function create_evaluation_state(
    parents::Tuple{Node,Node}, op::BinaryNodeOp{T,IntersectAlignment}
) where {T}
    # Intersect alignment doesn't require remembering any previous state, so just return
    # the operator state.
    return _create_operator_evaluation_state(parents, op)
end

function run_node!(
    node_op::BinaryNodeOp{T,IntersectAlignment},
    operator_state::NodeEvaluationState,
    ::DateTime,  # time_start
    ::DateTime,  # time_end
    input_l::Block,
    input_r::Block,
) where {T}
    if isempty(input_l) || isempty(input_r)
        # Output will be empty unless both inputs have ticked.
        return Block{T}()
    end

    if _equal_times(input_l, input_r)
        # Times are indistinguishable.
        return _apply_fast_align_binary!(node_op, operator_state, input_l, input_r)
    end

    # Create our outputs as the maximum possible size.
    nl = length(input_l)
    nr = length(input_r)
    max_size = min(nl, nr)
    times = _allocate_times(max_size)
    values = _allocate_values(T, max_size)

    # Store indices into the inputs. The index represents the next time point for
    # consideration for each series.
    il = 1
    ir = 1

    # Index into the output.
    j = 1

    # If we get to the end of either series, we know that we cannot add any more elements to
    # the output.
    @inbounds while (il <= nl && ir <= nr)
        # Obtain the *next available* times from each entity. We know that the current
        # state, and last emitted, time is strictly less than either of these.
        next_time_l = input_l.times[il]
        next_time_r = input_r.times[ir]

        if next_time_l < next_time_r
            # Left ticks next; consider the next knot.
            il += 1
        elseif next_time_r < next_time_l
            # Right ticks next; consider the next knot.
            ir += 1
        else  # next_time_l == next_time_r
            # Shared time point, so emit a knot.
            j = _maybe_add_knot!(
                node_op,
                operator_state,
                times,
                values,
                j,
                next_time_l,
                input_l.values[il],
                input_r.values[ir],
            )
            il += 1
            ir += 1
        end
    end

    # Package the outputs into a block, resizing the outputs as necessary.
    _trim!(times, j - 1)
    _trim!(values, j - 1)
    return Block(unchecked, times, values)
end

abstract type LeftAlignmentState{R} <: NodeEvaluationState end

mutable struct LeftWithOpState{R,OperatorState} <: LeftAlignmentState{R}
    valid_r::Bool
    operator_state::OperatorState
    latest_r::R

    function LeftWithOpState{R}(operator_state::OperatorState) where {R,OperatorState}
        return new{R,OperatorState}(false, operator_state)
    end

    function LeftWithOpState{R}(
        operator_state::OperatorState, initial_r::R
    ) where {R,OperatorState}
        return new{R,OperatorState}(true, operator_state, initial_r)
    end
end

mutable struct LeftWithoutOpState{R} <: LeftAlignmentState{R}
    valid_r::Bool
    latest_r::R

    LeftWithoutOpState{R}() where {R} = new{R}(false)
    LeftWithoutOpState{R}(initial_r::R) where {R} = new{R}(true, initial_r)
end

operator_state(::LeftWithoutOpState) = EMPTY_NODE_STATE
operator_state(state::LeftWithOpState) = state.operator_state

function _set_r!(state::LeftAlignmentState{R}, x::R) where {R}
    state.latest_r = x
    state.valid_r = true
    return state
end

function create_evaluation_state(
    parents::Tuple{Node,Node}, op::BinaryNodeOp{T,LeftAlignment}
) where {T}
    R = value_type(parents[2])
    return if stateless_operator(op)
        if has_initial_values(op)
            LeftWithoutOpState{R}(initial_right(op))
        else
            LeftWithoutOpState{R}()
        end
    else
        operator_state = create_operator_evaluation_state(parents, op)
        if has_initial_values(op)
            LeftWithOpState{R}(operator_state, initial_right(op))
        else
            LeftWithOpState{R}(operator_state)
        end
    end
end

function run_node!(
    node_op::BinaryNodeOp{T,LeftAlignment},
    state::LeftAlignmentState,
    ::DateTime,  # time_start
    ::DateTime,  # time_end
    input_l::Block,
    input_r::Block,
) where {T}
    have_initial_r = state.valid_r

    if isempty(input_l)
        # We will not tick, but update state if necessary.
        if !isempty(input_r)
            _set_r!(state, @inbounds last(input_r.values))
        end
        return Block{T}()
    elseif isempty(input_r) && !have_initial_r
        # We cannot tick, since we have no values on the right. No state to update either.
        return Block{T}()
    end

    if _equal_times(input_l, input_r)
        # Times are indistinguishable.
        return _apply_fast_align_binary!(node_op, operator_state(state), input_l, input_r)
    end

    # The most we can emit is one knot for every knot in input_l.
    nl = length(input_l)
    nr = length(input_r)
    times = _allocate_times(nl)
    values = _allocate_values(T, nl)

    # Start with 0, indicating that input_r hasn't started ticking yet.
    ir = 0

    # The index into the output.
    j = 1

    @inbounds for il in 1:nl
        # Consume r while it would leave us before the current time in l, or until we reach
        # the end of r.
        next_time_l = input_l.times[il]
        while (ir < nr && input_r.times[ir + 1] <= next_time_l)
            ir += 1
        end

        if ir > 0
            j = _maybe_add_knot!(
                node_op,
                operator_state(state),
                times,
                values,
                j,
                next_time_l,
                input_l.values[il],
                input_r.values[ir],
            )

        elseif have_initial_r
            # R hasn't ticked in this batch, but we have an initial value.
            j = _maybe_add_knot!(
                node_op,
                operator_state(state),
                times,
                values,
                j,
                next_time_l,
                input_l.values[il],
                state.latest_r,
            )
        end
    end

    # Update state
    if !isempty(input_r)
        _set_r!(state, @inbounds last(input_r.values))
    end

    # Package the outputs into a block, resizing the outputs as necessary.
    _trim!(times, j - 1)
    _trim!(values, j - 1)
    return Block(unchecked, times, values)
end

# Some thought went into how to store `latest` for the nary case.
#
# Ideally one would use a mutable heterogeneous collection (i.e. a "mutable tuple"), but
# such a thing doesn't exist in Julia. There are tricks one can use if your elements are
# isbits (see StaticArrays.MArray), but we want a solution that can work for *any* input
# Julia type.
#
# Options considered:
#   1. Nary alignment, with latest::Vector{Any} and accept that state *cannot* store latest
#       values efficiently (will be a lot of runtime type checking). This WILL be slow, as
#       it is in the inner loop of evaluation.
#   2. Nary alignment, and use a Tuple for storage.
#       But, because this isn't mutable, will have to use `Base.setindex` to update, which
#       could theoretically do a lot of allocating.
#   3. Use code generation to make Union2, Union3, Union4, ...
#       This bloats the code somewhat, but would be guaranteed to be fast.
#       However, there is then an arbitrary limit to the number of things that we can align.
#
# Benchmarking suggests that option 2 can actually be quite efficient in practice, since if
# we immediately assign the tuple, LLVM has the ability to re-use memory. We hence take this
# approach.
#
# In the future, we could generate a SMALL number of additional special-cases using option 3
# (if, for example, we found ourselves doing a lot of ternary alignment).
#
# Also, to simplify matters, we use a single alignment state. It is slightly less memory
# efficient, because we store a pointer for operator state even if it is unnecessary.

# `Types` will look something like Tuple{In1,In2,...}
mutable struct NaryAlignmentState{N,Types<:Tuple,OperatorState} <: NodeEvaluationState
    # Since we may not have a latest value, we use the partial initialisation trick inside
    # `Maybe` to avoid having to invent an unused placeholder for arbitrary types.
    latest::Tuple{Vararg{Maybe}}
    operator_state::OperatorState

    function NaryAlignmentState{N,Types}(
        operator_state::OperatorState
    ) where {N,Types,OperatorState}
        return new{N,Types,OperatorState}(
            Tuple(Maybe{T}() for T in Types.parameters), operator_state
        )
    end

    function NaryAlignmentState{N,Types}(
        operator_state::OperatorState, initial_values::Types
    ) where {N,Types,OperatorState}
        return new{N,Types,OperatorState}(
            Tuple(Maybe(v) for v in initial_values), operator_state
        )
    end
end

operator_state(state::NaryAlignmentState) = state.operator_state

Base.@propagate_inbounds function _set!(state::NaryAlignmentState, x, i::Integer)
    # TODO Consider making a mutable Maybe type, to avoid possible reconstruction of the
    #   tuple? It might just end up being slower though due to extra dereferencing.
    # The state tuple is not mutable, so we need to use this trick.
    state.latest = Base.setindex(state.latest, Maybe(x), i)
    return state
end

function create_evaluation_state(
    parents::NTuple{N,Node}, op::NaryNodeOp{N,T,UnionAlignment}
) where {N,T}
    # Work out the tuple of input types necessary for constructing the alignment state.
    Types = Tuple{map(value_type, parents)...}
    operator_state = _create_operator_evaluation_state(parents, op)
    return if has_initial_values(op)
        NaryAlignmentState{N,Types}(operator_state, initial_values(op))
    else
        NaryAlignmentState{N,Types}(operator_state)
    end
end

function create_evaluation_state(
    parents::NTuple{N,Node}, op::NaryNodeOp{N,T,IntersectAlignment}
) where {N,T}
    # Intersect alignment doesn't require remembering any previous state, so just return
    # the operator state.
    return _create_operator_evaluation_state(parents, op)
end

function create_evaluation_state(
    parents::NTuple{N,Node}, op::NaryNodeOp{N,T,LeftAlignment}
) where {N,T}
    # Work out the tuple of input types necessary for constructing the alignment state.
    # Note that we should ignore the left parent, so we only capture the rightmost M = N-1.
    _, parents_r... = parents
    M = N - 1
    Types = Tuple{map(value_type, parents_r)...}
    operator_state = _create_operator_evaluation_state(parents, op)
    return if has_initial_values(op)
        _, initial_values_r... = initial_values(op)
        NaryAlignmentState{M,Types}(operator_state, initial_values_r)
    else
        NaryAlignmentState{M,Types}(operator_state)
    end
end

"""Apply, assuming all `inputs` have identical alignment."""
function _apply_fast_align_nary!(
    op::NaryNodeOp{N,T}, operator_state::NodeEvaluationState, inputs::NTuple{N,Block}
) where {N,T}
    times = @inbounds first(inputs).times
    n = length(times)
    values = _allocate_values(T, n)
    return if always_ticks(op)
        @inbounds for i in 1:n
            time = times[i]
            values[i] = _operator!(
                op, operator_state, time, map(x -> x.values[i], inputs)...
            )
        end
        Block(unchecked, times, values)
    else
        # FIXME Implement this branch!
        error("Not implemented!")
    end
end

"""
    equivalence_classes(f, collection) -> Vector{Vector{eltype(collection)}}

Generate the set of equivalence classes for `collection` generated by the equivalence
relation `f : T × T → Bool`.

Note that behaviour is undefined if `f` is non-transitive.
"""
function equivalence_classes(
    f::Function, x::Union{AbstractVector{T},Tuple{Vararg{T}}}
) where {T}
    result = Vector{Vector{T}}()
    isempty(x) && return result

    @inbounds for el in x
        for class in result
            f(el, first(class)) || continue
            push!(class, el)
            # Jump to "found" label below. This means that we don't need to add a new
            # equivalence class.
            @goto found
        end

        # We end up here if we didn't find an existing equivalence class for `el`, so we
        # need to create a new one.
        push!(result, T[el])

        # Label used for escaping from the for loop above.
        @label found
    end
    return result
end

function run_node!(
    node_op::NaryNodeOp{N,T,UnionAlignment},
    state::NaryAlignmentState{N},
    ::DateTime,  # time_start
    ::DateTime,  # time_end
    inputs::Block...,
) where {N,T}
    @assert N == length(inputs)

    inputs_empty = map(isempty, inputs)
    @inbounds if all(inputs_empty)
        # Nothing to do, as no inputs have ticked.
        return Block{T}()
    elseif any(inputs_empty .& map(!valid, state.latest))
        # There is at least one input which has an empty input and does *not* have an
        # initial value.

        # We need to make sure we update any states which do have inputs.
        for (k, input) in enumerate(inputs)
            isempty(input) && continue
            _set!(state, last(input.values), k)
        end

        # The output will, however, be empty.
        return Block{T}()
    end

    input_classes = equivalence_classes(_equal_times, inputs)

    if length(input_classes) == 1
        # All times are indistinguishable.
        # Update alignment state and use fast alignment.
        @inbounds for (k, input) in enumerate(inputs)
            _set!(state, last(input.values), k)
        end
        return _apply_fast_align_nary!(node_op, operator_state(state), inputs)
    end

    # TODO `max_size` could be a massive overestimate.
    #   In practice it may be better to pick something smaller, and then increase the buffer
    #   size where necessary.

    # We use an optimisation here, whereby we use the `_equal_times` equivalence
    # relation to partition the inputs into sets which have equal times. We know that times
    # that are equal can only appear once, so this avoids some double counting.
    max_size = sum(x -> length(first(x)), input_classes)
    times = _allocate_times(max_size)
    values = _allocate_values(T, max_size)

    # Indices into the inputs. The index represents the next time point for
    # consideration for each series.
    is = MVector{N,Int64}(ones(Int64, N))

    # Index into the output.
    j = 1

    ns = map(length, inputs)
    @inbounds while true
        unfinisheds = (is .<= ns)
        any(unfinisheds) || break  # Loop until we exhaust inputs.

        # For each input, figure out the next time it would tick.
        next_times = ntuple(Val(N)) do k
            # If we're at the end of the series, just use a placeholder.
            !unfinisheds[k] && return typemax(DateTime)
            return inputs[k].times[is[k]]
        end

        # This is when we will next tick.
        new_time = minimum(next_times)

        # For every input that ticks at exactly this time, update to use the new value.
        for k in 1:N
            unfinisheds[k] || continue
            next_times[k] == new_time || continue
            # If we are here, this input should advance.
            _set!(state, inputs[k].values[is[k]], k)
            is[k] += 1
        end

        # We must only output a knot if all inputs are active.
        all(map(valid, state.latest)) || continue

        # Compute and possibly output a knot.
        j = _maybe_add_knot!(
            node_op,
            operator_state(state),
            times,
            values,
            j,
            new_time,
            map(unsafe_value, state.latest)...,
        )
    end

    # Package the outputs into a block, resizing the outputs as necessary.
    _trim!(times, j - 1)
    _trim!(values, j - 1)

    return Block(unchecked, times, values)
end

function run_node!(
    node_op::NaryNodeOp{N,T,IntersectAlignment},
    operator_state::NodeEvaluationState,
    ::DateTime,  # time_start
    ::DateTime,  # time_end
    inputs::Block...,
) where {N,T}
    @assert N == length(inputs)

    if any(isempty, inputs)
        # Nothing to do, as no inputs have ticked.
        return Block{T}()
    end

    input_classes = equivalence_classes(_equal_times, inputs)

    if length(input_classes) == 1
        # All times are indistinguishable, so use fast alignment.
        return _apply_fast_align_nary!(node_op, operator_state, inputs)
    end

    # Create our output as the maximum possible size.
    ns = map(length, inputs)
    max_size = minimum(ns)
    times = _allocate_times(max_size)
    values = _allocate_values(T, max_size)

    # Store indices into the inputs. The index represents the next time point for
    # consideration for each series.
    is = MVector{N,Int64}(ones(Int64, N))

    # Index into the output.
    j = 1

    # If we get to the end of any series, we know that we cannot add any more elements to
    # the output.
    @inbounds while all(is .<= ns)
        # For each input, figure out the next time it would tick.
        next_times = ntuple(k -> inputs[k].times[is[k]], Val(N))

        # This is when we *might* next tick, if all inputs tick at this time.
        new_time = minimum(next_times)

        tick_mask = next_times .== new_time
        if all(tick_mask)
            # All inputs tick simultaneously, this means we should compute and possibly
            # output a knot.
            input_values = map(zip(is, inputs)) do pair
                i, input = pair
                return input.values[i]
            end
            j = _maybe_add_knot!(
                node_op, operator_state, times, values, j, new_time, input_values...
            )
            # Advance all pointers.
            is .+= 1
        else
            # We aren't going to emit a knot, so advance all inputs that tick next.
            for k in 1:N
                next_times[k] == new_time || continue
                # If we are here, this input should advance.
                is[k] += 1
            end
        end
    end

    # Package the outputs into a block, resizing the outputs as necessary.
    _trim!(times, j - 1)
    _trim!(values, j - 1)
    return Block(unchecked, times, values)
end

function run_node!(
    node_op::NaryNodeOp{N,T,LeftAlignment},
    state::NaryAlignmentState{M},
    ::DateTime,  # time_start
    ::DateTime,  # time_end
    input_l::Block,
    inputs_r::Block...,
) where {N,T,M}
    @assert M == length(inputs_r)
    @assert M == N - 1

    wont_emit = @inbounds (
        isempty(input_l) ||  # Left input empty
        (
            # at least one right input inactive
            any(zip(inputs_r, state.latest)) do pair
                input_r, latest = pair
                return (isempty(input_r) && !valid(latest))
            end
        )
    )

    @inbounds if wont_emit
        # We will not be emitting a knot, but we should go through and update the state
        # where necessary
        for (kr, input_r) in enumerate(inputs_r)
            isempty(input_r) && continue
            _set!(state, last(input_r.values), kr)
        end
        return Block{T}()
    end

    all_times_equal = all(input_r -> _equal_times(input_l, input_r), inputs_r)
    if all_times_equal
        # All times are indistinguishable.
        # Update alignment state and use fast alignment.
        @inbounds for (k, input) in enumerate(inputs_r)
            _set!(state, last(input.values), k)
        end
        inputs = (input_l, inputs_r...)
        return _apply_fast_align_nary!(node_op, operator_state(state), inputs)
    end

    # The most we can emit is one knot for every knot in input_l.
    nl = length(input_l)
    times = _allocate_times(nl)
    values = _allocate_values(T, nl)

    # Indices into the right inputs. The index represents the next time point for
    # consideration for each series.
    # Use zero as the intitial index to indicate that the corresponding input hasn't
    # started ticking yet.
    irs = MVector{M,Int64}(zeros(Int64, M))

    # Index into the output.
    j = 1

    # Length of each 'right' input.
    nrs = map(length, inputs_r)

    @inbounds for il in 1:nl
        new_time = input_l.times[il]

        # Consume right inputs until we get to at or before `new_time`, or until we reach
        # the end of the input.
        for kr in 1:M
            nr = nrs[kr]
            nr < 1 && continue  # Nothing to do in this case.
            input_r = inputs_r[kr]
            advanced = false
            while (irs[kr] < nr && input_r.times[irs[kr] + 1] <= new_time)
                irs[kr] += 1
                advanced = true
            end

            # If this input has advanced, we should update the state.
            if advanced
                _set!(state, input_r.values[irs[kr]], kr)
            end
        end

        # Ascertain whether we can emit a knot. The answer is "yes" so long as all
        # right-states are valid.
        if all(valid, state.latest)
            j = _maybe_add_knot!(
                node_op,
                operator_state(state),
                times,
                values,
                j,
                new_time,
                input_l.values[il],
                map(unsafe_value, state.latest)...,
            )
        end
    end

    # We now need to update the state for the right-inputs, as we may not have exhausted
    # them all in the loop above.
    @inbounds for (k, input) in enumerate(inputs_r)
        _set!(state, last(input.values), k)
    end

    # Package the outputs into a block, resizing the outputs as necessary.
    _trim!(times, j - 1)
    _trim!(values, j - 1)
    return Block(unchecked, times, values)
end


# convert an image to grayscale
using Images
img = load("sample-images/cats-3061372_640.jpg");
img_gray = Gray.(img)
imshow(img_gray)

# create 3 channel RGB from a grayscale
img_gray_rgb = RGB.(Gray.(img_gray))
imshow(img_gray_rgb)


using FCSFiles
using FileIO
using Test

flowrun = load("testdata/BD-FACS-Aria-II.fcs")

@test length(flowrun["SSC-A"]) == 100000


struct Cursor
    mx::Int
    my::Int

    max::Int
    may::Int

    x::Int
    y::Int

    ax::Int
    ay::Int

    Cursor(mx::Int, my::Int, max::Int, may::Int, x::Int, y::Int, ax::Int, ay::Int) = new(mx, my, max, may, x, y, ax, ay)

    function Cursor(event::eventMouse, camera::Camera, room::Maple.Room)
        mx, my = getMapCoordinates(camera, event.x, event.y)
        max, may = getMapCoordinatesAbs(camera, event.x, event.y)

        x, y = mapToRoomCoordinates(mx, my, room)
        ax, ay = mapToRoomCoordinatesAbs(max, may, room)

        return new(mx, my, max, may, x, y, ax, ay)
    end
end


using JuMP, Ipopt

#EV = Model(with_optimizer(Ipopt.Optimizer, print_level=2))
#@variable(EV, 0 <= Z[1:3] )
#EV[:Z]

#o_tmp = :(0)

#o1 = :( (Y)^2 )
#for i=1:3
#    global Y = Z[i]
#    println(Y)
#
#    #o1 = :($(Z[i])^2 * $(prob[i]))
#    bla = eval(o1)
#    #println(bla)
#    o_tmp = add_expr(o_tmp, bla)
#    println(o_tmp)
#end

#eval(o_tmp)

#add_NL_constraint(EV, :($(o_tmp) <= 2))


abstract type Tensor <: SymbolicObject end
struct TensorIndex <: SymbolicObject
    __pyobject__::PyCall.PyObject
end
struct TensorIndexType <: SymbolicObject
    __pyobject__::PyCall.PyObject
end
struct TensorSymmetry <: SymbolicObject
    __pyobject__::PyCall.PyObject
end
struct TensorHead <: SymbolicObject
    __pyobject__::PyCall.PyObject
end

#struct TensScalar <: Tensor
#    var::Sym;
#    expr::Sym;
#    __pyobject__::PyCall.PyObject;
#end

struct TensMul <: Tensor
    __pyobject__::PyCall.PyObject
end
struct TensAdd <: Tensor
    __pyobject__::PyCall.PyObject
end
struct IndexedTensor <: Tensor
    __pyobject__::PyCall.PyObject
end

function SymPy._convert(::Val{:MutableDenseNDimArray}, x)
    sh = x.shape
    reshape(x.tolist(), sh)
end

function sympy_type_convert(pyexp)
    if typeof(pyexp) in  [Sym,PyObject]
        cname = pyexp.__class__.__name__
        if cname == "TensMul"
            return convert(TensMul,pyexp)
        elseif cname == "TensAdd"
            return convert(TensAdd,pyexp)
        elseif cname == "Tensor"
            return convert(IndexedTensor,pyexp)
        elseif cname == "TensorSymmetry"
            return convert(TensorSymmetry,pyexp)
        elseif cname == "MutableDenseMatrix"
            return convert(Array{Sym},pyexp)
        elseif cname == "MutableDenseNDimArray"
            return convert(Array{Sym},pyexp)
        elseif cname == "ImmutableDenseNDimArray"
            if typeof(pyexp) == Sym
                return pyexp.__pyobject__.tolist()
            else
                return pyexp.tolist()
            end
        elseif cname == "Zero"
            return 0
        elseif cname == "Half"
            return 1//2
        elseif cname == "One"
            return 1
        elseif cname == "Rational"
            return convert(Rational,pyexp)
        elseif cname == "Integer"
            return convert(Int,pyexp)
        elseif cname == "Float"
            return convert(Float64,pyexp)
        else
            return pyexp
        end
    else
        return pyexp
    end
end


#========================================================================================#
#	Laboratory 300
#
# Welcome to course 300: Constructing Dynamical Models!
#
# Author: Niall Palfreyman, 24/04/2022
#========================================================================================#
[
	Activity(
		"""
		???
		""",
		"???",
		x -> true
	),
]


include("../src/MatrixReader.jl")

function fakedata(fakecount, blocksize, expsize; maxval=100)
    testdata = zeros(Int, (blocksize, expsize))
    for _ in 1:fakecount
        i = rand(1:blocksize)
        j = rand(1:expsize)
        v = rand(1:maxval)
        testdata[i,j] = v
    end
    testdata
end

# Testing with 10000 random data with random sparcity, dimensions.

println("For UInt8 version")

for itr in 1:10000
    expsize = rand(20:100)

    ######################
    # Write in fake data #
    ######################
    mw = MatrixWriter("test.data", expsize, UInt8)

    # data1
    t1 = fakedata(rand(1:200), 100, expsize, maxval=100)
    writeMatrix(mw, t1)
    # data2
    t2 = fakedata(rand(1:200), 100, expsize, maxval=100)
    writeMatrix(mw, t2)
    close(mw)

    #####################
    # Read in fake data #
    #####################
    mr = MatrixReader("test.data", 25, buffsize=rand(10:50))
    new = advance!(mr)
    m1 = mr.data
    new = advance!(mr)
    m2 = mr.data
    new = advance!(mr)
    m3 = mr.data
    new = advance!(mr)
    m4 = mr.data
    new = advance!(mr)
    m5 = mr.data
    new = advance!(mr)
    m6 = mr.data
    new = advance!(mr)
    m7 = mr.data
    new = advance!(mr)
    m8 = mr.data
    close(mr)

    #################
    # Test equality #
    #################
    x1 = vcat(t1, t2)
    new = vcat(m1, m2, m3, m4, m5, m6, m7, m8)
    @assert new == x1
    if itr%1000 == 0
        println(itr," test passed. ", size(x1), ":", sum(x1))
    end
end

println("For UInt16 version")

for itr in 1:10000
    expsize = rand(20:100)

    ######################
    # Write in fake data #
    ######################
    mw = MatrixWriter("test.data", expsize, UInt16)

    # data1
    t1 = fakedata(rand(1:200), 100, expsize, maxval=60000)
    writeMatrix(mw, t1)
    # data2
    t2 = fakedata(rand(1:200), 100, expsize, maxval=60000)
    writeMatrix(mw, t2)
    close(mw)

    #####################
    # Read in fake data #
    #####################
    mr = MatrixReader("test.data", 25, buffsize=rand(10:50))
    new = advance!(mr)
    m1 = mr.data
    new = advance!(mr)
    m2 = mr.data
    new = advance!(mr)
    m3 = mr.data
    new = advance!(mr)
    m4 = mr.data
    new = advance!(mr)
    m5 = mr.data
    new = advance!(mr)
    m6 = mr.data
    new = advance!(mr)
    m7 = mr.data
    new = advance!(mr)
    m8 = mr.data
    close(mr)

    #################
    # Test equality #
    #################
    x1 = vcat(t1, t2)
    new = vcat(m1, m2, m3, m4, m5, m6, m7, m8)
    @assert new == x1
    if itr%1000 == 0
        println(itr," test passed. ", size(x1), ":", sum(x1))
    end
end


# returns the following:
# rules, new_genes, gene_lookup, effects, rxn_genes
##################
# rules:
# rules read fx "x[1] && x[12]" where x[n] is a gene
##################
# new_genes
# new_genes are simply x[1] ... x[num_genes]
##################
# gene_lookup
# dictionary with all original gene names AND new_genes as keys and vals
# so the dict contains both
# b0001 => x[1]
# and
# x[1] => b0001
##################
# effects
# dict saying which genes affect which reactions
# 1 => [1,2,3] if gene number 1 affects reactions 1,2 and 3
##################
# rxn_genes
# dict which includes the genes that appear in every reaction
# 55 => [3,71] if the reaction rule for reaction 55 has genes 3 and 71
function prep_genes(model)
    # abstract genes
    genes = deepcopy(model.genes)

    # RETURN generate vector of x[1] .. x[num_genes]
    num_genes = length(genes)
    num_rxns = length(model.c)

    new_genes = Array(String, num_genes)
    for (i,v) in enumerate(genes)
        new_genes[i] = string("x[",i, "]")
    end

    # RETURN dictionary to easily switch between
    gene_lookup = Dict(zip([new_genes; genes], [genes; new_genes]))

    # RETURN abstract rules
    rules = deepcopy(model.rxn_rules)

    for (i,rule) in enumerate(rules)
        for element in split(rule)
            if haskey(gene_lookup, element)
                rules[i] = replace(rules[i], element, gene_lookup[element])
            end
        end
    end

    # RETURN the effect every gene makes, key => val is gene number and reactions touched
    effects = []
    for gene in new_genes
        push!(effects, [])

        for (i,rule) in enumerate(rules)
            if contains(rule, gene)
                push!(effects[end], i)
            end
        end
    end
    effects = convert(Array{Array{Int64,1},1}, effects)
    effects = Dict(zip(1:num_genes, effects))

    # RETURN the genes present in every reaction
    rxn_genes = []
    for i in 1:num_rxns
        push!(rxn_genes, [])
    end

    for (key,val) in effects
        for v in val
            push!(rxn_genes[v], key)
        end
    end
    rxn_genes = convert(Array{Array{Int64,1},1}, rxn_genes)
    rxn_genes = Dict(zip(1:num_rxns, rxn_genes))

    return rules, new_genes, gene_lookup, effects, rxn_genes
end

# WILL BE DEPRECATED, use prep_genes()
# Use this to convert gene names in model.genes & model.rxn_rules
# into a 'safe' format gene_xxxx
# This is due to some genes being called b1 and b12, so b1
# partially matches b12 in rules and results in errors
# returns new_gene_names, new_rules, new_old_genes, saved_rules
# new_gene_names is a list with reactions named gene_xx1, gene_xx2 ... gene_NNN
# new_rules is a list with all rules, with genes converted to those matching
# the genes in 'new gene names'
# new_old_genes is a dict with a mapping for new_gene_names => original_gene_names
# saved_rules is just the old reaction rules
function abstract_gene_names(genes, rules)
    num_genes = length(genes)
    num_digits = ndigits(num_genes)
    num_rules = length(rules)

    new_gene_names = Array(String, num_genes)
    new_rules = deepcopy(rules)

    for (i,v) in enumerate(genes)
        new_gene_names[i] = string("gene_","0"^(num_digits - ndigits(i)), i)
    end

    saved_names = Dict(zip(genes, new_gene_names))

    # Make sure no parenthesis touches a gene name
    for (i,v) in enumerate(new_rules)
        new_rules[i] = replace(new_rules[i], ")", " ) ")
        new_rules[i] = replace(new_rules[i], "(", " ( ")
    end

    for (i,rule) in enumerate(new_rules)
        rule = split(rule)

        for (j,v) in enumerate(rule )
            if haskey(saved_names, v)
                rule[j] = saved_names[v]
            end
        end

        rule = join(rule, " ")
        new_rules[i] = rule
    end

    saved_rules = Dict(zip(1:num_rules, rules))

    return new_gene_names, new_rules, invert_dict(saved_names), saved_rules
end

function abstract_gene_names(model)
    return abstract_gene_names(model.genes, model.rxn_rules)
end

# Returns the names of all genes that are in a reaction rule
# Note that rule needs to be converted to abstract
function genes_in_reaction(reaction_rule::String)
    rule = split(reaction_rule)
    indices = find(map(x -> startswith(x, "gene_"), rule))
    genes = rule[indices]

    return unique(genes)
end

# fetch the genes that appear in a reaction rule
function genes_in_reaction_rule(reaction)
    tmp = deepcopy(reaction)
    tmp = replace(tmp, '(', " ")
    tmp = replace(tmp, ')', " ")
    tmp = replace(tmp, '|', " ")
    tmp = replace(tmp, '&', " ")
    tmp = replace(tmp, "false", " ")
    tmp = replace(tmp, r"#=\b\S+?\b=#", "")
    tmp = split(tmp)
end

# set a gene to 'false' in a rule. Note use along with abstract_gene_names
function disable_genes_in_rules{T<:AbstractString}(gene::AbstractString, rules::Array{T})
    for (i,v) in enumerate(rules)
        rules[i] = replace(rules[i], gene, "false")
    end
    return
end

function disable_genes_in_rules{T<:AbstractString}(genes::Array, rules::Array{T})
    map(x -> disable_genes_in_rules(x, rules), genes)
    return
end

# set all genes to 'true'. Note use along with abstract_gene_names
function enable_genes_in_rules(rules)
    for (idx, rule) in enumerate(rules), val in split(rule)
        if startswith(val, "gene_")
            rules[idx] = replace(rules[idx], val, "true")
        end
    end
    return
end

# wrapper for disable_genes_in_rules and enable_genes_in_rules
function convert_rule_to_boolean{T <: AbstractString}(disabled_genes, rules::Array{T})
    disable_genes_in_rules(disabled_genes, rules)
    enable_genes_in_rules(rules)
end

# used to fetch old gene names from a list of new gene names
function convert_from_abstract{T<:String}(dict, gene_list::Array{T})
    for (i,v) in enumerate(gene_list)
        gene_list[i] = dict[v]
    end
    return
end

# find the location of a gene in model.genes from its string name
function find_gene(model::Model, gene::String)
    if gene_exists(model, gene)
        return find(gene .== model.genes)
    else
        warn("Gene does not exist")
        return
    end
end

# check if gene exists in model.genes
function gene_exists(model::Model, gene::String)
    any(gene .== model.genes)
end

# search all reaction gene rules for a gene
# Returns a list of indices corresponding to the reactions which a gene/list of genes
# appears in
function find_gene_effects(rules::Array{String}, gene::String)
    find(x -> contains(x, gene), rules)
end

function find_gene_effects(rules::Array{String}, genes::Array{String})
    map(x -> find_gene_effects(rules, x), genes)
end

function find_gene_effects(model::Model, gene::String)
    genes, rules, new_old, _ = abstract_gene_names(model)
    old_new = invert_dict(new_old)
    find(x -> contains(x, old_new[gene]), rules)
end

function find_gene_effects{T <: String}(model::Model, genes::Array{T})
    return map(x -> find_gene_effects(model, x), genes)
end

function find_gene_effects(model::Model, gene_indices::Array{Number})
    if (any(gene_indices .<= 0)) | (any(gene_indices .> length(model.genes)))
        warn("One or more invalid gene indices")
    end

    return find_gene_effects(model, model.genes[gene_indices])
end

function find_gene_effects(model::Model, gene_index::Number)
    return find_gene_effects(model, [gene_index])
end

# change this to whichever solver you want to use
# Note: only edit the global variable value
if !isdefined(:settings_default_lp)
    global settings_default_lp = "glpk"
end

# go through a list of blocked reactions and
# solve the lp, and restore
# sorts the blocked reactions lexiographically
# to minimize solver calls
function analyze_list_of_blocked_reaction_lp(lp, rxns)
    #progress_meter = ProgressMeter.Progress(length(rxns), 0.5, "\n\u1b[1FFVA:", 50)
    combos = sort(rxns, lt=lexless)
    num_combos = length(combos)

    wt = answer_lp(lp)

    lb, ub = get_variable_bounds(lp)

    flow = Array(Float64, num_combos)

    ####
    for d in combos[1]
        set_col_bounds(lp, d, 0.0, 0.0)
    end

    flow[1] = answer_lp(lp)

    disable = []
    for i in 2:num_combos
        enable = setdiff(combos[i-1], combos[i])
        disable = setdiff(combos[i], combos[i-1])

        for e in enable
            set_col_bounds(lp, e, lb[e], ub[e])
        end

        for d in disable
            set_col_bounds(lp, d, 0.0, 0.0)
        end

        flow[i] = answer_lp(lp)
        #next!(progress_meter)
    end

    for e in disable
        set_col_bounds(lp, e, lb[e], ub[e])
    end

    flow /= wt
    flow = round(flow, 16)

    result = Dict(zip(combos, flow))

    return result
end

# Solve lp and return:
# the objective value if optimial solution is found
# 0.0 otherwise
function answer_lp(lp)
    solve(lp)
    if get_solution_status(lp) == "Optimal"
        return get_objective_value(lp)
    else
        return 0.0
    end
end

# return a FBAsolution. lp must be solved
function construct_lp_solution(lp)
    f = get_objective_value(lp)
    x = get_solution(lp)
    y = get_slack(lp)
    w = get_reduced_costs(lp)
    success = get_solution_status(lp)
    info = SolverInfo(settings_default_lp, solver_status(lp))

    return  FBAsolution(f,x,y,w,success, info);
end

function createlpmodel()
  # Create a LP model structure and set solver specific parameters
  # THINK: Check memory allocation/deallocation
  # THINK: Support user-defined parameters
  global cobra_lp_solver
  if cobra_lp_solver == "cplex"
    # Use Primal Simplex
    m = JuMP.Model(solver=CplexSolver(CPX_PARAM_SCRIND=0, CPX_PARAM_LPMETHOD=1))
  elseif cobra_lp_solver == "gurobi"
    m = JuMP.Model(solver=GurobiSolver(OutputFlag=0, Method=0))
  elseif cobra_lp_solver == "glpk"
    m = JuMP.Model(solver=GLPKSolverLP()) # THINK: Fails!!!!
  else
    error("Solver ", cobra_lp_solver, "not implemented yet.")
  end
  m
end

function createmilpmodel()
  # Create a MILP model structure and set solver specific parameters
  # THINK: Support user-defined parameters
  global cobra_mip_solver
  if cobra_mip_solver == "cplex"
    # CPX_PARAM_SCRIND=0
    m = JuMP.Model(solver=CplexSolver())
  elseif cobra_mip_solver == "gurobi"
    #OutputFlag=0
    m = JuMP.Model(solver=GurobiSolver())
  elseif cobra_mip_solver == "glpk"
    m = JuMP.Model(solver=GLPKSolverMIP()) # THINK: Fails!!!!
  else
    error("Solver ", cobra_lp_solver, "not implemented yet.")
  end
  m
end

function createqpmodel()
  global cobra_qp_solver
  if cobra_qp_solver == "cplex"
    # Use Primal Simplex for QPs
    m = JuMP.Model(solver=CplexSolver(CPX_PARAM_SCRIND=0, CPX_PARAM_QPMETHOD=1))
  elseif cobra_qp_solver == "gurobi"
    assert("Not tested yet!")
    m = JuMP.Model(solver=GurobiSolver(OutputFlag=0, Method=0))
  elseif cobra_qp_solver == "glpk"
    error("GLPK cannot solve quadratic problems.")
  else
    error("Solver ", cobra_qp_solver, "not implemented yet.")
  end
  m
end

# change the default solver
function set_solver(solver)
  Solver = ""
  if solver == "glpk"
    Solver = "GLPK"
  elseif solver == "cplex"
    Solver = "CPLEX"
  elseif solver == "gurobi"
    Solver = "Gurobi"
  elseif solver == "clp"
    Solver = "Clp"
  else
    warn("choose one of the following solvers:")
    warn("glpk")
    warn("cplex")
    warn("gurobi")
    warn("clp")
  return
  end

  if !any(readdir(Pkg.dir()) .== Solver)
    warn(Solver * " is not installed")
    warn("Please run: Pkg.add(" * Solver * ") and restart Julia")
    return
  end

  global settings_default_lp = solver
end

# setup an lp model, GLPK, CPLEX, Gurobi or Clp depending on that is set as the default solver
function setup_lp(model::Model, objective::String = "max", solver::String = "")
	if solver == ""
		solver = settings_default_lp
	end
	if solver == "glpk"
		return setup_glpk(model, objective)
	elseif solver == "gurobi"
		return setup_gurobi(model, objective)
	elseif solver == "clp"
		return setup_clp(model, objective)
	elseif solver == "cplex"
		# Should be an elif, else should trigger an error (unknown solver)
		return setup_cplex(model, objective)
	else
		warn("Unknown solver. Check settings.jl for the return value of settings_lp_solver()")
	end
end

# THINK: This function should not be structured differently from the above one
function setup_lp{T <: Number}(c::Array{T}, lb::Array{T}, ub::Array{T}, b::Array{T}, S::SparseMatrixCSC, objective::String = "max", solver::String = "glpk")
	if solver == "CPLEX"
		return setup_cplex(c, lb, ub, b, S, objective)
	elseif solver == "Gurobi"
		return setup_gurobi(c, lb, ub, b, S, objective)
	else
		return setup_glpk(c, lb, ub, b, S, objective)
	end
end

# find the minimum and maximum values for a chosen objective
function min_max_lp(lp, objective::Number = 0, value::Number = 1.0)

	if (objective != 0) & isinteger(objective)
		change_objective_coef(lp, objective, value)
	end

	set_direction(lp, "min")
	solve(lp)
	min = get_objective_value(lp)

	set_direction(lp, "max")
	solve(lp)
	max = get_objective_value(lp)

	return min,max
end

# Delete a column from a sparse matrix
function delete_column(struct::AbstractSparseMatrix, n::Number)
	reaction_indices = collect(nzrange(struct, n))
	sparse_size = length(struct.nzval)
	reactions = get_sparse_columns(struct)

	new_j = deleteat!(deepcopy(struct.rowval), reaction_indices)
	new_i = deleteat!(reactions, reaction_indices)
	new_a = deleteat!(deepcopy(struct.nzval), reaction_indices)

	for i in reaction_indices[1]:length(new_i)
		new_i[i] -= 1
	end

	j = convert(Array{Int64}, new_j)
	i = convert(Array{Int64}, new_i)
	a = convert(Array{Float64}, new_a)

	return sparse(j,i,a)
end

# add a column to a sparse matrix
function add_column(struct::AbstractSparseMatrix)
	struct = hcat(struct,sparse(zeros(struct.m)))
end

# add a row to a sparse matrix
function add_row(struct::AbstractSparseMatrix)
	struct = vcat(struct,sparse(zeros(struct.n)'))
end

function get_sparse_columns(struct::AbstractSparseMatrix)
	reactions = []

	for i in 1:struct.n
		push!(reactions, i*ones(Int64, length(nzrange(struct, i))))
	end
	return vcat(reactions...)
end

function statistics(model::Model, verbose::Bool = false)
	# THINK: Include comment/name field
	rxns = length(model.rxns)
	mets = length(model.mets)
	genes = length(model.genes)
	if verbose
		@printf("Reactions: %d\nMetabolites: %d\nGenes: %d\n", rxns, mets, genes)
	end
	rxns, mets, genes
end

# Fill up empty model fields with netural data
# to keep all reaction/metabolite/gene fields the
# same length
function fix_empty_fields(model)
    nrxns = length(model.rxns)
    nmets = length(model.mets)
    ngenes = length(model.genes)

    if length(model.rxn_name) == 0
        model.rxn_name = fill("", nrxns)
    end

    if length(model.rxn_rules) == 0
        model.rxn_rules = fill("", nrxns)
    end

    if length(model.rxn_subsystem) == 0
        model.rxn_subsystem = fill("", nrxns)
    end

    if length(model.met_formula) == 0
        model.met_formula = fill("", nmets)
    end

    if length(model.met_name) == 0
        model.met_name = fill("", nmets)
    end

    if length(model.gene_name) == 0
        model.gene_name = fill("", ngenes)
    end
end

# wrapper for all fixing functions
function fix_model(model)
    fix_empty_fields(model)
    fix_gene_rules(model)
    fix_genes(model)
    fix_extra_dicts(model)
end

# converts the arrays in rxn_extra/met_extra/gene_extra
# dicitonaries to Array{Any}
function fix_extra_dicts(model)

    for (key,value) in model.rxn_extra
        model.rxn_extra[key] = convert(Array{Any}, value)
    end

    for (key,value) in model.met_extra
        model.met_extra[key] = convert(Array{Any}, value)
    end

    for (key,value) in model.gene_extra
        model.gene_extra[key] = convert(Array{Any}, value)
    end
end

# models may have odd string errors, such as ill-detectable
# whitespaces, and/or mushed agains parentheses or genes
function fix_gene_rules(model)
    model.rxn_rules = convert(Array{String}, model.rxn_rules)
    model.genes = convert(Array{String}, model.genes)

    # take care of whitespaces
    model.rxn_rules = split.(model.rxn_rules)
    model.rxn_rules =  map(z -> map(x -> map(y -> isspace(y) ? ' ' : y, x), z), model.rxn_rules)
    model.rxn_rules = map(x -> rstrip.(x), model.rxn_rules)
    model.rxn_rules = map(x -> lstrip.(x), model.rxn_rules)
    model.rxn_rules = map(x -> join(x, " "), model.rxn_rules)

    # detect where "and" or "or" are mushed to genes
    # some gene gene100 and gene10 will disrupt
    # each other so I sort them so the longest
    # genes are checked first
    # i.e if a rule reads "gene100and gene12"
    # and genes gene100 and gene10 exist, what happens
    # that if gene10 is checked I will do
    # replace(rule, "gene10", " gene10 ")
    # which results in the rule to read
    # "gene10 0and gene12"
    num_genes = length(model.genes)
    gene_name_lengths = sort(unique(length.(model.genes)), rev=true)
    gene_idx_by_name_length = map(y -> find(map(x -> length(x) == y, model.genes)), gene_name_lengths)
    gene_idx_by_name_length = vcat(gene_idx_by_name_length...)
    length_sorted_genes = model.genes[gene_idx_by_name_length]
    genes = length_sorted_genes
    num_digits = ndigits(num_genes)

    new_gene_names = Array(String, num_genes)

    for (i,v) in enumerate(genes)
        new_gene_names[i] = string("gene_","0"^(num_digits - ndigits(i)), i)
    end

    saved_names = Dict(zip(genes, new_gene_names))
    rules = deepcopy(model.rxn_rules)
    for (i,gene) in enumerate(genes)
        for (j,v) in enumerate(rules)
            if contains(v, gene)
                rules[j] = replace(rules[j], gene, saved_names[gene])
            end
        end
    end

    rules = map(x -> replace(x, "(", " ( "), rules)
    rules = map(x -> replace(x, ")", " ) "), rules)

    rules = map(x -> replace(x, "and", " and "), rules)
    rules = map(x -> replace(x, "&&", " && "), rules)
    rules = map(x -> replace(x, "and", "&&"), rules)

    rules = map(x -> replace(x, "or", " or "), rules)
    rules = map(x -> replace(x, "||", " || "), rules)
    rules = map(x -> replace(x, "or", "||"), rules)

    saved_names = invert_dict(saved_names)

    for (key,val) in saved_names
        for (i, rule) in enumerate(rules)
            rules[i] = replace(rules[i], key, val)
        end
    end

    model.rxn_rules = rules

    return
end

# remove genes that dont appear in any rules
function fix_genes(model)
    # remove genes that dont appear in any rules
    never_appear = []

    for (i,gene) in enumerate(model.genes)
        if isempty(find(x -> contains(x, gene), model.rxn_rules))
            push!(never_appear, i)
        end
    end

    deleteat!(model.genes, never_appear)
    deleteat!(model.gene_name, never_appear)

    for (key,val) in model.gene_extra
        deleteat!(model.gene_extra[key], never_appear)
    end
end
function sendto(p::Int; args...)
    for (nm, val) in args
        @spawnat(p, eval(Main, Expr(:(=), nm, val)))
    end
end

function sendto(ps::Vector{Int}; args...)
    for p in ps
        sendto(p; args...)
    end
end

function disable_cores()
    rmprocs(procs()[2:end])
end

function initialize_cores()
    addprocs(Sys.CPU_CORES-1)
    extra_cores = procs()[2:end]

    @sync for id in extra_cores
        @async remotecall_fetch(include, id, Pkg.dir() * "/CBM/src/CBM.jl")
    end
end

function initcores()
    addprocs(Sys.CPU_CORES-1)
    extra_cores = procs()

    include(Pkg.dir() * "/CBM/src/core/multicore.jl")
end

function clone(model)
	return deepcopy(model)
end

function find_metabolite(model::Model, metabolite::String)
	if metabolite_exists(model, metabolite)
		return findin(model.mets,[metabolite])[1]
	else
		return
	end
end

function metabolite_exists(model::Model, metabolite::String)
	any(model.mets .== metabolite)
end

function find_reaction{T<:String}(model::Model, reaction::T)
	index_rxns = find(x -> lowercase(x) == lowercase(reaction), model.rxns)
	index_rxn_name = find(x -> lowercase(x) == lowercase(reaction), model.rxn_name)

	return vcat([index_rxns; index_rxn_name]...)
end

function find_reaction{T<:String}(model::Model, reactions::Array{T})
	indices = map(x -> find_reaction(model,x), reactions)
	return indices
end

function reaction_exists(model::Model, reaction::String)
	any(model.rxns .== reaction)
end

function find_text(text)
	for folder in folders, file in readdir(folder)
		file = pwd() * "/" * folder * "/" * file
		f = open(file)
		try
			for (line_number, line) in enumerate(eachline(f))
				if contains(line, text)
					place = collect(search(line, text))
					@printf "%50s\x1b[31m\x1b[1m ::%4s:: \x1b[0m%s\x1b[32m\x1b[1m%s\x1b[0m%s" file line_number line[1:place[1]-1] line[place] line[place[end]+1:end]
				end
			end
		catch
			println("*** Read of "*file*" failed")
		end
	end
end

function invert_dict(dict, warning::Bool = false)
	vals = collect(values(dict))
	dict_length = length(unique(vals))

	if dict_length < length(dict)
		if warning
			warn("Keys/Vals are not one-to-one")
		end

		linked_list = Array[]

		for i in vals
			push!(linked_list,[])
		end

		new_dict = Dict(zip(vals, linked_list))

		for (key,val) in dict
			push!(new_dict[val],key)
		end
	else
		key = collect(keys(dict))

		counter = 0
		for (k,v) in dict
			counter += 1
			vals[counter] = v
			key[counter] = k
		end
		new_dict = Dict(zip(vals, key))
	end

	return new_dict
end

"""
Wrapper for `load_json()` and `load_matlab()`

    model = load_model(filename)
"""
function load_model(filename::String; fix = true)
    all_files = readdir(dirname(filename))

    if endswith(filename, ".json")
        return load_json(filename)
    elseif endswith(filename, ".mat")
        return load_matlab(filename)
    elseif any(all_files .== basename(filename) * ".json")
        return load_model(filename * ".json")
    elseif any(all_files .== basename(filename) * ".mat")
        return load_model(filename * ".mat")
    else
        warn("Model not found")
    end
end

"""
Returns a dictionary with variables saved in a `.mat` file

    open_mat_file(filename)

**Example**

    open_mat_file("/home/user/variables.mat")
     Dict{String,Any} with 2 entries:
     "var_a" => "[1,2,3,4]"
     "var_b" => "hello"
"""
function open_mat_file(filename::String)
    filename = endswith(filename, ".mat") ? filename : filename * ".mat"

    if any(readdir(dirname(filename)) .== basename(filename))
        mat_file = matread(filename)
    else
        warn("File not found, does it end with '.mat' ? ")
    end
end

"""
Use to save variables to `.mat` format

    save_mat_file(filename; args...)

**Example**

If you have the variables `a` and `b` defined as

    a = [1,2,3,4]
    b = "hello"

You can save them by calling

    save_mat_file("/home/user/variables.mat", var_a = a, var_b = b)

and they can be accessed within `Julia` by calling

    open_mat_file("/home/user/variables.mat")
     Dict{String,Any} with 2 entries:
     "var_a" => "[1,2,3,4]"
     "var_b" => "hello"
"""
function save_mat_file(filename::String; args...)
    filename = endswith(filename, ".mat") ? filename : filename * ".mat"
    varnames = []
    varvals = []

    for (nm, val) in args
        push!(varnames, string(nm))
        push!(varvals, val)
    end

    MAT.matwrite(filename, Dict(zip(varnames, varvals)))
end


module TestSymbolicNode
using MXNet
using Base.Test

using ..Main: mlp2, reldiff

################################################################################
# Test Implementations
################################################################################
function test_basic()
  info("SymbolicNode::basic")

  model = mlp2()
  @test mx.list_arguments(model) == [:data,:fc1_weight,:fc1_bias,:fc2_weight,:fc2_bias]
  @test mx.list_outputs(model) == [:fc2_output]
  @test mx.list_auxiliary_states(model) == Symbol[]
end

function test_internal()
  info("SymbolicNode::internal")

  data  = mx.Variable(:data)
  oldfc = mx.FullyConnected(data=data, name=:fc1, num_hidden=10)
  net1  = mx.FullyConnected(data=oldfc, name=:fc2, num_hidden=100)

  @test mx.list_arguments(net1) == [:data,:fc1_weight,:fc1_bias,:fc2_weight,:fc2_bias]

  internal = mx.get_internals(net1)
  fc1      = internal[:fc1_output]
  @test mx.list_arguments(fc1) == mx.list_arguments(oldfc)
end

function test_compose()
  info("SymbolicNode::compose")

  data = mx.Variable(:data)
  net1 = mx.FullyConnected(data=data, name=:fc1, num_hidden=10)
  net1 = mx.FullyConnected(data=net1, name=:fc2, num_hidden=100)

  net2 = mx.FullyConnected(name=:fc3, num_hidden=10)
  net2 = mx.Activation(data=net2, act_type=:relu)
  net2 = mx.FullyConnected(data=net2, name=:fc4, num_hidden=20)

  composed  = net2(fc3_data=net1, name=:composed)
  multi_out = mx.Group(composed, net1)
  @test mx.list_outputs(multi_out) == [:composed_output, :fc2_output]
end

function test_infer_shape()
  info("SymbolicNode::infer_shape::mlp2")

  model = mlp2()
  data_shape = (100, 100)
  arg_shapes, out_shapes, aux_shapes = mx.infer_shape(model, data=data_shape)
  arg_shape_dict = Dict{Symbol,Tuple}(zip(mx.list_arguments(model), arg_shapes))
  @test arg_shape_dict == Dict{Symbol,Tuple}(:fc2_bias => (10,),:fc2_weight => (1000,10),
                                             :fc1_bias => (1000,), :fc1_weight => (100, 1000),
                                             :data => data_shape)
  @test length(out_shapes) == 1
  @test out_shapes[1] == (10, 100)
end

function test_infer_shape_error()
  info("SymbolicNode::infer_shape::throws")

  model = mlp2()
  weight_shape = (100, 1)
  data_shape   = (100, 100)
  @test_throws mx.MXError mx.infer_shape(model, data=data_shape, fc1_weight=weight_shape)
end

function test_saveload()
  info("SymbolicNode::saveload::mlp2")

  model = mlp2()
  fname = tempname()
  mx.save(fname, model)
  model2 = mx.load(fname, mx.SymbolicNode)
  @test mx.to_json(model) == mx.to_json(model2)

  rm(fname)
end

function test_attrs()
  info("SymbolicNode::Attributes")

  data = mx.Variable(:data)

  result = mx.get_attr(data, :test)
  @test isnull(result)
  mx.set_attr(data, :test, "1.0")
  result = mx.get_attr(data, :test)
  @test !isnull(result)
  @test get(result) == "1.0"

  data2 = mx.Variable(:data2, attrs = Dict(:test => "hallo!"))
  @test get(mx.get_attr(data2, :test)) == "hallo!"

  conv = mx.Convolution(data = data2, kernel = (1,1), num_filter = 1, attrs = Dict(:a => "a", :π => "π"))
  @test isnull(mx.get_attr(conv, :b))
  @test get(mx.get_attr(conv, :a)) == "a"
  @test get(mx.get_attr(conv, :π)) == "π"
  @test mx.list_attr(conv) == Dict(:a => "a", :π => "π")

  @test_throws MethodError mx.Variable(:data3, attrs = Dict(:test => "1.0", :test2 => 1.0))
  @test_throws MethodError mx.Convolution(data=data2, kernel = (1,1), num_filter = 1, attrs = Dict(:test => "1.0", :test2 => 1.0))
end

function test_functions()
  info("SymbolicNode::Functions")
  data = mx.Variable(:data)
  typeof(mx.sum(data)) == mx.SymbolicNode
end

function test_dot()
  info("SymbolicNode::dot")
  x = mx.Variable(:x)
  y = mx.Variable(:y)
  z = mx.dot(x, y)
  z_exec = mx.bind(z, context=mx.cpu(),
                   args=Dict(:x=>mx.ones((100, 2)), :y=>mx.ones((2, 200))))
  mx.forward(z_exec)

  ret = copy(z_exec.outputs[1])
  @test size(ret) == (100, 200)
  @test reldiff(ret, 2*ones(100, 200)) < 1e-6
end

################################################################################
# Run tests
################################################################################
test_basic()
test_internal()
test_compose()
test_infer_shape()
test_infer_shape_error()
test_saveload()
test_attrs()
test_functions()
test_dot()

end


module XGBoost

using XGBoost_jll

using Printf
using Random: randperm, seed!
using SparseArrays: SparseMatrixCSC, nnz
using Statistics: mean, std

export DMatrix, Booster
export xgboost, predict, save, nfold_cv, slice, get_info, set_info, dump_model, importance
export rabit_init, rabit_finalize, rabit_is_distributed, rabit_get_rank, rabit_get_world_size, rabit_get_version_number

global const build_version = "0.82"

include("xgboost_wrapper_h.jl")
include("rabit_wrapper.jl")
include("xgboost_lib.jl")

end # module XGBoost


# src path
src_path = "C:\\Users\\hidee\\Desktop\\git\\general_2d_NS_Chemical-N2-N-\\src_c\\"

# main (変更しないこと)
src_read="read_grid.jl"
include(src_path*src_read)
src_read="read_para.jl"
include(src_path*src_read)
src_read="output.jl"
include(src_path*src_read)

src_read="advection_term.jl"
include(src_path*src_read)
src_read="boundary.jl"
include(src_path*src_read)
src_read="converge.jl"
include(src_path*src_read)
src_read="misc.jl"
include(src_path*src_read)
src_read="rhs.jl"
include(src_path*src_read)
src_read="value_setup.jl"
include(src_path*src_read)
src_read="viscos.jl"
include(src_path*src_read)
src_read="chemical_constant.jl"
include(src_path*src_read)
src_read="chemical_term.jl"
include(src_path*src_read)
src_read="setup.jl"
include(src_path*src_read)

src_read="cal_time_step.jl"
include(src_path*src_read)
src_main="main.jl"
include(src_path*src_main)


module Dijkstra

include("graph.jl")
export Graph
export add_edge!
export get_edge_weight
export get_adjacent_nodes
export generate_random_graph

include("shortestpath.jl")
export ShortestPath
export get_distance
export get_path

end # module


# Define standard forward and reverse propagation to switch of expression definitions
# for expressions.
function binary_switch(ids; is_forward = true)
    if length(ids) <= 3
        if is_forward
            out = Expr(:if, Expr(:call, :(==), :id, ids[1]), :(return fprop!(t, Val($(ids[1])), g, c, k)))
        else
            out = Expr(:if, Expr(:call, :(==), :id, ids[1]), :(return rprop!(t, Val$((ids[1])), g, c, k)))
        end
        (length(ids) > 1) && push!(out.args, binary_switch(ids[2:end]))
        return out
    else
        mid = length(ids) >>> 1
        return Expr(:if, Expr(:call, :(<=), :id, ids[mid]), binary_switch(ids[1:mid]), binary_switch(ids[mid+1:end]))
    end
end

function Node(aux_info, d::JuMP._Derivatives.NodeData, child_vec, c::UnitRange{Int}, op)
    nt = d.nodetype
    i = d.index
    (nt == JuMP._Derivatives.CALL)          && return _create_call_node(i, child_vec, c, op)
    (nt == JuMP._Derivatives.CALLUNIVAR)    && return _create_call_node_uni(i, child_vec, c, op)
    (nt == JuMP._Derivatives.VALUE)         && return Node(Constant(), i)
    (nt == JuMP._Derivatives.PARAMETER)     && return Node(Parameter(), i)
    (nt == JuMP._Derivatives.SUBEXPRESSION) && return Node(Subexpression(), i)
    (nt == JuMP._Derivatives.VARIABLE)      && return !is_auxilliary_variable(aux_info, i) ? Node(Variable(), i) : Node(Select(), i)
    (nt == JuMP._Derivatives.LOGIC)         && error("Unable to load JuMP expression. Logical operators not currently supported.")
    (nt == JuMP._Derivatives.COMPARISON)    && error("Unable to load JuMP expression. Comparisons not currently supported.")
    error("Node type = $nt not expected from JuMP.")
end

function _convert_node_list(aux_info, x::Vector{JuMP._Derivatives.NodeData}, op)
    y = Vector{Node}(undef, length(x))
    adj = JuMP._Derivatives.adjmat(x)
    child_vec = rowvals(adj)
    for i in eachindex(x)
        y[i] = Node(aux_info, x[i], child_vec, nzrange(adj, i), op)
    end
    return y
end

# Access gradient sparsity of JuMP storage.
sparsity(d::JuMP._FunctionStorage) = d.grad_sparsity
sparsity(d::JuMP._SubexpressionStorage) = d.sparsity

# Compute gradient sparsity from JuMP storage.
function _compute_sparsity(d::JuMP._FunctionStorage, sparse_dict::Dict{Int,Vector{Int}}, is_sub, subexpr_indx)
    dep_subexpression = Int[]
    variable_dict = Dict{Int,Bool}()
    for n in d.nd
        if n.nodetype == JuMP._Derivatives.VARIABLE
            if !haskey(variable_dict, n.index)
                variable_dict[n.index] = true
            end
        end
        if n.nodetype == JuMP._Derivatives.SUBEXPRESSION
            push!(dep_subexpression, n.index)
        end
    end
    sparsity = collect(keys(variable_dict))
    unique!(dep_subexpression)
    sort!(dep_subexpression)
    for s in dep_subexpression
        append!(sparsity, sparse_dict[s])
    end
    unique!(sparsity)
    sort!(sparsity)
    if is_sub
        sparse_dict[subexpr_indx] = sparsity
    end
    sparsity, dep_subexpression
end
function _compute_sparsity(d::JuMP._SubexpressionStorage, sparse_dict::Dict{Int,Vector{Int}}, is_sub, subexpr_indx)
    dep_subexpression = Int[]
    variable_dict = Dict{Int,Bool}()
    for n in d.nd
        if n.nodetype == JuMP._Derivatives.VARIABLE
            if !haskey(variable_dict, n.index)
                variable_dict[n.index] = true
            end
        end
        if n.nodetype == JuMP._Derivatives.SUBEXPRESSION
            push!(dep_subexpression, n.index)
        end
    end
    sparsity = collect(keys(variable_dict))
    unique!(dep_subexpression)
    sort!(dep_subexpression)
    for s in dep_subexpression
        append!(sparsity, sparse_dict[s])
    end
    unique!(sparsity)
    sort!(sparsity)
    if is_sub
        sparse_dict[subexpr_indx] = sparsity
    end
    sparsity, dep_subexpression
end

function linearity(d::JuMP._Derivatives.Linearity)
    (d == JuMP._Derivatives.LINEAR)            && return LIN_LINEAR
    (d == JuMP._Derivatives.PIECEWISE_LINEAR)  && return LIN_PIECEWISE_LINEAR
    (d == JuMP._Derivatives.NONLINEAR)         && return LIN_NONLINEAR
    LIN_CONSTANT                               # assumes d is then JuMP._Derivatives.CONSTANT
end
function linearity(nd::Vector{JuMP._Derivatives.NodeData}, adj::SparseMatrixCSC{Bool,Int}, d::Vector{JuMP._Derivatives.Linearity})
    x = JuMP._Derivatives.classify_linearity(nd, adj, d)
    linearity.(x)
end

function OperatorRegistry(d::JuMP._Derivatives.UserOperatorRegistry)
    mv_id = collect(keys(d.multivariate_operator_to_id))
    mv_operator_to_id = d.multivariate_operator_to_id
    mv_operator_evaluator = d.multivariate_operator_evaluator
    u_operator_id = collect(keys(d.univariate_operator_to_id))
    u_to_id = d.univariate_operator_to_id
    u_to_f = d.univariate_operator_f
    u_fprime = d.univariate_operator_fprime
    u_fprimeprime = d.univariate_operator_fprimeprime
    OperatorRegistry(mv_id, mv_operator_to_id, mv_operator_evaluator, u_operator_id, u_to_id, u_to_f, u_fprime, u_fprimeprime)
end


function testSolveSession(client, context, variableLabels; maxSeconds=180)
    # allVariableLabels = ls(client, context, variableLabels)

    # do the solve
    resultId = solveSession(client,context)

    # Wait for them to be done before proceeding.
    waitForCompletion(client, Task[resultId;]; expectedStatuses=["Complete"], maxSeconds)

    # Get PPE's are there for the connected variables.
    # TODO - complete the factor graph.
    for v in variableLabels # getVariables(client, context, detail=SUMMARY)
        # First solve key (only) is the default result
        @test fetch( getVariable(client, context, v) )["ppes"][1]["solveKey"] == "default"
    end
end

function testVizHelpersApp(context)
    @show GraphVizApp(context)
    @show MapVizApp(context)

    nothing
end

function runSolveTests(client, context)
    @testset "solve-testset" begin
        @info "Running solve-testset"

        # TODO: Test the rest of the variables.
        variableLabels = ["x0", "x1"]

        testSolveSession(client, context, variableLabels)
    end

    @testset "appviz-testset" begin
        # generate viz help objects from current context
        testVizHelpersApp(context)
    end
end


#using Queryverse
using XLSX
using DataFrames
using Dates
using DelimitedFiles
using CSV

# Parameters

const N_Gens =  132 # number of conventional generators
const N_StorgUs = 2 # number of storage units
const N_Zones = 2
const M_Zones = 2
const N_Blocks =7
const INITIAL_DAY = 1
const FINAL_DAY = 3
const INITIAL_HR_FUCR = 6 # represents the running time for the first WA unit commitment run. INITIAL_HR_FUCR=0 means the FUCR's optimal outcomes are ready at 00:00
const INITIAL_HR_SUCR = 17 # represents the running time for the second WA unit commitment run. INITIAL_HR_SUCR=17 means the SUCR's optimal outcomes are ready at 17:00
const N_Hrs_FUCR = 162 # N_Hrs_FUCR = 168-INITIAL_HR_FUCR, and INITIAL_HR_FUCR runs from 0 to 23; INITIAL_HR_FUCR=6 ==> N_Hrs_FUCR =162
const N_Hrs_SUCR = 151  # N_Hrs_SUCR = 168-INITIAL_HR_SUCR, and INITIAL_HR_SUCR runs from 17 to 23; INITIAL_HR_SUCR=17 ==> N_Hrs_FUCR =168

const FILE_ACCESS_OVER = "w+"
const FILE_ACCESS_APPEND = "a+"

##
########################### Importing input data from the input spreadsheets
# Generators' specification
# Logging file
io_log = open(
    string(
        ".//outputs//log//ReadInputs_",
        Dates.format(now(), "yyyy-mm-dd_HH-MM-SS"),
        ".txt",
    ),
    FILE_ACCESS_APPEND,
)

t1 = time_ns()

DF_Generators = CSV.read(".//inputs//csv//data_generators.csv", DataFrame);
Map_Gens = readdlm(".//inputs//csv//location_generators.csv", ','; header = true);
DF_Storage = CSV.read(".//inputs//csv//data_storage.csv", DataFrame);
Map_Storage = readdlm(".//inputs//csv//location_storage.csv", ','; header = true);

FUCR_Demands = readdlm(".//inputs//csv//data_demand.csv", ','; header = true);
SUCR_Demands = readdlm(".//inputs//csv//data_demand_updated.csv", ','; header = true);
BUCR_Demands = readdlm(".//inputs//csv//data_demand_actual.csv", ','; header = true);

# solar generation data at each location
FUCR_SolarGs = readdlm(".//inputs//csv//data_solar.csv", ','; header = true);
SUCR_SolarGs = readdlm(".//inputs//csv//data_solar_updated.csv", ','; header = true);
BUCR_SolarGs = readdlm(".//inputs//csv//data_solar_actual.csv", ','; header = true);

# wind energy data for each location
FUCR_WindGs = readdlm(".//inputs//csv//data_wind.csv", ','; header = true);
SUCR_WindGs = readdlm(".//inputs//csv//data_wind_updated.csv", ','; header = true);
BUCR_WindGs = readdlm(".//inputs//csv//data_wind_actual.csv", ','; header = true);

#hydro generation data for each location
FUCR_HydroGs = readdlm(".//inputs//csv//data_hydro.csv", ','; header = true);
SUCR_HydroGs = readdlm(".//inputs//csv//data_hydro_updated.csv", ','; header = true);
BUCR_HydroGs = readdlm(".//inputs//csv//data_hydro_actual.csv", ','; header = true);

tranC = readdlm(".//inputs//csv//LineCapacity.csv", ','; header = true);
tranS = readdlm(".//inputs//csv//LineSusceptance.csv", ','; header = true);
reserve_reqs = readdlm(".//inputs//csv//data_reserve_reqs.csv", ','; header = true);

fuelPrice = readdlm(".//inputs//csv//data_fuel_price.csv", ','; header = true);

fuelPrice_head = fuelPrice[2];
fuelPrice = fuelPrice[1];
fuelPrice = fuelPrice[2:N_Gens+1, 4:368];
#FuelPrice = XLSX.readdata(".\\inputs\\data_generators.XLSX", "data_fuel_price", "D3:ND134")

# Reorganize data
FUCR_Demands_head = FUCR_Demands[2];
FUCR_Demands = FUCR_Demands[1];
FUCR_Demands = FUCR_Demands[1:53999, 4:(4+N_Zones-1)];

SUCR_Demands_head = SUCR_Demands[2];
SUCR_Demands = SUCR_Demands[1];
SUCR_Demands = SUCR_Demands[1:53999, 4:(4+N_Zones-1)];

BUCR_Demands_head = BUCR_Demands[2];
BUCR_Demands = BUCR_Demands[1];
BUCR_Demands = BUCR_Demands[1:7728, 3:(3+N_Zones-1)];

# Solar
FUCR_SolarGs_head = FUCR_SolarGs[2];
FUCR_SolarGs = FUCR_SolarGs[1];
FUCR_SolarGs = FUCR_SolarGs[1:7728, 3:(3+N_Zones-1)];

SUCR_SolarGs_head = SUCR_SolarGs[2];
SUCR_SolarGs = SUCR_SolarGs[1];
SUCR_SolarGs = SUCR_SolarGs[1:7728, 3:(3+N_Zones-1)];

BUCR_SolarGs_head = BUCR_SolarGs[2];
BUCR_SolarGs = BUCR_SolarGs[1];
BUCR_SolarGs = BUCR_SolarGs[1:7728, 3:(3+N_Zones-1)];

#Wind
FUCR_WindGs_head = FUCR_WindGs[2];
FUCR_WindGs = FUCR_WindGs[1];
FUCR_WindGs = FUCR_WindGs[1:7728, 3:(3+N_Zones-1)];

SUCR_WindGs_head = SUCR_WindGs[2];
SUCR_WindGs = SUCR_WindGs[1];
SUCR_WindGs = SUCR_WindGs[1:7728, 3:(3+N_Zones-1)];

BUCR_WindGs_head = BUCR_WindGs[2];
BUCR_WindGs = BUCR_WindGs[1];
BUCR_WindGs = BUCR_WindGs[1:7728, 3:(3+N_Zones-1)];

#Hydro
FUCR_HydroGs_head = FUCR_HydroGs[2];
FUCR_HydroGs = FUCR_HydroGs[1];
FUCR_HydroGs = FUCR_HydroGs[1:7728, 2:(2+N_Zones-1)];

SUCR_HydroGs_head = SUCR_HydroGs[2];
SUCR_HydroGs = SUCR_HydroGs[1];
SUCR_HydroGs = SUCR_HydroGs[1:7728, 2:(2+N_Zones-1)];

BUCR_HydroGs_head = BUCR_HydroGs[2];
BUCR_HydroGs = BUCR_HydroGs[1];
BUCR_HydroGs = BUCR_HydroGs[1:7728, 2:(2+N_Zones-1)];

Map_Gens_head = Map_Gens[2];
Map_Gens = Map_Gens[1];
Map_Gens = Map_Gens[:,3:N_Zones+2]

Map_Storage_head = map_storage[2];
Map_Storage = Map_Storage[1];
Map_Storage = Map_Storage[:,2:N_Zones+1]
#map_storage = convert(Array{Int32,2}, map_storage[:,2:N_Zones+1]);

tranC_head = tranC[2];
tranC = tranC[1];
tranC = tranC[1:N_Zones,2:(2+N_Zones-1)];

tranS_head = tranS[2];
tranS = tranS[1];
tranS = tranS[1:N_Zones,2:(2+N_Zones-1)];

reserve_reqs_head = reserve_reqs[2];
reserve_reqs = reserve_reqs[1];
reserve_req_up = reserve_reqs
#reserve_req_dn = reserve_reqs[:,3];

##
# Importing input data from the input spreadsheets
# Generators' specification
#DF_Generators = DataFrame(XLSX.readtable(".\\inputs\\data_generators.XLSX", "data_generators")...)

# Generators location map: if a generator g is located in zone z Map_Gens[g,z]=1; and 0 otherwise
#DF_Map_Gens = DataFrame(XLSX.readtable(".\\inputs\\data_generators.XLSX", "location_generators")...)
#Map_Gens = convert(Matrix, DF_Map_Gens[:,3:N_Zones+2])

# Storage Units' specification and location
#DF_Storage = DataFrame(XLSX.readtable(".\\inputs\\data_storage.XLSX", "data_storage")...) # storage specs
#DF_Map_Storage = DataFrame(XLSX.readtable(".\\inputs\\data_storage.XLSX", "location_storage")...) # storage location as a dataframe
#Map_Storage = convert(Matrix, DF_Map_Storage[:,2:N_Zones+1]) # convert storage location data to  a matrix

# energy demand at each location
#DF_Dems = DataFrame(XLSX.readtable(".\\Inputs\\data_demand.XLSX", "data_demand")...)
#FUCR_Demands = XLSX.readdata(".\\inputs\\data_demand.XLSX", "data_demand", "D2:E54000")
#SUCR_Demands = XLSX.readdata(".\\inputs\\data_demand.XLSX", "data_demand_updated", "D2:E54000")
#BUCR_Demands = XLSX.readdata(".\\inputs\\data_demand.XLSX", "data_demand_actual", "C2:D7729")
# There is no map for the demand data. Instead we take the input demand data for each zone. In other words, Demand[t,z] represents demand at zone z and time t

# solar generation data at each location
#FUCR_SolarGs = XLSX.readdata(".\\inputs\\data_solar.XLSX", "data_solar", "C2:D7729")
#SUCR_SolarGs = XLSX.readdata(".\\inputs\\data_solar.XLSX", "data_solar_updated", "C2:D7729")
#BUCR_SolarGs = XLSX.readdata(".\\inputs\\data_solar.XLSX", "data_solar_actual", "C2:D7729")

# wind energy data for each location
#FUCR_WindGs = XLSX.readdata(".\\inputs\\data_wind.XLSX", "data_wind", "C2:D7729")
#SUCR_WindGs = XLSX.readdata(".\\inputs\\data_wind.XLSX", "data_wind_updated", "C2:D7729")
#BUCR_WindGs = XLSX.readdata(".\\inputs\\data_wind.XLSX", "data_wind_actual", "C2:D7729")

#hydro generation data for each location
#FUCR_HydroGs = XLSX.readdata(".\\inputs\\data_hydro.XLSX", "data_hydro", "B2:C7729")
#SUCR_HydroGs = XLSX.readdata(".\\inputs\\data_hydro.XLSX", "data_hydro_updated", "B2:C7729")
#BUCR_HydroGs = XLSX.readdata(".\\inputs\\data_hydro.XLSX", "data_hydro_actual", "B2:C7729")

#Zonal Reserve Targets
#Reserve_Req_Up = XLSX.readdata(".\\inputs\\data_reserve_reqs.XLSX", "data_reserve_reqs", "A2:B2")

# Transmission system data (Capacity and Susceptance)
#TranC = XLSX.readdata(".\\inputs\\data_tranSmission.XLSX", "LineCapacity","B2:C3") # Transmission line capacity
#TranS = XLSX.readdata(".\\inputs\\data_tranSmission.XLSX", "LineSusceptance","B2:C3")# Transmission line susceptance

# Daily Fuel Price data
#FuelPrice = XLSX.readdata(".\\inputs\\data_generators.XLSX", "data_fuel_price", "D3:ND134")

##
t2 = time_ns()
elapsedTime = (t2 -t1)/1.0e9;

write(io_log, "Whole program time execution (s):\t $elapsedTime\n")
close(io_log);


using Plots
using KernelAbstractions: CPU
using ClimateMachine.MPIStateArrays: array_device
using ClimateMachine.BalanceLaws: Prognostic, Auxiliary, GradientFlux

"""
    plot_friendly_name(ϕ)

Get plot-friendly string, since many Unicode
characters do not render in plot labels.
"""
function plot_friendly_name(ϕ)
    s = ϕ
    s = replace(s, "ρ" => "rho")
    s = replace(s, "α" => "alpha")
    s = replace(s, "∂" => "partial")
    s = replace(s, "∇" => "nabla")
    return s
end

"""
    export_plot(
        z,
        time_data,
        dons_arr::Array,
        ϕ_all,
        filename;
        xlabel,
        ylabel,
        time_units = "[s]",
        round_digits = 2,
        horiz_layout = false,
        kwargs...
    )

Export plot of all variables, or all
available time-steps in `dons_arr`.
"""
function export_plot(
    z,
    time_data,
    dons_arr::Array,
    ϕ_all,
    filename;
    xlabel,
    ylabel,
    time_units = "[s]",
    round_digits = 2,
    sample_rate = 1,
    horiz_layout = false,
    kwargs...,
)
    ϕ_all isa Tuple || (ϕ_all = (ϕ_all,))
    single_var = ϕ_all[1] == xlabel || length(ϕ_all) == 1
    p = plot()
    sample = 1:sample_rate:length(time_data)
    for (t, data) in zip(time_data[sample], dons_arr[sample])
        for ϕ in ϕ_all
            ϕ_string = String(ϕ)
            ϕ_data = data[ϕ_string][:]
            t_label = "t=$(round(t, digits=round_digits)) $time_units"
            label = single_var ? t_label : "$ϕ_string, $t_label"
            args = horiz_layout ? (z, ϕ_data) : (ϕ_data, z)
            plot!(
                args...;
                xlabel = xlabel,
                ylabel = ylabel,
                label = label,
                kwargs...,
            )
        end
    end
    savefig(filename)
end

"""
    export_contour(
        z,
        time_data,
        dons_arr::Array,
        ϕ,
        filename;
        xlabel = "time [s]",
        ylabel = "z [m]",
        label = String(ϕ),
        kwargs...
    )

Export contour plots given
 - `z` Array of altitude. Note: this must not include duplicate nodal points.
 - `time_data` array of time data
 - `dons_arr` an array whose elements are populated by `dict_of_nodal_states`
 - `ϕ` variable to contour
 - `filename` file name to export to.
 - `xlabel` x-label
 - `ylabel` y-label
 - `label` contour labels
"""
function export_contour(
    z,
    time_data,
    dons_arr::Array,
    ϕ,
    filename;
    xlabel = "time [s]",
    ylabel = "z [m]",
    label = String(ϕ),
    kwargs...,
)
    ϕ_string = String(ϕ)
    ϕ_data = hcat([data[ϕ_string][:] for data in dons_arr]...)
    args = (time_data, z, ϕ_data)
    try
        contourf(
            args...;
            xlabel = xlabel,
            ylabel = ylabel,
            label = label,
            c = :viridis,
            kwargs...,
        )
        savefig(filename)
    catch
        @warn "Contour plot $label failed. Perhaps the field is all zeros"
    end
end

function save_binned_surface_plots(
    x,
    y,
    z,
    title,
    filename,
    n_plots = (3, 3),
    z_label_prefix = "z",
    n_digits = 5,
)
    n_z_partitions = prod(n_plots)
    z_min_global = min(z...)
    z_max_global = max(z...)
    Δz = (z_max_global - z_min_global) / n_z_partitions
    z_min = ntuple(i -> z_min_global + (i - 1) * Δz, n_z_partitions)
    z_max = ntuple(i -> z_min_global + (i) * Δz, n_z_partitions)
    p = []
    for i in 1:n_z_partitions
        mask = z_min[i] .<= z .<= z_max[i]
        x_i = x[mask]
        y_i = y[mask]
        sz_min = string(z_min[i])[1:min(n_digits, length(string(z_min[i])))]
        sz_max = string(z_max[i])[1:min(n_digits, length(string(z_max[i])))]
        p_i = plot(
            x_i,
            y_i,
            title = "$(title), in ($sz_min, $sz_max)",
            seriestype = :scatter,
            markersize = 5,
        )
        push!(p, p_i)
    end
    plot(p..., layout = n_plots, legend = false)
    savefig(filename)
end;

state_prefix(::Prognostic) = "prog_"
state_prefix(::Auxiliary) = "aux_"
state_prefix(::GradientFlux) = "grad_flux_"

"""
    export_state_plots(
        solver_config,
        dons_arr,
        time_data,
        output_dir;
        state_types = (Prognostic(), Auxiliary()),
        z = Array(get_z(solver_config.dg.grid)),
        sample_rate = 1,
        time_units = "[s]",
        ylabel = "z [m]",
        kwargs...
    )

Export line plots of states given
 - `solver_config` a `SolverConfiguration`
 - `dons_arr` an array of dictionaries, returned from `dict_of_nodal_states`
 - `time_data` an array of time values
 - `output_dir` output directory
"""
function export_state_plots(
    solver_config,
    dons_arr,
    time_data,
    output_dir;
    state_types = (Prognostic(), Auxiliary()),
    z = Array(get_z(solver_config.dg.grid)),
    sample_rate = 1,
    time_units = "[s]",
    ylabel = "z [m]",
    kwargs...,
)
    FT = eltype(solver_config.Q)
    mkpath(output_dir)
    for st in state_types
        vs = vars_state(solver_config.dg.balance_law, st, FT)
        for fn in flattenednames(vs)
            base_name = state_prefix(st) * replace(fn, "." => "_")
            file_name = joinpath(output_dir, "$(base_name).png")
            export_plot(
                z,
                time_data,
                dons_arr,
                (fn,),
                file_name;
                xlabel = fn,
                sample_rate = sample_rate,
                ylabel = ylabel,
                time_units = time_units,
                round_digits = 5,
                kwargs...,
            )
        end
    end
end

"""
    export_state_contours(
        solver_config,
        dons_arr,
        time_data,
        output_dir;
        state_types = (Prognostic(),),
        xlabel = "time [s]",
        ylabel = "z [m]",
        z = Array(get_z(solver_config.dg.grid; rm_dupes=true)),
        kwargs...
    )

Call `export_contour` for every
state variable given `state_types`.
"""
function export_state_contours(
    solver_config,
    dons_arr,
    time_data,
    output_dir;
    state_types = (Prognostic(),),
    xlabel = "time [s]",
    ylabel = "z [m]",
    z = Array(get_z(solver_config.dg.grid; rm_dupes = true)),
    kwargs...,
)
    FT = eltype(solver_config.Q)
    mkpath(output_dir)
    for st in state_types
        vs = vars_state(solver_config.dg.balance_law, st, FT)
        for fn in flattenednames(vs)
            base_name = state_prefix(st) * replace(fn, "." => "_")
            filename = joinpath(output_dir, "cnt_$(base_name).png")
            label = string(replace(fn, "." => "_"))
            args = (z, time_data, dons_arr, fn, filename)
            export_contour(
                args...;
                xlabel = xlabel,
                ylabel = ylabel,
                label = label,
                kwargs...,
            )
        end
    end
end