/*
 * (c) 2005 Research Group Dr. Fuhrmann, University of Karlsruhe, Germany
 * All rights reserved.
 *
 * Author:
 *         Roman Krenicky
 *
 * $Id: epgigor_interface.cpp,v 1.19 2005/11/23 17:51:35 kutzner Exp $
 *
 */

/*
  epgigor_interface.cpp
  created: 1.12.2004
  last change: 30.7.2005
*/
/*! @file
    interface to the IGOR, adapted to the EPG part
*/

#include "globalheader.h"

#include <sstream>
#include <tools.h>
#include "epgigor_interface.h"
#include "kv_parsing.h"
#include "pluginname.h"
extern "C"
{
  #include <string.h>
  #include <stdlib.h>
}

//! our IGOR namespace/application type
#define APPLICATION_TYPE_EPG        "VDR_EPG"
//! subtype of VDR_EPG messages
#define EPG_MSGTYPE_DATA_LABEL      "data"
//! subtype of VDR_EPG messages
#define EPG_MSGTYPE_REQUEST_LABEL   "request"

//! incoming messages larger than @c MAX_RECV_MESSAGE_LENGTH bytes are dropped
#define MAX_RECV_MESSAGE_LENGTH     100000


/*! just a constructor, tries to open a IGOR socket

    @c use initialised() to check, whether the constructor could perform all
    of its tasks correctly
*/
epgigor_interface::epgigor_interface (void)  :  cThread("igorepg - listener")
{
   listener = NULL;
   socket_descriptor = -1;  // reconnect() has to see, we're not connected!
   in_bytes = out_bytes = 0;
   last_sendon_candidate_size = 0;
   reconnect();
} // epgigor_interface()


/*! just a destructor, stops the epgigor_interface thread if it is still running
*/
epgigor_interface::~epgigor_interface (void)
{
   if (listener != NULL)
      deregister_rcvr (listener);
   if (socket_descriptor >= 0)
      igor_close (socket_descriptor);
} // ~epgigor_interface()


/*! @brief (re)estabishes a connection to the IGOR
    @return @c true on success, @c false if an error occured

    only connects if currently disconnected
*/
bool epgigor_interface::reconnect (void)
{
   if (connected())
      return true;

   igor_apptype socket_type;
   socket_type.apptype = APPLICATION_TYPE_EPG;
   socket_type.len = strlen (socket_type.apptype);
   socket_type.mustbehashed = 1;
   int result;

   last_sendon_candidate_size = 0;
   socket_descriptor = igor_socket();
   if (socket_descriptor < 0)
      esyslog ("%s plugin: failed to create a IGOR socket", PLUGINNAME);
   else   
   {
      result = igor_bind (socket_descriptor, &socket_type);
      if (result < 0)
      {
         esyslog ("%s plugin: failed to bind the IGOR socket (%s)",
                  PLUGINNAME, igor_strerror_r(result));
         igor_close (socket_descriptor);
         socket_descriptor = -1;
      } // if (result < 0)
   } // if (socket_descriptor >= 0)


   return connected();
} // reconnect()


/*! @brief validity check
    @return @c true iff the object has been constructed correctly
*/
bool epgigor_interface::initialised (void)
{
   return (socket_descriptor >= 0);
} // initialised()


/*! @brief check connection
    @return @c false if the object is not connected to the IGOR
    @note this is no precise test, the return value might be incorrect, but
          should be reliable if is says @c false
*/
bool epgigor_interface::connected (void)
{
   return (socket_descriptor >= 0);
} // connected()


/*! @brief stops the epgigor_interface thread if it is running
*/
void epgigor_interface::Stop (void)
{
   Cancel (0);
} // Stop()


/*! @brief determines the message (sub-)type of a VDR_EPG message
    @param msg  the IGOR message data
    @param len  the size of @a msg
    @return the message (sub-)type
*/
epg_msgtype epgigor_interface::get_epg_msgtype (const char *msg, size_t len)
{
   map<string,string>* entries;
   string type;

   if (msg == NULL)
      return epg_msgtype_none;

   entries = parse_kvd (msg, len);
   if (entries == NULL)
      return epg_msgtype_none;
   type = findvalue_kv (*entries, "type");
   delete entries;  entries = NULL;

   if (type == EPG_MSGTYPE_DATA_LABEL)
      return epg_msgtype_data;
   if (type == EPG_MSGTYPE_REQUEST_LABEL)
      return epg_msgtype_request;
   return epg_msgtype_none;
} // get_epg_msgtype (...)


/*! @fn time_t epgigor_interface::normalise_timeblock (time_t timeblock);
    @brief normalises a time block identifier

    the normalised value is to be used in IGOR messages and it's the timeblock
    meant if we should happen to receive a non-normalised one
    @param timeblock  the value to normalise, i.e. a point in time inside the
           timeblock meant
    @return the normalised representation of the @a timeblock
*/
// time_t epgigor_interface::normalise_timeblock (time_t timeblock);


/*! @brief converts (interprets) an @c epg_msgtype::epg_msgtype_data message
    @param msg  the IGOR message data (may be NULL but this is considered an
                error)
    @param len  the size of @a msg
    @param channel  the message's 'channel' parameter is stored here
    @param timeblock  the message's 'timeblock' parameter is stored here
    @param update  will be @c true iff the message is the response to an
                   update request (i.e. has a "mode: update" entry)
    @param data  the message's EPG data is stored here
    @return @c true iff convertion has been successfull
    @warning the passed message has to be of the expected type, no test is
             made in here !
*/
bool epgigor_interface::extract_data_msg (const char *msg, size_t len,
      epgchannel *channel, time_t *timeblock, bool *update, epgdata *data)
{
   /* ---------------------------------------------
      VDR_EPG_DATA:
        "type: ", EPG_MSGTYPE_DATA_LABEL(e.g. "data"), '\n'
        "channel: ", <channel>, '\n'
        "timeblock: ", <timeblock> (time_t as string), '\n'
        "mode: update\n"  // for update responses
        '\n'
        <epg data>
   -* --------------------------------------------- */

   bool retval = false;
   map<string,string>* entries = NULL;
   const char *epg_data_ptr, *endptr;
   string timeblock_string;

   if ( (msg == NULL) || (channel == NULL) || (timeblock == NULL) ||
        (update == NULL) || (data == NULL) )
      return false;

   do
   {
      // parse:
      entries = parse_kvd (msg, len, &epg_data_ptr);
      if ( (entries == NULL) || (epg_data_ptr == NULL) )
         break;
      // get channel:
      *channel = findvalue_kv (*entries, "channel");
      if (channel->empty())
         break;
      // get timeblock:
      timeblock_string = findvalue_kv (*entries, "timeblock");
      if (timeblock_string.empty())
         break;
      *timeblock = (time_t)strtoul (timeblock_string.c_str(),
                                    (char**)&endptr, 10);
      if ( (*endptr != '\0') || (*timeblock == 0) )
         break;
      // get mode (ignore unknown ones):
      if ( findvalue_kv(*entries, "mode") == "update" )
         *update = true;
      else
         *update = false;
      // ignore further key-value entries (which might come in the future)
      delete entries;  entries = NULL;

      // extract the EPG data:
      data->data.str ("");  // clear
      data->data.write (epg_data_ptr, len - (size_t)(epg_data_ptr-msg));

      retval = true;  // extracting succeeded
   } while (false);

   if (entries != NULL)
      delete entries;
   return retval;
} // extract_data_msg (...)


/*! @brief converts (interprets) an @c epg_msgtype::epg_msgtype_request message
    @param msg  the IGOR message data
    @param len  the size of @a msg
    @param channel  the message's 'channel' parameter is stored here
    @param timeblock  the message's 'timeblock' parameter is stored here
    @param update  will be @c true iff the message is an update request
                   (i.e. has a "mode: update" entry)
    @return @c true iff convertion has been successfull
    @warning the passed message has to be of the expected type, no test is
             made in here !
*/
bool epgigor_interface::extract_request_msg (const char *msg, size_t len,
      epgchannel *channel, time_t *timeblock, bool *update)
{
   /* ---------------------------------------------
      VDR_EPG_REQ:
        "type: ", EPG_MSGTYPE_REQUEST_LABEL(e.g. "request"), '\n'
        "channel: ", <channel>, '\n'
        "timeblock: ", <timeblock> (time_t as string), '\n'
        "mode: update\n"  //  for update requests
   -* --------------------------------------------- */

   bool retval = false;
   map<string,string>* entries = NULL;
   const char *endptr;
   string timeblock_string;

   if ( (msg == NULL) || (channel == NULL) || (timeblock == NULL) ||
        (update == NULL) )
      return false;

   do
   {
      // parse:
      entries = parse_kvd (msg, len, &endptr);
      if ( (entries == NULL) || (endptr != NULL) )
         break;
      // get channel:
      *channel = findvalue_kv (*entries, "channel");
      if (channel->empty())
         break;
      // get timeblock:
      timeblock_string = findvalue_kv (*entries, "timeblock");
      if (timeblock_string.empty())
         break;
      *timeblock = (time_t)strtoul (timeblock_string.c_str(),
                                    (char**)&endptr, 10);
      if ( (*endptr != '\0') || (*timeblock == 0) )
         break;
      // get request mode (ignore unknown ones):
      if ( findvalue_kv(*entries, "mode") == "update" )
         *update = true;
      else
         *update = false;
      // ignore further key-value entries (which might come in the future)

      retval = true;  // extracting succeeded
   } while (false);

   if (entries != NULL)
      delete entries;
   return retval;
} // extract_request_msg (...)


/*! @brief creates an @c epg_msgtype::epg_msgtype_data message
    @param channel  the message's 'channel' parameter
    @param timeblock  the message's 'timeblock' parameter
    @param update  iff this is @c true, the response to an update request
                   will be created
    @param data  the message's EPG data
    @param msg  the IGOR message data is stored here
    @return @c true on success, @c false if an error occured
*/
bool epgigor_interface::create_data_msg (const epgchannel &channel,
      time_t timeblock, bool update, const epgdata &data, string *msg)
{
   /* ---------------------------------------------
      VDR_EPG_DATA:
        "type: ", EPG_MSGTYPE_DATA_LABEL(e.g. "data"), '\n'
        "channel: ", <channel>, '\n'
        "timeblock: ", <timeblock> (time_t as string), '\n'
        "mode: update\n"  // for update responses
        '\n'
        <epg data>
   -* --------------------------------------------- */

   if (msg == NULL)
      return false;
   msg->erase();


   ((*msg += "type: ") += EPG_MSGTYPE_DATA_LABEL) += '\n';
   ((*msg += "channel: ") += channel) += '\n';
   // 'timeblock' has to be converted to a string:
   *msg += "timeblock: ";
   *msg += ((ostringstream &)(ostringstream() << timeblock)).str(); // num2str
      *msg += '\n';
   if (update)
      *msg += "mode: update\n";
   *msg += '\n';
   *msg += data.data.str();

   return true;
} // create_data_msg (...)


/*! @brief creates an @c epg_msgtype::epg_msgtype_request message
    @param channel  the message's 'channel' parameter
    @param timeblock  the message's 'timeblock' parameter
    @param update  iff this is @c true, an update request will be created
    @param msg  the IGOR message data is stored here
    @return @c true on success, @c false if an error occured
*/
bool epgigor_interface::create_request_msg (const epgchannel &channel,
      time_t timeblock, bool update, string *msg)
{
   /* ---------------------------------------------
      VDR_EPG_REQ:
        "type: ", EPG_MSGTYPE_REQUEST_LABEL(e.g. "request"), '\n'
        "channel: ", <channel>, '\n'
        "timeblock: ", <timeblock> (time_t as string), '\n'
        "mode: update\n"  //  for update requests
   -* --------------------------------------------- */

   if (msg == NULL)
      return false;
   msg->erase();
   ((*msg += "type: ") += EPG_MSGTYPE_REQUEST_LABEL) += '\n';
   ((*msg += "channel: ") += channel) += '\n';
   // 'timeblock' has to be converted to a string:
   *msg += "timeblock: ";
   *msg += ((ostringstream &)(ostringstream() << timeblock)).str(); // num2str
      *msg += '\n';
   if (update)
      *msg += "mode: update\n";

   return true;
} // create_request_msg (...)


/*! @brief determines the IGOR destination assotiated with a channel and
           time block
    @param channel  the channel asked for
    @param timeblock  a timestamp in the time block asked for
    @return the IGOR destination of @a channel and @a timeblock

    the destination can be packed in a @c sa_igor structure and used in
    @c igor_sendto()
*/
string epgigor_interface::calculate_destination (const epgchannel &channel,
      time_t timeblock)
{
   string destination, igor_channel_id;

   // normalise time block identifier:
   timeblock = normalise_timeblock (timeblock);

   // map the channel to the corresponding IGOR-channel-id
   if ( !epgchannel2igorchannelid(channel, &igor_channel_id) )
      return destination;

   destination += APPLICATION_TYPE_EPG;
   destination += '_';
   destination += igor_channel_id;
   destination += '_';
   // convert the number to a string:
   destination +=
            ((ostringstream &)(ostringstream() << (unsigned)timeblock)).str();
   return destination;
} // calculate_destination(...)


/*! @brief packs a IGOR address string into the appropriate structure
    @param key  the string to pack
    @param address  the result is stored here
    @return @c true on success, @c false if an error occurred
    @warning @a address is valid only as long as @a key is @em not @em changed
             or even destroyed

    @a address can be used in @c igor_sendto()
*/
bool epgigor_interface::pack_igor_address (const string &key, sa_igor *address)
{
   if ( (address == NULL) || key.empty() )
      return false;

   address->address = (char*)key.data();
   address->len = key.size();
   address->mustbehashed = 1;

   return true;
} // pack_igor_address (...)


/*! @brief packs a IGOR address string into the appropriate structure
    @param key  the string to pack
    @param address  the result is stored here
    @return @c true on success, @c false if an error occurred
    @warning @a address is valid only as long as @a key is @em not @em changed
             or even destroyed

    @a address can be used in @c igor_sendto()
*/
bool epgigor_interface::pack_igor_address (const char *key, sa_igor *address)
{
   size_t keylen;

   if ( (address == NULL) || (key == NULL) )
      return false;
   keylen = strlen (key);
   if (keylen == 0)
      return false;

   address->address = (char*)key;
   address->len = keylen;
   address->mustbehashed = 1;

   return true;
} // pack_igor_address (...)


/*! @brief receives, parses and passes on incoming messages
    @note this method may returns if the IGOR can't be accessed
*/
void epgigor_interface::Action(void)
{
   // the thread's activity is receiving incoming IGOR messages and passing
   // them to the 'listener' via callback

   /* INTERNAL NOTE:
      don't receive further messages, before one call to
      receiver::receive_###() returns (see doc. for 'register_rcvr()')
   */

   char *message;
   size_t message_length;
   bool final;
   sa_igor source, destination;
   epg_msgtype message_type;
   epgchannel channel;
   time_t timeblock;
   bool update;
   epgdata epg_data;
   int recv_error;

   // the (igorlib)library will allocate for us:
   message = NULL;  message_length = 0;
   source.address = NULL;  source.len = 0;
   destination.address = NULL;  destination.len = 0;

   // we don't return if no error occurs:
   while (true)
   {
      if (socket_descriptor < 0)
      {
         esyslog ("%s plugin: not connected to IGOR", PLUGINNAME);
         return;  // terminate thread
      } // if (socket_descriptor < 0)
      // we want the library to allocate the memory:
      /* message = NULL; message_length = 0; (should be!) */
      recv_error = igor_recvfromto (socket_descriptor,
                                   (void**)&message, &message_length,
                                   &final, &source, &destination);
      if (recv_error < 0)
      {
         esyslog ("%s plugin: receiving from IGOR failed (%s)", PLUGINNAME,
                  igor_strerror_r(recv_error));
         if (recv_error == IGOR_ERR_NOTCONNECTED)
         {
            igor_close (socket_descriptor);
            socket_descriptor = -1;
         } // if (recv_error == IGOR_ERR_NOTCONNECTED)
         return;  // terminate thread
      } // if (recv_error < 0)

      in_bytes_mutex.Lock();
      in_bytes += message_length;
      in_bytes_mutex.Unlock();
      if (!final)
         last_sendon_candidate_size = message_length;

      // just to go for sure:
      if (listener == NULL)
         return;  // terminate thread

      // only accept messages up to a maximal size:
      if (message_length <= MAX_RECV_MESSAGE_LENGTH)
      {
         // decode message and call 'listener'
         message_type = get_epg_msgtype (message, message_length);
         switch (message_type)
         {
            case epg_msgtype_data:
                  if ( !extract_data_msg(message, message_length, &channel,
                                         &timeblock, &update, &epg_data) )
                  {
                     esyslog ("%s plugin: received invalid data message",
                              PLUGINNAME);
                     break;
                  } // if ( !extract_data_msg(...) )
                  // normalise time block identifier:
                  timeblock = normalise_timeblock (timeblock);
                  listener->receive_data (channel, timeblock, update, epg_data,
                                          final);
                  break;
            case epg_msgtype_request:
                  if ( !extract_request_msg(message, message_length,
                                            &channel, &timeblock, &update) )
                  {
                     esyslog ("%s plugin: received invalid request message",
                              PLUGINNAME);
                     break;
                  } // if ( !extract_request_msg(...) )
                  // normalise time block identifier:
                  timeblock = normalise_timeblock (timeblock);
                  listener->receive_req(channel,timeblock,update,source,final);
                  break;
            default:  // should be 'epg_msgtype_none'
                  esyslog ("%s plugin: received unexpected message type",
                           PLUGINNAME);
         } // switch (message_type)
      } // if (message_length <= MAX_RECV_MESSAGE_LENGTH)
      else
         esyslog ("%s plugin: dropping large incoming message", PLUGINNAME);

      // don't forget to free the memory again:
      if (message != NULL)
         {  free (message);  message = NULL;  message_length = 0;  }
      if (source.address != NULL)
         {  free ((void*)source.address);  source.address = NULL;
            source.len = 0;  }
      if (destination.address != NULL)
         {  free ((void*)destination.address);  destination.address = NULL;
            destination.len = 0;  }
   } // while (true)
} // Action()


/*! @brief registers a @c receiver for receiving VDR_EPG messages from the IGOR
    @param listener  the @c receiver to register
    @note resets the counters for received and transmitted bytes (accessible
          via @c get_bytes_received() and @c get_bytes_sent()
    @note no further message is received, before one call to
          @c receiver::receive_###() returns, so
          @c epgigor_interface::send_on_last_received_message() may be called
    @warning only to be called once!
*/
void epgigor_interface::register_rcvr (receiver *listener)
{
   if ( (listener != NULL) && (this->listener == NULL) )
   {
      this->listener = listener;
      in_bytes = out_bytes = 0;
      // we start the listening thread only if there's a listener:
      Start();
   } // if ( (listener != NULL) && (this->listener == NULL) )
} // register_rcvr (...)


/*! @brief deregisters a @c receiver
    @param listener  a previously registered @c receiver (@c register_rcvr() )
    @warning only to be called once!
*/
void epgigor_interface::deregister_rcvr (const receiver *listener)
{
   if ( (this->listener != NULL) && (listener == this->listener) )
   {
      // we stop the listening thread if there's no listener:
      Cancel (0);
      this->listener = NULL;
   } // if ( (this->listener != NULL) && (listener == this->listener) )
} // deregister_rcvr (...)


/*! @brief sends EPG data to the IGOR
    @param channel  channel the EPG data belongs to
    @param timeblock  starttime of the data, normalised
    @param data  the EPG data, should be exactly one time block
    @return @c true on success, @c false if an error occurred
    @note syslog-s a message on error
*/
bool epgigor_interface::send_data (const epgchannel &channel, time_t timeblock,
      const epgdata &data)
{
   string message, dest_string;
   sa_igor destination;
   int send_error;

   // normalise time block identifier:
   timeblock = normalise_timeblock (timeblock);
   dest_string = calculate_destination (channel, timeblock);
   if ( dest_string.empty() )
      return false;
   if ( !pack_igor_address(dest_string, &destination) )
      return false;
   if (!create_data_msg(channel, timeblock, /*update*/ false, data, &message))
      return false;
   if (socket_descriptor < 0)
   {
      esyslog ("%s plugin: not connected to IGOR", PLUGINNAME);
      return false;
   } // if (socket_descriptor < 0)
   send_error = igor_sendto (socket_descriptor, (const void*)message.data(),
                            message.size(), /* upcall */ true, &destination);
   if (send_error < 0)
   {
      esyslog ("%s plugin: sending to IGOR failed (%s)", PLUGINNAME,
               igor_strerror_r(send_error));
      if (send_error == IGOR_ERR_NOTCONNECTED)
      {
         igor_close (socket_descriptor);
         socket_descriptor = -1;
      } // if (send_error == IGOR_ERR_NOTCONNECTED)
      return false;
   } // if (send_error < 0)
   else
   {
      out_bytes_mutex.Lock();
      out_bytes += message.size();
      out_bytes_mutex.Unlock();
   } // else

   return true;
} // send_data (...)


/*! @brief sends EPG data as responce to a request (via IGOR)
    @param channel  channel the EPG data belongs to
    @param timeblock  starttime of the requested EPG data block, normalised
    @param update  iff this is @c true, the response to an update request
                   will be sent
    @param data  the requested EPG data, should be exactly one time block
    @param destination  id of the requestor as received via
           @c receiver::receive_req()
    @return @c true on success, @c false if an error occured
    @note syslog-s a message on error
*/
bool epgigor_interface::send_resp (const epgchannel &channel, time_t timeblock,
      bool update, const epgdata &data, const sa_igor &destination)
{
   string message;
   int send_error;

   // normalise time block identifier:
   timeblock = normalise_timeblock (timeblock);
   if ( !create_data_msg(channel, timeblock, update, data, &message) )
      return false;
   if (socket_descriptor < 0)
   {
      esyslog ("%s plugin: not connected to IGOR", PLUGINNAME);
      return false;
   } // if (socket_descriptor < 0)
   send_error = igor_sendto (socket_descriptor, (const void*)message.data(),
                            message.size(), /* upcall */ false, &destination);
   if (send_error < 0)
   {
      esyslog ("%s plugin: sending to IGOR failed (%s)", PLUGINNAME,
               igor_strerror_r(send_error));
      if (send_error == IGOR_ERR_NOTCONNECTED)
      {
         igor_close (socket_descriptor);
         socket_descriptor = -1;
      } // if (send_error == IGOR_ERR_NOTCONNECTED)
      return false;
   } // if (send_error < 0)
   else
   {
      out_bytes_mutex.Lock();
      out_bytes += message.size();
      out_bytes_mutex.Unlock();
   } // else

   return true;
} // send_resp (...)


/*! @brief sends a request for EPG data to the IGOR
    @param channel  channel of the requested EPG data
    @param timeblock  starttime of the requested EPG data block, normalised
    @param update  iff this is @c true, an update request will be sent
    @return @c true on success, @c false if an error occured
    @note syslog-s a message on error
*/
bool epgigor_interface::send_req
   (const epgchannel &channel, time_t timeblock, bool update)
{
   string message, dest_string;
   sa_igor destination;
   int send_error;

   // normalise time block identifier:
   timeblock = normalise_timeblock (timeblock);
   dest_string = calculate_destination (channel, timeblock);
   if ( dest_string.empty() )
      return false;
   if ( !pack_igor_address(dest_string, &destination) )
      return false;
   if ( !create_request_msg(channel, timeblock, update, &message) )
      return false;
   if (socket_descriptor < 0)
   {
      esyslog ("%s plugin: not connected to IGOR", PLUGINNAME);
      return false;
   } // if (socket_descriptor < 0)
   send_error = igor_sendto (socket_descriptor, (const void*)message.data(),
                            message.size(), /* upcall */ true, &destination);
   if (send_error < 0)
   {
      esyslog ("%s plugin: sending to IGOR failed (%s)", PLUGINNAME,
               igor_strerror_r(send_error));
      if (send_error == IGOR_ERR_NOTCONNECTED)
      {
         igor_close (socket_descriptor);
         socket_descriptor = -1;
      } // if (send_error == IGOR_ERR_NOTCONNECTED)
      return false;
   } // if (send_error < 0)
   else
   {
      out_bytes_mutex.Lock();
      out_bytes += message.size();
      out_bytes_mutex.Unlock();
   } // else

   return true;
} // send_req (...)


/*! @brief sends on the last message received from the IGOR
    @return @c true on success, @c false if an error occured
    @note syslog-s a message on error
*/
bool epgigor_interface::send_on_last_received_message (void)
{
   int send_error;

   if (socket_descriptor < 0)
   {
      esyslog ("%s plugin: not connected to IGOR", PLUGINNAME);
      return false;
   } // if (socket_descriptor < 0)
   send_error = igor_sendon (socket_descriptor);
   if (send_error < 0)
   {
      esyslog ("%s plugin: sending to IGOR failed (%s)", PLUGINNAME,
               igor_strerror_r(send_error));
      if (send_error == IGOR_ERR_NOTCONNECTED)
      {
         igor_close (socket_descriptor);
         socket_descriptor = -1;
      } // if (send_error == IGOR_ERR_NOTCONNECTED)
      return false;
   } // if (send_error < 0)
   else
   {
      out_bytes_mutex.Lock();
      out_bytes += last_sendon_candidate_size;
      out_bytes_mutex.Unlock();
   } // else

   return true;
} // send_on_last_received_message()


/*! @return the number of bytes received from the IGOR since the last reset
            (via @c reset_bytes_received() or @c register_rcvr())
    @note the counter may overflow
*/
unsigned long epgigor_interface::get_bytes_received (void)
{
   unsigned long retval;

   in_bytes_mutex.Lock();
   retval = in_bytes;
   in_bytes_mutex.Unlock();
   return retval;
} // get_bytes_received()


/*! @return the number of bytes transmitted to the IGOR since the last reset
            (via @c reset_bytes_sent() or @c register_rcvr())
    @note the counter may overflow
*/
unsigned long epgigor_interface::get_bytes_sent (void)
{
   unsigned long retval;

   out_bytes_mutex.Lock();
   retval = out_bytes;
   out_bytes_mutex.Unlock();
   return retval;
} // get_bytes_sent()


/*! @brief atomically read and reset the counter of received bytes
    @return the number of bytes received from the IGOR since the last reset
            (via @c reset_bytes_received() or @c register_rcvr())
    @note the counter may overflow
*/
unsigned long epgigor_interface::reset_bytes_received (void)
{
   unsigned long retval;

   in_bytes_mutex.Lock();
   retval = in_bytes;
   in_bytes = 0;
   in_bytes_mutex.Unlock();
   return retval;
} // reset_bytes_received()


/*! @brief atomically read and reset the counter of transmitted bytes
    @return the number of bytes transmitted to the IGOR since the last reset
            (via @c reset_bytes_sent() or @c register_rcvr())
    @note the counter may overflow
*/
unsigned long epgigor_interface::reset_bytes_sent (void)
{
   unsigned long retval;

   out_bytes_mutex.Lock();
   retval = out_bytes;
   out_bytes = 0;
   out_bytes_mutex.Unlock();
   return retval;
} // reset_bytes_sent()
