/*
 * Copyright © 2010 Codethink Limited
 * Copyright © 2013 Canonical Limited
 * Copyright © 2020 Emmanuel Gil Peyrot
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the licence, 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/>.
 *
 * Author: Ryan Lortie <desrt@desrt.ca>
 */

#include "config.h"

#include "gtkapplicationprivate.h"
#include "gtkapplicationwindowprivate.h"
#include "gtknative.h"
#include "gtkprivate.h"

#include <gdk/wayland/gdkwayland.h>
#include <gdk/wayland/gdktoplevel-wayland-private.h>
#include <gdk/wayland/gdkdisplay-wayland.h>

#include "a11y/gtkatspicontextprivate.h"
#include "gtkatcontextprivate.h"

typedef struct
{
  GtkApplicationImplDBusClass parent_class;
} GtkApplicationImplWaylandClass;

typedef struct
{
  guint cookie;
  guint dbus_cookie;
  GtkApplicationInhibitFlags flags;
  GdkSurface *surface;

} GtkApplicationWaylandInhibitor;

static void
gtk_application_wayland_inhibitor_free (GtkApplicationWaylandInhibitor *inhibitor)
{
  g_free (inhibitor);
}

typedef struct
{
  GtkApplicationImplDBus dbus;
  GSList *inhibitors;
  guint next_cookie;
  gboolean session_mgmt_supported;
} GtkApplicationImplWayland;

G_DEFINE_TYPE (GtkApplicationImplWayland, gtk_application_impl_wayland, GTK_TYPE_APPLICATION_IMPL_DBUS)

static void
restore_wayland_fallback_state (GtkWindow *window,
                                GVariant  *state)
{
  int width, height;
  gboolean is_maximized = FALSE;
  gboolean is_fullscreen = FALSE;

  GTK_DEBUG (SESSION, "No Wayland session management state found! Restoring fallback state");

  if (g_variant_lookup (state, "size", "(ii)", &width, &height))
    gtk_window_set_default_size (window, width, height);

  g_variant_lookup (state, "is-maximized", "b", &is_maximized);
  g_variant_lookup (state, "is-fullscreen", "b", &is_fullscreen);

  g_object_set (window,
                "maximized", is_maximized,
                "fullscreened", is_fullscreen,
                NULL);
}

static void
set_a11y_properties (GtkWindow *window)
{
  GdkSurface *gdk_surface;
  GtkATContext *at_context;

  gdk_surface = gtk_native_get_surface (GTK_NATIVE (window));

  at_context = gtk_accessible_get_at_context (GTK_ACCESSIBLE (window));

  if (GTK_IS_AT_SPI_CONTEXT (at_context))
    {
      const char *dbus_name = NULL, *object_path = NULL;
      GVariant *ref_data;

      gtk_at_context_realize (at_context);

      ref_data = gtk_at_spi_context_to_ref (GTK_AT_SPI_CONTEXT (at_context));

      g_variant_get (ref_data, "(&s&o)", &dbus_name, &object_path);

      gdk_wayland_toplevel_set_a11y_properties (GDK_TOPLEVEL (gdk_surface),
                                                dbus_name, object_path);
      g_variant_unref (ref_data);
    }

  g_clear_object (&at_context);
}

static void
gtk_application_impl_wayland_handle_window_realize (GtkApplicationImpl *impl,
                                                    GtkWindow          *window)
{
  GtkApplicationImplClass *impl_class =
    GTK_APPLICATION_IMPL_CLASS (gtk_application_impl_wayland_parent_class);
  GtkApplicationImplDBus *dbus = (GtkApplicationImplDBus *) impl;
  GdkSurface *gdk_surface;
  char *window_path;
  GVariant *state;
  char *id = NULL;
  GtkATContext *at_context = NULL;

  GTK_DEBUG (SESSION, "Handle window realize");

  gdk_surface = gtk_native_get_surface (GTK_NATIVE (window));

  if (!GDK_IS_WAYLAND_TOPLEVEL (gdk_surface))
    return;

  window_path = gtk_application_impl_dbus_get_window_path (dbus, window);

  gdk_wayland_toplevel_set_dbus_properties (GDK_TOPLEVEL (gdk_surface),
                                            dbus->application_id,
                                            dbus->app_menu_path,
                                            dbus->menubar_path,
                                            window_path,
                                            dbus->object_path,
                                            dbus->unique_name);

  g_free (window_path);

  state = gtk_application_impl_dbus_get_window_state (dbus, window);
  if (state)
    {
      if (g_variant_lookup (state, "session-id", "s", &id))
        GTK_DEBUG (SESSION, "Found saved session ID %s", id);
      else
        restore_wayland_fallback_state (window, state);
    }

  if (!id)
    {
      id = g_uuid_string_random ();
      GTK_DEBUG (SESSION, "No saved session ID, using %s", id);
    }

  GTK_DEBUG (SESSION, "Set Wayland toplevel session ID: %s", id);
  gdk_wayland_toplevel_set_session_id (GDK_TOPLEVEL (gdk_surface), id);
  gdk_wayland_toplevel_restore_from_session (GDK_TOPLEVEL (gdk_surface));
  g_free (id);

  at_context = gtk_accessible_get_at_context (GTK_ACCESSIBLE (window));
  if (at_context)
    {
      if (gtk_at_context_is_realized (at_context))
        set_a11y_properties (window);
      else
        g_signal_connect_object (at_context, "notify::realized", (GCallback) set_a11y_properties, window, G_CONNECT_SWAPPED);
      g_clear_object (&at_context);
    }

  impl_class->handle_window_realize (impl, window);
}

static void
gtk_application_impl_wayland_before_emit (GtkApplicationImpl *impl,
                                          GVariant           *platform_data)
{
  const char *startup_notification_id = NULL;

  g_variant_lookup (platform_data, "activation-token", "&s", &startup_notification_id);
  if (!startup_notification_id)
    g_variant_lookup (platform_data, "desktop-startup-id", "&s", &startup_notification_id);

G_GNUC_BEGIN_IGNORE_DEPRECATIONS
  gdk_wayland_display_set_startup_notification_id (gdk_display_get_default (), startup_notification_id);
G_GNUC_END_IGNORE_DEPRECATIONS
}

static void
gtk_application_impl_wayland_window_removed (GtkApplicationImpl *impl,
                                             GtkWindow          *window)
{
  GtkApplicationImplWayland *wayland = (GtkApplicationImplWayland *) impl;
  GSList *iter = wayland->inhibitors;

  GTK_APPLICATION_IMPL_CLASS (gtk_application_impl_wayland_parent_class)->window_removed (impl, window);

  while (iter)
    {
      GtkApplicationWaylandInhibitor *inhibitor = iter->data;
      GSList *next = iter->next;

      if (inhibitor->surface && inhibitor->surface == gtk_native_get_surface (GTK_NATIVE (window)))
        {
          inhibitor->surface = NULL;

          if (!inhibitor->dbus_cookie)
            {
              gtk_application_wayland_inhibitor_free (inhibitor);
              wayland->inhibitors = g_slist_delete_link (wayland->inhibitors, iter);
            }
        }

      iter = next;
    }
}

static void
gtk_application_impl_wayland_window_forget (GtkApplicationImpl *impl,
                                            GtkWindow          *window)
{
  GdkSurface *surface;

  GTK_APPLICATION_IMPL_CLASS (gtk_application_impl_wayland_parent_class)->window_forget (impl, window);

  surface = gtk_native_get_surface (GTK_NATIVE (window));
  if (GDK_IS_WAYLAND_TOPLEVEL (surface))
    {
      GTK_DEBUG (SESSION, "Forgetting saved toplevel: %s",
                 gdk_wayland_toplevel_get_session_id (GDK_TOPLEVEL (surface)));
      gdk_wayland_toplevel_remove_from_session (GDK_TOPLEVEL (surface));
    }
}

static void
gtk_application_impl_wayland_window_forget_by_state (GtkApplicationImpl *impl,
                                                     GVariant           *state)
{
  GdkDisplay *display = gdk_display_get_default ();
  char *id = NULL;

  GTK_APPLICATION_IMPL_CLASS (gtk_application_impl_wayland_parent_class)->window_forget_by_state (impl, state);

  if (g_variant_lookup (state, "session-id", "&s", &id))
    {
      GTK_DEBUG (SESSION, "Forgetting saved toplevel (by ID): %s", id);
      gdk_wayland_display_remove_session_toplevel (display, id);
    }
}

static void
gtk_application_impl_wayland_window_unforget (GtkApplicationImpl *impl,
                                              GtkWindow          *window)
{
  GdkSurface *surface;

  GTK_APPLICATION_IMPL_CLASS (gtk_application_impl_wayland_parent_class)->window_unforget (impl, window);

  surface = gtk_native_get_surface (GTK_NATIVE (window));
  GTK_DEBUG (SESSION, "Adding toplevel back to session: %s",
             gdk_wayland_toplevel_get_session_id (GDK_TOPLEVEL (surface)));
  gdk_wayland_toplevel_ensure_in_session (GDK_TOPLEVEL (surface));
}

static guint
gtk_application_impl_wayland_inhibit (GtkApplicationImpl         *impl,
                                      GtkWindow                  *window,
                                      GtkApplicationInhibitFlags  flags,
                                      const char                 *reason)
{
  GtkApplicationImplWayland *wayland = (GtkApplicationImplWayland *) impl;
  GdkSurface *surface;
  GtkApplicationWaylandInhibitor *inhibitor;
  gboolean success;

  if (!flags)
    return 0;

  inhibitor = g_new0 (GtkApplicationWaylandInhibitor, 1);
  inhibitor->cookie = ++wayland->next_cookie;
  inhibitor->flags = flags;
  wayland->inhibitors = g_slist_prepend (wayland->inhibitors, inhibitor);

  if (flags & GTK_APPLICATION_INHIBIT_IDLE && window && impl->application == gtk_window_get_application (window))
    {
      surface = gtk_native_get_surface (GTK_NATIVE (window));
      if (GDK_IS_WAYLAND_TOPLEVEL (surface))
        {
          success = gdk_wayland_toplevel_inhibit_idle (GDK_TOPLEVEL (surface));
          if (success)
            {
              flags &= ~GTK_APPLICATION_INHIBIT_IDLE;
              inhibitor->surface = surface;
            }
        }
    }

  if (flags)
    inhibitor->dbus_cookie = GTK_APPLICATION_IMPL_CLASS (gtk_application_impl_wayland_parent_class)->inhibit (impl, window, flags, reason);

  return inhibitor->cookie;
}

static void
gtk_application_impl_wayland_uninhibit (GtkApplicationImpl *impl,
                                        guint               cookie)
{
  GtkApplicationImplWayland *wayland = (GtkApplicationImplWayland *) impl;
  GSList *iter;

  for (iter = wayland->inhibitors; iter; iter = iter->next)
    {
      GtkApplicationWaylandInhibitor *inhibitor = iter->data;

      if (inhibitor->cookie == cookie)
        {
          if (inhibitor->dbus_cookie)
            GTK_APPLICATION_IMPL_CLASS (gtk_application_impl_wayland_parent_class)->uninhibit (impl, inhibitor->dbus_cookie);
          if (inhibitor->surface)
            gdk_wayland_toplevel_uninhibit_idle (GDK_TOPLEVEL (inhibitor->surface));
          gtk_application_wayland_inhibitor_free (inhibitor);
          wayland->inhibitors = g_slist_delete_link (wayland->inhibitors, iter);
          return;
        }
    }

  g_warning ("Invalid inhibitor cookie");
}

static void
gtk_application_impl_wayland_startup (GtkApplicationImpl *impl,
                                      gboolean            support_save)
{
  GtkApplicationImplWayland *wayland = (GtkApplicationImplWayland *) impl;
  GdkDisplay *display = gdk_display_get_default ();
  enum xdg_session_manager_v1_reason wl_reason;
  char *id = NULL;
  GVariant *state;

  GTK_APPLICATION_IMPL_CLASS (gtk_application_impl_wayland_parent_class)->startup (impl, support_save);

  if (!support_save)
    return;

  state = gtk_application_impl_retrieve_state (impl);
  if (state)
    {
      GVariant *global = g_variant_get_child_value (state, 0);
      g_variant_lookup (global, "wayland-session", "s", &id);
      g_clear_pointer (&global, g_variant_unref);
      g_clear_pointer (&state, g_variant_unref);
    }

  switch (wayland->dbus.reason)
    {
    case GTK_RESTORE_REASON_LAUNCH:
      wl_reason = XDG_SESSION_MANAGER_V1_REASON_LAUNCH;
      break;

    case GTK_RESTORE_REASON_RESTORE:
      wl_reason = XDG_SESSION_MANAGER_V1_REASON_SESSION_RESTORE;
      break;

    case GTK_RESTORE_REASON_RECOVER:
      wl_reason = XDG_SESSION_MANAGER_V1_REASON_RECOVER;
      break;

    case GTK_RESTORE_REASON_PRISTINE:
      wl_reason = XDG_SESSION_MANAGER_V1_REASON_LAUNCH;
      g_clear_pointer (&id, g_free);
      break;
    default:
      g_assert_not_reached ();
    }

  GTK_DEBUG (SESSION, "Wayland register session ID %s", id);
  wayland->session_mgmt_supported = gdk_wayland_display_register_session (display, wl_reason, id);
  g_free (id);
}

static void
collect_wayland_fallback_state (GtkWindow       *window,
                                GVariantBuilder *state)
{
  int width, height;
  gboolean is_maximized;
  gboolean is_fullscreen;

  GTK_DEBUG (SESSION, "Wayland compositor doesn't appear to support session management! Collecting fallback state");

  gtk_window_get_default_size (window, &width, &height);
  g_variant_builder_add (state, "{sv}", "size", g_variant_new ("(ii)", width, height));

  is_maximized = gtk_window_is_maximized (window);
  g_variant_builder_add (state, "{sv}", "is-maximized", g_variant_new_boolean (is_maximized));

  is_fullscreen = gtk_window_is_fullscreen (window);
  g_variant_builder_add (state, "{sv}", "is-fullscreen", g_variant_new_boolean (is_fullscreen));
}

static void
gtk_application_impl_wayland_collect_window_state (GtkApplicationImpl *impl,
                                                   GtkWindow          *window,
                                                   GVariantBuilder    *state)
{
  GtkApplicationImplWayland *wayland = (GtkApplicationImplWayland*) impl;
  GdkSurface *surface;
  const char *session_id;

  if (!wayland->session_mgmt_supported)
    {
      collect_wayland_fallback_state (window, state);
      return;
    }

  surface = gtk_native_get_surface (GTK_NATIVE (window));

  session_id = gdk_wayland_toplevel_get_session_id (GDK_TOPLEVEL (surface));
  if (session_id)
    g_variant_builder_add (state, "{sv}", "session-id", g_variant_new_string (session_id));
}

static void
gtk_application_impl_wayland_collect_global_state (GtkApplicationImpl *impl,
                                                   GVariantBuilder    *state)
{
  GdkDisplay *display = gdk_display_get_default ();
  const char *id;

  GTK_APPLICATION_IMPL_CLASS (gtk_application_impl_wayland_parent_class)->collect_global_state (impl, state);

  id = gdk_wayland_display_get_session_id (display);

  if (id)
    g_variant_builder_add (state, "{sv}", "wayland-session", g_variant_new_string (id));
}

static void
gtk_application_impl_wayland_init (GtkApplicationImplWayland *wayland)
{
}

static void
gtk_application_impl_wayland_class_init (GtkApplicationImplWaylandClass *class)
{
  GtkApplicationImplClass *impl_class = GTK_APPLICATION_IMPL_CLASS (class);

  impl_class->handle_window_realize = gtk_application_impl_wayland_handle_window_realize;
  impl_class->before_emit = gtk_application_impl_wayland_before_emit;
  impl_class->window_removed = gtk_application_impl_wayland_window_removed;
  impl_class->window_forget = gtk_application_impl_wayland_window_forget;
  impl_class->window_forget_by_state = gtk_application_impl_wayland_window_forget_by_state;
  impl_class->window_unforget = gtk_application_impl_wayland_window_unforget;
  impl_class->inhibit = gtk_application_impl_wayland_inhibit;
  impl_class->uninhibit = gtk_application_impl_wayland_uninhibit;
  impl_class->startup = gtk_application_impl_wayland_startup;
  impl_class->collect_window_state = gtk_application_impl_wayland_collect_window_state;
  impl_class->collect_global_state = gtk_application_impl_wayland_collect_global_state;
}
