using TcpInstruments
using Test

m = initialize(KeysightDMM34465A)

V = get_voltage(m)
@info get_voltage V
@test V isa Float64

A = get_current(m)
@info get_current A
@test A isa Float64

Ω = get_resistance(m; wire=2)
@info "get_resistance(; wire=2)" Ω
@test Ω isa Float64

Ω = get_resistance(m; wire=4)
@info "get_resistance(; wire=4)" Ω
@test Ω isa Float64

channel = get_channel(m)
@info get_channel channel
@test channel in ["FRON", "REAR"]

@info "°" get_tc_temperature(m) get_temp_unit(m)

set_temp_unit_kelvin(m)

@info get_tc_temperature(m) get_temp_unit(m)

# set_tc_type(m; type="K")

set_temp_unit_farenheit(m)

@info get_tc_temperature(m) get_temp_unit(m)

set_temp_unit_celsius(m)

@info get_tc_temperature(m) get_temp_unit(m)

terminate(m)


#####
##### Game trace
#####

"""
    MuTrace{State}

An object that collects all states visited during a game, along with the
rewards obtained at each step and the successive player policies to be used
as targets for the neural network.

# Constructor

    MuTrace(initial_state)

"""
struct MuTrace{State}
  states :: Vector{State}
  policies :: Vector{Vector{Float64}}
  rewards :: Vector{Float64}
  actions :: Vector{Int}
  rootvalues :: Vector{Float64}
  function MuTrace(init_state)
    return new{typeof(init_state)}([init_state], [], [], [], [])
  end
end

function valid_trace(t::MuTrace)
  return length(t.policies) == length(t.rewards) == length(t.actions) == length(t.states)-1
end

"""
    Base.push!(t::Trace, π, r, s, a, v)

Add a (target policy, reward, new state, selected action, rootvalue) to a trace.
"""
function Base.push!(t::MuTrace, π, r, s, a, v)
  push!(t.states, s)
  push!(t.policies, π)
  push!(t.rewards, r)
  push!(t.actions, a)
  push!(t.rootvalues, v)
end

function Base.length(t::MuTrace)
  return length(t.rewards)
end

function total_reward(t::MuTrace, gamma=1.)
  return sum(gamma^(i-1) * r for (i, r) in enumerate(t.rewards))
end

# function total_reward(t::Trace, gamma=1.)
#   return sum([gamma^(i-1) * r for (i, r) in enumerate(t.rewards)])
# end

# function debug_trace(gspec::AbstractGameSpec, t::Trace)
#   n = length(t)
#   for i in 1:n
#     println("Transition $i:")
#     game = GI.init(gspec, t.states[i])
#     GI.render(game)
#     for (a, p) in zip(GI.available_actions(game),  t.policies[i])
#       print("$(GI.action_string(gspec, a)): $(fmt(".3f", p))  ")
#     end
#     println("")
#     println("Obtained reward of: $(t.rewards[i]).")
#     println("")
#   end
#   println("Showing final state:")
#   GI.render(GI.init(gspec, t.states[n + 1]))
# end


struct Game
    state
    players::Tuple
end

Game(state, players...) = Game(state, players)

function play(game::Game)
    state = game.state
    count = 1
    while true
        for p in game.players
            println("player: $p")
            println(state)
            action = nextaction_play(p, state)
            state = transition(state, action)
            actions = getactions(state)
            if isempty(actions)
                r = reward(state)
                println(state)
                println("reward: $r")
                println("finish.")
                return
            end
        end
    end
end


#
# Hierarchical closure.
#

function ConnectQuery(refl::Bool, q::Query)
    @assert fits(output(q), input(q))
    @assert isoptional(output(q))
    Query(
        ConnectSig(refl),
        [q],
        input(q),
        output(q) |> setoptional(!refl) |> setplural())
end

immutable ConnectSig <: AbstractSignature
    refl::Bool
end

ev(sig::ConnectSig, args::Vector{Query}, ity::Input, oty::Output, iflow::InputFlow) =
    ev(sig, args..., ity, oty, iflow)

function ev(sig::ConnectSig, arg::Query, ::Input, oty::Output, iflow::InputFlow)
    cols = Column[]
    if sig.refl
        push!(cols, Column(OneTo(length(iflow)+1), values(iflow)))
    end
    oflow = ev(arg, iflow)
    while length(values(oflow)) > 0
        push!(cols, column(oflow))
        iflow = distribute(iflow, oflow)
        oflow = ev(arg, iflow)
    end
    if isempty(cols)
        pile = column(oflow)
    else
        pile = cols[end]
        for k = endof(cols)-1:-1:1
            pile = connect_impl(cols[k], pile)
        end
    end
    return OutputFlow(oty, pile)
end

function connect_impl(col1::Column, col2::Column)
    len1 = length(col1)
    len2 = length(col2)
    offs1 = offsets(col1)
    offs2 = offsets(col2)
    vals1 = values(col1)
    vals2 = values(col2)
    L = length(vals1)
    offs = Vector{Int}(len1+1)
    offs[1] = 1
    vals = vcat(vals1, vals2)
    idxs = Vector{Int}(length(vals))
    n = 1
    for i = 1:len1
        l1 = offs1[i]
        r1 = offs1[i+1]
        for j = l1:r1-1
            idxs[n] = j
            n += 1
            l2 = offs2[j]
            r2 = offs2[j+1]
            for k = l2:r2-1
                idxs[n] = L + k
                n += 1
            end
        end
        offs[i+1] = n
    end
    return Column(offs, vals[idxs])
end


# This file is part of EndemicModel project, which is licensed under BDS-3-Clause.
# See file LICENSE.md for more information.

# CSSE COVID-19 Repository

const CSSE_COLUMNS = (;
    Symbol("Country/Region") => :country,
    Symbol("Province/State") => :state,
    Symbol("Lat") => :latitude,
    Symbol("Long") => :longitude,
)

function _csse_url(tablename::String)
    "https://raw.githubusercontent.com/CSSEGISandData/COVID-19/master/" *
    "csse_covid_19_data/csse_covid_19_time_series/time_series_covid19_" *
    tablename *
    "_global.csv"
end

const CSSE_TABLES = (
    confirmed = _csse_url("confirmed"),
    deaths = _csse_url("deaths"),
    recovered = _csse_url("recovered"),
)

"""
Novel Coronavirus (COVID-19) Cases, provided by JHU CSSE
https://github.com/CSSEGISandData/COVID-19
https://systems.jhu.edu/research/public-health/ncov/
"""
struct CsseSource <: AbstractDataSource
    function CsseSource()
        new()
    end
end
CsseSource(::Nothing; kwargs...) = CsseSource()

DATA_SOURCES[:csse] = CsseSource

function _import_data(source::CsseSource; kwargs...)
    result = DataDict()
    for (dfsym, url) ∈ pairs(CSSE_TABLES)
        file = import_data(DownloadSource(url))
        df = csv_read(file; copycols = true)

        newcolnames = [
            if all(c -> c ∈ "0123456789/", string(colname))
                date_value = Date(string(colname), "m/d/y") + Year(2000)
                Symbol(string(date_value))
            else
                CSSE_COLUMNS[Symbol(colname)]
            end for colname ∈ names(df)
        ]
        rename!(df, newcolnames)
        df[!, :country] = correct_countries!(df.country)
        result[dfsym] = df
    end
    result
end

function import_data(source::CsseSource; kwargs...)
    @debug "Importing CSSE data."
    raw_data = _import_data(source)
    key_columns = [:country, :state, :latitude, :longitude]
    dataframes = []

    for (main_col_name, df) ∈ raw_data
        dates = []
        date_syms = []
        for colsym ∈ names(df)
            colstr = string(colsym)
            if all(c -> c ∈ "0123456789-", colstr)
                push!(dates, Date(colstr))
                push!(date_syms, colsym)
            end
        end
        datadf = combine(groupby(df, key_columns)) do subdf
            col_values = [sum(subdf[!, date_sym]) for date_sym ∈ date_syms]
            DataFrame(; date = dates, main_col_name => col_values)
        end
        push!(dataframes, datadf)
    end
    parsedata = innerjoin(dataframes...; on = key_columns ∪ [:date]) |> DataFrame
    sort!(parsedata, [:country, :state, :date])
    DataDict(:world => parsedata)
end

"""
Number of tests per country from Our World in Data.
https://ourworldindata.org/covid-testing
"""
struct TotalTestsOWID <: AbstractDataSource
    function TotalTestsOWID()
        new()
    end
end
TotalTestsOWID(::Nothing; kwargs...) = TotalTestsOWID()

DATA_SOURCES[:total_tests] = TotalTestsOWID

const TESTING_OWID_URL = "https://github.com/owid/covid-19-data/raw/master/public/data/testing/covid-testing-all-observations.csv"

function import_data(source::TotalTestsOWID; url = TESTING_OWID_URL, kwargs...)
    @debug "Importing testing data from Our World In Data."
    file = import_data(DownloadSource(url))
    col_select = [:Entity, :Date, Symbol("Cumulative total")]
    data::DataFrame = csv_read(file; copycols = true, select = col_select)
    rename!(data, [:country, :date, :total_tests])
    country_split = map(name -> split(name, " - "), data.country)
    data[!, :country] = map(row -> row[1], country_split) |> correct_countries!
    test_kinds = map(row -> row[2], country_split) |> correct_test_kind!
    insertcols!(data, 3, :test_kind => test_kinds)
    sort!(data, [:country, :date])

    rows = collect(eachrow(data))
    nextrows = rows[2:end]
    lastrow = rows[end]
    nlastrow = DataFrame(;
        country = [lastrow.country],
        date = [today()],
        test_kind = [lastrow.test_kind],
        total_tests = [lastrow.total_tests],
    )
    push!(nextrows, nlastrow[1, :])
    data = copy(data)

    for (row, nextrow) ∈ zip(rows, nextrows)
        if row.country == nextrow.country
            dtend = nextrow.date - Day(1)
        else
            dtend = today() - Day(1)
        end

        for dt ∈ (row.date+Day(1)):Day(1):dtend
            newrow = (;
                country = row.country,
                date = dt,
                test_kind = row.test_kind,
                total_tests = row.total_tests,
            )
            push!(data, newrow)
        end
    end
    sort!(data, [:country, :test_kind, :date])
    data
end

# Brasil.io COVID-19 database

const BRASIL_IO_URL = "https://data.brasil.io/dataset/covid19/caso.csv.gz"

"""
Novel Coronavirus (COVID-19) Cases in Brazil, provided by Brasil.io
https://brasil.io/dataset/covid19/caso_full/
"""
struct BrasilIo <: AbstractDataSource
    function BrasilIo()
        new()
    end
end
BrasilIo(::Nothing; kwargs...) = BrasilIo()

DATA_SOURCES[:brasil_io] = BrasilIo

const BRAZIL_POP_FACTOR = 1.01

function import_data(source::BrasilIo; url = BRASIL_IO_URL, kwargs...)
    @debug "Importing data from Brasil.io."
    col_drop = [:confirmed_per_100k_inhabitants, :death_rate, :is_last]
    gzfile = import_data(DownloadSource(url))
    file = gzopen(string(gzfile))
    fstring = read(file)
    close(file)
    raw_data::DataFrame = csv_read(
        IOBuffer(fstring);
        copycols = true,
        truestrings = ["True"],
        falsestrings = ["False"],
        drop = col_drop,
    )
    raw_data[(raw_data.deaths.===missing), :deaths] .= 0
    raw_data[(raw_data.city.===missing), :city] .= ""
    sort!(raw_data, [:state, :city, :date])

    #rename!(raw_data, "estimated_population_2019" => "estimated_population")
    raw_data.estimated_population =
        round.(OptInt, raw_data.estimated_population .* BRAZIL_POP_FACTOR)

    estimates = get_input(
        "brazil_estimates_per_state";
        types = Dict(:infected_per_confirmed => Float),
    )
    est_sel = select(estimates, :state, :infected_per_confirmed)
    raw_data = leftjoin(raw_data, est_sel; on = :state)
    indexes = .!ismissing.(raw_data.infected_per_confirmed)
    ipc, conf = raw_data.infected_per_confirmed[indexes], raw_data.confirmed[indexes]
    avg_ipc = est_sel[est_sel.state.=="total", :infected_per_confirmed][1]
    raw_data.infected_per_confirmed[.!indexes] .= avg_ipc

    ipc = raw_data.infected_per_confirmed
    infected = round.(Int, ipc .* raw_data.confirmed)
    insertcols!(raw_data, :infected => infected)

    states = @where(raw_data, :place_type .== "state")
    cities = @where(raw_data, :place_type .== "city")

    select!(states, Not([:city, :place_type]))
    select!(cities, Not([:place_type]))

    total = select(states, :date, :confirmed, :deaths, :infected)
    total = combine(groupby(total, :date)) do subdf
        (;
            confirmed = sum(subdf.confirmed),
            deaths = sum(subdf.deaths),
            infected = sum(subdf.infected),
        )
    end

    DataDict(:states => states, :cities => cities, :total => total, :estimates => estimates)
end


# __BEGIN_LICENSE__
#
# ThreeDeconv.jl
#
# Copyright (c) 2018, Stanford University
#
# All rights reserved.
#
# Redistribution and use in source and binary forms for academic and other
# non-commercial purposes with or without modification, are permitted provided
# that the following conditions are met:
#
# * Redistributions of source code, including modified source code, must retain
#   the above copyright notice, this list of conditions and the following
#   disclaimer.
#
# * Redistributions in binary form or a modified form of the source code must
#   reproduce the above copyright notice, this list of conditions and the
#   following disclaimer in the documentation and/or other materials provided with
#   the distribution.
#
# * Neither the name of The Leland Stanford Junior University, any of its
#   trademarks, the names of its employees, nor contributors to the source code
#   may be used to endorse or promote products derived from this software without
#   specific prior written permission.
#
# * Where a modified version of the source code is redistributed publicly in
#   source or binary forms, the modified source code must be published in a freely
#   accessible manner, or otherwise redistributed at no charge to anyone
#   requesting a copy of the modified source code, subject to the same terms as
#   this agreement.
#
# THIS SOFTWARE IS PROVIDED BY THE TRUSTEES OF THE LELAND STANFORD JUNIOR
# UNIVERSITY "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE LELAND STANFORD JUNIOR
# UNIVERSITY OR ITS TRUSTEES BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
# OF SUCH DAMAGE.
#
# __END_LICENSE__

abstract type SolverState end
abstract type SolverMetric end
abstract type Solver end

struct Optimizer
    method::Any
    initial_state::Any
    update_state!::Function
    check_state::Function
    check_convergence::Function
end

OptimizationTrace{T<:SolverMetric} = Vector{T}

mutable struct DeconvolutionOptions
    max_iters::Int
    show_trace::Bool
    check_every::Int
    time_limit::Float64
    x0::AbstractArray
end

function DeconvolutionOptions(;
    max_iters::Integer = 100,
    show_trace::Bool = true,
    check_every::Integer = 10,
    time_limit = NaN,
    x0::AbstractArray = [],
)
    check_every = check_every > 0 ? check_every : 1
    DeconvolutionOptions(Int(max_iters), show_trace, check_every, time_limit, x0)
end

mutable struct OptimizationResult
    x::AbstractArray
    trace::OptimizationTrace
    converged::Bool
end


"""
	prunenode!(node::TreeNode)

Prune node `node` by detaching it from its ancestor. Return pruned `node` and its previous
  ancestor.
"""
function prunenode!(node::TreeNode)
	if node.isroot
		@warn "Trying to prune root: no op."
		return node, TreeNode()
	end
	anc = node.anc
	for (i,c) in enumerate(anc.child)
		if c == node
			deleteat!(anc.child,i)
			break
		end
	end
	if isempty(anc.child)
		anc.isleaf = true
	end
	node.anc = nothing
	node.isroot = true
	return node, anc
end

"""
	prunenode(node::TreeNode)

Prune node `node` by detaching it from its ancestor. Return pruned `node` and previous root `r`. The tree defined by `node` is copied before the operation, and therefore not modified.
"""
function prunenode(node::TreeNode)
	if node.isroot
		@warn "Trying to prune root: no op."
		node_ = deepcopy(node)
		return node_, node_
	end
	node_ = deepcopy(node)
	r = node_findroot(node_)
	anc = node_.anc
	for (i,c) in enumerate(anc.child)
		if c == node_
			deleteat!(anc.child,i)
			break
		end
	end
	node_.anc = nothing
	node_.isroot = true
	return node_, r
end

"""
	prunesubtree!(tree, r::TreeNode)
	prunesubtree!(tree, labellist)

Prune and subtree corresponding to the MRCA of labels in `labellist`.
  Return the root of the subtree as well as its previous direct ancestor.
"""
function prunesubtree!(tree, labellist; clade_only=true)
	if clade_only && !isclade(labellist, tree)
		error("Can't prune non-clade $labellist")
	end
	r = lca(tree, labellist)
	return prunesubtree!(tree, r)
end

function prunesubtree!(tree, r::TreeNode; remove_singletons=true)
	if r.isroot
		@error "Trying to prune root"
	end

	a = r.anc
	delnode(n) = begin
		delete!(tree.lnodes, n.label)
		if n.isleaf
			delete!(tree.lleaves, n.label)
		end
	end
	map!(delnode, r)
	prunenode!(r)
	if remove_singletons
		remove_internal_singletons!(tree, ptau=true)
	end
	return r, a
end

"""
	remove_internal_singletons!(tree; ptau=true)

Remove nodes with one child. Root node is left as is.
If `ptau`, the length of branches above removed nodes is added to the branch length above their children.
"""
function remove_internal_singletons!(tree; ptau=true)
	root = tree.root
	for n in values(tree.lnodes)
		if length(n.child) == 1
			if !n.isroot
				delete_node!(n, ptau=ptau)
				delete!(tree.lnodes, n.label)
			end
		end
	end
	# If root itself is a singleton, delete its child and regraft onto it.
	if length(tree.root.child) == 1 && !tree.root.child[1].isleaf
		n = tree.root.child[1]
		delete_node!(n, ptau=ptau)
		delete!(tree.lnodes, n.label)
	end
	#
	#node2tree!(tree, root)
end

"""
	graftnode!(r::TreeNode, n::TreeNode ; tau=n.tau)

Graft `n` on `r`.
"""
function graftnode!(r::TreeNode, n::TreeNode ; tau=n.tau)
	if !n.isroot || n.anc != nothing
		@error "Trying to graft non-root node $(n)."
	end
	push!(r.child, n)
	r.isleaf = false
	n.anc = r
	n.isroot = false
	n.tau = tau
	return nothing
end

"""
	delete_node!(node::TreeNode; ptau=false)

Delete `node`. If it is an internal node, its children are regrafted on `node.anc`. Returns the new `node.anc`.
If `ptau`, branch length above `node` is added to the regrafted branch.
Otherwise, the regrafted branch's length is unchanged. Return modified `node.anc`.

Note that the previous node will still be in the dictionary `lnodes` and `lleaves` (if a leaf) and the print function will fail on the tree,
to fully remove from the tree and apply the print function use `delete_node!(t::Tree, label; ptau=false)`
"""
function delete_node!(node::TreeNode; ptau=false)
	if node.isroot
		@error "Cannot delete root node"
		error()
	end
	out = node.anc
	if node.isleaf
		prunenode!(node)
	else
		base_tau = node.tau
		for c in reverse(node.child)
			nc = prunenode!(c)[1]
			graftnode!(node.anc, nc, tau = (base_tau*ptau + nc.tau))
		end
		prunenode!(node)
	end
	return out
end

function delete_node!(t::Tree, label; ptau=false)
	delete_node!(t.lnodes[label]; ptau)
	delete!(t.lnodes, label)
	haskey(t.lleaves, label) && delete!(t.lleaves, label)
	remove_internal_singletons!(t; ptau)
	return nothing
end

function delete_null_branches!(node::TreeNode; threshold = 1e-10)
	if !node.isleaf
		if !ismissing(node.tau) && node.tau < threshold && !node.isroot
			nr = delete_node!(node)
			for c in nr.child
				delete_null_branches!(c, threshold=threshold)
			end
		else
			for c in node.child
				delete_null_branches!(c,threshold=threshold)
			end
		end
	elseif !ismissing(node.tau) && node.tau < threshold && !node.isroot
		node.tau = 0.
	end
	return node
end

"""
	delete_null_branches!(tree::Tree; threshold=1e-10)

Delete internal node with branch length smaller than `threshold`. Propagates recursively down the tree. For leaf nodes, set branch length to 0 if smaller than `threshold`.
- If `node` needs not be deleted, call `delete_null_branches!` on its children
- If need be, call `delete_null_branches!` on `node.anc.child`

Calling `delete_null_branches!(node::TreeNode; threshold = 1e-10)` on a node deletes null branches recursively from input `node`, nodes with null branchs will still be in
`lnodes` and `lleaves` (if a leaf) dictionaries. To print the resulting tree use delete_null_branches!(tree::Tree; threshold=1e-10) to remove all null branches from the tree,
or otherwise remove nodes with null branches from `lnodes` and `lleaves`.
"""
function delete_null_branches!(tree::Tree; threshold=1e-10)
	delete_null_branches!(tree.root, threshold=threshold)
	node2tree!(tree, tree.root)
	return nothing
end

function delete_branches!(f, n::TreeNode)
	if !n.isroot && f(n)
		if !n.isleaf
			if !ismissing(n.tau)
				for c in n.child
					if !ismissing(c.tau)
						c.tau += n.tau
					end
				end
			end
			nr = delete_node!(n)
			for c in nr.child
				delete_branches!(f, c)
			end
		else
			n.tau = ismissing(n.tau) ? missing : 0.
		end
	else
		for c in n.child
			delete_branches!(f, c)
		end
	end

	return n
end

"""
	delete_branches!(f, tree::Tree)

Delete branch above node `n` if `f(n)` returns `true` when called on node `n`. When called on `tree` propagates recursively down the tree.
Only when called on `tree` will nodes be additionally deleted from the `lnodes` and `lleaves` (if a leaf) dictionaries (necessary for printing resulting tree).
"""
function delete_branches!(f, tree::Tree)
	delete_branches!(f, tree.root)
	remove_internal_singletons!(tree)
	node2tree!(tree, tree.root)
	return nothing
end

#=
Reroot the tree to which `node` belongs at `node`.
- If `node.isroot`,
- Else if `newroot == nothing`, reroot the tree defined by `node` at `node`. Call `reroot!(node.anc; node)`.
- Else, call `reroot!(node.anc; node)`, then change the ancestor of `node` to be `newroot`.
=#
function reroot!(node::Union{TreeNode,Nothing}; newroot::Union{TreeNode, Nothing}=nothing)
	# Breaking cases
	if node.anc == nothing || node.isroot
		if !(node.anc == nothing && node.isroot)
			@warn "There was a problem with input tree: previous root node has an ancestor."
		elseif newroot != nothing
			i = findfirst(c->c.label==newroot.label, node.child)
			deleteat!(node.child, i)
			node.anc = newroot
			node.tau = newroot.tau
			node.isroot = false
		end
	else # Recursion
		if newroot == nothing
			if node.isleaf
				@warn "Rooting on a leaf node..."
			end
			node.isroot = true
			reroot!(node.anc, newroot=node)
			push!(node.child, node.anc)
			node.anc = nothing
			node.tau = missing
		else
			i = findfirst(c->c.label==newroot.label, node.child)
			deleteat!(node.child, i)
			reroot!(node.anc, newroot=node)
			push!(node.child, node.anc)
			node.anc = newroot
			node.tau = newroot.tau
		end
	end
end
"""
	reroot!(tree::Tree, node::AbstractString)

Reroot `tree` at `tree.lnodes[node]`.
"""
function reroot!(tree::Tree, node::AbstractString)
	reroot!(tree.lnodes[node])
	tree.root = tree.lnodes[node]

	return nothing
end

"""
	add_internal_singleton!(n::TreeNode, a::TreeNode, τ::Real)

Add internal singleton above `n` and below `a`, at heigh `τ` above `n`.
Return the singleton.
"""
function add_internal_singleton!(n::TreeNode, a::TreeNode, τ::Real, label; v = false)
	# Branch length above n and above the singleton
	nτ, sτ = n.tau >= τ ? (τ, n.tau - τ) : (n.tau, 0.)

	s = TreeNode(; tau = sτ, label)
	prunenode!(n)
	n.tau = nτ
	graftnode!(a, s)
	graftnode!(s, n)
	return s
end
function add_internal_singleton!(n::TreeNode, a::TreeNode, τ::Missing, label; v = false)
	@assert ismissing(n.tau)
	prunenode!(n)
	s = TreeNode(; label)
	graftnode!(a, s)
	graftnode!(s, n)
	return s
end


"""
    struct NodeToDofGlue{T}
      free_dof_to_node::Vector{Int32}
      free_dof_to_comp::Vector{Int16}
      dirichlet_dof_to_node::Vector{Int32}
      dirichlet_dof_to_comp::Vector{Int16}
      node_and_comp_to_dof::Vector{T}
    end
"""
struct NodeToDofGlue{T}
  free_dof_to_node::Vector{Int32}
  free_dof_to_comp::Vector{Int16}
  dirichlet_dof_to_node::Vector{Int32}
  dirichlet_dof_to_comp::Vector{Int16}
  node_and_comp_to_dof::Vector{T}
end

"""
    CLagrangianFESpace(::Type{T},grid::Grid) where T
"""
function CLagrangianFESpace(::Type{T},grid::Grid) where T
  vector_type = Vector{_dof_type(T)}
  node_to_tag = fill(Int8(UNSET),num_nodes(grid))
  tag_to_mask = fill(_default_mask(T),0)
  CLagrangianFESpace(T,grid,vector_type,node_to_tag,tag_to_mask)
end

"""
    CLagrangianFESpace(
    ::Type{T},
    grid::Grid,
    vector_type::Type,
    node_to_tag::AbstractVector,
    tag_to_mask::AbstractVector) where T
"""
function CLagrangianFESpace(
  ::Type{T},
  grid::Grid,
  vector_type::Type,
  node_to_tag::AbstractVector{<:Integer},
  tag_to_masks::AbstractVector) where T

  z = zero(T)
  glue, dirichlet_dof_tag = _generate_node_to_dof_glue_component_major(
    z,node_to_tag,tag_to_masks)
  cell_reffe = _generate_cell_reffe_clagrangian(z,grid)
  cell_dofs_ids = _generate_cell_dofs_clagrangian(z,grid,glue,cell_reffe)
  cell_shapefuns = lazy_map(get_shapefuns,cell_reffe)
  cell_dof_basis = lazy_map(get_dof_basis,cell_reffe)
  rd = ReferenceDomain()
  fe_basis, fe_dof_basis = compute_cell_space(
    cell_shapefuns,cell_dof_basis,rd,rd,grid)
  cell_is_dirichlet = _generate_cell_is_dirichlet(cell_dofs_ids)

  nfree = length(glue.free_dof_to_node)
  ndirichlet = length(glue.dirichlet_dof_to_node)
  ntags = length(tag_to_masks)
  dirichlet_cells = collect(Int32,findall(cell_is_dirichlet))

  space = UnconstrainedFESpace(
    vector_type,
    nfree,
    ndirichlet,
    cell_dofs_ids,
    fe_basis,
    fe_dof_basis,
    cell_is_dirichlet,
    dirichlet_dof_tag,
    dirichlet_cells,
    ntags,
    glue)

  space
end

function _use_clagrangian(trian,cell_reffe,conf)
  false
end

function _use_clagrangian(trian::Grid,cell_reffe,conf::H1Conformity)
  ctype_reffe1, cell_ctype1 = compress_cell_data(cell_reffe)
  ctype_reffe2 = get_reffes(trian)
  if length(ctype_reffe1) != 1 || length(ctype_reffe2) != 1
    return false
  end
  reffe1 = first(ctype_reffe1)
  reffe2 = first(ctype_reffe2)
  if get_orders(reffe1) != get_orders(reffe2)
    return false
  end
  if get_order(reffe1) != 1 # This can be relaxed in the future
    return false
  end
  return true
end

function _unsafe_clagrangian(
  cell_reffe,
  grid,
  labels,
  vector_type,
  dirichlet_tags,
  dirichlet_masks)

  ctype_reffe, cell_ctype = compress_cell_data(cell_reffe)
  prebasis = get_prebasis(first(ctype_reffe))
  T = return_type(prebasis)
  # Next line assumes linear grids
  node_to_tag = get_face_tag_index(labels,dirichlet_tags,0)
  _vector_type = vector_type === nothing ? Vector{Float64} : vector_type
  tag_to_mask = dirichlet_masks === nothing ? fill(_default_mask(T),length(dirichlet_tags)) : dirichlet_masks
  CLagrangianFESpace(T,grid,_vector_type,node_to_tag,tag_to_mask)
end

# Helpers

_default_mask(::Type) = true
_default_mask(::Type{T}) where T <: MultiValue = ntuple(i->true,Val{length(T)}())

_dof_type(::Type{T}) where T = T
_dof_type(::Type{T}) where T<:MultiValue = eltype(T)

function _generate_cell_is_dirichlet(cell_dofs)
  cell_is_dirichlet = Vector{Bool}(undef,length(cell_dofs))
  cache = array_cache(cell_dofs)
  isnegative(dof) = dof<0
  for cell in 1:length(cell_dofs)
    dofs = getindex!(cache,cell_dofs,cell)
    cell_is_dirichlet[cell] = any(isnegative,dofs)
  end
  cell_is_dirichlet
end

function _generate_node_to_dof_glue_component_major(z,node_to_tag,tag_to_mask)
  @notimplementedif length(z) != 1
  nfree_dofs = 0
  ndiri_dofs = 0
  for tag in node_to_tag
    if tag == UNSET
      nfree_dofs += 1
    else
      mask = tag_to_mask[tag]
      if mask
        ndiri_dofs += 1
      else
        nfree_dofs += 1
      end
    end
  end
  free_dof_to_node = Vector{Int32}(undef,nfree_dofs)
  free_dof_to_comp = ones(Int16,nfree_dofs)
  diri_dof_to_node = Vector{Int32}(undef,ndiri_dofs)
  diri_dof_to_comp = ones(Int16,ndiri_dofs)
  diri_dof_to_tag = ones(Int8,ndiri_dofs)
  nnodes = length(node_to_tag)
  node_and_comp_to_dof = Vector{Int32}(undef,nnodes)
  nfree_dofs = 0
  ndiri_dofs = 0
  for (node,tag) in enumerate(node_to_tag)
    if tag == UNSET
      nfree_dofs += 1
      free_dof_to_node[nfree_dofs] = node
      node_and_comp_to_dof[node] = nfree_dofs
    else
      mask = tag_to_mask[tag]
      if mask
        ndiri_dofs += 1
        diri_dof_to_node[ndiri_dofs] = node
        diri_dof_to_tag[ndiri_dofs] = tag
        node_and_comp_to_dof[node] = -ndiri_dofs
      else
        nfree_dofs += 1
        free_dof_to_node[nfree_dofs] = node
        node_and_comp_to_dof[node] = nfree_dofs
      end
    end
  end
  glue = NodeToDofGlue(
   free_dof_to_node,
   free_dof_to_comp,
   diri_dof_to_node,
   diri_dof_to_comp,
   node_and_comp_to_dof)

  glue, diri_dof_to_tag
end

function _generate_node_to_dof_glue_component_major(
  z::MultiValue,node_to_tag,tag_to_masks)
  nfree_dofs = 0
  ndiri_dofs = 0
  ncomps = length(z)
  @assert length(testitem(tag_to_masks)) == ncomps
  for (node,tag) in enumerate(node_to_tag)
    if tag == UNSET
      nfree_dofs += ncomps
    else
      for mask in tag_to_masks[tag]
        if mask
          ndiri_dofs += 1
        else
          nfree_dofs += 1
        end
      end
    end
  end
  free_dof_to_node = Vector{Int32}(undef,nfree_dofs)
  free_dof_to_comp = ones(Int16,nfree_dofs)
  diri_dof_to_node = Vector{Int32}(undef,ndiri_dofs)
  diri_dof_to_comp = ones(Int16,ndiri_dofs)
  diri_dof_to_tag = ones(Int8,ndiri_dofs)
  T = change_eltype(z,Int32)
  nnodes = length(node_to_tag)
  node_and_comp_to_dof = zeros(T,nnodes)
  nfree_dofs = 0
  ndiri_dofs = 0
  m = zero(Mutable(T))
  for (node,tag) in enumerate(node_to_tag)
    if tag == UNSET
      for comp in 1:ncomps
        nfree_dofs += 1
        free_dof_to_node[nfree_dofs] = node
        free_dof_to_comp[nfree_dofs] = comp
        m[comp] = nfree_dofs
      end
    else
      masks = tag_to_masks[tag]
      for comp in 1:ncomps
        mask = masks[comp]
        if mask
          ndiri_dofs += 1
          diri_dof_to_node[ndiri_dofs] = node
          diri_dof_to_comp[ndiri_dofs] = comp
          diri_dof_to_tag[ndiri_dofs] = tag
          m[comp] = -ndiri_dofs
        else
          nfree_dofs += 1
          free_dof_to_node[nfree_dofs] = node
          free_dof_to_comp[nfree_dofs] = comp
          m[comp] = nfree_dofs
        end
      end
    end
    node_and_comp_to_dof[node] = m
  end
  glue = NodeToDofGlue(
   free_dof_to_node,
   free_dof_to_comp,
   diri_dof_to_node,
   diri_dof_to_comp,
   node_and_comp_to_dof)

  glue, diri_dof_to_tag
end

function _generate_cell_reffe_clagrangian(z,grid)
  ctype_to_reffe = get_reffes(grid)
  cell_to_ctype = get_cell_type(grid)
  function fun(reffe)
    p = get_polytope(reffe)
    orders = get_orders(reffe)
    LagrangianRefFE(typeof(z),p,orders)
  end
  # using map instead of lazy_map seems to kill the compiler.
  ctype_to_reffe = collect(lazy_map(fun,ctype_to_reffe))
  cell_to_reffe = expand_cell_data(ctype_to_reffe,cell_to_ctype)
end

function _generate_cell_dofs_clagrangian(z,grid,glue,cell_to_reffe)
  ctype_to_reffe, cell_to_ctype = compress_cell_data(cell_to_reffe)
  cell_to_nodes = get_cell_node_ids(grid)
  cell_to_dofs = _generate_cell_dofs_clagrangian(
    z,
    cell_to_nodes,
    ctype_to_reffe,
    cell_to_ctype,
    glue.node_and_comp_to_dof)
  cell_to_dofs
end

function _generate_cell_dofs_clagrangian(
  z,
  cell_to_nodes,
  ctype_to_reffe,
  cell_to_ctype,
  node_and_comp_to_dof)

  cell_to_dofs = lazy_map(
    Broadcasting(Reindex(node_and_comp_to_dof)),cell_to_nodes)
  Table(cell_to_dofs)
end

function _generate_cell_dofs_clagrangian(
  z::MultiValue,
  cell_to_nodes,
  ctype_to_reffe,
  cell_to_ctype,
  node_and_comp_to_dof)

  ncomps = num_components(z)

  ctype_to_lnode_to_comp_to_ldof = map(get_node_and_comp_to_dof,ctype_to_reffe)
  ctype_to_num_ldofs = map(num_dofs,ctype_to_reffe)

  cell_to_dofs = _allocate_cell_dofs_clagrangian(
    ctype_to_num_ldofs,cell_to_ctype)

  cache = array_cache(cell_to_nodes)
  _fill_cell_dofs_clagrangian!(
    cell_to_dofs,
    cache,
    cell_to_nodes,
    ctype_to_lnode_to_comp_to_ldof,
    cell_to_ctype,
    node_and_comp_to_dof,
    ncomps)

  cell_to_dofs
end

function _allocate_cell_dofs_clagrangian(ctype_to_num_ldofs,cell_to_ctype)
  ncells = length(cell_to_ctype)
  ptrs = zeros(Int32,ncells+1)
  for cell in 1:ncells
    ctype = cell_to_ctype[cell]
    nldofs = ctype_to_num_ldofs[ctype]
    ptrs[cell+1] = nldofs
  end
  length_to_ptrs!(ptrs)
  ndata = ptrs[end]-1
  data = zeros(Int32,ndata)
  Table(data,ptrs)
end

function _fill_cell_dofs_clagrangian!(
  cell_to_dofs,
  cache,
  cell_to_nodes,
  ctype_to_lnode_and_comp_to_ldof,
  cell_to_ctype,
  node_and_comp_to_dof,
  ncomps)

  ncells = length(cell_to_ctype)
  for cell in 1:ncells
    nodes = getindex!(cache,cell_to_nodes,cell)
    ctype = cell_to_ctype[cell]
    lnode_and_comp_to_ldof = ctype_to_lnode_and_comp_to_ldof[ctype]
    p = cell_to_dofs.ptrs[cell]-1
    for (lnode, node) in enumerate(nodes)
      for comp in 1:ncomps
        ldof = lnode_and_comp_to_ldof[lnode][comp]
        dof = node_and_comp_to_dof[node][comp]
        cell_to_dofs.data[p+ldof] = dof
      end
    end
  end
end


using AbstractBijections
using Suppressor
using Test

@testset "AbstractBijections.jl" begin
  f = Bijection(["X1", "X2"], ["Y1", "Y2"])
  @test f["X1"] == "Y1"
  @test inv(f)["Y1"] == "X1"
  @test domain(f) == ["X1", "X2"]
  @test image(f) == ["Y1", "Y2"]

  f = bijection(["X1" => "Y1", "X2" => "Y2"])
  @test f["X1"] == "Y1"
  @test inv(f)["Y1"] == "X1"

  f = Bijection(["Y1", "Y2"])
  @test f[1] == "Y1"
  @test inv(f)["Y1"] == 1
end

@testset "examples.jl" begin
  examples_path = joinpath(pkgdir(AbstractBijections), "examples")
  files = readdir(examples_path; join=true)
  for file in files
    @suppress include(file)
  end
end


#CMDimData: Tools to easily manipulate continuous, multi-dimensional datasets.
#-------------------------------------------------------------------------------
__precompile__(true)
#=
TAGS:
	#WANTCONST, HIDEWARN_0.7
=#

module CMDimData

using Colors
using MDDatasets
import Pkg

const rootpath = realpath(joinpath(@__DIR__, ".."))
NullOr{T} = Union{Nothing, T}

include("EasyPlot/EasyPlot.jl")
include("EasyData/EasyData.jl")

#==Subpackage manipulations
===============================================================================#

#Add a CMDimData subpackage to the active environment.
function pkgadd(pkgname::Symbol)
	throw(:NOTIMPL)
	repo = "https://github.com/ma-laforge/CMDimData"
	@show Pkg.add(PackageSpec(path=repo, subdir="subpkgs/$pkgname"))
end

#==Initialization
===============================================================================#
#Mimick import by including package code into the caller's module
macro includepkg(pkgname::Symbol)
#=Import glue code in caller's module.
	Expected that caller's module has access to dependency information on both
	`CMDimData` & dependencies of included module.

	Sadly, precompile of package code cannot be reused across Julia sessions.
	NOTE: Must call __init__() because it is only called on legitimate packages (not modules)
=#
	path = realpath(joinpath(rootpath,"subpkgs/$pkgname/src/$pkgname.jl"))
	m = quote
		if !@isdefined $pkgname
			include($path)
			$pkgname.__init__()
		end
	end
	return esc(m) #esc: Evaluate in calling module
end

function __init__()
	return
end

end # module


"""
	struct ExtractArray{T}
		item::T
	end

	convert array of values to one bag of values converted with `item`. Note that in order the function to work properly,
	calling `item` on a single item has to return Matrix.

```juliadoctest
julia> sc = ExtractArray(ExtractCategorical(Float32,2:4))
julia> sc([2,3,1,4]).data
3×4 Array{Float32,2}:
 1.0  0.0  0.0  0.0
 0.0  1.0  0.0  0.0
 0.0  0.0  0.0  1.0

```

```juliadoctest
julia> sc = ExtractArray(ExtractScalar())
julia> sc([2,3,4]).data
 2.0  3.0  4.0
```
"""
struct ExtractArray{T} <: AbstractExtractor
	item::T
end

extractsmatrix(s::ExtractArray) = false

# (s::ExtractArray)(v::V) where {V<:Nothing} = BagNode(reduce(catobs, s.item.([nothing])),[1:1])
(s::ExtractArray)(v::V) where {V<:Nothing} = BagNode(missing, [0:-1])
(s::ExtractArray)(v::V) where {V<:Missing} = BagNode(missing, [0:-1])
(s::ExtractArray)(v) = isempty(v) ? BagNode(missing, [0:-1]) : BagNode(reduce(catobs, s.item.(v)),[1:length(v)])
function Base.show(io::IO, m::ExtractArray; pad = [], key::String="")
	c = COLORS[(length(pad)%length(COLORS))+1]
	key *= isempty(key) ? "" : ": "
	paddedprint(io,"$(key)Array of\n", color = c)
	paddedprint(io, "  └── ", color=c, pad=pad)
	show(io,m.item, pad = [pad; (c, "      ")])
end

Base.hash(e::ExtractArray, h::UInt) = hash(e.item, h)
Base.:(==)(e1::ExtractArray, e2::ExtractArray) = e1.item == e2.item


J = 400
B = 10.0
θ = 0.0024
M = 0.0025
Gt = 2.0
L = 1000.0
n0 = 0.033
nt = 0.0133
Q = 30
ηu = 0.9
ηd = 0.95
f = 2
Hu = 3
Hd = 3
Vmin = 1.0
Vmax = 2.8
N_turbine = 5
Pmax = 5000

upstream = false
downstream = false
enumerate = false
solver = bonmin
parameters = Dict()


"""
    plotDSM(DSM,label)

Plot the binary DSM.

# Arguments
- `DSM`: adjacency matrix.
- `label`: label for elements in this adjacency matrix.

# Examples

```jldoctest
Julia> DSM = [0 1 0 0 0 1 0;
            0 0 0 1 0 0 0;
            1 0 0 0 0 0 1;
            0 0 0 0 1 0 0;
            0 1 0 0 0 1 0;
            0 0 1 0 0 0 0;
            1 0 0 0 1 0 0];
julia> label = ["A", "B", "C", "D", "E", "F", "G"];
julia> plotDSM(DSM,label)
"hugahuga"
```

"""

function plotDSM(DSM,label)
    (DSMr,DSMc) = size(DSM);

    @svg begin

        CellSizeRow = min(50,450/(DSMr+5));
        CellSizeColumn = min(50,450/(DSMc+5));
        t = Table(DSMr+1,DSMc+1, CellSizeRow, CellSizeColumn);

        sethue("black")

        for r in 2 : DSMr+1
            for c in 2 : DSMc+1
                box(t[r,c], t.colwidths[t.currentcol], t.rowheights[t.currentrow], :stroke);
                if r == c
                    sethue("grey")
                    box(t[r,c], t.colwidths[t.currentcol], t.rowheights[t.currentrow], :fill);
                    sethue("white")
                    textfit(label[r-1], BoundingBox(box(t[r,c],CellSizeRow, CellSizeColumn)))
                    sethue("black")
                end

                if DSM[r-1,c-1] != 0
                    circle(t[r,c], CellSizeColumn/3, :fill)
                end
            end
        end

        for r in 1 : DSMr
            textfit(label[r], BoundingBox(box(t[r+1,1],CellSizeRow, CellSizeColumn)))
        end

        for c in 1 : DSMc
            textfit(label[c], BoundingBox(box(t[1,c+1],CellSizeRow, CellSizeColumn)))
        end

    end
end


import Random
# using Profile # only used for code profiling

# actiontypes
# "none"
# "pay" -- QTY gives amount
# "community"
# "chance"
# "jail"
# "goto" -- QTY gives location code to mode to
# "receive" -- QTY gives amount
# "getoutofjail" -- QTY 1 for chestcards. 2 for chancecards
# "goback" -- QTY gives n steps backward
# "10each" -- receive $10 from each player
# "repairs" -- For each house pay £25. For each hotel pay £100
# "st_repairs" -- Pay £40 per house, £115 per hotel
# "rent" -- pay rent to owner
# "station" -- pay rent to owner
# "utility" -- pay rent to owner

### FUNCTIONS ###
"""    spend!(g::Game, qty)
Spend money on taxes, fines, rent, etc. Players balance is reduced."""
function spend!(g::Game, qty)
    p = g.players[g.currentplayer]
    p.money -= qty
end

"""    community!(g::Game, qty)
Draw community chest card and do player actions. qty is ignored"""
function community!(g::Game, qty)
    # use modulo to cycle through 1 to ncards
    n = g.chestN
    # println(g.chestcards[n])
    doActions!(g, g.chestcards[n])
    # n could change if getoutofjail card is removed by doActions!
    n = g.chestN
    ncards = length(g.chestcards)
    g.chestN = 1 + n % ncards
end

"""    chance!(g::Game, qty)
Draw chance card and do player actions. qty is ignored"""
function chance!(g::Game, qty)
    # use modulo to cycle through 1 to ncards
    n = g.chanceN
    # println(g.chancecards[n])
    doActions!(g, g.chancecards[n])
    # n could change if getoutofjail card is removed by doActions!
    n = g.chanceN
    ncards = length(g.chancecards)
    g.chanceN = 1 + n % ncards
end

"""    toJail!(g::Game, qty)
Send player straight to Jail, do not collect 200. qty is ignored"""
function toJail!(g::Game, qty)
    p = g.players[g.currentplayer]
    p.injail = true
    p.location = 11
end

"""    goto!(g::Game, n)
Player goes to square, and does any player actions. Collect 200 if passing go."""
function goto!(g::Game, n)
    p = g.players[g.currentplayer]
    # get 200 if passing go
    if n < p.location
        p.money += 200
    end
    p.location = n
    doActions!(g, g.squares[n])
end

"""    receive!(g::Game, qty)
Receive money from cards, etc. Players balance to increase."""
function receive!(g::Game, qty)
    p = g.players[g.currentplayer]
    p.money += qty
end

"""    outofjailcard!(g::Game, qty)
Receive get out of jail free card. qty is ignored. Remove card from stack."""
function outofjailcard!(g::Game, qty)
    p = g.players[g.currentplayer]
    p.getoutjailfree += 1
    if qty == 1 # chestcards
        deleteat!(g.chestcards, g.chestN)
        g.chestN -= 1
    else
        deleteat!(g.chancecards, g.chanceN)
        g.chanceN -= 1
    end
end

"""    goback!(g::Game, n)
Go back n squares and do any actions"""
function goback!(g::Game, n)
    p = g.players[g.currentplayer]
    loc = 1 + (p.location + 39 - n) % 40
    p.location = loc
    doActions!(g, g.squares[loc])
end

"""    receive10all!(g::Game, qty)
Receive 10 pounds from each player. qty is ignored"""
function receive10all!(g::Game, qty)
    cp = g.players[g.currentplayer]
    # filter() to exclude the currentplayer from the list
    for p in filter(x -> x.i != g.currentplayer, g.players)
        cp.money += 10
        p.money -= 10
    end
end

"""    repairs!(g::Game, qty)
Pay 25 per house, and 100 per hotel. qty is ignored"""
function repairs!(g::Game, qty)
    p = g.players[g.currentplayer]
    for n in p.owned
        square = g.squares[n]
        # built is 5 for hotel
        if square.built == 5
            p.money -= 100
        else
            p.money -= 25 * square.built
        end
    end
end

"""    st_repairs!(g::Game, qty)
Pay 40 per house, and 115 per hotel. qty is ignored"""
function st_repairs!(g::Game, qty)
    p = g.players[g.currentplayer]
    for n in p.owned
        square = g.squares[n]
        # built is 5 for hotel
        if square.built == 5
            p.money -= 115
        else
            p.money -= 40 * square.built
        end
    end
end

"""    payrent!(g::Game, qty)
Pay rent to owner of property. qty is ignored"""
function payrent!(g::Game, qty)
    # check square is owned and that owner is not yourself
    # see if whole set owned
    # lookup rent
    # transfer from player to player
    p = g.players[g.currentplayer]
    square = g.squares[p.location]
    owner = square.owner
    if owner != 0 && p.i != owner
        samecolor = filter(x -> x.color == square.color, g.squares)
        if all(x -> x.owner == owner, samecolor)
            if square.built == 0
                amount = 2 * square.rents[1]
            else
                amount = square.rents[square.built + 1]
            end
        else
            amount = square.rents[square.built + 1]
        end
        p.money -= amount
        g.players[owner].money += amount
    end
end

"""    paystation!(g::Game, qty)
Pay rent to owner of station. qty is ignored.
1 station 25; 2 stations 50; 3 stations 100; 4 stations 200"""
function paystation!(g::Game, qty)
    # check square is owned and that owner is not yourself
    # count no. of stations owned
    # work out rent
    # transfer from player to player
    p = g.players[g.currentplayer]
    square = g.squares[p.location]
    owner = square.owner
    if owner != 0  &&  p.i != owner
        n = count(
            x -> x.owner == owner && x.actiontype == "station"
            ,g.squares
        )
        amount = 25 * 2^(n-1)
        p.money -= amount
        g.players[owner].money += amount
    end
end

"""    payutility!(g::Game, qty)
Pay rent to owner of utility. qty is ignored.
One utility 4 x rollval; both utilities 10 x rollval"""
function payutility!(g::Game, qty)
    # check square is owned and that owner is not yourself
    # count no. of utilities owned
    # work out rent
    # transfer from player to player
    p = g.players[g.currentplayer]
    square = g.squares[p.location]
    owner = square.owner
    if owner != 0  &&  p.i != owner
        n = count(
            x -> x.owner == owner && x.actiontype == "utility"
            ,g.squares
        )
        amount = 4*rollval + (n-1)*6*rollval
        p.money -= amount
        g.players[owner].money += amount
    end
end

"""    donothing(a, b)
Callable function that returns nothing. Needed by doActions! function."""
donothing(a, b) = nothing

const actiondefs = Dict{String, Any}(
    "pay" => spend!,
    "community" => community!,
    "chance" => chance!,
    "jail" => toJail!,
    "goto" => goto!,
    "receive" => receive!,
    "getoutofjail" => outofjailcard!,
    "goback" => goback!,
    "10each" => receive10all!,
    "repairs" => repairs!,
    "st_repairs" => st_repairs!,
    "rent" => payrent!,
    "station" => paystation!,
    "utility" => payutility!,
    "none" => donothing
)
"""    doActions!(g::Game, item::Union{Square,Community,Chance})
Lookup actions based on text string in data tables and execute"""
function doActions!(g::Game, item::Union{Square,Community,Chance})
    # actiondefs is a dictionary of callable functions
    # example: "jail" maps to toJail!(g, QTY)
    actiondefs[item.actiontype](g, item.actionQTY)
end

"""    goforward!(g::Game, n)
Go forward n squares."""
function goforward!(g::Game, n)
    p = g.players[g.currentplayer]
    loc = 1 + (p.location + n-1) % 40
    goto!(g, loc)
end

"""    releasejail!(g::Game)
Release player from jail. Either deduct £50 or use getoutofjailfree card."""
function releasejail!(g::Game)
    p = g.players[g.currentplayer]
    if p.getoutjailfree > 0
        nchest = length(g.chestcards)
        nchance = length(g.chancecards)
        # return to community chest or chance cards
        if nchest < 16 # stack not full
            append!(g.chestcards, [origchestcards[16]])
            g.chestN += 1
        else
            append!(g.chancecards, [origchancecards[16]])
            g.chanceN += 1
        end
        p.getoutjailfree -= 1
    else
        p.money -= 50
    end
    p.injail = false
end

### TESTS ###
# rollval = 5
# # Player(i, owned, location, injail, money, getoutjailfree)
# p1 = Player(1, [7], 1, false, 1500, 0)
# p2 = Player(2, [], 1, false, 1500, 0)
# p3 = Player(3, [], 1, false, 1500, 0)
# g1 = Game(1, [p1, p2, p3], origsquares, 1, origchestcards, 16, origchancecards)
# g1.squares[7].owner = 2 # angel islington
# g1.squares[9].owner = 2 # euston road
# g1.squares[10].owner = 2 # pentonville road
# g1.squares[6].owner = 2 # kings cross
# g1.squares[16].owner = 2 # marylebone
# g1.squares[13].owner = 3 # electric
# g1.squares[29].owner = 3 # water
# println(g1.players[g1.currentplayer])
# @time spend!(g1, 501)
# println(g1.players[g1.currentplayer])
# @time chance!(g1, 0)
# println(g1.players[g1.currentplayer])
# @time goforward!(g1, 6)
# println(g1.players[g1.currentplayer])
# g1.players

"""    roll()
Roll the dice. returns total of 2 throws and whether double was thrown.
"""
function roll()
    a, b = floor(Int, 1 + 6*rand()), floor(Int, 1 + 6*rand())
    isdouble = (a==b)
    return a + b, isdouble
end

"""    taketurn!(g::Game)
Player takes his turn to go. Roll dice. If 3rd double to Jail. Move to square. Do actions. Repeat if doubles.
Returns list of squares visited by player."""
function taketurn!(g::Game)
    # need to store dice value as utility rents calculated from dice roll
    global rollval
    doubles = 0
    squaresvisited = []
    p = g.players[g.currentplayer]
    if p.injail
        releasejail!(g)
    end
    while true
        if p.injail
            break
        end
        rollval, isdouble = roll()
        if isdouble
            doubles += 1
            if doubles == 3
                toJail!(g, 0)
                push!(squaresvisited, p.location)
                break
            end
            goforward!(g, rollval)
            push!(squaresvisited, p.location)
            continue
        end # if
        goforward!(g, rollval)
        push!(squaresvisited, p.location)
        break
    end # while
    return squaresvisited
end

"""    newgame(money...)
Setup new game with empty board. Args gives the start money of the players.
Number of players is inferred from number of arguments. eg
newgame(1500, 1500) gives player1 1500 and player2 1500"""
function newgame(money...)
    # Player(i, owned, location, injail, money, getoutjailfree)
    players = [
        Player(i, [], 1, false, amount, 0)
        for (i, amount) in enumerate(money)
    ]
    cardscom = Random.shuffle(origchestcards)
    cardscha = Random.shuffle(origchancecards)
    # Game(currentplayer, players, squares, chestN, chestcards, chanceN, chancecards)
    return Game(1, players, deepcopy(origsquares), 1, cardscom, 1, cardscha)
end

"""    buildproperty(g::Game, builds)
Set ownership of land and build level of properties. builds is n x 3 array.
The columns of the array are:
[square, owner, built]
square = number from 1 to 40.
owner = number denoting player.
built = 0 for nothing; 1,2,3,4 for houses; 5 for hotel"""
function buildproperty!(g::Game, builds)
    for data in builds
        n = data[1]
        owner = data[2]
        g.squares[n].owner = owner
        g.squares[n].built = data[3]
        if owner > 0
            player = g.players[owner]
            push!(player.owned, n)
        end
    end
end

### TESTS ###
# function testing(niter)
#     # define players
#     # Player(i, owned, location, injail, money, getoutjailfree)
#     p1 = Player(1, [], 1, false, 1500, 0)
#     p2 = Player(2, [6, 7, 9, 10, 16], 1, false, 1500, 0)
#     p3 = Player(3, [13, 29], 1, false, 1500, 0)
#     cardscom = Random.shuffle(origchestcards)
#     cardscha = Random.shuffle(origchancecards)
#     # Game(currentplayer, players, squares, chestN, chestcards, chanceN, chancecards)
#     mygame = Game(1, [p1, p2, p3], deepcopy(origsquares), 1, cardscom, 1, cardscha)
#     # define properties owned
#     mygame.squares[7].owner = 2 # angel islington
#     mygame.squares[9].owner = 2 # euston road
#     mygame.squares[10].owner = 2 # pentonville road
#     mygame.squares[7].built = 4 # angel islington
#     mygame.squares[9].built = 4 # euston road
#     mygame.squares[10].built = 4 # pentonville road
#     mygame.squares[6].owner = 2 # kings cross
#     mygame.squares[16].owner = 2 # marylebone
#     mygame.squares[13].owner = 3 # electric
#     mygame.squares[29].owner = 3 # water
#     # begin iterations
#     for iterN in 1:niter
#         taketurn!(mygame)
#         mygame.currentplayer = 1 + (mygame.currentplayer % length(mygame.players))
#         # println(iterN, ", ", mygame.players[3])
#     end
#     display(mygame.players)
# end
# @time testing(500)
# Profile.clear()
# Profile.init(n = 10^7, delay = 0.0001)
# @profile testing(5_000_000)
# Profile.print(IOContext(stdout, :displaysize => (24, 500)))


# 4x5x3 model

wet3d = falses(4,15,3)
wet3d[2, :, 1] .= true
wet3d[2, :, 2] .= true
wet3d[3:4, 7:9, 1] = true
wet3d[3:4, 12:14, 1] = true
wet3d[3, 7:9, 2] = true
wet3d[3, 12:14, 2] = true
wet3d[2, 2:4, 3] = true
wet3d[2, 7:9, 3] = true
wet3d[3, 12:14, 3] = true
wet3d[3, 7, 3] = true
wet3d[3, 9, 3] = true
wet3d[2, 12, 3] = true

const iwet = (LinearIndices(wet3d))[findall(!iszero, wet3d)] # replaces find(wet3d) :(nwet = length(iwet)


###### Operations for JuMPExpressionMatrix ######

function remove_undef!(expression_array::T) where {T <: JuMPExpressionMatrix}
    for j in 1:size(expression_array)[2]
        for i in 1:size(expression_array)[1]
            if !isassigned(expression_array, i, j)
                expression_array[i, j] = zero(eltype(expression_array))
            else
                continue
            end
        end
    end

    return
end

###### Operations for JuMPDenseAxisExpressionMatrix ######

function remove_undef!(axis_expression_array::JuMP.Containers.DenseAxisArray)
    remove_undef!(axis_expression_array.data)
    return
end

function add_to_expression!(
    expression_array::T,
    ix::Int,
    jx::Int,
    var::JV,
    multiplier::Float64,
) where {T, JV <: JuMP.AbstractVariableRef}
    if isassigned(expression_array, ix, jx)
        JuMP.add_to_expression!(expression_array[ix, jx], multiplier, var)
    else
        expression_array[ix, jx] = multiplier * var
    end

    return
end

function add_to_expression!(
    expression_array::T,
    ix::Int,
    jx::Int,
    var::JV,
    multiplier::Float64,
    constant::Float64,
) where {T, JV <: JuMP.AbstractVariableRef}
    if isassigned(expression_array, ix, jx)
        JuMP.add_to_expression!(expression_array[ix, jx], multiplier, var)
        JuMP.add_to_expression!(expression_array[ix, jx], constant)
    else
        expression_array[ix, jx] = multiplier * var + constant
    end

    return
end

function add_to_expression!(expression_array::T, ix::Int, jx::Int, value::Float64) where {T}
    if isassigned(expression_array, ix, jx)
        expression_array[ix, jx].constant += value
    else
        expression_array[ix, jx] = zero(eltype(expression_array)) + value
    end

    return
end

function add_to_expression!(
    expression_array::T,
    ix::Int,
    jx::Int,
    parameter::PJ.ParameterRef,
) where {T}
    if isassigned(expression_array, ix, jx)
        JuMP.add_to_expression!(expression_array[ix, jx], 1.0, parameter)
    else
        expression_array[ix, jx] = zero(eltype(expression_array)) + parameter
    end

    return
end


# Attributes

"""
    AbstractRecognizeAttribute

Abstract supertype for attribute objects that will be used in the recognize function
in order to specify which features of the provided model to look into.

"""

export ConstantObjective, DifferenceConstraints, AllIntegerVariables, AllIntegerConstraintBounds

abstract type AbstractRecognizeAttribute end

# TODO: Describe each attribute

struct DifferenceConstraints <: AbstractRecognizeAttribute end
struct AllIntegerVariables <: AbstractRecognizeAttribute end
struct ConstantObjective <: AbstractRecognizeAttribute end
struct AllIntegerConstraintBounds <: AbstractRecognizeAttribute end

"""
    AbstractSolveAttribute

Abstract supertype for attribute objects that will be used in the solve function
in order to specify which ways to solve the model.

"""

export ShortestPath

abstract type AbstractSolveAttribute end

struct ShortestPath <: AbstractSolveAttribute end

"""
    AbstractOptimizeAttribute

Abstract supertype for attribute objects that will be used in order to specify what to do when optimizing the model.

"""

abstract type AbstractOptimizeAttribute end


# Copyright © 2021 Alexander L. Hayes
# Apache 2.0 License

module TestConvert

using Test
using RelationalDatasets

@testset "Test converting propositional datasets" begin

    @test RelationalDatasets.from_vector(
        [0 1 1; 1 0 2; 2 2 0],
        [0, 0, 1],
    ) == (RelationalDatasets.RelationalDataset((
            pos=["v4(id3)."],
            neg=["v4(id1).", "v4(id2)."],
            facts=[
                "v1(id1,0).",
                "v1(id2,1).",
                "v1(id3,2).",
                "v2(id1,1).",
                "v2(id2,0).",
                "v2(id3,2).",
                "v3(id1,1).",
                "v3(id2,2).",
                "v3(id3,0).",
            ])),
        [
            "v1(+id,#varv1).",
            "v2(+id,#varv2).",
            "v3(+id,#varv3).",
            "v4(+id).",
        ]
    )

    @test RelationalDatasets.from_vector(
        [0 1 1; 1 0 2; 2 2 0],
        [0.1, 0.2, 0.3],
    ) == (RelationalDatasets.RelationalDataset((
        pos=[
            "regressionExample(v4(id1),0.1).",
            "regressionExample(v4(id2),0.2).",
            "regressionExample(v4(id3),0.3).",
        ],
        neg=[],
        facts=[
            "v1(id1,0).",
            "v1(id2,1).",
            "v1(id3,2).",
            "v2(id1,1).",
            "v2(id2,0).",
            "v2(id3,2).",
            "v3(id1,1).",
            "v3(id2,2).",
            "v3(id3,0).",
        ])),
        [
            "v1(+id,#varv1).",
            "v2(+id,#varv2).",
            "v3(+id,#varv3).",
            "v4(+id).",
        ]
    )

    @test RelationalDatasets.from_vector(
        [1 1; 0 1],
        [0, 1],
        ["a", "b", "c"],
    ) == (RelationalDatasets.RelationalDataset((
        pos=["c(id2)."],
        neg=["c(id1)."],
        facts=[
            "a(id1,1).",
            "a(id2,0).",
            "b(id1,1).",
            "b(id2,1).",
        ])),
        [
            "a(+id,#vara).",
            "b(+id,#varb).",
            "c(+id).",
        ]
    )

    @test RelationalDatasets.from_vector(
        [1 1; 0 1],
        [0.5, 1.0],
        ["a", "b", "c"],
    ) == (RelationalDatasets.RelationalDataset((
        pos=["regressionExample(c(id1),0.5).", "regressionExample(c(id2),1.0)."],
        neg=[],
        facts=[
            "a(id1,1).",
            "a(id2,0).",
            "b(id1,1).",
            "b(id2,1).",
        ])),
        [
            "a(+id,#vara).",
            "b(+id,#varb).",
            "c(+id).",
        ]
    )

    @test RelationalDatasets.from_vector(
        [0 1 1; 1 0 2; 2 2 0],
        [0, 1, 2],
    ) == (RelationalDatasets.RelationalDataset((
            pos=["v4(id1,0).", "v4(id2,1).", "v4(id3,2)."],
            neg=[],
            facts=[
                "v1(id1,0).",
                "v1(id2,1).",
                "v1(id3,2).",
                "v2(id1,1).",
                "v2(id2,0).",
                "v2(id3,2).",
                "v3(id1,1).",
                "v3(id2,2).",
                "v3(id3,0).",
            ])),
        [
            "v1(+id,#varv1).",
            "v2(+id,#varv2).",
            "v3(+id,#varv3).",
            "v4(+id,#classlabel).",
        ]
    )

end

@testset "Misaligned Classification and Regression" begin
    @test_throws AssertionError RelationalDatasets.from_vector([1 2], [1])
    @test_throws AssertionError RelationalDatasets.from_vector([1 2], [0.5])
end

@testset "Misaligned Custom Variable Names" begin
    @test_throws AssertionError RelationalDatasets.from_vector(
        [1 2],
        [1],
        ["a", "b", "c", "d"],
    )

    @test_throws AssertionError RelationalDatasets.from_vector(
        [1 2],
        [0.5],
        ["a", "b", "c", "d"]
    )
end

end # module


using Plots, Colors, ColorTypes # for plotting

export bar, dimplot

################################################################
function invert(c::RGB, distinguish::Bool = false)
     cout = RGB{Float64}(1-c.r,1-c.g,1-c.b)
     if distinguish && cout == c #gray
         cout = RGB{Float64}(0,0,0)
     end
     return cout
end
################################################################
"""
    bar(config::StructArray{Funb}; uniqueType::Bool = false, dimAnnotation::Bool = true, uniqueDim::Bool = false)

Plots bars for each dimension set.

It is assumed that number of dimension sets are the same.

- To have a same color for the same types, set true as the 2nd argument.
- To turn off dimension annotations pass false as the 3rd argument.
- In case of unique dimensions pass true as 4th argument to print dimension in title instead.

# Examples
```julia
bar(configs)
bar(configs, uniqueType = true, dimAnnotation = false, uniqueDim = true)
```
"""
function bar(config::StructArray{Funb}; uniqueType::Bool = false, dimAnnotation::Bool = true, uniqueDim::Bool = false)

    mkpath("bar")

    local titleText

    numFun = length(config.fun)

    bar_width = numFun/3.0*0.2
    bar_text_font = Int64(bar_width*40)
    dim_text_font = Int64(bar_width*30)
    xticks_font = Int64(bar_width*5)

    if uniqueType
        uniqueTypes = uniqueflatten(config.types[:])
        numUniqueTypes =length(uniqueTypes)
        colors = distinguishable_colors(numUniqueTypes, [RGB(1,1,1)])
    end

    _, numDimsSets = numArgsDims(config.dims[1])

    for iDimSet = 1:numDimsSets
        plt = plot()  # different figure for different dims

        for iFun = 1:numFun

            fname = [string(config.fun[iFun])]

            for iType = 1:length(config.types[iFun])

                x = [(iFun-1)+(bar_width*(iType-1))]

                if uniqueType
                    iTypeUnique = findfirst(x -> x==config.types[iFun][iType], uniqueTypes)
                    fillcolor = colors[iTypeUnique]
                    type_text_color = invert(fillcolor, true)'

                    # labels
                    if iFun == 1
                        labels = string(uniqueTypes[iType])
                    else
                        labels = ""
                    end
                    legend = :topleft
                    barText = ""

                else
                    fillcolor = :auto
                    type_text_color = :black

                    barText = Plots.text(string(config.types[iFun][iType]), pointsize = bar_text_font, :center, rotation = 90, color = type_text_color )

                    labels = ""
                    legend = false
                end

                if uniqueDim
                    dimText = ""
                    titleText = "Dimension $(string(config[1].dims[iFun][1,iDimSet]))"
                else
                    if dimAnnotation
                        dimText = Plots.text(stringMatrix(config.dims[iFun][:,iDimSet]), pointsize = dim_text_font, :center)
                    else
                        dimText = ""
                    end
                    titleText = "Dimension set $iDimSet"
                end

                y = [config.median[iFun][iType][iDimSet]]
                # adding bar
                bar!(plt,
                    x,
                    y,
                    labels = labels,
                    legend = legend,
                    fillcolor = fillcolor,
                    bar_width = bar_width,
                    annotations = ([x, x], [y./2, y .+ bar_width/8], [barText, dimText]),
                    dpi = 600
                )
            end
        end

        xticks!(0:numFun-1, string.(config.fun), rotation = 70, fontsize = xticks_font)

        title!(titleText)
        ylabel!("Time [micro seconds]")
        if uniqueType
            filename = "bar/bench-dims-set$iDimSet-unique.png"
        else
            filename = "bar/bench-dims-set$iDimSet.png"
        end
        savefig(filename)
    end
end

bar(config::Funb; uniqueType::Bool = false, dimAnnotation::Bool = true, uniqueDim::Bool = false) = bar(FunbArray(config), uniqueType=uniqueType, dimAnnotation=dimAnnotation, uniqueDim = uniqueDim)

"""
    bar(config::Pair{StructArray{Funb}}; uniqueType::Bool = false, dimAnnotation::Bool = false, uniqueDim::Bool =false, configName::Pair{String,String} = "1" => "2")

Gets a pair of StructArrays and calculates relative speed of coresponding elements and plots them.

Uses the first element of the pair for the configurations and only uses runtimes from the 2nd element of the pair. The relative speed is calculated like this:

1st element runtimes / 2nd element runtimes

Give the configsets names as a pair for the ylabel.
```julia
bar(configs => configs, uniqueType = true, dimAnnotation = false, uniqueDim =true, "group 1" => "group 2")
```
"""
function bar(config::Pair{StructArray{Funb,T1,T2,T3}, StructArray{Funb,T1,T2,T3}}; uniqueType::Bool = false, dimAnnotation::Bool = false, uniqueDim::Bool =false, configName::Pair{String,String} = "1" => "2") where {T1,T2,T3}

    mkpath("bar")
    local titleText

    bar_width = 0.2
    bar_text_font = Int64(bar_width*40)
    dim_text_font = Int64(bar_width*30)
    xticks_font = Int64(bar_width*5)

    if uniqueType
        uniqueTypes = uniqueflatten(config[1].types[:])
        numUniqueTypes =length(uniqueTypes)
        colors = distinguishable_colors(numUniqueTypes, [RGB(1,1,1)])
    end

    numFun = length(config[1].fun)
    _, numDimsSets = numArgsDims(config[1].dims[1])

    for iDimSet = 1:numDimsSets
        plt = plot()  # different figure for different dims

        for iFun = 1:numFun

            fname = [string(config[1].fun[iFun])]

            for iType = 1:length(config[1].types[iFun])

                x = [(iFun-1)+(bar_width*(iType-1))]

                if uniqueType
                    iTypeUnique = findfirst(x -> x==config[1].types[iFun][iType], uniqueTypes)
                    fillcolor = colors[iTypeUnique]
                    type_text_color = invert(fillcolor, true)

                    # labels
                    if iFun == 1
                        labels = string(uniqueTypes[iType])
                    else
                        labels = ""
                    end
                    legend = :topleft
                    barText = ""
                else
                    fillcolor = :auto
                    type_text_color = :black

                    barText = Plots.text(string(config[1].types[iFun][iType]), pointsize = bar_text_font, :center, rotation = 90, color = type_text_color )

                    labels = ""
                    legend = false
                end

                if uniqueDim
                    dimText = ""
                    titleText = "Dimension $(string(config[1].dims[iFun][1,iDimSet]))"
                else
                    if dimAnnotation
                        dimText = Plots.text(stringMatrix(config[1].dims[iFun][:,iDimSet]), pointsize = dim_text_font, :center)
                    else
                        dimText = ""
                    end
                    titleText = "Dimension set $iDimSet"
                end

                y = [config[1].median[iFun][iType][iDimSet] / config[2].median[iFun][iType][iDimSet]]
                # adding bar
                bar!(plt,
                    x,
                    y,
                    labels = labels,
                    legend = legend,
                    fillcolor = fillcolor,
                    bar_width = bar_width,
                    annotations = ([x, x], [y./2, y .+ bar_width/8], [barText, dimText]),
                    dpi = 600
                )
            end
        end

        xticks!(0:numFun-1, string.(config[1].fun), rotation = 70, fontsize = xticks_font)

        title!(titleText)
        ylabel!("Relative Speed ($(configName[2])/$(configName[1]))")
        if uniqueType
            hline!([1], line=(4, :dash, 0.6, [:green]), labels = 1)
        end
        savefig("bar/bench-dims-set$iDimSet-relative.png")
    end
end

################################################################
"""
    dimplot(config::StructArray{Funb})

Shows runtime of a function for different dimension sets. The axes are logarithmic.

# Examples
```julia
configs2 = Funb( sin, [(-1,1)],[Float32, Float64], [10 20 30 40] );

benchmark!(configs2)

dimplot(configs2)
```
"""
function dimplot(config::StructArray{Funb})

mkpath("dimplot")

bar_width = 0.2
bar_text_font = Int64(bar_width*40)
dim_text_font = Int64(bar_width*30)
xticks_font = Int64(bar_width*5)

numFun = length(config.fun)
_, numDimsSets = numArgsDims(config.dims[1])

    for iFun = 1:numFun

        plt = plot()  # different figure for different dims

        fname = [string(config.fun[iFun])]

        for iType = 1:length(config.types[iFun])

            x = 1:numDimsSets
            xticks = string.(dropdims(flatten([config.dims[iFun][1,iDimSet] for iDimSet = 1:numDimsSets]), dims=1))
            y = [config.median[iFun][iType][iDimSet] for iDimSet = 1:numDimsSets]
            # adding bar
            plot!(plt,
                x,
                y,
                labels = string(config.types[iFun][iType]),
                legend = :bottomright,
                xscale = :log10,
                yscale = :log10,
                dpi = 600
            )
            xticks!(x, xticks, fontsize = xticks_font)
        end

        title!("$(fname[1])")
        xlabel!("Dimension")
        ylabel!("Time [micro seconds]")

        savefig("dimplot/bench-$(fname[1]).png")
    end
end

dimplot(config::Funb) = dimplot(FunbArray(config))

"""
    dimplot(config::Vector{StructArray{Funb}}, labels::Vector{String})

By passing a vector different benchmark sets can be grouped together to be shown in a single plot. The axes are logarithmic.

# Examples
```julia
dimplot([config1,config2, config3], ["group 1", "group 2", "group3"])
```
"""
function dimplot(config::Vector{StructArray{Funb,T1,T2,T3}}, labels::Vector{String}) where {T1,T2,T3}

mkpath("dimplot")
numLabels = length(config)

bar_width = 0.2
bar_text_font = Int64(bar_width*40)
dim_text_font = Int64(bar_width*30)
xticks_font = Int64(bar_width*5)

numFun = length(config[1].fun)
_, numDimsSets = numArgsDims(config[1].dims[1])

    for iFun = 1:numFun

        fname = [string(config[1].fun[iFun])]

        for iType = 1:length(config[1].types[iFun])

            plt = plot()  # different figure for different dims

            x = 1:numDimsSets
            xticks = string.(dropdims(flatten([config[1].dims[iFun][1,iDimSet] for iDimSet = 1:numDimsSets]), dims=1))

            for iLabel = 1:numLabels

                y = [config[iLabel].median[iFun][iType][iDimSet] for iDimSet = 1:numDimsSets]

                # adding bar
                plot!(plt,
                    x,
                    y,
                    # labels = string(config[1].types[iFun][iType]),
                    labels = labels[iLabel],
                    legend = :right,
                    xscale = :log10,
                    yscale = :log10,
                    dpi = 600
                )
                xticks!(x, xticks, fontsize = xticks_font)

            end

            title!("$(fname[1]) - Type $(config[1].types[iFun][iType])")
            xlabel!("Dimension")
            ylabel!("Time [micro seconds]")

            savefig("dimplot/bench-$(fname[1])-Type-$(config[1].types[iFun][iType]).png")
        end
    end
end


#include("./sampleTest.jl")


module BeliefUpdaters

using POMDPs
import POMDPs: Updater, update, initialize_belief, pdf, mode, updater, support
import Base: ==
import Statistics
using POMDPModelTools
using StatsBase
using Random

export
    NothingUpdater
include("void.jl")

export
    DiscreteBelief,
    DiscreteUpdater,
    uniform_belief

include("discrete.jl")

export
    PreviousObservationUpdater,
    FastPreviousObservationUpdater,
    PrimedPreviousObservationUpdater

include("previous_observation.jl")

export
    KMarkovUpdater

include("k_previous_observations.jl")

end


function test_poly_mi(s)
    global solver = s

    test_dir = dirname(@__FILE__)

    @testset "Nonconvex MIPolynomial Models" begin end
end


@testset "legend_merging" begin
    mwe_data = (; t = 1:20, y1 = ones(20), y2 = rand(20))
    mwe_names = ["f_n", "f_d"]
    plt = data(mwe_data) *
        mapping("t", ["y1", "y2"] .=> "y"; color = dims(1) => (i -> mwe_names[i]), marker=dims(1)) *
        (visual(Lines) + visual(Scatter))
    @test_throws ArgumentError draw(plt)
end


description(i::AnchoredInterval{P}) where P = description(i, P > zero(P) ? "B" : "E")

function description(interval::AnchoredInterval{P, T}, s::String) where {P, T}
    return string(
        first(inclusivity(interval)) ? '[' : '(',
        description(anchor(interval), abs(P), s),
        last(inclusivity(interval)) ? ']' : ')',
    )
end

function description(interval::AnchoredInterval{P, ZonedDateTime}, s::String) where P
    return string(
        first(inclusivity(interval)) ? '[' : '(',
        description(anchor(interval), abs(P), s),
        anchor(interval).zone.offset,
        last(inclusivity(interval)) ? ']' : ')',
    )
end

function description(dt::Date, p::Period, suffix::String)
    ds = @sprintf("%04d-%02d-%02d", year(dt), month(dt), day(dt))
    return "$(prefix(p))$suffix $ds"
end

function description(dt::AbstractDateTime, p::Period, suffix::String)
    ts = time_string(dt, p)

    # If we would display only HE00, display HE24 for the previous day instead
    if ts == "00" && suffix == "E"
        P, max_val = coarserperiod(typeof(p))
        dt -= oneunit(P)
        ts = string(max_val)
    end

    @static if VERSION < v"0.7.0-DEV.3216"
        isa(dt, Time) && return string(prefix(p), suffix, " ", ts)
    end

    ds = @sprintf("%04d-%02d-%02d", year(dt), month(dt), day(dt))

    if p isa TimePeriod
        return "$ds $(prefix(p))$suffix$ts"
    else
        ts = (ts == "00:00:00" && !isa(dt, ZonedDateTime)) ? "" : " $ts"
        return "$(prefix(p))$suffix $ds$ts"
    end
end

function time_string(dt::AbstractDateTime, p::Period)
    t = (hour(dt), minute(dt), second(dt), millisecond(dt))
    if p isa Hour && all(t[2:end] .== 0)
        return @sprintf("%02d", t[1])
    elseif p isa Minute && all(t[3:end] .== 0)
        return @sprintf("%02d:%02d", t[1:2]...)
    elseif !isa(p, Millisecond) && t[4] == 0
        return @sprintf("%02d:%02d:%02d", t[1:3]...)
    else
        return @sprintf("%02d:%02d:%02d.%03d", t...)
    end
end

function prefix(p::Period)
    string(value(p) == 1 ? "" : value(p), prefix(typeof(p)))
end

prefix(::Type{Year}) = "Y"
prefix(::Type{Month}) = "Mo"
prefix(::Type{Day}) = "D"
prefix(::Type{Hour}) = "H"
prefix(::Type{Minute}) = "M"
prefix(::Type{Second}) = "S"
prefix(::Type{Millisecond}) = "ms"


module NanoDates

export NanoDate,
        nnow, ntoday, date_time,
        nanodate2rata, rata2nanodate,
        nanodate2unixnanos, nanodate2unixmicros, nanodate2unixmillis, nanodate2unixseconds,
        unixnanos2nanodate, unixmicros2nanodate, unixmillis2nanodate, unixseconds2nanodate,
        format,
        CapitalT, SmallCapitalT,
        SingleSpace, Underscore,
        SmallWhiteCircle, SmallWhiteStar

using Dates
using Dates: AbstractDateTime, CompoundPeriod,
             format, value, days,
             toms, tons, UTD, UTM

export AbstractDateTime, CompoundPeriod,
    value, days, format

include("constants.jl")
include("nanodate.jl")
include("accessors.jl")
include("conversions.jl")
include("compare.jl")
include("interop.jl")
include("arith.jl")
include("fastintmath.jl")
include("compound.jl")
include("strings.jl")
include("timestamp.jl")
include("io.jl")

@inline nanos_elapsed() = time_ns() - NanosAtStart

function __init__()
    global NanosAtStart = time_ns()
end

end  # NanoDates


module benchmark

__precompile__(true)
reload("j4pr")
using DataArrays
import j4pr

function benchmark_training(a::T where T<:j4pr.CellData, w::S where S<:j4pr.CellFun)

	a[:,1:2] |> w

	@printf("* %s:", w.tinfo)
	@time out = a|>w
end

function benchmark_execution(a::T where T<:j4pr.CellData, w::S where S<:j4pr.AbstractCell)

	out1 = [];
	out2 = [];
	out3 = [];

	adc = deepcopy(a)
	ad = DataArray(deepcopy(a))
	aa = deepcopy(j4pr.getx(a))

	@printf("* %s ...\n", w.tinfo)

	# Test datacell interface
	@printf("\tDataCell |> :")
	#try
		deepcopy(adc[:,1:10]) |> w;
		@time out1 = adc |> w
	#catch
	#	@printf("FAIL\n")
	#end

	# Test DataArray interface
	#@printf("\tDataArray |> :")
	#try
	#	deepcopy(ad[:,1:10]) |>w
	#	@time out2 = ad |> w
	#catch
	#	@printf("FAIL\n")
	#end

	# Test Array interface
	@printf("\tArray |> :")
	try
		deepcopy(aa[:,1:10]) |>w
		@time out3 = aa |> w
	catch
		@printf("FAIL\n")
	end

	# Test returned values
	try
		all(out1.x.==out3) ? @printf("\tValues check OK.") : begin
			@printf("\tValues check failed.")
			# Specify which version(s) disagree ...
		end
	catch
		@printf("\tCould not execute values check.")
	end

	@printf("\n\n")
	return out1, out2, out3
end

function benchmark_execution_pipe(a::T where T<:j4pr.CellData, w::S where S<:j4pr.PipeGeneric)
	@printf("\n\n=============== Testing %s =========================\n",w.tinfo)
	@printf("Compiling pipe elements ...");
	a[:,1] |> w;

	@printf("Passing %s data through pipe ...", typeof(a));
	@time a |> w

end

function run_benchmarks_basic()

	@printf("\n\nRunning some basic tests ...");

	# Get some dataset
	#A= dgenr("datasets", "iris")

	#A = datacell( rand(10000,100), round(rand(10000)*20) )
	#A=datacell( [[1,2,1,1,1] [1,1,0,0,0.0] ["a","v","x","v","a"]] )

	M=100;
	N=100;
	C = 3;
	A = j4pr.datacell( rand(M, N), round.((C-1)*rand(N)) );

	# Execution only cells
	W = [		j4pr.cslice([0,1]),
      			j4pr.filterdomain!(Dict(i=>[0 10] for i in size(A,1))),
			j4pr.sample(0.5),
	     ]
	# Trainable cells
	Wt = [		j4pr.scaler!("mean"),
			j4pr.filterg("mean"),
			j4pr.ohenc("binary"),
			j4pr.ohenc("integer"),
	     ]

	# Benchmark training for trainable cells
	@printf("\n\n=============== Training [Basic] =========================\n")
	out = Any[]
	for (i,w) in enumerate(Wt)
		push!(out, benchmark_training(A,w))
	end

	# Benchmark execution for executable only cells
	@printf("\n\n=============== Testing [Basic] =========================\n")
	for w in W
		#@show w.m
		benchmark_execution(A,w)
	end

	# Benchmark execution for trainable cells
	for w in out
		benchmark_execution(A,w)
	end

end

function run_benchmarks_pipe()

	@printf("\n\nRunning some more advanced tests ...");

	# Get some dataset
	#A= dgenr("datasets", "iris")

	#A = datacell( rand(100,10000), round.(rand(10000)*20) )
	#A=datacell( [[1,2,1,1,1] [1,1,0,0,0.0] ["a","v","x","v","a"]] )
	M=100;
	N=10000;
	C = 3;
	A = 	j4pr.datacell( round.(50*rand(M, N)), round.((C-1)*rand(N)) ); # training dataset
	At = 	j4pr.datacell( round.(60*rand(M, N)), round.((C-1)*rand(N)) ); # testing dataset

	#Build pipe

	Wclass =	j4pr.cslice([0,1])
	Wdom = 		j4pr.filterdomain!(Dict(i=>[0 1] for i in 1:50))
	Wprocmv = 	j4pr.filterg("majority")
	Wremove =	j4pr.FunctionCell(getindex, (setdiff(1:100,collect(1:5)),:),"Data remover")
	Wsample =	j4pr.sample(0.5)
	Wuscale = 	j4pr.scaler!(Dict(i=>"mean" for i in 1:50))
	Wbu =		j4pr.ohenc("binary")
	Wiu	=	j4pr.ohenc("integer")

	# Train pipe
	@printf("\nPreliminary training ...\n")
	Wb = A |> Wbu
	PIPE = deepcopy(A) |> (deepcopy(A) |> (Wb+Wremove+Wdom+Wprocmv+Wuscale+Wsample))

	benchmark_execution(At,PIPE)
	return A, At, PIPE
end

run_benchmarks_basic();

a,at,pipe = run_benchmarks_pipe()

end


using LimitOrderBook
using Profile
using Printf
using DataStructures

function generate_orderbook(max_price)
    ob = OrderBook(UInt128(max_price), UInt128(1))
end

#Profile.clear()
#@profile generate_orderbook(UInt128(100000))
#Profile.print(format=:flat)

for max_price in [1e2, 1e6, 1e9]
    @printf "Creates orderbook with max price:%.0E \n" max_price
    @time generate_orderbook(max_price)
end

function send_passive_limit_orders(iterations)
    max_price = UInt128(100000)
    min_price = UInt128(1)
    ob = OrderBook(max_price, min_price)
    t = time()
    for i in 1:iterations
        for price in min_price + 1:max_price - 1
            limitorder!(ob, BUY, price, UInt128(1), UInt128(1))
        end
    end
    @printf "Passive limit orders per second:%.2E \n" (iterations*max_price)/(time() - t)
end

println("1 iterations")
@time send_passive_limit_orders(1)

println("10 iterations")
@time send_passive_limit_orders(10)

#Profile.clear()
#@profile send_passive_limit_orders()
#Profile.print(format=:flat)

function cancel_orders(iterations)
    max_price = UInt128(100000)
    min_price = UInt128(1)
    ob = OrderBook(max_price, min_price)
    order_ids = Deque{UInt128}()
    for i in 1:iterations
        for price in min_price + 1:max_price - 1
            trades, order_id = limitorder!(ob, BUY, price, UInt128(1), UInt128(1))
            push!(order_ids, order_id)
        end
    end

    n_orders = order_ids.len
    t = time()
    for order_id in order_ids
        cancel!(ob, order_id)
    end
    @printf "Canceled %.0E orders per second \n" n_orders/(time() - t)

end

println("Cancels orders: 1 iteration")
@time cancel_orders(1)

println("Cancels orders: 10 iteration")
@time cancel_orders(10)

function match_orders(iterations)
    max_price = UInt128(100000)
    min_price = UInt128(1)
    ob = OrderBook(max_price, min_price)
    order_ids = Deque{UInt128}()
    size = UInt128(1)
    n_orders = UInt128(1)
    for i in 1:iterations
        for price in min_price + 1:max_price - 1
            trades, order_id = limitorder!(ob, BUY, price, size, UInt128(1))
            n_orders = n_orders + 1
        end
    end

    # starts selling
    t = time()
    for i in 1:n_orders - 2
        limitorder!(ob, SELL, min_price, size, UInt128(1))
    end
    @printf "Matched %.3E orders per second \n" n_orders/(time() - t)
end

println("Matches orders: 1 iteration")
@time match_orders(1)

println("Matches orders: 10 iteration")
@time match_orders(10)

function match_large_order(iterations)
    max_price = UInt128(100000)
    min_price = UInt128(1)
    ob = OrderBook(max_price, min_price)
    order_ids = Deque{UInt128}()
    size = UInt128(1)
    n_orders = UInt128(1)
    for i in 1:iterations
        for price in min_price + 1:max_price - 1
            trades, order_id = limitorder!(ob, BUY, price, size, UInt128(1))
            n_orders = n_orders + 1
        end
    end
    t = time()
    limitorder!(ob, SELL, min_price, size*(n_orders - 2), UInt128(1))
    @printf "Matched %.0E orders per second with one order \n" n_orders/(time() - t)
end

println("Matches orders with one large order: 1 iteration")
@time match_large_order(1)

println("Matches orders with one large order: 10 iteration")
@time match_large_order(10)


@testset "Plotting" begin
  Random.seed!(123)
  z₁ = randn(10000)
  z₂ = z₁ + randn(10000)
  d = georef((z₁=z₁,z₂=z₂), CartesianGrid(100,100))

  agri = georef(CSV.File(joinpath(datadir,"agriculture.csv")), (:x,:y))

  if visualtests
    @test_reference "data/agriculture.png" plot(agri, (:band4,:crop), ms=0.2)
  end

  @testset "HScatter" begin
    if visualtests
      sdata = georef(CSV.File(joinpath(datadir,"samples2D.tsv")), (:x,:y))
      p0 = hscatter(sdata, :value, lag=0)
      p1 = hscatter(sdata, :value, lag=1)
      p2 = hscatter(sdata, :value, lag=2)
      p3 = hscatter(sdata, :value, lag=3)
      plt = plot(p0, p1, p2, p3, layout=(2,2), size=(600,600))
      @test_reference "data/hscatter.png" plt
    end
  end
end


module ScSTO

using OptiMo

export scstoproblem

include("types.jl")
include("interface.jl")
include("l0norm.jl")
include("evaluator.jl")
include("simulation.jl")

end


import Libdl
import Random: rand, seed!
import RandomNumbers: AbstractRNG

const librandom123 = Libdl.find_library(["librandom123"],
                                        [joinpath(dirname(@__FILE__), "../deps/")])

"True when AES-NI library has been compiled."
const R123_USE_AESNI = librandom123 != ""

const R123Array1x{T<:Union{UInt128}}        = NTuple{1, T}
const R123Array2x{T<:Union{UInt32, UInt64}} = NTuple{2, T}
const R123Array4x{T<:Union{UInt32, UInt64}} = NTuple{4, T}

"The base abstract type for RNGs in [Random123 Family](@ref)."
abstract type AbstractR123{T<:Union{UInt32, UInt64, UInt128}} <: AbstractRNG{T} end

"RNG that generates one number at a time."
abstract type R123Generator1x{T} <: AbstractR123{T} end
"RNG that generates two numbers at a time."
abstract type R123Generator2x{T} <: AbstractR123{T} end
"RNG that generates four numbers at a time."
abstract type R123Generator4x{T} <: AbstractR123{T} end

"Set the counter of a Random123 RNG."
@inline function set_counter!(r::R123Generator1x{T}, ctr::Integer) where T <: UInt128
    r.ctr = ctr % T
    random123_r(r)
    r
end

@inline function set_counter!(r::AbstractR123{T}, ctr::Integer) where T <: Union{UInt32, UInt64}
    r.p = 0
    r.ctr1 = ctr % T
    random123_r(r)
    r
end

@inline function rand(r::R123Generator1x{T}, ::Type{T}) where T <: UInt128
    r.ctr += 1
    random123_r(r)
    r.x
end

@inline function rand(r::R123Generator2x{T}, ::Type{T}) where T <: Union{UInt32, UInt64}
    if r.p == 1
        r.ctr1 += 1
        random123_r(r)
        r.p = 0
        return r.x2
    end
    r.p = 1
    r.x1
end

@inline function rand(r::R123Generator4x{T}, ::Type{T}) where T <: Union{UInt32, UInt64}
    if r.p == 4
        r.ctr1 += 1
        random123_r(r)
        r.p = 0
    end
    r.p += 1
    unsafe_load(Ptr{T}(pointer_from_objref(r)), r.p)
end

@inline function rand(r::R123Generator1x{T}, ::Type{R123Array1x{T}}) where T <: UInt128
    r.ctr += 1
    random123_r(r)
end

@inline function rand(r::R123Generator2x{T}, ::Type{R123Array2x{T}}) where T <: Union{UInt32, UInt64}
    if r.p > 0
        r.ctr1 += 1
    end
    ret = random123_r(r) # which returns a Tuple{T, T}
    r.p = 1
    ret
end

@inline function rand(r::R123Generator4x{T}, ::Type{R123Array4x{T}}) where T <: Union{UInt32, UInt64}
    if r.p > 0
        r.ctr1 += 1
    end
    ret = random123_r(r)
    r.p = 4
    ret
end

for (T, DT) in ((UInt32, UInt64), (UInt64, UInt128))
    @eval @inline function rand(r::R123Generator2x{$T}, ::Type{$DT})
        if r.p == 1
            r.ctr1 += 1
            random123_r(r)
        end
        r.p = 1
        unsafe_load(Ptr{$DT}(pointer_from_objref(r)), 1)
    end
end

@inline function rand(r::R123Generator4x{UInt32}, ::Type{UInt128})
    if r.p > 0
        r.ctr1 += 1
        random123_r(r)
    end
    r.p = 4
    unsafe_load(Ptr{UInt128}(pointer_from_objref(r)), 1)
end

"Do one iteration and return the a tuple of a Random123 RNG object."
random123_r


using Logging, WeightsAndBiasLogger

args = (n_epochs=1_000, lr=1e-3)

logger = WBLogger(project="sample-project")

config!(logger, args)

with(logger) do
    loss = 0
    for i in 1:args.n_epochs
        loss += randn() * args.lr
        @info "train" i=i loss=loss
    end
end


module Fields

export
    Face, Cell,
    AbstractField, AbstractLocatedField,
    Field, CellField, FaceFieldX, FaceFieldY, FaceFieldZ,
    interior, interiorparent,
    xnode, ynode, znode, location,
    set!,
    VelocityFields, TracerFields, tracernames, PressureFields, Tendencies

include("field_utils.jl")
include("field.jl")
include("set!.jl")
include("field_tuples.jl")
include("show_fields.jl")

end


abstract LaneFollowingDriver <: DriverModel{LaneFollowingAccel}
track_longitudinal!(model::LaneFollowingDriver, v_ego::Float64, v_oth::Float64, headway::Float64) = model # do nothing by default

function observe!(model::LaneFollowingDriver, scene::Scene1D, roadway::StraightRoadway, egoid::Int)

    vehicle_index = findfirst(scene, egoid)

    fore_res = get_neighbor_fore(scene, vehicle_index, roadway)

    v_ego = scene[vehicle_index].state.v
    v_oth = scene[fore_res.ind].state.v
    headway = fore_res.Δs

    track_longitudinal!(model, v_ego, v_oth, headway)

    return model
end


using MimiFUND
using Mimi
using Test
using DataFrames
using CSVFiles

@testset "fund" begin

#------------------------------------------------------------------------------
#   1. Run the whole model
#------------------------------------------------------------------------------

@testset "fund-model" begin

#default model exported by fund module
default_nsteps = 1050
m = MimiFUND.get_model()
run(m)
@test Mimi.time_labels(m) == collect(1950:1:1950+default_nsteps)

#default model created by MimiFUND.get_model()
m1 = MimiFUND.get_model()
run(m1)
@test Mimi.time_labels(m1) == collect(1950:1:1950+default_nsteps)

#use optional args for MimiFUND.get_model()
new_nsteps = 10
@test_throws ErrorException m2 = MimiFUND.get_model(nsteps = new_nsteps) #should error because parameter lenghts won't match time dim

end #fund-model testset

#------------------------------------------------------------------------------
#   2. Run tests to make sure integration version (Mimi v0.5.0)
#   values match Mimi 0.4.0 values
#------------------------------------------------------------------------------
@testset "test-integration" begin

Mimi.reset_compdefs()

m = MimiFUND.get_model()
run(m)

missingvalue = -999.999
err_number = 1.0e-9
err_array = 0.0

for c in map(name, Mimi.compdefs(m)), v in Mimi.variable_names(m, c)

    #load data for comparison
    filename = joinpath(@__DIR__, "../contrib/validation_data_v040/$c-$v.csv")
    results = m[c, v]

    df = load(filename) |> DataFrame
    if typeof(results) <: Number
        validation_results = df[1,1]
        @test results ≈ validation_results atol = err_number #slight imprecision with these values due to rounding

    else
        validation_results = convert(Matrix, df)

        #replace missings with missingvalue so they can be compared
        results[ismissing.(results)] .= missingvalue
        validation_results[ismissing.(validation_results)] .= missingvalue

        #match dimensions
        if size(validation_results,1) == 1
            validation_results = validation_results'
        end

        @test results ≈ validation_results atol = err_array

    end
end

end #fund-integration testset

#------------------------------------------------------------------------------
# 3. Test marginal damages functions (test that each function does not error)
#------------------------------------------------------------------------------

@testset "test-marginaldamages" begin

# Test functions from file "new_marginaldamages.jl"

# Test the compute_scc function with various keyword arguments
scc1 = MimiFUND.compute_scc(year = 2020)
@test scc1 isa Float64   # test that it's not missing or a NaN
scc2 = MimiFUND.compute_scc(year = 2020, last_year=2300)
@test scc2 < scc1  # test that shorter horizon made it smaller
scc3 = MimiFUND.compute_scc(year = 2020, last_year=2300, equity_weights=true)
@test scc3 > scc2  # test that equity weights made itbigger
scc4 = MimiFUND.compute_scc(year = 2020, last_year=2300, equity_weights=true, eta=.8, prtp=0.01)
@test scc4 > scc3   # test that lower eta and prtp make scc higher
scc5 = MimiFUND.compute_scc(year = 2020, gas=:CH4)
@test scc5 > scc1   # test social cost of methane is higher

# Test with a modified model
m = MimiFUND.get_model()
update_param!(m, :climatesensitivity, 5)
scc6 = MimiFUND.compute_scc(m, year=2020, last_year=2300)
@test scc6 > scc2 # test that it's higher than the default because of a higher climate sensitivity

# Test get_marginal_model
mm = MimiFUND.get_marginal_model(year=2020, gas=:CH4)
run(mm)

# Test compute_scc_mm
result = MimiFUND.compute_scc_mm(year=2050)
@test result.scc isa Float64
@test result.mm isa Mimi.MarginalModel

# Test old exported versions of the functions
scc = MimiFUND.get_social_cost()
md = MimiFUND.getmarginaldamages()

end #marginaldamages testset

#------------------------------------------------------------------------------
# 4. Run basic test of Marginal Damages and MCS functionality
#------------------------------------------------------------------------------

@testset "test-mcs" begin

# mcs
MimiFUND.run_fund_mcs(10)        # Run 10 trials of basic FUND MCS
MimiFUND.run_fund_scc_mcs(10)    # Run 10 trials of FUND MCS SCC calculations

end #test-mcs testset

end #fund testset

nothing


"""Create a `storage` array for [`propagate`](@ref).

```julia
storage = init_storage(state, tlist)
```

creates a storage array suitable for storing a `state` for each point in
`tlist`.

```julia
storage = init_storage(state, tlist, observables))
```

creates a storage array suitable for the data generated by the `observables`
applied to `state`, see [`map_observables`](@ref), for each point in `tlist`.

```julia
storage = init_storage(data, nt))
```

creates a storage arrays suitable for storing `data` nt times, where
`nt=length(tlist)`. By default, this will be a vector of `typeof(data)` and
length `nt`, or a `n × nt` Matrix with the same `eltype` as `data` if `data` is
a Vector of length `n`.
"""
function init_storage(state, tlist::AbstractVector)
    nt = length(tlist)
    return init_storage(state, nt)
end

function init_storage(state, tlist, observables)
    data = map_observables(observables, state)
    nt = length(tlist)
    return init_storage(data, nt)
end

init_storage(data, nt::Integer) = Vector{typeof(data)}(undef, nt)

init_storage(data::Vector, nt::Integer) = Matrix{eltype(data)}(undef, length(data), nt)

"""Obtain "observable" data from `state`.

```julia
data = map_observables(observables, state)
```

calculates the data for a tuple of `observables` applied to `state`.
For a single observable (tuple of length 1), simply return the result of
[`map_observable`](@ref).

For multiple observables, return the tuple resulting from applying
[`map_observable`](@ref) for each observable. If the tuple is "uniform" (all
elements are of the same type, e.g. if each observable calculates the
expectation value of a Hermitian operator), it is converted to a Vector. This
allows for compact storage in a storage array, see [`init_storage`](@ref).
"""
function map_observables(observables, state)
    if length(observables) == 1
        return map_observable(observables[1], state)
    else
        val_tuple = Tuple(map_observable(O, state) for O in observables)
        uniform_type = typeof(val_tuple[1])
        is_uniform = all(typeof(v) == uniform_type for v in val_tuple[2:end])
        if is_uniform
            return collect(val_tuple)  # convert to Vector
        else
            return val_tuple
        end
    end
end

"""Apply a single `observable` to `state`.

```julia
data = map_observable(observable, state)
```

By default, `observable` is assumed to be callable, and the above is equivalent
to `data = observable(state)`.

If `observable` is a matrix and `state` is a vector evaluate the expectation
value of the observable as `dot(state, observable, state)`.

"""
function map_observable(observable, state)
    return observable(state)
end

function map_observable(observable::AbstractMatrix, state::AbstractVector)
    return dot(state, observable, state)
end

"""Place data into `storage` for time slot `i`.

```julia
    write_to_storage!(storage, i, state, observables)
```

For a `storage` array created by [`init_storage`](@ref), store the data obtains
from [`map_observables`](@ref) into the `storage` for time slot `i`. This
delegates to the more general

```julia
    write_to_storage!(storage, i, data)
```

Conceptually, this corresponds roughly to `storage[i] = data`, but `storage`
may have its own idea on how to store data for a specific time slot. For
example, with the default [`init_storage`](@ref) Vector data will be stored in
a matrix, and `write_to_storage!` will in this case write data to the i'th
column of the matrix.

For a given type of `storage` and `data`, it is the developer's responsibility
that `init_storage` and `write_to_storage!` are compatible.
"""
function write_to_storage!(storage, i::Integer, state, observables)
    data = map_observables(observables, state)
    write_to_storage!(storage, i, data)
end

function write_to_storage!(storage::AbstractVector, i::Integer, data)
    storage[i] = data
end

function write_to_storage!(storage::Matrix{T}, i::Integer, data::Vector{T}) where T
    storage[:, i] .= data
end

"""Obtain data from storage

```julia
get_from_storage!(state, storage, i)
```

extracts data from the `storage` for the i'th time slot. Invese of
[`write_to_storage!`](@ref)
"""
get_from_storage!(state, storage::AbstractVector, i) = copyto!(state, storage[i])
get_from_storage!(state, storage::Matrix, i) = copyto!(state, storage[:, i])


###
### push
###
function Base.push!(a::IndicesVector, items...)
    push_index!(a, items...)
    push!(parent(a), items...)
end
function Base.push!(x::IndicesVector, items::Pair...)
    push_index!(x, items...)
    for i in items
        k, v = i
        push!(parent(x), v)
    end
    return x
end

###
### push
###
function StaticRanges.push(x::IndicesVector, items...)
    return rebuild(x, push_index(x, items...), push(parent(x), items...))
end
function StaticRanges.push(x::IndicesVector, items::Pair...)
    p = parent(x)
    for i in items
        k, v = i
        p = push(parent(x), v)
    end
    return rebuild(x, p, push_index(x, items...))
end

###
### pushfirst!
###
function Base.pushfirst!(x::IndicesVector, items...)
    pushfirst_index!(x, items...)
    pushfirst!(parent(x), items...)
end
function Base.pushfirst!(x::IndicesVector, items::Pair...)
    pushfirst_index!(x, items...)
    for i in items
        k, v = i
        pushfirst!(parent(x), v)
    end
    return x
end

###
### pushfirst
###
function StaticRanges.pushfirst(x::IndicesVector, items...)
    return rebuild(x, pushfirst_index(x, items...), pushfirst(parent(x), items...))
end
function StaticRanges.pushfirst(x::IndicesVector, items::Pair...)
    p = parent(x)
    for i in items
        k, v = i
        p = pushfirst(parent(x), v)
    end
    return rebuild(x, p, pushfirst_index(x, items...))
end

###
### pop[!]
###
function Base.pop!(a::IndicesVector)
    pop_index!(axes(a, 1))
    return pop!(parent(a))
end

function StaticRanges.pop(a::IndicesVector)
    return rebuild(a, pop(parent(a)), (pop_index(axes(a, 1)),))
end

###
### popfirst[!]
###
function Base.popfirst!(a::IndicesVector)
    popfirst_index!(axes(a, 1))
    return popfirst!(parent(a))
end

function StaticRanges.popfirst(a::IndicesVector)
    return rebuild(a, popfirst(parent(a)), (popfirst_index(axes(a, 1)),))
end