/* bz-fading-clamp.c
 *
 * Copyright 2025 Alexander Vanhee
 *
 * 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 <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#include "bz-fading-clamp.h"

#define FADE_HEIGHT 75
#define CLAMP_LEEWAY 100

struct _BzFadingClamp
{
  GtkWidget     parent_instance;
  GtkWidget    *child;
  int           max_height;
  int           min_max_height;
  AdwAnimation *animation;
  int           current_height;
  int           allocated_width;
  gboolean      animating_max_height;
  gboolean      will_change;
};

enum
{
  PROP_0,
  PROP_CHILD,
  PROP_MAX_HEIGHT,
  PROP_MIN_MAX_HEIGHT,
  PROP_WILL_CHANGE,
  N_PROPS
};

static GParamSpec *properties[N_PROPS];

G_DEFINE_TYPE (BzFadingClamp, bz_fading_clamp, GTK_TYPE_WIDGET)

static void
on_animation_value_changed (double value, BzFadingClamp *self)
{
  self->current_height = (int) value;
  gtk_widget_queue_resize (GTK_WIDGET (self));
}

static void
on_animation_done (AdwAnimation *animation, BzFadingClamp *self)
{
  self->animating_max_height = FALSE;
}

static void
bz_fading_clamp_update_will_change (BzFadingClamp *self)
{
  gboolean new_value = FALSE;
  int      natural_height;
  int      width;

  if (self->child)
    {
      if (self->allocated_width > 0)
        width = self->allocated_width;
      else
        gtk_widget_measure (self->child, GTK_ORIENTATION_HORIZONTAL, -1, NULL, &width, NULL, NULL);

      gtk_widget_measure (self->child, GTK_ORIENTATION_VERTICAL, width, NULL, &natural_height, NULL, NULL);
      new_value = natural_height > self->min_max_height + CLAMP_LEEWAY;
    }

  if (self->will_change != new_value)
    {
      self->will_change = new_value;
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_WILL_CHANGE]);
    }
}

static void
bz_fading_clamp_dispose (GObject *object)
{
  BzFadingClamp *self = BZ_FADING_CLAMP (object);
  g_clear_pointer (&self->child, gtk_widget_unparent);
  g_clear_object (&self->animation);
  G_OBJECT_CLASS (bz_fading_clamp_parent_class)->dispose (object);
}

static void
bz_fading_clamp_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
{
  BzFadingClamp *self = BZ_FADING_CLAMP (object);
  switch (prop_id)
    {
    case PROP_CHILD:
      g_value_set_object (value, self->child);
      break;
    case PROP_MAX_HEIGHT:
      g_value_set_int (value, self->max_height);
      break;
    case PROP_MIN_MAX_HEIGHT:
      g_value_set_int (value, self->min_max_height);
      break;
    case PROP_WILL_CHANGE:
      g_value_set_boolean (value, self->will_change);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
bz_fading_clamp_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
{
  BzFadingClamp *self = BZ_FADING_CLAMP (object);
  switch (prop_id)
    {
    case PROP_CHILD:
      bz_fading_clamp_set_child (self, g_value_get_object (value));
      break;
    case PROP_MAX_HEIGHT:
      bz_fading_clamp_set_max_height (self, g_value_get_int (value));
      break;
    case PROP_MIN_MAX_HEIGHT:
      self->min_max_height = g_value_get_int (value);
      bz_fading_clamp_update_will_change (self);
      gtk_widget_queue_resize (GTK_WIDGET (self));
      g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MIN_MAX_HEIGHT]);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static GtkSizeRequestMode
bz_fading_clamp_get_request_mode (GtkWidget *widget)
{
  return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
}

static void
bz_fading_clamp_measure (GtkWidget *widget, GtkOrientation orientation, int for_size, int *minimum, int *natural, int *minimum_baseline, int *natural_baseline)
{
  int            target_height;
  BzFadingClamp *self = BZ_FADING_CLAMP (widget);

  if (!self->child)
    {
      *minimum = 0;
      *natural = 0;
      return;
    }

  if (orientation == GTK_ORIENTATION_HORIZONTAL)
    {
      gtk_widget_measure (self->child, orientation, for_size, minimum, natural, minimum_baseline, natural_baseline);
    }
  else
    {
      gtk_widget_measure (self->child, GTK_ORIENTATION_VERTICAL, for_size, minimum, natural, minimum_baseline, natural_baseline);

      if (*natural <= self->max_height + CLAMP_LEEWAY)
        target_height = *natural;
      else
        target_height = self->max_height;

      bz_fading_clamp_update_will_change (self);

      if (!self->animating_max_height && target_height != self->current_height)
        {
          self->current_height = target_height;
        }

      *minimum = MIN (*minimum, self->current_height);
      *natural = MIN (*natural, self->current_height);
    }
}

static void
bz_fading_clamp_size_allocate (GtkWidget *widget, int width, int height, int baseline)
{
  BzFadingClamp *self = BZ_FADING_CLAMP (widget);

  if (self->allocated_width != width)
    {
      self->allocated_width = width;
      bz_fading_clamp_update_will_change (self);
    }

  if (self->child)
    {
      int child_height = height;
      int natural_height;
      gtk_widget_measure (self->child, GTK_ORIENTATION_VERTICAL, width, NULL, &natural_height, NULL, NULL);
      if (natural_height > height)
        child_height = natural_height;
      gtk_widget_allocate (self->child, width, child_height, baseline, NULL);
    }
}

static void
bz_fading_clamp_snapshot (GtkWidget *widget, GtkSnapshot *snapshot)
{
  BzFadingClamp   *self = BZ_FADING_CLAMP (widget);
  int              width, height, natural_height;
  int              gradient_start;
  graphene_rect_t  gradient_rect;
  GskColorStop     stops[2];
  graphene_point_t start_point, end_point;
  int              effective_fade_height;
  float            stop_offset;

  if (!self->child || !gtk_widget_get_visible (self->child))
    return;

  width  = gtk_widget_get_width (widget);
  height = gtk_widget_get_height (widget);

  if (height <= 0)
    return;

  gtk_widget_measure (self->child, GTK_ORIENTATION_VERTICAL, width, NULL, &natural_height, NULL, NULL);

  if (natural_height <= height)
    {
      gtk_widget_snapshot_child (widget, self->child, snapshot);
      return;
    }

  effective_fade_height = MIN (natural_height - height, FADE_HEIGHT);

  gtk_snapshot_push_mask (snapshot, GSK_MASK_MODE_ALPHA);

  gradient_start = height - effective_fade_height;
  graphene_rect_init (&gradient_rect, 0, 0, width, height);

  stop_offset = CLAMP ((float) gradient_start / height, 0.0f, 1.0f);

  stops[0] = (GskColorStop) {
    stop_offset, { 1, 1, 1, 1 }
  };
  stops[1] = (GskColorStop) {
    1.0, { 1, 1, 1, 0 }
  };

  graphene_point_init (&start_point, 0, 0);
  graphene_point_init (&end_point, 0, height);
  gtk_snapshot_append_linear_gradient (snapshot, &gradient_rect, &start_point, &end_point, stops, 2);

  gtk_snapshot_pop (snapshot);

  gtk_widget_snapshot_child (widget, self->child, snapshot);

  gtk_snapshot_pop (snapshot);
}

static void
bz_fading_clamp_class_init (BzFadingClampClass *klass)
{
  GObjectClass   *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->dispose          = bz_fading_clamp_dispose;
  object_class->get_property     = bz_fading_clamp_get_property;
  object_class->set_property     = bz_fading_clamp_set_property;
  widget_class->get_request_mode = bz_fading_clamp_get_request_mode;
  widget_class->measure          = bz_fading_clamp_measure;
  widget_class->size_allocate    = bz_fading_clamp_size_allocate;
  widget_class->snapshot         = bz_fading_clamp_snapshot;

  properties[PROP_CHILD] =
      g_param_spec_object ("child", NULL, NULL, GTK_TYPE_WIDGET,
                           G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
  properties[PROP_MAX_HEIGHT] =
      g_param_spec_int ("max-height", NULL, NULL, 0, G_MAXINT, 300,
                        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
  properties[PROP_MIN_MAX_HEIGHT] =
      g_param_spec_int ("min-max-height", NULL, NULL, 0, G_MAXINT, 150,
                        G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
  properties[PROP_WILL_CHANGE] =
      g_param_spec_boolean ("will-change", NULL, NULL, FALSE,
                            G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);

  g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
bz_fading_clamp_init (BzFadingClamp *self)
{
  AdwAnimationTarget *target;

  self->max_height           = 300;
  self->min_max_height       = 150;
  self->current_height       = 0;
  self->allocated_width      = 0;
  self->animating_max_height = FALSE;
  self->will_change          = FALSE;

  target          = adw_callback_animation_target_new ((AdwAnimationTargetFunc) on_animation_value_changed, self, NULL);
  self->animation = adw_timed_animation_new (GTK_WIDGET (self), 0, 300, 250, target);
  g_signal_connect (self->animation, "done", G_CALLBACK (on_animation_done), self);
}

GtkWidget *
bz_fading_clamp_new (void)
{
  return g_object_new (BZ_TYPE_FADING_CLAMP, NULL);
}

void
bz_fading_clamp_set_child (BzFadingClamp *self, GtkWidget *child)
{
  g_return_if_fail (BZ_IS_FADING_CLAMP (self));
  g_return_if_fail (child == NULL || GTK_IS_WIDGET (child));

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

  g_clear_pointer (&self->child, gtk_widget_unparent);

  if (child)
    {
      self->child = child;
      gtk_widget_set_parent (child, GTK_WIDGET (self));
      self->current_height = 0;
    }

  bz_fading_clamp_update_will_change (self);
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_CHILD]);
  gtk_widget_queue_resize (GTK_WIDGET (self));
}

GtkWidget *
bz_fading_clamp_get_child (BzFadingClamp *self)
{
  g_return_val_if_fail (BZ_IS_FADING_CLAMP (self), NULL);
  return self->child;
}

void
bz_fading_clamp_set_max_height (BzFadingClamp *self, int max_height)
{
  int natural_height, target_height;
  int width;

  g_return_if_fail (BZ_IS_FADING_CLAMP (self));

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

  self->max_height = max_height;

  if (self->child)
    {
      if (self->allocated_width > 0)
        width = self->allocated_width;
      else
        gtk_widget_measure (self->child, GTK_ORIENTATION_HORIZONTAL, -1, NULL, &width, NULL, NULL);

      gtk_widget_measure (self->child, GTK_ORIENTATION_VERTICAL, width, NULL, &natural_height, NULL, NULL);

      if (natural_height <= max_height + CLAMP_LEEWAY)
        target_height = natural_height;
      else
        target_height = max_height;
    }
  else
    {
      target_height = max_height;
    }

  self->animating_max_height = TRUE;
  adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (self->animation), self->current_height);
  adw_timed_animation_set_value_to (ADW_TIMED_ANIMATION (self->animation), target_height);
  adw_animation_play (self->animation);

  bz_fading_clamp_update_will_change (self);
  g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MAX_HEIGHT]);
}

int
bz_fading_clamp_get_max_height (BzFadingClamp *self)
{
  g_return_val_if_fail (BZ_IS_FADING_CLAMP (self), 0);
  return self->max_height;
}
