// * -*- 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/exception.hh>
#include <minizinc/file_utils.hh>
#include <minizinc/utils_savestream.hh>

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

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

#include <minizinc/solvers/MIP/MIP_gurobi_wrap.hh>
#include <minizinc/utils.hh>

using namespace std;

string MIPGurobiWrapper::getDescription(FactoryOptions& factoryOpt,
                                        MiniZinc::SolverInstanceBase::Options* opt) {
  ostringstream oss;
  oss << "MIP wrapper for Gurobi library " << getVersion(factoryOpt, nullptr);
  oss << ".  Compiled  " __DATE__ "  " __TIME__;
  return oss.str();
}

string MIPGurobiWrapper::getVersion(FactoryOptions& factoryOpt,
                                    MiniZinc::SolverInstanceBase::Options* opt) {
  ostringstream oss;
  MIPGurobiWrapper mgw(factoryOpt, nullptr);  // to avoid opening the env
  try {
    mgw.checkDLL();
    int major;
    int minor;
    int technical;
    mgw.dll_GRBversion(&major, &minor, &technical);
    oss << major << '.' << minor << '.' << technical;
    return oss.str();
  } catch (MiniZinc::Error&) {
    return "<unknown version>";
  }
}

vector<string> MIPGurobiWrapper::getRequiredFlags(FactoryOptions& f) {
  FactoryOptions factoryOpt;
  MIPGurobiWrapper mgw(factoryOpt, nullptr);
  try {
    mgw.checkDLL();
    return {};
  } catch (MiniZinc::Error&) {
    return {"--gurobi-dll"};
  }
}

vector<string> MIPGurobiWrapper::getFactoryFlags() { return {"--gurobi-dll"}; }

string MIPGurobiWrapper::getId() { return "gurobi"; }

string MIPGurobiWrapper::getName() { return "Gurobi"; }

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

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

vector<string> gurobi_dlls() {
  const vector<string> versions = {
      "1303", "1302", "1301",  // Potential future versions which should load correctly
      "1300", "1203", "1202", "1201", "1200", "1103", "1102", "1101", "1100",
      "1003", "1002", "1001", "1000", "952",  "951",  "950",  "912",  "911",
      "910",  "903",  "902",  "901",  "900",  "811",  "810",  "801",  "800",
      "752",  "751",  "750",  "702",  "701",  "700",  "652",  "651",  "650"};
  vector<string> dlls;
  string lastMajorVersion;
  for (const auto& version : versions) {
    string majorVersion = version.substr(0, version.length() - 1);
    if (majorVersion != lastMajorVersion) {
      dlls.push_back("gurobi" + majorVersion);
      lastMajorVersion = majorVersion;
    }
#ifdef _WIN32
    dlls.push_back("C:\\gurobi" + version + "\\win64\\bin\\gurobi" + majorVersion + ".dll");
#elif __APPLE__
    dlls.push_back("/Library/gurobi" + version + "/mac64/lib/libgurobi" + majorVersion + ".dylib");
    dlls.push_back("/Library/gurobi" + version + "/macos_universal2/lib/libgurobi" + majorVersion +
                   ".dylib");
#else
    dlls.push_back("/opt/gurobi" + version + "/linux64/lib/libgurobi" + majorVersion + ".so");
#endif
  }

  return dlls;
}

void MIPGurobiWrapper::Options::printHelp(ostream& os) {
  os << "GUROBI MIP wrapper options:"
     << std::endl
     // -s                  print statistics
     //            << "  --readParam <file>  read GUROBI parameters from file
     //               << "--writeParam <file> write GUROBI parameters to file
     //               << "--tuneParam         instruct GUROBI to tune parameters instead of solving
     << "  -f\n    free search (default)" << std::endl
     << "  --fixed-search\n    fixed search (approximation of the model's one by branching "
        "priorities)"
     << std::endl
     << "  --uniform-search\n    'more fixed' search (all variables in the search anns get "
        "priority 1)"
     << std::endl
     << "  --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>, --solver-time\n"
        "    stop search after N milliseconds wall time"
     << std::endl
     << "  --solver-time-limit-feas <N>, --solver-tlf\n"
        "    stop search after N milliseconds wall time after the first feasible solution"
     << 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 node tree 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 GUROBI parameters from file" << std::endl
     << "  --writeParam <file>\n    write GUROBI parameters to file" << std::endl
     << "  --readConcurrentParam <fileN>\n"
        "    read GUROBI parameters from file. Several such commands provide the"
        "    parameter files for concurrent solves (applied after all other settings)"
     << std::endl
     //   << "  --tuneParam         instruct GUROBI to tune parameters instead of solving   NOT
     //   IMPL"

     << "\n  --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
     << "  --feasTol <n>\n   primal feasibility tolerance. Default 1e-8" << std::endl
     << "  --intTol <n>\n    integrality tolerance for a variable. Gurobi recommends at least "
        "feasTol. Default 1e-8"
     << std::endl
     //   << "  --objDiff <n>       objective function discretization. Default 1.0" << std::endl

     << "  --nonConvex <n>\n    non-convexity. -1: solver default, 0: none, 1: if presolved, 2: "
        "global. Default value 2."
     << std::endl

     << "\n  --gurobi-dll <file> or <basename>\n    Gurobi DLL, or base name, such as gurobi75, "
        "when using plugin. Default range tried: "
     << gurobi_dlls().front() << " .. " << gurobi_dlls().back() << std::endl
     << std::endl;
}

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

bool MIPGurobiWrapper::Options::processOption(int& i, std::vector<std::string>& argv,
                                              const std::string& workingDir) {
  MiniZinc::CLOParser cop(i, argv);
  std::string buf;
  if (cop.get("-i")) {
    flagIntermediate = true;
  } else if (string(argv[i]) == "-f" ||
             string(argv[i]) == "--free-search") {   // NOLINT: Allow repeated empty if
  } else if (string(argv[i]) == "--fixed-search") {  // NOLINT: Allow repeated empty if
    nFreeSearch = MIPGurobiWrapper::SearchType::FIXED_SEARCH;
  } else if (string(argv[i]) == "--uniform-search") {  // NOLINT: Allow repeated empty if
    nFreeSearch = MIPGurobiWrapper::SearchType::UNIFORM_SEARCH;
  } else if (cop.get("--mipfocus --mipFocus --MIPFocus --MIPfocus",
                     &nMIPFocus)) {  // NOLINT: Allow repeated empty if
  } else if (cop.get("--writeModel --exportModel --writemodel --exportmodel", &buf)) {
    sExportModel = MiniZinc::FileUtils::file_path(buf, workingDir);
  } else if (cop.get("-p --parallel", &nThreads)) {  // NOLINT: Allow repeated empty if
  } else if (cop.get("--solver-time-limit --solver-time",
                     &nTimeout1000)) {  // NOLINT: Allow repeated empty if
  } else if (cop.get("--solver-time-limit-feas --solver-tlf",
                     &nTimeoutFeas1000)) {                 // 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 --readParams", &buf)) {
    sReadParams = MiniZinc::FileUtils::file_path(buf, workingDir);
  } else if (cop.get("--writeParam --writeParams", &buf)) {
    sWriteParams = MiniZinc::FileUtils::file_path(buf, workingDir);
  } else if (cop.get("--readConcurrentParam --readConcurrentParams", &buf)) {
    sConcurrentParamFiles.push_back(MiniZinc::FileUtils::file_path(buf, 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("--feasTol", &feasTol)) {  // NOLINT: Allow repeated empty if
  } else if (cop.get("--intTol", &intTol)) {    // NOLINT: Allow repeated empty if
  } else if (cop.get("--nonConvex --nonconvex --NonConvex",
                     &nonConvex)) {  // NOLINT: Allow repeated empty if
    //   } else if ( cop.get( "--objDiff", &objDiff ) ) {
  } else {
    return false;
  }
  return true;
}

void MIPGurobiWrapper::wrapAssert(bool cond, const string& msg, bool fTerm) {
  if (!cond) {
    _gurobiBuffer = "[NO ERROR STRING GIVEN]";
    if (_error != 0) {
      _gurobiBuffer = dll_GRBgeterrormsg(_env);
    }
    string msgAll =
        ("  MIPGurobiWrapper runtime error:  " + _gurobiBuffer + "\nMessage from caller: " + msg);
    if (fTerm) {
      throw MiniZinc::Error(msgAll);
    }
    cerr << msgAll << "\nGurobi error code: " << _error << endl;
  }
}

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

void MIPGurobiWrapper::checkDLL() {
  _gurobiDll = nullptr;
  if (!_factoryOptions.gurobiDll.empty()) {
    _gurobiDll = dll_open(_factoryOptions.gurobiDll.c_str());
  } else {
    for (const auto& s : gurobi_dlls()) {
      _gurobiDll = dll_open(s.c_str());
      if (nullptr != _gurobiDll) {
        break;
      }
    }
  }

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

  *(void**)(&dll_GRBversion) = dll_sym(_gurobiDll, "GRBversion");
  *(void**)(&dll_GRBaddconstr) = dll_sym(_gurobiDll, "GRBaddconstr");
  *(void**)(&dll_GRBaddgenconstrMin) = dll_sym(_gurobiDll, "GRBaddgenconstrMin");
  *(void**)(&dll_GRBaddqconstr) = dll_sym(_gurobiDll, "GRBaddqconstr");
  *(void**)(&dll_GRBaddgenconstrIndicator) = dll_sym(_gurobiDll, "GRBaddgenconstrIndicator");
  *(void**)(&dll_GRBaddvars) = dll_sym(_gurobiDll, "GRBaddvars");
  *(void**)(&dll_GRBcbcut) = dll_sym(_gurobiDll, "GRBcbcut");
  *(void**)(&dll_GRBcbget) = dll_sym(_gurobiDll, "GRBcbget");
  *(void**)(&dll_GRBcblazy) = dll_sym(_gurobiDll, "GRBcblazy");
  *(void**)(&dll_GRBfreeenv) = dll_sym(_gurobiDll, "GRBfreeenv");
  *(void**)(&dll_GRBfreemodel) = dll_sym(_gurobiDll, "GRBfreemodel");
  *(void**)(&dll_GRBgetdblattr) = dll_sym(_gurobiDll, "GRBgetdblattr");
  *(void**)(&dll_GRBgetdblattrarray) = dll_sym(_gurobiDll, "GRBgetdblattrarray");
  *(void**)(&dll_GRBgetenv) = dll_sym(_gurobiDll, "GRBgetenv");
  *(void**)(&dll_GRBgeterrormsg) = dll_sym(_gurobiDll, "GRBgeterrormsg");
  *(void**)(&dll_GRBgetintattr) = dll_sym(_gurobiDll, "GRBgetintattr");
  *(void**)(&dll_GRBloadenv) = try_dll_sym(_gurobiDll, "GRBloadenv");
  if (dll_GRBloadenv == nullptr) {
    // Workaround for missing symbol in 12.0.0
    *(void**)(&dll_GRBloadenvinternal) = dll_sym(_gurobiDll, "GRBloadenvinternal");
  }
  *(void**)(&dll_GRBgetconcurrentenv) = dll_sym(_gurobiDll, "GRBgetconcurrentenv");
  *(void**)(&dll_GRBnewmodel) = dll_sym(_gurobiDll, "GRBnewmodel");
  *(void**)(&dll_GRBoptimize) = dll_sym(_gurobiDll, "GRBoptimize");
  *(void**)(&dll_GRBreadparams) = dll_sym(_gurobiDll, "GRBreadparams");
  *(void**)(&dll_GRBsetcallbackfunc) = dll_sym(_gurobiDll, "GRBsetcallbackfunc");
  *(void**)(&dll_GRBsetdblparam) = dll_sym(_gurobiDll, "GRBsetdblparam");
  *(void**)(&dll_GRBsetintattr) = dll_sym(_gurobiDll, "GRBsetintattr");
  *(void**)(&dll_GRBsetintattrlist) = dll_sym(_gurobiDll, "GRBsetintattrlist");
  *(void**)(&dll_GRBsetdblattrelement) = dll_sym(_gurobiDll, "GRBsetdblattrelement");
  *(void**)(&dll_GRBsetdblattrlist) = dll_sym(_gurobiDll, "GRBsetdblattrlist");
  *(void**)(&dll_GRBsetobjectiven) = dll_sym(_gurobiDll, "GRBsetobjectiven");
  *(void**)(&dll_GRBsetintparam) = dll_sym(_gurobiDll, "GRBsetintparam");
  *(void**)(&dll_GRBsetstrparam) = dll_sym(_gurobiDll, "GRBsetstrparam");
  *(void**)(&dll_GRBterminate) = dll_sym(_gurobiDll, "GRBterminate");
  *(void**)(&dll_GRBupdatemodel) = dll_sym(_gurobiDll, "GRBupdatemodel");
  *(void**)(&dll_GRBwrite) = dll_sym(_gurobiDll, "GRBwrite");
  *(void**)(&dll_GRBwriteparams) = dll_sym(_gurobiDll, "GRBwriteparams");
  *(void**)(&dll_GRBemptyenv) = try_dll_sym(_gurobiDll, "GRBemptyenv");
  if (dll_GRBemptyenv == nullptr) {
    // Workaround for missing symbol in 12.0.0
    *(void**)(&dll_GRBemptyenvinternal) = dll_sym(_gurobiDll, "GRBemptyenvinternal");
  }
  *(void**)(&dll_GRBgetnumparams) = dll_sym(_gurobiDll, "GRBgetnumparams");
  *(void**)(&dll_GRBgetparamname) = dll_sym(_gurobiDll, "GRBgetparamname");
  *(void**)(&dll_GRBgetparamtype) = dll_sym(_gurobiDll, "GRBgetparamtype");
  *(void**)(&dll_GRBgetintparaminfo) = dll_sym(_gurobiDll, "GRBgetintparaminfo");
  *(void**)(&dll_GRBgetdblparaminfo) = dll_sym(_gurobiDll, "GRBgetdblparaminfo");
  *(void**)(&dll_GRBgetstrparaminfo) = dll_sym(_gurobiDll, "GRBgetstrparaminfo");
}

void MIPGurobiWrapper::openGUROBI() {
  checkDLL();

  /* Initialize the GUROBI environment */
  {
    //   cout << "% " << flush;               // Gurobi 7.5.2 prints "Academic License..."
    MiniZinc::StreamRedir redirStdout(stdout, stderr);
    if (dll_GRBloadenv == nullptr) {
      // Workaround for missing symbol in 12.0.0
      _error = dll_GRBloadenvinternal(&_env, nullptr, 12, 0, 0);
    } else {
      _error = dll_GRBloadenv(&_env, nullptr);
    }
  }
  wrapAssert(_error == 0, "Could not open GUROBI environment.");
  _error = dll_GRBsetintparam(_env, "OutputFlag", 0);  // Switch off output
  //   _error = dll_GRBsetintparam(_env, "LogToConsole",
  //                            fVerbose ? 1 : 0);  // also when flagIntermediate?  TODO
  /* Create the problem. */
  _error =
      dll_GRBnewmodel(_env, &_model, "mzn_gurobi", 0, nullptr, nullptr, nullptr, nullptr, nullptr);
  wrapAssert(_model != nullptr, "Failed to create LP.");
}

void MIPGurobiWrapper::closeGUROBI() {
  /* Free model */

  // If not allocated, skip
  if (nullptr != _model) {
    /* Free up the problem as allocated by GRB_createprob, if necessary */
    dll_GRBfreemodel(_model);
    _model = nullptr;
  }

  /* Free environment */

  if (nullptr != _env) {
    dll_GRBfreeenv(_env);
  }
  /// and at last:
  //   MIPWrapper::cleanup();
  // dll_close(_gurobiDll);    // Is called too many times, disabling. 2019-05-06
}

std::vector<MiniZinc::SolverConfig::ExtraFlag> MIPGurobiWrapper::getExtraFlags(
    FactoryOptions& factoryOpt) {
  enum GurobiParamType { T_INT = 1, T_DOUBLE = 2, T_STRING = 3 };

  MIPGurobiWrapper mgw(factoryOpt, nullptr);
  GRBenv* env;
  try {
    mgw.checkDLL();
    if (mgw.dll_GRBemptyenv == nullptr) {
      // Workaround for missing symbol in 12.0.0
      mgw.dll_GRBemptyenvinternal(&env, 12, 0, 0);
    } else {
      mgw.dll_GRBemptyenv(&env);
    }
    int num_params = mgw.dll_GRBgetnumparams(env);
    std::vector<MiniZinc::SolverConfig::ExtraFlag> flags;
    flags.reserve(num_params);
    for (int i = 0; i < num_params; i++) {
      char* name;
      mgw.dll_GRBgetparamname(env, i, &name);
      std::string param_name(name);
      MiniZinc::SolverConfig::ExtraFlag::FlagType param_type;
      std::vector<std::string> param_range;
      std::string param_default;
      int type = mgw.dll_GRBgetparamtype(env, name);
      if (param_name == GRB_INT_PAR_THREADS || param_name == GRB_DBL_PAR_TIMELIMIT ||
          param_name == GRB_INT_PAR_SOLUTIONLIMIT || param_name == GRB_INT_PAR_SEED ||
          param_name == GRB_DBL_PAR_NODEFILESTART || param_name == GRB_STR_PAR_NODEFILEDIR ||
          param_name == GRB_DBL_PAR_MIPGAPABS || param_name == GRB_INT_PAR_MIPFOCUS ||
          param_name == GRB_DBL_PAR_MIPGAP || param_name == GRB_DBL_PAR_INTFEASTOL ||
          param_name == GRB_DBL_PAR_FEASIBILITYTOL ||
#ifdef GRB_INT_PAR_NONCONVEX
          param_name == GRB_INT_PAR_NONCONVEX ||
#endif
          param_name == GRB_INT_PAR_PRECRUSH || param_name == GRB_INT_PAR_LAZYCONSTRAINTS ||
          param_name == GRB_STR_PAR_DUMMY) {
        // These parameters are handled by us or are not useful
        continue;
      }
      switch (type) {
        case T_INT: {
          int current_value;
          int min_value;
          int max_value;
          int default_value;
          mgw.dll_GRBgetintparaminfo(env, name, &current_value, &min_value, &max_value,
                                     &default_value);
          param_type = MiniZinc::SolverConfig::ExtraFlag::FlagType::T_INT;
          param_range = {std::to_string(min_value), std::to_string(max_value)};
          param_default = std::to_string(default_value);
          break;
        }
        case T_DOUBLE: {
          double current_value;
          double min_value;
          double max_value;
          double default_value;
          mgw.dll_GRBgetdblparaminfo(env, name, &current_value, &min_value, &max_value,
                                     &default_value);
          param_type = MiniZinc::SolverConfig::ExtraFlag::FlagType::T_FLOAT;
          param_range = {std::to_string(min_value), std::to_string(max_value)};
          param_default = std::to_string(default_value);
          break;
        }
        case T_STRING: {
          char current_value[GRB_MAX_STRLEN];
          char default_value[GRB_MAX_STRLEN];
          mgw.dll_GRBgetstrparaminfo(env, name, current_value, default_value);
          param_type = MiniZinc::SolverConfig::ExtraFlag::FlagType::T_STRING;
          param_default = default_value;
          break;
        }
        default:
          break;
      }
      flags.emplace_back("--gurobi-" + param_name, param_name, param_type, param_range,
                         param_default);
    }
    return flags;
  } catch (MiniZinc::Error&) {
    return {};
  }
  return {};
}

void MIPGurobiWrapper::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] = GRB_CONTINUOUS;
        break;
      case INT:
        ctype[i] = GRB_INTEGER;
        break;
      case BINARY:
        ctype[i] = GRB_BINARY;
        break;
      default:
        throw MiniZinc::InternalError("  MIPWrapper: unknown variable type");
    }
  }
  _error = dll_GRBaddvars(_model, static_cast<int>(n), 0, nullptr, nullptr, nullptr, obj, lb, ub,
                          ctype.data(), pcNames.data());
  wrapAssert(_error == 0, "Failed to declare variables.");
  _error = dll_GRBupdatemodel(_model);
  wrapAssert(_error == 0, "Failed to update model.");
}

static char get_grb_sense(MIPWrapper::LinConType s) {
  switch (s) {
    case MIPWrapper::LQ:
      return GRB_LESS_EQUAL;
    case MIPWrapper::EQ:
      return GRB_EQUAL;
    case MIPWrapper::GQ:
      return GRB_GREATER_EQUAL;
    default:
      throw MiniZinc::InternalError("  MIPGurobiWrapper: unknown constraint sense");
  }
}

void MIPGurobiWrapper::addRow(int nnz, int* rmatind, double* rmatval, MIPWrapper::LinConType sense,
                              double rhs, int mask, const string& rowName) {
  //// Make sure in order to notice the indices of lazy constr:
  ++nRows;
  /// Convert var types:
  char ssense = get_grb_sense(sense);
  const char* pRName = rowName.c_str();
  _error = dll_GRBaddconstr(_model, nnz, rmatind, rmatval, ssense, rhs, pRName);
  wrapAssert(_error == 0, "Failed to add constraint.");
  int nLazyAttr = 0;
  const bool fUser = (MaskConsType_Usercut & mask) != 0;
  const bool fLazy = (MaskConsType_Lazy & mask) != 0;
  /// Gurobi 6.5.2 has lazyness 1-3.
  if (fUser) {
    if (fLazy) {
      nLazyAttr = 2;  // just active lazy
    } else {
      nLazyAttr = 3;  // even LP-active
    }
  } else if (fLazy) {
    nLazyAttr = 1;  // very lazy
  }
  if (nLazyAttr != 0) {
    nLazyIdx.push_back(nRows - 1);
    nLazyValue.push_back(nLazyAttr);
  }
}

void MIPGurobiWrapper::addIndicatorConstraint(int iBVar, int bVal, int nnz, int* rmatind,
                                              double* rmatval, MIPWrapper::LinConType sense,
                                              double rhs, const string& rowName) {
  wrapAssert(0 <= bVal && 1 >= bVal, "Gurobi: addIndicatorConstraint: bVal not 0/1");
  //// Make sure in order to notice the indices of lazy constr: also here?   TODO
  ++nRows;
  char ssense = get_grb_sense(sense);
  _error = dll_GRBaddgenconstrIndicator(_model, rowName.c_str(), iBVar, bVal, nnz, rmatind, rmatval,
                                        ssense, rhs);
  wrapAssert(_error == 0, "Failed to add indicator constraint.");
}

void MIPGurobiWrapper::addMinimum(int iResultVar, int nnz, int* ind, const std::string& rowName) {
  _error = dll_GRBaddgenconstrMin(_model, rowName.c_str(), iResultVar, nnz, (const int*)ind,
                                  GRB_INFINITY);
  wrapAssert(_error == 0, "Failed: GRBaddgenconstrMin.");
}

void MIPGurobiWrapper::addTimes(int x, int y, int z, const string& rowName) {
  /// As x*y - z == 0
  double zCoef = -1.0;
  double xyCoef = 1.0;
  _error =
      dll_GRBaddqconstr(_model, 1, &z, &zCoef, 1, &x, &y, &xyCoef, GRB_EQUAL, 0.0, rowName.c_str());
  /// Gurobi 9.0.1 says we cannot have GRB_EQUAL but seems to work.
  wrapAssert(_error == 0, "Failed: GRBaddqconstr.");
}

bool MIPGurobiWrapper::addSearch(const std::vector<VarId>& vars, const std::vector<int>& pri) {
  assert(vars.size() == pri.size());
  static_assert(sizeof(VarId) == sizeof(int), "VarId should be (u)int currently");
  _error = dll_GRBsetintattrlist(_model, "BranchPriority", static_cast<int>(vars.size()),
                                 (int*)vars.data(), (int*)pri.data());
  wrapAssert(_error == 0, "Failed to add branching priorities");
  return true;
}

int MIPGurobiWrapper::getFreeSearch() { return _options->nFreeSearch; }

bool MIPGurobiWrapper::addWarmStart(const std::vector<VarId>& vars,
                                    const std::vector<double>& vals) {
  assert(vars.size() == vals.size());
  static_assert(sizeof(VarId) == sizeof(int), "VarId should be (u)int currently");
  // _error = GRBsetdblattrelement(_model, "Start", 0, 1.0);
  _error = dll_GRBsetdblattrlist(_model, "Start", static_cast<int>(vars.size()), (int*)vars.data(),
                                 (double*)vals.data());
  wrapAssert(_error == 0, "Failed to add warm start");
  return true;
}

bool MIPGurobiWrapper::defineMultipleObjectives(const MultipleObjectives& mo) {
  setObjSense(1);  // Maximize
  for (int iobj = 0; iobj < mo.size(); ++iobj) {
    const auto& obj = mo.getObjectives()[iobj];
    int objvar = obj.getVariable();
    double coef = 1.0;
    _error = dll_GRBsetobjectiven(_model, iobj, static_cast<int>(mo.size()) - iobj, obj.getWeight(),
                                  0.0, 0.0, nullptr, 0.0, 1, &objvar, &coef);
    wrapAssert(_error == 0, "Failed to set objective " + std::to_string(iobj));
  }
  return true;
}

void MIPGurobiWrapper::setVarBounds(int iVar, double lb, double ub) {
  wrapAssert(lb <= ub, "mzn-gurobi: setVarBounds: lb>ub");
  _error = dll_GRBsetdblattrelement(_model, GRB_DBL_ATTR_LB, iVar, lb);
  wrapAssert(_error == 0, "mzn-gurobi: failed to set var lb.");
  _error = dll_GRBsetdblattrelement(_model, GRB_DBL_ATTR_UB, iVar, ub);
  wrapAssert(_error == 0, "mzn-gurobi: failed to set var ub.");
}

void MIPGurobiWrapper::setVarLB(int iVar, double lb) {
  _error = dll_GRBsetdblattrelement(_model, GRB_DBL_ATTR_LB, iVar, lb);
  wrapAssert(_error == 0, "mzn-gurobi: failed to set var lb.");
}

void MIPGurobiWrapper::setVarUB(int iVar, double ub) {
  _error = dll_GRBsetdblattrelement(_model, GRB_DBL_ATTR_UB, iVar, ub);
  wrapAssert(_error == 0, "mzn-gurobi: failed to set var ub.");
}

/// SolutionCallback ------------------------------------------------------------------------
/// Gurobi ensures thread-safety
static int __stdcall solcallback(GRBmodel* model, void* cbdata, int where, void* usrdata) {
  auto* info = (MIPWrapper::CBUserInfo*)usrdata;
  auto* gw = static_cast<MIPGurobiWrapper*>(info->wrapper);

  double nodecnt = 0.0;
  double actnodes = 0.0;
  double objVal = 0.0;
  int solcnt = 0;
  int newincumbent = 0;

  if (GRB_CB_MIP == where) {
    /* General MIP callback */
    gw->dll_GRBcbget(cbdata, where, GRB_CB_MIP_OBJBND, &info->pOutput->bestBound);
    gw->dll_GRBcbget(cbdata, where, GRB_CB_MIP_NODLFT, &actnodes);
    info->pOutput->nOpenNodes = static_cast<int>(actnodes);
    /// Check time after the 1st feas
    if (-1e100 != info->nTime1Feas) {
      double tNow;
      gw->dll_GRBcbget(cbdata, where, GRB_CB_RUNTIME, (void*)&tNow);
      if (tNow - info->nTime1Feas >= info->nTimeoutFeas) {
        gw->dll_GRBterminate(model);
      }
    }
  } else if (GRB_CB_MESSAGE == where) {
    /* Message callback */
    if (info->fVerb) {
      char* msg;
      gw->dll_GRBcbget(cbdata, where, GRB_CB_MSG_STRING, static_cast<void*>(&msg));
      cerr << msg << flush;
    }
  } else if (GRB_CB_MIPSOL == where) {
    /* MIP solution callback */
    gw->dll_GRBcbget(cbdata, where, GRB_CB_MIPSOL_NODCNT, &nodecnt);
    info->pOutput->nNodes = static_cast<int>(nodecnt);
    gw->dll_GRBcbget(cbdata, where, GRB_CB_MIPSOL_OBJ, &objVal);
    gw->dll_GRBcbget(cbdata, where, GRB_CB_MIPSOL_SOLCNT, &solcnt);

    if (fabs(info->pOutput->objVal - objVal) > 1e-12 * (1.0 + fabs(objVal))) {
      newincumbent = 1;
      // Not confirmed yet, see lazy cuts
      //      info->pOutput->objVal = objVal;
      //      info->pOutput->status = MIPWrapper::SAT;
      //      info->pOutput->statusName = "feasible from a callback";
    }
    if (newincumbent != 0) {
      assert(info->pOutput->x);
      gw->dll_GRBcbget(cbdata, where, GRB_CB_MIPSOL_SOL, (void*)info->pOutput->x);

      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;
    }

    /// Callback for lazy cuts
    /// Before printing
    if ((info->cutcbfn != nullptr) && ((info->cutMask & MIPWrapper::MaskConsType_Lazy) != 0)) {
      MIPWrapper::CutInput cutInput;
      cerr << "  GRB: GRB_CB_MIPSOL (" << objVal << ") -> cut callback " << endl;
      info->cutcbfn(*info->pOutput, cutInput, info->psi, true);
      for (auto& cd : cutInput) {
        //         assert( cd.mask & MIPWrapper::MaskConsType_Lazy );
        if ((cd.mask & MIPWrapper::MaskConsType_Lazy) != 0) {  // take only lazy constr generators
          int _error =
              gw->dll_GRBcblazy(cbdata, static_cast<int>(cd.rmatind.size()), cd.rmatind.data(),
                                cd.rmatval.data(), get_grb_sense(cd.sense), cd.rhs);
          if (_error != 0) {
            cerr << "  GRB_wrapper: failed to add lazy cut. " << endl;
          } else {
            newincumbent = -1;
          }
          //             info->pOutput->objVal = 1e100;  // to mark that we can get a new incumbent
          //             which should be printed
        }
      }
    }
    if (solcnt >= 0 /*This is solution number for Gurobi*/ && newincumbent >= 0) {
      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 (newincumbent > 0) {
      info->pOutput->dCPUTime = double(std::clock() - info->pOutput->cCPUTime0) / CLOCKS_PER_SEC;

      /// Set time for the 1st feas
      if (0 <= info->nTimeoutFeas && -1e100 == info->nTime1Feas) {
        gw->dll_GRBcbget(cbdata, where, GRB_CB_RUNTIME, (void*)&info->nTime1Feas);
      }

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

      if (0 == info->nTimeoutFeas) {
        gw->dll_GRBterminate(model);  // Straight after feas
      }
    }
  } else if (GRB_CB_MIPNODE == where) {
    int status;
    gw->dll_GRBcbget(cbdata, where, GRB_CB_MIPNODE_STATUS, &status);
    if (status == GRB_OPTIMAL && (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);
      //       dll_GRBcbget(cbdata, where, GRB_CB_MIPNODE_RELOBJ, outpRlx.objVal);
      gw->dll_GRBcbget(cbdata, where, GRB_CB_MIPNODE_REL, (void*)outpRlx.x);
      //       cerr << "  GRB: GRB_CB_MIPNODE -> cut callback " << endl;
      MIPWrapper::CutInput cutInput;
      info->cutcbfn(outpRlx, cutInput, info->psi, false);
      //       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");
        }
        if ((cd.mask & MIPWrapper::MaskConsType_Usercut) != 0) {
          int _error =
              gw->dll_GRBcbcut(cbdata, static_cast<int>(cd.rmatind.size()), cd.rmatind.data(),
                               cd.rmatval.data(), get_grb_sense(cd.sense), cd.rhs);
          if (_error != 0) {
            cerr << "  GRB_wrapper: failed to add user cut. " << endl;
          }
        }
        if ((cd.mask & MIPWrapper::MaskConsType_Lazy) != 0) {
          int _error =
              gw->dll_GRBcblazy(cbdata, static_cast<int>(cd.rmatind.size()), cd.rmatind.data(),
                                cd.rmatval.data(), get_grb_sense(cd.sense), cd.rhs);
          if (_error != 0) {
            cerr << "  GRB_wrapper: failed to add lazy cut. " << endl;
          }
        }
      }
    }
  }
  return 0;
} /* END logcallback */
// end SolutionCallback ---------------------------------------------------------------------

MIPGurobiWrapper::Status MIPGurobiWrapper::convertStatus(int gurobiStatus) {
  Status s = Status::UNKNOWN;
  ostringstream oss;
  /* Converting the status. */
  if (gurobiStatus == GRB_OPTIMAL) {
    s = Status::OPT;
    oss << "Optimal";
  } else if (gurobiStatus == GRB_INF_OR_UNBD) {
    s = Status::UNSATorUNBND;
    oss << "Infeasible or unbounded";
  } else if (gurobiStatus == GRB_INFEASIBLE) {
    s = Status::UNSAT;
    oss << "Infeasible";
  } else if (gurobiStatus == GRB_UNBOUNDED) {
    oss << "Unbounded";
    s = Status::UNBND;
  } else {
    int solcount = 0;
    _error = dll_GRBgetintattr(_model, "SolCount", &solcount);
    wrapAssert(_error == 0, "  Failure to access solution count.", false);
    if (solcount != 0) {
      s = Status::SAT;
    }
    oss << "Gurobi stopped with status " << gurobiStatus;
  }
  output.statusName = _gurobiStatusBuffer = oss.str();
  return s;
}

void MIPGurobiWrapper::solve() {        // Move into ancestor?
  _error = dll_GRBupdatemodel(_model);  // for model export
  wrapAssert(_error == 0, "Failed to update model.");

  /// ADDING LAZY CONSTRAINTS IF ANY
  if (!nLazyIdx.empty()) {
    assert(nLazyIdx.size() == nLazyValue.size());
    if (fVerbose) {
      cerr << "  MIPGurobiWrapper: marking " << nLazyIdx.size() << " lazy cuts." << endl;
    }
    _error = dll_GRBsetintattrlist(_model, "Lazy", static_cast<int>(nLazyIdx.size()),
                                   nLazyIdx.data(), nLazyValue.data());
    wrapAssert(_error == 0, "Failed to set constraint attribute.");
    nLazyIdx.clear();
    nLazyValue.clear();
    _error = dll_GRBupdatemodel(_model);  // for model export
    wrapAssert(_error == 0, "Failed to update model after modifying some constraint attr.");
  }

  /////////////// Last-minute solver options //////////////////
  /* Turn on output to file */
  _error = dll_GRBsetstrparam(dll_GRBgetenv(_model), "LogFile",
                              "");  // FAILS to switch off in Ubuntu 15.04
                                    /* Turn on output to the screen */
  _error = dll_GRBsetintparam(dll_GRBgetenv(_model), "OutputFlag",
                              /*fVerbose ? 1 :*/ 0);  // switch off, redirect in callback
  //    _error = dll_GRBsetintparam(dll_GRBgetenv(_model), "LogToConsole",
  //                             fVerbose ? 1 : 0);  // also when flagIntermediate?  TODO
  wrapAssert(_error == 0, "  GUROBI Warning: Failure to switch screen indicator.", false);
  //    _error =  dll_GRB_setintparam (_env, GRB_PARAM_ClockType, 1);            // CPU time
  //    _error =  dll_GRB_setintparam (_env, GRB_PARAM_MIP_Strategy_CallbackReducedLP, GRB__OFF); //
  //    Access original model
  if (!_options->sExportModel.empty()) {
    _error = dll_GRBwrite(_model, _options->sExportModel.c_str());
    wrapAssert(_error == 0, "Failed to write LP to disk.", false);
  }

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

  if (_options->nThreads > 0) {
    _error = dll_GRBsetintparam(dll_GRBgetenv(_model), GRB_INT_PAR_THREADS, _options->nThreads);
    //      int nn;    // THE SETTING FAILS TO WORK IN 6.0.5.
    //      _error = dll_getintparam(_env, GRB_INT_PAR_THREADS, &nn);
    //      cerr << "Set " << nThreads << " threads, reported " << nn << endl;
    wrapAssert(_error == 0, "Failed to set GRB_INT_PAR_THREADS.", false);
  }

  if (_options->nTimeout1000 > 0) {
    _error = dll_GRBsetdblparam(dll_GRBgetenv(_model), GRB_DBL_PAR_TIMELIMIT,
                                static_cast<double>(_options->nTimeout1000) / 1000.0);
    wrapAssert(_error == 0, "Failed to set GRB_PARAM_TimeLimit.", false);
  }

  if (_options->nSolLimit > 0) {
    _error =
        dll_GRBsetintparam(dll_GRBgetenv(_model), GRB_INT_PAR_SOLUTIONLIMIT, _options->nSolLimit);
    wrapAssert(_error == 0, "Failed to set GRB_INT_PAR_SOLLIMIT.", false);
  }

  if (_options->nSeed >= 0) {
    _error = dll_GRBsetintparam(dll_GRBgetenv(_model), GRB_INT_PAR_SEED, _options->nSeed);
    wrapAssert(_error == 0, "Failed to set GRB_INT_PAR_SEED.", false);
  }

  if (_options->nWorkMemLimit > 0 && _options->nWorkMemLimit < 1e200) {
    _error = dll_GRBsetdblparam(dll_GRBgetenv(_model), "NodefileStart", _options->nWorkMemLimit);
    wrapAssert(_error == 0, "Failed to set NodefileStart.", false);
  }

  if (!_options->sNodefileDir.empty()) {
    _error =
        dll_GRBsetstrparam(dll_GRBgetenv(_model), "NodefileDir", _options->sNodefileDir.c_str());
    wrapAssert(_error == 0, "Failed to set NodefileDir.", false);
  }

  if (_options->absGap >= 0.0) {
    _error = dll_GRBsetdblparam(dll_GRBgetenv(_model), "MIPGapAbs", _options->absGap);
    wrapAssert(_error == 0, "Failed to set  MIPGapAbs.", false);
  }
  if (_options->nMIPFocus > 0) {
    _error = dll_GRBsetintparam(dll_GRBgetenv(_model), GRB_INT_PAR_MIPFOCUS, _options->nMIPFocus);
    wrapAssert(_error == 0, "Failed to set GRB_INT_PAR_MIPFOCUS.", false);
  }

  if (_options->relGap >= 0.0) {
    _error = dll_GRBsetdblparam(dll_GRBgetenv(_model), "MIPGap", _options->relGap);
    wrapAssert(_error == 0, "Failed to set  MIPGap.", false);
  }
  if (_options->intTol >= 0.0) {
    _error = dll_GRBsetdblparam(dll_GRBgetenv(_model), "IntFeasTol", _options->intTol);
    wrapAssert(_error == 0, "Failed to set   IntFeasTol.", false);
  }
  if (_options->feasTol >= 0.0) {
    _error = dll_GRBsetdblparam(dll_GRBgetenv(_model), "FeasibilityTol", _options->feasTol);
    wrapAssert(_error == 0, "Failed to set   FeasTol.", false);
  }
  if (_options->nonConvex >= 0) {
#ifdef GRB_INT_PAR_NONCONVEX
    int major;
    int minor;
    int technical;
    dll_GRBversion(&major, &minor, &technical);
    if (major >= 9) {
      _error =
          dll_GRBsetintparam(dll_GRBgetenv(_model), GRB_INT_PAR_NONCONVEX, _options->nonConvex);
      wrapAssert(_error == 0, "Failed to set   " GRB_INT_PAR_NONCONVEX, false);
    } else {
      std::cerr << "WARNING: Non-convex solving is unavailable in this version of Gurobi"
                << std::endl;
    }
#else
    std::cerr << "WARNING: Non-convex solving is unavailable in this version of Gurobi"
              << std::endl;
#endif
  }

  /// Solution callback
  output.nCols = static_cast<int>(colObj.size());
  _x.resize(output.nCols);
  output.x = _x.data();
  SolCallbackFn solcbfn = cbui.solcbfn;
  if (true) {  // NOLINT: Need for logging
    cbui.fVerb = fVerbose;
    cbui.nTimeoutFeas = _options->nTimeoutFeas1000 / 1000.0;
    if (!_options->flagIntermediate) {
      cbui.solcbfn = nullptr;
    }
    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 << "  MIPGurobiWrapper: user cut callback enabled, setting PreCrush=1" << endl;
        }
        _error = dll_GRBsetintparam(dll_GRBgetenv(_model), GRB_INT_PAR_PRECRUSH, 1);
        wrapAssert(_error == 0, "Failed to set GRB_INT_PAR_PRECRUSH.", false);
      }
      if ((cbui.cutMask & MaskConsType_Lazy) != 0) {
        // For lazy cuts, Gurobi disables some presolves
        if (fVerbose) {
          cerr << "  MIPGurobiWrapper: lazy cut callback enabled, setting LazyConstraints=1"
               << endl;
        }
        _error = dll_GRBsetintparam(dll_GRBgetenv(_model), GRB_INT_PAR_LAZYCONSTRAINTS, 1);
        wrapAssert(_error == 0, "Failed to set GRB_INT_PAR_LAZYCONSTRAINTS.", false);
      }
    }
    _error = dll_GRBsetcallbackfunc(_model, solcallback, (void*)&cbui);
    wrapAssert(_error == 0, "Failed to set callback", false);
  }

  // Process extra flags options
  for (auto& it : _options->extraParams) {
    auto name = it.first.substr(9);
    int type = dll_GRBgetparamtype(dll_GRBgetenv(_model), name.c_str());
    enum GurobiParamType { T_INT = 1, T_DOUBLE = 2, T_STRING = 3 };
    switch (type) {
      case T_INT:
        _error = dll_GRBsetintparam(dll_GRBgetenv(_model), name.c_str(), stoi(it.second));
        break;
      case T_DOUBLE:
        _error = dll_GRBsetdblparam(dll_GRBgetenv(_model), name.c_str(), stod(it.second));
        break;
      case T_STRING:
        _error = dll_GRBsetstrparam(dll_GRBgetenv(_model), name.c_str(), it.second.c_str());
        break;
      default:
        wrapAssert(false, "Could not determine type of parameter " + name, false);
        break;
    }
    wrapAssert(_error == 0, "Failed to set parameter " + name + " = " + it.second, false);
  }

  /// after all modifs
  if (!_options->sReadParams.empty()) {
    _error = dll_GRBreadparams(dll_GRBgetenv(_model), _options->sReadParams.c_str());
    wrapAssert(_error == 0, "Failed to read GUROBI parameters.", false);
  }

  if (!_options->sWriteParams.empty()) {
    _error = dll_GRBwriteparams(dll_GRBgetenv(_model), _options->sWriteParams.c_str());
    wrapAssert(_error == 0, "Failed to write GUROBI parameters.", false);
  }

  /* See if we should set up concurrent solving */
  if (!_options->sConcurrentParamFiles.empty()) {
    int iSetting = -1;
    for (const auto& paramFile : _options->sConcurrentParamFiles) {
      ++iSetting;
      auto* env_i = dll_GRBgetconcurrentenv(_model, iSetting);
      _error = dll_GRBreadparams(env_i, paramFile.c_str());
      wrapAssert(_error == 0, "Failed to read GUROBI parameters from file " + paramFile, false);
    }
  }

  cbui.pOutput->dWallTime0 = output.dWallTime0 = std::chrono::steady_clock::now();
  output.dCPUTime = static_cast<double>(cbui.pOutput->cCPUTime0 = std::clock());

  /* Optimize the problem and obtain solution. */
  _error = dll_GRBoptimize(_model);
  wrapAssert(_error == 0, "Failed to optimize MIP.");

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

  int solstat;
  _error = dll_GRBgetintattr(_model, GRB_INT_ATTR_STATUS, &solstat);
  wrapAssert(_error == 0, "Failed to get MIP status.", false);
  output.status = convertStatus(solstat);

  /// Continuing to fill the output object:
  if (Status::OPT == output.status || Status::SAT == output.status) {
    _error = dll_GRBgetdblattr(_model, GRB_DBL_ATTR_OBJVAL, &output.objVal);
    wrapAssert(_error == 0, "No MIP objective value available.");

    //    int cur_numrows = dll_GRB_getnumrows (env, lp);
    int cur_numcols = getNCols();
    assert(cur_numcols == colObj.size());

    _x.resize(cur_numcols);
    output.x = _x.data();
    _error = dll_GRBgetdblattrarray(_model, GRB_DBL_ATTR_X, 0, cur_numcols, (double*)output.x);
    wrapAssert(_error == 0, "Failed to get variable values.");
    if ((!_options->flagIntermediate || !cbui.printed) && (solcbfn != nullptr)) {
      solcbfn(output, cbui.psi);
    }
  }
  output.bestBound = std::numeric_limits<double>::has_quiet_NaN
                         ? std::numeric_limits<double>::quiet_NaN()
                         : std::numeric_limits<double>::max();
  int nObj = 0;
  dll_GRBgetintattr(_model, GRB_INT_ATTR_NUMOBJ, &nObj);
  if (1 >= nObj) {  // This can fail, e.g., for LP or QP
    dll_GRBgetdblattr(_model, GRB_DBL_ATTR_OBJBOUNDC, &output.bestBound);
  }
  double nNodes = -1;
  _error = dll_GRBgetdblattr(_model, GRB_DBL_ATTR_NODECOUNT, &nNodes);
  output.nNodes = static_cast<int>(nNodes);
  output.nOpenNodes = 0;
}

void MIPGurobiWrapper::setObjSense(int s) {
  _error = dll_GRBsetintattr(_model, GRB_INT_ATTR_MODELSENSE, s > 0 ? GRB_MAXIMIZE : GRB_MINIMIZE);
  wrapAssert(_error == 0, "Failed to set obj sense.");
}
