/*
    This file is part of darktable,
    Copyright (C) 2015-2021 darktable developers.

    darktable 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.

    darktable 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 darktable.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "dtgtk/expander.h"
#include "common/darktable.h"
#include "control/conf.h"
#include "gui/gtk.h"
#include "libs/lib.h"

#include <gtk/gtk.h>

G_DEFINE_TYPE(GtkDarktableExpander, dtgtk_expander, GTK_TYPE_BOX);

static void dtgtk_expander_class_init(GtkDarktableExpanderClass *class)
{
}

GtkWidget *dtgtk_expander_get_frame(GtkDarktableExpander *expander)
{
  g_return_val_if_fail(DTGTK_IS_EXPANDER(expander), NULL);

  return gtk_bin_get_child(GTK_BIN(expander->frame));
}

GtkWidget *dtgtk_expander_get_header(GtkDarktableExpander *expander)
{
  g_return_val_if_fail(DTGTK_IS_EXPANDER(expander), NULL);

  return expander->header;
}

GtkWidget *dtgtk_expander_get_header_event_box(GtkDarktableExpander *expander)
{
  g_return_val_if_fail(DTGTK_IS_EXPANDER(expander), NULL);

  return expander->header_evb;
}

GtkWidget *dtgtk_expander_get_body(GtkDarktableExpander *expander)
{
  g_return_val_if_fail(DTGTK_IS_EXPANDER(expander), NULL);

  return expander->body;
}

GtkWidget *dtgtk_expander_get_body_event_box(GtkDarktableExpander *expander)
{
  g_return_val_if_fail(DTGTK_IS_EXPANDER(expander), NULL);

  return expander->body_evb;
}

static GtkWidget *_scroll_widget = NULL;
static GtkWidget *_last_expanded = NULL;
static GtkWidget *_drop_widget = NULL;
static GtkAllocation _start_pos = {0};

static void _set_last_expanded(GtkWidget *widget)
{
  if(_last_expanded)
    g_object_remove_weak_pointer(G_OBJECT(_last_expanded), (gpointer *)&_last_expanded);

  _last_expanded = widget;

  if(_last_expanded)
    g_object_add_weak_pointer(G_OBJECT(_last_expanded), (gpointer *)&_last_expanded);
}

void dtgtk_expander_set_expanded(GtkDarktableExpander *expander, gboolean expanded)
{
  g_return_if_fail(DTGTK_IS_EXPANDER(expander));

  expanded = expanded != FALSE;

  if(expander->expanded != expanded)
  {
    expander->expanded = expanded;

    if(expanded)
    {
      // expose expansion state to CSS so themes can style expanded modules
      dt_gui_add_class(GTK_WIDGET(expander), "dt_module_expanded");
      _set_last_expanded(GTK_WIDGET(expander));
      GtkWidget *sw = gtk_widget_get_ancestor(_last_expanded, GTK_TYPE_SCROLLED_WINDOW);
      if(sw)
      {
        gtk_widget_get_allocation(_last_expanded, &_start_pos);
        _start_pos.x = gtk_adjustment_get_value(gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(sw)));
      }
    }
    else
      dt_gui_remove_class(GTK_WIDGET(expander), "dt_module_expanded");

    GtkWidget *frame = expander->body;
    if(frame)
    {
      gtk_widget_set_visible(frame, TRUE); // for collapsible sections
      gtk_revealer_set_transition_duration(GTK_REVEALER(expander->frame), dt_conf_get_int("darkroom/ui/transition_duration"));
      gtk_revealer_set_reveal_child(GTK_REVEALER(expander->frame), expander->expanded);
    }
  }
  else if(expanded)
  {
    // The expander is already expanded. Still update scroll tracking
    // so that _expander_resize can scroll to this widget. This is
    // needed when navigating from the Quick Access Panel to a module
    // that was already expanded on the destination tab.
    _set_last_expanded(GTK_WIDGET(expander));
    GtkWidget *sw = gtk_widget_get_ancestor(GTK_WIDGET(expander), GTK_TYPE_SCROLLED_WINDOW);
    if(sw)
    {
      gtk_widget_get_allocation(GTK_WIDGET(expander), &_start_pos);
      _start_pos.x = gtk_adjustment_get_value(
        gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(sw)));
    }
    // Force a size-allocate so _expander_resize fires even if the
    // widget layout has not changed (e.g. module already visible and
    // expanded on the current tab).
    gtk_widget_queue_resize(GTK_WIDGET(expander));
  }
}

gboolean dtgtk_expander_get_expanded(GtkDarktableExpander *expander)
{
  g_return_val_if_fail(DTGTK_IS_EXPANDER(expander), FALSE);

  return expander->expanded;
}

static gboolean _expander_scroll(GtkWidget *widget, GdkFrameClock *frame_clock, gpointer user_data)
{
  GtkWidget *sw = gtk_widget_get_ancestor(widget, GTK_TYPE_SCROLLED_WINDOW);
  if(!sw) return G_SOURCE_REMOVE;

  GtkAllocation allocation, available;
  gtk_widget_get_allocation(widget, &allocation);
  gtk_widget_get_allocation(sw, &available);

  GtkAdjustment *adjustment = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(sw));
  gdouble value = gtk_adjustment_get_value(adjustment);

  GtkWidget *header = dtgtk_expander_get_header(DTGTK_EXPANDER(widget));
  const int drop_space = (widget == _drop_widget) && header ? gtk_widget_get_allocated_height(header) : 0;
  allocation.y -= drop_space;

  const gboolean is_iop = !g_strcmp0("iop-expander", gtk_widget_get_name(widget));

  // try not to get dragged upwards if a module above is collapsing
  if(is_iop
     && widget == _last_expanded
     && allocation.y < _start_pos.y)
  {
    const int offset = _start_pos.y - allocation.y - _start_pos.x + value;
    value -= offset;
  }
  // scroll up if more space is needed below
  // if "scroll_to_module" is enabled scroll up or down
  // but don't scroll if not the whole module can be shown
  float prop = 1.0f;
  const gboolean scroll_to_top = !_drop_widget &&
    dt_conf_get_bool(is_iop ? "darkroom/ui/scroll_to_module" : "lighttable/ui/scroll_to_module");

  const int spare = available.height - allocation.height - 2 * drop_space;
  const int from_top = allocation.y - value;
  const int move = MAX(scroll_to_top ? from_top : from_top - MAX(0, MIN(from_top, spare)),
                       - MAX(0, spare - from_top));
  if(move)
  {
    gint64 interval = 0;
    gdk_frame_clock_get_refresh_info(frame_clock, 0, &interval, NULL);
    const int remaining = GPOINTER_TO_INT(user_data) - gdk_frame_clock_get_frame_time(frame_clock);
    prop = (float)interval / MAX(interval, remaining);
    value += prop * move;
  }

  if(is_iop)
  {
    _start_pos = allocation;
    _start_pos.x = value;
  }
  gtk_adjustment_set_value(adjustment, value);

  if(prop != 1.0f) return G_SOURCE_CONTINUE;

  _scroll_widget = NULL;
  return G_SOURCE_REMOVE;
}

static void _expander_resize(GtkWidget *widget, GdkRectangle *allocation, gpointer user_data)
{
  // Already scrolling to this widget
  if(widget == _scroll_widget)
    return;

  // Handle drag-and-drop case
  if(_drop_widget)
  {
    if(widget != _drop_widget)
      return;
  }
  else
  {
    const gboolean is_lib_gui_module = darktable.lib->gui_module
      && darktable.lib->gui_module->expander == widget;
    const gboolean last_target_is_iop = _last_expanded
      && !g_strcmp0("iop-expander", gtk_widget_get_name(_last_expanded));

    if(_last_expanded)
    {
      // When _last_expanded is set (by dtgtk_expander_set_expanded),
      // only allow that specific widget (or lib gui_module) to
      // trigger scroll.  This prevents modules with stale
      // GTK_STATE_FLAG_SELECTED (left over from previous images or
      // sessions) from stealing the scroll target.
      // For IOP targets, do not allow lib gui_module to steal the
      // target on first allocations after image load.
      if(widget != _last_expanded
         && !(is_lib_gui_module && !last_target_is_iop))
        return;

      // Wait until the target widget has a valid layout (positive
      // height means it is mapped and sized).  This handles the case
      // where the module is on a different tab that hasn't been
      // shown yet.
      if(gtk_widget_get_allocated_height(widget) <= 0)
        return;

      // Clear _last_expanded so that, once the scroll animation
      // finishes and _scroll_widget becomes NULL, subsequent
      // size-allocate events do not re-trigger scrolling.
      _set_last_expanded(NULL);
    }
    else
    {
      const gboolean height_changed =
        gtk_widget_get_allocated_height(widget) != _start_pos.height;

      if(!height_changed)
        return;

      const gboolean frame_selected =
        gtk_widget_get_state_flags(user_data) & GTK_STATE_FLAG_SELECTED;

      if(!frame_selected && !is_lib_gui_module)
        return;
    }
  }

  _scroll_widget = widget;
  GdkFrameClock *clock = gtk_widget_get_frame_clock(widget);
  if(clock)
    gtk_widget_add_tick_callback(widget, _expander_scroll,
                                GINT_TO_POINTER(gdk_frame_clock_get_frame_time(clock)
                                + dt_conf_get_int("darkroom/ui/transition_duration") * 1000), NULL);
}

void dtgtk_expander_set_drag_hover(GtkDarktableExpander *expander, gboolean allow, gboolean below, guint time)
{
  GtkWidget *widget = expander ? GTK_WIDGET(expander) : _drop_widget;
  // don't remove drop zone when switching between last expander and empty space to avoid jitter
  static guint last_time;
  if(!widget || (!allow && !below && widget == _drop_widget && time == last_time)) return;

  dt_gui_remove_class(widget, "module_drop_after");
  dt_gui_remove_class(widget, "module_drop_before");

  if(allow || below)
  {
    _drop_widget = widget;
    _set_last_expanded(NULL);
    last_time = time;

    if(!allow)
      gtk_widget_queue_resize(widget);
    else if(below)
      dt_gui_add_class(widget, "module_drop_before");
    else
      dt_gui_add_class(widget, "module_drop_after");
  }
}

static void _expander_drag_leave(GtkDarktableExpander *widget,
                           GdkDragContext *dc,
                           guint time,
                           gpointer user_data)
{
  dtgtk_expander_set_drag_hover(widget, FALSE, FALSE, time);
}

// FIXME: default highlight for the dnd is barely visible
// it should be possible to configure it
static void _expander_drag_begin(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
{
  GtkAllocation allocation = {0};
  gtk_widget_get_allocation(widget, &allocation);
  // method from https://blog.gtk.org/2017/04/23/drag-and-drop-in-lists/
  cairo_surface_t *surface = dt_cairo_image_surface_create(CAIRO_FORMAT_RGB24, allocation.width, allocation.height);
  cairo_t *cr = cairo_create(surface);

  // hack to render not transparent
  dt_gui_add_class(widget, "module_drag_icon");
  gtk_widget_size_allocate(widget, &allocation);
  gtk_widget_draw(widget, cr);
  dt_gui_remove_class(widget, "module_drag_icon");

  int pointerx, pointery;
  gdk_window_get_device_position(gtk_widget_get_window(widget),
      gdk_seat_get_pointer(gdk_display_get_default_seat(gtk_widget_get_display(widget))),
      &pointerx, &pointery, NULL);
  cairo_surface_set_device_offset(surface, -pointerx, -CLAMP(pointery, 0, allocation.height));
  gtk_drag_set_icon_surface(context, surface);

  cairo_destroy(cr);
  cairo_surface_destroy(surface);

  gtk_widget_set_opacity(widget, 0.5);
}

static void _expander_drag_end(GtkWidget *widget, GdkDragContext *context, gpointer user_data)
{
  dtgtk_expander_set_drag_hover(NULL, FALSE, FALSE, 0);
  _drop_widget = NULL;
  gtk_widget_set_opacity(widget, 1.0);
}

static void dtgtk_expander_init(GtkDarktableExpander *expander)
{
}

// public functions
GtkWidget *dtgtk_expander_new(GtkWidget *header, GtkWidget *body)
{
  GtkDarktableExpander *expander;

  g_return_val_if_fail(GTK_IS_WIDGET(header), NULL);

  expander
      = g_object_new(dtgtk_expander_get_type(), "orientation", GTK_ORIENTATION_VERTICAL, "spacing", 0, NULL);
  expander->expanded = TRUE;
  dt_gui_add_class(GTK_WIDGET(expander), "dt_module_expanded");
  expander->header = header;
  expander->body = body;

  expander->header_evb = gtk_event_box_new();
  gtk_container_add(GTK_CONTAINER(expander->header_evb), expander->header);
  expander->body_evb = gtk_event_box_new();
  if(expander->body)
    gtk_container_add(GTK_CONTAINER(expander->body_evb), expander->body);
  GtkWidget *frame = gtk_frame_new(NULL);
  gtk_container_add(GTK_CONTAINER(frame), expander->body_evb);
  expander->frame = gtk_revealer_new();
  gtk_revealer_set_transition_duration(GTK_REVEALER(expander->frame), 0);
  gtk_revealer_set_reveal_child(GTK_REVEALER(expander->frame), TRUE);
  gtk_container_add(GTK_CONTAINER(expander->frame), frame);

  dt_gui_box_add(expander, expander->header_evb, expander->frame);

  g_signal_connect(expander->header_evb, "drag-begin", G_CALLBACK(_expander_drag_begin), NULL);
  g_signal_connect(expander->header_evb, "drag-end", G_CALLBACK(_expander_drag_end), NULL);
  g_signal_connect(expander, "drag-leave", G_CALLBACK(_expander_drag_leave), NULL);
  g_signal_connect(expander, "size-allocate", G_CALLBACK(_expander_resize), frame);

  return GTK_WIDGET(expander);
}

// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
// clang-format on
