/* -*- Mode: C; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
/* Copyright (C) 2008 Cosimo Cecchi <cosimoc@gnome.org>
 * Copyright (C) 2012-2021 MATE Developers
 *
 * This file is part of MATE Utils.
 *
 * MATE Utils 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.
 *
 * MATE Utils 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 MATE Utils.  If not, see <https://www.gnu.org/licenses/>.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include "logview-app.h"

#include "logview-manager.h"
#include "logview-window.h"
#include "logview-prefs.h"

#include <glib/gi18n.h>

struct _LogviewAppPrivate {
  LogviewPrefs *prefs;
  LogviewManager *manager;
  LogviewWindow *window;
};

enum {
  APP_QUIT,
  LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

static LogviewApp *app_singleton = NULL;

G_DEFINE_TYPE_WITH_PRIVATE (LogviewApp, logview_app, G_TYPE_OBJECT);

static gboolean
main_window_delete_cb (GtkWidget *widget,
                       GdkEvent *event,
                       gpointer user_data)
{
  LogviewApp *app = user_data;

  g_signal_emit (app, signals[APP_QUIT], 0, NULL);

  return FALSE;
}

static gboolean
logview_app_set_window (LogviewApp *app)
{
  LogviewWindow *window;
  gboolean retval = FALSE;

  window = LOGVIEW_WINDOW (logview_window_new ());

  if (window) {
    app->priv->window = window;
    g_signal_connect (window, "delete-event",
                      G_CALLBACK (main_window_delete_cb), app);
    retval = TRUE;
  }

  gtk_window_set_default_icon_name ("mate-system-log");

  return retval;
}

typedef struct {
  LogviewApp *app;
  GSList *logs;
} EnumerateJob;

/* TODO: ideally we should parse configuration files in /etc/logrotate.conf
 * and all the files in /etc/logrotate.d/ and group all the logs referring
 * to the same entry under a category. Right now, we just do some
 * parsing instead, and fill with quasi-sensible defaults.
 */

/* adapted from sysklogd sources */
static GSList*
parse_syslog (void)
{
  char cbuf[BUFSIZ];
  char *cline, *p;
  FILE *cf;
  GSList *logfiles = NULL;

  if ((cf = fopen ("/etc/syslog.conf", "r")) == NULL) {
    return NULL;
  }

  cline = cbuf;
  while (fgets (cline, sizeof (cbuf) - (cline - cbuf), cf) != NULL) {
    gchar **list;
    gint i;

    for (p = cline; g_ascii_isspace (*p); ++p);
    if (*p == '\0' || *p == '#' || *p == '\n')
      continue;

    list = g_strsplit_set (p, ", -\t()\n", 0);

    for (i = 0; list[i]; ++i) {
      if (*list[i] == '/' &&
          g_slist_find_custom (logfiles, list[i],
                               (GCompareFunc) g_ascii_strcasecmp) == NULL)
      {
        logfiles = g_slist_insert (logfiles,
                                   g_strdup (list[i]), 0);
      }
    }

    g_strfreev (list);
  }

  fclose (cf);

  return logfiles;
}

static void
enumerate_job_finish (EnumerateJob *job)
{
  GSList *files = job->logs;
  LogviewApp *app = job->app;

  logview_manager_add_logs_from_name_list (app->priv->manager, files, files->data);

  g_slist_free_full (files, g_free);

  g_object_unref (job->app);
  g_slice_free (EnumerateJob, job);
}

static void
enumerate_next_files_async_cb (GObject *source,
                               GAsyncResult *res,
                               gpointer user_data)
{
  EnumerateJob *job = user_data;
  GList *enumerated_files, *l;
  GFileInfo *info;
  GSList *logs;
  const char *content_type, *name;
  char *parse_string, *container_path;
  GFileType type;
  GFile *container;

  enumerated_files = g_file_enumerator_next_files_finish (G_FILE_ENUMERATOR (source),
                                                          res, NULL);
  if (!enumerated_files) {
    enumerate_job_finish (job);
    return;
  }

  logs = job->logs;
  container = g_file_enumerator_get_container (G_FILE_ENUMERATOR (source));
  container_path = g_file_get_path (container);

  /* TODO: we don't support grouping rotated logs yet, skip gzipped files
   * and those which name contains a formatted date.
   */
  for (l = enumerated_files; l; l = l->next) {
    info = l->data;
    type = g_file_info_get_file_type (info);
    content_type = g_file_info_get_content_type (info);
    name = g_file_info_get_name (info);

    if (!g_file_info_get_attribute_boolean (info, "access::can-read")) {
      g_object_unref (info);
      continue;
    }

    if (type != (G_FILE_TYPE_REGULAR || G_FILE_TYPE_SYMBOLIC_LINK) ||
        !g_content_type_is_a (content_type, "text/plain"))
    {
      g_object_unref (info);
      continue;
    }

    if (g_content_type_is_a (content_type, "application/x-gzip")) {
      g_object_unref (info);
      continue;
    }

    if (g_regex_match_simple ("\\d{8}$", name, 0, 0)) {
      g_object_unref (info);
      continue;
    }

    parse_string = g_build_filename (container_path, name, NULL);

    if (g_slist_find_custom (logs, parse_string, (GCompareFunc) g_ascii_strcasecmp) == NULL) {
      logs = g_slist_append (logs, parse_string);
    } else {
      g_free (parse_string);
    }

    g_object_unref (info);
    parse_string = NULL;
  }

  g_list_free (enumerated_files);
  g_object_unref (container);
  g_free (container_path);

  job->logs = logs;

  enumerate_job_finish (job);
}

static void
enumerate_children_async_cb (GObject *source,
                             GAsyncResult *res,
                             gpointer user_data)
{
  EnumerateJob *job = user_data;
  GFileEnumerator *enumerator;

  enumerator = g_file_enumerate_children_finish (G_FILE (source),
                                                 res, NULL);
  if (!enumerator) {
    enumerate_job_finish (job);
    return;
  }

  g_file_enumerator_next_files_async (enumerator, G_MAXINT,
                                      G_PRIORITY_DEFAULT,
                                      NULL, enumerate_next_files_async_cb, job);
}

static void
logview_app_first_time_initialize (LogviewApp *app)
{
  GSList *logs;
  GFile *log_dir;
  EnumerateJob *job;

  /* let's add all accessible files in /var/log and those mentioned
   * in /etc/syslog.conf.
   */

  logs = parse_syslog ();

  job = g_slice_new0 (EnumerateJob);
  job->app = g_object_ref (app);
  job->logs = logs;

  log_dir = g_file_new_for_path ("/var/log/");
  g_file_enumerate_children_async (log_dir,
                                   "standard::*,access::can-read", 0,
                                   G_PRIORITY_DEFAULT, NULL,
                                   enumerate_children_async_cb, job);

  g_object_unref (log_dir);
}

static void
do_finalize (GObject *obj)
{
  LogviewApp *app = LOGVIEW_APP (obj);

  g_object_unref (app->priv->manager);
  g_object_unref (app->priv->prefs);

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

static void
logview_app_class_init (LogviewAppClass *klass)
{
  GObjectClass *oclass = G_OBJECT_CLASS (klass);

  oclass->finalize = do_finalize;

  signals[APP_QUIT] =
    g_signal_new ("app-quit",
                  G_OBJECT_CLASS_TYPE (oclass),
                  G_SIGNAL_RUN_LAST,
                  G_STRUCT_OFFSET (LogviewAppClass, app_quit),
                  NULL, NULL,
                  g_cclosure_marshal_VOID__VOID,
                  G_TYPE_NONE, 0);
}

static void
logview_app_init (LogviewApp *self)
{
  LogviewAppPrivate *priv = self->priv = logview_app_get_instance_private (self);

  priv->prefs = logview_prefs_get ();
  priv->manager = logview_manager_get ();
}

LogviewApp*
logview_app_get (void)
{
  if (!app_singleton) {
    app_singleton = g_object_new (LOGVIEW_TYPE_APP, NULL);

    if (!logview_app_set_window (app_singleton)) {
      g_object_unref (app_singleton);
      app_singleton = NULL;
    }
  }

  return app_singleton;
}

void
logview_app_initialize (LogviewApp *app, char **log_files)
{
  LogviewAppPrivate *priv;

  g_assert (LOGVIEW_IS_APP (app));

  priv = app->priv;

  /* open regular logs and add each log passed as a parameter */

  if (log_files == NULL) {
    char *active_log;
    gchar **logs;

    active_log = logview_prefs_get_active_logfile (priv->prefs);
    logs = logview_prefs_get_stored_logfiles (priv->prefs);

    if (!logs || !logs[0]) {
      logview_app_first_time_initialize (app);
    } else {
      logview_manager_add_logs_from_names (priv->manager,
                                           logs, active_log);

      g_free (active_log);
      g_strfreev (logs);
    }
  } else {
    logview_manager_add_logs_from_names (priv->manager, log_files, NULL);
  }

  gtk_widget_show (GTK_WIDGET (priv->window));
}

void
logview_app_add_error (LogviewApp *app,
                       const char *file_path,
                       const char *secondary)
{
  LogviewWindow *window;
  char *primary;

  g_assert (LOGVIEW_IS_APP (app));

  window = app->priv->window;
  primary = g_strdup_printf (_("Impossible to open the file %s"), file_path);

  logview_window_add_error (window, primary, secondary);

  g_free (primary);
}

static void
check_error_prefs (gpointer data,
                   gpointer user_data)
{
  gchar **strings = data;
  LogviewApp *app = user_data;
  GFile *file = g_file_new_for_path (strings[0]);

  logview_prefs_remove_stored_log (app->priv->prefs, file);
  g_object_unref (file);
}

void
logview_app_add_errors (LogviewApp *app,
                        GPtrArray *errors)
{
  LogviewWindow *window;

  g_assert (LOGVIEW_IS_APP (app));

  window = app->priv->window;

  if (errors->len == 0) {
    return;
  }

  g_ptr_array_foreach (errors, check_error_prefs, app);

  if (errors->len == 1) {
    char **err;

    err = g_ptr_array_index (errors, 0);
    logview_window_add_error (window, err[0], err[1]);
  } else {
    logview_window_add_errors (window, errors);
  }
}
