/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
/* logview-log.c - object representation of a logfile
 *
 * Copyright (C) 1998 Cesar Miquel <miquel@df.uba.ar>
 * Copyright (C) 2008 Cosimo Cecchi <cosimoc@gnome.org>
 *
 * 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., 551 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
 * USA.
 */

#include "config.h"

#include <glib.h>
#include <glib/gi18n.h>
#include <gio/gio.h>

#ifdef HAVE_ZLIB
#include <zlib.h>
#endif

#include "logview-log.h"
#include "logview-utils.h"

G_DEFINE_TYPE (LogviewLog, logview_log, G_TYPE_OBJECT);

#define GET_PRIVATE(o) \
  (G_TYPE_INSTANCE_GET_PRIVATE ((o), LOGVIEW_TYPE_LOG, LogviewLogPrivate))

enum {
  LOG_CHANGED,
  LAST_SIGNAL
};

static guint signals [LAST_SIGNAL] = { 0 };

struct _LogviewLogPrivate {
  /* file and monitor */
  GFile *file;
  GFileMonitor *mon;

  /* stats about the file */
  time_t file_time;
  goffset file_size;
  char *display_name;
  gboolean has_days;

  /* lines and relative days */
  GSList *days;
  GPtrArray *lines;
  guint lines_no;

  /* stream poiting to the log */
  GDataInputStream *stream;
  gboolean has_new_lines;
};

typedef struct {
  LogviewLog *log;
  GError *err;
  LogviewCreateCallback callback;
  gpointer user_data;
} LoadJob;

typedef struct {
  LogviewLog *log;
  GError *err;
  const char **lines;
  GSList *new_days;
  GCancellable *cancellable;
  LogviewNewLinesCallback callback;
  gpointer user_data;
} NewLinesJob;

typedef struct {
  GInputStream *parent_str;
  guchar * buffer;
  GFile *file;

  gboolean last_str_result;
  int last_z_result;
  z_stream zstream;
} GZHandle;

static void
do_finalize (GObject *obj)
{
  LogviewLog *log = LOGVIEW_LOG (obj);
  char ** lines;

  if (log->priv->stream) {
    g_object_unref (log->priv->stream);
    log->priv->stream = NULL;
  }

  if (log->priv->file) {
    g_object_unref (log->priv->file);
    log->priv->file = NULL;
  }

  if (log->priv->mon) {
    g_object_unref (log->priv->mon);
    log->priv->mon = NULL;
  }

  if (log->priv->days) {
    g_slist_free_full (log->priv->days,
                       (GDestroyNotify) logview_utils_day_free);
    log->priv->days = NULL;
  }

  if (log->priv->lines) {
    lines = (char **) g_ptr_array_free (log->priv->lines, FALSE);
    g_strfreev (lines);
    log->priv->lines = NULL;
  }

  G_OBJECT_CLASS (logview_log_parent_class)->finalize (obj);
}

static void
logview_log_class_init (LogviewLogClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = do_finalize;

  signals[LOG_CHANGED] = g_signal_new ("log-changed",
                                       G_OBJECT_CLASS_TYPE (object_class),
                                       G_SIGNAL_RUN_LAST,
                                       G_STRUCT_OFFSET (LogviewLogClass, log_changed),
                                       NULL, NULL,
                                       g_cclosure_marshal_VOID__VOID,
                                       G_TYPE_NONE, 0);

  g_type_class_add_private (klass, sizeof (LogviewLogPrivate));
}

static void
logview_log_init (LogviewLog *self)
{
  self->priv = GET_PRIVATE (self);

  self->priv->lines = NULL;
  self->priv->lines_no = 0;
  self->priv->days = NULL;
  self->priv->file = NULL;
  self->priv->mon = NULL;
  self->priv->has_new_lines = FALSE;
  self->priv->has_days = FALSE;
}

static void
monitor_changed_cb (GFileMonitor *monitor,
                    GFile *file,
                    GFile *unused,
                    GFileMonitorEvent event,
                    gpointer user_data)
{
  LogviewLog *log = user_data;

  if (event == G_FILE_MONITOR_EVENT_CHANGED) {
    log->priv->has_new_lines = TRUE;
    g_signal_emit (log, signals[LOG_CHANGED], 0, NULL);
  }
  /* TODO: handle the case where the log is deleted? */
}

static void
setup_file_monitor (LogviewLog *log)
{
  GError *err = NULL;

  log->priv->mon = g_file_monitor (log->priv->file,
                                   0, NULL, &err);
  if (err) {
    /* it'd be strange to get this error at this point but whatever */
    g_warning ("Impossible to monitor the log file: the changes won't be notified");
    g_error_free (err);
    return;
  }
  
  /* set the rate to 1sec, as I guess it's not unusual to have more than
   * one line written consequently or in a short time, being a log file.
   */
  g_file_monitor_set_rate_limit (log->priv->mon, 1000);
  g_signal_connect (log->priv->mon, "changed",
                    G_CALLBACK (monitor_changed_cb), log);
}

static GSList *
add_new_days_to_cache (LogviewLog *log, const char **new_lines, guint lines_offset)
{
  GSList *new_days, *l, *last_cached;
  int res;
  Day *day, *last;

  new_days = log_read_dates (new_lines, log->priv->file_time);

  /* the days are stored in chronological order, so we compare the last cached
   * one with the new we got.
   */
  last_cached = g_slist_last (log->priv->days);

  if (!last_cached) {
    /* this means the day list is empty (i.e. we're on the first read */
    log->priv->days = logview_utils_day_list_copy (new_days);
    return new_days;
  }

  for (l = new_days; l; l = l->next) {
    res = days_compare (l->data, last_cached->data);
    day = l->data;

    if (res > 0) {
      /* this day in the list is newer than the last one, append to
       * the cache.
       */
      day->first_line += lines_offset;
      day->last_line += lines_offset;
      log->priv->days = g_slist_append (log->priv->days, logview_utils_day_copy (day));
    } else if (res == 0) {
      last = last_cached->data;
 
      /* update the lines number */
      last->last_line += day->last_line;
    }
  }

  return new_days;
}

static gboolean
new_lines_job_done (gpointer data)
{
  NewLinesJob *job = data;

  if (job->err) {
    job->callback (job->log, NULL, NULL, job->err, job->user_data);
    g_error_free (job->err);
  } else {
    job->callback (job->log, job->lines, job->new_days, job->err, job->user_data);
  }

  g_clear_object (&job->cancellable);

  g_slist_free_full (job->new_days, (GDestroyNotify) logview_utils_day_free);

  /* drop the reference we acquired before */
  g_object_unref (job->log);

  g_slice_free (NewLinesJob, job);

  return FALSE;
}

static gboolean
do_read_new_lines (GIOSchedulerJob *io_job,
                   GCancellable *cancellable,
                   gpointer user_data)
{
  /* this runs in a separate thread */
  NewLinesJob *job = user_data;
  LogviewLog *log = job->log;
  char *line;
  GError *err = NULL;
  GPtrArray *lines;

  g_assert (LOGVIEW_IS_LOG (log));
  g_assert (log->priv->stream != NULL);

  if (!log->priv->lines) {
    log->priv->lines = g_ptr_array_new ();
    g_ptr_array_add (log->priv->lines, NULL);
  }

  lines = log->priv->lines;

  /* remove the NULL-terminator */
  g_ptr_array_remove_index (lines, lines->len - 1);

  while ((line = g_data_input_stream_read_line (log->priv->stream, NULL,
                                                job->cancellable, &err)) != NULL)
  {
    g_ptr_array_add (lines, (gpointer) line);
  }

  /* NULL-terminate the array again */
  g_ptr_array_add (lines, NULL);

  if (err) {
    job->err = err;
    goto out;
  }

  log->priv->has_new_lines = FALSE;

  /* we'll return only the new lines in the callback */
  line = g_ptr_array_index (lines, log->priv->lines_no);
  job->lines = (const char **) lines->pdata + log->priv->lines_no;

  /* save the new number of days and lines */
  job->new_days = add_new_days_to_cache (log, job->lines, log->priv->lines_no);
  log->priv->lines_no = (lines->len - 1);

out:
  g_io_scheduler_job_send_to_mainloop_async (io_job,
                                             new_lines_job_done,
                                             job, NULL);
  return FALSE;
}

static gboolean
log_load_done (gpointer user_data)
{
  LoadJob *job = user_data;

  if (job->err) {
    /* the callback will have NULL as log, and the error set */
    g_object_unref (job->log);
    job->callback (NULL, job->err, job->user_data);
    g_error_free (job->err);
  } else {
    job->callback (job->log, NULL, job->user_data);
    setup_file_monitor (job->log);
  }

  g_slice_free (LoadJob, job);

  return FALSE;
}

#ifdef HAVE_ZLIB

/* GZip functions adapted for GIO from mate-vfs/gzip-method.c */

#define Z_BUFSIZE 16384

#define GZIP_HEADER_SIZE 10
#define GZIP_MAGIC_1 0x1f
#define GZIP_MAGIC_2 0x8b
#define GZIP_FLAG_ASCII        0x01 /* bit 0 set: file probably ascii text */
#define GZIP_FLAG_HEAD_CRC     0x02 /* bit 1 set: header CRC present */
#define GZIP_FLAG_EXTRA_FIELD  0x04 /* bit 2 set: extra field present */
#define GZIP_FLAG_ORIG_NAME    0x08 /* bit 3 set: original file name present */
#define GZIP_FLAG_COMMENT      0x10 /* bit 4 set: file comment present */
#define GZIP_FLAG_RESERVED     0xE0 /* bits 5..7: reserved */

static gboolean
skip_string (GInputStream *is)
{
	guchar c;
	gssize bytes_read;

	do {
		bytes_read = g_input_stream_read (is, &c, 1, NULL, NULL);

		if (bytes_read != 1) {
			return FALSE;
    }
	} while (c != 0);

	return TRUE;
} 

static gboolean
read_gzip_header (GInputStream *is,
                  time_t *modification_time)
{
  gboolean res;
	guchar buffer[GZIP_HEADER_SIZE];
	gssize bytes, to_skip;
	guint mode;
	guint flags;

	bytes = g_input_stream_read (is, buffer, GZIP_HEADER_SIZE,
                               NULL, NULL);
  if (bytes == -1) {
    return FALSE;
  }

	if (bytes != GZIP_HEADER_SIZE)
		return FALSE;

	if (buffer[0] != GZIP_MAGIC_1 || buffer[1] != GZIP_MAGIC_2)
		return FALSE;

	mode = buffer[2];
	if (mode != 8) /* Mode: deflate */
		return FALSE;

	flags = buffer[3];

	if (flags & GZIP_FLAG_RESERVED)
		return FALSE;

	if (flags & GZIP_FLAG_EXTRA_FIELD) {
		guchar tmp[2];

    bytes = g_input_stream_read (is, tmp, 2, NULL, NULL);
  
    if (bytes != 2) {
			return FALSE;
    }

    to_skip = tmp[0] | (tmp[0] << 8);
    bytes = g_input_stream_skip (is, to_skip, NULL, NULL);
    if (bytes != to_skip) {
      return FALSE;
    }
	}

	if (flags & GZIP_FLAG_ORIG_NAME) {
    if (!skip_string (is)) {
      return FALSE;
    }
  }

	if (flags & GZIP_FLAG_COMMENT) {
    if (!skip_string (is)) {
      return FALSE;
    }
  }

	if (flags & GZIP_FLAG_HEAD_CRC) {
    bytes = g_input_stream_skip (is, 2, NULL, NULL);
		if (bytes != 2) {
      return FALSE;
    }
  }

  *modification_time = (buffer[4] | (buffer[5] << 8)
                        | (buffer[6] << 16) | (buffer[7] << 24));

	return TRUE;
}

static GZHandle *
gz_handle_new (GFile *file,
               GInputStream *parent_stream)
{
  GZHandle *ret;

  ret = g_new (GZHandle, 1);
  ret->parent_str = g_object_ref (parent_stream);
  ret->file = g_object_ref (file);
  ret->buffer = NULL;

  return ret;
}

static gboolean
gz_handle_init (GZHandle *gz)
{
  gz->zstream.zalloc = NULL;
  gz->zstream.zfree = NULL;
  gz->zstream.opaque = NULL;

  g_free (gz->buffer);
  gz->buffer = g_malloc (Z_BUFSIZE);
  gz->zstream.next_in = gz->buffer;
  gz->zstream.avail_in = 0;

  if (inflateInit2 (&gz->zstream, -MAX_WBITS) != Z_OK) {
    return FALSE;
  }

  gz->last_z_result = Z_OK;
  gz->last_str_result = TRUE;

  return TRUE;
}

static void
gz_handle_free (GZHandle *gz)
{
  g_object_unref (gz->parent_str);
  g_object_unref (gz->file);
  g_free (gz->buffer);
  g_free (gz);
}

static gboolean
fill_buffer (GZHandle *gz,
             gsize num_bytes)
{
  gboolean res;
  gsize count;

  z_stream * zstream = &gz->zstream;

  if (zstream->avail_in > 0) {
    return TRUE;
  }

  count = g_input_stream_read (gz->parent_str,
                               gz->buffer,
                               Z_BUFSIZE,
                               NULL, NULL);
  if (count == -1) {
    if (zstream->avail_out == num_bytes) {
      return FALSE;
    }
    gz->last_str_result = FALSE;
  } else {
    zstream->next_in = gz->buffer;
    zstream->avail_in = count;
  }

  return TRUE;
}

static gboolean
result_from_z_result (int z_result)
{
  switch (z_result) {
    case Z_OK:
    case Z_STREAM_END:
        return TRUE;
    case Z_DATA_ERROR:
      return FALSE;
    default:
      return FALSE;
  }
}

static gboolean
gz_handle_read (GZHandle *gz,
                guchar *buffer,
                gsize num_bytes,
                gsize * bytes_read)
{
  z_stream *zstream;
  gboolean res;
  int z_result;

  *bytes_read = 0;
  zstream = &gz->zstream;

  if (gz->last_z_result != Z_OK) {
    if (gz->last_z_result == Z_STREAM_END) {
      *bytes_read = 0;
      return TRUE;
    } else {
      return result_from_z_result (gz->last_z_result);
    }
  } else if (gz->last_str_result == FALSE) {
    return FALSE;
  }

  zstream->next_out = buffer;
  zstream->avail_out = num_bytes;

  while (zstream->avail_out != 0) {
    res = fill_buffer (gz, num_bytes);

    if (!res) {
      return res;
    }

    z_result = inflate (zstream, Z_NO_FLUSH);
    if (z_result == Z_STREAM_END) {
      gz->last_z_result = z_result;
      break;
    } else if (z_result != Z_OK) {
      gz->last_z_result = z_result;
    }
    
    if (gz->last_z_result != Z_OK && zstream->avail_out == num_bytes) {
      return result_from_z_result (gz->last_z_result);
    }
  }

  *bytes_read = num_bytes - zstream->avail_out;

  return TRUE;
}

static GError *
create_zlib_error (void)
{
  GError *err;

  err = g_error_new_literal (LOGVIEW_ERROR_QUARK, LOGVIEW_ERROR_ZLIB,
                             _("Error while uncompressing the GZipped log. The file "
                               "might be corrupt."));
  return err;
}

#endif /* HAVE_ZLIB */

static gboolean
log_load (GIOSchedulerJob *io_job,
          GCancellable *cancellable,
          gpointer user_data)
{
  /* this runs in a separate i/o thread */
  LoadJob *job = user_data;
  LogviewLog *log = job->log;
  GFile *f = log->priv->file;
  GFileInfo *info;
  GInputStream *is;
  const char *peeked_buffer;
  const char * parse_data[2];
  GSList *days;
  const char *content_type;
  GFileType type;
  GError *err = NULL;
  GTimeVal timeval;
  gboolean is_archive, can_read;

  info = g_file_query_info (f,
                            G_FILE_ATTRIBUTE_ACCESS_CAN_READ ","
                            G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE ","
                            G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
                            G_FILE_ATTRIBUTE_STANDARD_TYPE ","
                            G_FILE_ATTRIBUTE_STANDARD_SIZE ","
                            G_FILE_ATTRIBUTE_TIME_MODIFIED ",",
                            0, NULL, &err);
  if (err) {
    if (err->code == G_IO_ERROR_PERMISSION_DENIED) {
      /* TODO: PolicyKit integration */
    }
    goto out;
  }

  can_read = g_file_info_get_attribute_boolean (info,
                                                G_FILE_ATTRIBUTE_ACCESS_CAN_READ);
  if (!can_read) {
    /* TODO: PolicyKit integration */
    err = g_error_new_literal (LOGVIEW_ERROR_QUARK, LOGVIEW_ERROR_PERMISSION_DENIED,
                               _("You don't have enough permissions to read the file."));
    g_object_unref (info);

    goto out;
  }

  type = g_file_info_get_file_type (info);
  content_type = g_file_info_get_content_type (info);

  is_archive = g_content_type_equals (content_type, "application/x-gzip");

  if (type != (G_FILE_TYPE_REGULAR || G_FILE_TYPE_SYMBOLIC_LINK) ||
      (!g_content_type_is_a (content_type, "text/plain") && !is_archive))
  {
    err = g_error_new_literal (LOGVIEW_ERROR_QUARK, LOGVIEW_ERROR_NOT_A_LOG,
                               _("The file is not a regular file or is not a text file."));
    g_object_unref (info);

    goto out;
  }

  log->priv->file_size = g_file_info_get_size (info);
  g_file_info_get_modification_time (info, &timeval);
  log->priv->file_time = timeval.tv_sec;
  log->priv->display_name = g_strdup (g_file_info_get_display_name (info));

  g_object_unref (info);

  /* initialize the stream */
  is = G_INPUT_STREAM (g_file_read (f, NULL, &err));

  if (err) {
    if (err->code == G_IO_ERROR_PERMISSION_DENIED) {
      /* TODO: PolicyKit integration */
    }

    goto out;
  }

  if (is_archive) {
#ifdef HAVE_ZLIB
    GZHandle *gz;
    gboolean res;
    guchar * buffer;
    gsize bytes_read;
    GInputStream *real_is;
    time_t mtime; /* seconds */

    /* this also skips the header from |is| */
    res = read_gzip_header (is, &mtime);

    if (!res) {
      g_object_unref (is);

      err = create_zlib_error ();
      goto out;
    }

    log->priv->file_time = mtime;

    gz = gz_handle_new (f, is);
    res = gz_handle_init (gz);

    if (!res) {
      g_object_unref (is);
      gz_handle_free (gz);

      err = create_zlib_error ();
      goto out;
    }

    real_is = g_memory_input_stream_new ();

    do {
      buffer = g_malloc (1024);
      res = gz_handle_read (gz, buffer, 1024, &bytes_read);
      g_memory_input_stream_add_data (G_MEMORY_INPUT_STREAM (real_is),
                                      buffer, bytes_read, g_free);
    } while (res == TRUE && bytes_read > 0);

    if (!res) {
      gz_handle_free (gz);
      g_object_unref (real_is);
      g_object_unref (is);

      err = create_zlib_error ();
      goto out;
    }
 
    g_object_unref (is);
    is = real_is;

    gz_handle_free (gz);
#else /* HAVE_ZLIB */
    g_object_unref (is);

    err = g_error_new_literal (LOGVIEW_ERROR_QUARK, LOGVIEW_ERROR_NOT_SUPPORTED,
                               _("This version of System Log does not support GZipped logs."));
    goto out;
#endif /* HAVE_ZLIB */
  }

  log->priv->stream = g_data_input_stream_new (is);

  /* sniff into the stream for a timestamped line */
  g_buffered_input_stream_fill (G_BUFFERED_INPUT_STREAM (log->priv->stream),
                                (gssize) g_buffered_input_stream_get_buffer_size (G_BUFFERED_INPUT_STREAM (log->priv->stream)),
                                NULL, &err);
  if (err == NULL) {
    peeked_buffer = g_buffered_input_stream_peek_buffer
        (G_BUFFERED_INPUT_STREAM (log->priv->stream), NULL);
    parse_data[0] = peeked_buffer;
    parse_data[1] = NULL;

    if ((days = log_read_dates (parse_data, time (NULL))) != NULL) {
      log->priv->has_days = TRUE;
      g_slist_free_full (days, (GDestroyNotify) logview_utils_day_free);
    } else {
      log->priv->has_days = FALSE;
    }
  } else {
    log->priv->has_days = FALSE;
    g_clear_error (&err);
  }

  g_object_unref (is);

out:
  if (err) {
    job->err = err;
  }

  g_io_scheduler_job_send_to_mainloop_async (io_job,
                                             log_load_done,
                                             job, NULL);
  return FALSE;
}

static void
log_setup_load (LogviewLog *log, LogviewCreateCallback callback,
                gpointer user_data)
{
  LoadJob *job;

  job = g_slice_new0 (LoadJob);
  job->callback = callback;
  job->user_data = user_data;
  job->log = log;
  job->err = NULL;

  /* push the loading job into another thread */
  g_io_scheduler_push_job (log_load,
                           job,
                           NULL, 0, NULL);
}

/* public methods */

void
logview_log_read_new_lines (LogviewLog *log,
                            GCancellable *cancellable,
                            LogviewNewLinesCallback callback,
                            gpointer user_data)
{
  NewLinesJob *job;

  /* initialize the job struct with sensible values */
  job = g_slice_new0 (NewLinesJob);
  job->callback = callback;
  job->user_data = user_data;
  job->cancellable = (cancellable != NULL) ? g_object_ref (cancellable) : NULL;
  job->log = g_object_ref (log);
  job->err = NULL;
  job->lines = NULL;
  job->new_days = NULL;

  /* push the fetching job into another thread */
  g_io_scheduler_push_job (do_read_new_lines,
                           job,
                           NULL, 0,
                           job->cancellable);
}

void
logview_log_create (const char *filename, LogviewCreateCallback callback,
                    gpointer user_data)
{
  LogviewLog *log = g_object_new (LOGVIEW_TYPE_LOG, NULL);

  log->priv->file = g_file_new_for_path (filename);

  log_setup_load (log, callback, user_data);
}

void
logview_log_create_from_gfile (GFile *file, LogviewCreateCallback callback,
                               gpointer user_data)
{
  LogviewLog *log = g_object_new (LOGVIEW_TYPE_LOG, NULL);

  log->priv->file = g_object_ref (file);

  log_setup_load (log, callback, user_data);
}

const char *
logview_log_get_display_name (LogviewLog *log)
{
  g_assert (LOGVIEW_IS_LOG (log));

  return log->priv->display_name;
}

time_t
logview_log_get_timestamp (LogviewLog *log)
{
  g_assert (LOGVIEW_IS_LOG (log));

  return log->priv->file_time;
}

goffset
logview_log_get_file_size (LogviewLog *log)
{
  g_assert (LOGVIEW_IS_LOG (log));

  return log->priv->file_size;
}

guint
logview_log_get_cached_lines_number (LogviewLog *log)
{
  g_assert (LOGVIEW_IS_LOG (log));

  return log->priv->lines_no;
}

const char **
logview_log_get_cached_lines (LogviewLog *log)
{
  const char ** lines = NULL;

  g_assert (LOGVIEW_IS_LOG (log));

  if (log->priv->lines) {
    lines = (const char **) log->priv->lines->pdata;
  }

  return lines;
}

GSList *
logview_log_get_days_for_cached_lines (LogviewLog *log)
{
  g_assert (LOGVIEW_IS_LOG (log));

  return log->priv->days;
}

gboolean
logview_log_has_new_lines (LogviewLog *log)
{
  g_assert (LOGVIEW_IS_LOG (log));

  return log->priv->has_new_lines;
}

char *
logview_log_get_uri (LogviewLog *log)
{
  g_assert (LOGVIEW_IS_LOG (log));

  return g_file_get_uri (log->priv->file);
}

GFile *
logview_log_get_gfile (LogviewLog *log)
{
  g_assert (LOGVIEW_IS_LOG (log));

  return g_object_ref (log->priv->file);
}

gboolean
logview_log_get_has_days (LogviewLog *log)
{
  g_assert (LOGVIEW_IS_LOG (log));

  return log->priv->has_days;
}

