/* $Id: irplib_error.c,v 1.10 2008-02-27 09:43:52 rhaigron Exp $
 *
 * This file is part of the irplib package
 * Copyright (C) 2002,2003,2004,2005,2006 European Southern Observatory
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02111-1307  USA
 */

/*
 * $Author: rhaigron $
 * $Date: 2008-02-27 09:43:52 $
 * $Revision: 1.10 $
 * $Name: not supported by cvs2svn $
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

/*-----------------------------------------------------------------------------
                                   Includes
 -----------------------------------------------------------------------------*/

#include "irplib_error.h"
#include <string.h>
#include <cpl.h>
#include <cxutils.h>
#include <stdarg.h>
/*----------------------------------------------------------------------------*/
/**
 * @defgroup irplib_error   IRPLIB error handling
 * @ingroup xsh_error
 */
/*----------------------------------------------------------------------------*/
/**@{*/

/*-----------------------------------------------------------------------------
                                   Define
 -----------------------------------------------------------------------------*/
/*
 *  As the error handler itself should never fail, it cannot not rely on
 *  allocating memory dynamically. Therefore, define max limits for
 *  string lengths, size of error queue.
 */

#define MAX_STRING_LENGTH 200
#define MAX_ERRORS 20
/* MAX_ERRORS is the maximum recursion depth
   for error tracking. If the call-tree exeeds this limit,
   the deepest errors are removed (as the user is probably more interested
   in the higher level errors)
*/
/*-----------------------------------------------------------------------------
                                   Typedefs
 -----------------------------------------------------------------------------*/

typedef struct
{
  char filename[MAX_STRING_LENGTH];
  char function[MAX_STRING_LENGTH];
  unsigned int linenumber;
  cpl_error_code errorcode;
  char errormessage[MAX_STRING_LENGTH];
  char cplmessage[MAX_STRING_LENGTH];
} irplib_error;

/*-----------------------------------------------------------------------------
                                   Local variables
 -----------------------------------------------------------------------------*/

/* The error queue */
static struct
{
  irplib_error errors[MAX_ERRORS];
  cpl_boolean is_empty;
  unsigned int first;		/* Index of first error (inclusive) */
  unsigned int last;		/* Index of last  error (inclusive) */

  /* Invariant: 
     The last error in the queue matches
     the current CPL error state.

     This will be violated if the user calls
     cpl_error_set() or cpl_error_reset() or friends
     (directly).

     To (re-)establish this invariant
     irplib_error_validate_state()
     is called at the entry of every function
   */

} queue;

/* Used only to avoid variadic macros which might not be supported
   on some platforms */
static char error_msg[MAX_STRING_LENGTH];

/*
 *  This module uses static memory; therefore it must be initialized
 *  (by calling irplib_error_reset()) before use.
 *  Complain loudly if the caller forgot.
 */
static cpl_boolean is_initialized = CPL_FALSE;

/*-----------------------------------------------------------------------------
                                   Functions code
 -----------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------*/
/**
   @brief   Synchronize IRPLIB error state with CPL's error state
   @param   func          Error handler was called from this function
   @param   file          Error handler was called from this file
   @param   line          Error handler was called from this linenumber

   This function
   - checks that the error module has been properly initialized and
   prints an error message if not,
   - makes sure that the latest error in the queue matches CPL's error state.
   (If the cpl_error_code is set, it must match the latest error in the queue;
   otherwise it's inserted into the queue. If the cpl_error_code is not set, 
   the queue must be empty; otherwise it is cleared. )

*/
/*----------------------------------------------------------------------------*/
static void
irplib_error_validate_state (const char *func, const char *file,
			     unsigned int line)
{
  if (!is_initialized) {
    cpl_msg_error (__func__, "Error handling system was not initialized "
		   "when called from %s:%s:%d!", file, func, line);
    return;
  }
  if (cpl_error_get_code () == CPL_ERROR_NONE) {
    if (queue.is_empty) {
      /* Fine */
    }
    else {
      /* The cpl_error_code has been reset without resetting
         the the queue.
       */
      xsh_irplib_error_reset ();
    }
  }
  else {
    /* An error code is set. Verify that it matches the
       latest error in the queue (which might not be the
       case if the error was set without using irplib_error_push(). 
     */
 
    irplib_error er;
    int is_mismatch = 0;

    
    if (!queue.is_empty) {
      er = queue.errors[queue.last];
      is_mismatch = !(strcmp (er.filename, cpl_error_get_file ()) == 0 &&
                      strcmp (er.function, cpl_error_get_function ()) == 0 &&
                      strcmp (er.cplmessage, cpl_error_get_message ()) == 0 &&
                      /* Don't match er.errormessage */
                      er.errorcode == cpl_error_get_code () &&
                      er.linenumber == cpl_error_get_line ()
                    );
    }

    /* if mismatch... */
    if (queue.is_empty || is_mismatch) {
      /* Insert this error into the queue */

      /* But to avoid infinite recursion (as we might be
         called from irplib_error_push_macro):
         1. Store the current CPL error state in a safe place.
         2. Set/reset CPL's error state to match the queue.
         3. Insert the previously stored state.
         4. Also, be careful to leave 
         error_msg[MAX_STRING_LENGTH] unchanged.
       */

      char file_cpl[MAX_STRING_LENGTH];
      char func_cpl[MAX_STRING_LENGTH];
      cpl_error_code ec_cpl;
      unsigned int line_cpl;

      char message_local[MAX_STRING_LENGTH];

      /* 1 */

      strncpy (file_cpl, cpl_error_get_file (), MAX_STRING_LENGTH - 1);
      file_cpl[MAX_STRING_LENGTH - 1] = '\0';

      strncpy (func_cpl, cpl_error_get_function (), MAX_STRING_LENGTH - 1);
      func_cpl[MAX_STRING_LENGTH - 1] = '\0';

      ec_cpl = cpl_error_get_code ();
      line_cpl = cpl_error_get_line ();

      /* 2 */
      if (queue.is_empty) {
	cpl_error_reset ();
      }
      else {
	cpl_error_set_message_macro (er.function,
			     er.errorcode, er.filename, er.linenumber," ");
      }

      /* 3 + 4 */
      strncpy (message_local, error_msg, MAX_STRING_LENGTH - 1);
      message_local[MAX_STRING_LENGTH - 1] = '\0';


      xsh_irplib_error_set_msg (" ");
      xsh_irplib_error_push_macro (func_cpl, ec_cpl, file_cpl, line_cpl);
      xsh_irplib_error_set_msg ("%s", message_local);

    }				/* mismatch with CPL's error state */

  }				/* cpl_error_code is set */

  return;
}

/*----------------------------------------------------------------------------*/
/**
   @brief   Set or propagate an error
   @param   ec           The error code
   @param   file         Filename
   @param   func         Function name
   @param   line         Line number
   @return  The provided error code

   This function should not be called directly, use @c irplib_error_push() .

   The function inserts an error into the (empty or non-empty) error queue,
   and sets the CPL error state to @em ec.

   If the error code @em ec is equal to CPL_ERROR_NONE (which is not considered
   to be a valid error code), an error message is displayed (because it is
   considered a bug in the caller), and a CPL_ERROR_UNSPECIFIED is set instead.
   
   It uses the error message previously set by @c irplib_error_set_msg() .

*/
/*----------------------------------------------------------------------------*/

cpl_error_code
xsh_irplib_error_push_macro (const char *func,
			     cpl_error_code ec,
			     const char *file, unsigned int line)
{
  irplib_error_validate_state (func, file, line);

  if (ec == CPL_ERROR_NONE) {
    cpl_msg_error (__func__, "The error code CPL_ERROR_NONE was set from "
		   "%s:%s:%d! Code changed to CPL_ERROR_UNSPECIFIED",
		   file, func, line);

    ec = CPL_ERROR_UNSPECIFIED;
  }

  /* Compute new first/last indices */
  if (queue.is_empty) {
    /* First error */
    queue.first = 0;
    queue.last = 0;
  }
  else {
    /* If queue is full */
    if ((queue.last + 1) % MAX_ERRORS == (queue.first % MAX_ERRORS)) {
      /* Delete oldest error */
      queue.first = (queue.first + 1) % MAX_ERRORS;
    }
    queue.last = (queue.last + 1) % MAX_ERRORS;
  }
  queue.is_empty = CPL_FALSE;
  cpl_error_set_message_macro (func, ec, file, line," ");
  
  /* Insert current error into the queue
   *
   * Make sure that the target string is always 0-terminated.
   * This is not guaranteed by strncpy.
   */
  strncpy (queue.errors[queue.last].filename, file, MAX_STRING_LENGTH - 1);
  strncpy (queue.errors[queue.last].function, func, MAX_STRING_LENGTH - 1);
  strncpy (queue.errors[queue.last].cplmessage, cpl_error_get_message (),
	   MAX_STRING_LENGTH - 1);
  strncpy (queue.errors[queue.last].errormessage, error_msg,
	   MAX_STRING_LENGTH - 1);
  queue.errors[queue.last].filename[MAX_STRING_LENGTH - 1] = '\0';
  queue.errors[queue.last].function[MAX_STRING_LENGTH - 1] = '\0';
  queue.errors[queue.last].cplmessage[MAX_STRING_LENGTH - 1] = '\0';
  queue.errors[queue.last].errormessage[MAX_STRING_LENGTH - 1] = '\0';

  queue.errors[queue.last].linenumber = line;
  queue.errors[queue.last].errorcode = ec;

  return ec;
}

/*----------------------------------------------------------------------------*/
/**
   @brief   Temporarily store an error message
   @param   format         printf-style format string

   This function stores an error message to be used later by 
   @c irplib_error_push_macro() .

   Neither of these functions should be called directly.
   Use @c irplib_error_push() .
*/
/*----------------------------------------------------------------------------*/
void
xsh_irplib_error_set_msg (const char *format, ...)
{
  va_list al;

  va_start (al, format);
  /* vsnprintf is C99, so use cx_vsnprintf
     vsnprintf(error_msg, MAX_STRING_LENGTH, format, al);
   */
  cx_vsnprintf (error_msg, MAX_STRING_LENGTH, format, al);
  va_end (al);
}

/*----------------------------------------------------------------------------*/
/**
   @brief   Reset the error state

   This function empties the error queue and resets the current 
   cpl_error_code to CPL_ERROR_NONE.

*/
/*----------------------------------------------------------------------------*/
void
xsh_irplib_error_reset ( void )
{
  cpl_error_reset ();
  queue.is_empty = CPL_TRUE;
  error_msg[0] = '\0';

  is_initialized = CPL_TRUE;
}

/*----------------------------------------------------------------------------*/
/**
   @brief   Print the error queue 
   @param   func             Caller function
   @param   file             Caller filename
   @param   line             Caller linenumber
   @param   severity         The error message is printed using this
                             message level (usually CPL_MSG_ERROR or
			     CPL_MSG_WARNING)
   @param   trace_severity   The error tracing information is printed
                             using this message level (e.g. CPL_MSG_ERROR,
			     CPL_MSG_DEBUG or CPL_MSG_OFF)
   
   This function should not be called directly. 
   Use @c irplib_error_dump()

*/
/*----------------------------------------------------------------------------*/
void
xsh_irplib_error_dump_macro (const char *func,
			     const char *file,
			     unsigned int line,
			     cpl_msg_severity severity,
			     cpl_msg_severity trace_severity)
{
  /* Pointing to the CPL messaging functions
     that correspond to the specified severities: 
   */
  void (*error_msg_func) (const char *, const char *, ...);
  void (*trace_msg_func) (const char *, const char *, ...);

  irplib_error_validate_state (func, file, line);

  switch (severity) {
  case CPL_MSG_DEBUG:
    error_msg_func = (void(*)(const char*,const char*,...))&cpl_msg_debug;
    break;
  case CPL_MSG_INFO:
    error_msg_func = (void(*)(const char*,const char*,...))&cpl_msg_info;
    break;
  case CPL_MSG_WARNING:
    error_msg_func = (void(*)(const char*,const char*,...))&cpl_msg_warning;
    break;
  case CPL_MSG_ERROR:
    error_msg_func = (void(*)(const char*,const char*,...))&cpl_msg_error;
    break;
  case CPL_MSG_OFF:
    error_msg_func = NULL;
    break;
  default:
    cpl_msg_error (func, "Unknown message level: %d !", severity);
    error_msg_func = (void(*)(const char*,const char*,...))&cpl_msg_error;
    break;
  }

  switch (trace_severity) {
  case CPL_MSG_DEBUG:
    trace_msg_func = (void(*)(const char*,const char*,...))&cpl_msg_debug;
    break;
  case CPL_MSG_INFO:
    trace_msg_func = (void(*)(const char*,const char*,...))&cpl_msg_info;
    break;
  case CPL_MSG_WARNING:
    trace_msg_func = (void(*)(const char*,const char*,...))&cpl_msg_warning;
    break;
  case CPL_MSG_ERROR:
    trace_msg_func = (void(*)(const char*,const char*,...))&cpl_msg_error;
    break;
  case CPL_MSG_OFF:
    trace_msg_func = NULL;
    break;
  default:
    cpl_msg_error (func, "Unknown message level: %d !", severity);
    trace_msg_func = (void(*)(const char*,const char*,...))&cpl_msg_error;
    break;
  }

  if (cpl_error_get_code () == CPL_ERROR_NONE) {
    if (error_msg_func != NULL) {
      error_msg_func (func, "No error has occurred");
    }
  }
  else {
    int i;
    cpl_error_code current_ec;

    /* Don't reset indentation:  cpl_msg_indent(0); 
       This will interfere with error-recovery. It is anyway
       the responsibility of the client never to change the net
       indentation, also when an error occurs (so don't try to
       hide such bug).
     */

    if (trace_msg_func != NULL) {
      trace_msg_func (func, "An error occurred, " "dumping error trace:");
      trace_msg_func (func, " ");
    }

    /* 
     * Print errors from first to last
     * (both inclusive, and maybe wrap around) 
     */
    current_ec = CPL_ERROR_NONE;
    i = queue.first - 1;
    do {
      const char *c;
      cpl_boolean empty_message;

      i = (i + 1) % MAX_ERRORS;

      c = queue.errors[i].errormessage;
      empty_message = CPL_TRUE;
      while (*c != '\0') {
	empty_message = empty_message && (*c == ' ');
	c++;
      }

      /* There are 2x2 cases to consider
         Was a non-empty error message was provided?
         Did the cpl_error_code change from the previous
         (lower) level? */
      if (empty_message) {
	/* No error message provided, print the standard
	   CPL error message */
	if (error_msg_func != NULL) {
	  error_msg_func (func, "%s", queue.errors[i].cplmessage);
	}
      }
      else if (queue.errors[i].errorcode == current_ec) {
	/* Error message provided, don't repeat error code */
	if (error_msg_func != NULL) {
	  error_msg_func (func, "%s", queue.errors[i].errormessage);
	}
      }
      else {
	/* Error message provided, error code different
	   from previous (lower) level */
	if (error_msg_func != NULL) {
	  error_msg_func (func, "%s (%s)",
			  queue.errors[i].errormessage,
			  queue.errors[i].cplmessage);
	}
      }

      if (trace_msg_func != NULL) {
	trace_msg_func (func, " in [%d]%s() at %s:%-3d",
			/* Here errors are numbered
			   according to depth: from N to 1 */
			((queue.last - i + MAX_ERRORS) %
			 MAX_ERRORS) + 1,
			queue.errors[i].function,
			queue.errors[i].filename, queue.errors[i].linenumber);
	trace_msg_func (func, " ");
      }

      current_ec = queue.errors[i].errorcode;

    } while ((unsigned int) i != queue.last);

  }				/* If an error occurred */
}

/**@}*/
