/*
    This file is part of darktable,
    Copyright (C) 2011-2026 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 "common/darktable.h"
#include "common/color_picker.h"
#include "gui/accelerators.h"
#include "scopes.h"
#include "scopes/vectorscope.h"

// FIXME: is this used?
#ifdef GDK_WINDOWING_QUARTZ
#include "osx/osx.h"
#endif

DT_MODULE(1)

static void _lib_histogram_get_harmony(dt_lib_module_t *self,
                                       dt_color_harmony_guide_t *guide);
static void _lib_histogram_set_harmony(dt_lib_module_t *self,
                                       const dt_color_harmony_guide_t *guide);
static void _lib_histogram_set_scope(dt_lib_module_t *self, int scope);
static void _lib_histogram_set_type(dt_lib_module_t *self, int type);
static void _lib_histogram_set_harmony_callback(dt_lib_module_t *self,
    void (*cb)(const dt_color_harmony_guide_t *, void *), void *user_data);
static void _lib_histogram_get_sector_angles(dt_lib_module_t *self,
                                             const dt_color_harmony_type_t type,
                                             const int rotation,
                                             float *angles,
                                             int *n);

static const gchar *rgb_names[DT_SCOPES_RGB_N] =
  { N_("red"),
    N_("green"),
    N_("blue")
  };

const char *name(dt_lib_module_t *self)
{
  return _("scopes");
}

dt_view_type_flags_t views(dt_lib_module_t *self)
{
  return DT_VIEW_DARKROOM | DT_VIEW_TETHERING;
}

uint32_t container(dt_lib_module_t *self)
{
  return g_strcmp0
    (dt_conf_get_string_const("plugins/darkroom/histogram/panel_position"), "right")
    ? DT_UI_CONTAINER_PANEL_LEFT_TOP
    : DT_UI_CONTAINER_PANEL_RIGHT_TOP;
}

gboolean expandable(dt_lib_module_t *self)
{
  return FALSE;
}

int position(const dt_lib_module_t *self)
{
  return 1000;
}

static void _scope_process
  (struct dt_lib_module_t *self,
   const float *const input,
   int width,
   int height,
   const dt_iop_order_iccprofile_info_t *const profile_info_from,
   const dt_iop_order_iccprofile_info_t *const profile_info_to)
{
  dt_times_t start;
  dt_get_perf_times(&start);

  dt_scopes_t *const s = self->data;

  // special case, clear the scopes
  if(!input)
  {
    dt_pthread_mutex_lock(&s->lock);
    // FIXME: is better to do this or just advance update_counter by one?
    for(dt_scopes_mode_type_t i = 0; i < DT_SCOPES_MODE_N; i++)
      dt_scopes_call_if_exists(&s->modes[i], clear);
    dt_pthread_mutex_unlock(&s->lock);
    return;
  }

  // FIXME: scope goes black when click histogram lib colorpicker on
  // -- is this meant to happen?
  //
  // FIXME: scope doesn't redraw when click histogram lib colorpicker
  // off -- is this meant to happen?
  dt_histogram_roi_t roi = { .width = width,
                             .height = height,
                             .crop_x = 0,
                             .crop_y = 0,
                             .crop_right = 0,
                             .crop_bottom = 0 };

  // Constraining the area if the colorpicker is active in area mode
  //
  // FIXME: only need to do colorspace conversion below on roi
  //
  // FIXME: if the only time we use roi in histogram to limit area is
  // here, and whenever we use tether there is no colorpicker (true?),
  // and if we're always doing a colorspace transform in darkroom and
  // clip to roi during conversion, then can get rid of all roi code
  // for common/histogram?  when darkroom colorpicker is active,
  // gui_module is set to colorout
  if(dt_view_get_current() == DT_VIEW_DARKROOM
     && darktable.lib->proxy.colorpicker.restrict_histogram)
  {
    const dt_colorpicker_sample_t *const sample =
      darktable.lib->proxy.colorpicker.primary_sample;
    const dt_iop_color_picker_t *proxy = darktable.lib->proxy.colorpicker.picker_proxy;
    if(proxy && !proxy->module)
    {
      // FIXME: for histogram process whole image, then pull point
      // sample #'s from primary_picker->scope_mean (point) or _mean,
      // _min, _max (as in rgb curve) and draw them as an overlay
      //
      // FIXME: for waveform point sample, could process whole image,
      // then do an overlay of the point sample from
      // primary_picker->scope_mean as red/green/blue dots (or short
      // lines) at appropriate position at the horizontal/vertical
      // position of sample
      dt_boundingbox_t pos;
      const gboolean isbox = sample->size == DT_LIB_COLORPICKER_SIZE_BOX;
      const gboolean ispoint = sample->size == DT_LIB_COLORPICKER_SIZE_POINT;
      if(ispoint || isbox)
      {
        dt_color_picker_transform_box(darktable.develop,
                                     isbox ? 2 : 1,
                                     isbox ? sample->box : sample->point,
                                     pos, TRUE);
        roi.crop_x = MIN(width, MAX(0, pos[0] * width));
        roi.crop_y = MIN(height, MAX(0, pos[1] * height));
        roi.crop_right = width -    MIN(width,  MAX(0, (isbox ? pos[2] : pos[0]) * width));
        roi.crop_bottom = height -  MIN(height, MAX(0, (isbox ? pos[3] : pos[1]) * height));
      }
    }
  }

  // Convert pixelpipe output in display RGB to histogram profile. If
  // in tether view, then the image is already converted by the
  // caller.

  float *img_display = dt_alloc_align_float((size_t)4 * width * height);
  if(!img_display) return;

  // FIXME: we might get called with profile_info_to == NULL due to caller errors
  if(!profile_info_to)
  {
    dt_print(DT_DEBUG_ALWAYS,
       "[histogram] no histogram profile, replaced with linear Rec2020");
    dt_control_log(_("unsupported profile selected for histogram,"
                     " it will be replaced with linear Rec2020"));
  }

  const dt_iop_order_iccprofile_info_t *fallback =
    dt_ioppr_add_profile_info_to_list(darktable.develop,
      DT_COLORSPACE_LIN_REC2020, "", DT_INTENT_RELATIVE_COLORIMETRIC);

  const dt_iop_order_iccprofile_info_t *profile_info_out = !profile_info_to ? fallback : profile_info_to;

  dt_ioppr_transform_image_colorspace_rgb(input, img_display, width, height,
                                            profile_info_from, profile_info_out,
                                            "final histogram");
  dt_pthread_mutex_lock(&s->lock);

  s->update_counter++;
  // if using a non-rgb profile_info_out as in cmyk softproofing we pass
  // DT_COLORSPACE_LIN_REC2020 for calculating the vertex_rgb data.
  dt_scopes_call(s->cur_mode, process, img_display, &roi,
                 profile_info_out->type ? profile_info_out : fallback);

  dt_pthread_mutex_unlock(&s->lock);
  dt_free_align(img_display);

  dt_show_times_f(&start, "[histogram]", "final %s",
                  dt_scopes_call(s->cur_mode, name));
}


// FIXME: make this default in _drawable_draw_callback() and only if there is a
// draw_bkgd method do somethign else?
void lib_histogram_draw_bkgd(const dt_scopes_mode_t *const self,
                             cairo_t *cr,
                             const int width,
                             const int height)
{
  cairo_save(cr);
  cairo_rectangle(cr, 0, 0, width, height);
  set_color(cr, darktable.bauhaus->graph_bg);
  cairo_fill(cr);
  cairo_restore(cr);
}

// FIXME: have different drawable for each scope in a stack --
// simplifies this function from being a swath of conditionals -- then
// essentially draw callbacks _lib_histogram_draw_waveform,
// _lib_histogram_draw_rgb_parade, etc.
//
// FIXME: if exposure change regions are separate widgets, then we
// could have a menu to swap in different overlay widgets (sort of
// like basic adjustments) to adjust other things about the image,
// e.g. tone equalizer, color balance, etc.
static gboolean _drawable_draw_callback(GtkWidget *widget,
                                        cairo_t *crf,
                                        dt_scopes_t *s)
{
  dt_times_t start;
  dt_get_perf_times(&start);

  dt_scopes_mode_t *const cur_mode = s->cur_mode;
  const dt_develop_t *const dev = darktable.develop;

  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  const int width = allocation.width, height = allocation.height;

  cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
  cairo_t *cr = cairo_create(cst);

  gtk_render_background(gtk_widget_get_style_context(widget), cr, 0, 0, width, height);
  cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(.5)); // borders width

  dt_scopes_call_if_exists(cur_mode, draw_bkgd, cr, width, height);

  // exposure change regions
  // FIXME: should draw these if there is no data currently for this scope?
  set_color(cr, darktable.bauhaus->graph_overlay);
  dt_scopes_call_if_exists(cur_mode, draw_highlight,
                           cr, s->highlight, width, height);

  // draw grid
  // FIXME: set this in individual draw code
  set_color(cr, darktable.bauhaus->graph_grid);
  dt_scopes_call_if_exists(cur_mode, draw_grid, cr, width, height);

  // FIXME: should set histogram buffer to black if have just entered
  // tether view and nothing is displayed
  dt_pthread_mutex_lock(&s->lock);
  // darkroom view: draw scope so long as preview pipe is finished
  // tether view: draw whatever has come in from tether
  if((dt_view_get_current() == DT_VIEW_TETHERING
      || dev->image_storage.id == dev->preview_pipe->output_imgid)
     && (cur_mode->update_counter == s->update_counter))
  {
    if(dt_scopes_func_exists(cur_mode, draw_scope_channels))
      dt_scopes_call(cur_mode, draw_scope_channels,
                     cr, width, height, s->channels);
    else
      dt_scopes_call(cur_mode, draw_scope, cr, width, height);
  }
  dt_pthread_mutex_unlock(&s->lock);

  // finally a thin border
  cairo_rectangle(cr, 0, 0, width, height);
  set_color(cr, darktable.bauhaus->graph_border);
  cairo_stroke(cr);

  cairo_destroy(cr);
  cairo_set_source_surface(crf, cst, 0, 0);
  cairo_paint(crf);
  cairo_surface_destroy(cst);

  dt_show_times_f(&start, "[histogram]", "scope draw");
  return FALSE;
}

void lib_histogram_update_tooltip(const dt_scopes_t *const scopes)
{
  const char *const scope_name = dt_scopes_call(scopes->cur_mode, name);
  gchar *tip = g_strdup_printf("%s\n(%s)\n%s\n%s",
                               _(scope_name),
                               _("use buttons at top of graph to change type"),
                               _("click on ❓ and then graph for documentation"),
                               _("use color picker module to restrict area"));
  if(scopes->highlight == DT_SCOPES_HIGHLIGHT_BLACK_POINT)
    dt_util_str_cat(&tip, "\n%s\n%s",
                          _("drag to change black point"),
                          _("double-click resets"));
  if(scopes->highlight == DT_SCOPES_HIGHLIGHT_EXPOSURE)
    dt_util_str_cat(&tip, "\n%s\n%s",
                          _("drag to change exposure"),
                    _("double-click resets"));
  dt_scopes_call_if_exists(scopes->cur_mode, append_to_tooltip, &tip);
  gtk_widget_set_tooltip_text(scopes->scope_draw, tip);
  g_free(tip);
}

static void _drawable_drag_begin(GtkGestureDrag* gesture,
                                 const gdouble start_x,
                                 const gdouble start_y,
                                 dt_scopes_t *s)
{
  if(s->highlight != DT_SCOPES_HIGHLIGHT_NONE
     && dt_scopes_func_exists(s->cur_mode, get_exposure_delta)
     && dt_gui_claim(gesture))
  {
    dt_control_change_cursor("grabbing");
    s->last_offset_x = s->last_offset_y = 0.0;
    s->dragging = TRUE;
  }
  else
    dt_gui_deny(gesture);
}

static void _drawable_drag_end(GtkGestureDrag* g,
                               const gdouble ox,
                               const gdouble oy,
                               dt_scopes_t *s)
{
  s->dragging = FALSE;
  dt_control_change_cursor(s->highlight == DT_SCOPES_HIGHLIGHT_NONE
                           ? "default" : "grab");
}

static void _drawable_drag_update(GtkGestureDrag* gesture,
                                  const gdouble offset_x,
                                  const gdouble offset_y,
                                  dt_scopes_t *s)
{
  dt_scopes_mode_t *const cur_mode = s->cur_mode;
  // GTK4: use gtk_event_controller_get_current_event_state()
  GdkEvent *event = gtk_get_current_event();
  if(!event) return;
  if(gdk_event_get_event_type(event) == GDK_MOTION_NOTIFY)
  {
    // at any time user may user a modifier key to adjust speed
    // multiplier, so calculate delta since last move, rather than
    // offset since drag start
    const gdouble ox = offset_x - s->last_offset_x;
    const gdouble oy = offset_y - s->last_offset_y;
    const double delta = dt_scopes_call(cur_mode, get_exposure_delta, ox, oy);
    dt_dev_exposure_handle_event(1, delta, event->motion.state,
                                 s->highlight == DT_SCOPES_HIGHLIGHT_BLACK_POINT);
    s->last_offset_x = offset_x;
    s->last_offset_y = offset_y;
  }
  gdk_event_free(event);
}

static void _drawable_button_press(GtkGestureSingle *gesture,
                                   const int n_press,
                                   const double x,
                                   const double y,
                                   dt_scopes_t *s)
{
  if(s->highlight != DT_SCOPES_HIGHLIGHT_NONE
     && n_press == 2
     && dt_gui_claim(gesture))
    dt_dev_exposure_handle_event(2, 0.0, 0,
                                 s->highlight == DT_SCOPES_HIGHLIGHT_BLACK_POINT);
}

static void _drawable_motion(GtkEventControllerMotion *controller,
                             const double x,
                             const double y,
                             dt_scopes_t *s)
{
  if(s->dragging) return;

  GtkAllocation allocation;
  gtk_widget_get_allocation(s->scope_draw, &allocation);
  const double posx = x / (double)(allocation.width);
  const double posy = y / (double)(allocation.height);
  const dt_scopes_highlight_t prior_highlight = s->highlight;

  if(dt_scopes_func_exists(s->cur_mode, get_highlight))
    s->highlight = dt_scopes_call(s->cur_mode, get_highlight, posx, posy);
  else
    s->highlight = DT_SCOPES_HIGHLIGHT_NONE;

  if(prior_highlight != s->highlight)
  {
    lib_histogram_update_tooltip(s);
    dt_scopes_refresh(s);
    dt_control_change_cursor(s->highlight == DT_SCOPES_HIGHLIGHT_NONE
                             ? "default" : "grab");
  }
}

static void _drawable_leave(GtkEventControllerMotion *controller,
                            dt_scopes_t *s)
{
  // if dragging, gtk keeps up motion notifications until mouse button
  // is released, at which point we'll get another leave event for
  // drawable if pointer is still outside of the widget
  if(!(dt_key_modifier_state() & GDK_BUTTON1_MASK)
     && s->highlight != DT_SCOPES_HIGHLIGHT_NONE)
  {
    s->highlight = DT_SCOPES_HIGHLIGHT_NONE;
    dt_control_change_cursor("default");
    dt_scopes_refresh(s);
  }
}

static void _mode_toggle(GtkWidget *button, dt_scopes_t *s)
{
  // create radio-button-like behavior for choosing scope modes with a
  // GtkDarktableToggleButton group, using signal blocking to avoid
  // re-entering this signal handler
  dt_scopes_mode_t *prior_mode = s->cur_mode;
  for(int i = 0; i < DT_SCOPES_MODE_N; i++)
  {
    if(s->modes[i].button_activate == button)
    {
      // clicking on current mode button leaves it on
      if(s->cur_mode == &s->modes[i]
         && !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button)))
      {
        g_signal_handler_block(button, s->modes[i].toggle_signal_handler);
        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->modes[i].button_activate), TRUE);
        g_signal_handler_unblock(button, s->modes[i].toggle_signal_handler);
        return;
      }
      s->cur_mode = &s->modes[i];
    }
    else if(prior_mode == &s->modes[i])
    {
      // user clicked on a different mode button, set the prior mode
      // toggle button to inactive
      g_signal_handler_block(s->modes[i].button_activate, s->modes[i].toggle_signal_handler);
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->modes[i].button_activate), FALSE);
      g_signal_handler_unblock(s->modes[i].button_activate, s->modes[i].toggle_signal_handler);
    }
  }

  dt_conf_set_string("plugins/darkroom/histogram/mode",
                     dt_scopes_call(s->cur_mode, name));
  lib_histogram_update_tooltip(s);

  dt_scopes_call(prior_mode, mode_leave);
  gtk_widget_set_visible(s->button_box_rgb,
                         dt_scopes_func_exists(s->cur_mode, draw_scope_channels));
  dt_scopes_call(s->cur_mode, update_buttons);
  dt_scopes_call(s->cur_mode, mode_enter);

  // even if no current data, GUI should still respond to update
  dt_scopes_refresh(s);
  // FIXME: does this comparison of update_counter need to be protected within a
  // mutex?
  if(s->update_counter != s->cur_mode->update_counter)
    dt_scopes_reprocess();
}

static void _channel_toggle(GtkWidget *button,
                            dt_scopes_t *s)
{
  for(int i = 0; i < DT_SCOPES_RGB_N; i++)
    if(s->channel_buttons[i] == button)
    {
      char conf[48];
      g_snprintf(conf, sizeof(conf),
                 "plugins/darkroom/histogram/show_%s", rgb_names[i]);
      s->channels[i]
        = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(button));
      dt_conf_set_bool(conf, s->channels[i]);
      dt_scopes_refresh(s);
    }
}

static void _eventbox_scroll_callback(GtkEventControllerScroll* self,
                                      const gdouble dx,
                                      const gdouble dy,
                                      dt_scopes_t *s)
{
  GdkEvent *event = gtk_get_current_event();
  if(!event) return;
  if(gdk_event_get_event_type(event) == GDK_SCROLL)
  {
    // FIXME: so long as we have event, test its flags -- and for GTK 4 we can
    // use gtk_get_current_event() and get flags -- make a helper function to do
    // this.
    if(dt_modifier_is(event->scroll.state,
                      GDK_SHIFT_MASK | GDK_MOD1_MASK))
    {
      // bubble to adjusting the overall widget size
      gtk_widget_event(s->scope_draw, event);
    }
    else if(s->highlight != DT_SCOPES_HIGHLIGHT_NONE)
    {
      // FIXME: should scroll for exposure change be handled by each scope, rather than here?
      // FIXME: should handle smooth scrolling rather than discrete?
      // FIXME: should scrolling of scope be handled in the drawable rather than
      //        the eventbox.
      dt_dev_exposure_handle_event(0, dy - dx, event->scroll.state,
                                   s->highlight == DT_SCOPES_HIGHLIGHT_BLACK_POINT);
    }
    else
    {
      int ebx, eby;
      gtk_widget_translate_coordinates(gtk_get_event_widget(event), s->scope_draw,
                                       (int)event->scroll.x, (int)event->scroll.y, &ebx, &eby);
      dt_scopes_call_if_exists(s->cur_mode, eventbox_scroll, ebx, eby,
                               dx, dy, event->scroll.state);
    }
  }
  gdk_event_free(event);
}

// TRUE if the point (x, y) -- given in `from` widget coordinates -- lies
// over the visible widget `w`.
static gboolean _pointer_over_widget(GtkWidget *from,
                                     GtkWidget *w,
                                     const double x,
                                     const double y)
{
  if(!w || !gtk_widget_is_visible(w)) return FALSE;
  gint tx, ty;
  if(!gtk_widget_translate_coordinates(from, w, (gint)x, (gint)y, &tx, &ty))
    return FALSE;
  GtkAllocation alloc;
  gtk_widget_get_allocation(w, &alloc);
  return tx >= 0 && tx < alloc.width && ty >= 0 && ty < alloc.height;
}

static void _eventbox_motion_notify_callback(GtkEventControllerMotion *controller,
                                             const double x,
                                             const double y,
                                             dt_scopes_t *s)
{
  // This is required in order to correctly display the button tooltips
  //
  // FIXME: it should be ok to update button tooltips only when the
  // main widget tooltip has changed, but calling this only at the end
  // of lib_histogram_update_tooltip() doesn't work
  dt_scopes_call_if_exists(s->cur_mode, update_buttons);

  // overlay pass-through OFF for a button box while the
  // pointer is over its buttons -> tooltips
  // pass-through on otherwise -> exposure dragging reaches scope.
  // For the split option we test the inner button_box_split, not the full-width
  // outer_box_split, so dragging still works across the rest of the top.
  GtkWidget *eb = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(controller));
  gtk_overlay_set_overlay_pass_through
    (GTK_OVERLAY(s->overlay), s->button_box_left,
     !_pointer_over_widget(eb, s->button_box_left, x, y));
  if(s->outer_box_split)
    gtk_overlay_set_overlay_pass_through
      (GTK_OVERLAY(s->overlay), s->outer_box_split,
       !_pointer_over_widget(eb, s->button_box_split, x, y));
}

static void _eventbox_enter_notify_callback(GtkEventControllerMotion *controller,
                                            const double x,
                                            const double y,
                                            dt_scopes_t *s)
{
  // FIXME: do need to do this, or should this already be updated?
  lib_histogram_update_tooltip(s);
  // right after startup, vectorscope can display color harmony box,
  // so be sure to hide non-current mode buttons
  for(dt_scopes_mode_type_t i = 0; i < DT_SCOPES_MODE_N; i++)
    if(s->cur_mode != &s->modes[i])
      dt_scopes_call(&s->modes[i], mode_leave);
  dt_scopes_call(s->cur_mode, mode_enter);
  gtk_widget_set_visible(s->button_box_rgb,
                         dt_scopes_func_exists(s->cur_mode, draw_scope_channels));
  gtk_widget_show(s->button_box_left);
  gtk_widget_show(s->button_box_right);
}

static void _eventbox_leave_notify_callback(GtkEventControllerMotion *controller,
                                            dt_scopes_t *s)
{
  // when click between buttons on the buttonbox a leave event is
  // generated -- ignore it (for GTK 4, replace this with the simpler
  // gtk_event_controller_get_current_event())
  GdkEvent *event = gtk_get_current_event();
  if(event)
  {
    if(gdk_event_get_event_type(event) == GDK_LEAVE_NOTIFY)
    {
      const GdkEventCrossing *xc = &event->crossing;
      if(xc->mode == GDK_CROSSING_UNGRAB && xc->detail == GDK_NOTIFY_INFERIOR)
      {
        gdk_event_free(event);
        return;
      }
    }
    gdk_event_free(event);
  }
  gtk_widget_hide(s->button_box_left);
  gtk_widget_hide(s->button_box_right);
  gtk_widget_hide(s->button_box_split);
  // a fast exit can skip the motion event that would restore pass-through,
  // make sure dragging is re-enabled when the pointer leaves
  gtk_overlay_set_overlay_pass_through(GTK_OVERLAY(s->overlay), s->button_box_left, TRUE);
  if(s->outer_box_split)
    gtk_overlay_set_overlay_pass_through(GTK_OVERLAY(s->overlay), s->outer_box_split, TRUE);
}

static void _lib_histogram_collapse_callback(dt_action_t *action)
{
  dt_lib_module_t *self = darktable.lib->proxy.histogram.module;

  // Get the state
  const gint visible = dt_lib_is_visible(self);

  // Inverse the visibility
  dt_lib_set_visible(self, !visible);
}

// this is only called in darkroom view
static void _lib_histogram_preview_updated_callback(gpointer instance,
                                                    const dt_lib_module_t *self)
{
  // preview pipe has already given process() the high quality
  // pre-gamma image. Now that preview pipe is complete, draw it
  //
  // FIXME: it would be nice if process() just queued a redraw if not
  // in live view, but then our draw code would have to have some
  // other way to assure that the histogram image is current besides
  // checking the pixelpipe to see if it has processed the current
  // image
  //
  // FIXME: now that have update_counter can we do this? and get rid of catching
  // DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED?
  const dt_scopes_t *s = self->data;
  dt_scopes_refresh(s);
}

void view_enter(struct dt_lib_module_t *self,
                struct dt_view_t *old_view,
                struct dt_view_t *new_view)
{
  const dt_scopes_t *s = self->data;
  if(new_view->view(new_view) == DT_VIEW_DARKROOM)
  {
    DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_PREVIEW_PIPE_FINISHED, _lib_histogram_preview_updated_callback);
  }
  // button box should be hidden when enter view, unless mouse is over
  // histogram, in which case gtk kindly generates enter events
  gtk_widget_hide(s->button_box_left);
  gtk_widget_hide(s->button_box_right);
  gtk_widget_hide(s->button_box_split);

  // FIXME: set histogram data to blank if enter tether with no active image
}

void view_leave(struct dt_lib_module_t *self,
                struct dt_view_t *old_view,
                struct dt_view_t *new_view)
{
  DT_CONTROL_SIGNAL_DISCONNECT(_lib_histogram_preview_updated_callback, self);
}

static gboolean _overlay_get_child_position(GtkOverlay *overlay,
                                            GtkWidget *child,
                                            GdkRectangle *alloc,
                                            GtkWidget *match)
{
  // GtkOverlay clips main child to its parent widget size, but not
  // other children. Make sure that we clip match.
  // GTK4: use gtk_overlay_set_clip_overlay() and gtk_widget_set_valign()
  if(child != match) return FALSE;
  GtkAllocation main_alloc;
  GtkRequisition req;
  GtkWidget *main = gtk_bin_get_child(GTK_BIN(overlay));
  gtk_widget_get_allocation(main, &main_alloc);
  gtk_widget_get_preferred_size(child, NULL, &req);
  alloc->width = req.width;
  alloc->height = MIN(req.height, main_alloc.height);
  alloc->x = main_alloc.width - req.width;   // right align
  alloc->y = 0;
  return TRUE;
}

void gui_init(dt_lib_module_t *self)
{
  /* initialize ui widgets */
  dt_scopes_t *const s = dt_calloc1_align_type(dt_scopes_t);
  self->data = (void *)s;

  // must match order of dt_scopes_mode_type_t
  const dt_scopes_functions_t *const dt_scopes_mode_func_tables[DT_SCOPES_MODE_N] =
    { &dt_scopes_functions_vectorscope,
      &dt_scopes_functions_waveform,
      &dt_scopes_functions_split,
      &dt_scopes_functions_parade,
      &dt_scopes_functions_histogram,};
  const char *str = dt_conf_get_string_const("plugins/darkroom/histogram/mode");
  s->update_counter = 1;
  s->dragging = FALSE;
  s->cur_mode = &s->modes[DT_SCOPES_MODE_WAVEFORM];  // failsafe

  // FIXME: is there a better way to init this?
  for(dt_scopes_mode_type_t i = 0; i < DT_SCOPES_MODE_N; i++)
  {
    s->modes[i].functions = dt_scopes_mode_func_tables[i];
    s->modes[i].update_counter = 0;
    s->modes[i].scopes = s;
    dt_scopes_call(&s->modes[i], gui_init, s);
    if(g_strcmp0(str, dt_scopes_call(&s->modes[i], name)) == 0)
      s->cur_mode = &s->modes[i];
  }

  dt_pthread_mutex_init(&s->lock, NULL);

  s->channels[DT_SCOPES_RGB_RED]
    = dt_conf_get_bool("plugins/darkroom/histogram/show_red");
  s->channels[DT_SCOPES_RGB_GREEN]
    = dt_conf_get_bool("plugins/darkroom/histogram/show_green");
  s->channels[DT_SCOPES_RGB_BLUE]
    = dt_conf_get_bool("plugins/darkroom/histogram/show_blue");

  // proxy functions and data so that pixelpipe or tether can
  // provide data for a histogram
  // FIXME: do need to pass self, or can wrap a callback as a lambda
  darktable.lib->proxy.histogram.module = self;
  darktable.lib->proxy.histogram.process = _scope_process;
  darktable.lib->proxy.histogram.get_harmony = _lib_histogram_get_harmony;
  darktable.lib->proxy.histogram.set_harmony = _lib_histogram_set_harmony;
  darktable.lib->proxy.histogram.set_scope = _lib_histogram_set_scope;
  darktable.lib->proxy.histogram.set_type = _lib_histogram_set_type;
  darktable.lib->proxy.histogram.get_sector_angles = _lib_histogram_get_sector_angles;
  darktable.lib->proxy.histogram.set_harmony_callback = _lib_histogram_set_harmony_callback;

  // create widgets
  dt_action_t *dark =
    dt_action_section(&darktable.view_manager->proxy.darkroom.view->actions,
                      N_("histogram"));

  // shows the scope, scale, and has draggable areas
  s->scope_draw = dt_ui_resize_wrap(NULL,
                                    0,
                                    "plugins/darkroom/histogram/graphheight");
  dt_action_t *ac = dt_action_define(dark, NULL, N_("hide histogram"), s->scope_draw, NULL);
  dt_action_register(ac, NULL, _lib_histogram_collapse_callback,
                     GDK_KEY_H, GDK_CONTROL_MASK | GDK_SHIFT_MASK);

  // a row of control buttons, split in two button boxes, on left and right side
  // overlaying the current scope
  //
  // self->widget (GtkEventBox)
  //   '--> GtkOverlay
  //          |--> scope_draw (DtGtkDrawingArea with dt_ui_resize_wrap)
  //          |--> button_box_left (GtkBox hori)
  //          |      '--> mode buttons
  //          |--> button_box_right (GtkBox hori)
  //          |      '--> option buttons for non-split or right side of split
  //          '--> button_box_split (GtkBox hori)
  //                 '--> option buttons for left side of split, if shown
  s->button_box_left = dt_gui_hbox();
  dt_gui_add_class(s->button_box_left, "button_box");
  gtk_widget_set_valign(s->button_box_left, GTK_ALIGN_START);
  gtk_widget_set_halign(s->button_box_left, GTK_ALIGN_START);

  s->button_box_split = dt_gui_hbox();
  dt_gui_add_class(s->button_box_split, "button_box");
  gtk_widget_set_valign(s->button_box_split, GTK_ALIGN_START);
  gtk_widget_set_halign(s->button_box_split, GTK_ALIGN_END);
  GtkWidget *outer_box_split = dt_gui_hbox();
  gtk_box_set_homogeneous(GTK_BOX(outer_box_split), TRUE);
  GtkWidget *split_spacer = dt_gui_hbox();
  dt_gui_box_add(outer_box_split, s->button_box_split, split_spacer);

  s->button_box_right = dt_gui_hbox();
  dt_gui_add_class(s->button_box_right, "button_box");
  gtk_widget_set_valign(s->button_box_right, GTK_ALIGN_START);
  gtk_widget_set_halign(s->button_box_right, GTK_ALIGN_END);
  gtk_widget_set_hexpand(s->button_box_right, TRUE);

  s->overlay = gtk_overlay_new();
  gtk_container_add(GTK_CONTAINER(s->overlay), s->scope_draw);
  gtk_overlay_add_overlay(GTK_OVERLAY(s->overlay), s->button_box_left);
  gtk_overlay_add_overlay(GTK_OVERLAY(s->overlay), s->button_box_right);
  gtk_overlay_add_overlay(GTK_OVERLAY(s->overlay), outer_box_split);
  s->outer_box_split = outer_box_split;
  // Pass-through by default to allow exposure-dragging reach the scope.
  // Set to FALSE on hover to allow tooltips, see _eventbox_motion_notify_callback()
  gtk_overlay_set_overlay_pass_through (GTK_OVERLAY(s->overlay), s->button_box_left, TRUE);
  gtk_overlay_set_overlay_pass_through (GTK_OVERLAY(s->overlay), outer_box_split, TRUE);

  // FIXME: the button transitions when they appear on mouseover
  // (mouse enters scope widget) or change (mouse click) cause redraws
  // of the entire scope -- is there a way to avoid this?

  // must match order of dt_scopes_mode_type_t
  // FIXME: should these be defined in each scope mode, or in a register function call?
  const void *dt_lib_histogram_scope_type_icons[DT_SCOPES_MODE_N] =
    { dtgtk_cairo_paint_vectorscope,
      dtgtk_cairo_paint_waveform_scope,
      dtgtk_cairo_paint_split_waveform_vectorscope,
      dtgtk_cairo_paint_rgb_parade,
      dtgtk_cairo_paint_histogram_scope };
  // FIXME: can use use GtkStack or tabless GtkNotebook to handle
  // mode-switching behavior -- drawable and right buttons?
  for(int i=0; i<DT_SCOPES_MODE_N; i++)
  {
    const char *const name = dt_scopes_call(&s->modes[i], name);
    s->modes[i].button_activate =
      dtgtk_togglebutton_new(dt_lib_histogram_scope_type_icons[i], CPF_NONE, NULL);
    gtk_widget_set_halign(s->modes[i].button_activate, GTK_ALIGN_CENTER);
    gtk_widget_set_valign(s->modes[i].button_activate, GTK_ALIGN_START);
    gtk_widget_set_tooltip_text(s->modes[i].button_activate, _(name));
    dt_action_define(dark, N_("modes"), name,
                     s->modes[i].button_activate, &dt_action_def_toggle);
    // GTK4: use gtk_toggle_button_set_group(), GTK3: handle in callback
    s->modes[i].toggle_signal_handler =
      g_signal_connect_data(G_OBJECT(s->modes[i].button_activate), "toggled",
                            G_CALLBACK(_mode_toggle), s, NULL, 0);

    if(s->modes[i].functions->add_options)
    {
      s->modes[i].options_box = dt_gui_hbox();
      dt_scopes_call(&s->modes[i], add_options, dark);
      dt_gui_box_add(s->button_box_right, s->modes[i].options_box);
    }
    else
      s->modes[i].options_box = NULL;
  }

  // allow for clustering mode buttons in responsive layout
  const int btns_per_row = 3;
  for(int row_start=0; row_start < DT_SCOPES_MODE_N; row_start += btns_per_row)
  {
    GtkWidget *row = dt_gui_hbox();
    for(int pos=row_start; pos < row_start+btns_per_row && pos < DT_SCOPES_MODE_N; pos++)
      dt_gui_box_add(row, s->modes[pos].button_activate);
    dt_gui_box_add(s->button_box_left, row);
  }

  dt_action_t *teth = &darktable.view_manager->proxy.tethering.view->actions;
  if(teth)
    dt_action_register(teth, N_("hide histogram"),
                       _lib_histogram_collapse_callback,
                       GDK_KEY_H, GDK_CONTROL_MASK | GDK_SHIFT_MASK);

  // RGB channel buttons
  s->button_box_rgb = dt_gui_hbox();
  gtk_widget_set_valign(s->button_box_rgb, GTK_ALIGN_CENTER);
  gtk_widget_set_halign(s->button_box_rgb, GTK_ALIGN_CENTER);
  // red/green/blue channel on/off
  for(int i=DT_SCOPES_RGB_RED; i < DT_SCOPES_RGB_N; i++)
  {
    g_autofree char *name = g_strdup_printf("%s-channel-button", rgb_names[i]);
    g_autofree char *tip = g_strdup_printf(_("toggle %s channel"), _(rgb_names[i]));
    GtkWidget *btn = dtgtk_togglebutton_new(dtgtk_cairo_paint_color,
                                            CPF_NONE, NULL);
    dt_gui_add_class(btn, "rgb_toggle");
    gtk_widget_set_name(btn, name);
    gtk_widget_set_tooltip_text(btn, tip);
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(btn),
                                 s->channels[i]);
    dt_action_define(dark, N_("toggle colors"), rgb_names[i], btn, &dt_action_def_toggle);
    dt_gui_box_add(s->button_box_rgb, btn);
    g_signal_connect(G_OBJECT(btn), "toggled", G_CALLBACK(_channel_toggle), s);
    s->channel_buttons[i] = btn;
  }
  // RGB channels are always rightmost
  dt_gui_box_add(s->button_box_right, s->button_box_rgb);

  for(dt_scopes_mode_type_t i = 0; i < DT_SCOPES_MODE_N; i++)
  {
    dt_scopes_call_if_exists(&s->modes[i], update_buttons);
    if(s->cur_mode == &s->modes[i])
      gtk_toggle_button_set_active
        (GTK_TOGGLE_BUTTON(s->modes[i].button_activate), TRUE);
  }

  // FIXME: add a brightness control (via GtkScaleButton?). Different per each mode?

  // The overlay has no window, and hence can't catch events. We need
  // something on top to catch events to show/hide the buttons. The
  // drawable is below the buttons, and hence won't catch motion
  // events for the buttons, and gets a leave event when the cursor
  // moves over the buttons.
  GtkWidget *eventbox = gtk_event_box_new();
  gtk_container_add(GTK_CONTAINER(eventbox), s->overlay);
  self->widget = eventbox;
  gtk_widget_set_name(self->widget, "main-histogram");

  /* connect callbacks */
  // FIXME: why does cursor motion over buttons trigger multiple draw callbacks?
  g_signal_connect(G_OBJECT(s->scope_draw),
                   "draw", G_CALLBACK(_drawable_draw_callback), s);
  dt_gui_connect_drag(s->scope_draw, _drawable_drag_begin, _drawable_drag_end,
                      _drawable_drag_update, s);
  dt_gui_connect_click(s->scope_draw, _drawable_button_press, NULL, s);
  dt_gui_connect_motion(s->scope_draw, _drawable_motion, NULL,
                        _drawable_leave, s);
  // constrain height of button_box_right to s->scope_draw, necessary
  // so that harmony buttons are scrollable within overlay
  g_signal_connect(G_OBJECT(s->overlay), "get-child-position",
                   G_CALLBACK(_overlay_get_child_position), s->button_box_right);

  // FIXME: add (optional) propagation phase argument to dt_gui_connect_*()
  GtkEventController *scroll_controller =
    dt_gui_connect_scroll(eventbox, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES
                                    | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE,
                          _eventbox_scroll_callback, s);
  gtk_event_controller_set_propagation_phase(scroll_controller, GTK_PHASE_CAPTURE);
  // use GTK_PHASE_TARGET to capture enter/leave events, as
  // enter/leave events apparently not bubbled in GTK < 3.24.43.
  dt_gui_connect_motion(eventbox, NULL,
                        _eventbox_enter_notify_callback,
                        _eventbox_leave_notify_callback, s);
  // but need GTK_PHASE_BUBBLE to capture motion events
  GtkEventController *eventbox_motion_controller =
    dt_gui_connect_motion(eventbox, _eventbox_motion_notify_callback,
                          NULL, NULL, s);
  gtk_event_controller_set_propagation_phase
    (eventbox_motion_controller, GTK_PHASE_BUBBLE);

  gtk_widget_show_all(self->widget);
}

void gui_cleanup(dt_lib_module_t *self)
{
  dt_scopes_t *s = self->data;

  for(dt_scopes_mode_type_t i = 0; i < DT_SCOPES_MODE_N; i++)
    dt_scopes_call(&s->modes[i], gui_cleanup);
  dt_pthread_mutex_destroy(&s->lock);

  dt_free_align(self->data);
  self->data = NULL;
}

static void _lib_histogram_get_harmony(dt_lib_module_t *self,
                                       dt_color_harmony_guide_t *guide)
{
  if(self && guide)
  {
    dt_scopes_t *s = (dt_scopes_t *)self->data;
    dt_vec_get_harmony(&s->modes[DT_SCOPES_MODE_VECTORSCOPE], guide);
  }
}

static void _lib_histogram_set_harmony(dt_lib_module_t *self,
                                       const dt_color_harmony_guide_t *guide)
{
  if(self && guide)
  {
    dt_scopes_t *s = (dt_scopes_t *)self->data;
    dt_vec_set_harmony(&s->modes[DT_SCOPES_MODE_VECTORSCOPE], guide);
  }
}

static void _lib_histogram_set_harmony_callback(dt_lib_module_t *self,
    void (*cb)(const dt_color_harmony_guide_t *, void *),
    void *user_data)
{
  if(self)
  {
    dt_scopes_t *s = (dt_scopes_t *)self->data;
    dt_vec_set_harmony_changed_callback(&s->modes[DT_SCOPES_MODE_VECTORSCOPE], cb, user_data);
  }
}

static void _lib_histogram_set_scope(dt_lib_module_t *self, int scope)
{
  if(self && scope >= 0 && scope < DT_SCOPES_MODE_N)
  {
    dt_scopes_t *s = (dt_scopes_t *)self->data;
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(s->modes[scope].button_activate), TRUE);
  }
}

static void _lib_histogram_set_type(dt_lib_module_t *self, const int type)
{
  if(self && type >= 0)
  {
    dt_scopes_t *s = (dt_scopes_t *)self->data;
    dt_vec_set_vectorscope_type(&s->modes[DT_SCOPES_MODE_VECTORSCOPE], type);
  }
}

static void _lib_histogram_get_sector_angles(dt_lib_module_t *self,
                                             const dt_color_harmony_type_t type,
                                             const int rotation,
                                             float *angles,
                                             int *n)
{
  dt_vec_get_sector_angles(type, rotation, angles, n);
}

// 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
