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

/*
 * Copyright (C) 2015-2017 Red Hat Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 *
 */

#include "config.h"

#include "backends/meta-screen-cast.h"

#include <pipewire/pipewire.h>

#include "backends/meta-backend-private.h"
#include "backends/meta-remote-desktop-session.h"
#include "backends/meta-screen-cast-session.h"

#define META_SCREEN_CAST_DBUS_SERVICE "org.gnome.Mutter.ScreenCast"
#define META_SCREEN_CAST_DBUS_PATH "/org/gnome/Mutter/ScreenCast"
#define META_SCREEN_CAST_API_VERSION 4

struct _MetaScreenCast
{
  MetaDBusScreenCastSkeleton parent;

  int dbus_name_id;

  int inhibit_count;

  GList *sessions;

  MetaDbusSessionWatcher *session_watcher;
  MetaBackend *backend;

  gboolean disable_dma_bufs;
};

static void
meta_screen_cast_init_iface (MetaDBusScreenCastIface *iface);

G_DEFINE_TYPE_WITH_CODE (MetaScreenCast, meta_screen_cast,
                         META_DBUS_TYPE_SCREEN_CAST_SKELETON,
                         G_IMPLEMENT_INTERFACE (META_DBUS_TYPE_SCREEN_CAST,
                                                meta_screen_cast_init_iface))

void
meta_screen_cast_inhibit (MetaScreenCast *screen_cast)
{
  screen_cast->inhibit_count++;
  if (screen_cast->inhibit_count == 1)
    {
      while (screen_cast->sessions)
        {
          MetaScreenCastSession *session = screen_cast->sessions->data;

          meta_screen_cast_session_close (session);
        }
    }
}

void
meta_screen_cast_uninhibit (MetaScreenCast *screen_cast)
{
  g_return_if_fail (screen_cast->inhibit_count > 0);

  screen_cast->inhibit_count--;
}

GDBusConnection *
meta_screen_cast_get_connection (MetaScreenCast *screen_cast)
{
  GDBusInterfaceSkeleton *interface_skeleton =
    G_DBUS_INTERFACE_SKELETON (screen_cast);

  return g_dbus_interface_skeleton_get_connection (interface_skeleton);
}

MetaBackend *
meta_screen_cast_get_backend (MetaScreenCast *screen_cast)
{
  return screen_cast->backend;
}

void
meta_screen_cast_disable_dma_bufs (MetaScreenCast *screen_cast)
{
  screen_cast->disable_dma_bufs = TRUE;
}

CoglDmaBufHandle *
meta_screen_cast_create_dma_buf_handle (MetaScreenCast *screen_cast,
                                        int             width,
                                        int             height)
{
  ClutterBackend *clutter_backend =
    meta_backend_get_clutter_backend (screen_cast->backend);
  CoglContext *cogl_context =
    clutter_backend_get_cogl_context (clutter_backend);
  CoglRenderer *cogl_renderer = cogl_context_get_renderer (cogl_context);
  g_autoptr (GError) error = NULL;
  CoglDmaBufHandle *dmabuf_handle;

  if (screen_cast->disable_dma_bufs)
    return NULL;

  dmabuf_handle = cogl_renderer_create_dma_buf (cogl_renderer,
                                                width, height,
                                                &error);
  if (!dmabuf_handle)
    {
      g_warning ("Failed to allocate DMA buffer, "
                 "disabling DMA buffer based screen casting: %s",
                 error->message);
      screen_cast->disable_dma_bufs = TRUE;
      return NULL;
    }

  return dmabuf_handle;
}

static gboolean
register_remote_desktop_screen_cast_session (MetaScreenCastSession  *session,
                                             const char             *remote_desktop_session_id,
                                             GError                **error)
{
  MetaScreenCast *screen_cast =
    meta_screen_cast_session_get_screen_cast (session);
  MetaBackend *backend = meta_screen_cast_get_backend (screen_cast);
  MetaRemoteDesktop *remote_desktop = meta_backend_get_remote_desktop (backend);
  MetaRemoteDesktopSession *remote_desktop_session;

  remote_desktop_session =
    meta_remote_desktop_get_session (remote_desktop, remote_desktop_session_id);
  if (!remote_desktop_session)
    {
      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                   "No remote desktop session found");
      return FALSE;
    }

  if (!meta_remote_desktop_session_register_screen_cast (remote_desktop_session,
                                                         session,
                                                         error))
    return FALSE;

  return TRUE;
}

static void
on_session_closed (MetaScreenCastSession *session,
                   MetaScreenCast        *screen_cast)
{
  screen_cast->sessions = g_list_remove (screen_cast->sessions, session);
}

static gboolean
handle_create_session (MetaDBusScreenCast    *skeleton,
                       GDBusMethodInvocation *invocation,
                       GVariant              *properties)
{
  MetaScreenCast *screen_cast = META_SCREEN_CAST (skeleton);
  const char *peer_name;
  MetaScreenCastSession *session;
  GError *error = NULL;
  const char *session_path;
  const char *client_dbus_name;
  char *remote_desktop_session_id = NULL;
  gboolean disable_animations;
  MetaScreenCastSessionType session_type;

  if (screen_cast->inhibit_count > 0)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Session creation inhibited");

      return TRUE;
    }

  g_variant_lookup (properties, "remote-desktop-session-id", "s",
                    &remote_desktop_session_id);

  if (remote_desktop_session_id)
    session_type = META_SCREEN_CAST_SESSION_TYPE_REMOTE_DESKTOP;
  else
    session_type = META_SCREEN_CAST_SESSION_TYPE_NORMAL;

  peer_name = g_dbus_method_invocation_get_sender (invocation);
  session = meta_screen_cast_session_new (screen_cast,
                                          session_type,
                                          peer_name,
                                          &error);
  if (!session)
    {
      g_warning ("Failed to create screen cast session: %s",
                 error->message);

      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "Failed to create session: %s",
                                             error->message);
      g_error_free (error);

      return TRUE;
    }

  if (remote_desktop_session_id)
    {
      if (!register_remote_desktop_screen_cast_session (session,
                                                        remote_desktop_session_id,
                                                        &error))
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                                 G_DBUS_ERROR_FAILED,
                                                 "%s", error->message);
          g_error_free (error);
          g_object_unref (session);
          return TRUE;
        }
    }

  if (g_variant_lookup (properties, "disable-animations", "b",
                        &disable_animations))
    {
      meta_screen_cast_session_set_disable_animations (session,
                                                       disable_animations);
    }

  client_dbus_name = g_dbus_method_invocation_get_sender (invocation);
  meta_dbus_session_watcher_watch_session (screen_cast->session_watcher,
                                           client_dbus_name,
                                           META_DBUS_SESSION (session));

  session_path = meta_screen_cast_session_get_object_path (session);
  meta_dbus_screen_cast_complete_create_session (skeleton,
                                                 invocation,
                                                 session_path);

  screen_cast->sessions = g_list_append (screen_cast->sessions, session);

  g_signal_connect (session, "session-closed",
                    G_CALLBACK (on_session_closed),
                    screen_cast);

  return TRUE;
}

static void
meta_screen_cast_init_iface (MetaDBusScreenCastIface *iface)
{
  iface->handle_create_session = handle_create_session;
}

static void
on_bus_acquired (GDBusConnection *connection,
                 const char      *name,
                 gpointer         user_data)
{
  MetaScreenCast *screen_cast = user_data;
  GDBusInterfaceSkeleton *interface_skeleton =
    G_DBUS_INTERFACE_SKELETON (screen_cast);
  GError *error = NULL;

  if (!g_dbus_interface_skeleton_export (interface_skeleton,
                                         connection,
                                         META_SCREEN_CAST_DBUS_PATH,
                                         &error))
    g_warning ("Failed to export remote desktop object: %s", error->message);
}

static void
on_name_acquired (GDBusConnection *connection,
                  const char      *name,
                  gpointer         user_data)
{
  g_info ("Acquired name %s", name);
}

static void
on_name_lost (GDBusConnection *connection,
              const char      *name,
              gpointer         user_data)
{
  g_warning ("Lost or failed to acquire name %s", name);
}

static void
meta_screen_cast_constructed (GObject *object)
{
  MetaScreenCast *screen_cast = META_SCREEN_CAST (object);

  screen_cast->dbus_name_id =
    g_bus_own_name (G_BUS_TYPE_SESSION,
                    META_SCREEN_CAST_DBUS_SERVICE,
                    G_BUS_NAME_OWNER_FLAGS_NONE,
                    on_bus_acquired,
                    on_name_acquired,
                    on_name_lost,
                    screen_cast,
                    NULL);
}

static void
meta_screen_cast_finalize (GObject *object)
{
  MetaScreenCast *screen_cast = META_SCREEN_CAST (object);

  if (screen_cast->dbus_name_id)
    g_bus_unown_name (screen_cast->dbus_name_id);

  g_assert (!screen_cast->sessions);

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

static void
on_prepare_shutdown (MetaBackend    *backend,
                     MetaScreenCast *screen_cast)
{
  while (screen_cast->sessions)
    {
      MetaScreenCastSession *session = screen_cast->sessions->data;

      if (meta_screen_cast_session_get_session_type (session) !=
          META_SCREEN_CAST_SESSION_TYPE_REMOTE_DESKTOP)
        meta_screen_cast_session_close (session);
    }
}

MetaScreenCast *
meta_screen_cast_new (MetaBackend            *backend,
                      MetaDbusSessionWatcher *session_watcher)
{
  MetaScreenCast *screen_cast;

  screen_cast = g_object_new (META_TYPE_SCREEN_CAST, NULL);
  screen_cast->backend = backend;
  screen_cast->session_watcher = session_watcher;

  g_signal_connect (backend, "prepare-shutdown",
                    G_CALLBACK (on_prepare_shutdown),
                    screen_cast);

  return screen_cast;
}

static void
meta_screen_cast_init (MetaScreenCast *screen_cast)
{
  static gboolean is_pipewire_initialized = FALSE;

  if (!is_pipewire_initialized)
    {
      pw_init (NULL, NULL);
      is_pipewire_initialized = TRUE;
    }

  meta_dbus_screen_cast_set_version (META_DBUS_SCREEN_CAST (screen_cast),
                                     META_SCREEN_CAST_API_VERSION);
}

static void
meta_screen_cast_class_init (MetaScreenCastClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed = meta_screen_cast_constructed;
  object_class->finalize = meta_screen_cast_finalize;
}
