/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* cc-mask-paintable.c
 *
 * Copyright (C) 2024 Alice Mikhaylenko <alicem@gnome.org>
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "cc-mask-paintable"

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <adwaita.h>

#include "cc-mask-paintable.h"
#include "cc-texture-utils.h"

struct _CcMaskPaintable
{
  GObject       parent_instance;

  GdkPaintable *paintable;
  GdkRGBA       rgba;
  GskMaskMode   mask_mode;

  gboolean      follow_accent;
  gboolean      updating_accent;

  gboolean      reloading_resource;
  char         *resource_path;
  GtkWidget    *parent_widget;
};

static void cc_mask_paintable_iface_init (GdkPaintableInterface *iface);

G_DEFINE_FINAL_TYPE_WITH_CODE (CcMaskPaintable, cc_mask_paintable, G_TYPE_OBJECT,
                               G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, cc_mask_paintable_iface_init))

enum
{
  PROP_0,
  PROP_PAINTABLE,
  PROP_RGBA,
  PROP_FOLLOW_ACCENT,
  N_PROPS,
};

static GParamSpec *props[N_PROPS];

static void
update_mask_color (CcMaskPaintable *self)
{
  AdwStyleManager *style_manager = adw_style_manager_get_default ();
  AdwAccentColor color;
  GdkRGBA rgba;

  color = adw_style_manager_get_accent_color (style_manager);
  adw_accent_color_to_rgba (color, &rgba);

  self->updating_accent = TRUE;
  cc_mask_paintable_set_rgba (self, &rgba);
  self->updating_accent = FALSE;
}

static void
reload_scalable_resource (CcMaskPaintable *self)
{
  g_autoptr (GdkPaintable) paintable = NULL;
  int scale;

  g_return_if_fail (self->parent_widget != NULL);

  scale = gtk_widget_get_scale_factor (GTK_WIDGET (self->parent_widget));
  paintable = cc_texture_new_from_resource_scaled (self->resource_path, scale);

  self->reloading_resource = TRUE;
  cc_mask_paintable_set_paintable (self, paintable);
  self->reloading_resource = FALSE;
}

static void
on_parent_widget_root_cb (CcMaskPaintable *self)
{
  g_return_if_fail (self->parent_widget != NULL);

  g_signal_handlers_disconnect_by_func (self->parent_widget, on_parent_widget_root_cb, self);

  g_signal_connect_swapped (self->parent_widget, "notify::scale-factor",
                            G_CALLBACK (reload_scalable_resource), self);

  reload_scalable_resource (self);
}

static void
clear_parent_widget (CcMaskPaintable *self)
{
  if (self->parent_widget)
    {
      g_signal_handlers_disconnect_by_func (self->parent_widget, on_parent_widget_root_cb, self);
      g_signal_handlers_disconnect_by_func (self->parent_widget, reload_scalable_resource, self);
    }

  g_clear_weak_pointer (&self->parent_widget);
}

static void
cc_mask_paintable_dispose (GObject *object)
{
  CcMaskPaintable *self = CC_MASK_PAINTABLE (object);

  clear_parent_widget (self);

  if (self->paintable)
    {
      g_signal_handlers_disconnect_by_func (self->paintable,
                                            gdk_paintable_invalidate_size,
                                            self);
      g_signal_handlers_disconnect_by_func (self->paintable,
                                            gdk_paintable_invalidate_contents,
                                            self);
    }

  g_clear_object (&self->paintable);

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

static void
cc_mask_paintable_get_property (GObject    *object,
                                guint       prop_id,
                                GValue     *value,
                                GParamSpec *pspec)
{
  CcMaskPaintable *self = CC_MASK_PAINTABLE (object);

  switch (prop_id)
    {
    case PROP_PAINTABLE:
      g_value_set_object (value, cc_mask_paintable_get_paintable (self));
      break;
    case PROP_RGBA:
      g_value_take_boxed (value, cc_mask_paintable_get_rgba (self));
      break;
    case PROP_FOLLOW_ACCENT:
      g_value_set_boolean (value, self->follow_accent);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
cc_mask_paintable_set_property (GObject      *object,
                                guint         prop_id,
                                const GValue *value,
                                GParamSpec   *pspec)
{
  CcMaskPaintable *self = CC_MASK_PAINTABLE (object);

  switch (prop_id)
    {
    case PROP_PAINTABLE:
      cc_mask_paintable_set_paintable (self, g_value_get_object (value));
      break;
    case PROP_RGBA:
      cc_mask_paintable_set_rgba (self, g_value_get_boxed (value));
      break;
    case PROP_FOLLOW_ACCENT:
      cc_mask_paintable_set_follow_accent (self, g_value_get_boolean (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
cc_mask_paintable_class_init (CcMaskPaintableClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = cc_mask_paintable_dispose;
  object_class->get_property = cc_mask_paintable_get_property;
  object_class->set_property = cc_mask_paintable_set_property;

  props[PROP_PAINTABLE] =
    g_param_spec_object ("paintable", NULL, NULL,
                         GDK_TYPE_PAINTABLE,
                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  props[PROP_RGBA] =
    g_param_spec_boxed ("rgba", NULL, NULL,
                        GDK_TYPE_RGBA,
                        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  props[PROP_FOLLOW_ACCENT] =
    g_param_spec_boolean ("follow-accent", NULL, NULL,
                          FALSE,
                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, N_PROPS, props);
}

static void
cc_mask_paintable_init (CcMaskPaintable *self)
{
  self->mask_mode = GSK_MASK_MODE_ALPHA;
}

static void
cc_mask_paintable_snapshot (GdkPaintable *paintable,
                            GdkSnapshot  *snapshot,
                            double        width,
                            double        height)
{
  CcMaskPaintable *self = CC_MASK_PAINTABLE (paintable);
  GtkSnapshot *inner_snapshot;
  g_autoptr (GskRenderNode) node = NULL;

  if (!self->paintable)
    return;

  inner_snapshot = gtk_snapshot_new ();
  gdk_paintable_snapshot (self->paintable, inner_snapshot, width, height);
  node = gtk_snapshot_free_to_node (inner_snapshot);

  if (!node)
    return;

  gtk_snapshot_push_mask (snapshot, self->mask_mode);

  gtk_snapshot_append_node (snapshot, node);
  gtk_snapshot_pop (snapshot);

  gtk_snapshot_append_color (snapshot,
                             &self->rgba,
                             &GRAPHENE_RECT_INIT (0, 0, width, height));
  gtk_snapshot_pop (snapshot);
}

static int
cc_mask_paintable_get_intrinsic_width (GdkPaintable *paintable)
{
  CcMaskPaintable *self = CC_MASK_PAINTABLE (paintable);

  if (self->paintable)
    return gdk_paintable_get_intrinsic_width (self->paintable);

  return 0;
}

static int
cc_mask_paintable_get_intrinsic_height (GdkPaintable *paintable)
{
  CcMaskPaintable *self = CC_MASK_PAINTABLE (paintable);

  if (self->paintable)
    return gdk_paintable_get_intrinsic_height (self->paintable);

  return 0;
}

static double
cc_mask_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable)
{
  CcMaskPaintable *self = CC_MASK_PAINTABLE (paintable);

  if (self->paintable)
    return gdk_paintable_get_intrinsic_aspect_ratio (self->paintable);

  return 0;
}

static void
cc_mask_paintable_iface_init (GdkPaintableInterface *iface)
{
  iface->snapshot = cc_mask_paintable_snapshot;
  iface->get_intrinsic_width = cc_mask_paintable_get_intrinsic_width;
  iface->get_intrinsic_height = cc_mask_paintable_get_intrinsic_height;
  iface->get_intrinsic_aspect_ratio = cc_mask_paintable_get_intrinsic_aspect_ratio;
}

GdkPaintable *
cc_mask_paintable_new (void)
{
  return g_object_new (CC_TYPE_MASK_PAINTABLE, NULL);
}

GdkPaintable *
cc_mask_paintable_get_paintable (CcMaskPaintable *self)
{
  g_return_val_if_fail (CC_IS_MASK_PAINTABLE (self), NULL);

  return self->paintable;
}

void
cc_mask_paintable_set_paintable (CcMaskPaintable *self,
                                 GdkPaintable    *paintable)
{
  g_return_if_fail (CC_IS_MASK_PAINTABLE (self));
  g_return_if_fail (GDK_IS_PAINTABLE (paintable));

  if (!self->reloading_resource)
    clear_parent_widget (self);

  if (self->paintable)
    {
      g_signal_handlers_disconnect_by_func (self->paintable,
                                            gdk_paintable_invalidate_size,
                                            self);
      g_signal_handlers_disconnect_by_func (self->paintable,
                                            gdk_paintable_invalidate_contents,
                                            self);
    }

  g_set_object (&self->paintable, paintable);

  if (self->paintable)
    {
      g_signal_connect_swapped (self->paintable, "invalidate-size",
                                G_CALLBACK (gdk_paintable_invalidate_size), self);
      g_signal_connect_swapped (self->paintable, "invalidate-contents",
                                G_CALLBACK (gdk_paintable_invalidate_contents), self);
    }

  gdk_paintable_invalidate_size (GDK_PAINTABLE (self));
  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));

  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PAINTABLE]);
}

GdkRGBA *
cc_mask_paintable_get_rgba (CcMaskPaintable *self)
{
  g_return_val_if_fail (CC_IS_MASK_PAINTABLE (self), NULL);

  return gdk_rgba_copy (&self->rgba);
}

void
cc_mask_paintable_set_rgba (CcMaskPaintable *self,
                            GdkRGBA         *rgba)
{
  g_return_if_fail (CC_IS_MASK_PAINTABLE (self));
  g_return_if_fail (rgba != NULL);

  if (self->follow_accent && !self->updating_accent)
    cc_mask_paintable_set_follow_accent (self, FALSE);

  self->rgba = *rgba;

  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));

  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_RGBA]);
}

gboolean
cc_mask_paintable_get_follow_accent (CcMaskPaintable *self)
{
  g_return_val_if_fail (CC_IS_MASK_PAINTABLE (self), FALSE);

  return self->follow_accent;
}

void
cc_mask_paintable_set_follow_accent (CcMaskPaintable *self,
                                     gboolean         follow_accent)
{
  AdwStyleManager *style_manager;

  g_return_if_fail (CC_IS_MASK_PAINTABLE (self));

  follow_accent = !!follow_accent;

  if (self->follow_accent == follow_accent)
    return;
  self->follow_accent = follow_accent;

  style_manager = adw_style_manager_get_default ();

  if (self->follow_accent)
    {
      g_signal_connect_object (style_manager, "notify::accent-color",
                               G_CALLBACK (update_mask_color), self,
                               G_CONNECT_SWAPPED);
      update_mask_color (self);
    }
  else
    {
      g_signal_handlers_disconnect_by_func (style_manager, update_mask_color, self);
    }

  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FOLLOW_ACCENT]);
}

void
cc_mask_paintable_set_resource_scaled (CcMaskPaintable *self,
                                       const char      *resource_path,
                                       GtkWidget       *parent_widget)
{
  gboolean resource_is_webm, resource_is_scalable;

  g_return_if_fail (CC_IS_MASK_PAINTABLE (self));
  g_return_if_fail (resource_path != NULL);
  g_return_if_fail (GTK_IS_WIDGET (parent_widget));

  clear_parent_widget (self);

  g_set_str (&self->resource_path, resource_path);

  /* FIXME: As long as VP9 alpha decoding is in gstreamer-plugins-bad,
   * we should probably use B&W assets and luminance masking
   * https://gitlab.gnome.org/GNOME/gnome-control-center/-/issues/3173 
   * https://gitlab.freedesktop.org/gstreamer/gstreamer/-/issues/3978 */
  resource_is_webm = g_str_has_suffix (self->resource_path, ".webm");
  if (resource_is_webm)
    self->mask_mode = GSK_MASK_MODE_LUMINANCE;
  else
    self->mask_mode = GSK_MASK_MODE_ALPHA;

  resource_is_scalable = g_str_has_suffix (self->resource_path, ".svg");

  if (!resource_is_scalable)
    {
      g_autoptr (GtkMediaStream) media_stream = NULL;

      media_stream = gtk_media_file_new_for_resource (self->resource_path);
      cc_mask_paintable_set_paintable (self, GDK_PAINTABLE (media_stream));

      return;
    }

  self->parent_widget = parent_widget;
  g_object_add_weak_pointer (G_OBJECT (self->parent_widget), (gpointer *) &self->parent_widget);

  /* We will wait until the parent widget is rooted, as otherwise the scale-factor can't
     be retrieved reliably. Doing a resource update with the wrong scale-factor can be
     costly, so it's worth the wait */
  if (gtk_widget_get_root (self->parent_widget))
    on_parent_widget_root_cb (self);
  else
    g_signal_connect_swapped (self->parent_widget, "notify::root",
                              G_CALLBACK (on_parent_widget_root_cb), self);
}
