/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */

#include "config.h"

#include <errno.h>
#include <math.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#ifdef HAVE_SYS_RESOURCE_H
#include <sys/resource.h>
#endif

#include <GL/gl.h>
#include <cogl/cogl.h>

#include "shell-app-cache-private.h"
#include "shell-util.h"
#include <glib/gi18n-lib.h>
#include <gtk/gtk.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <meta/display.h>
#include <meta/meta-x11-display.h>

#include <locale.h>
#ifdef HAVE__NL_TIME_FIRST_WEEKDAY
#include <langinfo.h>
#endif

#ifdef HAVE_SYSTEMD
#include <systemd/sd-daemon.h>
#include <systemd/sd-login.h>
#else
/* So we don't need to add ifdef's everywhere */
#define sd_notify(u, m)            do {} while (0)
#define sd_notifyf(u, m, ...)      do {} while (0)
#endif

static void
stop_pick (ClutterActor *actor)
{
  g_signal_stop_emission_by_name (actor, "pick");
}

/**
 * shell_util_set_hidden_from_pick:
 * @actor: A #ClutterActor
 * @hidden: Whether @actor should be hidden from pick
 *
 * If @hidden is %TRUE, hide @actor from pick even with a mode of
 * %CLUTTER_PICK_ALL; if @hidden is %FALSE, unhide @actor.
 */
void
shell_util_set_hidden_from_pick (ClutterActor *actor,
                                 gboolean      hidden)
{
  gpointer existing_handler_data;

  existing_handler_data = g_object_get_data (G_OBJECT (actor),
                                             "shell-stop-pick");
  if (hidden)
    {
      if (existing_handler_data != NULL)
        return;
      g_signal_connect (actor, "pick", G_CALLBACK (stop_pick), NULL);
      g_object_set_data (G_OBJECT (actor),
                         "shell-stop-pick", GUINT_TO_POINTER (1));
    }
  else
    {
      if (existing_handler_data == NULL)
        return;
      g_signal_handlers_disconnect_by_func (actor, stop_pick, NULL);
      g_object_set_data (G_OBJECT (actor), "shell-stop-pick", NULL);
    }
}

/**
 * shell_util_get_week_start:
 *
 * Gets the first week day for the current locale, expressed as a
 * number in the range 0..6, representing week days from Sunday to
 * Saturday.
 *
 * Returns: A number representing the first week day for the current
 *          locale
 */
/* Copied from gtkcalendar.c */
int
shell_util_get_week_start (void)
{
  int week_start;
#ifdef HAVE__NL_TIME_FIRST_WEEKDAY
  union { unsigned int word; char *string; } langinfo;
  int week_1stday = 0;
  int first_weekday = 1;
  guint week_origin;
#else
  char *gtk_week_start;
#endif

#ifdef HAVE__NL_TIME_FIRST_WEEKDAY
  langinfo.string = nl_langinfo (_NL_TIME_FIRST_WEEKDAY);
  first_weekday = langinfo.string[0];
  langinfo.string = nl_langinfo (_NL_TIME_WEEK_1STDAY);
  week_origin = langinfo.word;
  if (week_origin == 19971130) /* Sunday */
    week_1stday = 0;
  else if (week_origin == 19971201) /* Monday */
    week_1stday = 1;
  else
    g_warning ("Unknown value of _NL_TIME_WEEK_1STDAY.\n");

  week_start = (week_1stday + first_weekday - 1) % 7;
#else
  /* Use a define to hide the string from xgettext */
# define GTK_WEEK_START "calendar:week_start:0"
  gtk_week_start = dgettext ("gtk30", GTK_WEEK_START);

  if (strncmp (gtk_week_start, "calendar:week_start:", 20) == 0)
    week_start = *(gtk_week_start + 20) - '0';
  else
    week_start = -1;

  if (week_start < 0 || week_start > 6)
    {
      g_warning ("Whoever translated calendar:week_start:0 for GTK+ "
                 "did so wrongly.\n");
      week_start = 0;
    }
#endif

  return week_start;
}

/**
 * shell_util_translate_time_string:
 * @str: String to translate
 *
 * Translate @str according to the locale defined by LC_TIME; unlike
 * dcgettext(), the translations is still taken from the LC_MESSAGES
 * catalogue and not the LC_TIME one.
 *
 * Returns: the translated string
 */
const char *
shell_util_translate_time_string (const char *str)
{
  const char *locale = g_getenv ("LC_TIME");
  const char *res;
  char *sep;
  locale_t old_loc;
  locale_t loc = (locale_t) 0;

  if (locale)
    loc = newlocale (LC_MESSAGES_MASK, locale, (locale_t) 0);

  old_loc = uselocale (loc);

  sep = strchr (str, '\004');
  res = g_dpgettext (NULL, str, sep ? sep - str + 1 : 0);

  uselocale (old_loc);

  if (loc != (locale_t) 0)
    freelocale (loc);

  return res;
}

/**
 * shell_util_regex_escape:
 * @str: a UTF-8 string to escape
 *
 * A wrapper around g_regex_escape_string() that takes its argument as
 * \0-terminated string rather than a byte-array that confuses gjs.
 *
 * Returns: @str with all regex-special characters escaped
 */
char *
shell_util_regex_escape (const char *str)
{
  return g_regex_escape_string (str, -1);
}

/**
 * shell_write_string_to_stream:
 * @stream: a #GOutputStream
 * @str: a UTF-8 string to write to @stream
 * @error: location to store GError
 *
 * Write a string to a GOutputStream as UTF-8. This is a workaround
 * for not having binary buffers in GJS.
 *
 * Return value: %TRUE if write succeeded
 */
gboolean
shell_write_string_to_stream (GOutputStream *stream,
                              const char    *str,
                              GError       **error)
{
  return g_output_stream_write_all (stream, str, strlen (str),
                                    NULL, NULL, error);
}

/**
 * shell_get_file_contents_utf8_sync:
 * @path: UTF-8 encoded filename path
 * @error: a #GError
 *
 * Synchronously load the contents of a file as a NUL terminated
 * string, validating it as UTF-8.  Embedded NUL characters count as
 * invalid content.
 *
 * Returns: (transfer full): File contents
 */
char *
shell_get_file_contents_utf8_sync (const char *path,
                                   GError    **error)
{
  char *contents;
  gsize len;
  if (!g_file_get_contents (path, &contents, &len, error))
    return NULL;
  if (!g_utf8_validate (contents, len, NULL))
    {
      g_free (contents);
      g_set_error (error,
                   G_IO_ERROR,
                   G_IO_ERROR_FAILED,
                   "File %s contains invalid UTF-8",
                   path);
      return NULL;
    }
  return contents;
}

static void
touch_file (GTask        *task,
            gpointer      object,
            gpointer      task_data,
            GCancellable *cancellable)
{
  GFile *file = object;
  g_autoptr (GFile) parent = NULL;
  g_autoptr (GFileOutputStream) stream = NULL;
  GError *error = NULL;

  parent = g_file_get_parent (file);
  g_file_make_directory_with_parents (parent, cancellable, &error);

  if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
    {
      g_task_return_error (task, error);
      return;
    }
  g_clear_error (&error);

  stream = g_file_create (file, G_FILE_CREATE_NONE, cancellable, &error);

  if (error && !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
    {
      g_task_return_error (task, error);
      return;
    }
  g_clear_error (&error);

  if (stream)
    g_output_stream_close (G_OUTPUT_STREAM (stream), NULL, NULL);

  g_task_return_boolean (task, stream != NULL);
}

void
shell_util_touch_file_async (GFile               *file,
                             GAsyncReadyCallback  callback,
                             gpointer             user_data)
{
  g_autoptr (GTask) task = NULL;

  g_return_if_fail (G_IS_FILE (file));

  task = g_task_new (file, NULL, callback, user_data);
  g_task_set_source_tag (task, shell_util_touch_file_async);

  g_task_run_in_thread (task, touch_file);
}

gboolean
shell_util_touch_file_finish (GFile         *file,
                              GAsyncResult  *res,
                              GError       **error)
{
  g_return_val_if_fail (G_IS_FILE (file), FALSE);
  g_return_val_if_fail (G_IS_TASK (res), FALSE);

  return g_task_propagate_boolean (G_TASK (res), error);
}

/**
 * shell_util_wifexited:
 * @status: the status returned by wait() or waitpid()
 * @exit: (out): the actual exit status of the process
 *
 * Implements libc standard WIFEXITED, that cannot be used JS
 * code.
 * Returns: TRUE if the process exited normally, FALSE otherwise
 */
gboolean
shell_util_wifexited (int  status,
                      int *exit)
{
  gboolean ret;

  ret = WIFEXITED(status);

  if (ret)
    *exit = WEXITSTATUS(status);

  return ret;
}

/**
 * shell_util_create_pixbuf_from_data:
 * @data: (array length=len) (element-type guint8) (transfer full):
 * @len:
 * @colorspace:
 * @has_alpha:
 * @bits_per_sample:
 * @width:
 * @height:
 * @rowstride:
 *
 * Workaround for non-introspectability of gdk_pixbuf_from_data().
 *
 * Returns: (transfer full):
 */
GdkPixbuf *
shell_util_create_pixbuf_from_data (const guchar      *data,
                                    gsize              len,
                                    GdkColorspace      colorspace,
                                    gboolean           has_alpha,
                                    int                bits_per_sample,
                                    int                width,
                                    int                height,
                                    int                rowstride)
{
  return gdk_pixbuf_new_from_data (data, colorspace, has_alpha,
                                   bits_per_sample, width, height, rowstride,
                                   (GdkPixbufDestroyNotify) g_free, NULL);
}

typedef const gchar *(*ShellGLGetString) (GLenum);

static gboolean
canvas_draw_cb (ClutterContent *content,
                cairo_t        *cr,
                gint            width,
                gint            height,
                gpointer        user_data)
{
  cairo_surface_t *surface = user_data;

  cairo_set_source_surface (cr, surface, 0, 0);
  cairo_paint (cr);

  return FALSE;
}

/**
 * shell_util_get_content_for_window_actor:
 * @window_actor: a #MetaWindowActor
 * @window_rect: a #MetaRectangle
 *
 * Returns: (transfer full) (nullable): a new #ClutterContent
 */
ClutterContent *
shell_util_get_content_for_window_actor (MetaWindowActor *window_actor,
                                         MetaRectangle   *window_rect)
{
  ClutterContent *content;
  cairo_surface_t *surface;
  cairo_rectangle_int_t clip;
  gfloat actor_x, actor_y;

  clutter_actor_get_position (CLUTTER_ACTOR (window_actor), &actor_x, &actor_y);

  clip.x = window_rect->x - (gint) actor_x;
  clip.y = window_rect->y - (gint) actor_y;
  clip.width = window_rect->width;
  clip.height = window_rect->height;

  surface = meta_window_actor_get_image (window_actor, &clip);

  if (!surface)
    return NULL;

  content = clutter_canvas_new ();
  clutter_canvas_set_size (CLUTTER_CANVAS (content),
                           cairo_image_surface_get_width (surface),
                           cairo_image_surface_get_height (surface));
  g_signal_connect (content, "draw",
                    G_CALLBACK (canvas_draw_cb), surface);
  clutter_content_invalidate (content);
  cairo_surface_destroy (surface);

  return content;
}

cairo_surface_t *
shell_util_composite_capture_images (ClutterCapture  *captures,
                                     int              n_captures,
                                     int              x,
                                     int              y,
                                     int              target_width,
                                     int              target_height,
                                     float            target_scale)
{
  int i;
  cairo_format_t format;
  cairo_surface_t *image;
  cairo_t *cr;

  g_assert (n_captures > 0);
  g_assert (target_scale > 0.0f);

  format = cairo_image_surface_get_format (captures[0].image);
  image = cairo_image_surface_create (format, target_width, target_height);
  cairo_surface_set_device_scale (image, target_scale, target_scale);

  cr = cairo_create (image);

  for (i = 0; i < n_captures; i++)
    {
      ClutterCapture *capture = &captures[i];

      cairo_save (cr);

      cairo_translate (cr,
                       capture->rect.x - x,
                       capture->rect.y - y);
      cairo_set_source_surface (cr, capture->image, 0, 0);
      cairo_paint (cr);

      cairo_restore (cr);
    }
  cairo_destroy (cr);

  return image;
}

#ifndef HAVE_FDWALK
static int
fdwalk (int  (*cb)(void *data, int fd),
        void  *data)
{
  gint open_max;
  gint fd;
  gint res = 0;

#ifdef HAVE_SYS_RESOURCE_H
  struct rlimit rl;
#endif

#ifdef __linux__
  DIR *d;

  if ((d = opendir("/proc/self/fd")))
    {
      struct dirent *de;

      while ((de = readdir(d)))
        {
          glong l;
          gchar *e = NULL;

          if (de->d_name[0] == '.')
            continue;

          errno = 0;
          l = strtol(de->d_name, &e, 10);
          if (errno != 0 || !e || *e)
            continue;

          fd = (gint) l;

          if ((glong) fd != l)
            continue;

          if (fd == dirfd(d))
            continue;

          if ((res = cb (data, fd)) != 0)
            break;
        }

      closedir(d);
      return res;
  }

  /* If /proc is not mounted or not accessible we fall back to the old
   * rlimit trick */

#endif

#ifdef HAVE_SYS_RESOURCE_H
  if (getrlimit (RLIMIT_NOFILE, &rl) == 0 && rl.rlim_max != RLIM_INFINITY)
    open_max = rl.rlim_max;
  else
#endif
    open_max = sysconf (_SC_OPEN_MAX);

  for (fd = 0; fd < open_max; fd++)
    if ((res = cb (data, fd)) != 0)
      break;

  return res;
}
#endif

static int
check_cloexec (void *data,
               gint  fd)
{
  int r;

  if (fd < 3)
    return 0;

  r = fcntl (fd, F_GETFD);
  if (r < 0)
    return 0;

  if (!(r & FD_CLOEXEC))
    g_warning ("fd %d is not CLOEXEC", fd);

  return 0;
}

/**
 * shell_util_check_cloexec_fds:
 *
 * Walk over all open file descriptors. Check them for the FD_CLOEXEC flag.
 * If this flag is not set, log the offending file descriptor number.
 *
 * It is important that scarecrow-shell's file descriptors are all marked CLOEXEC,
 * so that the shell's open file descriptors are not passed to child processes
 * that we launch.
 */
void
shell_util_check_cloexec_fds (void)
{
  fdwalk (check_cloexec, NULL);
  g_info ("Open fd CLOEXEC check complete");
}

/**
 * shell_util_get_uid:
 *
 * A wrapper around getuid() so that it can be used from JavaScript. This
 * function will always succeed.
 *
 * Returns: the real user ID of the calling process
 */
gint
shell_util_get_uid (void)
{
  return getuid ();
}

typedef struct {
  GDBusConnection *connection;
  gchar           *command;

  GCancellable *cancellable;
  gulong        cancel_id;

  guint    job_watch;
  gchar   *job;
} SystemdCall;

static void
shell_util_systemd_call_data_free (SystemdCall *data)
{
  if (data->job_watch)
    {
      g_dbus_connection_signal_unsubscribe (data->connection, data->job_watch);
      data->job_watch = 0;
    }

  if (data->cancellable)
    {
      g_cancellable_disconnect (data->cancellable, data->cancel_id);
      g_clear_object (&data->cancellable);
      data->cancel_id = 0;
    }

  g_clear_object (&data->connection);
  g_clear_pointer (&data->job, g_free);
  g_clear_pointer (&data->command, g_free);
  g_free (data);
}

static void
shell_util_systemd_call_cancelled_cb (GCancellable *cancellable,
                                      GTask        *task)
{
  SystemdCall *data = g_task_get_task_data (task);

  /* Task has returned, but data is not yet free'ed, ignore signal. */
  if (g_task_get_completed (task))
    return;

  /* We are still in the DBus call; it will return the error. */
  if (data->job == NULL)
    return;

  g_task_return_error_if_cancelled (task);
  g_object_unref (task);
}

static void
on_systemd_call_cb (GObject      *source,
                    GAsyncResult *res,
                    gpointer      user_data)
{
  g_autoptr (GVariant) reply = NULL;
  g_autoptr (GError) error = NULL;
  GTask *task = G_TASK (user_data);
  SystemdCall *data;

  reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source),
                                         res, &error);

  data = g_task_get_task_data (task);

  if (error) {
    g_warning ("Could not issue '%s' systemd call", data->command);
    g_task_return_error (task, g_steal_pointer (&error));
    g_object_unref (task);

    return;
  }

  g_assert (data->job == NULL);
  g_variant_get (reply, "(o)", &data->job);

  /* And we wait for the JobRemoved notification. */
}

static void
on_systemd_job_removed_cb (GDBusConnection *connection,
                           const gchar *sender_name,
                           const gchar *object_path,
                           const gchar *interface_name,
                           const gchar *signal_name,
                           GVariant *parameters,
                           gpointer user_data)
{
  GTask *task = G_TASK (user_data);
  SystemdCall *data;
  guint32 id;
  const char *path, *unit, *result;

  /* Task has returned, but data is not yet free'ed, ignore signal. */
  if (g_task_get_completed (task))
    return;

  data = g_task_get_task_data (task);

  /* No job information yet, ignore. */
  if (data->job == NULL)
    return;

  g_variant_get (parameters, "(u&o&s&s)", &id, &path, &unit, &result);

  /* Is it the job we are waiting for? */
  if (g_strcmp0 (path, data->job) != 0)
    return;

  /* Task has completed; return the result of the job */
  if (g_strcmp0 (result, "done") == 0)
    g_task_return_boolean (task, TRUE);
  else
    g_task_return_new_error (task,
                             G_IO_ERROR,
                             G_IO_ERROR_FAILED,
                             "Systemd job completed with status \"%s\"",
                             result);

  g_object_unref (task);
}

static void
shell_util_systemd_call (const char           *command,
                         const char           *unit,
                         const char           *mode,
                         GCancellable         *cancellable,
                         GAsyncReadyCallback   callback,
                         gpointer              user_data)
{
  g_autoptr (GTask) task = g_task_new (NULL, cancellable, callback, user_data);

#ifdef HAVE_SYSTEMD
  g_autoptr (GDBusConnection) connection = NULL;
  GError *error = NULL;
  SystemdCall *data;
  g_autofree char *self_unit = NULL;
  int res;

  connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);

  if (connection == NULL) {
    g_task_return_error (task, error);
    return;
  }

  /* We look up the systemd unit that our own process is running in here.
   * This way we determine whether the session is managed using systemd.
   */
  res = sd_pid_get_user_unit (getpid (), &self_unit);

  if (res == -ENODATA)
    {
      g_task_return_new_error (task,
                               G_IO_ERROR,
                               G_IO_ERROR_NOT_SUPPORTED,
                               "Not systemd managed");
      return;
    }
  else if (res < 0)
    {
      g_task_return_new_error (task,
                               G_IO_ERROR,
                               g_io_error_from_errno (-res),
                               "Error fetching own systemd unit: %s",
                               g_strerror (-res));
      return;
    }

  data = g_new0 (SystemdCall, 1);
  data->command = g_strdup (command);
  data->connection = g_object_ref (connection);
  data->job_watch = g_dbus_connection_signal_subscribe (connection,
                                                        "org.freedesktop.systemd1",
                                                        "org.freedesktop.systemd1.Manager",
                                                        "JobRemoved",
                                                        "/org/freedesktop/systemd1",
                                                        NULL,
                                                        G_DBUS_SIGNAL_FLAGS_NONE,
                                                        on_systemd_job_removed_cb,
                                                        task,
                                                        NULL);
  g_task_set_task_data (task,
                        data,
                        (GDestroyNotify) shell_util_systemd_call_data_free);

  if (cancellable)
    {
      data->cancellable = g_object_ref (cancellable);
      data->cancel_id = g_cancellable_connect (cancellable,
                                               G_CALLBACK (shell_util_systemd_call_cancelled_cb),
                                               task,
                                               NULL);
    }

  g_dbus_connection_call (connection,
                          "org.freedesktop.systemd1",
                          "/org/freedesktop/systemd1",
                          "org.freedesktop.systemd1.Manager",
                          command,
                          g_variant_new ("(ss)",
                                         unit, mode),
                          G_VARIANT_TYPE ("(o)"),
                          G_DBUS_CALL_FLAGS_NONE,
                          -1, cancellable,
                          on_systemd_call_cb,
                          g_steal_pointer (&task));
#else /* HAVE_SYSTEMD */
  g_task_return_new_error (task,
                           G_IO_ERROR,
                           G_IO_ERROR_NOT_SUPPORTED,
                           "systemd not supported by scarecrow-shell");
#endif /* !HAVE_SYSTEMD */
}

void
shell_util_start_systemd_unit (const char           *unit,
                               const char           *mode,
                               GCancellable         *cancellable,
                               GAsyncReadyCallback   callback,
                               gpointer              user_data)
{
  shell_util_systemd_call ("StartUnit", unit, mode,
                           cancellable, callback, user_data);
}

gboolean
shell_util_start_systemd_unit_finish (GAsyncResult  *res,
                                      GError       **error)
{
  return g_task_propagate_boolean (G_TASK (res), error);
}

void
shell_util_stop_systemd_unit (const char           *unit,
                              const char           *mode,
                              GCancellable         *cancellable,
                              GAsyncReadyCallback   callback,
                              gpointer              user_data)
{
  shell_util_systemd_call ("StopUnit", unit, mode,
                           cancellable, callback, user_data);
}

gboolean
shell_util_stop_systemd_unit_finish (GAsyncResult  *res,
                                     GError       **error)
{
  return g_task_propagate_boolean (G_TASK (res), error);
}

void
shell_util_sd_notify (void)
{
  /* We only use NOTIFY_SOCKET exactly once; unset it so it doesn't remain in
   * our environment. */
  sd_notify (1, "READY=1");
}

/**
 * shell_util_has_x11_display_extension:
 * @display: A #MetaDisplay
 * @extension: An X11 extension
 *
 * If the corresponding X11 display provides the passed extension, return %TRUE,
 * otherwise %FALSE. If there is no X11 display, %FALSE is passed.
 */
gboolean
shell_util_has_x11_display_extension (MetaDisplay *display,
                                      const char  *extension)
{
  MetaX11Display *x11_display;
  Display *xdisplay;
  int op, event, error;

  x11_display = meta_display_get_x11_display (display);
  if (!x11_display)
    return FALSE;

  xdisplay = meta_x11_display_get_xdisplay (x11_display);
  return XQueryExtension (xdisplay, extension, &op, &event, &error);
}

/**
 * shell_util_get_translated_folder_name:
 * @name: the untranslated folder name
 *
 * Attempts to translate the folder @name using translations provided
 * by .directory files.
 *
 * Returns: (nullable): a translated string or %NULL
 */
char *
shell_util_get_translated_folder_name (const char *name)
{
  return shell_app_cache_translate_folder (shell_app_cache_get_default (), name);
}
