# For generating symbols that work as dataframe column names; symbols
# generated by gensym() don't work for this.
global _rvnum = 0

function _make_rvname(name)
    global _rvnum += 1
    return Symbol("$(name)!$(_rvnum)")
end

function _make_dims(args)
    dims = Vector{Any}()
    for arg in args
        # println("Arg: $arg")

        if @capture(arg, i_Int)  # scalar (must be integer)
            # println("arg is Int")
            dim = i

        elseif @capture(arg, first_Int:last_)   # last can be an int or 'end', which is converted to 0
            # println("arg is range")
            last = last == :end ? 0 : last
            dim = :($first:$last)

        elseif @capture(arg, first_Int:step_:last_)
            # println("arg is step range")
            last = last == :end ? 0 : last
            dim = :($first:$step:$last)

        elseif @capture(arg, s_Symbol)
            if arg == :(:)
                # println("arg is Colon")
                dim = Colon()
            else
                # println("arg is Symbol")
                dim = :($(QuoteNode(s)))
            end

        elseif @capture(arg, s_String)
            dim = s

        elseif isa(arg, Expr) && arg.head == :tuple  # tuple of Strings/Symbols (@capture didn't work...)
            argtype = typeof(arg.args[1])            # ensure all are same type as first element
            if ! isempty(filter(s -> typeof(s) != argtype, arg.args))
                error("A parameter dimension tuple must all String or all Symbol (got $arg)")
            end
            dim = :(convert(Vector{$argtype}, $(arg.args)))

        else
            error("Unrecognized stochastic parameter specification: $arg")
        end
        push!(dims, dim)
    end
    # println("dims = $dims")
    return dims
end

macro defmcs(expr)
    let # to make vars local to each macro invocation
        local _rvs        = []
        local _corrs      = []
        local _transforms = []
        local _saves      = []

        # distilled into a function since it's called from two branches below
        function saverv(rvname, distname, distargs)
            expr = :(RandomVariable($(QuoteNode(rvname)), $distname($(distargs...))))
            push!(_rvs, esc(expr))
        end

        @capture(expr, elements__)
        for elt in elements
            # Meta.show_sexpr(elt)
            # println("")
            # e.g.,  rv(name1) = Normal(10, 3)
            if @capture(elt, rv(rvname_) = distname_(distargs__))
                saverv(rvname, distname, distargs)

            elseif @capture(elt, save(vars__))
                for var in vars
                    # println("var: $var")
                    if @capture(var, comp_.datum_)
                        expr = :($(QuoteNode(comp)), $(QuoteNode(datum)))
                        push!(_saves, esc(expr))
                    else
                        error("Save arg spec must be of the form comp_name.datum_name; got ($var)")
                    end
                end

            # handle vector of distributions
            elseif @capture(elt, extvar_ = [items__])
                for pair in items
                    if (@capture(pair, [dims__] => distname_(distargs__)) ||
                        @capture(pair,     dim_ => distname_(distargs__)))

                        dims = _make_dims(dims === nothing ? [dim] : dims)

                        rvname = _make_rvname(extvar)
                        saverv(rvname, distname, distargs)

                        expr = :(TransformSpec($(QuoteNode(extvar)), :(=), $(QuoteNode(rvname)), [$(dims...)]))
                        push!(_transforms, esc(expr))
                    end
                end

            # e.g., name1:name2 = 0.7
            elseif @capture(elt, name1_:name2_ = value_)
                expr = :(CorrelationSpec($(QuoteNode(name1)), $(QuoteNode(name2)), $value))
                push!(_corrs, esc(expr))

            # e.g., ext_var5[2010:2050, :] *= name2
            # A bug in Macrotools prevents this shorter expression from working:
            # elseif @capture(elt, ((extvar_  = rvname_Symbol) |
            #                       (extvar_ += rvname_Symbol) |
            #                       (extvar_ *= rvname_Symbol) |
            #                       (extvar_  = distname_(distargs__)) |
            #                       (extvar_ += distname_(distargs__)) |
            #                       (extvar_ *= distname_(distargs__))))
            elseif (@capture(elt, extvar_  = rvname_Symbol) ||
                    @capture(elt, extvar_ += rvname_Symbol) ||
                    @capture(elt, extvar_ *= rvname_Symbol) ||
                    @capture(elt, extvar_  = distname_(distargs__)) ||
                    @capture(elt, extvar_ += distname_(distargs__)) ||
                    @capture(elt, extvar_ *= distname_(distargs__)))

                # For "anonymous" RVs, e.g., ext_var2[2010:2100, :] *= Uniform(0.8, 1.2), we
                # gensym a name based on the external var name and process it as a named RV.
                if rvname === nothing
                    param_name = @capture(extvar, name_[args__]) ? name : extvar
                    rvname = _make_rvname(param_name)
                    saverv(rvname, distname, distargs)
                end

                op = elt.head
                if @capture(extvar, name_[args__])
                    # println("Ref:  $name, $args")
                    # Meta.show_sexpr(extvar)
                    # println("")

                    # if extvar.head == :ref, extvar.args must be one of:
                    # - a scalar value, e.g., name[2050] => (:ref, :name, 2050)
                    #   convert to tuple of dimension specifiers (:name, 2050)
                    # - a slice expression, e.g., name[2010:2050] => (:ref, :name, (:(:), 2010, 2050))
                    #   convert to (:name, 2010:2050) [convert it to actual UnitRange instance]
                    # - a tuple of symbols, e.g., name[(US, CHI)] => (:ref, :name, (:tuple, :US, :CHI))
                    #   convert to (:name, (:US, :CHI))
                    # - combinations of these, e.g., name[2010:2050, (US, CHI)] => (:ref, :name, (:(:), 2010, 2050), (:tuple, :US, :CHI))
                    #   convert to (:name, 2010:2050, (:US, :CHI))

                    dims = _make_dims(args)
                    expr = :(TransformSpec($(QuoteNode(name)), $(QuoteNode(op)), $(QuoteNode(rvname)), [$(dims...)]))
                else
                    expr = :(TransformSpec($(QuoteNode(extvar)), $(QuoteNode(op)), $(QuoteNode(rvname))))
                end
                push!(_transforms, esc(expr))
            else
                error("Unrecognized expression '$elt' in @defmcs")
            end
        end
        return :(MonteCarloSimulation([$(_rvs...)],
                                      [$(_transforms...)],
                                      CorrelationSpec[$(_corrs...)],
                                      Tuple{Symbol, Symbol}[$(_saves...)]))
    end
end


# types from SymEngine to Julia
## CSetBasic
mutable struct CSetBasic
    ptr::Ptr{Cvoid}
end

function CSetBasic()
    z = CSetBasic(ccall((:setbasic_new, libsymengine), Ptr{Cvoid}, ()))
    _finalizer(CSetBasic_free, z)
    z
end

function CSetBasic_free(x::CSetBasic)
    if x.ptr != C_NULL
        ccall((:setbasic_free, libsymengine), Nothing, (Ptr{Cvoid},), x.ptr)
        x.ptr = C_NULL
    end
end

function Base.length(s::CSetBasic)
    ccall((:setbasic_size, libsymengine), UInt, (Ptr{Cvoid},), s.ptr)
end

function Base.getindex(s::CSetBasic, n::UInt)
    result = Basic()
    ccall((:setbasic_get, libsymengine), Nothing, (Ptr{Cvoid}, UInt, Ref{Basic}), s.ptr, n, result)
    result
end

function Base.convert(::Type{Vector}, x::CSetBasic)
    n = Base.length(x)
    [x[i-1] for i in 1:n]
end
Base.convert(::Type{Set}, x::CSetBasic) = Set(convert(Vector, x))

## VecBasic Need this for get_args...

mutable struct CVecBasic
    ptr::Ptr{Cvoid}
end

function CVecBasic()
    z = CVecBasic(ccall((:vecbasic_new, libsymengine), Ptr{Cvoid}, ()))
    _finalizer(CVecBasic_free, z)
    z
end

function CVecBasic_free(x::CVecBasic)
    if x.ptr != C_NULL
        ccall((:vecbasic_free, libsymengine), Nothing, (Ptr{Cvoid},), x.ptr)
        x.ptr = C_NULL
    end
end

function Base.length(s::CVecBasic)
    ccall((:vecbasic_size, libsymengine), UInt, (Ptr{Cvoid},), s.ptr)
end

function Base.getindex(s::CVecBasic, n)
    result = Basic()
    ccall((:vecbasic_get, libsymengine), Nothing, (Ptr{Cvoid}, UInt, Ref{Basic}), s.ptr, UInt(n), result)
    result
end

function Base.convert(::Type{Vector}, x::CVecBasic)
    n = Base.length(x)
    [x[i-1] for i in 1:n]
end

start(s::CVecBasic) = 0
done(s::CVecBasic, i) = (i == Base.length(s))
next(s::CVecBasic, i) = s[i], i+1
if VERSION < v"0.7.0-rc1"
    Base.start(s::CVecBasic) = start(s)
    Base.done(s::CVecBasic, i) = done(s, i)
    Base.next(s::CVecBasic, i) = next(s, i)
else
    function Base.iterate(s::CVecBasic, i=start(s))
        done(s, i) && return nothing
        next(s, i)
    end
end

## CMapBasicBasic
mutable struct CMapBasicBasic
    ptr::Ptr{Cvoid}
end

function CMapBasicBasic()
    z = CMapBasicBasic(ccall((:mapbasicbasic_new, libsymengine), Ptr{Cvoid}, ()))
    _finalizer(CMapBasicBasic_free, z)
    z
end

function CMapBasicBasic(dict::Dict)
    c = CMapBasicBasic()
    for (key, value) in dict
        c[Basic(key)] = Basic(value)
    end
    return c
end

function CMapBasicBasic_free(x::CMapBasicBasic)
    if x.ptr != C_NULL
        ccall((:mapbasicbasic_free, libsymengine), Nothing, (Ptr{Cvoid},), x.ptr)
        x.ptr = C_NULL
    end
end

function Base.length(s::CMapBasicBasic)
    ccall((:mapbasicbasic_size, libsymengine), UInt, (Ptr{Cvoid},), s.ptr)
end

function Base.getindex(s::CMapBasicBasic, k::Basic)
    result = Basic()
    ret = ccall((:mapbasicbasic_get, libsymengine), Cint, (Ptr{Cvoid}, Ref{Basic}, Ref{Basic}), s.ptr, k, result)
    if ret == 0
        throw(KeyError("Key not found"))
    end
    result
end

function Base.setindex!(s::CMapBasicBasic, k::Basic, v::Basic)
    ccall((:mapbasicbasic_insert, libsymengine), Nothing, (Ptr{Cvoid}, Ref{Basic}, Ref{Basic}), s.ptr, k, v)
end

Base.convert(::Type{CMapBasicBasic}, x::Dict{Any, Any}) = CMapBasicBasic(x)

## Dense matrix

mutable struct CDenseMatrix <: DenseArray{Basic, 2}
    ptr::Ptr{Cvoid}
end

Base.promote_rule(::Type{CDenseMatrix}, ::Type{Matrix{T}} ) where {T <: Basic} = CDenseMatrix

function CDenseMatrix_free(x::CDenseMatrix)
    if x.ptr != C_NULL
        ccall((:dense_matrix_free, libsymengine), Nothing, (Ptr{Cvoid},), x.ptr)
        x.ptr = C_NULL
    end
end

function CDenseMatrix()
    z = CDenseMatrix(ccall((:dense_matrix_new, libsymengine), Ptr{Cvoid}, ()))
    _finalizer(CDenseMatrix_free, z)
    z
end

function CDenseMatrix(m::Int, n::Int)
    z = CDenseMatrix(ccall((:dense_matrix_new_rows_cols, libsymengine), Ptr{Cvoid}, (Int, Int), m, n))
    _finalizer(CDenseMatrix_free, z)
    z
end

function CDenseMatrix(x::Array{T, 2}) where T
    r,c = size(x)
    M = CDenseMatrix(r, c)
    for j in 1:c
        for i in 1:r ## fill column by column
            M[i,j] = x[i,j]
        end
    end
    M
end

function Base.convert(::Type{Matrix}, x::CDenseMatrix)
    m,n = Base.size(x)
    [x[i,j] for i in 1:m, j in 1:n]
end

Base.convert(::Type{CDenseMatrix}, x::Array{T, 2}) where {T} = CDenseMatrix(x)
Base.convert(::Type{CDenseMatrix}, x::Array{T, 1}) where {T} = convert(CDenseMatrix, reshape(x, length(x), 1))

function toString(b::CDenseMatrix)
    a = ccall((:dense_matrix_str, libsymengine), Cstring, (Ptr{Cvoid}, ), b.ptr)
    string = unsafe_string(a)
    ccall((:basic_str_free, libsymengine), Nothing, (Cstring, ), a)
    string = replace(string, "**", "^") # de pythonify
    return string
end

function Base.show(io::IO, m::CDenseMatrix)
    r, c = size(m)
    println(io, "CDenseMatrix: $r x $c")
    println(io, toString(m))
end


using KungIChi
using Test

function multiply_by_two(y)
    @test true
    return 2y
end

@schema struct Foo
    x::Int = 1
    y::Int

    @validate function (x)
        @test true
        @assert x < 10
        return x
    end

    @validate multiply_by_two(y)

    @check multiple_of(y, 1.0)
    @check multiple_of(x, 2.0)
    @check function (x)
        x > 0
    end
end

@testset "@validate/@check" begin
    @test Foo(2, 2).y == 4
    @test_throws ValidationError Foo(1, 2)
    @test_throws ValidationError Foo(-2, 2)
end


#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Imports
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

using Main.JuliaSub: countTextualConstr
using Main.JuliaSub: subtc, suptc
using Main.JuliaSub: TxtConstrStat, LBValsFreq, LBStat, FileLBInfo
using Main.JuliaSub: extractLowerBound, extractLowerBounds
using Main.JuliaSub: nonVacuous, lbStatInfo, lbFileInfo
using Main.JuliaSub: FilesLBInfo, PackageStat
using Main.JuliaSub: processPkg, processPkgsDir

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Aux values and functions
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
# Tests
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

# Analysis relies on the fact that there are only few patterns
# where a lower bound can appear.
# If language changes and there are more, the analysis needs
# to be extended
@testset "lb-analysis :: type bounds format" begin
    @test (Meta.parse("f(x::T) where T = 0"); true)
    @test (Meta.parse("f(x::T) where T<:Number = 0"); true)
    @test (Meta.parse("f(x::T) where T>:Int = 0"); true)
    @test (Meta.parse("f(x::T) where Int<:T<:Number = 0"); true)
    @test (Meta.parse("Vector{>:Int}"); true)

    @test_throws Exception (eval(Meta.parse("f(x::T) where Number>:T>:Int = 0")); true)
    @test_throws UndefVarError (eval(Meta.parse("f(x::T) where Int<:T = 0")); true)
end

@testset "lb-analysis :: sub/sup symbols" begin
    @test countTextualConstr(subtc, "")  == 0
    @test countTextualConstr(subtc, ":") == 0
    @test countTextualConstr(subtc, "<") == 0
    @test countTextualConstr(subtc, ">:")  == 0
    @test countTextualConstr(suptc, "  ab ") == 0
    @test countTextualConstr(suptc, "> :") == 0
    @test countTextualConstr(suptc, "<:")  == 0

    @test countTextualConstr(subtc, "<:")   == 1
    @test countTextualConstr(subtc, "<:<")  == 1
    @test countTextualConstr(suptc, ">:")   == 1
    @test countTextualConstr(suptc, "{>:}") == 1
    @test countTextualConstr(suptc, " >::") == 1

    @test countTextualConstr(subtc, "where T <: Int") == 1
    @test countTextualConstr(subtc, "where Int<:T<:Number") == 2
    @test countTextualConstr(suptc, "where T >:   Int ") == 1

    @test countTextualConstr("<: T>:Int Nothing<:S<:Number") ==
        TxtConstrStat(3, 1)
    @test countTextualConstr("< : T>:Int >:>: S>:Number </:") ==
        TxtConstrStat(0, 4)
end

@testset "lb-analysis :: capture direct lower bound" begin
    @test extractLowerBound(:(<:Int)) == nothing
    @test extractLowerBound(:(T <: Number)) == nothing
    @test extractLowerBound(:(Number>:T>:Int)) == nothing

    @test extractLowerBound(:(>:Int)) == :Int
    @test extractLowerBound(:(T>:Int)) == :Int
    @test extractLowerBound(:( T >: Nothing )) == :Nothing
    @test extractLowerBound(:(Int <: T <: Bool)) == :Int
end

@testset "lb-analysis :: capture lower bounds" begin
    @test extractLowerBounds(:(g(xs::Vector{<:Real}) = 0)) ==
        Multiset()
    @test extractLowerBounds(:(f(x::T) where T = 0)) ==
        Multiset()

    @test extractLowerBounds(:(g(xs::Vector{>:Int}) = 0)) ==
        Multiset(:Int)
    @test extractLowerBounds(:(g(xs::Dict{>:String, >:Int}) = 0)) ==
        Multiset(:String, :Int)

    @test extractLowerBounds(:(f(x::T) where T>:Int)) ==
        Multiset(:Int)
    @test extractLowerBounds(:(f(x::T) where {T>:Int, Nothing<:S<:Number} = 0)) ==
        Multiset(:Int, :Nothing)
    @test extractLowerBounds(:(f(x::T) where Int<:T where S<:Number = 0)) ==
        Multiset()
    @test extractLowerBounds(:(f(x::T) where T>:Int where S>:Int = 0)) ==
        Multiset(:Int, :Int)
    @test extractLowerBounds(:(f(x::T) where Int<:T<:S where S<:Number = 0)) ==
        Multiset(:Int)
    @test extractLowerBounds(:(f(x::T) where Int<:T<:S where S<:Number where U>:Bool = 0)) ==
        Multiset(:Int, :Bool)
    @test extractLowerBounds(:(f(x::T) where Int<:T<:S where {S<:Number, Int<:Q<:Nothing} where U>:Bool = 0)) ==
        Multiset(:Int, :Int, :Bool)
    @test extractLowerBounds(quote
            f(x) = 5
            f(x, y::T) where T>:S where S = x
            const X = 7
            g(x::T) where {Int<:T<:Number, S<:Bool} = x
            h(d::Dict{K,>:Int}) where K<:Vector{>:Bool} :: Vector{>:Missing} = d
        end) ==
        Multiset(:Int, :S, :Int, :Bool, :Missing)
end

@testset "lb-analysis :: nonVacuous" begin
    @test nonVacuous(TxtConstrStat(1, 0))
    @test nonVacuous(TxtConstrStat(0, 6))
    @test nonVacuous(TxtConstrStat(4, 1))
    @test !nonVacuous(TxtConstrStat(0, 0))
end

@testset "lb-analysis :: lower-bounds statistics" begin
    @test lbStatInfo("f(x::T) where T>:Int = 0") ==
        LBStat(1, 1, LBValsFreq(:Int))
    @test lbStatInfo("f(x::T) where {T>:Int, Int<:S<:Number} = 0") ==
        LBStat(2, 1, LBValsFreq(:Int, :Int))
    @test lbStatInfo("f(x::T) where {T>:Int, Int<:S<:Number, Q>:Missing} = 0") ==
        LBStat(3, 2, LBValsFreq(:Int, :Int, :Missing))
end

@testset "lb-analysis :: lower-bounds file statistics" begin
    @test lbFileInfo("")  == FileLBInfo(TxtConstrStat())
    @test lbFileInfo("<") == FileLBInfo(TxtConstrStat())
    @test lbFileInfo(">:") ==
        FileLBInfo(TxtConstrStat(0,1), LBStat(LBValsFreq()))
    @test lbFileInfo("f(x::T) where {T>:Int, Int<:S<:Number, Q>:Missing} = 0") ==
        FileLBInfo(TxtConstrStat(2,2), LBStat(LBValsFreq(:Int, :Int, :Missing)))

    @test lbFileInfo(testFilePath("fRet10.jl"); isPath=true) ==
        FileLBInfo(TxtConstrStat(3,4), LBStat(LBValsFreq(:Int, :Bool, :Nothing, :String, :T)))

    failedFileLBInfo = lbFileInfo("f(x::T) wher T>:Int = 0")
    @test failedFileLBInfo.txtStat == TxtConstrStat(0, 1)
    @test isa(failedFileLBInfo.err, Base.Meta.ParseError)
    @test failedFileLBInfo.lbStat == nothing
end

@testset "lb-analysis :: lower-bounds package statistics" begin
    pkgBad = "FAKE-bad.jl"
    pkgStatBad = PackageStat(pkgBad, false)
    @test processPkg(testFilePath(joinpath("pkgs", pkgBad)), pkgBad) ==
        pkgStatBad

    pkgGood = "FAKE-fRet10.jl"
    pkgStatGood = PackageStat(
        pkgGood, true, 3, 0, 2,
        FilesLBInfo(
            joinpath("src", "fRet10.jl") =>
                FileLBInfo(TxtConstrStat(3,4), LBStat(LBValsFreq(:Int, :Bool, :Nothing, :String, :T))),
            joinpath("src", "id.jl") =>
                FileLBInfo(TxtConstrStat(0,2), LBStat(LBValsFreq(:Any, :Int)))),
        LBStat(LBValsFreq(:Int,:Int, :Bool, :Nothing, :Any, :String, :T)))
    @test processPkg(testFilePath(joinpath("pkgs", pkgGood)), pkgGood) ==
        pkgStatGood

    (badPkgs, goodPkgs, totalStat) = processPkgsDir(testFilePath("pkgs"))
    getName(pkg :: PackageStat) = pkg.name
    @test badPkgs == [pkgStatBad]
    @test sort(goodPkgs, by=getName) ==
        sort([pkgStatGood, PackageStat("FAKE-empty.jl", true)], by=getName)
    @test totalStat == LBStat(7, 6, LBValsFreq(:Int,:Int, :Bool, :Nothing, :Any, :String, :T))
end


export OffPolicy, VectorWSARTTrajectory

Base.@kwdef struct OffPolicy{P,B} <: AbstractPolicy
    π_target::P
    π_behavior::B
end

(π::OffPolicy)(env) = π.π_behavior(env)

const VectorWSARTTrajectory = Trajectory{<:NamedTuple{(:weight, SART...)}}

function VectorWSARTTrajectory(;weight=Float64, state=Int, action=Int, reward=Float32, terminal=Bool)
    VectorTrajectory(;weight=Float64, state=state, action=action, reward=reward, terminal=terminal)
end

function RLBase.update!(
    p::OffPolicy,
    t::VectorWSARTTrajectory,
    e::AbstractEnv,
    s::AbstractStage
)
    update!(p.π_target, t, e, s)
end

function RLBase.update!(
    t::VectorWSARTTrajectory,
    p::OffPolicy,
    env::AbstractEnv,
    ::PreActStage,
    a
)
    s = state(env)
    push!(t[:state], s)
    push!(t[:action], a)

    w = prob(p.π_target, env, a) / prob(p.π_behavior, env, a)
    push!(t[:weight], w)
end

function RLBase.update!(
    t::VectorWSARTTrajectory,
    p::Union{OffPolicy{<:QBasedPolicy{<:TDLearner}}, OffPolicy{<:VBasedPolicy{<:MonteCarloLearner}}},
    env::AbstractEnv,
    s::PreEpisodeStage,
)
    empty!(t)
end

function RLBase.update!(
    t::VectorWSARTTrajectory,
    p::Union{OffPolicy{<:QBasedPolicy{<:TDLearner}}, OffPolicy{<:VBasedPolicy{<:MonteCarloLearner}}},
    env::AbstractEnv,
    s::PostEpisodeStage,
)
    action = rand(action_space(env))

    push!(t[:state], state(env))
    push!(t[:action], action)
    push!(t[:weight], 1.0)
end


# Masked Eps Greedy Policy
"""
Epsilon greedy policy that operates within a safety mask. Both actions from the greedy part and the random part are drawn from the safe actions returned by
the safety mask.
`MaskedEpsGreedyPolicy{S, A, M}(mdp::MDP{S, A}, epsilon::Float64, mask::M, rng::AbstractRNG)`
"""
struct MaskedEpsGreedyPolicy{M <: SafetyMask} <: Policy
    val::ValuePolicy # the greedy policy
    epsilon::Float64
    mask::M
    rng::AbstractRNG
end

MaskedEpsGreedyPolicy(mdp::MDP{S, A}, epsilon::Float64, mask::M, rng::AbstractRNG) where {S, A, M <: SafetyMask} = MaskedEpsGreedyPolicy{M}(ValuePolicy(mdp), epsilon, mask, rng)

function POMDPs.action(policy::MaskedEpsGreedyPolicy{M}, s) where M
    acts = safe_actions(policy.mask, s)
    if rand(policy.rng) < policy.epsilon
        return rand(policy.rng, acts)
    else
        return best_action(acts, policy.val, s)
    end
end

"""
A value policy that operates within a safety mask, it takes the action in the set of safe_actions that maximizes the given value function.
`MaskedValuePolicy{M <: SafetyMask}(val::ValuePolicy, mask::M`
"""
struct MaskedValuePolicy{M <: SafetyMask} <: Policy
    val::ValuePolicy
    mask::M
end

function POMDPs.action(policy::MaskedValuePolicy{M}, s) where M
    acts = safe_actions(policy.mask, s)
    return best_action(acts, policy.val, s)
end

function best_action(acts::Vector{A}, policy::ValuePolicy{A, M}, s) where {A,M}
    si = stateindex(policy.mdp, s)
    best_ai = 1
    best_val = policy.value_table[si, best_ai]
    for a in acts
        ai = actionindex(policy.mdp, a)
        val =  policy.value_table[si, ai]
        if val > best_val
            best_ai = ai
            best_val = val
        end
    end
    return policy.act[best_ai]
end


# Julia Periodic Table data + lookups
# Walter A. Harrison - Electronic structure & the property of solids

export AtoZ

# First we need to dereference atom name to atomic charge (Z)
AtoZ=Dict(j=>i for (i,j) in enumerate(["H","He",
    "Li","Be","B","C","N","O","F","Ne",
    "Na","Mg","Al","Si","P","S","Cl","Ar",
    "K","Ca","Sc","Ti","V","Cr","Mn","Fe","Co","Ni","Cu","Zn","Ga","Ge","As","Se","Br","Kr",
    "Rb","Sr","Y","Zr","Nb","Mo","Tc","Ru","Rh","Pd","Ag","Cd","In","Sn","Sb","Te","I","Xe",
    "Cs","Ba","La",
    "Ce","Pr","Nd","Pm","Sm","Eu","Gd","Tb","Dy","Ho","Er","Tm","Yb","Lu",
    "Hf","Ta","W","Re","Os","Ir","Pt","Au","Hg","Tl","Pb","Bi","Po","At","Rn",
    "Fr","Ra","Ac","Th","Pa","U","Np","Pu","Am","Cm","Bk","Cf","Es","Fm","Md","No","Lr",
    "Rf","Db","Sg","Bh","Hs","Mt","Ds","Rg","Cn","Nh","Fl","Mc","Lv","Ts","Og"]))
# Magic comprehension ends up like:
# ZfromAtom = Dict("H" => 1, "He" => 2, "Li" => 3)


# Tests that init_score works for multiclass
# reference URL
# https://github.com/Microsoft/LightGBM/issues/1778
# https://stackoverflow.com/questions/57275029/using-the-score-from-first-lightgbm-as-init-score-to-second-lightgbm-gives-diffe

@testset "initScoreTest.jl" begin
    try
        # Test regression estimator.
        if isfile( joinpath(ENV["LIGHTGBM_PATH"],"examples/regression/regression.test") )
            regression_test = readdlm(joinpath(ENV["LIGHTGBM_PATH"] , "examples/regression/regression.test"), '\t');
            regression_train = readdlm(joinpath(ENV["LIGHTGBM_PATH"] , "examples/regression/regression.train"), '\t');
            regression_test_init = readdlm(joinpath(ENV["LIGHTGBM_PATH"] , "examples/regression/regression.test.init"), '\t')[:,1];
            regression_train_init = readdlm(joinpath(ENV["LIGHTGBM_PATH"] , "examples/regression/regression.train.init"), '\t')[:,1];
        else
            res = HTTP.get("https://raw.githubusercontent.com/microsoft/LightGBM/v2.3.1/examples/regression/regression.test");
            work=String(res.body);
            regression_test =convert(Matrix,CSV.read(IOBuffer(work),delim='\t',header=false));

            res = HTTP.get("https://raw.githubusercontent.com/microsoft/LightGBM/v2.3.1/examples/regression/regression.train");
            work=String(res.body);
            regression_train =convert(Matrix,CSV.read(IOBuffer(work),delim='\t',header=false));

            res = HTTP.get("https://raw.githubusercontent.com/microsoft/LightGBM/v2.3.1/examples/regression/regression.test.init");
            work=String(res.body);
            regression_test_init =convert(Matrix,CSV.read(IOBuffer(work),delim='\t',header=false))[:,1];

            res = HTTP.get("https://raw.githubusercontent.com/microsoft/LightGBM/v2.3.1/examples/regression/regression.train.init");
            work=String(res.body);
            regression_train_init =convert(Matrix,CSV.read(IOBuffer(work),delim='\t',header=false))[:,1];
        end

        X_train = regression_train[:, 2:end]
        y_train = regression_train[:, 1]
        X_test = regression_test[:, 2:end]
        y_test = regression_test[:, 1]

        estimator = LightGBM.LGBMRegression(num_iterations = 100,
                                            learning_rate = .05,
                                            feature_fraction = .9,
                                            bagging_fraction = .8,
                                            bagging_freq = 5,
                                            num_leaves = 31,
                                            metric = ["l2"],
                                            metric_freq = 1,
                                            is_training_metric = true,
                                            max_bin = 255,
                                            min_sum_hessian_in_leaf = 5.,
                                            min_data_in_leaf = 100,
                                            max_depth = -1);

        LightGBM.fit(estimator, X_train, y_train, verbosity = 0,init_score=regression_train_init);
        LightGBM.fit(estimator, X_train, y_train, (X_test, y_test), verbosity = 0,init_score=regression_train_init);
        @test true
    catch
        @test false
    end
end


module IPFT

using Distributions
using Random
using Statistics
using StatsBase: sample, mean, weights
import StatsBase: entropy
using LinearAlgebra

# using Parameters
using CPUTime

using POMDPs
using POMDPSimulators
using POMDPModelTools
using MCTS
using MCTS: convert_estimator
using BasicPOMCP
using ParticleFilters
using QMDP

using Test
# using InteractiveUtils

include("information_measures/InformationMeasures.jl")

export
    AbstractMultiObjectiveCriterion,
    MultiObjective,
    MultiObjectiveUCB,
    q_value
include("util/action_selection.jl")

export
    InformationMCTSPlanner,
    InformationBelief,
    pdf,
    resample
include("util/util.jl")

export
    IPFTPlanner,
    IPFTSolver,
    IPFTStateNode,
    IPFTree,
    action_info,
    InformationRewardBeliefMDP
include("core/ipft_types.jl")
include("core/ipft.jl")
include("core/information_mdp.jl")

export
    InformationFilter,
    update,
    initialize_belief
include("util/information_filter.jl")

include("util/rollout.jl")

export
    IBMDPSolver
include("util/ibmdp_solver.jl")

end # module


@testset "BernoulliLikelihood" begin
    lik = BernoulliLikelihood()
    test_interface(lik, SqExponentialKernel(), rand(10))
end


fname = :TRLDLt
c = Combi(hessian_dense,PDataLDLt,solve_modelTRDiag,preprocessLDLt,decreaseFact,Tparam())
include("Template.jl")


# ------------------------------------------------------------------
# Licensed under the ISC License. See LICENSE in the project root.
# ------------------------------------------------------------------

"""
    readgeotable(args; coordnames=[:x,:y,:z], kwargs)

Read data from disk using `CSV.read`, optionally specifying
the columns `coordnames` with spatial coordinates.

The arguments `args` and keyword arguments `kwargs` are
forwarded to the `CSV.read` function, please check their
documentation for more details.

This function returns a [`GeoDataFrame`](@ref) object.
"""
readgeotable(args...; coordnames=[:x,:y,:z], kwargs...) =
  GeoDataFrame(read(args...; kwargs...), coordnames)

"""
      split(object, fraction, [direction])

Split spatial `object` into two parts where the first
part has a `fraction` of the total volume. The split
is performed along a `direction`. The default direction
is aligned with the first spatial dimension of the object.
"""
Base.split(object::AbstractSpatialObject{T,N}, fraction::Real,
           normal=ntuple(i -> i == 1 ? one(T) : zero(T), N)) where {N,T} =
  partition(object, BisectFractionPartitioner(normal, fraction))


using ITensors
using ITensorInfiniteMPS
using Test
using Random

#Currently, VUMPS cannot give the right result as the subspace expansion is too small
#This is meant to test the generalized translation rules
@time @testset "vumpsmpo_fqhe" begin
  Random.seed!(1234)

  function fermions_space_shift(pos; p=1, q=1, conserve_momentum=true, momentum_shift=1)
    if !conserve_momentum
      return [QN("Nf", -p) => 1, QN("Nf", q - p) => 1]
    else
      return [
        QN(("Nf", -p), ("NfMom", -p * pos + momentum_shift)) => 1,
        QN(("Nf", q - p), ("NfMom", (q - p) * pos + momentum_shift)) => 1,
      ]
    end
  end

  function initstate(n)
    if mod(n, 3) == 1
      return 2
    else
      return 1
    end
  end

  # VUMPS arguments
  cutoff = 1e-8
  maxdim = 3
  tol = 1e-8
  maxiter = 50
  outer_iters = 1

  #### Test 1/3
  model = Model"fqhe_2b_pot"()
  model_kwargs = (Ly=3.0, Vs=[1.0], prec=1e-6) #parent Hamiltonian of Laughlin 13
  p = 1
  q = 3
  conserve_momentum = true
  momentum_shift = 1

  #ψ1 = InfMPS(s, n->[1, 2, 2, 1, 1, 1][n]);

  @testset "VUMPS/TDVP with: multisite_update_alg = $multisite_update_alg, conserve_qns = $conserve_qns, nsites = $nsites" for multisite_update_alg in
                                                                                [
      "sequential"
    ],
    conserve_qns in [true],
    nsites in [6],
    time_step in [-Inf]

    vumps_kwargs = (
      multisite_update_alg=multisite_update_alg,
      tol=tol,
      maxiter=maxiter,
      outputlevel=0,
      time_step=time_step,
    )
    subspace_expansion_kwargs = (cutoff=cutoff, maxdim=maxdim)

    function fermion_momentum_translator(i::Index, n::Integer; N=nsites)
      ts = tags(i)
      translated_ts = ITensorInfiniteMPS.translatecelltags(ts, n)
      new_i = replacetags(i, ts => translated_ts)
      for j in 1:length(new_i.space)
        ch = new_i.space[j][1][1].val
        mom = new_i.space[j][1][2].val
        new_i.space[j] = Pair(
          QN(("Nf", ch), ("NfMom", mom + n * N * ch)), new_i.space[j][2]
        )
      end
      return new_i
    end

    fermionic_space = [
      fermions_space_shift(
        x; p=p, q=q, conserve_momentum=conserve_momentum, momentum_shift=momentum_shift
      ) for x in 1:nsites
    ]
    s = infsiteinds(
      "Fermion", nsites; space=fermionic_space, translator=fermion_momentum_translator
    )
    ψ = InfMPS(s, initstate)

    Hmpo = InfiniteMPOMatrix(model, s; model_kwargs...)
    # Alternate steps of running VUMPS and increasing the bond dimension
    ψ = tdvp(Hmpo, ψ; vumps_kwargs...)
    for _ in 1:outer_iters
      println("Subspace expansion")
      ψ = @time subspace_expansion(ψ, Hmpo; subspace_expansion_kwargs...)
      println("TDVP")
      ψ = @time tdvp(Hmpo, ψ; vumps_kwargs...)
    end
  end
end


using SparseArrays
using LinearAlgebra
SigmoidFunc(x) =(0.5*tanh.(5*(x.-0.5)).+0.5 .+ (1-0.5*tanh.(5*(1-0.5))-0.5).*2 .*(x.-0.5));
dsigmoidFunc(x) = 0.0133857 .+ 2.5*((sech.(5*(-0.5 .+ x))).^2);
export getData,SigmoidFunc
function getData(m::Array,pFor::SfSParam,doClear::Bool=false)
d = 0;
Mesh = pFor.ObjMesh;
cellVolume = prod(Mesh.h);
mask = pFor.mask;
n = Mesh.n;
theta_phi = pFor.theta_phi_rad;
nshots = size(theta_phi,1);

numParamOfRBF = 5;

b = pFor.b;

heavisideVisData = (d)->heavySide!(d,copy(d),0.5,0.5);

if pFor.method == MATBased
	error("Not working well. need to fix projection matrix.");
	# println("MAT version")
	# if length(pFor.Jacobian) == 0
		# S = generateProjectionOperatorWithRotation(pFor);
		# pFor.Jacobian = S;
	# end
	# d = pFor.Jacobian'*m[:];
	# d = reshape(d,div(length(d),nshots),nshots);
elseif pFor.method == MATFree
	if length(pFor.SampleMatT) == 0
		pFor.SampleMatT = generateProjectionOperator(pFor);
	end
	traceLength = prod(pFor.ScreenMeshX2X3.n);
	d = zeros(Float32,traceLength,nshots);
	m = reshape(m,tuple(n...));
	mr = zeros(eltype(m),size(m));
	XT = zeros(0);
	XTT = zeros(0);
	for ii = 1:nshots
		doTranspose = false;
		(mr,XT,XTT) = rotateAndMove3D(m,theta_phi[ii,:],(b[ii,:]./Mesh.h),doTranspose,mr,XT,XTT);
		d[:,ii] = softMaxProjection(pFor.SampleMatT,mr[:]);
	end

elseif pFor.method == RBFBasedSimple1 || pFor.method == RBF10BasedSimple1
	error("Not working well. need to fix projection matrix.");
	# if pFor.method == RBF10BasedSimple1
		# numParamOfRBF = 10;
	# else
		# numParamOfRBF = 5;
	# end
	# sigmaH = getDefaultHeavySide();
	# u,I1,J1,V1 = ParamLevelSetModelFunc(Mesh,m;computeJacobian = 1,sigma = sigmaH,bf = 1,numParamOfRBF = numParamOfRBF);
	# J1 = sparse(I1,J1,V1,prod(n),length(m));
	# if length(pFor.SampleMatT) == 0
		# pFor.SampleMatT = generateProjectionOperatorWithRotation(pFor);
	# end
	# d = pFor.SampleMatT'*u[:];
	# d = reshape(d,div(length(d),nshots),nshots);
	# pFor.Jacobian = J1'*pFor.SampleMatT;
elseif pFor.method == RBFBasedSimple2 || pFor.method == RBF10BasedSimple2
	if pFor.method == RBF10BasedSimple2
		numParamOfRBF = 10;
	else
		numParamOfRBF = 5;
	end
	if length(pFor.SampleMatT) == 0
		pFor.SampleMatT = generateProjectionOperator(pFor);
	end
	mrot,Jrot = rotateAndMoveRBFsimple(m,Mesh,theta_phi,b;computeJacobian = 1,numParamOfRBF=numParamOfRBF);
	d,JacT = getVisDataRBF(pFor, mrot,theta_phi,b,mask,heavisideVisData,numParamOfRBF);
	# multiply Jacobian with Jrot, and then take the transpose.
	pFor.Jacobian = Jrot'*JacT;
elseif pFor.method == RBFBased || pFor.method == RBF10Based
	if pFor.method == RBF10Based
		numParamOfRBF = 10;
	else
		numParamOfRBF = 5;
	end
	if length(pFor.SampleMatT) == 0
		pFor.SampleMatT = generateProjectionOperator(pFor);
	end
	## here theta_phi is not constant - so we ignore the theta_phi in pFor
	nRBF 				= div(length(m)- 5*pFor.nshotsAll,numParamOfRBF);
	(m1,theta_phi,b) 	= splitRBFparamAndRotationsTranslations(m,nRBF,pFor.nshotsAll,numParamOfRBF);
	theta_phi 			= theta_phi[pFor.workerSubIdxs,:];
	b 					= b[pFor.workerSubIdxs,:];
	mrot,Jrot 			= rotateAndMoveRBF(m1,Mesh,theta_phi,b;computeJacobian = 1,numParamOfRBF=numParamOfRBF);
	d,JacT 				= getVisDataRBF(pFor, mrot,theta_phi,b,mask,heavisideVisData,numParamOfRBF);
	# multiply Jacobian with Jrot, and then take the transpose.
	Idxs = 				getIpIdxs;

	JacobianT = convert(SparseMatrixCSC{Float64,Int32},spzeros(length(m),prod(size(d))));
	IpIdxs = getIpIdxs(pFor.workerSubIdxs,nRBF,pFor.nshotsAll,numParamOfRBF);
	JacobianT[IpIdxs,:] = Jrot'*JacT;
	pFor.Jacobian = JacobianT;
	GC.gc();
	# pFor.Jacobian 		= Jrot'*JacT;
end
return d,pFor;
end

function getVisDataRBF(pFor, mrot,theta_phi,b,mask,heavisideVisData::Function,numParamOfRBF = 5)
Mesh = pFor.ObjMesh;
n = Mesh.n;
h = Mesh.h;
traceLength = prod(pFor.ScreenMeshX2X3.n);
nshots = size(theta_phi,1);
d = zeros(Float32,traceLength,nshots);
lengthRBFparams = size(mrot,1);
JBuilder = getSpMatBuilder(Int64,Float64,prod(n),lengthRBFparams,10*prod(n))

sigmaH = getDefaultHeaviside();
u = zeros(prod(n));
dsu = zeros(prod(n));
Xc = convert(Array{Float32,2},getCellCenteredGrid(Mesh));

Jacobians = Array{SparseMatrixCSC{Float64,Int64}}(undef,nshots);

for ii = 1:nshots
	u,JBuilder = ParamLevelSetModelFunc(Mesh,mrot[:,ii];computeJacobian = 1,sigma = sigmaH,
										Xc = Xc,u = u,dsu = dsu,Jbuilder = JBuilder,numParamOfRBF=numParamOfRBF);

	# d[:,ii] = softMaxProjWithSensMat(pFor.SampleMatT,u);
	d[:,ii],Jii = softMaxProjWithSigmoid(pFor.SampleMatT,u)
	Jii = getSparseMatrixTransposed(JBuilder)*Jii;
	Jacobians[ii] = Jii;
end
JacT = blockdiag(Jacobians...);
return d,JacT
end

export softMaxProjWithSigmoid
function softMaxProjWithSigmoid(Proj_g::SparseMatrixCSC,u::Vector{Float64})
	etta = 40.0;
	Proj = u.*Proj_g;
	dropzeros!(Proj);
	for rayIdx = 1:size(Proj,2)
		# if Proj.colptr[rayIdx+1] > Proj.colptr[rayIdx]
			# println(Proj.nzval[Proj.colptr[rayIdx]:Proj.colptr[rayIdx+1]-1])
		# end
		kmax = Proj.colptr[rayIdx];
		for k = (Proj.colptr[rayIdx]+1):(Proj.colptr[rayIdx+1]-1)
			if Proj.nzval[k] < Proj.nzval[kmax] + 1e-5
				@inbounds Proj.nzval[k] = 0.0;
			else
				kmax = k;
			end
		end
		# if Proj.colptr[rayIdx+1] > Proj.colptr[rayIdx]
			# println(Proj.nzval[Proj.colptr[rayIdx]:Proj.colptr[rayIdx+1]-1])
			# println("~~~~~~~~~~~~~~~~~~~")
		# end

	end
	dropzeros!(Proj)
	Proj.nzval[:].=1.0;
	relevant = findall(sum(Proj,dims=2)[:].> 1e-16);
	y = zeros(size(u));
	v = zeros(size(u));
	y[relevant]  = exp.(etta*u[relevant]);
	Ay = Proj'*y;
	v[relevant]  = y[relevant].*u[relevant];
	Av = Proj'*v;
	AYinv = 1.0./(Ay .+ 1e-7);
	ans2 = SigmoidFunc(Av.*AYinv);
	dsig = dsigmoidFunc(Av.*AYinv)

	dy = etta*y;
	dv = etta*y.*u + y;

	#Ans = sig(Av./Ay) => J_ans = dsig(Av./Ay)*(dAv./Ay) - dsig(Av./Ay)*(Av*dy/Ay*Ay)
	# dsig(Av./Ay)*Proj*spdiagm(dv)*Proj*spdiagm(AYinv) - dsig(Av./Ay)
	# Jt = spdiagm(dsig(Av./Ay))*spdiagm(dv)*Proj*spdiagm(AYinv) - spdiagm(dsig(Av./Ay))*spdiagm(dy)*Proj*spdiagm(AYinv.*AYinv.*Av)
	for j = 1:size(Proj,2)
		@inbounds AYinv_j = AYinv[j];
		@inbounds Av_j = Av[j];
		for gidx = Proj.colptr[j]:(Proj.colptr[j+1]-1)
			@inbounds nzv = Proj.nzval[gidx];
			@inbounds i = Proj.rowval[gidx];
			@inbounds Proj.nzval[gidx] = dsig[j]*dv[i]*nzv*AYinv_j - dsig[j]*dy[i]*nzv*AYinv_j*AYinv_j*Av_j;
		end
	end
	dropzeros(Proj);
	return ans2,Proj;
end

export softMaxProjection
function softMaxProjection(Proj::SparseMatrixCSC,u::Vector{Float64})
	etta = 40.0;
	y  = exp.(etta*u);
	Ay = Proj'*y;
	v  = y.*u;
	Av = Proj'*v;
	ans = Av./Ay;
	return ans;
end

export softMaxSensMatVec
function softMaxSensMatVec(Proj::SparseMatrixCSC,u::Vector{Float64},vec::Vector{Float64})
	etta = 40.0;
	y  = exp.(etta*u);
	Ay = Proj'*y;
	v  = y.*u;
	Av = Proj'*v;
	dy = etta*y;
	dv = etta*y.*u + y;
	AYinv = 1.0./(Ay .+ 1e-7);
	# Jt = spdiagm(dv)*Proj*spdiagm(AYinv) - spdiagm(dy)*Proj*spdiagm(AYinv.*AYinv.*Av);
	Jvec = AYinv.*(Proj'*(dv.*vec)) - AYinv.*AYinv.*Av.*(Proj'*(dy.*vec))
	return Jvec;
end
export softMaxProjWithSensMat
function softMaxProjWithSensMat(Proj::SparseMatrixCSC,u::Vector{Float64})
	etta = 40.0;
	y  = exp.(etta*u);
	Ay = Proj'*y;
	v  = y.*u;
	Av = Proj'*v;
	ans = Av./Ay;

	dy = etta*y;
	dv = etta*y.*u + y;
	AYinv = 1.0./(Ay .+ 1e-7);
	# The code below does this line only much faster:
	# Jt = spdiagm(dv)*Proj*spdiagm(AYinv) - spdiagm(dy)*Proj*spdiagm(AYinv.*AYinv.*Av);
	Jtt = copy(Proj);
	for j = 1:size(Jtt,2)
		@inbounds AYinv_j = AYinv[j];
		@inbounds Av_j = Av[j];
		for gidx = Jtt.colptr[j]:(Jtt.colptr[j+1]-1)
			@inbounds nzv = Jtt.nzval[gidx];
			@inbounds i = Jtt.rowval[gidx];
			@inbounds Jtt.nzval[gidx] = dv[i]*nzv*AYinv_j - dy[i]*nzv*AYinv_j*AYinv_j*Av_j;
		end
	end
	# println(norm(Jt - Jtt,1));

	return ans,Jtt;
end


function calculate_full_rules_vector(data_dictionary::Dict{String,Any}, value_array::BitArray{2})

	# Initialize -
	v = zeros(159)

	function estimate_default(data_dictionary)
		return 1.0
	end
	default() = true

	function calculate_fn_value(x, value_array)
		return value_array[x]
	end
	fn(x) = calculate_fn_value(x, value_array)

	v[1] = default()
	v[2] = ~(fn(158))
	v[3] = ~(fn(159))
	v[4] = max(~(fn(3)), fn(110))
	v[5] = max(~(fn(3)), fn(110))
	v[6] = default()
	v[7] = default()
	v[8] = default()
	v[9] = default()
	v[10] = fn(11)
	v[11] = ~(fn(169))
	v[12] = default()
	v[13] = default()
	v[14] = default()
	v[15] = default()
	v[16] = max(max(~(max(fn(152), fn(43))), fn(111)), fn(110))
	v[17] = max(max(~(max(fn(152), fn(43))), fn(111)), fn(110))
	v[18] = max(max(~(max(fn(152), fn(43))), fn(111)), fn(110))
	v[19] = max(max(~(max(fn(152), fn(43))), fn(111)), fn(110))
	v[20] = default()
	v[21] = default()
	v[22] = default()
	v[23] = default()
	v[24] = max(~(fn(43)), fn(152))
	v[25] = max(~(fn(43)), fn(152))
	v[26] = default()
	v[27] = default()
	v[28] = default()
	v[29] = default()
	v[30] = default()
	v[31] = default()
	v[32] = max(fn(152), min(fn(43), fn(111)))
	v[33] = max(fn(152), min(fn(43), fn(111)))
	v[34] = max(fn(152), min(fn(43), fn(111)))
	v[35] = default()
	v[36] = default()
	v[37] = max(~(fn(48)), ~(fn(2)))
	v[38] = default()
	v[39] = max(fn(163), ~(fn(160)))
	v[40] = max(max(~(fn(168)), ~(min(fn(168), fn(2)))), fn(110))
	v[41] = default()
	v[42] = default()
	v[43] = ~(fn(168))
	v[44] = default()
	v[45] = default()
	v[46] = default()
	v[47] = max(~(fn(163)), min(fn(167), ~(fn(111))))
	v[48] = ~(fn(163))
	v[49] = default()
	v[50] = default()
	v[51] = ~(fn(152))
	v[52] = ~(max(fn(152), fn(43)))
	v[53] = default()
	v[54] = ~(fn(2))
	v[55] = fn(2)
	v[56] = default()
	v[57] = ~(max(fn(67), fn(164)))
	v[58] = default()
	v[59] = default()
	v[60] = default()
	v[61] = max(fn(155), ~(fn(48)))
	v[62] = max(fn(155), ~(fn(48)))
	v[63] = max(fn(155), ~(fn(48)))
	v[64] = default()
	v[65] = default()
	v[66] = default()
	v[67] = fn(157)
	v[68] = default()
	v[69] = default()
	v[70] = default()
	v[71] = ~(max(fn(152), fn(43)))
	v[72] = ~(max(fn(152), fn(43)))
	v[73] = ~(max(fn(152), fn(43)))
	v[74] = ~(max(fn(152), fn(43)))
	v[75] = ~(max(fn(152), fn(43)))
	v[76] = ~(max(fn(152), fn(43)))
	v[77] = ~(max(fn(152), fn(43)))
	v[78] = ~(max(fn(152), fn(43)))
	v[79] = ~(max(fn(152), fn(43)))
	v[80] = ~(max(fn(152), fn(43)))
	v[81] = ~(max(fn(152), fn(43)))
	v[82] = ~(max(fn(152), fn(43)))
	v[83] = ~(max(fn(152), fn(43)))
	v[84] = default()
	v[85] = default()
	v[86] = default()
	v[87] = default()
	v[88] = default()
	v[89] = default()
	v[90] = default()
	v[91] = default()
	v[92] = default()
	v[93] = max(fn(152), min(fn(43), fn(111)))
	v[94] = default()
	v[95] = default()
	v[96] = default()
	v[97] = default()
	v[98] = default()
	v[99] = default()
	v[100] = default()
	v[101] = min(~(fn(152)), fn(103))
	v[102] = min(~(fn(152)), fn(103))
	v[103] = fn(160)
	v[104] = ~(fn(10))
	v[105] = max(fn(111), fn(43))
	v[106] = max(fn(111), fn(43))
	v[107] = ~(min(fn(156), fn(164)))
	v[108] = ~(min(fn(156), fn(164)))
	v[109] = ~(fn(152))
	v[110] = fn(171)
	v[111] = fn(154)
	v[112] = default()
	v[113] = default()
	v[114] = default()
	v[115] = min(min(fn(155), ~(fn(152))), fn(143))
	v[116] = ~(fn(152))
	v[117] = default()
	v[118] = default()
	v[119] = default()
	v[120] = default()
	v[121] = default()
	v[122] = default()
	v[123] = default()
	v[124] = default()
	v[125] = default()
	v[126] = default()
	v[127] = ~(fn(167))
	v[128] = fn(111)
	v[129] = default()
	v[130] = default()
	v[131] = default()
	v[132] = max(fn(152), fn(43))
	v[133] = max(fn(152), fn(43))
	v[134] = default()
	v[135] = default()
	v[136] = min(~(fn(138)), max(~(fn(152)), fn(2)))
	v[137] = min(~(fn(138)), max(~(fn(152)), fn(2)))
	v[138] = fn(39)
	v[139] = default()
	v[140] = default()
	v[141] = default()
	v[142] = max(max(fn(43), fn(111)), fn(143))
	v[143] = fn(144)
	v[144] = max(max(fn(170), fn(162)), fn(166))
	v[145] = max(fn(43), fn(143))
	v[146] = max(fn(43), fn(143))
	v[147] = max(fn(43), fn(143))
	v[148] = max(fn(43), fn(143))
	v[149] = default()
	v[150] = default()
	v[151] = default()
	v[152] = ~(fn(168))
	v[153] = default()
	v[154] = ~(fn(163))
	v[155] = ~(max(max(fn(163), fn(166)), fn(165)))
	v[156] = fn(157)
	v[157] = fn(127)
	v[158] = max(min(~(fn(172)), ~(max(max(fn(182), fn(181)), fn(178)))), fn(161))
	v[159] = min(~(max(fn(176), fn(175))), ~(max(max(max(max(fn(173), fn(179)), fn(177)), fn(174)), fn(180))))

	return v
end


const COMPRESSED = 2
const UNCOMPRESSED = 4
# const POINT_CONVERSION_HYBRID = 6

const COMPRESSED_LENGTH = 33
const UNCOMPRESSED_LENGTH = 65

# TODO: Assume maximum signature size is 144 bytes (way overkill)
const SIGNATURE_SIZE = 144

const NID_secp256k1 = 714

# curve_id: https://github.com/openssl/openssl/blob/master/crypto/objects/obj_mac.h
# form: POINT_CONVERSION_[UNCOMPRESSED|COMPRESSED]
function ec_pub_key(priv_key::Array{UInt8}; curve_id = NID_secp256k1, form = UNCOMPRESSED)
  pub_key_length = form == UNCOMPRESSED ? UNCOMPRESSED_LENGTH : COMPRESSED_LENGTH
  pub_key = zeros(UInt8, pub_key_length)

  ccall((:ec_pub_key, libcryptojl),                               # Function call
          Void,                                                   # Return type Void
          (Ptr{UInt8}, Ptr{UInt8}, Int, Int, Int),                # Argument types
          pub_key, priv_key, length(priv_key), curve_id, form)     # Arguments

  return pub_key
end

# priv_key as hex string
function ec_pub_key(priv_key::ByteString; curve_id = NID_secp256k1, form = UNCOMPRESSED)
  ec_pub_key(hex2bytes(priv_key), curve_id = curve_id, form = form)
end

# https://www.openssl.org/docs/crypto/ecdsa.html
function ec_sign(hash, priv_key; curve_id = NID_secp256k1)
  sig = zeros(UInt8, SIGNATURE_SIZE)
  siglen = ccall((:ec_sign, libcryptojl),
                  UInt,
                  (Ptr{UInt8}, Ptr{UInt8}, Int, Ptr{UInt8}, Int, Int),
                  sig, hash, length(hash), priv_key, length(priv_key), curve_id)
  return sig[1:siglen]
end

function ec_verify(hash, sig, pub_key; curve_id = NID_secp256k1)
  return true == ccall((:ec_verify, libcryptojl),
                        Int,
                        (Ptr{UInt8}, Int, Ptr{UInt8}, Int, Ptr{UInt8}, Int, Int),
                        hash, length(hash), sig, length(sig), pub_key, length(pub_key), curve_id)
end


# Copyright 2017 Eric S. Tellez
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

using KernelMethods
using KernelMethods: precision

using SimilaritySearch
# import KernelMethods.Nets: KlusterClassifier
using Test

@testset "Scores" begin
    @test accuracy([1,1,1,1,1], [1,1,1,1,1]) == 1.0
    @test accuracy([1,1,1,1,1], [0,0,0,0,0]) == 0.0
    @test accuracy([1,1,1,1,0], [0,1,1,1,1]) == 0.6
    @test precision_recall([0,1,1,1,0,1], [0,1,1,1,1,1]) == (precision=0.8333333333333334, recall=0.8333333333333334,
        per_class=Dict(0 => (precision=1.0, recall=0.5, population=2), 1 => (precision=0.8, recall=1.0, population=4)))
    @test precision([0,1,1,1,0,1], [0,1,1,1,1,1]) == 0.9
    @test recall([0,1,1,1,0,1], [0,1,1,1,1,1]) == 0.75
    @test precision([0,1,1,1,0,1], [0,1,1,1,1,1], weight=:weighted) == (1.0 * 2/6 + 0.8 * 4/6) / 2
    @test recall([0,1,1,1,0,1], [0,1,1,1,1,1], weight=:weighted) == (0.5 * 2/6 + 1.0 * 4/6) / 2
    @test f1([0,1,1,1,0,1], [0,1,1,1,1,1], weight=:macro) ≈ (2 * 0.5 / 1.5 + 2 * 0.8 / 1.8) / 2
    #@show f1([0,1,1,1,0,1], [0,1,1,1,1,1], weight=:weighted) # ≈ (2/6 * 2 * 0.5 / 1.5 + 4 / 6 * 2 * 0.8 / 1.8) / 2
end

@testset "CrossValidation" begin
    data = collect(1:100)
    function f(train_X, train_y, test_X, test_y)
        @test train_X == train_y
        @test test_X == test_y
        @test length(train_X ∩ test_X) == 0
        @test length(train_X ∪ test_X) >= 99
        1
    end
    @test montecarlo(f, data, data, runs=10) |> sum == 10
    @test kfolds(f, data, data, folds=10, shuffle=true) |> sum == 10
end

include("oneclass.jl")
include("loaddata.jl")

@testset "KNN" begin
    X, y = loadiris()
    nnc = NearNeighborClassifier(l2_distance, X, y)
    @test optimize!(nnc, accuracy, runs=5, trainratio=0.2, testratio=0.2)[1][1] > 0.8
    @test optimize!(nnc, accuracy, runs=5, trainratio=0.3, testratio=0.3)[1][1] > 0.8
    @test optimize!(nnc, accuracy, runs=5, trainratio=0.7, testratio=0.3)[1][1] > 0.8

    @test optimize!(nnc, accuracy, folds=2)[1][1] > 0.8
    @test optimize!(nnc, accuracy, folds=3)[1][1] > 0.8
    @test optimize!(nnc, accuracy, folds=5)[1][1] > 0.85
    @test optimize!(nnc, accuracy, folds=10)[1][1] > 0.85
    @show optimize!(nnc, accuracy, folds=5)
    @test sum([maximum(x) for x in predict_proba(nnc, X, smoothing=0)])/ length(X) > 0.8 ## close to have all ones, just in case
    @test sum([maximum(x) for x in predict_proba(nnc, X, smoothing=0.01)])/ length(X) > 0.8 ## close to have all ones, just in case
end

@testset "NB" begin
    X, y = loadiris()
    nbc = NaiveBayesClassifier(X, y)
    @test optimize!(nbc, X, y, accuracy, runs=5, trainratio=0.2, testratio=0.2)[1][1] > 0.8
    @test optimize!(nbc, X, y, accuracy, runs=5, trainratio=0.3, testratio=0.3)[1][1] > 0.8
    @test optimize!(nbc, X, y, accuracy, runs=5, trainratio=0.5, testratio=0.5)[1][1] > 0.8
    @test optimize!(nbc, X, y, accuracy, runs=5, trainratio=0.7, testratio=0.3)[1][1] > 0.8
    @show optimize!(nbc, X, y, accuracy, runs=5, trainratio=0.7, testratio=0.3)
end

include("knnreg.jl")
include("kmap.jl")

include("kclass.jl")


using Images
using ImageView
using EMIRT

println("compare image and segmentation")
println("Usage: julia compare_img_seg.jl /path/of/img.h5 /path/of/seg.h5")
@assert length(ARGS) >= 2

fimg = ARGS[1]
fseg = ARGS[2]

img = readimg(fimg)[:,:,1:64]
seg = readseg(fseg)[:,:,1:64]

include(joinpath(Pkg.dir(), "EMIRT/plugins/show.jl"))

imgc, imgslice = show(img, seg)

#If we are not in a REPL
if (!isinteractive())

    # Create a condition object
    c = Condition()

    # Get the main window (A Tk toplevel object)
    win = toplevel(imgc)

    # Notify the condition object when the window closes
    bind(win, "<Destroy>", e->notify(c))

    # Wait for the notification before proceeding ...
    wait(c)
end


# Trait to determine if an Index, Index collection, Tensor, or ITensor
# has symmetries
abstract type SymmetryStyle end

function symmetrystyle(T)
  return error("No SymmetryStyle defined for the specified object $T of type $(typeof(T))")
end

symmetrystyle(T, S, U, V...)::SymmetryStyle = (
  Base.@_inline_meta; symmetrystyle(symmetrystyle(T), symmetrystyle(S, U, V...))
)

symmetrystyle(T, S)::SymmetryStyle = symmetrystyle(symmetrystyle(T), symmetrystyle(S))

# Rules for basic collections
symmetrystyle(inds::Tuple) = symmetrystyle(inds...)
# `reduce(symmetrystyle, inds)` is not type stable for some reason
symmetrystyle(inds::AbstractVector) = mapreduce(symmetrystyle, symmetrystyle, inds)


using Base: ViewIndex, @propagate_inbounds, AbstractCartesianIndex

"""
Map type, contains an AbstractArray and a WCS object, but behaves like the
AbstractArray it contains for array operations.
It only implements the subset of Base.Array operations which are common on maps.
You should work with the data directly using `enmap_instance.data` if you need
additional Array functions.
"""
struct Enmap{T,N,AA<:AbstractArray,W<:AbstractWCSTransform} <: AbstractArray{T,N}
    data::AA  # some kind of abstract array
    wcs::W    # WCS object
end

function Enmap(data::A, wcs::W) where {A<:AbstractArray,W<:AbstractWCSTransform}
    Enmap{eltype(A),ndims(A),A,W}(data, wcs)
end

struct NoWCS end

Base.parent(x::Enmap) = x.data
getwcs(x::Enmap) = x.wcs
getwcs(x) = NoWCS()

# retrieve nonallocating WCS information. returns tuples of cdelt, crpix, crval
function unsafe_wcs_read_pair(wcs, sym::Symbol)
    field = WCS.getfield(wcs, sym)
    return unsafe_load(field, 1), unsafe_load(field, 2)
end
getcdelt(wcs::WCSTransform) = unsafe_wcs_read_pair(wcs, :cdelt)
getcrpix(wcs::WCSTransform) = unsafe_wcs_read_pair(wcs, :crpix)
getcrval(wcs::WCSTransform) = unsafe_wcs_read_pair(wcs, :crval)

"""
Fast custom WCS structure.
"""
struct CarClenshawCurtis{T} <: AbstractWCSTransform
    cdelt::Tuple{T,T}
    crpix::Tuple{T,T}
    crval::Tuple{T,T}
    unit::T  # conversion factor to radians
end
getunit(T::Type{<:Real}, wcs::CarClenshawCurtis) = T(wcs.unit)
getcdelt(wcs::CarClenshawCurtis) = wcs.cdelt
getcrpix(wcs::CarClenshawCurtis) = wcs.crpix
getcrval(wcs::CarClenshawCurtis) = wcs.crval

Base.copy(w::W) where {W<:CarClenshawCurtis} = w  # CarClenshawCurtis is fully immutable

function Base.convert(::Type{CarClenshawCurtis{T}}, w0::WCSTransform) where T
    return CarClenshawCurtis{T}(
        T.(getcdelt(w0)), T.(getcrpix(w0)), T.(getcrval(w0)), getunit(T, w0))
end

function Base.convert(::Type{WCSTransform}, w0::CarClenshawCurtis{T}) where T
    return WCSTransform(2;
        ctype = ["RA---CAR", "DEC--CAR"],
        cdelt = collect(getcdelt(w0)),
        crpix = collect(getcrpix(w0)),
        crval = collect(getcrval(w0)))
end

# this kind of WCS only has two spatial dimensions. this check should be constant-propagated
function Base.getproperty(wcs::CarClenshawCurtis, k::Symbol)
    if k == :naxis
        return 2
    end
    return getfield(wcs, k)
end

function Base.show(io::IO, wcs::CarClenshawCurtis{T}) where T
    expr = join(["$k=$(getproperty(wcs, Symbol(k)))"
                 for k in ["naxis","cdelt","crval","crpix"]], ",")
    print(io, "CarClenshawCurtis{$(T)}($expr)")
end

# forward all array traits to parent. based on MetaArrays
Base.size(x::Enmap) = size(parent(x))
Base.axes(x::Enmap) = Base.axes(parent(x))
Base.IndexStyle(x::Enmap) = IndexStyle(parent(x))

@propagate_inbounds function Base.view(x::Enmap, idxs::ViewIndex...)
    new_shape, new_wcs = slice_geometry(x, idxs...)
    Enmap(view(parent(x), idxs...), new_wcs)
end
@propagate_inbounds function Base.view(x::Enmap, idxs::Union{ViewIndex,AbstractCartesianIndex}...)
    new_shape, new_wcs = slice_geometry(x, idxs...)
    Enmap(view(parent(x), idxs...), new_wcs)
end
@propagate_inbounds function Base.view(x::Enmap, idxs...)
    new_shape, new_wcs = slice_geometry(x, idxs...)
    Enmap(view(parent(x), idxs...), new_wcs)
end

# totally give up if the RA and DEC slices are eliminated, and just return a view of the parent
@propagate_inbounds Base.view(x::Enmap, ix::Integer, idxs...) = view(parent(x), ix, idxs...)
@propagate_inbounds Base.view(x::Enmap, ix, iy::Integer, idxs...) = view(parent(x), ix, iy, idxs...)

@propagate_inbounds Base.getindex(x::Enmap, i::Int...) = getindex(parent(x), i...)
@propagate_inbounds function Base.getindex(x::Enmap, i...)
    enmapwrap(x, getindex(parent(x), i...), i...)
end
@propagate_inbounds Base.setindex!(x::Enmap, v, i...) = (setindex!(parent(x), v, i...); x)

function Base.similar(x::Enmap, ::Type{S}, dims::NTuple{<:Any,Int}) where {S}
    Enmap(similar(parent(x), S, dims), deepcopy(getwcs(x)))
end

enmapwrap(x::Enmap{T}, val::T, i...) where {T} = val
function enmapwrap(x::Enmap{T,N,AA,W}, val::AbstractArray{T,N}, i...) where {T,N,AA,W}
    new_shape, new_wcs = slice_geometry(x, i...)
    Enmap{T,N,AA,W}(val, new_wcs)
end

# if array slicing ends up changing dimension, follow the parent
function enmapwrap(x::Enmap{T,N,AA,W}, val::AAV, i...) where {T,N,AA,W,NV,AAV<:AbstractArray{T,NV}}
    new_shape, new_wcs = slice_geometry(x, i...)
    Enmap{T,NV,AAV,W}(val, new_wcs)
end

enmapwrap(x::Enmap{T,N,AA,W}, val::AAV, i1::Int, i...) where {T,N,AA,W,NV,AAV<:AbstractArray{T,NV}} = val
enmapwrap(x::Enmap{T,N,AA,W}, val::AAV, i1, i2::Int, i...) where {T,N,AA,W,NV,AAV<:AbstractArray{T,NV}} = val
enmapwrap(x, val, i...) = error("Unexpected result type $(typeof(val)).")

Base.strides(x::Enmap) = strides(parent(x))
Base.stride(x::Enmap, i::Int) = stride(parent(x), i)

# the meta array broadcast style should retain the nested style information for
# whatever array type the meta array wraps
struct EnmapStyle{S} <: Broadcast.BroadcastStyle end
EnmapStyle(s::S) where {S<:Broadcast.BroadcastStyle} = EnmapStyle{S}()
Base.Broadcast.BroadcastStyle(::Type{<:Enmap{T,N,A}}) where {T,N,A} =
    enmapstyle(Broadcast.BroadcastStyle(A))
Base.Broadcast.BroadcastStyle(a::EnmapStyle{A}, b::EnmapStyle{B}) where {A,B} =
    enmapstyle(Broadcast.BroadcastStyle(A(), B()))
function Base.Broadcast.BroadcastStyle(a::EnmapStyle{A}, b::B) where
{A,B<:Broadcast.BroadcastStyle}

    a_ = A()
    left = enmapstyle(Broadcast.BroadcastStyle(a_, b))
    if !(left isa Broadcast.Unknown)
        left
    else
        enmapstyle(Broadcast.BroadcastStyle(b, a_))
    end
end
enmapstyle(x) = EnmapStyle(x)
enmapstyle(x::Broadcast.Unknown) = x

combine(x::NoWCS, y) = copy(y)
combine(x, y::NoWCS) = copy(x)
combine(x::NoWCS, ::NoWCS) = x
combine(x::AbstractWCSTransform, y::AbstractWCSTransform) = copy(x)  # TODO: check compatibility

####
#### broadcasting wrap from MetaArrays
function enmap_broadcasted(bc::Broadcast.Broadcasted{S}, wcss) where {S}
    args = enmap_.(bc.args, wcss)
    Broadcast.Broadcasted{EnmapStyle{S}}(bc.f, args, bc.axes)
end
enmap_broadcasted(result, wcss) = Enmap(result, reduce(combine, wcss))

enmap_(x, ::NoWCS) = x
enmap_(x, wcs) = Enmap(x, wcs)
maybeparent_(x) = x
maybeparent_(x::Enmap) = parent(x)
getwcs_(x) = NoWCS()
getwcs_(x::Enmap) = getwcs(x)

# broadcasted:
function Base.Broadcast.broadcasted(::EnmapStyle{S}, f, xs...) where {S}
    bc = Broadcast.broadcasted(S(), f, maybeparent_.(xs)...)
    enmap_broadcasted(bc, getwcs_.(xs))
end

# instantiate:
# after instantiation, the broadcasted object is flattened and the
# argument contains all meteadata
function Base.Broadcast.instantiate(bc::Broadcast.Broadcasted{M}) where {S,M<:EnmapStyle{S}}

    # simplify
    bc_ = Broadcast.flatten(bc)
    # instantiate the nested broadcast (that the meta array wraps)
    bc_nested = Broadcast.Broadcasted{S}(bc_.f, maybeparent_.(bc_.args))
    inst = Broadcast.instantiate(bc_nested)
    # extract and combine the meta data
    meta = reduce(combine, getwcs_.(bc_.args))
    # place the meta data on the first argument
    args = ((meta, inst.args[1]), Base.tail(inst.args)...)
    # return the instantiated metadata broadcasting
    Broadcast.Broadcasted{M}(bc_.f, args, bc_.axes)
end

# similar: becuase we bypass the default broadcast machinery no call to similar
# is made directly: similar will be called within the nested call to `copy`
# after the metadata has been stripped and the wrapped array is exposed

# copyto!:
function Base.copyto!(dest::AbstractArray, bc::Broadcast.Broadcasted{<:EnmapStyle{S}}) where {S}
    args_ = (bc.args[1][2], Base.tail(bc.args)...)
    bc_ = Broadcast.Broadcasted{S}(bc.f, args_, bc.axes)
    copyto!(dest, bc_)
end

function Base.copyto!(dest::Enmap, bc::Broadcast.Broadcasted{Nothing})
    copyto!(parent(dest), bc)
end

# copy:
function Base.copy(bc::Broadcast.Broadcasted{<:EnmapStyle{S}}) where {S}
    # because the axes have been instantiated, we can safely assume the first
    # argument contains the meta data
    args_ = (bc.args[1][2], Base.tail(bc.args)...)
    bc_ = Broadcast.Broadcasted{S}(bc.f, args_, bc.axes)
    Enmap(copy(bc_), copy(bc.args[1][1]))
end

Base.deepcopy(x::Enmap) = Enmap(deepcopy(parent(x)), deepcopy(getwcs(x)))

# internal function for getting the unit of the angles in the WCS header
# multiply the result of this function with your angle to get radians.
getunit(wcs::AbstractWCSTransform) = getunit(Float64, wcs)
function getunit(T::Type{<:Real}, wcs::WCSTransform)
    cunit1, cunit2 = wcs.cunit
    @assert cunit1 == cunit2 "Units of RA and DEC must be the same."
    cunit = cunit1
    if cunit == "deg"
        return T(π/180)
    elseif cunit == "rad"
        return one(T)
    elseif cunit == "arcmin"
        return T(π/180/60)
    elseif cunit == "arcsec"
        return T(π/180/60/60)
    elseif cunit == "mas"
        return T(π/180/60/60/1000)
    end
    @warn "Can't recognize the WCS unit: $(cunit). Assuming degrees."
    return T(π/180)  # give up and return degrees
end

function Base.show(io::IO, imap::Enmap)
    expr = "Enmap(shape=$(size(imap)),wcs=$(imap.wcs))"
    print(io, expr)
end

# Select the first n axes, should move to WCS.jl at some point
function sub(src::WCS.WCSTransform, n::Int)
    dst = WCSTransform(n)
    nsub = Array{Cint}([n])
    axes = Array{Cint}(collect(1:n))
    ccall((:wcssub, libwcs), Cint,
          (Cint, Ref{WCSTransform}, Ptr{Cint}, Ptr{Cint}, Ref{WCSTransform}),
          0, src, nsub, axes, dst)
    dst
end

function resolve_polcconv!(data::A, header::FITSIO.FITSHeader, sel; verbose=true) where {A<:AbstractArray}
    naxis = header["NAXIS"]
    has_sel = length(sel) == naxis
    for i in 1:naxis
        if getkey(header, "CTYPE$i", "") == "STOKES"
            signs_size = ones(Int, header["NAXIS"])
            signs_size[i] = 3
            verbose && (println("convert to IAU: flip U in axis $i"))
            signs = ones(eltype(A), signs_size...)
            signs[signs_size] .= -1
            if has_sel
                data .*= selectdim(signs, i, sel[i])
            else
                data .*= signs
            end
        end
    end
end

# read fits file into Enmap
function read_map(path::String; hdu::Int=1, sel=(), wcs::Union{WCSTransform,Nothing}=nothing, verbose=true, trim=true)
    f = FITS(path, "r")
    data = read(f[hdu], sel...)
    if isnothing(wcs)
        # parse header from hdu as FITSIO.FITSHeader
        header = read_header(f[hdu])
        # handle IAU <--> COSMOS conversion
        if "STOKES" in header.values
            # default to no flipping (COSMO)
            verbose && !haskey(header, "POLCCONV") && (println("STOKES found but POLCCONV not found, assuming IAU"))
            polcconv = getkey(header, "POLCCONV", "COSMO")
            polcconv == "IAU" && (resolve_polcconv!(data, header, sel; verbose=verbose))
        end
        # WCS.from_header expects each key to be right-padded with len=80
        header_str = join([@sprintf("%-80s", f) for f in split(string(header), "\n")])
        if trim == false
            wcs = sub(WCS.from_header(header_str)[1], 2)
        else
            wcs0 = WCS.from_header(header_str)[1]
            @assert wcs0.ctype[1] == "RA---CAR"
            @assert wcs0.ctype[2] == "DEC--CAR"
            wcs = convert(CarClenshawCurtis{Float64}, wcs0)
        end
    end
    Enmap(data, wcs)
end

function write_map(fname::String, emap::Enmap)
    f = FITS(fname, "w")
    # it is important to write data first. The data will be written with
    # some default headers that describe the shape for us. We then update
    # header based on wcs information to populate the rest of the cards
    write(f, emap.data)
    header = WCS.to_header(Base.convert(WCSTransform, getwcs(emap)))
    cards = [header[1+(i-1)*80:i*80] for i = 1:round(Int, length(header)/80)]
    for c in cards
        FITSIO.CFITSIO.fits_write_record(f.fitsfile, c)
    end
    close(f)
end


module Barmat
export get_Zvec
export cmplx_impedance
include("impedance.jl")
end


import HoTRG_lemon.SpinModule: isClassical
#=
LatticeInfo(latticecode::AbstractString)
getDimension(latticeinfo::LatticeInfo)
getGeometry(latticeinfo::LatticeInfo)
getCoarserate(latticeinfo::LatticeInfo)
getQuantumOrClassical(latticeinfo::LatticeInfo)
isClassical(latticeinfo::LatticeInfo)
getDimension(latticeinfo::LatticeInfo, dimension::Int)
isTwoDimension(latticeinfo::LatticeInfo)
isSquareLattice(latticeinfo::LatticeInfo)
=#

type LatticeInfo
	quantumOrClassical::AbstractString
	dimension::Int
	geometry::AbstractString
	coarserate::Int
	legextension::Int

	function LatticeInfo(latticecode::AbstractString)
		this = new()
		initiateLattice!(latticecode, this)
		return this
	end
end

#---
# auxiliary function for constructor

function initiateLattice!(latticecode::AbstractString, latticeinfo::LatticeInfo)
	lattice = split(latticecode,"_")
	latticeinfo.quantumOrClassical = lowercase(lattice[1])
	latticeinfo.dimension = parse(Int,match(r"(?<dimension>\d+)d",lattice[2])[:dimension])
	latticeinfo.geometry = lowercase(lattice[3])
	if getGeometry(latticeinfo) == "square"
		latticeinfo.coarserate = 2
		latticeinfo.legextension = 0
	elseif getGeometry(latticeinfo) == "fractal"
		latticeinfo.legextension =
			try
				parse(Int,lattice[4])
			catch
				1
			end
	end
	return latticeinfo
end

#---
# functions for latticeinfo
function getDimension(latticeinfo::LatticeInfo)
	return latticeinfo.dimension
end

function getGeometry(latticeinfo::LatticeInfo)
	return latticeinfo.geometry
end

function getLegextension(latticeinfo::LatticeInfo)
	return latticeinfo.legextension
end

function getCoarserate(latticeinfo::LatticeInfo)
	return latticeinfo.coarserate
end

function getQuantumOrClassical(latticeinfo::LatticeInfo)
	return latticeinfo.quantumOrClassical
end

function isClassical(latticeinfo::LatticeInfo)
	if getQuantumOrClassical(latticeinfo) == "classical"
		return true
	else
		return false
	end
end

function getDimension(latticeinfo::LatticeInfo, dimension::Int)
	if getDimension(latticeinfo) == dimension
		return true
	else
		return false
	end
end

function isTwoDimension(latticeinfo::LatticeInfo)
	return getDimension(latticeinfo, 2)
end

function isSquareLattice(latticeinfo::LatticeInfo)
	if getGeometry(latticeinfo) == "square"
		return true
	else
		return false
	end
end

function isFractalLattice(latticeinfo::LatticeInfo)
	if getGeometry(latticeinfo) == "fractal"
		return true
	else
		return false
	end
end
#---
# test function

function testlatticeinfo()
	latticecode = "quantum_2d_square"
	latticeinfo = LatticeInfo(latticecode)
	println(isTwoDimension(latticeinfo))
	println(isSquareLattice(latticeinfo))
	println(getDimension(latticeinfo,4))
end


# m.linconstr needs to be updated to support th new JuMP

@testset "Basic Loading and Basic Dimension Measurement" begin
    m = fetch_model("bcp", "3pk")
    @test length(JuMP.all_variables(m)) == 31
    # @test length(m.linconstr) == 0

    m = fetch_model("global", "abel")
    @test length(JuMP.all_variables(m)) == 31
    # @test length(m.linconstr) == 14

    m = fetch_model("ibm/CLay0203H")
    @test length(JuMP.all_variables(m)) == 91
    # @test length(m.linconstr) == 109

    m = fetch_model("inf", "inf_minlp_203")
    @test length(JuMP.all_variables(m)) == 3
    # @test length(m.linconstr) == 0

    m = fetch_model("minlp/ex1221")
    @test length(JuMP.all_variables(m)) == 6
    # @test length(m.linconstr) == 4

    m = fetch_model("minlp2/meanvarx")
    @test length(JuMP.all_variables(m)) == 36
    # @test length(m.linconstr) == 44

    m = fetch_model("morg", "convex1")
    @test length(JuMP.all_variables(m)) == 47
    # @test length(m.linconstr) == 72

    m = fetch_model("mpec/bard1")
    @test length(JuMP.all_variables(m)) == 10
    # @test length(m.linconstr) == 4

    m = fetch_model("mult3/m_10_3_0_100_1")
    @test length(JuMP.all_variables(m)) == 11
    # @test length(m.linconstr) == 0

    m = fetch_model("mult4", "m_10_4_0_100_1")
    @test length(JuMP.all_variables(m)) == 11
    # @test length(m.linconstr) == 0

    m = fetch_model("minlp2", "blend029")
    @test length(JuMP.all_variables(m)) == 103
    # @test length(m.linconstr) == 202

    m = fetch_model("poly/d2-n30-pos10-v1")
    @test length(JuMP.all_variables(m)) == 31
    # @test length(m.linconstr) == 8

    m = fetch_model("prince", "biggs3")
    @test length(JuMP.all_variables(m)) == 7
    #@test length(m.linconstr) == 0

    m = fetch_model("qcqp", "unitbox_c_8_8_1_25")
    @test length(JuMP.all_variables(m)) == 9
    # @test length(m.linconstr) == 0

    m = fetch_model("qcqp2", "g1_020_001_002_25_1")
    @test length(JuMP.all_variables(m)) == 21
    # @test length(m.linconstr) == 2

    m = fetch_model("qcqp3", "q_20_0_25_1")
    @test length(JuMP.all_variables(m)) == 21
    # @test length(m.linconstr) == 0
end

# @testset "Basic Loading and Basic Dimension Measurement :: Special instances" begin
#     m = fetch_model("special", "multiKND")
#     @test length(JuMP.all_variables(m)) == 5
#     @test length(m.linconstr) == 2

#     m = fetch_model("special", "multiKND", options=Dict(:K=>4, :N=>10))
#     @test length(JuMP.all_variables(m)) == 31
#     @test length(m.linconstr) == 10
# end

@testset "Built-in functions testing" begin

    @test length(MINLPLib.fetch_names("ibm")) == 142

    MINLPLib.add_to_lib("testlib", "minlp2", "blend029")

    @test isdir(joinpath(minlplib_dir,"instances","testlib"))

    m = fetch_model("testlib", "blend029")

    @test length(JuMP.all_variables(m)) == 103
    # @test length(m.linconstr) == 202

    f = MINLPLib.fetch_meta("testlib", "blend029")
    @test !haskey(f, "INTERNALLINK")

    rm(joinpath(minlplib_dir,"instances","testlib"),recursive=true)
    rm(joinpath(minlplib_dir,"meta","testlib"),recursive=true)

end


# =========================================================================
# Function related to terms, variable names and I/O

_diff(t::NTuple{N, T}) where {N, T} = ntuple(i->t[i + 1] - t[i], N - 1)
_diffn(t::NTuple{N, T}) where {N, T} = ntuple(i->t[i] - t[i + 1], N - 1)

"""
    coefnames(<model>, anova::Val{:anova})
    coefnames(<term>, anova::Val{:anova})

Customize coefnames for anova.
"""
StatsBase.coefnames(t::MatrixTerm, anova::Val{:anova}) = mapreduce(coefnames, vcat, t.terms, repeat([anova], length(t.terms)))
StatsBase.coefnames(t::FormulaTerm, ::Val{:anova}) = (coefnames(t.lhs), coefnames(t.rhs))
StatsBase.coefnames(::InterceptTerm{H}, ::Val{:anova}) where H = H ? "(Intercept)" : []
StatsBase.coefnames(t::ContinuousTerm, ::Val{:anova}) = string(t.sym)
StatsBase.coefnames(t::CategoricalTerm, ::Val{:anova}) = string(t.sym)
StatsBase.coefnames(t::FunctionTerm, ::Val{:anova}) = string(t.exorig)
StatsBase.coefnames(ts::StatsModels.TupleTerm, anova::Val{:anova}) = reduce(vcat, coefnames.(ts, anova))
StatsBase.coefnames(t::InteractionTerm, anova::Val{:anova}) = begin
    join(coefnames.(t.terms, anova), " & ")
end

StatsBase.coefnames(aov::AnovaResult) = coefnames(aov.model, Val(:anova))

# Base.show(io::IO, t::FunctionTerm) = print(io, "$(t.exorig)")

"""
    getterms(<term>)

Get an array of `Expr` or `Symbol` of terms.
"""
getterms(::AbstractTerm) = []
getterms(term::Union{Term, CategoricalTerm, ContinuousTerm}) = [term.sym]
getterms(::InterceptTerm{true}) = [Symbol(1)]
getterms(term::InteractionTerm) = mapreduce(getterms, union, term.terms)
getterms(term::FunctionTerm) = [term.exorig]
getterms(term::StatsModels.TupleTerm) = mapreduce(getterms, union, term)
getterms(term::MatrixTerm) = getterms(term.terms)
#=
"""
    getterm(term::AbstractTerm)
    getterm(term::FunctionTerm)

Get the `Symbol` of a single term.
"""=#
# getterm(term::AbstractTerm) = term.sym
# getterm(term::FunctionTerm) = term.exorig
# getterm(term::InterceptTerm) = Symbol(1)

# Determine selected terms for type 2 ss
"""
    isinteract(f::MatrixTerm, id1::Int, id2::Int)

Determine if `f.terms[id2]` is an interaction term of `f.terms[id1]` and other terms.
"""
isinteract(f::MatrixTerm, id1::Int, id2::Int) = issubset(getterms(f.terms[id1]), getterms(f.terms[id2]))

"""
    selectcoef(f::MatrixTerm, id::Int)
    selectcoef(f::MatrixTerm, ::Val{N})
    selectcoef(f::MatrixTerm, ::Val{1})

A set of index of `f.terms` which are interaction terms of `f.terms[id]` and other terms.
"""
selectcoef(f::MatrixTerm, id::Int) = Set([comp for comp in eachindex(f.terms) if isinteract(f, id, comp)])
selectcoef(f::MatrixTerm, ::Val{N}) where N = selectcoef(f, N)
selectcoef(f::MatrixTerm, ::Val{1}) = Set(eachindex(f.terms)) # For InterceptTerm

# Create sub-formula
"""
    subformula(lhs::AbstractTerm, rhs::MatrixTerm, id::Int; reschema::Bool = false)
    subformula(lhs::AbstractTerm, rhs::MatrixTerm, id; reschema::Bool = false)
    subformula(lhs::AbstractTerm, rhs::NTuple{N, AbstractTerm}, id::Int)

Create formula from existing `lhs` and `rhs` truncated to `1:id` or excluded collection `id`.
"""
subformula(lhs::AbstractTerm, rhs::MatrixTerm, id::Int; reschema::Bool = false) =
    id > 0 ? reschema_formula(lhs, rhs.terms[1:id], reschema) : reschema_formula(lhs, (InterceptTerm{false}(),), reschema)
# For nestedmodels & GLM

function subformula(lhs::AbstractTerm, rhs::MatrixTerm, id; reschema::Bool = false)
    terms = rhs.terms[setdiff(eachindex(rhs.terms), id)]
    1 in id && (terms = (InterceptTerm{false}(), terms...))
    reschema_formula(lhs, terms, reschema)
end
# For type 2/3 & GLM

subformula(lhs::AbstractTerm, rhs::NTuple{N, AbstractTerm}, id::Int) where N =
    id > 0 ? FormulaTerm(lhs, (collect_matrix_terms(first(rhs).terms[1:id]), rhs[2:end]...)) : FormulaTerm(lhs, (collect_matrix_terms((InterceptTerm{false}(),)), rhs[2:end]...))
# For nestedmodels & MixedModels

"""
    extract_contrasts(f::FormulaTerm)

Extract a dictionary of contrasts.
"""
extract_contrasts(f::FormulaTerm) =
    Dict{Symbol, Any}(t.sym => t.contrasts.contrasts for t in f.rhs.terms if isa(t, CategoricalTerm))

"""
    clearschema(<terms with schema>) = <terms without schema>

Clear any applied schema on terms.
"""
clearschema(::InterceptTerm{true}) = ConstantTerm(1)
clearschema(::InterceptTerm{false}) = ConstantTerm(0)
clearschema(t::FunctionTerm) = t
clearschema(t::Union{CategoricalTerm, ContinuousTerm}) = Term(t.sym)
clearschema(t::InteractionTerm) = InteractionTerm(clearschema.(t.terms))

# reschema only happen when using TupleTerm rather than MatrixTerm
reschema_formula(lhs::AbstractTerm, ts::StatsModels.TupleTerm, reschema::Bool) =
    reschema ? FormulaTerm(clearschema(lhs), clearschema.(ts)) : FormulaTerm(lhs, collect_matrix_terms(ts))

# test name
tname(::Type{FTest}) = "F test"
tname(::Type{LRT}) = "Likelihood-ratio test"
#tname(M::AnovaStatsRao) = "Rao score test"
#tname(M::AnovaStatsCp) = "Mallow's Cp"

# ============================================================================================================================
# AnovaTable mostly from CoefTable
mutable struct AnovaTable
    cols::Vector
    colnms::Vector
    rownms::Vector
    pvalcol::Int
    teststatcol::Int
    function AnovaTable(cols::Vector, colnms::Vector, rownms::Vector,
                       pvalcol::Int=0, teststatcol::Int = 0)
        nc = length(cols)
        nrs = map(length, cols)
        nr = nrs[1]
        length(colnms) in [0, nc] || throw(ArgumentError("colnms should have length 0 or $nc"))
        length(rownms) in [0, nr] || throw(ArgumentError("rownms should have length 0 or $nr"))
        all(nrs .== nr) || throw(ArgumentError("Elements of cols should have equal lengths, but got $nrs"))
        pvalcol in 0:nc || throw(ArgumentError("pvalcol should be between 0 and $nc"))
        teststatcol in 0:nc || throw(ArgumentError("teststatcol should be between 0 and $nc"))
        new(cols, colnms, rownms, pvalcol, teststatcol)
    end

    function AnovaTable(mat::Matrix, colnms::Vector, rownms::Vector,
                       pvalcol::Int=0, teststatcol::Int=0)
        nc = size(mat, 2)
        cols = Any[mat[:, i] for i in 1:nc]
        AnovaTable(cols, colnms, rownms, pvalcol, teststatcol)
    end
end

"""
Show a p-value using 6 characters, either using the standard 0.XXXX
representation or as <Xe-YY.
"""
struct PValue
    v::Real
    function PValue(v::Real)
        0 <= v <= 1 || isnan(v) || error("p-values must be in [0; 1]")
        new(v)
    end
end

function show(io::IO, pv::PValue)
    v = pv.v
    if isnan(v)
        print(io,"")
    elseif v >= 1e-4
        @printf(io,"%.4f", v)
    else
        @printf(io,"<1e%2.2d", ceil(Integer, max(nextfloat(log10(v)), -99)))
    end
end

"""Show a test statistic using 2 decimal digits"""
struct TestStat <: Real
    v::Real
end

show(io::IO, x::TestStat) = isnan(x.v) ? print(io,"") : @printf(io, "%.4f", x.v)

"""Wrap a string so that show omits quotes"""
struct NoQuote
    s::String
end

show(io::IO, n::NoQuote) = print(io, n.s)

"""Filter NaN"""
struct OtherStat <: Real
    v::Real
end

function show(io::IO, x::OtherStat)
    v = x.v
    if isnan(v)
        print(io, "")
    elseif floor(v) == v
        print(io, Int(v))
    elseif v < 1e-4 && v > 0
        @printf(io,"<1e%2.2d", ceil(Integer, max(nextfloat(log10(v)), -99)))
    elseif abs(v) < 10
        @printf(io,"%.4f", v)
    else
        @printf(io,"%.2f", v)
    end
end

function show(io::IO, at::AnovaTable)
    cols = at.cols; rownms = at.rownms; colnms = at.colnms;
    nc = length(cols)
    nr = length(cols[1])
    if length(rownms) == 0
        rownms = [lpad("[$i]",floor(Integer, log10(nr)) + 3) for i in 1:nr]
    end
    mat = [j == 1 ? NoQuote(rownms[i]) :
           j - 1 == at.pvalcol ? PValue(cols[j - 1][i]) :
           j - 1 in at.teststatcol ? TestStat(cols[j - 1][i]) :
           cols[j-1][i] isa AbstractString ? NoQuote(cols[j - 1][i]) : OtherStat(cols[j - 1][i])
           for i in 1:nr, j in 1:nc + 1]
    # Code inspired by print_matrix in Base
    io = IOContext(io, :compact=>true, :limit=>false)
    A = Base.alignment(io, mat, 1:size(mat, 1), 1:size(mat, 2),
                       typemax(Int), typemax(Int), 3)
    nmswidths = pushfirst!(length.(colnms), 0)
    A = [nmswidths[i] > sum(A[i]) ? (A[i][1] + nmswidths[i] - sum(A[i]), A[i][2]) : A[i]
         for i in 1:length(A)]
    totwidth = sum(sum.(A)) + 2 * (length(A) - 1)
    println(io, repeat('─', totwidth))
    print(io, repeat(' ', sum(A[1])))
    for j in 1:length(colnms)
        print(io, "  ", lpad(colnms[j], sum(A[j + 1])))
    end
    println(io, '\n', repeat('─', totwidth))
    for i in 1:size(mat, 1)
        Base.print_matrix_row(io, mat, A, i, 1:size(mat, 2), "  ")
        i != size(mat, 1) && println(io)
    end
    print(io, '\n', repeat('─', totwidth))
    nothing
end

# ====================================================================================================================================
# AnovaTable api
function anovatable(aov::AnovaResult{<: RegressionModel}; kwargs...)
    at = _anovatable(aov; kwargs...)
    cfnames = coefnames(aov)
    # when first factor is null
    length(at.rownms) == length(cfnames) - 1 && popfirst!(cfnames)
    at.rownms = cfnames
    at
end

# check first and last modeltype
anovatable(aov::AnovaResult{<: Tuple}; kwargs...) =
    _anovatable(aov, typeof(first(aov.model)), typeof(last(aov.model)); kwargs...)

# default anovatable api for comparing multiple models
_anovatable(aov::AnovaResult{<: Tuple}, modeltype1, modeltype2; kwargs...) = _anovatable(aov; kwargs...)

function _anovatable(aov::AnovaResult{<: Tuple, FTest}; kwargs...)
    AnovaTable(hcat(vectorize.((
                    dof(aov),
                    [NaN, _diff(dof(aov))...],
                    dof_residual(aov),
                    deviance(aov),
                    [NaN, _diffn(deviance(aov))...],
                    teststat(aov),
                    pval(aov)
                    ))...),
              ["DOF", "ΔDOF", "Res.DOF", "Deviance", "ΔDeviance", "F value", "Pr(>|F|)"],
              ["$i" for i in eachindex(pval(aov))], 7, 6)
end

function _anovatable(aov::AnovaResult{<: Tuple, LRT}; kwargs...)
    AnovaTable(hcat(vectorize.((
                    dof(aov),
                    [NaN, _diff(dof(aov))...],
                    dof_residual(aov),
                    deviance(aov),
                    teststat(aov),
                    pval(aov)
                    ))...),
              ["DOF", "ΔDOF", "Res.DOF", "Deviance", "χ²", "Pr(>|χ²|)"],
              ["$i" for i in eachindex(pval(aov))], 6, 5)
end

# Show function that delegates to anovatable
function show(io::IO, aov::AnovaResult{<: RegressionModel, T}) where {T <: GoodnessOfFit}
    at = anovatable(aov)
    println(io, "Analysis of Variance")
    println(io)
    println(io, "Type $(aov.type) test / $(tname(T))")
    println(io)
    println(io, formula(aov.model))
    println(io)
    println(io, "Table:")
    show(io, at)
end

function show(io::IO, aov::AnovaResult{<: Tuple, T}) where {T <: GoodnessOfFit}
    at = anovatable(aov)
    println(io,"Analysis of Variance")
    println(io)
    println(io, "Type $(aov.type) test / $(tname(T))")
    println(io)
    for(id, m) in enumerate(aov.model)
        println(io,"Model $id: ", formula(m))
    end
    println(io)
    println(io,"Table:")
    show(io, at)
end


#
# This file is a part of MolecularGraph.jl
# Licensed under the MIT License http://opensource.org/licenses/MIT
#

@testset "graph.plaingraph" begin
    g = plaingraph(5, [(1, 2), (3, 4), (4, 5)])
    @test neighbors(g, 4)[3] == 5
    @test getedge(g, 2) == (3, 4)
    @test !hasedge(g, 2, 3)
    @test_throws ErrorException nodeattr(g, 1)
    @test_throws ErrorException edgeattr(g, 1)
    @test_throws ErrorException edgeattr(g, 3, 4)

    @test issetequal(adjacencies(g, 4), [3, 5])
    @test issetequal(incidences(g, 4), [2, 3])
    @test issetequal(nodeset(g), 1:5)
    @test issetequal(edgeset(g), 1:3)
    @test_throws ErrorException nodeattrs(g)
    @test_throws ErrorException edgeattrs(g)

    @test nodecount(g) == 5
    @test edgecount(g) == 3
    @test neighborcount(g, 2) == 1
    @test degree(g, 3) == 1

    addnode!(g)
    @test degree(g, 6) == 0
    addedge!(g, 4, 6)
    @test degree(g, 4) == 3
    @test_throws ErrorException nodeattrtype(g)
    @test_throws ErrorException edgeattrtype(g)

    emp = plaingraph()
    @test nodecount(emp) == 0

    imm = immutableplaingraph(5, [(1, 2), (3, 4), (4, 5)])
    @test_throws ErrorException addnode!(imm)
    @test_throws ErrorException addedge!(imm, 4, 6)

    # TODO build from attributed graph
    # TODO unlink -> graph.inducedsubgraph
end # graph.plaingraph


using Dates

abstract type AbstractHeader end

struct HeaderV0 <: AbstractHeader
    fileVersion       ::Int64
    dataType1ch       ::String     # 1chのデータ種別
    dataType2ch       ::String     # 2chのデータ種別
    fileHeaderSize    ::Int64      # ファイルヘッダサイズ（コメントのサイズ含む。）
    frameHeaderSize   ::Int64      # フレームヘッダサイズ（コメントのサイズ含む。）
    operatorNameSize  ::Int64
    commentOffsetSize ::Int64
    commentSize       ::Int64
    pixelX            ::Int64
    pixelY            ::Int64
    scanningRangeX    ::Int64
    scanningRangeY    ::Int64
    frameRate         ::Float64
    piezoExtensionZ   ::Float64    # Zピエゾ伸び係数[nm/V]
    piezoGainZ        ::Float64
    adRange           ::Float64    # AD電圧レンジ
    AdResolution      ::Float64    # AD分解能
    isAveraged        ::Bool       # 移動平均化フラグ（tureで移動平均）
    averageWindow     ::Int64      # 1ピクセルに使える最大データ数(最低１)
    day               ::DateTime
    roundingDegree    ::Int64
    maxRangeX         ::Float64
    maxRangeY         ::Float64
    booked1           ::Int64
    booked2           ::Int64
    booked3           ::Int64
    initFrame         ::Int64
    numFrames         ::Int64
    machineId         ::Int64
    fileId            ::Int64
    operatorName      ::Array{Int, 1}
    sensorSensitivity ::Float64       # センサー感度
    phaseSensitivity  ::Float64       # 位相感度
    scannigDirection  ::Int64         # データ取得ステータスコード
    comment           ::Array{Int, 1}
end


module GeometryBasics

using StaticArrays, Tables, StructArrays

using Base: @propagate_inbounds

include("fixed_arrays.jl")
include("basic_types.jl")
include("metadata.jl")
include("viewtypes.jl")

end # module


function assignStructure(df, tmArr)
	nrows = size(df, 1)
    df.element = repeat([""], nrows)
    df.structMap = zeros(nrows)
    df.resNum = 1:nrows

    init = 1
    for region in tmArr
        df.element[collect(region)] .= "tm"
        df.structMap[collect(region)] .= init

        init += 1
    end

    init = 0.5
    for row in eachrow(df)
        if row.structMap != 0
            init = row.structMap + 0.5
        else
            row.structMap = init
            row.element = "loop"
        end
    end

    nt = @view df[df.structMap .== minimum(df.structMap), :]
    ct = @view df[df.structMap .== maximum(df.structMap), :]
    nt[:,:element] .= "nterm"
    ct[:,:element] .= "cterm"

    return df
end


# TODO: Implement q-digest
# http://www.cs.virginia.edu/~son/cs851/papers/ucsb.sensys04.pdf


#=
This file tests the save and load feature of the Mapper.
=#
@testset "Testing Save/Load" begin
    # Create two maps that are the same
    m = place!(Example1.make_map(); move_attempts = 5000)
    sm = Mapper2.SAStruct(m)

    # Place struct sm
    Mapper2.SA.place!(sm; move_attempts = 5000)
    # Record the placed sm into map m
    Mapper2.SA.record(m, sm)
    route!(m)
    # Save the placement
    save(m, "tests")

    ##################################
    # Now create a new testmap.
    ##################################
    n = Example1.make_map()
    # Load the placement into "n"
    load(n, "tests")
    # Create a SA struct for the new map.
    sn = Mapper2.SAStruct(n)
    # Preplace "n" into "sn"
    Mapper2.SA.preplace(n, sn)
    # Assert the two costs are the same.
    @test Mapper2.SA.map_cost(sn) == Mapper2.SA.map_cost(sm)
    # Get the link histograms for the two maps. Make sure they match
    lm = MapperCore.global_link_histogram(m)
    ln = MapperCore.global_link_histogram(n)
    @test lm == ln

    rm("tests.jls")
end


using Dates: Day, Hour

warsaw = first(compile("Europe/Warsaw", tzdata["europe"]))

normal = DateTime(2015, 1, 1, 0)   # a 24 hour day in warsaw
spring = DateTime(2015, 3, 29, 0)  # a 23 hour day in warsaw
fall = DateTime(2015, 10, 25, 0)   # a 25 hour day in warsaw

# Unary plus
@test +ZonedDateTime(normal, warsaw) == ZonedDateTime(normal, warsaw)

# Period arithmetic
@test ZonedDateTime(normal, warsaw) + Day(1) == ZonedDateTime(normal + Day(1), warsaw)
@test ZonedDateTime(spring, warsaw) + Day(1) == ZonedDateTime(spring + Day(1), warsaw)
@test ZonedDateTime(fall, warsaw) + Day(1) == ZonedDateTime(fall + Day(1), warsaw)

@test ZonedDateTime(normal, warsaw) + Hour(24) == ZonedDateTime(normal + Hour(24), warsaw)
@test ZonedDateTime(spring, warsaw) + Hour(24) == ZonedDateTime(spring + Hour(25), warsaw)
@test ZonedDateTime(fall, warsaw) + Hour(24) == ZonedDateTime(fall + Hour(23), warsaw)

# Do the same calculations but backwards over the transitions.
@test ZonedDateTime(normal + Day(1), warsaw) - Day(1) == ZonedDateTime(normal, warsaw)
@test ZonedDateTime(spring + Day(1), warsaw) - Day(1) == ZonedDateTime(spring, warsaw)
@test ZonedDateTime(fall + Day(1), warsaw) - Day(1) == ZonedDateTime(fall, warsaw)

@test ZonedDateTime(normal + Day(1), warsaw) - Hour(24) == ZonedDateTime(normal, warsaw)
@test ZonedDateTime(spring + Day(1), warsaw) - Hour(23) == ZonedDateTime(spring, warsaw)
@test ZonedDateTime(fall + Day(1), warsaw) - Hour(25) == ZonedDateTime(fall, warsaw)

# Ensure that arithmetic around transitions works.
@test ZonedDateTime(spring, warsaw) + Hour(1) == ZonedDateTime(spring + Hour(1), warsaw)
@test ZonedDateTime(spring, warsaw) + Hour(2) == ZonedDateTime(spring + Hour(3), warsaw)
@test ZonedDateTime(fall, warsaw) + Hour(2) == ZonedDateTime(fall + Hour(2), warsaw, 1)
@test ZonedDateTime(fall, warsaw) + Hour(3) == ZonedDateTime(fall + Hour(2), warsaw, 2)

# Non-Associativity
explicit_hour_day = (ZonedDateTime(spring, warsaw) + Hour(24)) + Day(1)
explicit_day_hour = (ZonedDateTime(spring, warsaw) + Day(1)) + Hour(24)
implicit_hour_day = ZonedDateTime(spring, warsaw) + Hour(24) + Day(1)
implicit_day_hour = ZonedDateTime(spring, warsaw) + Day(1) + Hour(24)

@test explicit_hour_day == ZonedDateTime(2015, 3, 31, 1, warsaw)
@test explicit_day_hour == ZonedDateTime(2015, 3, 31, 0, warsaw)
@test implicit_hour_day == ZonedDateTime(2015, 3, 31, 0, warsaw)
@test implicit_day_hour == ZonedDateTime(2015, 3, 31, 0, warsaw)

# CompoundPeriod canonicalization interacting with period arithmetic. Since `spring_zdt` is
# a 23 hour day this means adding `Day(1)` and `Hour(23)` are equivalent.
spring_zdt = ZonedDateTime(spring, warsaw)
@test spring_zdt + Day(1) + Minute(1) == spring_zdt + Hour(23) + Minute(1)

# When canonicalization happens automatically `Hour(24) + Minute(1)` is converted into
# `Day(1) + Minute(1)`. Fixed in `JuliaLang/julia#19268`
@test spring_zdt + Hour(23) + Minute(1) < spring_zdt + Hour(24) + Minute(1)

# Arithmetic with a StepRange should always work even when the start/stop lands on
# ambiguous or non-existent DateTimes.
@testset "StepRange{ZonedDateTime}" begin
    @testset "time-period" begin
        dt = DateTime(2015, 6, 1)

        results = Hour(1) .+ StepRange(
            ZonedDateTime(dt - Hour(1), warsaw),
            Hour(1),
            ZonedDateTime(dt - Hour(0), warsaw),
        )
        expected = StepRange(
            ZonedDateTime(dt + Hour(0), warsaw),
            Hour(1),
            ZonedDateTime(dt + Hour(1), warsaw),
        )
        @test results == expected
        @test length(results) == 2
        @test results isa StepRange{ZonedDateTime}
    end

    @testset "date-period" begin
        dt = DateTime(2015, 6, 1)

        results =  Day(1) .- StepRange(
            ZonedDateTime(dt + Day(1), warsaw),
            Hour(1),
            ZonedDateTime(dt + Day(1) + Hour(1), warsaw),
        )
        expected = StepRange(
            ZonedDateTime(dt, warsaw),
            Hour(1),
            ZonedDateTime(dt + Hour(1), warsaw),
        )
        @test results == expected
        @test length(results) == 2
        @test results isa StepRange{ZonedDateTime}
    end

    @testset "ambiguous" begin
        ambiguous = DateTime(2015, 10, 25, 2)   # Ambiguous hour in Warsaw

        results = Day(1) .+ StepRange(
            ZonedDateTime(ambiguous - Day(1) - Hour(1), warsaw),
            Hour(1),
            ZonedDateTime(ambiguous - Day(1), warsaw),
        )
        expected = StepRange(
            ZonedDateTime(ambiguous - Hour(1), warsaw, 1),
            Hour(1),
            ZonedDateTime(ambiguous, warsaw, 2),
        )
        @test results == expected
        @test length(results) == 3
        @test results isa StepRange

        results = Day(1) .+ StepRange(
            ZonedDateTime(ambiguous - Day(1), warsaw),
            Hour(1),
            ZonedDateTime(ambiguous - Day(1) + Hour(1), warsaw),
        )
        expected = StepRange(
            ZonedDateTime(ambiguous, warsaw, 1),
            Hour(1),
            ZonedDateTime(ambiguous + Hour(1), warsaw),
        )
        @test results == expected
        @test length(results) == 3
        @test results isa StepRange
    end

    @testset "non-existent" begin
        nonexistent = DateTime(2014, 3, 30, 2)  # Non-existent hour in Warsaw

        results = Day(1) .+ StepRange(
            ZonedDateTime(nonexistent - Day(1) - Hour(1), warsaw),
            Hour(1),
            ZonedDateTime(nonexistent - Day(1), warsaw),
        )
        expected = StepRange(
            ZonedDateTime(nonexistent - Hour(1), warsaw),
            Hour(1),
            ZonedDateTime(nonexistent - Hour(1), warsaw),
        )
        @test results == expected
        @test length(results) == 1
        @test results isa StepRange

        results = Day(1) .+ StepRange(
            ZonedDateTime(nonexistent - Day(1), warsaw),
            Hour(1),
            ZonedDateTime(nonexistent - Day(1) + Hour(1), warsaw),
        )
        expected = StepRange(
            ZonedDateTime(nonexistent + Hour(1), warsaw),
            Hour(1),
            ZonedDateTime(nonexistent + Hour(1), warsaw),
        )
        @test results == expected
        @test length(results) == 1
        @test results isa StepRange
    end
end


module PosteriorFactorizationTest

using Test
using ForneyLab

using ForneyLab: Cluster, deterministicEdges, deterministicEdgeSet, isDeterministic

@testset "PosteriorFactorization" begin
    pfz = PosteriorFactorization()
    @test pfz.posterior_factors == Dict{Symbol, PosteriorFactor}()
    @test pfz.edge_to_posterior_factor == Dict{Edge, PosteriorFactor}()
    @test pfz.node_edge_to_cluster == Dict{Tuple{FactorNode, Edge}, Cluster}()
    @test currentPosteriorFactorization() === pfz
end

@testset "isDeterministic" begin
    # Clamp
    fg = FactorGraph()
    @RV x = constant(0.0)
    @test isDeterministic(fg.nodes[:clamp_1].i[:out], Dict{Interface, Bool}())

    # Stochastic
    fg = FactorGraph()
    @RV x ~ GaussianMeanVariance(0.0, 1.0)
    @test !isDeterministic(fg.nodes[:gaussianmeanvariance_1].i[:out], Dict{Interface, Bool}())

    # Equality
    fg = FactorGraph()
    @RV x = equal(constant(0.0), constant(0.0))
    @test isDeterministic(fg.nodes[:equality_1].i[1],
                          Dict{Interface, Bool}(fg.nodes[:clamp_1].i[:out] => true,
                                                fg.nodes[:clamp_2].i[:out] => false))
    @test !isDeterministic(fg.nodes[:equality_1].i[1],
                           Dict{Interface, Bool}(fg.nodes[:clamp_1].i[:out] => false,
                                                 fg.nodes[:clamp_2].i[:out] => false))
    # Deterministic
    fg = FactorGraph()
    @RV x ~ Addition(0.0, 0.0)
    @test isDeterministic(fg.nodes[:addition_1].i[:out],
                          Dict{Interface, Bool}(fg.nodes[:clamp_1].i[:out] => true,
                                                fg.nodes[:clamp_2].i[:out] => true))
    @test !isDeterministic(fg.nodes[:addition_1].i[:out],
                           Dict{Interface, Bool}(fg.nodes[:clamp_1].i[:out] => true,
                                                 fg.nodes[:clamp_2].i[:out] => false))
end

@testset "deterministicEdgeSet and deterministicEdges" begin
    fg = FactorGraph()

    @RV x ~ GaussianMeanVariance(0.0, 1.0)
    @RV y ~ GaussianMeanVariance(0.0, 1.0)
    @RV w = x + y
    @RV a = constant(1.0)
    @RV z = a*w
    placeholder(z, :z)

    e_x = x.edges[1]
    e_y = y.edges[1]
    e_w = w.edges[1]
    e_a = a.edges[1]
    e_z = z.edges[1]
    e_1 = fg.nodes[:clamp_1].i[:out].edge
    e_2 = fg.nodes[:clamp_2].i[:out].edge
    e_3 = fg.nodes[:clamp_3].i[:out].edge
    e_4 = fg.nodes[:clamp_4].i[:out].edge

    @test deterministicEdgeSet(e_x) == Set{Edge}([e_w, e_a, e_z])
    @test deterministicEdges(fg) == Set{Edge}([e_w, e_a, e_z, e_1, e_2, e_3, e_4])
end

@testset "deterministicEdges for graph with dangling edge" begin
    fg = FactorGraph()

    @RV x ~ GaussianMeanVariance(1.0, 1.0)
    @RV y = 4.0 * x

    e_1 = fg.nodes[:clamp_1].i[:out].edge
    e_2 = fg.nodes[:clamp_2].i[:out].edge
    e_3 = fg.nodes[:clamp_3].i[:out].edge

    @test deterministicEdges(fg) == Set{Edge}([e_1, e_2, e_3])
end

end # module


function summary(io::IO, x::fdawarp)
    @printf("-------------------------------------\n")
    @printf("Number of Functions: %d\n", size(x.f,2))
    @printf("Number of time points: %d\n", size(x.f,1))
    @printf("-------------------------------------\n")
    @printf("Alignment with lambda: %f\n", x.lambda)
    @printf("Method: %s\n", x.method)
    @printf("Optimization Method: %s\n", x.omethod)
    @printf("Original Variance: %f\n", x.orig_var)
    @printf("Amplitude Variance: %f\n", x.amp_var)
    @printf("Phase Variance: %f\n", x.phase_var)
    @printf("Number of Iterations: %d\n", length(x.cost))
end