/*   EXTRAITS DE LA LICENCE
	Copyright CEA, contributeurs : Damien
	CALISTE, laboratoire L_Sim, (2017)
  
	Adresses mèl :
	CALISTE, damien P caliste AT cea P fr.

	Ce logiciel est un programme informatique servant à visualiser des
	structures atomiques dans un rendu pseudo-3D. 

	Ce logiciel est régi par la licence CeCILL soumise au droit français et
	respectant les principes de diffusion des logiciels libres. Vous pouvez
	utiliser, modifier et/ou redistribuer ce programme sous les conditions
	de la licence CeCILL telle que diffusée par le CEA, le CNRS et l'INRIA 
	sur le site "http://www.cecill.info".

	Le fait que vous puissiez accéder à cet en-tête signifie que vous avez 
	pris connaissance de la licence CeCILL, et que vous en avez accepté les
	termes (cf. le fichier Documentation/licence.fr.txt fourni avec ce logiciel).
*/

/*   LICENCE SUM UP
	Copyright CEA, contributors : Damien
	CALISTE, laboratoire L_Sim, (2017)

	E-mail addresses :
	CALISTE, damien P caliste AT cea P fr.

	This software is a computer program whose purpose is to visualize atomic
	configurations in 3D.

	This software is governed by the CeCILL  license under French law and
	abiding by the rules of distribution of free software.  You can  use, 
	modify and/ or redistribute the software under the terms of the CeCILL
	license as circulated by CEA, CNRS and INRIA at the following URL
	"http://www.cecill.info". 

	The fact that you are presently reading this means that you have had
	knowledge of the CeCILL license and that you accept its terms. You can
	find a copy of this licence shipped with this software at Documentation/licence.en.txt.
*/

#include <coreTools/toolFiles.h>

#ifdef HAVE_LIB_ARCHIVE
#include <archive.h>
#endif
#include <string.h>

/**
 * SECTION:toolFiles
 * @short_description: abstract reading of compressed or not text files.
 *
 * <para>This class defines objects to read text files with the same
 * API than with #GIOChannel. The addition is that these files can be
 * transparently compressed or not.</para>
 */

struct _ToolFilesPrivate
{
  gboolean dispose_has_run;

  gchar *filename;
  
#ifdef HAVE_LIB_ARCHIVE
  struct archive *archive;
#define BUFF_SIZE 1024
  char buff[BUFF_SIZE + 1];
  char *cur;
  gssize buffSize;
#endif

  GIOChannel *direct;
  GIOStatus latest;

  gchar *data;
  gchar *at;
};

static void tool_files_dispose(GObject* obj);
static void tool_files_finalize(GObject* obj);

G_DEFINE_TYPE_WITH_CODE(ToolFiles, tool_files, VISU_TYPE_OBJECT,
                        G_ADD_PRIVATE(ToolFiles))

static void tool_files_class_init(ToolFilesClass *klass)
{
  DBG_fprintf(stderr, "ToolFiles: creating the class of the object.\n");

  /* DBG_fprintf(stderr, "                - adding new signals ;\n"); */

  /* Connect freeing methods. */
  G_OBJECT_CLASS(klass)->dispose  = tool_files_dispose;
  G_OBJECT_CLASS(klass)->finalize = tool_files_finalize;
}

static void tool_files_init(ToolFiles *obj)
{
  DBG_fprintf(stderr, "ToolFiles: creating a new file reader (%p).\n", (gpointer)obj);

  obj->priv = tool_files_get_instance_private(obj);

  obj->priv->dispose_has_run = FALSE;
  obj->priv->filename = (gchar*)0;
#ifdef HAVE_LIB_ARCHIVE
  obj->priv->archive = (struct archive*)0;
  obj->priv->buff[0] = '\0';
  obj->priv->cur = (char*)0;
  obj->priv->buffSize = -1;
#endif
  obj->priv->direct = (GIOChannel*)0;
  obj->priv->data   = (gchar*)0;
  obj->priv->at     = (gchar*)0;
}
static void tool_files_dispose(GObject* obj)
{
  ToolFiles *self = TOOL_FILES(obj);
  DBG_fprintf(stderr, "ToolFiles: dispose object %p.\n", (gpointer)obj);

  if (self->priv->dispose_has_run)
    return;
  self->priv->dispose_has_run = TRUE;

  if (self->priv->direct)
    g_io_channel_unref(self->priv->direct);

  /* Chain up to the parent class */
  G_OBJECT_CLASS(tool_files_parent_class)->dispose(obj);
}
static void tool_files_finalize(GObject* obj)
{
  ToolFiles *self = TOOL_FILES(obj);
  DBG_fprintf(stderr, "ToolFiles: finalize object %p.\n", (gpointer)obj);

  g_free(self->priv->filename);
#ifdef HAVE_LIB_ARCHIVE
  if (self->priv->archive)
    archive_read_free(self->priv->archive);
#endif
  g_free(self->priv->data);

  /* Chain up to the parent class */
  G_OBJECT_CLASS(tool_files_parent_class)->finalize(obj);
}

/**
 * tool_files_new:
 *
 * Creates a new #ToolFiles object.
 *
 * Since: 3.8
 *
 * Returns: (transfer full): a newly created #ToolFiles object.
 */
ToolFiles* tool_files_new()
{
  return TOOL_FILES(g_object_new(TOOL_TYPE_FILES, NULL));
}

/**
 * tool_files_open:
 * @file: a #ToolFiles object.
 * @filename: a filename.
 * @error: a location for an error.
 *
 * Open @filename for read access. The file can be compressed or not.
 *
 * Since: 3.8
 *
 * Returns: TRUE if no error occured when opening the file.
 **/
gboolean tool_files_open(ToolFiles *file, const gchar *filename, GError **error)
{
#ifdef HAVE_LIB_ARCHIVE
  int r;
  struct archive_entry *ae;
#else
  char *pt;
#endif

  g_return_val_if_fail(TOOL_IS_FILES(file), FALSE);
  g_return_val_if_fail(!error || *error == (GError*)0, FALSE);

  file->priv->filename = g_strdup(filename);
#ifdef HAVE_LIB_ARCHIVE
  file->priv->archive = archive_read_new();
  archive_read_support_filter_all(file->priv->archive);
  archive_read_support_format_raw(file->priv->archive);
  
  r = archive_read_open_filename(file->priv->archive, filename, 16384);
  if (r == ARCHIVE_OK)
    {
      r = archive_read_next_header(file->priv->archive, &ae);
      if (r != ARCHIVE_OK)
        {
          g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
                      _("cannot read next archive entry.\n"));
          return FALSE;
        }
      return TRUE;
    }
  else
    {
      archive_read_free(file->priv->archive);
      file->priv->archive = (struct archive*)0;
    }
#else
  pt = strrchr(filename, '.');
  if (pt && (!strcmp(pt, ".gz") || !strcmp(pt, ".bz2")))
    {
      g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_FAILED,
                  _("Format not supported."));
      return FALSE;
    }
#endif

  file->priv->direct = g_io_channel_new_file(filename, "r", error);
  if (!file->priv->direct)
    return FALSE;

  return TRUE;
}
/**
 * tool_files_setEncoding:
 * @file: a #ToolFiles object.
 * @encoding: an encoding.
 *
 * Set-up the encoding of @file.
 *
 * Since: 3.8
 **/
void tool_files_setEncoding(ToolFiles *file, const gchar *encoding)
{
  g_return_if_fail(TOOL_IS_FILES(file));

  if (file->priv->direct)
    g_io_channel_set_encoding(file->priv->direct, encoding, NULL);
}
/**
 * tool_files_fromMemory:
 * @file: a #ToolFiles object.
 * @data: some data.
 *
 * Create a #ToolFiles object from @data.
 *
 * Since: 3.8
 **/
void tool_files_fromMemory(ToolFiles *file, const gchar *data)
{
  g_return_if_fail(TOOL_IS_FILES(file));

  file->priv->data = g_strdup(data);
  file->priv->at = file->priv->data;
}

#ifdef HAVE_LIB_ARCHIVE
static GIOStatus _archiveReadChunk(ToolFiles *file, GError **error)
{
  gboolean empty = file->priv->cur == (char*)0;

  file->priv->buff[0] = '\0';
  file->priv->cur = (char*)0;
  file->priv->buffSize = archive_read_data(file->priv->archive,
                                           file->priv->buff, BUFF_SIZE);
  if (file->priv->buffSize < 0)
    {
      g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_IO,
                  _("read error from archive.\n"));
      return G_IO_STATUS_ERROR;
    }
  if (file->priv->buffSize == 0)
    return empty ? G_IO_STATUS_EOF : G_IO_STATUS_NORMAL;
  file->priv->buff[file->priv->buffSize] = '\0';
  file->priv->cur = file->priv->buff;
  return G_IO_STATUS_NORMAL;
}
#endif

/**
 * tool_files_read:
 * @file: a #ToolFiles object
 * @buffer: (array) (element-type char): a buffer
 * @count: the size of allocated @buffer in bytes.
 * @error: an error location or NULL
 *
 * Read @count bytes from @file and store them into @buffer.
 *
 * Since: 3.8
 *
 * Returns: a IO status.
 **/
GIOStatus tool_files_read(ToolFiles *file,
                          void *buffer, gsize count, GError **error)
{
  g_return_val_if_fail(TOOL_IS_FILES(file), G_IO_STATUS_ERROR);
  g_return_val_if_fail(!error || *error == (GError*)0, G_IO_STATUS_ERROR);

#ifdef HAVE_LIB_ARCHIVE
  if (file->priv->archive)
    {
      gchar *cbuff = (gchar*)buffer;
      while (TRUE)
        {
          GIOStatus status;

          if (file->priv->cur)
            {
              gsize buffSize = file->priv->buffSize - (file->priv->cur - file->priv->buff);
              if (count <= buffSize)
                {
                  memcpy(cbuff, file->priv->cur, sizeof(char) * count);
                  file->priv->cur += count;
                  
                  return G_IO_STATUS_NORMAL;
                }
              
              memcpy(cbuff, file->priv->cur, sizeof(char) * buffSize);
              cbuff += buffSize;
              count -= buffSize;
            }

          status = _archiveReadChunk(file, error);
          if (status != G_IO_STATUS_NORMAL)
            return status;
        }
    }
#endif
  if (file->priv->direct)
    {
      file->priv->latest = g_io_channel_read_chars(file->priv->direct, buffer,
                                                   count, NULL, error);
      return file->priv->latest;
    }
  else if (file->priv->data)
    {
      gsize len = strlen(file->priv->at);
      gchar *cbuff = (gchar*)buffer;
      cbuff[count] = '\0';
      if (len >= count)
        memcpy(cbuff, file->priv->at, count);
      else
        cbuff[0] = '\0';
      file->priv->at += MIN(len, count);
      return *file->priv->at ? G_IO_STATUS_NORMAL : G_IO_STATUS_EOF;
    }
  g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_NOENT, _("file not opened.\n"));
  return G_IO_STATUS_ERROR;
}
/**
 * tool_files_skip:
 * @file: a #ToolFiles object.
 * @count: a number of bytes.
 * @error: an error location.
 *
 * Read @count bytes from @file but don't store them. 
 *
 * Since: 3.8
 *
 * Returns: a status.
 **/
GIOStatus tool_files_skip(ToolFiles *file, gsize count, GError **error)
{
  g_return_val_if_fail(TOOL_IS_FILES(file), G_IO_STATUS_ERROR);
  g_return_val_if_fail(!error || *error == (GError*)0, G_IO_STATUS_ERROR);

#ifdef HAVE_LIB_ARCHIVE
  if (file->priv->archive)
    {
      while (TRUE)
        {
          GIOStatus status;

          if (file->priv->cur)
            {
              gsize buffSize = file->priv->buffSize - (file->priv->cur - file->priv->buff);
              if (count <= buffSize)
                {
                  file->priv->cur += count;
                  
                  return G_IO_STATUS_NORMAL;
                }
              
              count -= buffSize;
            }

          status = _archiveReadChunk(file, error);
          if (status != G_IO_STATUS_NORMAL)
            return status;
        }
    }
#endif
  if (file->priv->direct)
    {
      file->priv->latest = g_io_channel_seek_position(file->priv->direct,
                                                      count, G_SEEK_CUR, error);
      return file->priv->latest;
    }
  else if (file->priv->data)
    {
      file->priv->at += MIN( strlen(file->priv->at), count);
      return *file->priv->at ? G_IO_STATUS_NORMAL : G_IO_STATUS_EOF;
    }
  g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_NOENT, _("file not opened.\n"));
  return G_IO_STATUS_ERROR;
}
/**
 * tool_files_read_line_string:
 * @file: a #ToolFiles object.
 * @buffer: (out caller-allocates): an allocated string.
 * @terminator_pos: (out caller-allocates): position of the terminator
 * in @buffer.
 * @error: an error location.
 *
 * Read a new line from @file and put it into @buffer. This works like
 * g_io_channel_read_line_string() but is transparent for compressed files.
 *
 * Since: 3.8
 *
 * Returns: a status.
 **/
GIOStatus tool_files_read_line_string(ToolFiles *file, GString *buffer,
                                      gsize *terminator_pos, GError **error)
{
  g_return_val_if_fail(TOOL_IS_FILES(file), G_IO_STATUS_ERROR);
  g_return_val_if_fail(!error || *error == (GError*)0, G_IO_STATUS_ERROR);

#ifdef HAVE_LIB_ARCHIVE
  if (file->priv->archive)
    {
      if (buffer->str)
        g_string_set_size(buffer, 0);
      while (TRUE)
        {
          GIOStatus status;

          if (file->priv->cur)
            {
              char *eol = strchr(file->priv->cur, '\n');
              if (eol)
                {
                  char at = eol[1];
                  eol += 1;
                  *eol = '\0';
                  g_string_append(buffer, file->priv->cur);
                  *eol = at;
                  file->priv->cur = eol;

                  return G_IO_STATUS_NORMAL;
                }
              else if (*file->priv->cur)
                g_string_append(buffer, file->priv->cur);
            }

          status = _archiveReadChunk(file, error);
          if (status != G_IO_STATUS_NORMAL)
            return status;
        }
    }
#endif
  if (file->priv->direct)
    {
      file->priv->latest = g_io_channel_read_line_string(file->priv->direct, buffer,
                                                         terminator_pos, error);
      return file->priv->latest;
    }
  else if (file->priv->data)
    {
      char *eol = strchr(file->priv->at, '\n');
      if (buffer->str)
        g_string_set_size(buffer, 0);
      if (eol)
        {
          char at = eol[1];
          eol += 1;
          *eol = '\0';
          g_string_append(buffer, file->priv->at);
          *eol = at;
          file->priv->at = eol;
          
          return G_IO_STATUS_NORMAL;
        }
      else if (*file->priv->at)
        {
          g_string_append(buffer, file->priv->at);
          for (; file->priv->at; file->priv->at++);
          
          return G_IO_STATUS_EOF;
        }
      else
        return G_IO_STATUS_EOF;
    }
  g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_NOENT, _("file not opened.\n"));
  return G_IO_STATUS_ERROR;
}

/**
 * tool_files_rewind:
 * @file: a #ToolFiles object.
 * @error: an error location.
 *
 * Transparently rewind @file at the beginning for compressed files or not.
 *
 * Since: 3.8
 *
 * Returns: a status.
 **/
GIOStatus tool_files_rewind(ToolFiles *file, GError **error)
{
#ifdef HAVE_LIB_ARCHIVE
  if (file->priv->archive)
    {
      /* Close and reopen. */
      archive_read_free(file->priv->archive);
      return tool_files_open(file, file->priv->filename, error);
    }
#endif
  if (file->priv->direct)
    {
      return g_io_channel_seek_position(file->priv->direct, 0, G_SEEK_SET, error);
    }
  else if (file->priv->data)
    {
      file->priv->at = file->priv->data;
      return G_IO_STATUS_NORMAL;
    }
  g_set_error(error, G_FILE_ERROR, G_FILE_ERROR_NOENT,
              _("file not opened.\n"));
  return G_IO_STATUS_ERROR;
}

/**
 * tool_files_atEnd:
 * @file: a #ToolFiles object.
 *
 * Inquires if @file is at end of file.
 *
 * Since: 3.8
 *
 * Returns: TRUE on success.
 **/
gboolean tool_files_atEnd(ToolFiles *file)
{
  g_return_val_if_fail(TOOL_IS_FILES(file), TRUE);

#ifdef HAVE_LIB_ARCHIVE
  if (file->priv->archive)
    {
      if (file->priv->buffSize < 0)
        _archiveReadChunk(file, NULL);
      return file->priv->buffSize == 0;
    }
#endif
  if (file->priv->direct)
    return file->priv->latest == G_IO_STATUS_EOF;
  else if (file->priv->data)
    return !*file->priv->at;

  return TRUE;
}
