# specific to Julia, that is, related to the output file format (language).

#=
F: read the file containing constants
I:
O:
BA
=#
function jl_include_constants_from_literature(src_file_name, pad_string)
  # create src_buffer -
  src_buffer = ""
  # path to distrubtion -
  path_to_src_file = src_file_name
  open(path_to_src_file,"r") do src_file
    for line in eachline(src_file)
      src_buffer *= pad_string*"$line"
    end
  end
  return src_buffer
end

#=
F: generate kinetic buffer
I:
O: return monod affinity constant symbol, mRNA species array,
  protein species array, W_string_array, disassociation_const_string_array.
BA
=#
function jl_build_kinetics_buffer(all_species_dict::Dict{String, Int},
  all_rnx_list::Array, all_txtl_dict::Dict, sys2user::Dict)
  kinetics = "function calculate_kinetics(X, data_dictionary)" *
             "\n\n\t# load all species dictionary" *
             "\n\tall_species_dict = data_dictionary[\"all_species_dict\"]  # String-->Int"
  # for signaling
  kinetics *= "\n\n\t# generate kinetics for signaling, assume saturation kinetics"
  totalRnxNo = length(all_rnx_list)
  kinetics *= "\n\t# load Monod affinity constants" *
              "\n\tMonodK = data_dictionary[\"MonodAffinityConstantDict\"]  # String-->Float"
  MonodAffinityConstant_String_Array = Array{String,1}()  # collect monod affinity constant symbol
  kinetics *= "\n\t# load reaction kinetic constants" *
              "\n\tkcat = data_dictionary[\"kcat_signaling\"]  # in order of rnx" *
              "\n\n\t# write reaction rate equations" *
              "\n\trnx_rate_vector = zeros($totalRnxNo)"
  for (index, rnx) in enumerate(all_rnx_list)  # go thru every rnx
    tmp_rnx_rate = "kcat[$index]"
    if isdefined(rnx, :catalysts)  # enzyme
      for token in rnx.catalysts
        tmp_rnx_rate *= "*X[$(all_species_dict[token.oriBioName])]"
      end
    end
    if isdefined(rnx,:reactants)  # substrate
      for token in rnx.reactants
        tmp_rnx_rate *= "*X[$(all_species_dict[token.oriBioName])]/(MonodK[\"MonodK~rnx$(index)~$(token.oriBioName)\"]+X[$(all_species_dict[token.oriBioName])])"
        push!(MonodAffinityConstant_String_Array, "MonodK~rnx$(index)~$(token.oriBioName)")
      end
    end
    kinetics *= "\n\trnx_rate_vector[$index] = "*tmp_rnx_rate
  end

  # for TXTL
  W_string_array = Array{String,1}()  # W: weight of protein action on transcription
  disassociation_const_string_array = Array{String,1}()  # disassociation constant in tranfer function
  kinetics *= "\n\n\t# generate kinetics & control terms for TXTL" *
              "\n\t# load data from data dictionary" *
              "\n\tW_value_dict = data_dictionary[\"W_value_dict\"]  #String-->Float" *
              "\n\tcoop = data_dictionary[\"cooperativity\"]" *
              "\n\tdisassociation_constant_dict = data_dictionary[\"transferFunctionDisassociationConstantDict\"]" *
              "\n\tbackground_control_term = data_dictionary[\"backgroundGeneExpressionControlTermDict\"]" *
              "\n\tkcatTX = data_dictionary[\"kcatTranscription\"]" *
              "\n\tRNAPconc = data_dictionary[\"RNAPConcentration\"]" *
              "\n\tgeneConc = data_dictionary[\"avgGeneConcentration\"]" *
              "\n\tsaturationTX = data_dictionary[\"transcriptionSaturationConstant\"]" *
              "\n\tkcatTL = data_dictionary[\"kcatTranslation\"]" *
              "\n\tRIBOconc = data_dictionary[\"RIBOConcentration\"]" *
              "\n\tsaturationTL = data_dictionary[\"translationSaturationConstant\"]"
  totalTXTLNo = length(all_txtl_dict)
  kinetics *= "\n\n\t# write control terms, transcription rate equations, and translation rate equations" *
             "\n\tTX_control_term = Dict{String, Float64}()  # control term" *
             "\n\tTX_rate_vector = Dict{String, Float64}()  # transcription rate" *
             "\n\tTL_rate_vector = Dict{String, Float64}()  # translation rate"
  for (key, txtl) in all_txtl_dict # go thru every txtl
    tmp_protein_string = replace(key, sys2user["MRNA"] => sys2user["PROTEIN"], count=1)
    kinetics *= "\n\t# $key and $tmp_protein_string"
    up_factors_array = Array{String,1}()  # collection of upregulation factors name: targetedmRNA_factor(s)
    if !isempty(txtl.activationProtein)  # upregulation
      for token_array in txtl.activationProtein
        tmp_up = ""
        tmp_up_name = key*"_"
        for token in token_array
          tmp_W = "W~$(token.oriBioName)~$(key)"
          tmp_up *= "W_value_dict[\"$(tmp_W)\"] * X[$(all_species_dict[token.oriBioName])]^coop/((disassociation_constant_dict[\"KD~$(token.oriBioName)\"])^coop+X[$(all_species_dict[token.oriBioName])]^coop)*"
          push!(W_string_array, tmp_W)
          push!(disassociation_const_string_array, "KD~$(token.oriBioName)")
          tmp_up_name *= token.oriBioName*"_"
        end
        tmp_up = chop(tmp_up)
        tmp_up_name = chop(tmp_up_name)
        push!(up_factors_array, tmp_up_name)
        kinetics *= "\n\t$(tmp_up_name) = $(tmp_up)"
      end
    end
    down_factors_array = Array{String,1}()  # collection of downregulation factors name: targetedmRNA_factor(s)
    if !isempty(txtl.inhibitionProtein)  # downregulation
      for token_array in txtl.inhibitionProtein
        tmp_down = ""
        tmp_down_name = key*"_"
        for token in token_array
          tmp_W = "W~$(token.oriBioName)~$(key)"
          tmp_down *= "W_value_dict[\"$(tmp_W)\"] * X[$(all_species_dict[token.oriBioName])]^coop/((disassociation_constant_dict[\"KD~$(token.oriBioName)\"])^coop+X[$(all_species_dict[token.oriBioName])]^coop)*"
          push!(W_string_array, tmp_W)
          push!(disassociation_const_string_array, "KD~$(token.oriBioName)")
          tmp_down_name *= token.oriBioName*"_"
        end
        tmp_down = chop(tmp_down)
        tmp_down_name = chop(tmp_down_name)
        push!(down_factors_array, tmp_down_name)
        kinetics *= "\n\t$(tmp_down_name) = $(tmp_down)"
      end
    end
    # combine to form up and down, then merge up and down to form control term
    if !isempty(up_factors_array) && !isempty(down_factors_array)
      up_action = ""
      for up in up_factors_array
        up_action *= up*" +"
      end
      kinetics *= "\n\tup_action_on_$key = $(chop(up_action))"
      down_action = ""
      for down in down_factors_array
        down_action *= down*" +"
      end
      kinetics *= "\n\tdown_action_on_$key = $(chop(down_action))" *
                  "\n\tTX_control_term[\"$key\"] = (background_control_term[\"$key\"] + up_action_on_$key)/(1 + background_control_term[\"$key\"] + up_action_on_$key + down_action_on_$key)"
    elseif !isempty(up_factors_array)
      up_action = ""
      for up in up_factors_array
        up_action *= up*" +"
      end
      kinetics *= "\n\tup_action_on_$key = $(chop(up_action))" *
                  "\n\tTX_control_term[\"$key\"] = (background_control_term[\"$key\"] + up_action_on_$key)/(1 + background_control_term[\"$key\"] + up_action_on_$key)"
    elseif !isempty(down_factors_array)
      down_action = ""
      for down in down_factors_array
        down_action *= down*" +"
      end
      kinetics *= "\n\tdown_action_on_$key = $(chop(down_action))" *
                  "\n\tTX_control_term[\"$key\"] = background_control_term[\"$key\"]/(1 + background_control_term[\"$key\"] + down_action_on_$key)"
    else
      # seems impossible, a TXTL dict is constructed b/c something acts on the key
    end
    kinetics *= "\n\tTX_rate_vector[\"$key\"] = TX_control_term[\"$key\"]*kcatTX*RNAPconc*geneConc/(saturationTX+geneConc)" *
                "\n\tTL_rate_vector[\"$tmp_protein_string\"] = kcatTL*RIBOconc*X[$(all_species_dict[key])]/(saturationTL+X[$(all_species_dict[key])])"
  end

  kinetics *= "\n\n\treturn (rnx_rate_vector, TX_rate_vector, TL_rate_vector)" *
              "\nend"
  return (kinetics, MonodAffinityConstant_String_Array, W_string_array, disassociation_const_string_array)
end

function jl_build_data_dictionary_buffer(host_type::AbstractString, all_species_array::Array,
  all_species2index_dict::Dict,
  rnx_species_array::Array, all_rnx_list::Array, all_txtl_dict::Dict,
  Monod_affinity_constant_array::Array, W_string_array::Array,
  disassociation_const_array::Array, mRNA_species_array::Array, protein_species_array::Array)
  # data dictionary
  buffer = "function generate_model_parameters_dictionary()"
  # stoichiometry
  buffer *= "\n\t# load stoichiometry"
  buffer *= "\n\tstoichiometric_matrix = readdlm(\"./stoichiometry.dat\")"

  No_species = length(all_species_array)
  # initial concentration
  buffer *= "\n\n\t# initial species concentration" *
    "\n\tinitial_condition = [\n\t\t"
  buffer *= join(["0.0,  # $(i)  $(all_species_array[i])" for i = 1:(No_species - 1)], "\n\t\t")
  buffer *= "\n\t\t0.0   # $(No_species) $(all_species_array[No_species])"
  buffer *= "\n\t]"

  # all species dictionary
  buffer *= "\n\n\t# all species dictionary"
  buffer *= "\n\tall_species_dict = Dict{String, Int64}()  # String --> Int"
  #all_species_dict = Dict{String, Int64}()  # create all species dict: string --> int64
  for (st, index) in all_species2index_dict
    buffer *= "\n\tall_species_dict[\"$st\"] = $index"
    #all_species_dict[st] = index
  end
  # all species dictionary_reversed
  buffer *= "\n\n\t# all species dictionary_reversed"
  buffer *= "\n\tall_species_reversed_dict = Dict{Int64, String}()  # Int --> String"
  buffer *= "\n\tfor (key,val) in all_species_dict"
  buffer *= "\n\t\tall_species_reversed_dict[val] = key"
  buffer *= "\n\tend"
  # all rnx species array
  buffer *= "\n\n\t# all rnx species array"
  buffer *= "\n\trnx_species_array::Array{String,1} = ["
  for st in rnx_species_array
    buffer *= "\n\t\t\"$st\","
  end
  buffer = chop(buffer)*"\n\t]"
  # all mRNA species array
  buffer *= "\n\n\t# all mRNA species array"
  buffer *= "\n\tmRNA_species_array::Array{String,1} = ["
  for st in mRNA_species_array
    buffer *= "\n\t\t\"$st\","
  end
  buffer = chop(buffer)*"\n\t]"
  # all protein species array
  buffer *= "\n\n\t# all protein species array"
  buffer *= "\n\tprotein_species_array::Array{String,1} = ["
  for st in protein_species_array
    buffer *= "\n\t\t\"$st\","
  end
  buffer = chop(buffer)*"\n\t]"

  # signaling reaction kinetic constants, reaction name: reactant(s)<rnx type:catalyst(s)>product(s)
  buffer *= "\n\n\t# signaling reaction kinetic constants, reaction name: reactant(s)<rnx type:catalyst(s)>product(s)"
  buffer *= "\n\tkcat_signaling = ones($(length(all_rnx_list)))  # kcat[#reaction]: reaction name"
  for (id, rnx) in enumerate(all_rnx_list)
    buffer*= "\n\tkcat_signaling[$id] = 1e-4  # kcat: $(rnx.rnxName)"
  end
  # Monod affinity constant in signaling reaction, nomenclature: MonodK_#reaction_Substrate
  buffer *= "\n\n\t# Monod affinity constant in signaling reaction, nomenclature: MonodK_#reaction_Substrate"
  buffer *= "\n\tMonodAffinityConstantDict = Dict{String, Float64}()"
  for MK in Monod_affinity_constant_array
    # buffer *= "\n\t$MK = 1.0"
    # buffer *= "\n\tMonodAffinityConstant_dict[\"$MK\"] = $MK"
    buffer *= "\n\tMonodAffinityConstantDict[\"$MK\"] = 0.1"
  end

  # W_value in transcription control term, nomenclature: W_targetmRNA_actor
  buffer *= "\n\n\n\t# W_value in transcription control term, nomenclature: W_targetmRNA_actor"
  buffer *= "\n\tW_value_dict = Dict{String, Float64}()"
  for WS in W_string_array
    buffer *= "\n\tW_value_dict[\"$WS\"] = 1.0"
  end
  # disassociation constants in transfer function, nomenclature: KD_speciesName
  buffer *= "\n\n\t# disassociation constants in transfer function, nomenclature: KD_speciesName"
  buffer *= "\n\ttransferFunctionDisassociationConstantDict = Dict{String, Float64}()"
  for DC in disassociation_const_array
    buffer *= "\n\ttransferFunctionDisassociationConstantDict[\"$DC\"] = 1.0"
  end
  # transcription specific correction factor
  buffer *= "\n\n\t# transcription specific correction factor"
  buffer *= "\n\ttranscriptionSpecificCorrectionFactorDict = Dict{String, Float64}()"
  for mrna in mRNA_species_array
    buffer *= "\n\ttranscriptionSpecificCorrectionFactorDict[\"$mrna\"] = 1.0  # $mrna"
  end
  # background gene expression control term
  buffer *= "\n\n\t# background gene expression control term"
  buffer *= "\n\tbackgroundGeneExpressionControlTermDict = Dict{String, Float64}()"
  for mrna in mRNA_species_array
    buffer *= "\n\tbackgroundGeneExpressionControlTermDict[\"$mrna\"] = 0.001  # $mrna"
  end
  # translation specific correction factor
  buffer *= "\n\n\t# translation specific correction factor"
  buffer *= "\n\ttranslationSpecificCorrectionFactor = Dict{String, Float64}()"
  for pro in protein_species_array
    buffer *= "\n\ttranslationSpecificCorrectionFactor[\"$pro\"] = 1.0  # $pro"
  end
  # cooperativity number in transfer function
  buffer *= "\n\n\t# cooperativity number in transfer function"
  buffer *= "\n\tcooperativity = 1"
  #buffer *= "\n\n\tbackground_mRNA_synthesis_rate_vector = 0.01*ones($length_TXTL)"
  # load txtl constants buffer
  buffer *= "\n\n"
  if host_type == "bacteria"
    buffer *= jl_include_constants_from_literature(
              joinpath(Base.@__DIR__, "txtl_constants_ecoli.jl"),"\n\t")
  else
    buffer *= jl_include_constants_from_literature(
              joinpath(Base.@__DIR__, "txtl_constants_hl60.jl"), "\n\t")
  end

  #---------------------------------
  # put all stuff in a Dictionary
  buffer *= "\n\n\t####################################"
  buffer *= "\n\t# put all stuff in a Dictionary"
  buffer *= "\n\tdataDictionary = Dict{String, Any}()"
  buffer *= "\n\tdataDictionary[\"stoichiometric_matrix\"] = stoichiometric_matrix"
  buffer *= "\n\tdataDictionary[\"all_species_dict\"] = all_species_dict"
  buffer *= "\n\tdataDictionary[\"all_species_reversed_dict\"] = all_species_reversed_dict"
  buffer *= "\n\tdataDictionary[\"rnx_species_array\"] = rnx_species_array"
  buffer *= "\n\tdataDictionary[\"mRNA_species_array\"] = mRNA_species_array"
  buffer *= "\n\tdataDictionary[\"protein_species_array\"] = protein_species_array"
  buffer *= "\n\tdataDictionary[\"initial_condition\"] = initial_condition"

  buffer *= "\n\tdataDictionary[\"kcat_signaling\"] = kcat_signaling"
  buffer *= "\n\tdataDictionary[\"MonodAffinityConstantDict\"] = MonodAffinityConstantDict"
  buffer *= "\n\tdataDictionary[\"W_value_dict\"] = W_value_dict"
  buffer *= "\n\tdataDictionary[\"transferFunctionDisassociationConstantDict\"] = transferFunctionDisassociationConstantDict"
  buffer *= "\n\tdataDictionary[\"transcriptionSpecificCorrectionFactorDict\"] = transcriptionSpecificCorrectionFactorDict"
  buffer *= "\n\tdataDictionary[\"backgroundGeneExpressionControlTermDict\"] = backgroundGeneExpressionControlTermDict"
  buffer *= "\n\tdataDictionary[\"translationSpecificCorrectionFactor\"] = translationSpecificCorrectionFactor"
  buffer *= "\n\tdataDictionary[\"cooperativity\"] = cooperativity"
  buffer *= "\n\tdataDictionary[\"RNAPConcentration\"] = rnapII_concentration"
  buffer *= "\n\tdataDictionary[\"RIBOConcentration\"] = ribosome_concentration"
  buffer *= "\n\tdataDictionary[\"degradationConstantmRNA\"] = degradation_constant_mRNA"
  buffer *= "\n\tdataDictionary[\"degradationConstantProtein\"] = degradation_constant_protein"
  buffer *= "\n\tdataDictionary[\"kcatTranscription\"] = kcat_transcription"
  buffer *= "\n\tdataDictionary[\"kcatTranslation\"] = kcat_translation"
  buffer *= "\n\tdataDictionary[\"avgGeneConcentration\"] = avg_gene_concentration"
  buffer *= "\n\tdataDictionary[\"transcriptionSaturationConstant\"] = saturation_transcription"
  buffer *= "\n\tdataDictionary[\"translationSaturationConstant\"] = saturation_translation"
  buffer *= "\n\tdataDictionary[\"specificGrowthRate\"] = maximum_specific_growth_rate - death_rate_constant"

  buffer *= "\n\n\treturn dataDictionary"
  buffer *= "\nend"

  return buffer
end

function jl_build_simulation_buffer(NoExtracellularSpecies::Int64)
  # buffer = build_copyright_header_buffer()
  buffer = "\n# set up ODE, get the derivatives
function Balances(t,y,dataDictionary)
  # correct for negatives
  idx_small = findall(y.<0)
  y[idx_small] .= 0.0

  # load data from data dictionary
  rnx_species = dataDictionary[\"rnx_species_array\"]
  mRNA_species = dataDictionary[\"mRNA_species_array\"]
  protein_species = dataDictionary[\"protein_species_array\"]
  stoichiometry = dataDictionary[\"stoichiometric_matrix\"]
  all_species_dict = dataDictionary[\"all_species_dict\"]  # String --> number
  specific_growth_rate = dataDictionary[\"specificGrowthRate\"]
  transcription_correction = dataDictionary[\"transcriptionSpecificCorrectionFactorDict\"]
  translation_correction = dataDictionary[\"translationSpecificCorrectionFactor\"]
  degradation_constant_mRNA = dataDictionary[\"degradationConstantmRNA\"]
  degradation_constant_protein = dataDictionary[\"degradationConstantProtein\"]

  # kinetics
  (rnx_rate, transcription_rate, translation_rate) = calculate_kinetics(y, dataDictionary)

  # calculate the derivatives
  dydt = zeros(length(all_species_dict))

  # signaling reaction network
  X2 = zeros(length(rnx_species)-$(NoExtracellularSpecies))  # species inside the cell
  for (id, st) in enumerate(rnx_species[$(NoExtracellularSpecies)+1:end])  # get X2 value from y
    X2[id] = y[all_species_dict[st]]
  end
  dX1 = stoichiometry[1:$(NoExtracellularSpecies), :]*rnx_rate  # extracellular metabolites
  dX2 = stoichiometry[$(NoExtracellularSpecies)+1:end, :]*rnx_rate - specific_growth_rate*X2  # INTRA
  dX = [dX1; dX2]
  for (id, st) in enumerate(rnx_species)  # return derivatives back to dydt
    dydt[all_species_dict[st]] = dX[id]
  end
  dydt[1] = specific_growth_rate*y[1]  # for Biomass

  # TXTL network
  for i = 1:length(mRNA_species)
  # seems easy to add additional terms to account for the signaling effect
    mRNA_id = all_species_dict[mRNA_species[i]]
    p_id = all_species_dict[protein_species[i]]
    dydt[mRNA_id] = transcription_rate[mRNA_species[i]] - (specific_growth_rate + transcription_correction[mRNA_species[i]]*degradation_constant_mRNA) * y[mRNA_id]
    dydt[p_id] = translation_rate[protein_species[i]] - (specific_growth_rate + translation_correction[protein_species[i]]*degradation_constant_protein) * y[p_id]
  end

  return dydt
end"
  return buffer
end

function jl_build_solveODEBalances_buffer(all_species_array::Array,
  all_species_dict::Dict{String, Int}, mRNA_species::Array, protein_species::Array)

  buffer = "include(\"./include.jl\")" *
    "\n\nfunction SolveBalances(TStart, TStop, TStep, dataDictionary)" *
    "\n\tt = collect(TStart:TStep:TStop)" *
    "\n\tall_species_reversed_dict = dataDictionary[\"all_species_reversed_dict\"]"
  # initial concentration
  buffer *= "\n\t# initial species concentration"
  buffer *= "\n\ty0 = dataDictionary[\"initial_condition\"]"
  # run simulation
  buffer *= "\n\t# call ODE solver"
  buffer *= "\n\tf(t, y) = Balances(t, y, dataDictionary)" *
    "\n\tt, y = ode23s(f, y0, t; points=:specified)"
  # data transfer
  buffer *= "\n\n\t# data transfer" *
    "\n\trow = length(t)" *
    "\n\tcol = length(y0)" *
    "\n\tY = zeros(row, col)" *
    "\n\tforeach(x->(Y[x, :] = y[x]), collect(1:row))"
  # plotting
  buffer *= "\n\t# plot results" *
    "\n\tplt.figure(\"simulation \$TStart to \$TStop\")" *
    "\n\tsubplt_col = ceil(Int, col/4)" *
    "\n\tfor i = 1:col" *
    "\n\t\tplt.subplot(4, subplt_col, i)" *
    "\n\t\tplt.plot(t, Y[:,i])" *
    "\n\t\tplt.title(all_species_reversed_dict[i])" *
    "\n\tend" *
    "\n\tplt.show()"
  buffer *= "\n\n\treturn (t, Y)" *
    "\nend"

  # add a test
  buffer *= "\n\n\n"
  buffer *= "# # uncomment to test"
  buffer *= "\n# dataDictionary = generate_model_parameters_dictionary()"
  buffer *= "\n# SolveBalances(0, 10, 0.1, dataDictionary)"

  return buffer
end

# function build_copyright_header_buffer()
#   current_year = string(Dates.year(now()))
#   # Get comment data from
#   buffer = ""
#   buffer *=
# "# ----------------------------------------------------------------------------------- #
# # Copyright (c) $(current_year) Varnerlab
# # Robert Frederick Smith School of Chemical and Biomolecular Engineering
# # Cornell University, Ithaca NY 14850
# #
# # Permission is hereby granted, free of charge, to any person obtaining a copy
# # of this software and associated documentation files (the \"Software\"), to deal
# # in the Software without restriction, including without limitation the rights
# # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# # copies of the Software, and to permit persons to whom the Software is
# # furnished to do so, subject to the following conditions:
# #
# # The above copyright notice and this permission notice shall be included in
# # all copies or substantial portions of the Software.
# #
# # THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# # THE SOFTWARE.
# # ----------------------------------------------------------------------------------- #"
#
#   return buffer
# end

# FBA data dictionary generation
function jl_generate_FBA_data_dictionary(all_rnx_list::Array,
  rnx_species_array::Array, extra_species_num::Int)
  secrete_id = Set{Int64}()  # For initialize the coefficient array
  # data dictionary
  buffer = "function DataDictionary(time_start,time_stop,time_step)"
  # stoichiometry
  buffer *= "\n\n\t# load stoichiometry"
  buffer *= "\n\tstoichiometric_matrix = readdlm(\"./stoichiometry.dat\")"
  # Setup default flux bounds array
  buffer *= "\n\t# Setup default flux bounds array"
  buffer *= "\n\tdefault_bounds_array = ["
  for (id, rnx) in enumerate(all_rnx_list)
    buffer *= "\n\t\t0 100.0; # $(id) $(rnx.rnxName)"
  end
  buffer *= "\n\t]"
  # Setup default species bounds array
  buffer *= "\n\n\t# Setup default species bounds array"
  buffer *= "\n\tspecies_bounds_array = ["
  for (id, sp) in enumerate(rnx_species_array)
    if id > extra_species_num
      buffer *= "\n\t\t 0.0 0.0; # $(id) $(sp)"
    else
      buffer *= "\n\t\t-1.0 1.0; # $(id) $(sp)"
    end
  end
  buffer *= "\n\t]"
  # Setup the objective coefficient array
  buffer *= "\n\n\t# Setup the objective coefficient array"
  buffer *= "\n\tobjective_coefficient_array = ["
  for (id, rnx) in enumerate(all_rnx_list)
    buffer *= "\n\t\t0.0; # $(id) $(rnx.rnxName)"
    if rnx.rnxType == "secrete"
        push!(secrete_id, id)
    end
  end
  buffer *= "\n\t]"
  # Min/Max flag - default is minimum -
  buffer *= "\n\n\t# Min/Max flag - default is minimum -"
  buffer *= "\n\tis_minimum_flag = true"
  # List of reation strings - used to write flux report
  buffer *= "\n\n\t# List of reation strings - used to write flux report"
  buffer *= "\n\tlist_of_reaction_strings = ["
  for rnx in all_rnx_list
    buffer *= "\n\t\t\"$(rnx.rnxName)\""
  end
  buffer *= "\n\t]"
  # List of metabolite strings - used to write flux report
  buffer *= "\n\n\t# List of metabolite strings - used to write flux report"
  buffer *= "\n\tlist_of_metabolite_symbols = ["
  for sp in rnx_species_array
    buffer *= "\n\t\t\"$(sp)\""
  end
  buffer *= "\n\t]"
  # return block -
  buffer *= "\n\n"
  buffer *= "\t# =============================== DO NOT EDIT BELOW THIS LINE ============================== #\n"
  buffer *= "\tdata_dictionary = Dict{AbstractString,Any}()\n"
  buffer *= "\tdata_dictionary[\"stoichiometric_matrix\"] = stoichiometric_matrix\n"
  buffer *= "\tdata_dictionary[\"objective_coefficient_array\"] = objective_coefficient_array\n"
  buffer *= "\tdata_dictionary[\"default_flux_bounds_array\"] = default_bounds_array;\n"
  buffer *= "\tdata_dictionary[\"species_bounds_array\"] = species_bounds_array\n"
  buffer *= "\tdata_dictionary[\"list_of_reaction_strings\"] = list_of_reaction_strings\n"
  buffer *= "\tdata_dictionary[\"list_of_metabolite_symbols\"] = list_of_metabolite_symbols\n"
  buffer *= "\tdata_dictionary[\"is_minimum_flag\"] = is_minimum_flag\n"
  buffer *= "\t# =============================== DO NOT EDIT ABOVE THIS LINE ============================== #\n"
  buffer *= "\treturn data_dictionary\n"
  buffer *= "end\n"

  # maximize_product_dictionary
  buffer *= "\n\n\n# maximize_product_dictionary"
  buffer *= "\nfunction maximize_product_dictionary(time_start,time_stop,time_step)"
  # load the data dictionary -
  buffer *= "\n\t# load the data dictionary -"
  buffer *= "\n\tdata_dictionary = DataDictionary(time_start,time_stop,time_step)"
  # Modify the data dictionary -
  buffer *= "\n\t# Modify the data dictionary -"
  buffer *= "\n\tobjective_coefficient_array = data_dictionary[\"objective_coefficient_array\"]"
  secrete_id = sort(collect(secrete_id))
  for id in secrete_id
      buffer *= "\n\tobjective_coefficient_array[$(id)] = -1"
  end
  buffer *= "\n\t# return -"
  buffer *= "\n\treturn data_dictionary"
  buffer *= "\nend"

  return buffer
end


using Pythia
cxx"""
#include "fastjet/PseudoJet.hh"
#include "fastjet/ClusterSequence.hh"
"""

jetdef = icxx"""
  double Rparam = 0.4;
  fastjet::Strategy               strategy = fastjet::Best;
  fastjet::RecombinationScheme    recombScheme = fastjet::E_scheme;
  new fastjet::JetDefinition(fastjet::kt_algorithm, Rparam,
                                      recombScheme, strategy);
"""
fjinputs = icxx"new std::vector <fastjet::PseudoJet>";

pythia = newPythia()

icxx"""$pythia->readString("Beams:idA = 2212");"""
icxx"""$pythia->readString("Beams:idB = 2212");"""
icxx"""$pythia->readString("Beams:eCM = 8000.");"""
icxx"""$pythia->readString("HardQCD:gg2qqbar = on");"""
icxx"""$pythia->readString("PhaseSpace:pTHatMin = 50.0");"""
icxx"""$pythia->init();"""

function cluster(jetDef,e)
    icxx"$fjInputs->resize(0);"
    for p in particles(e)
        # Final State only
        if !icxx"$p.isFinal();"
            continue
        end

        # No neutrinos
        idabs = abs(icxx"$p.id();")
        if idabs == 12 || idabs == 14 || idabs == 16
            continue
        end

        if abs(icxx"$p.eta();" > 3.6)
            continue
        end

        icxx"$fjInputs->push_back( fastjet::PseudoJet( $p.px(),
        $p.py(), $p.pz(), $p.e() ) );"
    end
    icxx"new fastjet::ClusterSequence(*$fjinputs, *$jetDef);"
end

function jets(jetDef,e)
    clusterSeq = cluster(jetDef,e)
    sortedJets = icxx"""
        vector <fastjet::PseudoJet> inclusiveJets;
        inclusiveJets = $clusterSeq->inclusive_jets(20.0);
        new vector <fastjet::PseudoJet> (sorted_by_pt(inclusiveJets));
    """
end

for e in take(events(pythia),10)
    icxx"$pythia->process->list()"
    sortedJets = jets(jetDef,e)
    icxx"""
        for(unsigned int j = 0; j < $sortedJets->size(); j++) {
        cout << "Jet #" << j << " (px,py,pz,E) = (" << (*$sortedJets)[j].px() << ", " << (*$sortedJets)[j].py() << ", " << (*$sortedJets)[j].pz() << ", "
        << (*$sortedJets)[j].e() << ")\n";
        }
    """
end


struct Element
    name::AbstractString
    refstate::AbstractString # reference state
    mass::Float64
    H298::Float64
    S298::Float64
end

abstract type AbstractGFunction end

mutable struct GFunction <: AbstractGFunction
    name::AbstractString
    temp::Tuple{<:Real,<:Real}
    temps::Vector{<:Real}
    exprs::Vector{AbstractString}
    funcname::AbstractString
    funcstr::AbstractString
end

function GFunction(name::AbstractString,
                   temp::Tuple{<:Real,<:Real},
                   funcstr::AbstractString)
    return GFunction(name, temp, Real[], AbstractString[], getfuncname(name), funcstr)
end

function GFunction(name::AbstractString,
                   temps::Vector{<:Real},
                   exprs::Vector{<:AbstractString})
    temp = (minimum(temps), maximum(temps))
    return GFunction(name, temp, temps, exprs, getfuncname(name), "")
end

const Sublattice = Vector{AbstractString}
const Constitution = Vector{Sublattice}

mutable struct Parameter <: AbstractGFunction
    name::AbstractString
    symbol::Char
    comb::Constitution
    order::Int
    temp::Tuple{<:Real,<:Real}
    temps::Vector{<:Real}
    exprs::Vector{AbstractString}
    funcname::AbstractString
    funcstr::AbstractString
end

function Parameter(name::AbstractString,
                   symbol::Char,
                   comb::Vector{<:Vector},
                   order::Int,
                   temps::Vector{<:Real},
                   exprs::Vector{<:AbstractString},
                   funcname::AbstractString)
    temp = (minimum(temps), maximum(temps))
    return Parameter(name, symbol, comb, order, temp, temps, exprs, funcname, "")
end

function Parameter(name::AbstractString,
                   symbol::Char,
                   comb::Vector{<:Vector},
                   order::Int,
                   temp::Tuple{<:Real,<:Real},
                   funcname::AbstractString,
                   funcstr::AbstractString)
    return Parameter(name, symbol, comb, order, temp,
                     Real[], AbstractString[], funcname, funcstr)
end

mutable struct Phase
    name::AbstractString
    state::Char
    type::AbstractString
    sites::Vector{<:Real}
    cons::Constitution
    params::Vector{Parameter}
end

function Phase(name::AbstractString,
               state::Char,
               sites::Vector{<:Real})
    return Phase(name, state, "%", sites, Sublattice[], Parameter[])
end

function Phase(name::AbstractString,
               state::Char,
               sites::Vector{<:Real},
               cons::Vector{<:Vector},
               params::Vector{Parameter})
    return Phase(name, state, "%", sites, cons, params)
end

function Phase(name::AbstractString,
               state::Char,
               type::AbstractString,
               sites::Vector{<:Real})
    phas = Phase(name, state, type, sites, Sublattice[], Parameter[])
end

struct TypeDefinition
    code::Char
    key::AbstractString # SEQ, GES, etc.
    args::AbstractString
end

struct Database
    elems::Vector{Element}
    funcs::Vector{GFunction}
    phass::Vector{Phase}
    types::Vector{TypeDefinition}
end

function Database(elems::Vector{Element},
                  funcs::Vector{GFunction},
                  phass::Vector{Phase})
    return Database(elems, funcs, phass, TypeDefinition[])
end

function Base.display(db::Database)
    println("Database Summary:")
    nelem = length(db.elems)
    println("\tElements: $nelem")
    nfunc = length(db.funcs)
    println("\tFunctions: $nfunc")
    nphas = length(db.phass)
    println("\tPhases: $nphas")
end

function Base.print(db::Database)
    println("Database:")

    nelem = length(db.elems)
    println("\tElements: $nelem")
    for elem in db.elems
        println("\t\t$(elem.name)")
    end

    nfunc = length(db.funcs)
    println("\tFunctions: $nfunc")
    for func in db.funcs
        println("\t\t$(func.name)")
    end

    nphas = length(db.phass)
    println("\tPhases: $nphas")
    for k in 1:nphas
        phas = db.phass[k]
        print("\t\t$k: $(phas.name); ")
        println(constitutionstring(phas))
        for param in phas.params
            println("\t\t\t" * param.symbol * '(' * localname(param) * ')')
        end
    end
end


# MIT License
#
# Copyright (c) 2018 Martin Biel
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

function restore!(model::MOI.ModelLike, ::AbstractIntegerAlgorithm)
    return nothing
end

function check_optimality(lshaped::AbstractLShaped, ::AbstractIntegerAlgorithm)
    return all(1:num_subproblems(lshaped)) do idx
        if has_metadata(lshaped.execution.metadata, idx, :integral_solution)
            return get_metadata(lshaped.execution.metadata, idx, :integral_solution)
        else
            error("Worker $idx has not reported if latest solution satisfies integral restrictions.")
        end
    end
end

function is_approx_integer(val::AbstractFloat)
    return ceil(val) - val <= sqrt(eps()) || val - floor(val) <= sqrt(eps())
end

function gather_integer_variables(subproblem::SubProblem)
    model = subproblem.model
    N = num_stages(model.ext[:decisions])
    all_known = mapreduce(vcat, 1:N-1) do s
        index.(all_known_decision_variables(model, s))
    end
    all_decisions = index.(all_decision_variables(model, N))
    indices = Vector{MOI.VariableIndex}()
    for var in all_variables(model)
        vi = index(var)
        if vi in all_known
            # Known decision, skip
            continue
        end
        if vi in all_decisions
            # Decision variable
            dref = DecisionRef(model, vi)
            if is_integer(dref) || is_binary(dref)
                push!(indices, vi)
            else
                continue
            end
        else
            if is_integer(var) || is_binary(var)
                push!(indices, vi)
            else
                continue
            end
        end
    end
    return indices
end

function check_integrality_restrictions(subproblem::SubProblem, fractional = Vector{MOI.VariableIndex}())
    satisfies_integer = map(integer_variables(subproblem.integer_algorithm)) do vi
        val = MOI.get(subproblem.optimizer, MOI.VariablePrimal(), vi)
        if is_approx_integer(MOI.get(subproblem.optimizer, MOI.VariablePrimal(), vi))
            return true
        else
            push!(fractional, vi)
            return false
        end
    end
    return all(satisfies_integer)
end

mutable struct SecondStageLP{T_ <: AbstractFloat}
    T::SparseMatrixCSC{T_,Int}
    W::SparseMatrixCSC{T_,Int}
    h::Vector{T_}
    lb::Vector{T_}
    ub::Vector{T_}
end

function SecondStageLP(subproblem::SubProblem{T_}; standard_form = false) where T_ <: AbstractFloat
    model = subproblem.model
    nx = StochasticPrograms.num_known_decisions(model)
    ny = num_variables(model) - nx
    # Prepare data
    Tᵢ = Vector{Int}()
    Tⱼ = Vector{Int}()
    Tᵥ = Vector{T_}()
    Wᵢ = Vector{Int}()
    Wⱼ = Vector{Int}()
    Wᵥ = Vector{T_}()
    lhs = Vector{T_}()
    rhs = Vector{T_}()
    lb = fill(-Inf, ny)
    ub = fill(Inf, ny)
    i = 0
    slack = 0
    # Loop over constraints
    for (F,S) in list_of_constraint_types(model)
        for cref in all_constraints(model, F, S)
            if S <: StochasticPrograms.SingleDecisionSet ||
                S <: StochasticPrograms.MultipleDecisionSet
                continue
            elseif F <: VariableRef || F <: DecisionRef
                f = MOI.get(model, MOI.ConstraintFunction(), cref)
                if f isa MOI.SingleVariable
                    idx = f.variable.value - nx
                elseif f isa SingleDecision
                    idx = f.decision.value - nx
                end
                set = MOI.get(model, MOI.ConstraintSet(), cref)
                if set isa MOI.LessThan
                    ub[idx] = set.upper
                    if standard_form
                        i += 1
                        slack += 1
                        push!(Wᵢ, i)
                        push!(Wⱼ, idx)
                        push!(Wᵥ, 1.0)
                        push!(Wᵢ, i)
                        push!(Wⱼ, ny + slack)
                        push!(Wᵥ, 1.0)
                        push!(lhs, -Inf)
                        push!(rhs, set.upper)
                    end
                elseif set isa MOI.GreaterThan
                    lb[idx] = set.lower
                elseif set isa MOI.EqualTo
                    lb[idx] = set.value
                    ub[idx] = set.value
                end
            elseif F <: AffExpr
                i += 1
                f = MOI.get(model, MOI.ConstraintFunction(), cref)
                for term in f.terms
                    push!(Wᵢ, i)
                    push!(Wⱼ, term.variable_index.value - nx)
                    push!(Wᵥ, term.coefficient)
                end
                set = MOI.get(model, MOI.ConstraintSet(), cref)
                if set isa MOI.LessThan
                    push!(lhs, -Inf)
                    push!(rhs, set.upper)
                    if standard_form
                        slack += 1
                        push!(Wᵢ, i)
                        push!(Wⱼ, ny + slack)
                        push!(Wᵥ, 1.0)
                    end
                elseif set isa MOI.GreaterThan
                    push!(lhs, set.lower)
                    push!(rhs, Inf)
                    if standard_form
                        slack += 1
                        push!(Wᵢ, i)
                        push!(Wⱼ, ny + slack)
                        push!(Wᵥ, -1.0)
                    end
                elseif set isa MOI.EqualTo
                    push!(lhs, set.value)
                    push!(rhs, set.value)
                    if standard_form
                        slack += 1
                        push!(Wᵢ, i)
                        push!(Wⱼ, ny + slack)
                        push!(Wᵥ, 1.0)
                    end
                end
            elseif F <: Vector{<:AffExpr}
                f = MOI.get(model, MOI.ConstraintFunction(), cref)
                set = MOI.get(model, MOI.ConstraintSet(), cref)
                for (j,fⱼ) in enumerate(MOIU.eachscalar(f))
                    i += 1
                    for term in fⱼ.terms
                        push!(Wᵢ, i)
                        push!(Wⱼ, term.variable_index.value - nx)
                        push!(Wᵥ, term.coefficient)
                    end
                    if set isa MOI.Nonpositives
                        push!(lhs, -Inf)
                        push!(rhs, -f.constants[j])
                        if standard_form
                            slack += 1
                            push!(Wᵢ, i)
                            push!(Wⱼ, ny + slack)
                            push!(Wᵥ, 1.0)
                        end
                    elseif set isa MOI.Nonnegatives
                        push!(lhs, -f.constants[j])
                        push!(rhs, Inf)
                        if standard_form
                            slack += 1
                            push!(Wᵢ, i)
                            push!(Wⱼ, ny + slack)
                            push!(Wᵥ, -1.0)
                        end
                    elseif set isa MOI.Zeros
                        push!(lhs, -f.constants[j])
                        push!(rhs, -f.constants[j])
                        if standard_form
                            slack += 1
                            push!(Wᵢ, i)
                            push!(Wⱼ, ny + slack)
                            push!(Wᵥ, 1.0)
                        end
                    end
                end
            elseif F <: DecisionAffExpr
                i += 1
                aff = jump_function(model, MOI.get(model, MOI.ConstraintFunction(), cref))
                for (coeff, var) in linear_terms(aff.variables)
                    push!(Wᵢ, i)
                    push!(Wⱼ, index(var).value - nx)
                    push!(Wᵥ, coeff)
                end
                for (coeff, dvar) in linear_terms(aff.decisions)
                    if state(dvar) == Known
                        push!(Tᵢ, i)
                        push!(Tⱼ, index(dvar).value)
                        push!(Tᵥ, coeff)
                    else
                        push!(Wᵢ, i)
                        push!(Wⱼ, index(dvar).value - nx)
                        push!(Wᵥ, coeff)
                    end
                end
                set = MOI.get(model, MOI.ConstraintSet(), cref)
                if set isa MOI.LessThan
                    push!(lhs, -Inf)
                    push!(rhs, set.upper)
                    if standard_form
                        slack += 1
                        push!(Wᵢ, i)
                        push!(Wⱼ, ny + slack)
                        push!(Wᵥ, 1.0)
                    end
                elseif set isa MOI.GreaterThan
                    push!(lhs, set.lower)
                    push!(rhs, Inf)
                    if standard_form
                        slack += 1
                        push!(Wᵢ, i)
                        push!(Wⱼ, ny + slack)
                        push!(Wᵥ, -1.0)
                    end
                elseif set isa MOI.EqualTo
                    push!(lhs, set.value)
                    push!(rhs, set.value)
                    if standard_form
                        slack += 1
                        push!(Wᵢ, i)
                        push!(Wⱼ, ny + slack)
                        push!(Wᵥ, 1.0)
                    end
                end
            elseif F <: Vector{<:DecisionAffExpr}
                f = MOI.get(model, MOI.ConstraintFunction(), cref)
                set = MOI.get(model, MOI.ConstraintSet(), cref)
                for (j,fⱼ) in enumerate(MOIU.eachscalar(f))
                    i += 1
                    aff = jump_function(model, fⱼ)
                    for (coeff, var) in linear_terms(aff.variables)
                        push!(Wᵢ, i)
                        push!(Wⱼ, index(var).value - nx)
                        push!(Wᵥ, coeff)
                    end
                    for (coeff, dvar) in linear_terms(aff.decisions)
                        if state(dvar) == Known
                            push!(Tᵢ, i)
                            push!(Tⱼ, index(dvar).value)
                            push!(Tᵥ, coeff)
                        else
                            push!(Wᵢ, i)
                            push!(Wⱼ, index(dvar).value - nx)
                            push!(Wᵥ, coeff)
                        end
                    end
                    if set isa MOI.Nonpositives
                        push!(lhs, -Inf)
                        push!(rhs, -f.variable_part.constants[j])
                        if standard_form
                            slack += 1
                            push!(Wᵢ, i)
                            push!(Wⱼ, ny + slack)
                            push!(Wᵥ, 1.0)
                        end
                    elseif set isa MOI.Nonnegatives
                        push!(lhs, -f.variable_part.constants[j])
                        push!(rhs, Inf)
                        if standard_form
                            slack += 1
                            push!(Wᵢ, i)
                            push!(Wⱼ, ny + slack)
                            push!(Wᵥ, -1.0)
                        end
                    elseif set isa MOI.Zeros
                        push!(lhs, -f.variable_part.constants[j])
                        push!(rhs, -f.variable_part.constants[j])
                        if standard_form
                            slack += 1
                            push!(Wᵢ, i)
                            push!(Wⱼ, ny + slack)
                            push!(Wᵥ, 1.0)
                        end
                    end
                end
            else
                error("Cannot extract linear representation from $F-$S constraint.")
            end
        end
    end
    T̃ = sparse(Tᵢ, Tⱼ, Tᵥ, i, nx)
    W̃ = sparse(Wᵢ, Wⱼ, Wᵥ, i, ny + slack)
    if standard_form
        h = lhs
        i = .!isfinite.(h)
        h[i] .= rhs[i]
        return SecondStageLP(T̃, W̃, h, lb, ub)
    else
        T, h = StochasticPrograms.canonical(T̃, lhs, rhs)
        W, h = StochasticPrograms.canonical(W̃, lhs, rhs)
        return SecondStageLP(-T, -W, -h, lb, ub)
    end
end

function optimal_basis(subproblem::SubProblem)
    model = subproblem.model
    nx = StochasticPrograms.num_known_decisions(model)
    ny = num_variables(model) - nx
    variable_basis = Int[]
    slack_basis = Int[]
    slack = 0
    # Loop over constraints
    for (F,S) in list_of_constraint_types(model)
        for cref in all_constraints(model, F, S)
            if S <: StochasticPrograms.SingleDecisionSet ||
                S <: StochasticPrograms.MultipleDecisionSet
                continue
            elseif F <: VariableRef || F <: DecisionRef
                f = MOI.get(model, MOI.ConstraintFunction(), cref)
                set = MOI.get(model, MOI.ConstraintSet(), cref)
                if f isa MOI.SingleVariable
                    idx = f.variable.value - nx
                elseif f isa SingleDecision
                    idx = f.decision.value - nx
                end
                if set isa MOI.LessThan || set isa MOI.EqualTo
                    slack += 1
                    if MOI.get(model, MOI.ConstraintBasisStatus(), cref) == MOI.BASIC
                        push!(slack_basis, ny + slack)
                    end
                elseif set isa MOI.GreaterThan
                    if MOI.get(model, MOI.ConstraintBasisStatus(), cref) == MOI.BASIC
                        push!(variable_basis, idx)
                    end
                end
            elseif F <: AffExpr || F <: DecisionAffExpr
                slack += 1
                if MOI.get(model, MOI.ConstraintBasisStatus(), cref) == MOI.BASIC
                    push!(slack_basis, ny + slack)
                end
            else
                error("Cannot extract basis from $F-$S constraint.")
            end
        end
    end
    return vcat(variable_basis, slack_basis)
end

function add_row!(lp::SecondStageLP, T̄::AbstractVector, W̄::AbstractVector, h̄::AbstractFloat; standard_form = false)
    # Get current size
    m = size(lp.W, 1)
    nx = size(lp.T, 2)
    ny = size(lp.W, 2)
    if standard_form
        # Prepare to add new slack variable
        ny += 1
    end
    # Get current matrix structure
    Tᵢ, Tⱼ, Tᵥ = findnz(lp.T)
    Wᵢ, Wⱼ, Wᵥ = findnz(lp.W)
    # Add T row
    for (j, coeff) in enumerate(T̄)
        if abs(coeff) >= sqrt(eps())
            push!(Tᵢ, m + 1)
            push!(Tⱼ, j)
            push!(Tᵥ, coeff)
        end
    end
    lp.T = sparse(Tᵢ, Tⱼ, Tᵥ, m + 1, nx)
    # Add W row
    for (j, coeff) in enumerate(W̄)
        if abs(coeff) >= sqrt(eps())
            push!(Wᵢ, m + 1)
            push!(Wⱼ, j)
            push!(Wᵥ, coeff)
        end
    end
    if standard_form
        # Add slack term
        push!(Wᵢ, m + 1)
        push!(Wⱼ, ny)
        push!(Wᵥ, -1.0)
    end
    lp.W = sparse(Wᵢ, Wⱼ, Wᵥ, m + 1, ny)
    # Add new rhs
    push!(lp.h, h̄)
    return nothing
end


using JuMP, Gurobi, Ipopt
using DataFrames, JLD, CSVFiles
using MatpowerCases

### Include the file with input data functions
include("NetworkDataType.jl")
include("NetworkLoad.jl")
include("P2PTradeType.jl")
include("TradeFunction.jl")
include("DLMP.jl")

testsystem = "AP15busDN"
#testsystem = "case141_OPF"

buses, lines, generators, datamat = NetworkLoad(testsystem)
lineset = 1:length(lines)
busset = 1:length(buses)
genset = 1:length(generators)
buses[1].Vmax = 1
buses[1].Vmin = 1
windset = Int[]
for g in genset
    if datamat["gen"][g,2] == "Wind"
        push!(windset, generators[g].gindex)
    end
end

root = -1
for g in genset
    if generators[g].gtype=="Root"
        root = g
    end
end
if root == -1
    display("No generator at root node")
    generators[1].gtype = "Root"
    root = 1
end

temp = Generator
for g in genset
    if generators[g].gtype == "Root"
        temp = Generator(length(genset)+1, "DG", generators[g].location, generators[g].Pg, generators[g].Qg, generators[g].Qmax,
        generators[g].Qmin, generators[g].Vg, generators[g].mBase, generators[g].status, generators[g].Pmax, generators[g].Pmin,
        generators[g].cost, generators[g].SUcost, generators[g].SDcost, generators[g].RU, generators[g].RD, generators[g].UPtime, generators[g].DNtime)
        #push!(generators, temp)
    end
end

genset = 1:length(generators)
gensetU = root
gensetP = setdiff(genset, gensetU)

B_g = []
for g in genset
   if ~any(x->x==generators[g].location, B_g)
      push!(B_g, generators[g].location)
      buses[generators[g].location].Pd = 0
   end
end
B_d = setdiff(busset,B_g)

B_gn = Array{Array{Int64}}(length(buses),1)
for ii in 1:length(buses)
    B_gn[ii] = Int64[]
end
for g in genset
    push!(B_gn[generators[g].location], g)
end
############
partlevel = 0.0
gam = partlevel*ones(busset)
############
###
trade_scale = 1e-3 # [MWh]

agents = Agent[]
for b in 1:length(buses)
    if isempty(B_gn[b])
        temp_agent = Agent(b, b, 1, 0, 0, [], gam[b]*buses[b].Pd, gam[b]*buses[b].Qd)
        push!(agents,temp_agent)
    else
        for g in B_gn[b]
            if generators[g].gtype != "Root"
                temp_agent = Agent(b, b, 1, generators[g].Pmin, generators[g].Pmax, generators[g].cost, gam[b]*buses[b].Pd, gam[b]*buses[b].Qd)
                push!(agents,temp_agent)
            end
        end
    end
end
# agents = Agent[]
# for b in 1:length(buses)
#     if isempty(B_gn[b])
#         temp_agent = Agent(b, b, 1, 0, 0, [], gam[b]*buses[b].Pd, gam[b]*buses[b].Qd)
#         push!(agents,temp_agent)
#     elseif generators[B_gn[b][1]].gtype != "Root"
#         g = B_gn[b][1]
#         temp_agent = Agent(b, b, 1, generators[g].Pmin, generators[g].Pmax, generators[g].cost, gam[b]*buses[b].Pd, gam[b]*buses[b].Qd)
#         push!(agents,temp_agent)
#     end
# end

# Trade(tradeID, kind, Pes, Peb, As, Ab, costNw)
trades = generateTrade(agents, trade_scale)
trades_old = generateTrade(agents, trade_scale)

trades_mat = Array{Any}(length(agents),length(agents))
trades_mat_buy = Array{Any}(length(agents),length(agents))
trades_mat_sell = Array{Any}(length(agents),length(agents))
for ii in 1:length(agents), jj in 1:length(agents)
    trades_mat[ii,jj] = []
    trades_mat_buy[ii,jj] = []
    trades_mat_sell[ii,jj] = []
end
trades_selected_mat = Array{Any}(length(buses),length(buses))
trades_selected_mat_buy = Array{Any}(length(buses),length(buses))
trades_selected_mat_sell = Array{Any}(length(buses),length(buses))
for ii in 1:length(buses), jj in 1:length(buses)
    trades_selected_mat[ii,jj] = []
    trades_selected_mat_buy[ii,jj] = []
    trades_selected_mat_sell[ii,jj] = []
end

for w in 1:length(trades)
    push!(trades_mat[trades[w].As,trades[w].Ab],trades[w].tradeID)
end

trades_selected = Int[]
trades_rest = Int[]
trades_selected_old = Int[]
trades_rest_old = Int[]
dispatch = zeros(length(buses))
GenCon = zeros(length(buses))
Generation = zeros(length(buses))
Consumption = zeros(length(buses))
GenCon_old = zeros(length(buses))
dispatch_old = zeros(length(buses))
status = Symbol()
DLMP = Float64[]
DLMP_stack = Any[]
dispatch_stack = Any[]
DLMPInfo = DataFrame()
NodeInfo = DataFrame()
LineInfo = DataFrame()
dispatch_peerG = 0.0*zeros(genset)
param_delta = 1.0
tic()
while true
    #for _ in 1:10
    accepted = Array{Int64}[]
    rejected = Array{Int64}[]
    pg = Float64[]
    Revenue_Sell = Float64[]
    Cost_Buy = Float64[]
    Cost_Network = Float64[]
    Cost_DG = Float64[]
    trades_old[:] = trades[:]
    trades_selected_old = trades_selected
    for w in 1:length(trades)
        trades[w].Pes = 0
        trades[w].Peb = 0
    end

    while true
        accepted = Array{Int64}[]
        rejected = Array{Int64}[]
        pg = Float64[]

        gap = 1e-3
        for agentID in 1:length(agents)
            temp_accepted, temp_rejected, temp_pg, Revenue_Sell, Cost_Buy, Cost_Network, Cost_DG = selectTrade(trades, agents, agentID, gap, trade_scale)
            push!(accepted, temp_accepted)
            push!(rejected, temp_rejected)
            push!(pg, temp_pg)
        end

        deltaP = 5*trade_scale #$5/MWh = $5e-3/kWh
        for w in 1:length(trades)
            if (w in accepted[trades[w].Ab]) && ~(w in accepted[trades[w].As])
                if trades[w].Peb > trades[w].Pes
                    trades[w].Pes += deltaP
                else
                    trades[w].Peb += deltaP
                end
            end
        end
        # diff = sum((w in accepted[trades[w].Ab]) && ~(w in accepted[trades[w].As]) for w in 1:length(trades))
        # if diff == 0
        #     break
        # end

        ###diff = sum(abs(trades[w].Pes - trades_old[w].Pes) for w in 1:length(trades)) + sum(abs(trades[w].Peb - trades_old[w].Peb) for w in 1:length(trades))
        if isempty(trades)
            diff = 0
        else
            diff = sum((w in accepted[trades[w].Ab]) && ~(w in accepted[trades[w].As]) for w in 1:length(trades))
        end
        if diff == 0
            break
        end
    end

    trades_selected = Int[]
    trades_rest = Int[]
    for w in 1:length(trades)
        if (w in accepted[trades[w].Ab]) && (w in accepted[trades[w].As])
            push!(trades_selected, w)
        else
            push!(trades_rest, w)
        end
    end

    for i in 1:length(agents)
        dispatch[i] = ceil(agents[i].Pd/trade_scale) # MW to kW conversion
    end

    for w in trades_selected
        dispatch[agents[trades[w].Ab].location] -= 1
        dispatch[agents[trades[w].As].location] += 1
        GenCon[agents[trades[w].Ab].location] -= 1
        GenCon[agents[trades[w].As].location] += 1
        Consumption[agents[trades[w].Ab].location] -= 1
        Generation[agents[trades[w].As].location] += 1
    end

    GenCon = GenCon*trade_scale
    dispatch = dispatch*trade_scale # Kw to MW conversion
    Generation = Generation*trade_scale
    Consumption = Consumption*trade_scale

    #dispatch_peerG = 0.0*zeros(genset)
    for g in gensetP
        dispatch_peerG[g] = Generation[generators[g].location]
    end

    ###### Calculate Network Charge #####
    SMP = generators[root].cost[end-1]
    status, DLMP, pgextra, NodeInfo, LineInfo, DLMPInfo = calculateDLMP(dispatch_peerG, buses, generators, lines, SMP, gensetP, gensetU);

    ###### Update Network Charge #####
    deltaNw = 1*trade_scale # $1/MWh = $1e-3/kWh
    if status == Symbol(:Optimal)
        for w in 1:length(trades)
            if w in trades_selected
                trades[w].costNw = (DLMP[agents[trades[w].Ab].location] - DLMP[agents[trades[w].As].location])/2
            end
        end
    else
        for w in trades_selected
            trades[w].costNw += param_delta*deltaNw/trade_scale
        end
    end
    push!(dispatch_stack, dispatch)
    push!(DLMP_stack, DLMP)
    if GenCon_old == GenCon && status == Symbol(:Optimal)
        break
    else
        dispatch_old[:] = dispatch[:]
        GenCon_old[:] = GenCon[:]
    end
end
simulation_time = toc()

trades_dis = zeros(length(buses),length(buses));
NWcharge_dis = zeros(length(buses),length(buses));
Echarge_dis = zeros(length(buses),length(buses));
temp = Array{Array{Int64,1}}(length(buses),1)
for i in 1:length(buses)
    temp[i] = []
end

for w in trades_selected
    trades_dis[agents[trades[w].Ab].location,agents[trades[w].As].location] -= 1*trade_scale
    trades_dis[agents[trades[w].As].location,agents[trades[w].Ab].location] += 1*trade_scale
    Echarge_dis[agents[trades[w].Ab].location,agents[trades[w].As].location] -= trades[w].Peb
    Echarge_dis[agents[trades[w].As].location,agents[trades[w].Ab].location] += trades[w].Pes
    NWcharge_dis[agents[trades[w].Ab].location,agents[trades[w].As].location] -= round(trades[w].costNw,4)
    NWcharge_dis[agents[trades[w].As].location,agents[trades[w].Ab].location] += round(trades[w].costNw,4)
    push!(temp[agents[trades[w].Ab].location], w)
    push!(temp[agents[trades[w].As].location], w)
end

dispatch_peerG = 0.0*zeros(genset)
for g in gensetP
    dispatch_peerG[g] = Generation[generators[g].location]
end

###### Calculate Network Charge #####
SMP = generators[root].cost[end-1]
status, DLMP, pg, NodeInfo, LineInfo, DLMPInfo = calculateDLMP(dispatch_peerG, buses, generators, lines, SMP, gensetP, gensetU);

#CSV.write("PeerCentric_DLMP.csv", DLMPInfo);
#CSV.write("PeerCentric_NodeInfo.csv", NodeInfo);
#CSV.write("PeerCentric_LineInfo.csv", LineInfo);

price_mat = zeros(length(buses),length(buses))

#############################################
for w in trades_selected
    push!(trades_selected_mat[agents[trades[w].As].location,agents[trades[w].Ab].location],trades[w].tradeID)
end

price_mat = zeros(length(buses),length(buses));
pricemin = zeros(length(buses),length(buses));
pricemax = zeros(length(buses),length(buses));
for ii in 1:length(buses), jj in 1:length(buses)
    temp_mat = []
    for ww in trades_selected_mat[ii,jj]
        push!(temp_mat,trades[ww].Pes)
        pricemin[ii,jj] = minimum(temp_mat)
        pricemin[jj,ii] = -minimum(temp_mat)
        pricemax[ii,jj] = maximum(temp_mat)
        pricemax[jj,ii] = -maximum(temp_mat)
        if pricemin[ii,jj] == pricemax[ii,jj]
            price_mat[ii,jj] = pricemin[ii,jj]
            price_mat[jj,ii] = -pricemin[ii,jj]
        else
            error("pricemin != pricemax")
        end
    end
end

Nprice_mat = zeros(length(buses),length(buses));
for w in trades_selected
    Nprice_mat[agents[trades[w].Ab].location,agents[trades[w].As].location] = 0.5*round(trades[w].costNw,5)
    Nprice_mat[agents[trades[w].As].location,agents[trades[w].Ab].location] = 0.5*round(trades[w].costNw,5)
end

Nprice_mat_min = zeros(length(buses),length(buses));
Nprice_mat_max = zeros(length(buses),length(buses));
for w = 1:length(trades)
    Nprice_mat_min[agents[trades[w].Ab].location,agents[trades[w].As].location] = min(round(trades[w].costNw,5),Nprice_mat_min[agents[trades[w].Ab].location,agents[trades[w].As].location])
    Nprice_mat_min[agents[trades[w].As].location,agents[trades[w].Ab].location] = min(round(trades[w].costNw,5),Nprice_mat_min[agents[trades[w].As].location,agents[trades[w].Ab].location])
    Nprice_mat_max[agents[trades[w].Ab].location,agents[trades[w].As].location] = max(round(trades[w].costNw,5),Nprice_mat_max[agents[trades[w].Ab].location,agents[trades[w].As].location])
    Nprice_mat_max[agents[trades[w].As].location,agents[trades[w].Ab].location] = max(round(trades[w].costNw,5),Nprice_mat_max[agents[trades[w].As].location,agents[trades[w].Ab].location])
end

Eprice_mat = zeros(length(buses),length(buses));
for w in trades_selected
    Eprice_mat[agents[trades[w].Ab].location,agents[trades[w].As].location] = round(trades[w].Pes,5)
    Eprice_mat[agents[trades[w].As].location,agents[trades[w].Ab].location] = round(trades[w].Peb,5)
end

Eprice_mat_min = zeros(length(buses),length(buses));
Eprice_mat_max = zeros(length(buses),length(buses));
for w = 1:length(trades)
    Eprice_mat_min[agents[trades[w].Ab].location,agents[trades[w].As].location] = min(round(trades[w].Pes,5),Eprice_mat_min[agents[trades[w].Ab].location,agents[trades[w].As].location])
    Eprice_mat_min[agents[trades[w].As].location,agents[trades[w].Ab].location] = min(round(trades[w].Peb,5),Eprice_mat_min[agents[trades[w].As].location,agents[trades[w].Ab].location])
    Eprice_mat_max[agents[trades[w].Ab].location,agents[trades[w].As].location] = max(round(trades[w].Pes,5),Eprice_mat_max[agents[trades[w].Ab].location,agents[trades[w].As].location])
    Eprice_mat_max[agents[trades[w].As].location,agents[trades[w].Ab].location] = max(round(trades[w].Peb,5),Eprice_mat_max[agents[trades[w].As].location,agents[trades[w].Ab].location])
end

Peer_revenue = sum(Echarge_dis,2);
Consumer_cost = 0
Generator_revenue = 0
for bb in B_d
    Consumer_cost -= Peer_revenue[bb]
end
for bb in B_g
    Generator_revenue += Peer_revenue[bb]
end

line_marginrate = zeros(length(lines),1)
for ll in 1:length(lines)
    line_marginrate[ll] = round.((lines[ll].u  - sqrt((LineInfo[:fp][ll])^2 + (LineInfo[:fq][ll])^2))/ lines[ll].u,3)
end
line_loading = 1-line_marginrate
bus_voltage = sqrt.(NodeInfo[:v])

v_marginrate = round.(0.1-abs.(1-sqrt.(NodeInfo[:v])),3)/0.1

Loading = DataFrame(line_loading=vec(line_loading))
Vmargin = DataFrame(v_marginrate=vec(v_marginrate))
#####
loading = zeros(length(lines))
for l in 1:length(lines)
   loading[l] = LineInfo[:flow][l] / lines[l].u
end

flow = LineInfo[:flow]
voltage = sqrt.(NodeInfo[:v])

#######
pnm = trades_dis
demand_P2P = zeros(length(buses))
for m in B_d
    demand_P2P[m] = sum(pnm[n,m] for n in B_g)
end

EP_P = zeros(length(busset))
for w in trades_selected
   EP_P[agents[trades[w].Ab].location] += trades[w].Pes
end
EP_U = zeros(length(busset))
for m in B_d
   EP_U[m] = (buses[m].Pd-demand_P2P[m])*DLMP[m]
end

NUC = zeros(length(busset))
for w in trades_selected
   NUC[agents[trades[w].Ab].location] += 0.5*trades[w].costNw
   NUC[agents[trades[w].As].location] += 0.5*trades[w].costNw
end
UR = EP_U + NUC
CP = EP_U + EP_P + NUC

GC = zeros(length(genset))
for g in genset
   GC[g] = generators[g].cost[end-1] * round(pg[g],5)
end
sum_GC_P = sum(GC[g] for g in gensetP)
sum_GC_U = sum(GC[g] for g in gensetU)
sum_DGR = sum(EP_P)
sum_DGP = sum_DGR - sum_GC_P

UP = sum(UR) - sum_GC_U
CashFlow_mat = DataFrame(EP_P=EP_P,EP_U=EP_U,NUC=NUC,UR=UR,CP=CP)
CashFlow_sum = DataFrame(sum_CP=sum(CP), sum_EP_P=sum(EP_P), sum_EP_U=sum(EP_U), sum_NUC=sum(NUC), sum_DGR=sum_DGR, sum_DGP=sum_DGP, UP=UP)

println("partlevel = ""$partlevel")
println("status = ""$status")
println("max loading = ""$(maximum(loading)*100)""%")
println("max voltage = ""$(round(maximum(sqrt.(NodeInfo[:v])),3))")
println("min voltage = ""$(round(minimum(sqrt.(NodeInfo[:v])),3))")
println("max DLMP = ""$(round(maximum(DLMP),3))")
println("min DLMP = ""$(round(minimum(DLMP),3))")
println("simulation time = ""$simulation_time")
method = "PC"
voltagefile = "$method$(length(buses))""_""$(Int(100*partlevel))""_voltage"".csv"
loadingfile = "$method$(length(buses))""_""$(Int(100*partlevel))""_loading"".csv"
DLMPfile = "$method$(length(buses))""_""$(Int(100*partlevel))""_DLMP"".csv"
cashflowmatfile = "$method$(length(buses))""_""$(Int(100*partlevel))""_cfmat"".csv"
cashflowsumfile = "$method$(length(buses))""_""$(Int(100*partlevel))""_cfsum"".csv"
# CSVFiles.save(voltagefile, DataFrame(voltage=voltage))
# CSVFiles.save(loadingfile, DataFrame(loading=100*loading))
# CSVFiles.save(DLMPfile, DataFrame(DLMP=DLMP))
# CSVFiles.save(cashflowmatfile, CashFlow_mat)
# CSVFiles.save(cashflowsumfile, CashFlow_sum)

#######
resultfile = "$method$(length(buses))""_""$(Int(round(100*partlevel)))""_""$(Int(1000*trade_scale))""kW"".jld"
#@save "$resultfile"

#######################################
trades_selected_n = Array{Array{Int64,1}}(length(buses),1)
trades_n = Array{Array{Int64,1}}(length(buses),1)
for i in 1:length(buses)
    trades_selected_n[i] = []
    trades_n[i] = []
end
for w in trades_selected
    push!(trades_selected_n[trades[w].Ab],w)
    push!(trades_selected_n[trades[w].As],w)
end
for w in 1:length(trades)
    push!(trades_n[trades[w].Ab],w)
    push!(trades_n[trades[w].As],w)
end
Erho = zeros(length(buses),1)
Ecn = zeros(length(buses),1)
Tvol = zeros(length(buses),1)
NUC_total = zeros(length(buses),1)
for i in 1:length(buses)
    if ~isempty(trades_selected_n[i])
        Tvol[i] = length(trades_selected_n[i])
        Ecn[i] = trade_scale*sum(0.5*(DLMP[trades[w].Ab]-DLMP[trades[w].As]) for w in trades_selected_n[i]) / Tvol[i]
        NUC_total[i] = trade_scale*sum(0.5*(DLMP[trades[w].Ab]-DLMP[trades[w].As]) for w in trades_selected_n[i])
        Erho[i] = sum(trades[w].Pes for w in trades_selected_n[i]) / Tvol[i]
    end
end
println("NUC total = ",sum(NUC_total))
println("Tvol = ", sum(Tvol))
#######################################


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"