#include <cstdio>
#include <cstring>

#include "HCheckConfig.h"
#include "Highs.h"
#include "catch.hpp"
#include "lp_data/HighsCallback.h"

const bool dev_run = false;

const double egout_optimal_objective = 568.1007;
const double egout_objective_target = 610;
const HighsInt adlittle_ipm_iteration_limit = 5;
const HighsInt adlittle_simplex_iteration_limit = 30;

const HighsInt kLogBufferSize = kIoBufferSize;
const HighsInt kUserCallbackNoData = -1;
const HighsInt kUserCallbackData = 99;

char printed_log[kLogBufferSize];

using std::memset;
using std::strcmp;
using std::strcpy;
using std::strlen;
using std::strncmp;
using std::strstr;

struct MipData {
  HighsInt num_col;
  HighsVarType* integrality;
};

// Callback that saves message for comparison
HighsCallbackFunctionType myLogCallback =
    [](int callback_type, const std::string& message,
       const HighsCallbackDataOut* data_out, HighsCallbackDataIn* data_in,
       void* user_callback_data) { strcpy(printed_log, message.c_str()); };

HighsCallbackFunctionType userMipSolutionCallback =
    [](int callback_type, const std::string& message,
       const HighsCallbackDataOut* data_out, HighsCallbackDataIn* data_in,
       void* user_callback_data) {
      if (dev_run) {
        printf(
            "MipSolutionCallback with objective = %15.8g and bounds [%15.8g, "
            "%15.8g]",
            data_out->objective_function_value, data_out->mip_dual_bound,
            data_out->mip_primal_bound);
        MipData callback_data = *(static_cast<MipData*>(user_callback_data));
        HighsInt num_col = callback_data.num_col;
        HighsVarType* integrality = callback_data.integrality;
        HighsInt num_integer = 0;
        for (HighsInt iCol = 0; iCol < num_col; iCol++)
          if (integrality[iCol] == HighsVarType::kInteger) num_integer++;
        if (num_integer < 50) {
          printf(" and solution [");
          for (HighsInt iCol = 0; iCol < num_col; iCol++) {
            if (integrality[iCol] != HighsVarType::kInteger) continue;
            double value = data_out->mip_solution[iCol];
            if (std::abs(value) < 1e-5) {
              printf("0");
            } else if (std::abs(value - 1) < 1e-5) {
              printf("1");
            } else {
              bool printed = false;
              for (HighsInt k = 2; k < 10; k++) {
                if (std::abs(value - k) < 1e-5) {
                  printf("%1d", int(k));
                  printed = true;
                }
              }
              if (printed) continue;
              for (HighsInt k = 10; k < 999; k++) {
                if (std::abs(value - k) < 1e-5) {
                  printf(" %d ", int(k));
                  printed = true;
                }
              }
              if (printed) continue;
              printf("*");
            }
          }
          printf("]\n");
        } else {
          printf("\n");
        }
        fflush(stdout);
      }
    };

HighsCallbackFunctionType userInterruptCallback =
    [](int callback_type, const std::string& message,
       const HighsCallbackDataOut* data_out, HighsCallbackDataIn* data_in,
       void* user_callback_data) {
      // Extract local_callback_data from user_callback_data unless it
      // is nullptr
      if (callback_type == kCallbackMipImprovingSolution) {
        // Use local_callback_data to maintain the objective value from
        // the previous callback
        assert(user_callback_data);
        // Extract the double value pointed to from void* user_callback_data
        const double local_callback_data = *(double*)user_callback_data;
        if (dev_run)
          printf(
              "userCallback(type %2d; data %11.4g): %s with objective %g and "
              "solution[0] = %g\n",
              callback_type, local_callback_data, message.c_str(),
              data_out->objective_function_value, data_out->mip_solution[0]);
        REQUIRE(local_callback_data >= data_out->objective_function_value);
        // Update the double value pointed to from void* user_callback_data
        *(double*)user_callback_data = data_out->objective_function_value;
      } else {
        const int local_callback_data =
            user_callback_data ? static_cast<int>(reinterpret_cast<intptr_t>(
                                     user_callback_data))
                               : kUserCallbackNoData;
        if (user_callback_data) {
          REQUIRE(local_callback_data == kUserCallbackData);
        } else {
          REQUIRE(local_callback_data == kUserCallbackNoData);
        }
        if (callback_type == kCallbackLogging) {
          if (dev_run) printf("Callback: %s", message.c_str());
          //            printf("userInterruptCallback(type %2d; data %2d): %s",
          //                   callback_type, local_callback_data,
          //                   message.c_str());
        } else if (callback_type == kCallbackSimplexInterrupt) {
          if (dev_run)
            printf(
                "userInterruptCallback(type %2d; data %2d): %s with iteration "
                "count = "
                "%d\n",
                callback_type, local_callback_data, message.c_str(),
                int(data_out->simplex_iteration_count));
          data_in->user_interrupt = data_out->simplex_iteration_count >
                                    adlittle_simplex_iteration_limit;
        } else if (callback_type == kCallbackIpmInterrupt) {
          if (dev_run)
            printf(
                "userInterruptCallback(type %2d; data %2d): %s with iteration "
                "count = "
                "%d\n",
                callback_type, local_callback_data, message.c_str(),
                int(data_out->ipm_iteration_count));
          data_in->user_interrupt =
              data_out->ipm_iteration_count > adlittle_ipm_iteration_limit;
        } else if (callback_type == kCallbackMipInterrupt) {
          if (dev_run)
            printf(
                "userInterruptCallback(type %2d; data %2d): %s with Bounds "
                "(%11.4g, %11.4g); Gap = %11.4g; Objective = "
                "%g\n",
                callback_type, local_callback_data, message.c_str(),
                data_out->mip_dual_bound, data_out->mip_primal_bound,
                data_out->mip_gap, data_out->objective_function_value);
          data_in->user_interrupt =
              data_out->objective_function_value < egout_objective_target;
        }
      }
    };

HighsCallbackFunctionType userMipCutPoolCallback =
    [](int callback_type, const std::string& message,
       const HighsCallbackDataOut* data_out, HighsCallbackDataIn* data_in,
       void* user_callback_data) {
      if (dev_run) {
        printf("userMipCutPoolCallback: dim(%2d, %2d, %2d)\n",
               int(data_out->cutpool_num_col), int(data_out->cutpool_num_cut),
               int(data_out->cutpool_num_nz));
        for (HighsInt iCut = 0; iCut < data_out->cutpool_num_cut; iCut++) {
          printf("Cut %d\n", int(iCut));
          for (HighsInt iEl = data_out->cutpool_start[iCut];
               iEl < data_out->cutpool_start[iCut + 1]; iEl++) {
            printf("   %2d %11.5g\n", int(data_out->cutpool_index[iEl]),
                   data_out->cutpool_value[iEl]);
          }
        }
      }
    };

std::function<void(int, const std::string&, const HighsCallbackDataOut*,
                   HighsCallbackDataIn*, void*)>
    userDataCallback = [](int callback_type, const std::string& message,
                          const HighsCallbackDataOut* data_out,
                          HighsCallbackDataIn* data_in,
                          void* user_callback_data) {
      assert(callback_type == kCallbackMipInterrupt ||
             callback_type == kCallbackMipLogging ||
             callback_type == kCallbackMipImprovingSolution);
      if (dev_run)
        printf(
            "userDataCallback: Node count = %" PRId64
            "; LP total iterations = %" PRId64
            "; Time = %6.2f; "
            "Bounds (%11.4g, %11.4g); Gap = %11.4g; Objective = %11.4g: %s\n",
            data_out->mip_node_count, data_out->mip_total_lp_iterations,
            data_out->running_time, data_out->mip_dual_bound,
            data_out->mip_primal_bound, data_out->mip_gap,
            data_out->objective_function_value, message.c_str());
    };

TEST_CASE("my-callback-logging", "[highs-callback]") {
  bool output_flag = true;  // Still runs quietly
  bool log_to_console = false;
  HighsInt log_dev_level = kHighsLogDevLevelInfo;
  HighsLogOptions log_options;
  log_options.clear();
  log_options.log_stream = stdout;
  log_options.output_flag = &output_flag;
  log_options.log_to_console = &log_to_console;
  log_options.log_dev_level = &log_dev_level;
  log_options.user_callback = myLogCallback;
  log_options.user_callback_active = true;

  highsLogDev(log_options, HighsLogType::kInfo, "Hi %s!", "HiGHS");
  if (dev_run) printf("Log callback yields \"%s\"\n", printed_log);
  REQUIRE(strcmp(printed_log, "Hi HiGHS!") == 0);

  // Check that nothing is printed if the type is VERBOSE when
  // log_dev_level is kHighsLogDevLevelInfo;
  *printed_log = '\0';
  highsLogDev(log_options, HighsLogType::kVerbose, "Hi %s!", "HiGHS");
  REQUIRE(*printed_log == '\0');

  {
    char long_message[sizeof(printed_log)];
    memset(long_message, 'H', sizeof(long_message));
    long_message[sizeof(long_message) - 2] = '\0';
    long_message[sizeof(long_message) - 1] = '\n';
    highsLogDev(log_options, HighsLogType::kInfo, long_message);
    if (dev_run) printf("Log callback yields \"%s\"\n", printed_log);
    REQUIRE(strncmp(printed_log, "HHHH", 4) == 0);
    REQUIRE(strlen(printed_log) <= sizeof(printed_log));
  }

  highsLogUser(log_options, HighsLogType::kInfo, "Hello %s!\n", "HiGHS");
  REQUIRE(strlen(printed_log) > 9);
  REQUIRE(strcmp(printed_log, "Hello HiGHS!\n") == 0);

  {
    char long_message[sizeof(printed_log)];
    memset(long_message, 'H', sizeof(long_message));
    long_message[sizeof(long_message) - 2] = '\0';
    long_message[sizeof(long_message) - 1] = '\n';
    highsLogUser(log_options, HighsLogType::kWarning, long_message);
    if (dev_run) printf("Log callback yields \"%s\"\n", printed_log);
    REQUIRE(strstr(printed_log, "HHHH") != nullptr);
    REQUIRE(strlen(printed_log) <= sizeof(printed_log));
  }
}

TEST_CASE("highs-callback-logging", "[highs-callback]") {
  // Uses userInterruptCallback to start logging lines with
  // "userInterruptCallback(kUserCallbackData): " since
  // Highs::setCallback has second argument p_user_callback_data
  std::string filename = std::string(HIGHS_DIR) + "/check/instances/avgas.mps";
  int user_callback_data = kUserCallbackData;
  void* p_user_callback_data =
      reinterpret_cast<void*>(static_cast<intptr_t>(user_callback_data));
  Highs highs;
  highs.setOptionValue("output_flag", dev_run);
  highs.setCallback(userInterruptCallback, p_user_callback_data);
  highs.startCallback(kCallbackLogging);
  highs.readModel(filename);
  highs.run();
}

TEST_CASE("highs-callback-solution-basis-logging", "[highs-callback]") {
  std::string filename = std::string(HIGHS_DIR) + "/check/instances/avgas.mps";
  int user_callback_data = kUserCallbackData;
  void* p_user_callback_data =
      reinterpret_cast<void*>(static_cast<intptr_t>(user_callback_data));
  Highs highs;
  highs.setOptionValue("output_flag", dev_run);
  highs.readModel(filename);
  highs.run();
  highs.setCallback(userInterruptCallback, p_user_callback_data);
  highs.startCallback(kCallbackLogging);
  if (dev_run) highs.writeSolution("", kSolutionStylePretty);
  if (dev_run) highs.writeBasis("");
}

TEST_CASE("highs-callback-simplex-interrupt", "[highs-callback]") {
  std::string filename =
      std::string(HIGHS_DIR) + "/check/instances/adlittle.mps";
  Highs highs;
  highs.setOptionValue("output_flag", dev_run);
  highs.setCallback(userInterruptCallback);
  highs.startCallback(kCallbackSimplexInterrupt);
  highs.readModel(filename);
  HighsStatus status = highs.run();
  REQUIRE(status == HighsStatus::kWarning);
  REQUIRE(highs.getModelStatus() == HighsModelStatus::kInterrupt);
  REQUIRE(highs.getInfo().simplex_iteration_count >
          adlittle_simplex_iteration_limit);
}

TEST_CASE("highs-callback-ipm-interrupt", "[highs-callback]") {
  std::string filename =
      std::string(HIGHS_DIR) + "/check/instances/adlittle.mps";
  Highs highs;
  highs.setOptionValue("output_flag", dev_run);
  highs.setCallback(userInterruptCallback);
  highs.startCallback(kCallbackIpmInterrupt);
  highs.readModel(filename);
  highs.setOptionValue("solver", kIpmString);
  HighsStatus status = highs.run();
  REQUIRE(status == HighsStatus::kWarning);
  REQUIRE(highs.getModelStatus() == HighsModelStatus::kInterrupt);
  REQUIRE(highs.getInfo().ipm_iteration_count > adlittle_ipm_iteration_limit);
}

TEST_CASE("highs-callback-mip-interrupt", "[highs-callback]") {
  std::string filename = std::string(HIGHS_DIR) + "/check/instances/egout.mps";
  Highs highs;
  highs.setOptionValue("output_flag", dev_run);
  highs.setOptionValue("presolve", kHighsOffString);
  highs.setCallback(userInterruptCallback);
  highs.startCallback(kCallbackMipInterrupt);
  highs.readModel(filename);
  HighsStatus status = highs.run();
  REQUIRE(status == HighsStatus::kWarning);
  REQUIRE(highs.getModelStatus() == HighsModelStatus::kInterrupt);
  REQUIRE(highs.getInfo().objective_function_value > egout_optimal_objective);
}

TEST_CASE("highs-callback-mip-improving", "[highs-callback]") {
  std::string filename = std::string(HIGHS_DIR) + "/check/instances/egout.mps";
  Highs highs;
  highs.setOptionValue("output_flag", dev_run);
  highs.setOptionValue("presolve", kHighsOffString);
  double user_callback_data = kHighsInf;
  void* p_user_callback_data = (void*)(&user_callback_data);
  highs.setCallback(userInterruptCallback, p_user_callback_data);
  highs.startCallback(kCallbackMipImprovingSolution);
  highs.readModel(filename);
  highs.run();
}

TEST_CASE("highs-callback-mip-data", "[highs-callback]") {
  std::string filename = std::string(HIGHS_DIR) + "/check/instances/egout.mps";
  Highs highs;
  highs.setOptionValue("output_flag", dev_run);
  highs.setOptionValue("presolve", kHighsOffString);
  highs.setCallback(userDataCallback);
  highs.startCallback(kCallbackMipImprovingSolution);
  highs.startCallback(kCallbackMipLogging);
  highs.readModel(filename);
  highs.run();
}

TEST_CASE("highs-callback-mip-solution", "[highs-callback]") {
  std::string filename = std::string(HIGHS_DIR) + "/check/instances/egout.mps";
  Highs highs;
  highs.setOptionValue("output_flag", dev_run);
  highs.setOptionValue("presolve", kHighsOffString);
  highs.readModel(filename);
  // To print the values of the integer variables in the callback,
  // need the number of columns and the integrality. Set this up in a
  // struct to be passed via user_callback_data
  HighsLp lp = highs.getLp();
  MipData user_callback_data;
  user_callback_data.num_col = int(lp.num_col_);
  user_callback_data.integrality = lp.integrality_.data();
  void* p_user_callback_data = &user_callback_data;

  highs.setCallback(userMipSolutionCallback, p_user_callback_data);
  highs.startCallback(kCallbackMipSolution);
  highs.run();
}

TEST_CASE("highs-callback-mip-cut-pool", "[highs-callback]") {
  std::string filename = std::string(HIGHS_DIR) + "/check/instances/flugpl.mps";
  Highs highs;
  highs.setOptionValue("output_flag", dev_run);
  highs.readModel(filename);
  //  MipData user_callback_data;
  highs.setCallback(userMipCutPoolCallback);  //, p_user_callback_data);
  highs.startCallback(kCallbackMipGetCutPool);
  highs.run();
}
