// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "flutter/shell/platform/linux/fl_settings_portal.h"

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

static constexpr char kPortalName[] = "org.freedesktop.portal.Desktop";
static constexpr char kPortalPath[] = "/org/freedesktop/portal/desktop";
static constexpr char kPortalSettings[] = "org.freedesktop.portal.Settings";

struct FlSetting {
  const gchar* ns;
  const gchar* key;
  const GVariantType* type;
};

static constexpr char kXdgAppearance[] = "org.freedesktop.appearance";
static const FlSetting kColorScheme = {
    kXdgAppearance,
    "color-scheme",
    G_VARIANT_TYPE_UINT32,
};

static constexpr char kGnomeA11yInterface[] =
    "org.gnome.desktop.a11y.interface";
static const FlSetting kHighContrast = {
    kGnomeA11yInterface,
    "high-contrast",
    G_VARIANT_TYPE_BOOLEAN,
};

static constexpr char kGnomeDesktopInterface[] = "org.gnome.desktop.interface";
static const FlSetting kClockFormat = {
    kGnomeDesktopInterface,
    "clock-format",
    G_VARIANT_TYPE_STRING,
};
static const FlSetting kEnableAnimations = {
    kGnomeDesktopInterface,
    "enable-animations",
    G_VARIANT_TYPE_BOOLEAN,
};
static const FlSetting kGtkTheme = {
    kGnomeDesktopInterface,
    "gtk-theme",
    G_VARIANT_TYPE_STRING,
};
static const FlSetting kTextScalingFactor = {
    kGnomeDesktopInterface,
    "text-scaling-factor",
    G_VARIANT_TYPE_DOUBLE,
};

static const FlSetting kAllSettings[] = {
    kClockFormat, kColorScheme,  kEnableAnimations,
    kGtkTheme,    kHighContrast, kTextScalingFactor,
};

static constexpr char kClockFormat12Hour[] = "12h";
static constexpr char kGtkThemeDarkSuffix[] = "-dark";

typedef enum { kDefault, kPreferDark, kPreferLight } ColorScheme;

struct _FlSettingsPortal {
  GObject parent_instance;

  GDBusProxy* dbus_proxy;
  GVariantDict* values;
};

static void fl_settings_portal_iface_init(FlSettingsInterface* iface);

G_DEFINE_TYPE_WITH_CODE(FlSettingsPortal,
                        fl_settings_portal,
                        G_TYPE_OBJECT,
                        G_IMPLEMENT_INTERFACE(fl_settings_get_type(),
                                              fl_settings_portal_iface_init))

static gchar* format_key(const FlSetting* setting) {
  return g_strconcat(setting->ns, "::", setting->key, nullptr);
}

static gboolean get_value(FlSettingsPortal* portal,
                          const FlSetting* setting,
                          GVariant** value) {
  g_autofree const gchar* key = format_key(setting);
  *value = g_variant_dict_lookup_value(portal->values, key, setting->type);
  return *value != nullptr;
}

static void set_value(FlSettingsPortal* portal,
                      const FlSetting* setting,
                      GVariant* value) {
  g_autofree const gchar* key = format_key(setting);

  // ignore redundant changes from multiple XDG desktop portal backends
  g_autoptr(GVariant) old_value =
      g_variant_dict_lookup_value(portal->values, key, nullptr);
  if (old_value != nullptr && value != nullptr &&
      g_variant_equal(old_value, value)) {
    return;
  }

  g_variant_dict_insert_value(portal->values, key, value);
  fl_settings_emit_changed(FL_SETTINGS(portal));
}

// Based on
// https://gitlab.gnome.org/GNOME/Initiatives/-/wikis/Dark-Style-Preference#other
static gboolean settings_portal_read(GDBusProxy* proxy,
                                     const gchar* ns,
                                     const gchar* key,
                                     GVariant** out) {
  g_autoptr(GError) error = nullptr;
  g_autoptr(GVariant) value =
      g_dbus_proxy_call_sync(proxy, "Read", g_variant_new("(ss)", ns, key),
                             G_DBUS_CALL_FLAGS_NONE, G_MAXINT, nullptr, &error);

  if (error) {
    if (error->domain == G_DBUS_ERROR &&
        error->code == G_DBUS_ERROR_SERVICE_UNKNOWN) {
      g_debug("XDG desktop portal unavailable: %s", error->message);
      return false;
    }

    if (error->domain == G_DBUS_ERROR &&
        error->code == G_DBUS_ERROR_UNKNOWN_METHOD) {
      g_debug("XDG desktop portal settings unavailable: %s", error->message);
      return false;
    }

    g_critical("Failed to read XDG desktop portal settings: %s",
               error->message);
    return false;
  }

  g_autoptr(GVariant) child = nullptr;
  g_variant_get(value, "(v)", &child);
  g_variant_get(child, "v", out);

  return true;
}

static void settings_portal_changed_cb(GDBusProxy* proxy,
                                       const char* sender_name,
                                       const char* signal_name,
                                       GVariant* parameters,
                                       gpointer user_data) {
  FlSettingsPortal* portal = FL_SETTINGS_PORTAL(user_data);
  if (g_strcmp0(signal_name, "SettingChanged")) {
    return;
  }

  FlSetting setting;
  g_autoptr(GVariant) value = nullptr;
  g_variant_get(parameters, "(&s&sv)", &setting.ns, &setting.key, &value);
  set_value(portal, &setting, value);
}

static FlClockFormat fl_settings_portal_get_clock_format(FlSettings* settings) {
  FlSettingsPortal* self = FL_SETTINGS_PORTAL(settings);

  FlClockFormat clock_format = FL_CLOCK_FORMAT_24H;

  g_autoptr(GVariant) value = nullptr;
  if (get_value(self, &kClockFormat, &value)) {
    const gchar* clock_format_str = g_variant_get_string(value, nullptr);
    if (g_strcmp0(clock_format_str, kClockFormat12Hour) == 0) {
      clock_format = FL_CLOCK_FORMAT_12H;
    }
  }

  return clock_format;
}

static FlColorScheme fl_settings_portal_get_color_scheme(FlSettings* settings) {
  FlSettingsPortal* self = FL_SETTINGS_PORTAL(settings);

  FlColorScheme color_scheme = FL_COLOR_SCHEME_LIGHT;

  g_autoptr(GVariant) value = nullptr;
  if (get_value(self, &kColorScheme, &value)) {
    if (g_variant_get_uint32(value) == kPreferDark) {
      color_scheme = FL_COLOR_SCHEME_DARK;
    }
  } else if (get_value(self, &kGtkTheme, &value)) {
    const gchar* gtk_theme_str = g_variant_get_string(value, nullptr);
    if (g_str_has_suffix(gtk_theme_str, kGtkThemeDarkSuffix)) {
      color_scheme = FL_COLOR_SCHEME_DARK;
    }
  }

  return color_scheme;
}

static gboolean fl_settings_portal_get_enable_animations(FlSettings* settings) {
  FlSettingsPortal* self = FL_SETTINGS_PORTAL(settings);

  gboolean enable_animations = true;

  g_autoptr(GVariant) value = nullptr;
  if (get_value(self, &kEnableAnimations, &value)) {
    enable_animations = g_variant_get_boolean(value);
  }

  return enable_animations;
}

static gboolean fl_settings_portal_get_high_contrast(FlSettings* settings) {
  FlSettingsPortal* self = FL_SETTINGS_PORTAL(settings);

  gboolean high_contrast = false;

  g_autoptr(GVariant) value = nullptr;
  if (get_value(self, &kHighContrast, &value)) {
    high_contrast = g_variant_get_boolean(value);
  }

  return high_contrast;
}

static gdouble fl_settings_portal_get_text_scaling_factor(
    FlSettings* settings) {
  FlSettingsPortal* self = FL_SETTINGS_PORTAL(settings);

  gdouble scaling_factor = 1.0;

  g_autoptr(GVariant) value = nullptr;
  if (get_value(self, &kTextScalingFactor, &value)) {
    scaling_factor = g_variant_get_double(value);
  }

  return scaling_factor;
}

static void fl_settings_portal_dispose(GObject* object) {
  FlSettingsPortal* self = FL_SETTINGS_PORTAL(object);

  g_clear_object(&self->dbus_proxy);
  g_clear_pointer(&self->values, g_variant_dict_unref);

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

static void fl_settings_portal_class_init(FlSettingsPortalClass* klass) {
  GObjectClass* object_class = G_OBJECT_CLASS(klass);
  object_class->dispose = fl_settings_portal_dispose;
}

static void fl_settings_portal_iface_init(FlSettingsInterface* iface) {
  iface->get_clock_format = fl_settings_portal_get_clock_format;
  iface->get_color_scheme = fl_settings_portal_get_color_scheme;
  iface->get_enable_animations = fl_settings_portal_get_enable_animations;
  iface->get_high_contrast = fl_settings_portal_get_high_contrast;
  iface->get_text_scaling_factor = fl_settings_portal_get_text_scaling_factor;
}

static void fl_settings_portal_init(FlSettingsPortal* self) {}

FlSettingsPortal* fl_settings_portal_new() {
  g_autoptr(GVariantDict) values = g_variant_dict_new(nullptr);
  return fl_settings_portal_new_with_values(values);
}

FlSettingsPortal* fl_settings_portal_new_with_values(GVariantDict* values) {
  g_return_val_if_fail(values != nullptr, nullptr);
  FlSettingsPortal* portal =
      FL_SETTINGS_PORTAL(g_object_new(fl_settings_portal_get_type(), nullptr));
  portal->values = g_variant_dict_ref(values);
  return portal;
}

gboolean fl_settings_portal_start(FlSettingsPortal* self, GError** error) {
  g_return_val_if_fail(FL_IS_SETTINGS_PORTAL(self), false);
  g_return_val_if_fail(self->dbus_proxy == nullptr, false);

  self->dbus_proxy = g_dbus_proxy_new_for_bus_sync(
      G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, nullptr, kPortalName,
      kPortalPath, kPortalSettings, nullptr, error);

  if (self->dbus_proxy == nullptr) {
    return false;
  }

  for (const FlSetting setting : kAllSettings) {
    g_autoptr(GVariant) value = nullptr;
    if (settings_portal_read(self->dbus_proxy, setting.ns, setting.key,
                             &value)) {
      set_value(self, &setting, value);
    }
  }

  g_signal_connect_object(self->dbus_proxy, "g-signal",
                          G_CALLBACK(settings_portal_changed_cb), self,
                          static_cast<GConnectFlags>(0));

  return true;
}
