#ifndef FILE_IO_C
#define FILE_IO_C
/* include section */

#include "file_io.h"
#include "../config.h"

#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#else
#error Need <stdlib.h>
#endif

#include <assert.h>
#include <errno.h>

#define ensure( X ) assert((X))
#define require( X ) assert((X))

struct io_class_type
{
  void (*close_fn)(struct file_io*);
  void (*read_fn)(struct file_io*, void *, size_t, size_t);
  void (*write_fn)(struct file_io*, const void*,  size_t,  size_t);
};

struct file_io
{
  struct io_class_type* class;
  void* file_stream;
  int status;
  const char* error_message;
};

/* Report number of files open/closed */
/* #define OPEN_COUNT */
#ifdef OPEN_COUNT
static int file_stream_counter = 0;
void 
counter_open()
{
  ++file_stream_counter;
  printf ("Stream open: streams = [%d]\n",file_stream_counter);
}
void 
counter_close()
{
  --file_stream_counter;
  printf ("Stream close: streams = [%d]\n",file_stream_counter);
}
#else
void counter_open() {};
void counter_close() {};
#endif

/****************************************/
/* Queries */
/****************************************/
/* Is there an error condition from last operation on object? */
int 
io_in_error (struct file_io *io)
{
  require (NULL != io);
  ensure (0 == io->status || NULL != io->error_message);
  return (0 != io->status);
}

int
io_at_eof (struct file_io *io)
{
  require (NULL != io);
  ensure (0 == io->status || NULL != io->error_message);
  return (EOF == io->status);
}

int 
io_no_stream (struct file_io *io)
{
  require (NULL != io);
  return NULL == io->file_stream;
}

int 
io_error_number (struct file_io *io)
{
  require (NULL != io);
  ensure (0 == io->status || NULL != io->error_message);
  return io->status;
}

const char*
io_error_message (struct file_io *io)
{
  require (NULL != io);
  ensure (NULL == io->error_message || 0 != io->status);
  return io->error_message;
}

/****************************************/
/* Destructor */
/****************************************/
/* Close and destroy object */
void 
io_delete (struct file_io **io, char* error_message, int msgsize)
{
  require (NULL != io);
  require (NULL != *io);
  require (0 == io_error_number(*io)); /* status must be zero  */
  if (!io_no_stream (*io))
    {
      (*io)->class->close_fn (*io);
      if (!io_no_stream (*io))
	{
	  memset (error_message, 0, msgsize);
	  strncpy (error_message, io_error_message (*io), msgsize);
	  io_reset (*io);
	}
      else
	{
	  counter_close();
	}
    }
  free (*io);
  *io = NULL;
  ensure (NULL == *io);
}

/****************************************/
/* Manipulation procedures */
/****************************************/
/* These procedures will do nothing if io_in_error(..) is TRUE */

/* Read from file */
void 
io_read (struct file_io *io, void *ptr, size_t size, size_t amt)
{
  require (NULL != io);
  require (0 == io_error_number(io)); /* status must be zero  */
  io->class->read_fn (io, ptr, size, amt);
}

/* Write to file */
void 
io_write(struct file_io *io, void *ptr, size_t size, size_t amt)
{
  require (NULL != io);
  require (0 == io_error_number(io)); /* status must be zero  */
  io->class->write_fn (io, ptr, size, amt);
};

/* Reset any error condition */
void 
io_reset(struct file_io *io)
{
  require (io != NULL);
  require (io_in_error(io));
  io->status = 0;
  if (io->error_message)
    {
      free ((void*)io->error_message);
      io->error_message = NULL;
    }
  ensure (0 == io_error_number(io));
  ensure (NULL == io->error_message);
}

/****************************************/
/* Support methods for std files */
#include <string.h>
static void
file_close (struct file_io* io)
{
  require (io); /* io must not be NULL  */
  require (!io_in_error(io)); /* status must be zero  */
  require (!io_no_stream (io)); /* stream must exist  */
  if (0 != fclose (io->file_stream))
    {
      io->status = errno;
      io->error_message = strdup (strerror (errno));
    }
  else
    {
      io->file_stream = NULL;
    }
  ensure (io_no_stream (io) || io_in_error (io));
}

static void 
file_read (struct file_io* io, void * ptr, size_t siz, size_t amt)
{
  require (io); /* io must not be NULL  */
  require (!io_in_error (io)); /* status must be zero  */
  require (!io_no_stream (io)); /* stream must exist  */
  if (amt != fread (ptr, siz, amt, io->file_stream))
    {
      if (0 != feof (io->file_stream))
	{
	  io->status = EOF;
	  io->error_message = strdup (strerror (EOF));
	}
      else
	{
	  io->status = errno;
	  io->error_message = strdup (strerror (errno));
	}
    }
}

static void 
file_write (struct file_io* io, const void* ptr,  size_t siz,  size_t amt)
{
  assert (io); /* io must not be NULL  */
  require (!io_in_error (io)); /* status must be zero  */
  require (!io_no_stream (io)); /* stream must exist  */
  if (amt != fwrite (ptr, siz, amt, io->file_stream))
    {
      io->status = errno;
      io->error_message = strdup (strerror (errno));
    }
}

#ifdef HAVE_LIBZ
/****************************************/
/* Support methods for std files */
#include <zlib.h>
static void
gz_file_close (struct file_io* io)
{
  int l_error;
  require (io); /* io must not be NULL  */
  require (!io_in_error (io)); /* status must be zero  */
  require (!io_no_stream (io)); /* stream must exist  */
  l_error = gzclose (io->file_stream);
  if (0 != l_error)
    {
      if (l_error == Z_ERRNO)
	{
	  io->status = errno;
	  io->error_message = strdup (strerror (errno));
	}
      else
	{
	  io->error_message = strdup (gzerror (io->file_stream, &(io->status)));
	}
    }
  else
    {
      io->file_stream = NULL;
    }
  ensure (io_no_stream (io) || io_in_error (io));
}

static void 
gz_file_read (struct file_io* io, void * ptr, size_t siz, size_t amt)
{
  int l_read_size;
  require (io); /* io must not be NULL  */
  require (io_in_error (io) == 0); /* status must be zero  */
  l_read_size = gzread (io->file_stream, ptr, siz*amt);
  if (0 == l_read_size)
    {
      io->status = EOF;
      io->error_message = strdup (strerror (EOF));
    }
  if (-1 == l_read_size)
    {
      io->error_message = strdup (gzerror (io->file_stream, &(io->status))); 
      if (io->status == Z_ERRNO)
	{
	  io->status = errno;
	  if (io->error_message != NULL)
	    {
	      free ((void*)io->error_message);
	    }
	  io->error_message = strdup (strerror (errno));
	}
    }
}

static void 
gz_file_write (struct file_io* io, const void* ptr,  size_t siz,  size_t amt)
{
  assert (io); /* io must not be NULL  */
  assert (!io_in_error (io)); /* status must be zero  */
  require (!io_no_stream (io)); /* stream must exist  */
  if (0 == gzwrite (io->file_stream, (void*)ptr, siz*amt))
    {
      io->error_message = strdup (gzerror (io->file_stream, &(io->status)));
      if (io->status == Z_ERRNO)
	{
	  io->status = errno;
	  if (io->error_message != NULL)
	    {
	      free ((void*)io->error_message);
	    }
	  io->error_message = strdup (strerror (errno));
	}
    }
}
#endif /* !HAVE_LIBZ */

#ifdef HAVE_LIBBZ2
/****************************************/
/* Support methods for std files */
#include <bzlib.h>

static void 
bz_file_close (struct file_io* io)
{
/*   int l_error; */
  assert (io); /* io must not be NULL  */
  require (!io_in_error(io)); /* status must be zero  */
  require (!io_no_stream (io)); /* stream must exist  */
  BZ2_bzclose (io->file_stream);
  /*   l_error =  */
  /*   if (0 != l_error) */
  /*     { */
  /*       if (l_error == Z_ERRNO) */
  /* 	{ */
  /* 	  io->status = errno; */
  /* 	  io->error_message = strdup (strerror (errno)); */
  /* 	} */
  /*       else */
  /* 	{ */
  /* 	  io->error_message = strdup (BZ2_bzerror (io->file_stream, &(io->status))); */
  /* 	} */
  /*     } */
  io->file_stream = NULL;
  ensure (io_no_stream (io) || io_in_error (io));
}

static void 
bz_file_read (struct file_io* io, void * ptr, size_t siz, size_t amt)
{
  int l_read_size;
  assert (io); /* io must not be NULL  */
  require (!io_in_error (io)); /* status must be zero  */
  require (!io_no_stream (io)); /* stream must exist  */
  l_read_size = BZ2_bzread (io->file_stream, ptr, siz*amt);
  if (0 == l_read_size)
    {
      io->status = EOF;
      io->error_message = strdup (strerror (EOF));
    }
  if (-1 == l_read_size)
    {
      if (errno != 0)
	{
	  io->status = errno;
	  io->error_message = strdup (strerror (errno));
	}
      else
	{
	  io->error_message = strdup (BZ2_bzerror (io->file_stream, &(io->status))); 
	}
    }
}

static void 
bz_file_write (struct file_io* io, const void* ptr,  size_t siz,  size_t amt)
{
  assert (io); /* io must not be NULL  */
  require (!io_in_error (io)); /* status must be zero  */
  require (!io_no_stream (io)); /* stream must exist  */
  if (0 == BZ2_bzwrite (io->file_stream, (void*)ptr, siz*amt))
    {
      if (errno != 0)
	{
	  io->status = errno;
	  io->error_message = strdup (strerror (errno));
	}
      else
	{
	  io->error_message = strdup (BZ2_bzerror (io->file_stream, &(io->status))); 
	}
    }
}
#endif /* !HAVE_LIBBZ2 */

/****************************************/
/* Creation/destruction */

/* System features instantion is acquisition. If creation is
   successful then return value of io_new.. will not be NULL and
   io_in_error will be false. After io_delete is called any memory is
   freed and io == NULL. */

/****************************************/
static struct io_class_type* BASE_TYPE = NULL;
static struct io_class_type* LIBZ_TYPE = NULL;
static struct io_class_type* LIBBZ2_TYPE = NULL;

static struct io_class_type base_type, libz_type, libbz2_type;

inline static void
initialise_classes()
{
  base_type.close_fn = file_close;
  base_type.read_fn = file_read;
  base_type.write_fn = file_write;
  BASE_TYPE = &base_type;
  ensure (NULL != BASE_TYPE);
#ifdef HAVE_LIBZ
  libz_type.close_fn = gz_file_close;
  libz_type.read_fn = gz_file_read;
  libz_type.write_fn = gz_file_write;
  LIBZ_TYPE = &libz_type;
  ensure (NULL != LIBZ_TYPE);
#endif
#ifdef HAVE_LIBBZ2
  libbz2_type.close_fn = bz_file_close;
  libbz2_type.read_fn = bz_file_read;
  libbz2_type.write_fn = bz_file_write;
  LIBBZ2_TYPE = &libbz2_type;
  ensure (NULL != LIBBZ2_TYPE);
#endif
};


/* Create object for regular file */
struct file_io* 
io_new_file(const char *path, const char *mode)
{
  struct file_io* result;
  require(path);
  require(mode);
  result = (struct file_io*)malloc (sizeof(struct file_io));
  if (result != NULL)
    {
      /* initialise structure */
      if (NULL == BASE_TYPE)
	{
	  initialise_classes();
	}
      result->class = BASE_TYPE;
      result->file_stream = NULL;
      result->status = 0;
      result->error_message = NULL;

      /* open file */
      result->file_stream = fopen (path, mode);
      if (result->file_stream == NULL)
	{
	  result->status = errno;
	  result->error_message = strdup (strerror (errno));
	}
      else
	{
	  counter_open();
	}
    }
  ensure(NULL == result || !io_no_stream (result) || io_in_error (result));
  return result;
}

#ifdef HAVE_LIBZ
/* Create object for zlib compressed file */
struct file_io* 
io_new_zfile (const char *path, const char *mode)
{
  struct file_io* result;
  require(path);
  require(mode);
  result = (struct file_io*)malloc (sizeof(struct file_io));
  if (result != NULL)
    {
      /* initialise structure */
      if (NULL == LIBZ_TYPE)
	{
	  initialise_classes();
	}
      result->class = LIBZ_TYPE;
      result->file_stream = NULL;
      result->status = 0;
      result->error_message = NULL;

      /* open file */
      result->file_stream = gzopen (path, mode);
      if (result->file_stream == NULL)
	{
	  if (errno != 0)
	    {
	      result->status = errno;
	      result->error_message = strdup (strerror (errno));
	    }
	  else
	    {
	      result->status = Z_MEM_ERROR;
	      result->error_message = strdup ("Error allocating memory in zlib.");
	    }
	}
      else
	{
	  counter_open();
	}
    }
  ensure(NULL == result || !io_no_stream (result) || io_in_error (result));
  return result;
}
#else /* !HAVE_LIBZ */
struct file_io* 
io_new_zfile (const char *path, const char *mode)
{
  require(path);
  require(mode);
  return NULL;
}
#endif /* !HAVE_LIBZ */

#ifdef HAVE_LIBBZ2
/* Create object for bz2 compressed file */

struct file_io* 
io_new_bzfile (const char *path, const char *mode)
{
  struct file_io* result;
  require(path);
  require(mode);
  result = (struct file_io*)malloc (sizeof(struct file_io));
  if (result != NULL)
    {
      /* initialise structure */
      if (NULL == LIBBZ2_TYPE)
	{
	  initialise_classes();
	}
      result->class = LIBBZ2_TYPE;
      result->file_stream = NULL;
      result->status = 0;
      result->error_message = NULL;
      /* open file */
      result->file_stream = BZ2_bzopen (path, mode);
      if (result->file_stream == NULL)
	{
	  if (errno != 0)
	    {
	      result->status = errno;
	      result->error_message = strdup (strerror (errno));
	    }
	  else
	    {
	      result->status = -1;
	      result->error_message = strdup ("Error openning bzipped file");
	    }
	}
      else
	{
	  counter_open();
	}
    }
  ensure(NULL == result || !io_no_stream (result) || io_in_error (result));
  return result;
}
#else /* !HAVE_LIBBZ2 */
struct file_io* 
io_new_bzfile (const char *path, const char *mode)
{
  require(path);
  require(mode);
  return NULL;
}
#endif /* !HAVE_LIBBZ2 */

#endif /* !FILE_IO_C */
