"""
    IdealPDController

This piece of software relies on a truth sensor and an ideal actuator to align
the spacecraft with the ICRF.

"""
function IdealPDController()

    function step(t, constants, state, inputs, outputs_bus,
                  inputs_bus) # for output only

        # Extract what we care about.
        ω_BI_B = outputs_bus["truth_sensor"].ω_BI_B
        q_BI   = outputs_bus["truth_sensor"].q_BI
        q_TI   = constants # Target quaternion

        # Run the actual algorithm that will fly.
        (f_B, τ_B) = pd_controller(q_TI, q_BI, ω_BI_B)

        # Write out our command for the ideal actuator.
        inputs_bus["ideal_actuator"] = IdealActuatorCommand(f_B, τ_B)

        return (state,      # Updated state (none)
                nothing,    # Outputs
                inputs_bus, # Updated inputs bus (this is not necessary)
                true)       # Active

    end

    DynamicalModel("ideal_pd_controller",
                   init = step,
                   update = step,
                   timing = ModelTiming(0.05),
                   constants = [0.; 0.; 0.; 1.]) # target quaternion

end

# This represents the "real" algorithm under development.
function pd_controller(q_TI, q_BI, ω_BI_B)
    theta, r_B = q2aa(qdiff(q_TI, q_BI))
    τ_B        = 0.2 * theta * r_B - 2 * ω_BI_B
    f_B        = [0.; 0.; 0.]
    return (f_B, τ_B)
end

"""
ReactionWheelController

This piece of software relies on a truth sensor and an ideal actuator to align
the spacecraft with the ICRF.

"""
function ReactionWheelController()

    function step(t, q_TI, state, inputs, outputs_bus,
                  inputs_bus) # for output only

        # Extract what we care about.
        q_BI   = outputs_bus["star_tracker"]
        ω_BI_B = outputs_bus["gyro"]
        ω_rw   = [outputs_bus["rw1"]; outputs_bus["rw2"]; outputs_bus["rw3"]]

        # Get the desired net torque on the body.
        (f_B, τ_B) = pd_controller(q_TI, q_BI, ω_BI_B)

        # Feed the dynamics forward to get the resulting torque necessary from the RW motors.
        I_b = 5.
        I_r = 0.25
        τ_br_B_cmd = (  τ_B
                      # + ω_BI_B × (I_b * ω_BI_B) # Zero (MOI are all the same)
                      + ω_BI_B × (I_r * ω_rw))

        # H_B = (  I_b * ω_BI_B
        #        + 0.25*[1.; 1/sqrt(2.); 1/sqrt(2.)] .* ([ω_rw[1]; 0.; 0.] + ω_BI_B)
        #        + 0.25*[1/sqrt(2.); 1.; 1/sqrt(2.)] .* ([0.; ω_rw[2]; 0.] + ω_BI_B)
        #        + 0.25*[1/sqrt(2.); 1/sqrt(2.); 1.] .* ([0.; 0.; ω_rw[3]] + ω_BI_B))
        # H_I = qrot(qinv(q_BI), H_B)
        # println("Angular momentum in inertial frame: ", H_I)

        # Write out our command for the ideal actuator.
        inputs_bus["rw1"] = τ_br_B_cmd[1]
        inputs_bus["rw2"] = τ_br_B_cmd[2]
        inputs_bus["rw3"] = τ_br_B_cmd[3]

        return (state,      # Updated state (none)
                nothing,    # Outputs
                inputs_bus, # Updated inputs bus (this is not necessary)
                true)       # Active

    end

    DynamicalModel("rw_controller",
                   init = step,
                   update = step,
                   timing = ModelTiming(0.05),
                   constants = [0.; 0.; 0.; 1.]) # target quaternion

end


# Lightweight Representative Agent Version of HetDSGEGovDebt
mutable struct RepDSGEGovDebt{T} <: AbstractRepModel{T}
    parameters::ParameterVector{T}                # vector of all time-invariant model parameters
    steady_state::ParameterVector{T}              # model steady-state values

    grids::OrderedDict{Symbol,Union{Grid, Array}} # Human-readable names for all the model
    keys::OrderedDict{Symbol,Int}                 # parameters and steady-states

    state_variables::Vector{Symbol}               # Vector of symbols of the state variables
    jump_variables::Vector{Symbol}                # Vector of symbols of the jump variables

    # Vector of unnormalized ranges of indices
    endogenous_states::OrderedDict{Symbol,Int}
    exogenous_shocks::OrderedDict{Symbol,Int}
    expected_shocks::OrderedDict{Symbol,Int}
    equilibrium_conditions::OrderedDict{Symbol,Int}
    endogenous_states_augmented::OrderedDict{Symbol, Int}
    observables::OrderedDict{Symbol,Int}

    spec::String                                  # Model specification number (eg "m990")
    subspec::String                               # Model subspecification (eg "ss0")
    settings::Dict{Symbol,Setting}                # Settings/flags for computation
    testing::Bool                                 # Whether we are in testing mode or not
    observable_mappings::OrderedDict{Symbol, Observable}
end

description(m::RepDSGEGovDebt) = "RepDSGEGovDebt, $(m.subspec)"

function RepDSGEGovDebt(het::HetDSGEGovDebt;
                        custom_settings::Dict{Symbol, Setting} = Dict{Symbol, Setting}())

    @assert !isnan(het[:βstar].value) "Must have already called steadystate! on `het` before instantiating a RepDSGEGovDebt model"

    # Model-specific specifications
    spec               = "rep_dsge"
    subspec            = deepcopy(het.subspec)
    settings           = deepcopy(het.settings)

    # initialize empty model
    m = RepDSGEGovDebt{Float64}(
            # model parameters and steady state values
            Vector{AbstractParameter{Float64}}(),
            Vector{Float64}(),

            # grids and keys
            OrderedDict{Symbol,Union{Grid, Array}}(), OrderedDict{Symbol,Int}(),

            # state_inds, jump_inds
            Vector{Symbol}(), Vector{Symbol}(),

            # model indices
            OrderedDict{Symbol,Int}(), OrderedDict{Symbol,Int}(),
            OrderedDict{Symbol,Int}(), OrderedDict{Symbol,Int}(),
            OrderedDict{Symbol,Int}(), OrderedDict{Symbol,Int}(),

            spec,
            subspec,
            settings,
            deepcopy(het.testing),
            deepcopy(het.observable_mappings))

    # Set settings
    model_settings!(m, het)
    for custom_setting in values(custom_settings)
        m <= custom_setting
    end

    # Initialize model indices
    init_model_indices!(m)

    # Initialize parameters
    init_parameters!(m, het)

    # Initialize grids
    init_grids!(m, het)

    # initialize subspec from het
    init_subspec!(m, het)

    # Solve for the steady state
    steadystate!(m, het, same_beta = get_setting(m, :use_same_beta_as_hank))

    return m
end

function model_settings!(m::RepDSGEGovDebt, het::HetDSGEGovDebt)
    for (key, setting) in deepcopy(het.settings)
        m <= setting
    end

    # Total grid x*s
    # Not sure what to do with the below line. Leave commented for now
    # m <= Setting(:n, (get_setting(m, :nx1) +get_setting(m, :nx2)),
                 # "Total grid size, multiplying across grid dimensions.")
    # The rest of these lines were taken directly from the rank_irfs.jl file
    m <= Setting(:n_jumps, 15, "num scalar jumps")
    m <= Setting(:n_backward_looking_states, 14, "num scalar states")
    m <= Setting(:n_model_states, get_setting(m, :n_backward_looking_states) +
                 get_setting(m, :n_jumps),
                 "Number of 'states' in the state space model. Because backward and forward
                 looking variables need to be explicitly tracked for the Klein solution
                 method, we have n_states and n_jumps")
    m <= Setting(:n_model_states_augmented, get_setting(m, :n_model_states) +
                 length([:c_t1]),
                 "The number of 'states' in the augmented state space model.")

    m <= Setting(:use_same_beta_as_hank, false)
end

function init_model_indices!(m::RepDSGEGovDebt)

    # Endogenous states
    states = collect([:k′_t, :R′_t1, :i′_t1, :y′_t1, :w′_t1, :I′_t1, :bg′_t,
                      :b′_t, :g′_t, :z′_t, :μ′_t, :λ_w′_t, :λ_f′_t, :rm′_t])

    jumps = collect([:R′_t, :i′_t, :t′_t, :w′_t, :L′_t, :π′_t, :π_w′_t, :margutil′_t, :y′_t, :I′_t,
                     :mc′_t, :Q′_t, :capreturn′_t, :l′_t, :tg′_t])

    endogenous_states = collect(vcat(states, jumps))

    # Exogenous shocks
    exogenous_shocks = collect([:b_sh,:g_sh,:z_sh,:μ_sh,:λ_w_sh,:λ_f_sh,:rm_sh])

    # Equilibrium conditions
    equilibrium_conditions = collect([:eq_euler, :eq_market_clearing, :eq_lambda, :eq_transfers,
                                      :eq_investment, :eq_tobin_q, :eq_capital_accumulation,
                                      :eq_wage_phillips, :eq_price_phillips, :eq_marginal_cost,
                                      :eq_gdp, :eq_optimal_kl, :eq_taylor, :eq_fisher,
                                      :eq_nominal_wage_inflation, :eq_fiscal_rule,
                                      :eq_g_budget_constraint, :LR, :LI, :LY,
                                      :LW, :LX, :eq_b, :eq_g, :eq_z, :eq_μ, :eq_λ_w,
                                      :eq_λ_f, :eq_rm]) #, :eq_consumption])

    # Additional states added after solving model
    # Lagged states and observables measurement error
    endogenous_states_augmented = [:c_t1]

    observables = keys(m.observable_mappings)
    endo   = m.endogenous_states
    eqcond = equilibrium_conditions

    for (i,k) in enumerate(endogenous_states);m.endogenous_states[k] = i end
    for (i,k) in enumerate(endogenous_states_augmented); m.endogenous_states_augmented[k] = i+length(endogenous_states) end
    for (i,k) in enumerate(exogenous_shocks); m.exogenous_shocks[k]  = i end
    for (i,k) in enumerate(observables);      m.observables[k]       = i end
    for (i,k) in enumerate(equilibrium_conditions);      m.equilibrium_conditions[k]      = i end
end

function init_parameters!(m::RepDSGEGovDebt, het::HetDSGEGovDebt)
    for param in deepcopy(het.parameters)
        m <= param
    end
end

function init_grids!(m::RepDSGEGovDebt, het::HetDSGEGovDebt)
    m.grids = deepcopy(het.grids)
end

function steadystate!(m::RepDSGEGovDebt)
    ns = get_setting(m, :ns)

    #m <= SteadyStateParameter(:βstar, het[:βstar].value, description = "Steady-state discount factor", tex_label = "\\beta_*")
    #m[:r] = exp(m[:γ])/m[:βstar]
    m <= SteadyStateParameter(:Rkstar, m[:r] + m[:δ],
                              description = "Rental rate on capital", tex_label = "Rk_*")
    m <= SteadyStateParameter(:ωstar,
                              (m[:α]^(m[:α]/(1-m[:α])))*(1-m[:α])*m[:Rkstar]^(-m[:α]/(1-m[:α])),
                              description = "Real wage", tex_label = "\\omega_*")
    m <= SteadyStateParameter(:klstar, (m[:α]/(1-m[:α]))*(m[:ωstar]/m[:Rkstar])*exp(m[:γ]),
                              description = "Capital/Labor ratio", tex_label = "kl_*")
    m <= SteadyStateParameter(:kstar, m[:klstar]*m[:H], description = "Capital",
                              tex_label = "k_*")
    m <= SteadyStateParameter(:ystar, (exp(-m[:α]*m[:γ])*(m[:kstar]^m[:α])*m[:H]^(1-m[:α])),
                              description = "GDP", tex_label = "y_*")
    m <= SteadyStateParameter(:xstar, (1-(1-m[:δ])*exp(-m[:γ]))*m[:kstar],
                              description = "Investment", tex_label = "x_*")
    m <= SteadyStateParameter(:cstar, m[:ystar]/m[:g] - m[:xstar],
                              description = "Steady-state consumption", tex_label = "c_*")
end

function steadystate!(m::RepDSGEGovDebt, het::HetDSGEGovDebt;
                      same_beta::Bool = false)
    ns = get_setting(m, :ns)

    if same_beta
        m <= SteadyStateParameter(:βstar, het[:βstar].value, description = "Steady-state discount factor", tex_label = "\\beta_*")
        m[:r] = exp(m[:γ])/m[:βstar]
        m <= SteadyStateParameter(:Rkstar, m[:r] + m[:δ],
                                  description = "Rental rate on capital", tex_label = "Rk_*")
        m <= SteadyStateParameter(:ωstar,
                                  (m[:α]^(m[:α]/(1-m[:α])))*(1-m[:α])*m[:Rkstar]^(-m[:α]/(1-m[:α])),
                                  description = "Real wage", tex_label = "\\omega_*")
        m <= SteadyStateParameter(:klstar, (m[:α]/(1-m[:α]))*(m[:ωstar]/m[:Rkstar])*exp(m[:γ]),
                                  description = "Capital/Labor ratio", tex_label = "kl_*")
        m <= SteadyStateParameter(:kstar, m[:klstar]*m[:H], description = "Capital",
                                  tex_label = "k_*")
        m <= SteadyStateParameter(:ystar, (exp(-m[:α]*m[:γ])*(m[:kstar]^m[:α])*m[:H]^(1-m[:α])),
                                  description = "GDP", tex_label = "y_*")
        m <= SteadyStateParameter(:xstar, (1-(1-m[:δ])*exp(-m[:γ]))*m[:kstar],
                                  description = "Investment", tex_label = "x_*")
        m <= SteadyStateParameter(:cstar, m[:ystar]/m[:g] - m[:xstar],
                                  description = "Steady-state consumption", tex_label = "c_*")
    else
        m <= SteadyStateParameter(:βstar, exp(m[:γ])/(1 + m[:r]), description = "Steady-state discount factor",
                                  tex_label = "\\beta_*")
        m <= SteadyStateParameter(:Rkstar, m[:r] + m[:δ],
                                  description = "Rental rate on capital", tex_label = "Rk_*")
        m <= SteadyStateParameter(:ωstar,
                                  (m[:α]^(m[:α]/(1-m[:α])))*(1-m[:α])*m[:Rkstar]^(-m[:α]/(1-m[:α])),
                                  description = "Real wage", tex_label = "\\omega_*")
        m <= SteadyStateParameter(:klstar, (m[:α]/(1-m[:α]))*(m[:ωstar]/m[:Rkstar])*exp(m[:γ]),
                                  description = "Capital/Labor ratio", tex_label = "kl_*")
        m <= SteadyStateParameter(:kstar, m[:klstar]*m[:H], description = "Capital",
                                  tex_label = "k_*")
        m <= SteadyStateParameter(:ystar, (exp(-m[:α]*m[:γ])*(m[:kstar]^m[:α])*m[:H]^(1-m[:α])),
                                  description = "GDP", tex_label = "y_*")
        m <= SteadyStateParameter(:xstar, (1-(1-m[:δ])*exp(-m[:γ]))*m[:kstar],
                                  description = "Investment", tex_label = "x_*")
        m <= SteadyStateParameter(:cstar, (m.grids[:weights_total].*het[:μstar].value)'*(min.(1.0 ./ het[:lstar].value, repeat(m.grids[:xgrid].points, ns) .+ m[:η])),
                                  description = "Steady-state consumption", tex_label = "c_*")
    end
    m <= SteadyStateParameter(:lstar, 1/m[:cstar],
                              description = "Steady-state expected discounted
                              marginal utility of consumption", tex_label = "l_*")
    m <= SteadyStateParameter(:bg, m[:BoverY]*m[:ystar]*exp(m[:γ]), description = "Govt Debt",
                              tex_label = "bg")
    m <= SteadyStateParameter(:Tg, (exp(-m[:γ]) - 1/(1+m[:r]))*m[:bg] + (1-(1/m[:g]))*m[:ystar],
                              description = "Net lump sump taxes", tex_label = "Tg")
    m <= SteadyStateParameter(:Tstar, m[:Rkstar]*m[:kstar]*exp(-m[:γ]) - m[:xstar] - m[:Tg],
                              description = "Net transfer to households", tex_label = "T_*")

    return m
end


include("./dce_models/tofts.jl")
include("./dce_models/uptake.jl")
include("./dce_models/exchange.jl")
include("./dce_models/filtration.jl")
include("./dce_models/reference_region.jl")

function fit_model(modelname, modelopt = :default; kwargs...)
    return model_dict[modelname][modelopt](; kwargs...)
end

const model_dict = Dict{Symbol,Dict{Symbol,Function}}(
    :tofts => Dict{Symbol,Function}(
        :lls => fit_tofts_lls,
        :nls => fit_tofts_nls,
        :default => fit_tofts_nls,
    ),
    :extendedtofts => Dict{Symbol,Function}(
        :lls => fit_extendedtofts_lls,
        :nls => fit_extendedtofts_nls,
        :default => fit_extendedtofts_nls,
    ),
    :uptake => Dict{Symbol,Function}(
        :lls => fit_uptake_lls,
        :nls => fit_uptake_nls,
        :default => fit_uptake_nls,
    ),
    :exchange => Dict{Symbol,Function}(
        :lls => fit_exchange_lls,
        :nls => fit_exchange_nls,
        :default => fit_exchange_nls,
    ),
    :filtration => Dict{Symbol,Function}(
        :lls => fit_filtration_lls,
        :nls => fit_filtration_nls,
        :default => fit_filtration_nls,
    ),
    :rrm => Dict{Symbol,Function}(
        :lls => fit_rrm_lls,
        :nls => fit_rrm_nls,
        :default => fit_rrm_nls,
    ),
    :crrm => Dict{Symbol,Function}(
        :lls => fit_crrm_lls,
        :nls => fit_crrm_nls,
        :default => fit_crrm_nls,
    ),
    :errm => Dict{Symbol,Function}(:lls => fit_errm_lls, :default => fit_errm_lls),
    :cerrm => Dict{Symbol,Function}(:lls => fit_cerrm_lls, :default => fit_cerrm_lls),
    :rrift => Dict{Symbol,Function}(
        :crrm => fit_rrift_with_crrm,
        :cerrm => fit_rrift_with_cerrm,
        :default => fit_rrift_with_crrm,
    ),
)


using Random

function create_modulation(modulation_dict)
  if isnothing(modulation_dict)
    return nothing
  end
  params_dict = get(modulation_dict, "params", Dict{String,Any}())
  modulation_name = modulation_dict["function"]
  modulation_params =  NamedTuple{Tuple(Symbol.(keys(params_dict)))}(values(params_dict))
  MocosSim.make_infection_modulation( modulation_name; modulation_params...)
end

function read_params(config, rng::AbstractRNG)
  population_path = config["population_path"] # <= JSON
  population_path::AbstractString # checks if it was indeed a string
  individuals_df = load(population_path)["individuals_df"]

  progression_params = MocosSim.make_progression_params()

  infection_modulation = get(config, "infection_modulation", nothing) |> create_modulation
  mild_detection_modulation = get(config, "mild_detection_modulation", nothing) |> create_modulation
  tracing_modulation = get(config, "tracing_modulation", nothing)

  constant_kernel_param = config["transmission_probabilities"]["constant"]  |> float
  household_kernel_param = config["transmission_probabilities"]["household"] |> float

  hospital_detections = get(config, "hospital_detections", true) |> Bool
  mild_detection_prob = config["mild_detection_prob"]  |> float
  mild_detection_delay = get(config, "mild_detection_delay", 2.0) |> float

  tracing_prob = config["contact_tracing"]["probability"]  |> float
  tracing_delay = config["contact_tracing"]["detection_delay"]  |> float
  testing_time = config["contact_tracing"]["testing_time"]  |> float
  quarantine_length = get(config, "quarantine_length", 14.0) |> float

  age_coupling_kernel_param = get(config["transmission_probabilities"], "age_coupling_param", nothing)
  age_coupling_data_path = get(config["transmission_probabilities"], "age_coupling_data_path", nothing)
  @assert isnothing(age_coupling_kernel_param) == isnothing(age_coupling_data_path)
  age_coupling_thresholds, age_coupling_weights, age_coupling_use_genders =
    isnothing(age_coupling_data_path) ? (nothing, nothing, false) : load(age_coupling_data_path, "age_thresholds", "contact_mat", "uses_genders")

  screening_params = if !haskey(config, "screening")
      nothing
    else
      screen = config["screening"]
      NamedTuple{Tuple(Symbol.(keys(screen)))}(values(screen))
      MocosSim.ScreeningParams(;NamedTuple{Tuple(Symbol.(keys(screen)))}(values(screen))...)
    end

  spreading = get(config, "spreading", nothing)
  spreading_alpha = isnothing(spreading) ? nothing : spreading["alpha"]
  spreading_x0 = isnothing(spreading) ? 1 : get(spreading, "x0", 1)
  spreading_truncation = isnothing(spreading) ? Inf : get(spreading, "truncation", Inf)

  phone_tracing = get(config, "phone_tracing", nothing)
  phone_tracing_usage = isnothing(phone_tracing) ? 0.0 : phone_tracing["usage"] |> float
  phone_tracing_testing_delay = isnothing(phone_tracing) ? 1.0 : phone_tracing["detection_delay"] |> float
  phone_tracing_usage_by_household = isnothing(phone_tracing) ? false : phone_tracing["usage_by_household"] |> Bool

  british_strain_multiplier = get(config["transmission_probabilities"], "british_strain_multiplier", 1.5) |> float
  delta_strain_multiplier = get(config["transmission_probabilities"], "delta_strain_multiplier", 1.5 * 1.5) |> float

  hospital_kernel_param = get(config["transmission_probabilities"], "hospital", 0.0) |> float
  healthcare_detection_prob, healthcare_detection_delay =  if !haskey(config, "healthare_detections")
    0.8, 1.0
  else
    float(config["healthare_detections"]["probability"]), float(config["healthare_detections"]["delay"])
  end

  MocosSim.make_params(
    rng;
    individuals_df = individuals_df,
    progression_params = progression_params,

    infection_modulation = infection_modulation,
    mild_detection_modulation = mild_detection_modulation,
    forward_tracing_modulation = tracing_modulation,
    backward_tracing_modulation = tracing_modulation,

    constant_kernel_param = constant_kernel_param,
    household_kernel_param = household_kernel_param,

    hospital_detections = hospital_detections,
    mild_detection_prob = mild_detection_prob,
    mild_detection_delay = mild_detection_delay,

    backward_tracing_prob = tracing_prob,
    backward_detection_delay = tracing_delay,

    forward_tracing_prob = tracing_prob,
    forward_detection_delay = tracing_delay,

    testing_time = testing_time,
    quarantine_length = quarantine_length,

    age_coupling_param = age_coupling_kernel_param,
    age_coupling_thresholds = age_coupling_thresholds,
    age_coupling_weights = age_coupling_weights,
    age_coupling_use_genders = age_coupling_use_genders,

    screening_params = screening_params,

    spreading_alpha=spreading_alpha,
    spreading_x0=spreading_x0,
    spreading_truncation=spreading_truncation,

    phone_tracing_usage = phone_tracing_usage,
    phone_detection_delay = phone_tracing_testing_delay,
    phone_tracing_usage_by_household = phone_tracing_usage_by_household,

    british_strain_multiplier=british_strain_multiplier,
    delta_strain_multiplier=delta_strain_multiplier,

    hospital_kernel_param = hospital_kernel_param,
    healthcare_detection_prob = healthcare_detection_prob,
    healthcare_detection_delay = healthcare_detection_delay,
  )
end


module ALifeBenchmark

# export RandomModel
# export run_simulation
#
# export History
# export get_default_history, get_detailed_history, get_by_environment
#
# export get_total_fitness_variance, get_abiotic_variance, get_mutational_variance, get_biotic_variance, get_inherent_variance

# Analysis

using SimpleWeightedGraphs: induced_subgraph, adjacency_matrix
export load_collected_data, save_calculated, add_logged_organisms

export get_genotype, get_snapshot_ids, get_time, get_snapshot, get_trials

export get_phenotype_similarity, get_most_frequent_genotypes, get_reachable_diversity
export get_reachable_fitness, get_snapshot, get_T_similarity, get_evolutionary_potential
export get_adaption_of_genotype, get_adaption_of_snapshot
export is_reproducing
export get_entropy
export get_genotype_diversity
export get_neutrality, get_neutrality_null_model

export _wasserstein, get_genotype_distribution

export save_offspring_log, build_genotype_graph, analyse_graph, calculate_phenotype_graph!
export GGraphData, save_graph_data, load_graph_data
export build_genotype_graph!, build_phenotype_graph!, build_neutral_networks!, analyse_phenotype_graph, get_neutral_networks_by_g_sampling

export get_diversity_threshold, analyse_neutral_networks, analyse_neutral_network_graph
export get_average_nn_size, get_average_radius, get_average_diameter, get_average_clustering, get_nn_size_percentile
export get_average_phenotype_robustness, get_average_phenotype_evolvability, get_average_phenotype_evolvability_robustness_cor
export get_shape_space_covering

# Null Models

export get_house_of_cards_null_model, get_complete_house_of_cards_null_model

# Tierra

export TierraModel
export TierrianOrganism

export SMALL_ANCESTOR, LARGE_ANCESTOR

export execute_slice!, SLICE_SIZE

export collect_distribution

export print_program

# Geb

export GebModel
export GebOrganism

export execute!

# Analysis

include("utils/organism.jl")

# include("history/types.jl")
# include("history/history.jl")#

# include("utils/group_by.jl")#

# include("utils/model.jl")
# include("random_model.jl")#

# include("analysis/statistics.jl")
# include("analysis/selective_factors.jl")

include("analysis/config.jl")
include("logging/statistic_model.jl")
include("logging/collect_distribution.jl")
include("logging/logger.jl")
include("logging/run_logger.jl")

include("analysis/collected_data.jl")
include("analysis/sample_distributions.jl")

include("analysis/estimator.jl")

include("analysis/utils.jl")
include("analysis/graph_utils.jl")

include("analysis/GP_maps/graph_data.jl")

include("analysis/statistics/phenotype_similarity.jl")
include("analysis/statistics/reachable_diversity.jl")
include("analysis/statistics/reachable_fitness.jl")
include("analysis/statistics/T_similarity.jl")
include("analysis/statistics/evolutionary_potential.jl")
include("analysis/statistics/level_of_adaption.jl")
include("analysis/statistics/reproducability.jl")
include("analysis/statistics/entropy.jl")
include("analysis/statistics/population_metrics.jl")

include("analysis/GP_maps/build_gp_map.jl")
include("analysis/GP_maps/analyse_neutral_networks.jl")

# Null Models

include("null_model/gp_null_models.jl")

# Tierra

include("tierra/config.jl")

include("tierra/instructions/search_directions.jl")

include("tierra/instructions/definitions.jl")

include("tierra/world/free_memory_block.jl")
include("tierra/mutations.jl")

include("tierra/organism/organism.jl")
include("tierra/world/tierra_model.jl")

include("tierra/statistics/statistic_model.jl")

include("tierra/world/memory_utils.jl")
include("tierra/world/memory.jl")
include("tierra/world/division.jl")
include("tierra/world/execution.jl")

include("tierra/instructions/template_search.jl")

include("tierra/instructions/implementation.jl")

include("tierra/small_ancestor.jl")
include("tierra/large_ancestor.jl")

include("tierra/show.jl")
include("tierra/logging.jl")

# Geb

include("geb/config.jl")

include("geb/utils.jl")

include("geb/network/rule.jl")
include("geb/network/node.jl")
include("geb/network/network.jl")
include("geb/network/BFS.jl")

include("geb/network/axiom_network.jl")
include("geb/network/rule_decoding.jl")
include("geb/network/development.jl")

include("geb/organism/organism.jl")
include("geb/model.jl")

include("geb/network/activation_propagation.jl")

include("geb/grid_utils.jl")
include("geb/organism/mutation.jl")
include("geb/organism/actions.jl")
include("geb/organism/input_routing.jl")

include("geb/execution.jl")

include("geb/statistics/statistic_model.jl")

include("geb/show.jl")

end


"""
al_methods.jl
"""

function solve!(solver::AugmentedLagrangianSolver{T,S}) where {T,S}
    # pull vars
    convals = solver.solver_uncon.obj.convals
    solver_uncon = solver.solver_uncon
    c_max = Inf
    J = 0.
    # intialize
    set_verbosity!(solver)
    clear_cache!(solver)
    reset!(solver)
    # run AL iterations
    for i = 1:solver.opts.al_max_iterations
        # solve the unconstrained problem
        J = solve!(solver.solver_uncon)
        # record the updated information
        record_iteration!(solver, J, max_violation_penalty(convals)...)
        # exit if converged
        exit = evaluate_convergence(solver, i)
        exit && break
        # outer loop update
        update_dual_penalty!(convals)
        # reset verbosity level after it's modified
        set_verbosity!(solver)
        reset!(solver_uncon)
    end
    terminate!(solver)
    return J
end

function evaluate_convergence(solver::AugmentedLagrangianSolver, al_iterations::Int)
    # exit if the maximum number of iterations has elapsed
    if al_iterations == solver.opts.al_max_iterations
        solver.stats.status = MAX_ITERATIONS_OUTER
        return true
    end

    # exit if the maximum total number of iterations has been reached
    if solver.stats.iterations >= solver.opts.iterations
        return true
    end

    # exit if violation tolerance satisfied or the maximum penalty has been reached
    if (solver.stats.c_max[solver.stats.iterations] < solver.opts.al_vtol ||
	    solver.stats.penalty_max[solver.stats.iterations] >= solver.opts.penalty_max)
        return true
    end

    # exit if iLQR converged
    if solver.stats.status > SOLVE_SUCCEEDED
        return true
    end

    return false
end

function record_iteration!(solver::AugmentedLagrangianSolver{T,S},
                           J::T, c_max::T, μ_max::T) where {T,S}
    # Just update constraint violation and max penalty
    record_iteration!(solver.stats, c_max=c_max, penalty_max=μ_max, is_outer=true)
    vmax, vmax_info = max_violation_info(solver.solver_uncon.obj.convals)
    j = solver.stats.iterations_outer::Int
    @logmsg OuterLoop :iter value=j
    @logmsg OuterLoop :total value=solver.stats.iterations
    @logmsg OuterLoop :cost value=J
    @logmsg OuterLoop :c_max value=c_max
    if is_verbose(solver)
	    print_level(OuterLoop, global_logger())
        println("vmax: $(vmax_info)")
    end
    return nothing
end


# Test multihypo computation assembly

using Distributions
using DocStringExtensions
using Base: Test

using IncrementalInference

@testset "test IncrementalInference.assembleHypothesesElements! without multihypothesis..." begin

s2_1_gt1 = 1:2
s2_1_gt2 = (1:20,Int[])
s2_1_gt3 = [(1,1:2); (2,Int[])]
s2_1_gt4 = Int[]
s2_1 = IncrementalInference.assembleHypothesesElements!(nothing, 20, 1, 2 )
@test sum(s2_1_gt1 .- s2_1[1]) == 0
@test sum(s2_1_gt2[1] .- s2_1[2][1]) == 0
@test sum(s2_1_gt2[2] .- s2_1[2][2]) == 0
@test s2_1_gt3[1][1] == s2_1[3][1][1]
@test sum(s2_1_gt3[1][2] .- s2_1[3][1][2]) == 0
@test s2_1_gt3[2][1] == s2_1[3][2][1]
@test sum(s2_1_gt3[2][2] .- s2_1[3][2][2]) == 0
@test sum(s2_1_gt4 .- s2_1[4])  == 0

s2_2_gt1 = 1:2
s2_2_gt2 = (1:20,Int[])
s2_2_gt3 = [(1,1:2); (2,Int[])]
s2_2_gt4 = Int[]

s2_2 = IncrementalInference.assembleHypothesesElements!(nothing, 20, 2, 2 )

@test sum(s2_2_gt1 .- s2_2[1]) == 0
@test sum(s2_2_gt2[1] .- s2_2[2][1]) == 0
@test sum(s2_2_gt2[2] .- s2_2[2][2]) == 0
@test s2_2_gt3[1][1] == s2_2[3][1][1]
@test sum(s2_2_gt3[1][2] .- s2_2[3][1][2]) == 0
@test s2_2_gt3[2][1] == s2_2[3][2][1]
@test sum(s2_2_gt3[2][2] .- s2_2[3][2][2]) == 0
@test sum(s2_2_gt4 .- s2_2[4])  == 0

end

@testset "test IncrementalInference.assembleHypothesesElements! with bi-modality..." begin

s3_1_gt1 = [1]
s3_1_gt2 = (0,5,5,20)
s3_1_gt3 = [(1,Int[]); (2,Int[1;2]); (3,Int[1;3])]
s3_1_gt4 = 20

s3_1 = IncrementalInference.assembleHypothesesElements!(Categorical([0.0;0.5;0.5]), 20, 1, 3 )

@test sum(s3_1_gt1 - s3_1[1]) == 0
@test sum(s3_1_gt2[1] .- s3_1[2][1]) == 0
@test length(s3_1[2][2]) > s3_1_gt2[2]
@test length(s3_1[2][3]) > s3_1_gt2[3]
@test length(s3_1[2][2]) + length(s3_1[2][3]) == s3_1_gt2[4]
@test s3_1_gt3[1][1] == s3_1[3][1][1]
@test s3_1_gt3[2][1] == s3_1[3][2][1]
@test s3_1_gt3[3][1] == s3_1[3][3][1]
@test sum(s3_1_gt3[1][2] .- s3_1[3][1][2]) == 0
@test sum(s3_1_gt3[2][2] .- s3_1[3][2][2]) == 0
@test sum(s3_1_gt3[3][2] .- s3_1[3][3][2]) == 0

@test sum(s3_1[4] .== 2) > s3_1_gt2[2]
@test sum(s3_1[4] .== 3) > s3_1_gt2[3]

@test sum( [1:20;][s3_1[4] .== 2] .== s3_1[2][2] ) == length(s3_1[2][2])
@test sum( [1:20;][s3_1[4] .== 3] .== s3_1[2][3] ) == length(s3_1[2][3])
@test length(s3_1[4]) == s3_1_gt4

s3_2_gt1 = [1]
s3_2_gt2 = (0,5,5,20)
s3_2_gt3 = [(1,Int[1;2]); (2,Int[2;3]); (3,Int[2;3])]
s3_2_gt4 = 20

s3_2 = IncrementalInference.assembleHypothesesElements!(Categorical([0.0;0.5;0.5]), 20, 2, 3 )

@test sum(s3_2_gt1 - s3_2[1]) == 0
@test sum(s3_2_gt2[1] .- s3_2[2][1]) == 0
@test length(s3_2[2][2]) > s3_2_gt2[2]
@test length(s3_2[2][3]) > s3_2_gt2[3]
@test length(s3_2[2][2]) + length(s3_2[2][3]) == s3_2_gt2[4]
@test s3_2_gt3[1][1] == s3_2[3][1][1]
@test s3_2_gt3[2][1] == s3_2[3][2][1]
@test s3_2_gt3[3][1] == s3_2[3][3][1]
@test sum(s3_2_gt3[1][2] .- s3_2[3][1][2]) == 0
@test sum(s3_2_gt3[2][2] .- s3_2[3][2][2]) == 0
@test sum(s3_2_gt3[3][2] .- s3_2[3][3][2]) == 0

@test sum(s3_2[4] .== 2) > s3_2_gt2[2]
@test sum(s3_2[4] .== 3) > s3_2_gt2[3]

@test sum( [1:20;][s3_2[4] .== 2] .== s3_2[2][2] ) == length(s3_2[2][2])
@test sum( [1:20;][s3_2[4] .== 3] .== s3_2[2][3] ) == length(s3_2[2][3])
@test length(s3_2[4]) == s3_2_gt4

s3_3_gt1 = [1]
s3_3_gt2 = (0,5,5,20)
s3_3_gt3 = [(1,Int[1;3]); (2,Int[2;3]); (3,Int[2;3])]
s3_3_gt4 = 20

s3_3 = IncrementalInference.assembleHypothesesElements!(Categorical([0.0;0.5;0.5]), 20, 3, 3 )

@test sum(s3_3_gt1 - s3_3[1]) == 0
@test sum(s3_3_gt2[1] .- s3_3[2][1]) == 0
@test length(s3_3[2][2]) > s3_3_gt2[2]
@test length(s3_3[2][3]) > s3_3_gt2[3]
@test length(s3_3[2][2]) + length(s3_3[2][3]) == s3_3_gt2[4]
@test s3_3_gt3[1][1] == s3_3[3][1][1]
@test s3_3_gt3[2][1] == s3_3[3][2][1]
@test s3_3_gt3[3][1] == s3_3[3][3][1]
@test sum(s3_3_gt3[1][2] .- s3_3[3][1][2]) == 0
@test sum(s3_3_gt3[2][2] .- s3_3[3][2][2]) == 0
@test sum(s3_3_gt3[3][2] .- s3_3[3][3][2]) == 0

@test sum(s3_3[4] .== 2) > s3_3_gt2[2]
@test sum(s3_3[4] .== 3) > s3_3_gt2[3]

@test sum( [1:20;][s3_3[4] .== 2] .== s3_3[2][2] ) == length(s3_3[2][2])
@test sum( [1:20;][s3_3[4] .== 3] .== s3_3[2][3] ) == length(s3_3[2][3])
@test length(s3_3[4]) == s3_3_gt4

end

@testset "test IncrementalInference.assembleHypothesesElements! with tri-modality..." begin

# s3_4_gt1 = [1]
# s3_4_gt2 = (0,5,5,20)
# s3_4_gt3 = [(1,Int[1;3]); (2,Int[2;3]); (3,Int[2;3])]
# s3_4_gt4 = 20

s4_2 = IncrementalInference.assembleHypothesesElements!(Categorical([0.0;0.3;0.3;0.4]), 20, 2, 4 )

end


module Resolver

const SetOrVector{T} = Union{AbstractSet{T}, AbstractVector{T}}

function resolve(
    compat   :: Function, # (p₁ => v₁, p₂ => v₂) -> Bool
    versions :: AbstractDict{P, <:AbstractVector{V}},
    required :: SetOrVector{P},
) where {P,V}
    vertices, conflicts = vertices_and_conflicts(
        compat,
        versions,
        required,
    )
    packages = P[p for (p, v) in vertices]
    dummies = Bool[isnothing(v) for (p, v) in vertices]
    let resolved
        M = length(versions)
        for relax = 0:M*(M-1)÷2
            solutions = resolve_core(packages, conflicts; dummies, relax)
            # sort solutions
            for S in solutions
                sort!(S, by = v -> packages[v])
            end
            sort!(solutions)
            # map back to version identifiers
            resolved = Vector{Pair{P,V}}[
                [vertices[v] for v in S if !dummies[v]]
                    for S in solutions
            ]
            # only keep the most satisfying solutions
            sat = [sum(p in required for (p, v) in S; init=0) for S in resolved]
            resolved = resolved[sat .≥ maximum(sat; init = 1-isempty(versions))]
            !isempty(resolved) && break
        end
        resolved
    end
end

function vertices_and_conflicts(
    compat   :: Function, # (p₁ => v₁, p₂ => v₂) -> Bool
    versions :: AbstractDict{P, <:AbstractVector{V}},
    required :: SetOrVector{P},
) where {P,V}
    # check compat callback function signature
    hasmethod(compat, Tuple{Pair{P,V}, Pair{P,V}}) &&
    hasmethod(compat, Tuple{Pair{P,V}, Pair{P,Nothing}}) ||
        throw(ArgumentError("compat: callback takes two package-version pairs"))

    # check package versions data structure
    let seen = Set{V}()
        for (p, vers) in versions
            isempty(vers) &&
                throw(ArgumentError("versions: package $p: no versions"))
            for v in vers
                v in seen &&
                    throw(ArgumentError("versions: package $p: duplicate version $v"))
                push!(seen, v)
            end
            empty!(seen)
        end
    end

    # check required packages set
    for p in required
        p in keys(versions) ||
            throw(ArgumentError("required: package $p: not in versions"))
    end

    # co-compute reachable versions and conflicts between them
    package_names = sort!(collect(keys(versions)))
    reachable = Pair{P,Int}[p => Int(p in required) for p in package_names]
    conflicts = Set{NTuple{2,Int}}()
    conflicted = Set(required)
    while true
        clean = true
        for (i₁, (p₁, k₁)) in enumerate(reachable),
            (i₂, (p₂, k₂)) in enumerate(reachable)
            p₁ < p₂ || continue
            v₁ = get(versions[p₁], k₁, nothing)
            v₂ = get(versions[p₂], k₂, nothing)
            (v₁ === nothing || compat(p₁ => v₁, p₂ => v₂)) &&
            (v₂ === nothing || compat(p₂ => v₂, p₁ => v₁)) && continue
            push!(conflicts, minmax(i₁, i₂))
            for (p, k) in (p₁ => k₁ + 1, p₂ => k₂ + 1)
                if k ≤ length(versions[p]) + (p ∈ required) && (p => k) ∉ reachable
                    push!(reachable, p => k)
                    clean = false
                end
                push!(conflicted, p)
            end
        end
        clean && break
    end
    vertices = Pair{P,Union{V,Nothing}}[
        p => get(versions[p], k, nothing)
            for (p, k) in reachable if p in conflicted
    ]

    return vertices, conflicts
end

function resolve_core(
    packages  :: AbstractVector,
    conflicts :: SetOrVector{<:NTuple{2,Integer}};
    dummies   :: Vector{Bool} = fill(false, length(packages)),
    relax     :: Integer = 0,
)
    # check conflicts
    for (v₁, v₂) in conflicts
        v₁ in keys(packages) ||
            throw(ArgumentError("conflicts: invalid version index: $v₁"))
        v₂ in keys(packages) ||
            throw(ArgumentError("conflicts: invalid version index: $v₂"))
        v₁ != v₂ ||
            throw(ArgumentError("conflicts: package $(packages[v₁]): self-conflict $v₁"))
        packages[v₁] != packages[v₂] ||
            throw(ArgumentError("conflicts: package $(packages[v₁]): conflict between $v₁, $v₂"))
    end

    # conflict count matrix
    N = length(packages)
    X = zeros(UInt32, N, N)
    for (v₁, p₁) in enumerate(packages),
        (v₂, p₂) in enumerate(packages)
        x = (v₁, v₂) ∈ conflicts || (v₂, v₁) ∈ conflicts
        d = x && (dummies[v₁] || dummies[v₂])
        X[v₁, v₂] = X[v₂, v₁] = p₁ == p₂ ? relax + 2 : d ? relax + 1 : x
    end

    # deduplicate nodes by conflict sets
    keep = UInt32[]
    seen = Set{Vector{UInt32}}()
    for (v, p) in enumerate(packages)
        X[:, v] in seen && continue
        push!(seen, X[:, v])
        push!(keep, v)
    end
    X = X[keep, keep]
    packages = packages[keep]

    # package indices
    P = zeros(UInt32, length(packages))
    let indices = Dict{eltype(packages), UInt32}()
        for (v, p) in enumerate(packages)
            P[v] = get!(indices, p, length(indices) + 1)
        end
    end

    # find all optimal solutions
    solutions = optimal_solutions(P, X, Int(relax))

    # translate back to original version indices
    Vector{Int}[Int[keep[v] for v in S] for S in solutions]
end

# The core solver is based on naive k-clique listing algorithm in Chiba &
# Nishizeki 1985, "Arboricity and subgraph listing algorithms". It is also
# similar to the well-known Bron-Kerbosch maximal clique listing algorithm, but
# doesn't require keeping track of the set of excluded nodes because the clique
# size is known in advance. This version lists only optimal solutions in the
# following sense: we have a total ordering on the versions of each package and
# we must choose some version of each package (possibly the special "no version"
# version); this induces a partial ordering on solutions and the solutions that
# are listed are those that are optimal by this partial ordering. These are the
# conflict-free solutions that a user might want to choose based on version
# preferences. If there is a conflict between the latest versions of packages A
# and B, they might want the latest version of A and an older version of B or
# the latest version of B and an older version of A. Whereas the total number of
# feasible conflict-free solutions is exponential in the number of conflict
# edges, the number of optimal solutions tends to be fairly small.
#
# The pivoting optimization that can be used to speed up Bron-Kerbosch cannot be
# used here because if we both limit vertices considered at each recursion level
# to the neighborhood of some node and require choosing a node that dominates a
# specific solution in the set of already-found solutions, then we miss valid,
# optimal solutions. However, the domination requirement acts as a form of
# pivoting anyway: after finding an optimal solution, we only consider versions
# that improve on that solution for some package. Those versions must also
# conflict with that solution since otherwise it wouldn't be optimal. So rather
# than pivoting around a node, we are pivoting around an entire solution.

function optimal_solutions(P::Vector{T}, X::Matrix{T}, c::Int = 0) where {T<:Integer}
    # number of packages & versions
    M = maximum(P, init=0)
    L = maximum(X, init=1) + M
    N = size(X, 1)

    # conflict count vector, level vector, solution vector, solutions set
    C = zeros(T, N)
    S = zeros(T, M)
    solutions = typeof(S)[]
    M == 0 && return push!(solutions, S)

    function search!(c::Int, r::Int = 1, d::Int = 1)
        for j = 1:N
            # check subgraph inclusion at this level
            c′ = c - C[j]
            c′ ≥ 0 || continue
            # record version choice
            S[r] = j
            # advance dominance frontier (if necessary & possible)
            l = length(solutions)
            d′ = d
            if d ≤ l
                # check for advancement
                j < solutions[d][P[j]] || continue
                # advance past already-dominated solutions
                while true
                    d′ += 1
                    d′ ≤ l || break
                    S′ = solutions[d′]
                    any(k < S′[P[k]] for r′ = 1:r for k = S[r′]) || break
                end
            end
            # check for a complete solution
            if r == M
                if d′ > l # it's optimal, save it!
                    push!(solutions, sort(S, by = k -> P[k]))
                end
                break
            end
            # restrict next subgraph to neighbors without too many conflicts
            for k = 1:N
                C[k] += X[k, j]
            end
            # recursion
            search!(c′, r + 1, d′)
            # restore the conflict count & level vectors
            for k = 1:N
                C[k] -= X[k, j]
            end
            # exclude vertex from future iterations
            C[j] += r*L
        end
        for j = 1:N
            dr = divrem(C[j], L)
            if dr[1] ≥ r
                C[j] = dr[2]
            end
        end
    end
    search!(c)

    foreach(sort!, solutions)
    return sort!(solutions)
end

end # module


"""
Part of a measurement unit definition that corresponds to the SBML definition
of `Unit`. For example, the unit "per square megahour", Mh^(-2), is written as:

    SBML.UnitPart("second",  # base SI unit, this says we are measuring time
             -2,        # exponent, says "per square"
             6,         # log-10 scale of the unit, says "mega"
             1/3600)    # second-to-hour multiplier

Compound units (such as "volt-amperes" and "dozens of yards per ounce") are
built from multiple `UnitPart`s; see the definition of field `units` in
[`SBML.Model`](@ref).
"""
Base.@kwdef struct UnitPart
    kind::String
    exponent::Int
    scale::Int
    multiplier::Float64
end

"""
Abstract type for all kinds of gene product associations
"""
abstract type GeneProductAssociation end

"""
Gene product reference in the association expression
"""
struct GPARef <: GeneProductAssociation
    gene_product::String
end

"""
Boolean binary "and" in the association expression
"""
struct GPAAnd <: GeneProductAssociation
    terms::Vector{GeneProductAssociation}
end

"""
Boolean binary "or" in the association expression
"""
struct GPAOr <: GeneProductAssociation
    terms::Vector{GeneProductAssociation}
end

"""
A simplified representation of MathML-specified math AST
"""
abstract type Math end

"""
A literal value (usually a numeric constant) in mathematical expression
"""
struct MathVal{T} <: Math where {T}
    val::T
end

"""
An identifier (usually a variable name) in mathematical expression
"""
struct MathIdent <: Math
    id::String
end

"""
A constant identified by name (usually something like `pi`, `e` or `true`) in
mathematical expression
"""
struct MathConst <: Math
    id::String
end

"""
A special value representing the current time of the simulation, with a special
name.
"""
struct MathTime <: Math
    id::String
end

"""
Function application ("call by name", no tricks allowed) in mathematical expression
"""
struct MathApply <: Math
    fn::String
    args::Vector{Math}
end

"""
Function definition (aka "lambda") in mathematical expression
"""
struct MathLambda <: Math
    args::Vector{String}
    body::Math
end

"""
$(TYPEDEF)

SBML Compartment with sizing information.

# Fields
$(TYPEDFIELDS)
"""
Base.@kwdef struct Compartment
    name::Maybe{String} = nothing
    constant::Maybe{Bool} = nothing
    spatial_dimensions::Maybe{Int} = nothing
    size::Maybe{Float64} = nothing
    units::Maybe{String} = nothing
    notes::Maybe{String} = nothing
    annotation::Maybe{String} = nothing
end

"""
$(TYPEDEF)

Reaction with stoichiometry that assigns reactants and products their relative
consumption/production rates, lower/upper bounds (in tuples `lb` and `ub`, with
unit names), and objective coefficient (`oc`). Also may contains `notes` and
`annotation`.

# Fields
$(TYPEDFIELDS)
"""
Base.@kwdef struct Reaction
    reactants::Dict{String,Float64} = Dict()
    products::Dict{String,Float64} = Dict()
    kinetic_parameters::Dict{String,Tuple{Float64,String}} = Dict()
    lower_bound::Maybe{String} = nothing
    upper_bound::Maybe{String} = nothing
    gene_product_association::Maybe{GeneProductAssociation} = nothing
    kinetic_math::Maybe{Math} = nothing
    reversible::Bool
    notes::Maybe{String} = nothing
    annotation::Maybe{String} = nothing
end

"""
$(TYPEDEF)

Abstract type representing SBML rules.
"""
abstract type Rule end

"""
$(TYPEDEF)

SBML algebraic rule.

# Fields
$(TYPEDFIELDS)
"""
struct AlgebraicRule <: Rule
    math::Math
end

"""
$(TYPEDEF)

SBML assignment rule.

# Fields
$(TYPEDFIELDS)
"""
struct AssignmentRule <: Rule
    id::String
    math::Math
end

"""
$(TYPEDEF)

SBML rate rule.

# Fields
$(TYPEDFIELDS)
"""
struct RateRule <: Rule
    id::String
    math::Math
end

"""
$(TYPEDEF)

SBML constraint.

# Fields
$(TYPEDFIELDS)
"""
struct Constraint
    math::Math
    message::String
end

"""
$(TYPEDEF)

Species metadata -- contains a human-readable `name`, a `compartment`
identifier, `formula`, `charge`, and additional `notes` and `annotation`.

# Fields
$(TYPEDFIELDS)
"""
Base.@kwdef struct Species
    name::Maybe{String} = nothing
    compartment::String
    boundary_condition::Maybe{Bool} = nothing
    formula::Maybe{String} = nothing
    charge::Maybe{Int} = nothing
    initial_amount::Maybe{Tuple{Float64,Maybe{String}}} = nothing
    initial_concentration::Maybe{Tuple{Float64,Maybe{String}}} = nothing
    only_substance_units::Maybe{Bool} = nothing
    notes::Maybe{String} = nothing
    annotation::Maybe{String} = nothing
end

"""
$(TYPEDEF)

Gene product metadata.

# Fields
$(TYPEDFIELDS)
"""
Base.@kwdef struct GeneProduct
    name::Maybe{String} = nothing
    label::Maybe{String} = nothing
    notes::Maybe{String} = nothing
    annotation::Maybe{String} = nothing
end

"""
Custom function definition.
"""
Base.@kwdef struct FunctionDefinition
    name::Maybe{String} = nothing
    body::Maybe{Math} = nothing
    notes::Maybe{String} = nothing
    annotation::Maybe{String} = nothing
end

"""
$(TYPEDEF)

# Fields
$(TYPEDFIELDS)
"""
Base.@kwdef struct EventAssignment
    variable::String
    math::Maybe{Math} = nothing
end

"""
$(TYPEDEF)

# Fields
$(TYPEDFIELDS)
"""
Base.@kwdef struct Event
    name::Maybe{String} = nothing
    trigger::Maybe{Math} = nothing
    event_assignments::Maybe{Vector{EventAssignment}} = nothing
end

"""
$(TYPEDEF)

Structure that collects the model-related data. Contains `parameters`, `units`,
`compartments`, `species` and `reactions` and `gene_products`, and additional
`notes` and `annotation` (also present internally in some of the data fields).
The contained dictionaries are indexed by identifiers of the corresponding
objects.

# Fields
$(TYPEDFIELDS)
"""
Base.@kwdef struct Model
    parameters::Dict{String,Tuple{Float64,String}} = Dict()
    units::Dict{String,Vector{UnitPart}} = Dict()
    compartments::Dict{String,Compartment} = Dict()
    species::Dict{String,Species} = Dict()
    initial_assignments::Dict{String,Math} = Dict()
    rules::Vector{Rule} = Rule[]
    constraints::Vector{Constraint} = Constraint[]
    reactions::Dict{String,Reaction} = Dict()
    objective::Dict{String,Float64} = Dict()
    gene_products::Dict{String,GeneProduct} = Dict()
    function_definitions::Dict{String,FunctionDefinition} = Dict()
    events::Dict{String,Event} = Dict()
    name::Maybe{String} = nothing
    id::Maybe{String} = nothing
    conversion_factor::Maybe{String} = nothing
    area_units::Maybe{String} = nothing
    extent_units::Maybe{String} = nothing
    length_units::Maybe{String} = nothing
    substance_units::Maybe{String} = nothing
    time_units::Maybe{String} = nothing
    volume_units::Maybe{String} = nothing
    notes::Maybe{String} = nothing
    annotation::Maybe{String} = nothing
end


function search_template(model::TierraModel, organism::TierrianOrganism, start_address, direction::SearchDirection)
    template = _get_template(model, start_address)
    template_length = length(template)

    if 0 < template_length
        for distance in 1:MAX_SEARCH_DISTANCE
            template_address = _get_address_if_match(direction, model, start_address, template, distance)

            if (template_address != -1)
                return template_address, template_length
            end
        end
    end

    # no match found -> set error flag and return address after template
    organism.error_flag = true

    template_address = normalize_address(start_address + template_length)
    return template_address, template_length
end

function _get_address_if_match(direction::Union{SearchBackward, SearchForward}, model, template_address, template, distance)
    template_length = length(template)
    start_address = _get_address_within_distance(direction, template_address, template_length, distance)

    for i in 0:(template_length - 1)
        instruction = read_memory(model, start_address + i)

        if instruction != template[i+1]
            return -1
        end
    end

    next_address = normalize_address(start_address + template_length)
    return next_address
end

function _get_address_if_match(::SearchBoth, model, template_address, template, distance)
    forward_address = _get_address_if_match(SearchForward(), model, template_address, template, distance)

    if forward_address != -1
        return forward_address
    else
        return _get_address_if_match(SearchBackward(), model, template_address, template, distance)
    end
end

function _get_address_within_distance(::SearchBackward, template_address, template_length, distance)
    return normalize_address(template_address - distance - template_length + one(UInt16))
end
function _get_address_within_distance(::SearchForward, template_address, template_length, distance)
    return normalize_address(template_address + template_length + distance - one(UInt16))
end

function _get_template(model, address)
    template = []

    current_address = address

    while length(template) < MAX_TEMPLATE_LENGTH
        instruction_int = read_memory(model, current_address)
        instruction = convert_instruction(instruction_int)

        if (instruction != NOP_0() && instruction != NOP_1()) break end

        push!(template, 1-instruction_int)
        current_address += one(UInt16)
    end

    return template
end


# Victoria Park using-server
addprocs(3)
using IncrementalInference
using HDF5, JLD, Gadfly, Colors, Cairo
using KernelDensityEstimate, Distributions
using Caesar, RoME
using RoMEPlotting

## load all the model data
# d = odometry information
# f = laser scanner detections
# MM = multi-modal individual id references
# MMr = reworked to map to only one previous feature
# examplefolder, datafolder

function evalLikelihood(fg::FactorGraph, sym::Symbol, point::Vector{Float64})
  p = getVertKDE(fg, sym)
  Ndim(p) == length(point) ? nothing : error("point (dim=$(length(point))) must have same dimension as belief (dim=$(Ndim(p)))")
  evaluateDualTree(p, (point')')[1]
end

# Evaluate the likelihood of an Array{2} of points on the marginal belief of some variable
# note the dimensions must match
function evalLikelihood(fg::FactorGraph, sym::Symbol, points::Array{Float64,2})
  p = getVertKDE(fg, sym)
  Ndim(p) == size(points,1) ? nothing : error("points (dim=$(size(points,1))) must have same dimension as belief (dim=$(Ndim(p)))")
  evaluateDualTree(p, (points))
end

include(joinpath(Pkg.dir("Caesar"),"examples","wheeled","loadVicPrkData.jl"))

include(joinpath(ENV["HOME"],"Documents","blandauthlocal.jl"))
backend_config, user_config = standardcloudgraphsetup(addrdict=addrdict)
# Start new session
addrdict["sessionId"] = "VICPRK_VID"
addrdict["robotId"] = "Ute"

# Figure export folder
currdirtime = now()
imgdir = joinpath(ENV["HOME"], "Pictures", "vicprkimgs", "$(currdirtime)")
mkdir(imgdir)

fg = Caesar.initfg(sessionname=user_config["sessionId"], robotname=user_config["robotId"], cloudgraph=backend_config)
deleteServerSession!(fg.cg, user_config["sessionId"])

# init pose
prevn = initFactorGraph!(fg, init=d[1][1:3])[1]
Podo=diagm([0.5;0.5;0.005])
N=100
lcmode=:mmodal
lsrNoise=diagm([0.1;1.0])

#build :# poses for the factorGraph
for idx=9:30
  prev, X, nextn = getLastPose2D(fg)
  vp, fp = addOdoFG!(fg, nextn, d[idx][1:3], Podo, N=N)
  # add landmarks
  addLandmarksFactoGraph!(fg, f, idx, prevn, nextn, lcmode=lcmode, lsrNoise=lsrNoise, N=N, MM=MM)
  prevn = nextn
  if (idx%10==0)
     # Solve
     tree = wipeBuildNewTree!(fg, drawpdf=false);
    @time inferOverTree!(fg,tree, N=100);
  end
  pl=drawPosesLandms(fg,window=(nextn,25))
  Gadfly.draw(PNG(joinpath(imgdir,"$(nextn).png"),20cm,20cm),pl)
end

0

# run(`ffmpeg -y -i x%d.png -threads 4 -vcodec libx264 -s 1920x1080 -b:v 2M -filter:v "setpts=10*PTS" /home/dehann/Videos/vicprk_test.mp4`)

# pl=drawPosesLandms(fg);
# Gadfly.draw(PDF("/tmp/before.pdf",20cm,20cm),pl)

# batchSolve(fg)
# on ssh terminal, run slamindb(iterations=1)

# fetch a local copy

fg = Caesar.initfg(sessionname=user_config["sessionId"], cloudgraph=backend_config) #, robotname=user_config["robotId"]
fullLocalGraphCopy!(fg)
# pl1=drawPosesLandms(fg)

pl1=drawPosesLandms(fg,window=(:x9,25))

Gadfly.draw(PDF("/tmp/after.pdf",20cm,20cm),pl1)

@async run(`evince /tmp/after.pdf`)

subLocalGraphCopy!(fg, string.([:x1,:x2,:x3,:x4,:x5]), neighbors=1)

X,L = ls(fg)
d=0
t=[]
z=zeros(length(L)+1,length(L)+1)

j=1;
for l1 in L
  i=1
  for l2 in L
    b=getKDEMax(getVertKDE(fg, l1))
    c=evalLikelihood(fg, l2 , b)
    if ( c>0.002 && l1!=l2 )
      d+=1
      #Replace this with A delete and replace
      #what the hell should i do now
      addSoftEqualityPoint2D(fg, l1, l2 )

      push!(t,(l1,l2))
      z[i,j]=1
    elseif (l1==l2)
      z[i,j]=2
    end
    i+=1
  end
  j+=1
end
#run Server Code
fg = Caesar.initfg(sessionname=user_config["sessionId"], cloudgraph=backend_config)
fullLocalGraphCopy!(fg)
pl2=drawPosesLandms(fg)
draw(PDF("daniel.pdf",20cm,20cm),pl2)

# Remove the new session from the server
deleteServerSession!(fg.cg, user_config["sessionId"])

#


"""
Add constraints of the following form to a CoreModelCoupled and return a modified one.

The arguments are same as for in-place `add_coupling_constraints!`.
"""
function add_coupling_constraints(m::CoreModelCoupled, args...)
    new_lp = deepcopy(m)
    add_coupling_constraints!(new_lp, args...)
    return new_lp
end

"""
Add constraints to a plain `CoreModel` (converts it to the coupled type)
"""
add_coupling_constraints(m::CoreModel, args...) =
    add_coupling_constraints(convert(CoreModelCoupled, m), args...)

"""
In-place add coupling constraints in form
```
    cₗ ≤ C x ≤ cᵤ
```
"""
function add_coupling_constraints!(
    m::CoreModelCoupled,
    c::V,
    cl::AbstractFloat,
    cu::AbstractFloat,
) where {V<:VecType}
    return add_coupling_constraints!(
        m,
        sparse(reshape(c, (1, length(c)))),
        sparse([cl]),
        sparse([cu]),
    )
end

function add_coupling_constraints!(
    m::CoreModelCoupled,
    C::M,
    cl::V,
    cu::V,
) where {M<:MatType,V<:VecType}

    all([length(cu), length(cl)] .== size(C, 1)) ||
        throw(DimensionMismatch("mismatched numbers of constraints"))
    size(C, 2) == n_reactions(m) ||
        throw(DimensionMismatch("mismatched number of reactions"))

    m.C = vcat(m.C, sparse(C))
    m.cl = vcat(m.cl, sparse(cl))
    m.cu = vcat(m.cu, sparse(cu))
end

"""
Remove coupling constraints from the linear model and return the modified model.

Arguments are the same as for in-place version `remove_coupling_constraints!`.
"""
function remove_coupling_constraints(m::CoreModelCoupled, args...)
    new_model = deepcopy(m)
    remove_coupling_constraints!(new_model, args...)
    return new_model
end

"""
Removes a set of coupling constraints from a CoreModelCoupled in-place.
"""
function remove_coupling_constraints!(m::CoreModelCoupled, constraint::Int)
    remove_coupling_constraints!(m, [constraint])
end

function remove_coupling_constraints!(m::CoreModelCoupled, constraints::Vector{Int})
    to_be_kept = filter(e -> e ∉ constraints, 1:n_coupling_constraints(m))
    m.C = m.C[to_be_kept, :]
    m.cl = m.cl[to_be_kept]
    m.cu = m.cu[to_be_kept]
end

"""
Change the lower and/or upper bounds ('cl' and 'cu') for given coupling constraints
"""
function change_coupling_bounds!(
    model::CoreModelCoupled,
    constraints::Vector{Int};
    cl::V = Float64[],
    cu::V = Float64[],
) where {V<:VecType}
    found = [index ∈ 1:n_coupling_constraints(model) for index in constraints]
    red_constraints = constraints[found]

    length(red_constraints) == length(unique(red_constraints)) ||
        error("`constraints` appears to contain duplicates")
    if !isempty(cl)
        length(constraints) == length(cl) ||
            throw(DimensionMismatch("`constraints` size doesn't match with `cl`"))
        model.cl[red_constraints] = cl[found]
    end

    if !isempty(cu)
        length(constraints) == length(cu) ||
            throw(DimensionMismatch("`constraints` size doesn't match with `cu`"))
        model.cu[red_constraints] = cu[found]
    end
end


using Printf

using BenchmarkTools
using CUDA
using DataFrames
using PrettyTables

using Oceananigans
using Oceananigans.Architectures
using JULES

using BenchmarkTools: prettytime, prettymemory

Archs = [CPU]
@hascuda Archs = [CPU, GPU]

Ns = [32, 128]
Tvars = [Energy, Entropy]
Gases = [DryEarth, DryEarth3]

tags = ["arch", "N", "gases", "tvar"]

suite = BenchmarkGroup(
    "cfl" => BenchmarkGroup(),
    "acoustic_cfl" => BenchmarkGroup()
)

_cfl(model) = cfl(model, 1)
_cfl(model::CompressibleModel{GPU}) = CUDA.@sync cfl(model, 1)
_acoustic_cfl(model) = acoustic_cfl(model, 1)
_acoustic_cfl(model::CompressibleModel{GPU}) = CUDA.@sync acoustic_cfl(model, 1)

for Arch in Archs, N in Ns, Gas in Gases, Tvar in Tvars
    @info "Running CFL benchmark [$Arch, N=$N, $Tvar, $Gas]..."

    grid = RegularCartesianGrid(size=(N, N, N), extent=(1, 1, 1))
    model = CompressibleModel(architecture=Arch(), grid=grid, thermodynamic_variable=Tvar(),
                              gases=Gas())

    # warmup
    _cfl(model)
    _acoustic_cfl(model)

    b_cfl = @benchmark _cfl($model) samples=10
    display(b_cfl)

    b_acfl = @benchmark _acoustic_cfl($model) samples=10
    display(b_acfl)

    key = (Arch, N, Gas, Tvar)
    suite["cfl"][key] = b_cfl
    suite["acoustic_cfl"][key] = b_acfl
end

function benchmarks_to_dataframe(suite)
    df = DataFrame(architecture=[], size=[], gases=[],
                   thermodynamic_variable=[], min=[], median=[],
                   mean=[], max=[], memory=[], allocs=[])

    for Arch in Archs, N in Ns, Gas in Gases, Tvar in Tvars
        b = suite[Arch, N, Gas, Tvar]

        d = Dict(
            "architecture" => Arch,
            "size" => "$(N)³",
            "gases" => Gas,
            "thermodynamic_variable" => Tvar,
            "min" => minimum(b.times) |> prettytime,
            "median" => median(b.times) |> prettytime,
            "mean" => mean(b.times) |> prettytime,
            "max" => maximum(b.times) |> prettytime,
            "memory" => prettymemory(b.memory),
            "allocs" => b.allocs
        )

        push!(df, d)
    end

    return df
end

header = ["Arch" "Size" "Gases" "ThermoVar" "min" "median" "mean" "max" "memory" "allocs"]

df = benchmarks_to_dataframe(suite["cfl"])
pretty_table(df, header, title="CFL benchmarks", nosubheader=true)

df = benchmarks_to_dataframe(suite["acoustic_cfl"])
pretty_table(df, header, title="Acoustic CFL benchmarks", nosubheader=true)

function gpu_speedups(suite)
    df = DataFrame(size=[], gases=[], thermodynamic_variable=[], speedup=[])

    for N in Ns, Gas in Gases, Tvar in Tvars
        b_cpu = suite[CPU, N, Gas, Tvar] |> median
        b_gpu = suite[GPU, N, Gas, Tvar] |> median
        b_ratio = ratio(b_cpu, b_gpu)

        d = Dict(
            "size" => "$(N)³",
            "gases" => Gas,
            "thermodynamic_variable" => Tvar,
            "speedup" => @sprintf("%.3fx", b_ratio.time)
        )

        push!(df, d)
    end

    return df
end

header = ["Size" "Gases" "ThermoVar" "speedup"]

df = gpu_speedups(suite["cfl"])
pretty_table(df, header, title="CFL speedups", nosubheader=true)

df = gpu_speedups(suite["acoustic_cfl"])
pretty_table(df, header, title="Acoustic CFL speedups", nosubheader=true)


function ensemble_history(e::GMC_NS_Ensemble, bins=25)
    !e.sample_posterior && throw(ArgumentError("This ensemble has no posterior samples to show a history for!"))
    livec=vcat([model.log_Li for model in e.models],[model.log_Li for model in e.posterior_samples])
    show(histogram(livec, nbins=bins))
end

function e_backup(e::GMC_NS_Ensemble, tuner::Dict{Int64,<:GMC_Tuner})
    cp(string(e.path,'/',"ens"),string(e.path,'/',"ens.bak"), force=true)
    serialize(string(e.path,'/',"ens"), e)
    serialize(string(e.path,'/',"tuner"), tuner)
end

function clean_ensemble_dir!(e::GMC_NS_Ensemble)
    if e.sample_posterior
        ens_paths=vcat(
            [basename(m.path) for m in e.models],
            [basename(m.path) for m in e.posterior_samples],
            "ens", "tuner"
        )
    else
        ens_paths=vcat(
            [basename(m.path) for m in e.models],
            "ens", "tuner"
        )
    end

    for file in readdir(e.path)
        !(file in ens_paths) && rm(e.path*'/'*file)
    end
end

function complete_evidence(e::GMC_NS_Ensemble)
    log_Z=e.log_Zi[end]
    H=e.Hi[end]
    live_weight=lps(e.log_Xi[length(e.log_Li)], -log(length(e.models)))
    lis=sort([model.log_Li for model in e.models])
    liwis=sort(lps.(lis, live_weight))

    for (li,liwi) in zip(lis,liwis)
        last_Z=log_Z
        log_Z=logaddexp(log_Z,liwi)
        H=lps(
            (exp(lps(liwi,-log_Z)) * li),
            (exp(lps(last_Z,-log_Z)) * lps(H,last_Z)),
            -log_Z)
    end

    return log_Z, H
end

function measure_evidence(e::GMC_NS_Ensemble)
    log_Z, H = complete_evidence(e)
    return measurement(log_Z,sqrt(abs(H)/length(e.models)))
end

function reset_ensemble!(e::GMC_NS_Ensemble)
    Ni=Int64.(round.(collect(-1:-1:-length(e.log_Li)+1)./e.log_Xi[2:end]))
    N=Ni[1]

    new_models=Vector{typeof(e.models[1])}()

    for traj in 1:N
        if string(traj)*".1" in [basename(record.path) for record in e.models]
            push!(new_models,e.models[findfirst(isequal(string(traj)*".1"), [basename(record.path) for record in e.models])])
        else
            push!(new_models,e.posterior_samples[findfirst(isequal(string(traj)*".1"), [basename(record.path) for record in e.posterior_samples])])
        end
    end

    e.models=new_models
    e.posterior_samples=Vector{typeof(e.models[1])}()

    e.contour=minimum([record.log_Li for record in e.models])

    e.log_Li=[e.log_Li[1]]
    e.log_Xi=[e.log_Xi[1]]
    e.log_wi=[e.log_wi[1]]
    e.log_Liwi=[e.log_Liwi[1]]
    e.log_Zi=[e.log_Zi[1]]
    e.Hi=[e.Hi[1]]

    e.t_counter=length(e.models)+1

    clean_ensemble_dir!(e)
    isfile(e.path*"/tuner") && rm(e.path*"/tuner")
    serialize(e.path*"/ens", e)

    return e
end

function move_ensemble!(e::GMC_NS_Ensemble,path::String)
    !isdir(path) && mkpath(path)
    rectype=typeof(e.models[1])

    for file in readdir(e.path)
        mv(e.path*'/'*file,path*'/'*file)
    end

    for (n,model) in enumerate(e.models)
        e.models[n]=rectype(model.trajectory, model.i, model.pos, path*'/'*basename(model.path), model.log_Li)
    end
    if e.sample_posterior
        for (n,model) in enumerate(e.posterior_samples)
            e.posterior_samples[n]=rectype(model.trajectory, model.i, model.pos, path*'/'*basename(model.path), model.log_Li)
        end
    end

    rm(e.path)
    e.path=path
    serialize(e.path*"/ens",e)
    return e
end

function copy_ensemble(e::GMC_NS_Ensemble,path::String)
    new_e=deepcopy(e)
    !isdir(path) && mkdir(path)
    rectype=typeof(e.models[1])
    for file in readdir(e.path)
        cp(e.path*'/'*file,path*'/'*file)
    end

    for (n,model) in enumerate(e.models)
        new_e.models[n]=rectype(model.trajectory, model.i, model.pos, path*'/'*basename(model.path), model.log_Li)
    end
    if e.sample_posterior
        for (n,model) in enumerate(e.posterior_samples)
            new_e.posterior_samples[n]=rectype(model.trajectory, model.i, model.pos, path*'/'*basename(model.path), model.log_Li)
        end
    end

    new_e.path=path
    serialize(new_e.path*"/ens",new_e)
    return new_e
end

function rewind_ensemble!(e::GMC_NS_Ensemble,rewind_idx)
    !e.sample_posterior && throw(ArgumentError("An ensemble not retaining posterior samples cannot be rewound!"))
    rewind_idx >= length(e.log_Li) && throw(ArgumentError("rewind_idx must be less than the current iterate!"))

    Ni=Int64.(round.(collect(-1:-1:-length(e.log_Li)+1)./e.log_Xi[2:end]))
    insert!(Ni,1,Ni[1])

    for it in length(e.log_Li)-1:-1:rewind_idx
        last_post=pop!(e.posterior_samples)

        if Ni[it]==Ni[it-1]
            models_idx=findfirst(isequal(last_post.trajectory), [rec.trajectory for rec in e.models])
            insert!(e.models, models_idx, last_post)
            deleteat!(e.models, models_idx+1)
        else
            push!(e.models, last_post)
        end

        !(length(e.models)==Ni[it-1]) && throw(DomainError("it: $it Ni[it]:$(Ni[it]) Ni[it-1]:$(Ni[it-1])"))
    end

    e.contour=e.log_Li[rewind_idx]
    e.log_Li=e.log_Li[1:rewind_idx]
    e.log_Xi=e.log_Xi[1:rewind_idx]
    e.log_wi=e.log_wi[1:rewind_idx]
    e.log_Liwi=e.log_Liwi[1:rewind_idx]
    e.log_Zi=e.log_Zi[1:rewind_idx]
    e.Hi=e.Hi[1:rewind_idx]

    traj_nos=vcat([rec.trajectory for rec in e.models],[rec.trajectory for rec in e.posterior_samples])

    e.t_counter=maximum(traj_nos)

    return e
end

function show_models(e::GMC_NS_Ensemble,idxs)
    liperm=sortperm([model.log_Li for model in e.models],rev=true)
    for idx in idxs
        m=deserialize(e.models[liperm[idx]].path)
        show(m)
    end
end

function show_models_e(e::GMC_NS_Ensemble,idxs)
    liperm=sortperm([model.log_Li for model in e.models],rev=true)
    for idx in idxs
        m=deserialize(e.models[liperm[idx]].path)
        show(stdout,m,e)
    end
end

function get_model(e::GMC_NS_Ensemble,no)
    return deserialize(e.path*'/'*string(no))
end

function reestimate_ensemble!(e::GMC_NS_Ensemble, wi_mode="trapezoidal")
    Ni=Int64.(round.(collect(-1:-1:-length(e.log_Li)+1)./e.log_Xi[2:end]))
    push!(Ni, length(e.models))

    #fix any borked starting values
    e.log_Li[1]=-Inf #L0 = 0
	e.log_Xi[1]=0. #X0 = 1
	e.log_wi[1]=-Inf #w0 = 0
	e.log_Liwi[1]=-Inf #Liwi0 = 0
	e.log_Zi[1]=-Inf #Z0 = 0
	e.Hi[1]=0. #H0 = 0,

    for i in 1:length(e.log_Li)-1
        j=i+1

        e.log_Li[j]=e.posterior_samples[i].log_Li

        e.log_Xi[j]=-i/Ni[i]

        if wi_mode=="trapezoidal"
            e.log_wi[j]= logsubexp(e.log_Xi[i], -j/Ni[j]) - log(2) #log width of prior mass spanned by the last step-trapezoidal approx
        elseif wi_mode=="simple"
            e.log_wi[j]= logsubexp(e.log_Xi[i], e.log_Xi[j]) #log width of prior mass spanned by the last step-simple approx
        else
            throw(ArgumentError("Unsupported wi_mode!"))
        end
        e.log_Liwi[j]=lps(e.log_Li[j],e.log_wi[j]) #log likelihood + log width = increment of evidence spanned by iterate
        e.log_Zi[j]=logaddexp(e.log_Zi[i],e.log_Liwi[j])    #log evidence
        #information- dimensionless quantity
        Hj=lps(
            (exp(lps(e.log_Liwi[j],-e.log_Zi[j])) * e.log_Li[j]),
            (exp(lps(e.log_Zi[i],-e.log_Zi[j])) * lps(e.Hi[i],e.log_Zi[i])),
            -e.log_Zi[j])
        Hj === -Inf ? (e.Hi[j]=0.) : (e.Hi[j]=Hj)
    end
end

#get a posterior kernel density estimate from the ensemble
function posterior_kde(e::GMC_NS_Ensemble; bivar=Vector{Pair{<:Integer}}())
    !e.sample_posterior && throw(ArgumentError("This ensemble is not set to collect posterior samples!"))

    θ_mat=zeros(length(e.models[1].pos),length(e.models)+length(e.posterior_samples))
    weight_vec=zeros(length(e.models)+length(e.posterior_samples))

    logz, _=complete_evidence(e)

    for (n,mrec) in enumerate(e.posterior_samples)
        m=deserialize(mrec.path)
        θ_mat[:,n]=m.θ
        weight_vec[n]=lps([m.log_Li,e.log_Xi[n+1],-logz])
    end

    for (n,mrec) in enumerate(e.models)
        m=deserialize(mrec.path)
        p=length(e.posterior_samples)
        θ_mat[:,n+p]=m.θ
        weight_vec[n+p]=lps([m.log_Li, e.log_Xi[end], -log(length(e.models)),-logz])
    end

    kdes=Vector{AbstractKDE}()
    for t in 1:size(θ_mat,1)
        push!(kdes,kde(θ_mat[t,:],weights=exp.(weight_vec)))
    end

    for bv in bivar
        push!(kdes,kde((θ_mat[bv[1],:],θ_mat[bv[2],:]),weights=exp.(weight_vec)))
    end

    return kdes
end


function get_incoming(obj::Vertex)
    return jcall(obj, "getIncoming", Edge, ())
end

function get_location(obj::Vertex)
    return jcall(obj, "getLocation", S2Point, ())
end

function get_outgoing(obj::Vertex)
    return jcall(obj, "getOutgoing", Edge, ())
end


# @todo change "Observation" to "Image"
struct Observation
    channels::Array{Float32, 4}
    function Observation(channel1, channel2, channel3)
        @assert size(channel1) == size(channel2) == size(channel3)
        channels = cat(channel1, channel2, channel3; dims=3)
        new(Flux.batch([channels]))
    end
    function Observation(images::Vector{Observation})
        channels = map(x->x.channels[:,:,:,1], images)
        new(Flux.batch(channels))
    end
end
Base.length(image::Observation) = size(image)[end]
Base.size(image::Observation) = size(image.channels)


@reexport using Plots
@reexport using ColorSchemes

get_colors(scheme, v::AbstractArray) = get.((scheme, ), 1:length(v), ((1, length(v)),))

@export function plot_energy_barriers(E_barriers, Hs, offset=15; plot_kwargs...)
    o = 0.0
    cs = get_colors(ColorSchemes.rainbow, E_barriers)
    # p1 = plot(xlabel = "State", ylabel="Energy", yticks=([],[]), legend_title="H", legend=false)
    p1 = plot(xlabel = "State", yticks=([],[]), legend_title="", colorbar_title="H")
    ytickvals = []
    yticklabels = []
    horizontal_energy = E_barriers[1][1]
    for (i, (E, h)) in enumerate(zip(E_barriers, Hs))
        o += offset
        n = length(E)
        # E_ = [E[1:30]; E[11:20]]
        E_ = E
        horizontal_energy = E_[1][1]
        # plot!(p1, 1:n, E_ .+ o, color=cs[i], xticks=([1, 11, 21, 31, 40],["1", "2", "3", "4", "1"]), colorbar_entry=true, marker_z = round(h, digits=2))
        plot!(p1, 1:n, E_ .+ o, seriescolor=cs[i], xticks=([1, 11, 21, 31, 40],["1", "2", "3", "4", "1"]), marker_z = round(h, digits=2), markercolor=:rainbow,label="")
        push!(ytickvals, o + horizontal_energy)
        push!(yticklabels, "H=$(round(h, digits=2))")
        plot!(p1, 1:n, fill(o + horizontal_energy, n), color=cs[i], label="")
    end

    # return plot(p1, yticks=(ytickvals,yticklabels), title="Nudged Elastic Band")
    return plot(p1; plot_kwargs...)
end

@export function plot_fields(ledgers, Gd_id1, Gd_id2, L_id; L_H_arrow_scale=0.05, dip_arrow_scale=0.5, cs = get_colors(ColorSchemes.rainbow, ledgers))
    fields_on_Gds = map(field_on_Gds, ledgers)
    fields_on_Ls = map(field_on_L, ledgers)
    p1 = scatter(map(x->x[1], spins(ledgers, Gd_id1)),
                map(x->x[2], spins(ledgers, Gd_id1)), color=cs, title="Gd$(Gd_id1)", xlabel="spin x", ylabel="spin y")

    quiver!(p1, map(x->x[1], spins(ledgers, Gd_id1)),
            map(x->x[2], spins(ledgers, Gd_id1)),
            quiver=(map(x->x.dipole[Gd_id1][1], fields_on_Gds) .* dip_arrow_scale, map(x->x.dipole[Gd_id1][2], fields_on_Gds) .* dip_arrow_scale ),color=:red)

    quiver!(p1, map(x->x[1], spins(ledgers, Gd_id1)),
            map(x->x[2], spins(ledgers, Gd_id1)),
            quiver=(map(x->x.L[Gd_id1][1], fields_on_Gds) .* L_H_arrow_scale, map(x->x.L[Gd_id1][2], fields_on_Gds) .*L_H_arrow_scale ),color=:green)

    quiver!(p1, map(x->x[1], spins(ledgers, Gd_id1)),
            map(x->x[2], spins(ledgers, Gd_id1)),
            quiver=(map(x->x[1], H_vecs(ledgers)) .* L_H_arrow_scale, map(x->x[2], H_vecs(ledgers)) .* L_H_arrow_scale ),color=:purple)

    quiver!(p1, map(x->x[1], spins(ledgers, Gd_id1)),
            map(x->x[2], spins(ledgers, Gd_id1)),
            quiver=(map(x->x.sum[Gd_id1][1], fields_on_Gds) .* L_H_arrow_scale, map(x->x.sum[Gd_id1][2], fields_on_Gds) .* L_H_arrow_scale),color=:cyan)

    p2 = scatter(map(x->x[1], spins(ledgers, Gd_id2)),
                map(x->x[2], spins(ledgers, Gd_id2)), color=cs, title="Gd$(Gd_id2)", xlabel="spin x", ylabel="spin y")

    quiver!(p2, map(x->x[1], spins(ledgers, Gd_id2)),
            map(x->x[2], spins(ledgers, Gd_id2)),
            quiver=(map(x->x.dipole[Gd_id2][1], fields_on_Gds) .*  dip_arrow_scale, map(x->x.dipole[Gd_id2][2], fields_on_Gds) .* dip_arrow_scale ),color=:red)

    quiver!(p2, map(x->x[1], spins(ledgers, Gd_id2)),
            map(x->x[2], spins(ledgers, Gd_id2)),
            quiver=(map(x->x.L[Gd_id2][1], fields_on_Gds) .* L_H_arrow_scale, map(x->x.L[Gd_id2][2], fields_on_Gds) .*L_H_arrow_scale),color=:green)

    quiver!(p2, map(x->x[1], spins(ledgers, Gd_id2)),
            map(x->x[2], spins(ledgers, Gd_id2)),
            quiver=(map(x->x.sum[Gd_id2][1], fields_on_Gds) .* L_H_arrow_scale, map(x->x.sum[Gd_id2][2], fields_on_Gds) .* L_H_arrow_scale),color=:cyan)

    quiver!(p2, map(x->x[1], spins(ledgers, Gd_id2)),
            map(x->x[2], spins(ledgers, Gd_id2)),
            quiver=(map(x->x[1], H_vecs(ledgers)) .* L_H_arrow_scale, map(x->x[2], H_vecs(ledgers)) .* L_H_arrow_scale),color=:purple)

    p4 = scatter(map(x->x[1], L_vecs(ledgers, L_id)),
                 map(x->x[2], L_vecs(ledgers, L_id)), color=cs, title="L$L_id", xlabel="spin x", ylabel="spin y")

    quiver!(p4, map(x->x[1], L_vecs(ledgers, L_id)),
                map(x->x[2], L_vecs(ledgers, L_id)),
            quiver=(map(x->x.L[L_id][1], fields_on_Ls), map(x->x.L[L_id][2], fields_on_Ls)),color=:red)

    quiver!(p4, map(x->x[1], L_vecs(ledgers, L_id)),
                map(x->x[2], L_vecs(ledgers, L_id)),
            quiver=(map(x->x.Gd[L_id][1], fields_on_Ls) .* L_H_arrow_scale, map(x->x.Gd[L_id][2], fields_on_Ls) .* L_H_arrow_scale),color=:green)

    quiver!(p4, map(x->x[1], L_vecs(ledgers, L_id)),
                map(x->x[2], L_vecs(ledgers, L_id)),
            quiver=(map(x->x.easy[L_id][1], fields_on_Ls) .* L_H_arrow_scale, map(x->x.easy[L_id][2], fields_on_Ls).*L_H_arrow_scale),color=:purple)

    p1,p2,p4
end


include("als_model.jl")
include("als_dist_model.jl")

setU{M<:Model}(model::M, u::Int64, vals) = setU(model.U, u, vals)
setU(NU::Nullable, u::Int64, vals) = setU(get(NU), u, vals)
function setU{M<:ModelFactor}(U::M, u::Int64, vals)
    U[:,u] = vals
    nothing
end

setP{M<:Model}(model::M, i::Int64, vals) = setP(model.P, i, vals)
setP(NP::Nullable, i, vals) = setP(get(NP), i, vals)
function setP{M<:ModelFactor}(P::M, i::Int64, vals)
    P[:,i] = vals
    nothing
end

getU{M<:Model}(model::M, users) = getU(model.U, users)
getU(NU::Nullable, users) = getU(get(NU), users)
getU{M<:ModelFactor}(U::M, users) = U[:, users]

getP{M<:Model}(model::M, items) = getP(model.P, items)
getP(NP::Nullable, items) = getP(get(NP), items)
getP{M<:ModelFactor}(P::M, items) = P[:, items]

mul_p{M<:Model}(model::M, v) = mul_p(model.P, v)
mul_p(NM::Nullable, v) = mul_p(get(NM), v)
mul_p{M<:ModelFactor}(model::M, v) = v * model


#a = rc("font", family="Times New Roman", size=16)
#println("a = ", a)

"""
Plots each step of a simulation.

`visualize(m, x, f, p, n_steps=10; pause_time=0.3)`

where
* `m` is a `SearchDomain`
* `x` is a `Vehicle`
* `f` is a subtype of `AbstractFilter`
* `p` is a subtype of `Policy`
* `n_steps` is the number of steps to simulate
* `pause_time` is the time to wait between steps

An alternative call uses the `SimUnit` type:

`visualize(m::SearchDomain, su::SimUnit; pause_time=0.3)`
"""
function visualize(m::SearchDomain,
                   x::Vehicle,
                   f::AbstractFilter,
                   p::Policy,
                   n_steps::Int=10;
                   pause_time::Real = 0.3
                  )

    tc = StepThreshold(n_steps)
    su = SimUnit(x, f, p, tc)
    visualize(m, su; pause_time=pause_time)
end

# doesn't reset the filter, vehicle, and policy
# assume SimUnit comes in clean and ready to go
function visualize(m::SearchDomain, uav::SimUnit;
                   pause_time=0.3,
                   alpha=0.1,
                   show_mean::Bool=false,
                   save_gif::Bool=true
                  )

    frames = Frames(MIME("image/png"), fps=5)

    #println("rc_context = ", rc_context())
    #rc("font", family="Times New Roman", size=16)
    #rc("text", usetext=true)

    figure("Simulation")
    plot(m, uav.f, uav.x, alpha=alpha)
    #title("i = 0")
    title("\$t\$ = 0 s")
    push!(frames, gcf())

    # What was the cost to getting this first observation?
    cost_sum = get_cost(uav, m)

    # before doing anything else, we observe
    #  and update filter once
    o = observe(m, uav.x)
    update!(uav, o)

    # This was our first step; steps count number of observations
    step_count = 1

    # plot if need be
    plot(m, uav.f, uav.x, alpha=alpha)
    #title("i = $(step_count)")
    title("\$t\$ = $(step_count) s")
    push!(frames, gcf())

    while !is_complete(uav.f, uav.tc, step_count)
        # act
        #tic()
        a = action(m, uav, o)
        #ta = toq()
        #println("time = ", ta)
        act!(m, uav.x, a)

        move_target!(m)

        # get cost and update step count
        cost_val = get_cost(uav, m, a)
        cost_sum += cost_val
        step_count += 1

        # observe and update
        o = observe(m, uav.x)
        update!(uav, o)

        # plot if need be
        pause(pause_time)
        figure("Simulation")
        cla()
        plot(m, uav.f, uav.x, alpha=alpha, show_mean=show_mean)
        #max_b = maximum(uav.f.b)
        #title("i = $(step_count), cost = $(round(cost_val,3))")
        #title("\$t\$ = $(step_count) s, cost = $(round(cost_val,3))")
        title("\$t\$ = $(step_count) s")
        push!(frames, gcf())
    end
    write("temp.mp4", frames)
    write("temp.gif", frames)

    return cost_sum
end

# visualize function for multiple UAVs at once
function visualize(m::SearchDomain, vsu::Vector{SimUnit};
                   pause_time=0.3,
                   alpha=0.2,
                   show_mean::Bool=false
                  )

    #println("rc_context = ", rc_context())
    #rc("font", family="Times New Roman", size=16)
    #rc("text", usetext=true)

    # What was the cost to getting this first observation?
    # TODO: commented out on 8/08/2018, put it back soon
    #cost_sum = get_cost(uav, m)

    # before doing anything else, we observe
    #  and update filter once
    o_vals = []
    for uav in vsu
        o = observe(m, uav.x)
        push!(o_vals, o)
        update!(uav, o)
    end

    # This was our first step; steps count number of observations
    step_count = 1

    # plot if need be
    figure("Simulation")
    plot(m, vsu, alpha=alpha)
    title("i = $(step_count)")

    #while !is_complete(uav.f, uav.tc, step_count)
    while !is_complete(vsu, step_count)
        # act

        for (uav_idx,uav) in enumerate(vsu)
            a = action(m, uav, o_vals[uav_idx])
            act!(m, uav.x, a)
        end

        move_target!(m)

        # get cost and update step count
        # TODO: handle costs for the entire group
        #cost_val = get_cost(uav, m, a)
        #cost_sum += cost_val
        step_count += 1

        # observe and update
        o_vals = []
        for uav in vsu
            o = observe(m, uav.x)
            push!(o_vals, o)
            update!(uav, o)
        end

        # plot if need be
        pause(pause_time)
        figure("Simulation")
        cla()
        plot(m, vsu, alpha=alpha, show_mean=show_mean)
        title("i = $(step_count)")
    end

end


# This file is a part of Julia. License is MIT: http://julialang.org/license

## type join (closest common ancestor, or least upper bound) ##

typejoin() = (@_pure_meta; Bottom)
typejoin(t::ANY) = (@_pure_meta; t)
typejoin(t::ANY, ts...) = (@_pure_meta; typejoin(t, typejoin(ts...)))
function typejoin(a::ANY, b::ANY)
    @_pure_meta
    if isa(a,TypeConstructor); a = a.body; end
    if isa(b,TypeConstructor); b = b.body; end
    if a <: b
        return b
    elseif b <: a
        return a
    end
    if isa(a,TypeVar)
        return typejoin(a.ub, b)
    end
    if isa(b,TypeVar)
        return typejoin(a, b.ub)
    end
    if isa(a,Union) || isa(b,Union)
        u = Union{a, b}
        if !isa(u,Union)
            return u
        end
        return reduce(typejoin, Bottom, u.types)
    end
    if a <: Tuple
        if !(b <: Tuple)
            return Any
        end
        ap, bp = a.parameters, b.parameters
        lar = length(ap)::Int; lbr = length(bp)::Int
        laf, afixed = full_va_len(ap)
        lbf, bfixed = full_va_len(bp)
        if lar==0 || lbr==0
            return Tuple
        end
        if laf < lbf
            if isvarargtype(ap[lar]) && !afixed
                c = Vector{Any}(laf)
                c[laf] = Vararg{typejoin(ap[lar].parameters[1], tailjoin(bp,laf))}
                n = laf-1
            else
                c = Vector{Any}(laf+1)
                c[laf+1] = Vararg{tailjoin(bp,laf+1)}
                n = laf
            end
        elseif lbf < laf
            if isvarargtype(bp[lbr]) && !bfixed
                c = Vector{Any}(lbf)
                c[lbf] = Vararg{typejoin(bp[lbr].parameters[1], tailjoin(ap,lbf))}
                n = lbf-1
            else
                c = Vector{Any}(lbf+1)
                c[lbf+1] = Vararg{tailjoin(ap,lbf+1)}
                n = lbf
            end
        else
            c = Vector{Any}(laf)
            n = laf
        end
        for i = 1:n
            ai = ap[min(i,lar)]; bi = bp[min(i,lbr)]
            ci = typejoin(unwrapva(ai),unwrapva(bi))
            c[i] = i == length(c) && (isvarargtype(ai) || isvarargtype(bi)) ? Vararg{ci} : ci
        end
        return Tuple{c...}
    elseif b <: Tuple
        return Any
    end
    while b !== Any
        if a <: b.name.primary
            while a.name !== b.name
                a = supertype(a)
            end
            # join on parameters
            n = length(a.parameters)
            p = Vector{Any}(n)
            for i = 1:n
                ai, bi = a.parameters[i], b.parameters[i]
                if ai === bi || (isa(ai,Type) && isa(bi,Type) && typeseq(ai,bi))
                    p[i] = ai
                else
                    p[i] = a.name.primary.parameters[i]
                end
            end
            return a.name.primary{p...}
        end
        b = supertype(b)
    end
    return Any
end

# Returns length, isfixed
function full_va_len(p)
    isempty(p) && return 0, true
    if isvarargtype(p[end])
        N = p[end].parameters[2]
        if isa(N, Integer)
            return (length(p) + N - 1)::Int, true
        end
        return length(p)::Int, false
    end
    return length(p)::Int, true
end

# reduce typejoin over A[i:end]
function tailjoin(A, i)
    if i > length(A)
        return unwrapva(A[end])
    end
    t = Bottom
    for j = i:length(A)
        t = typejoin(t, unwrapva(A[j]))
    end
    return t
end

## promotion mechanism ##

promote_type()  = (@_pure_meta; Bottom)
promote_type(T) = (@_pure_meta; T)
promote_type(T, S, U, V...) = (@_pure_meta; promote_type(T, promote_type(S, U, V...)))

promote_type(::Type{Bottom}, ::Type{Bottom}) = (@_pure_meta; Bottom)
promote_type{T}(::Type{T}, ::Type{T}) = (@_pure_meta; T)
promote_type{T}(::Type{T}, ::Type{Bottom}) = (@_pure_meta; T)
promote_type{T}(::Type{Bottom}, ::Type{T}) = (@_pure_meta; T)

# Try promote_rule in both orders. Typically only one is defined,
# and there is a fallback returning Bottom below, so the common case is
#   promote_type(T, S) =>
#   promote_result(T, S, result, Bottom) =>
#   typejoin(result, Bottom) => result
function promote_type{T,S}(::Type{T}, ::Type{S})
    @_pure_meta
    promote_result(T, S, promote_rule(T,S), promote_rule(S,T))
end

promote_rule(T, S) = (@_pure_meta; Bottom)

promote_result(t,s,T,S) = (@_pure_meta; promote_type(T,S))
# If no promote_rule is defined, both directions give Bottom. In that
# case use typejoin on the original types instead.
promote_result{T,S}(::Type{T},::Type{S},::Type{Bottom},::Type{Bottom}) = (@_pure_meta; typejoin(T, S))

promote() = ()
promote(x) = (x,)
function promote{T,S}(x::T, y::S)
    (convert(promote_type(T,S),x), convert(promote_type(T,S),y))
end
promote_typeof(x) = (@_pure_meta; typeof(x))
promote_typeof(x, xs...) = (@_pure_meta; promote_type(typeof(x), promote_typeof(xs...)))
function promote(x, y, z)
    (convert(promote_typeof(x,y,z), x),
     convert(promote_typeof(x,y,z), y),
     convert(promote_typeof(x,y,z), z))
end
function promote(x, y, zs...)
    (convert(promote_typeof(x,y,zs...), x),
     convert(promote_typeof(x,y,zs...), y),
     convert(Tuple{Vararg{promote_typeof(x,y,zs...)}}, zs)...)
end
# TODO: promote{T}(x::T, ys::T...) here to catch all circularities?

## promotions in arithmetic, etc. ##

# Because of the promoting fallback definitions for Number, we need
# a special case for undefined promote_rule on numeric types.
# Otherwise, typejoin(T,S) is called (returning Number) so no conversion
# happens, and +(promote(x,y)...) is called again, causing a stack
# overflow.
function promote_result{T<:Number,S<:Number}(::Type{T},::Type{S},::Type{Bottom},::Type{Bottom})
    @_pure_meta
    promote_to_supertype(T, S, typejoin(T,S))
end

# promote numeric types T and S to typejoin(T,S) if T<:S or S<:T
# for example this makes promote_type(Integer,Real) == Real without
# promoting arbitrary pairs of numeric types to Number.
promote_to_supertype{T<:Number          }(::Type{T}, ::Type{T}, ::Type{T}) = (@_pure_meta; T)
promote_to_supertype{T<:Number,S<:Number}(::Type{T}, ::Type{S}, ::Type{T}) = (@_pure_meta; T)
promote_to_supertype{T<:Number,S<:Number}(::Type{T}, ::Type{S}, ::Type{S}) = (@_pure_meta; S)
promote_to_supertype{T<:Number,S<:Number}(::Type{T}, ::Type{S}, ::Type) =
    error("no promotion exists for ", T, " and ", S)

+(x::Number, y::Number) = +(promote(x,y)...)
*(x::Number, y::Number) = *(promote(x,y)...)
-(x::Number, y::Number) = -(promote(x,y)...)
/(x::Number, y::Number) = /(promote(x,y)...)
^(x::Number, y::Number) = ^(promote(x,y)...)

fma(x::Number, y::Number, z::Number) = fma(promote(x,y,z)...)
muladd(x::Number, y::Number, z::Number) = muladd(promote(x,y,z)...)

(&)(x::Integer, y::Integer) = (&)(promote(x,y)...)
(|)(x::Integer, y::Integer) = (|)(promote(x,y)...)
xor(x::Integer, y::Integer) = xor(promote(x,y)...)

==(x::Number, y::Number) = (==)(promote(x,y)...)
<( x::Real, y::Real)     = (< )(promote(x,y)...)
<=(x::Real, y::Real)     = (<=)(promote(x,y)...)

div(x::Real, y::Real) = div(promote(x,y)...)
fld(x::Real, y::Real) = fld(promote(x,y)...)
cld(x::Real, y::Real) = cld(promote(x,y)...)
rem(x::Real, y::Real) = rem(promote(x,y)...)
mod(x::Real, y::Real) = mod(promote(x,y)...)

mod1(x::Real, y::Real) = mod1(promote(x,y)...)
fld1(x::Real, y::Real) = fld1(promote(x,y)...)

max(x::Real, y::Real) = max(promote(x,y)...)
min(x::Real, y::Real) = min(promote(x,y)...)
minmax(x::Real, y::Real) = minmax(promote(x, y)...)

# "Promotion" that takes a function into account and tries to preserve
# non-concrete types. These are meant to be used mainly by elementwise
# operations, so it is advised against overriding them
_default_type(T::Type) = (@_pure_meta; T)

promote_op(::Any...) = (@_pure_meta; Any)
function promote_op{S}(f, ::Type{S})
    @_inline_meta
    Z = Tuple{_default_type(S)}
    T = _default_eltype(Generator{Z, typeof(f)})
    isleaftype(S) && return isleaftype(T) ? T : Any
    return typejoin(S, T)
end
function promote_op{R,S}(f, ::Type{R}, ::Type{S})
    @_inline_meta
    Z = Iterators.Zip2{Tuple{_default_type(R)}, Tuple{_default_type(S)}}
    T = _default_eltype(Generator{Z, typeof(a -> f(a...))})
    isleaftype(R) && isleaftype(S) && return isleaftype(T) ? T : Any
    return typejoin(R, S, T)
end

## catch-alls to prevent infinite recursion when definitions are missing ##

no_op_err(name, T) = error(name," not defined for ",T)
+{T<:Number}(x::T, y::T) = no_op_err("+", T)
*{T<:Number}(x::T, y::T) = no_op_err("*", T)
-{T<:Number}(x::T, y::T) = no_op_err("-", T)
/{T<:Number}(x::T, y::T) = no_op_err("/", T)
^{T<:Number}(x::T, y::T) = no_op_err("^", T)

fma{T<:Number}(x::T, y::T, z::T) = no_op_err("fma", T)
fma(x::Integer, y::Integer, z::Integer) = x*y+z
muladd{T<:Number}(x::T, y::T, z::T) = x*y+z

(&){T<:Integer}(x::T, y::T) = no_op_err("&", T)
(|){T<:Integer}(x::T, y::T) = no_op_err("|", T)
xor{T<:Integer}(x::T, y::T) = no_op_err("xor", T)

=={T<:Number}(x::T, y::T) = x === y
 <{T<:Real}(x::T, y::T) = no_op_err("<" , T)
<={T<:Real}(x::T, y::T) = no_op_err("<=", T)

rem{T<:Real}(x::T, y::T) = no_op_err("rem", T)
mod{T<:Real}(x::T, y::T) = no_op_err("mod", T)

min(x::Real) = x
max(x::Real) = x
minmax(x::Real) = (x, x)

max{T<:Real}(x::T, y::T) = ifelse(y < x, x, y)
min{T<:Real}(x::T, y::T) = ifelse(y < x, y, x)
minmax{T<:Real}(x::T, y::T) = y < x ? (y, x) : (x, y)


module AFK

include("Knetapi.jl");   export Parameter
include("core.jl")
include("primitive.jl"); export Multiply, Embed, BatchMul, Linear, Dense
include("compound.jl");  export MLP

end # module