/*
 * Copyright © 2016 Red Hat, Inc
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * This program 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.1 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/>.
 *
 * Authors:
 *       Matthias Clasen <mclasen@redhat.com>
 */

#include "config.h"

#include "screenshot.h"

#include <errno.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>

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

#include "xdp-context.h"
#include "xdp-dbus.h"
#include "xdp-documents.h"
#include "xdp-impl-dbus.h"
#include "xdp-permissions.h"
#include "xdp-portal-config.h"
#include "xdp-request.h"
#include "xdp-utils.h"

typedef enum
{
  SCREENSHOT_TARGET_SCREEN = 1u << 0,
  SCREENSHOT_TARGET_WINDOW = 1u << 1,
  SCREENSHOT_TARGET_AREA = 1u << 2,
  SCREENSHOT_TARGET_ACTIVE_WINDOW = 1u << 3,
} ScreenshotTarget;

typedef struct _Screenshot Screenshot;
typedef struct _ScreenshotClass ScreenshotClass;

struct _Screenshot
{
  XdpDbusScreenshotSkeleton parent_instance;

  XdpDbusImplScreenshot *impl;
  XdpDbusImplAccess *access_impl;
  guint32 impl_version;
};

struct _ScreenshotClass
{
  XdpDbusScreenshotSkeletonClass parent_class;
};

GType screenshot_get_type (void) G_GNUC_CONST;
static void screenshot_iface_init (XdpDbusScreenshotIface *iface);

G_DEFINE_TYPE_WITH_CODE (Screenshot, screenshot, XDP_DBUS_TYPE_SCREENSHOT_SKELETON,
                         G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_SCREENSHOT,
                                                screenshot_iface_init));

G_DEFINE_AUTOPTR_CLEANUP_FUNC (Screenshot, g_object_unref)

static void
send_response (XdpRequest *request,
               guint response,
               GVariant *results)
{
  if (request->exported)
    {
      g_debug ("sending response: %d", response);
      xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), response, results);
      xdp_request_unexport (request);
    }
  else
    {
      g_variant_ref_sink (results);
      g_variant_unref (results);
    }
}

static void
send_response_in_thread_func (GTask *task,
                              gpointer source_object,
                              gpointer task_data,
                              GCancellable *cancellable)
{
  XdpRequest *request = task_data;
  g_auto(GVariantBuilder) results =
    G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
  guint response;
  GVariant *options;
  g_autoptr(GError) error = NULL;
  const char *retval;

  REQUEST_AUTOLOCK (request);

  response = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (request), "response"));
  options = (GVariant *)g_object_get_data (G_OBJECT (request), "options");

  if (response != 0)
    goto out;

  retval = g_object_get_data (G_OBJECT (task), "retval");
  if (g_strcmp0 (retval, "url") == 0)
    {
      const char *uri;
      g_autofree char *ruri = NULL;

      if (!g_variant_lookup (options, "uri", "&s", &uri))
        {
          g_warning ("No URI was provided");
          goto out;
        }

      if (xdp_app_info_is_host (request->app_info))
        ruri = g_strdup (uri);
      else
        ruri = xdp_register_document (uri, xdp_app_info_get_id (request->app_info), XDP_DOCUMENT_FLAG_DELETABLE, &error);

      if (ruri == NULL)
        g_warning ("Failed to register %s: %s", uri, error->message);
      else
        g_variant_builder_add (&results, "{&sv}", "uri", g_variant_new_string (ruri));
    }
  else if (g_strcmp0 (retval, "color") == 0)
    {
      double red, green, blue;

      if (!g_variant_lookup (options, "color", "(ddd)", &red, &green, &blue))
        {
          g_warning ("No color was provided");
          goto out;
        }

      g_variant_builder_add (&results, "{&sv}", "color", g_variant_new ("(ddd)", red, green, blue));
    }
  else
    {
      g_warning ("Don't know what to return");
    }

out:
  send_response (request, response, g_variant_builder_end (&results));
}

static void
screenshot_done (GObject *source,
                 GAsyncResult *result,
                 gpointer data)
{
  g_autoptr(XdpRequest) request = data;
  guint response = 2;
  g_autoptr(GVariant) options = NULL;
  g_autoptr(GError) error = NULL;
  g_autoptr(GTask) task = NULL;

  if (!xdp_dbus_impl_screenshot_call_screenshot_finish (XDP_DBUS_IMPL_SCREENSHOT (source),
                                                        &response,
                                                        &options,
                                                        result,
                                                        &error))
    {
      g_dbus_error_strip_remote_error (error);
      g_warning ("A backend call failed: %s", error->message);
    }

  g_object_set_data (G_OBJECT (request), "response", GINT_TO_POINTER (response));
  if (options)
    g_object_set_data_full (G_OBJECT (request), "options", g_variant_ref (options), (GDestroyNotify)g_variant_unref);

  task = g_task_new (NULL, NULL, NULL, NULL);
  g_task_set_source_tag (task, screenshot_done);
  g_task_set_task_data (task, g_object_ref (request), g_object_unref);
  g_object_set_data (G_OBJECT (task), "retval", "url");
  g_task_run_in_thread (task, send_response_in_thread_func);
}

static gboolean
validate_screenshot_target (Screenshot *screenshot,
                            uint32_t    target,
                            GError    **error)
{
  uint32_t available_targets;

  switch (target)
    {
    case SCREENSHOT_TARGET_SCREEN:
    case SCREENSHOT_TARGET_WINDOW:
    case SCREENSHOT_TARGET_AREA:
    case SCREENSHOT_TARGET_ACTIVE_WINDOW:
      break;
    default:
      g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   "Invalid screenshot target %x", target);
      return FALSE;
    }

  available_targets =
    xdp_dbus_screenshot_get_available_targets (XDP_DBUS_SCREENSHOT (screenshot));
  if (!(available_targets & target))
    {
      g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   "Unavailable screenshot target %x", target);
      return FALSE;
    }

  return TRUE;
}

static gboolean
validate_screenshot_target_filter (const char  *key,
                                   GVariant    *value,
                                   GVariant    *options,
                                   gpointer     user_data,
                                   GError     **error)
{
  return validate_screenshot_target (user_data, g_variant_get_uint32 (value), error);
}

static XdpOptionKey screenshot_options_v3[] = {
  { "modal", G_VARIANT_TYPE_BOOLEAN, NULL },
  { "interactive", G_VARIANT_TYPE_BOOLEAN, NULL },
  { "target", G_VARIANT_TYPE_UINT32, validate_screenshot_target_filter }
};

static gboolean
check_non_interactive_permission_in_thread (Screenshot *screenshot,
                                            XdpRequest *request,
                                            const char *parent_window,
                                            gboolean    modal)
{
  g_autoptr(GError) error = NULL;
  g_autoptr(GVariant) access_results = NULL;
  g_auto(GVariantBuilder) access_opt_builder =
    G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
  XdpPermission permission;
  g_autofree char *subtitle = NULL;
  g_autofree char *title = NULL;
  const char *body = "";
  uint access_response = 2;
  const char *app_id;

  permission = xdp_get_permission_sync (request->app_info,
                                        SCREENSHOT_PERMISSION_TABLE,
                                        SCREENSHOT_PERMISSION_ID);

  if (permission == XDP_PERMISSION_YES)
    return TRUE;

  if (permission == XDP_PERMISSION_NO)
    return FALSE;

  g_variant_builder_add (&access_opt_builder, "{sv}",
                         "deny_label", g_variant_new_string (_("Deny")));
  g_variant_builder_add (&access_opt_builder, "{sv}",
                         "grant_label", g_variant_new_string (_("Allow")));
  g_variant_builder_add (&access_opt_builder, "{sv}",
                         "icon", g_variant_new_string ("applets-screenshooter-symbolic"));
  g_variant_builder_add (&access_opt_builder, "{sv}",
                         "modal", g_variant_new_boolean (modal));

  app_id = xdp_app_info_get_id (request->app_info);

  if (g_strcmp0 (app_id, "") != 0)
    {
      g_autoptr(GDesktopAppInfo) info = NULL;
      g_autofree gchar *id = NULL;
      const gchar *name = NULL;

      id = g_strconcat (app_id, ".desktop", NULL);
      info = g_desktop_app_info_new (id);

      if (info)
        name = g_app_info_get_display_name (G_APP_INFO (info));
      else
        name = app_id;

      if (permission == XDP_PERMISSION_UNSET)
        {
          title = g_strdup_printf (_("Allow %s to Take Screenshots?"), name);
          subtitle = g_strdup_printf (_("%s wants to take screenshots at any time"), name);
        }
      else /* permission == XDP_PERMISSION_ASK */
        {
          title = g_strdup_printf (_("Allow %s to Take a Screenshot?"), name);
          subtitle = g_strdup_printf (_("%s wants to take a screenshot"), name);
        }
    }
  else
    {
      g_assert (xdp_app_info_is_host (request->app_info));
      if (permission == XDP_PERMISSION_UNSET)
        {
          /* Note: this will set the screenshot permission for all unsandboxed
           * apps for which an app ID can't be determined.
           */
          title = g_strdup (_("Allow Apps to Take Screenshots?"));
          subtitle = g_strdup (_("An app wants to take screenshots at any time"));
        }
      else /* permission == XDP_PERMISSION_ASK */
        {
          title = g_strdup (_("Allow to Take a Screenshot?"));
          subtitle = g_strdup (_("An app wants to take a screenshot"));
        }
    }

  if (permission == XDP_PERMISSION_UNSET)
    body = _("This permission can be changed at any time from the privacy settings");

  if (!xdp_dbus_impl_access_call_access_dialog_sync (screenshot->access_impl,
                                                     request->id,
                                                     app_id,
                                                     parent_window,
                                                     title,
                                                     subtitle,
                                                     body,
                                                     g_variant_builder_end (&access_opt_builder),
                                                     &access_response,
                                                     &access_results,
                                                     NULL,
                                                     &error))
    {
      g_warning ("Failed to show access dialog: %s", error->message);
      return FALSE;
    }

  if (permission == XDP_PERMISSION_UNSET)
    {
      xdp_set_permission_sync (request->app_info,
                               SCREENSHOT_PERMISSION_TABLE,
                               SCREENSHOT_PERMISSION_ID,
                               access_response == 0 ? XDP_PERMISSION_YES : XDP_PERMISSION_NO);
    }

  return access_response == 0;
}


static void
handle_screenshot_in_thread_func (GTask *task,
                                  gpointer source_object,
                                  gpointer task_data,
                                  GCancellable *cancellable)
{
  Screenshot *screenshot = (Screenshot *) source_object;
  XdpRequest *request = XDP_REQUEST (task_data);
  g_autoptr(GError) error = NULL;
  g_autoptr(XdpDbusImplRequest) impl_request = NULL;
  g_auto(GVariantBuilder) opt_builder =
    G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
  GVariantIter options_iter;
  GVariant *options;
  const char *key;
  GVariant *value;
  gboolean permission_store_checked = FALSE;
  gboolean interactive;
  gboolean modal;
  const char *parent_window;
  const char *app_id;

  REQUEST_AUTOLOCK (request);

  app_id = xdp_app_info_get_id (request->app_info);
  parent_window = ((const char *)g_object_get_data (G_OBJECT (request), "parent-window"));
  options = ((GVariant *)g_object_get_data (G_OBJECT (request), "options"));

  if (!g_variant_lookup (options, "interactive", "b", &interactive))
    interactive = FALSE;

  if (!g_variant_lookup (options, "modal", "b", &modal))
    modal = TRUE;

  if (xdp_dbus_impl_screenshot_get_version (screenshot->impl) >= 2)
    {
      if (!interactive &&
          !check_non_interactive_permission_in_thread (screenshot,
                                                       request,
                                                       parent_window,
                                                       modal))
        {
          send_response (request, 2, g_variant_builder_end (&opt_builder));
          return;
        }
      permission_store_checked = TRUE;
    }

  impl_request =
    xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (screenshot->impl)),
                                          G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
                                          g_dbus_proxy_get_name (G_DBUS_PROXY (screenshot->impl)),
                                          request->id,
                                          NULL, &error);
  if (!impl_request)
    {
      g_warning ("Failed to to create screenshot implementation proxy: %s", error->message);
      send_response (request, 2, g_variant_builder_end (&opt_builder));
      return;
    }

  xdp_request_set_impl_request (request, impl_request);

  g_variant_iter_init (&options_iter, options);
  while (g_variant_iter_next (&options_iter, "{&sv}", &key, &value))
    {
      g_variant_builder_add (&opt_builder, "{sv}", key, value);
      g_clear_pointer (&value, g_variant_unref);
    }

  if (permission_store_checked)
    {
      g_variant_builder_add (&opt_builder, "{sv}", "permission_store_checked",
                             g_variant_new_boolean (TRUE));
    }

  g_debug ("Calling Screenshot with interactive=%d", interactive);
  xdp_dbus_impl_screenshot_call_screenshot (screenshot->impl,
                                            request->id,
                                            app_id,
                                            parent_window,
                                            g_variant_builder_end (&opt_builder),
                                            NULL,
                                            screenshot_done,
                                            g_object_ref (request));

}

static gboolean
handle_screenshot (XdpDbusScreenshot *object,
                   GDBusMethodInvocation *invocation,
                   const gchar *arg_parent_window,
                   GVariant *arg_options)
{
  Screenshot *screenshot = (Screenshot *) object;
  XdpRequest *request = xdp_request_from_invocation (invocation);
  g_auto(GVariantBuilder) opt_builder =
    G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
  g_autoptr(GVariant) options = NULL;
  g_autoptr(GTask) task = NULL;
  g_autoptr(GError) error = NULL;

  g_debug ("Handle Screenshot");

  if (!xdp_filter_options (arg_options, &opt_builder,
                           screenshot_options_v3, G_N_ELEMENTS (screenshot_options_v3),
                           screenshot, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }
  options = g_variant_ref_sink (g_variant_builder_end (&opt_builder));

  g_object_set_data_full (G_OBJECT (request), "parent-window", g_strdup (arg_parent_window), g_free);
  g_object_set_data_full (G_OBJECT (request),
                          "options",
                          g_steal_pointer (&options),
                          (GDestroyNotify)g_variant_unref);

  xdp_request_export (request, g_dbus_method_invocation_get_connection (invocation));
  xdp_dbus_screenshot_complete_screenshot (object, invocation, request->id);

  task = g_task_new (object, NULL, NULL, NULL);
  g_task_set_source_tag (task, handle_screenshot);
  g_task_set_task_data (task, g_object_ref (request), g_object_unref);
  g_task_run_in_thread (task, handle_screenshot_in_thread_func);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static void
pick_color_done (GObject *source,
                 GAsyncResult *result,
                 gpointer data)
{
  g_autoptr(XdpRequest) request = data;
  guint response = 2;
  g_autoptr(GVariant) options = NULL;
  g_autoptr(GError) error = NULL;
  g_autoptr(GTask) task = NULL;

  if (!xdp_dbus_impl_screenshot_call_pick_color_finish (XDP_DBUS_IMPL_SCREENSHOT (source),
                                                        &response,
                                                        &options,
                                                        result,
                                                        &error))
    {
      g_dbus_error_strip_remote_error (error);
      g_warning ("A backend call failed: %s", error->message);
    }

  g_object_set_data (G_OBJECT (request), "response", GINT_TO_POINTER (response));
  if (options)
    g_object_set_data_full (G_OBJECT (request), "options", g_variant_ref (options), (GDestroyNotify)g_variant_unref);

  task = g_task_new (NULL, NULL, NULL, NULL);
  g_task_set_source_tag (task, pick_color_done);
  g_task_set_task_data (task, g_object_ref (request), g_object_unref);
  g_object_set_data (G_OBJECT (task), "retval", "color");
  g_task_run_in_thread (task, send_response_in_thread_func);
}

static XdpOptionKey pick_color_options[] = {
};

static gboolean
handle_pick_color (XdpDbusScreenshot *object,
                   GDBusMethodInvocation *invocation,
                   const gchar *arg_parent_window,
                   GVariant *arg_options)
{
  Screenshot *screenshot = (Screenshot *) object;
  XdpRequest *request = xdp_request_from_invocation (invocation);
  g_autoptr(GError) error = NULL;
  g_autoptr(XdpDbusImplRequest) impl_request = NULL;
  g_auto(GVariantBuilder) opt_builder =
    G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);

  REQUEST_AUTOLOCK (request);

  impl_request =
    xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (screenshot->impl)),
                                          G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
                                          g_dbus_proxy_get_name (G_DBUS_PROXY (screenshot->impl)),
                                          request->id,
                                          NULL, &error);
  if (!impl_request)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return G_DBUS_METHOD_INVOCATION_HANDLED;
    }

  xdp_request_set_impl_request (request, impl_request);
  xdp_request_export (request, g_dbus_method_invocation_get_connection (invocation));

  xdp_filter_options (arg_options, &opt_builder,
                      pick_color_options, G_N_ELEMENTS (pick_color_options),
                      NULL, NULL);

  xdp_dbus_impl_screenshot_call_pick_color (screenshot->impl,
                                            request->id,
                                            xdp_app_info_get_id (request->app_info),
                                            arg_parent_window,
                                            g_variant_builder_end (&opt_builder),
                                            NULL,
                                            pick_color_done,
                                            g_object_ref (request));

  xdp_dbus_screenshot_complete_pick_color (object, invocation, request->id);

  return G_DBUS_METHOD_INVOCATION_HANDLED;
}

static void
screenshot_iface_init (XdpDbusScreenshotIface *iface)
{
  iface->handle_screenshot = handle_screenshot;
  iface->handle_pick_color = handle_pick_color;
}

static void
screenshot_dispose (GObject *object)
{
  Screenshot *screenshot = (Screenshot *) object;

  g_clear_object (&screenshot->impl);
  g_clear_object (&screenshot->access_impl);

  G_OBJECT_CLASS (screenshot_parent_class)->dispose (object);
}

static void
screenshot_init (Screenshot *screenshot)
{
}

static void
screenshot_class_init (ScreenshotClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = screenshot_dispose;
}

static Screenshot *
screenshot_new (XdpDbusImplScreenshot *impl,
                XdpDbusImplAccess     *access_impl)
{
  Screenshot *screenshot;

  screenshot = g_object_new (screenshot_get_type (), NULL);
  screenshot->access_impl = g_object_ref (access_impl);
  screenshot->impl = g_object_ref (impl);

  g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (screenshot->impl), G_MAXINT);


  /* Before there was a version property, the version was hardcoded to 2, so
   * make sure we retain that behaviour */
  screenshot->impl_version =
    MAX (xdp_dbus_impl_screenshot_get_version (screenshot->impl), 2);

  xdp_dbus_screenshot_set_version (XDP_DBUS_SCREENSHOT (screenshot),
                                   MIN (screenshot->impl_version, 3));

  if (screenshot->impl_version >= 3)
    {
      g_object_bind_property (G_OBJECT (screenshot->impl), "available-targets",
                              G_OBJECT (screenshot), "available-targets",
                              G_BINDING_SYNC_CREATE);
    }

  return screenshot;
}

void
init_screenshot (XdpContext *context)
{
  g_autoptr(Screenshot) screenshot = NULL;
  GDBusConnection *connection = xdp_context_get_connection (context);
  XdpPortalConfig *config = xdp_context_get_config (context);
  XdpDbusImplAccess *access_impl;
  XdpImplConfig *impl_config;
  g_autoptr(XdpDbusImplScreenshot) impl = NULL;
  g_autoptr(GError) error = NULL;

  impl_config = xdp_portal_config_find (config, SCREENSHOT_DBUS_IMPL_IFACE);
  if (impl_config == NULL)
    return;

  access_impl = xdp_context_get_access_impl (context);
  if (access_impl == NULL)
    {
      g_warning ("The screenshot portal requires an access impl");
      return;
    }

  impl = xdp_dbus_impl_screenshot_proxy_new_sync (connection,
                                                  G_DBUS_PROXY_FLAGS_NONE,
                                                  impl_config->dbus_name,
                                                  DESKTOP_DBUS_PATH,
                                                  NULL,
                                                  &error);
  if (impl == NULL)
    {
      g_warning ("Failed to create screenshot proxy: %s", error->message);
      return;
    }

  screenshot = screenshot_new (impl, access_impl);

  xdp_context_take_and_export_portal (context,
                                      G_DBUS_INTERFACE_SKELETON (g_steal_pointer (&screenshot)),
                                      XDP_CONTEXT_EXPORT_FLAGS_NONE);
}
