/*   EXTRAITS DE LA LICENCE
	Copyright CEA, contributeurs : Damien
	CALISTE, laboratoire L_Sim, (2016)
  
	Adresse 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, (2016)

	E-mail address:
	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 "visu_dataloadable.h"

#include <glib/gstdio.h>
#include <sys/stat.h>

#include "visu_commandLine.h"
#include "visu_dataatomic.h"
#include "visu_dataspin.h"

/**
 * SECTION: visu_dataloadable
 * @short_description: a base class for all loadable representation of
 * #VisuData objects.
 *
 * <para>#VisuData is a memory representation of node data. This class
 * is defining common methods for all #VisuDataLoadable object that
 * provide disk method to load #VisuData objects.</para>
 */

enum {
  PROP_0,
  PROP_N_SOURCES,
  PROP_LABEL,
  PROP_LOADING,
  PROP_STATUS,
  PROP_AUTO_REFRESH,
  PROP_REFRESH_PERIOD,
  N_PROPS
};
static GParamSpec *_properties[N_PROPS];

/**
 * VisuDataLoadable:
 *
 * An opaque structure storing #VisuDataLoadable objects.
 *
 * Since: 3.8
 */
struct _VisuDataLoadablePrivate
{
  guint nFiles;
  guint nSets;
  guint iSet;
  gchar **labels;

  gboolean loading;
  gchar *status;

  gboolean autoRefresh;
  guint refreshPeriod, refreshId;
  time_t *lastReadTime;
};

static void visu_data_loadable_finalize(GObject* obj);
static void visu_data_loadable_get_property(GObject* obj, guint property_id,
                                            GValue *value, GParamSpec *pspec);
static void visu_data_loadable_set_property(GObject* obj, guint property_id,
                                            const GValue *value, GParamSpec *pspec);
static gboolean _reload(VisuDataLoadable *loadable);

G_DEFINE_TYPE_WITH_CODE(VisuDataLoadable, visu_data_loadable, VISU_TYPE_DATA,
                        G_ADD_PRIVATE(VisuDataLoadable))

static void visu_data_loadable_class_init(VisuDataLoadableClass *klass)
{
  G_OBJECT_CLASS(klass)->finalize     = visu_data_loadable_finalize;
  G_OBJECT_CLASS(klass)->get_property = visu_data_loadable_get_property;
  G_OBJECT_CLASS(klass)->set_property = visu_data_loadable_set_property;

  /**
   * VisuDataLoadable::n-files:
   *
   * Number of input files.
   *
   * Since: 3.8
   */
  _properties[PROP_N_SOURCES] =
    g_param_spec_uint("n-files", "N files", "number of input files",
                      1, 10, 1, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
  /**
   * VisuDataLoadable::label:
   *
   * A string that can be displayed representing the file names.
   *
   * Since: 3.8
   */
  _properties[PROP_LABEL] =
    g_param_spec_string("label", "Label", "representation of the filenames",
                        _("No input files"), G_PARAM_READABLE);
  /**
   * VisuDataLoadable::loading:
   *
   * TRUE during the loading of a file.
   *
   * Since: 3.8
   */
  _properties[PROP_LOADING] =
    g_param_spec_boolean("loading", "Loading", "TRUE when a file is loading",
                         FALSE, G_PARAM_READABLE);
  /**
   * VisuDataLoadable::status:
   *
   * A string describing the current status of the load process.
   *
   * Since: 3.8
   */
  _properties[PROP_STATUS] =
    g_param_spec_string("status", "Status", "loading status", "",
                       G_PARAM_READABLE);
  /**
   * VisuDataLoadable::auto-refresh:
   *
   * TRUE if any modification on input file should produce a refresh.
   *
   * Since: 3.8
   */
  _properties[PROP_AUTO_REFRESH] =
    g_param_spec_boolean("auto-refresh", "Auto refresh", "automatically reload on modification",
                         FALSE, G_PARAM_READWRITE);
  /**
   * VisuDataLoadable::refresh-period:
   *
   * Polling period for automatic reload in seconds.
   *
   * Since: 3.8
   */
  _properties[PROP_REFRESH_PERIOD] =
    g_param_spec_uint("refresh-period", "Refresh period", "Refresh period in seconds",
                      1, 3600, 1, G_PARAM_READWRITE);

  g_object_class_install_properties(G_OBJECT_CLASS(klass), N_PROPS, _properties);
}
static void visu_data_loadable_init(VisuDataLoadable *obj)
{
  obj->priv = visu_data_loadable_get_instance_private(obj);
  obj->priv->iSet = 0;
  obj->priv->nSets = 0;
  obj->priv->status = (gchar*)0;
  obj->priv->refreshId = 0;
  obj->priv->autoRefresh = FALSE;
  obj->priv->refreshPeriod = 1;
  obj->priv->lastReadTime = (time_t*)0;

  obj->priv->labels = (gchar**)0;
  visu_data_loadable_setNSets(obj, 1);
}
static void visu_data_loadable_get_property(GObject* obj, guint property_id,
                                            GValue *value, GParamSpec *pspec)
{
  VisuDataLoadable *self = VISU_DATA_LOADABLE(obj);

  switch (property_id)
    {
    case PROP_N_SOURCES:
      g_value_set_uint(value, self->priv->nFiles);
      break;
    case PROP_LABEL:
      g_value_set_static_string(value, _("No file"));
      break;
    case PROP_LOADING:
      g_value_set_boolean(value, self->priv->loading);
      break;
    case PROP_STATUS:
      if (self->priv->loading)
        g_value_set_string(value, self->priv->status);
      else
        g_value_set_string(value, (const gchar*)0);
      break;
    case PROP_AUTO_REFRESH:
      g_value_set_boolean(value, self->priv->autoRefresh);
      break;
    case PROP_REFRESH_PERIOD:
      g_value_set_uint(value, self->priv->refreshPeriod);
      break;
    default:
      /* We don't have any other property... */
      G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, property_id, pspec);
      break;
    }
}
static void visu_data_loadable_set_property(GObject* obj, guint property_id,
                                            const GValue *value, GParamSpec *pspec)
{
  VisuDataLoadable *self = VISU_DATA_LOADABLE(obj);

  switch (property_id)
    {
    case PROP_N_SOURCES:
      self->priv->nFiles = g_value_get_uint(value);
      self->priv->lastReadTime = g_malloc0(sizeof(time_t) * self->priv->nFiles);
      break;
    case PROP_AUTO_REFRESH:
      if (g_value_get_boolean(value) == self->priv->autoRefresh)
        return;
      self->priv->autoRefresh = g_value_get_boolean(value);
      if (self->priv->refreshId && !self->priv->autoRefresh)
        {
          g_source_remove(self->priv->refreshId);
          self->priv->refreshId = 0;
        }
      if (!self->priv->refreshId && self->priv->autoRefresh)
        self->priv->refreshId = g_timeout_add_seconds(self->priv->refreshPeriod,
                                                      (GSourceFunc)_reload, obj);
      break;
    case PROP_REFRESH_PERIOD:
      if (g_value_get_uint(value) ==  self->priv->refreshPeriod)
        return;
      self->priv->refreshPeriod = g_value_get_uint(value);
      if (self->priv->refreshId)
        {
          g_source_remove(self->priv->refreshId);
          self->priv->refreshId = 0;
        }
      if (self->priv->autoRefresh)
        self->priv->refreshId = g_timeout_add_seconds(self->priv->refreshPeriod,
                                                      (GSourceFunc)_reload, obj);
      break;
    default:
      /* We don't have any other property... */
      G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, property_id, pspec);
      break;
    }
}
static void visu_data_loadable_finalize(GObject* obj)
{
  VisuDataLoadable *self;

  self = VISU_DATA_LOADABLE(obj);

  g_free(self->priv->status);
  if (self->priv->refreshId)
    g_source_remove(self->priv->refreshId);
  g_free(self->priv->lastReadTime);
  g_strfreev(self->priv->labels);

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

static GQuark quark = (GQuark)0;
/**
 * visu_data_loadable_getErrorQuark:
 * 
 * Internal function to handle error.
 *
 * Returns: a #GQuark for #VisuDataLoadable method errors.
 */
GQuark visu_data_loadable_getErrorQuark(void)
{
  if (!quark)
    quark = g_quark_from_static_string("visu_data_loadable");

  return quark;
}

/**
 * visu_data_loadable_getFilename:
 * @self: a #VisuDataLoadable object.
 * @type: when the loadable is multi-files, the type of file.
 *
 * Returns the source filename for @type.
 *
 * Since: 3.8
 *
 * Returns: a filename.
 **/
const gchar* visu_data_loadable_getFilename(const VisuDataLoadable *self, guint type)
{
  g_return_val_if_fail(VISU_IS_DATA_LOADABLE(self), (const gchar*)0);

  if (VISU_DATA_LOADABLE_GET_CLASS(self)->getFilename)
    return VISU_DATA_LOADABLE_GET_CLASS(self)->getFilename(self, type);
  else
    return (const gchar*)0;
}

static gboolean _reload(VisuDataLoadable *loadable)
{
  struct stat statBuf;
  gboolean res;
  const gchar *file;
  guint i;
  GError *error;

  for (i = 0; i < loadable->priv->nFiles; i++)
    {
      file = visu_data_loadable_getFilename(loadable, i);
      g_return_val_if_fail(file, FALSE);

      if (!stat(file, &statBuf) && statBuf.st_ctime > loadable->priv->lastReadTime[i])
        {
          error = (GError*)0;
          res = visu_data_loadable_reload(loadable, (GCancellable*)0, &error);
          return loadable->priv->autoRefresh && res;
        }
    }
  return loadable->priv->autoRefresh;
}

/**
 * visu_data_loadable_checkFile:
 * @self: a #VisuDataLoadable object.
 * @fileType: a file type.
 * @error: an error location.
 *
 * Tests if the provided file for a given @fileType is a valid file.
 *
 * Since: 3.8
 *
 * Returns: TRUE if the file set for @fileType is a valid file.
 **/
gboolean visu_data_loadable_checkFile(VisuDataLoadable *self,
                                      guint fileType, GError **error)
{
  const gchar *file;
#if GLIB_MINOR_VERSION > 5
  struct stat buf;
#endif

  g_return_val_if_fail(!error || !*error, FALSE);

  file = visu_data_loadable_getFilename(self, fileType);
  if (!file)
    {
      *error = g_error_new(VISU_DATA_LOADABLE_ERROR, DATA_LOADABLE_ERROR_FILE,
                           _("No filename available."));
      return FALSE;
    }
  
  if (!g_file_test(file, G_FILE_TEST_IS_REGULAR))
    {
      *error = g_error_new(VISU_DATA_LOADABLE_ERROR, DATA_LOADABLE_ERROR_FILE,
                           _("File '%s' is not a regular file or may not exist."),
                           file);
      return FALSE;
    }
#if GLIB_MINOR_VERSION > 5
  if (!g_stat(file, &buf) && buf.st_size == 0)
    {
      *error = g_error_new(VISU_DATA_LOADABLE_ERROR, DATA_LOADABLE_ERROR_FILE,
                           _("File '%s' is empty."), file);
      return FALSE;
    }
#endif
  return TRUE;
}

#define USE_TASK 0
#if GLIB_MINOR_VERSION > 35 && USE_TASK == 1
static void _load(GTask *task _U_, VisuDataLoadable *self, gpointer data, GCancellable *cancel)
{
  GError *error;

  error = (GError*)0;
  VISU_DATA_LOADABLE_GET_CLASS(self)->load(self, GPOINTER_TO_INT(data),
                                           cancel, &error);
  if (error)
    g_task_return_error(task, error);
  else
    g_task_return_boolean(task, TRUE);
}
#endif

/**
 * visu_data_loadable_load:
 * @self: a #VisuDataLoadable object.
 * @iSet: an integer.
 * @cancel: (allow-none): a cancellation object.
 * @error: an error location.
 *
 * Call the load class method of @self. Also store the last read time
 * for each file of @self.
 *
 * Since: 3.8
 *
 * Returns: TRUE if @delf is successfully loaded.
 **/
gboolean visu_data_loadable_load(VisuDataLoadable *self, guint iSet,
                                 GCancellable *cancel, GError **error)
{
  gboolean res;
  guint i;
  const gchar *file;
  struct stat statBuf;
#if GLIB_MINOR_VERSION > 35 && USE_TASK == 1
  GTask *task;
#endif

  g_return_val_if_fail(VISU_IS_DATA_LOADABLE(self), FALSE);
  g_return_val_if_fail(!self->priv->loading &&
                       VISU_DATA_LOADABLE_GET_CLASS(self)->load, FALSE);
  g_return_val_if_fail(!visu_node_array_getNNodes(VISU_NODE_ARRAY(self)), FALSE);

  self->priv->loading = TRUE;
  g_object_notify_by_pspec(G_OBJECT(self), _properties[PROP_LOADING]);

#if GLIB_MINOR_VERSION > 35 && USE_TASK == 1
  task = g_task_new(self, cancel, NULL, NULL);
  g_task_set_task_data(task, GINT_TO_POINTER(iSet), (GDestroyNotify)0);
  g_task_run_in_thread_sync(task, (GTaskThreadFunc)_load);
  res = GPOINTER_TO_INT(g_task_propagate_pointer(task, error));
  g_object_unref(task);
#else
  res = VISU_DATA_LOADABLE_GET_CLASS(self)->load(self, iSet, cancel, error);
#endif

  self->priv->iSet = iSet;
  self->priv->loading = FALSE;
  g_object_notify_by_pspec(G_OBJECT(self), _properties[PROP_LOADING]);

  if (res)
    for (i = 0; i < self->priv->nFiles; i++)
      {
        file = visu_data_loadable_getFilename(self, i);
        g_return_val_if_fail(file, res);

        if (!stat(file, &statBuf))
          self->priv->lastReadTime[i] = statBuf.st_ctime;
      }

  return res;
}

/**
 * visu_data_loadable_reload:
 * @self: a #VisuDataLoadable object.
 * @cancel: (allow-none): a cancellation object.
 * @error: an error location.
 *
 * A wrapper to load again the same sub set of self.
 *
 * Since: 3.8
 *
 * Returns: TRUE if reload of @self is successful.
 **/
gboolean visu_data_loadable_reload(VisuDataLoadable *self,
                                   GCancellable *cancel, GError **error)
{
  g_return_val_if_fail(VISU_IS_DATA_LOADABLE(self), FALSE);

  visu_data_freePopulation(VISU_DATA(self));
  return visu_data_loadable_load(self, self->priv->iSet, cancel, error);
}

/**
 * visu_data_loadable_getSetId:
 * @self: a #VisuDataLoadable object.
 *
 * Some loadable format can store several #VisuData (like a MD
 * trajectory), see the @iSet attribute in visu_data_loadable_load().
 *
 * Since: 3.8
 *
 * Returns: the set id that has been loaded.
 **/
guint visu_data_loadable_getSetId(const VisuDataLoadable *self)
{
  g_return_val_if_fail(VISU_IS_DATA_LOADABLE(self), G_MAXUINT);

  return self->priv->iSet;
}

/**
 * visu_data_loadable_setNSets:
 * @self: a #VisuDataLoadable object.
 * @nSets: a positive number.
 *
 * Setup the number of #VisuData that @self is storing on disk.
 *
 * Since: 3.8
 *
 * Returns: TRUE if the actual number of sets for @self has changed.
 **/
gboolean visu_data_loadable_setNSets(VisuDataLoadable *self, guint nSets)
{
  guint i;

  g_return_val_if_fail(VISU_IS_DATA_LOADABLE(self), FALSE);

  if (self->priv->nSets == nSets)
    return FALSE;

  self->priv->nSets = nSets;
  if (self->priv->labels)
    g_strfreev(self->priv->labels);
  self->priv->labels = g_malloc(sizeof(gchar*) * (nSets + 1));
  for (i = 0; i < nSets; i++)
    self->priv->labels[i] = g_strdup("");
  self->priv->labels[nSets] = (gchar*)0;

  return TRUE;
}

/**
 * visu_data_loadable_getNSets:
 * @self: a #VisuDataLoadable object.
 *
 * Some loadable format can store several #VisuData (like a MD
 * trajectory), see the @iSet attribute in visu_data_loadable_load().
 *
 * Since: 3.8
 *
 * Returns: the set number of sets that has been read.
 **/
guint visu_data_loadable_getNSets(const VisuDataLoadable *self)
{
  g_return_val_if_fail(VISU_IS_DATA_LOADABLE(self), G_MAXUINT);

  return self->priv->nSets;
}

/**
 * visu_data_loadable_getSetLabel:
 * @self: a #VisuDataLoadable object.
 * @iSet: an id.
 *
 * Retrieves a possible label for the given set. If @iSet is the
 * currently loaded set, then this method is equivalent to calling
 * visu_data_getDescription().
 *
 * Since: 3.8
 *
 * Returns: a label.
 **/
const gchar* visu_data_loadable_getSetLabel(const VisuDataLoadable *self, guint iSet)
{
  g_return_val_if_fail(VISU_IS_DATA_LOADABLE(self) && iSet < self->priv->nSets,
                       (const gchar*)0);

  if (iSet == self->priv->iSet)
    return visu_data_getDescription(VISU_DATA(self));
  else
    return self->priv->labels[iSet];
}

/**
 * visu_data_loadable_setSetLabel:
 * @self: a #VisuDataLoadable object ;
 * @label: the message to be stored (null terminated) ;
 * @iSet: an integer.
 *
 * This method is used to store a description of the given @data. Before using this
 * method, the number of possible node sets must have been defined
 * using visu_data_loadable_setNSets(), if not, only iSet == 0 is allowed.
 */
void visu_data_loadable_setSetLabel(VisuDataLoadable *self, const gchar* label, guint iSet)
{
  g_return_if_fail(VISU_IS_DATA_LOADABLE(self) && iSet < self->priv->nSets);
  
  g_free(self->priv->labels[iSet]);
  self->priv->labels[iSet] = g_strdup(label);
  if (self->priv->iSet == iSet)
    visu_data_setDescription(VISU_DATA(self), label);
}

/**
 * visu_data_loadable_new_fromCLI:
 * 
 * Read the command line option and set the filenames for a new
 * #VisuData. The object is not loaded (files are not parsed), just prepared.
 *
 * Returns: (transfer full): a newly allocated #VisuData if required.
 */
VisuDataLoadable* visu_data_loadable_new_fromCLI(void)
{
  char* filename, *spin_filename;
  VisuDataLoadable *newData;

  newData = (VisuDataLoadable*)0;

  DBG_fprintf(stderr, "Visu DataLoadable: create data from command line arguments.\n");
  filename = commandLineGet_ArgFilename();
  spin_filename = commandLineGet_ArgSpinFileName();
  if (filename && !spin_filename)
    newData = VISU_DATA_LOADABLE(visu_data_atomic_new(filename, (VisuDataLoader*)0));
  else if(filename && spin_filename)
    newData = VISU_DATA_LOADABLE(visu_data_spin_new(filename, spin_filename,
                                                    (VisuDataLoader*)0,
                                                    (VisuDataLoader*)0));
  return newData;
}
