# download input files from: https://gist.github.com/isaacsas/7648fe065f1884307a2906a0a48ea12b
# then add ReactionNetworkImporter package (unregistered)
#]
# add https://github.com/isaacsas/ReactionNetworkImporter.jl.git

using DiffEqBase, DiffEqBiological,Plots,OrdinaryDiffEq, Sundials
using ReactionNetworkImporter
using TimerOutputs

# parameters
networkname = "tester"
tf = 10.

# input files, specify path
#speciesf = "SOME-PATH-TO/BCR_pop.txt"
#rxsf = "SOME-PATH-TO/BCR_rxn.txt"

const to = TimerOutput()
reset_timer!(to)

# get the reaction network
@timeit to "netgen" rn,initialpop = get_rxnetwork_simple(RSSAFile(), networkname, speciesf, rxsf; printrxs = false)
@timeit to "addodes" addodes!(rn; build_jac=false, build_symfuncs=false)
@timeit to "ODEProb" oprob = ODEProblem(rn,convert.(Float64,initialpop),(0.,tf))
show(to)

# note solvers run _much_ faster the second time
reset_timer!(to); @timeit to "Rodas4" begin sol = solve(oprob,Rodas4(autodiff=false),dense=false,calck=false); end; show(to)
reset_timer!(to); @timeit to "Rodas4" begin sol = solve(oprob,Rodas4(autodiff=false),dense=false,calck=false); end; show(to)
reset_timer!(to); @timeit to "Rodas4" begin sol = solve(oprob,Rodas4(autodiff=false),dense=false); end;

reset_timer!(to); @timeit to "CVODE_BDF" begin sol = solve(oprob,CVODE_BDF(linear_solver=:GMRES),dense=false); end; show(to)
reset_timer!(to); @timeit to "CVODE_BDF" begin sol = solve(oprob,CVODE_BDF(linear_solver=:GMRES),dense=false); end; show(to)

# note, restart REPL to see timing with first pass overhead for the next solver
reset_timer!(to); @timeit to "CVODE_BDF" begin sol = solve(oprob,CVODE_BDF(),dense=false); end; show(to)
reset_timer!(to); @timeit to "CVODE_BDF" begin sol = solve(oprob,CVODE_BDF(),dense=false); end; show(to)


#####################################################################################
# MPCCMCounters type ; based on NLPModels Counters:
# https://github.com/JuliaSmoothOptimizers/NLPModels.jl/blob/master/src/nlp_types.jl
#
# Problem both NLPModels and MPCC export neval_hess,...
#####################################################################################

"""
MPCCCounters

Initialization: `MPCCCounters()`
"""
mutable struct MPCCCounters

  neval_obj     :: Int  # Number of objective evaluations.
  neval_grad    :: Int  # Number of objective gradient evaluations.
  neval_cons    :: Int  # Number of constraint vector evaluations.
  neval_consG   :: Int  # Number of constraint vector evaluations of G(x).
  neval_consH   :: Int  # Number of constraint vector evaluations of H(x).
  neval_jcon    :: Int  # Number of individual constraint evaluations.
  neval_jgrad   :: Int  # Number of individual constraint gradient evaluations.
  neval_jac     :: Int  # Number of constraint Jacobian evaluations.
  neval_jacG    :: Int  # Number of constraint Jacobian evaluations of G(x).
  neval_jacH    :: Int  # Number of constraint Jacobian evaluations of H(x).
  neval_jprod   :: Int  # Number of Jacobian-vector products.
  neval_jGprod  :: Int  # Number of Jacobian-vector products.
  neval_jHprod  :: Int  # Number of Jacobian-vector products.
  neval_jtprod  :: Int  # Number of transposed Jacobian-vector products.
  neval_jGtprod :: Int  # Number of transposed Jacobian-vector products.
  neval_jHtprod :: Int  # Number of transposed Jacobian-vector products.
  neval_hess    :: Int  # Number of Lagrangian/objective Hessian evaluations.
  neval_hessG   :: Int  # Number of Lagrangian/objective Hessian evaluations.
  neval_hessH   :: Int  # Number of Lagrangian/objective Hessian evaluations.
  neval_hprod   :: Int  # Number of Lagrangian/objective Hessian-vector products.
  neval_hGprod  :: Int  # Number of Lagrangian/objective Hessian-vector products.
  neval_hHprod  :: Int  # Number of Lagrangian/objective Hessian-vector products.
  neval_jhprod  :: Int  # Number of individual constraint Hessian-vector products.

  function MPCCCounters()
    return new(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
  end
end

# simple default API for retrieving counters
for counter in fieldnames(MPCCCounters)
  @eval begin
    """
        $($counter)(nlp)
    Get the number of `$(split("$($counter)", "_")[2])` evaluations.
    """
    $counter(nlp :: AbstractMPCCModel) = nlp.counters.$counter
    export $counter
  end
end

"""
    increment!(nlp, s)
Increment counter `s` of problem `nlp`.
"""
function increment!(nlp :: AbstractMPCCModel, s :: Symbol)
  setfield!(nlp.counters, s, getfield(nlp.counters, s) + 1)
end

"""
    decrement!(nlp, s)
Increment counter `s` of problem `nlp`.
"""
function decrement!(nlp :: AbstractMPCCModel, s :: Symbol)
  if s in fieldnames(MPCCCounters)
    setfield!(nlp.counters, s, getfield(nlp.counters, s) - 1)
  end
end

"""
    sum_counters(counters)
Sum all counters of `counters`.
"""
sum_counters(c :: MPCCCounters) = sum(getfield(c, x) for x in fieldnames(MPCCCounters))

"""
    sum_counters(nlp)
Sum all counters of problem `nlp`.
"""
sum_counters(nlp :: AbstractMPCCModel) = sum_counters(nlp.counters)

"""
    reset!(counters)
Reset evaluation counters
"""
function reset!(counters :: MPCCCounters)
  for f in fieldnames(MPCCCounters)
    setfield!(counters, f, 0)
  end
  return counters
end

"""
    reset!(nlp)
Reset evaluation count in `nlp`
"""
function reset!(nlp :: AbstractMPCCModel)
  reset!(nlp.counters)
  return nlp
end


function action(prob::GameProblem; time=false)
    """
       Solve problem and return array of one-step actions
    """

    if time
        @time newton_solve!(prob)
    else
        newton_solve!(prob)
    end
    u = controls(prob.pdtraj.pr)
    return u[1]
end


"""
    ModelFrame(formula, data; model=StatisticalModel, contrasts=Dict())

Wrapper that encapsulates a `FormulaTerm`, schema, data table, and model type.

This wrapper encapsulates all the information that's required to transform data
of the same structure as the wrapped data frame into a model matrix (the
`FormulaTerm`), as well as the information about how that formula term was
instantiated (the schema and model type)

Creating a model frame involves first extracting the [`schema`](@ref) for the
data (using any contrasts provided as hints), and then applying that schema with
[`apply_schema`](@ref) to the formula in the context of the provided model type.

# Constructors

```julia
ModelFrame(f::FormulaTerm, data; model::Type{M} = StatisticalModel, contrasts::Dict = Dict())
```

# Fields

* `f::FormulaTerm`: Formula whose left hand side is the *response* and right hand
  side are the *predictors*.
* `schema::Any`: The schema that was applied to generate `f`.
* `data::D`: The data table being modeled.  The only restriction is that `data`
  is a table (`Tables.istable(data) == true`)
* `model::Type{M}`: The type of the model that will be fit from this model frame.

# Examples

```julia
julia> df = (x = 1:4, y = 5:8)
julia> mf = ModelFrame(@formula(y ~ 1 + x), df)
```
"""
mutable struct ModelFrame{D,M}
    f::FormulaTerm
    schema
    data::D
    model::Type{M}
end

## copied from DataFrames:
function _nonmissing!(res, col)
    # workaround until JuliaLang/julia#21256 is fixed
    eltype(col) >: Missing || return
    res .&= .!ismissing.(col)
end

function missing_omit(d::T) where T<:ColumnTable
    nonmissings = trues(length(first(d)))
    for col in d
        _nonmissing!(nonmissings, col)
    end

    rows = findall(nonmissings)
    d_nonmissing =
        NamedTuple{Tables.names(T)}(tuple((copyto!(similar(col,
                                                           Base.nonmissingtype(eltype(col)),
                                                           length(rows)),
                                                   view(col, rows)) for col in d)...))
    d_nonmissing, nonmissings
end

missing_omit(data::T, formula::AbstractTerm) where T<:ColumnTable =
    missing_omit(NamedTuple{tuple(termvars(formula)...)}(data))

function ModelFrame(f::FormulaTerm, data::ColumnTable;
                    model::Type{M}=StatisticalModel, contrasts=Dict{Symbol,Any}()) where M
    data, _ = missing_omit(data, f)

    sch = schema(f, data, contrasts)
    f = apply_schema(f, sch, M)

    ModelFrame(f, sch, data, model)
end

ModelFrame(f::FormulaTerm, data; model=StatisticalModel, contrasts=Dict{Symbol,Any}()) =
    ModelFrame(f, columntable(data); model=model, contrasts=contrasts)

StatsBase.modelmatrix(f::FormulaTerm, data; kwargs...) = modelmatrix(f.rhs, data; kwargs...)

function StatsBase.modelmatrix(t::Union{AbstractTerm, TupleTerm}, data;
                               hints=Dict{Symbol,Any}(), mod::Type{M}=StatisticalModel) where M
    Tables.istable(data) ||
        throw(ArgumentError("expected data in a Table, got $(typeof(data))"))
    t = has_schema(t) ? t : apply_schema(t, schema(t, data, hints), M)
    modelcols(collect_matrix_terms(t), columntable(data))
end
function StatsBase.response(f::FormulaTerm, data;
                            hints=Dict{Symbol,Any}(),
                            mod::Type{M}=StatisticalModel) where M
    Tables.istable(data) ||
        throw(ArgumentError("expected data in a Table, got $(typeof(data))"))
    f = has_schema(f) ? f : apply_schema(f, schema(f, data, hints), M)
    modelcols(f.lhs, columntable(data))
end

StatsBase.modelmatrix(mf::ModelFrame; data=mf.data) = modelcols(mf.f.rhs, data)
StatsBase.response(mf::ModelFrame; data=mf.data) = modelcols(mf.f.lhs, data)

StatsBase.coefnames(mf::ModelFrame) = vectorize(coefnames(mf.f.rhs))

"""
    setcontrasts!(mf::ModelFrame; kwargs...)
    setcontrasts!(mf::ModelFrame, contrasts::Dict{Symbol})

Update the contrasts used for coding categorical variables in
[`ModelFrame`](@ref) in place.  This is accomplished by computing a new schema
based on the provided contrasts and the `ModelFrame`'s data, and applying it to
the `ModelFrame`'s `FormulaTerm`.

Note that only the `ModelFrame` itself is mutated: because `AbstractTerm`s are
immutable, any changes will produce a copy.

"""
setcontrasts!(mf::ModelFrame; kwargs...) = setcontrasts!(mf, Dict(kwargs))
function setcontrasts!(mf::ModelFrame, contrasts::Dict{Symbol})
    # TODO: don't consume the whole table again if it's not needed
    new_schema = schema([term(k) for k in keys(contrasts)], mf.data, contrasts)

    # warn of no-op for keys that dont correspond to known terms from old schema
    unknown_keys = [k for k in keys(new_schema) if !haskey(mf.schema, k)]
    if !isempty(unknown_keys)
        unknown_keys_str = join(unknown_keys, ", ", " and ")
        @warn "setcontrasts! for terms " * unknown_keys_str *
            " has no effect since they are not found in original schema"
    end

    # apply only the re-mapped terms
    mf.f = apply_schema(mf.f, new_schema, mf.model)
    merge!(mf.schema, new_schema)
    mf
end

"""
    ModelMatrix(mf::ModelFrame)

Convert a `ModelFrame` into a numeric matrix suitable for modeling

# Fields

* `m::AbstractMatrix{<:AbstractFloat}`: the generated numeric matrix
* `assign::Vector{Int}` the index of the term corresponding to each column of `m`.

# Constructors

```julia
ModelMatrix(mf::ModelFrame)
# Specify the type of the resulting matrix (default Matrix{Float64})
ModelMatrix{T <: AbstractMatrix{<:AbstractFloat}}(mf::ModelFrame)
```
"""
mutable struct ModelMatrix{T <: AbstractMatrix{<:AbstractFloat}}
    m::T
    assign::Vector{Int}
end

Base.size(mm::ModelMatrix, dim...) = size(mm.m, dim...)

asgn(f::FormulaTerm) = asgn(f.rhs)
asgn(mt::MatrixTerm) = asgn(mt.terms)
asgn(t) = mapreduce(((i,t), ) -> i*ones(width(t)),
                    append!,
                    enumerate(vectorize(t)),
                    init=Int[])

function ModelMatrix{T}(mf::ModelFrame) where T<:AbstractMatrix{<:AbstractFloat}
    mat = modelmatrix(mf)
    ModelMatrix(convert(T, mat), asgn(mf.f))
end

ModelMatrix(mf::ModelFrame) = ModelMatrix{Matrix{Float64}}(mf)


#
println("Dl) Test of the PhotoDoubleIonization module with ASF from an internally generated initial- and final-state multiplet.")
#
setDefaults("print summary: open", "zzz-PhotoDoubleIonization.sum")
setDefaults("method: continuum, Galerkin")           ## setDefaults("method: continuum, Galerkin")  "method: continuum, asymptotic Coulomb"
                                                     ## setDefaults("method: normalization, Ong-Russek")
setDefaults("method: normalization, pure sine")      ## setDefaults("method: normalization, pure Coulomb")    setDefaults("method: normalization, pure sine")
setDefaults("unit: rate", "a.u.")

grid = Radial.Grid(Radial.Grid(false), rnt = 4.0e-6, h = 5.0e-2, hp = 1.0e-2, rbox = 10.0)

if  false
    # Green function for single-photon 2p^2 photoionization of neon
    name             = "Double Auger decay of 1s photoionized neon"
    refConfigs       = [Configuration("1s^2 2s^2 2p^6")]
    levelSymmetries  = [LevelSymmetry(0, Basics.minus), LevelSymmetry(1, Basics.minus)]
    greenSettings    = GreenSettings(3, [0, 1], 0.01, true, LevelSelection())
    greenRep         = Representation(name, Nuclear.Model(10.), Radial.Grid(true), refConfigs,
                                      GreenExpansion( AtomicState.DampedSpaceCI(), Basics.DeExciteSingleElectron(), levelSymmetries, 3, greenSettings) )
    greenOut         = generate(greenRep, output=true)
    doubleGreen      = greenOut["Green channels"]

elseif true
    # Single-photon 2p^2 photoionization of neon
    doubleSettings   = PhotoDoubleIonization.Settings(EmMultipole[E1], UseGauge[UseCoulomb, UseBabushkin], [50.0, 70.], doubleGreen, 2,
                                                      false, true, 2, LineSelection(true, indexPairs=[(1,0)]))

    wa = Atomic.Computation(Atomic.Computation(), name="xx", grid=grid, nuclearModel=Nuclear.Model(10.),
                            initialConfigs  =[Configuration("1s^2 2s^2 2p^6")],
                            finalConfigs    =[Configuration("1s^2 2s^2 2p^4")],
                            process = PhotoDouble(),  processSettings = doubleSettings )

    wb = perform(wa)

end
setDefaults("print summary: close", "")


using PowerModels
using Ipopt
using DelimitedFiles
using Memento
using ArgParse
include("qc_relaxation_methods.jl")

function parse_cli()
    s = ArgParseSettings()
    @add_arg_table! s begin
        "--log", "-l"
            help = "Define log level. Possible values are: [debug | info |
            notice | warn | error]."
            arg_type = String
            default = "info"
        "--vol", "-v"
            help = "Save A,b polytope data. Define the frequency of saving files
            e.g.: -v 1000 means that A,b data saved in every 1000th iteration."
            arg_type = Int
            default = 0
        "--bit"
            help = "Bound tightening ITerations. Max number of iterations in OBBT."
            arg_type = Int
            default = 5
        "--bto"
            help = "Bound tightening Time Out. Max time spent in OBBT."
            arg_type = Int
            default = 120
        "--tightened", "-t"
            help = "Tightened output case file name (relative or full path with
            extension). Supported file formats:\n\t- matpower v2.0"
            arg_type = String
        "--out_file_name_A", "-A"
            help = "Output file of the convex polytope's A matrix. File fullpath
            should be defined including the extension (.csv)."
            arg_type = String
        "--out_file_name_B", "-b"
            help = "Output file of the convex polytope's b vector. File fullpath
            should be defined including the extension (.csv)."
        "--out_file_name_X", "-x"
            help = "Output file of optimal set points. The first row contains the
            variable names."
            arg_type = String
        "case_file_name"
            help = "Case file name (relative or full path with extension).
            Supported file formats:\n\t- matpower v2.0"
            required = true
        "number_of_iterations"
            help = "Number of polytopes added."
            arg_type = Int
            required = true
        "out_samples_file_name"
            help = "Output file containing N2 number of uniform samples from the
            convex polytope. File fullpath should be defined including the
            extension (.csv)."
            arg_type = String
            required = true
        "n2_number_of_samples"
            help = "N2 number of samples from the convex polytope."
            arg_type = Int
            required = true
    end
    # to debug: parse_args([raw"C:\Users\Timon\OneDrive - Danmarks Tekniske Universitet\Denmark\DTU\2019_20_II\software\case_files\case9.m","10",raw"C:\Users\Timon\OneDrive - Danmarks Tekniske Universitet\Denmark\DTU\2019_20_II\software\.data\case9_QCRM_1.csv","10"],s)
    return parse_args(s)
end

function main()
    """ Run N number of iterations that executes a modified QC relaxed ACOPF to created
        separating hyper-planes and event reduce."""
    parsed_inputs = parse_cli()
    # Logger settings
    # Silence all: PowerModels.silence()
    logger = Memento.config!(parsed_inputs["log"]; fmt="[{level} | {name}]: {msg}")
    Memento.setlevel!(Memento.getlogger(PowerModels.InfrastructureModels), "error")
    if parsed_inputs["log"] == "debug"
        # enable debug:
        ENV["JULIA_DEBUG"] = "all"
    end

    # Input data: number of iterations , model name
    debug(logger, string("ARGS: ", parsed_inputs))

    start = time();
    # Load network data from file
    Memento.setlevel!(Memento.getlogger(PowerModels), "error")
    network_data = PowerModels.parse_file(parsed_inputs["case_file_name"])
    Memento.setlevel!(Memento.getlogger(PowerModels), "info")
    # Run bound tightening
    info(logger, string("Run obbt for ", parsed_inputs["case_file_name"],"."))

    to = convert(AbstractFloat, parsed_inputs["bto"])
    # suppress Ipopt message
    local network_data_tight, stats
    @suppress_out network_data_tight, stats = PowerModels.run_obbt_opf!(network_data,
        optimizer_with_attributes(Ipopt.Optimizer, "print_level" => 0);
        model_constructor=ACPPowerModel, max_iter=parsed_inputs["bit"],
        time_limit=to);

    if  parsed_inputs["bit"] < 2
        warn(logger,"obbt opf max_iter should be set to higher!")
    end
    # MATPOWER export
    # update original data with solution

    if parsed_inputs["tightened"] != nothing
        info(logger, string("Writing tightened case file: ", parsed_inputs["tightened"],"."))
        update_data!(network_data, network_data_tight) #network_data_tight["solution"]
        f = open(parsed_inputs["tightened"],"w")
        export_matpower(f,network_data)
        close(f)
    end

    # Initialize abstract power system model
    #TODO: I think ACPPowerModel is the acopf formulation and PowerModels.QCRMPowerModel is the quadratic relaxation
    # ref: https://lanl-ansi.github.io/PowerModels.jl/stable/formulation-details/#Formulation-Details-1
    power_model = instantiate_model(network_data_tight, QCRMPowerModel, PowerModels.build_opf)

    info(logger,string("Run QC relaxation for, ",  parsed_inputs["case_file_name"],"."))
    A_out,b_out,x_opt,header,scaling_factor = run_qc_relax(power_model, parsed_inputs["number_of_iterations"],parsed_inputs["vol"])
    debug(logger,string("scaling_factor",scaling_factor))

    if parsed_inputs["out_file_name_A"] != nothing
        info(logger,string("Write file:\n\t- ", parsed_inputs["out_file_name_A"]))
        writedlm(parsed_inputs["out_file_name_A"], A_out, ',')
    end
    if parsed_inputs["out_file_name_B"] != nothing
        info(logger,string("Write file:\n\t- ", parsed_inputs["out_file_name_B"]))
        writedlm(parsed_inputs["out_file_name_B"], b_out, ',')
    end
    if parsed_inputs["out_file_name_X"] != nothing
        writedlm(parsed_inputs["out_file_name_X"], header, ',')
        open(parsed_inputs["out_file_name_X"],"a") do io
            writedlm(io, x_opt, ',')
        end
    end

    info(logger,string("Sample ",  parsed_inputs["n2_number_of_samples"]," points."))
    N2_out = sample_polyhedron(parsed_inputs["n2_number_of_samples"])
    N2_scaled = tt(scaling_factor,N2_out)
    writedlm(parsed_inputs["out_samples_file_name"], header, ',')
    open(parsed_inputs["out_samples_file_name"],"a") do io
        writedlm(io, N2_scaled, ',')
    end

    elapsed = time() - start
    info(logger,"Elapsed time: $elapsed sec .")
end

logger = Memento.config!("info"; fmt="[{level} | {name}]: {msg}")
main()


let
    roadlength = 50.0
    lane_width = 5.0
    roadway,junction = gen_intersection(roadlength = roadlength,lane_width = lane_width)
    scene = Frame(Entity{VehicleState, BicycleModel, Int},100)
    id=1
    push!(scene,Entity(VehicleState(VecSE2(0.0,0.0,0.0), roadway, 10.0),
        BicycleModel(VehicleDef(AgentClass.CAR, 4.826, 1.81)),id))
    id += 1
    push!(scene,Entity(VehicleState(VecSE2(10.0,0.0,0.0), roadway, 10.0),
        BicycleModel(VehicleDef(AgentClass.CAR, 4.826, 1.81)),id))
    id += 1
    push!(scene,Entity(VehicleState(VecSE2(20.0,0.0,0.0), roadway, 9.0),
        BicycleModel(VehicleDef(AgentClass.CAR, 4.826, 1.81)),id))
    connect_two_seg!(roadway.segments[2],roadway.segments[3],roadway)
    connect_two_seg!(roadway.segments[4],roadway.segments[5],roadway)
    connect_two_seg!(roadway.segments[6],roadway.segments[7],roadway)
    connect_two_seg!(roadway.segments[8],roadway.segments[1],roadway)
    framerate = 24
    timeStep = 0.05
    models = Dict{Int, DriverModel}()

    models[1]=UrbanDriver(timeStep,direction=1,a_cp_max=0.7*9.8)
    models[2]=UrbanDriver(timeStep,direction=2,a_cp_max=0.6*9.8)
    models[3]=MultiPtsTurningDriver(timeStep,v_max=20.0,v_min=0.0,turning_direction=3)
    excute_action!(models[3],0.0,0,scene, roadway, 1)
    actions = get_actions!(Array{Any}(undef, length(scene)), scene, roadway, models)
    for i=1:100
        get_actions!(actions, scene, roadway, models)
        tick!(scene, roadway, actions, timeStep)
    end
    actions = get_actions!(Array{Any}(undef, length(scene)), scene, roadway, models)
    @test abs(actions[1].a_lat-0.0)<0.01
    @test abs(actions[1].a_lon-(-0.05641834275191615))<0.01
    @test actions[1].direction==1
    @test abs(actions[2].a_lat-0.0)<0.01
    @test abs(actions[2].a_lon-(-0.0023799116323655056))<0.1
    @test actions[2].direction==2
    @test abs(actions[3].a-0.07816860295178636)<0.01
    @test abs(actions[3].δ-0.00531000497036248)<0.01
    @test abs(actions[3].direction-3 )<0.01
end


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

module Nets
export Net, enet, kmnet, dnet, gen_features, KernelClassifier
import KernelMethods.Kernels: sigmoid, gaussian, linear, cauchy
import KernelMethods.Scores: accuracy, recall
import KernelMethods.Supervised: NearNeighborClassifier, NaiveBayesClassifier, optimize!, predict_one, predict_one_proba, LabelEncoder, transform, inverse_transform
using SimilaritySearch: KnnResult, L2Distance, L2SquaredDistance, CosineDistance, DenseCosine, JaccardDistance
using TextModel
using PyCall

@pyimport sklearn.naive_bayes as nb
@pyimport sklearn.model_selection as ms

#using JSON
#using DataStructures

mutable struct Net{ItemType,LabelType}
    data::Vector{ItemType}
    labels::Vector{Int}
    le::LabelEncoder{LabelType}
    references::Vector{Int32}
    partitions::Vector{Int32}
    centers::Vector{ItemType}
    centroids::Vector{ItemType}
    dists::Vector{Float64}
    csigmas::Vector{Float64}
    sigmas::Dict{Int,Float64}
    stats::Dict{LabelType,Float64}
    reftype::Symbol
    distance
    kernel
end

function Net(data::Vector{ItemType},labels::Vector{LabelType}) where {ItemType, LabelType}
    le = LabelEncoder(labels)
    y = transform.(le, labels)

    Net(data,y,le,
        Int32[],Int32[],ItemType[],
        ItemType[],Float64[],Float64[],
        Dict{Int,Float64}(), Dict{LabelType,Float64}(),:centroids,L2SquaredDistance(),gaussian)
end

function cosine(x1,x2)::Float64
    xc1=DenseCosine(x1)
    xc2=DenseCosine(x2)
    d=CosineDistance()
    return d(xc1,xc2)
end

function maxmin(data,centers,ind,index::KnnResult,distance,partitions)::Tuple{Int64,Float64}
    c=last(centers)
    if length(index)==0
        for i in ind
            if i!=c
                push!(index,i,Inf)
            end
        end
    end
    nindex=KnnResult(length(index))
    for fn in index
        dist=distance(data[fn.objID],data[c])
        #push!(lK[fn.objID],dist)
        dist = if (dist<fn.dist) dist else fn.dist end
        partitions[fn.objID] = if (dist<fn.dist) c else partitions[fn.objID] end
        if fn.objID!=c
            push!(nindex,fn.objID,dist)
        end
    end
    index.k=nindex.k
    index.pool=nindex.pool
    fn=pop!(index)
    return fn.objID,fn.dist
end

function get_centroids(data::Vector{T}, partitions::Vector{Int})::Vector{T} where T
    centers=[j for j in Set(partitions)]
    sort!(centers)
    centroids=Vector{T}(length(centers))
    for (ic,c) in enumerate(centers)
        ind=[i for (i,v) in enumerate(partitions) if v==c]
        centroids[ic]=mean(data[ind])
    end
    return centroids
end

# Epsilon Network using farthest first traversal Algorithm

function enet(N::Net,num_of_centers::Int; distance=L2SquaredDistance(),
              per_class=false,reftype=:centroids, kernel=linear)
    N.distance=distance
    N.kernel=kernel
    n=length(N.data)
    partitions=[0 for i in 1:n]
    gcenters,dists,sigmas=Vector{Int}(0),Vector{Float64}(num_of_centers-1),Dict{Int,Float64}()
    indices=[[i for i in  1:n]]
    for ind in indices
        centers=Vector{Int}(0)
        s=rand(1:length(ind))
        push!(centers,ind[s])
        #ll=N.labels[ind[s]]
        index=KnnResult(length(ind))
        partitions[ind[s]]=ind[s]
        k=1
        while  k<=num_of_centers-1 && k<=length(ind)
            fnid,d=maxmin(N.data,centers,ind,index,distance,partitions)
            push!(centers,fnid)
            dists[k]=d
            partitions[fnid]=fnid
            k+=1
        end
        sigmas[0]=minimum(dists)
        gcenters=vcat(gcenters,centers)
    end
    N.references,N.partitions,N.dists,N.sigmas=gcenters,partitions,dists,sigmas
    N.centers,N.centroids=N.data[gcenters],get_centroids(N.data,partitions)
    N.csigmas,N.stats=get_csigmas(N.data,N.centroids,N.partitions,distance=N.distance)
    N.reftype=reftype
end

# KMeans ++ seeding Algorithm

function kmpp(N::Net,num_of_centers::Int)::Vector{Int}
    n=length(N.data)
    s=rand(1:n)
    centers, d = Vector{Int}(num_of_centers), L2SquaredDistance()
    centers[1]=s
    D=[d(N.data[j],N.data[s]) for j in 1:n]
    for i in 1:num_of_centers-1
        cp=cumsum(D/sum(D))
        r=rand()
        sl=[j for j in 1:length(cp) if cp[j]>=r]
        s=sl[1]
        centers[i+1]=s
        for j in 1:n
            dist=d(N.data[j],N.data[s])
            if dist<D[j]
                D[j]=dist
            end
        end
    end
    centers
end

#Assign Elementes to thier nearest centroid

function assign(data,centroids,partitions;distance=L2SquaredDistance())
    d=distance
    for i in 1:length(data)
        partitions[i]=sortperm([d(data[i],c) for c in centroids])[1]
    end
end

#Distances for each element to its nearest cluster centroid

function get_distances(data,centroids,partitions;distance=L2SquaredDistance())::Vector{Float64}
    dists=Vector{Float64}(length(centroids))
    for i in 1:length(centroids)
        ind=[j for (j,l) in enumerate(partitions) if l==i]
        if length(ind)>0
            X=data[ind]
            dd=[distance(centroids[i],x) for x in X]
            dists[i]=maximum(dd)
        end
    end
    sort!(dists)
    return dists
end

#Calculated the sigma for each ball

function get_csigmas(data,centroids,partitions;distance=L2SquaredDistance())::Tuple{Vector{Float64},Dict{String,Float64}}
    stats=Dict("SSE"=>0.0,"BSS"=>0.0)
    refs=[j for j in Set(partitions)]
    sort!(refs)
    csigmas=Vector{Float64}(length(refs))
    df=distance
    m=mean(data)
    for (ii,i) in enumerate(refs)
        ind=[j for (j,l) in enumerate(partitions) if l==i]
        #if length(ind)>0
        X=data[ind]
        dd=[df(data[i],x) for x in X]
        csigmas[ii]=max(0,maximum(dd))
        stats["SSE"]+=sum(dd)
        stats["BSS"]+=length(X)*(sum(mean(X)-m))^2
        #end
    end
    return csigmas,stats
end

#Feature generator using kmeans centroids

function kmnet(N::Net,num_of_centers::Int; max_iter=1000,kernel=linear,distance=L2SquaredDistance(),reftype=:centroids)
    n=length(N.data)
    #lK,partitions=[[] for i in 1:n],[0 for i in 1:n],[0 for i in 1:n]
    partitions=[0 for i in 1:n]
    dists=Vector{Float64}
    init=kmpp(N,num_of_centers)
    centroids=N.data[init]
    i,aux=1,Vector{Float64}(length(centroids))
    while centroids != aux && i < max_iter
        i=i+1
        aux = centroids
        assign(N.data,centroids,partitions)
        centroids=get_centroids(N.data,partitions)
    end
    N.distance=distance
    dists=get_distances(N.data,centroids,partitions,distance=N.distance)
    N.partitions,N.dists=partitions,dists
    N.centroids,N.sigmas[0]=centroids,maximum(N.dists)
    N.csigmas,N.stats=get_csigmas(N.data,N.centroids,N.partitions,distance=N.distance)
    N.sigmas[0]=maximum(N.csigmas)
    N.reftype=:centroids
    N.kernel=kernel
end

#Feature generator using naive algorithm for density net

function dnet(N::Net,num_of_elements::Int64; distance=L2SquaredDistance(),kernel=linear,reftype=:centroids)
    n,d,k=length(N.data),distance,num_of_elements
    partitions,references=[0 for i in 1:n],Vector{Int}(0)
    pk=1
    dists,sigmas=Vector{Float64},Dict{Int,Float64}()
    while 0 in partitions
        pending=[j for (j,v) in enumerate(partitions) if partitions[j]==0]
        s=rand(pending)
        partitions[s]=s
        pending=[j for (j,v) in enumerate(partitions) if partitions[j]==0]
        push!(references,s)
        pc=sortperm([d(N.data[j],N.data[s]) for j in pending])
        if length(pc)>=k
            partitions[pending[pc[1:k]]]=s
        else
            partitions[pending[pc]]=s
        end
    end
    N.references,N.partitions=references,partitions
    N.centers,N.centroids=N.data[references],get_centroids(N.data,partitions)
    N.csigmas,N.stats=get_csigmas(N.data,N.centroids,N.partitions,distance=N.distance)
    N.sigmas[0]=maximum(N.csigmas)
    N.reftype=:centroids
    N.distance=distance
    N.kernel=kernel
end

#Generates feature espace using cluster centroids or centers

function gen_features(Xo::Vector{T},N::Net)::Vector{Vector{Float64}} where T
    n=length(Xo)
    sigmas,Xr=N.csigmas,Vector{T}(n)
    Xm = N.reftype==:centroids || length(N.centers)==0 ? N.centroids : N.centers
    nf=length(Xm[1])
    kernel=N.kernel
    for i in 1:n
        xd=Vector{Float64}(nf)
        for j in 1:nf
            xd[j]=kernel(Xo[i],Xm[j],sigma=sigmas[j],distance=N.distance)
        end
        Xr[i]=xd
    end
    Xr
end

function traintest(N; op_function=recall, runs=3, folds=0, trainratio=0.7, testratio=0.3)
    clf, avg = nb.GaussianNB(), Vector{Float64}(runs)
    skf = ms.ShuffleSplit(n_splits=runs, train_size=trainratio, test_size=testratio)
    X=gen_features(N.data,N)
    y=N.labels
    skf[:get_n_splits](X,y)
    for (ei,vi) in skf[:split](X,y)
        ei,vi=ei+1,vi+1
        xt,xv=X[ei],X[vi]
        yt,yv=y[ei],y[vi]
        clf[:fit](xt,yt)
        y_pred=clf[:predict](xv)
        push!(avg,op_function(yv,y_pred))
    end
    #println("========== ",length(N.centroids) ,"  ",avg/folds)
    #@show typeof(clf)
    clf[:fit](X,y)
    return clf,mean(avg)
end

#function transductive(){
#    continue
#}

function predict_test(xt,y,xv,desc,cl)::Vector{Int64}
    y_pred=[]
    if contains(desc,"KNN")
        #@show typeof(xt), typeof(y), typeof(cl.X.dist), Symbol(cl.X.dist)
        cln=NearNeighborClassifier(xt,y,cl.X.dist,cl.k,cl.weight)
        y_pred=[predict_one(cln,x)[1] for x in xv]
    else
        cln=nb.GaussianNB()
        cln[:fit](xt,y)
        y_pred=cln[:predict](xv)
    end
    y_pred
end

HammingDistance(x1,x2)::Float64 = length(x1)-sum(x1.==x2)

L2Squared = L2SquaredDistance()

function KlusterClassifier(Xe, Ye; op_function=recall,
                        K=[4, 8, 16, 32],
                        kernels=[:gaussian, :sigmoid, :linear, :cauchy],
                        runs=3,
                        trainratio=0.6,
                        testratio=0.4,
                        folds=0,
                        top_k=32,
                        threshold=0.03,
                        distances=[:cosine, :L2Squared],
                        nets=[:enet, :kmnet, :dnet], nsplits=3)::Vector{Tuple{Tuple{Any,Net},Float64,String}}
    top=Vector{Tuple{Float64,String}}(0)
    DNNC=Dict()

    for (k, nettype, reftype, kernel, distancek) in zip(K, nets, [:centers, :centroids], kernels, distances)
        if (distancek==:L2Squared || reftype==:centers) && nettype=="kmeans"
            continue
        else
            N=Net(Xe,Ye)
            eval(nettype)(N, k, kernel=eval(kernel), distance = eval(distancek), reftype=reftype)
            X=gen_features(N.data, N)
        end
        for distance in distances
            nnc = NearNeighborClassifier(X,Ye, eval(distance))
            opval,_tmp=optimize!(nnc, op_function,runs=runs, trainratio=trainratio,
                                    testratio=testratio,folds=folds)[1]
            kknn,w = _tmp
            key="$nettype/$kernel/$k/KNN$kknn/$reftype/$distance/$w"
            push!(top,(opval,key))
            DNNC[key]=(nnc,N)
        end
        key="$nettype/$kernel/$k/NaiveBayes/$reftype/NA"
        nbc,opval=traintest(N,op_function=op_function,folds=folds,
                            trainratio=trainratio,testratio=testratio)
        push!(top,(opval,key))
        DNNC[key]=(nbc,N)
    end

    sort!(top, rev=true)
    top=top[1:min(12, length(top))]
    # if top_k>0
    #     top=ctop[1:k]
    # else
    #     top=[t for t in top if (ctop[1][1]-t[1])<threshold]
    # end
    # @show length(top)
    LN=[(DNNC[t[2]],t[1],t[2]) for t in top ]
    #@show typeof(LN)
end

function ensemble_cfft(knc,k::Int64=7;testratio=0.4,distance=HammingDistance)::Vector{Tuple{Tuple{Any,Net},Float64,String}}
    (cl,n),opv,desc=knc[1]
    data=Vector{Vector{String}}(length(knc))
    for i in 1:length(knc)
        kc,opv,desc=knc[i]
        v=split(desc,"/")[1:6]
        data[i]=v
    end
    ind=[i for (i,x) in enumerate(data)]
    partitions,centers,index=[0 for x in ind],[1],KnnResult(k)
    while length(centers)<k && length(centers)<=length(ind)
        oid,dist=maxmin(data,centers,ind,index,distance,partitions)
        push!(centers,oid)
    end
    knc[centers]
end

function ensemble_pfft(knc,k::Int64=7;trainratio=0.6,distance=HammingDistance)::Vector{Tuple{Tuple{Any,Net},Float64,String}}
    (cl,N),opv,desc=knc[1]
    n=length(N.data)
    tn=Int(trunc(n*trainratio));
    data=Vector{Vector{Int}}(length(knc))
    perm=randperm(n)
    ti,vi=perm[1:tn],perm[tn+1:n]
    for i in 1:length(knc)
        (cl,N),opv,desc=knc[i]
        xv=gen_features(N.data[vi],N)
        xt=gen_features(N.data[ti],N)
        y=N.labels[ti]
        data[i]=predict_test(xt,y,xv,desc,cl)
    end
    ind=[i for (i,x) in enumerate(data)]
    partitions,centers,index=[0 for x in ind],[1],KnnResult(k)
    while length(centers)<k && length(centers)<=length(ind)
        oid,dist=maxmin(data,centers,ind,index,distance,partitions)
        push!(centers,oid)
    end
    knc[centers]
end

function predict(knc,X;ensemble_k=1)::Vector{Int64}
    y_t=Vector{Int}(0)
    for i in 1:ensemble_k
        kc,opv,desc=knc[i]
        cl,N=kc
        xv=gen_features(X,N)
        if contains(desc,"KNN")
            y_i=[predict_one(cl,x)[1] for x in xv]
        else
            y_i=cl[:predict](xv)
        end
        y_t = length(y_t)>0 ? hcat(y_t,y_i) : hcat(y_i)
    end
    y_pred=Vector{Int}(length(X))
    for i in 1:length(y_t[:,1])
        y_r=y_t[i,:]
        y_pred[i]=last(sort([(count(x->x==k,y_r),k) for k in unique(y_r)]))[2]
    end
    y_pred
end

function predict_proba(knc,X;ensemble_k=1):Vector{Vector{Float64}}
    kc,opv,desc=knc[1]
    cl,N=kc
    xv=gen_features(X,N)
    if contains(desc,"KNN")
        y_pred=[predict_one_proba(cl,x) for x in xv]
    else
        y_pred=cl[:predict_proba](xv)
    end
    y_pred
end

end


using MathOptInterface
const MOI = MathOptInterface
const MOIU = MOI.Utilities

# indices
const VI = MOI.VariableIndex
const CI = MOI.ConstraintIndex
# supported functions
const SAF = MOI.ScalarAffineFunction{Float64}
const SQF = MOI.ScalarQuadraticFunction{Float64}
const VAF = MOI.VectorAffineFunction{Float64}
const VECTOR = MOI.VectorOfVariables
# supported sets
const BOUNDS = Union{MOI.EqualTo{Float64}, MOI.GreaterThan{Float64},
                     MOI.LessThan{Float64}, MOI.Interval{Float64}}
const VAR_TYPES = Union{MOI.ZeroOne, MOI.Integer}
const SOS1 = MOI.SOS1{Float64}
const SOS2 = MOI.SOS2{Float64}
# other MOI types
const AFF_TERM = MOI.ScalarAffineTerm{Float64}
const QUAD_TERM = MOI.ScalarQuadraticTerm{Float64}
const VEC_TERM = MOI.VectorAffineTerm{Float64}

const PtrMap = Dict{Ptr{Cvoid}, Union{VarRef, ConsRef}}
const ConsTypeMap = Dict{Tuple{DataType, DataType}, Set{ConsRef}}

mutable struct Optimizer <: MOI.AbstractOptimizer
    inner::SCIPData
    reference::PtrMap
    constypes::ConsTypeMap
    binbounds::Dict{VI,BOUNDS} # only for binary variables
    params::Dict{String,Any}
    start::Dict{VI,Float64} # can be partial
    moi_separator::Any # ::Union{CutCbSeparator, Nothing}
    objective_sense::MOI.OptimizationSense

    function Optimizer(; kwargs...)
        scip = Ref{Ptr{SCIP_}}(C_NULL)
        @SCIP_CALL SCIPcreate(scip)
        @assert scip[] != C_NULL
        @SCIP_CALL SCIPincludeDefaultPlugins(scip[])
        @SCIP_CALL SCIP.SCIPcreateProbBasic(scip[], "")

        scip_data = SCIPData(scip, Dict(), Dict(), 0, 0, Dict(), Dict(), Dict(), [])

        o = new(scip_data, PtrMap(), ConsTypeMap(), Dict(), Dict(), Dict(), nothing, MOI.MIN_SENSE)
        finalizer(free_scip, o)

        # Set all parameters given as keyword arguments, replacing the
        # delimiter, since "/" is used by all SCIP parameters, but is not
        # allowed in Julia identifiers.
        for (key, value) in kwargs
            name = replace(String(key),"_" => "/")
            MOI.set(o, MOI.RawOptimizerAttribute(name), value)
        end
        return o
    end
end

free_scip(o::Optimizer) = free_scip(o.inner)

Base.cconvert(::Type{Ptr{SCIP_}}, o::Optimizer) = o
# Protect Optimizer from GC for ccall with Ptr{SCIP_} argument.
Base.unsafe_convert(::Type{Ptr{SCIP_}}, o::Optimizer) = o.inner.scip[]

## convenience functions (not part of MOI)

"Return pointer to SCIP variable."
function var(o::Optimizer, v::VI)::Ptr{SCIP_VAR}
    return var(o.inner, VarRef(v.value))
end

"Return var/cons reference of SCIP variable/constraint."
ref(o::Optimizer, ptr::Ptr{Cvoid}) = o.reference[ptr]

"Return pointer to SCIP constraint."
function cons(o::Optimizer, c::CI{F,S})::Ptr{SCIP_CONS} where {F,S}
    return cons(o.inner, ConsRef(c.value))
end

"Extract bounds from sets."
bounds(set::MOI.EqualTo{Float64}) = (set.value, set.value)
bounds(set::MOI.GreaterThan{Float64}) = (set.lower, nothing)
bounds(set::MOI.LessThan{Float64}) = (nothing, set.upper)
bounds(set::MOI.Interval{Float64}) = (set.lower, set.upper)

"Make set from bounds."
from_bounds(::Type{MOI.EqualTo{Float64}}, lower, upper) = MOI.EqualTo{Float64}(lower)
from_bounds(::Type{MOI.GreaterThan{Float64}}, lower, upper) = MOI.GreaterThan{Float64}(lower)
from_bounds(::Type{MOI.LessThan{Float64}}, lower, upper) = MOI.LessThan{Float64}(upper)
from_bounds(::Type{MOI.Interval{Float64}}, lower, upper) = MOI.Interval{Float64}(lower, upper)

"Register pointer in mapping, return var/cons reference."
function register!(o::Optimizer, ptr::Ptr{Cvoid}, ref::R) where R <: Union{VarRef, ConsRef}
    @assert !haskey(o.reference, ptr)
    o.reference[ptr] = ref
    return ref
end

"Register constraint in mapping, return constraint reference."
function register!(o::Optimizer, c::CI{F,S}) where {F,S}
    cr = ConsRef(c.value)
    if haskey(o.constypes, (F, S))
        push!(o.constypes[F,S], cr)
    else
        o.constypes[F,S] = Set([cr])
    end
    return c
end

"Go back from solved stage to problem modification stage, invalidating results."
function allow_modification(o::Optimizer)
    if !(SCIPgetStage(o) in (SCIP_STAGE_PROBLEM, SCIP_STAGE_SOLVING))
        @SCIP_CALL SCIPfreeTransform(o)
    end
    return nothing
end

## general queries and support

MOI.get(::Optimizer, ::MOI.SolverName) = "SCIP"

MOI.supports_incremental_interface(::Optimizer) = true

function _throw_if_invalid(o::Optimizer, ci::CI{F, S}) where {F, S}
    if !haskey(o.constypes, (F, S)) || !in(ConsRef(ci.value), o.constypes[F, S])
        throw(MOI.InvalidIndex(ci))
    end
    return nothing
end

function MOI.get(o::Optimizer, param::MOI.RawOptimizerAttribute)
    return get_parameter(o.inner, param.name)
end

function MOI.set(o::Optimizer, param::MOI.RawOptimizerAttribute, value)
    set_parameter(o.inner, param.name, value)
    o.params[param.name] = value
    return nothing
end

MOI.supports(o::Optimizer, ::MOI.Silent) = true

function MOI.get(o::Optimizer, ::MOI.Silent)
    return MOI.get(o, MOI.RawOptimizerAttribute("display/verblevel")) == 0
end

function MOI.set(o::Optimizer, ::MOI.Silent, value)
    param = MOI.RawOptimizerAttribute("display/verblevel")
    if value
        MOI.set(o, param, 0) # no output at all
    else
        MOI.set(o, param, 4) # default level
    end
end

MOI.supports(o::Optimizer, ::MOI.TimeLimitSec) = true

function MOI.get(o::Optimizer, ::MOI.TimeLimitSec)
    raw_value = MOI.get(o, MOI.RawOptimizerAttribute("limits/time"))
    if raw_value == SCIPinfinity(o)
        return nothing
    else
        return raw_value
    end
end

function MOI.set(o::Optimizer, ::MOI.TimeLimitSec, value)
    if value === nothing
        MOI.set(o, MOI.RawOptimizerAttribute("limits/time"), SCIPinfinity(o))
    else
        MOI.set(o, MOI.RawOptimizerAttribute("limits/time"), value)
    end
end

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

MOI.get(::Optimizer, ::MOI.SolverVersion) = "v" * string(SCIP_versionnumber())

## model creation, query and modification

function MOI.is_empty(o::Optimizer)
    return length(o.inner.vars) == 0 && length(o.inner.conss) == 0
end

function MOI.empty!(o::Optimizer)
    # free the underlying problem
    free_scip(o.inner)
    # clear auxiliary mapping structures
    o.reference = PtrMap()
    o.constypes = ConsTypeMap()
    o.binbounds = Dict()
    o.start = Dict()
    # manually recreate empty o.inner (formerly done by creating a new mscip before ManagedSCIP was removed)
    scip = Ref{Ptr{SCIP_}}(C_NULL)
    @SCIP_CALL SCIPcreate(scip)
    @assert scip[] != C_NULL
    @SCIP_CALL SCIPincludeDefaultPlugins(scip[])
    @SCIP_CALL SCIP.SCIPcreateProbBasic(scip[], "")
    # create a new problem
    o.inner = SCIPData(scip, Dict(), Dict(), 0, 0, Dict(), Dict(), Dict(), [])
    # reapply parameters
    for pair in o.params
        set_parameter(o.inner, pair.first, pair.second)
    end
    return nothing
end

function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike)
    return MOIU.default_copy_to(dest, src)
end

MOI.get(o::Optimizer, ::MOI.Name) = unsafe_string(SCIPgetProbName(o))
MOI.set(o::Optimizer, ::MOI.Name, name::String) = @SCIP_CALL SCIPsetProbName(o, name)

function MOI.get(o::Optimizer, ::MOI.NumberOfConstraints{F,S}) where {F,S}
    return haskey(o.constypes, (F, S)) ? length(o.constypes[F, S]) : 0
end

function MOI.get(o::Optimizer, ::MOI.ListOfConstraintTypesPresent)
    return collect(keys(o.constypes))
end

function MOI.get(o::Optimizer, ::MOI.ListOfConstraintIndices{F, S}) where {F, S}
    list_indices = Vector{CI{F,S}}()
    if !haskey(o.constypes, (F, S))
        return list_indices
    end
    for cref in o.constypes[F, S]
        push!(list_indices, CI{F,S}(cref.val))
    end
    return sort!(list_indices, by=v->v.value)
end

function set_start_values(o::Optimizer)
    if isempty(o.start)
        # no primal start values are given
        return
    end

    # create new partial solution object
    sol__ = Ref{Ptr{SCIP_SOL}}(C_NULL)
    @SCIP_CALL SCIPcreatePartialSol(o, sol__, C_NULL)
    @assert sol__[] != C_NULL

    # set all given values
    sol_ = sol__[]
    for (vi, value) in o.start
        @SCIP_CALL SCIPsetSolVal(o, sol_, var(o, vi), value)
    end

    # submit the candidate
    stored_ = Ref{SCIP_Bool}(FALSE)
    @SCIP_CALL SCIPaddSolFree(o, sol__, stored_)
    @assert sol__[] == C_NULL
end

function MOI.optimize!(o::Optimizer)
    set_start_values(o)
    if o.objective_sense == MOI.FEASIBILITY_SENSE
        MOI.set(o, MOI.ObjectiveFunction{SAF}(), SAF([], 0.0))
    end
    @SCIP_CALL SCIPsolve(o)
    return nothing
end

function MOI.delete(o::Optimizer, ci::CI{F, S}) where {F, S}
    _throw_if_invalid(o, ci)
    allow_modification(o)
    delete!(o.constypes[F, S], ConsRef(ci.value))
    if isempty(o.constypes[F, S])
        delete!(o.constypes, (F, S))
    end
    delete!(o.reference, cons(o, ci))
    delete(o.inner, ConsRef(ci.value))
    return nothing
end

function MOI.get(o::Optimizer, ::MOI.ListOfVariableAttributesSet)
    attributes = MOI.AbstractVariableAttribute[MOI.VariableName()]
    if !isempty(o.start)
        push!(attributes, MOI.VariablePrimalStart())
    end
    return attributes
end

MOI.get(::Optimizer, ::MOI.ListOfModelAttributesSet) = MOI.AbstractModelAttribute[MOI.Name(), MOI.ObjectiveSense()]

function MOI.get(
    ::Optimizer,
    ::MOI.ListOfConstraintAttributesSet{F,S},
) where {F,S}
    attributes = MOI.AbstractConstraintAttribute[]
    if F != MOI.VariableIndex
        return push!(attributes, MOI.ConstraintName())
    end
    return attributes
end

function MOI.get(::Optimizer, ::MOI.ListOfOptimizerAttributesSet)
    attributes = MOI.ListOfOptimizerAttributesSet[]
    timelim = MOI.get(o, MOI.TimeLimitSec())
    if timelim !== nothing
        push!(attributes, MOI.TimeLimitSec())
    end
    return attributes
end

include(joinpath("MOI_wrapper", "variable.jl"))
include(joinpath("MOI_wrapper", "constraints.jl"))
include(joinpath("MOI_wrapper", "linear_constraints.jl"))
include(joinpath("MOI_wrapper", "quadratic_constraints.jl"))
include(joinpath("MOI_wrapper", "sos_constraints.jl"))
include(joinpath("MOI_wrapper", "indicator_constraints.jl"))
include(joinpath("MOI_wrapper", "nonlinear_constraints.jl"))
include(joinpath("MOI_wrapper", "objective.jl"))
include(joinpath("MOI_wrapper", "results.jl"))
include(joinpath("MOI_wrapper", "conshdlr.jl"))
include(joinpath("MOI_wrapper", "sepa.jl"))


# define the generative model for the multiple intersection environment

### REWARD MODEL ##################################################################################

function POMDPs.reward(pomdp::OIPOMDP, s::OIState, a::OIAction, sp::OIState)
    r = 0.
    ego = sp[findfirst(EGO_ID, sp)]
    if is_crash(sp)
        r += pomdp.collision_cost
    end
    if ego.state.posF.s >= get_end(pomdp.env.roadway[pomdp.ego_goal]) &&
       get_lane(pomdp.env.roadway, ego).tag == pomdp.ego_goal
        r += pomdp.goal_reward
    elseif a.acc > 0.
        r += pomdp.action_cost
    else
        r += pomdp.action_cost
    end
    return r
end

### TERMINAL STATES ###############################################################################

function POMDPs.isterminal(pomdp::OIPOMDP, s::OIState)
    ego = s[findfirst(EGO_ID, s)]
    return is_crash(s) || (ego.state.posF.s >= get_end(pomdp.env.roadway[pomdp.ego_goal]) &&
                           get_lane(pomdp.env.roadway, ego).tag == pomdp.ego_goal)
end

### TRANSITION MODEL ##############################################################################

function POMDPs.transition(pomdp::OIPOMDP, s::OIState, a::OIAction)
    ImplicitDistribution(s, a) do s, a, rng
        actions = Array{Any}(length(s))
        pomdp.models[1].a = a
        is_ego_here = clamp(findfirst(EGO_ID, s),0, 1)
        sp = deepcopy(s) #XXX bad
        max_id = 0
        for veh in sp
            if veh.id > max_id
                max_id = veh.id
            end
        end
        if rand(rng) < pomdp.p_birth && max_id < pomdp.max_cars +  is_ego_here
            new_car = initial_car(pomdp, sp, rng)
            if can_push(pomdp.env, sp, new_car)
                lane = get_lane(pomdp.env.roadway, new_car)
                route = random_route(rng, pomdp.env.roadway, lane)
                intersection_entrances = get_start_lanes(pomdp.env.roadway)
                if !(route[1] ∈ intersection_entrances)
                    intersection = Lane{Float64}[]
                    intersection_exits = Lane{Float64}[]
                else
                    intersection_exits = get_exit_lanes(pomdp.env.roadway)
                    intersection=Lane{Float64}[route[1], route[2]]
                end
                pomdp.models[new_car.id] = StopIntersectionDriver(navigator=RouteFollowingIDM(route=route),
                                                        intersection=intersection,
                                                        intersection_entrances = intersection_entrances,
                                                        intersection_exits = intersection_exits,
                                                        stop_delta=0.,
                                                        accel_tol=0.,
                                                        priorities = pomdp.env.priorities)
                push!(sp, new_car)
            end
            actions = Array{Any}(undef, length(sp))
        end
        get_actions!(actions, sp, pomdp.env.roadway, pomdp.models)
        tick!(sp, pomdp.env.roadway, actions, pomdp.ΔT)
        clean_scene!(pomdp.env, sp)
        return sp
    end
end

#TODO move to helpers

function AutomotiveDrivingModels.propagate(veh::Vehicle, a::OIAction, roadway::Roadway, Δt::Float64)
    acc = LonAccelDirection(a.acc, 4) #TODO Parameterize
    return propagate(veh, acc, roadway, Δt)
end

### INITIAL STATES ################################################################################

function POMDPs.initialstate(pomdp::OIPOMDP)
    ImplicitDistribution() do rng
        burn_in = 1 # this is now redundant
        scene = initial_scene(pomdp, rng)
        # clean_scene!(pomdp.env, scene)
        for t = 1:burn_in
            scene = @gen(:sp)(pomdp, scene, OIAction(0.), rng)
        end
        return scene
    end
end

function initial_scene(pomdp::OIPOMDP, rng::AbstractRNG, no_ego::Bool=false)
    scene = Scene()
    if !no_ego
        ego = initial_ego(pomdp, rng)
        push!(scene, ego)
    end
    for i=1:pomdp.max_cars
        if rand(rng) < pomdp.p_birth && n_cars(scene) < pomdp.max_cars + 1-Int(no_ego)
            new_car = initial_car(pomdp, scene, rng, true)
            if can_push(pomdp.env, scene, new_car)
                push!(scene, new_car)
                lane = get_lane(pomdp.env.roadway, new_car)
                route = random_route(rng, pomdp.env.roadway, lane)
                intersection_entrances = get_start_lanes(pomdp.env.roadway)
                if !(route[1] ∈ intersection_entrances)
                    intersection = Lane{Float64}[]
                    intersection_exits = Lane{Float64}[]
                else
                    intersection_exits = get_exit_lanes(pomdp.env.roadway)
                    intersection=Lane{Float64}[route[1], route[2]]
                end
                pomdp.models[new_car.id] = StopIntersectionDriver(navigator=RouteFollowingIDM(route=route),
                                                           intersection=intersection,
                                                           intersection_entrances = intersection_entrances,
                                                           intersection_exits = intersection_exits,
                                                           stop_delta=0.,
                                                           accel_tol=0.,
                                                           priorities = pomdp.env.priorities)
            end
            # pomdp.models[new_car.id] = RouteFollowingIDM(route = random_route(rng,
            #                                                                   pomdp.env.roadway,
            #                                                                   get_lane(pomdp.env.roadway, new_car)))
            clean_scene!(pomdp.env, scene)
        end
    end

    return scene
end

function initial_car(pomdp::OIPOMDP, scene::Scene, rng::AbstractRNG, first_scene::Bool = false)
    env = pomdp.env

    #velocity
    v0 = rand(rng, 4.0:1.0:8.0) #XXX parameterize!
    s0 = 0.
    if first_scene
        lane = rand(rng, get_lanes(pomdp.env.roadway)) # could be precomputed
        s0 = rand(rng, 0.:0.5:get_end(lane))
    else
        lane = rand(rng, get_start_lanes(pomdp.env.roadway)) # could be precomputed
    end
    initial_posF = Frenet(lane, s0)
    initialstate = VehicleState(initial_posF, env.roadway, v0)

    # new id, increment last id
    max_id = 0
    for veh in scene
        if veh.id > max_id
            max_id = veh.id
        end
    end
    id = max_id + 1
    if max_id == 0
        id = 2
    end

    return Vehicle(initialstate, pomdp.car_type, id)
end

function initial_ego(pomdp::OIPOMDP, rng::AbstractRNG)
    posF = Frenet(get_ego_route(pomdp)[1], pomdp.ego_start)
    state = VehicleState(posF, pomdp.env.roadway, 0.)
    return Vehicle(state, pomdp.ego_type, EGO_ID)
end

### Observations ##################################################################################

# uncomment for vector representation
# TODO find a better way to implement this to switch more easily between representations
function POMDPs.observation(pomdp::OIPOMDP, s::Scene, a::OIAction, sp::Scene)
    ImplicitDistribution(s, a, sp) do s, a, sp, rng
        n_features = 4
        pos_noise = pomdp.pos_obs_noise
        vel_noise = pomdp.vel_obs_noise
        o = zeros(n_features*(pomdp.max_cars + 1))
        ego = sp[findfirst(EGO_ID, sp)].state
        o[1] = ego.posG.x
        o[2] = ego.posG.y
        o[3] = ego.posG.θ
        o[4] = ego.v
        car_off = get_off_the_grid(pomdp)
        for i=2:pomdp.max_cars+1
            o[n_features*i - 3] = car_off.posG.x
            o[n_features*i - 2] = car_off.posG.y
            o[n_features*i - 1] = car_off.posG.θ
            o[n_features*i] = 0.
        end
        for veh in sp
            if veh.id == EGO_ID
                continue
            end
            @assert veh.id <= pomdp.max_cars+1
            car = veh.state
            if is_observable_fixed(ego, car, pomdp.env)
                o[n_features*veh.id - 3] = car.posG.x + pos_noise*randn(rng)
                o[n_features*veh.id - 2] = car.posG.y + pos_noise*randn(rng)
                o[n_features*veh.id - 1] = car.posG.θ
                o[n_features*veh.id] = car.v + vel_noise*randn(rng)
            end
        end
        return o
    end
end

function POMDPs.initialobs(pomdp::OIPOMDP, s::OIState)
    return observation(pomdp, s, OIAction(0.), s)
end

function POMDPs.convert_o(::Type{Vector{Float64}}, o::OIObs, pomdp::OIPOMDP)
    # rescale
    n_features = 4
    max_ego_dist = get_end(pomdp.env.roadway[pomdp.ego_goal])
    o[1] /= max_ego_dist
    o[2] /= max_ego_dist
    o[3] /= pi/2
    o[4] /= pomdp.env.params.speed_limit
    for i=2:pomdp.max_cars+1
        o[n_features*i - 3] /= max_ego_dist
        o[n_features*i - 2] /= max_ego_dist
        o[n_features*i - 1] /= pi
        o[n_features*i] /= pomdp.env.params.speed_limit # XXX parameterized
    end
    return o
end

#### Helpers
# """
#     ego_state(pomdp:ICPOMDP, s::Float64, v::Float64)
# Helper that returns the ego car state given its position on the lane and the velocity
# """
# function ego_state(pomdp::OIPOMDP, s::Float64, v::Float64)
#     # ego_lane = pomdp.env.lane_map["ego_left"] #TODO parameterized
#     posF = Frenet(get_ego_route(pomdp)[1], s)
#     return VehicleState(posF, pomdp.env.roadway, v)
# end

"""
    car_state(pomdp::OIPOMDP, lane_id::String, s::Float64, v::Float64)
    car_state(pomdp::OIPOMDP, lane::Lane, s::Float64, v::Float64)
    car_state(pomdp::OIPOMDP, x::Float64, y::Float64, θ::Float64, v::Float64)
Helper that returns the state of a car given the lane and the position on the lane
"""
function car_state(pomdp::OIPOMDP, lane_id::String, s::Float64, v::Float64)
    lane = pomdp.env.lane_map[lane_id]
    posF = Frenet(lane, s)
    return VehicleState(posF, pomdp.env.roadway, v)
end
function car_state(pomdp::OIPOMDP, lane::Lane, s::Float64, v::Float64)
    posF = Frenet(lane, s)
    return VehicleState(posF, pomdp.env.roadway, v)
end
function car_state(pomdp::OIPOMDP, x::Float64, y::Float64, θ::Float64, v::Float64)
    posG = VecSE2(x, y, θ)
    return VehicleState(posG, pomdp.env.roadway, v)
end

"""
    get_off_the_grid(pomdp::OIPOMDP)
return the off the grid state
"""
function get_off_the_grid(pomdp::OIPOMDP)
    posG = pomdp.off_grid
    return VehicleState(posG, pomdp.env.roadway, 0.)
end

"""
Reconstruct a scene object from an OIPOMDP observation
"""
function obs_to_scene(pomdp::OIPOMDP, o::OIObs)
    # rebuild the scene from the observation
    scene = Scene()
    for id=1:pomdp.max_cars+1
        x, y, θ, v = o[4*id-3], o[4*id-2], o[4*id-1], o[4*id]
        if VecSE2(x, y, θ) == pomdp.off_grid
            continue
        end
        state = car_state(pomdp, x, y, θ, v)
        car_type = id == 1 ? pomdp.ego_type : pomdp.car_type
        veh = Vehicle(state, car_type, id)
        push!(scene, veh)
    end
    return scene
end

"""
return the list of lanes the ego car should take
"""
function get_ego_route(pomdp::OIPOMDP)
    tags = [LaneTag(6,1), LaneTag(13, 1), LaneTag(2,pomdp.env.params.nlanes_main)]
    lanes = Array{Lane}(undef, length(tags))
    for (i,tag) in enumerate(tags)
        lanes[i] = pomdp.env.roadway[tag]
    end
    lanes
end


# ============================================================================ #
function plot_with_error(x::AbstractArray, y::Vector, yerr::Vector,
    col::AbstractString, ax=nothing; linewidth=4.0, args...)
    plot_with_error(x, y, yerr, parse(Colorant, col), ax, args...)
end
# ---------------------------------------------------------------------------- #
function plot_with_error(x::AbstractArray, y::Vector, yerr::Vector,
    col::ColorTypes.RGB, ax=nothing; linewidth=4.0, args...)
    if ax == nothing
        ax = default_axes()
    end
    col_array = [col.r, col.g, col.b]
    fcol, ecol = shading_color(col_array)
    ax.fill_between(x, y.-yerr, y.+yerr, facecolor=fcol, edgecolor=ecol)
    ax.plot(x, y, "-", color=col_array, linewidth=linewidth, args...)
end
# ---------------------------------------------------------------------------- #
function shading_color(col::Vector{T}) where {T<:Number}
    ferr = 8.0
    fedge = 0.25
    orig = convert(HSV, RGB(col...))
    hsv = HSV(orig.h, orig.s/ferr, 1.0 - (abs(1.0 - orig.s)^ferr/ferr))
    col_err = hsv2rgb(hsv)
    col_edge = (1.0 - fedge) * col_err + fedge * col
    return col_err, col_edge
end
# ---------------------------------------------------------------------------- #
function hsv2rgb(x::ColorTypes.HSV{T}) where {T<:Number}
    # xv = HSV(min([x.h, x.s, x.v], T(1.0))...)
    rgb = convert(RGB, x)
    return T[rgb.r, rgb.g, rgb.b]
end
# ============================================================================ #
plot_hist(x::AbstractVector) = _plot_hist(hist(x)...)
plot_hist(x::AbstractVector, n::Integer) = _plot_hist(hist(x, n)...)
plot_hist(x::AbstractVector, e::AbstractVector) = _plot_hist(hist(x, e)...)
# ============================================================================ #
edges2centers(edges::AbstractVector) = edges[1:end-1] .+ (step(edges)/2.0)
# ============================================================================ #
function _plot_hist(edges::AbstractVector, counts::AbstractVector)
    return qbar(edges2centers(edges), counts, step(edges))
end
# ============================================================================ #
function vline(x::Number; args...)
    plot([x, x], ylim(); args...)
end
# --------------------------------------------------------------------------- #
function vline(x::AbstractArray; args...)
    for item in x
        vline(item; args...)
    end
end
# ============================================================================ #
function hline(y::Number; args...)
    plot(xlim(), [y, y]; args...)
end
# --------------------------------------------------------------------------- #
function hline(y::AbstractArray; args...)
    for item in y
        hline(item; args...)
    end
end
# ============================================================================ #
function qplot(x, y)
    h = default_figure()
    p = plot(x, y, color="blue", linewidth=3.0)

    return h
end
# ============================================================================ #
function qplot(y)
    x = 1:length(y)
    h = qplot(x, y)
    return h
end
# ============================================================================ #
function qbar(x, y, width, col="black")
    h = default_figure()

    #width = absolute bar width
    # add error with yerr=...
    p = bar(x, y, width, color=col)

    return h
end
# ============================================================================ #
function qbar(y)
    x = 1:length(y)
    h = qbar(x-0.5, y, 1)
    return h
end
# ============================================================================ #
function closeall()
    plt.close()
end
# ============================================================================ #
default_figure() = default_figure(figure())
# ---------------------------------------------------------------------------- #
function default_figure(h::PyPlot.Figure)

    if !plt.fignum_exists(h.number)
        h = figure()
    end

    h.set_facecolor("white")
    ax = h.get_axes()
    if !isempty(ax)
        for x in ax
            delaxes(x)
        end
    end

    ax = h.add_axes(default_axes())

    return h
end
# ============================================================================ #
function default_axes(ax=nothing)
    if ax == nothing
        ax = PyPlot.axes()
    end
    #set ticks to face out
    ax.tick_params(direction="out", length=8.0, width=4.0)

    #turn off top and right axes
    ax.spines["right"].set_visible(false)
    ax.spines["top"].set_visible(false)

    #remove top and right tick marks
    tmp = ax.get_xaxis()
    tmp.tick_bottom()

    tmp = ax.get_yaxis()
    tmp.tick_left()

    ax.spines["left"].set_linewidth(4.0)
    ax.spines["bottom"].set_linewidth(4.0)

    return ax
end
# ============================================================================ #

# ============================================================================ #
# NOTES
# ============================================================================ #
#  #update figure
#  h.canvas.draw()
#
#  #close all figures
#  plt.close()


## header to provide surface and context
using Cairo
c = CairoRGBSurface(256,256);
cr = CairoContext(c);

save(cr);
set_source_rgb(cr,0.8,0.8,0.8);    # light gray
rectangle(cr,0.0,0.0,256.0,256.0); # background
fill(cr);
restore(cr);

save(cr);

## original example, following here
move_to(cr, 16.0, 32.0);
curve_to(cr, 16.0, 16.0, 16.0, 16.0, 32.0, 16.0);

opath = Cairo.convert_cairo_path_data(Cairo.copy_path(cr));
dx,dy,ex,ey = path_extents(cr);

fpath = Cairo.convert_cairo_path_data(Cairo.copy_path_flat(cr));

stroke(cr);

restore(cr);

l = 2 # something like a line counter

# loop over all entries and just print out command and available points
for x in opath
    if x.element_type == Cairo.CAIRO_PATH_MOVE_TO
        s0 = "moveto"
    elseif x.element_type == Cairo.CAIRO_PATH_LINE_TO
        s0 = "lineto"
    elseif x.element_type == Cairo.CAIRO_PATH_CURVE_TO
        s0 = "curveto"
    elseif x.element_type == Cairo.CAIRO_PATH_CLOSE_PATH
        s0 = "closepath"
    end
    s1 = repr(x.points)
    move_to(cr,10.0,16.0+(14.0*l))
    l += 1
    show_text(cr,s0*s1); #
end

l = 2 # something like a line counter

# loop over all entries and just print out command and available points
for x in fpath
    if x.element_type == Cairo.CAIRO_PATH_MOVE_TO
        s0 = "moveto"
    elseif x.element_type == Cairo.CAIRO_PATH_LINE_TO
        s0 = "lineto"
    elseif x.element_type == Cairo.CAIRO_PATH_CURVE_TO
        s0 = "curveto"
    elseif x.element_type == Cairo.CAIRO_PATH_CLOSE_PATH
        s0 = "closepath"
    end
    s1 = repr(x.points)
    move_to(cr,10.0,50.0+(14.0*l))
    l += 1
    show_text(cr,s0*s1); #
end

## mark picture with current date
restore(cr);

move_to(cr,0.0,12.0);
set_source_rgb(cr, 0,0,0);
show_text(cr,Libc.strftime(time()));

write_to_png(c,"sample_copy_path_flat.png");


"    BufferedHDF5Dataset2D{T}(g::Union{HDF5File,HDF5Group}, name, nbases, chunksize)"
mutable struct BufferedHDF5Dataset2D{T}
  ds::HDF5Dataset
  v::Vector{Vector{T}} #naievley you want a 2d array here, but there is no
  # equivalent of push!, so it uses less copying to have a vector of vectors
  # see https://github.com/JuliaLang/julia/issues/10546
  lastrow::Int64 # last index in hdf5 dataset
end
function BufferedHDF5Dataset2D{T}(g::Union{HDF5File,HDF5Group}, name, nbases, chunksize) where T
  ds = d_create(g, name, T, ((nbases,1), (nbases,-1)), "chunk", (nbases,chunksize))
  BufferedHDF5Dataset2D{T}(ds, Vector{Vector{T}}(),0)
end
function write_to_hdf5(b::BufferedHDF5Dataset2D)
  r = b.lastrow .+ (1:length(b.v))
  if length(r)>0
    set_dims!(b.ds, (size(b.ds,1), last(r)) )
    b.ds[:,r] = hcat(b.v...)
    empty!(b.v)
    b.lastrow=last(r)
  end
  return
end
Base.write(b::BufferedHDF5Dataset2D{T},x::Vector{T}) where T = push!(b.v,x)
Base.write(b::BufferedHDF5Dataset2D{T},x::Vector{Vector{T}}) where T = append!(b.v,x)

# d_create(h5, "ds", Float32, ((6,1), (6,-1)), "chunk", (6,chunksize))
# a single pulse is stored in a column, and accessed a[:,1]

struct BasisDataProduct <: DataProduct
    reduced::Vector{Float32} # reduced pulse, aka projection of pulse into subspace defined by basis
    residual_std::Float32
    frame1index::Int
    timestamp_usec::Int
    first_rising_sample::Int32
    nsamples::Int32
end
function Base.write(io::IO, d::BasisDataProduct)
  write(io, d.reduced)
  write(io, d.residual_std)
  write(io, d.frame1index)
  write(io, d.timestamp_usec)
  write(io, d.first_rising_sample)
  write(io, d.nsamples)
end

"""
    BasisAnalyzer(projectors,basis)
    `projectors` has size `(nbases,nsamples)`, basis has size '(nsamples, nabses)'
    comonly called as `BasisAnalyzer(projectors, projectors')` for testing or white noise case
An implementaion of AbstractBasisAnalyzer must have a field `projectors` that is an array with size ``(nbases, nsamples)` such that
the reduced pulse is calculated as `projectors*data` where `data` is a `Vector` of with length `nsamples`. It must also overload calling itself
to call `record2dataproduct`. See Julia issue #14919 for why this is neccesary.
"""
abstract type AbstractBasisAnalyzer end
struct BasisAnalyzer <: AbstractBasisAnalyzer
    projectors::Array{Float32,2} # size (nbases,nsamples)
    basis::Array{Float32,2} # size (nsamples,nbases)
    function BasisAnalyzer(projectors,basis)
      size(projectors) == reverse(size(basis)) || error("size(projectors) must == size(basis')")
      new(projectors,basis)
    end
end
nbases(a::AbstractBasisAnalyzer) = size(a.projectors,1)
nsamples(a::AbstractBasisAnalyzer) = size(a.projectors,2)
check_compatability(a::AbstractBasisAnalyzer, ljh) = nothing
(a::BasisAnalyzer)(record) = record2dataproduct(a,record)
modelreduce(a::AbstractBasisAnalyzer,data::AbstractVector)=a.projectors*data
modelpulse(a::AbstractBasisAnalyzer,reduced)=a.basis*reduced

function record2dataproduct(a::AbstractBasisAnalyzer,record::Union{LJH.LJHRecord, LJH.LJH3Record})
  reduced = modelreduce(a,LJH.data(record))
  modeled = modelpulse(a,reduced)
  residual_std = std(modeled-LJH.data(record))
  BasisDataProduct(reduced, residual_std, LJH.frame1index(record),
    LJH.timestamp_usec(record), LJH.first_rising_sample(record), length(record))
end

mutable struct BasisBufferedWriter <: BufferedWriter
    endchannel           ::Channel{Bool}
    timeout_s            ::Float64
    task                 ::Task
    reduced              ::BufferedHDF5Dataset2D{Float32}
    residual_std         ::BufferedHDF5Dataset{Float32}
    frame1index          ::BufferedHDF5Dataset{Int}
    timestamp_usec       ::BufferedHDF5Dataset{Int}
    first_rising_sample  ::BufferedHDF5Dataset{Int32}
    nsamples             ::BufferedHDF5Dataset{Int32}
end
function BasisBufferedWriter(h5::HDF5File, channel_number, nbases, chunksize, timeout_s;start=true)
  BasisBufferedWriter(g_create(h5,"$channel_number"),nbases,chunksize,timeout_s,start=start)
end
function BasisBufferedWriter(g::HDF5Group, nbases, chunksize, timeout_s;start=true)
  b=BasisBufferedWriter(Channel{Bool}(1), timeout_s, Task(nothing),
  BufferedHDF5Dataset2D{Float32}(g,"reduced", nbases, chunksize),
  BufferedHDF5Dataset{Float32}(g,"residual_std", chunksize),
  BufferedHDF5Dataset{Int}(g,"frame1index", chunksize),
  BufferedHDF5Dataset{Int}(g,"timestamp_usec", chunksize),
  BufferedHDF5Dataset{Int32}(g,"first_rising_sample", chunksize),
  BufferedHDF5Dataset{Int32}(g,"nsamples", chunksize), )
  start && schedule(b)
  b
end
"    hdf5file(b::BasisBufferedWriter)
Return the filename of the hdf5 file associated with `b`."
hdf5file(b::BasisBufferedWriter) = file(b.residual_std.ds)
function write_to_hdf5(b::BasisBufferedWriter)
  write_to_hdf5(b.reduced);write_to_hdf5(b.residual_std);write_to_hdf5(b.frame1index)
  write_to_hdf5(b.timestamp_usec);write_to_hdf5(b.first_rising_sample);write_to_hdf5(b.nsamples)
end
function Base.write(b::BasisBufferedWriter,x::BasisDataProduct)
  write(b.reduced, x.reduced)
  write(b.residual_std, x.residual_std)
  write(b.frame1index, x.frame1index)
  write(b.timestamp_usec, x.timestamp_usec)
  write(b.first_rising_sample, x.first_rising_sample)
  write(b.nsamples, x.nsamples)
  return nothing
end

function write_header(d::BasisBufferedWriter,r)
  channelgroup = parent(d.residual_std.ds)
  channelgroup["header"]="header"
end
function write_header_end(d::BasisBufferedWriter,ljh,analyzer::AbstractBasisAnalyzer)
  # dont add datasets, groups or attributes after SWMR writing is started
  # channelgroup = parent(d.filt_value.ds)
  # channelattrs = attrs(channelgroup)
end
function write_header_allchannel(d::BasisBufferedWriter, r::LJHReaderFeb2017)
  # should be called exact once when `schedule` is called on a Readers
  h5 = hdf5file(d)
  h5["BasisBufferedWriterHeader"]="BasisBufferedWriterHeader"
  h5["jointheader"]="jointheader"
  # execute only once for the whole HDF5 file
  flush(h5)
  try
    HDF5.start_swmr_write(h5)
  catch
    println("SKIPPING START_SWMR_WRITE, HDF5 VERSION TOO LOW")
  end
end

function make_buffered_hdf5_writer(h5, channel_number, analyzer::AbstractBasisAnalyzer, chunksize=1000, timeout_s=1.0)
  g = g_require(h5,"$channel_number")
  BasisBufferedWriter(g,nbases(analyzer), chunksize, timeout_s,start=true)
end


"""
    InfiniteOpt.add_measure_variable(model::JuMP.Model,
                                     var::InfiniteOpt.PointVariable,
                                     key::Val{:TransData}
                                     )::InfiniteOpt.GeneralVariableRef

Make a `PointVariableRef` and map it to the appropriate transcription variable
and return the `GeneralVariableRef`. This is an extension of
[`add_measure_variable`](@ref InfiniteOpt.add_measure_variable(::JuMP.Model,::Any,::Any))
for `TranscriptionOpt`.
"""
function InfiniteOpt.add_measure_variable(
    model::JuMP.Model,
    var::InfiniteOpt.PointVariable,
    key::Val{:TransData}
    )::InfiniteOpt.GeneralVariableRef
    # make negative index to not conflict with the InfiniteModel
    raw_index = transcription_data(model).last_point_index -= 1
    # make the reference and map it to a transcription variable
    ivref = var.infinite_variable_ref
    pvref = InfiniteOpt.GeneralVariableRef(JuMP.owner_model(ivref), raw_index,
                                           InfiniteOpt.PointVariableIndex)
    trans_var = lookup_by_support(model, ivref, var.parameter_values)
    transcription_data(model).finvar_mappings[pvref] = trans_var
    return pvref
end

"""
    InfiniteOpt.add_measure_variable(model::JuMP.Model,
                                     var::InfiniteOpt.SemiInfiniteVariable,
                                     key::Val{:TransData}
                                     )::InfiniteOpt.GeneralVariableRef

Make a `SemiInfiniteVariableRef` and add `var` to the transcription data
and return the `GeneralVariableRef`. This is an extension of
[`add_measure_variable`](@ref InfiniteOpt.add_measure_variable(::JuMP.Model,::Any,::Any))
for `TranscriptionOpt`. Note that `internal_semi_infinite_variable` is also extended
to be able to access the `var`.
"""
function InfiniteOpt.add_measure_variable(
    model::JuMP.Model,
    var::InfiniteOpt.SemiInfiniteVariable,
    key::Val{:TransData}
    )::InfiniteOpt.GeneralVariableRef
    # make negative index to not conflict with the InfiniteModel
    semi_infinite_vars = transcription_data(model).semi_infinite_vars
    raw_index = -1 * (length(semi_infinite_vars) + 1)
    # make the reference and map it to a transcription variable
    ivref = var.infinite_variable_ref
    rvref = InfiniteOpt.GeneralVariableRef(JuMP.owner_model(ivref), raw_index,
                                           InfiniteOpt.SemiInfiniteVariableIndex)
    push!(semi_infinite_vars, var)
    _set_semi_infinite_variable_mapping(model, var, rvref, InfiniteOpt._index_type(ivref))
    return rvref
end

"""
    InfiniteOpt.delete_semi_infinite_variable(model::JuMP.Model,
                                        vref::InfiniteOpt.SemiInfiniteVariableRef,
                                        key::Val{:TransData})::Nothing

This is an extension of
[`delete_semi_infinite_variable`](@ref InfiniteOpt.delete_semi_infinite_variable(::JuMP.Model, ::Any, ::Any))
for use in `TranscriptionOpt`. Here we do not delete semi-infinite variables once they
have been used since there is no performance gain for this paradigm and the
memory saving is small. Note this may change in the future.
"""
function InfiniteOpt.delete_semi_infinite_variable(
    model::JuMP.Model,
    rvref::InfiniteOpt.SemiInfiniteVariableRef,
    key::Val{:TransData}
    )::Nothing
    return
end


function construct_device!(canonical::Canonical, sys::PSY.System,
                           model::DeviceModel{R, D},
                           ::Type{S};
                           kwargs...) where {R<:PSY.RenewableGen,
                                             D<:AbstractRenewableDispatchFormulation,
                                             S<:PM.AbstractPowerModel}

    devices = PSY.get_components(R, sys)

    if validate_available_devices(devices, R)
        return
    end

    #Variables
    activepower_variables!(canonical, devices);

    reactivepower_variables!(canonical, devices);

    #Constraints
    activepower_constraints!(canonical, devices, D, S)

    reactivepower_constraints!(canonical, devices, D, S)

    feedforward!(canonical, R, model.feedforward)

    #Cost Function
    cost_function(canonical, devices, D, S)

    return

end

function construct_device!(canonical::Canonical, sys::PSY.System,
                           model::DeviceModel{R, D},
                           ::Type{S};
                           kwargs...) where {R<:PSY.RenewableGen,
                                             D<:AbstractRenewableDispatchFormulation,
                                             S<:PM.AbstractActivePowerModel}

    devices = PSY.get_components(R, sys)

    if validate_available_devices(devices, R)
        return
    end

    #Variables
    activepower_variables!(canonical, devices)

    #Constraints
    activepower_constraints!(canonical, devices, D, S)

    feedforward!(canonical, R, model.feedforward)

    #Cost Function
    cost_function(canonical, devices, D, S)

    return

end

function construct_device!(canonical::Canonical, sys::PSY.System,
                           model::DeviceModel{R, RenewableFixed},
                           system_formulation::Type{S};
                           kwargs...) where {R<:PSY.RenewableGen,
                                             S<:PM.AbstractPowerModel}

    devices = PSY.get_components(R, sys)

    if validate_available_devices(devices, R)
        return
    end

    nodal_expression!(canonical, devices, system_formulation)

    return

end

function construct_device!(canonical::Canonical, sys::PSY.System,
                           model::DeviceModel{PSY.RenewableFix, D},
                           system_formulation::Type{S};
                           kwargs...) where {D<:AbstractRenewableDispatchFormulation,
                                             S<:PM.AbstractPowerModel}

    @warn("The Formulation $(D) only applies to FormulationControllable Renewable Resources, \n Consider Changing the Device Formulation to RenewableFixed")

    construct_device!(canonical,
                      sys,
                      DeviceModel(PSY.RenewableFix,RenewableFixed),
                      system_formulation;
                      kwargs...)

    return

end

function construct_device!(canonical::Canonical, sys::PSY.System,
                           model::DeviceModel{PSY.RenewableFix, RenewableFixed},
                           system_formulation::Type{S};
                           kwargs...) where {S<:PM.AbstractPowerModel}

    devices = PSY.get_components(PSY.RenewableFix, sys)

    if validate_available_devices(devices, PSY.RenewableFix)
        return
    end

    nodal_expression!(canonical, devices, system_formulation)

    return

end


mutable struct GlobalApproximationValueIterationSolver{GFA <: GlobalFunctionApproximator, RNG <: AbstractRNG, F} <: Solver
    gfa::GFA
    num_samples::Int64
    belres::Float64
    num_iterations::Int64
    verbose::Bool
    rng::RNG
    is_mdp_generative::Bool
    n_generative_samples::Int64
    fv_type::F
end

function GlobalApproximationValueIterationSolver(gfa::GFA; num_samples::Int64, belres::Float64=1e-3,
                                                 max_iterations::Int64=1000, verbose::Bool=false,
                                                 rng::RNG=Random.GLOBAL_RNG, is_mdp_generative::Bool=false,
                                                 n_generative_samples::Int64=0, fv_type::F=Vector{Float64}) where {GFA <: GlobalFunctionApproximator, F, RNG <: AbstractRNG}
    return GlobalApproximationValueIterationSolver(gfa, num_samples, belres, max_iterations, verbose, rng, is_mdp_generative, n_generative_samples, fv_type)
end

function GlobalApproximationValueIterationSolver()
    throw(ArgumentError("GlobalApproximationValueIterationSolver needs a GlobalFunctionApproximator object for construction!"))
end

mutable struct GlobalApproximationValueIterationPolicy{GFA <: GlobalFunctionApproximator, RNG <: AbstractRNG, F} <: Policy
    gfa::GFA
    action_map::Vector
    mdp::Union{MDP,POMDP}
    is_mdp_generative::Bool
    n_generative_samples::Int64
    fv_type::F
    rng::RNG
end

function GlobalApproximationValueIterationPolicy(mdp::Union{MDP,POMDP},
                                                 solver::GlobalApproximationValueIterationSolver)
    return GlobalApproximationValueIterationPolicy(deepcopy(solver.gfa), ordered_actions(mdp), mdp,
                                                   solver.is_mdp_generative, solver.n_generative_samples, solver.fv_type, solver.rng)
end

function convert_featurevector(::Type{V}, s::S, mdp::Union{MDP,POMDP}) where {V <: AbstractArray, S}
    return convert_s(V, s, mdp)
end

# If global function approximator is non-linear, a default convert_s is required
@POMDP_require convert_featurevector(t::Type{V} where {V <: AbstractArray}, s::S where S, mdp::Union{MDP,POMDP}) begin
    @req convert_s(::Type{V} where {V <: AbstractArray}, ::S, ::typeof(mdp))
end

@POMDP_require solve(solver::GlobalApproximationValueIterationSolver, mdp::Union{MDP,POMDP}) begin

    P = typeof(mdp)
    S = statetype(P)
    A = actiontype(P)
    @req discount(::P)
    @subreq ordered_actions(mdp)

    @req actionindex(::P, ::A)
    @req actions(::P, ::S)
    as = actions(mdp)
    a = first(as)

    # Need to be able to sample states
    @req sample_state(::P, ::typeof(solver.rng))

    # Have different requirements depending on whether solver MDP is generative or explicit
    if solver.is_mdp_generative
        @req gen(::P, ::S, ::A, ::typeof(solver.rng))
    else
        @req transition(::P, ::S, ::A)
        ss = sample_state(mdp, solver.rng)
        dist = transition(mdp, ss, a)
        D = typeof(dist)
        @req support(::D)
    end

    # Feature vector conversion must be defined either directly or by default (through convert_s)
    @subreq convert_featurevector(solver.fv_type, sample_state(mdp, solver.rng), mdp)
end

function POMDPs.solve(solver::GlobalApproximationValueIterationSolver, mdp::Union{MDP,POMDP})

    @warn_requirements solve(solver,mdp)

    # Ensure that generative model has a non-zero number of samples
    if solver.is_mdp_generative
        @assert solver.n_generative_samples > 0
    end

    # Solver parameters
    belres = solver.belres
    num_iterations = solver.num_iterations
    num_samples = solver.num_samples
    discount_factor = discount(mdp)
    gfa_type = typeof(solver.gfa)

    # Initialize the policy
    policy = GlobalApproximationValueIterationPolicy(mdp, solver)

    total_time = 0.0
    iter_time = 0.0

    temp_s = sample_state(mdp, solver.rng)
    state_dim = length(convert_featurevector(solver.fv_type, temp_s, mdp))

    for iter = 1:num_iterations

        residual = 0.0

        # Setup input and outputs for fit functions
        state_matrix = zeros(eltype(solver.fv_type), num_samples, state_dim)
        val_vector = zeros(eltype(solver.fv_type), num_samples)

        iter_time = @elapsed begin

        # Loop over the chosen number of samples for approximation
        # and compute the current value function estimate at each sample
        for i = 1:num_samples

            s = sample_state(mdp, solver.rng)

            pt = convert_featurevector(solver.fv_type, s, mdp)
            state_matrix[i,:] = pt

            sub_aspace = actions(mdp,s)

            if isterminal(mdp, s)
                val_vector[i] = 0.0
            else
                old_util = value(policy, s)
                max_util = -Inf

                # Compute the approximate Q value for each action and choose the best
                for a in sub_aspace
                    iaction = actionindex(mdp,a)
                    u = 0.0

                    if solver.is_mdp_generative
                        for j in 1:solver.n_generative_samples
                            sp, r = @gen(:sp,:r)(mdp, s, a, solver.rng)
                            u += r

                            if !isterminal(mdp,sp)
                                sp_feature = convert_featurevector(solver.fv_type, sp, mdp)
                                u += discount_factor*compute_value(policy.gfa, sp_feature)
                            end
                        end
                        u = u / solver.n_generative_samples
                    else
                        dist = transition(mdp,s,a)
                        for (sp, p) in weighted_iterator(dist)
                            p == 0.0 ? continue : nothing
                            r = reward(mdp, s, a, sp)
                            u += p*r

                            # Only interpolate sp if it is non-terminal
                            if !isterminal(mdp,sp)
                                sp_feature = convert_featurevector(solver.fv_type, sp, mdp)
                                u += p * (discount_factor*compute_value(policy.gfa, sp_feature))
                            end
                        end
                    end

                    max_util = (u > max_util) ? u : max_util
                    util_diff = abs(max_util - old_util)
                    util_diff > residual ? (residual = util_diff) : nothing
                end #action

                val_vector[i] = max_util
            end
        end
        # Now fit!
        fit!(policy.gfa, state_matrix, val_vector)

        end # time

        total_time += iter_time
        solver.verbose ? @printf("[Iteration %-4d] residual: %10.3G | iteration runtime: %10.3f ms, (%10.3G s total)\n", iter, residual, iter_time*1000.0, total_time) : nothing
        residual < belres ? break : nothing
    end

    return policy
end

function POMDPs.value(policy::GlobalApproximationValueIterationPolicy, s::S) where S

    s_point = convert_featurevector(policy.fv_type, s, policy.mdp)
    val = compute_value(policy.gfa, s_point)
    return val
end

# Not explicitly stored in policy - extract from value function interpolation
function POMDPs.action(policy::GlobalApproximationValueIterationPolicy, s::S) where S

    mdp = policy.mdp
    best_a_idx = -1
    max_util = -Inf
    sub_aspace = actions(mdp,s)
    discount_factor = discount(mdp)

    for a in sub_aspace

        iaction = actionindex(mdp, a)
        u = value(policy,s,a)

        if u > max_util
            max_util = u
            best_a_idx = iaction
        end
    end

    return policy.action_map[best_a_idx]
end

function POMDPs.value(policy::GlobalApproximationValueIterationPolicy, s::S, a::A) where {S,A}

    mdp = policy.mdp
    discount_factor = discount(mdp)
    u = 0.0

    # As in solve(), do different things based on whether
    # mdp is generative or explicit
    if policy.is_mdp_generative
        for j in 1:policy.n_generative_samples
            sp, r = @gen(:sp,:r)(mdp, s, a, solver.rng)
            sp_point = convert_featurevector(policy.fv_type, sp, mdp)
            u += r + discount_factor*compute_value(policy.gfa, sp_point)
        end
        u = u / policy.n_generative_samples
    else
        dist = transition(mdp,s,a)
        for (sp, p) in weighted_iterator(dist)
            p == 0.0 ? continue : nothing
            r = reward(mdp, s, a, sp)
            u += p*r

            # Only interpolate sp if it is non-terminal
            if !isterminal(mdp,sp)
                sp_point = convert_featurevector(policy.fv_type, sp, mdp)
                u += p*(discount_factor*compute_value(policy.gfa, sp_point))
            end
        end
    end

    return u
end


using TensorBoardLogger, Logging
using Test
using TestImages
using ValueHistories

test_log_dir = "test_logs/"
ENV["GKSwstype"] = "100"

@testset "Deserialization_simple" begin
    logger = TBLogger(test_log_dir*"d", tb_overwrite)

    mri = testimage("mri")
    with_logger(logger) do
        for i=1:5
            @info "test" val=i-0.5*i*im b=i*2 mat=i.*ones(3) mat2=10 .*i.*ones(3,2)
            @info "test2" mri
        end
    end

    # Issue #88: ignore non -tensorboard files
    dir = TensorBoardLogger.logdir(logger)
    f = open(joinpath(dir, "mockdata"), "w")
    write(f, "baddta")
    close(f)

    tgs = TensorBoardLogger.tags(logger)
    @test "test/val" ∈ tgs
    @test "test/b" ∈ tgs
    @test "test/mat" ∈ tgs
    @test "test/mat2" ∈ tgs
    @test "test2/mri" ∈ tgs

    hist = convert(MVHistory, logger)
    is = collect(1:5)
    @test all(hist["test/val"].values .== (1-0.5*im).* is)
    @test all(hist["test/b"].values == is .* 2)
    @test all([v == mri for v=hist["test2/mri"].values])

end


function reset_ensemble(e::Bayes_IPM_ensemble)
    new_e=deepcopy(e)
    for i in 1:length(e.models)
        if string(i) in [basename(record.path) for record in e.models]
            new_e.models[i]=e.models[findfirst(isequal(string(i)), [basename(record.path) for record in e.models])]
        else
            new_e.models[i]=e.retained_posterior_samples[findfirst(isequal(string(i)), [basename(record.path) for record in e.retained_posterior_samples])]
        end
    end

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

    new_e.sample_posterior && (new_e.retained_posterior_samples=Vector{Model_Record}())

    model_counter=length(new_e.models)+1

    return new_e
end