abstract type AbstractAlignedMapping{T <: Tuple, K, V} <: AbstractDict{K, V} end

struct AlignedMapping{T <: Tuple, K, R} <: AbstractAlignedMapping{
    T,
    K,
    Union{
        AbstractArray{<:Number},
        AbstractArray{Union{Missing, T}} where T <: Number,
        AbstractDataFrame,
    },
}
    ref::R # any type as long as it supports size()
    d::Dict{
        K,
        Union{
            AbstractArray{<:Number},
            AbstractArray{Union{Missing, T}} where T <: Number,
            DataFrame,
        },
    }

    function AlignedMapping{T, K}(r, d::AbstractDict{K}) where {T <: Tuple, K}
        for (k, v) in d
            checkdim(T, v, r, k)
        end
        return new{T, K, typeof(r)}(r, d)
    end
end

mutable struct BackedAlignedMapping{T <: Tuple, R} <:
               AbstractAlignedMapping{T, String, AbstractArray{<:Number}}
    ref::R
    d::Union{HDF5.Group, Nothing}
    parent::Union{HDF5.File, HDF5.Group, Nothing}
    path::Union{String, Nothing}

    function BackedAlignedMapping{T}(r, g::HDF5.Group) where {T <: Tuple}
        for k in keys(g)
            checkdim(T, backed_matrix(g[k]), r, k)
        end
        return new{T, typeof(r)}(r, g, nothing, nothing)
    end
    function BackedAlignedMapping{T}(
        r,
        parent::Union{HDF5.File, HDF5.Group},
        path::String,
    ) where {T <: Tuple}
        if haskey(parent, path)
            return BackedAlignedMapping{T}(r, parent[path])
        else
            return new{T, typeof(r)}(r, nothing, parent, path)
        end
    end
end

function checkdim(::Type{T}, v, ref, k) where {T <: Tuple}
    for (vdim, refdim) in T.parameters
        vsize = size(v, vdim)
        rsize = size(ref, refdim)
        if vsize != rsize
            throw(
                DimensionMismatch(
                    "Value passed for key $k had size $vsize for axis $vdim, but should have had size $rsize.",
                ),
            )
        end
    end
end

# @forward from ReusePatterns.jl doesn't work here because it also tries to forward methods from
# packages that we're not using, e.g. Serialization. This results in
# ERROR: LoadError: LoadError: Evaluation into the closed module `Serialization` breaks incremental
# compilation because the side effects will not be permanent. This is likely due to some other
# module mutating `Serialization` with `eval` during precompilation - don't do this.
#
# So we do this manually
Base.IteratorSize(::AbstractAlignedMapping) = Base.IteratorSize(Dict)
Base.IteratorEltype(::AbstractAlignedMapping) = Base.IteratorEltype(Dict)

Base.delete!(d::AlignedMapping, k) = delete!(d.d, k)
Base.empty!(d::AlignedMapping) = empty!(d.d)
Base.getindex(d::AlignedMapping, key) = getindex(d.d, key)
Base.get(d::AlignedMapping, key, default) = get(d.d, key, default)
Base.get(default::Base.Callable, d::AlignedMapping, key) = get(default, d.d, key)
Base.haskey(d::AlignedMapping, key) = haskey(d.d, key)
Base.isempty(d::AlignedMapping) = isempty(d.d)
Base.iterate(d::AlignedMapping) = iterate(d.d)
Base.iterate(d::AlignedMapping, i) = iterate(d.d, i)
Base.length(d::AlignedMapping) = length(d.d)
Base.pop!(d::AlignedMapping) = pop!(d.d)
Base.pop!(d::AlignedMapping, k) = pop!(d.d, k)
Base.pop!(d::AlignedMapping, k, default) = pop!(d.d, k, default)
function Base.setindex!(d::AlignedMapping{T}, v::Union{AbstractArray, DataFrame}, k) where {T}
    checkdim(T, v, d.ref, k)
    d.d[k] = v
end
Base.sizehint!(d::AlignedMapping, n) = sizehint!(d.d, n)

AlignedMapping{T}(r, d::AbstractDict) where {T <: Tuple} = AlignedMapping{T, keytype(d)}(r, d)
AlignedMapping{T, K}(ref) where {T, K} = AlignedMapping{T}(ref, Dict{K, AbstractMatrix{<:Number}}())
AlignedMapping{T, K}(ref, ::Nothing) where {T, K} = AlignedMapping{T, K}(ref)
AlignedMapping{T}(r, d::HDF5.Group) where {T <: Tuple} =
    AligedMapping{T}(ref, read_dict_of_mixed(d))

Base.delete!(d::BackedAlignedMapping, k) = !isnothing(d.d) && delete_object(d.d, k)
function Base.empty!(d::BackedAlignedMapping)
    if !isnothing(d.d)
        for k in keys(d.d)
            delete_object(d.d, k)
        end
    end
end
Base.getindex(d::BackedAlignedMapping, key) =
    isnothing(d.d) ? throw(KeyError(key)) : backed_matrix(d.d[key])
Base.get(d::BackedAlignedMapping, key, default) =
    isnothing(d.d) || !haskey(d.d, key) ? default : backed_matrix(d.d[key])
Base.get(default::Base.Callable, d::BackedAlignedMapping, key) =
    isnothing(d.d) || !haskey(d.d, key) ? default() : backed_matrix(d.d[key])
Base.haskey(d::BackedAlignedMapping, key) = isnothing(d.d) ? false : haskey(d.d, key)
Base.isempty(d::BackedAlignedMapping) = isnothing(d.d) ? true : isempty(d.d)
function Base.iterate(d::BackedAlignedMapping)
    if isnothing(d.d)
        return nothing
    else
        next = iterate(d.d)
        return isnothing(next) ? next :
               (hdf5_object_name(next[1]) => backed_matrix(next[1]), next[2])
    end
end
function Base.iterate(d::BackedAlignedMapping, i)
    if isnothing(d.d)
        return nothing
    else
        next = iterate(d.d, i)
        return isnothing(next) ? next :
               (hdf5_object_name(next[1]) => backed_matrix(next[1]), next[2])
    end
end
Base.length(d::BackedAlignedMapping) = isnothing(d.d) ? 0 : length(d.d)
function Base.pop!(d::BackedAlignedMapping)
    if isnothing(d.d)
        throw(ArgumentError("dict must be non-empty"))
    else
        obj = iterate(d.d)[1]
        mat = read_matrix(obj)
        delete_object(obj)
        return mat
    end
end
function Base.pop!(d::BackedAlignedMapping, k)
    if isnothing(d.d)
        throw(ArgumentError("dict must be non-empty"))
    else
        obj = d.d[k]
        mat = read_matrix(obj)
        delete_object(obj)
        return mat
    end
end
function Base.pop!(d::BackedAlignedMapping, k, default)
    try
        return pop!(d, k)
    catch e
        if e isa KeyError
            return default
        else
            rethrow()
        end
    end
end
function Base.setindex!(d::BackedAlignedMapping{T}, v::AbstractArray, k) where {T}
    checkdim(T, v, d.ref, k)
    if isnothing(d.d)
        d.d = create_group(d.parent, d.path)
    end
    write_attr(d.d, k, v)
end

function copy_subset(
    src::AbstractAlignedMapping{T},
    dst::AbstractAlignedMapping,
    I,
    J,
) where {T <: Tuple}
    idx = (
        if refdim == 1
            I
        elseif refdim == 2
            J
        else
            (:)
        end for (vdim, refdim) in T.parameters
    )
    for (k, v) in src
        dst[k] = v[idx..., ((:) for i in 1:(ndims(v) - length(idx)))...]
    end
end

struct AlignedMappingView{T <: Tuple, K, V, P <: AbstractAlignedMapping{T, K, V}} <:
       AbstractAlignedMapping{T, K, V}
    parent::P
    indices::Tuple
end

function aligned_view(d::AlignedMappingView{T}, A) where {T <: Tuple}
    idx = Vector{Union{Colon, typeof(d.indices).parameters...}}(undef, ndims(A))
    idx .= (:)
    for ((vdim, refdim), cidx) in zip(T.parameters, d.indices)
        idx[vdim] = cidx
    end
    return @inbounds view(A, idx...)
end

Base.delete!(d::AlignedMappingView, k) = delete!(d.parent, k)
Base.empty!(d::AlignedMappingView) = empty!(d.parent)
Base.getindex(d::AlignedMappingView, key) = aligned_view(d, getindex(d.parent, key))
Base.get(d::AlignedMappingView, key, default) =
    haskey(d.parent, key) ? aligned_view(d, get(d.parent, key)) : default
Base.get(default::Base.Callable, d::AlignedMappingView, key) =
    haskey(d.parent, key) ? aligned_view(d, get(d.parent, key)) : default()
Base.haskey(d::AlignedMappingView, key) = haskey(d.parent, key)
Base.isempty(d::AlignedMappingView) = isempty(d.parent)
function Base.iterate(d::AlignedMappingView)
    res = iterate(d.parent)
    if !isnothing(res)
        (k, v), state = res
        return (k, aligned_view(d, v)), state
    else
        return res
    end
end
function Base.iterate(d::AlignedMappingView, i)
    res = iterate(d.parent, i)
    if !isnothing(res)
        (k, v), state = res
        return (k, aligned_view(d, v)), state
    else
        return res
    end
end
Base.length(d::AlignedMappingView) = length(d.parent)
Base.pop!(d::AlignedMappingView) = aligned_view(d, pop!(d.parent))
Base.pop!(d::AlignedMappingView, k) = aligned_view(d, pop!(d.parent, k))
Base.pop!(d::AlignedMappingView, k, default) =
    haskey(d.parent, k) ? aligned_view(d, pop!(d.d, k)) : default
Base.parent(d::AlignedMappingView) = d.parent
Base.parentindices(d::AlignedMappingView) = d.indices

Base.setindex!(d::AlignedMappingView, v::AbstractArray, k) = throw(ArgumentError("Replacing or adding elements of an AlignedMappingView is not supported."))

function Base.view(parent::AbstractAlignedMapping{T}, indices...) where {T <: Tuple}
    @boundscheck if length(T.parameters) != length(indices)
        throw(
            DimensionMismatch(
                "Attempt to index into AlignedMapping with $(length(T.parameters)) aligned dimensions using index of length $(length(indices))",
            ),
        )
    end
    return AlignedMappingView(parent, indices)
end

function Base.view(parentview::AlignedMappingView{T}, indices...) where T <: Tuple
    @boundscheck if length(T.parameters) != length(indices)
        throw(
            DimensionMismatch(
                "Attempt to index into AlignedMappingView with $(length(T.parameters)) aligned dimensions using index of length $(length(indices))",
            ),
        )
    end
    return AlignedMappingView(parent(parentview), Base.reindex(parentindices(parentview), indices))
end

const StrAlignedMapping{T <: Tuple, R} = AlignedMapping{T, String, R}
const StrAlignedMappingView{T <: Tuple} = AlignedMappingView{T, String}


VERSION < v"0.7.0-beta2.199" && __precompile__()

module AbstractFFTs

using Compat

# After this version, the bindings can overwrite deprecated bindings in Base safely, but
# prior to it we want to extend/reexport the Base definitions
if VERSION < v"0.7.0-DEV.602"
    import Base: fft, ifft, bfft, fft!, ifft!, bfft!,
                 plan_fft, plan_ifft, plan_bfft, plan_fft!, plan_ifft!, plan_bfft!,
                 rfft, irfft, brfft, plan_rfft, plan_irfft, plan_brfft,
                 fftshift, ifftshift
    import Base.DFT: Plan, ScaledPlan, plan_inv, pinv_type, normalization,
                     rfft_output_size, brfft_output_size, realfloat, complexfloat
end
# Reexport the Base bindings unchanged for versions before FFTW was removed, or export the
# new definitions after overwritable deprecation bindings were introduced
export fft, ifft, bfft, fft!, ifft!, bfft!,
       plan_fft, plan_ifft, plan_bfft, plan_fft!, plan_ifft!, plan_bfft!,
       rfft, irfft, brfft, plan_rfft, plan_irfft, plan_brfft,
       fftshift, ifftshift

# Only define things if we aren't using the existing Base bindings
VERSION >= v"0.7.0-DEV.602" && include("definitions.jl")

end # module


module FiveThreeOne

include("TextTable.jl")
import .TextTable
using .TextTable
export TextTable

using Base: Float64
using Printf:@printf

import YAML

export print_main_lift_table, MainLiftSet

struct Order531 end
struct Order351 end
struct Week1 end
struct Week2 end
struct Week3 end

struct Reps
    number::Union{Int, UnitRange{Int}, AbstractString}
    is_pr_set::Bool
end

Reps(number) = Reps(number, false)

function Base.show(io::IO, reps::Reps)
    if reps.number isa UnitRange{Int}
        print(io, first(reps.number), "-", last(reps.number))
    else
        print(io, reps.number)
    end
    reps.is_pr_set && print(io, "+")
end

struct MainLift
    percentage::Float64
    weight::Float64
    sets::Int
    reps::Reps
    e1rm_to_beat::Union{Nothing, Float64}
end

MainLift(percentage, weight, sets, reps::Int) = MainLift(percentage, weight, sets, Reps(reps), nothing)
MainLift(percentage, weight, sets, reps::UnitRange{Int}) = MainLift(percentage, weight, sets, Reps(reps), nothing)
MainLift(percentage, weight, sets, reps::Reps) = MainLift(percentage, weight, sets, reps, nothing)
MainLift(percentage, weight, sets, reps::Int, e1rm_to_beat) = MainLift(percentage, weight, sets, Reps(reps), e1rm_to_beat)
MainLift(percentage, weight, sets, reps::UnitRange{Int}, e1rm_to_beat) = MainLift(percentage, weight, sets, Reps(reps), e1rm_to_beat)

struct AssistanceLift
    name::AbstractString
    weight::Union{Float64, Int, String}
    sets::Int
    reps::Reps
end

AssistanceLift(percentage, weight, sets, reps::Union{Int, UnitRange{Int}, AbstractString}) = AssistanceLift(percentage, weight, sets, Reps(reps, false))

function routine_from_file(; days, main_lifts, maxes_file, assistance_file, order, print_config=nothing)
    function _routine_for_week(week)
        active_print_config = Dict(
            :number_columns => 4,
        )
        if print_config !== nothing
            for k in keys(active_print_config)
                if haskey(print_config, k)
                    active_print_config[k] = print_config[k]
                end
            end
        end
        maxes_data = YAML.load_file(maxes_file)
        assistance_data = YAML.load_file(assistance_file)

        main_sets = Vector{FiveThreeOne.MainLift}[]
        assistance_sets = Vector{FiveThreeOne.AssistanceLift}[]
        for day in days
            day_mains = Vector{MainLift}[]
            push!(day_mains, warmup_sets(maxes_data["training"][day]))
            for lift in main_lifts
                push!(day_mains, lift(maxes_data, day, week, order))
            end
            day_mains = vcat(day_mains...)
            @show day_mains
            push!(main_sets, day_mains)
            day_assistance = AssistanceLift[]
            for lift in assistance_data[day]
                push!(day_assistance, AssistanceLift(lift...))
            end
            push!(assistance_sets, day_assistance)
        end
        print_routine(
            days,
            main_sets,
            assistance_sets,
            n_columns=active_print_config[:number_columns])
    end
    return _routine_for_week
end

function make_routine(training_maxes, supplemental, order, assistance, e1rm_for_pr=nothing; print_config=nothing)
    active_print_config = Dict(
        :number_columns => 4,
    )
    if print_config !== nothing
        for k in keys(active_print_config)
            if haskey(print_config, k)
                active_print_config[k] = print_config[k]
            end
        end
    end
    names = Vector{AbstractString}
    lifts = Vector{MainLift}
    assistance_lifts = Vector{AssistanceLift}

    if e1rm_for_pr === nothing
        e1rm_for_pr = repeat([nothing], length(training_maxes))
        has_pr_sets = false
    else
        has_pr_sets = true
    end

    function _print_routine(week; fives_progression=false)
        names = [n for (n, _) in training_maxes]
        if fives_progression
            _main = ((tm, week) -> main_lifts_5pro(tm, week, order))
        else
            _main = ((tm, week) -> main_lifts(tm, week, order, has_pr_sets, e1rm_to_beat))
        end
        main_sets = [
            vcat(warmup_sets(tm),
                 _main(tm, week),
                 supplemental(tm, week, order))
            for ((_, tm), lift_e1rm) in zip(training_maxes, e1rm_for_pr)
        ]
        assistance_sets = [
            [AssistanceLift(each_lift...) for each_lift in daily_assistance]
            for daily_assistance in assistance
        ]
        print_routine(
            names,
            main_sets,
            assistance_sets,
            n_columns=active_print_config[:number_columns])
    end

    return _print_routine
end

function make_main_lift_cell(name, lifts::Vector{MainLift})
    header_cell = TextCell([name])
    percentages = TextCell(vcat(
        ["%"],
        [string(Int(round(lift.percentage))) for lift in lifts]),
        horizontal_alignment=align_right,
        )
    set_width!(percentages, 4)
    weights = TextCell(vcat(
        ["Weight"],
        [string(lift.weight) for lift in lifts]),
        horizontal_alignment=align_right,
        )
    sets = TextCell(vcat(
        ["Sets"],
        [string(lift.sets) for lift in lifts]),
        horizontal_alignment=align_right,
        )
    reps = TextCell(vcat(
        ["Reps"],
        [string(lift.reps) for lift in lifts]),
        horizontal_alignment=align_right,
        )
    lift_details = hmerge([percentages, weights, sets, reps]; sep="  ")

    # append pr details if there's a pr set
    pr_content = AbstractString[]
    for lift in lifts
        if lift.reps.is_pr_set
            pr_goal = pr_set_goal_reps(lift.weight, lift.e1rm_to_beat)
            push!(pr_content, "$(pr_goal.reps) reps for E1RM $(pr_goal.new_e1rm)")
        end
    end
    if length(pr_content) > 0
        pr_cell = TextCell(pr_content)
        lift_details = vmerge(lift_details, pr_cell)
    end

    # add header to complete
    return vmerge(header_cell, lift_details; sep="-")
end

function make_assistance_cell(lifts::Vector{AssistanceLift})
    header_cell = TextCell(["Assistance"])
    weights = TextCell(vcat(
        ["Weight"],
        [string(lift.weight) for lift in lifts]),
        horizontal_alignment=align_right,
        )
    sets = TextCell(vcat(
        ["Sets"],
        [string(lift.sets) for lift in lifts]),
        horizontal_alignment=align_right,
        )
    reps = TextCell(vcat(
        ["Reps"],
        [string(lift.reps) for lift in lifts]),
        horizontal_alignment=align_right,
        )
    lift_details = hmerge([weights, sets, reps]; horizontal_alignment=align_right, sep="  ")

    names = TextCell([lift.name for lift in lifts])
    assistance = interleave(lift_details, names; horizontal_alignment=align_right)
    return vmerge(header_cell, assistance; sep="-")
end

function print_routine(
    names::Vector{<:AbstractString},
    main_lifts::Vector{Vector{MainLift}},
    assistance::Vector{Vector{AssistanceLift}};
    n_columns::Int=length(names),
    row_sep=" ",
    )
    main_lift_cells = [make_main_lift_cell(name, lifts) for (name, lifts) in zip(names, main_lifts)]
    assistance_cells = [make_assistance_cell(lifts) for lifts in assistance]
    daily_cells = [vmerge(main, assistance; sep="=") for (main, assistance) in zip(main_lift_cells, assistance_cells)]
    n_rows = Int(ceil(length(names) / n_columns))
    row_cells = Vector{TextCell}(undef, n_rows)
    groups = [(1+(i-1)*n_columns):i*n_columns for i in 1:n_rows]
    for i in 1:n_rows
        j1 = first(groups[i])
        j2 = min(last(groups[i]), length(daily_cells))
        row_cells[i] = hmerge(daily_cells[j1:j2], sep=" | ")
    end
    if length(row_cells) > 1
        routine = vmerge(row_cells; sep=row_sep)
    else
        routine = row_cells[1]
    end
    println(format_text(routine))
end

make_single_sets(percentages, weights, reps) = collect(map(MainLift, percentages, weights, repeat([1], length(percentages)), reps))
function make_single_sets(percentages, weights, reps, e1rm_to_beat)
    collect(map(MainLift, percentages, weights, repeat([1], length(percentages)), reps, repeat([e1rm_to_beat], length(percentages))))
end
round_to_half_pound(x) = round(x * 2, RoundNearest) / 2
make_weights(percentages, training_max) = round_to_half_pound.(percentages / 100 .* training_max)

function warmup_sets(training_max)
    percentages = [40, 50, 60]
    reps = [5,5,3]
    weights = make_weights(percentages, training_max)
    return make_single_sets(percentages, weights, reps)
end

function main_lifts(training_max, week, order, pr_sets, e1rm_to_beat)
    make_pr_reps(reps) = Reps(reps, pr_sets)

    if order === Order351 && week === Week1
        week = Week2
    elseif order === Order351 && week === Week2
        week = Week1
    end
    if week === Week1
        percentages = [65, 75, 85]
        reps = [5,5, make_pr_reps(5)]
    elseif week === Week2
        percentages = [70, 80, 90]
        reps = [3,3, make_pr_reps(3)]
    elseif week === Week3
        percentages = [75, 85, 95]
        reps = [5,3, make_pr_reps(1)]
    else
        throw(DomainError("Argument `week` must be Week1, Week2, or Week3."))
    end
    weights = make_weights(percentages, training_max)
    return make_single_sets(percentages, weights, reps, e1rm_to_beat)
end

function main_lifts_5pro(training_max, week, order)
    if order === Order351 && week === Week1
        week = Week2
    elseif order === Order351 && week === Week2
        week = Week1
    end
    if week === Week1
        percentages = [65, 75, 85]
        reps = [5,5, 5]
    elseif week === Week2
        percentages = [70, 80, 90]
        reps = [5,5, 5]
    elseif week === Week3
        percentages = [75, 85, 95]
        reps = [5,5, 5]
    else
        throw(DomainError("Argument `week` must be Week1, Week2, or Week3."))
    end
    weights = make_weights(percentages, training_max)
    return make_single_sets(percentages, weights, reps)
end

function deload_lifts(training_max)
    percentages = [70, 80, 90, 100]
    reps = [5, 3, 1, 1]
    weights = make_weights(percentages, training_max)
    return make_single_sets(percentages, weights, reps)
end

function training_max_test_lifts(training_max)
    percentages = [70, 80, 90, 100]
    reps = [5, 5, 5, 3:5]
    weights = make_weights(percentages, training_max)
    return make_single_sets(percentages, weights, reps)
end

function widowmaker(training_max, week, order)
    if order === Order351 && week === Week1
        week = Week2
    elseif order === Order351 && week === Week2
        week = Week1
    end
    if week === Week1
        percentages = [65]
        reps = [15:20]
    elseif week === Week2
        percentages = [70]
        reps = [15:20]
    elseif week === Week3
        percentages = [75]
        reps = [15:20]
    else
        throw(DomainError("Argument `week` must be Week1, Week2, or Week3."))
    end
    weights = make_weights(percentages, training_max)
    return make_single_sets(percentages, weights, reps)
end

function boringbutbig_light(training_max, week, order)
    if order === Order351 && week === Week1
        week = Week2
    elseif order === Order351 && week === Week2
        week = Week1
    end
    if week === Week1
        percentages = 40
    elseif week === Week2
        percentages = 50
    elseif week === Week3
        percentages = 60
    else
        throw(DomainError("Argument `week` must be Week1, Week2, or Week3."))
    end
    weights = make_weights(percentages, training_max)
    return [MainLift(percentages, weights, 5, 10)]
end

e1rm(weight, reps::Int) = weight * reps * 0.0333 + weight
e1rm(weight, reps::UnitRange{Int}) = collect(map(x -> e1rm(weight, x), reps))
e1rm(weight, reps::Reps) = e1rm(weight, reps.number)

struct PRGoal
    reps::Reps
    new_e1rm::Float64
end

function pr_set_goal_reps(pr_set_weight, e1rm_to_beat)
    rep_range = 1:50
    e1rm_above_goal = e1rm(pr_set_weight, rep_range) .>= e1rm_to_beat
    goal_reps = collect(rep_range)[e1rm_above_goal][1]
    return PRGoal(Reps(goal_reps), e1rm(pr_set_weight, goal_reps))
end

end


module HugepageMmap

using Mmap

export hugepage_mmap,
       PageSize4K,
       PageSize2M,
       PageSize1G

const MAP_HUGETLB    = Cint(0x40000)
const MAP_HUGE_SHIFT = Cint(26)
const MAP_HUGE_2MB   = Cint(21 << MAP_HUGE_SHIFT)
const MAP_HUGE_1GB   = Cint(30 << MAP_HUGE_SHIFT)

abstract type AbstractPageSize end
struct PageSize4K <: AbstractPageSize end
struct PageSize2M <: AbstractPageSize end
struct PageSize1G <: AbstractPageSize end

extraflags(::PageSize4K) = Cint(0)
extraflags(::PageSize2M) = MAP_HUGETLB | MAP_HUGE_2MB
extraflags(::PageSize1G) = MAP_HUGETLB | MAP_HUGE_1GB

# Align length for `munmap` to a multiple of page size.
pagesize(::PageSize4K) = 4096
pagesize(::PageSize2M) = 2097152
pagesize(::PageSize1G) = 1073741824

align(x, p::AbstractPageSize) = ceil(Int, x / pagesize(p)) * pagesize(p)
align(x, ::PageSize4K) = x

# This is heavily based on the Mmap stdlib
function hugepage_mmap(::Type{T}, pagesize::AbstractPageSize, dims::Integer...) where {T}
    return hugepage_mmap(T, pagesize, convert.(Int, dims))
end

"""
    hugepage_mmap(::Type{T}, pagesize::AbstractPageSize, dims...)

Allocate a `Array{T}` with dimensions `dims` backed by `pagesize`.
Choices for `pagesize` are `PageSize4k()`, `PageSize2m()`, or `PageSize1G()`.
Make sure the corresponding hugepages are already allocated on your system.
"""
function hugepage_mmap(::Type{T}, pagesize::AbstractPageSize, dims::NTuple{N,Int}) where {N,T}
    mmaplen = sizeof(T) * prod(dims)

    # Build the PROT flags - we want to be able to read and write.
    prot = Mmap.PROT_READ | Mmap.PROT_WRITE
    flags = Mmap.MAP_PRIVATE | Mmap.MAP_ANONYMOUS
    flags |= extraflags(pagesize)

    fd = Base.INVALID_OS_HANDLE
    offset = Cint(0)

    # Fordward this call into the Julia C library.
    ptr = ccall(
        :jl_mmap,
        Ptr{Cvoid},
        (Ptr{Cvoid}, Csize_t, Cint, Cint, RawFD, Int64),
        C_NULL,     # No address we really want.
        mmaplen,
        prot,
        flags,
        fd,
        offset,
    )

    # Wrap this into an Array and attach a finalizer that will unmap the underlying pointer
    # when the Array if GC'd
    A = Base.unsafe_wrap(Array, Ptr{T}(ptr), dims)
    finalizer(A) do x
        systemerror(
            "munmap",
            ccall(:munmap, Cint, (Ptr{Cvoid}, Csize_t), ptr, align(mmaplen, pagesize)) != 0
        )
    end
    return A
end

end # module


## ExoplanetsSysSim/examples/syssim_dist.jl
## (c) 2015 Eric B. Ford

# Calculaties the distance between simulation computed from an input "parameter file" and the summary statistics already stored in comp_file
# usage:  syssim_dist.jl param_file comp_file
# output: scalar_dist d_1 d_2 ...
#         where scalar_dist is the scalar distance between the simulated model's summary statistics and the precomputed summary statistics,
#         and d_1 d_2 ... represent a list of the individual distance metrics between the two sets of summary statistics

verbose = true

if length(ARGS)<2
   warn("usage:  syssim_dist.jl param_file comp_file")
   exit(255)
end

using Compat
import Compat: UTF8String, ASCIIString

param_file = convert(Compat.ASCIIString,ARGS[1])
if !isfile(param_file)
   warn(string("Can't read ",param_file))
   exit(255)
end

comp_file = convert(Compat.ASCIIString,ARGS[2])
if !isfile(comp_file)
   warn(string("Can't read ",comp_file))
   exit(255)
end

if verbose println("# Loading ExoplanetsSysSim module."); end
using ExoplanetsSysSim

if verbose println("# Reading parameter file ", param_file,"."); end
include(param_file)
if verbose println("# Calling setup_sim_param( ", ARGS[3:end], " )."); end
sim_param = setup_sim_param( convert(Array{Compat.ASCIIString,1},ARGS[3:end]) )
if verbose println("# Active parameter values: ", make_vector_of_sim_param(sim_param) ); end

#add_param_fixed(sim_param,"rng_seed",1234)   # If you want to be able to reproduce simulations
if haskey(sim_param,"rng_seed")
   seed = get_int(sim_param,"rng_seed")
     if verbose println("# Seeding RNG with seed = ", seed); end
   srand(seed)
end

if verbose println("# Loading summary statistics from ", comp_file,"."); end
ss_comp = load_summary_stats(comp_file)

num_repeats = 1
for k in 1:num_repeats
if num_repeats > 1  print("\n\n"); end
if verbose println("# Generating physical catalog."); end
@time cat_phys = generate_kepler_physical_catalog(sim_param)
@time cat_phys = generate_kepler_physical_catalog(sim_param)

if verbose println("# Generating observational catalog."); end
@time cat_obs = observe_kepler_targets_single_obs(cat_phys,sim_param)
@time cat_obs = observe_kepler_targets_single_obs(cat_phys,sim_param)

ss = CatalogSummaryStatistics()
if haskey(sim_param,"use obs data only") &&  convert(Bool,get(sim_param,"use obs data only"))
  if verbose println("# Calculating summary statistics based on all data."); end
  @time ss = calc_summary_stats_obs_demo(cat_obs,sim_param)
else
  if verbose println("# Calculating summary statistics based on observational data only."); end
  @time ss = calc_summary_stats_sim_pass_one_demo(cat_obs,cat_phys,sim_param)
  @time ss = calc_summary_stats_sim_pass_two_demo(cat_obs,cat_phys,ss,sim_param)
end

if verbose println("# Calculating distance (pass 1)"); end
@time d1 = calc_distance_vector_demo(ss_comp, ss, 1, sim_param)
if verbose println("# Calculating distance (pass 2)"); end
@time d2 = calc_distance_vector_demo(ss_comp, ss, 2, sim_param)

d = [d1; d2]
@time dist = calc_scalar_distance(d)
print(dist, "   ")
print(STDOUT,join([d1;d2],' '))
print("\n")
end


type Light
	position::Vec3f
	color::Vec4f
	brightness::Vec4f

	Light(position=Vec3f(),color=Vec4f(),brightness=ones(Vec4f)) = new(position,color,brightness)
end

getLightData(light::Light) = [
	light.position.x,light.position.y,light.position.z,
	light.color.x,light.color.y,light.color.z,light.color.a,
	light.brightness.x,light.brightness.y,light.brightness.z,light.brightness.a
	]


#=
This file is auto-generated. Do not edit.
=#
"""
    mutable struct AVRSimple <: AVR
        Kv::Float64
        V_ref::Float64
        ext::Dict{String, Any}
        states::Vector{Symbol}
        n_states::Int64
        internal::InfrastructureSystemsInternal
    end

Parameters of a simple proportional AVR in the derivative of EMF
i.e. an integrator controller on EMF

# Arguments
- `Kv::Float64`: Proportional Gain, validation range: (0, nothing)
- `V_ref::Float64`: Reference Voltage Set-point, validation range: (0, nothing)
- `ext::Dict{String, Any}`
- `states::Vector{Symbol}`
- `n_states::Int64`: Fixed AVR has 1 states
- `internal::InfrastructureSystemsInternal`: power system internal reference, do not modify
"""
mutable struct AVRSimple <: AVR
    "Proportional Gain"
    Kv::Float64
    "Reference Voltage Set-point"
    V_ref::Float64
    ext::Dict{String, Any}
    states::Vector{Symbol}
    "Fixed AVR has 1 states"
    n_states::Int64
    "power system internal reference, do not modify"
    internal::InfrastructureSystemsInternal
end

function AVRSimple(Kv, V_ref=1.0, ext=Dict{String, Any}(), )
    AVRSimple(Kv, V_ref, ext, [:Vf], 1, InfrastructureSystemsInternal(), )
end

function AVRSimple(; Kv, V_ref=1.0, ext=Dict{String, Any}(), )
    AVRSimple(Kv, V_ref, ext, )
end

# Constructor for demo purposes; non-functional.
function AVRSimple(::Nothing)
    AVRSimple(;
        Kv=0,
        V_ref=0,
        ext=Dict{String, Any}(),
    )
end

"""Get AVRSimple Kv."""
get_Kv(value::AVRSimple) = value.Kv
"""Get AVRSimple V_ref."""
get_V_ref(value::AVRSimple) = value.V_ref
"""Get AVRSimple ext."""
get_ext(value::AVRSimple) = value.ext
"""Get AVRSimple states."""
get_states(value::AVRSimple) = value.states
"""Get AVRSimple n_states."""
get_n_states(value::AVRSimple) = value.n_states
"""Get AVRSimple internal."""
get_internal(value::AVRSimple) = value.internal

"""Set AVRSimple Kv."""
set_Kv!(value::AVRSimple, val::Float64) = value.Kv = val
"""Set AVRSimple V_ref."""
set_V_ref!(value::AVRSimple, val::Float64) = value.V_ref = val
"""Set AVRSimple ext."""
set_ext!(value::AVRSimple, val::Dict{String, Any}) = value.ext = val
"""Set AVRSimple states."""
set_states!(value::AVRSimple, val::Vector{Symbol}) = value.states = val
"""Set AVRSimple n_states."""
set_n_states!(value::AVRSimple, val::Int64) = value.n_states = val
"""Set AVRSimple internal."""
set_internal!(value::AVRSimple, val::InfrastructureSystemsInternal) = value.internal = val


export BrickletGPSCoordinates
struct BrickletGPSCoordinates
    latitude::Integer
    ns::Char
    longitude::Integer
    ew::Char
    pdop::Integer
    hdop::Integer
    vdop::Integer
    epe::Integer
end

export BrickletGPSStatus
struct BrickletGPSStatus
    fix::Integer
    satellites_view::Integer
    satellites_used::Integer
end

export BrickletGPSAltitude
struct BrickletGPSAltitude
    altitude::Integer
    geoidal_separation::Integer
end

export BrickletGPSMotion
struct BrickletGPSMotion
    course::Integer
    speed::Integer
end

export BrickletGPSDateTime
struct BrickletGPSDateTime
    date::Integer
    time::Integer
end

export BrickletGPSIdentity
struct BrickletGPSIdentity
    uid::String
    connected_uid::String
    position::Char
    hardware_version::Vector{Integer}
    firmware_version::Vector{Integer}
    device_identifier::Integer
end

export BrickletGPS
"""
Determine position, velocity and altitude using GPS
"""
mutable struct BrickletGPS <: TinkerforgeDevice
    ipcon::IPConnection
    deviceInternal::PyObject

    """
    Creates an object with the unique device ID *uid* and adds it to
    the IP Connection *ipcon*.
    """
    function BrickletGPS(uid::String, ipcon::IPConnection)
        package = pyimport("tinkerforge.bricklet_gps")
        deviceInternal = package.BrickletGPS(uid, ipcon.ipconInternal)

        return new(ipcon, deviceInternal)
    end
end

export get_coordinates
"""
    $(SIGNATURES)

Returns the GPS coordinates. Latitude and longitude are given in the
``DD.dddddd°`` format, the value 57123468 means 57.123468°.
The parameter ``ns`` and ``ew`` are the cardinal directions for
latitude and longitude. Possible values for ``ns`` and ``ew`` are 'N', 'S', 'E'
and 'W' (north, south, east and west).

PDOP, HDOP and VDOP are the dilution of precision (DOP) values. They specify
the additional multiplicative effect of GPS satellite geometry on GPS
precision. See
`here <https://en.wikipedia.org/wiki/Dilution_of_precision_(GPS)>`__
for more information.

EPE is the "Estimated Position Error". This is not the
absolute maximum error, it is the error with a specific confidence. See
`here <https://www.nps.gov/gis/gps/WhatisEPE.html>`__ for more information.

This data is only valid if there is currently a fix as indicated by
:func:`Get Status`.
"""
function get_coordinates(device::BrickletGPS)
    return device.deviceInternal.get_coordinates()
end

export get_status
"""
    $(SIGNATURES)

Returns the current fix status, the number of satellites that are in view and
the number of satellites that are currently used.

Possible fix status values can be:

.. csv-table::
 :header: "Value", "Description"
 :widths: 10, 100

 "1", "No Fix, :func:`Get Coordinates`, :func:`Get Altitude` and :func:`Get Motion` return invalid data"
 "2", "2D Fix, only :func:`Get Coordinates` and :func:`Get Motion` return valid data"
 "3", "3D Fix, :func:`Get Coordinates`, :func:`Get Altitude` and :func:`Get Motion` return valid data"

There is also a :ref:`blue LED <gps_bricklet_fix_led>` on the Bricklet that
indicates the fix status.
"""
function get_status(device::BrickletGPS)
    return device.deviceInternal.get_status()
end

export get_altitude
"""
    $(SIGNATURES)

Returns the current altitude and corresponding geoidal separation.

This data is only valid if there is currently a fix as indicated by
:func:`Get Status`.
"""
function get_altitude(device::BrickletGPS)
    return device.deviceInternal.get_altitude()
end

export get_motion
"""
    $(SIGNATURES)

Returns the current course and speed. A course of 0° means the Bricklet is
traveling north bound and 90° means it is traveling east bound.

Please note that this only returns useful values if an actual movement
is present.

This data is only valid if there is currently a fix as indicated by
:func:`Get Status`.
"""
function get_motion(device::BrickletGPS)
    return device.deviceInternal.get_motion()
end

export get_date_time
"""
    $(SIGNATURES)

Returns the current date and time. The date is
given in the format ``ddmmyy`` and the time is given
in the format ``hhmmss.sss``. For example, 140713 means
14.07.13 as date and 195923568 means 19:59:23.568 as time.
"""
function get_date_time(device::BrickletGPS)
    return device.deviceInternal.get_date_time()
end

export restart
"""
    $(SIGNATURES)

Restarts the GPS Bricklet, the following restart types are available:

.. csv-table::
 :header: "Value", "Description"
 :widths: 10, 100

 "0", "Hot start (use all available data in the NV store)"
 "1", "Warm start (don't use ephemeris at restart)"
 "2", "Cold start (don't use time, position, almanacs and ephemeris at restart)"
 "3", "Factory reset (clear all system/user configurations at restart)"
"""
function restart(device::BrickletGPS, restart_type)
    device.deviceInternal.restart(restart_type)
end

export set_coordinates_callback_period
"""
    $(SIGNATURES)

Sets the period with which the :cb:`Coordinates` callback is triggered
periodically. A value of 0 turns the callback off.

The :cb:`Coordinates` callback is only triggered if the coordinates changed
since the last triggering.
"""
function set_coordinates_callback_period(device::BrickletGPS, period)
    device.deviceInternal.set_coordinates_callback_period(period)
end

export get_coordinates_callback_period
"""
    $(SIGNATURES)

Returns the period as set by :func:`Set Coordinates Callback Period`.
"""
function get_coordinates_callback_period(device::BrickletGPS)
    return device.deviceInternal.get_coordinates_callback_period()
end

export set_status_callback_period
"""
    $(SIGNATURES)

Sets the period with which the :cb:`Status` callback is triggered
periodically. A value of 0 turns the callback off.

The :cb:`Status` callback is only triggered if the status changed since the
last triggering.
"""
function set_status_callback_period(device::BrickletGPS, period)
    device.deviceInternal.set_status_callback_period(period)
end

export get_status_callback_period
"""
    $(SIGNATURES)

Returns the period as set by :func:`Set Status Callback Period`.
"""
function get_status_callback_period(device::BrickletGPS)
    return device.deviceInternal.get_status_callback_period()
end

export set_altitude_callback_period
"""
    $(SIGNATURES)

Sets the period with which the :cb:`Altitude` callback is triggered
periodically. A value of 0 turns the callback off.

The :cb:`Altitude` callback is only triggered if the altitude changed since
the last triggering.
"""
function set_altitude_callback_period(device::BrickletGPS, period)
    device.deviceInternal.set_altitude_callback_period(period)
end

export get_altitude_callback_period
"""
    $(SIGNATURES)

Returns the period as set by :func:`Set Altitude Callback Period`.
"""
function get_altitude_callback_period(device::BrickletGPS)
    return device.deviceInternal.get_altitude_callback_period()
end

export set_motion_callback_period
"""
    $(SIGNATURES)

Sets the period with which the :cb:`Motion` callback is triggered
periodically. A value of 0 turns the callback off.

The :cb:`Motion` callback is only triggered if the motion changed since the
last triggering.
"""
function set_motion_callback_period(device::BrickletGPS, period)
    device.deviceInternal.set_motion_callback_period(period)
end

export get_motion_callback_period
"""
    $(SIGNATURES)

Returns the period as set by :func:`Set Motion Callback Period`.
"""
function get_motion_callback_period(device::BrickletGPS)
    return device.deviceInternal.get_motion_callback_period()
end

export set_date_time_callback_period
"""
    $(SIGNATURES)

Sets the period with which the :cb:`Date Time` callback is triggered
periodically. A value of 0 turns the callback off.

The :cb:`Date Time` callback is only triggered if the date or time changed
since the last triggering.
"""
function set_date_time_callback_period(device::BrickletGPS, period)
    device.deviceInternal.set_date_time_callback_period(period)
end

export get_date_time_callback_period
"""
    $(SIGNATURES)

Returns the period as set by :func:`Set Date Time Callback Period`.
"""
function get_date_time_callback_period(device::BrickletGPS)
    return device.deviceInternal.get_date_time_callback_period()
end

export get_identity
"""
    $(SIGNATURES)

Returns the UID, the UID where the Bricklet is connected to,
the position, the hardware and firmware version as well as the
device identifier.

The position can be 'a', 'b', 'c', 'd', 'e', 'f', 'g' or 'h' (Bricklet Port).
A Bricklet connected to an :ref:`Isolator Bricklet <isolator_bricklet>` is always at
position 'z'.

The device identifier numbers can be found :ref:`here <device_identifier>`.
|device_identifier_constant|
"""
function get_identity(device::BrickletGPS)
    return device.deviceInternal.get_identity()
end

export register_callback
"""
Registers the given *function* with the given *callback_id*.
"""
function register_callback(device::BrickletGPS, callback_id, function_)
    if isnothing(function_)
        device.registered_callbacks.pop(callback_id, None)
    else
        device.registered_callbacks[callback_id] = function_
    end
end


# unary # 2^2 - 2 = 2 definitions
@inline fmap(f::F, x::Tuple{X}) where {F,X} = (f(first(x)),)
@inline fmap(f::F, x::NTuple) where {F} = (f(first(x)), fmap(f, Base.tail(x))...)

# binary # 2^3 - 2 = 6 definitions
@inline fmap(f::F, x::Tuple{X}, y::Tuple{Y}) where {F,X,Y} = (f(first(x), first(y)),)
@inline fmap(f::F, x::Tuple{X}, y) where {F,X} = (f(first(x), y),)
@inline fmap(f::F, x, y::Tuple{Y}) where {F,Y} = (f(x, first(y)),)
@inline fmap(f::F, x::Tuple{Vararg{Any,N}}, y::Tuple{Vararg{Any,N}}) where {F,N} = (f(first(x), first(y)), fmap(f, Base.tail(x), Base.tail(y))...)
@inline fmap(f::F, x::Tuple, y) where {F} = (f(first(x), y), fmap(f, Base.tail(x), y)...)
@inline fmap(f::F, x, y::Tuple) where {F} = (f(x, first(y)), fmap(f, x, Base.tail(y))...)

fmap(f::F, x::Tuple{X}, y::Tuple) where {F,X} = throw("Dimension mismatch.")
fmap(f::F, x::Tuple, y::Tuple{Y}) where {F,Y} = throw("Dimension mismatch.")
fmap(f::F, x::Tuple, y::Tuple) where {F} = throw("Dimension mismatch.")

@generated function fmap(f::F, x::Vararg{Any,N}) where {F,N}
    q = Expr(:block, Expr(:meta, :inline))
    t = Expr(:tuple)
    U = 1
    call = Expr(:call, :f)
    syms = Vector{Symbol}(undef, N)
    istup = Vector{Bool}(undef, N)
    for n ∈ 1:N
        syms[n] = xₙ = Symbol(:x_, n)
        push!(q.args, Expr(:(=), xₙ, Expr(:ref, :x, n)))
        istup[n] = ist = (x[n] <: Tuple)
        if ist
            U = length(x[n].parameters)
            push!(call.args, Expr(:ref, xₙ, 1))
        else
            push!(call.args, xₙ)
        end
    end
    push!(t.args, call)
    for u ∈ 2:U
        call = Expr(:call, :f)
        for n ∈ 1:N
            xₙ = syms[n]
            if istup[n]
                push!(call.args, Expr(:ref, xₙ, u))
            else
                push!(call.args, xₙ)
            end
        end
        push!(t.args, call)
    end
    push!(q.args, t); q
end

for op ∈ [:vsub, :vabs, :vfloor, :vceil, :vtrunc, :vround, :vsqrt, :vnot, :vleading_zeros, :vtrailing_zeros, :vsub_fast]
    @eval @inline $op(v1::VecUnroll{N,W,T}) where {N,W,T} = VecUnroll(fmap($op, v1.data))
end
# only for `Float32` and `Float64`
@inline inv_approx(v::VecUnroll{N,W,T}) where {N,W,T<:Union{Float32,Float64}} = VecUnroll(fmap(inv_approx, v.data))

@inline vreinterpret(::Type{T}, v::VecUnroll) where {T<:Number} = VecUnroll(fmap(vreinterpret, T, v.data))
for op ∈ [:vadd,:vsub,:vmul,:vand,:vor,:vxor,:vlt,:vle,:vgt,:vge,:veq,:vne,:vadd_fast,:vsub_fast,:vmul_fast]
    @eval begin
        @inline $op(v1::VecUnroll, v2::VecUnroll) = VecUnroll(fmap($op, v1.data, v2.data))
        @inline $op(v1::VecUnroll{N,W,T,V}, ::StaticInt{M}) where {N,W,T,V,M} = VecUnroll(fmap($op, v1.data, vbroadcast(Val{W}(), T(M))))
        @inline $op(::StaticInt{M}, v1::VecUnroll{N,W,T,V}) where {N,W,T,V,M} = VecUnroll(fmap($op, vbroadcast(Val{W}(), T(M)), v1.data))

        @inline $op(v1::VecUnroll{N,W,T,V}, ::StaticInt{1}) where {N,W,T,V} = VecUnroll(fmap($op, v1.data, One()))
        @inline $op(::StaticInt{1}, v1::VecUnroll{N,W,T,V}) where {N,W,T,V} = VecUnroll(fmap($op, One(), v1.data))

        @inline $op(v1::VecUnroll{N,W,T,V}, ::StaticInt{0}) where {N,W,T,V} = VecUnroll(fmap($op, v1.data, Zero()))
        @inline $op(::StaticInt{0}, v1::VecUnroll{N,W,T,V}) where {N,W,T,V} = VecUnroll(fmap($op, Zero(), v1.data))
    end
end
for op ∈ [:vmax,:vmax_fast,:vmin,:vmin_fast,:vcopysign]
    @eval begin
        @inline $op(v1::VecUnroll, v2::VecUnroll) = VecUnroll(fmap($op, v1.data, v2.data))
    end
end
for op ∈ [:vgt,:vge,:vlt,:vle,:veq,:vne,:vmax,:vmax_fast,:vmin,:vmin_fast]
    @eval begin
        @inline $op(v::VecUnroll{N,W,T,V}, s::Union{NativeTypes,AbstractSIMDVector}) where {N,W,T,V} = VecUnroll(fmap($op, v.data, vbroadcast(Val{W}(), s)))
        @inline $op(s::Union{NativeTypes,AbstractSIMDVector}, v::VecUnroll{N,W,T,V}) where {N,W,T,V} = VecUnroll(fmap($op, vbroadcast(Val{W}(), s), v.data))
    end
end

for op ∈ [:vrem, :vshl, :vashr, :vlshr, :vdiv, :vfdiv, :vrem_fast, :vfdiv_fast]
    @eval begin
        @inline $op(v1::VecUnroll, v2::VecUnroll) = VecUnroll(fmap($op, v1.data, v2.data))
    end
end
for op ∈ [:vrem, :vand, :vor, :vxor, :vshl, :vashr, :vlshr, :vlt, :vle,:vgt,:vge,:veq,:vne]
    @eval begin
        @inline $op(vu::VecUnroll, i::MM) = $op(vu, Vec(i))
        @inline $op(i::MM, vu::VecUnroll) = $op(Vec(i), vu)
    end
end
for op ∈ [:vshl, :vashr, :vlshr]
    @eval @inline $op(m::Mask, vu::VecUnroll) = $op(Vec(m), vu)
end
for op ∈ [:rotate_left,:rotate_right,:funnel_shift_left,:funnel_shift_right]
    @eval begin
        @inline $op(v1::VecUnroll{N,W,T,V}, v2::R) where {N,W,T,V,R<:IntegerTypes} = VecUnroll(fmap($op, v1.data, promote_type(V, R)(v2)))
        @inline $op(v1::R, v2::VecUnroll{N,W,T,V}) where {N,W,T,V,R<:IntegerTypes} = VecUnroll(fmap($op, promote_type(V, R)(v1), v2.data))
        @inline $op(v1::VecUnroll, v2::VecUnroll) = VecUnroll(fmap($op, v1.data, v2.data))
    end
end

for op ∈ [:vfma, :vmuladd, :vfma_fast, :vmuladd_fast, :vfnmadd, :vfmsub, :vfnmsub, :vfnmadd_fast, :vfmsub_fast, :vfnmsub_fast,
          :vfmadd231, :vfnmadd231, :vfmsub231, :vfnmsub231, :ifmahi, :ifmalo]
    @eval begin
        @inline $op(v1::VecUnroll{N,W}, v2::VecUnroll{N,W}, v3::VecUnroll{N,W}) where {N,W} = VecUnroll(fmap($op, v1.data, v2.data, v3.data))
    end
end
@inline vifelse(v1::VecUnroll{N,W,<:Boolean}, v2::T, v3::T) where {N,W,T<:NativeTypes} = VecUnroll(fmap(vifelse, v1.data, Vec{W,T}(v2), Vec{W,T}(v3)))
@inline vifelse(v1::VecUnroll{N,W,<:Boolean}, v2::T, v3::T) where {N,W,T<:Real} = VecUnroll(fmap(vifelse, v1.data, v2, v3))
@inline vifelse(v1::Vec{W,Bool}, v2::VecUnroll{N,W,T}, v3::Union{NativeTypes,AbstractSIMDVector,StaticInt}) where {N,W,T} = VecUnroll(fmap(vifelse, Vec{W,T}(v1), v2.data, Vec{W,T}(v3)))
@inline vifelse(v1::Vec{W,Bool}, v2::Union{NativeTypes,AbstractSIMDVector,StaticInt}, v3::VecUnroll{N,W,T}) where {N,W,T} = VecUnroll(fmap(vifelse, Vec{W,T}(v1), Vec{W,T}(v2), v3.data))
@inline vifelse(v1::VecUnroll{N,W,<:Boolean}, v2::VecUnroll{N,W,T}, v3::Union{NativeTypes,AbstractSIMDVector,StaticInt}) where {N,W,T} = VecUnroll(fmap(vifelse, v1.data, v2.data, Vec{W,T}(v3)))
@inline vifelse(v1::VecUnroll{N,W,<:Boolean}, v2::Union{NativeTypes,AbstractSIMDVector,StaticInt}, v3::VecUnroll{N,W,T}) where {N,W,T} = VecUnroll(fmap(vifelse, v1.data, Vec{W,T}(v2), v3.data))
@inline vifelse(v1::Vec{W,Bool}, v2::VecUnroll{N,W,T}, v3::VecUnroll{N,W,T}) where {N,W,T} = VecUnroll(fmap(vifelse, Vec{W,T}(v1), v2.data, v3.data))
@inline vifelse(v1::VecUnroll{N,W,<:Boolean}, v2::VecUnroll{N,W,T}, v3::VecUnroll{N,W,T}) where {N,W,T} = VecUnroll(fmap(vifelse, v1.data, v2.data, v3.data))
@inline function vifelse(v1::VecUnroll{N,W,<:Boolean}, v2::VecUnroll{N,W}, v3::VecUnroll{N,W}) where {N,W}
    v4, v5 = promote(v2, v3)
    VecUnroll(fmap(vifelse, v1.data, v4.data, v5.data))
end

@inline veq(v::VecUnroll{N,W,T}, x::AbstractIrrational) where {N,W,T} = v == vbroadcast(Val{W}(), T(x))
@inline veq(x::AbstractIrrational, v::VecUnroll{N,W,T}) where {N,W,T} = vbroadcast(Val{W}(), T(x)) == v

@inline vunsafe_trunc(::Type{T}, v::VecUnroll) where {T<:Real} = VecUnroll(fmap(vunsafe_trunc, T, v.data))
@inline vrem(v::VecUnroll, ::Type{T}) where {T<:Real} = VecUnroll(fmap(vrem, v.data, T))
@inline vrem(v::VecUnroll{N,W}, ::Type{VecUnroll{N,W,T,V}}) where {N,W,T,V} = VecUnroll(fmap(vrem, v.data, V))

@inline (::Type{VecUnroll{N,W,T,V}})(vu::VecUnroll{N,W,T,V}) where {N,W,T,V<:AbstractSIMDVector{W,T}} = vu
@inline (::Type{VecUnroll{N,W,T,VT}})(vu::VecUnroll{N,W,S,VS})  where {N,W,T,VT<:AbstractSIMDVector{W,T},S,VS<:AbstractSIMDVector{W,S}} = VecUnroll(fmap(convert, Vec{W,T}, vu.data))

function collapse_expr(N, op)
    N += 1
    t = Expr(:tuple); s = Vector{Symbol}(undef, N)
    for n ∈ 1:N
        s_n = s[n] = Symbol(:v_, n)
        push!(t.args, s_n)
    end
    q = quote
        $(Expr(:meta,:inline))
        $t = data(vu)
    end
    while N > 1
        for n ∈ 1:N >>> 1
            push!(q.args, Expr(:(=), s[n], Expr(:call, op, s[n], s[n + (N >>> 1)])))
        end
        isodd(N) && push!(q.args, Expr(:(=), s[1], Expr(:call, op, s[1], s[N])))
        N >>>= 1
    end
    q
end
@generated collapse_add(vu::VecUnroll{N}) where {N} = collapse_expr(N, :vadd)
@generated collapse_mul(vu::VecUnroll{N}) where {N} = collapse_expr(N, :vmul)
@generated collapse_max(vu::VecUnroll{N}) where {N} = collapse_expr(N, :max)
@generated collapse_min(vu::VecUnroll{N}) where {N} = collapse_expr(N, :min)
@generated collapse_and(vu::VecUnroll{N}) where {N} = collapse_expr(N, :&)
@generated collapse_or(vu::VecUnroll{N}) where {N} = collapse_expr(N, :|)
@inline vsum(vu::VecUnroll) = vsum(collapse_add(vu))
@inline vsum(s, vu::VecUnroll) = vsum(s, collapse_add(vu))
@inline vprod(vu::VecUnroll) = vprod(collapse_mul(vu))
@inline vprod(s, vu::VecUnroll) = vprod(s, collapse_mul(vu))
@inline vmaximum(vu::VecUnroll) = vmaximum(collapse_max(vu))
@inline vminimum(vu::VecUnroll) = vminimum(collapse_min(vu))
@inline vall(vu::VecUnroll) = vall(collapse_and(vu))
@inline vany(vu::VecUnroll) = vany(collapse_or(vu))


# The intakes bounds of the network are determined by the
# medium concentration in the Chemostat model (see Cossios paper)
# This is a base medium for modeling
function load_base_intake_info()
    base_intake_info = Dict(
        "EX_glc_LPAREN_e_RPAREN_" => Dict("c"=>  100.0, "lb"=> -100.0),
        "EX_nh4_LPAREN_e_RPAREN_" => Dict("c"=>99999.0, "lb"=> -100.0),
        "EX_o2_LPAREN_e_RPAREN_"  => Dict("c"=>99999.0, "lb"=> -100.0),
        "EX_pi_LPAREN_e_RPAREN_"  => Dict("c"=>99999.0, "lb"=> -100.0),
        "EX_so4_LPAREN_e_RPAREN_" => Dict("c"=>99999.0, "lb"=> -100.0),
        "EX_h2o_LPAREN_e_RPAREN_" => Dict("c"=>99999.0, "lb"=> -100.0),
        "EX_h_LPAREN_e_RPAREN_" => Dict("c"=>99999.0, "lb"=> -100.0),
    )
end


import Base:in, +, -, *, ==, convert, isempty, isequal, length, push!, sizeof, append!
import SeisIO: GeoLoc, PZResp
export CorrData

const corrfields = [:name, :loc, :comp, :rotated, :corr_type, :fs, :gain,
                    :freqmin, :freqmax, :cc_len, :cc_step, :whitened, :time_norm, :resp,
                    :maxlag, :dist, :azi, :baz]

"""
   CorrData

A structure for cross-correlations of ambient noise data.

## Fields: CorrData
| **Field** | **Description** |
|:------------|:-------------|
| :name       | Freeform channel name in NET1.STA1.LOC1.CHAN1.NET2.STA2.LOC2.CHAN2 form |
| :id         | Channel ids. use NET.STA.LOC.CHAN format when possible. |
| :loc        | Location (position) object for station 1. |
| :fs         | Sampling frequency in Hz. |
| :gain       | Scalar gain; divide data by the gain to convert to units  |
| :freqmin    | Minimum frequency for whitening.  |
| :freqmax    | Maximum frequency for whitening. |
| :cc_len     | Length of each correlation in seconds. |
| :cc_step    | Spacing between correlation windows in seconds. |
| :whitened   | Whitening applied.
| :time_norm  | Apply one-bit whitening with "one_bit". |
| :resp       | Instrument response object, format [zeros poles] |
| :misc       | Dictionary for non-critical information. |
| :notes      | Timestamped notes; includes automatically-logged acquisition and |
|             | processing information. |
| :dist       | Distance between station 1 and station 2 in Km. |
| :azi        | Azimuth from station 1 and station 2 in degrees. |
| :baz        | Back azimuth between station 1 and station 2 in degrees. |
| :maxlag     | Maximum lag time in seconds to keep from correlations. |
| :t          | Starttime of each correlation. |
| :corr       | Correlations stored in columns. |
"""
mutable struct CorrData
  name::String                                # name [Net1.Sta1.Loc1.Chan1.Net2.Sta2.Loc2.Chan2]
  id::String                                  # id [Y-mm-dd] this is date of corr
  loc::GeoLoc                                 # loc
  comp::String                                # 1st channel, 2nd channel [ZZ,RT,..]
  rotated::Bool                               # rotation applied?
  corr_type::String                           # "corr", "deconv", "coh"
  fs::Float64                                 # sampling rate [Hz]
  gain::Float64                               # gain
  freqmin::Float64                            # minumum frequency [Hz]
  freqmax::Float64                            # maximum frequency [Hz]
  cc_len::Int                                 # window_length [s]
  cc_step::Int                                # step between windows [s]
  whitened::Bool                              # whitening applied
  time_norm::Union{Bool,String}               # time normaliation
  resp::PZResp                                # response poles/zeros
  misc::Dict{String,Any}                      # misc
  notes::Array{String,1}                      # notes
  dist::Float64                               # distance between stations [km]
  azi::Float64                                # azimuth between stations [degrees]
  baz::Float64                                # back azimuth between stations [degrees]
  maxlag::Float64                             # maximum lag time [s]
  t::Array{Float64,1}                         # time
  corr::Array{<:Union{Float32,Float64},2}     # corr data

  function CorrData(
      name     ::String,
      id       ::String,
      loc      ::GeoLoc,
      comp     ::String,
      rotated  ::Bool,
      corr_type::String,
      fs       ::Float64,
      gain     ::Float64,
      freqmin  ::Float64,
      freqmax  ::Float64,
      cc_len   ::Int,
      cc_step  ::Int,
      whitened ::Bool,
      time_norm::Union{Bool,String},
      resp     ::PZResp,
      misc     ::Dict{String,Any},
      notes    ::Array{String,1},
      dist     ::Float64,
      azi      ::Float64,
      baz      ::Float64,
      maxlag   ::Float64,
      t        ::Array{Float64,1},
      corr     ::Array{<:Union{Float32,Float64},2}
      )

      return new(name, id, loc, comp, rotated, corr_type, fs, gain, freqmin,
                 freqmax, cc_len, cc_step, whitened, time_norm, resp, misc,
                 notes, dist, azi, baz, maxlag, t, corr)
    end
end

CorrData(;
          name     ::String                    = "",
          id       ::String                    = "",
          loc      ::GeoLoc                    = GeoLoc(),
          comp     ::String                    = "",
          rotated  ::Bool                      = false,
          corr_type::String                    = "",
          fs       ::Float64                   = zero(Float64),
          gain     ::Float64                   = one(Float64),
          freqmin  ::Float64                   = zero(Float64),
          freqmax  ::Float64                   = zero(Float64),
          cc_len   ::Int                       = zero(Int),
          cc_step  ::Int                       = zero(Int),
          whitened ::Bool                      = false,
          time_norm::Union{Bool,String}        = false,
          resp     ::PZResp                    = PZResp(),
          misc     ::Dict{String,Any}          = Dict{String,Any}(),
          notes    ::Array{String,1}           = Array{String,1}(undef, 0),
          dist     ::Float64                   = zero(Float64),
          azi      ::Float64                   = zero(Float64),
          baz      ::Float64                   = zero(Float64),
          maxlag   ::Float64                   = zero(Float64),
          t        ::Array{Float64,1}          = Array{Float64,1}(undef, 0),
          corr     ::Array{<:Union{Float32,Float64},2} = Array{Float32,2}(undef, 0, 2)
          ) = CorrData(name, id, loc, comp, rotated, corr_type, fs, gain,
                      freqmin, freqmax, cc_len, cc_step, whitened, time_norm,
                      resp, misc, notes, dist, azi, baz, maxlag, t, corr)

CorrData(F1::FFTData, F2::FFTData, comp::String, rotated::Bool, corr_type::String,
        maxlag::Float64, t::Array{Float64,1}, corr::Array{<:Union{Float32,Float64},2}
       ) = CorrData(F1.name * '.' * F2.name, F1.id, F1.loc, comp, rotated,
                  corr_type, F1.fs, F1.gain, F1.freqmin, F1.freqmax, F1.cc_len,
                  F1.cc_step, F1.whitened, F1.time_norm, F1.resp, F1.misc,
                  F1.notes, get_dist(F1,F2), get_azi(F1,F2), get_baz(F1,F2),
                  maxlag, t, corr)

in(s::String, C::CorrData) = C.id==s

isempty(C::CorrData) = min(C.name=="",C.id == "",isempty(C.loc), C.comp=="",
                          C.rotated==false,C.fs == zero(typeof(C.fs)),
                          C.corr_type=="",
                          C.gain == one(typeof(C.gain)),
                          C.freqmin == zero(typeof(C.freqmin)),
                          C.freqmax == zero(typeof(C.freqmax)),
                          C.cc_len == zero(typeof(C.cc_len)),
                          C.cc_step == zero(typeof(C.cc_step)),
                          C.whitened == false,
                          C.time_norm == false,
                          isempty(C.resp), isempty(C.misc),isempty(C.notes),
                          C.maxlag == zero(typeof(C.maxlag)), isempty(C.t),
                          isempty(C.corr))

isequal(C::CorrData, U::CorrData) = minimum([hash(getfield(C,i))==hash(getfield(U,i)) for i in corrfields]::Array{Bool,1})
==(C::CorrData, U::CorrData) = isequal(C,U)::Bool

append!(C::CorrData, U::CorrData)  = (if C == U;
  setfield!(C, :t, vcat(getfield(C,:t), getfield(U,:t)));
  setfield!(C, :corr, hcat(getfield(C,:corr), getfield(U,:corr)))
  elseif isempty(C)
  [setfield!(C, i, getfield(U,i)) for i in corrfields];
  [setfield!(C, i, getfield(U,i)) for i in [:t,:corr]];
  end;
  return C)
+(C::CorrData, U::CorrData) = (T = deepcopy(C); return append!(T, U))

function sizeof(C::CorrData)
s = sum([sizeof(getfield(C,f)) for f in corrfields])
if !isempty(C.notes)
  s += sum([sizeof(i) for i in C.notes])
end
if !isempty(C.misc)
  s += sum([sizeof(i) for i in values(C.misc)])
end
return s
end


function convertchemkin2yml(chemkinpath;spcdictpath="",output="chem.rms")
    if spcdictpath != ""
        spcs,rxns = chemkin.load_chemkin_file(chemkinpath,dictionary_path=spcdictpath)
    else
        spcs,rxns = chemkin.load_chemkin_file(chemkinpath)
    end
    writeyml(spcs,rxns;path=output)
end

function writeyml(spcs,rxns;path="chem.rms")
    D = getmechdict(spcs,rxns)
    yaml.dump(D,stream=pybuiltin("open")(path,"w"))
end

function getmechdict(spcs,rxns)
    D = Dict([])
    D["Units"] = Dict([])
    D["Reactions"] = []
    D["Phases"] = [Dict([])]
    D["Phases"][1]["name"] = "phase"
    D["Phases"][1]["Species"] = [obj2dict(x,spcs) for x in spcs]
    D["Reactions"] = [obj2dict(x,spcs) for x in rxns]
    return D
end

function getradicals(obj::T) where {T}
    sm = obj.molecule[1].to_smiles()
    if sm == "[O][O]"
        return 0
    else
        return obj.molecule[1].multiplicity-1
    end
end

function obj2dict(obj,spcs;label="solvent")
    D = Dict([])
    if pybuiltin("isinstance")(obj,species.Species)
        D["name"] = obj.label
        D["type"] = "Species"
        if length(obj.molecule) == 0
            println(obj)
            println(obj.label)
        end
        D["smiles"] = obj.molecule[1].to_smiles()
        D["thermo"] = obj2dict(obj.thermo,spcs)
        if D["smiles"] != "[O][O]"
            D["radicalelectrons"] = obj.molecule[1].multiplicity-1
        else
            D["radicalelectrons"] = 0
        end
    elseif pybuiltin("isinstance")(obj,nasa.NASA)
        D["polys"] = [obj2dict(k,spcs) for k in obj.polynomials]
        D["type"] = "NASA"
    elseif pybuiltin("isinstance")(obj,nasa.NASAPolynomial)
        D["type"] = "NASApolynomial"
        D["coefs"] = PyVector(obj.coeffs)
        D["Tmax"] = obj.Tmax.value_si
        D["Tmin"] = obj.Tmin.value_si
    elseif pybuiltin("isinstance")(obj,reaction.Reaction)
        D["reactants"] = [x.label for x in obj.reactants]
        D["products"] = [x.label for x in obj.products]
        D["kinetics"] = obj2dict(obj.kinetics,spcs)
        D["type"] = "ElementaryReaction"
        D["radicalchange"] = sum([getradicals(x) for x in obj.products])-sum([getradicals(x) for x in obj.reactants])
    elseif pybuiltin("isinstance")(obj,arrhenius.Arrhenius)
        D["type"] = "Arrhenius"
        D["A"] = obj.A.value_si
        D["Ea"] = obj.Ea.value_si
        D["n"] = obj.n.value_si
    elseif pybuiltin("isinstance")(obj,arrhenius.PDepArrhenius)
        D["type"] = "PdepArrhenius"
        D["Ps"] = PyVector(obj.pressures.value_si)
        D["arrs"] = [obj2dict(x,spcs) for x in obj.arrhenius]
    elseif pybuiltin("isinstance")(obj,arrhenius.MultiArrhenius)
        D["type"] = "MultiArrhenius"
        D["arrs"] = [obj2dict(x,spcs) for x in obj.arrhenius]
    elseif pybuiltin("isinstance")(obj,arrhenius.MultiPDepArrhenius)
        D["type"] = "MultiPdepArrhenius"
        D["parrs"] = [obj2dict(x,spcs) for x in obj.arrhenius]
    elseif pybuiltin("isinstance")(obj,falloff.ThirdBody)
        D["type"] = "ThirdBody"
        D["arr"] = obj2dict(obj.arrheniusLow,spcs)
        D["efficiencies"] = Dict([spcs[i].label=>float(val) for (i,val) in enumerate(obj.get_effective_collider_efficiencies(spcs)) if val != 1])
    elseif pybuiltin("isinstance")(obj,falloff.Lindemann)
        D["type"] = "Lindemann"
        D["arrhigh"] = obj2dict(obj.arrheniusHigh,spcs)
        D["arrlow"] = obj2dict(obj.arrheniusLow,spcs)
        D["efficiencies"] = Dict([spcs[i].label=>float(val) for (i,val) in enumerate(obj.get_effective_collider_efficiencies(spcs)) if val != 1])
    elseif pybuiltin("isinstance")(obj,falloff.Troe)
        D["type"] = "Troe"
        D["arrhigh"] = obj2dict(obj.arrheniusHigh,spcs)
        D["arrlow"] = obj2dict(obj.arrheniusLow,spcs)
        D["efficiencies"] = Dict([spcs[i].label=>float(val) for (i,val) in enumerate(obj.get_effective_collider_efficiencies(spcs)) if val != 1])
        D["a"] = obj.alpha
        D["T1"] = obj.T1.value_si
        if !isa(obj.T2,Nothing)
            D["T2"] = obj.T2.value_si
        else
            D["T2"] = 0.0
        end
        D["T3"] = obj.T3.value_si
    elseif pybuiltin("isinstance")(obj,chebyshev.Chebyshev)
        D["type"] = "Chebyshev"
        n,m = size(obj.coeffs.value_si)
        D["coefs"] = PyVector([PyVector(obj.coeffs.value_si[i,:]) for i in 1:n])
        D["Tmin"] = obj.Tmin.value_si
        D["Tmax"] = obj.Tmax.value_si
        D["Pmin"] = obj.Pmin.value_si
        D["Pmax"] = obj.Pmax.value_si
    elseif pybuiltin("isinstance")(obj,wilhoit.Wilhoit)
        D["type"] = "Wilhoit"
        D["coefs"] = [obj.a0,obj.a1,obj.a2,obj.a3]
        D["Cp0"] = obj.Cp0.value_si
        D["Cpinf"] = obj.CpInf.value_si
        D["H0"] = obj.H0.value_si
        D["S0"] = obj.S0.value_si
        D["B"] = obj.B.value_si
    elseif pybuiltin("isinstance")(obj,solvation.SolventData)
        D["type"] = "Solvent"
        D["name"] = label
        dsub = dict()
        dsub["type"] = "RiedelViscosity"
        dsub["A"] = obj.A.value_si
        dsub["B"] = obj.B.value_si
        dsub["C"] = obj.C.value_si
        dsub["D"] = obj.D.value_si
        dsub["E"] = obj.E.value_si
        D["mu"] = dsub
    elseif pybuiltin("is")(obj,pybuiltin("None"))
        return pybuiltin("None")
    else
        throw(error("object $obj not understood"))
    end
    return D
end


# Kernels that handle broadcasting with different sized arrays.
# Full broadcasting index computation is complicated.  I am going to handle the
# case where the input arrays are either the same size as the result or vectors
# (have a single dim > 1).  If the result array has size (a1,a2,...) and input
# array b has size (1,...,bn,...,1), we need to find the linear index in b given
# the linear index in a.  If we are at position (i1,i2,...) the answer is just
# i_n=mod(div(i,stride(a,n)),size(a,n)) with 0 indexing.  So we can just pass in
# stride(a,n) and size(a,n) as an argument for each input.

fp = open("cuda12.cu","w")
#using Knet: broadcast_ops

function cuda12src(f, j=f, ex="$f(xi,yi)"; BLK=256, THR=256)
  sprint() do s
    for (T,F) in [("float","$(f)_32"),("double","$(f)_64")]
        print(s,
"""
__global__ void _$(F)_12(int n, $T *x, int sx, int nx, $T *y, int sy, int ny, $T *z) {
  int i = threadIdx.x + blockIdx.x * blockDim.x;
  while (i < n) {
    $T xi = (nx==n ? x[i] : sx==1 ? x[i%nx] : nx==1 ? x[0] : x[(i/sx)%nx]);
    $T yi = (ny==n ? y[i] : sy==1 ? y[i%ny] : ny==1 ? y[0] : y[(i/sy)%ny]);
    z[i] = $ex;
    i += blockDim.x * gridDim.x;
  }
}
extern "C" {
  $DLLEXPORT void $(F)_12(int n, $T *x, int sx, int nx, $T *y, int sy, int ny, $T *z) {
    _$(F)_12<<<$BLK,$THR>>>(n,x,sx,nx,y,sy,ny,z);
  }
}
""")
    end
  end
end

for a in broadcast_ops
    if !isa(a,Tuple); a=(a,); end
    print(fp,cuda12src(a...))
end

close(fp)


# It's not a model --- its a simulation!

mutable struct Simulation{M, Δ, C, I, T, W, R, D, O, P, F, Π}
                 model :: M
                    Δt :: Δ
         stop_criteria :: C
        stop_iteration :: I
             stop_time :: T
       wall_time_limit :: W
              run_time :: R
           diagnostics :: D
        output_writers :: O
              progress :: P
    iteration_interval :: F
            parameters :: Π
end

"""
    Simulation(model; Δt,
         stop_criteria = Function[iteration_limit_exceeded, stop_time_exceeded, wall_time_limit_exceeded],
        stop_iteration = Inf,
             stop_time = Inf,
       wall_time_limit = Inf,
           diagnostics = OrderedDict{Symbol, AbstractDiagnostic}(),
        output_writers = OrderedDict{Symbol, AbstractOutputWriter}(),
              progress = nothing,
    iteration_interval = 1,
            parameters = nothing)

Construct an Oceananigans.jl `Simulation` for a `model` with time step `Δt`.

Keyword arguments
=================
- `Δt`: Required keyword argument specifying the simulation time step. Can be a `Number`
  for constant time steps or a `TimeStepWizard` for adaptive time-stepping.
- `stop_criteria`: A list of functions or callable objects (each taking a single argument,
  the `simulation`). If any of the functions return `true` when the stop criteria is
  evaluated the simulation will stop.
- `stop_iteration`: Stop the simulation after this many iterations.
- `stop_time`: Stop the simulation once this much model clock time has passed.
- `wall_time_limit`: Stop the simulation if it's been running for longer than this many
   seconds of wall clock time.
- `progress`: A function with a single argument, the `simulation`. Will be called every
  `iteration_interval` iterations. Useful for logging simulation health.
- `iteration_interval`: How often to update the time step, check stop criteria, and call
  `progress` function (in number of iterations).
- `parameters`: Parameters that can be accessed in the `progress` function.
"""
function Simulation(model; Δt,
        stop_criteria = Any[iteration_limit_exceeded, stop_time_exceeded, wall_time_limit_exceeded],
       stop_iteration = Inf,
            stop_time = Inf,
      wall_time_limit = Inf,
          diagnostics = OrderedDict{Symbol, AbstractDiagnostic}(),
       output_writers = OrderedDict{Symbol, AbstractOutputWriter}(),
             progress = nothing,
   iteration_interval = 1,
           parameters = nothing)

   if stop_iteration == Inf && stop_time == Inf && wall_time_limit == Inf
       @warn "This simulation will run forever as stop iteration = stop time " *
             "= wall time limit = Inf."
   end

   if Δt isa TimeStepWizard && iteration_interval == 1
       @warn "You have used the default iteration_interval=1. This simulation will " *
             "recalculate the time step every iteration which can be slow."
   end

   run_time = 0.0

   return Simulation(model, Δt, stop_criteria, stop_iteration, stop_time, wall_time_limit,
                     run_time, diagnostics, output_writers, progress, iteration_interval,
                     parameters)
end

Base.show(io::IO, s::Simulation) =
    print(io, "Simulation{$(typeof(s.model).name){$(typeof(s.model.architecture)), $(eltype(s.model.grid))}}\n",
            "├── Model clock: time = $(prettytime(s.model.clock.time)), iteration = $(s.model.clock.iteration) \n",
            "├── Next time step ($(typeof(s.Δt))): $(prettytime(get_Δt(s.Δt))) \n",
            "├── Iteration interval: $(s.iteration_interval)\n",
            "├── Stop criteria: $(s.stop_criteria)\n",
            "├── Run time: $(prettytime(s.run_time)), wall time limit: $(s.wall_time_limit)\n",
            "├── Stop time: $(prettytime(s.stop_time)), stop iteration: $(s.stop_iteration)\n",
            "├── Diagnostics: $(ordered_dict_show(s.diagnostics, "│"))\n",
            "└── Output writers: $(ordered_dict_show(s.output_writers, "│"))")


mutable struct _ConflictRefinerData
    confstat::Cint
    rowind::Vector{Cint}
    rowbdstat::Vector{Cint}
    confnumrows::Cint
    colind::Vector{Cint}
    colbdstat::Vector{Cint}
    confnumcols::Cint
end

function MOI.compute_conflict!(model::Optimizer)
    # This function always calls refineconflict first, which starts the conflict
    # refiner. In other words, any call to this function is expensive.
    confnumrows_p = Ref{Cint}(0)
    confnumcols_p = Ref{Cint}(0)
    ret = CPXrefineconflict(model.env, model.lp, confnumrows_p, confnumcols_p)
    _check_ret(model, ret)
    # Then, retrieve it.
    confstat_p = Ref{Cint}()
    rowind = Vector{Cint}(undef, confnumrows_p[])
    rowbdstat = Vector{Cint}(undef, confnumrows_p[])
    confnumrows_p = Ref{Cint}()
    colind = Vector{Cint}(undef, confnumcols_p[])
    colbdstat = Vector{Cint}(undef, confnumcols_p[])
    confnumcols_p  = Ref{Cint}()
    ret = CPXgetconflict(
        model.env,
        model.lp,
        confstat_p,
        rowind,
        rowbdstat,
        confnumrows_p,
        colind,
        colbdstat,
        confnumcols_p,
    )
    if ret == CPXERR_NO_CONFLICT
        model.conflict = _ConflictRefinerData(
            CPX_STAT_CONFLICT_FEASIBLE, Cint[], Cint[], 0, Cint[], Cint[], 0
        )
    else
        _check_ret(model, ret)
        model.conflict = _ConflictRefinerData(
            confstat_p[],
            rowind,
            rowbdstat,
            confnumrows_p[],
            colind,
            colbdstat,
            confnumcols_p[],
        )
    end
    return
end

function _ensure_conflict_computed(model::Optimizer)
    if model.conflict === nothing
        error(
            "Cannot access conflict status. Call " *
            "`MOI.compute_conflict!(model)` first. In case the model is " *
            "modified, the computed conflict will not be purged."
        )
    end
end

"""
    ConflictStatus()

Return the raw status from CPLEX indicating the status of the last computed
conflict.

It returns an integer value defined in
the [CPLEX documentation](https://www.ibm.com/support/knowledgecenter/SSSA5P_12.10.0/ilog.odms.cplex.help/refcallablelibrary/macros/homepagesolutionstatus.html)
(with a name like `CPX_STAT_CONFLICT_*`) or `nothing` if the function
`MOI.compute_conflict!` has not yet been called.
"""
struct ConflictStatus <: MOI.AbstractModelAttribute end

MOI.supports(::Optimizer, ::ConflictStatus) = true

function MOI.get(model::Optimizer, ::ConflictStatus)
    return model.conflict === nothing ? nothing : model.conflict.confstat
end

MOI.supports(::Optimizer, ::MOI.ConflictStatus) = true

function MOI.get(model::Optimizer, ::MOI.ConflictStatus)
    status = MOI.get(model, ConflictStatus())
    if status === nothing
        return MOI.COMPUTE_CONFLICT_NOT_CALLED
    elseif status == CPX_STAT_CONFLICT_MINIMAL
        return MOI.CONFLICT_FOUND
    elseif status == CPX_STAT_CONFLICT_FEASIBLE
        return MOI.NO_CONFLICT_EXISTS
    else
        return MOI.NO_CONFLICT_FOUND
    end
end

function _get_conflict_status(
    model::Optimizer, index::MOI.ConstraintIndex{MOI.SingleVariable, <:Any}
)
    _ensure_conflict_computed(model)
    cindex = findfirst(
        x -> x == Cint(_info(model, index).column - 1), model.conflict.colind
    )
    return cindex === nothing ? nothing : model.conflict.colbdstat[cindex]
end

function MOI.get(
    model::Optimizer,
    ::MOI.ConstraintConflictStatus,
    index::MOI.ConstraintIndex{MOI.SingleVariable, <:MOI.LessThan},
)
    status = _get_conflict_status(model, index)
    if status in (CPX_CONFLICT_MEMBER, CPX_CONFLICT_UB)
        return MOI.IN_CONFLICT
    elseif status in (CPX_CONFLICT_POSSIBLE_MEMBER, CPX_CONFLICT_POSSIBLE_UB)
        return MOI.MAYBE_IN_CONFLICT
    else
        return MOI.NOT_IN_CONFLICT
    end
end

function MOI.get(
    model::Optimizer,
    ::MOI.ConstraintConflictStatus,
    index::MOI.ConstraintIndex{MOI.SingleVariable, <:MOI.GreaterThan},
)
    status = _get_conflict_status(model, index)
    if status in (CPX_CONFLICT_MEMBER, CPX_CONFLICT_LB)
        return MOI.IN_CONFLICT
    elseif status in (CPX_CONFLICT_POSSIBLE_MEMBER, CPX_CONFLICT_POSSIBLE_LB)
        return MOI.MAYBE_IN_CONFLICT
    else
        return MOI.NOT_IN_CONFLICT
    end
end

function MOI.get(
    model::Optimizer,
    ::MOI.ConstraintConflictStatus,
    index::MOI.ConstraintIndex{
        MOI.SingleVariable, <:Union{MOI.EqualTo, MOI.Interval}
    }
)
    status = _get_conflict_status(model, index)
    if status in (CPX_CONFLICT_MEMBER, CPX_CONFLICT_LB, CPX_CONFLICT_UB)
        return MOI.IN_CONFLICT
    elseif status in (CPX_CONFLICT_POSSIBLE_MEMBER, CPX_CONFLICT_POSSIBLE_LB, CPX_CONFLICT_POSSIBLE_UB)
        return MOI.MAYBE_IN_CONFLICT
    else
        return MOI.NOT_IN_CONFLICT
    end
end

function MOI.get(
    model::Optimizer,
    ::MOI.ConstraintConflictStatus,
    index::MOI.ConstraintIndex{
        <:MOI.ScalarAffineFunction,
        <:Union{MOI.LessThan, MOI.GreaterThan, MOI.EqualTo}
    },
)
    _ensure_conflict_computed(model)
    rindex = findfirst(
        x -> x == Cint(_info(model, index).row - 1), model.conflict.rowind
    )
    if rindex === nothing
        return MOI.NOT_IN_CONFLICT
    elseif model.conflict.rowbdstat[rindex] == CPX_CONFLICT_MEMBER
        return MOI.IN_CONFLICT
    else
        return MOI.MAYBE_IN_CONFLICT
    end
end

function MOI.supports(
    ::Optimizer,
    ::MOI.ConstraintConflictStatus,
    ::Type{MOI.ConstraintIndex{<:MOI.SingleVariable, <:_SCALAR_SETS}},
)
    return true
end

function MOI.supports(
    ::Optimizer,
    ::MOI.ConstraintConflictStatus,
    ::Type{MOI.ConstraintIndex{
        <:MOI.ScalarAffineFunction,
        <:Union{MOI.LessThan, MOI.GreaterThan, MOI.EqualTo}
    }},
)
    return true
end


@testset "test internal functions" begin
        @testset "test for pandapower to powermodels format convertion" begin
                pm = _PdM.load_pm_from_json(case_pm)

                @test length(pm["bus"]) == 6
                @test length(pm["gen"]) == 3
                @test length(pm["branch"]) == 7
                @test length(pm["load"]) == 3

                model = _PdM.get_model(pm["pm_model"])
                @test string(model) == "PowerModels.DCPPowerModel"

                solver = _PdM.get_solver(pm)

                @test string(solver.optimizer_constructor) == "Ipopt.Optimizer"
        end

        @testset "test for pandapower parameters" begin
                pm = _PdM.load_pm_from_json(case_vstab)
                params = _PdM.extract_params!(pm)

                @test haskey(params, :setpoint_v)
                @test length(params[:setpoint_v]) >= 9

        end

        @testset "test for current limit" begin
                pm = _PdM.load_pm_from_json(case_opf_cl)
                cl = _PdM.check_current_limit!(pm)
                @test cl == length(pm["branch"])
                @test cl == 7

                pm = _PdM.load_pm_from_json(case_opf_ac)
                @test pm["ac"] == true
                cl = _PdM.check_current_limit!(pm)
                @test cl == 0

        end

end


export LazyΩ
using Random
using ..IDS, ..Util, ..Tagging, ..RNG, ..Space
import ..Basis: replacetags
import ..Var

"Lazily constructs randp, values as they are needed"
struct LazyΩ{TAGS <: Tags, T} <: AbstractΩ
  data::T
  tags::TAGS
end

# AbstractDict interface
Base.keys(ω::LazyΩ) = Base.keys(ω.data)
Base.values(ω::LazyΩ) = Base.values(ω.data)
Base.pairs(ω::LazyΩ) = Base.pairs(ω.data)
Base.getindex(ω::LazyΩ, id) = ω.data[id]

# Move to specific omega types
Base.merge!(ω::LazyΩ, ω_) = error("Unimplemented")
Basis.like(ω::LazyΩ{Tags, T}, kv::Pair) where {Tags, T} =
  LazyΩ{Tags, T}(T(kv), ω.tags)

## Constructors
const EmptyTags = Tags{(),Tuple{}}
LazyΩ{EmptyTags, T}() where {T} = LazyΩ(T(), Tags())
LazyΩ{EmptyTags, T}(data) where T = LazyΩ(T(data), Tags())

"Construct `LazyΩ` from `rng` -- `ω.data` will be generated from `rng`"
LazyΩ{T}(rng::AbstractRNG) where T = tagrng(LazyΩ{T}(), rng)

## Ids
Basis.idtype(::Type{Dict{T, V}}) where {T, V} = T
Basis.idtype(::Dict{T, V}) where {T, V} = T
Basis.idtype(ω::LazyΩ{TAGS, Dict{T, V}}) where {TAGS, T, V} = T
ids(ω::LazyΩ) = keys(ω.data)

## Tags
replacetags(ω::LazyΩ, tags) = LazyΩ(ω.data, tags)
traits(::Type{LazyΩ{TAGS, T}}) where {TAGS, T} = traits(TAGS)

Tagging.mergetag(ω::LazyΩ, tag) = LazyΩ(ω.data, merge(ω.tags, tag))

Tagging.tags(ω::LazyΩ) = ω.tags

Base.setindex!(ω::LazyΩ, value, id) =
  ω.data[id] = value

function Var.recurse(primrv::Var.PrimRandVar, ω::LazyΩ)
  result = get(ω.data, primrv, 0)
  if result === 0
    ω.data[primrv] = rand(rng(ω), primrv.class)
  else
    result
  end::eltype(primrv.class)
end

## Updating interface
import ..Util:update
export update, update!
# export .Util:update
update!(ω::LazyΩ, k, v) = (ω[k] = v; ω)
Util.update(ω::LazyΩ, k, v) = (ω_ = deepcopy(ω); ω_[k] = v; ω_)

## Display
function Base.show(io::IO, m::MIME"text/plain", ω::LazyΩ)
  println(io, typeof(ω))
  println("tags:")
  show(io, m, ω.tags)

  println("\ndata:")
  show(io, m, ω.data)
end


export  Problem,
            result, step_cost, goal_test, actions, heuristic, state_value, successor_states,
        Node,
            solution, failure, isless,
        SearchAlgorithm,
            BreadthFirstSearch,
                execute,
            UniformCostSearch,
            DepthLimitedSearch,
            IterativeDeepeningSearch,
            RecursiveBestFirstSearch,
            GraphSearch,
                GraphSearchDepth,
                GraphSearchBreadth,
                GraphSearchUniformCost,
                GraphSearchBestFirst,
                GraphSearchAStar,
            HillClimbingSearch,
            SimulatedAnnealingSearch,
            GeneticAlgorithmSearch,
                mutate, reproduce, random_state,
        AIMASearchFIFO,
        AIMASearchLIFO,
        AIMASearchPQ,
        AIMASearchSet

using Compat

using DataStructures

import Base: ==, isless, in, eltype, length, empty!

@compat abstract type Problem end

result(problem::Problem, state::State, action::Action) = error(E_ABSTRACT)
step_cost(problem::Problem, state::State, action::Action) = error(E_ABSTRACT)
goal_test(problem::Problem, state::State) = error(E_ABSTRACT)
actions(problem::Problem,state::State) = error(E_ABSTRACT)
heuristic(problem::Problem, state::State) = error(E_ABSTRACT)
state_value(problem::Problem, state::State) = error(E_ABSTRACT)
successor_states(problem::Problem, state::State) = error(E_ABSTRACT)

search(problem::Problem) = execute(problem.search_algorithm, problem)

mutable struct NodeT{S<:State, T}
    state::S
    parent::Union{NodeT{S, T}, Void}
    action::Action
    path_cost::Number
    f::Number
end

const Node{S <: State} = NodeT{S, :none}

NodeT(state::S, s::Symbol=:none,
      action::Action=Action_NoOp, path_cost::Number=0, f::Number=0) where {S<: State} =
    NodeT{S, s}(state, nothing, action, path_cost, f)

NodeT(state::S, parent::NodeT{S,T},
    action::Action=Action_NoOp,
    path_cost::Number=zero(parent.path_cost),
    f::Number=zero(parent.f)) where {S <:State, T} =
    NodeT{S, T}(state, parent, action, path_cost, f)

isless(n1::NodeT, n2::NodeT) = isless(n1.f, n2.f)

make_node{S<:State}(state::S, s::Symbol) = NodeT(state, s)
make_node{S<:State}(state::S) = make_node(state, :none)

function child_node{S <:State}(problem, parent::NodeT{S, :none}, action)
    state = result(problem, parent.state, action)
    path_cost = parent.path_cost + step_cost(problem, parent.state, action)
    return NodeT(state, parent, action, path_cost, path_cost)
end

function child_node{S <:State}(problem, parent::NodeT{S, :greedy}, action)
    state = result(problem, parent.state, action)
    path_cost = parent.path_cost + step_cost(problem, parent.state, action)
    return NodeT(state, parent, action, path_cost, heuristic(problem, state))
end

function child_node{S <:State}(problem, parent::NodeT{S, :astar}, action)
    state = result(problem, parent.state, action)
    path_cost = parent.path_cost + step_cost(problem, parent.state, action)
    return NodeT(state, parent, action, path_cost, path_cost + heuristic(problem, state))
end

"""
The default implementation of *solution* is returning the sequence of nodes.
This may not be ideal for many implementations. They may provide a more
elaborate reporting as well.
"""
function solution{S<:State, T}(node::NodeT{S, T})
    nodes = Vector{NodeT{S, T}}()
    while true
        unshift!(nodes, node)
        node.parent === nothing && break
        node = node.parent
    end
    return nodes
end

"""
The default implementation of *failure* is throwing an error
"""
failure(node::NodeT) = error(E_NODE_NOT_REACHABLE)

@compat abstract type SearchAlgorithm end

const AIMASearchSet{S<:State} = Set{S}

mutable struct AIMASearchQueue{S<:State, T}
     s::Vector{Node{S}}
     AIMASearchQueue{S, T}() where {S<:State, T} = new(Vector{Node{S}}())
end
eltype(q::AIMASearchQueue) = eltype(q.s)
empty(q::AIMASearchQueue) = empty!(q.s)

const AIMASearchFIFO{S<:State} = AIMASearchQueue{S, :fifo}
const AIMASearchLIFO{S<:State} = AIMASearchQueue{S, :lifo}

isempty(q::AIMASearchQueue) = isempty(q.s)
insert{S<:State, T}(q::AIMASearchQueue{S, T}, d::Node{S}) = push!(q.s, d)
length(q::AIMASearchQueue) = length(q.s)

pop(q::AIMASearchFIFO) = shift!(q.s)
pop(q::AIMASearchLIFO) = pop!(q.s)

function insert_or_replace{S <: State, T}(f::Function,
    q::AIMASearchQueue{S, T}, d::Node{S})
    state = start(q.s)
    while !done(q.s, state)
        ps = state
        (i, state) = next(q.s, state)
        i.state == d.state && return f(i) ? splice!(q.s, ps, [d]) : nothing
    end
    return insert(q.s, d)
end

in{S <: State, T}(state::S, q::AIMASearchQueue{S, T}) = any(x -> x.state == state, q.s)

"""
*BreadthFirstSearch* An uninformed graph search technique to reach goal.

Cost of search is completely ignored.

*frontier* is a FIFO
*explored* is a Set

pg. 82 Fig. 3.11 AIMA 3ed
"""
mutable struct BreadthFirstSearch{FIFO, SET, S<:State} <: SearchAlgorithm
    frontier::FIFO
    explored::SET
    BreadthFirstSearch{FIFO, SET, S}() where{FIFO, SET, S<:State} =
        new(has_trait_queue(FIFO{S}()), has_trait_set(SET{S}()))
end

BreadthFirstSearch{S<:State}(::S) = BreadthFirstSearch{AIMASearchFIFO, AIMASearchSet, S}()

function execute(search::BreadthFirstSearch, problem::Problem)
    empty(search.frontier)
    empty(search.explored)
    node = NodeT(problem.initial_state, :none)
    goal_test(problem, node.state) && return solution(node)
    insert(search.frontier, node)

    while(true)
        isempty(search.frontier) && return failure(node)
        node = pop(search.frontier)
        append(search.explored, node.state)

        for action in actions(problem, node.state)
            child = child_node(problem, node, action)
            !(child.state in search.explored) && !(child.state in search.frontier) &&
            goal_test(problem, child.state) && return solution(child)
            insert(search.frontier, child)
        end
    end
end

const AIMASearchPQ_Base{S<:State, NT} = PriorityQueue{S, NodeT{S, NT}}

pop(pq::AIMASearchPQ_Base) = ((key,val) = peek(pq); dequeue!(pq); val)
peekv(pq::AIMASearchPQ_Base) = ((key,val) = peek(pq); val)
empty(pq::AIMASearchPQ_Base) = while(!isempty(pq));dequeue!(pq);end
insert{S <: State, NT}(pq::AIMASearchPQ_Base{S, NT}, node::NodeT{S, NT}) =
    enqueue!(pq, node.state, node)
replace{S <: State, NT}(pq::AIMASearchPQ_Base{S, NT}, node::NodeT{S, NT}) =
    (pq[node.state] = node)
in{S <: State, NT}(state::S, pq::AIMASearchPQ_Base{S, NT}) = haskey(pq, state)

function insert_or_replace{S <: State, NT}(f::Function,
    pq::AIMASearchPQ_Base{S, NT}, node::NodeT{S, NT})
    !haskey(pq, node.state) && return enqueue!(pq, node.state, node)
    f(pq[node.state]) && return (pq[node.state] = node)
end

const AIMASearchPQ{S <: State}   = AIMASearchPQ_Base{S, :none}
const AIMASearchPQ_G{S <: State} = AIMASearchPQ_Base{S, :greedy}
const AIMASearchPQ_A{S <: State} = AIMASearchPQ_Base{S, :astar}

"""
*UniformCostSearch* An uninformed graph search technique to reach goal.

Cost of search is used in taking decisions.

*frontier* is a priority queue
*explored* is a Set

pg. 82 Fig. 3.11 AIMA 3ed
"""
mutable struct UniformCostSearch{PQ, SET, S<:State} <: SearchAlgorithm
    frontier::PQ
    explored::SET
    UniformCostSearch{PQ, SET, S}() where {PQ, SET, S<:State} =
        has_trait_queue(PQ{S}, Node{S}) &&
        method_exists(replace, (PQ{S}, Node{S})) &&
        method_exists(in, (S, PQ{S})) &&
        method_exists(get, (PQ{S}, S, Node{S})) &&
        new(PQ{S}(), has_trait_set(SET{S}()))
end

UniformCostSearch{S<:State}(::S) = UniformCostSearch{AIMASearchPQ, AIMASearchSet, S}()

function execute(search::UniformCostSearch, problem) #returns a solution, or failure
    empty(search.frontier)
    empty(search.explored)
    node = make_node(problem.initial_state)
    insert(search.frontier, node)

    while(true)
        isempty(search.frontier) && return failure(node)
        node = pop(search.frontier)
        goal_test(problem, node.state) && return solution(node)
        append(search.explored, node.state)

        for action in actions(problem, node.state)
            child = child_node(problem, node, action)
            if !(child.state in search.explored) && !(child.state in search.frontier)
                insert(search.frontier, child)
            elseif (child.state in search.frontier) &&
                    (child.path_cost < get(search.frontier, child.state,
                                           make_node(problem.initial_state)).path_cost)
                replace(search.frontier, child)
            end
        end
    end
end

"""
*DepthLimitedSearch* An uninformed graph search technique to reach goal.

This is a depth first search limited by the depth of the search.

*limit* depth to which search should be conducted.

pg. 88 Fig. 3.17 AIMA 3ed
"""
struct DepthLimitedSearch <: SearchAlgorithm
    limit::Int
end

execute(search::DepthLimitedSearch, problem) =
    recursive_DLS(make_node(problem.initial_state), problem, search.limit)

function recursive_DLS(node, problem, limit)
    goal_test(problem, node.state) && return solution(node)
    limit == 0 && return :cutoff
    cutoff_occured = false
    for action in actions(problem, node.state)
        child = child_node(problem, node, action)
        result = recursive_DLS(child, problem, limit-1)
        result != :failure && result != :cutoff && return result
        cutoff_occured = (result == :cutoff)
    end
    return cutoff_occured ? :cutoff : :failure
end

"""
*IterativeDeepeningSearch* An uninformed graph search technique to reach goal.

Iteratively *DepthLimitedSearch* is used by incrementing the depth limit till a point comes
when there are no `:cutoff` return value.

pg. 89 Fig. 3.18 AIMA 3ed
"""
struct IterativeDeepeningSearch <: SearchAlgorithm
end

function execute(search::IterativeDeepeningSearch, problem)
    depth = 0
    while true
        result = execute(DepthLimitedSearch(depth), problem)
        result != :cutoff && return result
        depth += 1
    end
end

mutable struct GraphSearch{SQ, SET, S<:State, NT} <: SearchAlgorithm
    frontier::SQ
    explored::SET
    GraphSearch{SQ, SET, S, NT}() where {SQ, SET, S<:State, NT} =
        has_trait_queue(SQ{S}, NodeT{S, NT}) &&
        method_exists(insert_or_replace, (Function, SQ{S}, NodeT{S, NT})) &&
        new(SQ{S}(), has_trait_set(SET{S}()))
end

const GraphSearchBreadth{S <: State} = GraphSearch{AIMASearchFIFO, AIMASearchSet, S, :none}
const GraphSearchDepth{S <: State}   = GraphSearch{AIMASearchLIFO, AIMASearchSet, S, :none}
const GraphSearchUniformCost{S <: State} =
    GraphSearch{AIMASearchPQ, AIMASearchSet, S, :none}

const GraphSearchBestFirst{S <: State} =
    GraphSearch{AIMASearchPQ_G, AIMASearchSet, S, :greedy}
const GraphSearchAStar{S <: State} =
        GraphSearch{AIMASearchPQ_A, AIMASearchSet, S, :astar}

GraphSearchBreadth{S<:State}(::S)      = GraphSearchBreadth{S}()
GraphSearchDepth{S<:State}(::S)        = GraphSearchDepth{S}()
GraphSearchUniformCost{S<:State}(::S)  = GraphSearchUniformCost{S}()
GraphSearchBestFirst{S<:State}(::S)    = GraphSearchBestFirst{S}()
GraphSearchAStar{S<:State}(::S)        = GraphSearchAStar{S}()

function execute{SQ, SET, S<:State, T}(search::GraphSearch{SQ, SET, S, T}, problem)
    empty(search.frontier)
    empty(search.explored)
    node = NodeT(problem.initial_state, T)
    insert(search.frontier, node)

    while(true)
        isempty(search.frontier) && return failure(node)
        node = pop(search.frontier)
        goal_test(problem, node.state) && return solution(node)
        append(search.explored, node.state)
        for action in actions(problem, node.state)
            child = child_node(problem, node, action)
            if !(child.state in search.explored)
                insert_or_replace(x-> child < x, search.frontier, child)
            end
        end
    end
end

const AIMASearchSequence{S<:State} = Vector{Node{S}}
sort(sq::AIMASearchSequence) = sort!(sq)

struct RecursiveBestFirstSearch{SQ, S<:State} <: SearchAlgorithm
    SQ_t::Type
    RecursiveBestFirstSearch{SQ, S}() where {SQ, S<:State} =
        has_trait_sequence(SQ{S}, Node{S}) && new(Type(SQ{S}))
end

RecursiveBestFirstSearch{S<:State}(::S) = RecursiveBestFirstSearch{AIMASearchSequence, S}()

function execute(search::RecursiveBestFirstSearch, problem)
    node = make_node(problem.initial_state)
    status, val = RBFS(search, problem, node, Infinite(node.f))
    return status == :failure ? :failure : val
end

function RBFS(search, problem, node, f_limit)
    goal_test(problem, node.state) && return :success, solution(node)
    successors = search.SQ_t()

    for action in actions(problem, node.state)
        child = child_node(problem, node, action)
        insert(successors, child)
    end
    isempty(successors) && return :failure, Infinite(node.f)

    for s in successors
        s.f = max(s.path_cost + heuristic(problem, s.state), node.f)
    end

    while(true)
        sort(successors)
        best = successors[1]
        best.f > f_limit && return :failure, best.f
        alternative = length(successors) > 1 ? successors[2].f : Infinite(node.f)
        result = RBFS(search, problem, best, min(f_limit, alternative))
        result[1] == :success && return result
        best.f = result[2]
    end
end

struct StateNode{S<:State}
    state::S
    value::Number
end

StateNode(state::S) where S <: State = StateNode(state, 0)

isless(n1::StateNode, n2::StateNode)  = isless(n1.value, n2.value)
==(n1::StateNode, n2::StateNode) = n1.value == n2.value

successor_node(problem, state::State) = StateNode(state, state_value(problem, state))

const AIMASearchStateSequence{S<:State} = Vector{StateNode{S}}
sort(sq::AIMASearchStateSequence) = sort!(sq, rev=true)

struct HillClimbingSearch{SQ, S<:State} <: SearchAlgorithm
    SQ_t::Type
    plateau::Int
    HillClimbingSearch{SQ, S}(plateau::Int=typemax(Int)) where {SQ, S<:State} =
        has_trait_sequence(SQ{S}, StateNode{S}) && new(Type(SQ{S}), plateau)
end

HillClimbingSearch{S<:State}(::S, plateau::Int=typemax(Int)) =
    HillClimbingSearch{AIMASearchStateSequence, S}(plateau)

function execute(search::HillClimbingSearch, problem)
    node = StateNode(problem.initial_state, state_value(problem, problem.initial_state))
    cnt = 0
    while true
        successors = search.SQ_t()
        for state in successor_states(problem, node.state)
            successor = successor_node(problem, state)
            insert(successors, successor)
        end
        sort(successors)
        neighbor = successors[1]
        neighbor < node && return node.state
        if neighbor == node
            cnt >= search.plateau && return node.state
            cnt += 1
        elseif neighbor > node
            cnt = 0
        end
        node = neighbor
    end
end

struct SimulatedAnnealingSearch{SQ, S<:State} <: SearchAlgorithm
    SQ_t::Type
    schedule::Function
    SimulatedAnnealingSearch{SQ, S}(schedule::Function) where {SQ, S<:State} =
        has_trait_sequence(SQ{S}, Node{S}) && new(Type(SQ{S}), schedule)
end

SimulatedAnnealingSearch{S<:State}(::S,
    schedule::Function=(x-> x > 10000.0 ? 0.0 : 1.0/x)) =
    SimulatedAnnealingSearch{AIMASearchStateSequence, S}(schedule)

function execute(search::SimulatedAnnealingSearch, problem)
    node = StateNode(problem.initial_state, state_value(problem, problem.initial_state))
    schedule = search.schedule
    t = 1.0
    while true
        T = schedule(t)
        T <= 0.0000001 && return node.state
        successors = search.SQ_t()
        for state in successor_states(problem, node.state)
            successor = successor_node(problem, state)
            insert(successors, successor)
        end
        next = rand(successors)
        de = next.value - node.value
        p = de > 0 ? 1.0 : exp(de/T)
        if rand(Float64) < p
            node = next
        end
        t += 1.0
    end
end

mutable struct Population{SQN, SQF}
    nodes::SQN
    cweights::SQF
    Population{SQN, SQF}() where {SQN, SQF} = new(SQN(), SQF())
end

length(p::Population) = length(p.nodes)

function add(p::Population{SQN, SQF}, s::S, v::Number) where {SQN, SQF, S <:State }
    append(p.nodes, StateNode(s, v))
    append(p.cweights, isempty(p.cweights) ? Float64(v) : Float64(v) + p.cweights[end])
end

function random_selection(p::Population)
    wt = rand()*p.cweights[end]
    i = findfirst(x -> x > wt, p.cweights)
    return p.nodes[i].state
end

function initialize(p::Population{SQN, SQF},
    st::S, fitnessFN::Function, size::Int) where {SQN, SQF, S <:State}
    p.nodes = SQN()
    p.cweights = SQF()
    for i = 1:size
        s = random_state(st)
        add(p, s, fitnessFN(s))
    end
    return p
end

best_individual(p::Population) = findmax(p.nodes)

mutate(s::S) where {S <: State} = error(E_ABSTRACT)
reproduce(s::S) where {S <: State} = error(E_ABSTRACT)
random_state(s::S) where {S <: State} = error(E_ABSTRACT)

struct GeneticAlgorithmSearch{SQ, S<:State} <: SearchAlgorithm
    SQ_t::Type
    fitnessFN::Function
    niter::Int
    pop_size::Int
    p_mutate::Float64
    GeneticAlgorithmSearch{SQ, S}(fitnessFN::Function,
        niter::Int=1000, pop_size::Int=1000, p_mutate::Float64=0.1) where {SQ, S<:State} =
        has_trait_sequence(SQ{S}, S) &&
            new(Type(SQ), fitnessFN, niter, pop_size, p_mutate)
end

GeneticAlgorithmSearch{S<:State}(::S, fitnessFN::Function) =
    GeneticAlgorithmSearch{AIMASequence, S}(fitnessFN)

function execute(search::GeneticAlgorithmSearch, problem)
    SQN = search.SQ_t{StateNode}
    SQF = search.SQ_t{Float64}
    population = Population{SQN, SQF}()
    initialize(population, problem.initial_state, search.fitnessFN, search.pop_size)

    citer = 0
    while true
        new_population = Population{SQN, SQF}()
        for i = 1:length(population)
            x = random_selection(population)
            y = random_selection(population)
            child = reproduce(x , y)
            if (rand() < search.p_mutate)
                child = mutate(child)
            end
            add(new_population, child, search.fitnessFN(child))
        end
        population = new_population
        citer += 1
        result, idx = best_individual(population)
        if (citer > search.niter || goal_test(problem, result.state))
            return result.state
        end
    end
end
struct AND_ORGraphSearch{SQ,S<:State} <: SearchAlgorithm
    SQ_t::Type
    #AND_Search::SearchAlgorithm
    AND_ORGraphSearch{SQ,S}() where {SQ,S<:State} =

end
function OR_Search(::