import SymbolicUtils: symtype
struct Parameter{T} end

isparameter(x) = false
isparameter(::Sym{<:Parameter}) = true
isparameter(::Sym{<:FnType{<:Any, <:Parameter}}) = true

SymbolicUtils.symtype(s::Symbolic{Parameter{T}}) where T = T
SymbolicUtils.similarterm(t::Term{T}, f, args) where {T<:Parameter} = Term{T}(f, args)

Base.convert(::Type{Num}, x::Symbolic{Parameter{T}}) where {T<:Number} = Num(x)

"""
$(SIGNATURES)

Define one or more known variables.
"""
macro parameters(xs...)
    esc(_parse_vars(:parameters, Parameter{Real}, xs))
end


using MICE
using Test
import CSV
using Glob
using FilePathsBase
using FilePathsBase: /
using DataFrames

Glob.glob(pattern, path::AbstractPath) = glob(pattern, string(path))

@testset "MICE.jl" begin
    input_file_paths = Path.(glob("*.csv", cwd() / "comparison" / "data" / "input"))
    @test length(input_file_paths) > 0 # Find input files
    input_datasets = Dict()
    for input_file_path in input_file_paths
        file_name = filename(input_file_path)
        input_datasets[file_name] = CSV.File(input_file_path, missingstring = "NA") |> DataFrame
    end

    output_datasets = Dict()
    for (dataset_name, datset) in input_datasets
        output_datasets[dataset_name] = mice(datset; max_iterations = 1)
    end
end


# Packages required for testing
using Test
using Random
using Logging

# Package Under Test
using JuliaPackageTemplate

# Set logging level
global_logger(SimpleLogger(stderr, Logging.Debug))

# Fix randomness during tests
Random.seed!(0)

# Check equality of two arrays
@inline function array_isapprox(x::AbstractArray{F},
                  y::AbstractArray{F};
                  rtol::F=sqrt(eps(F)),
                  atol::F=zero(F)) where {F<:AbstractFloat}

    # Easy check on matching size
    if length(x) != length(y)
        return false
    end

    for (a,b) in zip(x,y)
        @test isapprox(a,b, rtol=rtol, atol=atol)
    end
end

# Check if array equals a single value
@inline function array_isapprox(x::AbstractArray{F},
                  y::F;
                  rtol::F=sqrt(eps(F)),
                  atol::F=zero(F)) where {F<:AbstractFloat}

    for a in x
        @test isapprox(a, y, rtol=rtol, atol=atol)
    end
end

# Define package tests
@time @testset "JuliaPackageTemplate Package Tests" begin
    testdir = joinpath(dirname(@__DIR__), "test")
    @time @testset "JuliaPackageTemplate.YourSubmodule" begin
        include(joinpath(testdir, "test_submodule.jl"))
    end
    @time @testset "JuliaPackageTemplate.SingleSatellite" begin
        include(joinpath(testdir, "test_rubberducks.jl"))
    end
end


using Cbc
using Hungarian
using JuMP
using Kombinator
using LightGraphs
using Munkres
using Test

is_travis = "GITHUB_ACTIONS" in keys(ENV)
# TODO: need for optional dependencies for this to work, I suppose.
# https://github.com/JuliaLang/Pkg.jl/issues/1285
if !is_travis
    using JuMP
    # Why Gurobi?
    # - Need support for lazy constraints (elementary paths):
    #   - Mosek does not support them
    # - Need support for MISOCP
    #   - Pajarito is not yet ported to MOI
    using Gurobi
end

@testset "Kombinator.jl" begin
    include("um.jl")
    include("st.jl")
    include("ep.jl")
    # include("matching.jl")

    include("nlcop.jl")
end


## @primitive

function primitive_sig(sig)
    fn, args = sig.args[1], sig.args[2:end]
    targs = Array{Any}(undef, length(args))
    vnames = Array{Symbol}(undef, length(args))
    for i=1:length(args)
        if args[i] isa Expr
            name, typ_ex = args[i].args
            typ = Core.eval(Base, typ_ex)
            ttyp = typ <: AbstractArray ? TArray : TReal
            targs[i] = :($name :: $ttyp)
            vnames[i] = name
        elseif args[i] isa Symbol
            name = args[i]
            targs[i] = :($name :: $TAny)
            vnames[i] = name
        else
            error("Unexpected signature in @primitive: $(sig)")
        end
    end
    tsig = :($fn($(targs...)))
    return tsig, fn, vnames
end

function _primitive(sig)
    @assert sig.head == :call
    p_sig, fn, vnames = primitive_sig(sig)
    ex = :($p_sig = $record!($(vnames[1]).tape, $Call, $fn, ($(vnames...),)))
    return ex
end

macro primitive(sig)
    return esc(_primitive(sig))
end

## @grad

function grad_sig(sig, idx)
    fn, args = sig.args[1], sig.args[2:end]
    targs = Array{Any}(undef, length(args))
    type_params = Symbol[]
    type_param_idx = 1
    vnames = Array{Symbol}(undef, length(args))
    for i=1:length(args)
        if args[i] isa Expr
            name, typ_ex = args[i].args
            typ = Core.eval(Base, typ_ex)
            if typ  <: AbstractArray
                T, N = Symbol("T$type_param_idx"), Symbol("N$type_param_idx")
                type_param_idx += 1
                push!(type_params, T, N)
                ttyp = Expr(:curly, TArray, T, N)
            else
                ttyp = TReal
            end
            vnames[i] = name
            targs[i] = ttyp
        elseif args[i] isa Symbol
            name = args[i]
            vnames[i] = name
            targs[i] = TAny
        else
            error("Unexpected signature in @primitive: $(sig)")
        end
    end
    grad_fn = Expr(:., Symbol(@__MODULE__), QuoteNode(:grad!))
    if isempty(type_params)
        tsig = :($grad_fn(dy::$TAny, ::Val{$idx}, op::$Call{typeof($fn), Tuple{$(targs...)}}))
    else
        tsig = :($grad_fn(dy::$TAny, ::Val{$idx}, op::$Call{typeof($fn), Tuple{$(targs...)}})
                 where {$(type_params...)})
    end
    return tsig, vnames
end

function grad_body(vnames, body)
    tbody = (body isa Expr && body.head == :block) ? body : :(begin $body end)
    init_vars_ex = :(($(vnames...),) = op.args)
    pushfirst!(tbody.args, init_vars_ex)
    return tbody
end

function _grad(sig, idx, body)
    tsig, vnames = grad_sig(sig, idx)
    tbody = grad_body(vnames, body)
    ex = :($tsig = $tbody)
    return ex
end

macro grad(sig, idx, body)
    esc(_grad(sig, idx, body))
end


struct SocketCounterRecord{N}
    # One entry in the tuple for each imc channel.
    #
    # Each entry is a dictionary keyed by counter name.
    imc_channels::NTuple{N, Dict{Symbol,Vector{Int64}}}

    # Counter results aggregated across the socket.
    aggregate_core::Dict{Symbol,Vector{Int64}}
end

# Walk all entries in the SocketCounter
function walkleaves(S::SocketCounterRecord)
    return Iterators.flatten(values.((S.aggregate_core, S.imc_channels...)))
end

# For DataTable compatibility
Slabs.datamerge(a::T, b::T) where {T <: SocketCounterRecord} = merge!(a, b)

function SocketCounterRecord{N}() where {N}
    # Create the channels tuple
    imc_channels = ntuple(i -> Dict{Symbol, Vector{Int}}(), N)
    aggregate_core = Dict{Symbol, Vector{Int64}}()
    return SocketCounterRecord{N}(
        imc_channels,
        aggregate_core,
    )
end

function retrieve(S::SocketCounterRecord{N}, s::Symbol) where {N}
    # Check if this corresponds to a Core counter.
    # If so, just return the Core counter values.
    val = get(S.aggregate_core, s, nothing)
    isnothing(val) || return val

    # Now, we have to gather up the Uncore counters.
    @assert all(x -> haskey(x, s), S.imc_channels)

    # Get all of the arrays for these counter values
    arrays = getindex.(S.imc_channels, s)
    len = minimum(length, arrays)

    # Convert into an array of tuples.
    return [ntuple(i -> arrays[i][j], N) for j in 1:len]
end

# Support for merging two SocketCounterRecords together.
function Base.merge!(a::T, b::T) where {N, T <: SocketCounterRecord{N}}
    # Recursively call merge on each of the channel counters
    for (a, b) in zip(a.imc_channels, b.imc_channels)
        merge!(a, b)
    end

    # merge the `aggregate_core counters
    merge!(a.aggregate_core, b.aggregate_core)
    return a
end

# Support the non-mutating version because it's easy.
function Base.merge(a::T, b::T) where {N, T <: SocketCounterRecord{N}}
    x = SocketCounterRecord{N}()
    merge!(x, a)
    merge!(x, b)
    return x
end

# See `pcm/types.h - lines 746-747`
perf_event(x) = (UInt(x) << 0)
perf_umask(x) = (UInt(x) << 8)

#####
##### PCM Core
#####

# By default - all our experiments happen on Socket 2 (of 2) - only gather counter values
# for CPU 0 on each socket.
#
# `numactl` makes sure that threads run on the correct cores.
default_cpu_mask() = 25:48

# Type stable wrapping for making the return value of SystemSnoop nice.
struct CoreMonitorWrapper{names}
    monitor::PCM.CoreMonitor
end

default_events() = [
    PCM.EventDescription(0x08, 0x0E, :dtlb_load_miss),
    PCM.EventDescription(0x49, 0x0E, :dtlb_store_miss),
    PCM.EventDescription(0xD0, 0x11, :stlb_load_miss),
    PCM.EventDescription(0xD0, 0x12, :stlb_store_miss),
]

function CoreMonitorWrapper(; events = default_events(), cores = default_cpu_mask())
    monitor = PCM.CoreMonitor(; cores = cores)
    PCM.program(monitor, events)
    names = ntuple(i -> events[i].name, length(events))
    return CoreMonitorWrapper{names}(monitor)
end

# SystemSnoop API
SystemSnoop.prepare(C::CoreMonitorWrapper) = PCM.sample!(C.monitor)

function SystemSnoop.measure(C::CoreMonitorWrapper{names}) where {names}
    PCM.sample!(C.monitor)
    vals = PCM.getcounters(C.monitor)
    return NamedTuple{names}(ntuple(i -> vals[i], length(names)))
end

SystemSnoop.clean(::CoreMonitorWrapper) = PCM.cleanup()

# Post processing - pop up to a couple SocketCounterWrappers
#
# Sum up all counter data across all cores.
function SystemSnoop.postprocess(
        C::CoreMonitorWrapper{names},
        data::Vector{<:NamedTuple}
    ) where {names}

    # Create an entry in a dictionary for each counter name
    datadict = Dict(name => getproperty.(data, name) for name in names)

    # NOTE: running this by default on a system with 6 memory channels.
    record = SocketCounterRecord{6}()
    merge!(record.aggregate_core, datadict)
    return (socket_1 = record,)
end

#####
##### PCM Uncore
#####

# some common events from download.01.org
"All DRAM Read CAS Commands issued (including underfills)"
cas_count_rd() = perf_event(0x4) + perf_umask(0x3)

"All DRAM Write CAS commands issued"
cas_count_wr() = perf_event(0x4) + perf_umask(0xc)

"Regular reads(RPQ) commands for Intel Optane DC persistent memory"
pmm_read_cmd() = perf_event(0xe3)

"Write commands for Intel Optane DC persistent memory"
pmm_write_cmd() = perf_event(0xe7)

"All hits to Near Memory(DRAM cache) in Memory Mode"
tagchk_hit() = perf_event(0xd3) + perf_umask(0x1)

"All Clean line misses to Near Memory(DRAM cache) in Memory Mode"
tagchk_miss_clean() = perf_event(0xd3) + perf_umask(0x2)

"All dirty line misses to Near Memory(DRAM cache) in Memory Mode"
tagchk_miss_dirty() = perf_event(0xd3) + perf_umask(0x4)

"All commands for Intel Optane DC persistent memory"
all_pmm_cmd() = perf_event(0xea) + perf_umask(0x1)

"Clockticks of the memory controller which uses a programmable counter"
unc_clocks() = perf_event(0x0) + perf_umask(0x0)

"Read Pending Queue Occupancy of all read requests for Intel Optane DC persistent memory"
pmm_rq() = perf_event(0xe0) + perf_umask(0x1)

"Write Pending Queue Occupancy of all write requests for Intel Optane DC persistent memory"
pmm_wq() = perf_event(0xe4) + perf_umask(0x1)

dram_rq() = perf_event(0x80) + perf_umask(0x0)
dram_wq() = perf_event(0x81) + perf_umask(0x0)

"Underfill read commands for Intel Optane DC persistent memory"
pmm_underfill_rd() = perf_event(0xea) + perf_umask(0x8)

"Read requests allocated in the PMM Read Pending Queue for Intel Optane DC persistent memory"
pmm_read_insert() = perf_event(0xe3) + perf_umask(0x0)

"Write requests allocated in the PMM Write Pending Queue for Intel Optane DC persistent memory"
pmm_write_insert() = perf_event(0xe7) + perf_umask(0x0)

llc_data_read() = perf_event(0x34) + perf_umask(0x3)
llc_data_write() = perf_event(0x34) + perf_umask(0x5)

makevar(i) = "PCM_COUNTER_$(i-1)"
function setvars(nt::NamedTuple)
    # Iterate through the named tuple - programming!
    for (i, v) in enumerate(nt)
        ENV[makevar(i)] = string(v, base = 16)
    end
end

"""
* `NS`: Number of Sockets
* `NIMC`: Number of Integrated Memory Controllers per Socket
* `NCH`: Number of Memory Channels per Socket
"""
struct Uncore{NS, NIMC, NCH, names}
    monitor::PCM.UncoreMemoryMonitor
end

# Instantiate the controller and start the counters
function Uncore{NS, NIMC, NCH}(counters::NamedTuple{names}) where {NS, NIMC, NCH, names}
    PCM.cleanup()

    # program the counters
    setvars(counters)

    monitor = PCM.UncoreMemoryMonitor(NIMC, NCH)
    return Uncore{NS, NIMC, NCH, names}(monitor)
end

# Return type from measurements for a single socket
const _NT{NCH} = NTuple{NCH,Int}

function SystemSnoop.prepare(U::Uncore{NS, NIMC, NCH}, kw) where {NS, NIMC, NCH}
    # Sample once to clear running counters
    PCM.sample!(U.monitor)
end

function SystemSnoop.measure(U::Uncore{NS, NIMC, NCH, names}, kw) where {NS, NIMC, NCH,names}
    PCM.sample!(U.monitor)
    return map(Tuple(1:NS)) do idx
        # Subtract 1 to get the socket
        socket = idx - 1
        return NamedTuple{names}(
            ntuple(i -> Tuple(PCM.getcounter(U.monitor, i-1, socket)), 4)
        )
    end
end

SystemSnoop.clean(U::Uncore) = PCM.cleanup()

# Turn our crazy nested tuple thing into something actually somewhat usable.
function SystemSnoop.postprocess(
        U::Uncore{NS, NIMC, NCH, names},
        data
    ) where {NS, NIMC, NCH, names}

    records = map(1:NS) do socket
        # Create a SocketCounterRecord for this socket.
        record = SocketCounterRecord{NCH}()
        for channel in 1:NCH
            # Get the data dictionary from this entry in the `imc_channels` field.
            channel_counters = record.imc_channels[channel]

            # Transform the socket data into the dicrionary form.
            socket_data = data[socket]
            for name in names
                reformatted_counters = [getproperty(x[socket], name)[channel] for x in data]
                channel_counters[name] = reformatted_counters
            end
        end
        return record
    end

    # Create names for the socket counters
    socket_names = ntuple(i -> Symbol("socket_$(i-1)"), NS)
    return NamedTuple{socket_names}(Tuple(records))
end

const DEFAULT_NT = (
    dram_reads  = cas_count_rd(),
    dram_writes = cas_count_wr(),
    pmm_reads   = pmm_read_cmd(),
    pmm_writes  = pmm_write_cmd(),
)


module ObjectDetectionStats
    using DataFrames
    include("LocalizationMeasures.jl")
    export Box, ==, barycenterbox, LLURtoULLR, translate, translate!,
        clamp, clamp!, area, intersection_area, intersection_over_union

    include("LocalizationStatistics.jl")
    export HotClassLocalization, ColdClassLocalization, ObjectDetectionScore,
        add!, micro_precision, micro_recall, micro_F1, macro_precision,
        macro_recall, macro_F1, classwise_PR_estimate
end # module


import Base: isvalid

#### Boundary Conditions ####

abstract BoundaryCondition
type BCnil <: BoundaryCondition; end # ignore edges (error when invalid)
type BCnan <: BoundaryCondition; end  # NaN when over the edge
type BCna <: BoundaryCondition; end  # Normalize to available data (NaN when none)
type BCreflect <: BoundaryCondition; end # Reflecting boundary conditions
type BCperiodic <: BoundaryCondition; end # Periodic boundary conditions
type BCnearest <: BoundaryCondition; end # Return closest edge element
type BCfill <: BoundaryCondition; end # Use specified fill value

# Note: for interpolation, BCna is currently defined to be identical
# to BCnan. Other applications might define different behavior,
# particularly for filtering operations.

#needs_validating{BC<:BoundaryCondition}(::Type{BC}) = false
#needs_validating{BC<:Union(BCnil,BCnan,BCna)}(::Type{BC}) = true
isvalid{BC<:BoundaryCondition}(::Type{BC}, pos::Int, min::Int, max::Int) = true
isvalid{BC<:Union(BCnan,BCna)}(::Type{BC}, pos::Int, min::Int, max::Int) = min <= pos <= max
wrap{BC<:BoundaryCondition}(::Type{BC}, pos::Int, len::Int) = pos
wrap(::Type{BCreflect}, pos::Int, len::Int) = wraprefl(mod(pos-1, 2*len), len)
wraprefl(posrem::Int, len::Int) = posrem < len ? posrem+1 : 2*len-posrem
wrap(::Type{BCperiodic}, pos::Int, len::Int) = mod(pos-1, len) + 1
wrap{BC<:Union(BCnearest,BCfill)}(::Type{BC}, pos::Int, len::Int) = pos < 1 ? 1 : (pos > len ? len : pos)


abstract Classical2dLattice{T} <: ClassicalLattice{T}

function getSpaceDimension(lattice::Classical2dLattice)
	return 2
end


##############################################################################
##
## PooledDataArray type definition
##
## An AbstractDataArray with efficient storage when values are repeated. A
## PDA wraps an array of unsigned integers, which are used to index into a
## compressed pool of values. NA's are 0's in the refs array.  The unsigned
## integer type used for the refs array defaults to UInt32.  The `compact`
## function converts to a smallest integer size that will index the entire pool.
##
## TODO: Allow ordering of factor levels
## TODO: Add metadata for dummy conversion
##
##############################################################################

# This is used as a wrapper during PooledDataArray construction only, to distinguish
# arrays of pool indices from normal arrays
type RefArray{R<:Integer,N}
    a::Array{R,N}
end

type PooledDataArray{T, R<:Integer, N} <: AbstractDataArray{T, N}
    refs::Array{R, N}
    pool::Vector{T}

    function PooledDataArray(rs::RefArray{R, N},
                             p::Vector{T})
        # refs mustn't overflow pool
        if length(rs.a) > 0 && maximum(rs.a) > prod(size(p))
            throw(ArgumentError("Reference array points beyond the end of the pool"))
        end
        new(rs.a,p)
    end
end
typealias PooledDataVector{T,R} PooledDataArray{T,R,1}
typealias PooledDataMatrix{T,R} PooledDataArray{T,R,2}

##############################################################################
##
## PooledDataArray constructors
##
# How do you construct a PooledDataArray from an Array?
# From the same sigs as a DataArray!
#
# Algorithm:
# * Start with:
#   * A null pool
#   * A pre-allocated refs
#   * A hash from T to Int
# * Iterate over d
#   * If value of d in pool already, set the refs accordingly
#   * If value is new, add it to the pool, then set refs
##############################################################################

# Echo inner constructor as an outer constructor
function PooledDataArray{T,R<:Integer,N}(refs::RefArray{R, N},
                                         pool::Vector{T})
    PooledDataArray{T,R,N}(refs, pool)
end

# A no-op constructor
PooledDataArray(d::PooledDataArray) = d

# Constructor from array, w/ pool, missingness, and ref type
function PooledDataArray{T,R<:Integer,N}(d::AbstractArray{T, N},
                                         pool::Vector{T},
                                         m::AbstractArray{Bool, N},
                                         r::Type{R} = DEFAULT_POOLED_REF_TYPE)
    if length(pool) > typemax(R)
        throw(ArgumentError("Cannot construct a PooledDataVector with type $R with a pool of size $(length(pool))"))
    end

    newrefs = Array(R, size(d))
    poolref = Dict{T, R}()

    # loop through once to fill the poolref dict
    for i = 1:length(pool)
        poolref[pool[i]] = i
    end

    # fill in newrefs
    for i = 1:length(d)
        if m[i]
            newrefs[i] = 0
        else
            newrefs[i] = get(poolref, d[i], 0)
        end
    end
    return PooledDataArray(RefArray(newrefs), pool)
end

# Constructor from array, w/ missingness and ref type
function PooledDataArray{T,R<:Integer,N}(d::AbstractArray{T, N},
                                         m::AbstractArray{Bool, N},
                                         r::Type{R} = DEFAULT_POOLED_REF_TYPE)
    pool = convert(Array, unique(d[!m]))
    if method_exists(isless, (T, T))
        sort!(pool)
    end
    PooledDataArray(d, pool, m, r)
end

# Construct an all-NA PooledDataVector of a specific type
PooledDataArray(t::Type, dims::@compat Tuple{Vararg{Int}}) = PooledDataArray(Array(t, dims), trues(dims))
PooledDataArray(t::Type, dims::Int...) = PooledDataArray(Array(t, dims), trues(dims))
PooledDataArray{R<:Integer}(t::Type, r::Type{R}, dims::@compat Tuple{Vararg{Int}}) = PooledDataArray(Array(t, dims), trues(dims), r)
PooledDataArray{R<:Integer}(t::Type, r::Type{R}, dims::Int...) = PooledDataArray(Array(t, dims), trues(dims), r)

# Construct an empty PooledDataVector of a specific type
PooledDataArray(t::Type) = PooledDataArray(similar(Array(t,1),0), trues(0))
PooledDataArray{R<:Integer}(t::Type, r::Type{R}) = PooledDataArray(similar(Array(t,1),0), trues(0), r)

# Convert a BitArray to an Array{Bool} (m = missingness)
# For some reason an additional method is needed but even that doesn't work
# For a BitArray a refs type of UInt8 will always be sufficient as the size of the pool is 0, 1 or 2
PooledDataArray{N}(d::BitArray{N}) = PooledDataArray(Array(d), falses(size(d)), UInt8)
PooledDataArray{N}(d::BitArray{N}, m::AbstractArray{Bool, N}) = PooledDataArray(Array(d), m, UInt8)

# Convert a DataArray to a PooledDataArray
PooledDataArray{T,R<:Integer}(da::DataArray{T},
                              r::Type{R} = DEFAULT_POOLED_REF_TYPE) = PooledDataArray(da.data, da.na, r)
PooledDataArray{T,R<:Integer}(da::DataArray{T},
                              pool::Vector{T},
                              r::Type{R} = DEFAULT_POOLED_REF_TYPE) = PooledDataArray(da.data, pool, da.na, r)

# Convert a Array{T} to a PooledDataArray
PooledDataArray{T,R<:Integer}(d::Array{T},
                              r::Type{R} = DEFAULT_POOLED_REF_TYPE) = PooledDataArray(d, falses(size(d)), r)
PooledDataArray{T,R<:Integer}(d::Array{T},
                              pool::Vector{T},
                              r::Type{R} = DEFAULT_POOLED_REF_TYPE) = PooledDataArray(d, pool, falses(size(d)), r)

# Explicitly convert Ranges into a PooledDataVector
PooledDataArray{R<:Integer}(rs::Range,
                            r::Type{R} = DEFAULT_POOLED_REF_TYPE) = PooledDataArray(collect(rs), falses(length(rs)), r)

# Initialized constructors with 0's, 1's
for (f, basef) in ((:pdatazeros, :zeros), (:pdataones, :ones))
    @eval begin
        ($f)(dims::Int...) = PooledDataArray(($basef)(dims...), falses(dims...))
        ($f)(t::Type, dims::Int...) = PooledDataArray(($basef)(t, dims...), falses(dims...))
        ($f){R<:Integer}(t::Type, r::Type{R}, dims::Int...) = PooledDataArray(($basef)(t, dims...), falses(dims...), r)
    end
end

##############################################################################
##
## Basic size properties of all Data* objects
##
##############################################################################

Base.size(pda::PooledDataArray) = size(pda.refs)
Base.length(pda::PooledDataArray) = length(pda.refs)
Base.endof(pda::PooledDataArray) = endof(pda.refs)

##############################################################################
##
## Copying Data* objects
##
##############################################################################

Base.copy(pda::PooledDataArray) = PooledDataArray(RefArray(copy(pda.refs)),
                                                  copy(pda.pool))
# TODO: Implement copy_to()

function Base.resize!{T,R}(pda::PooledDataArray{T,R,1}, n::Int)
    oldn = length(pda.refs)
    resize!(pda.refs, n)
    pda.refs[oldn+1:n] = zero(R)
    pda
end

##############################################################################
##
## Predicates, including the new isna()
##
##############################################################################

function Base.isnan(pda::PooledDataArray)
    PooledDataArray(RefArray(copy(pda.refs)), isnan(pda.pool))
end

function Base.isfinite(pda::PooledDataArray)
    PooledDataArray(RefArray(copy(pda.refs)), isfinite(pda.pool))
end

isna(pda::PooledDataArray) = pda.refs .== 0

#' @description
#'
#' Safe and type-stable way to determine if element `i` of an
#' PooledDataArray is `NA`.
#'
#' @param pda::PooledDataArray The PooledDataArray whose missingness will
#'        be assessed.
#' @param i::Integer The index of the element to be checked for `NA`.
#'
#' @returns na::Bool Is the element `NA` or not?
#'
#' @examples
#'
#' a = @pdata([1, 2, 3])
#' isna(a, 1)
isna(pda::PooledDataArray, i::Real) = pda.refs[i] == 0 # -> Bool

#' @description
#'
#' Determine if any of the entries of an PooledDataArray are `NA`.
#'
#' @param pda::PooledDataArray The PooledDataArray whose elements will
#'        be assessed.
#'
#' @returns out::Bool Are any of the elements of `pda` an `NA` value?
#'
#' @examples
#'
#' pda = @pdata([1, 2, 3])
#' anyna(pda)
function anyna(pda::PooledDataArray)
    for ref in pda.refs
        ref == 0 && return true
    end
    return false
end

#' @description
#'
#' Determine if all of the entries of an PooledDataArray are `NA`.
#'
#' @param pda::PooledDataArray The PooledDataArray whose elements will
#'        be assessed.
#'
#' @returns out::Bool Are all of the elements of `pda` an `NA` value?
#'
#' @examples
#'
#' pda = @pdata([1, 2, 3])
#' allna(pda)
function allna(pda::PooledDataArray)
    for ref in pda.refs
        ref == 0 || return false
    end
    return true
end

##############################################################################
##
## PooledDataArray utilities
##
## TODO: Add methods with these names for DataArray's
##
##############################################################################

function compact{T,R<:Integer,N}(d::PooledDataArray{T,R,N})
    sz = length(d.pool)

    REFTYPE = sz <= typemax(UInt8)  ? UInt8 :
              sz <= typemax(UInt16) ? UInt16 :
              sz <= typemax(UInt32) ? UInt32 :
                                      UInt64

    if REFTYPE == R
        return d
    end

    newrefs = convert(Array{REFTYPE, N}, d.refs)
    PooledDataArray(RefArray(newrefs), d.pool)
end

#' @description
#'
#' Return a DataVector containing the unique values of a `PooledDataArray`,
#' in the order they appear in the data, including `NA` if any missing entries
#' are encountered. For `PooledDataArray`s, this function is much less efficient
#' than `levels`, which does not return the values in the same order.
#'
#' @param da::DataArray{T} `DataArray` whose unique values are desired.
#'
#' @returns dv::DataVector{T} `DataVector` containing the unique values
#'          from `pda`, in the order they appear, including `NA` if there are
#'          any missing entries in `pda`.
#'
#' @examples
#'
#' pdv = @pdata [1, -2, 1, NA, 4]
#' distinct_values = unique(pdv)
function Base.unique{T}(pda::PooledDataArray{T})
    n = length(pda)
    nlevels = length(pda.pool)
    unique_values = Array(T, 0)
    sizehint!(unique_values, nlevels)
    seen = Set{eltype(pda.refs)}()

    firstna = 0
    for i in 1:n
        if isna(pda, i)
            if firstna == 0
                firstna = length(unique_values) + 1
            end
        elseif !in(pda.refs[i], seen)
            push!(seen, pda.refs[i])
            push!(unique_values, pda.pool[pda.refs[i]])
        else
            continue
        end

        if firstna > 0 && length(unique_values) == nlevels
            break
        end
    end

    if firstna > 0
        res = DataArray(Array(T, nlevels + 1))
        i = 0
        for val in unique_values
            i += 1
            if i == firstna
                res.na[i] = true
                i += 1
            end
            res.data[i] = val
        end

        if firstna == nlevels + 1
            res.na[nlevels + 1] = true
        end

        return res
    else
        return DataArray(unique_values)
    end
end

#' @description
#'
#' Return a DataVector containing the levels of a `PooledDataArray`,
#' excluding `NA`. For `PooledDataArray`s, this function is much more
#' efficient than `unique`, and it returns the levels in the order used
#' when calling `setlevels!` rather than in the order of appearance.
#' Contrary to `unique`, it also returns levels which are not present in
#' the vector.
#'
#'
#' @param pda::PooledDataArray{T} PooledDataArray whose levels values are
#' desired.
#'
#' @returns dv::DataVector{T} DataVector containing the levels
#'          of `da`, excluding `NA`.
#'
#' @examples
#'
#' pdv = @pdata [1, -2, 1, NA, 4]
#' distinct_values = levels(pdv)
levels{T}(pda::PooledDataArray{T}) = copy(pda.pool)

function PooledDataArray{S,R,N}(x::PooledDataArray{S,R,N},
                                newpool::Vector{S})
    # QUESTION: should we have a ! version of this? If so, needs renaming?
    tidx::Array{R} = findat(newpool, x.pool)
    refs = zeros(R, length(x))
    for i in 1:length(refs)
        if x.refs[i] != 0
            refs[i] = tidx[x.refs[i]]
        end
    end
    PooledDataArray(RefArray(refs), newpool)
end

myunique(x::AbstractVector) = x[sort(unique(findat(x, x)))]  # gets the ordering right
myunique(x::AbstractDataVector) = myunique(dropna(x))   # gets the ordering right; removes NAs

function setlevels{T,R}(x::PooledDataArray{T,R}, newpool::AbstractVector)
    pool = myunique(newpool)
    refs = zeros(R, length(x))
    tidx::Array{R} = findat(pool, newpool)
    tidx[isna(newpool)] = 0
    for i in 1:length(refs)
        if x.refs[i] != 0
            refs[i] = tidx[x.refs[i]]
        end
    end
    return PooledDataArray(RefArray(refs), pool)
end

function setlevels!{T,R}(x::PooledDataArray{T,R}, newpool::AbstractVector{T})
    if newpool == myunique(newpool) # no NAs or duplicates
        x.pool = newpool
        return x
    else
        x.pool = myunique(newpool)
        tidx::Array{R} = findat(x.pool, newpool)
        tidx[isna(newpool)] = 0
        for i in 1:length(x.refs)
            if x.refs[i] != 0
                x.refs[i] = tidx[x.refs[i]]
            end
        end
        return x
    end
end

setlevels!{T, R}(x::PooledDataArray{T, R},
                 newpool::AbstractVector) = setlevels!(x, convert(Array{T}, newpool))

function setlevels(x::PooledDataArray, d::Dict)
    newpool = copy(DataArray(x.pool))
    # An NA in `v` is put in the pool; that will cause it to become NA
    for (k,v) in d
        idx = findin(newpool, [k])
        if length(idx) == 1
            newpool[idx[1]] = v
        end
    end
    setlevels(x, newpool)
end

function setlevels!{T,R}(x::PooledDataArray{T,R}, d::Dict{T,T})
    for (k,v) in d
        idx = findin(x.pool, [k])
        if length(idx) == 1
            x.pool[idx[1]] = v
        end
    end
    x
end

function setlevels!{T,R}(x::PooledDataArray{T,R}, d::Dict{T,Any}) # this version handles NAs in d's values
    newpool = copy(DataArray(x.pool))
    # An NA in `v` is put in the pool; that will cause it to become NA
    for (k,v) in d
        idx = findin(newpool, [k])
        if length(idx) == 1
            newpool[idx[1]] = v
        end
    end
    setlevels!(x, newpool)
end

reorder(x::PooledDataArray) = PooledDataArray(x, sort(levels(x)))  # just re-sort the pool

reorder(x::PooledDataArray, y::AbstractVector...) = reorder(mean, x, y...)

### FIXME: this can't work because we don't know about DataFrames
# reorder(fun::Function, x::PooledDataArray, y::AbstractVector...) =
#     reorder(fun, x, DataFrame({y...}))

Base.reverse(x::PooledDataArray) = PooledDataArray(RefArray(reverse(x.refs)), x.pool)

function Base.permute!!{T<:Integer}(x::PooledDataArray, p::AbstractVector{T})
    Base.permute!!(x.refs, p)
    x
end

function Base.ipermute!!{T<:Integer}(x::PooledDataArray, p::AbstractVector{T})
    Base.ipermute!!(x.refs, p)
    x
end

##############################################################################
##
## similar()
##
##############################################################################

function Base.similar{T,R}(pda::PooledDataArray{T,R}, S::Type, dims::Dims)
    PooledDataArray(RefArray(zeros(R, dims)), S[])
end

##############################################################################
##
## find()
##
##############################################################################

Base.find(pdv::PooledDataVector{Bool}) = find(convert(Vector{Bool}, pdv, false))

##############################################################################
##
## setindex!() definitions
##
##############################################################################

function getpoolidx{T,R}(pda::PooledDataArray{T,R}, val::Any)
    val::T = convert(T,val)
    pool_idx = findfirst(pda.pool, val)
    if pool_idx <= 0
        push!(pda.pool, val)
        pool_idx = length(pda.pool)
        if pool_idx > typemax(R)
            throw(ArgumentError(
                "You're using a PooledDataArray with ref type $R,\n" *
                "which can only hold $(Int(typemax(R))) values, and you just tried to add the $(Int(typemax(R))+1)th\n" *
                "reference. Please change the ref type to a larger int type, or use the\n" *
                "default ref type ($DEFAULT_POOLED_REF_TYPE)."
            ))
        end
    end
    return pool_idx
end

getpoolidx{T,R}(pda::PooledDataArray{T,R}, val::NAtype) = zero(R)

##############################################################################
##
## show() and similar methods
##
##############################################################################

function Base.string(x::PooledDataVector)
    tmp = join(x, ", ")
    return "[$tmp]"
end

# # Need setindex!()'s to make this work
# This is broken now because the inner show returns to the outer show.
# function show(io::IO, pda::PooledDataArray)
#     invoke(show, Tuple{Any,AbstractArray}, io, pda)
#     print(io, "\nlevels: ")
#     print(io, levels(pda))
# end

##############################################################################
##
## Replacement operations
##
##############################################################################

function replace!(x::PooledDataArray{NAtype}, fromval::NAtype, toval::NAtype)
    NA # no-op to deal with warning
end
function replace!(x::PooledDataArray, fromval::NAtype, toval::NAtype)
    NA # no-op to deal with warning
end
function replace!{S, T}(x::PooledDataArray{S}, fromval::T, toval::NAtype)
    fromidx = findfirst(x.pool, fromval)
    if fromidx == 0
        throw(ErrorException("can't replace a value not in the pool in a PooledDataVector!"))
    end

    x.refs[x.refs .== fromidx] = 0

    return NA
end
function replace!{S, T}(x::PooledDataArray{S}, fromval::NAtype, toval::T)
    toidx = findfirst(x.pool, toval)
    # if toval is in the pool, just do the assignment
    if toidx != 0
        x.refs[x.refs .== 0] = toidx
    else
        # otherwise, toval is new, add it to the pool
        push!(x.pool, toval)
        x.refs[x.refs .== 0] = length(x.pool)
    end

    return toval
end
function replace!{R, S, T}(x::PooledDataArray{R}, fromval::S, toval::T)
    # throw error if fromval isn't in the pool
    fromidx = findfirst(x.pool, fromval)
    if fromidx == 0
        throw(ErrorException("can't replace a value not in the pool in a PooledDataArray!"))
    end

    # if toval is in the pool too, use that and remove fromval from the pool
    toidx = findfirst(x.pool, toval)
    if toidx != 0
        x.refs[x.refs .== fromidx] = toidx
        #x.pool[fromidx] = None    TODO: what to do here??
    else
        # otherwise, toval is new, swap it in
        x.pool[fromidx] = toval
    end

    return toval
end

##############################################################################
##
## Sorting can use the pool to speed things up
##
##############################################################################

function Base.sortperm(pda::PooledDataArray; alg::Base.Sort.Algorithm=Base.Sort.DEFAULT_UNSTABLE,
                       lt::Function=isless, by::Function=identity,
                       rev::Bool=false, order=Base.Sort.Forward)
    order = Base.ord(lt, by, rev, order)

    # TODO handle custom ordering efficiently
    if !isa(order, Base.Order.ForwardOrdering) && !isa(order, Base.Order.ReverseOrdering)
        return sort!(collect(1:length(pda)), alg, Base.Order.Perm(order,pda))
    end

    # TODO handle non-sorted keys without copying
    perm = issorted(pda.pool) ? groupsort_indexer(pda, true)[1] : sortperm(reorder(pda))
    isa(order, Base.Order.ReverseOrdering) && reverse!(perm)
    perm
end

Base.sort(pda::PooledDataArray; kw...) = pda[sortperm(pda; kw...)]

type FastPerm{O<:Base.Sort.Ordering,V<:AbstractVector} <: Base.Sort.Ordering
    ord::O
    vec::V
end
Base.sortperm{V}(x::AbstractVector, a::Base.Sort.Algorithm, o::FastPerm{Base.Sort.ForwardOrdering,V}) = x[sortperm(o.vec)]
Base.sortperm{V}(x::AbstractVector, a::Base.Sort.Algorithm, o::FastPerm{Base.Sort.ReverseOrdering,V}) = x[reverse(sortperm(o.vec))]
Perm{O<:Base.Sort.Ordering}(o::O, v::PooledDataVector) = FastPerm(o, v)

##############################################################################
##
## PooledDataVecs: EXPLANATION SHOULD GO HERE
##
##############################################################################

function PooledDataVecs{S,Q<:Integer,R<:Integer,N}(v1::PooledDataArray{S,Q,N},
                                                   v2::PooledDataArray{S,R,N})
    pool = sort(unique([v1.pool; v2.pool]))
    sz = length(pool)

    REFTYPE = sz <= typemax(UInt8)  ? UInt8 :
              sz <= typemax(UInt16) ? UInt16 :
              sz <= typemax(UInt32) ? UInt32 :
                                      UInt64

    tidx1 = convert(Vector{REFTYPE}, findat(pool, v1.pool))
    tidx2 = convert(Vector{REFTYPE}, findat(pool, v2.pool))
    refs1 = zeros(REFTYPE, length(v1))
    refs2 = zeros(REFTYPE, length(v2))
    for i in 1:length(refs1)
        if v1.refs[i] != 0
            refs1[i] = tidx1[v1.refs[i]]
        end
    end
    for i in 1:length(refs2)
        if v2.refs[i] != 0
            refs2[i] = tidx2[v2.refs[i]]
        end
    end
    return (PooledDataArray(RefArray(refs1), pool),
            PooledDataArray(RefArray(refs2), pool))
end

function PooledDataVecs{S,R<:Integer,N}(v1::PooledDataArray{S,R,N},
                                        v2::AbstractArray{S,N})
    return PooledDataVecs(v1,
                          PooledDataArray(v2))
end

####
function PooledDataVecs{S,R<:Integer,N}(v1::AbstractArray{S,N},
                                        v2::PooledDataArray{S,R,N})
    return PooledDataVecs(PooledDataArray(v1),
                          v2)
end

function PooledDataVecs(v1::AbstractArray,
                        v2::AbstractArray)

    ## Return two PooledDataVecs that share the same pool.

    ## TODO: allow specification of REFTYPE
    refs1 = Array(DEFAULT_POOLED_REF_TYPE, size(v1))
    refs2 = Array(DEFAULT_POOLED_REF_TYPE, size(v2))
    poolref = Dict{promote_type(eltype(v1), eltype(v2)), DEFAULT_POOLED_REF_TYPE}()
    maxref = 0

    # loop through once to fill the poolref dict
    for i = 1:length(v1)
        if !isna(v1[i])
            poolref[v1[i]] = 0
        end
    end
    for i = 1:length(v2)
        if !isna(v2[i])
            poolref[v2[i]] = 0
        end
    end

    # fill positions in poolref
    pool = sort(collect(keys(poolref)))
    i = 1
    for p in pool
        poolref[p] = i
        i += 1
    end

    # fill in newrefs
    zeroval = zero(DEFAULT_POOLED_REF_TYPE)
    for i = 1:length(v1)
        if isna(v1[i])
            refs1[i] = zeroval
        else
            refs1[i] = poolref[v1[i]]
        end
    end
    for i = 1:length(v2)
        if isna(v2[i])
            refs2[i] = zeroval
        else
            refs2[i] = poolref[v2[i]]
        end
    end

    return (PooledDataArray(RefArray(refs1), pool),
            PooledDataArray(RefArray(refs2), pool))
end

Base.convert{S,T,R1<:Integer,R2<:Integer,N}(::Type{PooledDataArray{S,R1,N}}, pda::PooledDataArray{T,R2,N}) =
    PooledDataArray(RefArray(convert(Array{R1,N}, pda.refs)), convert(Vector{S}, pda.pool))
Base.convert{S,T,R<:Integer,N}(::Type{PooledDataArray{S,R,N}}, pda::PooledDataArray{T,R,N}) =
    PooledDataArray(RefArray(copy(pda.refs)), convert(Vector{S}, pda.pool))
Base.convert{T,R<:Integer,N}(::Type{PooledDataArray{T,R,N}}, pda::PooledDataArray{T,R,N}) = pda
Base.convert{S,T,R1<:Integer,R2<:Integer,N}(::Type{PooledDataArray{S,R1}}, pda::PooledDataArray{T,R2,N}) =
    convert(PooledDataArray{S,R1,N}, pda)
Base.convert{S,T,R<:Integer,N}(::Type{PooledDataArray{S}}, pda::PooledDataArray{T,R,N}) =
    convert(PooledDataArray{S,R,N}, pda)
Base.convert{T,R<:Integer,N}(::Type{PooledDataArray}, pda::PooledDataArray{T,R,N}) = pda

Base.convert{T,R<:Integer,N}(::Type{PooledDataArray{T,R,N}}, a::DataArray{T,N}) =
    PooledDataArray(a.data, a.na, R)
Base.convert{S,T,R<:Integer,N}(::Type{PooledDataArray{S,R,N}}, a::DataArray{T,N}) =
    convert(PooledDataArray{S,R,N}, PooledDataArray(a.data, a.na, R))
Base.convert{S,T,R<:Integer,N}(::Type{PooledDataArray{S,R}}, a::DataArray{T,N}) =
    convert(PooledDataArray{S,R,N}, PooledDataArray(a.data, a.na, R))
Base.convert{S,T,N}(::Type{PooledDataArray{S}}, a::DataArray{T,N}) =
    convert(PooledDataArray{S}, PooledDataArray(a.data, a.na))
Base.convert(::Type{PooledDataArray}, a::DataArray) =
    PooledDataArray(a.data, a.na)

Base.convert{S,T,R<:Integer,N}(::Type{PooledDataArray{S,R,N}}, a::AbstractArray{T,N}) =
    PooledDataArray(convert(Array{S,N}, a), falses(size(a)), R)
Base.convert{S,T,R<:Integer,N}(::Type{PooledDataArray{S,R}}, a::AbstractArray{T,N}) =
    PooledDataArray(convert(Array{S,N}, a), falses(size(a)), R)
Base.convert{S,T,N}(::Type{PooledDataArray{S}}, a::AbstractArray{T,N}) =
    PooledDataArray(convert(Array{S,N}, a), falses(size(a)))
Base.convert(::Type{PooledDataArray}, a::AbstractArray) =
    PooledDataArray(a, falses(size(a)))

function Base.convert{S,T,R<:Integer,N}(::Type{DataArray{S,N}},
                                        pda::PooledDataArray{T,R,N})
    res = DataArray(Array(S, size(pda)), BitArray(size(pda)))
    for i in 1:length(pda)
        r = pda.refs[i]
        if r == 0 # TODO: Use zero(R)
            res.na[i] = true
        else
            res.na[i] = false
            res.data[i] = pda.pool[r]
        end
    end
    return res
end
Base.convert{S,T,R<:Integer,N}(::Type{DataArray{S}}, pda::PooledDataArray{T,R,N}) =
    convert(DataArray{S,N}, pda)
Base.convert{T,R<:Integer,N}(::Type{DataArray}, pda::PooledDataArray{T,R,N}) =
    convert(DataArray{T,N}, pda)

pdata(a::AbstractArray) = convert(PooledDataArray, a)

function Base.convert{S, T, R, N}(
    ::Type{Array{S, N}},
    pda::PooledDataArray{T, R, N}
)
    res = Array(S, size(pda))
    for i in 1:length(pda)
        if pda.refs[i] == zero(R)
            throw(NAException())
        else
            res[i] = pda.pool[pda.refs[i]]
        end
    end
    return res
end

function Base.convert{T, R}(::Type{Vector}, pdv::PooledDataVector{T, R})
    return convert(Array{T, 1}, pdv)
end

function Base.convert{T, R}(::Type{Matrix}, pdm::PooledDataMatrix{T, R})
    return convert(Array{T, 2}, pdm)
end

function Base.convert{T, R, N}(::Type{Array}, pda::PooledDataArray{T, R, N})
    return convert(Array{T, N}, pda)
end

function Base.convert{S, T, R, N}(
    ::Type{Array{S, N}},
    pda::PooledDataArray{T, R, N},
    replacement::Any
)
    res = Array(S, size(pda))
    replacementS = convert(S, replacement)
    for i in 1:length(pda)
        if pda.refs[i] == zero(R)
            res[i] = replacementS
        else
            res[i] = pda.pool[pda.refs[i]]
        end
    end
    return res
end

function Base.convert{T, R}(::Type{Vector}, pdv::PooledDataVector{T, R}, replacement::Any)
    return convert(Array{T, 1}, pdv, replacement)
end

function Base.convert{T, R}(::Type{Matrix}, pdm::PooledDataMatrix{T, R}, replacement::Any)
    return convert(Array{T, 2}, pdm, replacement)
end

function Base.convert{T, R, N}(::Type{Array}, pda::PooledDataArray{T, R, N}, replacement::Any)
    return convert(Array{T, N}, pda, replacement)
end

function dropna{T}(pdv::PooledDataVector{T})
    n = length(pdv)
    res = Array(T, n)
    total = 0
    for i in 1:n
        if pdv.refs[i] > 0
            total += 1
            res[total] = pdv.pool[pdv.refs[i]]
        end
    end
    resize!(res, total)
    return res
end


"""
    default_printer(v)

Prints v rounded to three digits.  Here, `v` can be of any type accepted by `round`, which includes Real, Complex and many others.  To use your own custom datatype it is sufficient to define Base.round(x::NewType, r::RoundingMode).
"""
default_printer(v) = string(round(v, sigdigits=3))

"""
    sig_printer(v::Real)

Prints the first three significant digits of `v` in scientific notation.
```jldoctest
julia> -5:5 .|> exp .|> sig_printer
11-element Array{String,1}:
 "6.74e-03"
 "1.83e-02"
 "4.98e-02"
 "1.35e-01"
 "3.68e-01"
 "1.00e+00"
 "2.72e+00"
 "7.39e+00"
 "2.01e+01"
 "5.46e+01"
 "1.48e+02"
```
"""
sig_printer(v::Real) = @sprintf "%0.2e" v

@recipe(Slider) do scene
    Theme(
        value = 0,
        start = automatic,
        position = (0, 0),
        sliderlength = 200,
        sliderheight = 50,
        backgroundcolor = (:gray, 0.01),
        strokecolor = (:black, 0.4),
        strokewidth = 1,
        textcolor = :black,
        slidercolor = (:gray, 0.6),
        buttoncolor = :white,
        buttonsize = 15,
        buttonstroke = 1.5,
        textsize = 15,
        buttonstrokecolor = :black,
        valueprinter = default_printer
    )
end

convert_arguments(::Type{<: Slider}, x::AbstractRange) = (x,)

function range_label_bb(tplot, printer_func, range)
    bb = boundingbox(tplot, printer_func(first(range)))
    for elem in Iterators.drop(range, 1)
        bb = union(bb, boundingbox(tplot, printer_func(elem)))
    end
    bb
end

function find_closest(iter, val)
    last = first(iter)
    for (i, elem) in enumerate(iter)
        elem === val && return i
        last <= val && elem >= val && return i
        last = elem
    end
    error("$val isn't contained in $iter")
end

function plot!(slider::Slider)
    @extract(slider, (
        backgroundcolor, strokecolor, strokewidth, slidercolor, buttonstroke,
        buttonstrokecolor, buttonsize, buttoncolor, valueprinter,
        sliderlength, sliderheight, textcolor, textsize, position, start
    ))
    range = slider[1]
    val = slider[:value]
    p2f0 = lift(Point2f0, position)
    startval = start[] === automatic ? first(range[]) : start[]
    push!(val, startval)
    label = lift((v, f)-> f(v), val, valueprinter)
    lplot = text!(
        slider, label,
        textsize = textsize,
        align = (:left, :center), color = textcolor,
        position = lift((w, h, p)-> p .+ Point2f0(w, h/2), sliderlength, sliderheight, p2f0)
    ).plots[end]
    lbb = lift(range_label_bb, Node(lplot), valueprinter, range)
    bg_rect = lift(sliderlength, sliderheight, lbb, p2f0) do w, h, bb, p
        FRect(p, w + 10 + widths(bb)[1], h)
    end
    poly!(
        slider, bg_rect,
        color = backgroundcolor, linecolor = strokecolor,
        linewidth = strokewidth
    )
    line = lift(sliderlength, sliderheight, p2f0, buttonsize) do w, h, p, bs
        [p .+ Point2f0(bs/2, h / 2), p .+ Point2f0(w - (bs/2), h / 2)]
    end

    linesegments!(slider, line, color = slidercolor)
    button = scatter!(
        slider, lift(x-> x[1:1], line),
        markersize = buttonsize, color = buttoncolor, strokewidth = buttonstroke,
        strokecolor = buttonstrokecolor
    ).plots[end]
    dragslider(slider, button)
    move!(slider, find_closest(range[], startval))
    slider
end

function dragslider(slider, button)
    mpos = mouse_in_scene(slider)
    drag_started = Ref(false)
    startpos = Base.RefValue(Vec(0.0, 0.0))
    range = slider[1]
    @extract slider (value, sliderlength)
    on(events(slider).mousedrag) do drag
        if drag == Mouse.down && mouseover(slider, button)
            startpos[] = mpos[]
            drag_started[] = true
        elseif drag == Mouse.pressed && drag_started[]
            diff = startpos[] .- mpos[]
            startpos[] = mpos[]
            spos = translation(button)[][1] - diff[1]
            l = sliderlength[] - button[:markersize][]
            if spos >= 0 && spos <= l
                idx = round(Int, ((spos / l) .* (length(range[]) - 1)) + 1)
                value[] = range[][idx]
                translate!(button, spos, 0, 0)
            end
        else
            drag_started[] = false
            startpos[] = Vec(0.0, 0.0)
        end
        return
    end
end

function move!(x::Slider, idx::Integer)
    r = x[1][]
    len = x[:sliderlength][] - x[:buttonsize][]
    x[:value] = r[idx]
    xpos = ((idx - 1) / (length(r) - 1)) * len
    translate!(x.plots[end], xpos, 0, 0)
    return
end
export move!
@recipe(Button) do scene
    Theme(
        dimensions = (40, 40),
        backgroundcolor = (:white, 0.4),
        strokecolor = (:black, 0.4),
        strokewidth = 1,
        textcolor = :black,
        textsize = 20,
        clicks = 0,
        position = (10, 10),
        padvalue = 0.15
    )
end

function button!(func::Function, scene::Scene, txt; camera = campixel!, kw_args...)
    b = button!(scene, txt; raw = true, camera = camera, kw_args...)[end]
    on(b[:clicks]) do clicks
        func(clicks)
        return
    end
    b
end

function plot!(splot::Button)
    @extract(splot, (
        backgroundcolor, strokecolor, strokewidth,
        dimensions, textcolor, clicks, textsize, position,
        padvalue
    ))
    txt = splot[1]
    lplot = text!(
        splot, txt,
        color = textcolor,
        textsize = textsize, position = position,
        align = (:center, :center)
    ).plots[end]
    bb = boundingbox(lplot)
    pad = mean(widths(bb)) .* padvalue[]
    poly!(splot, padrect(FRect2D(boundingbox(lplot)), pad), color = backgroundcolor, strokecolor = strokecolor, strokewidth = strokewidth)
    reverse!(splot.plots) # make poly first
    on(events(splot).mousebuttons) do mb
        if ispressed(mb, Mouse.left) && mouseover(parent_scene(splot), splot.plots...)
            clicks[] = clicks[] + 1
        end
        return
    end
    splot
end

window_open(scene::Scene) = getscreen(scene) != nothing && isopen(getscreen(scene))

function playbutton(f, scene, range, rate = (1/30))
    b = button!(scene, "▶", raw = true)[end]
    isplaying = Ref(false)
    play_idx = Ref(1)
    on(b[:clicks]) do x
        if !isplaying[] && x > 0 # check that this isn't before any clicks
            isplaying[] = true
            @async begin
                b.plots[1][1][] = "𝅛𝅛"
                tstart = time()
                while (isplaying[] && window_open(scene))
                    if time() - tstart >= rate
                        f(range[play_idx[]])
                        play_idx[] = mod1(play_idx[] + 1, length(range))
                        tstart = time()
                        force_update!()
                    end
                    yield()
                end
                isplaying[] = false
            end
        else
            b.plots[1][1][] = "▶ "
            isplaying[] = false
        end
        nothing
    end
    b
end

struct Popup
    scene::Scene
    open::Node
    position::Node{Point2f0}
    width::Node{Point2f0}
end

function textslider(range, label, scene = Scene(camera = campixel!); start = first(range))
    t = text!(scene, "$label:", raw = true, position = (0, 50), align = (:left, :center))[end]
    xp = widths(boundingbox(t))[1]
    s = slider!(scene, range, position = Point2f0(xp, 0), raw = true, start = start)[end]
    scene, s[:value]
end

function sample_color(f, ui, colormesh, v)
    mpos = ui.events.mouseposition
    sub = Scene(ui, transformation = Transformation(), px_area = pixelarea(ui), theme = theme(ui))
    select = scatter!(
        sub, lift((p, a)-> [Point2f0(p) .- minimum(a)], mpos, pixelarea(sub)),
        markersize = 15, color = (:white, 0.2), strokecolor = :white,
        strokewidth = 6, visible = lift(identity, theme(ui, :visible)), raw = true
    )[end]
    onany(mpos, ui.events.mousebuttons) do mp, mb
        bb = FRect2D(boundingbox(colormesh))
        mp = Point2f0(mp) .- minimum(pixelarea(sub)[])
        if Point2f0(mp) in bb
            select[:visible] = true
            if ispressed(mb, Mouse.left)
                sv = (Point2f0(mp) .- minimum(bb)) ./ widths(bb)
                f(RGBAf0(HSV(v[], sv[1], sv[2])))
            end
        else
            select[:visible] = false
        end
        return
    end
end

function popup(parent, position, width)
    pos_n = Node(Point2f0(position))
    pos_cam = lift(pos_n, camera(parent).projectionview) do pos, pview
        to_world(parent, Point2f0(pos))
    end
    width_n = Node(Point2f0(width))
    hwidth = 30
    harea = lift(pos_cam, width_n) do p, wh
        IRect(1, wh[2] - hwidth + 1, wh[1] - 2, hwidth - 2)
    end
    parea = lift(pos_cam, width_n) do p, wh
        IRect(p, Point2f0(wh) .- Point2f0(0, hwidth - 1))
    end
    vis = Node(false)
    popup = Scene(parent, parea,
        visible = vis, raw = true, camera = campixel!,
        backgroundcolor = RGBAf0(0.95, 0.95, 0.95, 1.0)
    )
    header = Scene(popup, harea,
        backgroundcolor = RGBAf0(0.90, 0.90, 0.90, 1.0), visible = vis,
        raw = true, camera = campixel!
    )
    initialized = Ref(false)
    but = button!(header, "x", strokewidth = 0.0) do click
        if initialized[]
            vis[] = !vis[]
        else
            initialized[] = true
        end
        return
    end
    poly!(popup, lift(wh-> FRect(2, 2, (wh - 4)...), width_n), color = :white)
    scene2 = Scene(popup, theme = theme(popup))
    campixel!(scene2)
    Popup(scene2, vis, pos_n, width_n)
end

function colorswatch(scene = Scene(camera = campixel!))
    pop = popup(scene, (0, 0), (250, 300))
    sub_ui = pop.scene
    st, hsv_hue = textslider(1:360, "hue", sub_ui)
    colors = lift(hsv_hue) do V
        [HSV(V, 0, 0), HSV(V, 1, 0), HSV(V, 1, 1), HSV(V, 0, 1)]
    end
    S = 200
    colormesh = mesh!(
        sub_ui,
        # TODO implement decompose correctly to just have this be IRect(0, 0, S, S)
        [(0, 0), (S + 30, 0), (S + 30, S - 15), (0, S - 15)],
        [1, 2, 3, 3, 4, 1],
        color = colors, raw = true, shading = false
    )[end]
    color = Node(RGBAf0(0,0,0,1))
    sample_color(sub_ui, colormesh, hsv_hue) do c
        color[] = c
    end
    hbox!(sub_ui.plots)
    rect = IRect(0, 0, 50, 50)
    swatch = poly!(scene, rect, color = color, raw = true, visible = true)[end]
    pop.open[] = false
    on(scene.events.mousebuttons) do mb
        if ispressed(mb, Mouse.left)
            plot, idx = mouse_selection(scene)
            if plot in swatch.plots
                mpos = Point2f0(events(scene).mouseposition[])
                mpos = mpos .- Point2f0(minimum(pixelarea(scene)[]))
                pop.position[] = mpos .+ Point2f0(50, -50)
                pop.open[] = true
            end
        end
        return
    end
    hbox!(sub_ui.plots)
    translate!(sub_ui, 10, 0, 0)
    scene, color, pop
end


##
using ENN.Neurons, ENN.TimePetriNets, GLMakie, GraphMakie, DataStructures

function combine(all_neurons, all_connections)
    neurons = merge(all_neurons...)
    connections = all_connections[1]
    for cons in all_connections[2:end]
        for (key, con) in pairs(cons)
            if key in keys(connections)
                append!(connections[key], con)
            else
                connections[key] = con
            end
        end
    end
    (; neurons, connections)
end

function prune!(neurons, connections)
    all_targets = Tuple{Symbol,Symbol}[]
    for connection in values(connections)
        append!(all_targets, connection)
    end
    unique!(all_targets)

    for (key,neuron) in pairs(neurons)
        for segment in neuron.all_segments
            to_delete = [i for (i,inp) in enumerate(segment.exc_inputs) if (key, inp) ∉ all_targets]
            deleteat!(segment.exc_inputs, to_delete)

            to_delete = [i for (i,inp) in enumerate(segment.inh_inputs) if (key, inp) ∉ all_targets]
            deleteat!(segment.inh_inputs, to_delete)
        end
    end
end

function make_up_counter(name, size, init_name=Symbol("$(name)_init"), inc_name=Symbol("$(name)_inc"))
    neurons = OrderedDict((Symbol("$(name)_$(i)") => Neuron{Int}((((), [:input1, :input2], [:inh], :dendrite),), [:inc], [], :soma; axon_delay=2) for i in 1:size)...)
    connections = OrderedDict(
        (Symbol("$(name)_$(i)") => [(Symbol("$(name)_$(i+1)"), :input1)] for i in 1:size-1)...,
        init_name => [(Symbol("$(name)_1"), :input1)],
        inc_name => [(key, :inc) for key in keys(neurons)]
    )
    (; neurons, connections)
end

function make_loop(name, loop_label, loop_position, loop_origin, condition_name=Symbol("$(name)_condition"))
    neurons = OrderedDict(Symbol("$(name)_loop_$(loop_label)") => Neuron{Int}((((), [:condition], [], :dendrite),), [:trigger], [], :soma; spike_duration=1))
    connections = OrderedDict(
        Symbol("$(name)_loop_$(loop_label)") => [(Symbol("$(name)_$(loop_origin)"), :input2), (Symbol("$(name)_$(loop_position+1)"), :inh)],
        Symbol("$(name)_$(loop_position)") => [(Symbol("$(name)_loop_$(loop_label)"),:trigger)],
        condition_name => [(Symbol("$(name)_loop_$(loop_label)"), :condition)]
    )
    (; neurons, connections)
end

function make_up_down_counter(name, size, init_name=Symbol("$(name)_init"), inc_name=Symbol("$(name)_inc"), dec_name=Symbol("$(name)_dec"), refresh_name=Symbol("$(name)_refresh"))
    neurons = OrderedDict(
        Symbol("$(name)_$(:interneuron)")=>Neuron{Int}((),[:input_dec, :input_inc, :input_refresh, :input_init],[],:soma; axon_delay=1),
        (Symbol("$(name)_$(i)") => Neuron{Int}(
        (
            # left dendrite branch
            (
                (
                # left top branch
                    ((), [:input_121], [:input_122], :dendrite_12),
                ),
                [:input_111], [:input_112],
                :dendrite_11
            ),
            # center dendrite branch
            (
                (
                # left top branch
                    ((), [:input_321], [:input_322], :dendrite_32),
                ),
                [:input_311], [:input_312],
                :dendrite_31
            ),
            # right dendrite branch
            (
                (
                # right top branch
                    ((), i==1 ? [:input_221,:input_223] : [:input_221], [:input_222], :dendrite_22),
                ),
                [:input_211], [:input_212],
                :dendrite_21
            )
            # soma
        ), i==1 ? [:input_01, :input_02, :input_03, :input_04] : [:input_01, :input_02, :input_03], [], :soma; axon_delay=10)
                           for i in 1:size)...
    )

    connections = OrderedDict(
        # from i to i+1, left top dendrite branch

        (Symbol("$(name)_$(i)") => [
            (Symbol("$(name)_$(i+1)"), :input_121),
            (Symbol("$(name)_$(i)"), :input_321),
            (Symbol("$(name)_$(i-1)"), :input_221)
        ] for i in 2:size-1)...,
        dec_name => [
            [(Symbol("$(name)_$(i)"), :input_02) for i in 1:size-1];
            [(Symbol("$(name)_$(i)"), :input_211) for i in 1:size-1];
             (Symbol("$(name)_$(:interneuron)"), :input_dec)
        ],
        inc_name => [
            [(Symbol("$(name)_$(i)"), :input_01) for i in 2:size];
            [(Symbol("$(name)_$(i)"), :input_111) for i in 2:size];
             (Symbol("$(name)_1"), :input_01);
             (Symbol("$(name)_$(:interneuron)"), :input_inc)
        ],
        refresh_name => [
            [(Symbol("$(name)_$(i)"), :input_03) for i in 1:size];
            [(Symbol("$(name)_$(i)"), :input_311) for i in 1:size];
             (Symbol("$(name)_$(:interneuron)"), :input_refresh)
        ],
        Symbol("$(name)_$(:interneuron)") => [
            [(Symbol("$(name)_$(i)"), :input_222) for i in 1:size];
            [(Symbol("$(name)_$(i)"), :input_212) for i in 1:size];
            [(Symbol("$(name)_$(i)"), :input_122) for i in 1:size];
            [(Symbol("$(name)_$(i)"), :input_112) for i in 1:size];
            [(Symbol("$(name)_$(i)"), :input_322) for i in 1:size];
            [(Symbol("$(name)_$(i)"), :input_312) for i in 1:size]
        ],
        init_name => [
            (Symbol("$(name)_1"), :input_04),
            (Symbol("$(name)_1"), :input_111),
            (Symbol("$(name)_1"), :input_121),
            (Symbol("$(name)_$(:interneuron)"), :input_init)
        ],
    )
    if size>1
        push!(connections,
            Symbol("$(name)_1") => [
                (Symbol("$(name)_2"), :input_121),
                (Symbol("$(name)_1"), :input_321),
                (Symbol("$(name)_1"), :input_223)
            ],
            Symbol("$(name)_$(size)") => [
                (Symbol("$(name)_$(size)"), :input_321),
                (Symbol("$(name)_$(size-1)"), :input_221)
            ]
        )
    else
        push!(connections,
            Symbol("$(name)_1") => [
                (Symbol("$(name)_1"), :input_321),
                (Symbol("$(name)_1"), :input_223)
            ]
        )
    end
    (; neurons, connections)
end

function make_relay_neuron(name, sources)
    neurons = OrderedDict(name => Neuron{Int}((), [Symbol("input_$(src)") for src in sources], [], :soma))

    connections = OrderedDict(
        (src => [(name, Symbol("input_$(src)"))] for src in sources)...
    )
    (; neurons, connections)
end

function make_input_trigger(netplot, input_name)
    net_tpn = netplot[:tpn][]
    state = netplot[:state]
    t_id = net_tpn.T_index[Symbol("input_$(input_name)_start")]

    function trigger()
        fire!(net_tpn, state[], t_id)
        fire_necessary!(net_tpn, state[])
        notify(state)
    end
end

function make_time_advance(netplot, trigger_period)
    net_tpn = netplot[:tpn][]
    state = netplot[:state]

    function advance_trigger()
        sim_time = 0
        # execute all transitions until next trigger time
        while true
            τ = max_delay(net_tpn, state[])
            if sim_time + τ > trigger_period
                break
            end
            sim_time += τ
            elapse!(net_tpn, state[], τ)
            fire_necessary!(net_tpn, state[])
        end

        # advance to next trigger time
        elapse!(net_tpn, state[], trigger_period - sim_time)
        notify(state)
    end
end

##

prog_counter=make_up_counter(:program_counter, 5, :init);
prog_loop = make_loop(:program_counter, "loop_1", 3, 1, :condition);
counter1 = make_up_down_counter(:counter1, 3, :init);
relay_inc1 = make_relay_neuron(:counter1_inc, [:program_counter_2, :program_counter_3])
relay_dec1 = make_relay_neuron(:counter1_dec, Symbol[:program_counter_4,:program_counter_5])
relay_refresh1 = make_relay_neuron(:counter1_refresh, Symbol[:program_counter_1])
prog = combine([prog_counter.neurons, prog_loop.neurons, relay_inc1.neurons, relay_dec1.neurons, relay_refresh1.neurons, counter1.neurons], [prog_counter.connections, counter1.connections, prog_loop.connections, relay_inc1.connections, relay_dec1.connections, relay_refresh1.connections]);
prune!(prog.neurons, prog.connections)

net = NeuralNetwork(
    prog.neurons,
    [:init,:program_counter_inc, :condition],
    Symbol[],
    prog.connections
);

f = Figure()
ax = Axis(f[1, 1], aspect=DataAspect())
res=netplot!(ax, net)
display(f)
trigger_init = make_input_trigger(res, :init)
trigger_condition = make_input_trigger(res, :condition)
trigger_inc = make_input_trigger(res, :program_counter_inc)
advance_cycle = make_time_advance(res, 75)

## Run through without looping
trigger_init()
advance_cycle()
trigger_inc()
advance_cycle()
trigger_inc()
advance_cycle()
trigger_inc()
advance_cycle()
trigger_inc()
advance_cycle()
trigger_inc()
advance_cycle()
trigger_inc()
advance_cycle()

## Run through with looping
trigger_init()
advance_cycle()
trigger_inc()
advance_cycle()
trigger_inc()
advance_cycle()
trigger_inc()
trigger_condition()
advance_cycle()
trigger_inc()
advance_cycle()
trigger_inc()
advance_cycle()
trigger_inc()
advance_cycle()

##
net = NeuralNetwork(
    res.neurons,
    [:up1_init,:up1_inc,:up1_dec],
    Symbol[],
    res.connections
);

##

net = NeuralNetwork(
    program_counter_neurons,
    [:init,:trigger],
    Symbol[],
    counter_ff_connections
)
##
function parse_program(expr::Expr, counters...)
    neurons = []
    connections = []
    groups = Vector{Symbol}[]

    counter_sizes = OrderedDict(counters...)

    counter_ops = OrderedDict()
    for key in keys(counter_sizes)
        counter_ops[key] = (inc=Int[],dec=Int[])
    end
    # go through program and create loop neurons as we go
    i=0
    for instruction in expr.args
        if (instruction isa Expr) && (instruction.head == :call)
            i+=1
            if(length(instruction.args)==3 && instruction.args[1] == :JZ)
                (counter, loopback) = instruction.args[2:3]
                (n,c) = make_loop(:program_counter,Symbol("inst_$(i)_loop"),i+1,loopback,Symbol("counter_$(counter)_1"))
                if !haskey(counter_ops, counter)
                    counter_ops[counter] = (inc=Int[],dec=Int[])
                end
                push!(neurons,n)
                push!(connections,c)
            elseif (length(instruction.args)==2 && instruction.args[1] == :INC)
                counter = instruction.args[2]
                if !haskey(counter_ops, counter)
                    counter_ops[counter] = (inc=Int[],dec=Int[])
                end
                push!(counter_ops[counter].inc, i)
            elseif (length(instruction.args)==2 && instruction.args[1] == :DEC)
                counter = instruction.args[2]
                if !haskey(counter_ops, counter)
                    counter_ops[counter] = (inc=Int[],dec=Int[])
                end
                push!(counter_ops[counter].dec, i)
            else
                error("Cannot parse instruction '$(instruction)'.")
            end
        end
    end
    num_instructions = i+2 # +1 for the start instruction, +1 to have a buffer if the program ends in a JZ
    # make program counter
    (n,c) = make_up_counter(:program_counter, num_instructions, :init, :step)
    pushfirst!(neurons, n)
    pushfirst!(connections, c)

    prog = combine(neurons, connections)
    push!(groups, collect(keys(prog.neurons)))

    # make up-down counters & relay neurons
    for (counter,ops) in pairs(counter_ops)
        sz = get(counter_sizes, counter, length(ops.inc))+1
        cnt = make_up_down_counter(Symbol("counter_$(counter)"), sz, :init)
        rly_inc = make_relay_neuron(Symbol("counter_$(counter)_inc"),[Symbol("program_counter_$(i)") for i in ops.inc])
        rly_dec = make_relay_neuron(Symbol("counter_$(counter)_dec"),[Symbol("program_counter_$(i)") for i in ops.dec])
        rly_refresh = make_relay_neuron(Symbol("counter_$(counter)_refresh"),[Symbol("program_counter_$(i)") for i in 1:num_instructions if ((i ∉ ops.inc) && (i∉ ops.dec))])

        push!(neurons, cnt.neurons, rly_inc.neurons, rly_dec.neurons, rly_refresh.neurons)
        push!(connections, cnt.connections, rly_inc.connections, rly_dec.connections, rly_refresh.connections)
        push!(groups, collect(keys(cnt.neurons)))
        append!(groups[1],collect(keys(rly_inc.neurons)),collect(keys(rly_dec.neurons)),collect(keys(rly_refresh.neurons)))
    end

    prog = combine(neurons, connections)
    prune!(prog.neurons, prog.connections)

    net=NeuralNetwork(
            (prog.neurons),
            [:init,:step],
            Symbol[],
            (prog.connections)
    )
    return net,groups
end

macro program(expr, counters...)
    net,groups = parse_program(expr, eval.(counters)...)
    return :($net,$groups)
end

net,groups = @program begin
    INC(1)
    INC(1)
    INC(1)
    INC(1)
    INC(1)
    INC(2)
    INC(2)
    INC(2)
    INC(2)
    INC(2)
    DEC(2)
    DEC(2)
    DEC(2)
    DEC(2)
    DEC(2)
    DEC(1)
    DEC(1)
    DEC(1)
    DEC(1)
    DEC(1)
end 1=>5 2=>5

net,groups = @program begin
    INC(1)
    INC(1)
    INC(1)
    INC(1)
    INC(1)
    INC(2)
    INC(2)
    INC(2)
    JZ(2,13)
    DEC(2)
    DEC(1)
    JZ(3,9)
    DEC(3)
end 1=>5 2=>5 3=>1

begin
f = Figure()
ax = Axis(f[1, 1], aspect=DataAspect())
res=netplot!(ax, net, groups)
hidedecorations!(ax)
display(f)
trigger_init = make_input_trigger(res, :init)
trigger_inc = make_input_trigger(res, :step)
advance_cycle = make_time_advance(res, 75)
advance_minimal = make_time_advance(res, 1)
end

## Run through without looping
begin
sleep(2)
trigger_init()
for i in 1:75
    sleep(0.001)
    advance_minimal()
end
for j in 1:15
    trigger_inc()
    for i in 1:75
        sleep(0.001)
        advance_minimal()
    end
end
end
advance_cycle()
trigger_inc()
## Run through with looping
trigger_init()
advance_cycle()
advance_cycle()
trigger_inc()
advance_cycle()
trigger_inc()
trigger_condition()
advance_cycle()
trigger_inc()
advance_cycle()
trigger_inc()
advance_cycle()
trigger_inc()
advance_cycle()

##
f = Figure()
ax = Axis(f[1, 1], aspect=DataAspect())
res=netplot!(ax, net)
net_tpn = res[:tpn][]
state = res[:state]
t_init_id = res[:tpn][].T_index[:input_init_start]
t_trigger_id = res[:tpn][].T_index[:input_trigger_start]
display(f)

trigger_period = 50
last_trigger = 0
sim_time = 0
##
function trigger_init()
    global sim_time = 0
    global last_trigger = 0
    fire!(net_tpn, state[], t_init_id)
    notify(state)
    fire_necessary!(net_tpn, state[])
    notify(state)
    println("Time: $(sim_time)")
end

function advance_trigger()
    next_trigger = last_trigger + trigger_period
    # execute all transitions until next trigger time
    while true
        τ = max_delay(net_tpn, state[])
        if sim_time + τ > next_trigger
            break
        end
        global sim_time += τ
        elapse!(net_tpn, state[], τ)
        fire_necessary!(net_tpn, state[])
        notify(state)
    end

    # advance to next trigger time
    elapse!(net_tpn, state[], next_trigger - sim_time)
    fire!(net_tpn, state[], t_trigger_id)
    notify(state)

    global sim_time = next_trigger
    global last_trigger = next_trigger
    # execute all transitions until next trigger time
    while true
        τ = max_delay(net_tpn, state[])
        if sim_time + τ > next_trigger
            break
        end
        global sim_time += τ
        elapse!(net_tpn, state[], τ)
        fire_necessary!(net_tpn, state[])
        notify(state)
    end

    println("Time: $(sim_time)")
end

##
trigger_init()
advance_trigger()
##
τ=max_delay(net_tpn,state[])
elapse!(net_tpn, state[], τ)
fire_necessary!(net_tpn, state[])
fire!(net_tpn, state[], t_trigger_id)
notify(state)


#: ============================================

@inline lookup_code(P::Vector{TPattern},c::Int) = any(x->any(y->y==c,x),P)
@inline lookup_code(P::Vector{TPattern},cs::Vector{Int}) = any(x->any(c->any(y->y==c,x),cs),P)
@inline lookup_code(P::Vector{TPattern},cs::NTuple{N,Int}) where N = any(x->any(c->any(y->y==c,x),cs),P)

lookup_codes(P::Vector{TPattern},cs::Vector) = all(lookup_code(P,c) for c in cs)

#: ============================================

function get_DATA_by_codes(
    b::Block{RT},
    codes::Vector
    )::Vector{DATATYPE} where RT
    if is_multi(b)
        @assert length(b.R[2])>1  # R[2] = [[1,0,2],[23,24,25,1],...]
        return vcat([get_DATA_by_codes(c,codes)
                        for c in children(b)
                            if lookup_codes(c.R[2],codes) ]...)
    elseif is_single(b) && lookup_codes(b.R[2],codes)
        return DATATYPE[b.DATA]
    else
        return DATATYPE[]
    end
end

get_DATA_by_codes(b::Block{RT}, code::Int) where RT = get_DATA_by_codes(b, [code])

#: ============================================

function get_n_blocks_by_codes(b::Block, codes::Vector; n=1)
    status = 0
    function f(x)
        if is_single(x)
            @assert length(x.R[2])==1
            if lookup_codes(x.R[2],codes)
                status = 1
            end
        end
        return
    end
    return DFS( b,
                f,
                x->nothing,
                Vb->( status==0
                        ? vcat(Vb[1]...)    # hit; reset status and return
                        : (status>=1+n  ? (status=0 ; vcat(Vb[1]...))    # enough recorded; reset status
                                        : (status+=1; vcat(Vb[1]..., Any[(status-1,Vb[2])]))) )  # not yet
            )
                #Vb->( status>=1+n
                #        ? (status=0 ; vcat(Vb[1]...))    # enough recorded; reset status
                #        : (status>0 ? (status+=1; vcat(Vb[1]..., Any[(status-1,Vb[2])]))
                #                    : vcat(Vb[1]...)) ) ) # not yet
end

get_n_blocks_by_code(b::Block, code::Int; n=1) =
    get_n_blocks_by_codes(b, [code]; n=n)

#: ============================================

function get_blocks_max_by_codes(
    b::Block,
    codes::Vector,
    stop_codes::Vector;
    n=800
    )
    status = 0
    function f(x)
        if is_single(x)
            @assert length(x.R[2])==1
            if lookup_codes(x.R[2],codes)
                status = 1
            elseif lookup_codes(x.R[2],stop_codes)
                status = 0
            end
        end
        return
    end
    return DFS( b,
                f,
                x->nothing,
                Vb->(status==0 ? vcat(Vb[1]...)
                               : (status+=1; vcat(Vb[1]..., Any[(status-1,Vb[2])])))  # not yet
            )
end

get_blocks_max_by_code(b::Block, code::Int, stop_code::Int; n=300) =
    get_blocks_max_by_codes(b, [code], [stop_code]; n=n)

#: ============================================

function next_block_by_codes(b::Block, codes::Vector; delay=1)
    status = 0
    function f(x)
        if is_single(x)
            @assert length(x.R[2])==1
            if lookup_codes(x.R[2],codes)
                status = 1
            end
        end
        return
    end
    return DFS( b,
                f,
                x->nothing,
                Vb->( status==0
                        ? vcat(Vb[1]...)    # hit; reset status and return
                        : (status>=1+delay  ? (status=0;  Any[Vb[2],])
                                            : (status+=1; vcat(Vb[1]...)) ) )  # not yet
            )
                #Vb->( status>=1+delay
                #        ? (status=0; Any[Vb[2],])    # hit; reset status and return
                #        : (status>0 ? (status+=1; vcat(Vb[1]...))
                #                    : vcat(Vb[1]...)) ) ) # not yet
end

next_block_by_code(b::Block, code::Int; delay=1) = next_block_by_codes(b, [code]; delay=delay)

#: ============================================

comp(p1::TPattern,p2::TPattern) = (length(p1)==length(p2) && all(i->(p1[i]==p2[i]), 1:length(p1)))

lookup_patt(P::Vector{TPattern},patt::TPattern) = any(x->comp(patt,x),P)

function select_block_by_patt(
    b::Block{TR},
    patt::TPattern
    )::Vector{Block{TR}} where TR
    if lookup_patt(x.R[2],patt)  # R[2] = [[1,0,2],[23,24,25,1],...]
        return vcat([ select_block_by_patt(c,patt)
                        for c in children(b)
                            if lookup_patt(c.R[2],patt) ]...)
    else
        return Block{TR}[b,]
    end
end

function get_DATA_by_patt(
    b::Block{TR},
    patt::TPattern
    )::Vector{DATATYPE} where TR
    if  is_multi(b)  # R[2] = [[1,0,2],[23,24,25,1],...]
        return vcat([ get_DATA_by_patt(c,patt)
                        for c in children(b)
                            if lookup_patt(c.R[2],patt) ]...)
    elseif is_single(b) && lookup_patt(b.R[2],patt)
        return  DATATYPE[ b.DATA, ]
    else
        return  DATATYPE[  ]
    end
end


@doc """
    print_matrix(solver::DuneIstlSolver)

Print the BCRS matrix in `solver`.

# Examples
```jldoctest
julia> using DuneIstlSolvers

julia> solver = DuneIstlSolver{Float64, Int32(2)}(3)

julia> print_matrix(solver);

```
""" print_matrix

@doc """
    print_rhs(solver::DuneIstlSolver)

Print the right-hand-side vector in `solver`.

# Examples
```jldoctest
julia> using DuneIstlSolvers

julia> solver = DuneIstlSolver{Float64, Int32(2)}(3)

julia> print_rhs(solver);

```
""" print_rhs


map(calc_first,grammar)

function isProduction(p)
	# if we just do return typeof(p) == Symbol it might bug when p is an Enum
	#Dont aske me why
	if typeof(p) == Symbol
		return true
	end

	return false
end

function calc_follow(p::Union{Production,Int})
	subprods = -1

	if typeof(p) == Int
		subprods = getProd(p).subprods
	else
		subprods = p.subprods
	end

	for sprod in subprods
		for elemsprod_idx=1:(length(sprod)-1)

			if isProduction(sprod[elemsprod_idx]) #Symbols are non-terminals. Enums are tokens
				father_id=getProd_idx(sprod[elemsprod_idx])
				if isProduction(sprod[elemsprod_idx+1])

					for fst in getProd(elemsprod_idx+1).firsts
						#@show sprod[elemsprod_idx]
						#println("----")
						grammar[father_id].follows =
														unique(vcat(grammar[father_id].follows,fst))
					end
				else
					nxtToken = Int(sprod[elemsprod_idx+1])
					if nxtToken != Int(EPS)
						grammar[father_id].follows = unique(vcat(grammar[father_id].follows,nxtToken))
					end
				end

			else
				#If it's not a symbol, then it's a token, alas, there's no follows for it
			end

		end

		if typeof(last(sprod)) == Symbol
			son_id = getProd_idx(last(sprod))
			father_id = p.enum
			#@show p
			for fl in grammar[father_id].follows
				grammar[son_id].follows =
											 unique(vcat(grammar[son_id].follows,fl))
			end
		end
	end

end

map(calc_follow,grammar)


using PkgBenchmark
using DataFrames
using CSV
using Statistics: median
import SymbolicRegression
config = BenchmarkConfig(;juliacmd=`julia -O3`,
                          env=Dict("JULIA_NUM_THREADS" => 4))
_results = benchmarkpkg(SymbolicRegression, config; script="benchmark/.benchmarks.jl")
results = vcat([[["evaluation_"*k, median(v.times)] for (k, v) in bigV]
      for (bigK, bigV) in _results.benchmarkgroup.data]...)
df = DataFrame(commit=_results.commit,
               name=[results[i][1] for i=1:length(results)],
               time=[results[i][2] for i=1:length(results)])

CSV.write("output.csv", df; append=true)


using Flux, Test, Random, Statistics
using Random

Random.seed!(0)

# So we can use the system CuArrays
insert!(LOAD_PATH, 2, "@v#.#")

@testset "Flux" begin

@info "Testing Basics"

include("utils.jl")
include("onehot.jl")
include("optimise.jl")
include("data.jl")

@info "Testing Layers"

include("layers/basic.jl")
include("layers/normalisation.jl")
include("layers/stateless.jl")
include("layers/conv.jl")

@info "Running Gradient Checks"

include("tracker.jl")

if Base.find_package("CuArrays") != nothing
  include("cuda/cuda.jl")
end

end


module JuliaCipher

using HDF5, JLD

""" saveChart(chart::Dict, chartLoc::String)

Handles saving the chart to file, given the location. """

function saveChart(chart::Dict, chartLoc::String)
	#saves a chart to file in a nice easy format. -NEEDS PERMISSIONS
	#handle bad permissions? where should it go?
	save(chartLoc, "chart", chart)

end

""" loadChart(chartLoc::String)

Handles loading a chart from given location, returns as a Dict. """

function loadChart(chartLoc::String)
	#grabs a chart from a file, and loads it properly
	#do a check if you even can first, then pop back a new chart? flag to allow new charting? just outright reject?
	return load(chartLoc)["chart"]
end

""" createChart(chartLoc::String; codeSpace::String="ADFGLTVXYZ", alphabetPool::String="abcde...")

Handles creation of chart based on a given codespace and alphabet. Assumes standard if none given. """

function createChart(chartLoc::String; codeSpace::String="ADFGLTVXYZ", alphabetPool::String="abcdefghiklmnopqrstuvwxyzj0123456789{}()[],.:;+-=*/@%_~!?#^&'<>|ABCDEFGHIJKLMNOPQRSTUVWXYZ `\u0000\u000D\u0020\u005C\u0024\u0022\u0009\u000A")
	chart=Dict()
	charArray=split(alphabetPool, "")
	for i in 1:length(codeSpace)
		for j in 0:length(codeSpace)-1
			randSpot=rand(Int8)%length(charArray)
			randSpot=(randSpot==0?1:(randSpot<0?randSpot*-1:randSpot))
			chart[charArray[randSpot][1]] = string(codeSpace[i])*string(codeSpace[j+1])
			deleteat!(charArray, randSpot)
		end
	end
	saveChart(chart, chartLoc)
	return chart
end

""" encryptFile(fileName::String, passPhrase::String, chart::String)

Handles encryption of file-loads a file from given location, uses given phrase, loads a chart from given location. """

function encryptFile(fileName::String, passPhrase::String, chart::String)
	chart = loadChart(chart) #catch an error if no chart(?)
	#encrypt phase
	preString=""
	inFile = open(fileName,"r")
	fname=split(fileName, '.')\
	fnameOut=fname[1]*".adfgvx"*"."*fname[end]
	outFile = open(fnameOut, "w")
	inString = readstring(inFile)
	for char in inString
		preString*=get(chart, char, 'x')
	end
	fillAmt=length(preString)%length(passPhrase)
	if fillAmt > 0
		for i in 1:(length(passPhrase)-fillAmt)
			preString*=chart['#']
		end
	end

	#phraseShifting phase- take phrase, arrange letters below, sort by phrase alphabetically
	#-turn code letters into array with len(phrase) cols and however many rows
	#-sort array, carry the letters array along with it.
	#=
	codeArray=fill(' ', Int(ceil(length(preString)/length(passPhrase))), length(passPhrase))
	#codeArray=[ceil(length(outString)/length(prePhrase))][length(prePhrase)]
	k=1
	for i in 1:Int(ceil(length(preString)/length(passPhrase)))
		for j in 1:length(passPhrase)
			codeArray[i, j]=preString[k]
			k+=1
		end
	end
	#sortedString=phraseSort(codeArray, passPhrase, true)
	=#
	#temp fix/bypass
	sortedString=preString

	#output phase
	outString=""
	spacing=0; lines=0
	for char in sortedString
		if spacing == 8
			outString*=" "
			spacing=0
			lines+=1
		end
		if lines == 8
			outString*="\n"
			lines=0
		end
		outString*=char
		spacing+=1
	end
	print(outFile, outString)
	close(outFile); close(inFile)
end

""" decryptFile(fileName::String, passPhrase::String, chartLoc::String)

Handles decryption of file- given location of file (.adfgvx.ext), given phrase, and given the chart location. """

function decryptFile(fileName::String, passPhrase::String, chartLoc::String)
	chart=loadChart(chartLoc)
	#reverse the Dict
	decodeChart=Dict()
	for(key, value) in chart
		decodeChart[value]=key
	end
	#reading from file
	fname=split(fileName, '.')
	fnameOut = fname[1]*".test"*"."*fname[end]
	outFile = open(fnameOut, "w")
	spacing=0; lines=0
	inFile = open(fileName,"r")
	fileString = readstring(inFile)

	#phraseshifting-
	#take filestring, unshift it according to phrase, carry on decoding
	fileArray = split(fileString)
	codeArray=fill(' ', length(passPhrase), Int(ceil(length(fileString)/length(passPhrase))))
	#codeArray=[ceil(length(outString)/length(prePhrase))][length(prePhrase)]
	#=
	k=1
	for i in 1:Int(ceil(length(fileString)/length(passPhrase)))
		for j in 1:length(passPhrase)
			codeArray[j, i]=fileArray[k]
			k+=1
		end
	end
	sortedArray=phraseSort(codeArray, passPhrase, false)
	=#

	#output phase
	#can do this better- allow for custom block sizes, and just grab 2 chars at a time, throw them at the chart, keep on going
	for block in fileArray
		for char in 1:2:length(block)
			code = string(block[char])*string(block[char+1])
			print(outFile, get(decodeChart, code, 'x'))
		end
	end
	close(outFile); close(inFile)
end

""" phraseSort(message::Any, passPhrase::String, encoding::Bool)

Handles sorting the entire 'message' by a given phrase. also takes a flag to decide if shifting for encoding or decoding. """

function phraseSort(message::Any, passPhrase::String, encoding::Bool)
	postPhrase=[]
	prePhrase=split(passPhrase,"")
	stringOut=message #just because i dont want to do this YET
	if encoding == true
		#encoding
		#sort the phrase into alphabetical order, and drag the array 'rows' with you
		#for loops? per char in that char of phrase? seems slow-
		#get offset library - phrase => aehprs == [3,1,2,-3,1,-4], and just apply the offsets when re-assigning chars to new/duped array
		#grab chars in phrase, and math out their ascii offsets? then simply put all chars in newArray[j+offset[j], i]

		#return as finished string
	else
		#decoding
		#like above, but do opposite- subtract offsets when putting in out array- keep single by putting in newArray[i+(j-1)*len(phrase)]

		#return as a single dimensional code char array
	end

	return stringOut #prolly not stringout
end

""" testModule()

Runs a very basic test of the programs features- creates chart, encrypts, decrypts. """

function testModule()
	fname = ""
	dname = ""
	#chart = createChart("chart.jld")
	#print(chart)
	encryptFile(fname, "phrase", "chart.jld")
	decryptFile(dname, "phrase", "chart.jld")
end

end


function SecantSolver()
    return SecantSolver(())
end

function SecantSolver(arg0::jdouble)
    return SecantSolver((jdouble,), arg0)
end

function SecantSolver(arg0::jdouble, arg1::jdouble)
    return SecantSolver((jdouble, jdouble), arg0, arg1)
end


# Styles - mainly for switching between hexagonal and rectangular layouts.
abstract type Style end

# Style API
links(x::Style) = x.links
iolinks(x::Style) = x.io_links

# For unifying filters for creating source/sink rules.
filter_proc(x) = search_metadata!(x, typekey(), MTypes.proc, in)
filter_io(x) = search_metadata!(
    x,
    typekey(),
    [MTypes.proc, MTypes.input, MTypes.output],
    oneofin
)

filter_memproc(x) = search_metadata!(x, typekey(), MTypes.memoryproc, in)
filter_memory(n) = x -> search_metadata!(x, typekey(), MTypes.memory(n), in)

cartesian(s::Style, x::Address) = cartesian(s, Tuple(x))
cartesian(s::Style, x::Tuple) = cartesian(s, x...)

include("styles/rectangular.jl")
include("styles/five.jl")
include("styles/hexagonal.jl")


using ITensors
using ITensorNetworkAD
using ITensorNetworkAD.ITensorNetworks: Square, bonds

@testset "test lattice" begin
  lattice = Square((2, 3))
  bds = bonds(lattice; periodic=false)
  @test length(bds) == 7
end


using Coord

# run it
@time run_simulation()


### More advanced data structures

struct FaceBasedDS
    faces::AbstractArray{Int,2}
    neighs::AbstractArray{Int,2} ### For now keeping simple
    vfaces::AbstractArray{Int,1}
end

function FaceBasedDS(faces::Array{Int,2})

    vfaces = Array{Int}(undef,maximum(faces))
    neighs = Array{Int}(undef,size(faces)...)

    for vi in 1:maximum(faces)
        vfaces[vi] = find_triangle_vertex(vi,faces)
    end

    for ti in 1:size(faces,2)
        v1, v2, v3 = faces[:,ti]
        t1 = find_other_triangle_edge(v2,v3,ti,faces)
        t2 = find_other_triangle_edge(v1,v3,ti,faces)
        t3 = find_other_triangle_edge(v1,v2,ti,faces)
        neighs[:,ti] = [t1,t2,t3]
    end

    return FaceBasedDS(faces,neighs,vfaces)
end

"""
Iterator for getting all triangles around vertex
"""
struct FaceVRingFB
    v::Int
    faces::Array{Int,2}
    neighs::Array{Int,2} ### For now keeping simple
    vfaces::Array{Int,1}
end

FaceVRing(v::Int,ds::FaceBasedDS) = FaceVRingFB(v,ds.faces,ds.neighs,ds.vfaces)

function start(iter::FaceVRingFB)
    vface = iter.vfaces[iter.v]
    i0 = 1
    return (i0,vface)
end

function done(iter::FaceVRingFB,state::Tuple{Int,Int})
    i, face = state
    i0, vface = start(iter)
    if !(i==i0) & (face==vface)
        return true
    else
        return false
    end
end

function next(iter::FaceVRingFB,state::Tuple{Int,Int})

    i, tri = state
    v = iter.v
    face = iter.faces[:,tri]
    neighbours = iter.neighs[:,tri]

    index = face.==v
    w = index[[1,2,3]]
    cw = index[[3,1,2]]

    nexttri, = neighbours[cw]

    if nexttri==-1
        error("The surface is not closed")
    end

    return tri, (i+1,nexttri)
end

Base.iterate(iter::FaceVRingFB) = next(iter,start(iter))
function Base.iterate(iter::FaceVRingFB,ti)
    if done(iter,ti)
        return nothing
    else
        return next(iter,ti)
    end
end

###

struct DoubleVertexVRingFB
    v::Int
    faces::Array{Int,2}
    neighs::Array{Int,2} ### For now keeping simple
    vfaces::Array{Int,1}
end

DoubleVertexVRing(v::Int,ds::FaceBasedDS) = DoubleVertexVRingFB(v,ds.faces,ds.neighs,ds.vfaces)

function start(iter::DoubleVertexVRingFB)
    vface = iter.vfaces[iter.v]
    i0 = 1
    return (i0,vface)
end

function done(iter::DoubleVertexVRingFB,state::Tuple{Int,Int})
    i, face = state
    i0, vface = start(iter)
    if !(i==i0) & (face==vface)
        return true
    else
        return false
    end
end

function next(iter::DoubleVertexVRingFB,state::Tuple{Int,Int})

    i, tri = state
    v = iter.v
    face = iter.faces[:,tri]
    neighbours = iter.neighs[:,tri]

    index = face.==v
    w = index[[1,2,3]]
    cw = index[[3,1,2]]
    ccw = index[[2,3,1]]

    nexttri, = neighbours[cw]

    if nexttri==-1
        error("The surface is not closed")
    end

    return (face[cw]...,face[ccw]...), (i+1,nexttri)
end

Base.iterate(iter::DoubleVertexVRingFB) = next(iter,start(iter))
function Base.iterate(iter::DoubleVertexVRingFB,ti)
    if done(iter,ti)
        return nothing
    else
        return next(iter,ti)
    end
end

"""
Now the magic of making VertexIterator
"""
struct VertexVRingFB
    v::Int
    faces::Array{Int,2}
    neighs::Array{Int,2} ### For now keeping simple
    vface::Int
    # function VertexRingFBDS(v,faces,neighs,vfaces)
    #     new(v,faces,neighs,vfaces[v])
    # end
end

VertexVRing(v::Int,ds::FaceBasedDS) = VertexVRingFB(v,ds.faces,ds.neighs,ds.vfaces[v])

function start(iter::VertexVRingFB)
    vface = iter.vface
    i0 = 1
    return (i0,vface)
end

function done(iter::VertexVRingFB,state::Tuple{Int,Int})
    i, face = state
    if !(i==1) & (face==iter.vface)
        return true
    else
        return false
    end
end

function next(iter::VertexVRingFB,state::Tuple{Int,Int})

    i, tri = state
    v = iter.v
    face = iter.faces[:,tri]
    neighbours = iter.neighs[:,tri]

    index = face.==v
    w = index[[1,2,3]]
    cw = index[[3,1,2]]

    nexttri, = neighbours[cw]

    if nexttri==-1
        error("The surface is not closed")
    end

    ### Code for extracting vertex from face tri

    face = iter.faces[:,tri]
    cw = (face.==v)[[3,1,2]]
    vi, = face[cw]

    return vi, (i+1,nexttri)
end

Base.iterate(iter::VertexVRingFB) = next(iter,start(iter))
function Base.iterate(iter::VertexVRingFB,ti)
    if done(iter,ti)
        return nothing
    else
        return next(iter,ti)
    end
end

### Using conectivity for improving performance

struct ConnectivityDS
    connectivity
end

function ConnectivityTable(faces,valence)
    vmax = maximum(faces)
    connectivity = zeros(Int,valence,vmax)
    for i in 1:vmax
        v1 = Array{Int}(undef,0)
        v2 = Array{Int}(undef,0)
        for (v1i,v2i) in DoubleVertexVRing(i,faces)
            push!(v1,v1i)
            push!(v2,v2i)
        end

        vj = v2[1]
        connectivity[1,i] = vj
        for j in 2:size(v1,1)
            w = (v1.==vj)
            vj, = v2[w]
            connectivity[j,i] = vj
        end
    end
    return connectivity
end

ConnectivityDS(faces,valence) = ConnectivityDS(ConnectivityTable(faces,valence))

struct VertexVRingCon{T<:Integer}
    v::T
    connectivity::Array{T,2}
end

VertexVRing(v::Int,ds::ConnectivityDS) = VertexVRingCon(v,ds.connectivity)

function start(iter::VertexVRingCon)
    return 1
end

function done(iter::VertexVRingCon,i::T) where {T<:Integer}
    return (i>size(iter.connectivity,1)) || (iter.connectivity[i,iter.v]==0) ? true : false
end

function next(iter::VertexVRingCon,i::T) where {T<:Integer}
    return iter.connectivity[i,iter.v], i+1
end

Base.iterate(iter::VertexVRingCon) = next(iter,start(iter))
function Base.iterate(iter::VertexVRingCon,ti)
    if done(iter,ti)
        return nothing
    else
        return next(iter,ti)
    end
end

struct DoubleVertexVRingCon{T<:Integer}
    v::T
    connectivity::Array{T,2}
end

DoubleVertexVRing(v::Int,ds::ConnectivityDS) = DoubleVertexVRingCon(v,ds.connectivity)

function start(iter::DoubleVertexVRingCon)
    return 1
end

function done(iter::DoubleVertexVRingCon,i::T) where {T<:Integer}
    return (i>size(iter.connectivity,1)) || (iter.connectivity[i,iter.v]==0) ? true : false
end

function next(iter::DoubleVertexVRingCon,i::T) where {T<:Integer}
    v1 = iter.connectivity[i,iter.v]
    v2 = done(iter,i+1) ? iter.connectivity[1,iter.v] : iter.connectivity[i+1,iter.v] # (i==size(iter.connectivity,1)) || (iter.connectivity[i+1,iter.v]==0)
    return (v1,v2), i+1
end

Base.iterate(iter::DoubleVertexVRingCon) = next(iter,start(iter))
function Base.iterate(iter::DoubleVertexVRingCon,ti)
    if done(iter,ti)
        return nothing
    else
        return next(iter,ti)
    end
end


###################################
# Sim statistics aggregator
###################################

"""
node statistics
Creates a dictionary with keys as unique intersections ids and values as NodeStat struct containing coordinates of each intersection, number of agents driving by each intersection and their demographic profiles stored in data frame.

**Arguments**
* `sim_data` : `SimData` object used in sumulation

"""
function node_statistics(sim_data::OpenStreetMapXSim.SimData)::Dict{Int,OpenStreetMapXSim.NodeStat}
    nodes_stats = Dict{Int,OpenStreetMapXSim.NodeStat}()
    for (key,value) in sim_data.map_data.intersections
        coords = OpenStreetMapX.LLA(sim_data.map_data.nodes[key],OpenStreetMapX.center(sim_data.map_data.bounds))
        latitude, longitude = coords.lat, coords.lon
        nodes_stats[key] = OpenStreetMapXSim.NodeStat(0,latitude,longitude,nothing)
    end
    return nodes_stats
end

"""
Statistics aggregator

Aggregates data for each route intersection

**Arguments**
* `nodes_stats` : dictionary with keys as unique nodes_id and values as NodeStat struct
* `agent_profile` : selected agent profile as a DemoProfile object
* `route` : route represented by nodes ids

"""
function stats_aggregator!(nodes_stats::Dict{Int,OpenStreetMapXSim.NodeStat},
                        agent_profile::DataFrames.DataFrame,
                        route::Array{Int,1} )
    for indice in route
        node = nodes_stats[indice]
        node.count += 1
		if isa(node.agents_data,Nothing)
			node.agents_data = deepcopy(agent_profile)
		else
			append!(node.agents_data,agent_profile)
		end
    end
end