# IntervalTree map operations

typealias IntervalMap{K, V} IntervalTree{K, IntervalValue{K, V}}

function Base.setindex!{K, V, B}(t::IntervalBTree{K, V, B}, value,
                                 key::Tuple{Any, Any})
    push!(t, V(key[1], key[2], value), true)
end

function Base.setindex!{K, V, B}(t::IntervalBTree{K, V, B}, value,
                                 key::Interval{K})
    push!(t, V(key.first, key.last, value), true)
end

function Base.setindex!{K, V, B}(t::IntervalBTree{K, V, B}, value,
                                 first, last)
    push!(t, V(first, last, value), true)
end

function Base.getindex{K, V, B}(t::IntervalBTree{K, V, B}, key::AbstractInterval{K})
    return _getindex(t.root, key)
end

function _getindex{K, V, B}(t::InternalNode{K, V, B}, key::AbstractInterval{K})
    i = findidx(t, key)
    if 1 <= length(t) - 1 && key >= t.keys[i]
        return _getindex(t.children[i+1], key)
    else
        return _getindex(t.children[i], key)
    end
end

function _getindex{K, V, B}(t::LeafNode{K, V, B}, key::AbstractInterval{K})
    i = findidx(t, key)
    if 1 <= i <= length(t) && first(t.entries[i]) == first(key) &&
        last(t.entries[i]) == last(key)
        return t.values[i]
    else
        error(KeyError((key.a, key.b)))
    end
end

function Base.get{K, V, B}(t::IntervalBTree{K, V, B}, key::Tuple{Any, Any}, default)
    return _get(t.root, Interval{K}(key[1], key[2]), default)
end

function Base.get{K, V, B}(t::IntervalBTree{K, V, B}, key::AbstractInterval{K}, default)
    return _get(t.root, key, default)
end

function _get{K, V, B}(t::InternalNode{K, V, B}, key::AbstractInterval{K}, default)
    i = findidx(t, key)
    if 1 <= length(t) - 1 && key >= t.keys[i]
        return _get(t.children[i+1], key, default)
    else
        return _get(t.children[i], key, default)
    end
end

function _get{K, V, B}(t::LeafNode{K, V, B}, key::AbstractInterval{K}, default)
    i = findidx(t, key)
    if 1 <= i <= length(t) &&
        first(t.entries[i]) == first(key) && last(t.entries[i]) == last(key)
        return t.entries[i]
    else
        return default
    end
end

function Base.get!{K, V, B}(t::IntervalBTree{K, V, B}, key::Tuple{Any, Any}, default)
    return push!(t, V(key[1], key[2], default), true, false)
end

function Base.get!{K, V, B}(t::IntervalBTree{K, V, B}, key::AbstractInterval{K}, default)
    return push!(t, V(key.first, key.last, default), true, false)
end

function Base.delete!{K, V, B}(t::IntervalBTree{K, V, B}, first, last)
    return deletefirst!(t, first, last)
end

function Base.delete!{K, V, B}(t::IntervalBTree{K, V, B}, key::Tuple{Any, Any})
    return deletefirst!(t, key)
end

function Base.delete!{K, V, B}(t::IntervalBTree{K, V, B}, key::Interval{K})
    return deletefirst!(t, key)
end

immutable IntervalKeyIterator{K, V, B}
    t::IntervalBTree{K, V, B}
end

function Base.keys(t::IntervalBTree)
    return IntervalKeyIterator(t)
end

function Base.start{K, V, B}(it::IntervalKeyIterator{K, V, B})
    # traverse to the first leaf node
    node = it.t.root
    while !isa(node, LeafNode{K, V, B})
        node = node.children[1]
    end

    return IntervalBTreeIteratorState(Nullable(node), 1)
end

function Base.next{K, V, B}(t::IntervalKeyIterator{K, V, B},
                            state::IntervalBTreeIteratorState{K, V, B})
    leaf = get(state.leaf)
    key = Interval{K}(first(leaf.entries[state.i]),
                      last(leaf.entries[state.i]))
    if state.i < length(leaf)
        state = IntervalBTreeIteratorState{K, V, B}(leaf, state.i + 1)
    else
        state = IntervalBTreeIteratorState{K, V, B}(leaf.right, 1)
    end
    return key, state
end

function Base.done{K, V, B}(t::IntervalKeyIterator{K, V, B},
                            state::IntervalBTreeIteratorState{K, V, B})
    return isnull(state.leaf) || isempty(get(state.leaf))
end

immutable IntervalValueIterator{K, V <: IntervalValue, B}
    t::IntervalBTree{K, V, B}
end

function Base.values{K, V <: IntervalValue, B}(t::IntervalBTree{K, V, B})
    return IntervalValueIterator(t)
end

function Base.start{K, V, B}(it::IntervalValueIterator{K, V, B})
    # traverse to the first leaf node
    node = it.t.root
    while !isa(node, LeafNode{K, V, B})
        node = node.children[1]
    end

    return IntervalBTreeIteratorState(Nullable(node), 1)
end

function Base.next{K, V, B}(t::IntervalValueIterator{K, V, B},
                            state::IntervalBTreeIteratorState{K, V, B})
    leaf = get(state.leaf)
    value = leaf.entries[state.i].value
    if state.i < length(leaf)
        state = IntervalBTreeIteratorState{K, V, B}(leaf, state.i + 1)
    else
        state = IntervalBTreeIteratorState{K, V, B}(leaf.right, 1)
    end
    return value, state
end

function Base.done{K, V, B}(t::IntervalValueIterator{K, V, B},
                            state::IntervalBTreeIteratorState{K, V, B})
    return isnull(state.leaf) || isempty(get(state.leaf))
end


module TiledArrays

# Ceiling division
cdiv(a::Integer, b::Integer) = cdiv(promote(a, b)...)
cdiv(a::T, b::T) where {T<:Integer} = one(T) + div(a - one(T), b)

# Opposite of `Base.tail`.
# Keep all but the last element in a tuple.
head(x, y...) = (x, head(y...)...)
head(x) = ()

# # Type traits for describing if there's something scary going on with the strides.
# # For now, assume memory strides are monotonically increasing such that the underlying
# # array describes a contiguous block of memory.
# abstract type AbstractStrides end
# struct DenseStrides <: AbstractStrides end

struct Tile
    dim::Int
    size::Int
end
Base.show(io::IO, tile::Tile) = print(io, "Tile($(tile.dim), $(tile.size))")

# Note that `NonBlockedLayout <: MemoryLayout`, but without any tiling, allowing
# for specific handling of the easy case.
const MemoryLayout{N} = Tuple{Vararg{Union{Int,Tile},N}} where {N}
const NonBlockedLayout{N} = Tuple{Vararg{Int,N}} where {N}

# Utility function for removing all "Tile"s from the front of an array.
function strip_leading_tiles(x::Tuple{Vararg{Union{Int,Tile},N}}) where {N}
    return strip_leading_tiles(x...)
end

function strip_leading_tiles(x::Tile, y::Vararg{Union{Int,Tile},N}) where {N}
    return strip_leading_tiles(y...)
end

strip_leading_tiles(x::Int, y::Vararg{Union{Int,Tile},N}) where {N} = (x, y...)

#####
##### Layout Validation
#####

# Validation
# Invariants to maintain:
#
# 1. We must have exactly one occurance of an Integer index.
# 2. All `Tile` indexes must occur BEFORE the corresponding Integer index.
# 3. Each `Tile` index must have a corresponding Integer index
function validate(layout::MemoryLayout{N}) where {N}
    @nospecialize

    # Pass 1 - Make sure there are no nonpositive indices.
    # Also, find the maximum index.
    maximum_index = 0
    for desc in layout
        if isa(desc, Int)
            desc <= 0 && error("Found non-positive index: $desc")
            maximum_index = max(maximum_index, desc)
        elseif isa(desc, Tile)
            desc.dim <= 0 && error("Found non-positive tile index: $desc")
            maximum_index = max(maximum_index, desc.dim)
        else
            # Should never happen due to type restrictions.
            error("Unreachable reached")
        end
    end

    # Pass 2 - Check invariants.
    for index in Base.OneTo(maximum_index)
        saw_int = false
        saw_tile = false
        for desc in layout
            if isa(desc, Int) && desc == index
                saw_int && error("Integer index $index occurs more than once!")
                saw_int = true
            elseif isa(desc, Tile) && desc.dim == index
                saw_int && error(
                    "Integer index $index occurs before its corresponding tile index!"
                )
                saw_tile = true
            end
        end
        # We saw a tile but not an int, throw error
        saw_tile &&
            !saw_int &&
            error("Found tile with index $index with no corresponding Int")

        # We didn't find anything
        !saw_tile && !saw_int && error("Found no entry with index $index")
    end
    return true
end

#####
##### Adjust indexes for padding.
#####

unchecked_divrem(x::Int, y::Int) = (Base.sdiv_int(x, y), Base.srem_int(x, y))

# Note: not necessarily safe since we are using the unchecked integer division functions.
# Also, `index` should already be converted from index-1 to index-0.
function adjust_for_padding(
    size::NTuple{N,Int}, padded_size::NTuple{N,Int}, index::NTuple{N,Int}
) where {N}
    return ntuple(i -> adjust_for_padding(size[i], padded_size[i], index[i]), Val(N))
end

function adjust_for_padding(size::Int, padded_size::Int, index::Int)
    size == padded_size && return index
    a, b = unchecked_divrem(index, size)
    return (a * padded_size) + b
end

#####
##### Index Conversion Logic
#####

# Expand the index according to the given layout.
# NOTE: `I` should be post padding adjusting if necessary.
function splitindex(::Val{T}, I::Tuple{Vararg{Int,N}}) where {T,N}
    return _splitindex(T, I)
end

# Work from the first index to the last.
# At each step, strip off the index we processed from `layout` and recurse.
#
# When `layout[1]` is an Integer, we just return the corresponding index from `I`.
#
# When `layout[1]` is a Tile, then we grab the corresponding index from `I`,
# `divrem` that value, then reconstruct `I` with the reduced index.
#
# NB: This depends on Julia's super agressive constant propagation heuristics when dealing
# with simplifying tuple recursive functions to generate great assembly code.
@inline function _splitindex(layout::MemoryLayout, I::Tuple{Vararg{Int,N}}) where {N}
    v, _I = process(layout[1], I)
    return (v, _splitindex(Base.tail(layout), _I)...)
end

### Base case - last item should never be a `Tile`.
@inline function _splitindex(layout::Tuple{Int}, I::Tuple{Vararg{Int,N}}) where {N}
    return (I[layout[1]],)
end

function _splitindex(::Tuple{Tile}, I::Tuple{Vararg{Int,N}}) where {N}
    return error("Malformed memory layout with a `Tile` in the tail position!")
end

@inline process(x::Int, I::Tuple{Vararg{Int,N}}) where {N} = (I[x], I)
@inline function process(x::Tile, I::Tuple{Vararg{Int,N}}) where {N}
    outer, inner = unchecked_divrem(I[x.dim], x.size)
    return (inner, ntuple(i -> i == x.dim ? outer : I[i], Val(N)))
end

#####
##### Obtaining full dimensions
#####

# Move layout from type domain to value domain, providing "const" information to the
# compiler.
@inline dims(valT::Val{T}, size::Tuple{Vararg{Int,N}}) where {T,N} = _dims(T, size)

@inline function _dims(layout::MemoryLayout, size::Tuple{Vararg{Int,N}}) where {N}
    v, _size = _dimcheck(layout[1], size)
    return (v, _dims(Base.tail(layout), _size)...)
end

# Bottom of recursion.
@inline _dims(layout::Tuple{Int}, size::Tuple{Vararg{Int,N}}) where {N} = (size[layout[1]],)

@inline _dimcheck(x::Int, size::Tuple{Vararg{Int,N}}) where {N} = (size[x], size)
@inline function _dimcheck(x::Tile, size::Tuple{Vararg{Int,N}}) where {N}
    newdim = cdiv(size[x.dim], x.size)
    return (x.size, ntuple(i -> i == x.dim ? newdim : size[i], Val(N)))
end

#####
##### Users of "dims"
#####

# NB: Cannot extend `Base.strides` because a `MemoryLayout` can potentially just be a tuple
# of Ints, so we'd be committing type piracy.
function _strides(valT::Val{T}, size::Tuple{Vararg{Int,N}}) where {T,N}
    # Drop the last value from `dims`.
    return cumprod((one(Int), head(dims(valT, size)...)...))
end

function fullsize(valT::Val{T}, size::Tuple{Vararg{Int,N}}) where {T,N}
    # Constant propagation to the rescue.
    # If the layout is non-blocked, just standard or permuted, then there is no
    # over-allocation necessary.
    if isa(T, NonBlockedLayout)
        return prod(size)
        # Otherwise, we need to compute the full dimensions, which could include zero padding
        # to keep the inner blocksizes constant.
    else
        return prod(dims(valT, size))
    end
end

#####
##### Linear Offset
#####

function getoffset(
    valT::Val{T},
    size::Tuple{Vararg{Int,N}},     # Padded Size
    I::Tuple{Vararg{Int,N}},        # Post-padded index
) where {T,N}
    Base.@_inline_meta
    strides = _strides(valT, size)
    index = splitindex(valT, I)
    offset = zero(Int)
    for i in eachindex(strides)
        @inbounds offset += strides[i] * index[i]
    end
    return offset
end

# Generates indices into a tiled array.
# Parameters:
#
# `L`: Layout type parameter.
# `N`: Number of Dims.
struct TiledIndexer{L,N}
    size::NTuple{N,Int}
    padded_size::NTuple{N,Int}
end

function TiledIndexer{L}(size::NTuple{N,Int}, padded_size::NTuple{N,Int}) where {L,N}
    return TiledIndexer{L,N}(size, padded_size)
end

function genindex(x::TiledIndexer{L,N}, I::NTuple{N,Int}) where {L,N}
    return 1 + getoffset(
        Val(L), x.padded_size, adjust_for_padding(x.size, x.padded_size, I .- 1)
    )
end

# function genindex(x::TiledIndexer{L,N,Nothing}, I::NTuple{N,Int}) where {L,N}
#     return getoffset(Val(N), x.size, I)
# end

# #####
# ##### Tiled Array
# #####
#
# struct TiledArray{T,L,N} <: AbstractArray{T,N}
#     parent::Vector{T}
#     size::NTuple{N,Int}
# end
# Base.parent(A::TiledArray) = A.parent
#
# function TiledArray{T,L}(::UndefInitializer, size::Tuple{Vararg{Int,N}}) where {T,L,N}
#     # Allocate enough room for the whole layout.
#     parent = Vector{T}(undef, fullsize(Val(L), size))
#     return TiledArray{T,L,N}(parent, size)
# end
#
# layout(::Type{<:TiledArray{<:Any,L}}) where {L} = L
# layout(x::T) where {T<:TiledArray} = layout(T)
# vlayout(x) = Val(layout(x))
#
# # Array interface
# @inline Base.size(x::TiledArray) = x.size
# Base.IndexStyle(::Type{<:TiledArray}) = Base.IndexCartesian()
# Base.@propagate_inbounds function Base.getindex(
#     A::TiledArray{T,L,N}, I::Vararg{Int,N}; strides = nothing
# ) where {T,L,N}
#     @boundscheck checkbounds(A, I...)
#     offset = getoffset(vlayout(A), size(A), I, strides) + one(Int)
#     return @inbounds(getindex(parent(A), offset))
# end
#
# Base.@propagate_inbounds function Base.setindex!(
#     A::TiledArray{T,L,N}, v, I::Vararg{Int,N}; strides = nothing
# ) where {T,L,N}
#     @boundscheck checkbounds(A, I...)
#     offset = getoffset(vlayout(A), size(A), I, strides) + one(Int)
#     return @inbounds setindex!(parent(A), v, offset)
# end

end # module


# main-sc for movies
gmtset(FONT_TAG="14p,Helvetica-Bold")
grdimage("USEast_Coast.nc", proj="GMOVIE_COL0/MOVIE_COL1/160/210/55/0/36/34/MOVIE_WIDTH+", region=:g, I="int_US.nc", C="globe_US.cpt", X=0, Y=0)
plot("flight_path.txt", lw=1)


using MonteCarlo, MonteCarloObservable, StableDQMC
using Test
using LinearAlgebra, Random, Dates
using MonteCarlo: @bm, TimerOutputs

using MonteCarlo: vmul!, lvmul!, rvmul!, rdivp!, udt_AVX_pivot!, rvadd!, vsub!
using MonteCarlo: vmin!, vmininv!, vmax!, vmaxinv!, vinv!
using MonteCarlo: BlockDiagonal, CMat64, CVec64, StructArray

struct DummyModel <: MonteCarlo.Model
    lattice
end
DummyModel() = DummyModel(SquareLattice(2))
struct DummyField <: MonteCarlo.AbstractField end

# check elementwise, not matrix norm
function check(A::Array, B::Array, atol, rtol=atol)
    for (x, y) in zip(A, B)
        if !isapprox(x, y, atol=atol, rtol=rtol)
            @info "$x ≉ $y "
            return false
        end
    end
    true
end
function check(x::Number, y::Number, atol, rtol=atol)
    result = isapprox(x, y, atol=atol, rtol=rtol)
    result || @info "$x ≉ $y"
    result
end

# In case some test failed and left behind a .jld file
for f in readdir()
    if endswith(f, ".jld") || endswith(f, "jld2")
        @warn "Removing $f"
        rm(f)
    end
end

macro benchmark_test(name, code)
    TimerOutputs.timer_expr(MonteCarlo, true, name, code)
end

@testset "All Tests" begin
    println("Utilities")
    @time @testset "Utilities" begin
        @bm function test1(x, y)
            sleep(x+y)
        end
        @bm test2(x, y) = sleep(x+y)
        # @eval MonteCarlo ... will put the body of test3, test4 in the wrong scope
        # eval(timer_expr(...)) doesn't work
        function test3(x, y)
            @benchmark_test "test1(::Any, ::Any)" begin sleep(x+y) end
        end
        test4(x, y) = @benchmark_test "test2(::Any, ::Any)" begin sleep(x+y) end

        x = code_lowered(test1, Tuple{Float64, Float64})[1]
        y = code_lowered(test3, Tuple{Float64, Float64})[1]
        @test x.code == y.code

        x = code_lowered(test2, Tuple{Float64, Float64})[1]
        y = code_lowered(test4, Tuple{Float64, Float64})[1]
        @test x.code == y.code

        @test !MonteCarlo.timeit_debug_enabled()
        enable_benchmarks()
        @test MonteCarlo.timeit_debug_enabled()
        disable_benchmarks()
        @test !MonteCarlo.timeit_debug_enabled()
    end
    # 0.473232 seconds (778.10 k allocations: 47.050 MiB, 2.72% gc time, 99.50% compilation time)

    println("Lattices")
    @time @testset "Lattices" begin
        include("lattices.jl")
    end
    # 22.329261 seconds (51.70 M allocations: 2.487 GiB, 3.09% gc time, 63.07% compilation time)

    println("Model")
    @time @testset "Models" begin
        include("modeltests_IsingModel.jl")
        include("modeltests_HubbardModel.jl")
    end
    # 11.418479 seconds (29.34 M allocations: 1.301 GiB, 4.78% gc time, 7.60% compilation time)

    println("Linear Algebra")
    @time @testset "Linear Algebra" begin
        include("linalg.jl")
    end
    # 72.325652 seconds (119.03 M allocations: 5.922 GiB, 3.30% gc time, 33.24% compilation time)

    println("Slice Matrices")
    @time @testset "Slice Matrices" begin
        include("slice_matrices.jl")
    end
    # 5.212298 seconds (7.85 M allocations: 424.303 MiB, 1.66% gc time, 96.86% compilation time)

    println("DQMC")
    @time @testset "Flavors" begin
        include("flavortests_DQMC.jl")
    end
    # 27.297888 seconds (55.24 M allocations: 3.149 GiB, 2.60% gc time, 2.67% compilation time)

    println("Fields")
    @time @testset "Fields" begin
        include("fields.jl")
    end

    println("Scheduler")
    @time @testset "Scheduler & (DQMC) Updates" begin
        include("updates.jl")
    end
    # 17.197345 seconds (33.06 M allocations: 1.846 GiB, 2.53% gc time, 4.69% compilation time)

    println("Measurement")
    @time @testset "Measurements" begin
        include("measurements.jl")
    end
    # 20.091780 seconds (37.56 M allocations: 1.877 GiB, 2.46% gc time, 1.43% compilation time)

    println("Integration")
    @time @testset "Integration tests" begin
        include("integration_tests.jl")
    end
    # 11.359702 seconds (19.67 M allocations: 1.132 GiB, 2.00% gc time, 28.81% compilation time)

    println("ED")
    @time @testset "Exact Diagonalization" begin
        include("ED/ED_tests.jl")
    end
    # 71.714791 seconds (52.37 M allocations: 14.521 GiB, 1.82% gc time, 4.57% compilation time)

    println("File IO")
    @time @testset "File IO" begin
        include("FileIO.jl")
    end
    # 72.591676 seconds (86.55 M allocations: 4.172 GiB, 1.27% gc time, 9.25% compilation time)
end


# This file is a part of SimilaritySearch.jl

# Database interface
using Random

export AbstractDatabase, MatrixDatabase, DynamicMatrixDatabase, VectorDatabase, SubDatabase

"""
    abstract type AbstractDatabase end

Base type to represent databases. A database is a collection of objects that can be accessed like a
similar interface to `AbstractVector`. It is separated to allow `SimilaritySearch` methods to know what is a database and what is an object (since most object
representations will look as vectors and matrices).

The basic implementations are:
- `MatrixDatabase`: A wrapper for object-vectors stored in a `Matrix`, columns are the objects. It is static.
- `DynamicMatrixDatabase`: A dynamic representation for vectors that allows adding new vectors.
- `VectorDatabase`: A wrapper for vector-like structures. It can contain any kind of objects.
- `SubDatabase`: A sample of a given database

In particular, the storage details are not used by `VectorDatabase` and `MatrixDatabase`.
For instance, it is possible to use matrices like `Matrix`, `SMatrix` or `StrideArrays`; or even use
generated objects with `VectorDatabase` (supporting a vector-like interface).

If the storage backend support it, it is possible to use vector operations, for example:
- get the `i`-th element `obj = db[i]`, elements in the database are identified by position
- get the elements list in a list of indices `lst` as `db[lst]` (also using `view`)
- set a value at the `i`-th element `db[i] = obj`
- random sampling `rand(db)`, `rand(db, 3)`
- iterate and collect objects in the database
- get the number of elements in the database `length(db)`
- add new objects to the end of the database (not all internal containers will support it)
  - `push!(db, u)` adds a single element `u`
  - `append!(db, lst)` adds a list of objects to the end of the database
"""
abstract type AbstractDatabase end

"""
    view(db::AbstractDatabase, map)

Constructs a `SubDatabase` from `db` using the specified indexes in `map` (e.g., an array or an slice of indices)
"""
@inline Base.view(db::AbstractDatabase, map) = SubDatabase(db, map)
"""
    size(db::AbstractDatabase)

The size of the database `(length(db),)`. There is no concept of explicit dimension for `AbstractDatabase` implementations.
"""
@inline Base.size(db::AbstractDatabase) = (length(db),)

"""
    rand(db::AbstractDatabase)

Retrieves a random element from `db`
"""
@inline Random.rand(db::AbstractDatabase) = @inbounds db[rand(eachindex(db))]

"""
    rand(db::AbstractDatabase, n)

Retrieves `n` random elements from `db`, returnes a `SubDatabase` object
"""
@inline Random.rand(db::AbstractDatabase, n::Integer) = SubDatabase(db, rand(1:length(db), n))

"""
    firstindex(db::AbstractDatabase)

First index in `db` (1)
"""
@inline Base.firstindex(db::AbstractDatabase) = 1

"""
    lastindex(db::AbstractDatabase)

Last index in `db` (1)
"""
@inline Base.lastindex(db::AbstractDatabase) = length(db)

"""
    eachindex(db::AbstractDatabase)

An index iterator of `db`
"""
@inline Base.eachindex(db::AbstractDatabase) = firstindex(db):lastindex(db)

"""
    getindex(db::AbstractDatabase, lst)

A subset of `db` (using a collection of indexes `lst`)
"""
@inline Base.getindex(db::AbstractDatabase, lst::AbstractVector) = SubDatabase(db, lst)
@inline Base.getindex(db::AbstractDatabase, lst::AbstractVector{Bool}) = SubDatabase(db, findall(p -> p > 0, lst))

"""
    struct DynamicMatrixDatabase{DType,Dim} <: AbstractDatabase

A dynamic matrix with elements of type `DType` and dimension `Dim`
"""
struct DynamicMatrixDatabase{DType,Dim} <: AbstractDatabase
    data::Vector{DType}
end

"""
    struct MatrixDatabase{M<:AbstractDatabase} <: AbstractDatabase

    MatrixDatabase(matrix::AbstractMatrix)

Wraps a matrix-like object `matrix` into a `MatrixDatabase`. Please see [`AbstractDatabase`](@ref) for general usage.
"""
struct MatrixDatabase{M<:AbstractMatrix} <: AbstractDatabase
    matrix::M  # abstract matrix
end

"""
    struct VectorDatabase{V} <: AbstractDatabase

A wrapper for vector-like databases
"""
struct VectorDatabase{V} <: AbstractDatabase
    vecs::V  # abstract vector or something that looks like a vector
end

#
# Dynamic matrix-like dataset, i.e., columns are objects. Stored as a large vector to support appending items
#

"""
    eltype(db::DynamicMatrixDatabase{DType,Dim})

The type of stored elements `AbstractVector{DType}`
"""
@inline Base.eltype(db::DynamicMatrixDatabase{DType,Dim}) where {DType,Dim} = AbstractVector{DType}

"""
    DynamicMatrixDatabase(matrix::AbstractMatrix{DType})

Creates a `DynamicMatrixDatabase` from a matrix-like object. Please see [`AbstractDatabase`](@ref) for general usage.
"""
DynamicMatrixDatabase(matrix::AbstractMatrix{DType}) where DType = DynamicMatrixDatabase{DType,size(matrix, 1)}(vec(matrix))

"""
    DynamicMatrixDatabase(::Type{DType}, Dim::Integer)

Creates an empty `DynamicMatrixDatabase` such that `length(db[i]) == Dim` and `eltype(db[i]) == DType`
"""
DynamicMatrixDatabase(::Type{DType}, Dim::Integer) where DType = DynamicMatrixDatabase{DType,Dim}(Vector{DType}(undef, 0))

"""
    DynamicMatrixDatabase(V::DynamicMatrixDatabase{DType,Dim})

Creates a `DynamicMatrixDatabase` from another `DynamicMatrixDatabase`, they will share their internal data. Please see [`AbstractDatabase`](@ref) for general usage.
"""
DynamicMatrixDatabase(V::DynamicMatrixDatabase{DType,Dim}) where {DType,Dim} = DynamicMatrixDatabase{DType,Dim}(V.data)

"""
    DynamicMatrixDatabase(M::MatrixDatabase)

Creates a `DynamicMatrixDatabase` from a `MatrixDatabase`, copies internal data. Please see [`AbstractDatabase`](@ref) for general usage.
"""
DynamicMatrixDatabase(M::MatrixDatabase) = DynamicMatrixDatabase{eltype{M.data},size(M.matrix, 1)}(copy(vec(M.matrix)))

@inline function Base.getindex(db::DynamicMatrixDatabase{DType,Dim}, i::Integer) where {DType,Dim}
    ep = i * Dim
    @inbounds @view db.data[(ep - Dim + 1):ep]
end
@inline Base.setindex!(db::DynamicMatrixDatabase, value, i) = @inbounds (db.data[i] .= value)

@inline Base.length(db::DynamicMatrixDatabase{DType,Dim}) where {DType,Dim} = length(db.data) ÷ Dim
@inline function Base.push!(db::DynamicMatrixDatabase{DType,Dim}, v) where {DType,Dim}
    append!(db.data, v)
    db
end

@inline Base.append!(a::DynamicMatrixDatabase{DType,Dim}, b::DynamicMatrixDatabase{DType2,Dim})  where {DType,Dim,DType2} = append!(a.data, b.data)
@inline function Base.append!(db::DynamicMatrixDatabase{DType,Dim}, B) where {DType,Dim}
    sizehint!(db.data, length(db.data) + Dim * length(B))
    for b in B
        push!(db, b)
    end

    db
end

#
# Wrapper for matrix-like containers
#

@inline Base.eltype(db::MatrixDatabase) = AbstractVector{eltype(db.matrix)}

"""
    MatrixDatabase(V::MatrixDatabase)

Creates another `MatrixDatabase` from another `MatrixDatabase`. They will share their internal data. Please see [`AbstractDatabase`](@ref) for general usage.
"""
MatrixDatabase(V::MatrixDatabase) = MatrixDatabase(V.matrix)

@inline Base.getindex(db::MatrixDatabase, i::Integer) = view(db.matrix, :, i)
@inline Base.setindex!(db::MatrixDatabase, value, i::Integer) = @inbounds (db.matrix[:, i] .= value)
@inline Base.push!(db::MatrixDatabase, v) = error("push! is not supported for MatrixDatabase, please see DynamicMatrixDatabase")
@inline Base.append!(a::MatrixDatabase, b) = error("append! is not supported for MatrixDatabase, please see DynamicMatrixDatabase")
@inline Base.length(db::MatrixDatabase) = size(db.matrix, 2)

#
# Wrapper for array-like containers
#

@inline Base.eltype(db::VectorDatabase) = eltype(db.vecs)

"""
    VectorDatabase(vecs::T)

Creates a `VectorDatabase` from `vecs`. Please see [`AbstractDatabase`](@ref) for general usage.
"""
VectorDatabase(vecs::T) where {T<:AbstractVector} = VectorDatabase{T}(vecs)

"""
    VectorDatabase(M::AbstractMatrix)

Creates a `VectorDatabase` from a matrix-like object. It will copy columns as objects  of the new `VectorDatabase`.
Please see [`AbstractDatabase`](@ref) for general usage.
"""
VectorDatabase(M::T) where {T<:AbstractMatrix} = VectorDatabase([Vector(c) for c in eachcol(M)])

"""
    VectorDatabase(D::AbstractDatabase)

Creates a `VectorDatabase` from an `AbstractDatabase`. It copies internal data.
Please see [`AbstractDatabase`](@ref) for general usage.
"""
VectorDatabase(D::AbstractDatabase) = VectorDatabase([Vector(c) for c in D])

"""
    VectorDatabase(V::VectorDatabase)

Creates a `VectorDatabase` from another an `AbstractDatabase`. They will share their internal data.
Please see [`AbstractDatabase`](@ref) for general usage.
"""
VectorDatabase(V::VectorDatabase) = VectorDatabase(V.vecs)

"""
    VectorDatabase(; type=Vector{Float32})

Creates an empty `VectorDatabase` where each object is of type `type`.
Please see [`AbstractDatabase`](@ref) for general usage.
"""
VectorDatabase(; type=Vector{Float32}) = VectorDatabase(type[])

@inline Base.getindex(db::VectorDatabase, i::Integer) = db.vecs[i]
@inline Base.setindex!(db::VectorDatabase, value, i::Integer) = setindex!(db.vecs, value, i)
@inline Base.length(db::VectorDatabase) = length(db.vecs)
@inline Base.push!(db::VectorDatabase, v) = (push!(db.vecs, v); db)

@inline function Base.append!(db::VectorDatabase, B)
    for b in B
        push!(db, b)
    end

    db
end

# SubDatabase ~ view of the dataset
#
struct SubDatabase{DBType,RType} <: AbstractDatabase
    db::DBType
    map::RType
end

@inline Base.getindex(sdb::SubDatabase, i::Integer) = @inbounds sdb.db[sdb.map[i]]
@inline Base.length(sdb::SubDatabase) = length(sdb.map)
@inline Base.eachindex(sdb::SubDatabase) = eachindex(sdb.map)
@inline function Base.push!(sdb::SubDatabase, v)
    error("push! unsupported operation on SubDatabase")
end
@inline Base.eltype(sdb::SubDatabase) = eltype(sdb.db)
@inline Random.rand(db::SubDatabase, n::Integer) = SubDatabase(db.db, rand(db.map, n))
#
# Generic functions
#
function Base.iterate(db::AbstractDatabase, state::Int=1)
    n = length(db)
    if n == 0 || state > n
        nothing
    else
        @inbounds db[state], state + 1
    end
end

"""
    convert(::Type{AbstractDatabase}, M::AbstractDatabase)
    convert(::Type{AbstractDatabase}, M::AbstractMatrix)
    convert(::Type{AbstractDatabase}, M::Vector)
    convert(::Type{AbstractDatabase}, M::Vector{Any})
    convert(::Type{AbstractDatabase}, M::AbstractVector)
    convert(::Type{<:AbstractVector}, M::VectorDatabase)
    convert(::Type{<:AbstractVector}, M::AbstractDatabase)

Convenience functions to convert different kinds of data into a some kind of database.
"""
Base.convert(::Type{AbstractDatabase}, M::AbstractDatabase) = M
Base.convert(::Type{AbstractDatabase}, M::AbstractMatrix) = MatrixDatabase(M)
Base.convert(::Type{AbstractDatabase}, M::Vector) = VectorDatabase(M)
Base.convert(::Type{AbstractDatabase}, M::Vector{Any}) = VectorDatabase(typeof(first(M)).(M))
Base.convert(::Type{AbstractDatabase}, M::AbstractVector) = VectorDatabase(typeof(first(M)).(M))
Base.convert(::Type{<:AbstractVector}, M::VectorDatabase{T}) where T = M.data
Base.convert(::Type{<:AbstractVector}, M::AbstractDatabase) = collect(M)


function valuetype end
function makevalue end

struct Environment
    contacts::Vector{Contact}

    function Environment(mechanism, urdf, obstacles)
        elements = visual_elements(mechanism, URDFVisuals(urdf; tag="collision"))
        point_elements = filter(e -> e.geometry isa GeometryTypes.HyperSphere && GeometryTypes.radius(e.geometry) == 0, elements)
        points = map(point_elements) do element
            p = element.transform(SVector(GeometryTypes.origin(element.geometry)))
            Point3D(element.frame, p)
        end
        contacts = vec(map(Base.Iterators.product(points, obstacles)) do p
            point, obstacle = p
            body = body_fixed_frame_to_body(mechanism, point.frame)
            Contact(mechanism, body, point, obstacle)
        end)
        # removes collision with itself
        contacts = filter(c -> c.point.frame != c.obstacle.normal.frame, contacts)
        new(contacts)
    end
end

struct EnvironmentJacobian{T}
    contact_jacobians::Vector{ContactJacobian{T}}

    function EnvironmentJacobian{T}(env::Environment) where T
        contact_jacobians = [ContactJacobian{T}(contact) for contact in env.contacts]

        new{T}(contact_jacobians)
    end
end

struct EnvironmentJacobianCache <: RigidBodyDynamics.AbstractTypeDict
    env::Environment
    keys::Vector{Tuple{UInt64, Int}}
    values::Vector{EnvironmentJacobian}
end

function EnvironmentJacobianCache(env::Environment)
    EnvironmentJacobianCache(env, [], [])
end

Base.show(io::IO, ::EnvironmentJacobianCache) = print(io, "EnvironmentJacobianCache{…}(…)")

@inline RigidBodyDynamics.valuetype(::Type{EnvironmentJacobianCache}, ::Type{T}) where {T} = EnvironmentJacobian{T}
@inline RigidBodyDynamics.makevalue(envj_c::EnvironmentJacobianCache, ::Type{T}) where {T} = EnvironmentJacobian{T}(envj_c.env)

function contact_jacobian!(envj::EnvironmentJacobian, state::MechanismState)
    # TODO: parallel
    for cj in envj.contact_jacobians
        contact_jacobian!(cj, state)
    end
end


function fill_string(data, value=""; method="mostfrequent")
    nonempty = [x for x in data if x != "?"]

    if method == "sampling"
        data = [x == "?" ? sample(nonempty) : x for x in data]
    elseif method == "constant"
        data = [x == "?" ? value : x for x in data]
    elseif method == "mostfrequent"
        mostfrequent = frequentvalue(nonempty)
        data = [x == "?" ? mostfrequent : x for x in data]
    else
        throw("Undefined filling strategy")
    end
end

function fill_int(data, value=0; method="mean")
    nonmissing = [x for x in data if x != "?"]
    ints = [typeof(x) == String ? Int64(parse(Float64, x)) : Int64(x)
            for x in nonmissing]

    if method == "sampling"
        data = [x == "?" ? sample(ints) : x for x in data]
    elseif method == "mean"
        meanvalue = Int64(round(mean(ints)))
        data = [x == "?" ? meanvalue : x for x in data]
    elseif method == "constant"
        data = [x == "?" ? value : x for x in data]
    elseif method == "mostfrequent"
        mostfrequent = frequentvalue(nonempty)
        data = [x == "?" ? mostfrequent : x for x in data]
    else
        throw("Undefined filling strategy")
    end

    data = [typeof(x) == String ? Int64(parse(Float64, x)) : Int64(x)
            for x in data]
end

function fill_bin_category(data, value=""; method="mostfrequent")
    nonempty = [x for x in data if x != "?"]

    if method == "sampling"
        data = [x == "?" ? sample(nonempty) : x for x in data]
    elseif method == "constant"
        data = [x == "?" ? value : x for x in data]
    elseif method == "mostfrequent"
        mostfrequent = frequentvalue(nonempty)
        data = [x == "?" ? mostfrequent : x for x in data]
    else
        throw("Undefined filling strategy")
    end
end

function fill_category(data, value=""; method="mostfrequent")
    nonempty = [x for x in data if x != "?"]

    if method == "sampling"
        data = [x == "?" ? sample(nonempty) : x for x in data]
    elseif method == "constant"
        data = [x == "?" ? value : x for x in data]
    elseif method == "mostfrequent"
        mostfrequent = frequentvalue(nonempty)
        data = [x == "?" ? mostfrequent : x for x in data]
    else
        throw("Undefined filling strategy")
    end
end

function fill_date(data, value=""; method="mostfrequent")
    dates = [Date(x) for x in data if x != "?"]

    if method == "sampling"
       data = [x == "?" ? sample(dates) : x for x in data]
    elseif method == "constant"
        data = [x == "?" ? value : x for x in data]
    elseif method == "mostfrequent"
        mostfrequent = frequentvalue(dates)
        data = [x == "?" ? mostfrequent : x for x in data]
    else
        throw("Undefined filling strategy")
    end
end

function fill_timestamp(data, value=""; method="mostfrequent")
    timestamps = [DateTime(x) for x in data if x != "?"]

    if method == "sampling"
       data = [x == "?" ? sample(timestamps) : x for x in data]
    elseif method == "constant"
        data = [x == "?" ? value : x for x in data]
    elseif method == "mostfrequent"
        mostfrequent = frequentvalue(timestamps)
        data = [x == "?" ? mostfrequent : x for x in data]
    else
        throw("Undefined filling strategy")
    end
end

function fill_text(data, fname, value=""; method="constant")
    ids = [i for i in 1:length(data[fname]) if data[fname][i] != "?"]

    if method == "drop"
        data = Dict(fname => value[ids] for (fname, value) in data)
    elseif method == "constant"
        data[fname] = replace(data[fname], "?" => "")

        return data
    else
        throw("Undefined filling strategy")
    end
end

function fill_image(data, fname, value=""; method="drop")
    ids = [i for i in 1:length(data[fname]) if data[fname][i] != "?"]

    if method == "drop"
        data = Dict(fname => value[ids] for (fname, value) in data)
    elseif method == "constant"
        data[fname] = [x == "?" ? value : x for x in data[fname]]

        return data
    else
        throw("Undefined filling strategy")
    end
end

function remove_invalid(data, fname)
    data[fname] = [isvalid(docs) ? docs : string(c for c in docs if isvalid(c))
                   for docs in data[fname]]
    data
end


#  Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors
#  This Source Code Form is subject to the terms of the Mozilla Public
#  License, v. 2.0. If a copy of the MPL was not distributed with this
#  file, You can obtain one at https://mozilla.org/MPL/2.0/.

# The tests here check JuMP's model generation and communication with solvers.
# Model generation is checked by comparing the internal model with a serialized
# test model (in MOIU's lightweight text format).
# Communication with solvers is tested by using a mock solver with solution data
# that we feed to it. Prior to using this testing approach, we would test JuMP
# by calling real solvers, which was flakey and slow.

# Note: No attempt is made to use correct solution data. We're only testing
# that the plumbing works. This could change if JuMP gains the ability to verify
# feasibility independently of a solver.

using LinearAlgebra, Test
using JuMP

@testset "Generation and solve with fake solver" begin
    @testset "LP" begin
        m = Model()
        @variable(m, x <= 2.0)
        @variable(m, y >= 0.0)
        @objective(m, Min, -x)

        c = @constraint(m, x + y <= 1)

        JuMP.set_name(JuMP.UpperBoundRef(x), "xub")
        JuMP.set_name(JuMP.LowerBoundRef(y), "ylb")
        JuMP.set_name(c, "c")

        modelstring = """
        variables: x, y
        minobjective: -1.0*x
        xub: x <= 2.0
        ylb: y >= 0.0
        c: x + y <= 1.0
        """

        model = MOIU.Model{Float64}()
        MOIU.loadfromstring!(model, modelstring)
        MOIU.test_models_equal(
            JuMP.backend(m).model_cache,
            model,
            ["x", "y"],
            ["c", "xub", "ylb"],
        )

        set_optimizer(
            m,
            () -> MOIU.MockOptimizer(
                MOIU.Model{Float64}(),
                eval_objective_value = false,
            ),
        )
        JuMP.optimize!(m)

        mockoptimizer = JuMP.backend(m).optimizer.model
        MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL)
        MOI.set(mockoptimizer, MOI.RawStatusString(), "solver specific string")
        MOI.set(mockoptimizer, MOI.ObjectiveValue(), -1.0)
        MOI.set(mockoptimizer, MOI.ResultCount(), 1)
        MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT)
        MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT)
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(x),
            1.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(y),
            0.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(c),
            -1.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(JuMP.UpperBoundRef(x)),
            0.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(JuMP.LowerBoundRef(y)),
            1.0,
        )
        MOI.set(mockoptimizer, MOI.SimplexIterations(), 1)
        MOI.set(mockoptimizer, MOI.BarrierIterations(), 1)
        MOI.set(mockoptimizer, MOI.NodeCount(), 1)

        #@test JuMP.isattached(m)
        @test JuMP.has_values(m)

        @test MOI.OPTIMAL == @inferred JuMP.termination_status(m)
        @test "solver specific string" == JuMP.raw_status(m)
        @test MOI.FEASIBLE_POINT == @inferred JuMP.primal_status(m)

        @test 1.0 == @inferred JuMP.value(x)
        @test 0.0 == @inferred JuMP.value(y)
        @test 1.0 == @inferred JuMP.value(x + y)
        @test 1.0 == @inferred JuMP.value(c)
        @test -1.0 == @inferred JuMP.objective_value(m)
        @test -1.0 == @inferred JuMP.dual_objective_value(m)

        @test MOI.FEASIBLE_POINT == @inferred JuMP.dual_status(m)
        @test -1.0 == @inferred JuMP.dual(c)
        @test 0.0 == @inferred JuMP.dual(JuMP.UpperBoundRef(x))
        @test 1.0 == @inferred JuMP.dual(JuMP.LowerBoundRef(y))
        @test 1 == JuMP.simplex_iterations(m)
        @test 1 == JuMP.barrier_iterations(m)
        @test 1 == JuMP.node_count(m)
    end

    @testset "LP (Direct mode)" begin
        mockoptimizer = MOIU.MockOptimizer(
            MOIU.Model{Float64}(),
            eval_objective_value = false,
        )

        m = JuMP.direct_model(mockoptimizer)
        @variable(m, x <= 2.0)
        @variable(m, y >= 0.0)
        @objective(m, Min, -x)

        c = @constraint(m, x + y <= 1)
        MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL)
        MOI.set(mockoptimizer, MOI.RawStatusString(), "solver specific string")
        MOI.set(mockoptimizer, MOI.ObjectiveValue(), -1.0)
        MOI.set(mockoptimizer, MOI.ResultCount(), 1)
        MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT)
        MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT)
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(x),
            1.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(y),
            0.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(c),
            -1.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(JuMP.UpperBoundRef(x)),
            0.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(JuMP.LowerBoundRef(y)),
            1.0,
        )
        MOI.set(mockoptimizer, MOI.SimplexIterations(), 1)
        MOI.set(mockoptimizer, MOI.BarrierIterations(), 1)
        MOI.set(mockoptimizer, MOI.NodeCount(), 1)

        JuMP.optimize!(m)

        #@test JuMP.isattached(m)
        @test JuMP.has_values(m)

        @test MOI.OPTIMAL == @inferred JuMP.termination_status(m)
        @test "solver specific string" == JuMP.raw_status(m)
        @test MOI.FEASIBLE_POINT == @inferred JuMP.primal_status(m)

        @test 1.0 == @inferred JuMP.value(x)
        @test 0.0 == @inferred JuMP.value(y)
        @test 1.0 == @inferred JuMP.value(x + y)
        @test -1.0 == @inferred JuMP.objective_value(m)

        @test MOI.FEASIBLE_POINT == @inferred JuMP.dual_status(m)
        @test -1.0 == @inferred JuMP.dual(c)
        @test 0.0 == @inferred JuMP.dual(JuMP.UpperBoundRef(x))
        @test 1.0 == @inferred JuMP.dual(JuMP.LowerBoundRef(y))
        @test 1 == JuMP.simplex_iterations(m)
        @test 1 == JuMP.barrier_iterations(m)
        @test 1 == JuMP.node_count(m)
    end

    # TODO: test Manual mode

    @testset "IP" begin
        # Tests the solver= keyword.
        m = Model(
            () -> MOIU.MockOptimizer(
                MOIU.Model{Float64}(),
                eval_objective_value = false,
            ),
            caching_mode = MOIU.AUTOMATIC,
        )
        @variable(m, x == 1.0, Int)
        @variable(m, y, Bin)
        @objective(m, Max, x)

        JuMP.set_name(JuMP.FixRef(x), "xfix")
        JuMP.set_name(JuMP.IntegerRef(x), "xint")
        JuMP.set_name(JuMP.BinaryRef(y), "ybin")

        modelstring = """
        variables: x, y
        maxobjective: x
        xfix: x == 1.0
        xint: x in Integer()
        ybin: y in ZeroOne()
        """

        model = MOIU.Model{Float64}()
        MOIU.loadfromstring!(model, modelstring)
        MOIU.test_models_equal(
            JuMP.backend(m).model_cache,
            model,
            ["x", "y"],
            ["xfix", "xint", "ybin"],
        )

        MOIU.attach_optimizer(m)

        mockoptimizer = JuMP.backend(m).optimizer.model
        MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL)
        MOI.set(mockoptimizer, MOI.RawStatusString(), "solver specific string")
        MOI.set(mockoptimizer, MOI.ObjectiveValue(), 1.0)
        MOI.set(mockoptimizer, MOI.ResultCount(), 1)
        MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT)
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(x),
            1.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(y),
            0.0,
        )
        MOI.set(mockoptimizer, MOI.DualStatus(), MOI.NO_SOLUTION)
        MOI.set(mockoptimizer, MOI.SimplexIterations(), 1)
        MOI.set(mockoptimizer, MOI.BarrierIterations(), 1)
        MOI.set(mockoptimizer, MOI.NodeCount(), 1)
        MOI.set(mockoptimizer, MOI.RelativeGap(), 0.0)

        JuMP.optimize!(m)

        #@test JuMP.isattached(m)
        @test JuMP.has_values(m)

        @test MOI.OPTIMAL == @inferred JuMP.termination_status(m)
        @test "solver specific string" == JuMP.raw_status(m)
        @test MOI.FEASIBLE_POINT == @inferred JuMP.primal_status(m)

        @test 1.0 == @inferred JuMP.value(x)
        @test 0.0 == @inferred JuMP.value(y)
        @test 1.0 == @inferred JuMP.objective_value(m)

        @test 1 == JuMP.simplex_iterations(m)
        @test 1 == JuMP.barrier_iterations(m)
        @test 1 == JuMP.node_count(m)
        @test 0.0 == @inferred JuMP.relative_gap(m)

        @test !JuMP.has_duals(m)
    end

    @testset "QCQP" begin
        m = Model()
        @variable(m, x)
        @variable(m, y)
        @objective(m, Min, x^2)

        @constraint(m, c1, 2x * y <= 1)
        @constraint(m, c2, y^2 == x^2)
        @constraint(m, c3, 2x + 3y * x >= 2)

        modelstring = """
        variables: x, y
        minobjective: 1*x*x
        c1: 2*x*y <= 1.0
        c2: 1*y*y + -1*x*x == 0.0
        c3: 2x + 3*y*x >= 2.0
        """

        model = MOIU.Model{Float64}()
        MOIU.loadfromstring!(model, modelstring)
        MOIU.test_models_equal(
            JuMP.backend(m).model_cache,
            model,
            ["x", "y"],
            ["c1", "c2", "c3"],
        )

        set_optimizer(
            m,
            () -> MOIU.MockOptimizer(
                MOIU.Model{Float64}(),
                eval_objective_value = false,
            ),
        )
        JuMP.optimize!(m)

        mockoptimizer = JuMP.backend(m).optimizer.model
        MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL)
        MOI.set(mockoptimizer, MOI.RawStatusString(), "solver specific string")
        MOI.set(mockoptimizer, MOI.ObjectiveValue(), -1.0)
        MOI.set(mockoptimizer, MOI.ResultCount(), 1)
        MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT)
        MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT)
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(x),
            1.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(y),
            0.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(c1),
            -1.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(c2),
            2.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(c3),
            3.0,
        )
        MOI.set(mockoptimizer, MOI.SimplexIterations(), 1)
        MOI.set(mockoptimizer, MOI.BarrierIterations(), 1)
        MOI.set(mockoptimizer, MOI.NodeCount(), 1)

        #@test JuMP.isattached(m)
        @test JuMP.has_values(m)

        @test MOI.OPTIMAL == @inferred JuMP.termination_status(m)
        @test "solver specific string" == JuMP.raw_status(m)
        @test MOI.FEASIBLE_POINT == @inferred JuMP.primal_status(m)

        @test 1.0 == @inferred JuMP.value(x)
        @test 0.0 == @inferred JuMP.value(y)
        @test -1.0 == @inferred JuMP.objective_value(m)
        @test 5.0 == @inferred JuMP.dual_objective_value(m)

        @test MOI.FEASIBLE_POINT == @inferred JuMP.dual_status(m)
        @test -1.0 == @inferred JuMP.dual(c1)
        @test 2.0 == @inferred JuMP.dual(c2)
        @test 3.0 == @inferred JuMP.dual(c3)

        @test 2.0 == @inferred JuMP.value(2 * x + 3 * y * x)
        @test 1 == JuMP.simplex_iterations(m)
        @test 1 == JuMP.barrier_iterations(m)
        @test 1 == JuMP.node_count(m)
    end

    @testset "SOC" begin
        m = Model()
        @variables m begin
            x
            y
            z
        end
        @objective(m, Max, 1.0 * x)
        @constraint(m, varsoc, [x, y, z] in SecondOrderCone())
        # Equivalent to `[x+y,z,1.0] in SecondOrderCone()`
        @constraint(m, affsoc, [x + y, z, 1.0] in MOI.SecondOrderCone(3))
        @constraint(m, rotsoc, [x + 1, y, z] in RotatedSecondOrderCone())

        modelstring = """
        variables: x, y, z
        maxobjective: 1.0*x
        varsoc: [x,y,z] in SecondOrderCone(3)
        affsoc: [x+y,z,1.0] in SecondOrderCone(3)
        rotsoc: [x+1,y,z] in RotatedSecondOrderCone(3)
        """

        model = MOIU.Model{Float64}()
        MOIU.loadfromstring!(model, modelstring)
        MOIU.test_models_equal(
            JuMP.backend(m).model_cache,
            model,
            ["x", "y", "z"],
            ["varsoc", "affsoc", "rotsoc"],
        )

        mockoptimizer = MOIU.MockOptimizer(
            MOIU.Model{Float64}(),
            eval_objective_value = false,
            eval_variable_constraint_dual = false,
        )
        MOIU.reset_optimizer(m, mockoptimizer)
        MOIU.attach_optimizer(m)

        MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL)
        MOI.set(mockoptimizer, MOI.RawStatusString(), "solver specific string")
        MOI.set(mockoptimizer, MOI.ResultCount(), 1)
        MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT)
        MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT)
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(x),
            1.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(y),
            0.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(z),
            0.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(varsoc),
            [-1.0, -2.0, -3.0],
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(affsoc),
            [1.0, 2.0, 3.0],
        )
        MOI.set(mockoptimizer, MOI.SimplexIterations(), 1)
        MOI.set(mockoptimizer, MOI.BarrierIterations(), 1)
        MOI.set(mockoptimizer, MOI.NodeCount(), 1)

        JuMP.optimize!(m)

        #@test JuMP.isattached(m)
        @test JuMP.has_values(m)

        @test MOI.OPTIMAL == @inferred JuMP.termination_status(m)
        @test "solver specific string" == JuMP.raw_status(m)
        @test MOI.FEASIBLE_POINT == @inferred JuMP.primal_status(m)

        @test 1.0 == @inferred JuMP.value(x)
        @test 0.0 == @inferred JuMP.value(y)
        @test 0.0 == @inferred JuMP.value(z)

        @test JuMP.has_duals(m)
        @test [-1.0, -2.0, -3.0] == @inferred JuMP.dual(varsoc)
        @test [1.0, 2.0, 3.0] == @inferred JuMP.dual(affsoc)
        @test 1 == JuMP.simplex_iterations(m)
        @test 1 == JuMP.barrier_iterations(m)
        @test 1 == JuMP.node_count(m)
    end

    @testset "SDP" begin
        m = Model()
        @variable(m, x[1:2, 1:2], Symmetric)
        set_name(x[1, 1], "x11")
        set_name(x[1, 2], "x12")
        set_name(x[2, 2], "x22")
        @objective(m, Max, tr(x))
        var_psd = @constraint(m, x in PSDCone())
        set_name(var_psd, "var_psd")
        sym_psd = @constraint(m, Symmetric(x - [1.0 0.0; 0.0 1.0]) in PSDCone())
        set_name(sym_psd, "sym_psd")
        con_psd = @SDconstraint(m, x ⪰ [1.0 0.0; 0.0 1.0])
        set_name(con_psd, "con_psd")

        modelstring = """
        variables: x11, x12, x22
        maxobjective: 1.0*x11 + 1.0*x22
        var_psd: [x11,x12,x22] in PositiveSemidefiniteConeTriangle(2)
        sym_psd: [x11 + -1.0,x12,x22 + -1.0] in PositiveSemidefiniteConeTriangle(2)
        con_psd: [x11 + -1.0,x12,x12,x22 + -1.0] in PositiveSemidefiniteConeSquare(2)
        """

        model = MOIU.Model{Float64}()
        MOIU.loadfromstring!(model, modelstring)
        MOIU.test_models_equal(
            JuMP.backend(m).model_cache,
            model,
            ["x11", "x12", "x22"],
            ["var_psd", "sym_psd", "con_psd"],
        )

        mockoptimizer = MOIU.MockOptimizer(
            MOIU.Model{Float64}(),
            eval_objective_value = false,
            eval_variable_constraint_dual = false,
        )
        MOIU.reset_optimizer(m, mockoptimizer)
        MOIU.attach_optimizer(m)

        MOI.set(mockoptimizer, MOI.TerminationStatus(), MOI.OPTIMAL)
        MOI.set(mockoptimizer, MOI.RawStatusString(), "solver specific string")
        MOI.set(mockoptimizer, MOI.ResultCount(), 1)
        MOI.set(mockoptimizer, MOI.PrimalStatus(), MOI.FEASIBLE_POINT)
        MOI.set(mockoptimizer, MOI.DualStatus(), MOI.FEASIBLE_POINT)
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(x[1, 1]),
            1.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(x[1, 2]),
            2.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.VariablePrimal(),
            JuMP.optimizer_index(x[2, 2]),
            4.0,
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(var_psd),
            [1.0, 2.0, 3.0],
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(sym_psd),
            [4.0, 5.0, 6.0],
        )
        MOI.set(
            mockoptimizer,
            MOI.ConstraintDual(),
            JuMP.optimizer_index(con_psd),
            [7.0, 8.0, 9.0, 10.0],
        )
        MOI.set(mockoptimizer, MOI.SimplexIterations(), 1)
        MOI.set(mockoptimizer, MOI.BarrierIterations(), 1)
        MOI.set(mockoptimizer, MOI.NodeCount(), 1)

        JuMP.optimize!(m)

        @test MOI.OPTIMAL == @inferred JuMP.termination_status(m)
        @test "solver specific string" == JuMP.raw_status(m)
        @test MOI.FEASIBLE_POINT == @inferred JuMP.primal_status(m)

        @test JuMP.has_values(m)
        @test [1.0 2.0; 2.0 4.0] == JuMP.value.(x)
        @test JuMP.value(var_psd) isa Symmetric
        @test [1.0 2.0; 2.0 4.0] == @inferred JuMP.value(var_psd)
        @test JuMP.value(sym_psd) isa Symmetric
        @test [0.0 2.0; 2.0 3.0] == @inferred JuMP.value(sym_psd)
        @test JuMP.value(con_psd) isa Matrix
        @test [0.0 2.0; 2.0 3.0] == @inferred JuMP.value(con_psd)

        @test JuMP.has_duals(m)
        @test JuMP.dual(var_psd) isa Symmetric
        @test [1.0 2.0; 2.0 3.0] == @inferred JuMP.dual(var_psd)
        @test JuMP.dual(sym_psd) isa Symmetric
        @test [4.0 5.0; 5.0 6.0] == @inferred JuMP.dual(sym_psd)
        @test JuMP.dual(con_psd) isa Matrix
        @test [7.0 9.0; 8.0 10.0] == @inferred JuMP.dual(con_psd)
        @test 1 == JuMP.simplex_iterations(m)
        @test 1 == JuMP.barrier_iterations(m)
        @test 1 == JuMP.node_count(m)
    end

    @testset "Solver doesn't support nonlinear constraints" begin
        model = Model(() -> MOIU.MockOptimizer(MOIU.Model{Float64}()))
        @variable(model, x)
        @NLobjective(model, Min, sin(x))
        err = ErrorException(
            "The solver does not support nonlinear problems " *
            "(i.e., NLobjective and NLconstraint).",
        )
        @test_throws err JuMP.optimize!(model)
    end

    @testset "ResultCount" begin
        m = Model()
        @variable(m, x >= 0.0)
        @variable(m, y >= 0.0)
        @objective(m, Max, x + y)
        @constraint(m, c1, x <= 2)
        @constraint(m, c2, x + y <= 1)

        model = MOIU.Model{Float64}()
        MOIU.loadfromstring!(
            model,
            """
variables: x, y
maxobjective: x + y
xub: x >= 0.0
ylb: y >= 0.0
c1: x <= 2.0
c2: x + y <= 1.0
""",
        )
        set_optimizer(
            m,
            () -> MOIU.MockOptimizer(
                MOIU.Model{Float64}(),
                eval_objective_value = false,
            ),
        )
        JuMP.optimize!(m)

        mock = JuMP.backend(m).optimizer.model
        MOI.set(mock, MOI.TerminationStatus(), MOI.OPTIMAL)
        MOI.set(mock, MOI.ResultCount(), 2)

        aff_expr = @expression(m, x + y)
        quad_expr = @expression(m, x * y)
        nl_expr = @NLexpression(m, log(x + y))

        @test JuMP.result_count(m) == 2

        MOI.set(mock, MOI.PrimalStatus(1), MOI.FEASIBLE_POINT)
        MOI.set(mock, MOI.DualStatus(1), MOI.FEASIBLE_POINT)
        MOI.set(mock, MOI.ObjectiveValue(1), 1.0)
        MOI.set(mock, MOI.DualObjectiveValue(1), 1.0)
        MOI.set(mock, MOI.VariablePrimal(1), JuMP.optimizer_index(x), 1.0)
        MOI.set(mock, MOI.VariablePrimal(1), JuMP.optimizer_index(y), 0.0)
        MOI.set(mock, MOI.ConstraintDual(1), JuMP.optimizer_index(c1), 0.0)
        MOI.set(mock, MOI.ConstraintDual(1), JuMP.optimizer_index(c2), -1.0)

        @test MOI.OPTIMAL == @inferred JuMP.termination_status(m)
        @test MOI.FEASIBLE_POINT == @inferred JuMP.primal_status(m, result = 1)
        @test MOI.FEASIBLE_POINT == @inferred JuMP.dual_status(m, result = 1)
        @test 1.0 == @inferred JuMP.objective_value(m, result = 1)
        @test 1.0 == @inferred JuMP.dual_objective_value(m, result = 1)
        @test 1.0 == @inferred JuMP.value(x, result = 1)
        @test 0.0 == @inferred JuMP.value(y, result = 1)
        @test 1.0 == @inferred JuMP.value(aff_expr, result = 1)
        @test 0.0 == @inferred JuMP.value(quad_expr, result = 1)
        @test 0.0 == @inferred JuMP.value(nl_expr, result = 1)
        @test 1.0 == @inferred JuMP.value(c2, result = 1)
        @test 0.0 == @inferred JuMP.dual(c1, result = 1)
        @test -1.0 == @inferred JuMP.dual(c2, result = 1)
        @test 0.0 == @inferred JuMP.dual(JuMP.LowerBoundRef(x), result = 1)
        @test 0.0 == @inferred JuMP.dual(JuMP.LowerBoundRef(y), result = 1)

        MOI.set(mock, MOI.PrimalStatus(2), MOI.FEASIBLE_POINT)
        MOI.set(mock, MOI.DualStatus(2), MOI.FEASIBLE_POINT)
        MOI.set(mock, MOI.ObjectiveValue(2), 1.0)
        MOI.set(mock, MOI.DualObjectiveValue(2), 1.0)
        MOI.set(mock, MOI.VariablePrimal(2), JuMP.optimizer_index(x), 0.0)
        MOI.set(mock, MOI.VariablePrimal(2), JuMP.optimizer_index(y), 1.0)
        MOI.set(mock, MOI.ConstraintDual(2), JuMP.optimizer_index(c1), 0.0)
        MOI.set(mock, MOI.ConstraintDual(2), JuMP.optimizer_index(c2), -1.0)

        @test MOI.FEASIBLE_POINT == @inferred JuMP.primal_status(m, result = 2)
        @test MOI.FEASIBLE_POINT == @inferred JuMP.dual_status(m, result = 2)
        @test 1.0 == @inferred JuMP.objective_value(m, result = 2)
        @test 1.0 == @inferred JuMP.dual_objective_value(m, result = 2)
        @test 0.0 == @inferred JuMP.value(x, result = 2)
        @test 1.0 == @inferred JuMP.value(y, result = 2)
        @test 1.0 == @inferred JuMP.value(aff_expr, result = 2)
        @test 0.0 == @inferred JuMP.value(quad_expr, result = 2)
        @test 0.0 == @inferred JuMP.value(nl_expr, result = 2)
        @test 1.0 == @inferred JuMP.value(c2, result = 2)
        @test 0.0 == @inferred JuMP.dual(c1, result = 2)
        @test -1.0 == @inferred JuMP.dual(c2, result = 2)
        @test 0.0 == @inferred JuMP.dual(JuMP.LowerBoundRef(x), result = 2)
        @test 0.0 == @inferred JuMP.dual(JuMP.LowerBoundRef(y), result = 2)

        @test MOI.NO_SOLUTION == @inferred JuMP.primal_status(m, result = 3)
        @test MOI.NO_SOLUTION == @inferred JuMP.dual_status(m, result = 3)
        @test_throws MOI.ResultIndexBoundsError JuMP.objective_value(
            m,
            result = 3,
        )
        @test_throws MOI.ResultIndexBoundsError JuMP.dual_objective_value(
            m,
            result = 3,
        )
        @test_throws MOI.ResultIndexBoundsError JuMP.value(x, result = 3)
        @test_throws MOI.ResultIndexBoundsError JuMP.value(aff_expr, result = 3)
        @test_throws MOI.ResultIndexBoundsError JuMP.value(
            quad_expr,
            result = 3,
        )
        @test_throws MOI.ResultIndexBoundsError JuMP.value(nl_expr, result = 3)
        @test_throws MOI.ResultIndexBoundsError JuMP.value(c2, result = 3)
        @test_throws MOI.ResultIndexBoundsError JuMP.dual(c1, result = 3)
        @test_throws MOI.ResultIndexBoundsError JuMP.dual(c2, result = 3)
        @test_throws MOI.ResultIndexBoundsError JuMP.dual(
            JuMP.LowerBoundRef(x),
            result = 3,
        )
        @test_throws MOI.ResultIndexBoundsError JuMP.dual(
            JuMP.LowerBoundRef(y),
            result = 3,
        )
    end
end


using Peridynamics
using Test
using SafeTestsets

@testset "Peridynamics" begin

    @safetestset "Discretization" begin
        include("discretization.jl")
    end

end


# functions that each physics module must implement

"""
  This function evalutes dq/dt = R(q).  For steady problems it evalutes R(q)
  at some state q.  The state is stored in eqn.q, and eqn.res is populated with
  R(q).  Note that these are 3 dimensional arrays.  The physics modules only
  interact with the 3 dimensional arrays, never the vectors eqn.q_vec and
  eqn.res_vec.  Each physics module must implement this function for its
  own subtype of AbstractSolutionData (ie. with a more specific type for
  the eqn argument and equallty specific types for the other arguments).
  This is important because evalResidual is common to all physics modules,
  so a user or some other part of the code can call evalResidual(mesh, sbp
  eqn, opts), and Julia's multiple dispatch will figure out the right method
  to call based on the type of the eqn argument.

  The evaluation of the residual R(q) should depend only on the data stored in
  mesh, sbp, eqn, and opts, and any data that depend on q should be recalculated
  every time the function is called.  This function is used as a building block
  by other parts of the solver, particularly the NonlinearSolvers.  See
  interfaces.md for details

  **Inputs**

   * mesh: an AbstractMesh describing the mesh on which to solve the physics
   * sbp: an SBP operator
   * eqn: a subtype of AbstractSolution data, used to store all of the data used by the physics module
   * opts: the options dictionary
   * t: the current time value, defaults to 0.0

  #TODO: list required options keys
"""
function evalResidual(mesh::AbstractMesh, sbp::AbstractOperator, eqn::AbstractSolutionData, opts::Dict, t = 0.0)

  throw(ErrorException("Generic fallback evalResidual reached: did you forget to extend evalResidual() with a new method for your AbstractSolutionData?"))

  return nothing
end

function evalHomotopy(mesh::AbstractMesh, sbp::AbstractOperator,
                      eqn::AbstractSolutionData, opts::Dict,
                      res::Abstract3DArray, t = 0.0)

  throw(ErrorException("Generic fallback evalHomotopy reached: did you forget to extend evalHomotopy() with a new method for your AbstractSolutionData?"))

  return nothing
end

"""
  Similar function to [`evalResidual`](@ref), but instead of computing the
  residual, it computes and assembles the Jacobian of the residual with
  respect to `eqn.q` into the system matrix.

  The functions [`assembleElement`](@ref) and [`assembleInterface`](@ref)
  in `NonlinearSolvers` should be used to assembles the Jacobians of
  individual elements and interfaces into the system matrix.

  Physics modules that can compute element Jacobians directly
  should extend this function with a new method, otherwise the coloring
  algorithm with be used with [`evalResidual`](@ref) to compute the
  system matrix.  For physics modules that support multiple formuations,
  this function should throw an exception if called with an unsupported
  formulation.

  **Inputs**

   * mesh: an AbstractMesh
   * sbp: an SBP operator
   * eqn: AbstractSolutionData (physics modules should specialize this
          argument)
   * opts: options dictionary
   * assembler: object that must be passed to `assembleElement` and
                `assembleInterface`

  **Keyword Arguments**

   * start_comm: whether or not to start parallel communication,
                 default false because the functions Nonlinear solvers
                 have already done parallel communication when this
                 function gets called.

  **Options Keys**

  TODO: describe the key that controls whether this function gets called
        or not.
"""
function evalJacobian(mesh::AbstractMesh, sbp::AbstractOperator,
                      eqn::AbstractSolutionData, opts::Dict,
                      assembler::AssembleElementData, t=0.0;
                      start_comm=false)

  throw(ErrorException("Generic fallback evalJacobian reached: did you forget to extend evalJacobian() with a new method for your AbstractSolutionData?"))
  return nothing
end

"""
  Evaluates the Jacobian of the [`evalHomotopy`](@ref) multiplied by the
  homotopy parameter lambda.  Conceptually similar to [`evalJacobian`](@ref).

  **Inputs**

   * mesh: an AbstractMesh
   * sbp: an AbstractOperator
   * eqn: an AbstractSolutionData (physics modules should specialize this
          argument)
   * opts: options dictionary
   * assembler: an object used to assemble contributions into the Jacobian
   * lambda: the homotopy parameter
"""
function evalHomotopyJacobian(mesh::AbstractMesh, sbp::AbstractOperator,
                              eqn::AbstractSolutionData, opts::Dict,
                              assembler::AssembleElementData, lambda::Number)

  throw(ErrorException("Generic fallback evalHomotopyJacobian reached: did you forget to extend evalHomotopyJacobian() with a new method for your AbstractSolutionData?"))

  return nothing
end

#TODO: debugging
function evalHomotopy(mesh::AbstractMesh, sbp::AbstractOperator, eqn::AbstractSolutionData, opts::Dict, t = 0.0)

  evalHomotopy(mesh, sbp, eqn, opts, eqn.res, t)

  return nothing
end

"""
  This function evaluates the Jacobian of the strong form of the spatial
  residual.  Note that the residual is written

  du/dt = -(Q * f) + SAT

  Note the negative sign.

  Currently this function neglects the SAT terms (both interface and boundary
  conditions)

  **Inputs**

   * mesh: an AbstractMesh
   * sbp: an SBP operator
   * eqn: AbstractSolutionData (physics modules should specialize this
          argument)
   * opts: options dictionary
   * assembler: object that must be passed to `assembleElement` and
                `assembleInterface`
"""
function evalJacobianStrong(mesh::AbstractMesh, sbp::AbstractOperator,
                      eqn::AbstractSolutionData, opts::Dict,
                      assembler::AssembleElementData, t=0.0;
                      start_comm=false)

  throw(ErrorException("Generic fallback evalJacobianStrongDiag reached: did you forget to extend evalJacobian() with a new method for your AbstractSolutionData?"))

end

"""
  Creates a functional object.

  Each physics modules should extend this function with a new method,
  specializing the `eqn` argument.

  **Inputs**

  * `mesh` : Abstract PUMI mesh
  * `sbp`  : Summation-by-parts operator
  * `eqn`  : AbstractSolutionData object
  * `opts` : Options dictionary
  * `functional_name`: the name of the functional
  * `functional_bcs`: the boundary condition numbers the functional is
                      computed on.

  **Outputs**

   * functional: an [`AbstractFunctional`](@ref).  Usually an
                 [`AbstractIntegralFunctional`](@ref), although this is not
                 required.
"""
function createFunctional(mesh::AbstractMesh, sbp::AbstractOperator,
                        eqn::AbstractSolutionData, opts,
                        functional_name::AbstractString,
                        functional_bcs::Vector{I}) where I<:Integer

  error("generic fallback for createFunctional() reached.  Did you forget to extend createFunctional() with a new method for your AbstractSolutionData?")

end

"""
  High level function that evaluates the given functional
  This function is agnostic to the type of the functional being
  computed and calls a mid level functional-type specific function for the
  actual evaluation.

  This is the method physics modules must implemented.  Users should call
  the other method, which takes care of starting parallel communication.
  On entry, `eqn.q` will contain the solution.  If the
  functional requires parallel communication of the solution, it will have
  been started before this function is called, and this function is responsible
  for finishing parallel communication by calling [`finishExchangeData`](@ref).

  This function should perform any global reduction required (ex. calling
  `MPI.Allreduce`).

  **Arguments**

  *  `mesh` :  Abstract mesh object
  *  `sbp`  : Summation-By-Parts operator
  *  `eqn`  : AbstractSolutionData object
  *  `opts` : Options dictionary
  *  `functionalData` : Object of type AbstractFunctional.
                        This type determines the functional being computed and
                        holds all the relevant data.
"""
function _evalFunctional(mesh::AbstractMesh{Tmsh},
            sbp::AbstractOperator, eqn::AbstractSolutionData{Tsol}, opts,
            functionalData::AbstractFunctional) where {Tmsh, Tsol}

  error("Generic fallback for _evalFunctional() reached: did you forget to extend evalFunctional() with a new method for you AbstractSolutionData")

end

"""
  Performs reverse-mode differentiation of a functional with respect to the
  metrics.  When this function is called, the solution will be in `eqn.q` and
  parallel communication will already have started if required by the functional.
  This function should update the `_bar` fields of the mesh.  These fields
  should never be overwritten.
  It is the callers responsiblity to zero out these fields beforehand, if
  required.  Mesh implementation should provide a function `zeroBarArrays`
  to do this.  See the documentation of [`_evalFunctional`](@ref) for
  information on parallel communication.

  **Inputs**

   * mesh
   * sbp
   * eqn
   * opts
   * functionalData: the [`AbstractFunctional`](@ref)
   * val_bar: seed value for back-propigation, default value 1.  This is
              rarely used by the user-facing interface, but is useful for
              functionals that are composed of other functionals.
"""
function _evalFunctionalDeriv_m(mesh::AbstractDGMesh{Tmsh},
                           sbp::AbstractOperator,
                           eqn::AbstractSolutionData{Tsol}, opts,
                           functionalData::AbstractFunctional,
                           val_bar::Number=1,
                           ) where {Tmsh, Tsol}

  error("Generic fallback for evalFunctionalDeriv_m() reached: did you forget to extend evalFunctionalDeriv_m() with a new method for you AbstractSolutionData")

end

"""
  Computes a 3D array of the derivative of a functional wrt eqn.q.

  The derivative is evaluated at the state in eqn.q.  See the documentation
  for [`_evalFunctional`](@ref) for information on parallel communication.

  **Inputs**

   * mesh
   * sbp
   * eqn
   * opts
   * functionalData: `AbstractFunctional` to evaluate

  **Inputs/Outputs**

   * func_deriv_arr: array to hold derivative of function wrt eqn.q, same
                     size as equation.q.  To be overwritten.

  **Options Keys**

  This function is not compatible with `precompute_q_bndry` = false in most
  cases
"""
function _evalFunctionalDeriv_q(mesh::AbstractDGMesh{Tmsh},
                           sbp::AbstractOperator,
                           eqn::AbstractSolutionData{Tsol}, opts,
                           functionalData::AbstractFunctional,
                           func_deriv_arr::Abstract3DArray) where {Tmsh, Tsol}

  error("Generic fallback for evalFunctionalDeriv_q() reached: did you forget to extend evalFunctionalDeriv_q() with a new method for you AbstractSolutionData")

end

"""
  This function recalculates all quantities that depend on the mesh metrics.
  This function handles changes in the metric *values* only (not changed in
  mesh topology).

  This function should be called after the mesh is warped (ie. using mesh
  movement).

  Every physics module should extend this function with a new method
  specializing the `eqn` argument type.

  **Inputs**

   * mesh
   * sbp
   * eqn
   * opts
"""
function updateMetricDependents(mesh::AbstractMesh, sbp::AbstractOperator,
                                 eqn::AbstractSolutionData, opts)

  error("Generic fallback for updateMetricDependents() reached: did you forget to extend updateMetricDependents() with a new method for you AbstractSolutionData")

  return nothing
end

"""
  This function takes fully initialzed objects and solves the partial
  differential equation.

  Every physics should extend this function with a new method, specialzing the
  `eqn` argument type.

  Objects can be created with the [`createObjects`](@ref) function.

  The functions in the `SolverCommon` module are helpful in writing
  this function.

  **Inputs**

   * mesh: an AbstractMesh
   * sbp: an SBP Operator
   * eqn: an AbstractSolutionData
   * opts: the options dictionary
   * pmesh: mesh for preconditioning (optional)

  **Outputs**

   * mesh
   * sbp
   * eqn: on exit, eqn.q_vec should have the converged solution in it.
   * opts

  **Options Keys**

   * Relfunc_name: also writes vtk files called "solution_relfunc"
                  if key not present, ignored
                   TODO: fix that
   * IC_name
   * calc_error: also write vtk files called "solution_error"
   * calc_trunc_error
   * perturb_ic
   * calc_dt

    For options like calc_dt and Relfunc_name, it is very important that
    the computed quantity be saved to the options dictionary for use later
    in the code (ie. and not passed directly to another function).  The
    code won't restart correctly if this happens.

"""
function solvePDE(mesh::AbstractMesh, sbp::AbstractOperator, eqn::AbstractSolutionData,
                  opts::Dict, pmesh::AbstractMesh=mesh)

  error("Generic fallback for solvePDE() reaches: did you forget to extend solvePDE with a new method for your AbstractSolutionData?")

end

"""
  This function performs the reverse mode calculation of the residual with
  respect to the metrics.  The derivative should be evaluated at the solution
  in `eqn.q`.  The following fields of the mesh
  are updated with the result:

   * dxidx_bar
   * jac_bar
   * nrm_bndry_bar
   * nrm_face_bar
   * coords_bndry_bar

  It is the caller's responsibility to zero out these arrays before calling
  this function, if required.  Mesh implementations should provide a
  `zeroBarArrays` function to do this.

  Note: the mesh and equation objects must have the `need_adjoint` option set
  to true in the options dictionary used to create them.

  This function must finish parallel communication for `eqn.shared_data` and
  `eqn.shared_data_bar` by calling either [`finishExchangeData`](@ref) or
  [`finishExchangeData_rev`](@ref).  The parallel data setting of
  `eqn.shared_data` and `eqn.shared_data_bar` are always the same, and
  (currently) always equal to PARALLEL_DATA_ELEMENT.

  **Inputs**

   * mesh:
   * sbp
   * eqn: when this function is called, `eqn.res_bar` must already have the 3D
          array form of the seed vector in it
   * opts
   * t: optional argument, gives current time value

  See the other method of this function if the seed vector is in 1D form.
"""
function evalResidual_revm(mesh::AbstractMesh, sbp::AbstractOperator,
                           eqn::AbstractSolutionData, opts::Dict, t=0.0)

  error("Generic fallback for evalResidual_revm: did you forget to extend evalResidual_revm with a new method for your AbstractSolutionData")

end

"""
  This function performs reverse mode calculation of the residual with
  respect to the solution.  The `eqn.q_bar` array is updated with the result.
  It is the callers responsibility to zero out this array before calling this
  function, if required.

  Note: the mesh and equation objects must have the `need_adjoint` option set
  to true in the options dictionary used to create them.

  This function must finish parallel communication for `eqn.shared_data` and
  `eqn.shared_data_bar` by calling [`finishExchangeData_rev2`](@ref).
  The parallel data setting of`eqn.shared_data` and `eqn.shared_data_bar` are
  always the same.

  In this function `eqn.shared_data_bar` contains the part of `res_bar`
  that is sent in parallel, not `q_bar`.  This allows each process to compute
  the *local* contribution to `eqn.q_bar`, and makes hte parallel communication
  more efficient.  This function should call [`finishExchangeData_rev2`](@ref)
  to finish the parallel comunication.

  **Inputs**

   * mesh
   * sbp
   * eqn: `eqn.res_bar` must have the 3D array form of the seed vector in it
           when this function is called
   * opts
   * t: optional argument giving current time value

"""
function evalResidual_revq(mesh::AbstractMesh, sbp::AbstractOperator,
                           eqn::AbstractSolutionData, opts::Dict, t=0.0)

  error("Generic fallback for evalResidual_revq: did you forget to extend evalResidual_revq with a new method for your AbstractSolutionData")

end

"""
  Function that returns one of the sparsity pattern enums from the Jacobian
  module.  The sparsity pattern enum returned must be at least as wide as
  the true sparsity pattern (it may be wider, but this will result in more
  memory being allocated than necessary).

  **Inputs**

   * mesh: `AbstractMesh` object
   * sbp:  `AbstractOperator` object
   * eqn: `AbstractSolutionData` object, full initialized.  Each physics
          module should specialize this argument
   * opts: options dictonary.

  **Outputs**

   * disc_type: one of the enums
"""
function _getSparsityPattern(mesh::AbstractMesh, sbp::AbstractOperator,
                            eqn::AbstractSolutionData, opts)

  error("Generic fallback for getSparsityPattern() reached: did you forget to extend it with a new method for your AbstractSolutionData?")
end

"""
  This function returns the shock sensor currently in use by the `eqn`
  object.  This function may error if shock capturing is not enabled.

  This function is not required to be type-stable

  **Inputs**

   * eqn: an `AbstractSolutionData`

  **Outputs**

   * an `AbstractShockSensor`
"""
function getShockSensor(eqn::AbstractSolutionData)

  error("Generic fallback for getShockSensor() reached: did you forget to extend it with a new method for your AbstractSolutionData?")

end

"""
  This function sets the shock sensor used by the `eqn`
  object.  This function may error if shock capturing is not enabled.

  **Inputs**

   * eqn: an `AbstractSolutionData`
   * an `AbstractShockSensor`
"""
function setShockSensor(eqn::AbstractSolutionData, sensor::AbstractShockSensor)

  error("Generic fallback for setShockSensor() reached: did you forget to extend it with a new method for your AbstractSolutionData?")

end

"""
  This function sets the arbitrary constant in front of the shock capturing
  term for the `AbstractShockSensor`.

  **Inputs**

   * sensor: an `AbstractShockSensor`
   * alpha: number
"""
function setShockSensorAlpha(eqn::AbstractSolutionData, alpha::Number)

  error("Generic fallback for setShockSensorAlpha() reached: did you forget to extend it with a new method for your AbstractSolutionData?")

end

"""
  Create a new `AbstractShockSensor` object

  **Inputs**

   * mesh
   * sbp
   * eqn: physics modules should specialize this object
   * opts
   * name: the name of the shock sensor to create, default
           opts["shock_sensor_name"]

"""
function createShockSensor(mesh::AbstractMesh, sbp::AbstractOperator,
                        eqn::AbstractSolutionData, opts,
                        name=opts["shock_sensor_name"])

  error("Generic fallback for createShockSensor() reached: did you forget to extend it with a new method for your AbstractSolutionData?")
end

"""
  Copies parameters (Mach number, angle of attack, etc.) from one `eqn` object
  to another`.

  **Inputs**

   * eqn_old

  **Inputs/Outputs**

   * eqn_new
"""
function copyParameters(eqn_old::AbstractSolutionData, eqn_new::AbstractSolutionData)

  error("Generic fallback for copyParameters() reached: did you forget to extend it with a new method for your AbstractSolutionData?")

end

"""
  Function for opening a new set of logging files.  This is useful for
  doing several solves with the same `eqn` object in different directories.

  It is an error to call this function without calling `closeLoggingFiles`
  first.

  This function must be implemented if the `eqn` object keeps file handles
  open, but is optional if not.

  **Inputs**

   * eqn: `AbstractSolutionData`
   * opts: options dictionary
"""
function openLoggingFiles(eqn::AbstractSolutionData, opts)

  return nothing
end

"""
  Closes any logging files the `eqn` object may have open.

  This function must be implemented if the `eqn` object keeps file handles
  open, but is optional if not.

  **Inputs**

   * eqn: `AbstractSolutionData`
   * opts: options dictonary
"""
function closeLoggingFiles(eqn::AbstractSolutionData, opts)

end

"""
  Sets a new flux function for DG-type methods

  **Inputs**

   * mesh
   * sbp
   * eqn
   * opts
   * name: name of new flux function, defaults to opts["flux_name"]
"""
function setFluxFunction(mesh::AbstractDGMesh, sbp::AbstractOperator,
                         eqn::AbstractSolutionData, opts,
                         name::String=opts["Flux_name"])

  error("Generic fallback for setFluxFunction() reached: did you forget to extend it with a new method for your AbstractSolutionData?")
end


using MeshCat, MeshCatMechanisms, RigidBodyDynamics, GeometryTypes, CoordinateTransformations
using Zygote
using LinearAlgebra: I
using BatchedRoutines

"""
Visualize a trajectory in the browser

$(SIGNATURES)

**Arguments**:

- `qs :: Vector{Vector{Float64}}` Position vectors of the trajectory
- `ts` Timesteps of the trajectory
- `mechanism` Robot model as per `RigidBodyDynamics`
- `urdf` Path to URDF of the model
"""
function viz_traj(qs, ts, mechanism, urdf)
    vis = Visualizer()
    open(vis)
    mvis = MechanismVisualizer(mechanism, URDFVisuals(urdf), vis)
    settransform!(vis["/Cameras/default"], compose(Translation(0.0, 1.0, 1.0), LinearMap(RotZ(pi/2))));
    setanimation!(mvis, ts, qs)
end

"""
Matrix-matrix multiplication _batched_ and adjoint defined

$(SIGNATURES)
"""
function batchmul(A::AbstractArray{T,3},B::AbstractArray{V,3}) where {T, V}
    # XXX: To deal with Dual numbers from ForwardDiff
    if T ∉ (Float64, Float32) || V ∉ (Float64, Float32)
        return batchmul_slow(A, B)
    end
    batched_gemm('N','N', A, B)
end

function batchmul_slow(A, B)
    batchmm = [A[:,:,i] * B[:, :, i] for i in 1:size(A, 3)]
    C = reduce((xs,x)->cat(xs, x; dims=3),
               [reshape(batchmm[i], size(batchmm[i])..., 1) for i in 1:size(A)[end]])
    return C
end

Zygote.@adjoint function batchmul(A, B)
    batchmul(A, B), Δ -> (batched_gemm('N' ,'T', Δ , B), batched_gemm( 'T' ,'N', A, Δ))
end

eye(T, p) = Matrix{T}(I, p, p)
Zygote.@nograd eye


# TODO: These tests should be expanded to cover everything

@testset "fixhorizon Grid World" begin
    gw = SimpleGridWorld()
    fhgw = fixhorizon(gw, 2)

    @test length(states(fhgw)) == 3*length(states(gw))
    @test collect(first(state) for state in stage_states(fhgw, 1)) == collect(states(gw))
    @test HorizonLength(fhgw) == FiniteHorizon()
    @test horizon(fhgw) == 2
    @test has_consistent_distributions(fhgw)
    is = (convert(statetype(gw), [1,1]), 1)::statetype(fhgw)
    @show actiontype(fhgw)
    @test length(collect(stepthrough(fhgw, FunctionPolicy(s->:left), is, "s,a,r,sp"))) == 2
end

@testset "fixhorizon Baby" begin
    m = BabyPOMDP()
    fhb = fixhorizon(m, 2)

    @test length(states(fhb)) == 3*length(states(m))
    @test collect(first(state) for state in stage_states(fhb, 1)) == collect(states(m))
    @test HorizonLength(fhb) == FiniteHorizon()
    @test horizon(fhb) == 2
    @test has_consistent_distributions(fhb)
    @test length(collect(stepthrough(fhb, FunctionPolicy(s->true)))) == 2
end

@testset "fixhorizon General" begin
    m = BabyPOMDP()
    fhb = fixhorizon(m, 3)
    state = (false, 2)
    action = true

    states = [(false, 2), (true, 2)]
    @test stateindex(fhb, ordered_states(fhb)[3]) == 3
    @test stage_stateindex(fhb, ordered_states(fhb)[3]) == 1
    @test ordered_states(fhb)[3] == state
    @test ordered_stage_states(fhb, 2) == [(false, 2), (true, 2)]

    obs = [(false, 2), (true, 2)]
    @test collect(first(obs) for obs in stage_observations(fhb, 1)) == collect(observations(m))
    @test obsindex(fhb, obs[2]) == 4
    @test stage_obsindex(fhb, obs[2]) == 2
    @test ordered_stage_observations(fhb, 2) == [(false, 2), (true, 2)]
end

@testset "solver" begin
    fhgw = fixhorizon(SimpleGridWorld(), 3)
    # TODO: Change test to pass without boolean value
    # Dependencies are destroying testing with this (probably until the repos are registered ?)
    # @test test_solver(FiniteHorizonSolver(), fhgw) == 9.025
end


using OpenStreetMapParser, FactCheck

MAP_FILENAME = "tech_square.osm"

if !isfile(MAP_FILENAME)
    url = "https://dl.dropboxusercontent.com/u/8297575/$MAP_FILENAME"
    download(url, MAP_FILENAME)
end

nodes = parseNodes(MAP_FILENAME)
@fact length(nodes.ids) => length(nodes.latlon)
@fact length(nodes.ids) => 1410

@fact length(nodes.tags) => 24
@fact sort(collect(keys(nodes.tags))) =>
    UTF8String["FIXME", "addr:housenumber", "addr:postcode", "addr:street",
               "amenity", "atm", "attribution", "bus", "created_by", "crossing",
               "highway", "line", "massgis:geom_id", "name", "network", "noexit",
               "operator", "public_transport", "railway", "ref", "source",
               "traffic_signals:sound", "url", "wikipedia"]

crossing = nodes.tags["crossing"]
@fact length(crossing) => 14
@fact crossing[61326778]   => "traffic_signals"
@fact crossing[1628602211] => "traffic_signals"
@fact crossing[1628602029] => "traffic_signals"
@fact crossing[1705312831] => "zebra"
@fact crossing[1705308323] => "zebra"
@fact crossing[61317367]   => "traffic_signals"
@fact crossing[1628602239] => "traffic_signals"
@fact crossing[1628602041] => "traffic_signals"
@fact crossing[1628602219] => "traffic_signals"
@fact crossing[1628602201] => "traffic_signals"
@fact crossing[61329208]   => "traffic_signals"
@fact crossing[1628602010] => "traffic_signals"
@fact crossing[1705308324] => "zebra"
@fact crossing[61327535]   => "traffic_signals"

noexit = nodes.tags["noexit"]
@fact length(noexit) => 1
@fact noexit[1230207187] => "yes"

line = nodes.tags["line"]
@fact length(line) => 1
@fact line[69480325] => "Red"

railway = nodes.tags["railway"]
@fact length(railway) => 6
railway[1819633311] => "level_crossing"
railway[69480325]   => "station"
railway[365930364]  => "level_crossing"
railway[1819633315] => "level_crossing"
railway[1819633313] => "level_crossing"
railway[1819633310] => "level_crossing"

highway = nodes.tags["highway"]
@fact highway[1034918607] => "crossing"
@fact highway[1053454370] => "traffic_signals"
@fact highway[1052804034] => "traffic_signals"
@fact highway[61318536]   => "traffic_signals"
@fact highway[1829969262] => "crossing"
@fact highway[1053524037] => "traffic_signals"
@fact highway[1829969266] => "crossing"
@fact highway[1052777483] => "traffic_signals"
@fact highway[1829975813] => "crossing"
@fact highway[1053454390] => "crossing"
@fact highway[1068396444] => "crossing"
@fact highway[1829975825] => "crossing"
@fact highway[1052761909] => "traffic_signals"
@fact highway[1053524038] => "crossing"
@fact highway[2291907804] => "crossing"
@fact highway[61321277]   => "traffic_signals"
@fact highway[1068396443] => "crossing"
@fact highway[1829969270] => "crossing"
@fact highway[1705299287] => "turning_circle"
@fact highway[1053508679] => "traffic_signals"

amenity = nodes.tags["amenity"]
@fact length(amenity) => 4
@fact amenity[1710142648] => "bank"
@fact amenity[1914343564] => "restaurant"
@fact amenity[1749306041] => "restaurant"
@fact amenity[182895807]  => "university"

addr = nodes.tags["addr:housenumber"]
@fact length(addr) => 1
@fact addr[1710142648] => "700"

name = nodes.tags["name"]
@fact length(name) => 8
name[1399777651] => "Broadway opp Hampshire St"
name[1710142648] => "MIT Federal Credit Union"
name[1914343564] => "Catalyst"
name[69480325]   => "Alewife"
name[1399777278] => "Broadway @ Galileo Way"
name[1749306041] => "Area Four"
name[182895807] => "McGovern Institute for Brain Research (MIT)"
name[1399777296] => "Broadway @ Hampshire St"

atm = nodes.tags["atm"]
@fact length(atm) => 1
@fact atm[1710142648] => "yes"

@fact nodes.tags["addr:street"] => Dict(1710142648=>"Technology Square")
@fact nodes.tags["addr:postcode"] => Dict(1710142648=>"02139")
@fact nodes.tags["created_by"] => Dict(61327003 => "JOSM", 61327001 => "JOSM",
                                       61326996 => "JOSM", 61326990 => "JOSM",
                                       61326983 => "JOSM", 61326992 => "JOSM",
                                       61326994 => "JOSM", 61327000 => "JOSM",
                                       61317286 => "JOSM", 61332582 => "JOSM")
@fact nodes.tags["ref"] => Dict(1399777651 => "24485", 1399777278 => "2228",
                                1399777296 => "24486")

ways = parseWays(MAP_FILENAME)
@fact length(ways.ids) => 110
@fact length(ways.ids) => length(ways.nodes)
@fact length(ways.tags) => 89
@fact ways.tags["created_by"] => Dict(8615138  => "JOSM",
                                      29860015 => "Potlatch 0.10f",
                                      8616240  => "Potlatch 0.9c")
@fact ways.tags["name:he"] => Dict(27366257=>"המכון הטכנולוגי של מסצ'וסטס")
@fact ways.tags["massgis:DCAM_ID"] => Dict(29835182=>"0")
@fact ways.tags["layer"] => Dict(236626982=>"-1")
@fact ways.tags["gnis:state_id"] => Dict(27366257=>"25")
@fact ways.tags["ref"] => Dict(29740162 => "NE49", 17660188 => "46",
                               29558550 => "NE80", 29609886 => "NE47",
                               29650626 => "NE48", 29803755 => "48")
@fact ways.tags["massgis:OS_DEED_PA"] => Dict(29835182=>"0")
@fact ways.tags["ramp:wheelchair"] => Dict(106991439=>"yes")
@fact ways.tags["massgis:TOWN_ID"] => Dict(29835182=>"49")
@fact length(ways.tags["building"]) => 32
@fact ways.tags["massgis:ref"] => Dict(90719013  => "0 N324", 239457938 => "0 N323",
                                       8614848   => "0 N324", 33720430  => "0 N324",
                                       34065677  => "0 N323", 239457937 => "0 N323",
                                       138749269 => "0 N323", 8614999   => "0 N324")

relations = parseRelations(MAP_FILENAME)

@fact relations.ids => [1763392, 2119819, 2119824, 2119848, 1600292, 1412944]

@fact length(relations.members) => 6
@fact relations.members[2119819] => [Dict("role"=>"outer","type"=>"way","ref"=>"158302371"),
                                     Dict("role"=>"inner","type"=>"way","ref"=>"29860629"),
                                     Dict("role"=>"inner","type"=>"way","ref"=>"29624640")]

@fact relations.members[2119824] => [Dict("role"=>"outer","type"=>"way","ref"=>"38096750"),
                                     Dict("role"=>"inner","type"=>"way","ref"=>"29947888"),
                                     Dict("role"=>"inner","type"=>"way","ref"=>"29810320"),
                                     Dict("role"=>"inner","type"=>"way","ref"=>"29863742"),
                                     Dict("role"=>"inner","type"=>"way","ref"=>"158302371")]
@fact relations.members[2119848] => [Dict("role"=>"outer","type"=>"way","ref"=>"158305166"),
                                    Dict("role"=>"inner","type"=>"way","ref"=>"158305165"),
                                    Dict("role"=>"inner","type"=>"way","ref"=>"158305356")]
@fact relations.members[1763392] => [Dict("role"=>"via","type"=>"node","ref"=>"1034918626"),
                                     Dict("role"=>"from","type"=>"way","ref"=>"8615585"),
                                     Dict("role"=>"to","type"=>"way","ref"=>"233319829")]
@fact length(relations.members[1600292]) => 58
@fact length(relations.members[1412944]) => 18

@fact length(relations.tags) => 17
@fact relations.tags["name"] => Dict(2119824 => "One Kendall Square",
                                     1600292 => "Red Line",
                                     1412944 => "Grand Junction Branch")
@fact relations.tags["wikipedia"] => Dict(1600292=>"en:Red Line (MBTA)")
@fact relations.tags["old_railway_operator"] => Dict(1412944=>"NYC")
@fact relations.tags["ref"] => Dict(1600292=>"Red Line")
@fact relations.tags["landuse"] => Dict(2119824=>"commercial")
@fact relations.tags["colour"] => Dict(1600292=>"#FF0000")
@fact relations.tags["route"] => Dict(1600292=>"subway",1412944=>"railway")
@fact relations.tags["area"] => Dict(2119819=>"yes",2119848=>"yes")
@fact relations.tags["highway"] => Dict(2119819=>"pedestrian",2119848=>"pedestrian")
@fact relations.tags["operator"] => Dict(1600292=>"MTBA")
@fact relations.tags["restriction"] => Dict(1763392=>"no_right_turn")
@fact relations.tags["network"] => Dict(1600292=>"BOS subway")
@fact relations.tags["website"] => Dict(2119824=>"http://www.onekendallsquare.com/")
@fact relations.tags["to"] => Dict(1600292=>"Mattapan")
@fact relations.tags["from"] => Dict(1600292=>"Alewife")
@fact relations.tags["type"] => Dict(2119819 => "multipolygon",
                                     2119824 => "multipolygon",
                                     2119848 => "multipolygon",
                                     1763392 => "restriction",
                                     1600292 => "route",
                                     1412944 => "route")
@fact relations.tags["FIXME"] => Dict(1412944=>"Was this originally continuous?")

nodes_df = osmDataFrame(nodes)
ways_df = osmDataFrame(ways)
relations_df = osmDataFrame(relations)

building = ways.tags["building"]
@fact length(building) => 32
@fact building[29842810]  => "yes"
@fact building[29740162]  => "university"
@fact building[29810276]  => "yes"
@fact building[37095599]  => "yes"
@fact building[212292730] => "yes"
@fact building[29866161]  => "yes"
@fact building[29627050]  => "yes"
@fact building[29625886]  => "yes"
@fact building[29627492]  => "yes"
@fact building[29628336]  => "yes"
@fact building[29947888]  => "yes"
@fact building[29575544]  => "yes"
@fact building[29558550]  => "university"
@fact building[29609886]  => "university"
@fact building[29862486]  => "yes"
@fact building[29650626]  => "university"
@fact building[29616645]  => "yes"
@fact building[29650632]  => "yes"

highways = ways.tags["highway"]
@fact length(highway) => 55
@fact highway[84967434]  => "footway"
@fact highway[106989193] => "footway"
@fact highway[158302370] => "service"
@fact highway[8615863]   => "footway"
@fact highway[34032974]  => "cycleway"
@fact highway[106987173] => "footway"
@fact highway[39551418]  => "footway"
@fact highway[239457940] => "secondary"
@fact highway[90758903]  => "secondary"
@fact highway[58336059]  => "service"
@fact highway[158304546] => "service"
@fact highway[106989189] => "pedestrian"
@fact highway[8614893]   => "secondary"
@fact highway[239457935] => "secondary"
@fact highway[8616187]   => "residential"
@fact highway[239457936] => "secondary"
@fact highway[149924230] => "secondary"
@fact highway[8615495]   => "secondary"
@fact highway[90719013]  => "secondary"
@fact highway[239457939] => "secondary"


# Quick example

using BinaryTraits
using BinaryTraits.Prefix: Can

# Let's define two ability traits
abstract type Ability end
@trait Fly as Ability
@trait Swim as Ability

# Define interface contracts for the type.
@implement Can{Fly} by liftoff(_)
@implement Can{Fly} by fly(_, direction::Float64, altitude::Float64)
@implement Can{Fly} by speed(_)::Float64

# Define a data type and assign it traits.
struct Crane end
@assign Crane with Can{Fly},Can{Swim}

# Check compliance.
@check(Crane)
#=
┌ Warning: Missing implementation
│   contract = BinaryTrait{Fly}: Positive{Fly} ⇢ liftoff(🔹)::Any
└ @ BinaryTraits ~/.julia/dev/BinaryTraits.jl/src/interface.jl:59
┌ Warning: Missing implementation
│   contract = BinaryTrait{Fly}: Positive{Fly} ⇢ fly(🔹, ::Float64, ::Float64)::Any
└ @ BinaryTraits ~/.julia/dev/BinaryTraits.jl/src/interface.jl:59
┌ Warning: Missing implementation
│   contract = BinaryTrait{Fly}: Positive{Fly} ⇢ speed(🔹)::Float64
└ @ BinaryTraits ~/.julia/dev/BinaryTraits.jl/src/interface.jl:59
❌ Crane is missing these implementations:
1. BinaryTrait{Fly}: Positive{Fly} ⇢ liftoff(🔹)::Any
2. BinaryTrait{Fly}: Positive{Fly} ⇢ fly(🔹, ::Float64, ::Float64)::Any
3. BinaryTrait{Fly}: Positive{Fly} ⇢ speed(🔹)::Float64
=#

# What about composite traits?
@trait FlySwim with Can{Fly},Can{Swim}

# Define a new type and assign with composite trait
struct Swan end
@assign Swan with Can{Fly}, Can{Swim}

# Check compliance. It should automatically drill down to figure out
# the required interface contracts.
@check(Swan)
#=
┌ Warning: Missing implementation
│   contract = BinaryTrait{Fly}: Positive{Fly} ⇢ liftoff(🔹)::Any
└ @ BinaryTraits ~/Library/Mobile Documents/com~apple~CloudDocs/Programming/Julia/BinaryTraits.jl/src/interface.jl:59
┌ Warning: Missing implementation
│   contract = BinaryTrait{Fly}: Positive{Fly} ⇢ fly(🔹, ::Float64, ::Float64)::Any
└ @ BinaryTraits ~/Library/Mobile Documents/com~apple~CloudDocs/Programming/Julia/BinaryTraits.jl/src/interface.jl:59
┌ Warning: Missing implementation
│   contract = BinaryTrait{Fly}: Positive{Fly} ⇢ speed(🔹)::Float64
└ @ BinaryTraits ~/Library/Mobile Documents/com~apple~CloudDocs/Programming/Julia/BinaryTraits.jl/src/interface.jl:59
❌ Swan is missing these implementations:
1. BinaryTrait{Fly}: Positive{Fly} ⇢ liftoff(🔹)::Any
2. BinaryTrait{Fly}: Positive{Fly} ⇢ fly(🔹, ::Float64, ::Float64)::Any
3. BinaryTrait{Fly}: Positive{Fly} ⇢ speed(🔹)::Float64
=#