/* KInterbasDB Python Package - Implementation of "Exception-Raising" Functions
 *
 * 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>
 */

/******************** EXCEPTION FUNCTIONS:BEGIN ********************/

#ifndef _KINTERBASDB_EXCEPTION_FUNCTIONS_C
#define _KINTERBASDB_EXCEPTION_FUNCTIONS_C

#include "_kinterbasdb_exceptions.h"

static PyObject *exc_support__str_join;
static PyObject *exc_support__str_splitlines;
static PyObject *exc_support__str_startswith;

static PyObject *exc_support__str_exception_header_start;
static PyObject *exc_support__str_newline;
static PyObject *exc_support__str_spaces_2;
static PyObject *exc_support__str_tb_caption;

/* This file is included in both _kinterbasdb.c and _kiservices.c, which are
 * compiled in separate compilation units.  init_kidb_exception_support
 * therefore needs to be called once in *each* compilation unit.
 * The required functions could be made non-static and the _kiservices dynamic
 * lib could then be linked against the _kinterbasdb dynamic lib, but that's
 * not worth the complications to the build process. */
static int init_kidb_exception_support(void) {
  #define IKES_INIT_CACHED_STRING(var, str_const) \
    var = PyString_FromString(str_const); \
    if (var == NULL) { goto fail; }

  IKES_INIT_CACHED_STRING(exc_support__str_join, "join");
  IKES_INIT_CACHED_STRING(exc_support__str_splitlines, "splitlines");
  IKES_INIT_CACHED_STRING(exc_support__str_startswith, "startswith");

  IKES_INIT_CACHED_STRING(exc_support__str_exception_header_start,
      "exception "
    );
  IKES_INIT_CACHED_STRING(exc_support__str_newline, "\n");
  IKES_INIT_CACHED_STRING(exc_support__str_spaces_2, "  ");
  IKES_INIT_CACHED_STRING(exc_support__str_tb_caption,
      "SQL traceback (most recent call last):"
    );

  return 0;

  fail:
    /* This function is indirectly called by the module loader, which makes no
     * provision for error recovery. */
    return -1;
} /* init_kidb_exception_support */

void raise_sql_exception_exc_type_filter(
    PyObject *exc_type, const char *preamble, ISC_STATUS *status_vector,
    PyObject *filter
  )
{
  /* Given the type of exception to raise (exc_type), an introductory message
   * (preamble), and the status vector into which a Firebird API function has
   * stored numeric error codes (status_vector), set a Python exception bearing
   * the following payload:
   *   (int numeric database API error code, str error message) */
  const ISC_STATUS *status_vector_start = status_vector;

  char 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. */
  ISC_LONG db_error_code;

  #ifdef USE_MODERN_INTERP_FUNC
    const
  #endif
  ISC_STATUS *ptr_status_vector =
      #ifdef USE_MODERN_INTERP_FUNC
        (const ISC_STATUS *)
      #endif
      status_vector
    ;

  Py_ssize_t i;

  PyObject *py_msg = NULL;
  PyObject *py_msg_lines = NULL;

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

  py_msg_lines = PyList_New(0);
  if (py_msg_lines == NULL) { goto fail; }

  if (preamble != NULL) {
    PyObject *py_preamble = PyString_FromString(preamble);
    if (py_preamble == NULL) {
      goto fail;
    } else {
      const int append_res = PyList_Append(py_msg_lines, py_preamble);
      Py_DECREF(py_preamble);
      if (append_res != 0) { goto fail; }
    }
  }

  #define RSE__CLEAR_SEGMENT_PYVARS \
    if (py_segment != NULL) { \
      Py_DECREF(py_segment); \
      py_segment = NULL; \
    } \
    \
    if (py_segment_lines != NULL) { \
      Py_DECREF(py_segment_lines); \
      py_segment_lines = NULL; \
    }

  #define RSE__FAIL \
    assert (PyErr_Occurred()); \
    RSE__CLEAR_SEGMENT_PYVARS; \
    goto fail;

  {
    PyObject *py_segment = NULL;
    PyObject *py_segment_lines = NULL;

    ENTER_GDAL
    db_error_code = isc_sqlcode(status_vector);
    LEAVE_GDAL

    for (;;) {
      ISC_STATUS interp_result;

      ENTER_GDAL
      interp_result =
          #ifdef USE_MODERN_INTERP_FUNC
            fb_interpret(buf, MAX_ISC_ERROR_MESSAGE_BUFFER_SIZE,
                &ptr_status_vector
              )
          #else
            isc_interprete(buf, &ptr_status_vector)
          #endif
        ;
      LEAVE_GDAL

      if (interp_result == 0) {
        /* No more messages available. */
        break;
      }

      py_segment = PyString_FromString(buf);
      if (py_segment == NULL) { RSE__FAIL; }

      py_segment_lines = PyObject_CallMethodObjArgs(py_segment,
          exc_support__str_splitlines, NULL
        );
      if (py_segment_lines == NULL) { RSE__FAIL; }
      assert (PyList_CheckExact(py_segment_lines));

      if (
             ptr_status_vector - status_vector_start + 1 < STATUS_VECTOR_SIZE
          && *(ptr_status_vector + 1) == isc_stack_trace
         )
      {
        if (PyList_Append(py_segment_lines, exc_support__str_tb_caption) != 0)
        { RSE__FAIL; }
      }

      /* If an exception-type-filter was provided, call it now. */
      if (filter != NULL) {
        ISC_STATUS raw_code = 0;
        PyObject *py_raw_code = NULL;
        PyObject *py_sql_code = NULL;
        PyObject *py_filter_res = NULL;

        /* Extract the "raw" code from the status vector: */
        {
          /* These constants can be found in the src/include/gen/codes.h file
           * in a Firebird source distribution, but they're not included in
           * the normal headers. */
          const long gds_arg_end = 0;
          const long gds_arg_gds = 1;

          ISC_STATUS *sv = status_vector;
          Py_ssize_t sv_index = 0; /* Solely for bounds assertion below. */
          while (*sv != gds_arg_end) {
            assert (sv_index < STATUS_VECTOR_SIZE);
            if (*sv == gds_arg_gds) {
              raw_code = status_vector[1];
              break;
            }
            sv++;
            sv_index++;
          }
        }

        /* Create filter arguments: */
        /* 2007.02.10: FB 2.1 fix: ISC_STATUS can be > INT_MAX in FB 2.1: */
        py_raw_code = PythonIntOrLongFrom64BitValue(raw_code);
        if (py_raw_code == NULL) { goto exception_type_filter_finish; }

        py_sql_code = PyInt_FromLong(db_error_code);
        if (py_sql_code == NULL) { goto exception_type_filter_finish; }

        /* Call filter: */
        py_filter_res = PyObject_CallFunctionObjArgs(filter,
            py_raw_code, py_sql_code, py_segment_lines, NULL
          );
        if (py_filter_res == NULL) { goto exception_type_filter_finish; }
        if (py_filter_res != Py_None) {
          /* The filter has indicated that a different exception type should be
           * used. */

          /* We don't own a reference to exc_type, so although we'll keep a
           * reference to py_filter_res, we'll discard our ownership of it.
           * Because this feature is only used by kinterbasdb's internals, we
           * can guarantee the exception type won't be collected when we DECREF
           * it. */
          assert (py_filter_res->ob_refcnt >= 2);
          exc_type = py_filter_res;
        }
        Py_DECREF(py_filter_res);

        /* Fall through to finish: */
        exception_type_filter_finish:
          Py_XDECREF(py_raw_code);
          Py_XDECREF(py_sql_code);
          if (PyErr_Occurred()) { RSE__FAIL; }
      }

      for (i = 0; i < PyList_GET_SIZE(py_segment_lines); i++) {
        PyObject *line = PyList_GET_ITEM(py_segment_lines, i);
        boolean should_indent = TRUE;

        /* If the current line is the first in this segment, and it starts with
         * 'exception ', don't indent it: */
        if (i == 0) {
          PyObject *py_is_exception_header_line = PyObject_CallMethodObjArgs(
              line, exc_support__str_startswith,
              exc_support__str_exception_header_start, NULL
            );
          if (py_is_exception_header_line == NULL) {
            RSE__FAIL;
          } else {
            const int is_exception_header_line =
              PyObject_IsTrue(py_is_exception_header_line);
            Py_DECREF(py_is_exception_header_line);
            if (is_exception_header_line == -1) {
              RSE__FAIL;
            } else if (is_exception_header_line == 1) {
              should_indent = FALSE;
            }
          }
        }

        /* If the current line is the kinterbasdb-injected header for a SQL
         * traceback, don't indent it: */
        if (should_indent) {
          const int cmp_res = PyObject_Compare(exc_support__str_tb_caption,
              line
            );
          if (PyErr_Occurred()) { RSE__FAIL; }
          if (cmp_res == 0) {
            should_indent = FALSE;
          }
        }

        if (!should_indent) {
          if (PyList_Append(py_msg_lines, line) != 0) { RSE__FAIL; }
        } else {
          /* Establish a new reference to the string consisting of two spaces,
           * then concatenate the original line to it (our reference to the
           * original line is borrowed, so there's no need to use
           * PyString_ConcatAndDel). */
          PyObject *line_indented = exc_support__str_spaces_2;
          Py_INCREF(line_indented);
          PyString_Concat(&line_indented, line);
          if (line_indented == NULL) { RSE__FAIL; }

          {
            const int append_res = PyList_Append(py_msg_lines, line_indented);
            Py_DECREF(line_indented);
            if (append_res != 0) { RSE__FAIL; }
          }
        }
      } /* for line in py_segment_lines */

      RSE__CLEAR_SEGMENT_PYVARS;
    }
  }

  /* py_msg = '\n'.join(py_msg_lines) */
  py_msg = PyObject_CallMethodObjArgs(exc_support__str_newline,
      exc_support__str_join, py_msg_lines, NULL
    );
  if (py_msg == NULL) { goto fail; }

  { /* Raise an exception whose payload consists of a tuple of the form
     *   (error_code, error_message): */
    PyObject *exc_tuple = Py_BuildValue("(lO)", (long) db_error_code, py_msg);
    if (exc_tuple == NULL) { goto fail; }
    PyErr_SetObject(exc_type, exc_tuple);
    Py_DECREF(exc_tuple);
  }

  goto exit;

  fail:
    assert (PyErr_Occurred());
    /* Fall through to exit: */

  exit:
    Py_XDECREF(py_msg);
    Py_XDECREF(py_msg_lines);
    return;
} /* raise_sql_exception */

void raise_sql_exception(
    PyObject *exc_type, const char *preamble, ISC_STATUS *status_vector
  )
{
  raise_sql_exception_exc_type_filter(exc_type, preamble, status_vector, NULL);
}

static void raise_exception_with_numeric_error_code(
    PyObject *exc_type,
    const LONG_LONG error_code, /* 2007.02.10: FB 2.1 fix: long->LONG_LONG */
    const char *description
  )
{
  /* raise_exception_with_numeric_error_code allows the database API error code
   * to be set directly, rather than having it extracted from a status vector.
   * Thus, raise_exception_with_numeric_error_code might be said to "fall
   * midway between raise_sql_exception and raise_exception". */
  PyObject *exceptionTuple = Py_BuildValue("(Ls)", error_code, description);
  if (exceptionTuple == NULL) { return; }
  PyErr_SetObject(exc_type, exceptionTuple);
  Py_DECREF(exceptionTuple);
} /* raise_exception_with_numeric_error_code */

void raise_exception(PyObject *exc_type, const char *description) {
  raise_exception_with_numeric_error_code(exc_type, 0, description);
} /* raise_exception */

#define SUPPRESS_EXCEPTION \
  suppress_python_exception_if_any(__FILE__, __LINE__)

#define SUPPRESS_DB_API_ERROR(sv, preamble) \
  suppress_database_api_error(sv, preamble, __FILE__, __LINE)

static void suppress_python_exception_if_any(
    const char *file_name, const int line
  )
{
  if (PyErr_Occurred()) {
    fprintf(stderr, "kinterbasdb ignoring exception\n");
    fprintf(stderr, "  on line %d\n", line);
    fprintf(stderr, "  of file %s:\n  ", file_name);
    PyErr_Print();
    /* PyErr_Print cleared the exception: */
    assert (!PyErr_Occurred());
  }
} /* suppress_exception_if_any */

/******************** EXCEPTION FUNCTIONS:END ********************/

#endif /* _KINTERBASDB_EXCEPTION_FUNCTIONS_C */
