#include "pch.h"
#include <math.h>
#include "MariaBinding.h"
#include "integer64.h"

MariaBinding::MariaBinding() : statement(NULL), p(0), i(0), n_rows(0) {}

MariaBinding::~MariaBinding() {}

void MariaBinding::setup(MYSQL_STMT* statement_) {
  statement = statement_;
  p = static_cast<int>(mysql_stmt_param_count(statement));

  bindings.resize(p);
  types.resize(p);
  is_null.resize(p);
  time_buffers.resize(p);
}

void MariaBinding::init_binding(const cpp11::list& params_) {
  params = params_;

  if (params.size() == 0) {
    cpp11::stop("Query has no parameters");
  }

  if (p != params.size()) {
    cpp11::stop("Number of params don't match (%i vs %i)", p, params.size());
  }

  i = 0;

  for (int j = 0; j < p; ++j) {
    cpp11::sexp param(params[j]);
    const auto type = variable_type_from_object(param);
    types[j] = type;

    if (j == 0) {
      n_rows = Rf_xlength(param);
    }

    switch (type) {
    case MY_LGL:
      binding_update(j, MYSQL_TYPE_TINY, 1);
      break;
    case MY_INT32:
      binding_update(j, MYSQL_TYPE_LONG, 4);
      break;
    case MY_DBL:
      binding_update(j, MYSQL_TYPE_DOUBLE, 8);
      break;
    case MY_DATE:
      binding_update(j, MYSQL_TYPE_DATE, sizeof(MYSQL_TIME));
      break;
    case MY_DATE_TIME:
      binding_update(j, MYSQL_TYPE_DATETIME, sizeof(MYSQL_TIME));
      break;
    case MY_TIME:
      binding_update(j, MYSQL_TYPE_TIME, sizeof(MYSQL_TIME));
      break;
    case MY_STR:
      binding_update(j, MYSQL_TYPE_STRING, 0);
      break;
    case MY_RAW:
      binding_update(j, MYSQL_TYPE_BLOB, 0);
      break;
    case MY_INT64:
      binding_update(j, MYSQL_TYPE_LONGLONG, 0);
      break;
    }
  }
}

bool MariaBinding::bind_next_row() {
  if (i >= n_rows) {
    return false;
  }

  for (int j = 0; j < p; ++j) {
    bool missing = false;
    cpp11::sexp col(params[j]);

    switch (types[j]) {
    case MY_LGL:
      if (LOGICAL(col)[i] == NA_LOGICAL) {
        missing = true;
        break;
      }
      bindings[j].buffer = &LOGICAL(col)[i];
      break;
    case MY_INT32:
      if (INTEGER(col)[i] == NA_INTEGER) {
        missing = true;
        break;
      }
      bindings[j].buffer = &INTEGER(col)[i];
      break;
    case MY_DBL:
      if (ISNA(REAL(col)[i])) {
        missing = true;
        break;
      }
      bindings[j].buffer = &REAL(col)[i];
      break;
    case MY_STR:
      if (STRING_ELT(col, i) == NA_STRING) {
        missing = true;
        break;
      } else {
        SEXP string = STRING_ELT(col, i);
        bindings[j].buffer = const_cast<char*>(CHAR(string));
        bindings[j].buffer_length = Rf_length(string);
      }
      break;
    case MY_RAW:
      {
        SEXP raw = VECTOR_ELT(col, i);
        if (Rf_isNull(raw)) {
          missing = true;
        } else {
          bindings[j].buffer_length = Rf_length(raw);
          bindings[j].buffer = RAW(raw);
        }
        break;
      }
    case MY_DATE:
    case MY_DATE_TIME:
      if (ISNAN(REAL(col)[i])) {
        missing = true;
      } else {
        double val = REAL(col)[i];
        if (types[j] == MY_DATE) {
          set_date_buffer(j, static_cast<int>(::floor(val)));
          clear_time_buffer(j);
        } else {
          double days = ::floor(val / 86400.0);
          set_date_buffer(j, static_cast<int>(days));
          set_time_buffer(j, val - days * 86400.0);
        }
        bindings[j].buffer_length = sizeof(MYSQL_TIME);
        bindings[j].buffer = &time_buffers[j];
      }
      break;
    case MY_TIME:
      if (ISNAN(REAL(col)[i])) {
        missing = true;
        break;
      } else {
        double val = REAL(col)[i];
        clear_date_buffer(j);
        set_time_buffer(j, val);
        bindings[j].buffer_length = sizeof(MYSQL_TIME);
        bindings[j].buffer = &time_buffers[j];
      }
      break;
    case MY_INT64:
      if (INTEGER64(col)[i] == NA_INTEGER64) {
        missing = true;
        break;
      }
      bindings[j].buffer = &INTEGER64(col)[i];
      break;
    }
    is_null[j] = missing;
  }

  mysql_stmt_bind_param(statement, &bindings[0]);

  i++;
  return true;
}

void MariaBinding::binding_update(int j, enum_field_types type, int size) {
  bindings[j].buffer_length = size;
  bindings[j].buffer_type = type;
  bindings[j].is_null = &is_null[j];
}

void MariaBinding::clear_date_buffer(int j) {
  time_buffers[j].year = 0;
  time_buffers[j].month = 0;
  time_buffers[j].day = 0;
}

void MariaBinding::set_date_buffer(int j, const int date) {
  // https://howardhinnant.github.io/date_algorithms.html#civil_from_days
  const int date_0 = date + 719468;
  const int era = (date_0 >= 0 ? date_0 : date_0 - 146096) / 146097;
  const unsigned doe =
    static_cast<unsigned>(date_0 - era * 146097);  // [0, 146096]
  const unsigned yoe =
    (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;  // [0, 399]
  const int y = static_cast<int>(yoe) + era * 400;
  const unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100);  // [0, 365]
  const unsigned mp = (5 * doy + 2) / 153;                       // [0, 11]
  const unsigned d = doy - (153 * mp + 2) / 5 + 1;               // [1, 31]
  const unsigned m = mp < 10 ? mp + 3 : mp - 9;                  // [1, 12]
  const unsigned yr = y + (m <= 2);

  // gmtime() fails for dates < 1970 on Windows

  time_buffers[j].year = yr;
  time_buffers[j].month = m;
  time_buffers[j].day = d;
}

void MariaBinding::clear_time_buffer(int j) {
  time_buffers[j].hour = 0;
  time_buffers[j].minute = 0;
  time_buffers[j].second = 0;
  time_buffers[j].second_part = 0;
  time_buffers[j].neg = 0;
}

void MariaBinding::set_time_buffer(int j, double time) {
  bool neg = false;
  if (time < 0) {
    neg = true;
    time = -time;
  }
  double whole_seconds = ::trunc(time);
  double frac_seconds = time - whole_seconds;
  double whole_minutes = ::trunc(time / 60.0);
  double seconds = whole_seconds - whole_minutes * 60.0;
  double hours = ::trunc(time / 3600.0);
  double minutes = whole_minutes - hours * 60.0;

  time_buffers[j].hour = static_cast<unsigned int>(hours);
  time_buffers[j].minute = static_cast<unsigned int>(minutes);
  time_buffers[j].second = static_cast<unsigned int>(seconds);
  time_buffers[j].second_part =
    static_cast<unsigned long>(frac_seconds * 1000000.0);
  time_buffers[j].neg = neg;
}
