// * -*- mode: C++; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
 *  Main authors:
 *     Gleb Belov <gleb.belov@monash.edu>
 */

/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifdef _MSC_VER
#define _CRT_SECURE_NO_WARNINGS
#endif

#include <minizinc/config.hh>
#include <minizinc/file_utils.hh>
#include <minizinc/utils.hh>

#include <cmath>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string>

#ifdef CPLEX_PLUGIN
#ifdef _WIN32
#include <Windows.h>
#undef ERROR
#else
#include <dlfcn.h>
#endif
#endif

#include <minizinc/solvers/MIP/MIP_cplex_wrap.hh>

using namespace std;

#ifdef CPLEX_PLUGIN

namespace {
void* dll_open(const std::string& file) {
#ifdef _WIN32
  if (MiniZinc::FileUtils::is_absolute(file)) {
    return LoadLibrary(file.c_str());
  }
  return LoadLibrary((file + ".dll").c_str());
#else
  if (MiniZinc::FileUtils::is_absolute(file)) {
    return dlopen(file.c_str(), RTLD_NOW);
  }
  if (void* so = dlopen(("lib" + file + ".so").c_str(), RTLD_NOW)) {
    return so;
  }
  return dlopen(("lib" + file + ".jnilib").c_str(), RTLD_NOW);
#endif
}
void* dll_sym(void* dll, const char* sym) {
#ifdef _WIN32
  void* ret = GetProcAddress((HMODULE)dll, sym);
#else
  void* ret = dlsym(dll, sym);
#endif
  if (ret == nullptr) {
    throw MiniZinc::Error("cannot load symbol " + string(sym) + " from CPLEX dll");
  }
  return ret;
}
void dll_close(void* dll) {
#ifdef _WIN32
  FreeLibrary((HMODULE)dll);
#else
  dlclose(dll);
#endif
}
}  // namespace

#endif

const vector<string>& cplex_dlls() {
  static const vector<string> sCPLEXDLLs = {
      "cplex2412", "cplex2411",  "cplex2410", "cplex2312", "cplex2311", "cplex2310",
      "cplex2222", "cplex2221",  "cplex2220", "cplex2212", "cplex2211", "cplex2210",
      "cplex2010", "cplex12100", "cplex1290", "cplex1280", "cplex1270"};
  return sCPLEXDLLs;
}

void MIPCplexWrapper::checkDLL() {
#ifdef CPLEX_PLUGIN
  _cplexDll = nullptr;
  if (!_factoryOptions.cplexDll.empty()) {
    _cplexDll = dll_open(_factoryOptions.cplexDll);
  } else {
    for (const auto& s : cplex_dlls()) {
      _cplexDll = dll_open(s);
      if (nullptr != _cplexDll) {
        break;
      }
    }
  }

  if (_cplexDll == nullptr) {
    if (_factoryOptions.cplexDll.empty()) {
      throw MiniZinc::Error("cannot load cplex dll, specify --cplex-dll");
    }
    throw MiniZinc::Error("cannot load cplex dll `" + _factoryOptions.cplexDll + "'");
  }

  *(void**)(&dll_CPXaddfuncdest) = dll_sym(_cplexDll, "CPXaddfuncdest");
  *(void**)(&dll_CPXaddindconstr) = dll_sym(_cplexDll, "CPXaddindconstr");
  *(void**)(&dll_CPXaddlazyconstraints) = dll_sym(_cplexDll, "CPXaddlazyconstraints");
  *(void**)(&dll_CPXaddmipstarts) = dll_sym(_cplexDll, "CPXaddmipstarts");
  *(void**)(&dll_CPXaddrows) = dll_sym(_cplexDll, "CPXaddrows");
  *(void**)(&dll_CPXaddusercuts) = dll_sym(_cplexDll, "CPXaddusercuts");
  *(void**)(&dll_CPXchgbds) = dll_sym(_cplexDll, "CPXchgbds");
  *(void**)(&dll_CPXchgmipstarts) = dll_sym(_cplexDll, "CPXchgmipstarts");
  *(void**)(&dll_CPXchgobjsen) = dll_sym(_cplexDll, "CPXchgobjsen");
  *(void**)(&dll_CPXcloseCPLEX) = dll_sym(_cplexDll, "CPXcloseCPLEX");
  *(void**)(&dll_CPXcreateprob) = dll_sym(_cplexDll, "CPXcreateprob");
  *(void**)(&dll_CPXcutcallbackadd) = dll_sym(_cplexDll, "CPXcutcallbackadd");
  *(void**)(&dll_CPXfreeprob) = dll_sym(_cplexDll, "CPXfreeprob");
  *(void**)(&dll_CPXgetbestobjval) = dll_sym(_cplexDll, "CPXgetbestobjval");
  *(void**)(&dll_CPXgetcallbackincumbent) = dll_sym(_cplexDll, "CPXgetcallbackincumbent");
  *(void**)(&dll_CPXgetcallbackinfo) = dll_sym(_cplexDll, "CPXgetcallbackinfo");
  *(void**)(&dll_CPXgetcallbacknodeinfo) = dll_sym(_cplexDll, "CPXgetcallbacknodeinfo");
  *(void**)(&dll_CPXgetcallbacknodex) = dll_sym(_cplexDll, "CPXgetcallbacknodex");
  *(void**)(&dll_CPXgetchannels) = dll_sym(_cplexDll, "CPXgetchannels");
  *(void**)(&dll_CPXgetdettime) = dll_sym(_cplexDll, "CPXgetdettime");
  *(void**)(&dll_CPXgeterrorstring) = dll_sym(_cplexDll, "CPXgeterrorstring");
  *(void**)(&dll_CPXgetmipstartindex) = dll_sym(_cplexDll, "CPXgetmipstartindex");
  *(void**)(&dll_CPXgetnodecnt) = dll_sym(_cplexDll, "CPXgetnodecnt");
  *(void**)(&dll_CPXgetnodeleftcnt) = dll_sym(_cplexDll, "CPXgetnodeleftcnt");
  *(void**)(&dll_CPXgetnumcols) = dll_sym(_cplexDll, "CPXgetnumcols");
  *(void**)(&dll_CPXgetnumrows) = dll_sym(_cplexDll, "CPXgetnumrows");
  *(void**)(&dll_CPXgetobjsen) = dll_sym(_cplexDll, "CPXgetobjsen");
  *(void**)(&dll_CPXgetobjval) = dll_sym(_cplexDll, "CPXgetobjval");
  *(void**)(&dll_CPXgetsolnpoolnumsolns) = dll_sym(_cplexDll, "CPXgetsolnpoolnumsolns");
  *(void**)(&dll_CPXgetstat) = dll_sym(_cplexDll, "CPXgetstat");
  *(void**)(&dll_CPXgetstatstring) = dll_sym(_cplexDll, "CPXgetstatstring");
  *(void**)(&dll_CPXgettime) = dll_sym(_cplexDll, "CPXgettime");
  *(void**)(&dll_CPXgetx) = dll_sym(_cplexDll, "CPXgetx");
  *(void**)(&dll_CPXmipopt) = dll_sym(_cplexDll, "CPXmipopt");
  *(void**)(&dll_CPXnewcols) = dll_sym(_cplexDll, "CPXnewcols");
  *(void**)(&dll_CPXopenCPLEX) = dll_sym(_cplexDll, "CPXopenCPLEX");
  *(void**)(&dll_CPXreadcopyparam) = dll_sym(_cplexDll, "CPXreadcopyparam");
  *(void**)(&dll_CPXsetdblparam) = dll_sym(_cplexDll, "CPXsetdblparam");
  *(void**)(&dll_CPXsetinfocallbackfunc) = dll_sym(_cplexDll, "CPXsetinfocallbackfunc");
  *(void**)(&dll_CPXsetintparam) = dll_sym(_cplexDll, "CPXsetintparam");
  *(void**)(&dll_CPXsetstrparam) = dll_sym(_cplexDll, "CPXsetstrparam");
  *(void**)(&dll_CPXsetlazyconstraintcallbackfunc) =
      dll_sym(_cplexDll, "CPXsetlazyconstraintcallbackfunc");
  *(void**)(&dll_CPXsetusercutcallbackfunc) = dll_sym(_cplexDll, "CPXsetusercutcallbackfunc");
  *(void**)(&dll_CPXversion) = dll_sym(_cplexDll, "CPXversion");
  *(void**)(&dll_CPXwriteparam) = dll_sym(_cplexDll, "CPXwriteparam");
  *(void**)(&dll_CPXwriteprob) = dll_sym(_cplexDll, "CPXwriteprob");
  *(void**)(&dll_CPXgetparamname) = dll_sym(_cplexDll, "CPXgetparamname");
  *(void**)(&dll_CPXgetparamnum) = dll_sym(_cplexDll, "CPXgetparamnum");
  *(void**)(&dll_CPXgetparamtype) = dll_sym(_cplexDll, "CPXgetparamtype");
  *(void**)(&dll_CPXinfointparam) = dll_sym(_cplexDll, "CPXinfointparam");
  *(void**)(&dll_CPXinfolongparam) = dll_sym(_cplexDll, "CPXinfolongparam");
  *(void**)(&dll_CPXinfodblparam) = dll_sym(_cplexDll, "CPXinfodblparam");
  *(void**)(&dll_CPXinfostrparam) = dll_sym(_cplexDll, "CPXinfostrparam");
  *(void**)(&dll_CPXsetlongparam) = dll_sym(_cplexDll, "CPXsetlongparam");

#else

  dll_CPXaddfuncdest = CPXaddfuncdest;
  dll_CPXaddindconstr = CPXaddindconstr;
  dll_CPXaddlazyconstraints = CPXaddlazyconstraints;
  dll_CPXaddmipstarts = CPXaddmipstarts;
  dll_CPXaddrows = CPXaddrows;
  dll_CPXaddusercuts = CPXaddusercuts;
  dll_CPXchgbds = CPXchgbds;
  dll_CPXchgmipstarts = CPXchgmipstarts;
  dll_CPXchgobjsen = CPXchgobjsen;
  dll_CPXcloseCPLEX = CPXcloseCPLEX;
  dll_CPXcreateprob = CPXcreateprob;
  dll_CPXcutcallbackadd = CPXcutcallbackadd;
  dll_CPXfreeprob = CPXfreeprob;
  dll_CPXgetbestobjval = CPXgetbestobjval;
  dll_CPXgetcallbackincumbent = CPXgetcallbackincumbent;
  dll_CPXgetcallbackinfo = CPXgetcallbackinfo;
  dll_CPXgetcallbacknodeinfo = CPXgetcallbacknodeinfo;
  dll_CPXgetcallbacknodex = CPXgetcallbacknodex;
  dll_CPXgetchannels = CPXgetchannels;
  dll_CPXgetdettime = CPXgetdettime;
  dll_CPXgeterrorstring = CPXgeterrorstring;
  dll_CPXgetmipstartindex = CPXgetmipstartindex;
  dll_CPXgetnodecnt = CPXgetnodecnt;
  dll_CPXgetnodeleftcnt = CPXgetnodeleftcnt;
  dll_CPXgetnumcols = CPXgetnumcols;
  dll_CPXgetnumrows = CPXgetnumrows;
  dll_CPXgetobjsen = CPXgetobjsen;
  dll_CPXgetobjval = CPXgetobjval;
  dll_CPXgetsolnpoolnumsolns = CPXgetsolnpoolnumsolns;
  dll_CPXgetstat = CPXgetstat;
  dll_CPXgetstatstring = CPXgetstatstring;
  dll_CPXgettime = CPXgettime;
  dll_CPXgetx = CPXgetx;
  dll_CPXmipopt = CPXmipopt;
  dll_CPXnewcols = CPXnewcols;
  dll_CPXopenCPLEX = CPXopenCPLEX;
  dll_CPXreadcopyparam = CPXreadcopyparam;
  dll_CPXsetdblparam = CPXsetdblparam;
  dll_CPXsetinfocallbackfunc = CPXsetinfocallbackfunc;
  dll_CPXsetintparam = CPXsetintparam;
  dll_CPXsetstrparam = CPXsetstrparam;
  dll_CPXsetlazyconstraintcallbackfunc = CPXsetlazyconstraintcallbackfunc;
  dll_CPXsetusercutcallbackfunc = CPXsetusercutcallbackfunc;
  dll_CPXversion = CPXversion;
  dll_CPXwriteparam = CPXwriteparam;
  dll_CPXwriteprob = CPXwriteprob;
  dll_CPXgetparamname = CPXgetparamname;
  dll_CPXgetparamnum = CPXgetparamnum;
  dll_CPXgetparamtype = CPXgetparamtype;
  dll_CPXinfointparam = CPXinfointparam;
  dll_CPXinfolongparam = CPXinfolongparam;
  dll_CPXinfodblparam = CPXinfodblparam;
  dll_CPXinfostrparam = CPXinfostrparam;
  dll_CPXsetlongparam = CPXsetlongparam;

#endif
}

string MIPCplexWrapper::getDescription(FactoryOptions& factoryOpt,
                                       MiniZinc::SolverInstanceBase::Options* opt) {
  string v = "MIP wrapper for IBM ILOG CPLEX  ";
  int status;
  Options def_options;
  Options* options = opt != nullptr ? static_cast<Options*>(opt) : &def_options;
  try {
    MIPCplexWrapper mcw(factoryOpt, options);
    CPXENVptr env = mcw.dll_CPXopenCPLEX(&status);
    if (env != nullptr) {
      v += mcw.dll_CPXversion(env);
      status = mcw.dll_CPXcloseCPLEX(&env);
    } else {
      v += "[?? ...cannot open CPLEX env to query version]";
    }
  } catch (MiniZinc::Error&) {
    v += "[?? ...cannot open CPLEX env to query version]";
  }
  v += "  Compiled  " __DATE__ "  " __TIME__;
  return v;
}

string MIPCplexWrapper::getVersion(FactoryOptions& factoryOpt,
                                   MiniZinc::SolverInstanceBase::Options* opt) {
  string v;
  int status;
  Options def_options;
  Options* options = opt != nullptr ? static_cast<Options*>(opt) : &def_options;
  try {
    MIPCplexWrapper mcw(factoryOpt, options);
    CPXENVptr env = mcw.dll_CPXopenCPLEX(&status);
    if (env != nullptr) {
      v += mcw.dll_CPXversion(env);
      status = mcw.dll_CPXcloseCPLEX(&env);
    } else {
      v += "<unknown version>";
    }
  } catch (MiniZinc::Error&) {
    v += "<unknown version>";
  }
  return v;
}

vector<string> MIPCplexWrapper::getRequiredFlags(FactoryOptions& f) {
  int status;
  FactoryOptions factoryOptions;
  Options options;
  try {
    MIPCplexWrapper mcw(factoryOptions, &options);
    CPXENVptr env = mcw.dll_CPXopenCPLEX(&status);
    if (env != nullptr) {
      return {};
    }
  } catch (MiniZinc::Error&) { /* NOLINT(bugprone-empty-catch) */
  }
  return {"--cplex-dll"};
}

vector<string> MIPCplexWrapper::getFactoryFlags() { return {"--cplex-dll"}; }

string MIPCplexWrapper::getId() { return "cplex"; }

string MIPCplexWrapper::getName() { return "CPLEX"; }

vector<string> MIPCplexWrapper::getTags() { return {"mip", "float", "api"}; }

vector<string> MIPCplexWrapper::getStdFlags() { return {"-i", "-p", "-s", "-v"}; }

vector<MiniZinc::SolverConfig::ExtraFlag> MIPCplexWrapper::getExtraFlags(
    FactoryOptions& factoryOpt) {
  // CPLEX doesn't seem to have a way to enumerate all parameters
  static std::vector<std::string> all_params = {
      "CPXPARAM_Advance", "CPXPARAM_Barrier_Algorithm", "CPXPARAM_Barrier_ColNonzeros",
      "CPXPARAM_Barrier_ConvergeTol", "CPXPARAM_Barrier_Crossover", "CPXPARAM_Barrier_Display",
      "CPXPARAM_Barrier_Limits_Corrections", "CPXPARAM_Barrier_Limits_Growth",
      "CPXPARAM_Barrier_Limits_Iteration", "CPXPARAM_Barrier_Limits_ObjRange",
      "CPXPARAM_Barrier_Ordering", "CPXPARAM_Barrier_QCPConvergeTol", "CPXPARAM_Barrier_StartAlg",
      "CPXPARAM_Benders_Strategy", "CPXPARAM_Benders_Tolerances_feasibilitycut",
      "CPXPARAM_Benders_Tolerances_optimalitycut", "CPXPARAM_Benders_WorkerAlgorithm",
      "CPXPARAM_ClockType", "CPXPARAM_Conflict_Algorithm", "CPXPARAM_Conflict_Display",
      "CPXPARAM_CPUmask", "CPXPARAM_DetTimeLimit", "CPXPARAM_DistMIP_Rampup_DetTimeLimit",
      "CPXPARAM_DistMIP_Rampup_Duration", "CPXPARAM_DistMIP_Rampup_TimeLimit",
      "CPXPARAM_Emphasis_Memory",
      // "CPXPARAM_Emphasis_MIP",
      "CPXPARAM_Emphasis_Numerical", "CPXPARAM_Feasopt_Mode", "CPXPARAM_Feasopt_Tolerance",
      "CPXPARAM_LPMethod", "CPXPARAM_MIP_Cuts_BQP", "CPXPARAM_MIP_Cuts_Cliques",
      "CPXPARAM_MIP_Cuts_Covers", "CPXPARAM_MIP_Cuts_Disjunctive", "CPXPARAM_MIP_Cuts_FlowCovers",
      "CPXPARAM_MIP_Cuts_Gomory", "CPXPARAM_MIP_Cuts_GUBCovers", "CPXPARAM_MIP_Cuts_Implied",
      "CPXPARAM_MIP_Cuts_LiftProj", "CPXPARAM_MIP_Cuts_LocalImplied", "CPXPARAM_MIP_Cuts_MCFCut",
      "CPXPARAM_MIP_Cuts_MIRCut", "CPXPARAM_MIP_Cuts_PathCut", "CPXPARAM_MIP_Cuts_RLT",
      "CPXPARAM_MIP_Cuts_ZeroHalfCut",
      // "CPXPARAM_MIP_Display",
      "CPXPARAM_MIP_Interval", "CPXPARAM_MIP_Limits_AggForCut",
      "CPXPARAM_MIP_Limits_AuxRootThreads", "CPXPARAM_MIP_Limits_CutPasses",
      "CPXPARAM_MIP_Limits_CutsFactor", "CPXPARAM_MIP_Limits_EachCutLimit",
      "CPXPARAM_MIP_Limits_GomoryCand", "CPXPARAM_MIP_Limits_GomoryPass",
      "CPXPARAM_MIP_Limits_Nodes", "CPXPARAM_MIP_Limits_PolishTime", "CPXPARAM_MIP_Limits_Populate",
      "CPXPARAM_MIP_Limits_ProbeDetTime", "CPXPARAM_MIP_Limits_ProbeTime",
      "CPXPARAM_MIP_Limits_RepairTries",
      // "CPXPARAM_MIP_Limits_Solutions",
      "CPXPARAM_MIP_Limits_StrongCand", "CPXPARAM_MIP_Limits_StrongIt",
      "CPXPARAM_MIP_Limits_TreeMemory", "CPXPARAM_MIP_OrderType",
      "CPXPARAM_MIP_PolishAfter_AbsMIPGap", "CPXPARAM_MIP_PolishAfter_DetTime",
      "CPXPARAM_MIP_PolishAfter_MIPGap", "CPXPARAM_MIP_PolishAfter_Nodes",
      "CPXPARAM_MIP_PolishAfter_Solutions", "CPXPARAM_MIP_PolishAfter_Time",
      "CPXPARAM_MIP_Pool_AbsGap", "CPXPARAM_MIP_Pool_Capacity", "CPXPARAM_MIP_Pool_Intensity",
      "CPXPARAM_MIP_Pool_RelGap", "CPXPARAM_MIP_Pool_Replace", "CPXPARAM_MIP_Strategy_Backtrack",
      "CPXPARAM_MIP_Strategy_BBInterval", "CPXPARAM_MIP_Strategy_Branch",
      // "CPXPARAM_MIP_Strategy_CallbackReducedLP",
      "CPXPARAM_MIP_Strategy_Dive",
      // "CPXPARAM_MIP_Strategy_File",
      "CPXPARAM_MIP_Strategy_FPHeur", "CPXPARAM_MIP_Strategy_HeuristicEffort",
      "CPXPARAM_MIP_Strategy_HeuristicFreq", "CPXPARAM_MIP_Strategy_KappaStats",
      "CPXPARAM_MIP_Strategy_LBHeur", "CPXPARAM_MIP_Strategy_MIQCPStrat",
      "CPXPARAM_MIP_Strategy_NodeSelect", "CPXPARAM_MIP_Strategy_Order",
      "CPXPARAM_MIP_Strategy_PresolveNode", "CPXPARAM_MIP_Strategy_Probe",
      "CPXPARAM_MIP_Strategy_RINSHeur",
      // "CPXPARAM_MIP_Strategy_Search",
      "CPXPARAM_MIP_Strategy_StartAlgorithm", "CPXPARAM_MIP_Strategy_SubAlgorithm",
      "CPXPARAM_MIP_Strategy_VariableSelect", "CPXPARAM_MIP_SubMIP_StartAlg",
      "CPXPARAM_MIP_SubMIP_SubAlg", "CPXPARAM_MIP_SubMIP_NodeLimit", "CPXPARAM_MIP_SubMIP_Scale",
      // "CPXPARAM_MIP_Tolerances_AbsMIPGap",
      "CPXPARAM_MIP_Tolerances_Linearization",
      // "CPXPARAM_MIP_Tolerances_Integrality",
      "CPXPARAM_MIP_Tolerances_LowerCutoff",
      // "CPXPARAM_MIP_Tolerances_MIPGap",
      "CPXPARAM_MIP_Tolerances_ObjDifference", "CPXPARAM_MIP_Tolerances_RelObjDifference",
      "CPXPARAM_MIP_Tolerances_UpperCutoff", "CPXPARAM_MultiObjective_Display",
      "CPXPARAM_Network_Display", "CPXPARAM_Network_Iterations", "CPXPARAM_Network_NetFind",
      "CPXPARAM_Network_Pricing", "CPXPARAM_Network_Tolerances_Feasibility",
      "CPXPARAM_Network_Tolerances_Optimality", "CPXPARAM_OptimalityTarget",
      "CPXPARAM_Output_CloneLog", "CPXPARAM_Output_IntSolFilePrefix", "CPXPARAM_Output_MPSLong",
      "CPXPARAM_Output_WriteLevel", "CPXPARAM_Parallel", "CPXPARAM_ParamDisplay",
      "CPXPARAM_Preprocessing_Aggregator", "CPXPARAM_Preprocessing_BoundStrength",
      "CPXPARAM_Preprocessing_CoeffReduce", "CPXPARAM_Preprocessing_Dependency",
      "CPXPARAM_Preprocessing_Dual", "CPXPARAM_Preprocessing_Fill",
      "CPXPARAM_Preprocessing_Folding",
      // "CPXPARAM_Preprocessing_Linear",
      "CPXPARAM_Preprocessing_NumPass", "CPXPARAM_Preprocessing_Presolve",
      "CPXPARAM_Preprocessing_QCPDuals", "CPXPARAM_Preprocessing_QPMakePSD",
      "CPXPARAM_Preprocessing_QToLin", "CPXPARAM_Preprocessing_Reduce",
      "CPXPARAM_Preprocessing_Relax", "CPXPARAM_Preprocessing_RepeatPresolve",
      "CPXPARAM_Preprocessing_Symmetry", "CPXPARAM_QPMethod",
      // "CPXPARAM_RandomSeed",
      "CPXPARAM_Read_APIEncoding", "CPXPARAM_Read_Constraints", "CPXPARAM_Read_DataCheck",
      "CPXPARAM_Read_FileEncoding", "CPXPARAM_Read_Nonzeros", "CPXPARAM_Read_QPNonzeros",
      "CPXPARAM_Read_Scale", "CPXPARAM_Read_Variables", "CPXPARAM_Read_WarningLimit",
      "CPXPARAM_Record", "CPXPARAM_ScreenOutput", "CPXPARAM_Sifting_Algorithm",
      "CPXPARAM_Sifting_Simplex", "CPXPARAM_Sifting_Display", "CPXPARAM_Sifting_Iterations",
      "CPXPARAM_Simplex_Crash", "CPXPARAM_Simplex_DGradient", "CPXPARAM_Simplex_Display",
      "CPXPARAM_Simplex_DynamicRows", "CPXPARAM_Simplex_Limits_Iterations",
      "CPXPARAM_Simplex_Limits_LowerObj", "CPXPARAM_Simplex_Limits_Perturbation",
      "CPXPARAM_Simplex_Limits_Singularity", "CPXPARAM_Simplex_Limits_UpperObj",
      "CPXPARAM_Simplex_Perturbation_Constant", "CPXPARAM_Simplex_Perturbation_Indicator",
      "CPXPARAM_Simplex_PGradient", "CPXPARAM_Simplex_Pricing", "CPXPARAM_Simplex_Refactor",
      "CPXPARAM_Simplex_Tolerances_Feasibility", "CPXPARAM_Simplex_Tolerances_Markowitz",
      "CPXPARAM_Simplex_Tolerances_Optimality", "CPXPARAM_SolutionType",
      // "CPXPARAM_Threads",
      // "CPXPARAM_TimeLimit",
      "CPXPARAM_Tune_DetTimeLimit", "CPXPARAM_Tune_Display", "CPXPARAM_Tune_Measure",
      "CPXPARAM_Tune_Repeat", "CPXPARAM_Tune_TimeLimit",
      // "CPXPARAM_WorkDir",
      // "CPXPARAM_WorkMem",
      "CPX_PARAM_ADVIND", "CPX_PARAM_AGGFILL", "CPX_PARAM_AGGIND", "CPX_PARAM_CLOCKTYPE",
      "CPX_PARAM_CRAIND", "CPX_PARAM_DEPIND", "CPX_PARAM_DPRIIND", "CPX_PARAM_PRICELIM",
      "CPX_PARAM_EPMRK", "CPX_PARAM_EPOPT", "CPX_PARAM_EPPER", "CPX_PARAM_EPRHS",
      "CPX_PARAM_SIMDISPLAY", "CPX_PARAM_ITLIM", "CPX_PARAM_ROWREADLIM", "CPX_PARAM_NETFIND",
      "CPX_PARAM_COLREADLIM", "CPX_PARAM_NZREADLIM", "CPX_PARAM_OBJLLIM", "CPX_PARAM_OBJULIM",
      "CPX_PARAM_PERIND", "CPX_PARAM_PERLIM", "CPX_PARAM_PPRIIND", "CPX_PARAM_PREIND",
      "CPX_PARAM_REINV", "CPX_PARAM_SCAIND", "CPX_PARAM_SCRIND", "CPX_PARAM_SINGLIM",
      "CPX_PARAM_TILIM", "CPX_PARAM_PREDUAL", "CPX_PARAM_PREPASS", "CPX_PARAM_DATACHECK",
      "CPX_PARAM_REDUCE", "CPX_PARAM_PRELINEAR", "CPX_PARAM_LPMETHOD", "CPX_PARAM_QPMETHOD",
      "CPX_PARAM_WORKDIR", "CPX_PARAM_WORKMEM", "CPX_PARAM_THREADS", "CPX_PARAM_CONFLICTALG",
      "CPX_PARAM_CONFLICTDISPLAY", "CPX_PARAM_SIFTDISPLAY", "CPX_PARAM_SIFTALG",
      "CPX_PARAM_SIFTITLIM", "CPX_PARAM_MPSLONGNUM", "CPX_PARAM_MEMORYEMPHASIS",
      "CPX_PARAM_NUMERICALEMPHASIS", "CPX_PARAM_FEASOPTMODE", "CPX_PARAM_PARALLELMODE",
      "CPX_PARAM_TUNINGMEASURE", "CPX_PARAM_TUNINGREPEAT", "CPX_PARAM_TUNINGTILIM",
      "CPX_PARAM_TUNINGDISPLAY", "CPX_PARAM_WRITELEVEL", "CPX_PARAM_RANDOMSEED",
      "CPX_PARAM_DETTILIM", "CPX_PARAM_FILEENCODING", "CPX_PARAM_APIENCODING",
      "CPX_PARAM_OPTIMALITYTARGET", "CPX_PARAM_CLONELOG", "CPX_PARAM_TUNINGDETTILIM",
      "CPX_PARAM_CPUMASK", "CPX_PARAM_SOLUTIONTYPE", "CPX_PARAM_WARNLIM", "CPX_PARAM_SIFTSIM",
      "CPX_PARAM_DYNAMICROWS", "CPX_PARAM_RECORD", "CPX_PARAM_PARAMDISPLAY", "CPX_PARAM_FOLDING",
      "CPX_PARAM_WORKERALG", "CPX_PARAM_BENDERSSTRATEGY", "CPX_PARAM_BENDERSFEASCUTTOL",
      "CPX_PARAM_BENDERSOPTCUTTOL", "CPX_PARAM_MULTIOBJDISPLAY", "CPX_PARAM_BRDIR",
      "CPX_PARAM_BTTOL", "CPX_PARAM_CLIQUES", "CPX_PARAM_COEREDIND", "CPX_PARAM_COVERS",
      "CPX_PARAM_CUTLO", "CPX_PARAM_CUTUP", "CPX_PARAM_EPAGAP", "CPX_PARAM_EPGAP",
      "CPX_PARAM_EPINT", "CPX_PARAM_MIPDISPLAY", "CPX_PARAM_MIPINTERVAL", "CPX_PARAM_INTSOLLIM",
      "CPX_PARAM_NODEFILEIND", "CPX_PARAM_NODELIM", "CPX_PARAM_NODESEL", "CPX_PARAM_OBJDIF",
      "CPX_PARAM_MIPORDIND", "CPX_PARAM_RELOBJDIF", "CPX_PARAM_STARTALG", "CPX_PARAM_SUBALG",
      "CPX_PARAM_TRELIM", "CPX_PARAM_VARSEL", "CPX_PARAM_BNDSTRENIND", "CPX_PARAM_HEURFREQ",
      "CPX_PARAM_MIPORDTYPE", "CPX_PARAM_CUTSFACTOR", "CPX_PARAM_RELAXPREIND", "CPX_PARAM_PRESLVND",
      "CPX_PARAM_BBINTERVAL", "CPX_PARAM_FLOWCOVERS", "CPX_PARAM_IMPLBD", "CPX_PARAM_PROBE",
      "CPX_PARAM_GUBCOVERS", "CPX_PARAM_STRONGCANDLIM", "CPX_PARAM_STRONGITLIM",
      "CPX_PARAM_FRACCAND", "CPX_PARAM_FRACCUTS", "CPX_PARAM_FRACPASS", "CPX_PARAM_FLOWPATHS",
      "CPX_PARAM_MIRCUTS", "CPX_PARAM_DISJCUTS", "CPX_PARAM_AGGCUTLIM",
      // "CPX_PARAM_MIPCBREDLP",
      "CPX_PARAM_CUTPASS", "CPX_PARAM_MIPEMPHASIS", "CPX_PARAM_SYMMETRY", "CPX_PARAM_DIVETYPE",
      "CPX_PARAM_RINSHEUR", "CPX_PARAM_LBHEUR", "CPX_PARAM_REPEATPRESOLVE", "CPX_PARAM_PROBETIME",
      "CPX_PARAM_POLISHTIME", "CPX_PARAM_REPAIRTRIES", "CPX_PARAM_EPLIN", "CPX_PARAM_EPRELAX",
      "CPX_PARAM_FPHEUR", "CPX_PARAM_EACHCUTLIM", "CPX_PARAM_SOLNPOOLCAPACITY",
      "CPX_PARAM_SOLNPOOLREPLACE", "CPX_PARAM_SOLNPOOLGAP", "CPX_PARAM_SOLNPOOLAGAP",
      "CPX_PARAM_SOLNPOOLINTENSITY", "CPX_PARAM_POPULATELIM", "CPX_PARAM_MIPSEARCH",
      "CPX_PARAM_MIQCPSTRAT", "CPX_PARAM_ZEROHALFCUTS", "CPX_PARAM_HEUREFFORT",
      "CPX_PARAM_POLISHAFTEREPAGAP", "CPX_PARAM_POLISHAFTEREPGAP", "CPX_PARAM_POLISHAFTERNODE",
      "CPX_PARAM_POLISHAFTERINTSOL", "CPX_PARAM_POLISHAFTERTIME", "CPX_PARAM_MCFCUTS",
      "CPX_PARAM_MIPKAPPASTATS", "CPX_PARAM_AUXROOTTHREADS", "CPX_PARAM_INTSOLFILEPREFIX",
      "CPX_PARAM_PROBEDETTIME", "CPX_PARAM_POLISHAFTERDETTIME", "CPX_PARAM_LANDPCUTS",
      "CPX_PARAM_RAMPUPDURATION", "CPX_PARAM_RAMPUPDETTILIM", "CPX_PARAM_RAMPUPTILIM",
      "CPX_PARAM_LOCALIMPLBD", "CPX_PARAM_BQPCUTS", "CPX_PARAM_RLTCUTS", "CPX_PARAM_SUBMIPSTARTALG",
      "CPX_PARAM_SUBMIPSUBALG", "CPX_PARAM_SUBMIPSCAIND", "CPX_PARAM_SUBMIPNODELIMIT",
      "CPX_PARAM_BAREPCOMP", "CPX_PARAM_BARGROWTH", "CPX_PARAM_BAROBJRNG", "CPX_PARAM_BARALG",
      "CPX_PARAM_BARCOLNZ", "CPX_PARAM_BARDISPLAY", "CPX_PARAM_BARITLIM", "CPX_PARAM_BARMAXCOR",
      "CPX_PARAM_BARORDER", "CPX_PARAM_BARSTARTALG", "CPX_PARAM_BARCROSSALG",
      "CPX_PARAM_BARQCPEPCOMP", "CPX_PARAM_QPNZREADLIM", "CPX_PARAM_CALCQCPDUALS",
      "CPX_PARAM_QPMAKEPSDIND", "CPX_PARAM_QTOLININD", "CPX_PARAM_NETITLIM", "CPX_PARAM_NETEPOPT",
      "CPX_PARAM_NETEPRHS", "CPX_PARAM_NETPPRIIND", "CPX_PARAM_NETDISPLAY"};
  int status;
  Options def_options;
  try {
    MIPCplexWrapper mcw(factoryOpt, &def_options);
    CPXENVptr env = mcw.dll_CPXopenCPLEX(&status);
    if (env == nullptr) {
      return {};
    }
    std::vector<MiniZinc::SolverConfig::ExtraFlag> res;
    for (auto& param : all_params) {
      int num;
      int type;
      if (mcw.dll_CPXgetparamnum(env, param.c_str(), &num) != 0) {
        // Param not available
        continue;
      }
      mcw.dll_CPXgetparamtype(env, num, &type);
      MiniZinc::SolverConfig::ExtraFlag::FlagType param_type;
      std::vector<std::string> param_range;
      std::string param_default;
      switch (type) {
        case CPX_PARAMTYPE_INT: {
          CPXINT def;
          CPXINT min;
          CPXINT max;
          mcw.dll_CPXinfointparam(env, num, &def, &min, &max);
          param_type = MiniZinc::SolverConfig::ExtraFlag::FlagType::T_INT;
          param_range.push_back(std::to_string(min));
          param_range.push_back(std::to_string(max));
          param_default = std::to_string(def);
          break;
        }
        case CPX_PARAMTYPE_LONG: {
          CPXLONG def;
          CPXLONG min;
          CPXLONG max;
          mcw.dll_CPXinfolongparam(env, num, &def, &min, &max);
          param_type = MiniZinc::SolverConfig::ExtraFlag::FlagType::T_INT;
          param_range.push_back(std::to_string(min));
          param_range.push_back(std::to_string(max));
          param_default = std::to_string(def);
          break;
        }
        case CPX_PARAMTYPE_DOUBLE: {
          double def;
          double min;
          double max;
          mcw.dll_CPXinfodblparam(env, num, &def, &min, &max);
          param_type = MiniZinc::SolverConfig::ExtraFlag::FlagType::T_FLOAT;
          param_range.push_back(std::to_string(min));
          param_range.push_back(std::to_string(max));
          param_default = std::to_string(def);
          break;
        }
        case CPX_PARAMTYPE_STRING: {
          char def[CPX_STR_PARAM_MAX];
          mcw.dll_CPXinfostrparam(env, num, def);
          param_type = MiniZinc::SolverConfig::ExtraFlag::FlagType::T_STRING;
          param_default = std::string(def);
          break;
        }
        default:
          continue;
      }
      res.emplace_back("--cplex-" + param, param, param_type, param_range, param_default);
    }
    return res;
  } catch (MiniZinc::Error&) {
    return {};
  }
  return {};
}

void MIPCplexWrapper::Options::printHelp(ostream& os) {
  os << "IBM ILOG CPLEX  MIP wrapper options:"
     << std::endl
     // -s                  print statistics
     //            << "  --readParam <file>  read CPLEX parameters from file
     //               << "--writeParam <file> write CPLEX parameters to file
     //               << "--tuneParam         instruct CPLEX to tune parameters instead of solving
     << "  --mipfocus <n>\n    1: feasibility, 2: optimality, 3: move bound (default is 0, "
        "balanced)"
     << std::endl
     << "  -i\n    print intermediate solutions for optimization problems" << std::endl
     << "  -p <N>, --parallel <N>\n    use N threads, default: 1"
     << std::endl
     //   << "  --nomippresolve     disable MIP presolving   NOT IMPL" << std::endl
     << "  --solver-time-limit <N>\n    stop search after N milliseconds wall time" << std::endl
     << "  -n <N>, --num-solutions <N>\n"
        "    stop search after N solutions"
     << std::endl
     << "  -r <N>, --random-seed <N>\n"
        "    random seed, integer"
     << std::endl
     << "  --workmem <N>, --nodefilestart <N>\n"
        "    maximal RAM for working memory used before writing to node file, GB, default: 0.5"
     << std::endl
     << "  --nodefiledir <path>\n"
        "    nodefile directory"
     << std::endl
     << "  --writeModel <file>\n    write model to <file> (.lp, .mps, .sav, ...)" << std::endl
     << "  --readParam <file>\n    read CPLEX parameters from file" << std::endl
     << "  --writeParam <file>\n    write CPLEX parameters to file"
     << std::endl
     //   << "  --tuneParam         instruct CPLEX to tune parameters instead of solving   NOT IMPL"

     << "  --absGap <n>\n    absolute gap |primal-dual| to stop" << std::endl
     << "  --relGap <n>\n    relative gap |primal-dual|/<solver-dep> to stop. Default 1e-8, set <0 "
        "to use backend's default"
     << std::endl
     << "  --intTol <n>\n    integrality tolerance for a variable. Default 1e-8" << std::endl
     << "\n  --cplex-dll <file> or <basename>\n    CPLEX DLL, or base name, such as cplex1280, "
        "when using plugin. Default range tried: "
     << cplex_dlls().front() << " .. " << cplex_dlls().back()
     << std::endl
     //   << "  --objDiff <n>       objective function discretization. Default 1.0" << std::endl

     << std::endl;
}

bool MIPCplexWrapper::FactoryOptions::processOption(int& i, std::vector<std::string>& argv,
                                                    const std::string& workingDir) {
  MiniZinc::CLOParser cop(i, argv);
  return cop.get("--cplex-dll", &cplexDll);
}

bool MIPCplexWrapper::Options::processOption(int& i, std::vector<std::string>& argv,
                                             const std::string& workingDir) {
  MiniZinc::CLOParser cop(i, argv);
  std::string buffer;
  if (cop.get("-i")) {
    flagIntermediate = true;
  } else if (string(argv[i]) == "-f" ||
             string(argv[i]) == "--free-search") {  // NOLINT: Allow repeated empty if
    //     std::cerr << "  Flag -f: ignoring fixed strategy anyway." << std::endl;
  } else if (cop.get("--mipfocus --mipFocus --MIPFocus --MIPfocus",
                     &nMIPFocus)) {  // NOLINT: Allow repeated empty if
  } else if (cop.get("--writeModel", &buffer)) {
    sExportModel = MiniZinc::FileUtils::file_path(buffer, workingDir);
  } else if (cop.get("-p  --parallel", &nThreads)) {       // NOLINT: Allow repeated empty if
  } else if (cop.get("--solver-time-limit", &nTimeout)) {  // NOLINT: Allow repeated empty if
  } else if (cop.get("-n --num-solutions", &nSolLimit)) {  // NOLINT: Allow repeated empty if
  } else if (cop.get("-r --random-seed", &nSeed)) {        // NOLINT: Allow repeated empty if
  } else if (cop.get("--workmem --nodefilestart",
                     &nWorkMemLimit)) {  // NOLINT: Allow repeated empty if
  } else if (cop.get("--nodefiledir --NodefileDir",
                     &sNodefileDir)) {  // NOLINT: Allow repeated empty if
  } else if (cop.get("--readParam", &buffer)) {
    sReadParams = MiniZinc::FileUtils::file_path(buffer, workingDir);
  } else if (cop.get("--writeParam", &buffer)) {
    sWriteParams = MiniZinc::FileUtils::file_path(buffer, workingDir);
  } else if (cop.get("--absGap", &absGap)) {  // NOLINT: Allow repeated empty if
  } else if (cop.get("--relGap", &relGap)) {  // NOLINT: Allow repeated empty if
  } else if (cop.get("--intTol", &intTol)) {  // NOLINT: Allow repeated empty if
    //   } else if ( cop.get( "--objDiff", &objDiff ) ) {
  } else {
    return false;
  }
  return true;
}

void MIPCplexWrapper::wrapAssert(bool cond, const string& msg, bool fTerm) {
  if (!cond) {
    strcpy(_cplexBuffer, "[NO ERROR STRING GIVEN]");
    dll_CPXgeterrorstring(_env, _status, _cplexBuffer);
    string msgAll = ("  MIPCplexWrapper runtime error:  " + msg + "  " + _cplexBuffer);
    if (fTerm) {
      throw MiniZinc::Error(msgAll);
    }
    cerr << msgAll << endl;
  }
}

void MIPCplexWrapper::openCPLEX() {
  checkDLL();
  cbui.wrapper = this;
  /// Cleanup first.
  //   cleanup();
  /* Initialize the CPLEX environment */
  _env = dll_CPXopenCPLEX(&_status);
  /* If an error occurs, the status value indicates the reason for
     failure.  A call to CPXgeterrorstring will produce the text of
     the error message.  Note that CPXopenCPLEX produces no output,
     so the only way to see the cause of the error is to use
     CPXgeterrorstring.  For other CPLEX routines, the errors will
     be seen if the CPXPARAM_ScreenOutput indicator is set to CPX_ON.  */
  wrapAssert(_env != nullptr, "Could not open CPLEX environment.");
  /* Create the problem. */
  _lp = dll_CPXcreateprob(_env, &_status, "MIPCplexWrapper");
  /* A returned pointer of NULL may mean that not enough memory
     was available or there was some other problem.  In the case of
     failure, an error message will have been written to the error
     channel from inside CPLEX.  In this example, the setting of
     the parameter CPXPARAM_ScreenOutput causes the error message to
     appear on stdout.  */
  wrapAssert(_lp != nullptr, "Failed to create LP.");
}

void MIPCplexWrapper::closeCPLEX() {
  /// Freeing the problem can be slow both in C and C++, see IBM forums. Skipping.
  /* Free up the problem as allocated by CPXcreateprob, if necessary */
  //    if ( lp != NULL ) {
  //       status = CPXfreeprob (env, &lp);
  //       cplex_wrapAssert ( !status, "CPXfreeprob failed." );
  //    }
  _lp = nullptr;
  /* Free up the CPLEX environment, if necessary */
  if (_env != nullptr) {
    _status = dll_CPXcloseCPLEX(&_env);
    assert(_status == 0);  // Assume CPLEX environment is closed correctly
  }
  /// and at last:
//   MIPWrapper::cleanup();
#ifdef CPLEX_PLUGIN
//  dll_close(cplex_dll);
#endif
}

void MIPCplexWrapper::doAddVars(size_t n, double* obj, double* lb, double* ub,
                                MIPWrapper::VarType* vt, string* names) {
  /// Convert var types:
  vector<char> ctype(n);
  vector<char*> pcNames(n);
  for (size_t i = 0; i < n; ++i) {
    pcNames[i] = (char*)names[i].c_str();
    switch (vt[i]) {
      case REAL:
        ctype[i] = CPX_CONTINUOUS;
        break;
      case INT:
        ctype[i] = CPX_INTEGER;
        break;
      case BINARY:
        ctype[i] = CPX_BINARY;
        break;
      default:
        throw MiniZinc::InternalError("  MIPWrapper: unknown variable type");
    }
  }
  _status =
      dll_CPXnewcols(_env, _lp, static_cast<int>(n), obj, lb, ub, ctype.data(), pcNames.data());
  wrapAssert(_status == 0, "Failed to declare variables.");
}

static char get_cplex_constr_cense(MIPWrapper::LinConType sense) {
  switch (sense) {
    case MIPWrapper::LQ:
      return 'L';
    case MIPWrapper::EQ:
      return 'E';
    case MIPWrapper::GQ:
      return 'G';
    default:
      throw MiniZinc::InternalError("  MIPCplexWrapper: unknown constraint type");
  }
}

void MIPCplexWrapper::addRow(int nnz, int* rmatind, double* rmatval, MIPWrapper::LinConType sense,
                             double rhs, int mask, const string& rowName) {
  /// Convert var types:
  char ssense = get_cplex_constr_cense(sense);
  const int ccnt = 0;
  const int rcnt = 1;
  const int rmatbeg[] = {0};
  char* pRName = (char*)rowName.c_str();
  if ((MaskConsType_Normal & mask) != 0) {
    _status = dll_CPXaddrows(_env, _lp, ccnt, rcnt, nnz, &rhs, &ssense, rmatbeg, rmatind, rmatval,
                             nullptr, &pRName);
    wrapAssert(_status == 0, "Failed to add constraint.");
  }
  if ((MaskConsType_Usercut & mask) != 0) {
    _status =
        dll_CPXaddusercuts(_env, _lp, rcnt, nnz, &rhs, &ssense, rmatbeg, rmatind, rmatval, &pRName);
    wrapAssert(_status == 0, "Failed to add usercut.");
  }
  if ((MaskConsType_Lazy & mask) != 0) {
    _status = dll_CPXaddlazyconstraints(_env, _lp, rcnt, nnz, &rhs, &ssense, rmatbeg, rmatind,
                                        rmatval, &pRName);
    wrapAssert(_status == 0, "Failed to add lazy constraint.");
  }
}

void MIPCplexWrapper::addIndicatorConstraint(int iBVar, int bVal, int nnz, int* rmatind,
                                             double* rmatval, MIPWrapper::LinConType sense,
                                             double rhs, const string& rowName) {
  wrapAssert(0 <= bVal && 1 >= bVal, "mzn-cplex: addIndicatorConstraint: bVal not 0/1");
  char ssense = get_cplex_constr_cense(sense);
  _status = dll_CPXaddindconstr(_env, _lp, iBVar, 1 - bVal, nnz, rhs, ssense, rmatind, rmatval,
                                rowName.c_str());
  wrapAssert(_status == 0, "Failed to add indicator constraint.");
}

bool MIPCplexWrapper::addWarmStart(const std::vector<VarId>& vars,
                                   const std::vector<double>& vals) {
  assert(vars.size() == vals.size());
  const char* sMSName = "MZNMS";
  int msindex = -1;
  const int beg = 0;
  /// Check if we already added a start
  _status = dll_CPXgetmipstartindex(_env, _lp, sMSName, &msindex);
  if (_status != 0) {  // not existent
    // status = dll_CPXaddmipstarts (env, lp, mcnt, nzcnt, beg, varindices,
    //                            values, effortlevel, mipstartname);
    _status = dll_CPXaddmipstarts(_env, _lp, 1, static_cast<int>(vars.size()), &beg, vars.data(),
                                  vals.data(), nullptr, (char**)&sMSName);
    wrapAssert(_status == 0, "Failed to add warm start.");
  } else {
    // status = dll_CPXchgmipstarts (env, lp, mcnt, mipstartindices, nzcnt, beg, varindices, values,
    // effortlevel);
    _status = dll_CPXchgmipstarts(_env, _lp, 1, &msindex, static_cast<int>(vars.size()), &beg,
                                  vars.data(), vals.data(), nullptr);
    wrapAssert(_status == 0, "Failed to extend warm start.");
  }
  return true;
}

void MIPCplexWrapper::setVarBounds(int iVar, double lb, double ub) {
  wrapAssert(lb <= ub, "mzn-cplex: setVarBounds: lb>ub");
  char cl = 'L';
  char cu = 'U';
  _status = dll_CPXchgbds(_env, _lp, 1, &iVar, &cl, &lb);
  wrapAssert(_status == 0, "Failed to set lower bound.");
  _status = dll_CPXchgbds(_env, _lp, 1, &iVar, &cu, &ub);
  wrapAssert(_status == 0, "Failed to set upper bound.");
}

void MIPCplexWrapper::setVarLB(int iVar, double lb) {
  char cl = 'L';
  _status = dll_CPXchgbds(_env, _lp, 1, &iVar, &cl, &lb);
  wrapAssert(_status == 0, "Failed to set lower bound.");
}

void MIPCplexWrapper::setVarUB(int iVar, double ub) {
  char cu = 'U';
  _status = dll_CPXchgbds(_env, _lp, 1, &iVar, &cu, &ub);
  wrapAssert(_status == 0, "Failed to set upper bound.");
}

/// SolutionCallback ------------------------------------------------------------------------
/// CPLEX ensures thread-safety
static int CPXPUBLIC solcallback(CPXCENVptr env, void* cbdata, int wherefrom, void* cbhandle) {
  int status = 0;

  auto* info = (MIPWrapper::CBUserInfo*)cbhandle;
  auto* cw = static_cast<MIPCplexWrapper*>(info->wrapper);
  int hasincumbent = 0;
  int newincumbent = 0;
  double objVal;

  status = cw->dll_CPXgetcallbackinfo(env, cbdata, wherefrom, CPX_CALLBACK_INFO_NODE_COUNT,
                                      &info->pOutput->nNodes);
  if (status != 0) {
    goto TERMINATE;
  }

  status = cw->dll_CPXgetcallbackinfo(env, cbdata, wherefrom, CPX_CALLBACK_INFO_NODES_LEFT,
                                      &info->pOutput->nOpenNodes);
  if (status != 0) {
    goto TERMINATE;
  }

  status =
      cw->dll_CPXgetcallbackinfo(env, cbdata, wherefrom, CPX_CALLBACK_INFO_MIP_FEAS, &hasincumbent);
  if (status != 0) {
    goto TERMINATE;
  }

  if (hasincumbent != 0) {
    status =
        cw->dll_CPXgetcallbackinfo(env, cbdata, wherefrom, CPX_CALLBACK_INFO_BEST_INTEGER, &objVal);
    if (status != 0) {
      goto TERMINATE;
    }

    if (fabs(info->pOutput->objVal - objVal) > 1e-12 * (1.0 + fabs(objVal))) {
      newincumbent = 1;
      info->pOutput->objVal = objVal;
      info->pOutput->status = MIPWrapper::SAT;
      info->pOutput->statusName = "feasible from a callback";
    }
  }

  //    if ( nodecnt >= info->lastlog + 100  ||  newincumbent ) {
  //       double walltime;
  //       double dettime;

  status = cw->dll_CPXgetcallbackinfo(env, cbdata, wherefrom, CPX_CALLBACK_INFO_BEST_REMAINING,
                                      &info->pOutput->bestBound);
  //       if ( status )  goto TERMINATE;

  //       status = dll_CPXgettime (env, &walltime);
  //       if ( status )  goto TERMINATE;
  //
  //       status = dll_CPXgetdettime (env, &dettime);
  //       if ( status )  goto TERMINATE;
  //
  //    }

  if (newincumbent != 0) {
    assert(info->pOutput->x);
    status = cw->dll_CPXgetcallbackincumbent(env, cbdata, wherefrom, (double*)info->pOutput->x, 0,
                                             info->pOutput->nCols - 1);
    if (status != 0) {
      goto TERMINATE;
    }

    info->pOutput->dWallTime =
        std::chrono::duration<double>(std::chrono::steady_clock::now() - info->pOutput->dWallTime0)
            .count();
    info->pOutput->dCPUTime = double(std::clock() - info->pOutput->cCPUTime0) / CLOCKS_PER_SEC;

    /// Call the user function:
    if (info->solcbfn != nullptr) {
      (*info->solcbfn)(*info->pOutput, info->psi);
    }
    info->printed = true;
  }

TERMINATE:
  return status;

} /* END logcallback */
// end SolutionCallback ---------------------------------------------------------------------

/// Cut callbacks, mostly copied from admipex5.c, CPLEX 12.6.3
/* The following macro defines the smallest improvement
   on the value of the objective function that is required
   when adding user cuts from within a callback.
   If the improvement on the value of the ojective function
   is not large enough, the callback will abort the cut loop. */

#define EPSOBJ 0.1

/* The following structure will hold the information we need to
   pass to the cut callback function */

struct CutInfo {
  CPXLPptr lp;
  int numcols;
  int num;
  double* x;
  int* beg;
  int* ind;
  double* val;
  double* rhs;
  int nodeid;
  double nodeobjval;
  int objsen;
  MIPWrapper::CBUserInfo* info = nullptr;
};
typedef struct CutInfo CUTINFO, *CUTINFOptr;

/* Init information on the node objval for the user cut callback */

static void initnodeobjvalinfo(MIPCplexWrapper* cw, CPXENVptr env, CPXLPptr lp,
                               CUTINFOptr cutinfo) {
  cutinfo->nodeid = -1;
  cutinfo->nodeobjval = 0.0;
  cutinfo->objsen = cw->dll_CPXgetobjsen(env, lp);
  if (cutinfo->objsen == CPX_MIN) {
    cutinfo->objsen = 1;
  } else {
    cutinfo->objsen = -1;
  }
} /* END initnodeobjvalinfo */

static int CPXPUBLIC myusercutcallback(CPXCENVptr env, void* cbdata, int wherefrom, void* cbhandle,
                                       int* useraction_p) {
  int status = 0;

  auto* cutinfo = (CUTINFOptr)cbhandle;

  //   int      numcols  = cutinfo->numcols;
  //   int      numcuts  = cutinfo->num;
  //    double   *x       = cutinfo->x;
  //    int      *beg     = cutinfo->beg;
  //    int      *ind     = cutinfo->ind;
  //    double   *val     = cutinfo->val;
  //    double   *rhs     = cutinfo->rhs;
  //    int      *cutind  = NULL;
  //    double   *cutval  = NULL;
  //   double   cutvio;
  int addedcuts = 0;
  //   int      i, j, k; //, cutnz;
  MIPWrapper::CBUserInfo* info = cutinfo->info;
  auto* cw = static_cast<MIPCplexWrapper*>(info->wrapper);
  //    double   *x       = info->pCutOutput->x;

  *useraction_p = CPX_CALLBACK_DEFAULT;

  /* If we are called as a user cut callback, decide
     first if we want to add cuts or abort the cut loop.
     When adding user cuts with purgeable flag set to
     CPX_USECUT_PURGE or CPX_USECUT_FILTER, it is important
     to avoid the possibility of an infinite cut loop, where
     the same cuts are added to the LP and then immediately
     purged at every cut pass. Such a situation can be avoided,
     for instance, by applying a tailing off criterion and aborting
     the cut loop where no progress in the objval is observed.
     Note, however, that the same approach must not be applied
     with lazy constraints. In this case, if lazy constraints are
     added with purgeable flag set to CPX_USECUT_PURGE, adding
     the same lazy constraint more than once could be required
     to ensure the correctness of the final result. */

  bool fMIPSol = true;
  if (wherefrom == CPX_CALLBACK_MIP_CUT_LOOP || wherefrom == CPX_CALLBACK_MIP_CUT_LAST) {
    int oldnodeid = cutinfo->nodeid;
    double oldnodeobjval = cutinfo->nodeobjval;

    fMIPSol = false;

    /* Retrieve nodeid and node objval of the current node */

    status = cw->dll_CPXgetcallbacknodeinfo(env, cbdata, wherefrom, 0,
                                            CPX_CALLBACK_INFO_NODE_SEQNUM, &cutinfo->nodeid);
    if (status != 0) {
      fprintf(stderr, "Failed to get node id.\n");
      goto TERMINATE;
    }

    status = cw->dll_CPXgetcallbacknodeinfo(env, cbdata, wherefrom, 0,
                                            CPX_CALLBACK_INFO_NODE_OBJVAL, &cutinfo->nodeobjval);
    if (status != 0) {
      fprintf(stderr, "Failed to get node objval.\n");
      goto TERMINATE;
    }

    /* Abort the cut loop if we are stuck at the same node
       as before and there is no progress in the node objval */

    if (oldnodeid == cutinfo->nodeid) {
      double objchg = (cutinfo->nodeobjval - oldnodeobjval);
      /* Multiply objchg by objsen to normalize
         the change in the objective function to
         the case of a minimization problem */
      objchg *= cutinfo->objsen;
      if (objchg <= EPSOBJ) {
        *useraction_p = CPX_CALLBACK_ABORT_CUT_LOOP;
        goto TERMINATE;
      }
    }
  }

  /* If we reached this point, we are
     .. in a lazyconstraint callback, or
     .. in a user cut callback, and cuts seem to help
        improving the node objval.
     In both cases, we retrieve the x solution and
     look for violated cuts. */

  if (info->cutcbfn != nullptr) {  // if cut handler given
    MIPWrapper::Output outpRlx;
    outpRlx.x = info->pOutput->x;  // using the sol output storage  TODO?
    outpRlx.nCols = info->pOutput->nCols;
    assert(outpRlx.x && outpRlx.nCols);
    status = cw->dll_CPXgetcallbacknodex(env, cbdata, wherefrom, (double*)outpRlx.x, 0,
                                         outpRlx.nCols - 1);
    if (status != 0) {
      fprintf(stderr, "Cut callback: failed to get node solution.\n");
      goto TERMINATE;
    }
    MIPWrapper::CutInput cutInput;
    info->cutcbfn(outpRlx, cutInput, info->psi, fMIPSol);
    // static int nCuts = 0;
    // nCuts += cutInput.size();
    // if ( cutInput.size() )
    //  cerr << "\n   N CUTS:  " << nCuts << endl;
    for (auto& cd : cutInput) {
      if ((cd.mask & (MIPWrapper::MaskConsType_Usercut | MIPWrapper::MaskConsType_Lazy)) == 0) {
        throw MiniZinc::InternalError("Cut callback: should be user/lazy");
      }
      /* Use a cut violation tolerance of 0.01 */
      if (true) {  // NOLINT: cutvio > 0.01 ) {
        status = cw->dll_CPXcutcallbackadd(
            env, cbdata, wherefrom, static_cast<int>(cd.rmatind.size()), cd.rhs,
            get_cplex_constr_cense(cd.sense), cd.rmatind.data(), cd.rmatval.data(),
            CPX_USECUT_FORCE);  // PURGE?
        if (status != 0) {
          fprintf(stderr, "CPLEX callback: failed to add cut.\n");
          goto TERMINATE;
        }
        addedcuts++;
      }
    }
  }

  /* Tell CPLEX that cuts have been created */
  if (addedcuts > 0) {
    *useraction_p = CPX_CALLBACK_SET;
  }

TERMINATE:

  return status;

} /* END myusercutcallback */

// ----------------- END Cut callbacks ------------------

MIPCplexWrapper::Status MIPCplexWrapper::convertStatus(int cplexStatus) {
  Status s = Status::UNKNOWN;
  /* Converting the status. */
  switch (cplexStatus) {
    case CPXMIP_OPTIMAL:
      s = Status::OPT;
      wrapAssert(dll_CPXgetsolnpoolnumsolns(_env, _lp) != 0, "Optimality reported but pool empty?",
                 false);
      break;
    case CPXMIP_INFEASIBLE:
      s = Status::UNSAT;
      break;
      //      case CPXMIP_OPTIMAL_INFEAS:
    case CPXMIP_INForUNBD:
      s = Status::UNSATorUNBND;
      break;
    case CPXMIP_SOL_LIM:
    case CPXMIP_NODE_LIM_FEAS:
    case CPXMIP_TIME_LIM_FEAS:
    case CPXMIP_FAIL_FEAS:
    case CPXMIP_MEM_LIM_FEAS:
    case CPXMIP_ABORT_FEAS:
    case CPXMIP_FAIL_FEAS_NO_TREE:
      s = Status::SAT;
      wrapAssert(dll_CPXgetsolnpoolnumsolns(_env, _lp) != 0, "Feasibility reported but pool empty?",
                 false);
      break;
    case CPXMIP_UNBOUNDED:
      s = Status::UNBND;
      break;
      //      case CPXMIP_ABORT_INFEAS:
    case CPXMIP_FAIL_INFEAS:
      s = Status::ERROR_STATUS;
      break;
    default:
      //      case CPXMIP_OPTIMAL_TOL:
      //      case CPXMIP_ABORT_RELAXATION_UNBOUNDED:
      if (dll_CPXgetsolnpoolnumsolns(_env, _lp) != 0) {
        s = Status::SAT;
      } else {
        s = Status::UNKNOWN;
      }
  }
  return s;
}

void msgfunction(void* handle, const char* msg_string) { cerr << msg_string << flush; }

void MIPCplexWrapper::solve() {  // Move into ancestor?

  /////////////// Last-minute solver options //////////////////
  // Before all manual params ???
  if (!_options->sReadParams.empty()) {
    _status = dll_CPXreadcopyparam(_env, _options->sReadParams.c_str());
    wrapAssert(_status == 0, "Failed to read CPLEX parameters.", false);
  }

  /* Turn on output to the screen */
  if (fVerbose) {
    CPXCHANNELptr chnl[4];
    dll_CPXgetchannels(_env, &chnl[0], &chnl[1], &chnl[2], &chnl[3]);
    for (int i = 0; i < 3; ++i) {
      _status = dll_CPXaddfuncdest(_env, chnl[i], nullptr, msgfunction);
    }
    //     status = dll_CPXsetintparam(env, CPXPARAM_ScreenOutput,
    //       fVerbose ? CPX_ON : CPX_OFF);  // also when flagIntermediate?  TODO
    //     wrapAssert(!status, "  CPLEX Warning: Failure to switch screen indicator.", false);
  }
  _status = dll_CPXsetintparam(_env, CPXPARAM_MIP_Display,
                               fVerbose ? 2 : 0);  // also when flagIntermediate?  TODO
  wrapAssert(_status == 0, "  CPLEX Warning: Failure to switch logging.", false);
  /// Make it wall time by default, 12.8
  //    status =  dll_CPXsetintparam (env, CPXPARAM_ClockType, 1);            // CPU time
  //    wrapAssert(!status, "  CPLEX Warning: Failure to measure CPU time.", false);
  _status = dll_CPXsetintparam(_env, CPX_PARAM_MIPCBREDLP, CPX_OFF);  // Access original model
  wrapAssert(_status == 0, "  CPLEX Warning: Failure to set access original model in callbacks.",
             false);
  if (!_options->sExportModel.empty()) {
    _status = dll_CPXwriteprob(_env, _lp, _options->sExportModel.c_str(), nullptr);
    wrapAssert(_status == 0, "Failed to write LP to disk.", false);
  }

  /// TODO
  //     if(all_solutions && obj.getImpl()) {
  //       IloNum lastObjVal = (obj.getSense() == IloObjective::Minimize ) ?
  //       _ilocplex->use(SolutionCallback(_iloenv, lastObjVal, *this));
  // Turn off CPLEX logging

  if (_options->nThreads > 0) {
    _status = dll_CPXsetintparam(_env, CPXPARAM_Threads, _options->nThreads);
    wrapAssert(_status == 0, "Failed to set CPXPARAM_Threads.", false);
  }

  if (_options->nTimeout > 0) {
    _status = dll_CPXsetdblparam(_env, CPXPARAM_TimeLimit,
                                 static_cast<double>(_options->nTimeout) / 1000.0);
    wrapAssert(_status == 0, "Failed to set CPXPARAM_TimeLimit.", false);
  }
  if (_options->nSolLimit > 0) {
    _status = dll_CPXsetintparam(_env, CPXPARAM_MIP_Limits_Solutions,
                                 static_cast<CPXINT>(_options->nSolLimit));
    wrapAssert(_status == 0, "Failed to set CPXPARAM_MIP_Limits_Solutions.", false);
  }
  if (_options->nSeed >= 0) {
    _status = dll_CPXsetintparam(_env, CPXPARAM_RandomSeed, _options->nSeed);
    wrapAssert(_status == 0, "Failed to set CPXPARAM_RandomSeed.", false);
  }
  if (_options->nMIPFocus > 0) {
    _status = dll_CPXsetintparam(_env, CPXPARAM_Emphasis_MIP, _options->nMIPFocus);
    wrapAssert(_status == 0, "Failed to set CPXPARAM_Emphasis_MIP.", false);
  }

  if (_options->nWorkMemLimit > 0) {
    _status = dll_CPXsetintparam(_env, CPXPARAM_MIP_Strategy_File, 3);
    wrapAssert(_status == 0, "Failed to set CPXPARAM_MIP_Strategy_File.", false);
    _status = dll_CPXsetdblparam(_env, CPXPARAM_WorkMem,
                                 1024.0 * _options->nWorkMemLimit);  // MB in CPLEX
    wrapAssert(_status == 0, "Failed to set CPXPARAM_WorkMem.", false);
  }

  if (!_options->sNodefileDir.empty()) {
    _status = dll_CPXsetstrparam(_env, CPXPARAM_WorkDir, _options->sNodefileDir.c_str());
    wrapAssert(_status == 0, "Failed to set CPXPARAM_WorkDir.", false);
  }

  if (_options->absGap >= 0.0) {
    _status = dll_CPXsetdblparam(_env, CPXPARAM_MIP_Tolerances_AbsMIPGap, _options->absGap);
    wrapAssert(_status == 0, "Failed to set CPXPARAM_MIP_Tolerances_AbsMIPGap.", false);
  }
  if (_options->relGap >= 0.0) {
    _status = dll_CPXsetdblparam(_env, CPXPARAM_MIP_Tolerances_MIPGap, _options->relGap);
    wrapAssert(_status == 0, "Failed to set CPXPARAM_MIP_Tolerances_MIPGap.", false);
  }
  if (_options->intTol >= 0.0) {
    _status = dll_CPXsetdblparam(_env, CPXPARAM_MIP_Tolerances_Integrality, _options->intTol);
    wrapAssert(_status == 0, "Failed to set CPXPARAM_MIP_Tolerances_Integrality.", false);
  }

  //    status =  dll_CPXsetdblparam (_env, CPXPARAM_MIP_Tolerances_ObjDifference, objDiff);
  //    wrapAssert(!status, "Failed to set CPXPARAM_MIP_Tolerances_ObjDifference.", false);

  /// Solution callback
  output.nCols = static_cast<int>(colObj.size());
  _x.resize(output.nCols);
  output.x = _x.data();
  if (_options->flagIntermediate && (cbui.solcbfn != nullptr)) {
    _status = dll_CPXsetinfocallbackfunc(_env, solcallback, &cbui);
    wrapAssert(_status == 0, "Failed to set solution callback", false);
  }
  if (cbui.cutcbfn != nullptr) {
    assert(cbui.cutMask & (MaskConsType_Usercut | MaskConsType_Lazy));
    if ((cbui.cutMask & MaskConsType_Usercut) != 0) {
      // For user cuts, needs to keep some info after presolve
      if (fVerbose) {
        cerr << "  MIPCplexWrapper: user cut callback enabled, setting params" << endl;
      }
      CUTINFO usercutinfo;  // THREADS?  TODO
      usercutinfo.info = &cbui;
      /* Init information on the node objval for the user cut callback */
      initnodeobjvalinfo(this, _env, _lp, &usercutinfo);
      /* Assure linear mappings between the presolved and original
          models */
      _status = dll_CPXsetintparam(_env, CPXPARAM_Preprocessing_Linear, 0);
      wrapAssert(_status == 0, "CPLEX: setting prepro_linear");
      /* Turn on traditional search for use with control callbacks */
      _status = dll_CPXsetintparam(_env, CPXPARAM_MIP_Strategy_Search, CPX_MIPSEARCH_TRADITIONAL);
      wrapAssert(_status == 0, "CPLEX: setting traditional search");
      /* Let MIP callbacks work on the original model */
      _status = dll_CPXsetintparam(_env, CPXPARAM_MIP_Strategy_CallbackReducedLP, CPX_OFF);
      wrapAssert(_status == 0, "CPLEX: setting callbacks to work on orig model");
      /// And
      /* Set up to use MIP usercut callback */

      _status = dll_CPXsetusercutcallbackfunc(_env, myusercutcallback, &usercutinfo);
      wrapAssert(_status == 0, "CPLEX: setting user cut callback");
    }
    if ((cbui.cutMask & MaskConsType_Lazy) != 0) {
      if (fVerbose) {
        cerr << "  MIPCplexWrapper: lazy cut callback enabled, setting params" << endl;
      }
      CUTINFO lazyconinfo;
      lazyconinfo.info = &cbui;
      /* Init information on the node objval for the user cut callback.
          No need to initialize the information on the node objval,
          for the lazy constraint callback, because those information are
          used only in the user cut callback. */
      initnodeobjvalinfo(this, _env, _lp, &lazyconinfo);
      /* Assure linear mappings between the presolved and original
          models */
      _status = dll_CPXsetintparam(_env, CPXPARAM_Preprocessing_Linear, 0);
      wrapAssert(_status == 0, "CPLEX: setting prepro_linear");
      /* Turn on traditional search for use with control callbacks */
      //       _status = dll_CPXsetintparam (_env, CPXPARAM_MIP_Strategy_Search,
      //                                 CPX_MIPSEARCH_TRADITIONAL);
      //       wrapAssert ( !_status, "CPLEX: setting traditional search" );
      /* Let MIP callbacks work on the original model */
      _status = dll_CPXsetintparam(_env, CPXPARAM_MIP_Strategy_CallbackReducedLP, CPX_OFF);
      wrapAssert(_status == 0, "CPLEX: setting callbacks to work on orig model");
      /* Set up to use MIP lazyconstraint callback. The callback funtion
       * registered is the same, but the data will be different. */

      _status = dll_CPXsetlazyconstraintcallbackfunc(_env, myusercutcallback, &lazyconinfo);
      wrapAssert(_status == 0, "CPLEX: setting lazy cut callback");
    }
  }

  // Process extra flags
  for (auto& it : _options->extraParams) {
    int param;
    int param_type;
    auto name = it.first.substr(8);
    _status = dll_CPXgetparamnum(_env, name.c_str(), &param);
    wrapAssert(_status == 0, "CPLEX: could not find parameter " + name);
    _status = dll_CPXgetparamtype(_env, param, &param_type);
    wrapAssert(_status == 0, "CPLEX: could not get type for parameter " + name);
    switch (param_type) {
      case CPX_PARAMTYPE_INT:
        _status = dll_CPXsetintparam(_env, param, stoi(it.second));
        wrapAssert(_status == 0, "CPLEX: failed to set parameter " + name);
        break;
      case CPX_PARAMTYPE_LONG:
        _status = dll_CPXsetlongparam(_env, param, stoll(it.second));
        wrapAssert(_status == 0, "CPLEX: failed to set parameter " + name);
        break;
      case CPX_PARAMTYPE_DOUBLE:
        _status = dll_CPXsetdblparam(_env, param, stod(it.second));
        wrapAssert(_status == 0, "CPLEX: failed to set parameter " + name);
        break;
      case CPX_PARAMTYPE_STRING:
        _status = dll_CPXsetstrparam(_env, param, it.second.c_str());
        wrapAssert(_status == 0, "CPLEX: failed to set parameter " + name);
        break;
      default:
        wrapAssert(false, "CPLEX: unknown type for parameter " + name);
        break;
    }
  }

  /// after all modifs
  if (!_options->sWriteParams.empty()) {
    _status = dll_CPXwriteparam(_env, _options->sWriteParams.c_str());
    wrapAssert(_status == 0, "Failed to write CPLEX parameters.", false);
  }

  // _status = dll_CPXgettime (_env, &output.dCPUTime);
  // wrapAssert(!_status, "Failed to get time stamp.", false);
  cbui.pOutput->dWallTime0 = output.dWallTime0 = std::chrono::steady_clock::now();
  cbui.pOutput->cCPUTime0 = std::clock();

  /* Optimize the problem and obtain solution. */
  _status = dll_CPXmipopt(_env, _lp);
  wrapAssert(_status == 0, "Failed to optimize MIP.");

  output.dWallTime =
      std::chrono::duration<double>(std::chrono::steady_clock::now() - output.dWallTime0).count();
  auto tmNow = std::clock();
  // _status = dll_CPXgettime (_env, &tmNow);   Buggy in 12.7.1.0
  wrapAssert(_status == 0, "Failed to get time stamp.", false);
  output.dCPUTime =
      static_cast<double>(tmNow - cbui.pOutput->cCPUTime0) / static_cast<double>(CLOCKS_PER_SEC);

  int solstat = dll_CPXgetstat(_env, _lp);
  output.status = convertStatus(solstat);
  output.statusName = dll_CPXgetstatstring(_env, solstat, _cplexStatusBuffer);

  /// Continuing to fill the output object:
  if (Status::OPT == output.status || Status::SAT == output.status) {
    _status = dll_CPXgetobjval(_env, _lp, &output.objVal);
    wrapAssert(_status == 0, "No MIP objective value available.");

    /* The size of the problem should be obtained by asking CPLEX what
        the actual size is, rather than using what was passed to CPXcopylp.
        cur_numrows and cur_numcols store the current number of rows and
        columns, respectively.  */   // ?????????????? TODO

    //    int cur_numrows = dll_CPXgetnumrows (_env, lp);
    int cur_numcols = dll_CPXgetnumcols(_env, _lp);
    assert(cur_numcols == colObj.size());

    _x.resize(cur_numcols);
    output.x = _x.data();
    _status = dll_CPXgetx(_env, _lp, _x.data(), 0, cur_numcols - 1);
    wrapAssert(_status == 0, "Failed to get variable values.");
    if (cbui.solcbfn != nullptr /*&& (!_options->flagIntermediate || !cbui.printed)*/) {
      cbui.solcbfn(output, cbui.psi);
    }
  }
  output.bestBound = 1e308;
  _status = dll_CPXgetbestobjval(_env, _lp, &output.bestBound);
  wrapAssert(_status == 0, "Failed to get the best bound.", false);
  output.nNodes = dll_CPXgetnodecnt(_env, _lp);
  output.nOpenNodes = dll_CPXgetnodeleftcnt(_env, _lp);
}

void MIPCplexWrapper::setObjSense(int s) {
  _status = dll_CPXchgobjsen(_env, _lp, -s);  // +1 for min in CPLEX
  wrapAssert(_status == 0, "Failed to set obj sense.");
}
