/* KInterbasDB Python Package - Implementation of Distributed Transaction
 *                              Operations
 *
 * Version 3.3
 *
 * The following contributors hold Copyright (C) over their respective
 * portions of code (see license.txt for details):
 *
 * [Original Author (maintained through version 2.0-0.3.1):]
 *   1998-2001 [alex]  Alexander Kuznetsov   <alexan@users.sourceforge.net>
 * [Maintainers (after version 2.0-0.3.1):]
 *   2001-2002 [maz]   Marek Isalski         <kinterbasdb@maz.nu>
 *   2002-2007 [dsr]   David Rushby          <woodsplitter@rocketmail.com>
 * [Contributors:]
 *   2001      [eac]   Evgeny A. Cherkashin  <eugeneai@icc.ru>
 *   2001-2002 [janez] Janez Jere            <janez.jere@void.si>
 */

static ISC_TEB *build_teb_buffer(PyObject *cons);

/*** StandaloneTransactionHandle TYPE DEFINITION AND INITIALIZATION : begin **/
static StandaloneTransactionHandle *StandaloneTransactionHandle_create(void) {
  StandaloneTransactionHandle *trans_handle =
    PyObject_New(StandaloneTransactionHandle, &StandaloneTransactionHandleType);
  if (trans_handle == NULL) { return NULL; }

  trans_handle->native_handle = NULL_TRANS_HANDLE;

  return trans_handle;
} /* StandaloneTransactionHandle_create */

static void StandaloneTransactionHandle___del__(
    StandaloneTransactionHandle *self
  )
{
  /* Normally, the database client library will have already set the
   * native_handle to NULL_TRANS_HANDLE when it either committed or rolled back
   * the transaction.  If the client library was not able to do either of
   * those, we simply forget about the handle (don't free, because a handle is
   * not necessarily a pointer). */
  if (self->native_handle != NULL_TRANS_HANDLE) {
    self->native_handle = NULL_TRANS_HANDLE;
  }

  /* Delete the memory of the StandaloneTransactionHandle struct itself: */
  PyObject_Del(self);
} /* StandaloneTransactionHandle___del__ */
/*** StandaloneTransactionHandle TYPE DEFINITION AND INITIALIZATION : end ****/

/*********** PYTHON WRAPPERS FOR DISTRIBUTED TRANS OPS : begin ***************/
static PyObject *pyob_distributed_begin(PyObject *self, PyObject *args) {
  PyObject *group;
  /* cons, the input object from the Python level, is a list of instances of
   * Python class kinterbasdb.Connection, not a list of instances of C type
   * ConnectionType. */
  PyObject *cons;

  StandaloneTransactionHandle *trans_handle = NULL;
  ISC_TEB *tebs = NULL;
  Py_ssize_t teb_count;
  ISC_STATUS status_vector[STATUS_VECTOR_SIZE];

  if (!PyArg_ParseTuple(args, "OO!", &group, &PyList_Type, &cons)) {
    goto exit;
  }

  teb_count = PyList_GET_SIZE(cons);
  /* The Python layer should prevent the programmer from starting a distributed
   * transaction with an empty group. */
  assert (teb_count > 0);
  /* The Python layer (ConnectionGroup class) should prevent the programmer
   * from exceeding the database engine's limits on the number of database
   * handles that can participate in a single distributed transaction. */
  assert (teb_count <= DIST_TRANS_MAX_DATABASES);

  tebs = build_teb_buffer(cons);
  if (tebs == NULL) { goto exit; }

  trans_handle = StandaloneTransactionHandle_create();
  if (trans_handle == NULL) { goto exit; }

  trans_handle->native_handle = begin_transaction(
      NULL_DB_HANDLE, NULL, -1, /* All parameters for singleton transactions are null. */
      tebs, (short) teb_count, /* Cast is safe b/c already checked val. */
      status_vector
    );
  if (trans_handle->native_handle == NULL_TRANS_HANDLE) { goto exit; }

  /* Change the state of each of the Connection's main_trans to indicate that
   * it is now involved in an unresolved transaction: */
  if (change_resolution_of_all_con_main_trans(group, cons, FALSE) != 0) {
    assert (PyErr_Occurred());
    goto exit;
  }

  exit:
    if (tebs != NULL) { kimem_main_free(tebs); }

    if (trans_handle == NULL) {
      assert (PyErr_Occurred());
      return NULL;
    } else if (trans_handle->native_handle == NULL_TRANS_HANDLE) {
      Py_DECREF(trans_handle);
      return NULL;
    } else {
      return (PyObject *) trans_handle;
    }
} /* pyob_distributed_begin */

static PyObject *pyob_distributed_prepare(PyObject *self, PyObject *args) {
  StandaloneTransactionHandle *py_handle;
  ISC_STATUS status_vector[STATUS_VECTOR_SIZE];

  if (!PyArg_ParseTuple(args, "O!", &StandaloneTransactionHandleType, &py_handle)) {
    goto fail;
  }

  if (   prepare_transaction(&py_handle->native_handle, status_vector)
      != OP_RESULT_OK
     )
  { goto fail; }

  RETURN_PY_NONE;

  fail:
    assert (PyErr_Occurred());
    return NULL;
} /* pyob_distributed_prepare */

static PyObject *_pyob_distributed_commit_or_rollback(
    WhichTransactionOperation op, PyObject *self, PyObject *args
  )
{
  PyObject *group; /* We perform no type checking on group. */
  StandaloneTransactionHandle *trans_handle;
  PyObject *cons_list;
  boolean retaining;
  ISC_STATUS status_vector[STATUS_VECTOR_SIZE];

  {
    PyObject *py_retaining;
    if (!PyArg_ParseTuple(args, "OO!O!O",
           &group,
           &StandaloneTransactionHandleType, &trans_handle,
           &PyList_Type, &cons_list,
           &py_retaining
         )
       )
    { goto fail; }
    retaining = (boolean) PyObject_IsTrue(py_retaining);
  }

  {
    /* action_result is only initialized because otherwise, GCC incorrectly
     * warns that it might be used unitialized (the switch statement covers all
     * possible values of WhichTransactionOperation op). */
    TransactionalOperationResult action_result = OP_RESULT_ERROR;
    switch (op) {
      case OP_COMMIT:
        action_result = commit_transaction(
            &trans_handle->native_handle, retaining, status_vector
          );
        break;

      case OP_ROLLBACK:
        action_result = rollback_transaction(
            &trans_handle->native_handle, retaining, TRUE, status_vector
          );
        break;
    }
    if (action_result != OP_RESULT_OK) { goto fail; }
  }

  if (retaining) {
    assert (trans_handle->native_handle != NULL_TRANS_HANDLE);
  } else {
    trans_handle->native_handle = NULL_TRANS_HANDLE;
  }

  /* Change the state of each of the Connection's main_trans to indicate that
   * the distributed transaction in which it was involved has now been
   * resolved: */
  if (change_resolution_of_all_con_main_trans(group, cons_list, TRUE) != 0) {
    assert (PyErr_Occurred());
    goto fail;
  }

  RETURN_PY_NONE;

  fail:
    assert (PyErr_Occurred());
    return NULL;
} /* _pyob_distributed_commit_or_rollback */

static PyObject *pyob_distributed_commit(PyObject *self, PyObject *args) {
  return _pyob_distributed_commit_or_rollback(OP_COMMIT, self, args);
} /* pyob_distributed_commit */

static PyObject *pyob_distributed_rollback(PyObject *self, PyObject *args) {
  return _pyob_distributed_commit_or_rollback(OP_ROLLBACK, self, args);
} /* pyob_distributed_rollback */
/************* PYTHON WRAPPERS FOR DISTRIBUTED TRANS OPS : end ****************/

/*********************** SUPPORTING FUNCTIONS : begin ************************/
static int change_resolution_of_all_con_main_trans(
    PyObject *group, PyObject *cons, const boolean is_resolved
  )
{
  Py_ssize_t cons_count;
  Py_ssize_t i;

  assert (group != NULL); /* We perform no type checking on group. */

  assert (cons != NULL);
  /* The caller (internal kinterbasdb code) should have already ensured this: */
  assert (PyList_Check(cons));

  cons_count = PyList_GET_SIZE(cons);
  for (i = 0; i < cons_count; ++i) {
    PyObject *py_con = PyList_GET_ITEM(cons, i); /* borrowed ref */

    {
      /* PyObject_GetAttr returns new ref: */
      PyObject *_C_con = PyObject_GetAttr(py_con, shared___s__C_con);
      if (_C_con == NULL) {
        goto fail;
      } else if (!PyObject_TypeCheck(_C_con, &ConnectionType)) {
        Py_DECREF(_C_con);

        raise_exception(InternalError, "Connection._C_con was not a"
            " CConnection."
          );
        goto fail;
      } else {
        CConnection *con = (CConnection *) _C_con;
        assert (con->main_trans != NULL);
        Transaction_dist_trans_indicate_resultion(con->main_trans,
            group, is_resolved
          );
        Py_DECREF(_C_con);
      }
    }
  }

  assert (!PyErr_Occurred());
  return 0;
  fail:
    assert (PyErr_Occurred());
    return -1;
} /* change_resolution_of_all_con_main_trans */

static ISC_TEB *build_teb_buffer(PyObject *cons) {
  ISC_TEB *tebs = NULL;
  Py_ssize_t teb_count;
  Py_ssize_t tebs_size;
  CConnection *con = NULL;
  PyObject *tpb = NULL;
  Py_ssize_t i;

  assert (cons != NULL);
  /* The caller (internal kinterbasdb code) should have already ensured this: */
  assert (PyList_Check(cons));

  teb_count = PyList_GET_SIZE(cons);
  tebs_size = sizeof(ISC_TEB) * teb_count;
  tebs = kimem_main_malloc(tebs_size);
  if (tebs == NULL) { goto fail; }

  for (i = 0; i < teb_count; ++i) {
    ISC_TEB *t = tebs + i;
    PyObject *py_con = PyList_GET_ITEM(cons, i); /* borrowed ref */

    /* PyObject_GetAttr returns a new reference.  These new references are
     * released at the end of each iteration of this loop (normally), or in the
     * fail clause in case of error. */
    con = (CConnection *) PyObject_GetAttr(py_con, shared___s__C_con);
    if (con == NULL) { goto fail; }
    assert (con->main_trans != NULL);
    tpb = pyob_Transaction_get_default_tpb(con->main_trans);
    if (tpb == NULL) { goto fail; }
    /* The Python layer should have already ensured this: */
    assert (con->db_handle != NULL_DB_HANDLE);

    t->db_ptr = (long *) &con->db_handle;

    if (tpb == Py_None) {
      t->tpb_len = 0;
      t->tpb_ptr = NULL;
    } else if (PyString_Check(tpb)) {
      const Py_ssize_t tpb_len = PyString_GET_SIZE(tpb);
      #if PY_SSIZE_T_MAX__CIRCUMVENT_COMPILER_COMPLAINT > LONG_MAX
      if (tpb_len > LONG_MAX) {
        raise_exception(ProgrammingError, "TPB size must be <= LONG_MAX.");
        goto fail;
      }
      #endif
      t->tpb_len = (long) tpb_len;

      t->tpb_ptr = PyString_AS_STRING(tpb);
    } else {
      PyErr_SetString(InternalError, "Connection._default_tpb_str_ must be a"
          " str or None."
        );
      goto fail;
    }
    Py_DECREF(con);
    Py_DECREF(tpb);
    /* Set to NULL to prevent double-decref in case of error during next
     * iteration. */
    con = NULL;
    tpb = NULL;
  }
  /* Upon successful exit, all references will have been released. */

  return tebs;

  fail:
    assert (PyErr_Occurred());

    Py_XDECREF(con);
    Py_XDECREF(tpb);
    if (tebs != NULL) { kimem_main_free(tebs); }

    return NULL;
} /* build_teb_buffer */
/************************ SUPPORTING FUNCTIONS : end *************************/
