struct FTBLConstr
    kind::Char # can be F or C , (D are skipped)
    value::Float64
end

struct FTBL_MDMeasurement_Entry
    semu::SpeciesEMU
    mass::Int64
    data::Vector{Float64}
end

function FTBL_MDMeasurement_Entry(semu::SpeciesEMU,mass::Int64,mean::Float64,stdev::Float64)
    return FTBL_MDMeasurement_Entry(semu,mass,[mean;stdev])
end

struct FTBL_Data
    fluxes::Vector{Flux}
    constraints_net::Dict{String,FTBLConstr}
    constraints_xch::Dict{String,FTBLConstr}

    eq_constr_net::Vector{LinConstr}
    eq_constr_xch::Vector{LinConstr}
    ineq_constr_net::Vector{LinConstr}
    ineq_constr_xch::Vector{LinConstr}

    substrate_conf::Dict{ConfiguredSEMU,Float64}
    md_measurements::Vector{FTBL_MDMeasurement_Entry}

    flux_measurements::Dict{String,Vector{Float64}}
end

struct FTBL_Import
    fluxidata::FluxiData
    wx_transform::WX_Transform
    wx_constraints_fluxes::Vector{LinConstr}
    wx_constraints_eq_and_ineq::Vector{LinConstr}
    wx_constraints_flux_measurements::Vector{LinConstr}
    wx_constraints_fluxes_proposed::Vector{LinConstr} # contains the "F" recommended values vas lin. constraints
end

"""
  ftbl_to_fluxi(data::FTBL_Data)

This function creates a fluxi dataset including the fluxi network from the parsed FTBL data.
It also creates a WX-Parametrization for the network, on which the constraints from the FTBL_Data are applied.

# NOTES

1. For fluxes with non-zero xch flux, we create a fw and a bw flux (name of forward flux is original name, backward flux has _bw postfix)

2. Create In/Out-Flows: To "open" the network, we create Influxes for:
    (i)  the metabolites in "substrate_conf"
  and we create Outfluxes for:
    (i)  all "dead-end" metabolites, i.e. metabolites with only in-fluxes     (I guess this is what 13C flux does?!)

"""
function ftbl_to_fluxi(data::FTBL_Data)

    fluxes = copy(data.fluxes)

    # create the fluxi network..

    # 1.  Collect all species:
    # 1.1 collect sas of all metabolites:
    species = Set{Species}()
    all_sas = reduce( (x,y) -> union(x,y) , map( x -> union( map( ri->ri.sas , keys(x.transition)) , map(pi->pi.sas ,values(x.transition)))  ,  data.fluxes ) )
    # 1.2 create species:
    all_species_ids = unique( map( sas -> sas.species , all_sas) )
    for si in all_species_ids
        sas_si = filter( sas -> sas.species==si , all_sas )
        num_slots = maximum( map( sas -> sas.pos , sas_si ) )
        push!(species,Species(si,num_slots))
    end

    # 2. Check for dead-end species and create out-fluxes for those..
    all_ras = reduce( (x,y) -> union(x,y) , map( x -> keys(x.transition) , data.fluxes ) ) # !! Do this on the original data.fluxes
    # if a species never appears in a ras, then it is a dead-end metabolite..
    all_ras_species = unique( map( ri -> ri.sas.species , all_ras ) )
    dead_end_metabolites = setdiff( map( si -> si.id , species) , all_ras_species )
    print("dead end metabolites: \n")
    for dmi in dead_end_metabolites;
        print("$(dmi) -> create Ouflux..\n")
        # create outflux:
        push!( fluxes , MetabolicFluxAnalysis.parseFlux(string("Outflux_",dmi) , string( dmi , " --> * " ) ) )
    end

    # 2.b collect all influx sas:
    influx_sas = reduce( (x,y)->union(x,y) , sas.(collect(keys(data.substrate_conf))) )
    #print(influx_sas)

    # 3. Loop over all fluxes and check which need bw flux:
    # NOTE: also fluxes which lead into dead-end metabolites do NOT have a bw flux (I guess?!)
    # NOTE: also the in-fluxes do NOT have a bw flux (I guess?!)
    # NOTE: when deciding, that a flux has no bw flux, based on the "C" constraint, then
    # we HAVE TO REMOVE the constraint, else it will fail..
    for fi in data.fluxes
        needs_bw_flux = true
        if( haskey(data.constraints_xch,fi.id) )
            if( data.constraints_xch[fi.id].kind=='C' )
                if(data.constraints_xch[fi.id].value == 0)
                    needs_bw_flux = false
                end
            end
        end

        # check for fluxes to dead end metabolites:
        if( length( setdiff( dead_end_metabolites , map( si->si.sas.species ,values(fi.transition))) ) < length(dead_end_metabolites) )
            print("found flux to dead-end metabolite -> no reverse flux : $(fi.id)\n")
            needs_bw_flux = false
        end
        if(  any( in.( map( ri -> ri.sas , collect( keys(fi.transition ) ) ) , [  influx_sas  ] ) ) )
            print("found input flux -> no reverse flux : $(fi.id)\n")
            needs_bw_flux = false
        end
        if(needs_bw_flux)
            print("create reverse flux for $(fi.id)\n")
            insert!( fluxes  , find( f -> f==fi , fluxes )[1]+1 ,  create_reverse_flux(fi) )
        end
    end

    # 4a. Prepare substrates:
    # just collect the different semus
    # !! AND ADD THE INFLUX !!
    substr_semus = unique( map( sci -> sci.semu , collect(keys(data.substrate_conf)) ) )
    sorted_substr = Vector{ConfiguredSEMUMixture}()
    for ssemu in substr_semus
        csemu = Dict{ConfiguredSEMU,Float64}()
        for sci in filter( sci -> sci.semu==ssemu , collect(keys(data.substrate_conf)) )
            csemu[sci] = data.substrate_conf[sci]
        end
        push!( sorted_substr , csemu )
    end
    substr_configs = Dict{String,Vector{ConfiguredSEMUMixture}}()
    substr_configs["ftbl_default"] = sorted_substr
    # 4b. add influxes:
    influx_species = unique( reduce( (x,y) -> union(x,y) , map( si -> map(s->s.species , si.semu) , collect(keys(data.substrate_conf))  ) ) )
    for si in influx_species
        print("$(si) -> create Influx..\n")
        push!( fluxes , parseFlux( string("Influx_",si) , string(" * --> ",si) ))
    end

    # 5. The network should be complete now, so create it:
    species_db = Dict{String,Species}()
    for si in species; species_db[si.id] = si; end
    fluxes_db  = Dict{String,Flux}()
    for fi in fluxes; fluxes_db[fi.id] = fi; end

    net = Network( map(f -> f.id,fluxes) , map(f -> f.id,collect(species)) , fluxes_db , species_db )

    # 6. Create mass distribution measurements:
    # again, just collect the different semus
    mdm_semus   = unique( map( mdi -> mdi.semu , data.md_measurements ) )
    sorted_mdms = Vector{MassDistributionMeasurement}()
    for mdsi in mdm_semus
        ddi = Dict{Int64,Vector{Float64}}()
        for mmi in filter( mi -> mi.semu==mdsi , data.md_measurements)
            ddi[mmi.mass] = mmi.data
        end
        push!( sorted_mdms , MassDistributionMeasurement(mdsi,ddi) )
    end
    mdm_configs = Dict{String,Vector{MassDistributionMeasurement}}()
    mdm_configs["ftbl_default"] = sorted_mdms

    # 7. WX Parametrization and constraints..
    (p_pwx,wxt)  = MetabolicFluxAnalysis.parametrization_wiechert_exchange( net , Array{MetabolicFluxAnalysis.LinConstr,1}() , 200. )

    # 8. init the WX constraints
    # We can just create normal LinConstr, with "WX-Fluxes", i.e. with [x] [n] notation:
    constraints_c          = Vector{LinConstr}()
    constraints_proposed   = Vector{LinConstr}() # contains the "F" values..

    # 8.1 process the data.constraints_net
    for fi in keys(data.constraints_net)
        if(data.constraints_net[fi].kind == 'C')
            push!( constraints_c , parse_linconstr( string( fi,"[n] = ", data.constraints_net[fi].value) ) )
        else
            push!( constraints_proposed , parse_linconstr( string( fi,"[n] = ", data.constraints_net[fi].value) ) )
        end
    end
    # 8.2 process the data.constraints_xch
    for fi in keys(data.constraints_xch)
        if(is_wx_flux(net,wxt,fi)) # NO CONSTRAINT,IF CONSTR. TO ZERO, THEN WE DID NOT ADD THE XCH FLUX!!
            if( data.constraints_xch[fi].kind == 'C' )
                push!( constraints_c , parse_linconstr( string( fi,"[x] = ", data.constraints_xch[fi].value) ) )
            else
                push!( constraints_proposed , parse_linconstr( string( fi,"[x] = ", data.constraints_xch[fi].value) ) )
            end
        else
            # don't add anything
        end
    end
    # 8.3 process the data.eq_constraints_net and data.ineq_constriants_net
    # we just change the fids in the lin constraint and add the "[n]"
    net_constraints = [data.eq_constr_net;data.ineq_constr_net]
    wx_constraints  = Vector{LinConstr}()
    for nci in net_constraints
        new_rhs = Dict{String,Float64}()
        for rhi in keys(nci.rhs)
            if(rhi=="1"); new_rhs["1"]=nci.rhs[rhi];else; new_rhs[string(rhi,"[n]")]=nci.rhs[rhi];end
        end
        new_lhs = Dict{String,Float64}()
        for lhi in keys(nci.lhs)
            if(lhi=="1"); new_lhs["1"]=nci.lhs[lhi];else; new_lhs[string(lhi,"[n]")]=nci.lhs[lhi];end
        end
        push!( wx_constraints , LinConstr(nci.constr,new_lhs,new_rhs) )
    end
    # 8.4 process the data.eq_constraints_xch and data.ineq_constriants_xch
    # we just change the fids in the lin constraint and add the "[x]"
    # NOTE: We have to pay attention for [x] fluxes that we did not include, be-
    # cause they are constrained to zero. Therefore, we check for all LC entries,
    # if they are wx-parametrized, and if not, we replace the [x] flux with zero.
    xch_constraints = [data.eq_constr_xch;data.ineq_constr_xch]
    for xci in xch_constraints
        new_rhs = Dict{String,Float64}()
        for rhi in keys(xci.rhs)
            if(rhi=="1"); new_rhs["1"]=xci.rhs[rhi]
            else
                if(is_wx_flux(net,wxt,rhi))
                    new_rhs[string(rhi,"[x]")]=xci.rhs[rhi]
                else
                    # nothing to do, is just zero
                end
            end
        end
        new_lhs = Dict{String,Float64}()
        for lhi in keys(xci.lhs)
            if(lhi=="1")
                new_lhs["1"]=xci.lhs[lhi]
            else
                if(is_wx_flux(lhi))
                    new_lhs[string(lhi,"[x]")]=xci.lhs[lhi]
                else
                    # nothing to do, is just zero
                end
            end
        end
        push!( wx_constraints , LinConstr(xci.constr,new_lhs,new_rhs) )
    end

    # 8.5 add the constraints from .flux_measurements
    # for simplicity, we just add them as equality constraints (we could also derive ineq. constr easily, but..)
    # NOTE: as these flux measurements are usually for in/out fluxes (which do not have WX-parametrization), we
    # add the flux_id without [x/n] postfix..
    # We may have to fix this if we have flux measurements for WX-Fluxes..
    flux_measurements_wx_constraints = Vector{LinConstr}()
    for ki in keys( data.flux_measurements )
        push!( flux_measurements_wx_constraints , parse_linconstr( string( ki , " = " , data.flux_measurements[ki][1] ) ) )
    end

    # Ok, now we have everything to create the fluxi dataset:
    fdata = FluxiData(net,mdm_configs,substr_configs,Dict{String,Vector{LinConstr}}() )

    wx_constraints = [wx_constraints;]
    return FTBL_Import(fdata,wxt,constraints_c, wx_constraints,flux_measurements_wx_constraints,constraints_proposed)
    #return (net,substr_configs,mdm_configs,wxt,MetabolicFluxAnalysis.create_all_wx_fluxnames(net,wxt) , constraints_c , constraints_proposed , wx_constraints )
end

"""
  parse_ftbl_data(data::IOStream)

  parses the data from .ftbl data and returns FTBL_Data object

  #NOTES
  This function parses all sections, except: PROJECT , LABELING_MEASUREMENTS , PEAK_MEASUREMENTS , OPTIONS

"""
function parse_ftbl_data(data::IOStream)

    # loop over lines:
    lines = readlines(data)

    line_pos = 1

    #
    # parsing status can be:
    #
    # 10: in NETWORK section, waiting for new flux
    # 11: in NETWORK section, waiting for atom transition map
    #
    # 20: in FLUXES section, before NET or XCH
    # 21: in FLUXES NET section
    # 22: in FLUXES XCH section
    # 30: in EQUALITIES section, before NET or XCH
    # 31: in EQUALITIES NET section
    # 32: in EQUALITIES XCH section
    # 41: in INEQUALITIES section, before NET or XCH
    # 41: in INEQUALITIES NET section
    # 42: in INEQUALITIES XCH section
    # 50: in MASS_SPECTROMETRY section
    # 60: in LABEL_INPUT section
    # 70: in FLUX_MEASUREMENTS section
    parsing_status = -1
    parsing_done   = false

    # data while constructing fluxes:
    fluxes             = Vector{Flux}()
    mfluxes            = Vector{String} # holds the parsed tsv of the first part of a flux

    # data while collecting constraints
    constraints_net = Dict{String,FTBLConstr}()
    constraints_xch = Dict{String,FTBLConstr}()

    eq_constr_net   = Vector{LinConstr}()
    eq_constr_xch   = Vector{LinConstr}()
    ineq_constr_net   = Vector{LinConstr}()
    ineq_constr_xch   = Vector{LinConstr}()

    flux_measurements = Dict{String,Vector{Float64}}() # 2-element arrays, with [value ; deviation]

    # data for collecting label_input data:
    # we just put all lines that we find in here, and post-process it to create the substrate mixture
    configured_semus = Dict{ConfiguredSEMU,Float64}()
    current_config_metabolite = ""

    # data for collecting mass distribution measurements:
    md_measurements = Vector{FTBL_MDMeasurement_Entry}()
    current_md_semu = SpeciesEMU()

    while( line_pos <= length(lines) )
        li_a = lines[line_pos]

        li = preprocess_line(li_a)
        if( test_empty_line(li) )
            line_pos += 1
            continue
        end

        if(startswith(li,"NETWORK"))
            parsing_status = 10
            line_pos       = line_pos+2 # skip that bloody "info" line
            continue
        end
        if(startswith(li,"FLUXES"))
            parsing_status = 20
            line_pos       = line_pos+1
            continue
        end
        if(startswith(li,"EQUALITIES"))
            parsing_status = 30
            line_pos       = line_pos+1
            continue
        end
        if(startswith(li,"INEQUALITIES"))
            parsing_status = 40
            line_pos       = line_pos+1
            continue
        end
        if(startswith(li,"MASS_SPECTROMETRY"))
            parsing_status = 50
            line_pos       = line_pos+2 # skip that bloody "info" line
            continue
        end
        if(startswith(li,"LABEL_INPUT"))
            parsing_status = 60
            line_pos       = line_pos+2 # skip that bloody "info" line
            continue
        end
        if(startswith(li,"FLUX_MEASUREMENTS"))
            parsing_status = 70
            line_pos       = line_pos+2 # skip that bloody "info" line
            continue
        end
        # and parse the sections that we do not parse:
        if(startswith(li,"LABEL_MEASUREMENTS"))
            parsing_status = -1
            line_pos       = line_pos+2 # skip that bloody "info" line
            continue
        end
        if(startswith(li,"PEAK_MEASUREMENTS"))
            parsing_status = -1
            line_pos       = line_pos+2  # skip that bloody "info" line
            continue
        end
        if(startswith(li,"OPTIONS"))
            parsing_status = -1
            line_pos       = line_pos+2  # skip that bloody "info" line
            continue
        end

        if( parsing_status==20 || parsing_status==21 || parsing_status==22 )
            if( strip(li)=="NET"  )
                parsing_status = 21
                line_pos += 2 # skip that bloody "info" line
                continue
            end
            if( strip(li)=="XCH"  )
                parsing_status = 22
                line_pos += 2 # skip that bloody "info" line
                continue
            end
        end
        if( parsing_status==30 || parsing_status==31 || parsing_status==32 )
            if( strip(li)=="NET"  )
                parsing_status = 31
                line_pos += 2 # skip that bloody "info" line
                continue
            end
            if( strip(li)=="XCH"  )
                parsing_status = 32
                line_pos += 2 # skip that bloody "info" line
                continue
            end
        end
        if( parsing_status==40 || parsing_status==41 || parsing_status==42 )
            if( strip(li)=="NET"  )
                parsing_status = 41
                line_pos += 2 # skip that bloody "info" line
                continue
            end
            if( strip(li)=="XCH"  )
                parsing_status = 42
                line_pos += 2 # skip that bloody "info" line
                continue
            end
        end

        # now add the parse code..
        if(parsing_status==10)
            #mflux = matchall(r"\w+",li)
            mfluxes = parse_tsv_line(li)
            parsing_status = 11
            line_pos += 1
            continue;
        end
        if(parsing_status==11)
            tmap = parse_tsv_line(li)
            # now use fluxi parser to construct flux.. ;)
            fluxi_flux = ""
            if(!isempty(mfluxes[3]))
                fluxi_flux = string( fluxi_flux , mfluxes[3] , "(" , conv_tmap(tmap[3]) , ")" )
            end
            if(!isempty(mfluxes[4]))
                if(!isempty(mfluxes[3])); fluxi_flux = string(fluxi_flux , " + "); end
                fluxi_flux = string( fluxi_flux , mfluxes[4] , "(" , conv_tmap(tmap[4]) , ")" )
            end
            fluxi_flux = string(fluxi_flux , " --> " )
            if(!isempty(mfluxes[5]))
                fluxi_flux = string( fluxi_flux , mfluxes[5] , "(" , conv_tmap(tmap[5]) , ")" )
            end
            if(length(mfluxes)>=6)
                if(!isempty(mfluxes[6]))
                    if(!isempty(mfluxes[5])); fluxi_flux = string(fluxi_flux , " + "); end
                    fluxi_flux = string( fluxi_flux , mfluxes[6] , "(" , conv_tmap(tmap[6]) , ")" )
                end
            end
            push!( fluxes , MetabolicFluxAnalysis.parseFlux( mfluxes[2] , fluxi_flux) )

            parsing_status = 10
            line_pos += 1
            continue
        end

        if(parsing_status==21)
            fct = parse_tsv_line(li)
            if( fct[4]=="F" || fct[4]=="C" )
                constraints_net[ fct[3] ] = FTBLConstr( fct[4][1] , parse(Float64,fct[5]) )
            elseif( fct[4]=="D" )
            else
                print("Skipped constraint with unclear type: $(li)\n")
            end
            line_pos += 1
            continue
        end
        if(parsing_status==22)
            #NOTE: xch constraints to zero we DO include, because based on those
            #we later decide, how the wx transform looks..
            fct = parse_tsv_line(li)
            if(length(fct)<4)
                print("sumtingwong: $(li)\n")
            end
            if( fct[4]=="F" || fct[4]=="C" )
                #if( parse(Float64,fct[5])==0 && fct[4]=="C")
                    #print("Zero xch constraint sorted out\n")
                #else
                    constraints_xch[ fct[3] ] = FTBLConstr( fct[4][1] , parse(Float64,fct[5]) )
                #end
            elseif( fct[4]=="D" )
            else
                print("Skipped constraint with unclear type: $(li)")
            end
            line_pos += 1
            continue
        end

        if(parsing_status==31)
            cpt = parse_tsv_line(li)
            push!( eq_constr_net , MetabolicFluxAnalysis.parse_linconstr( string(cpt[3], " = ",cpt[4]) ) )
            line_pos += 1
            continue
        end
        if(parsing_status==32)
            cpt = parse_tsv_line(li)
            push!( eq_constr_xch , MetabolicFluxAnalysis.parse_linconstr( string(cpt[3], " = ",cpt[4]) ) )
            line_pos += 1
            continue
        end
        if(parsing_status==41)
            #NOTE!!! The ftbl. notation is weird. Constraint is in OPPOSITE direction of what you read!!!
            cpt = parse_tsv_line(li)
            #if( cpt[4]=="<=" || cpt[4]=="<" )
            if( cpt[4]==">=" || cpt[4]==">" )
                push!( ineq_constr_xch , MetabolicFluxAnalysis.parse_linconstr( string(cpt[3], " < ",cpt[5]) ) )
            #elseif( cpt[4]==">=" || cpt[4]==">" )
        elseif( cpt[4]=="<=" || cpt[4]=="<" )
                push!( ineq_constr_xch , MetabolicFluxAnalysis.parse_linconstr( string(cpt[3], " > ",cpt[5]) ) )
            else
                print("ineq constraint with unclear comp. $(li)")
            end
            line_pos += 1
            continue
        end
        if(parsing_status==42)
            cpt = parse_tsv_line(li)
            if( cpt[4]=="<=" || cpt[4]=="<" )
                push!( ineq_constr_xch , MetabolicFluxAnalysis.parse_linconstr( string(cpt[3], " < ",cpt[5]) ) )
            elseif( cpt[4]==">=" || cpt[4]==">" )
                push!( ineq_constr_xch , MetabolicFluxAnalysis.parse_linconstr( string(cpt[3], " > ",cpt[5]) ) )
            else
                print("ineq constraint with unclear comp. $(li)")
            end
            line_pos += 1
            continue
        end

        if(parsing_status==70)
            fmt = parse_tsv_line(li)
            flux_measurements[fmt[2]] = [ parse(Float64,fmt[3]) ; parse(Float64,fmt[4]) ]
            line_pos += 1
            continue
        end

        if(parsing_status==60)
            llt = parse_tsv_line(li)
            if(!isempty(llt[2]))
                current_config_metabolite = llt[2]
            end
            configured_semus[ ftbl_create_configured_semu(current_config_metabolite,llt[3]) ] = parse(Float64,llt[4])
            line_pos += 1
            continue
        end

        if(parsing_status==50)
            mdt = parse_tsv_line(li)
            if(!isempty(mdt[2]))
                current_md_semu = constructSpeciesEMU( mdt[2] , map( x -> parse(Int64,x) , split(mdt[3],",") ) )
            end
            push!( md_measurements , FTBL_MDMeasurement_Entry( current_md_semu , parse(Int64,mdt[4]) , parse(Float64,mdt[5]) , parse(Float64,mdt[6]) ) )
            line_pos += 1
            continue
        end

        # if we arrive here, nothing parsed this line..
        print("unparsed line: $(li)\n")
        line_pos += 1
        continue
    end

    #return ( fluxes,constraints_net,constraints_xch , (eq_constr_net,eq_constr_xch,ineq_constr_net,ineq_constr_xch) , configured_semus , md_measurements )
    return FTBL_Data(fluxes,constraints_net,constraints_xch , eq_constr_net,eq_constr_xch,ineq_constr_net,ineq_constr_xch , configured_semus , md_measurements , flux_measurements )
end

function preprocess_line(a::String)
    m_comment = match(r"//", a)
    if(isa(m_comment,Void))
        return a
    else
        a         = a[1:m_comment.offset-1]
        return a
    end
end

function test_empty_line(a::String)
    a2 = a[:]
    m_has_word_characters = match(r"\w",a2)
    return isa(m_has_word_characters,Void)
end

function parse_tsv_line(li::String)

    ti = find( x -> x=='\t',li)
    all_indeces = [1;ti;length(li)+1]

    tabbed::Vector{String} = Vector{String}(length(all_indeces)-1)

    for zi in 1:(length(all_indeces)-1)
        tabbed[zi] = li[ all_indeces[zi]+1 : all_indeces[zi+1]-1]
    end
    return tabbed
end
# convergts #ABC into A-B-C
function conv_tmap(orig::String)
    tmap = ""
    for zi=2:length(orig)
        tmap = string(tmap,orig[zi])
        if(zi<length(orig)); tmap = string(tmap,"-"); end
    end
    return tmap
end

# Converts "sA" , "#001100"
# into the corresponding ConfiguredSEMU
function ftbl_create_configured_semu( met::String , conf_str::String)
    conf_str = strip(conf_str)
    csemu    = constructSpeciesEMU( met , collect( 1:length(conf_str)-1 ) )
    conf     = map( x -> parse(Int,x) , collect(conf_str[2:end]))
    return ConfiguredSEMU(csemu,conf)
end


if Sys.islinux()
    py"""
    import tensorflow as tf
    import socket
    if socket.gethostname() != "Dolores":
        libFwiOp = tf.load_op_library('../Ops/FWI/build/libFwiOp.so')
    else:
        libFwiOp = tf.load_op_library('../Ops/FWI/build_dolores/libFwiOp.so')
    @tf.custom_gradient
    def fwi_op(λ,μ,ρ,stf,gpu_id,shot_ids,para_fname):
        misfit = libFwiOp.fwi_op(λ,μ,ρ,stf,gpu_id,shot_ids,para_fname)
        def grad(dy):
            return libFwiOp.fwi_op_grad(dy, tf.constant(1.0,dtype=tf.float64),λ,μ,ρ,stf,gpu_id,shot_ids,para_fname)
        return misfit, grad
    def fwi_obs_op(λ,μ,ρ,stf,gpu_id,shot_ids,para_fname):
        misfit = libFwiOp.fwi_obs_op(λ,μ,ρ,stf,gpu_id,shot_ids,para_fname)
        return misfit
    """
    elseif Sys.isapple()
    py"""
    import tensorflow as tf
    libFwiOp = tf.load_op_library('../Ops/FWI/build/libFwiOp.so')
    @tf.custom_gradient
    def fwi_op(λ,μ,ρ,stf,gpu_id,shot_ids,para_fname):
        misfit = libFwiOp.fwi_op(λ,μ,ρ,stf,gpu_id,shot_ids,para_fname)
        def grad(dy):
            return libFwiOp.fwi_op_grad(dy, tf.constant(1.0,dtype=tf.float64),λ,μ,ρ,stf,gpu_id,shot_ids,para_fname)
        return misfit, grad
    def fwi_obs_op(λ,μ,ρ,stf,gpu_id,shot_ids,para_fname):
        misfit = libFwiOp.fwi_obs_op(λ,μ,ρ,stf,gpu_id,shot_ids,para_fname)
        return misfit
    """
    elseif Sys.iswindows()
    py"""
    import tensorflow as tf
    libFwiOp = tf.load_op_library('../Ops/FWI/build/libFwiOp.so')
    @tf.custom_gradient
    def fwi_op(λ,μ,ρ,stf,gpu_id,shot_ids,para_fname):
        misfit = libFwiOp.fwi_op(λ,μ,ρ,stf,gpu_id,shot_ids,para_fname)
        def grad(dy):
            return libFwiOp.fwi_op_grad(dy, tf.constant(1.0,dtype=tf.float64),λ,μ,ρ,stf,gpu_id,shot_ids,para_fname)
        return misfit, grad
    def fwi_obs_op(λ,μ,ρ,stf,gpu_id,shot_ids,para_fname):
        misfit = libFwiOp.fwi_obs_op(λ,μ,ρ,stf,gpu_id,shot_ids,para_fname)
        return misfit
    """
    end

    fwi_op = py"fwi_op"
    fwi_obs_op = py"fwi_obs_op"


# Default functions

## 1. Variables creation
function create_vars!(element::T, bus::String, elemid::String, elem_formulation::Symbol, bus_vars::Dict{String, Variable}, scenario::String) where T
  return
end

function create_vars!(element::T, link::Link, elemid::String, elem_formulation::Symbol, link_vars::Dict{String, Variable}, scenario::String) where T
  return
end

## 2. Power balance
function Snodal(element::T, busid::String, elemid::String, elem_formulation::Symbol, bus_vars::Dict{String, Variable}) where T
  return ["", Polynomial(), 0, 0]
end

function Sorig(element::T, link::Link, elemid::String, elem_formulation::Symbol, link_vars::Dict{String, Variable}) where T
  return Polynomial()
end

function Sdest(element::T, link::Link, elemid::String, elem_formulation::Symbol, link_vars::Dict{String, Variable}) where T
  return Polynomial()
end

## 3. Constraints creation
function constraint(element::T, bus::String, elemid::String, elem_formulation::Symbol, bus_vars::Dict{String, Variable}, scenario::String, OPFpbs::OPFProblems) where T
  return Dict{String, Constraint}()
end

function constraint(element::T, link::Link, elemid::String, elem_formulation::Symbol, link_vars::Dict{String, Variable}, scenario::String, OPFpbs::OPFProblems) where T
  return Dict{String, Constraint}()
end

## 4. Element cost
function cost(element::T, bus::String, elemid::String, elem_formulation::Symbol, bus_vars::Dict{String, Variable}, Snode::Polynomial, lb, ub) where T
  return 0
end

function cost(element::T, link::Link, elemid::String, elem_formulation::Symbol, link_vars::Dict{Link, Variable}, Snode::Polynomial, lb, ub) where T
  return 0
end


const uri_pages = ["index", "home", "search", "main", "post", "homepage", "category", "register", "login", "faq", "about", "terms","privacy", "author"]
const uri_paths = ["app", "main", "wp-content", "search", "category", "tag", "categories", "tags", "blog", "posts", "list", "explore"]
const uri_extensions = [".html", ".html", ".html", ".htm", ".htm", ".php", ".php", ".jsp", ".asp"]

pixels()::Int = rand(1:1024)
user_name()::String = bothify(executor(data["faker"]["internet"]["user_name_formats"]))
domain_suffix()::String = executor(data["faker"]["internet"]["domain_suffix"])
safe_email(items::Tuple=(user_name(),"@example.", domain_suffix()))::String = reduce(string, items)
free_email_domain()::String = executor(data["faker"]["internet"]["free_email"])
free_email(items::Tuple=(user_name(),"@",free_email_domain()))::String = reduce(string, items)
domain_word()::String = lowercase(join(map( x -> length(x)>0 ? x[1] : "" , split(Faker.company(), [',', ' ','-'])),""))
domain_name(items::Tuple=(domain_word(),".",domain_suffix()))::String = reduce(string, items)
company_email(items::Tuple=(user_name(),"@",domain_name()))::String = reduce(string, items)

email()::String = executor(data["faker"]["internet"]["email_formats"])
url()::String = executor(data["faker"]["internet"]["url_formats"])

ipv4()::String = string(rand( 172:255),".",rand( 0:255),".",rand(0:255),".",rand(0:255))
ipv6()::String = join(map( x -> string(rand(4100:65535),base=16) ,1:8), ":")
mac_address()::String = join(map( x -> string(Base.hex(unsigned(rand( 0x00:0xff)),2,false)) ,1:6), ":")
uri_page()::String = rand(uri_pages)

uri_path(;deep::Int=0)::String =
    (if deep == 0
        deep = rand(1:3)
    end;
    return join(map( x -> string(rand(uri_paths)) , 0:deep), "/") )

uri_extension()::String = rand(uri_extensions)
uri()::String = executor(data["faker"]["internet"]["uri_formats"])
image_url()::String = executor(data["faker"]["internet"]["image_placeholder_services"])


# Inspired by CPLEX.jl.

@enum(VariableType, CONTINUOUS, BINARY, INTEGER, INTERVAL, SEQUENCEINTERVAL)

_type_to_variabletype(T::Type{<:Real}) = ((T == Float64) ? CONTINUOUS : INTEGER)
_variabletype_to_type(vt::VariableType) = ((vt == CONTINUOUS) ? Float64 : Int)

# TODO.
@enum(
    CallbackState,
    CB_NONE
    # Others at some point.
)

mutable struct VariableInfo
    index::MOI.VariableIndex
    variable::Variable
    name::String
    type::VariableType

    # Variable bounds: either integers or floats. Needed for is_valid on VariableIndex
    # and bound constraints.
    lb::Real
    ub::Real
    integer::Union{
        Nothing,
        Tuple{
            MOI.VariableIndex,
            MOI.ConstraintIndex{MOI.VariableIndex, MOI.Integer},
            Constraint,
        },
    }
    binary::Union{
        Nothing,
        Tuple{
            MOI.VariableIndex,
            MOI.ConstraintIndex{MOI.VariableIndex, MOI.ZeroOne},
            Constraint,
        },
    }

    # For second-order cones, the variable must be constrained to be >= 0.
    # However, when removing all cones this variable participates in,
    # the previous lower bound must be restored.
    n_socs::Int
    old_lb::Union{Nothing, Real}
end

function VariableInfo(index::MOI.VariableIndex, variable::Variable)
    return VariableInfo(
        index,
        variable,
        "",
        CONTINUOUS,
        -IloInfinity,
        IloInfinity,
        nothing,
        nothing,
        0,
        nothing,
    )
end

mutable struct ConstraintInfo
    # Only necessary information to access an existing constraint, delete it when needed.
    # No need to store more details, like subexpressions, to delete: the JVM's garbage collector
    # will take care of this.
    index::MOI.ConstraintIndex
    constraint::Constraint
    f::Union{MOI.AbstractScalarFunction, MOI.AbstractVectorFunction}
    set::MOI.AbstractSet
    name::String
end

function ConstraintInfo(
    index::MOI.ConstraintIndex,
    constraint::Constraint,
    f::Union{MOI.AbstractScalarFunction, MOI.AbstractVectorFunction},
    set::MOI.AbstractSet,
)
    return ConstraintInfo(index, constraint, f, set, "")
end

mutable struct Optimizer <: MOI.AbstractOptimizer
    # The low-level CPLEX model.
    inner::JavaCPOModel

    # The model name.
    name::String

    # A flag to keep track of MOI.Silent, which overrides the OutputFlag
    # parameter.
    silent::Bool

    # A mapping from the MOI.VariableIndex to the CPLEX variable object.
    # VariableInfo also stores some additional fields like the type of variable.
    variable_info::CleverDicts.CleverDict{MOI.VariableIndex, VariableInfo}

    # A mapping from the MOI.ConstraintIndex to the CPLEX variable object.
    # VariableInfo also stores some additional fields like the type of variable.
    constraint_info::Dict{MOI.ConstraintIndex, ConstraintInfo}

    # Memorise the objective sense and the function separately, as the Concert
    # API forces to give both at the same time.
    objective_sense::MOI.OptimizationSense
    objective_function::Union{Nothing, MOI.AbstractScalarFunction}
    objective_function_cp::Union{Nothing, NumExpr}
    objective_cp::Union{Nothing, IloObjective}

    # Cache parts of a solution.
    cached_solution_state::Union{Nothing, Bool}

    # Handle callbacks. WIP.
    callback_state::CallbackState

    # Mappings from variable and constraint names to their indices. These are
    # lazily built on-demand, so most of the time, they are `nothing`.
    # The solver's functionality is not useful in this case, as it can only
    # handle integer variables. Moreover, bound constraints do not have names
    # for the solver.
    name_to_variable::Union{Nothing, Dict{String, MOI.VariableIndex}}
    name_to_constraint::Union{Nothing, Dict{String, MOI.ConstraintIndex}}

    """
        Optimizer()

    Create a new Optimizer object.
    """
    function Optimizer()
        model = new()
        model.inner = JavaCPOModel()

        MOI.set(model, MOI.Silent(), false)

        model.variable_info =
            CleverDicts.CleverDict{MOI.VariableIndex, VariableInfo}()
        model.constraint_info = Dict{MOI.ConstraintIndex, ConstraintInfo}()

        model.objective_sense = MOI.FEASIBILITY_SENSE
        model.objective_function = nothing
        model.objective_function_cp = nothing
        model.objective_cp = nothing

        model.cached_solution_state = nothing
        model.callback_state = CB_NONE

        MOI.empty!(model)
        return model
    end
end

Base.show(io::IO, model::Optimizer) = show(io, model.inner)

function MOI.empty!(model::Optimizer)
    model.inner = JavaCPOModel()
    model.name = ""
    empty!(model.variable_info)
    empty!(model.constraint_info)

    model.objective_sense = MOI.FEASIBILITY_SENSE
    model.objective_function = nothing
    model.objective_function_cp = nothing
    model.objective_cp = nothing

    model.cached_solution_state = nothing
    model.name_to_variable = nothing
    model.name_to_constraint = nothing
    model.callback_state = CB_NONE
    return
end

function MOI.is_empty(model::Optimizer)
    !isempty(model.name) && return false
    !isempty(model.variable_info) && return false
    !isempty(model.constraint_info) && return false
    model.objective_sense != MOI.FEASIBILITY_SENSE && return false
    model.objective_function !== nothing && return false
    model.objective_function_cp !== nothing && return false
    model.objective_cp !== nothing && return false
    model.name_to_variable !== nothing && return false
    model.name_to_constraint !== nothing && return false
    model.cached_solution_state !== nothing && return false
    model.callback_state != CB_NONE && return false
    return true
end

MOI.get(::Optimizer, ::MOI.SolverName) = "CPLEX CP Optimizer"

## Types of objectives and constraints that are supported.
# TODO: everything CP.

function MOI.supports(
    ::Optimizer,
    ::MOI.ObjectiveFunction{F},
) where {
    F <: Union{
        MOI.VariableIndex,
        MOI.ScalarAffineFunction{Float64},
        MOI.ScalarQuadraticFunction{Float64},
    },
}
    return true
end

function MOI.supports_constraint(
    ::Optimizer,
    ::Type{MOI.VariableIndex},
    ::Type{F},
) where {
    T <: Union{Int, Float64},
    F <: Union{
        MOI.EqualTo{T},
        MOI.LessThan{T},
        MOI.GreaterThan{T},
        MOI.Interval{T},
        MOI.ZeroOne,
        MOI.Integer,
    },
}
    return true
end

function MOI.supports_constraint(
    ::Optimizer,
    ::Union{
        Type{MOI.ScalarAffineFunction{T}},
        Type{MOI.ScalarQuadraticFunction{T}},
    },
    ::Type{F},
) where {
    T <: Union{Int, Float64},
    F <:
    Union{MOI.EqualTo{T}, MOI.LessThan{T}, MOI.GreaterThan{T}, MOI.Interval{T}},
}
    return true
end

MOI.supports(::Optimizer, ::MOI.VariableName, ::Type{MOI.VariableIndex}) = true
function MOI.supports(
    ::Optimizer,
    ::MOI.ConstraintName,
    ::Type{<:MOI.ConstraintIndex},
)
    return true
end

MOI.supports(::Optimizer, ::MOI.Name) = true
MOI.supports(::Optimizer, ::MOI.Silent) = true
MOI.supports(::Optimizer, ::MOI.NumberOfThreads) = true
MOI.supports(::Optimizer, ::MOI.TimeLimitSec) = true
MOI.supports(::Optimizer, ::MOI.ObjectiveSense) = true
MOI.supports(::Optimizer, ::MOI.RawOptimizerAttribute) = true

MOI.supports_incremental_interface(::Optimizer) = true

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

function MOI.get(::Optimizer, ::MOI.ListOfVariableAttributesSet)
    return MOI.AbstractVariableAttribute[MOI.VariableName()]
end

function MOI.get(model::Optimizer, ::MOI.ListOfModelAttributesSet)
    attributes = Any[MOI.ObjectiveSense()]
    typ = MOI.get(model, MOI.ObjectiveFunctionType())
    if typ !== nothing
        push!(attributes, MOI.ObjectiveFunction{typ}())
    end
    if MOI.get(model, MOI.Name()) != ""
        push!(attributes, MOI.Name())
    end
    return attributes
end

function MOI.get(::Optimizer, ::MOI.ListOfConstraintAttributesSet)
    return MOI.AbstractConstraintAttribute[MOI.ConstraintName()]
end

## Optimize methods

function MOI.optimize!(model::Optimizer)
    model.cached_solution_state = cpo_java_solve(model.inner)
    return
end

function _throw_if_optimize_in_progress(model, attr)
    if model.callback_state != CB_NONE
        throw(MOI.OptimizeInProgress(attr))
    end
end

function MOI.get(model::Optimizer, attr::MOI.TerminationStatus)
    _throw_if_optimize_in_progress(model, attr)
    if model.cached_solution_state === nothing
        return MOI.OPTIMIZE_NOT_CALLED
    end

    if model.cached_solution_state
        return MOI.OPTIMAL
    else
        return MOI.INFEASIBLE
    end

    # TODO: exploit the attributes.

    # if stat == 1 # CPX_STAT_OPTIMAL
    #     return MOI.OPTIMAL
    # elseif stat == 3 # CPX_STAT_INFEASIBLE
    #     return MOI.INFEASIBLE
    # elseif stat == 4 # CPX_STAT_INForUNBD
    #     return MOI.INFEASIBLE_OR_UNBOUNDED
    # elseif stat == 2 # CPX_STAT_UNBOUNDED
    #     return MOI.DUAL_INFEASIBLE
    # elseif stat in (12, 21, 22, 36) # CPX_STAT_*ABORT*_OBJ_LIM
    #     return MOI.OBJECTIVE_LIMIT
    # elseif stat in (10, 34) # CPX_STAT_*ABORT_IT_LIM
    #     return MOI.ITERATION_LIMIT
    # elseif stat == 53 # CPX_STAT_CONFLICT_ABORT_NODE_LIM
    #     return MOI.NODE_LIMIT
    # elseif stat in (11, 25, 33, 39) # CPX_STAT_*ABORT*TIME_LIM
    #     return MOI.TIME_LIMIT
    # elseif stat == 5 # CPX_STAT_OPTIMAL_INFEAS
    #     return MOI.NUMERICAL_ERROR
    # # MIP STATUS
    # elseif stat in (101, 102) # CPXMIP_OPTIMAL, CPXMIP_OPTIMAL_TOL
    #     return MOI.OPTIMAL
    # elseif stat == 103 # CPXMIP_INFEASIBLE
    #     return MOI.INFEASIBLE
    # elseif stat == 119 # CPXMIP_INForUNBD
    #     return MOI.INFEASIBLE_OR_UNBOUNDED
    # elseif stat == 118 # CPXMIP_UNBOUNDED
    #     return MOI.DUAL_INFEASIBLE
    # elseif stat in (105, 106) # CPXMIP_NODE_LIM*
    #     return MOI.NODE_LIMIT
    # elseif stat in (107, 108, 131, 132) # CPXMIP_*TIME_LIM*
    #     return MOI.TIME_LIMIT
    # else
    #     return MOI.OTHER_ERROR
    # end
end

function MOI.get(model::Optimizer, attr::MOI.PrimalStatus)
    _throw_if_optimize_in_progress(model, attr)
    if model.cached_solution_state
        return MOI.FEASIBLE_POINT
    else
        return MOI.NO_SOLUTION
    end
end

function MOI.get(
    model::Optimizer,
    attr::MOI.VariablePrimal,
    x::MOI.VariableIndex,
)
    _throw_if_optimize_in_progress(model, attr)
    variable = _info(model, x).variable
    return cpo_java_getvalue(model.inner, variable)
end

function MOI.get(
    model::Optimizer,
    attr::MOI.ConstraintPrimal,
    c::MOI.ConstraintIndex{MOI.VariableIndex, <:Any},
)
    _throw_if_optimize_in_progress(model, attr)
    return MOI.get(model, MOI.VariablePrimal(), MOI.VariableIndex(c.value))
end

function MOI.get(
    model::Optimizer,
    attr::MOI.ConstraintPrimal,
    c::MOI.ConstraintIndex,
)
    _throw_if_optimize_in_progress(model, attr)
    constraint = _info(model, c).constraint # IloConstraint <: IloIntExpr
    return cpo_java_getvalue(model.inner, constraint)
end

# No dual values are available.

function MOI.get(model::Optimizer, attr::MOI.ObjectiveValue)
    _throw_if_optimize_in_progress(model, attr)
    return cpo_java_getobjvalue(model.inner)
end

function MOI.get(model::Optimizer, attr::MOI.ObjectiveBound)
    _throw_if_optimize_in_progress(model, attr)
    return cpo_java_getobjbound(model.inner)
end

function MOI.get(model::Optimizer, attr::MOI.SolveTimeSec)
    _throw_if_optimize_in_progress(model, attr)
    return cpo_java_getdoubleparameter(model.inner, "SolveTime")
end

# No SimplexIterations or BarrierIterations.

function MOI.get(model::Optimizer, attr::MOI.NodeCount)
    _throw_if_optimize_in_progress(model, attr)
    return cpo_java_getintparameter(model.inner, "NumberOfBranches")
end

function MOI.get(model::Optimizer, attr::MOI.RelativeGap)
    _throw_if_optimize_in_progress(model, attr)
    MOI.check_result_index_bounds(model, attr)
    return cpo_java_getobjgap(model.inner)
end

# No DualObjectiveValue.

function MOI.get(model::Optimizer, ::MOI.Silent)
    return model.silent
end

function MOI.get(model::Optimizer, attr::MOI.ResultCount)
    _throw_if_optimize_in_progress(model, attr)
    return cpo_java_getintinfo(model.inner, "NumberOfSolutions")
end

function MOI.set(model::Optimizer, ::MOI.Silent, flag::Bool)
    model.silent = flag
    cpo_java_setintparameter(
        model.inner,
        "LogVerbosity",
        flag ? "Normal" : "Quiet",
    )
    return
end

function MOI.get(model::Optimizer, ::MOI.NumberOfThreads)
    _throw_if_optimize_in_progress(model, attr)
    return cpo_java_getintparam(model.inner, "Workers")
end

function MOI.set(model::Optimizer, ::MOI.NumberOfThreads, x::Int)
    _throw_if_optimize_in_progress(model, attr)
    cpo_java_setintparameter(model.inner, "Workers", x)
    return
end

function MOI.get(model::Optimizer, ::MOI.TimeLimitSec)
    _throw_if_optimize_in_progress(model, attr)
    return cpo_java_getdoubleparam(model.inner, "TimeLimit")
end

function MOI.set(
    model::Optimizer,
    ::MOI.TimeLimitSec,
    x::Union{Number, Nothing},
)
    _throw_if_optimize_in_progress(model, attr)
    value = (x === nothing) ? IloInfinity : Float64(x)
    cpo_java_setdoubleparameter(model.inner, "TimeLimit", value)
    return
end

function MOI.get(model::Optimizer, param::MOI.RawOptimizerAttribute)
    _throw_if_optimize_in_progress(model, attr)

    if cpo_java_isparamint(model.inner, param.name)
        return cpo_java_getintparameter(model.inner, param.name)
    elseif cpo_java_isparamint(model.inner, param.name)
        return cpo_java_getdoubleparameter(model.inner, param.name)
    else
        error("Unknown parameter: $(param.name)")
    end
end

function MOI.set(model::Optimizer, param::MOI.RawOptimizerAttribute, x::Int)
    _throw_if_optimize_in_progress(model, attr)
    cpo_java_setintparameter(model.inner, param.name, x)
    return
end

function MOI.set(model::Optimizer, param::MOI.RawOptimizerAttribute, x::Float64)
    _throw_if_optimize_in_progress(model, attr)
    cpo_java_setdoubleparameter(model.inner, param.name, x)
    return
end

function MOI.get(model::Optimizer, ::MOI.Name)
    return model.name
end

function MOI.set(model::Optimizer, ::MOI.Name, name::String)
    model.name = name
    cpo_java_setname(model.inner, name)
    return
end

function MOI.get(model::Optimizer, ::MOI.NumberOfVariables)
    return length(model.variable_info)
end

function MOI.get(model::Optimizer, ::MOI.ListOfVariableIndices)
    return sort!(collect(keys(model.variable_info)), by=x -> x.value)
end

function MOI.get(model::Optimizer, ::MOI.RawSolver)
    return model.inner
end

# # TODO: initial values (also for constraints?). Use these values in `optimize!`?
# function MOI.set(model::Optimizer, ::MOI.VariablePrimalStart, x::MOI.VariableIndex, value::Union{Nothing, Float64})
#     info = _info(model, x)
#     info.start = value
#     return
# end
#
# function MOI.get(model::Optimizer, ::MOI.VariablePrimalStart, x::MOI.VariableIndex)
#     return _info(model, x).start
# end
#
# function MOI.supports(::Optimizer, ::MOI.VariablePrimalStart, ::Type{MOI.VariableIndex})
#     return true
# end

_type_enums(::Type{MOI.ZeroOne}) = (BINARY,)
_type_enums(::Type{MOI.Integer}) = (INTEGER,)
_type_enums(::Any) = (nothing,)

function _check_bound_compatible(
    model::Optimizer,
    idx::MOI.VariableIndex,
    ::Type{<:MOI.LessThan{T}},
) where {T}
    return _has_ub(model, idx)
end
function _check_bound_compatible(
    model::Optimizer,
    idx::MOI.VariableIndex,
    ::Type{<:MOI.GreaterThan{T}},
) where {T}
    return _has_lb(model, idx)
end
function _check_bound_compatible(
    model::Optimizer,
    idx::MOI.VariableIndex,
    ::Type{<:MOI.Interval{T}},
) where {T}
    return _has_lb(model, idx) && _has_ub(model, idx)
end
function _check_bound_compatible(
    model::Optimizer,
    idx::MOI.VariableIndex,
    ::Type{<:MOI.EqualTo{T}},
) where {T}
    return _get_lb(model, idx) == _get_ub(model, idx)
end
_check_bound_compatible(model::Optimizer, idx::MOI.VariableIndex, ::Any) = false

function MOI.get(
    model::Optimizer,
    ::MOI.NumberOfConstraints{MOI.VariableIndex, S},
) where {S}
    n = 0
    for (key, info) in model.variable_info
        if _check_bound_compatible(model, key, S) || info.type in _type_enums(S)
            n += 1
        end

        if S == MOI.ZeroOne && info.binary !== nothing
            n += 1
        end
        if S == MOI.Integer && info.integer !== nothing
            n += 1
        end
    end
    return n
end

function MOI.get(model::Optimizer, ::MOI.NumberOfConstraints{F, S}) where {F, S}
    n = 0
    for (key, info) in model.constraint_info
        if typeof(info.set) == S
            n += 1
        end
    end
    return n
end

function MOI.get(
    model::Optimizer,
    ::MOI.ListOfConstraintIndices{MOI.VariableIndex, S},
) where {S}
    indices = MOI.ConstraintIndex{MOI.VariableIndex, S}[]
    for (key, info) in model.variable_info
        if _check_bound_compatible(model, key, S) || info.type in _type_enums(S)
            push!(
                indices,
                MOI.ConstraintIndex{MOI.VariableIndex, S}(key.value),
            )
        end

        if S == MOI.ZeroOne && info.binary !== nothing
            push!(
                indices,
                MOI.ConstraintIndex{MOI.VariableIndex, S}(key.value),
            )
        end
        if S == MOI.Integer && info.integer !== nothing
            push!(
                indices,
                MOI.ConstraintIndex{MOI.VariableIndex, S}(key.value),
            )
        end
    end
    return sort!(indices, by=x -> x.value)
end

function MOI.get(
    model::Optimizer,
    ::MOI.ListOfConstraintIndices{F, S},
) where {F, S}
    indices = MOI.ConstraintIndex{F, S}[]
    for (key, info) in model.constraint_info
        if typeof(info.set) == S
            push!(indices, MOI.ConstraintIndex{F, S}(key.value))
        end
    end
    return sort!(indices, by=x -> x.value)
end

# No SOS1/SOS2.

function MOI.get(model::Optimizer, ::MOI.ListOfConstraintTypesPresent)
    constraints = Set{Tuple{Type, Type}}()

    for info in values(model.variable_info)
        if _get_lb(model, info.index) == _get_ub(model, info.index)
            push!(
                constraints,
                (
                    MOI.VariableIndex,
                    MOI.EqualTo{typeof(_get_lb(model, info.index))},
                ),
            )
        elseif _has_lb(model, idx) && _has_ub(model, idx)
            push!(
                constraints,
                (
                    MOI.VariableIndex,
                    MOI.Interval{typeof(_get_lb(model, info.index))},
                ),
            )
        elseif _has_ub(model, idx)
            push!(
                constraints,
                (
                    MOI.VariableIndex,
                    MOI.LessThan{typeof(_get_lb(model, info.index))},
                ),
            )
        elseif _has_lb(model, idx)
            push!(
                constraints,
                (
                    MOI.VariableIndex,
                    MOI.GreaterThan{typeof(_get_lb(model, info.index))},
                ),
            )
        end
        if info.type == CONTINUOUS
        elseif info.type == BINARY
            push!(constraints, (MOI.VariableIndex, MOI.ZeroOne))
        elseif info.type == INTEGER
            push!(constraints, (MOI.VariableIndex, MOI.Integer))
        end
    end

    for info in values(model.constraint_info)
        push!(constraints, (info.f, info.set))
    end

    return collect(constraints)
end

function MOI.get(model::Optimizer, ::MOI.ObjectiveFunctionType)
    if model.objective_sense == MOI.FEASIBILITY_SENSE
        return nothing
    else
        return typeof(model.objective_function)
    end
end

# TODO: modify the objective and the constraints.

# ConstraintBasisStatus makes no sense.


export PARAMETER_DEFAULT, PARAMETER_UNSTABLE_BUT_FAST, PARAMETER_STABLE_BUT_SLOW

using MathOptInterface
MOI = MathOptInterface
const MOIU = MOI.Utilities
const AFFEQ = MOI.ConstraintIndex{MOI.ScalarAffineFunction{Cdouble}, MOI.EqualTo{Cdouble}}

mutable struct Optimizer <: MOI.AbstractOptimizer
    objconstant::Cdouble
    objsign::Int
    blockdims::Vector{Int}
    varmap::Vector{Tuple{Int, Int, Int}} # Variable Index vi -> blk, i, j
    b::Vector{Cdouble}
    problem::Union{Nothing, SDPAProblem}
    solve_time::Float64
    silent::Bool
    options::Dict{Symbol, Any}
    function Optimizer(; kwargs...)
		optimizer = new(
            zero(Cdouble), 1, Int[], Tuple{Int, Int, Int}[], Cdouble[],
            nothing, NaN, false, Dict{Symbol, Any}())
		for (key, value) in kwargs
			MOI.set(optimizer, MOI.RawParameter(key), value)
		end
		return optimizer
    end
end

varmap(optimizer::Optimizer, vi::MOI.VariableIndex) = optimizer.varmap[vi.value]

function MOI.supports(optimizer::Optimizer, param::MOI.RawParameter)
	return param.name in keys(SET_PARAM)
end
function MOI.set(optimizer::Optimizer, param::MOI.RawParameter, value)
	if !MOI.supports(optimizer, param)
		throw(MOI.UnsupportedAttribute(param))
	end
	optimizer.options[param.name] = value
end
function MOI.get(optimizer::Optimizer, param::MOI.RawParameter)
	# TODO: This gives a poor error message if the name of the parameter is invalid.
	return optimizer.options[param.name]
end

MOI.supports(::Optimizer, ::MOI.Silent) = true
function MOI.set(optimizer::Optimizer, ::MOI.Silent, value::Bool)
	optimizer.silent = value
end
MOI.get(optimizer::Optimizer, ::MOI.Silent) = optimizer.silent

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

# See https://www.researchgate.net/publication/247456489_SDPA_SemiDefinite_Programming_Algorithm_User's_Manual_-_Version_600
# "SDPA (SemiDefinite Programming Algorithm) User's Manual — Version 6.00" Section 6.2
const RAW_STATUS = Dict(
    noINFO     => "The iteration has exceeded the maxIteration and stopped with no informationon the primal feasibility and the dual feasibility.",
    pdOPT      => "The normal termination yielding both primal and dual approximate optimal solutions.",
    pFEAS      => "The primal problem got feasible but the iteration has exceeded the maxIteration and stopped.",
    dFEAS      => "The dual problem got feasible but the iteration has exceeded the maxIteration and stopped.",
    pdFEAS     => "Both primal problem and the dual problem got feasible, but the iterationhas exceeded the maxIteration and stopped.",
    pdINF      => "At least one of the primal problem and the dual problem is expected to be infeasible.",
    pFEAS_dINF => "The primal problem has become feasible but the dual problem is expected to be infeasible.",
    pINF_dFEAS => "The dual problem has become feasible but the primal problem is expected to be infeasible.",
    pUNBD      => "The primal problem is expected to be unbounded.",
    dUNBD      => "The dual problem is expected to be unbounded.")

function MOI.get(optimizer::Optimizer, ::MOI.RawStatusString)
    if optimizer.problem === nothing
        return "`MOI.optimize!` not called."
    end
	return RAW_STATUS[getPhaseValue(optimizer.problem)]
end
function MOI.get(optimizer::Optimizer, ::MOI.SolveTime)
	return optimizer.solve_time
end

function MOI.is_empty(optimizer::Optimizer)
    return iszero(optimizer.objconstant) &&
        optimizer.objsign == 1 &&
        isempty(optimizer.blockdims) &&
        isempty(optimizer.varmap) &&
        isempty(optimizer.b)
end
function MOI.empty!(optimizer::Optimizer)
    optimizer.objconstant = zero(Cdouble)
    optimizer.objsign = 1
    empty!(optimizer.blockdims)
    empty!(optimizer.varmap)
    empty!(optimizer.b)
    optimizer.problem = nothing
end

function MOI.supports(
    optimizer::Optimizer,
    ::Union{MOI.ObjectiveSense,
            MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Cdouble}}})
    return true
end

function MOI.supports_constraint(
    ::Optimizer, ::Type{MOI.VectorOfVariables}, ::Type{MOI.Reals})
    return false
end
const SupportedSets = Union{MOI.Nonnegatives, MOI.PositiveSemidefiniteConeTriangle}
function MOI.supports_constraint(
    ::Optimizer, ::Type{MOI.VectorOfVariables},
    ::Type{<:SupportedSets})
    return true
end
function MOI.supports_constraint(
    ::Optimizer, ::Type{MOI.ScalarAffineFunction{Cdouble}},
    ::Type{MOI.EqualTo{Cdouble}})
    return true
end

function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kws...)
    return MOIU.automatic_copy_to(dest, src; kws...)
end
MOIU.supports_allocate_load(::Optimizer, copy_names::Bool) = !copy_names

function MOIU.allocate(optimizer::Optimizer, ::MOI.ObjectiveSense, sense::MOI.OptimizationSense)
    # To be sure that it is done before load(optimizer, ::ObjectiveFunction, ...), we do it in allocate
    optimizer.objsign = sense == MOI.MIN_SENSE ? -1 : 1
end
function MOIU.allocate(::Optimizer, ::MOI.ObjectiveFunction, ::MOI.ScalarAffineFunction) end

function MOIU.load(::Optimizer, ::MOI.ObjectiveSense, ::MOI.OptimizationSense) end
# Loads objective coefficient α * vi
function load_objective_term!(optimizer::Optimizer, α, vi::MOI.VariableIndex)
    blk, i, j = varmap(optimizer, vi)
    coef = optimizer.objsign * α
    if i != j
        coef /= 2
    end
    # in SDP format, it is max and in MPB Conic format it is min
    inputElement(optimizer.problem, 0, blk, i, j, float(coef), false)
end
function MOIU.load(optimizer::Optimizer, ::MOI.ObjectiveFunction, f::MOI.ScalarAffineFunction)
    obj = MOIU.canonical(f)
    optimizer.objconstant = f.constant
    for t in obj.terms
        if !iszero(t.coefficient)
            load_objective_term!(optimizer, t.coefficient, t.variable_index)
        end
    end
end

function new_block(optimizer::Optimizer, set::MOI.Nonnegatives)
    push!(optimizer.blockdims, -MOI.dimension(set))
    blk = length(optimizer.blockdims)
    for i in 1:MOI.dimension(set)
        push!(optimizer.varmap, (blk, i, i))
    end
end

function new_block(optimizer::Optimizer, set::MOI.PositiveSemidefiniteConeTriangle)
    push!(optimizer.blockdims, set.side_dimension)
    blk = length(optimizer.blockdims)
    for i in 1:set.side_dimension
        for j in 1:i
            push!(optimizer.varmap, (blk, i, j))
        end
    end
end

function MOIU.allocate_constrained_variables(optimizer::Optimizer,
                                             set::SupportedSets)
    offset = length(optimizer.varmap)
    new_block(optimizer, set)
    ci = MOI.ConstraintIndex{MOI.VectorOfVariables, typeof(set)}(offset + 1)
    return [MOI.VariableIndex(i) for i in offset .+ (1:MOI.dimension(set))], ci
end

function MOIU.load_constrained_variables(
    optimizer::Optimizer, vis::Vector{MOI.VariableIndex},
    ci::MOI.ConstraintIndex{MOI.VectorOfVariables},
    set::SupportedSets)
end

function MOIU.load_variables(optimizer::Optimizer, nvars)
    @assert nvars == length(optimizer.varmap)
    dummy = isempty(optimizer.b)
    if dummy
        optimizer.b = [one(Cdouble)]
        optimizer.blockdims = [optimizer.blockdims; -1]
    end
    optimizer.problem = SDPAProblem()
    setParameterType(optimizer.problem, PARAMETER_DEFAULT)
	# TODO Take `silent` into account here
    setparameters!(optimizer.problem, optimizer.options)
    inputConstraintNumber(optimizer.problem, length(optimizer.b))
    inputBlockNumber(optimizer.problem, length(optimizer.blockdims))
    for (i, blkdim) in enumerate(optimizer.blockdims)
        inputBlockSize(optimizer.problem, i, blkdim)
        inputBlockType(optimizer.problem, i, blkdim < 0 ? LP : SDP)
    end
    initializeUpperTriangleSpace(optimizer.problem)
    for i in eachindex(optimizer.b)
        inputCVec(optimizer.problem, i, optimizer.b[i])
    end
    if dummy
        inputElement(optimizer.problem, 1, length(optimizer.blockdims), 1, 1, one(Cdouble), false)
    end
end

function MOIU.allocate_constraint(optimizer::Optimizer,
                                  func::MOI.ScalarAffineFunction{Cdouble},
                                  set::MOI.EqualTo{Cdouble})
    push!(optimizer.b, MOI.constant(set))
    return AFFEQ(length(optimizer.b))
end

function MOIU.load_constraint(m::Optimizer, ci::AFFEQ,
                              f::MOI.ScalarAffineFunction, s::MOI.EqualTo)
    if !iszero(MOI.constant(f))
        throw(MOI.ScalarFunctionConstantNotZero{
            Cdouble, MOI.ScalarAffineFunction{Cdouble}, MOI.EqualTo{Cdouble}}(
                MOI.constant(f)))
    end
    f = MOIU.canonical(f) # sum terms with same variables and same outputindex
    for t in f.terms
        if !iszero(t.coefficient)
            blk, i, j = varmap(m, t.variable_index)
            coef = t.coefficient
            if i != j
                coef /= 2
            end
            inputElement(m.problem, ci.value, blk, i, j, float(coef), false)
        end
    end
end

function MOI.optimize!(m::Optimizer)
	start_time = time()
    SDPA.initializeUpperTriangle(m.problem, false)
    SDPA.initializeSolve(m.problem)
    SDPA.solve(m.problem)
    m.solve_time = time() - start_time
end

function MOI.get(m::Optimizer, ::MOI.TerminationStatus)
    if m.problem === nothing
        return MOI.OPTIMIZE_NOT_CALLED
    end
    status = getPhaseValue(m.problem)
    if status == noINFO
        return MOI.ITERATION_LIMIT
    elseif status == pFEAS
        return MOI.SLOW_PROGRESS
    elseif status == dFEAS
        return MOI.SLOW_PROGRESS
    elseif status == pdFEAS
        return MOI.OPTIMAL
    elseif status == pdINF
        return MOI.INFEASIBLE_OR_UNBOUNDED
    elseif status == pFEAS_dINF
        return MOI.DUAL_INFEASIBLE
    elseif status == pINF_dFEAS
        return MOI.INFEASIBLE
    elseif status == pdOPT
        return MOI.OPTIMAL
    elseif status == pUNBD
        return MOI.DUAL_INFEASIBLE
    elseif status == dUNBD
        return MOI.INFEASIBLE
    end
end

function MOI.get(m::Optimizer, attr::MOI.PrimalStatus)
    if attr.N > MOI.get(m, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    status = getPhaseValue(m.problem)
    if status == noINFO
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == pFEAS
        return MOI.FEASIBLE_POINT
    elseif status == dFEAS
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == pdFEAS
        return MOI.FEASIBLE_POINT
    elseif status == pdINF
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == pFEAS_dINF
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif status == pINF_dFEAS
        return MOI.INFEASIBLE_POINT
    elseif status == pdOPT
        return MOI.FEASIBLE_POINT
    elseif status == pUNBD
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif status == dUNBD
        return MOI.INFEASIBLE_POINT
    end
end

function MOI.get(m::Optimizer, attr::MOI.DualStatus)
    if attr.N > MOI.get(m, MOI.ResultCount())
        return MOI.NO_SOLUTION
    end
    status = getPhaseValue(m.problem)
    if status == noINFO
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == pFEAS
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == dFEAS
        return MOI.FEASIBLE_POINT
    elseif status == pdFEAS
        return MOI.FEASIBLE_POINT
    elseif status == pdINF
        return MOI.UNKNOWN_RESULT_STATUS
    elseif status == pFEAS_dINF
        return MOI.INFEASIBLE_POINT
    elseif status == pINF_dFEAS
        return MOI.INFEASIBILITY_CERTIFICATE
    elseif status == pdOPT
        return MOI.FEASIBLE_POINT
    elseif status == pUNBD
        return MOI.INFEASIBLE_POINT
    elseif status == dUNBD
        return MOI.INFEASIBILITY_CERTIFICATE
    end
end

MOI.get(m::Optimizer, ::MOI.ResultCount) = m.problem === nothing ? 0 : 1
function MOI.get(m::Optimizer, attr::MOI.ObjectiveValue)
    MOI.check_result_index_bounds(m, attr)
    return m.objsign * getPrimalObj(m.problem) + m.objconstant
end
function MOI.get(m::Optimizer, attr::MOI.DualObjectiveValue)
    MOI.check_result_index_bounds(m, attr)
    return m.objsign * getDualObj(m.problem) + m.objconstant
end
struct PrimalSolutionMatrix <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::PrimalSolutionMatrix) = true
MOI.get(optimizer::Optimizer, ::PrimalSolutionMatrix) = PrimalSolution(optimizer.problem)

struct DualSolutionVector <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::DualSolutionVector) = true
function MOI.get(optimizer::Optimizer, ::DualSolutionVector)
    return unsafe_wrap(Array, getResultXVec(optimizer.problem), getConstraintNumber(optimizer.problem))
end

struct DualSlackMatrix <: MOI.AbstractModelAttribute end
MOI.is_set_by_optimize(::DualSlackMatrix) = true
MOI.get(optimizer::Optimizer, ::DualSlackMatrix) = VarDualSolution(optimizer.problem)

function block(optimizer::Optimizer, ci::MOI.ConstraintIndex{MOI.VectorOfVariables})
    return optimizer.varmap[ci.value][1]
end
function dimension(optimizer::Optimizer, ci::MOI.ConstraintIndex{MOI.VectorOfVariables})
    blockdim = optimizer.blockdims[block(optimizer, ci)]
    if blockdim < 0
        return -blockdim
    else
        return MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(blockdim))
    end
end
function vectorize_block(M, blk::Integer, s::Type{MOI.Nonnegatives})
    return diag(block(M, blk))
end
function vectorize_block(M::AbstractMatrix{Cdouble}, blk::Integer, s::Type{MOI.PositiveSemidefiniteConeTriangle}) where T
    B = block(M, blk)
    d = LinearAlgebra.checksquare(B)
    n = MOI.dimension(MOI.PositiveSemidefiniteConeTriangle(d))
    v = Vector{Cdouble}(undef, n)
    k = 0
    for j in 1:d
        for i in 1:j
            k += 1
            v[k] = B[i, j]
        end
    end
    @assert k == n
    return v
end

function MOI.get(optimizer::Optimizer, attr::MOI.VariablePrimal, vi::MOI.VariableIndex)
    MOI.check_result_index_bounds(optimizer, attr)
    blk, i, j = varmap(optimizer, vi)
    return block(MOI.get(optimizer, PrimalSolutionMatrix()), blk)[i, j]
end

function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintPrimal,
                 ci::MOI.ConstraintIndex{MOI.VectorOfVariables, S}) where S<:SupportedSets
    MOI.check_result_index_bounds(optimizer, attr)
    return vectorize_block(MOI.get(optimizer, PrimalSolutionMatrix()), block(optimizer, ci), S)
end
function MOI.get(m::Optimizer, attr::MOI.ConstraintPrimal, ci::AFFEQ)
    MOI.check_result_index_bounds(m, attr)
    return m.b[ci.value]
end

function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintDual,
                 ci::MOI.ConstraintIndex{MOI.VectorOfVariables, S}) where S<:SupportedSets
    MOI.check_result_index_bounds(optimizer, attr)
    return vectorize_block(MOI.get(optimizer, DualSlackMatrix()), block(optimizer, ci), S)
end
function MOI.get(optimizer::Optimizer, attr::MOI.ConstraintDual, ci::AFFEQ)
    MOI.check_result_index_bounds(optimizer, attr)
    return -MOI.get(optimizer, DualSolutionVector())[ci.value]
end


import Statistics
using LinearAlgebra
using OrderedCollections
using ChainRulesCore
using ChainRules
using NNlib
using Ghost
using Ghost: Tape, Variable, V, Call, mkcall, Constant, inputs
using Ghost: bound, compile, play!, isstruct
using Ghost: remove_first_parameter, kwfunc_signature, call_signature

include("helpers.jl")
include("drules.jl")
include("chainrules.jl")
include("grad.jl")
include("update.jl")
include("gradcheck.jl")

function __init__()
    update_chainrules_primitives!()
end


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# Description
# ==============================================================================
#
#   API functions for all the orbit representations.
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #

export get_epoch, get_a, get_e, get_i, get_Ω, get_ω, get_f, get_raan, get_argp,
       get_M, get_r, get_v, get_rv

"""
    get_epoch(orb)

Return the epoch of the orbit representation `orb` [m].

"""
get_epoch(orb)

"""
    get_a(orb)

Return the semi-major axis of the orbit representation `orb` [m].

"""
get_a

"""
    get_e(orb)

Return the eccentricity of the orbit representation `orb`.

"""
get_e

"""
    get_i(orb)

Return the inclination of the orbit representation `orb` [rad].

"""
get_i

"""
    get_Ω(orb)

Return the right ascention of the ascending node (RAAN) of the orbit
representation `orb` [rad].

"""
get_Ω

"""
    get_ω(orb)

Return the argument of periapsis of the representation `orb` [rad].

"""
get_ω

"""
    get_f(orb)

Return the true anomaly of the representation `orb` [rad].

"""
get_f

"""
    get_raan(orb)

Return the right ascention of the ascending node (RAAN) of the orbit
representation `orb` [rad].

"""
@inline get_raan(orb) = get_Ω(orb)

"""
    get_argp(orb)

Return the argument of periapsis of the representation `orb` [rad].

"""
@inline get_argp(orb) = get_ω(orb)

"""
    get_M(orb)

Return the mean anomaly of the representation `orb` [rad].

"""
get_M

"""
    get_r(orb)

Return the position vector of the representation `orb` [m].

"""
get_r

"""
    get_r(orb)

Return the velocity vector of the representation `orb` [m].

"""
get_v

"""
    get_rv(orb)

Return the position and velocity vector of the representation `orb` [m].

"""
get_rv


using FixedPointNumbers, Base.Test

for f in ["normed.jl", "fixed.jl"]
    println("Testing $f")
    include(f)
end

if v"0.5.0" <= VERSION < v"0.6.0-dev"
    @test isempty(detect_ambiguities(FixedPointNumbers, Base, Core))
end


include("in_source.jl")
include("out_recording.jl")
include("out_recwf.jl")
include("out_wavefield.jl")

function runsimu(model::Union{elmod2d,acmod2d},
    sou::Array{MTsource};
    rec = nothing,
    showevery = 100,
    RecDataPath = nothing,
    WFDataPath = nothing,
    Slices = nothing,
    OptCpnt = nothing)

    message(rec,RecDataPath,Slices,WFDataPath,OptCpnt)
#=== No output ===#
    if rec == nothing && Slices == nothing
        for it = 1:model.medium.nT
            for isn in 1:length(sou)
                addmt!(model.wf, sou[isn], it)
            end
            run!(model)
        end

#=== Output Recordings only ===#
  elseif rec != nothing && Slices == nothing
      simurec(model,sou,rec,OptCpnt,RecDataPath)

#=== Output Wavefields only ===#
  elseif rec == nothing && Slices != nothing
      simuwf(model,sou,Slices,OptCpnt,WFDataPath)

#=== Output both Recordings and Wavefields ===#
  elseif rec != nothing && Slices != nothing
      simurecwf(model,sou,rec,RecDataPath,Slices,WFDataPath,OptCpnt)
  end
end


using CUDA.CUDNN:
    cudnnMultiHeadAttnForward,
    cudnnMultiHeadAttnForward!,
    cudnnMultiHeadAttnBackwardData,
    cudnnMultiHeadAttnBackwardWeights,
    cudnnGetMultiHeadAttnBuffers,
    cudnnGetMultiHeadAttnWeights,
    cudnnAttnDescriptor,
        cudnnAttnDescriptor_t,
        cudnnCreateAttnDescriptor,
        cudnnDestroyAttnDescriptor,
        cudnnSetAttnDescriptor,
        cudnnGetAttnDescriptor,
        cudnnDataType_t,
        cudnnDropoutDescriptor_t,
    cudnnAttnQueryMap_t,
        CUDNN_ATTN_QUERYMAP_ALL_TO_ONE, # 0         /* multiple Q-s map to a single (K,V) set when beam size > 1, beam sizes for (K,V) = 1 */
        CUDNN_ATTN_QUERYMAP_ONE_TO_ONE, # (1U << 0) /* multiple Q-s map to multiple (K,V) sets when beam size > 1, beam sizes for (K,V) = beam size for (Q) */
        CUDNN_ATTN_DISABLE_PROJ_BIASES, # 0         /* no biases in attention input and output projections */
        CUDNN_ATTN_ENABLE_PROJ_BIASES,  # (1U << 1) /* use biases in attention input and output projections */
    cudnnMultiHeadAttnWeightKind_t,
        CUDNN_MH_ATTN_Q_WEIGHTS, # 0, /* input projection weights for 'queries' */
        CUDNN_MH_ATTN_K_WEIGHTS, # 1, /* input projection weights for 'keys' */
        CUDNN_MH_ATTN_V_WEIGHTS, # 2, /* input projection weights for 'values' */
        CUDNN_MH_ATTN_O_WEIGHTS, # 3, /* output projection weights */
        CUDNN_MH_ATTN_Q_BIASES,  # 4, /* input projection bias tensor for 'queries' */
        CUDNN_MH_ATTN_K_BIASES,  # 5, /* input projection bias for 'keys' */
        CUDNN_MH_ATTN_V_BIASES,  # 6, /* input projection bias for 'values' */
        CUDNN_MH_ATTN_O_BIASES,  # 7, /* output projection biases */
    cudnnMathType_t,
        CUDNN_DEFAULT_MATH,                    # 0,
        CUDNN_TENSOR_OP_MATH,                  # 1,
        CUDNN_TENSOR_OP_MATH_ALLOW_CONVERSION, # 2,
        CUDNN_FMA_MATH,                        # 3,
    cudnnWgradMode_t,
        CUDNN_WGRAD_MODE_ADD,  # 0,
        CUDNN_WGRAD_MODE_SET,  # 1,
    cudnnSeqDataDescriptor,
        cudnnSeqDataDescriptor_t,
        cudnnCreateSeqDataDescriptor,
        cudnnDestroySeqDataDescriptor,
        cudnnSetSeqDataDescriptor,
        cudnnGetSeqDataDescriptor,
    cudnnSeqDataAxis_t,
        CUDNN_SEQDATA_TIME_DIM,  # 0, /* index in time */
        CUDNN_SEQDATA_BATCH_DIM, # 1, /* index in batch */
        CUDNN_SEQDATA_BEAM_DIM,  # 2, /* index in beam */
        CUDNN_SEQDATA_VECT_DIM,  # 3  /* index in vector */
        CUDNN_SEQDATA_DIM_COUNT, # 4
    cudnnDataType,
    cudnnSeqDataDefaultAxes,
    math_mode,
    sdim

@testset "cudnn/multiheadattn" begin
    function mhatest(;
        # Input tensor descriptors
        axes::Vector{cudnnSeqDataAxis_t} = cudnnSeqDataDefaultAxes,
        seqLengthsQO::Vector{<:Integer} = fill(Cint(sdim(queries,axes,CUDNN_SEQDATA_TIME_DIM)), sdim(queries,axes,CUDNN_SEQDATA_BATCH_DIM)*sdim(queries,axes,CUDNN_SEQDATA_BEAM_DIM)),
        seqLengthsKV::Vector{<:Integer} = fill(Cint(sdim(keys,axes,CUDNN_SEQDATA_TIME_DIM)), sdim(keys,axes,CUDNN_SEQDATA_BATCH_DIM)*sdim(keys,axes,CUDNN_SEQDATA_BEAM_DIM)),
        #devSeqLengthsQO::CuVector{Cint} = convert(CuVector{Cint}, seqLengthsQO),
        #devSeqLengthsKV::CuVector{Cint} = convert(CuVector{Cint}, seqLengthsKV),
        #qDesc::cudnnSeqDataDescriptor = cudnnSeqDataDescriptor(queries; axes, seqLengthArray=seqLengthsQO),
        #kDesc::cudnnSeqDataDescriptor = cudnnSeqDataDescriptor(keys;    axes, seqLengthArray=seqLengthsKV),
        #vDesc::cudnnSeqDataDescriptor = cudnnSeqDataDescriptor(values;  axes, seqLengthArray=seqLengthsKV),

        # attnDesc parameters
        attnMode::Unsigned = CUDNN_ATTN_QUERYMAP_ALL_TO_ONE | CUDNN_ATTN_DISABLE_PROJ_BIASES |> Cuint,
        nHeads::Integer = Cint(1),
        smScaler::Real = Cdouble(1),
        # dataType::DataType = eltype(queries),
        # computePrec::DataType = eltype(queries),  ## No other option according to 8.0.2
        mathType::cudnnMathType_t = math_mode(),
        # attnDropout::Real = 0, ## The dropout option is currently not supported by the multi-head attention API
        # postDropout::Real = 0, ## The dropout option is currently not supported by the multi-head attention API
        qProjSize::Integer = 0, # Use zero to disable the corresponding projection
        kProjSize::Integer = 0,
        vProjSize::Integer = 0,
        oProjSize::Integer = 0,
        qoMaxSeqLength::Integer = sdim(queries,axes,CUDNN_SEQDATA_TIME_DIM),
        kvMaxSeqLength::Integer = sdim(keys,axes,CUDNN_SEQDATA_TIME_DIM),
        maxBatchSize::Integer = sdim(queries,axes,CUDNN_SEQDATA_BATCH_DIM),
        maxBeamSize::Integer = sdim(queries,axes,CUDNN_SEQDATA_BEAM_DIM),

        # forw parameters
        residuals = nothing,
        currIdx::Integer = -1,
        loWinIdx::Array{Cint} = fill(Cint(0), qoMaxSeqLength),
        hiWinIdx::Array{Cint} = fill(Cint(kvMaxSeqLength), qoMaxSeqLength),
        #workspace::Union{CuArray,Nothing}    = nothing,
        #reserveSpace::Union{CuArray,Nothing} = nothing,
    )
        attnDesc::cudnnAttnDescriptor = cudnnAttnDescriptor(
            Cuint(attnMode),
            Cint(nHeads),
            Cdouble(smScaler),
            cudnnDataType(eltype(queries)),    # dataType
            cudnnDataType(eltype(queries)),    # computePrec
            mathType,
            C_NULL,  # attnDropout
            C_NULL,  # postDropout
            Cint(sdim(queries,axes,CUDNN_SEQDATA_VECT_DIM)), # qSize
            Cint(sdim(keys,   axes,CUDNN_SEQDATA_VECT_DIM)), # kSize
            Cint(sdim(values, axes,CUDNN_SEQDATA_VECT_DIM)), # vSize
            Cint(qProjSize),
            Cint(kProjSize),
            Cint(vProjSize),
            Cint(oProjSize),
            Cint(qoMaxSeqLength),
            Cint(kvMaxSeqLength),
            Cint(maxBatchSize),
            Cint(maxBeamSize)
        )
        y = cudnnMultiHeadAttnForward(weights, queries, keys, values; axes, seqLengthsQO,
                                      seqLengthsKV, attnMode, nHeads, smScaler, mathType,
                                      qProjSize, kProjSize, vProjSize, oProjSize,
                                      qoMaxSeqLength, kvMaxSeqLength, maxBatchSize,
                                      maxBeamSize, residuals, currIdx, loWinIdx, hiWinIdx)
        @test Array(y) ≈ cudnnMultiHeadAttnForward!(zero(y), weights, queries, keys, values; axes,
                                                    seqLengthsQO, seqLengthsKV, attnMode, nHeads,
                                                    smScaler, mathType, qProjSize, kProjSize,
                                                    vProjSize, oProjSize, qoMaxSeqLength,
                                                    kvMaxSeqLength, maxBatchSize, maxBeamSize,
                                                    residuals, currIdx, loWinIdx, hiWinIdx) |> Array
        @test Array(y) ≈ cudnnMultiHeadAttnForward(weights, queries, keys, values, attnDesc;
                                                   axes, seqLengthsQO, seqLengthsKV, residuals,
                                                   currIdx, loWinIdx, hiWinIdx) |> Array
        @test Array(y) ≈ cudnnMultiHeadAttnForward!(zero(y), weights, queries, keys, values, attnDesc;
                                                    axes, seqLengthsQO, seqLengthsKV, residuals,
                                                    currIdx, loWinIdx, hiWinIdx) |> Array
    end

    Q,K,V,B,T,F = 6,6,5,4,3,Float32

    weights, queries, keys, values = (CUDA.randn(x...) for x in ((F,100),(F,Q,B,T),(F,K,B,T),(F,V,B,T)))
    mhatest()
    mhatest(attnMode = CUDNN_ATTN_QUERYMAP_ALL_TO_ONE | CUDNN_ATTN_ENABLE_PROJ_BIASES |> Cuint, vProjSize=7)
    mhatest(seqLengthsQO = Cint[1,2,3,1])
    mhatest(seqLengthsKV = Cint[1,2,3,1])
    mhatest(nHeads = 2)
    mhatest(smScaler = 2)
    mhatest(mathType = CUDNN_DEFAULT_MATH)
    mhatest(mathType = CUDNN_TENSOR_OP_MATH)
    mhatest(mathType = CUDNN_TENSOR_OP_MATH_ALLOW_CONVERSION)
    mhatest(mathType = CUDNN_FMA_MATH)
    mhatest(kProjSize = 7, qProjSize = 7) # k and q have to match
    mhatest(vProjSize = 7)
    mhatest(oProjSize = 7)
    mhatest(qoMaxSeqLength = 7)
    mhatest(kvMaxSeqLength = 7)
    mhatest(maxBatchSize = 7)
    mhatest(maxBeamSize = 7)
    mhatest(loWinIdx = fill(Cint(1),T))
    mhatest(hiWinIdx = fill(Cint(1),T))
    mhatest(currIdx = 0)

    # Test residuals: residuals and output (and thus values unless oProjSize>0) must match queries in vector size
    values, residuals = (CUDA.randn(x...) for x in ((F,Q,B,T),(F,Q,B,T)))
    mhatest(residuals = residuals)

    # Test nonstandard axes order
    weights, queries, keys, values = (CUDA.randn(x...) for x in ((F,100),(F,Q,T,B),(F,K,T,B),(F,V,T,B)))
    mhatest(axes = [CUDNN_SEQDATA_VECT_DIM, CUDNN_SEQDATA_TIME_DIM, CUDNN_SEQDATA_BATCH_DIM, CUDNN_SEQDATA_BEAM_DIM])

    # Test beam handling
    weights, queries, keys, values = (CUDA.randn(x...) for x in ((F,100),(F,Q,B,T,2),(F,K,B,T,1),(F,V,B,T,1)))
    mhatest()

    # CUDNN_ATTN_QUERYMAP_ONE_TO_ONE does not seem to be supported
    #weights, queries, keys, values = (CUDA.randn(x...) for x in ((F,100),(F,Q,B,T,M),(F,K,B,T,M),(F,V,B,T,M)))
    #mhatest(attnMode = CUDNN_ATTN_QUERYMAP_ONE_TO_ONE | CUDNN_ATTN_DISABLE_PROJ_BIASES |> Cuint)
end


"""
Translate mouse or modifier state into the corresponding XCB value.
"""
function state_xcb end

state_xcb(s::MouseState) = .&(UInt[s.left, s.middle, s.right, s.scroll_up, s.scroll_down, s.any] .* UInt[XCB_BUTTON_MASK_1, XCB_BUTTON_MASK_2, XCB_BUTTON_MASK_3, XCB_BUTTON_MASK_4, XCB_BUTTON_MASK_5, XCB_BUTTON_MASK_ANY]...)
state_xcb(s::KeyModifierState) = sum(2 .^ (0:3) .* Int[s.shift, s.ctrl, s.alt, s.super])
state_xcb(::Any) = 0

state_xcb(e::MouseEvent) = state_xcb(e.state)
state_xcb(e::KeyEvent) = state_xcb(e.modifiers)

"""
Translate an action into its corresponding XCB value.
"""
function response_type_xcb end

response_type_xcb(::ButtonPressed) = XCB_BUTTON_PRESS
response_type_xcb(::ButtonReleased) = XCB_BUTTON_RELEASE
response_type_xcb(::KeyPressed) = XCB_KEY_PRESS
response_type_xcb(::KeyReleased) = XCB_KEY_RELEASE
response_type_xcb(::PointerMoves) = XCB_MOTION_NOTIFY
response_type_xcb(::PointerEntersWindow) = XCB_ENTER_NOTIFY
response_type_xcb(::PointerLeavesWindow) = XCB_LEAVE_NOTIFY
response_type_xcb(::Expose) = XCB_EXPOSE
response_type_xcb(::Resize) = XCB_CONFIGURE_NOTIFY

event_type_xcb(::Union{ButtonPressed, ButtonReleased}) = xcb_button_press_event_t
event_type_xcb(::Union{KeyPressed, KeyReleased}) = xcb_key_press_event_t
event_type_xcb(::PointerMoves) = xcb_motion_notify_event_t
event_type_xcb(::Union{PointerEntersWindow, PointerLeavesWindow}) = xcb_enter_notify_event_t
event_type_xcb(::ResizeEvent) = xcb_configure_notify_event_t
event_type_xcb(::ExposeEvent) = xcb_expose_event_t

button_xcb(::ButtonLeft) = XCB_BUTTON_INDEX_1
button_xcb(::ButtonMiddle) = XCB_BUTTON_INDEX_2
button_xcb(::ButtonRight) = XCB_BUTTON_INDEX_3
button_xcb(::ButtonScrollUp) = XCB_BUTTON_INDEX_4
button_xcb(::ButtonScrollDown) = XCB_BUTTON_INDEX_5

detail_xcb(wh::XWindowHandler, e::MouseEvent) = button_xcb(e.button)
detail_xcb(wh::XWindowHandler, e::PointerMovesEvent) = XCB_MOTION_NORMAL
detail_xcb(wh::XWindowHandler, e::PointerEntersWindowEvent) = XCB_ENTER_NOTIFY
detail_xcb(wh::XWindowHandler, e::PointerLeavesWindowEvent) = XCB_LEAVE_NOTIFY
detail_xcb(wh::XWindowHandler, e::EventData) = 0
detail_xcb(wh::XWindowHandler, e::EventDetails) = detail_xcb(wh, e.data)
detail_xcb(wh::XWindowHandler, e::EventDetails{<:KeyEvent}) = keycode(wh, e)

event_xcb(wh::XWindowHandler, e::EventDetails) = event_type_xcb(action(e))(
    response_type_xcb(action(e)),
    detail_xcb(wh, e),
    0,
    e.time,
    e.win.parent_id,
    e.win.id,
    0,
    0,
    0,
    e.location...,
    state_xcb(e.data),
    true,
    false
)

event_xcb(wh::XWindowHandler, e::EventDetails{<:ExposeEvent}) = event_type_xcb(action(e))(
    response_type_xcb(action(e)),
    0,
    0,
    e.win.id,
    e.location...,
    extent(e.win)...,
    0,
    0
)

event_xcb(wh::XWindowHandler, e::EventDetails{<:ResizeEvent}) = event_type_xcb(action(e))(
    response_type_xcb(action(e)),
    0,
    0,
    e.win.id,
    e.win.id,
    0,
    e.location...,
    extent(e.win)...,
    0,
    0,
    0
)

send_event(wh::XWindowHandler, e::EventDetails) = send_event(e.win, event_xcb(wh, e))

function send_event(win::XCBWindow, event)
    ref = Ref(event)
    GC.@preserve ref begin
        event_ptr = Base.reinterpret(Cstring, Base.unsafe_convert(Ptr{typeof(event)}, ref))
        @flush @check :error xcb_send_event(win.conn, false, win.id, 0, event_ptr)
    end
end

hex(x) = "0x$(string(x, base=16))"

function send(wh::XWindowHandler, win::XCBWindow; location=(0, 0))
    (event; location=location) -> send_event(wh, EventDetails(event, location, floor(time()), win))
end


# Single decision #
# ========================== #
mutable struct DecisionBridge{T} <: MOIB.Variable.AbstractBridge
    decision::Decision{T}
    variable::MOI.VariableIndex

    function DecisionBridge(decision::Decision{T},
                            variable::MOI.VariableIndex) where T
        return new{T}(decision, variable)
    end
end

function MOIB.Variable.bridge_constrained_variable(::Type{DecisionBridge{T}},
                                                   model::MOI.ModelLike,
                                                   set::SingleDecisionSet{T,NoSpecifiedConstraint}) where T
    variable = MOI.add_variable(model)
    return DecisionBridge(set.decision, variable)
end

function MOIB.Variable.bridge_constrained_variable(::Type{DecisionBridge{T}},
                                                   model::MOI.ModelLike,
                                                   set::SingleDecisionSet{T,S}) where {T, S}
    variable, constraint = MOI.add_constrained_variable(model, set.constraint)
    return DecisionBridge(set.decision, variable)
end

function MOIB.Variable.supports_constrained_variable(
    ::Type{DecisionBridge{T}}, ::Type{SingleDecisionSet{T,S}}) where {T, S}
    return true
end

function MOIB.added_constrained_variable_types(::Type{<:DecisionBridge})
    return Tuple{DataType}[]
end
function MOIB.added_constraint_types(::Type{<:DecisionBridge})
    return Tuple{DataType, DataType}[]
end

# Attributes, Bridge acting as a model
MOI.get(bridge::DecisionBridge, ::MOI.NumberOfVariables) = 1

function MOI.get(bridge::DecisionBridge, ::MOI.ListOfVariableIndices)
    return [bridge.variable]
end

# References
function MOI.delete(::MOI.ModelLike, bridge::DecisionBridge)
    MOI.delete(bridge.variable)
end

# Attributes, Bridge acting as a constraint
function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet,
                 bridge::DecisionBridge)
    return SingleDecisionSet(bridge.decision)
end

function MOI.get(model::MOI.ModelLike, ::MOI.ConstraintPrimal,
                 bridge::DecisionBridge{T}) where T
    if bridge.decision.state == Taken
        return bridge.decision.value
    end
    return MOI.get(model, MOI.VariablePrimal(), bridge.variable)
end

function MOI.get(model::MOI.ModelLike, attr::Union{MOI.VariablePrimal, MOI.VariablePrimalStart},
                 bridge::DecisionBridge{T}) where T
    if bridge.decision.state == Taken
        return bridge.decision.value
    end
    return MOI.get(model, attr, bridge.variable)
end

function MOI.set(model::MOI.ModelLike, attr::MOI.VariablePrimalStart,
                 bridge::DecisionBridge{T}, value) where T
    MOI.set(model, attr, bridge.variable, value)
end

function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintSet,
                 bridge::DecisionBridge{T}, new_set::SingleDecisionSet{T}) where T
    bridge.decision = new_set.decision
end

function MOIB.bridged_function(bridge::DecisionBridge{T}) where T
    if bridge.decision.state == NotTaken
        # Use mapped variable as standard MOI variable.
        return MOI.ScalarAffineFunction{T}(MOI.SingleVariable(bridge.variable))
    else
        # Give mapped variable as dummy decision to be held by objective/constraint bridges
        # in order to unbridge properly. Give the value of the taken decision in the
        # decision part constant given to the objective/constraint bridges
        return MOI.ScalarAffineFunction{T}([MOI.ScalarAffineTerm(one(T), bridge.variable)],
                                        bridge.decision.value)
    end
end

function MOIB.Variable.unbridged_map(bridge::DecisionBridge, vi::MOI.VariableIndex)
    return (bridge.variable => MOI.SingleVariable(vi),)
end

# Single known decision #
# ========================== #
mutable struct KnownBridge{T} <: MOIB.Variable.AbstractBridge
    known::Decision{T}

    function KnownBridge(known::Decision{T}) where T
        return new{T}(known)
    end
end

function MOIB.Variable.bridge_constrained_variable(::Type{KnownBridge{T}},
                                                   model::MOI.ModelLike,
                                                   set::SingleKnownSet{T}) where T
    return KnownBridge(set.known)
end

function MOIB.Variable.supports_constrained_variable(
    ::Type{KnownBridge{T}}, ::Type{SingleKnownSet{T}}) where T
    return true
end

function MOIB.added_constrained_variable_types(::Type{<:KnownBridge})
    return Tuple{DataType}[]
end
function MOIB.added_constraint_types(::Type{<:KnownBridge})
    return Tuple{DataType, DataType}[]
end

# Attributes, Bridge acting as a model
MOI.get(bridge::KnownBridge, ::MOI.NumberOfVariables) = 0
function MOI.get(bridge::KnownBridge, ::MOI.ListOfVariableIndices)
    return MOI.VariableIndex[]
end

# References
function MOI.delete(::MOI.ModelLike, bridge::KnownBridge)
    # Nothing to do
    return nothing
end

# Attributes, Bridge acting as a constraint
function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet,
                 bridge::KnownBridge)
    return SingleKnownSet(bridge.known)
end

function MOI.get(model::MOI.ModelLike, ::MOI.ConstraintPrimal,
                 bridge::KnownBridge{T}) where T
    return bridge.known.value
end

function MOI.get(model::MOI.ModelLike, attr::Union{MOI.VariablePrimal, MOI.VariablePrimalStart},
                 bridge::KnownBridge{T}) where T
    return bridge.known.value
end

function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintSet,
                 bridge::KnownBridge{T}, new_set::SingleKnownSet{T}) where T
    bridge.known = new_set.known
    return nothing
end

function MOIB.bridged_function(bridge::KnownBridge{T}) where T
    # Give the value of the known decision in the
    # known part constant given to the objective/constraint bridges
    return convert(MOI.ScalarAffineFunction{T}, bridge.known.value)
end

function MOIB.Variable.unbridged_map(bridge::KnownBridge, vi::MOI.VariableIndex)
    # Ignore unbridging without errors
    return (vi => MOI.SingleVariable(vi),)
end

# Multiple decisions #
# ========================== #
struct DecisionsBridge{T} <: MOIB.Variable.AbstractBridge
    decisions::Vector{Decision{T}}
    variables::Vector{MOI.VariableIndex}
end

function MOIB.Variable.bridge_constrained_variable(::Type{DecisionsBridge{T}},
                                                   model::MOI.ModelLike,
                                                   set::MultipleDecisionSet{T,NoSpecifiedConstraint}) where T
    variables = MOI.add_variables(model, length(set.decisions))
    return DecisionsBridge(set.decisions, variables)
end

function MOIB.Variable.bridge_constrained_variable(::Type{DecisionsBridge{T}},
                                                   model::MOI.ModelLike,
                                                   set::MultipleDecisionSet{T,S}) where {T, S}
    variables, constraints = MOI.add_constrained_variables(model, set.constraint)
    return DecisionsBridge(set.decisions, variables)
end

function MOIB.Variable.supports_constrained_variable(
    ::Type{DecisionsBridge{T}}, ::Type{MultipleDecisionSet{T,S}}) where {T, S}
    return true
end

function MOIB.added_constrained_variable_types(::Type{<:DecisionsBridge})
    return Tuple{DataType}[]
end
function MOIB.added_constraint_types(::Type{<:DecisionsBridge})
    return Tuple{DataType, DataType}[]
end

# Attributes, Bridge acting as a model
MOI.get(bridge::DecisionsBridge, ::MOI.NumberOfVariables) = length(bridge.variables)

function MOI.get(bridge::DecisionsBridge, ::MOI.ListOfVariableIndices)
    return bridge.variables
end

# References
function MOI.delete(model::MOI.ModelLike, bridge::DecisionsBridge)
    MOI.delete(model, bridge.variables)
end

function MOI.delete(model::MOI.ModelLike, bridge::DecisionsBridge, i::MOIB.Variable.IndexInVector)
    MOI.delete(model, bridge.variables[i.value])
    deleteat!(bridge.variables, i.value)
end

# Attributes, Bridge acting as a constraint
function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet,
                 bridge::DecisionsBridge)
    return MultipleDecisionSet(bridge.decisions)
end

function MOI.get(model::MOI.ModelLike, ::MOI.ConstraintPrimal,
                 bridge::DecisionsBridge{T}) where T
    return [decision.state == Taken ? decision_value.(bridge.decisions) :
            MOI.get(model,
                    MOI.VariablePrimal(),
                    bridge.variables[i]) for (i,decision) in bridge.decisions]
end

function MOI.get(model::MOI.ModelLike,
                 attr::Union{MOI.VariablePrimal, MOI.VariablePrimalStart},
                 bridge::DecisionsBridge, i::MOIB.Variable.IndexInVector)
    decision = bridge.decisions[i.value]
    if decision.state == Taken
        return decision.value
    else
        return MOI.get(model, attr, bridge.variables[i.value])
    end
end

function MOI.set(model::MOI.ModelLike, attr::MOI.VariablePrimalStart,
                 bridge::DecisionsBridge{T}, val, i::MOIB.Variable.IndexInVector) where T
    MOI.set(model, attr, bridge.variables[i.value], val)
end

function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintSet,
                 bridge::DecisionsBridge{T}, new_set::MultipleDecisionSet{T}) where T
    bridge.decisions .= new_set.decisions
end

function MOIB.bridged_function(bridge::DecisionsBridge{T}, i::MOIB.Variable.IndexInVector) where T
    decision = bridge.decisions[i.value]
    if decision.state == NotTaken
        # Use mapped variable as standard MOI variable.
        return MOI.ScalarAffineFunction{T}(MOI.SingleVariable(bridge.variables[i.value]))
    else
        # Give mapped variable as dummy decision to be held by objective/constraint bridges
        # in order to unbridge properly. Give the value of the taken decision in the
        # decision part constant given to the objective/constraint bridges
        return MOI.ScalarAffineFunction{T}([MOI.ScalarAffineTerm(one(T), bridge.variables[i.value])],
                                           decision.value)
    end
end
function MOIB.Variable.unbridged_map(bridge::DecisionsBridge, vi::MOI.VariableIndex, i::MOIB.Variable.IndexInVector)
    return (bridge.variables[i.value] => MOI.SingleVariable(vi),)
end

# Multiple knowns #
# ========================== #
struct KnownsBridge{T} <: MOIB.Variable.AbstractBridge
    knowns::Vector{Decision{T}}
end

function MOIB.Variable.bridge_constrained_variable(::Type{KnownsBridge{T}},
                                                   model::MOI.ModelLike,
                                                   set::MultipleKnownSet{T}) where T
    return KnownsBridge(set.knowns)
end

function MOIB.Variable.supports_constrained_variable(
    ::Type{KnownsBridge{T}}, ::Type{MultipleKnownSet{T}}) where T
    return true
end

function MOIB.added_constrained_variable_types(::Type{<:KnownsBridge})
    return Tuple{DataType}[]
end
function MOIB.added_constraint_types(::Type{<:KnownsBridge})
    return Tuple{DataType, DataType}[]
end

# Attributes, Bridge acting as a model
MOI.get(bridge::KnownsBridge, ::MOI.NumberOfVariables) = 0
function MOI.get(bridge::KnownsBridge, ::MOI.ListOfVariableIndices)
    return MOI.VariableIndex[]
end

# References
function MOI.delete(model::MOI.ModelLike, bridge::KnownsBridge)
    # Nothing to do
    return nothing
end

function MOI.delete(model::MOI.ModelLike, bridge::KnownsBridge, i::MOIB.Variable.IndexInVector)
    # Nothing to do
    return nothing
end

# Attributes, Bridge acting as a constraint
function MOI.get(::MOI.ModelLike, ::MOI.ConstraintSet,
                 bridge::KnownsBridge)
    return MultipleKnownSet(bridge.knowns)
end

function MOI.get(model::MOI.ModelLike, ::MOI.ConstraintPrimal,
                 bridge::KnownsBridge{T}) where T
    return map(bridge.knowns) do known
        known.value
    end
end

function MOI.get(model::MOI.ModelLike,
                 attr::Union{MOI.VariablePrimal, MOI.VariablePrimalStart},
                 bridge::KnownsBridge, i::MOIB.Variable.IndexInVector)
    return bridge.knowns[i.value].value
end

function MOI.set(model::MOI.ModelLike, attr::MOI.ConstraintSet,
                 bridge::KnownsBridge{T}, new_set::MultipleDecisionSet{T}) where T
    bridge.knowns .= new_set.knowns
end

function MOIB.bridged_function(bridge::KnownsBridge{T}, i::MOIB.Variable.IndexInVector) where T
    known = bridge.knowns[i.value]
    # Give the value of the known decision in the
    # known part constant given to the objective/constraint bridges
    return convert(MOI.ScalarAffineFunction{T}, known.value)
end
function MOIB.Variable.unbridged_map(bridge::KnownsBridge, vi::MOI.VariableIndex, i::MOIB.Variable.IndexInVector)
    # Ignore unbridging without errors
    return (vi => MOI.SingleVariable(vi),)
end

# Modifications #
# ========================== #
# To enable certain modifications (decision coefficient changes and decision state updates)
# a few methods must be added to MathOptInterface
function MOIB.is_bridged(b::MOIB.AbstractBridgeOptimizer,
                         change::Union{DecisionCoefficientChange, DecisionMultirowChange})
    return MOIB.is_bridged(b, change.decision)
end

function MOIB.is_bridged(b::MOIB.AbstractBridgeOptimizer,
                         change::Union{DecisionStateChange, DecisionsStateChange,
                                       KnownCoefficientChange, KnownMultirowChange,
                                       KnownValueChange, KnownValuesChange})
    # These modifications should not be handled by the variable bridges
    return false
end

function MOIB.modify_bridged_change(b::MOIB.AbstractBridgeOptimizer, obj,
                                    change::DecisionCoefficientChange)
    f = MOIB.bridged_variable_function(b, change.decision)
    # Continue modification with mapped variable
    MOI.modify(b, obj, DecisionCoefficientChange(only(f.terms).variable_index, change.new_coefficient))
    return nothing
end

function MOIB.modify_bridged_change(b::MOIB.AbstractBridgeOptimizer, obj,
                                    change::DecisionMultirowChange)
    f = MOIB.bridged_variable_function(b, change.decision)
    # Continue modification with mapped variable
    MOI.modify(b, obj, DecisionMultirowChange(only(f.terms).variable_index, change.new_coefficients))
    return nothing
end


using BenchmarkTools
using FoldsCatwalk
using Transducers

returnnothing(_) = nothing
returnnothing(_, _) = nothing

foreach1(f, xs, exc) = transduce(Map(f), returnnothing, nothing, xs, exc)

type_instability(x) = Val(round(Int, 2x))

trydispatch(_) = nothing
trydispatch(::Val{typemax(Int)}) = error("unexpected!") # so that Julia has to try dispatch

function foreach_baseline(xs)
    itr = xs |> Map(type_instability)
    foreach1(trydispatch, itr, SequentialEx())
end

function foreach_catwalk(xs)
    itr = xs |> Map(type_instability) |> OptimizeInner()
    foreach1(trydispatch, itr, CatwalkEx())
end

function demo_foreach()
    xs = randn(1000_000)
    @btime foreach_baseline($xs)
    @btime foreach_catwalk($xs)
end


# We wish to extend operations to identically named methods dispatched
# on `Machine`s and `NodalMachine`s. For example, we have from the model API
#
# `predict(model::M, fitresult, X) where M<:Supervised`
#
# but want also want
#
# `predict(machine::Machine, X)` where `X` is data
#
# and "networks.jl" requires us to define
#
# `predict(machine::NodalMachine, X)` where `X` is data
#
# and we would like the syntactic sugar (for `X` a node):
#
# `predict(machine::NodalMachine, X::Node)=node(predict, machine, X)`
#
# (If an operation has zero arguments, we cannot achieve the last
# desire because of ambiguity with the preceding one.)

## TODO: need to add checks on the arguments of
## predict(::AbstractMachine, ) and transform(::AbstractMachine, )

for operation in (:predict, :predict_mean, :predict_mode, :predict_median,
                  :transform, :inverse_transform)
    ex = quote
        function $(operation)(machine::AbstractMachine{M}, args...) where M
            if isdefined(machine, :fitresult) || M <: Static
                return $(operation)(machine.model, machine.fitresult, args...)
            else
                throw(error("$machine has not been trained."))
            end
        end
        $(operation)(machine::Machine; rows=:) =
            $(operation)(machine, selectrows(machine.args[1], rows))
        $(operation)(machine::NodalMachine, args::AbstractNode...) =
            node($(operation), machine, args...)
    end
    eval(ex)
end

# the zero argument special cases:
"""
    fitted_params(mach)

Return the learned parameters for a machine `mach` that has been
`fit!`, for example the coefficients in a linear model.

This is a named tuple and human-readable if possible.

If `mach` is a machine for a composite model, then the returned value
has keys `machines` and `fitted_params_given_machine`, whose
corresponding values are a vector of (nodal) machines appearing in the
underlying learning network, and a dictionary of reports keyed on
those machines.

```julia
using MLJ
X, y = @load_crabs;
pipe = @pipeline MyPipe(
    std = Standardizer(),
    clf = @load LinearBinaryClassifier pkg=GLM
)
mach = machine(MyPipe(), X, y) |> fit!
fp = fitted_params(mach)
machs = fp.machines
2-element Array{Any,1}:
 NodalMachine{LinearBinaryClassifier{LogitLink}} @ 1…57
 NodalMachine{Standardizer} @ 7…33

fp.fitted_params_given_machine[machs[1]]
(coef = [121.05433477939319, 1.5863921128182814,
         61.0770377473622, -233.42699281787324, 72.74253591435117],
 intercept = 10.384459260848505,)
```

"""
function fitted_params(machine::AbstractMachine)
    if isdefined(machine, :fitresult)
        return fitted_params(machine.model, machine.fitresult)
    else
        throw(error("$machine has not been trained."))
    end
end