/* KInterbasDB Python Package - Implementation of
 *                              Python-Free SQL Error Extraction
 *
 * 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>
 */

/* Notice that no Python code is executed here; there is no need to hold the
 * GIL while calling these functions. */

#include "_kinterbasdb_exceptions.h"

static int _enlarge_message_if_necessary(
    NonPythonSQLErrorInfo *se, size_t next_segment_size,
    size_t *current_size, size_t *n_free
  );


static NonPythonSQLErrorInfo *NonPythonSQLErrorInfo_create(void) {
  NonPythonSQLErrorInfo *se = kimem_plain_malloc(sizeof(NonPythonSQLErrorInfo));
  if (se == NULL) { goto fail; }

  se->code = 0;
  se->msg = NULL;

  return se;

  fail:
    return NULL;
} /* NonPythonSQLErrorInfo_init */

static void NonPythonSQLErrorInfo_destroy(NonPythonSQLErrorInfo *se) {
  assert (se != NULL);

  if (se->msg != NULL) {
    kimem_plain_free(se->msg);
  }

  kimem_plain_free(se);
} /* NonPythonSQLErrorInfo_destroy */

static NonPythonSQLErrorInfo *extract_sql_error_without_python(
    ISC_STATUS *sv, const char *preamble
  )
{
  /* The caller should hold the GDAL while calling. */
  Py_ssize_t n_messages = 0;
  size_t preamble_size = (preamble == NULL ? 0 : strlen(preamble));

  char trans_buf[MAX_ISC_ERROR_MESSAGE_BUFFER_SIZE];
    /* Buffer overflow potential here is ultra-low, since only a single error
     * message supplied by the database library's error code interpretretation
     * function is placed in the fixed-size buffer, and in FB 2.0 and later,
     * the interpretation function checks the size of the output buffer. */

  #ifdef USE_MODERN_INTERP_FUNC
    const
  #endif
  ISC_STATUS *sv_walk =
      #ifdef USE_MODERN_INTERP_FUNC
        (const ISC_STATUS *)
      #endif
      sv
    ;

  /* If necessary, se->msg will be enlarged to hold multiple messages, each
   * placed only temporarily in trans_buf. */
  NonPythonSQLErrorInfo *se = NULL;
  size_t se_msg_size = MAX_ISC_ERROR_MESSAGE_BUFFER_SIZE + preamble_size;
  size_t se_msg_n_free = se_msg_size - 1; /* - 1 for null terminator */
  size_t cur_msg_size = 0;

  /* This function shouldn't have been called if an error hasn't occurred. */
  assert (DB_API_ERROR(sv));

  memset(trans_buf, '\0', MAX_ISC_ERROR_MESSAGE_BUFFER_SIZE);

  se = NonPythonSQLErrorInfo_create();
  if (se == NULL) { goto fail; }

  se->code = isc_sqlcode(sv);

  se->msg = kimem_plain_malloc(se_msg_size);
  if (se->msg == NULL) { goto fail; }
  se->msg[0] = '\0'; /* Must be null-terminated at all times. */

  if (preamble != NULL) {
    /* preamble_size doesn't include the null terminator. */
    assert (preamble[preamble_size] == '\0');

    strncat(se->msg, preamble, preamble_size + 1);
    se_msg_n_free -= preamble_size;
  }

  /* The description placed in se->msg by isc_sql_interprete won't be longer
   * than MAX_ISC_ERROR_MESSAGE_BUFFER_SIZE - 1, which is how much free space
   * we currently have in se->msg.  Even if the description occupies most of
   * the available space in se->msg, se->msg will be automatically enlarged by
   * _enlarge_message_if_necessary in the while loop below. */
  assert (se_msg_n_free == MAX_ISC_ERROR_MESSAGE_BUFFER_SIZE - 1);
  /* - 2 to allow for trailing period and newline: */
  isc_sql_interprete((short) se->code, se->msg, (short) (se_msg_n_free - 2));
  {
    const size_t sqlcode_msg_len = strlen(se->msg) - preamble_size;
    if (sqlcode_msg_len > 0) {
      strcat(se->msg, ".\n");
      se_msg_n_free -= sqlcode_msg_len + 2;
    }
  }

  #ifdef USE_MODERN_INTERP_FUNC
  while ((cur_msg_size = (size_t) fb_interpret(trans_buf,
          MAX_ISC_ERROR_MESSAGE_BUFFER_SIZE, &sv_walk
        )) != 0)
    {
  #else
  while ((cur_msg_size = (size_t) isc_interprete(trans_buf, &sv_walk)) != 0) {
  #endif
    n_messages++;
    /* The message that the interpretation function placed in trans_buf is
     * supposed to be null-terminated. */
    assert (cur_msg_size == strlen(trans_buf));

    if (n_messages > 1) {
      cur_msg_size += 1; /* Allow for delimiting newline. */
    }

    if (_enlarge_message_if_necessary(se, cur_msg_size,
          &se_msg_size, &se_msg_n_free
        ) != 0
       )
    { goto fail; }
    assert (cur_msg_size <= se_msg_n_free);

    if (n_messages > 1) {
      /* Room for the newline was already factored into cur_msg_size, so
       * there's no need to decrement se_msg_n_free. */
      strcat(se->msg, "\n");
    }

    strncat(se->msg, trans_buf, cur_msg_size);
    se_msg_n_free -= cur_msg_size;

    assert (strlen(se->msg) == (se_msg_size - 1) - se_msg_n_free);
  } /* end of while(interpret function produces non-empty string) loop */

  assert (se->msg != NULL);
  /* Trim trailing whitespace: */
  for (;;) {
    const size_t msg_len = strlen(se->msg);
    if (msg_len == 0) {
      break;
    }{
    const char last_char = se->msg[msg_len - 1];
    if (last_char == '\n' || last_char == '\r' || last_char == ' ') {
      se->msg[msg_len - 1] = '\0';
    } else {
      break;
    }}
  }

  return se;

  fail:
    if (se != NULL) {
      NonPythonSQLErrorInfo_destroy(se);
    }
    return NULL;
} /* extract_sql_error_without_python */

static int _enlarge_message_if_necessary(
    NonPythonSQLErrorInfo *se, size_t next_segment_size,
    size_t *current_size, size_t *n_free
  )
{
  /* This function sometimes ENLARGES se->msg, but never SHRINKS it. */
  const size_t orig_size = *current_size;
  const size_t n_used = orig_size - *n_free;

  if (n_used + next_segment_size > orig_size) {
    char *larger_buf = kimem_plain_realloc(se->msg, orig_size + orig_size);
    if (larger_buf == NULL) {
      goto fail;
    } else {
      se->msg = larger_buf;
      *n_free += orig_size;
      *current_size += orig_size;
    }
  }

  return 0;

  fail:
    return -1;
} /* _enlarge_message_if_necessary */
