/* -*- 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-session.h"

#include "backends/meta-backend-private.h"
#include "backends/meta-dbus-session-watcher.h"
#include "backends/meta-remote-access-controller-private.h"
#include "backends/meta-screen-cast-area-stream.h"
#include "backends/meta-screen-cast-monitor-stream.h"
#include "backends/meta-screen-cast-stream.h"
#include "backends/meta-screen-cast-window-stream.h"
#include "core/display-private.h"

#define META_SCREEN_CAST_SESSION_DBUS_PATH "/org/gnome/Mutter/ScreenCast/Session"

struct _MetaScreenCastSession
{
  MetaDBusScreenCastSessionSkeleton parent;

  MetaScreenCast *screen_cast;

  char *peer_name;

  MetaScreenCastSessionType session_type;
  char *object_path;

  GList *streams;

  MetaScreenCastSessionHandle *handle;

  gboolean disable_animations;
};

static void
meta_screen_cast_session_init_iface (MetaDBusScreenCastSessionIface *iface);

static void
meta_dbus_session_init_iface (MetaDbusSessionInterface *iface);

G_DEFINE_TYPE_WITH_CODE (MetaScreenCastSession,
                         meta_screen_cast_session,
                         META_DBUS_TYPE_SCREEN_CAST_SESSION_SKELETON,
                         G_IMPLEMENT_INTERFACE (META_DBUS_TYPE_SCREEN_CAST_SESSION,
                                                meta_screen_cast_session_init_iface)
                         G_IMPLEMENT_INTERFACE (META_TYPE_DBUS_SESSION,
                                                meta_dbus_session_init_iface))

struct _MetaScreenCastSessionHandle
{
  MetaRemoteAccessHandle parent;

  MetaScreenCastSession *session;
};

G_DEFINE_TYPE (MetaScreenCastSessionHandle,
               meta_screen_cast_session_handle,
               META_TYPE_REMOTE_ACCESS_HANDLE)

static MetaScreenCastSessionHandle *
meta_screen_cast_session_handle_new (MetaScreenCastSession *session);

static void
init_remote_access_handle (MetaScreenCastSession *session)
{
  MetaBackend *backend = meta_get_backend ();
  MetaRemoteAccessController *remote_access_controller;
  MetaRemoteAccessHandle *remote_access_handle;

  session->handle = meta_screen_cast_session_handle_new (session);

  remote_access_controller = meta_backend_get_remote_access_controller (backend);
  remote_access_handle = META_REMOTE_ACCESS_HANDLE (session->handle);

  meta_remote_access_handle_set_disable_animations (remote_access_handle,
                                                    session->disable_animations);

  meta_remote_access_controller_notify_new_handle (remote_access_controller,
                                                   remote_access_handle);
}

gboolean
meta_screen_cast_session_start (MetaScreenCastSession  *session,
                                GError                **error)
{
  GList *l;

  for (l = session->streams; l; l = l->next)
    {
      MetaScreenCastStream *stream = l->data;

      if (!meta_screen_cast_stream_start (stream, error))
        return FALSE;
    }

  init_remote_access_handle (session);

  return TRUE;
}

void
meta_screen_cast_session_close (MetaScreenCastSession *session)
{
  MetaDBusScreenCastSession *skeleton = META_DBUS_SCREEN_CAST_SESSION (session);

  g_list_free_full (session->streams, g_object_unref);

  meta_dbus_session_notify_closed (META_DBUS_SESSION (session));

  switch (session->session_type)
    {
    case META_SCREEN_CAST_SESSION_TYPE_NORMAL:
      meta_dbus_screen_cast_session_emit_closed (skeleton);
      break;
    case META_SCREEN_CAST_SESSION_TYPE_REMOTE_DESKTOP:
      break;
    }

  g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (session));

  if (session->handle)
    {
      MetaRemoteAccessHandle *remote_access_handle =
        META_REMOTE_ACCESS_HANDLE (session->handle);

      meta_remote_access_handle_notify_stopped (remote_access_handle);
    }

  g_object_unref (session);
}

MetaScreenCastStream *
meta_screen_cast_session_get_stream (MetaScreenCastSession *session,
                                     const char            *path)
{
  GList *l;

  for (l = session->streams; l; l = l->next)
    {
      MetaScreenCastStream *stream = l->data;

      if (g_strcmp0 (meta_screen_cast_stream_get_object_path (stream),
                     path) == 0)
        return stream;
    }

  return NULL;
}

MetaScreenCast *
meta_screen_cast_session_get_screen_cast (MetaScreenCastSession *session)
{
  return session->screen_cast;
}

void
meta_screen_cast_session_set_disable_animations (MetaScreenCastSession *session,
                                                 gboolean               disable_animations)
{
  session->disable_animations = disable_animations;
}

char *
meta_screen_cast_session_get_object_path (MetaScreenCastSession *session)
{
  return session->object_path;
}

char *
meta_screen_cast_session_get_peer_name (MetaScreenCastSession *session)
{
  return session->peer_name;
}

static gboolean
check_permission (MetaScreenCastSession *session,
                  GDBusMethodInvocation *invocation)
{
  return g_strcmp0 (session->peer_name,
                    g_dbus_method_invocation_get_sender (invocation)) == 0;
}

static gboolean
handle_start (MetaDBusScreenCastSession *skeleton,
              GDBusMethodInvocation     *invocation)
{
  MetaScreenCastSession *session = META_SCREEN_CAST_SESSION (skeleton);
  GError *error = NULL;

  if (!check_permission (session, invocation))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Permission denied");
      return TRUE;
    }

  switch (session->session_type)
    {
    case META_SCREEN_CAST_SESSION_TYPE_NORMAL:
      break;
    case META_SCREEN_CAST_SESSION_TYPE_REMOTE_DESKTOP:
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "Must be started from remote desktop session");
      return TRUE;
    }

  if (!meta_screen_cast_session_start (session, &error))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "Failed to start screen cast: %s",
                                             error->message);
      g_error_free (error);

      return TRUE;
    }

  meta_dbus_screen_cast_session_complete_start (skeleton, invocation);

  return TRUE;
}

static gboolean
handle_stop (MetaDBusScreenCastSession *skeleton,
             GDBusMethodInvocation     *invocation)
{
  MetaScreenCastSession *session = META_SCREEN_CAST_SESSION (skeleton);

  if (!check_permission (session, invocation))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Permission denied");
      return TRUE;
    }

  switch (session->session_type)
    {
    case META_SCREEN_CAST_SESSION_TYPE_NORMAL:
      break;
    case META_SCREEN_CAST_SESSION_TYPE_REMOTE_DESKTOP:
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "Must be stopped from remote desktop session");
      return TRUE;
    }

  meta_screen_cast_session_close (session);

  meta_dbus_screen_cast_session_complete_stop (skeleton, invocation);

  return TRUE;
}

static void
on_stream_closed (MetaScreenCastStream  *stream,
                  MetaScreenCastSession *session)
{
  meta_screen_cast_session_close (session);
}

static gboolean
is_valid_cursor_mode (MetaScreenCastCursorMode cursor_mode)
{
  switch (cursor_mode)
    {
    case META_SCREEN_CAST_CURSOR_MODE_HIDDEN:
    case META_SCREEN_CAST_CURSOR_MODE_EMBEDDED:
    case META_SCREEN_CAST_CURSOR_MODE_METADATA:
      return TRUE;
    }

  return FALSE;
}

static gboolean
handle_record_monitor (MetaDBusScreenCastSession *skeleton,
                       GDBusMethodInvocation     *invocation,
                       const char                *connector,
                       GVariant                  *properties_variant)
{
  MetaScreenCastSession *session = META_SCREEN_CAST_SESSION (skeleton);
  GDBusInterfaceSkeleton *interface_skeleton;
  GDBusConnection *connection;
  MetaBackend *backend = meta_get_backend ();
  MetaMonitorManager *monitor_manager =
    meta_backend_get_monitor_manager (backend);
  MetaMonitor *monitor;
  MetaScreenCastCursorMode cursor_mode;
  gboolean is_recording;
  MetaScreenCastFlag flags;
  ClutterStage *stage;
  GError *error = NULL;
  MetaScreenCastMonitorStream *monitor_stream;
  MetaScreenCastStream *stream;
  char *stream_path;

  if (!check_permission (session, invocation))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Permission denied");
      return TRUE;
    }

  interface_skeleton = G_DBUS_INTERFACE_SKELETON (skeleton);
  connection = g_dbus_interface_skeleton_get_connection (interface_skeleton);

  if (g_str_equal (connector, ""))
    monitor = meta_monitor_manager_get_primary_monitor (monitor_manager);
  else
    monitor = meta_monitor_manager_get_monitor_from_connector (monitor_manager,
                                                               connector);

  if (!monitor)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "Unknown monitor");
      return TRUE;
    }

  if (!g_variant_lookup (properties_variant, "cursor-mode", "u", &cursor_mode))
    {
      cursor_mode = META_SCREEN_CAST_CURSOR_MODE_HIDDEN;
    }
  else
    {
      if (!is_valid_cursor_mode (cursor_mode))
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                                 G_DBUS_ERROR_FAILED,
                                                 "Unknown cursor mode");
          return TRUE;
        }
    }

  if (!g_variant_lookup (properties_variant, "is-recording", "b", &is_recording))
    is_recording = FALSE;

  stage = CLUTTER_STAGE (meta_backend_get_stage (backend));

  flags = META_SCREEN_CAST_FLAG_NONE;
  if (is_recording)
    flags |= META_SCREEN_CAST_FLAG_IS_RECORDING;

  monitor_stream = meta_screen_cast_monitor_stream_new (session,
                                                        connection,
                                                        monitor,
                                                        stage,
                                                        cursor_mode,
                                                        flags,
                                                        &error);
  if (!monitor_stream)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "Failed to record monitor: %s",
                                             error->message);
      g_error_free (error);
      return TRUE;
    }

  stream = META_SCREEN_CAST_STREAM (monitor_stream);
  stream_path = meta_screen_cast_stream_get_object_path (stream);

  session->streams = g_list_append (session->streams, stream);

  g_signal_connect (stream, "closed", G_CALLBACK (on_stream_closed), session);

  meta_dbus_screen_cast_session_complete_record_monitor (skeleton,
                                                         invocation,
                                                         stream_path);

  return TRUE;
}

static gboolean
handle_record_window (MetaDBusScreenCastSession *skeleton,
                      GDBusMethodInvocation     *invocation,
                      GVariant                  *properties_variant)
{
  MetaScreenCastSession *session = META_SCREEN_CAST_SESSION (skeleton);
  GDBusInterfaceSkeleton *interface_skeleton;
  GDBusConnection *connection;
  MetaWindow *window;
  MetaScreenCastCursorMode cursor_mode;
  gboolean is_recording;
  MetaScreenCastFlag flags;
  GError *error = NULL;
  MetaDisplay *display;
  GVariant *window_id_variant = NULL;
  MetaScreenCastWindowStream *window_stream;
  MetaScreenCastStream *stream;
  char *stream_path;

  if (!check_permission (session, invocation))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Permission denied");
      return TRUE;
    }

  if (properties_variant)
    window_id_variant = g_variant_lookup_value (properties_variant,
                                                "window-id",
                                                G_VARIANT_TYPE ("t"));

  display = meta_get_display ();
  if (window_id_variant)
    {
      uint64_t window_id;

      g_variant_get (window_id_variant, "t", &window_id);
      window = meta_display_get_window_from_id (display, window_id);
    }
  else
    {
      window = meta_display_get_focus_window (display);
    }

  if (!window)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "Window not found");
      return TRUE;
    }

  if (!g_variant_lookup (properties_variant, "cursor-mode", "u", &cursor_mode))
    {
      cursor_mode = META_SCREEN_CAST_CURSOR_MODE_HIDDEN;
    }
  else
    {
      if (!is_valid_cursor_mode (cursor_mode))
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                                 G_DBUS_ERROR_FAILED,
                                                 "Unknown cursor mode");
          return TRUE;
        }
    }

  if (!g_variant_lookup (properties_variant, "is-recording", "b", &is_recording))
    is_recording = FALSE;

  interface_skeleton = G_DBUS_INTERFACE_SKELETON (skeleton);
  connection = g_dbus_interface_skeleton_get_connection (interface_skeleton);

  flags = META_SCREEN_CAST_FLAG_NONE;
  if (is_recording)
    flags |= META_SCREEN_CAST_FLAG_IS_RECORDING;

  window_stream = meta_screen_cast_window_stream_new (session,
                                                      connection,
                                                      window,
                                                      cursor_mode,
                                                      flags,
                                                      &error);
  if (!window_stream)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "Failed to record window: %s",
                                             error->message);
      g_error_free (error);
      return TRUE;
    }

  stream = META_SCREEN_CAST_STREAM (window_stream);
  stream_path = meta_screen_cast_stream_get_object_path (stream);

  session->streams = g_list_append (session->streams, stream);

  g_signal_connect (stream, "closed", G_CALLBACK (on_stream_closed), session);

  meta_dbus_screen_cast_session_complete_record_window (skeleton,
                                                        invocation,
                                                        stream_path);

  return TRUE;
}

static gboolean
handle_record_area (MetaDBusScreenCastSession *skeleton,
                    GDBusMethodInvocation     *invocation,
                    int                        x,
                    int                        y,
                    int                        width,
                    int                        height,
                    GVariant                  *properties_variant)
{
  MetaScreenCastSession *session = META_SCREEN_CAST_SESSION (skeleton);
  GDBusInterfaceSkeleton *interface_skeleton;
  GDBusConnection *connection;
  MetaBackend *backend;
  ClutterStage *stage;
  MetaScreenCastCursorMode cursor_mode;
  gboolean is_recording;
  MetaScreenCastFlag flags;
  g_autoptr (GError) error = NULL;
  MetaRectangle rect;
  MetaScreenCastAreaStream *area_stream;
  MetaScreenCastStream *stream;
  char *stream_path;

  if (!check_permission (session, invocation))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_ACCESS_DENIED,
                                             "Permission denied");
      return TRUE;
    }

  if (!g_variant_lookup (properties_variant, "cursor-mode", "u", &cursor_mode))
    {
      cursor_mode = META_SCREEN_CAST_CURSOR_MODE_HIDDEN;
    }
  else
    {
      if (!is_valid_cursor_mode (cursor_mode))
        {
          g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                                 G_DBUS_ERROR_FAILED,
                                                 "Unknown cursor mode");
          return TRUE;
        }
    }

  if (!g_variant_lookup (properties_variant, "is-recording", "b", &is_recording))
    is_recording = FALSE;

  interface_skeleton = G_DBUS_INTERFACE_SKELETON (skeleton);
  connection = g_dbus_interface_skeleton_get_connection (interface_skeleton);
  backend = meta_screen_cast_get_backend (session->screen_cast);
  stage = CLUTTER_STAGE (meta_backend_get_stage (backend));

  flags = META_SCREEN_CAST_FLAG_NONE;
  if (is_recording)
    flags |= META_SCREEN_CAST_FLAG_IS_RECORDING;

  rect = (MetaRectangle) {
    .x = x,
    .y = y,
    .width = width,
    .height = height
  };
  area_stream = meta_screen_cast_area_stream_new (session,
                                                  connection,
                                                  &rect,
                                                  stage,
                                                  cursor_mode,
                                                  flags,
                                                  &error);
  if (!area_stream)
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_FAILED,
                                             "Failed to record area: %s",
                                             error->message);
      return TRUE;
    }

  stream = META_SCREEN_CAST_STREAM (area_stream);
  stream_path = meta_screen_cast_stream_get_object_path (stream);

  session->streams = g_list_append (session->streams, stream);

  g_signal_connect (stream, "closed", G_CALLBACK (on_stream_closed), session);

  meta_dbus_screen_cast_session_complete_record_area (skeleton,
                                                      invocation,
                                                      stream_path);

  return TRUE;
}

static void
meta_screen_cast_session_init_iface (MetaDBusScreenCastSessionIface *iface)
{
  iface->handle_start = handle_start;
  iface->handle_stop = handle_stop;
  iface->handle_record_monitor = handle_record_monitor;
  iface->handle_record_window = handle_record_window;
  iface->handle_record_area = handle_record_area;
}

static void
meta_screen_cast_session_client_vanished (MetaDbusSession *dbus_session)
{
  meta_screen_cast_session_close (META_SCREEN_CAST_SESSION (dbus_session));
}

static void
meta_dbus_session_init_iface (MetaDbusSessionInterface *iface)
{
  iface->client_vanished = meta_screen_cast_session_client_vanished;
}

MetaScreenCastSession *
meta_screen_cast_session_new (MetaScreenCast             *screen_cast,
                              MetaScreenCastSessionType   session_type,
                              const char                 *peer_name,
                              GError                    **error)
{
  GDBusInterfaceSkeleton *interface_skeleton;
  MetaScreenCastSession *session;
  GDBusConnection *connection;
  static unsigned int global_session_number = 0;

  session = g_object_new (META_TYPE_SCREEN_CAST_SESSION, NULL);
  session->screen_cast = screen_cast;
  session->session_type = session_type;
  session->peer_name = g_strdup (peer_name);
  session->object_path =
    g_strdup_printf (META_SCREEN_CAST_SESSION_DBUS_PATH "/u%u",
                     ++global_session_number);

  interface_skeleton = G_DBUS_INTERFACE_SKELETON (session);
  connection = meta_screen_cast_get_connection (screen_cast);
  if (!g_dbus_interface_skeleton_export (interface_skeleton,
                                         connection,
                                         session->object_path,
                                         error))
    return NULL;

  return session;
}

static void
meta_screen_cast_session_finalize (GObject *object)
{
  MetaScreenCastSession *session = META_SCREEN_CAST_SESSION (object);

  g_clear_object (&session->handle);
  g_free (session->peer_name);
  g_free (session->object_path);

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

static void
meta_screen_cast_session_init (MetaScreenCastSession *session)
{
}

static void
meta_screen_cast_session_class_init (MetaScreenCastSessionClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->finalize = meta_screen_cast_session_finalize;
}

static gboolean
meta_screen_cast_session_is_recording (MetaScreenCastSession *session)
{
  GList *l;

  if (!session->streams)
    return FALSE;

  for (l = session->streams; l; l = l->next)
    {
      MetaScreenCastStream *stream = l->data;
      MetaScreenCastFlag flags;

      flags = meta_screen_cast_stream_get_flags (stream);
      if (!(flags & META_SCREEN_CAST_FLAG_IS_RECORDING))
        return FALSE;
    }

  return TRUE;
}

static MetaScreenCastSessionHandle *
meta_screen_cast_session_handle_new (MetaScreenCastSession *session)
{
  MetaScreenCastSessionHandle *handle;
  gboolean is_recording;

  is_recording = meta_screen_cast_session_is_recording (session);
  handle = g_object_new (META_TYPE_SCREEN_CAST_SESSION_HANDLE,
                         "is-recording", is_recording,
                         NULL);
  handle->session = session;

  return handle;
}

static void
meta_screen_cast_session_handle_stop (MetaRemoteAccessHandle *handle)
{
  MetaScreenCastSession *session;

  session = META_SCREEN_CAST_SESSION_HANDLE (handle)->session;
  if (!session)
    return;

  meta_screen_cast_session_close (session);
}

static void
meta_screen_cast_session_handle_init (MetaScreenCastSessionHandle *handle)
{
}

static void
meta_screen_cast_session_handle_class_init (MetaScreenCastSessionHandleClass *klass)
{
  MetaRemoteAccessHandleClass *remote_access_handle_class =
    META_REMOTE_ACCESS_HANDLE_CLASS (klass);

  remote_access_handle_class->stop = meta_screen_cast_session_handle_stop;
}
