/* GTK - The GIMP Toolkit
 * gtkprintoperation-portal.c: Print Operation Details for sandboxed apps
 * Copyright (C) 2016, Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <string.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#include <cairo-pdf.h>
#include <cairo-ps.h>

#include <gio/gunixfdlist.h>

#include "gtkprintoperation-private.h"
#include "gtkprintoperation-portal.h"
#include "gtkprintsettings.h"
#include "gtkpagesetup.h"
#include "gtkprintbackend.h"
#include "gtkshow.h"
#include "gtkintl.h"
#include "gtkwindowprivate.h"
#include "gtkprivate.h"


typedef struct {
  GtkPrintOperation *op;
  GDBusProxy *proxy;
  guint response_signal_id;
  gboolean do_print;
  GtkPrintOperationResult result;
  GtkPrintOperationPrintFunc print_cb;
  GtkWindow *parent;
  GMainLoop *loop;
  guint32 token;
  GDestroyNotify destroy;
  GVariant *settings;
  GVariant *setup;
  GVariant *options;
  char *prepare_print_handle;
} PortalData;

static void
portal_data_free (gpointer data)
{
  PortalData *portal = data;

  g_object_unref (portal->op);
  g_object_unref (portal->proxy);
  if (portal->loop)
    g_main_loop_unref (portal->loop);
  if (portal->settings)
    g_variant_unref (portal->settings);
  if (portal->setup)
    g_variant_unref (portal->setup);
  if (portal->options)
    g_variant_unref (portal->options);
  g_free (portal->prepare_print_handle);
  g_free (portal);
}

typedef struct {
  GDBusProxy *proxy;
  GtkPrintJob *job;
  guint32 token;
  cairo_surface_t *surface;
  GMainLoop *loop;
  gboolean file_written;
} GtkPrintOperationPortal;

static void
op_portal_free (GtkPrintOperationPortal *op_portal)
{
  g_clear_object (&op_portal->proxy);
  g_clear_object (&op_portal->job);
  if (op_portal->loop)
    g_main_loop_unref (op_portal->loop);
  g_free (op_portal);
}

static void
portal_start_page (GtkPrintOperation *op,
                   GtkPrintContext   *print_context,
                   GtkPageSetup      *page_setup)
{
  GtkPrintOperationPortal *op_portal = op->priv->platform_data;
  GtkPaperSize *paper_size;
  cairo_surface_type_t type;
  gdouble w, h;

  paper_size = gtk_page_setup_get_paper_size (page_setup);

  w = gtk_paper_size_get_width (paper_size, GTK_UNIT_POINTS);
  h = gtk_paper_size_get_height (paper_size, GTK_UNIT_POINTS);

  type = cairo_surface_get_type (op_portal->surface);

  if ((op->priv->manual_number_up < 2) ||
      (op->priv->page_position % op->priv->manual_number_up == 0))
    {
      if (type == CAIRO_SURFACE_TYPE_PS)
        {
          cairo_ps_surface_set_size (op_portal->surface, w, h);
          cairo_ps_surface_dsc_begin_page_setup (op_portal->surface);
          switch (gtk_page_setup_get_orientation (page_setup))
            {
              case GTK_PAGE_ORIENTATION_PORTRAIT:
              case GTK_PAGE_ORIENTATION_REVERSE_PORTRAIT:
                cairo_ps_surface_dsc_comment (op_portal->surface, "%%PageOrientation: Portrait");
                break;

              case GTK_PAGE_ORIENTATION_LANDSCAPE:
              case GTK_PAGE_ORIENTATION_REVERSE_LANDSCAPE:
                cairo_ps_surface_dsc_comment (op_portal->surface, "%%PageOrientation: Landscape");
                break;
            }
         }
      else if (type == CAIRO_SURFACE_TYPE_PDF)
        {
          if (!op->priv->manual_orientation)
            {
              w = gtk_page_setup_get_paper_width (page_setup, GTK_UNIT_POINTS);
              h = gtk_page_setup_get_paper_height (page_setup, GTK_UNIT_POINTS);
            }
          cairo_pdf_surface_set_size (op_portal->surface, w, h);
        }
    }
}

static void
portal_end_page (GtkPrintOperation *op,
                 GtkPrintContext   *print_context)
{
  cairo_t *cr;

  cr = gtk_print_context_get_cairo_context (print_context);

  if ((op->priv->manual_number_up < 2) ||
      ((op->priv->page_position + 1) % op->priv->manual_number_up == 0) ||
      (op->priv->page_position == op->priv->nr_of_pages_to_print - 1))
    cairo_show_page (cr);
}

static void
print_file_done (GObject *source,
                 GAsyncResult *result,
                 gpointer data)
{
  GtkPrintOperation *op = data;
  GtkPrintOperationPortal *op_portal = op->priv->platform_data;
  GError *error = NULL;
  GVariant *ret;

  ret = g_dbus_proxy_call_finish (op_portal->proxy,
                                  result,
                                  &error);
  if (ret == NULL)
    {
      if (op->priv->error == NULL)
        op->priv->error = g_error_copy (error);
      g_warning ("Print file failed: %s", error->message);
      g_error_free (error);
    }
  else
    g_variant_unref (ret);

  if (op_portal->loop)
    g_main_loop_quit (op_portal->loop);

  g_object_unref (op);
}

static void
portal_job_complete (GtkPrintJob  *job,
                     gpointer      data,
                     const GError *error)
{
  GtkPrintOperation *op = data;
  GtkPrintOperationPortal *op_portal = op->priv->platform_data;
  GtkPrintSettings *settings;
  const char *uri;
  char *filename;
  int fd, idx;
  GVariantBuilder opt_builder;
  GUnixFDList *fd_list;

  if (error != NULL && op->priv->error == NULL)
    {
      g_warning ("Print job failed: %s", error->message);
      op->priv->error = g_error_copy (error);
      return;
    }

  op_portal->file_written = TRUE;

  settings = gtk_print_job_get_settings (job);
  uri = gtk_print_settings_get (settings, GTK_PRINT_SETTINGS_OUTPUT_URI);
  filename = g_filename_from_uri (uri, NULL, NULL);

  fd = open (filename, O_RDONLY|O_CLOEXEC);
  fd_list = g_unix_fd_list_new ();
  idx = g_unix_fd_list_append (fd_list, fd, NULL);
  close (fd);

  g_free (filename);

  g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT);
  g_variant_builder_add (&opt_builder, "{sv}",  "token", g_variant_new_uint32 (op_portal->token));

  g_dbus_proxy_call_with_unix_fd_list (op_portal->proxy,
                                       "Print",
                                       g_variant_new ("(ssh@a{sv})",
                                                      "", /* window */
                                                      _("Print"), /* title */
                                                      idx,
                                                      g_variant_builder_end (&opt_builder)),
                                       G_DBUS_CALL_FLAGS_NONE,
                                       -1,
                                       fd_list,
                                       NULL,
                                       print_file_done,
                                       op);
  g_object_unref (fd_list);
}

static void
portal_end_run (GtkPrintOperation *op,
                gboolean           wait,
                gboolean           cancelled)
{
  GtkPrintOperationPortal *op_portal = op->priv->platform_data;

  cairo_surface_finish (op_portal->surface);

  if (cancelled)
    return;

  if (wait)
    op_portal->loop = g_main_loop_new (NULL, FALSE);

  /* TODO: Check for error */
  if (op_portal->job != NULL)
    {
      g_object_ref (op);
      gtk_print_job_send (op_portal->job, portal_job_complete, op, NULL);
    }

  if (wait)
    {
      g_object_ref (op);
      if (!op_portal->file_written)
        {
          gdk_threads_leave ();
          g_main_loop_run (op_portal->loop);
          gdk_threads_enter ();
        }
      g_object_unref (op);
    }
}

static void
finish_print (PortalData        *portal,
              GtkPrinter        *printer,
              GtkPageSetup      *page_setup,
              GtkPrintSettings  *settings)
{
  GtkPrintOperation *op = portal->op;
  GtkPrintOperationPrivate *priv = op->priv;
  GtkPrintJob *job;
  GtkPrintOperationPortal *op_portal;
  cairo_t *cr;

  if (portal->do_print)
    {
      gtk_print_operation_set_print_settings (op, settings);
      priv->print_context = _gtk_print_context_new (op);

      _gtk_print_context_set_hard_margins (priv->print_context, 0, 0, 0, 0);

      gtk_print_operation_set_default_page_setup (op, page_setup);
      _gtk_print_context_set_page_setup (priv->print_context, page_setup);

      op_portal = g_new0 (GtkPrintOperationPortal, 1);
      priv->platform_data = op_portal;
      priv->free_platform_data = (GDestroyNotify) op_portal_free;

      priv->start_page = portal_start_page;
      priv->end_page = portal_end_page;
      priv->end_run = portal_end_run;

      job = gtk_print_job_new (priv->job_name, printer, settings, page_setup);
      op_portal->job = job;

      op_portal->proxy = g_object_ref (portal->proxy);
      op_portal->token = portal->token;

      op_portal->surface = gtk_print_job_get_surface (job, &priv->error);
      if (op_portal->surface == NULL)
        {
          portal->result = GTK_PRINT_OPERATION_RESULT_ERROR;
          portal->do_print = FALSE;
          goto out;
        }

      cr = cairo_create (op_portal->surface);
      gtk_print_context_set_cairo_context (priv->print_context, cr, 72, 72);
      cairo_destroy (cr);

      priv->print_pages = gtk_print_job_get_pages (job);
      priv->page_ranges = gtk_print_job_get_page_ranges (job, &priv->num_page_ranges);
      priv->manual_num_copies = gtk_print_job_get_num_copies (job);
      priv->manual_collation = gtk_print_job_get_collate (job);
      priv->manual_reverse = gtk_print_job_get_reverse (job);
      priv->manual_page_set = gtk_print_job_get_page_set (job);
      priv->manual_scale = gtk_print_job_get_scale (job);
      priv->manual_orientation = gtk_print_job_get_rotate (job);
      priv->manual_number_up = gtk_print_job_get_n_up (job);
      priv->manual_number_up_layout = gtk_print_job_get_n_up_layout (job);
    }

out:
  if (portal->print_cb)
    portal->print_cb (op, portal->parent, portal->do_print, portal->result);

  if (portal->destroy)
    portal->destroy (portal);
}

static GtkPrinter *
find_file_printer (void)
{
  GList *backends, *l, *printers;
  GtkPrinter *printer;

  printer = NULL;

  backends = gtk_print_backend_load_modules ();
  for (l = backends; l; l = l->next)
    {
      GtkPrintBackend *backend = l->data;
      if (strcmp (G_OBJECT_TYPE_NAME (backend), "GtkPrintBackendFile") == 0)
        {
          printers = gtk_print_backend_get_printer_list (backend);
          printer = printers->data;
          g_list_free (printers);
          break;
        }
    }
  g_list_free (backends);

  return printer;
}

static void
prepare_print_response (GDBusConnection *connection,
                        const char      *sender_name,
                        const char      *object_path,
                        const char      *interface_name,
                        const char      *signal_name,
                        GVariant        *parameters,
                        gpointer         data)
{
  PortalData *portal = data;
  guint32 response;
  GVariant *options;

  if (portal->response_signal_id != 0)
    {
      g_dbus_connection_signal_unsubscribe (connection,
                                            portal->response_signal_id);
      portal->response_signal_id = 0;
    }

  g_variant_get (parameters, "(u@a{sv})", &response, &options);

  portal->do_print = (response == 0);

  if (portal->do_print)
    {
      GVariant *v;
      GtkPrintSettings *settings;
      GtkPageSetup *page_setup;
      GtkPrinter *printer;
      char *filename;
      char *uri;
      int fd;

      portal->result = GTK_PRINT_OPERATION_RESULT_APPLY;

      v = g_variant_lookup_value (options, "settings", G_VARIANT_TYPE_VARDICT);
      settings = gtk_print_settings_new_from_gvariant (v);
      g_variant_unref (v);

      v = g_variant_lookup_value (options, "page-setup", G_VARIANT_TYPE_VARDICT);
      page_setup = gtk_page_setup_new_from_gvariant (v);
      g_variant_unref (v);

      g_variant_lookup (options, "token", "u", &portal->token);

      printer = find_file_printer ();

      fd = g_file_open_tmp ("gtkprintXXXXXX", &filename, NULL);
      uri = g_filename_to_uri (filename, NULL, NULL);
      gtk_print_settings_set (settings, GTK_PRINT_SETTINGS_OUTPUT_URI, uri);
      g_free (uri);
      close (fd);

      finish_print (portal, printer, page_setup, settings);
      g_free (filename);
    }
  else
    portal->result = GTK_PRINT_OPERATION_RESULT_CANCEL;

  if (portal->loop)
    g_main_loop_quit (portal->loop);
}

static void
prepare_print_called (GObject      *source,
                      GAsyncResult *result,
                      gpointer      data)
{
  PortalData *portal = data;
  GError *error = NULL;
  const char *handle = NULL;
  GVariant *ret;

  ret = g_dbus_proxy_call_finish (portal->proxy, result, &error);
  if (ret == NULL)
    {
      if (portal->op->priv->error == NULL)
        portal->op->priv->error = g_error_copy (error);
      g_error_free (error);
      if (portal->loop)
        g_main_loop_quit (portal->loop);
      return;
    }
  else
    g_variant_get (ret, "(&o)", &handle);

  if (strcmp (portal->prepare_print_handle, handle) != 0)
    {
      g_free (portal->prepare_print_handle);
      portal->prepare_print_handle = g_strdup (handle);
      g_dbus_connection_signal_unsubscribe (g_dbus_proxy_get_connection (G_DBUS_PROXY (portal->proxy)),
                                            portal->response_signal_id);
      portal->response_signal_id =
        g_dbus_connection_signal_subscribe (g_dbus_proxy_get_connection (G_DBUS_PROXY (portal->proxy)),
                                            "org.freedesktop.portal.Desktop",
                                            "org.freedesktop.portal.Request",
                                            "Response",
                                            handle,
                                            NULL,
                                            G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
                                            prepare_print_response,
                                            portal, NULL);
     }

  g_variant_unref (ret);
}

PortalData *
create_portal_data (GtkPrintOperation          *op,
                    GtkWindow                  *parent,
                    GtkPrintOperationPrintFunc  print_cb)
{
  GDBusProxy *proxy;
  PortalData *portal;
  guint signal_id;
  GError *error = NULL;

  signal_id = g_signal_lookup ("create-custom-widget", GTK_TYPE_PRINT_OPERATION);
  if (g_signal_has_handler_pending (op, signal_id, 0, TRUE))
    g_warning ("GtkPrintOperation::create-custom-widget not supported with portal");

  proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION,
                                         G_DBUS_PROXY_FLAGS_NONE,
                                         NULL,
                                         "org.freedesktop.portal.Desktop",
                                         "/org/freedesktop/portal/desktop",
                                         "org.freedesktop.portal.Print",
                                         NULL,
                                         &error);

  if (proxy == NULL)
    {
      if (op->priv->error == NULL)
        op->priv->error = g_error_copy (error);
      g_error_free (error);
      return NULL;
    }

  portal = g_new0 (PortalData, 1);
  portal->proxy = proxy;
  portal->op = g_object_ref (op);
  portal->parent = parent;
  portal->result = GTK_PRINT_OPERATION_RESULT_CANCEL;
  portal->print_cb = print_cb;

  if (print_cb) /* async case */
    {
      portal->loop = NULL;
      portal->destroy = portal_data_free;
    }
  else
    {
      portal->loop = g_main_loop_new (NULL, FALSE);
      portal->destroy = NULL;
    }

  return portal;
}

static void
window_handle_exported (GtkWindow  *window,
                        const char *handle_str,
                        gpointer    user_data)
{
  PortalData *portal = user_data;

  g_dbus_proxy_call (portal->proxy,
                     "PreparePrint",
                     g_variant_new ("(ss@a{sv}@a{sv}@a{sv})",
                                    handle_str,
                                    _("Print"), /* title */
                                    portal->settings,
                                    portal->setup,
                                    portal->options),
                     G_DBUS_CALL_FLAGS_NONE,
                     -1,
                     NULL,
                     prepare_print_called,
                     portal);
}

static void
call_prepare_print (GtkPrintOperation *op,
                    PortalData        *portal)
{
  GtkPrintOperationPrivate *priv = op->priv;
  GVariantBuilder opt_builder;
  char *token;

  portal->prepare_print_handle =
      gtk_get_portal_request_path (g_dbus_proxy_get_connection (portal->proxy), &token);

  portal->response_signal_id =
    g_dbus_connection_signal_subscribe (g_dbus_proxy_get_connection (G_DBUS_PROXY (portal->proxy)),
                                        "org.freedesktop.portal.Desktop",
                                        "org.freedesktop.portal.Request",
                                        "Response",
                                        portal->prepare_print_handle,
                                        NULL,
                                        G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
                                        prepare_print_response,
                                        portal, NULL);

  g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT);
  g_variant_builder_add (&opt_builder, "{sv}", "handle_token", g_variant_new_string (token));
  g_free (token);
  portal->options = g_variant_builder_end (&opt_builder);

  if (priv->print_settings)
    portal->settings = gtk_print_settings_to_gvariant (priv->print_settings);
  else
    {
      GVariantBuilder builder;
      g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT);
      portal->settings = g_variant_builder_end (&builder);
    }

  if (priv->default_page_setup)
    portal->setup = gtk_page_setup_to_gvariant (priv->default_page_setup);
  else
    {
      GtkPageSetup *page_setup = gtk_page_setup_new ();
      portal->setup = gtk_page_setup_to_gvariant (page_setup);
      g_object_unref (page_setup);
    }

  g_variant_ref_sink (portal->options);
  g_variant_ref_sink (portal->settings);
  g_variant_ref_sink (portal->setup);

  if (portal->parent != NULL &&
      gtk_widget_is_visible (GTK_WIDGET (portal->parent)) &&
      gtk_window_export_handle (portal->parent, window_handle_exported, portal))
    return;

  g_dbus_proxy_call (portal->proxy,
                     "PreparePrint",
                     g_variant_new ("(ss@a{sv}@a{sv}@a{sv})",
                                    "",
                                    _("Print"), /* title */
                                    portal->settings,
                                    portal->setup,
                                    portal->options),
                     G_DBUS_CALL_FLAGS_NONE,
                     -1,
                     NULL,
                     prepare_print_called,
                     portal);
}

GtkPrintOperationResult
gtk_print_operation_portal_run_dialog (GtkPrintOperation *op,
                                       gboolean           show_dialog,
                                       GtkWindow         *parent,
                                       gboolean          *do_print)
{
  PortalData *portal;
  GtkPrintOperationResult result;

  portal = create_portal_data (op, parent, NULL);
  if (portal == NULL)
    return GTK_PRINT_OPERATION_RESULT_ERROR;

  call_prepare_print (op, portal);

  gdk_threads_leave ();
  g_main_loop_run (portal->loop);
  gdk_threads_enter ();

  *do_print = portal->do_print;
  result = portal->result;

  portal_data_free (portal);

  return result;
}

void
gtk_print_operation_portal_run_dialog_async (GtkPrintOperation          *op,
                                             gboolean                    show_dialog,
                                             GtkWindow                  *parent,
                                             GtkPrintOperationPrintFunc  print_cb)
{
  PortalData *portal;

  portal = create_portal_data (op, parent, print_cb);
  if (portal == NULL)
    return;

  call_prepare_print (op, portal);
}

void
gtk_print_operation_portal_launch_preview (GtkPrintOperation *op,
                                           cairo_surface_t   *surface,
                                           GtkWindow         *parent,
                                           const char        *filename)
{
  char *uri;

  uri = g_filename_to_uri (filename, NULL, NULL);
  gtk_show_uri_on_window (parent, uri, GDK_CURRENT_TIME, NULL);
  g_free (uri);
}
