/*
 * Copyright © 2017-2018 Red Hat, Inc
 *
 * 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 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 <stdint.h>
#include <pipewire/pipewire.h>
#include <gio/gunixfdlist.h>

#include "session.h"
#include "screen-cast.h"
#include "remote-desktop.h"
#include "request.h"
#include "permissions.h"
#include "pipewire.h"
#include "xdp-dbus.h"
#include "xdp-impl-dbus.h"
#include "xdp-utils.h"

#define RESTORE_DATA_TYPE "(suv)"
#define PERMISSION_ITEM(item_id, item_permissions) \
  ((struct pw_permission) { \
    .id = item_id, \
    .permissions = item_permissions \
  })

typedef struct _ScreenCast ScreenCast;
typedef struct _ScreenCastClass ScreenCastClass;

struct _ScreenCast
{
  XdpScreenCastSkeleton parent_instance;
};

struct _ScreenCastClass
{
  XdpScreenCastSkeletonClass parent_class;
};

static XdpImplScreenCast *impl;
static int impl_version;
static ScreenCast *screen_cast;

static GMutex transient_permissions_lock;
static GHashTable *transient_permissions;

static unsigned int available_cursor_modes = 0;

GType screen_cast_get_type (void);
static void screen_cast_iface_init (XdpScreenCastIface *iface);

static GQuark quark_request_session;

struct _ScreenCastStream
{
  uint32_t id;
  int32_t width;
  int32_t height;
};

G_DEFINE_TYPE_WITH_CODE (ScreenCast, screen_cast, XDP_TYPE_SCREEN_CAST_SKELETON,
                         G_IMPLEMENT_INTERFACE (XDP_TYPE_SCREEN_CAST,
                                                screen_cast_iface_init))

typedef enum _PersistMode
{
  PERSIST_MODE_NONE = 0,
  PERSIST_MODE_TRANSIENT = 1,
  PERSIST_MODE_PERSISTENT = 2,
} PersistMode;

typedef enum _ScreenCastSessionState
{
  SCREEN_CAST_SESSION_STATE_INIT,
  SCREEN_CAST_SESSION_STATE_SELECTING_SOURCES,
  SCREEN_CAST_SESSION_STATE_SOURCES_SELECTED,
  SCREEN_CAST_SESSION_STATE_STARTING,
  SCREEN_CAST_SESSION_STATE_STARTED,
  SCREEN_CAST_SESSION_STATE_CLOSED
} ScreenCastSessionState;

typedef struct _ScreenCastSession
{
  Session parent;

  ScreenCastSessionState state;

  GList *streams;
  char *restore_token;
  PersistMode persist_mode;
  GVariant *restore_data;
} ScreenCastSession;

typedef struct _ScreenCastSessionClass
{
  SessionClass parent_class;
} ScreenCastSessionClass;

GType screen_cast_session_get_type (void);

G_DEFINE_TYPE (ScreenCastSession, screen_cast_session, session_get_type ())

static void
set_persistent_permissions (const char *app_id,
                            const char *restore_token,
                            GVariant *restore_data)
{
  g_autoptr(GError) error = NULL;

  set_permission_sync (app_id, "screencast", restore_token, PERMISSION_YES);

  if (!xdp_impl_permission_store_call_set_value_sync (get_permission_store (),
                                                      "screencast",
                                                      TRUE,
                                                      restore_token,
                                                      g_variant_new_variant (restore_data),
                                                      NULL,
                                                      &error))
    {
      g_dbus_error_strip_remote_error (error);
      g_warning ("Error setting permission store value: %s", error->message);
    }
}

static GVariant *
get_persistent_permissions (const char *app_id,
                            const char *restore_token)
{
  g_autoptr(GVariant) perms = NULL;
  g_autoptr(GVariant) data = NULL;
  g_autoptr(GError) error = NULL;
  const char **permissions;

  if (!xdp_impl_permission_store_call_lookup_sync (get_permission_store (),
                                                   "screencast",
                                                   restore_token,
                                                   &perms,
                                                   &data,
                                                   NULL,
                                                   &error))
    {
      return NULL;
    }

  if (!perms || !g_variant_lookup (perms, app_id, "^a&s", &permissions))
    return NULL;

  if (!data)
    return NULL;

  return g_variant_get_child_value (data, 0);
}

void
delete_persistent_permissions (const char *app_id,
                               const char *restore_token)
{

  g_autoptr(GError) error = NULL;

  if (!xdp_impl_permission_store_call_delete_sync (get_permission_store (),
                                                   "screencast",
                                                   restore_token,
                                                   NULL,
                                                   &error))
    {
      g_dbus_error_strip_remote_error (error);
      g_warning ("Error deleting permission: %s", error->message);
    }
}

static void
set_transient_permissions (const char *sender,
                           const char *restore_token,
                           GVariant *restore_data)
{
  g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&transient_permissions_lock);

  if (!transient_permissions)
    {
      transient_permissions = g_hash_table_new_full (g_str_hash, g_str_equal,
                                                     g_free, (GDestroyNotify)g_variant_unref);
    }

  g_hash_table_insert (transient_permissions,
                       g_strdup_printf ("%s/%s", sender, restore_token),
                       g_variant_ref (restore_data));
}

static GVariant *
get_transient_permissions (const char *sender,
                           const char *restore_token)
{
  g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&transient_permissions_lock);
  g_autofree char *id = NULL;
  GVariant *permissions;

  if (!transient_permissions)
    return NULL;

  id = g_strdup_printf ("%s/%s", sender, restore_token);
  permissions = g_hash_table_lookup (transient_permissions, id);
  return permissions ? g_variant_ref (permissions) : NULL;
}

static void
delete_transient_permissions (const char *sender,
                              const char *restore_token)
{
  g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&transient_permissions_lock);
  g_autofree char *id = NULL;

  if (!transient_permissions)
    return;

  id = g_strdup_printf ("%s/%s", sender, restore_token);
  g_hash_table_remove (transient_permissions, id);
}

void
screen_cast_remove_transient_permissions_for_sender (const char *sender)
{
  g_autoptr(GMutexLocker) locker = NULL;
  GHashTableIter iter;
  const char *key;

  locker = g_mutex_locker_new (&transient_permissions_lock);

  if (!transient_permissions)
    return;

  g_hash_table_iter_init (&iter, transient_permissions);
  while (g_hash_table_iter_next (&iter, (gpointer *) &key, NULL))
    {
      g_auto(GStrv) split = g_strsplit (key, "/", 2);

      if (split && split[0] && g_strcmp0 (split[0], sender) == 0)
        g_hash_table_iter_remove (&iter);
    }
}

static gboolean
is_screen_cast_session (Session *session)
{
  return G_TYPE_CHECK_INSTANCE_TYPE (session, screen_cast_session_get_type ());
}

static ScreenCastSession *
screen_cast_session_new (GVariant *options,
                         Request *request,
                         GError **error)
{
  Session *session;
  GDBusInterfaceSkeleton *interface_skeleton =
    G_DBUS_INTERFACE_SKELETON (request);
  const char *session_token;
  GDBusConnection *connection =
    g_dbus_interface_skeleton_get_connection (interface_skeleton);
  GDBusConnection *impl_connection =
    g_dbus_proxy_get_connection (G_DBUS_PROXY (impl));
  const char *impl_dbus_name = g_dbus_proxy_get_name (G_DBUS_PROXY (impl));

  session_token = lookup_session_token (options);
  session = g_initable_new (screen_cast_session_get_type (), NULL, error,
                            "sender", request->sender,
                            "app-id", xdp_app_info_get_id (request->app_info),
                            "token", session_token,
                            "connection", connection,
                            "impl-connection", impl_connection,
                            "impl-dbus-name", impl_dbus_name,
                            NULL);

  if (session)
    g_debug ("screen cast session owned by '%s' created", session->sender);

  return (ScreenCastSession*)session;
}

static void
create_session_done (GObject *source_object,
                     GAsyncResult *res,
                     gpointer data)
{
  g_autoptr(Request) request = data;
  Session *session;
  guint response = 2;
  gboolean should_close_session;
  GVariantBuilder results_builder;
  g_autoptr(GError) error = NULL;

  REQUEST_AUTOLOCK (request);

  session = g_object_get_qdata (G_OBJECT (request), quark_request_session);
  SESSION_AUTOLOCK_UNREF (g_object_ref (session));
  g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL);

  g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT);

  if (!xdp_impl_screen_cast_call_create_session_finish (impl,
                                                        &response,
                                                        NULL,
                                                        res,
                                                        &error))
    {
      g_warning ("A backend call failed: %s", error->message);
      should_close_session = TRUE;
      goto out;
    }

  if (request->exported && response == 0)
    {
      if (!session_export (session, &error))
        {
          g_warning ("Failed to export session: %s", error->message);
          response = 2;
          should_close_session = TRUE;
          goto out;
        }

      should_close_session = FALSE;
      session_register (session);
    }
  else
    {
      should_close_session = TRUE;
    }

  g_variant_builder_add (&results_builder, "{sv}",
                         "session_handle", g_variant_new ("s", session->id));

out:
  if (request->exported)
    {
      xdp_request_emit_response (XDP_REQUEST (request),
                                 response,
                                 g_variant_builder_end (&results_builder));
      request_unexport (request);
    }
  else
    {
      g_variant_builder_clear (&results_builder);
    }

  if (should_close_session)
    session_close (session, FALSE);
}

static gboolean
handle_create_session (XdpScreenCast *object,
                       GDBusMethodInvocation *invocation,
                       GVariant *arg_options)
{
  Request *request = request_from_invocation (invocation);
  g_autoptr(GError) error = NULL;
  g_autoptr(XdpImplRequest) impl_request = NULL;
  Session *session;
  GVariantBuilder options_builder;
  GVariant *options;

  REQUEST_AUTOLOCK (request);

  impl_request =
    xdp_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)),
                                     G_DBUS_PROXY_FLAGS_NONE,
                                     g_dbus_proxy_get_name (G_DBUS_PROXY (impl)),
                                     request->id,
                                     NULL, &error);
  if (!impl_request)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return TRUE;
    }

  request_set_impl_request (request, impl_request);
  request_export (request, g_dbus_method_invocation_get_connection (invocation));

  session = (Session *)screen_cast_session_new (arg_options, request, &error);
  if (!session)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return TRUE;
    }

  g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT);
  options = g_variant_builder_end (&options_builder);

  g_object_set_qdata_full (G_OBJECT (request),
                           quark_request_session,
                           g_object_ref (session),
                           g_object_unref);

  xdp_impl_screen_cast_call_create_session (impl,
                                            request->id,
                                            session->id,
                                            xdp_app_info_get_id (request->app_info),
                                            options,
                                            NULL,
                                            create_session_done,
                                            g_object_ref (request));

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

  return TRUE;
}

static void
select_sources_done (GObject *source_object,
                     GAsyncResult *res,
                     gpointer data)
{
  g_autoptr(Request) request = data;
  Session *session;
  guint response = 2;
  gboolean should_close_session;
  g_autoptr(GError) error = NULL;
  g_autoptr(GVariant) results = NULL;

  REQUEST_AUTOLOCK (request);

  session = g_object_get_qdata (G_OBJECT (request), quark_request_session);
  SESSION_AUTOLOCK_UNREF (g_object_ref (session));
  g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL);

  if (!xdp_impl_screen_cast_call_select_sources_finish (impl,
                                                        &response,
                                                        &results,
                                                        res,
                                                        &error))
    g_warning ("A backend call failed: %s", error->message);

  should_close_session = !request->exported || response != 0;

  if (request->exported)
    {
      if (!results)
        {
          GVariantBuilder results_builder;

          g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT);
          results = g_variant_ref_sink (g_variant_builder_end (&results_builder));
        }

      xdp_request_emit_response (XDP_REQUEST (request), response, results);
      request_unexport (request);
    }

  if (should_close_session)
    {
      session_close (session, TRUE);
    }
  else if (!session->closed)
    {
      if (is_screen_cast_session (session))
        {
          ScreenCastSession *screen_cast_session = (ScreenCastSession *)session;

          g_assert_cmpint (screen_cast_session->state,
                           ==,
                           SCREEN_CAST_SESSION_STATE_SELECTING_SOURCES);
          screen_cast_session->state = SCREEN_CAST_SESSION_STATE_SOURCES_SELECTED;
        }
      else if (is_remote_desktop_session (session))
        {
          RemoteDesktopSession *remote_desktop_session =
            (RemoteDesktopSession *)session;

          remote_desktop_session_sources_selected (remote_desktop_session);
        }
    }
}

static gboolean
validate_device_types (const char *key,
                       GVariant *value,
                       GVariant *options,
                       GError **error)
{
  guint32 types = g_variant_get_uint32 (value);

  if ((types & ~(1 | 2 | 4)) != 0)
    {
      g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   "Unsupported device type: %x", types & ~(1 | 2 | 4));
      return FALSE;
    } 

  return TRUE;
}

static gboolean
validate_cursor_mode (const char *key,
                      GVariant *value,
                      GVariant *options,
                      GError **error)
{
  uint32_t mode = g_variant_get_uint32 (value);

  if (__builtin_popcount (mode) != 1)
    {
      g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   "Invalid cursor mode %x", mode);
      return FALSE;
    }

  if (!(available_cursor_modes & mode))
    {
      g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   "Unavailable cursor mode %x", mode);
      return FALSE;
    }

  return TRUE;
}

static gboolean
validate_restore_token (const char *key,
                        GVariant *value,
                        GVariant *options,
                        GError **error)
{
  const char *restore_token = g_variant_get_string (value, NULL);

  if (!g_uuid_string_is_valid (restore_token))
    {
      g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   "Restore token is not a valid UUID string");
      return FALSE;
    }

  return TRUE;
}

static gboolean
validate_persist_mode (const char *key,
                       GVariant *value,
                       GVariant *options,
                       GError **error)
{
  uint32_t mode = g_variant_get_uint32 (value);

  if (mode > PERSIST_MODE_PERSISTENT)
    {
      g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   "Invalid persist mode %x", mode);
      return FALSE;
    }

  return TRUE;
}

static XdpOptionKey screen_cast_select_sources_options[] = {
  { "types", G_VARIANT_TYPE_UINT32, validate_device_types },
  { "multiple", G_VARIANT_TYPE_BOOLEAN, NULL },
  { "cursor_mode", G_VARIANT_TYPE_UINT32, validate_cursor_mode },
  { "restore_token", G_VARIANT_TYPE_STRING, validate_restore_token },
  { "persist_mode", G_VARIANT_TYPE_UINT32, validate_persist_mode },
};

static gboolean
replace_restore_token_with_data (ScreenCastSession *screen_cast_session,
                                 GVariant **in_out_options,
                                 GError **error)
{
  Session *session = (Session *)screen_cast_session;
  GVariantBuilder options_builder;
  g_autoptr(GVariant) options = NULL;
  gsize i;

  options = *in_out_options;

  if (!g_variant_lookup (options, "persist_mode", "u", &screen_cast_session->persist_mode))
    screen_cast_session->persist_mode = PERSIST_MODE_NONE;

  if (is_remote_desktop_session (session) && screen_cast_session->persist_mode != PERSIST_MODE_NONE)
    {
      g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                   "Remote desktop sessions cannot persist");
      return FALSE;
    }

  g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT);
  for (i = 0; i < G_N_ELEMENTS (screen_cast_select_sources_options); i++)
    {
      g_autoptr(GVariant) value = NULL;

      value = g_variant_lookup_value (options,
                                      screen_cast_select_sources_options[i].key,
                                      screen_cast_select_sources_options[i].type);

      if (!value)
        continue;

      if (g_strcmp0 (screen_cast_select_sources_options[i].key, "restore_token") == 0)
        {
          g_autoptr(GVariant) restore_data = NULL;
          g_autofree char *restore_token = NULL;

          if (is_remote_desktop_session (session))
            {
              g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT,
                           "Remote desktop sessions cannot be restored");
              return FALSE;
            }

          restore_token = g_variant_dup_string (value, NULL);

          /* Lookup permissions in memory first, and fallback to the permission
           * store if not found. Immediately delete them now as a safety measure,
           * since they'll be stored again when the session is closed.
           *
           * Notice that transient mode uses the sender name, whereas persistent
           * mode uses the app id.
           */
          restore_data = get_transient_permissions (session->sender, restore_token);
          if (restore_data)
            {
              delete_transient_permissions (session->sender, restore_token);
            }
          else
            {
              restore_data = get_persistent_permissions (session->app_id, restore_token);
              if (restore_data)
                delete_persistent_permissions (session->app_id, restore_token);
            }

          if (!restore_data)
            continue;

          if (!g_variant_check_format_string (restore_data, RESTORE_DATA_TYPE, FALSE))
            {
              g_warning ("Restore data was stored with the wrong format, ignoring");
              continue;
            }

          /* Only store the restore token after checking if it exists, otherwise
           * apps can pass random UUIDs and yet predict what the next token will
           * be.
           */
          screen_cast_session->restore_token = g_steal_pointer (&restore_token);

          g_debug ("Replacing 'restore_token' with portal-specific data");
          g_variant_builder_add (&options_builder, "{sv}", "restore_data", restore_data);
        }
      else
        {
          g_variant_builder_add (&options_builder, "{sv}",
                                 screen_cast_select_sources_options[i].key,
                                 g_steal_pointer (&value));
        }
    }

  *in_out_options = g_variant_builder_end (&options_builder);

  return TRUE;
}

static gboolean
handle_select_sources (XdpScreenCast *object,
                       GDBusMethodInvocation *invocation,
                       const char *arg_session_handle,
                       GVariant *arg_options)
{
  Request *request = request_from_invocation (invocation);
  Session *session;
  ScreenCastSession *screen_cast_session;
  g_autoptr(GError) error = NULL;
  g_autoptr(XdpImplRequest) impl_request = NULL;
  GVariantBuilder options_builder;
  GVariant *options;

  REQUEST_AUTOLOCK (request);

  session = acquire_session (arg_session_handle, request);
  if (!session)
    {
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Invalid session");
      return TRUE;
    }

  SESSION_AUTOLOCK_UNREF (session);

  if (is_screen_cast_session (session))
    {
      ScreenCastSession *screen_cast_session = (ScreenCastSession *)session;

      switch (screen_cast_session->state)
        {
        case SCREEN_CAST_SESSION_STATE_INIT:
          break;
        case SCREEN_CAST_SESSION_STATE_SELECTING_SOURCES:
        case SCREEN_CAST_SESSION_STATE_SOURCES_SELECTED:
          g_dbus_method_invocation_return_error (invocation,
                                                 G_DBUS_ERROR,
                                                 G_DBUS_ERROR_FAILED,
                                                 "Sources already selected");
          return TRUE;
        case SCREEN_CAST_SESSION_STATE_STARTING:
        case SCREEN_CAST_SESSION_STATE_STARTED:
          g_dbus_method_invocation_return_error (invocation,
                                                 G_DBUS_ERROR,
                                                 G_DBUS_ERROR_FAILED,
                                                 "Can only select sources before starting");
          return TRUE;
        case SCREEN_CAST_SESSION_STATE_CLOSED:
          g_dbus_method_invocation_return_error (invocation,
                                                 G_DBUS_ERROR,
                                                 G_DBUS_ERROR_FAILED,
                                                 "Invalid session");
          return TRUE;
        }
    }
  else if (is_remote_desktop_session (session))
    {
      RemoteDesktopSession *remote_desktop_session =
        (RemoteDesktopSession *)session;

      if (!remote_desktop_session_can_select_sources (remote_desktop_session))
        {
          g_dbus_method_invocation_return_error (invocation,
                                                 G_DBUS_ERROR,
                                                 G_DBUS_ERROR_FAILED,
                                                 "Invalid state");
          return TRUE;
        }
    }
  else
    {
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "Invalid session");
      return TRUE;
    }

  impl_request =
    xdp_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)),
                                     G_DBUS_PROXY_FLAGS_NONE,
                                     g_dbus_proxy_get_name (G_DBUS_PROXY (impl)),
                                     request->id,
                                     NULL, &error);
  if (!impl_request)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return TRUE;
    }

  request_set_impl_request (request, impl_request);
  request_export (request, g_dbus_method_invocation_get_connection (invocation));

  g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT);
  if (!xdp_filter_options (arg_options, &options_builder,
                           screen_cast_select_sources_options,
                           G_N_ELEMENTS (screen_cast_select_sources_options),
                           &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return TRUE;
    }

  options = g_variant_builder_end (&options_builder);

  /* If 'restore_token' is passed, lookup the corresponding data in the
   * permission store and / or the GHashTable with transient permissions.
   * Portal implementations do not have access to the restore token.
   */
  screen_cast_session = (ScreenCastSession *) session;
  if (!replace_restore_token_with_data (screen_cast_session, &options, &error))
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return TRUE;
    }

  g_object_set_qdata_full (G_OBJECT (request),
                           quark_request_session,
                           g_object_ref (session),
                           g_object_unref);
  if (is_screen_cast_session (session))
    {
      ((ScreenCastSession *)session)->state =
        SCREEN_CAST_SESSION_STATE_SELECTING_SOURCES;
    }
  else
    {
      remote_desktop_session_selecting_sources ((RemoteDesktopSession *)session);
    }

  xdp_impl_screen_cast_call_select_sources (impl,
                                            request->id,
                                            arg_session_handle,
                                            xdp_app_info_get_id (request->app_info),
                                            options,
                                            NULL,
                                            select_sources_done,
                                            g_object_ref (request));

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

  return TRUE;
}

uint32_t
screen_cast_stream_get_pipewire_node_id (ScreenCastStream *stream)
{
  return stream->id;
}

static void
append_stream_permissions (PipeWireRemote *remote,
                           GArray *permission_items,
                           GList *streams)
{
  GList *l;

  for (l = streams; l; l = l->next)
    {
      ScreenCastStream *stream = l->data;
      uint32_t stream_id;

      stream_id = screen_cast_stream_get_pipewire_node_id (stream);
      g_array_append_val (permission_items,
                          PERMISSION_ITEM (stream_id, PW_PERM_RWX));
    }
}

static PipeWireRemote *
open_pipewire_screen_cast_remote (const char *app_id,
                                  GList *streams,
                                  GError **error)
{
  struct pw_properties *pipewire_properties;
  PipeWireRemote *remote;
  g_autoptr(GArray) permission_items = NULL;

  pipewire_properties = pw_properties_new ("pipewire.access.portal.app_id", app_id,
                                           "pipewire.access.portal.media_roles", "",
                                           NULL);
  remote = pipewire_remote_new_sync (pipewire_properties,
                                     NULL, NULL, NULL, NULL,
                                     error);
  if (!remote)
    return FALSE;

  permission_items = g_array_new (FALSE, TRUE, sizeof (struct pw_permission));

  /*
   * PipeWire:Interface:Core
   * Needs rwx to be able create the sink node using the create-object method
   */
  g_array_append_val (permission_items,
                      PERMISSION_ITEM (PW_ID_CORE, PW_PERM_RWX));

  /*
   * PipeWire:Interface:NodeFactory
   * Needs r-- so it can be passed to create-object when creating the sink node.
   */
  g_array_append_val (permission_items,
                      PERMISSION_ITEM (remote->node_factory_id, PW_PERM_R));

  append_stream_permissions (remote, permission_items, streams);

  /*
   * Hide all existing and future nodes (except the ones we explicitly list above).
   */
  g_array_append_val (permission_items,
                      PERMISSION_ITEM (PW_ID_ANY, 0));

  pw_client_update_permissions (pw_core_get_client(remote->core),
                                permission_items->len,
                                (const struct pw_permission *)permission_items->data);

  pipewire_remote_roundtrip (remote);

  return remote;
}

void
screen_cast_stream_get_size (ScreenCastStream *stream,
                             int32_t *width,
                             int32_t *height)
{
  *width = stream->width;
  *height = stream->height;
}

void
screen_cast_stream_free (ScreenCastStream *stream)
{
  g_free (stream);
}

GList *
collect_screen_cast_stream_data (GVariantIter *streams_iter)
{
  GList *streams = NULL;
  uint32_t stream_id;
  g_autoptr(GVariant) stream_options = NULL;

  while (g_variant_iter_next (streams_iter, "(u@a{sv})",
                              &stream_id, &stream_options))
    {
      ScreenCastStream *stream;

      stream = g_new0 (ScreenCastStream, 1);
      stream->id = stream_id;
      g_variant_lookup (stream_options, "size", "(ii)",
                        &stream->width, &stream->height);

      streams = g_list_prepend (streams, stream);
    }

  return streams;
}

static void
generate_and_save_restore_token (ScreenCastSession *screen_cast_session)
{
  Session *session = (Session *)screen_cast_session;

  if (!screen_cast_session->restore_data)
    {
      if (screen_cast_session->restore_token)
        {
          delete_persistent_permissions (session->app_id, screen_cast_session->restore_token);
          delete_transient_permissions (session->sender, screen_cast_session->restore_token);
        }

      g_clear_pointer (&screen_cast_session->restore_token, g_free);
      return;
    }

  switch (screen_cast_session->persist_mode)
    {
    case PERSIST_MODE_NONE:
      if (screen_cast_session->restore_token)
        {
          delete_persistent_permissions (session->app_id, screen_cast_session->restore_token);
          delete_transient_permissions (session->sender, screen_cast_session->restore_token);
        }

      g_clear_pointer (&screen_cast_session->restore_token, g_free);
      g_clear_pointer (&screen_cast_session->restore_data, g_variant_unref);
      break;

    case PERSIST_MODE_TRANSIENT:
      if (screen_cast_session->restore_token == NULL)
        screen_cast_session->restore_token = g_uuid_string_random ();

      set_transient_permissions (session->sender,
                                 screen_cast_session->restore_token,
                                 screen_cast_session->restore_data);
      break;

    case PERSIST_MODE_PERSISTENT:
      if (screen_cast_session->restore_token == NULL)
        screen_cast_session->restore_token = g_uuid_string_random ();

      set_persistent_permissions (session->app_id,
                                  screen_cast_session->restore_token,
                                  screen_cast_session->restore_data);

      break;
    }
}

static void
replace_restore_data_by_token (ScreenCastSession *screen_cast_session,
                               GVariant **in_out_results)
{
  g_autoptr(GVariant) results = *in_out_results;
  GVariantBuilder results_builder;
  GVariantIter iter;
  const char *key;
  GVariant *value;
  gboolean found_restore_data = FALSE;

  g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT);

  g_variant_iter_init (&iter, results);
  while (g_variant_iter_next (&iter, "{&sv}", &key, &value))
    {
      if (g_strcmp0 (key, "restore_data") == 0)
        {
          GVariant *child = g_variant_get_child_value (value, 0);

          if (g_variant_check_format_string (child, RESTORE_DATA_TYPE, FALSE))
            {
              screen_cast_session->restore_data = g_variant_ref_sink (child);
              found_restore_data = TRUE;
            }
          else
            {
              g_warning ("Received restore data in invalid variant format ('%s'; expected '%s')",
                         g_variant_get_type_string (value),
                         RESTORE_DATA_TYPE);
            }
        }
      else if (g_strcmp0 (key, "persist_mode") == 0)
        {
          screen_cast_session->persist_mode = MIN (screen_cast_session->persist_mode,
                                                   g_variant_get_uint32 (value));
        }
      else
        {
          g_variant_builder_add (&results_builder, "{sv}", key, value);
        }
    }

  if (found_restore_data)
    {
      g_debug ("Replacing restore data received from portal impl with a token");

      generate_and_save_restore_token (screen_cast_session);
      g_variant_builder_add (&results_builder, "{sv}", "restore_token",
                             g_variant_new_string (screen_cast_session->restore_token));
    }
  else
    {
      screen_cast_session->persist_mode = PERSIST_MODE_NONE;
    }

  *in_out_results = g_variant_builder_end (&results_builder);
}

static gboolean
process_results (ScreenCastSession *screen_cast_session,
                 GVariant **in_out_results,
                 GError **error)
{
  g_autoptr(GVariantIter) streams_iter = NULL;
  GVariant *results = *in_out_results;

  if (!g_variant_lookup (results, "streams", "a(ua{sv})", &streams_iter))
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "No streams");
      return FALSE;
    }

  screen_cast_session->streams = collect_screen_cast_stream_data (streams_iter);
  replace_restore_data_by_token (screen_cast_session, in_out_results);
  return TRUE;
}

static void
start_done (GObject *source_object,
            GAsyncResult *res,
            gpointer data)
{
  g_autoptr(Request) request = data;
  Session *session;
  ScreenCastSession *screen_cast_session;
  guint response = 2;
  gboolean should_close_session;
  GVariant *results = NULL;
  g_autoptr(GError) error = NULL;

  REQUEST_AUTOLOCK (request);

  session = g_object_get_qdata (G_OBJECT (request), quark_request_session);
  SESSION_AUTOLOCK_UNREF (g_object_ref (session));
  g_object_set_qdata (G_OBJECT (request), quark_request_session, NULL);

  if (!xdp_impl_screen_cast_call_start_finish (impl,
                                               &response,
                                               &results,
                                               res,
                                               &error))
    g_warning ("A backend call failed: %s", error->message);

  should_close_session = !request->exported || response != 0;

  screen_cast_session = (ScreenCastSession *)session;

  if (request->exported)
    {
      if (response == 0)
        {
          if (!process_results (screen_cast_session, &results, &error))
            {
              g_warning ("Failed to process results: %s", error->message);
              g_clear_error (&error);
              g_clear_pointer (&results, g_variant_unref);
              response = 2;
              should_close_session = TRUE;
            }
        }

      if (!results)
        {
          GVariantBuilder results_builder;

          g_variant_builder_init (&results_builder, G_VARIANT_TYPE_VARDICT);
          results = g_variant_builder_end (&results_builder);
        }

      xdp_request_emit_response (XDP_REQUEST (request), response, results);
      request_unexport (request);
    }

  if (should_close_session)
    {
      session_close (session, TRUE);
    }
  else if (!session->closed)
    {
      g_assert (screen_cast_session->state ==
                SCREEN_CAST_SESSION_STATE_STARTING);
      g_debug ("screen cast session owned by '%s' started", session->sender);
      screen_cast_session->state = SCREEN_CAST_SESSION_STATE_STARTED;
    }
}

static gboolean
handle_start (XdpScreenCast *object,
              GDBusMethodInvocation *invocation,
              const char *arg_session_handle,
              const char *arg_parent_window,
              GVariant *arg_options)
{
  Request *request = request_from_invocation (invocation);
  Session *session;
  ScreenCastSession *screen_cast_session;
  g_autoptr(GError) error = NULL;
  g_autoptr(XdpImplRequest) impl_request = NULL;
  GVariantBuilder options_builder;
  GVariant *options;

  REQUEST_AUTOLOCK (request);

  session = acquire_session (arg_session_handle, request);
  if (!session)
    {
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Invalid session");
      return TRUE;
    }

  SESSION_AUTOLOCK_UNREF (session);

  screen_cast_session = (ScreenCastSession *)session;
  switch (screen_cast_session->state)
    {
    case SCREEN_CAST_SESSION_STATE_SOURCES_SELECTED:
      break;
    case SCREEN_CAST_SESSION_STATE_INIT:
    case SCREEN_CAST_SESSION_STATE_SELECTING_SOURCES:
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "Sources not selected");
      return TRUE;
    case SCREEN_CAST_SESSION_STATE_STARTING:
    case SCREEN_CAST_SESSION_STATE_STARTED:
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "Can only start once");
      return TRUE;
    case SCREEN_CAST_SESSION_STATE_CLOSED:
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "Invalid session");
      return TRUE;
    }

  g_object_set_data_full (G_OBJECT (request),
                          "window", g_strdup (arg_parent_window), g_free);

  impl_request =
    xdp_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (impl)),
                                     G_DBUS_PROXY_FLAGS_NONE,
                                     g_dbus_proxy_get_name (G_DBUS_PROXY (impl)),
                                     request->id,
                                     NULL, &error);
  if (!impl_request)
    {
      g_dbus_method_invocation_return_gerror (invocation, error);
      return TRUE;
    }

  request_set_impl_request (request, impl_request);
  request_export (request, g_dbus_method_invocation_get_connection (invocation));

  g_variant_builder_init (&options_builder, G_VARIANT_TYPE_VARDICT);
  options = g_variant_builder_end (&options_builder);

  g_object_set_qdata_full (G_OBJECT (request),
                           quark_request_session,
                           g_object_ref (session),
                           g_object_unref);
  screen_cast_session->state = SCREEN_CAST_SESSION_STATE_STARTING;

  xdp_impl_screen_cast_call_start (impl,
                                   request->id,
                                   arg_session_handle,
                                   xdp_app_info_get_id (request->app_info),
                                   arg_parent_window,
                                   options,
                                   NULL,
                                   start_done,
                                   g_object_ref (request));

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

  return TRUE;
}

static gboolean
handle_open_pipewire_remote (XdpScreenCast *object,
                             GDBusMethodInvocation *invocation,
                             GUnixFDList *in_fd_list,
                             const char *arg_session_handle,
                             GVariant *arg_options)
{
  Call *call = call_from_invocation (invocation);
  Session *session;
  GList *streams;
  PipeWireRemote *remote;
  g_autoptr(GUnixFDList) out_fd_list = NULL;
  int fd;
  int fd_id;
  g_autoptr(GError) error = NULL;

  session = acquire_session_from_call (arg_session_handle, call);
  if (!session)
    {
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Invalid session");
      return TRUE;
    }

  SESSION_AUTOLOCK_UNREF (session);

  if (is_screen_cast_session (session))
    {
      ScreenCastSession *screen_cast_session = (ScreenCastSession *)session;

      streams = screen_cast_session->streams;
    }
  else if (is_remote_desktop_session (session))
    {
      RemoteDesktopSession *remote_desktop_session =
        (RemoteDesktopSession *)session;

      streams = remote_desktop_session_get_streams (remote_desktop_session);
    }
  else
    {
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Invalid session");
      return TRUE;
    }

  if (!streams)
    {
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "No streams available");
      return TRUE;
    }

  remote = open_pipewire_screen_cast_remote (session->app_id, streams, &error);
  if (!remote)
    {
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "%s", error->message);
      return TRUE;
    }

  out_fd_list = g_unix_fd_list_new ();
  fd = pw_core_steal_fd (remote->core);
  fd_id = g_unix_fd_list_append (out_fd_list, fd, &error);
  close (fd);
  pipewire_remote_destroy (remote);

  if (fd_id == -1)
    {
      g_dbus_method_invocation_return_error (invocation,
                                             G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Failed to append fd: %s",
                                             error->message);
      return TRUE;
    }

  xdp_screen_cast_complete_open_pipewire_remote (object, invocation,
                                                 out_fd_list,
                                                 g_variant_new_handle (fd_id));
  return TRUE;
}

static void
screen_cast_iface_init (XdpScreenCastIface *iface)
{
  iface->handle_create_session = handle_create_session;
  iface->handle_select_sources = handle_select_sources;
  iface->handle_start = handle_start;
  iface->handle_open_pipewire_remote = handle_open_pipewire_remote;
}

static void
sync_supported_source_types (ScreenCast *screen_cast)
{
  unsigned int available_source_types;

  available_source_types = xdp_impl_screen_cast_get_available_source_types (impl);
  xdp_screen_cast_set_available_source_types (XDP_SCREEN_CAST (screen_cast),
                                              available_source_types);
}

static void
on_supported_source_types_changed (GObject *gobject,
                                   GParamSpec *pspec,
                                   ScreenCast *screen_cast)
{
  sync_supported_source_types (screen_cast);
}

static void
sync_supported_cursor_modes (ScreenCast *screen_cast)
{

  available_cursor_modes = xdp_impl_screen_cast_get_available_cursor_modes (impl);
  xdp_screen_cast_set_available_cursor_modes (XDP_SCREEN_CAST (screen_cast),
                                              available_cursor_modes);
}

static void
on_supported_cursor_modes_changed (GObject *gobject,
                                   GParamSpec *pspec,
                                   ScreenCast *screen_cast)
{
  sync_supported_cursor_modes (screen_cast);
}

static void
screen_cast_init (ScreenCast *screen_cast)
{
  xdp_screen_cast_set_version (XDP_SCREEN_CAST (screen_cast), 4);

  g_signal_connect (impl, "notify::supported-source-types",
                    G_CALLBACK (on_supported_source_types_changed),
                    screen_cast);
  if (impl_version >= 2)
    {
      g_signal_connect (impl, "notify::supported-cursor-modes",
                        G_CALLBACK (on_supported_cursor_modes_changed),
                        screen_cast);
    }
  sync_supported_source_types (screen_cast);
  sync_supported_cursor_modes (screen_cast);
}

static void
screen_cast_class_init (ScreenCastClass *klass)
{
  quark_request_session =
    g_quark_from_static_string ("-xdp-request-screen-cast-session");
}

GDBusInterfaceSkeleton *
screen_cast_create (GDBusConnection *connection,
                    const char *dbus_name)
{
  g_autoptr(GError) error = NULL;

  impl = xdp_impl_screen_cast_proxy_new_sync (connection,
                                              G_DBUS_PROXY_FLAGS_NONE,
                                              dbus_name,
                                              DESKTOP_PORTAL_OBJECT_PATH,
                                              NULL,
                                              &error);
  if (impl == NULL)
    {
      g_warning ("Failed to create screen cast proxy: %s", error->message);
      return NULL;
    }

  impl_version = xdp_impl_screen_cast_get_version (impl);

  g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (impl), G_MAXINT);

  screen_cast = g_object_new (screen_cast_get_type (), NULL);

  return G_DBUS_INTERFACE_SKELETON (screen_cast);
}

static void
screen_cast_session_close (Session *session)
{
  ScreenCastSession *screen_cast_session = (ScreenCastSession *)session;

  screen_cast_session->state = SCREEN_CAST_SESSION_STATE_CLOSED;

  generate_and_save_restore_token (screen_cast_session);

  g_debug ("screen cast session owned by '%s' closed", session->sender);
}

static void
screen_cast_session_finalize (GObject *object)
{
  ScreenCastSession *screen_cast_session = (ScreenCastSession *)object;

  g_clear_pointer (&screen_cast_session->restore_token, g_free);
  g_clear_pointer (&screen_cast_session->restore_data, g_variant_unref);

  g_list_free_full (screen_cast_session->streams,
                    (GDestroyNotify)screen_cast_stream_free);

  G_OBJECT_CLASS (screen_cast_session_parent_class)->finalize (object);
}

static void
screen_cast_session_init (ScreenCastSession *screen_cast_session)
{
}

static void
screen_cast_session_class_init (ScreenCastSessionClass *klass)
{
  GObjectClass *object_class;
  SessionClass *session_class;

  object_class = G_OBJECT_CLASS (klass);
  object_class->finalize = screen_cast_session_finalize;

  session_class = (SessionClass *)klass;
  session_class->close = screen_cast_session_close;
}
