Ctrl K
ComunidadGuías…

src/iop/retouch.c

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/*
    This file is part of darktable,
    Copyright (C) 2017-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/gdk_event_utils.h"

#include "bauhaus/bauhaus.h"
#include "common/bilateral.h"
#include "common/bilateralcl.h"
#include "common/colorspaces_inline_conversions.h"
#include "common/dwt.h"
#include "common/gaussian.h"
#include "common/heal.h"
#include "common/imagebuf.h"
#include "common/opencl.h"
#include "develop/blend.h"
#include "develop/imageop_math.h"
#include "develop/imageop_gui.h"
#include "develop/masks.h"
#include "develop/tiling.h"
#include "iop/iop_api.h"
#include "dtgtk/drawingarea.h"
#include "gui/accelerators.h"
#include "gui/color_picker_proxy.h"
#include <stdlib.h>

// this is the version of the modules parameters,
// and includes version information about compile-time dt
DT_MODULE_INTROSPECTION(3, dt_iop_retouch_params_t)

#define RETOUCH_NO_FORMS 300
#define RETOUCH_MAX_SCALES 15
#define RETOUCH_NO_SCALES (RETOUCH_MAX_SCALES + 2)

#define RETOUCH_PREVIEW_LVL_MIN -3.0f
#define RETOUCH_PREVIEW_LVL_MAX 3.0f

typedef enum dt_iop_retouch_drag_types_t {
  DT_IOP_RETOUCH_WDBAR_DRAG_TOP = 1,
  DT_IOP_RETOUCH_WDBAR_DRAG_BOTTOM = 2,
} dt_iop_retouch_drag_types_t;

typedef enum dt_iop_retouch_fill_modes_t {
  DT_IOP_RETOUCH_FILL_ERASE = 0, // $DESCRIPTION: "erase"
  DT_IOP_RETOUCH_FILL_COLOR = 1  // $DESCRIPTION: "color"
} dt_iop_retouch_fill_modes_t;

typedef enum dt_iop_retouch_blur_types_t {
  DT_IOP_RETOUCH_BLUR_GAUSSIAN = 0, // $DESCRIPTION: "gaussian"
  DT_IOP_RETOUCH_BLUR_BILATERAL = 1 // $DESCRIPTION: "bilateral"
} dt_iop_retouch_blur_types_t;

typedef enum dt_iop_retouch_algo_type_t {
  DT_IOP_RETOUCH_NONE = 0,  // $DESCRIPTION: "unused"
  DT_IOP_RETOUCH_CLONE = 1, // $DESCRIPTION: "clone"
  DT_IOP_RETOUCH_HEAL = 2,  // $DESCRIPTION: "heal"
  DT_IOP_RETOUCH_BLUR = 3,  // $DESCRIPTION: "blur"
  DT_IOP_RETOUCH_FILL = 4   // $DESCRIPTION: "fill"
} dt_iop_retouch_algo_type_t;

typedef struct dt_iop_retouch_form_data_t
{
  dt_mask_id_t formid; // from masks, form->formid
  int scale;  // 0==original image; 1..RETOUCH_MAX_SCALES==scale; RETOUCH_MAX_SCALES+1==residual
  dt_iop_retouch_algo_type_t algorithm;  // clone, heal, blur, fill

  dt_iop_retouch_blur_types_t blur_type; // gaussian, bilateral
  float blur_radius;                     // radius for blur algorithm

  dt_iop_retouch_fill_modes_t fill_mode; // mode for fill algorithm, erase or fill with color
  float fill_color[3];                   // color for fill algorithm
  float fill_brightness;                 // value to be added to the color
  int distort_mode; // module v1 => 1, otherwise 2. mode 1 as issues if there's distortion before this module
} dt_iop_retouch_form_data_t;

typedef struct retouch_user_data_t
{
  dt_iop_module_t *self;
  dt_dev_pixelpipe_iop_t *piece;
  dt_iop_roi_t roi;
  int display_scale;
  gboolean mask_display;
  gboolean suppress_mask;
} retouch_user_data_t;

typedef struct dt_iop_retouch_params_t
{
  dt_iop_retouch_form_data_t rt_forms[RETOUCH_NO_FORMS]; // array of masks index and additional data

  dt_iop_retouch_algo_type_t algorithm; // $DEFAULT: DT_IOP_RETOUCH_HEAL clone, heal, blur, fill

  int num_scales;       // $DEFAULT: 0 number of wavelets scales
  int curr_scale;       // $DEFAULT: 0 current wavelet scale
  int merge_from_scale; // $DEFAULT: 0

  float preview_levels[3];

  dt_iop_retouch_blur_types_t blur_type; // $DEFAULT: DT_IOP_RETOUCH_BLUR_GAUSSIAN $DESCRIPTION: "blur type" gaussian, bilateral
  float blur_radius; // $MIN: 0.1 $MAX: 200.0 $DEFAULT: 10.0 $DESCRIPTION: "blur radius" radius for blur algorithm

  dt_iop_retouch_fill_modes_t fill_mode; // $DEFAULT: DT_IOP_RETOUCH_FILL_ERASE $DESCRIPTION: "fill mode" mode for fill algorithm, erase or fill with color
  float fill_color[3];   // $DEFAULT: 0.0 color for fill algorithm
  float fill_brightness; // $MIN: -1.0 $MAX: 1.0 $DESCRIPTION: "brightness" value to be added to the color
  int max_heal_iter;     // $DEFAULT: 2000 $DESCRIPTION: "max_iter" number of iterations for heal algorithm
} dt_iop_retouch_params_t;

typedef struct dt_iop_retouch_gui_data_t
{
  int copied_scale; // scale to be copied to another scale
  gboolean mask_display; // should we expose masks?
  gboolean suppress_mask;    // do not process masks
  gboolean display_wavelet_scale; // display current wavelet scale
  int displayed_wavelet_scale; // was display wavelet scale already used?
  int preview_auto_levels;   // should we calculate levels automatically?
  float preview_levels[3];   // values for the levels
  int first_scale_visible;   // 1st scale visible at current zoom level

  GtkLabel *label_form;                                                   // display number of forms
  GtkLabel *label_form_selected;                                          // display number of forms selected
  GtkWidget *bt_edit_masks, *bt_path, *bt_circle, *bt_ellipse, *bt_brush; // shapes
  GtkWidget *bt_clone, *bt_heal, *bt_blur, *bt_fill;                      // algorithms
  GtkWidget *bt_showmask, *bt_suppress;                                   // suppress & show masks

  GtkWidget *wd_bar; // wavelet decompose bar
  GtkLabel *lbl_num_scales;
  GtkLabel *lbl_curr_scale;
  GtkLabel *lbl_merge_from_scale;
  float wdbar_mouse_x, wdbar_mouse_y;
  int curr_scale; // scale box under mouse
  gboolean is_dragging;
  gboolean upper_cursor; // mouse on merge from scale cursor
  gboolean lower_cursor; // mouse on num scales cursor
  gboolean upper_margin; // mouse on the upper band
  gboolean lower_margin; // mouse on the lower band

  GtkWidget *bt_display_wavelet_scale; // show decomposed scale

  GtkWidget *bt_copy_scale; // copy all shapes from one scale to another
  GtkWidget *bt_paste_scale;

  GtkWidget *vbox_preview_scale;

  GtkDarktableGradientSlider *preview_levels_gslider;

  GtkWidget *bt_auto_levels;

  GtkWidget *vbox_blur;
  GtkWidget *cmb_blur_type;
  GtkWidget *sl_blur_radius;

  GtkWidget *vbox_fill;
  GtkWidget *hbox_color_pick;
  GtkWidget *colorpick;   // select a specific color
  GtkWidget *colorpicker; // pick a color from the picture

  GtkWidget *cmb_fill_mode;
  GtkWidget *sl_fill_brightness;

  GtkWidget *sl_mask_opacity; // draw mask opacity
} dt_iop_retouch_gui_data_t;

typedef struct dt_iop_retouch_params_t dt_iop_retouch_data_t;

typedef struct dt_iop_retouch_global_data_t
{
  int kernel_retouch_clear_alpha;
  int kernel_retouch_copy_alpha;
  int kernel_retouch_copy_buffer_to_buffer;
  int kernel_retouch_copy_buffer_to_image;
  int kernel_retouch_fill;
  int kernel_retouch_copy_image_to_buffer_masked;
  int kernel_retouch_copy_buffer_to_buffer_masked;
  int kernel_retouch_image_rgb2lab;
  int kernel_retouch_image_lab2rgb;
  int kernel_retouch_copy_mask_to_alpha;
} dt_iop_retouch_global_data_t;


// this returns a translatable name
const char *name()
{
  return _("retouch");
}

const char *aliases()
{
  return _("split-frequency|healing|cloning|stamp|blur|fill|wavelets|spot removal");
}


const char **description(dt_iop_module_t *self)
{
  return dt_iop_set_description(self, _("remove and clone spots,\n"
                                        "perform split-frequency skin editing"),
                                      _("corrective"),
                                      _("linear, RGB, scene-referred"),
                                      _("geometric and frequential, RGB"),
                                      _("linear, RGB, scene-referred"));
}

int default_group()
{
  return IOP_GROUP_CORRECT | IOP_GROUP_EFFECTS;
}

int flags()
{
  return IOP_FLAGS_SUPPORTS_BLENDING | IOP_FLAGS_NO_MASKS;
}

dt_iop_colorspace_type_t default_colorspace(dt_iop_module_t *self,
                                            dt_dev_pixelpipe_t *pipe,
                                            dt_dev_pixelpipe_iop_t *piece)
{
  return IOP_CS_RGB;
}

int operation_tags_filter()
{
  // switch off cropping and decoration, we want to see the full image.
  return IOP_TAG_DECORATION | IOP_TAG_CROPPING;
}

int legacy_params(dt_iop_module_t *self,
                  const void *const old_params,
                  const int old_version,
                  void **new_params,
                  int32_t *new_params_size,
                  int *new_version)
{
  typedef struct dt_iop_retouch_params_v3_t
  {
    dt_iop_retouch_form_data_t rt_forms[RETOUCH_NO_FORMS]; // array of masks index and additional data

    dt_iop_retouch_algo_type_t algorithm; // $DEFAULT: DT_IOP_RETOUCH_HEAL clone, heal, blur, fill

    int num_scales;       // $DEFAULT: 0 number of wavelets scales
    int curr_scale;       // $DEFAULT: 0 current wavelet scale
    int merge_from_scale; // $DEFAULT: 0

    float preview_levels[3];

    dt_iop_retouch_blur_types_t blur_type; // $DEFAULT: DT_IOP_RETOUCH_BLUR_GAUSSIAN $DESCRIPTION: "blur type" gaussian, bilateral
    float blur_radius; // $MIN: 0.1 $MAX: 200.0 $DEFAULT: 10.0 $DESCRIPTION: "blur radius" radius for blur algorithm

    dt_iop_retouch_fill_modes_t fill_mode; // $DEFAULT: DT_IOP_RETOUCH_FILL_ERASE $DESCRIPTION: "fill mode" mode for fill algorithm, erase or fill with color
    float fill_color[3];   // $DEFAULT: 0.0 color for fill algorithm
    float fill_brightness; // $MIN: -1.0 $MAX: 1.0 $DESCRIPTION: "brightness" value to be added to the color
    int max_heal_iter;     // $DEFAULT: 2000 $DESCRIPTION: "max_iter" number of iterations for heal algorithm
  } dt_iop_retouch_params_v3_t;

  if(old_version == 1)
  {
    typedef struct dt_iop_retouch_form_data_v1_t
    {
      dt_mask_id_t formid; // from masks, form->formid
      int scale;  // 0==original image; 1..RETOUCH_MAX_SCALES==scale; RETOUCH_MAX_SCALES+1==residual
      dt_iop_retouch_algo_type_t algorithm; // clone, heal, blur, fill

      dt_iop_retouch_blur_types_t blur_type; // gaussian, bilateral
      float blur_radius;                     // radius for blur algorithm

      dt_iop_retouch_fill_modes_t fill_mode; // mode for fill algorithm, erase or fill with color
      float fill_color[3];                   // color for fill algorithm
      float fill_brightness;                 // value to be added to the color
    } dt_iop_retouch_form_data_v1_t;
    typedef struct dt_iop_retouch_params_v1_t
    {
      dt_iop_retouch_form_data_v1_t rt_forms[RETOUCH_NO_FORMS]; // array of masks index and additional data

      dt_iop_retouch_algo_type_t algorithm; // $DEFAULT: DT_IOP_RETOUCH_HEAL clone, heal, blur, fill

      int num_scales;       // $DEFAULT: 0 number of wavelets scales
      int curr_scale;       // $DEFAULT: 0 current wavelet scale
      int merge_from_scale; // $DEFAULT: 0

      float preview_levels[3];

      dt_iop_retouch_blur_types_t blur_type; // $DEFAULT: DT_IOP_RETOUCH_BLUR_GAUSSIAN $DESCRIPTION: "blur type"
                                             // gaussian, bilateral
      float blur_radius; // $MIN: 0.1 $MAX: 200.0 $DEFAULT: 10.0 $DESCRIPTION: "blur radius" radius for blur
                         // algorithm

      dt_iop_retouch_fill_modes_t fill_mode; // $DEFAULT: DT_IOP_RETOUCH_FILL_ERASE $DESCRIPTION: "fill mode" mode
                                             // for fill algorithm, erase or fill with color
      float fill_color[3];                   // $DEFAULT: 0.0 color for fill algorithm
      float fill_brightness; // $MIN: -1.0 $MAX: 1.0 $DESCRIPTION: "brightness" value to be added to the color
    } dt_iop_retouch_params_v1_t;

    const dt_iop_retouch_params_v1_t *o = (dt_iop_retouch_params_v1_t *)old_params;
    dt_iop_retouch_params_v3_t *n = malloc(sizeof(dt_iop_retouch_params_v3_t));

    for(int i = 0; i < RETOUCH_NO_FORMS; i++)
    {
      dt_iop_retouch_form_data_v1_t of = o->rt_forms[i];
      n->rt_forms[i].algorithm = of.algorithm;
      n->rt_forms[i].blur_radius = of.blur_radius;
      n->rt_forms[i].blur_type = of.blur_type;
      n->rt_forms[i].distort_mode = 1;
      n->rt_forms[i].fill_brightness = of.fill_brightness;
      n->rt_forms[i].fill_color[0] = of.fill_color[0];
      n->rt_forms[i].fill_color[1] = of.fill_color[1];
      n->rt_forms[i].fill_color[2] = of.fill_color[2];
      n->rt_forms[i].fill_mode = of.fill_mode;
      n->rt_forms[i].formid = of.formid;
      n->rt_forms[i].scale = of.scale;
    }
    n->algorithm = o->algorithm;
    n->blur_radius = o->blur_radius;
    n->blur_type = o->blur_type;
    n->curr_scale = o->curr_scale;
    n->fill_brightness = o->fill_brightness;
    n->fill_color[0] = o->fill_color[0];
    n->fill_color[1] = o->fill_color[1];
    n->fill_color[2] = o->fill_color[2];
    n->fill_mode = o->fill_mode;
    n->merge_from_scale = o->merge_from_scale;
    n->num_scales = o->num_scales;
    n->preview_levels[0] = o->preview_levels[0];
    n->preview_levels[1] = o->preview_levels[1];
    n->preview_levels[2] = o->preview_levels[2];

    n->max_heal_iter = 1000;

    *new_params = n;
    *new_params_size = sizeof(dt_iop_retouch_params_v3_t);
    *new_version = 3;
    return 0;
  }

  if(old_version == 2)
  {
    typedef struct dt_iop_retouch_params_v2_t
    {
      dt_iop_retouch_form_data_t rt_forms[RETOUCH_NO_FORMS]; // array of masks index and additional data

      dt_iop_retouch_algo_type_t algorithm; // $DEFAULT: DT_IOP_RETOUCH_HEAL clone, heal, blur, fill

      int num_scales;       // $DEFAULT: 0 number of wavelets scales
      int curr_scale;       // $DEFAULT: 0 current wavelet scale
      int merge_from_scale; // $DEFAULT: 0

      float preview_levels[3];

      dt_iop_retouch_blur_types_t blur_type; // $DEFAULT: DT_IOP_RETOUCH_BLUR_GAUSSIAN $DESCRIPTION: "blur type" gaussian, bilateral
      float blur_radius; // $MIN: 0.1 $MAX: 200.0 $DEFAULT: 10.0 $DESCRIPTION: "blur radius" radius for blur algorithm

      dt_iop_retouch_fill_modes_t fill_mode; // $DEFAULT: DT_IOP_RETOUCH_FILL_ERASE $DESCRIPTION: "fill mode" mode for fill algorithm, erase or fill with color
      float fill_color[3];   // $DEFAULT: 0.0 color for fill algorithm
      float fill_brightness; // $MIN: -1.0 $MAX: 1.0 $DESCRIPTION: "brightness" value to be added to the color
    } dt_iop_retouch_params_v2_t;

    const dt_iop_retouch_params_v2_t *o = (dt_iop_retouch_params_v2_t *)old_params;
    dt_iop_retouch_params_v3_t *n = malloc(sizeof(dt_iop_retouch_params_v3_t));

    memcpy(n, o, sizeof(dt_iop_retouch_params_v2_t));

    n->max_heal_iter = 1000;

    *new_params = n;
    *new_params_size = sizeof(dt_iop_retouch_params_v3_t);
    *new_version = 3;
    return 0;
  }
  return 1;
}

static int rt_get_index_from_formid(const dt_iop_retouch_params_t *p, const dt_mask_id_t formid)
{
  int index = -1;
  if(dt_is_valid_maskid(formid))
  {
    int i = 0;

    while(index == -1 && i < RETOUCH_NO_FORMS)
    {
      if(p->rt_forms[i].formid == formid) index = i;
      i++;
    }
  }
  return index;
}

static dt_mask_id_t rt_get_selected_shape_id()
{
  return darktable.develop->mask_form_selected_id;
}

static dt_masks_point_group_t *rt_get_mask_point_group(dt_iop_module_t *self,
                                                       const dt_mask_id_t formid)
{
  dt_masks_point_group_t *form_point_group = NULL;

  const dt_develop_blend_params_t *bp = self->blend_params;
  if(!bp) return form_point_group;

  const dt_masks_form_t *grp = dt_masks_get_from_id(self->dev, bp->mask_id);
  if(grp && (grp->type & DT_MASKS_GROUP))
  {
    for(const GList *forms = grp->points; forms; forms = g_list_next(forms))
    {
      dt_masks_point_group_t *grpt = forms->data;
      if(grpt->formid == formid)
      {
        form_point_group = grpt;
        break;
      }
    }
  }

  return form_point_group;
}

static float rt_get_shape_opacity(dt_iop_module_t *self,
                                  const dt_mask_id_t formid)
{
  dt_masks_point_group_t *grpt = rt_get_mask_point_group(self, formid);
  return (grpt) ? grpt->opacity : 0.0f;
}

static void rt_display_selected_fill_color(dt_iop_retouch_gui_data_t *g,
                                           dt_iop_retouch_params_t *p)
{
  GdkRGBA c
      = (GdkRGBA){.red = p->fill_color[0],
                  .green = p->fill_color[1],
                  .blue = p->fill_color[2],
                  .alpha = 1.0 };
  gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(g->colorpick), &c);
}

static void rt_show_hide_controls(const dt_iop_module_t *self)
{
  dt_iop_retouch_gui_data_t *g = self->gui_data;
  dt_iop_retouch_params_t *p = self->params;

  const int creation_continuous =
    (darktable.develop->form_gui
     && darktable.develop->form_gui->creation_continuous
     && darktable.develop->form_gui->creation_continuous_module == self);

  switch(p->algorithm)
  {
    case DT_IOP_RETOUCH_HEAL:
      gtk_widget_hide(GTK_WIDGET(g->vbox_blur));
      gtk_widget_hide(GTK_WIDGET(g->vbox_fill));
      break;
    case DT_IOP_RETOUCH_BLUR:
      gtk_widget_show(GTK_WIDGET(g->vbox_blur));
      gtk_widget_hide(GTK_WIDGET(g->vbox_fill));
      break;
    case DT_IOP_RETOUCH_FILL:
      gtk_widget_hide(GTK_WIDGET(g->vbox_blur));
      gtk_widget_show(GTK_WIDGET(g->vbox_fill));
      if(p->fill_mode == DT_IOP_RETOUCH_FILL_COLOR)
        gtk_widget_show(GTK_WIDGET(g->hbox_color_pick));
      else
        gtk_widget_hide(GTK_WIDGET(g->hbox_color_pick));
      break;
    case DT_IOP_RETOUCH_CLONE:
    default:
      gtk_widget_hide(GTK_WIDGET(g->vbox_blur));
      gtk_widget_hide(GTK_WIDGET(g->vbox_fill));
      break;
  }

  if(g->display_wavelet_scale)
    gtk_widget_show(GTK_WIDGET(g->vbox_preview_scale));
  else
    gtk_widget_hide(GTK_WIDGET(g->vbox_preview_scale));

  const dt_masks_form_t *form =
    dt_masks_get_from_id(darktable.develop, rt_get_selected_shape_id());
  if(form && !creation_continuous)
    gtk_widget_show(GTK_WIDGET(g->sl_mask_opacity));
  else
    gtk_widget_hide(GTK_WIDGET(g->sl_mask_opacity));
}

static void rt_display_selected_shapes_lbl(dt_iop_retouch_gui_data_t *g)
{
  const dt_masks_form_t *form =
    dt_masks_get_from_id(darktable.develop, rt_get_selected_shape_id());
  if(form)
    gtk_label_set_text(g->label_form_selected, form->name);
  else
    gtk_label_set_text(g->label_form_selected, _("none"));
}

static int rt_get_selected_shape_index(dt_iop_retouch_params_t *p)
{
  return rt_get_index_from_formid(p, rt_get_selected_shape_id());
}

static void rt_shape_selection_changed(dt_iop_module_t *self)
{
  dt_iop_retouch_params_t *p = self->params;
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  DT_ENTER_GUI_UPDATE();

  gboolean selection_changed = FALSE;

  const int index = rt_get_selected_shape_index(p);
  if(index >= 0)
  {
    const float opacity = rt_get_shape_opacity(self, p->rt_forms[index].formid);
    if(opacity >= 0.0f)
      dt_bauhaus_slider_set(g->sl_mask_opacity, opacity);

    if(p->rt_forms[index].algorithm == DT_IOP_RETOUCH_BLUR)
    {
      p->blur_type = p->rt_forms[index].blur_type;
      p->blur_radius = p->rt_forms[index].blur_radius;

      dt_bauhaus_combobox_set(g->cmb_blur_type, p->blur_type);
      dt_bauhaus_slider_set(g->sl_blur_radius, p->blur_radius);

      selection_changed = TRUE;
    }
    else if(p->rt_forms[index].algorithm == DT_IOP_RETOUCH_FILL)
    {
      p->fill_mode = p->rt_forms[index].fill_mode;
      p->fill_brightness = p->rt_forms[index].fill_brightness;
      p->fill_color[0] = p->rt_forms[index].fill_color[0];
      p->fill_color[1] = p->rt_forms[index].fill_color[1];
      p->fill_color[2] = p->rt_forms[index].fill_color[2];

      dt_bauhaus_slider_set(g->sl_fill_brightness, p->fill_brightness);
      dt_bauhaus_combobox_set(g->cmb_fill_mode, p->fill_mode);
      rt_display_selected_fill_color(g, p);

      selection_changed = TRUE;
    }

    if(p->algorithm != p->rt_forms[index].algorithm)
    {
      p->algorithm = p->rt_forms[index].algorithm;

      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_clone),
                                   (p->algorithm == DT_IOP_RETOUCH_CLONE));
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_heal),
                                   (p->algorithm == DT_IOP_RETOUCH_HEAL));
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_blur),
                                   (p->algorithm == DT_IOP_RETOUCH_BLUR));
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_fill),
                                   (p->algorithm == DT_IOP_RETOUCH_FILL));

      selection_changed = TRUE;
    }

    if(selection_changed) rt_show_hide_controls(self);
  }

  rt_display_selected_shapes_lbl(g);

  const gboolean creation_continuous =
    (darktable.develop->form_gui
     && darktable.develop->form_gui->creation_continuous
     && darktable.develop->form_gui->creation_continuous_module == self);

  if(index >= 0 && !creation_continuous)
    gtk_widget_show(GTK_WIDGET(g->sl_mask_opacity));
  else
    gtk_widget_hide(GTK_WIDGET(g->sl_mask_opacity));

  DT_LEAVE_GUI_UPDATE();

  if(selection_changed)
    dt_dev_add_history_item(darktable.develop, self, TRUE);
}

//---------------------------------------------------------------------------------
// helpers
//---------------------------------------------------------------------------------

static void rt_masks_form_change_opacity(dt_iop_module_t *self,
                                         const dt_mask_id_t formid,
                                         const float opacity)
{
  dt_masks_point_group_t *grpt = rt_get_mask_point_group(self, formid);
  if(grpt)
  {
    const float new_opacity = CLAMP(opacity, 0.05f, 1.0f);
    if(!feqf(grpt->opacity, new_opacity, 1e-6))
    {
      grpt->opacity = new_opacity;
      dt_conf_set_float("plugins/darkroom/masks/opacity", grpt->opacity);
      dt_dev_add_masks_history_item(darktable.develop, self, TRUE);
    }
  }
}

static void rt_paste_forms_from_scale(dt_iop_retouch_params_t *p,
                                      const int source_scale,
                                      const int dest_scale)
{
  if(source_scale != dest_scale && source_scale >= 0 && dest_scale >= 0)
  {
    for(int i = 0; i < RETOUCH_NO_FORMS; i++)
    {
      if(p->rt_forms[i].scale == source_scale)
        p->rt_forms[i].scale = dest_scale;
    }
  }
}

static gboolean rt_allow_create_form(const dt_iop_module_t *self)
{
  gboolean allow = TRUE;

  const dt_iop_retouch_params_t *p = self->params;
  if(p)
  {
    allow = (p->rt_forms[RETOUCH_NO_FORMS - 1].formid == NO_MASKID);
  }
  return allow;
}

static void rt_reset_form_creation(GtkWidget *widget,
                                   const dt_iop_module_t *self)
{
  const dt_iop_retouch_gui_data_t *g = self->gui_data;

  if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->bt_path))
     || gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->bt_circle))
     || gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->bt_ellipse))
     || gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->bt_brush)))
  {
    // we unset the creation mode
    dt_masks_change_form_gui(NULL);
    darktable.develop->form_gui->creation_continuous = FALSE;
    darktable.develop->form_gui->creation_continuous_module = NULL;
  }

  if(widget != g->bt_path)
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_path), FALSE);
  if(widget != g->bt_circle)
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_circle), FALSE);
  if(widget != g->bt_ellipse)
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_ellipse), FALSE);
  if(widget != g->bt_brush)
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_brush), FALSE);

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_edit_masks), FALSE);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_showmask), FALSE);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_suppress), FALSE);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->colorpicker), FALSE);
}

static void rt_show_forms_for_current_scale(dt_iop_module_t *self)
{
  if(!self->enabled
     || darktable.develop->gui_module != self
     || darktable.develop->form_gui->creation
     || darktable.develop->form_gui->creation_continuous)
    return;

  dt_iop_retouch_params_t *p = self->params;
  dt_iop_gui_blend_data_t *bd = self->blend_data;
  dt_iop_retouch_gui_data_t *g = self->gui_data;
  if(bd == NULL) return;

  const int scale = p->curr_scale;
  int count = 0;

  // check if there is a shape on this scale
  for(int i = 0; i < RETOUCH_NO_FORMS && count == 0; i++)
  {
    if(dt_is_valid_maskid(p->rt_forms[i].formid)
       && p->rt_forms[i].scale == scale)
      count++;
  }

  // if there are shapes on this scale, make the cut shapes button sensitive
  gtk_widget_set_sensitive(g->bt_copy_scale, count > 0);

  // if no shapes on this scale, we hide all
  if(bd->masks_shown == DT_MASKS_EDIT_OFF || count == 0)
  {
    dt_masks_change_form_gui(NULL);

    if(g)
      gtk_toggle_button_set_active
        (GTK_TOGGLE_BUTTON(g->bt_edit_masks),
         (bd->masks_shown != DT_MASKS_EDIT_OFF)
         && (darktable.develop->gui_module == self));

    dt_control_queue_redraw_center();
    return;
  }

  // else, we create a new from group with the shapes and display it
  dt_masks_form_t *grp = dt_masks_create_ext(DT_MASKS_GROUP);
  for(int i = 0; i < RETOUCH_NO_FORMS; i++)
  {
    if(p->rt_forms[i].scale == scale)
    {
      const dt_mask_id_t grid = self->blend_params->mask_id;
      const dt_mask_id_t formid = p->rt_forms[i].formid;
      dt_masks_form_t *form = dt_masks_get_from_id(darktable.develop, formid);
      if(form)
      {
        dt_masks_point_group_t *fpt = calloc(1, sizeof(dt_masks_point_group_t));
        fpt->formid = formid;
        fpt->parentid = grid;
        fpt->state = DT_MASKS_STATE_USE;
        fpt->opacity = 1.0f;
        fpt->group_opacity = 1.0f;
        grp->points = g_list_append(grp->points, fpt);
      }
    }
  }

  dt_masks_form_t *grp2 = dt_masks_create_ext(DT_MASKS_GROUP);
  grp2->formid = NO_MASKID;
  dt_masks_group_ungroup(grp2, grp);
  dt_masks_change_form_gui(grp2);
  darktable.develop->form_gui->edit_mode = bd->masks_shown;

  if(g)
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_edit_masks),
                                 (bd->masks_shown != DT_MASKS_EDIT_OFF)
                                 && (darktable.develop->gui_module == self));

  dt_control_queue_redraw_center();
}

// called if a shape is added or deleted
static void rt_resynch_params(dt_iop_module_t *self)
{
  dt_iop_retouch_params_t *p = self->params;
  dt_develop_blend_params_t *bp = self->blend_params;

  dt_iop_retouch_form_data_t forms_d[RETOUCH_NO_FORMS];
  memset(forms_d, 0, sizeof(dt_iop_retouch_form_data_t) * RETOUCH_NO_FORMS);

  // we go through all forms in blend params
  dt_masks_form_t *grp = dt_masks_get_from_id(darktable.develop, bp->mask_id);
  if(grp && (grp->type & DT_MASKS_GROUP))
  {
    int new_form_index = 0;
    for(GList *forms = grp->points;
        (new_form_index < RETOUCH_NO_FORMS) && forms;
        forms = g_list_next(forms))
    {
      dt_masks_point_group_t *grpt = forms->data;
      if(grpt)
      {
        const dt_mask_id_t formid = grpt->formid;

        // search for the form on the shapes array
        const int form_index = rt_get_index_from_formid(p, formid);

        // if it exists copy it to the new array
        if(form_index >= 0)
        {
          forms_d[new_form_index] = p->rt_forms[form_index];

          new_form_index++;
        }
        else
        {
          // if it does not exists add it to the new array
          const dt_masks_form_t *parent_form =
            dt_masks_get_from_id(darktable.develop, formid);

          if(parent_form)
          {
            forms_d[new_form_index].formid = formid;
            forms_d[new_form_index].scale = p->curr_scale;
            forms_d[new_form_index].algorithm = p->algorithm;
            forms_d[new_form_index].distort_mode = 2;

            switch(forms_d[new_form_index].algorithm)
            {
              case DT_IOP_RETOUCH_BLUR:
                forms_d[new_form_index].blur_type = p->blur_type;
                forms_d[new_form_index].blur_radius = p->blur_radius;
                break;
              case DT_IOP_RETOUCH_FILL:
                forms_d[new_form_index].fill_mode = p->fill_mode;
                forms_d[new_form_index].fill_color[0] = p->fill_color[0];
                forms_d[new_form_index].fill_color[1] = p->fill_color[1];
                forms_d[new_form_index].fill_color[2] = p->fill_color[2];
                forms_d[new_form_index].fill_brightness = p->fill_brightness;
                break;
              default:
                break;
            }

            new_form_index++;
          }
        }
      }
    }
  }

  memcpy(p->rt_forms, forms_d, sizeof(p->rt_forms));
}

static gboolean rt_masks_form_is_in_roi(dt_iop_module_t *self,
                                        dt_dev_pixelpipe_iop_t *piece,
                                        dt_masks_form_t *form,
                                        const dt_iop_roi_t *roi_in,
                                        const dt_iop_roi_t *roi_out)
{
  // we get the area for the form
  int fl, ft, fw, fh;

  if(!dt_masks_get_area(self, piece, form, &fw, &fh, &fl, &ft)) return FALSE;

  // is the form outside of the roi?
  fw *= roi_in->scale, fh *= roi_in->scale, fl *= roi_in->scale, ft *= roi_in->scale;

  if(ft >= roi_out->y + roi_out->height
     || ft + fh <= roi_out->y
     || fl >= roi_out->x + roi_out->width
     || fl + fw <= roi_out->x)
    return FALSE;

  return TRUE;
}

static void rt_masks_point_denormalize(dt_dev_pixelpipe_iop_t *piece,
                                       const dt_iop_roi_t *roi,
                                       const float *points,
                                       size_t points_count,
                                       float *new)
{
  const float scalex = piece->pipe->iwidth * roi->scale;
  const float  scaley = piece->pipe->iheight * roi->scale;

  for(size_t i = 0; i < points_count * 2; i += 2)
  {
    new[i] = points[i] * scalex;
    new[i + 1] = points[i + 1] * scaley;
  }
}

static int rt_masks_point_calc_delta(dt_iop_module_t *self,
                                     dt_dev_pixelpipe_iop_t *piece,
                                     const dt_iop_roi_t *roi,
                                     const float *target,
                                     const float *source,
                                     float *dx,
                                     float *dy,
                                     const int distort_mode)
{
  // if distort_mode==1 we don't scale at the right place, hence false
  // positions if there's distortion before this module. we keep it
  // for backward compatibility only. all new forms have
  // distort_mode==2
  dt_boundingbox_t points;
  if(distort_mode == 1)
  {
    rt_masks_point_denormalize(piece, roi, target, 1, points);
    rt_masks_point_denormalize(piece, roi, source, 1, points + 2);
  }
  else
  {
    points[0] = target[0] * piece->pipe->iwidth;
    points[1] = target[1] * piece->pipe->iheight;
    points[2] = source[0] * piece->pipe->iwidth;
    points[3] = source[1] * piece->pipe->iheight;
  }

  const int res = dt_dev_distort_transform_plus(self->dev, piece->pipe,
                                                self->iop_order,
                                                DT_DEV_TRANSFORM_DIR_BACK_INCL,
                                                points, 2);
  if(!res) return res;

  if(distort_mode == 1)
  {
    *dx = points[0] - points[2];
    *dy = points[1] - points[3];
  }
  else
  {
    *dx = (points[0] - points[2]) * roi->scale;
    *dy = (points[1] - points[3]) * roi->scale;
  }

  return res;
}

/* returns (dx dy) to get from the source to the destination */
static int rt_masks_get_transform_to_destination(dt_iop_module_t *self,
                                             dt_dev_pixelpipe_iop_t *piece,
                                             const dt_iop_roi_t *roi,
                                             dt_masks_form_t *form,
                                             float *dx,
                                             float *dy,
                                             float *angle,
                                             const int distort_mode)
{
  int res = 0;

  if(form->type & DT_MASKS_PATH)
  {
    const dt_masks_point_path_t *pt = form->points->data;

    res = rt_masks_point_calc_delta(self, piece, roi, pt->corner,
                                    form->source, dx, dy, distort_mode);
  }
  else if(form->type & DT_MASKS_BRUSH)
  {
    const dt_masks_point_brush_t *pt = form->points->data;

    res = rt_masks_point_calc_delta(self, piece, roi, pt->corner,
                                    form->source, dx, dy, distort_mode);
  }
  else if(form->type & DT_MASKS_CIRCLE)
  {
    const dt_masks_point_circle_t *pt = form->points->data;

    res = rt_masks_point_calc_delta(self, piece, roi, pt->center,
                                    form->source, dx, dy, distort_mode);
  }
  else if(form->type & DT_MASKS_ELLIPSE)
  {
    const dt_masks_point_ellipse_t *pt = form->points->data;

    res = rt_masks_point_calc_delta(self, piece, roi, pt->center,
                                    form->source, dx, dy, distort_mode);
  }

  if(angle)
    *angle = form->source[2];

  return res;
}

static void rt_clamp_minmax(float levels_old[3], float levels_new[3])
{
  // left or right has changed
  if((levels_old[0] != levels_new[0]
      || levels_old[2] != levels_new[2])
     && levels_old[1] == levels_new[1])
  {
    // if old left and right are the same just use the new values
    if(levels_old[2] != levels_old[0])
    {
      // set the new value but keep the middle proportional
      const float left = MAX(levels_new[0], RETOUCH_PREVIEW_LVL_MIN);
      const float right = MIN(levels_new[2], RETOUCH_PREVIEW_LVL_MAX);

      const float percentage =
        (levels_old[1] - levels_old[0]) / (levels_old[2] - levels_old[0]);
      levels_new[1] = left + (right - left) * percentage;
      levels_new[0] = left;
      levels_new[2] = right;
    }
  }

  // if all zero make it gray
  if(levels_new[0] == 0.f && levels_new[1] == 0.f && levels_new[2] == 0.f)
  {
    levels_new[0] = -1.5f;
    levels_new[1] = 0.f;
    levels_new[2] = 1.5f;
  }

  // check the range
  if(levels_new[2] < levels_new[0] + 0.05f * 2.f)
    levels_new[2] = levels_new[0] + 0.05f * 2.f;
  if(levels_new[1] < levels_new[0] + 0.05f)
    levels_new[1] = levels_new[0] + 0.05f;
  if(levels_new[1] > levels_new[2] - 0.05f)
    levels_new[1] = levels_new[2] - 0.05f;

  {
    // set the new value but keep the middle proportional
    const float left = MAX(levels_new[0], RETOUCH_PREVIEW_LVL_MIN);
    const float right = MIN(levels_new[2], RETOUCH_PREVIEW_LVL_MAX);

    const float percentage =
      (levels_new[1] - levels_new[0]) / (levels_new[2] - levels_new[0]);
    levels_new[1] = left + (right - left) * percentage;
    levels_new[0] = left;
    levels_new[2] = right;
  }
}

static gboolean rt_shape_is_being_added(dt_iop_module_t *self,
                                        const dt_masks_type_t shape_type)
{
  gboolean being_added = FALSE;

  if(self->dev->form_gui && self->dev->form_visible
     && ((self->dev->form_gui->creation
          && self->dev->form_gui->creation_module == self)
         || (self->dev->form_gui->creation_continuous
             && self->dev->form_gui->creation_continuous_module == self)))
  {
    if(self->dev->form_visible->type & DT_MASKS_GROUP)
    {
      GList *forms = self->dev->form_visible->points;
      if(forms)
      {
        dt_masks_point_group_t *grpt = forms->data;
        if(grpt)
        {
          const dt_masks_form_t *form =
            dt_masks_get_from_id(darktable.develop, grpt->formid);
          if(form)
            being_added = (form->type & shape_type);
        }
      }
    }
    else
      being_added = (self->dev->form_visible->type & shape_type);
  }
  return being_added;
}

static gboolean rt_add_shape(GtkWidget *widget,
                             const int creation_continuous,
                             dt_iop_module_t *self)
{
  //turn module on (else shape creation won't work)
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), TRUE);

  //switch mask edit mode off
  dt_iop_gui_blend_data_t *bd = self->blend_data;
  if(bd) bd->masks_shown = DT_MASKS_EDIT_OFF;

  const gboolean allow = rt_allow_create_form(self);
  if(allow)
  {
    rt_reset_form_creation(widget, self);

    if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget)))
    {
      rt_show_forms_for_current_scale(self);

      return FALSE;
    }

    dt_iop_retouch_params_t *p = self->params;
    dt_iop_retouch_gui_data_t *g = self->gui_data;

    // we want to be sure that the iop has focus
    dt_iop_request_focus(self);

    dt_masks_type_t type = DT_MASKS_CIRCLE;
    if(widget == g->bt_path)
      type = DT_MASKS_PATH;
    else if(widget == g->bt_circle)
      type = DT_MASKS_CIRCLE;
    else if(widget == g->bt_ellipse)
      type = DT_MASKS_ELLIPSE;
    else if(widget == g->bt_brush)
      type = DT_MASKS_BRUSH;

    // we create the new form
    dt_masks_form_t *spot = NULL;
    if(p->algorithm == DT_IOP_RETOUCH_CLONE
       || p->algorithm == DT_IOP_RETOUCH_HEAL)
      spot = dt_masks_create(type | DT_MASKS_CLONE);
    else
      spot = dt_masks_create(type | DT_MASKS_NON_CLONE);

    dt_masks_change_form_gui(spot);
    darktable.develop->form_gui->creation_module = self;

    if(creation_continuous)
    {
      darktable.develop->form_gui->creation_continuous = TRUE;
      darktable.develop->form_gui->creation_continuous_module = self;
    }
    else
    {
      darktable.develop->form_gui->creation_continuous = FALSE;
      darktable.develop->form_gui->creation_continuous_module = NULL;
    }

    dt_control_queue_redraw_center();
  }
  else
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), FALSE);

  return !allow;
}

//---------------------------------------------------------------------------------
// GUI callbacks
//---------------------------------------------------------------------------------

static void rt_colorpick_color_set_callback(GtkColorButton *widget,
                                            dt_iop_module_t *self)
{
  DT_GUARD_GUI_UPDATE();
  dt_iop_retouch_params_t *p = self->params;

  // turn off the other color picker
  dt_iop_color_picker_reset(self, TRUE);

  GdkRGBA c
      = (GdkRGBA){.red   = p->fill_color[0],
                  .green = p->fill_color[1],
                  .blue  = p->fill_color[2],
                  .alpha = 1.0 };
  gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(widget), &c);
  p->fill_color[0] = c.red;
  p->fill_color[1] = c.green;
  p->fill_color[2] = c.blue;

  const int index = rt_get_selected_shape_index(p);
  if(index >= 0)
  {
    if(p->rt_forms[index].algorithm == DT_IOP_RETOUCH_FILL)
    {
      p->rt_forms[index].fill_color[0] = p->fill_color[0];
      p->rt_forms[index].fill_color[1] = p->fill_color[1];
      p->rt_forms[index].fill_color[2] = p->fill_color[2];
    }
  }

  dt_dev_add_history_item(darktable.develop, self, TRUE);
}

// wavelet decompose bar
#define RT_WDBAR_INSET 0.2f
#define lw DT_PIXEL_APPLY_DPI(1.0f)

static void rt_update_wd_bar_labels(const dt_iop_retouch_params_t *p,
                                    dt_iop_retouch_gui_data_t *g)
{
  char text[256];

  snprintf(text, sizeof(text), "%i", p->curr_scale);
  gtk_label_set_text(g->lbl_curr_scale, text);

  snprintf(text, sizeof(text), "%i", p->num_scales);
  gtk_label_set_text(g->lbl_num_scales, text);

  snprintf(text, sizeof(text), "%i", p->merge_from_scale);
  gtk_label_set_text(g->lbl_merge_from_scale, text);
}

static void rt_num_scales_update(const int _num_scales,
                                 dt_iop_module_t *self)
{
  DT_GUARD_GUI_UPDATE();

  dt_iop_retouch_params_t *p = self->params;
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  const int num_scales = CLAMP(_num_scales, 0, RETOUCH_MAX_SCALES);
  if(p->num_scales == num_scales)
    return;

  p->num_scales = num_scales;

  if(p->num_scales < p->merge_from_scale)
    p->merge_from_scale = p->num_scales;

  rt_update_wd_bar_labels(p, g);

  dt_dev_add_history_item(darktable.develop, self, TRUE);
}

static void rt_curr_scale_update(const int _curr_scale, dt_iop_module_t *self)
{
  DT_GUARD_GUI_UPDATE();

  dt_iop_retouch_params_t *p = self->params;
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  const int curr_scale = CLAMP(_curr_scale, 0, RETOUCH_MAX_SCALES + 1);

  if(p->curr_scale == curr_scale)
    return;

  p->curr_scale = curr_scale;

  rt_show_forms_for_current_scale(self);

  // compute auto levels only the first time display wavelet scale is used,
  // only if levels values are the default
  // and a detail scale is displayed
  dt_iop_gui_enter_critical_section(self);
  if(g->displayed_wavelet_scale == 0
     && p->preview_levels[0] == RETOUCH_PREVIEW_LVL_MIN
     && p->preview_levels[1] == 0.f
     && p->preview_levels[2] == RETOUCH_PREVIEW_LVL_MAX
     && g->preview_auto_levels == 0
     && p->curr_scale > 0 && p->curr_scale <= p->num_scales)
  {
    g->preview_auto_levels = 1;
    g->displayed_wavelet_scale = 1;
  }
  dt_iop_gui_leave_critical_section(self);

  rt_update_wd_bar_labels(p, g);

  dt_dev_add_history_item(darktable.develop, self, TRUE);
}

static void rt_merge_from_scale_update(const int _merge_from_scale,
                                       dt_iop_module_t *self)
{
  DT_GUARD_GUI_UPDATE();

  dt_iop_retouch_params_t *p = self->params;
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  const int merge_from_scale = CLAMP(_merge_from_scale, 0, p->num_scales);
  if(p->merge_from_scale == merge_from_scale) return;

  p->merge_from_scale = merge_from_scale;

  rt_update_wd_bar_labels(p, g);

  dt_dev_add_history_item(darktable.develop, self, TRUE);
}

static void rt_wdbar_leave_notify(GtkEventControllerMotion *controller, dt_iop_module_t *self)
{
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  g->wdbar_mouse_x = g->wdbar_mouse_y = -1;
  g->curr_scale = -1;
  g->lower_cursor = g->upper_cursor = FALSE;
  g->lower_margin = g->upper_margin = FALSE;

  gtk_widget_queue_draw(g->wd_bar);
}

static void rt_wdbar_button_press(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_iop_module_t *self)
{
  if(DT_IN_GUI_UPDATE())
    return;

  dt_iop_request_focus(self);

  dt_iop_retouch_gui_data_t *g = self->gui_data;
  GtkWidget *widget = dt_gui_get_widget(gesture);
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  const int inset = round(RT_WDBAR_INSET * allocation.height);
  const float box_w = (allocation.width - 2.0f * inset) / (float)RETOUCH_NO_SCALES;

  if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY)
  {
    if(g->lower_margin) // bottom slider
    {
      if(g->lower_cursor) // is over the arrow?
        g->is_dragging = DT_IOP_RETOUCH_WDBAR_DRAG_BOTTOM;
      else
        rt_num_scales_update(g->wdbar_mouse_x / box_w, self);
    }
    else if(g->upper_margin) // top slider
    {
      if(g->upper_cursor) // is over the arrow?
        g->is_dragging = DT_IOP_RETOUCH_WDBAR_DRAG_TOP;
      else
        rt_merge_from_scale_update(g->wdbar_mouse_x / box_w, self);
    }
    else if(g->curr_scale >= 0)
      rt_curr_scale_update(g->curr_scale, self);
  }

  gtk_widget_queue_draw(g->wd_bar);
}

static void rt_wdbar_button_release(GtkGestureSingle *gesture, gint n_press, gdouble x, gdouble y, dt_iop_module_t *self)
{
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  if(gtk_gesture_single_get_current_button(gesture) == GDK_BUTTON_PRIMARY)
    g->is_dragging = 0;

  gtk_widget_queue_draw(g->wd_bar);
}

static void rt_wdbar_scrolled(GtkEventControllerScroll *controller, gdouble dx, gdouble dy, dt_iop_module_t *self)
{
  if(DT_IN_GUI_UPDATE())
    return;

  dt_iop_retouch_params_t *p = self->params;
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  dt_iop_request_focus(self);

  if(dy != 0.0)
  {
    if(g->lower_margin) // bottom slider
      rt_num_scales_update(p->num_scales - dy, self);
    else if(g->upper_margin) // top slider
      rt_merge_from_scale_update(p->merge_from_scale - dy, self);
    else if(g->curr_scale >= 0)
      rt_curr_scale_update(p->curr_scale - dy, self);
  }

  gtk_widget_queue_draw(g->wd_bar);
}

static void rt_wdbar_motion_notify(GtkEventControllerMotion *controller,
                                    gdouble x,
                                    gdouble y,
                                    dt_iop_module_t *self)
{
  dt_iop_retouch_gui_data_t *g = self->gui_data;
  dt_iop_retouch_params_t *p = self->params;
  GtkWidget *widget = dt_gui_get_widget(controller);

  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  const int inset = round(RT_WDBAR_INSET * allocation.height);
  const float box_w = (allocation.width - 2.0f * inset) / (float)RETOUCH_NO_SCALES;
  const float sh = 3.0f * lw + inset;


  /* record mouse position within control */
  g->wdbar_mouse_x = CLAMP(x - inset, 0, allocation.width - 2.0f * inset - 1.0f);
  g->wdbar_mouse_y = y;

  g->curr_scale = g->wdbar_mouse_x / box_w;
  g->lower_cursor = g->upper_cursor = FALSE;
  g->lower_margin = g->upper_margin = FALSE;

  if(g->wdbar_mouse_y <= sh)
  {
    g->upper_margin = TRUE;
    const float middle = box_w * (0.5f + (float)p->merge_from_scale);
    g->upper_cursor = (g->wdbar_mouse_x >= (middle - inset))
      && (g->wdbar_mouse_x <= (middle + inset));
    if(!(g->is_dragging))
      g->curr_scale = -1;
  }
  else if(g->wdbar_mouse_y >= allocation.height - sh)
  {
    g->lower_margin = TRUE;
    const float middle = box_w * (0.5f + (float)p->num_scales);
    g->lower_cursor = (g->wdbar_mouse_x >= (middle - inset))
      && (g->wdbar_mouse_x <= (middle + inset));
    if(!(g->is_dragging))
      g->curr_scale = -1;
  }

  if(g->is_dragging == DT_IOP_RETOUCH_WDBAR_DRAG_BOTTOM)
    rt_num_scales_update(g->curr_scale, self);

  if(g->is_dragging == DT_IOP_RETOUCH_WDBAR_DRAG_TOP)
    rt_merge_from_scale_update(g->curr_scale, self);

  gtk_widget_queue_draw(g->wd_bar);
}

static int rt_scale_has_shapes(dt_iop_retouch_params_t *p,
                               const int scale)
{
  int has_shapes = 0;

  for(int i = 0; i < RETOUCH_NO_FORMS && has_shapes == 0; i++)
    has_shapes = (dt_is_valid_maskid(p->rt_forms[i].formid)
                  && p->rt_forms[i].scale == scale);

  return has_shapes;
}

static gboolean rt_wdbar_draw(GtkWidget *widget,
                              cairo_t *crf,
                              const dt_iop_module_t *self)
{
  dt_iop_retouch_gui_data_t *g = self->gui_data;
  dt_iop_retouch_params_t *p = self->params;


  GdkRGBA border      = {0.066, 0.066, 0.066, 1};
  GdkRGBA original    = {.1, .1, .1, 1};
  GdkRGBA inactive    = {.15, .15, .15, 1};
  GdkRGBA active      = {.35, .35, .35, 1};
  GdkRGBA merge_from  = {.5, .5, .5, 1};
  GdkRGBA residual    = {.8, .8, .8, 1};
  GdkRGBA shapes      = {.75, .5, .0, 1};
  GdkRGBA color;

  float middle;
  const int first_scale_visible = (g->first_scale_visible > 0)
    ? g->first_scale_visible
    : RETOUCH_MAX_SCALES;

  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);

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

  // clear background
  gdk_cairo_set_source_rgba(cr, &inactive);
  cairo_paint(cr);
  cairo_save(cr);

  // geometry
  const int inset = round(RT_WDBAR_INSET * allocation.height);
  const int mk = 2 * inset;
  const float sh = 3.0f * lw + inset;
  const float box_w = (allocation.width - 2.0f * inset) / (float)RETOUCH_NO_SCALES;
  const float box_h = allocation.height - 2.0f * sh;

  // render the boxes
  cairo_set_antialias(cr, CAIRO_ANTIALIAS_NONE);
  for(int i = 0; i < RETOUCH_NO_SCALES; i++)
  {
    // draw box background
    if(i == 0)
      color = original;
    else if(i == p->num_scales + 1)
      color = residual;
    else if(i >= p->merge_from_scale && i <= p->num_scales && p->merge_from_scale > 0)
      color = merge_from;
    else if(i <= p->num_scales)
      color = active;
    else
      color = inactive;

    gdk_cairo_set_source_rgba(cr, &color);
    cairo_rectangle(cr, box_w * i + inset, sh, box_w, box_h);
    cairo_fill(cr);

    // if detail scale is visible at current zoom level inform it
    if(i >= first_scale_visible && i <= p->num_scales)
    {
      gdk_cairo_set_source_rgba(cr, &merge_from);
      cairo_rectangle(cr, box_w * i + inset, lw, box_w, 2.0f * lw);
      cairo_fill(cr);
    }

    // if the scale has shapes inform it
    if(rt_scale_has_shapes(p, i))
    {
      cairo_set_line_width(cr, lw);
      gdk_cairo_set_source_rgba(cr, &shapes);
      cairo_rectangle(cr, box_w * i + inset + lw / 2.0f,
                      allocation.height - sh, box_w - lw, 2.0f * lw);
      cairo_fill(cr);
    }

    // draw the border
    cairo_set_line_width(cr, lw);
    gdk_cairo_set_source_rgba(cr, &border);
    cairo_rectangle(cr, box_w * i + inset, sh, box_w, box_h);
    cairo_stroke(cr);
  }

  cairo_set_antialias(cr, CAIRO_ANTIALIAS_DEFAULT);
  cairo_restore(cr);

  // dot for the current scale
  if(p->curr_scale >= p->merge_from_scale
     && p->curr_scale <= p->num_scales
     && p->merge_from_scale > 0)
    color = active;
  else
    color = merge_from;

  if(p->curr_scale >= 0 && p->curr_scale < RETOUCH_NO_SCALES)
  {
    cairo_set_line_width(cr, lw);
    gdk_cairo_set_source_rgba(cr, &color);
    middle = box_w * (0.5f + (float)p->curr_scale);
    cairo_arc(cr, middle + inset, 0.5f * box_h + sh, 0.5f * inset, 0, 2.0f * M_PI);
    cairo_fill(cr);
    cairo_stroke(cr);
  }

  // mouse hover on a scale
  if(g->curr_scale >= 0)
  {
    cairo_set_line_width(cr, lw);
    if(g->curr_scale == p->num_scales + 1) color = inactive;
    else color = residual;
    gdk_cairo_set_source_rgba(cr, &color);
    cairo_rectangle(cr, box_w * g->curr_scale + inset + lw,
                    sh + lw, box_w - 2.0f * lw, box_h - 2.0f * lw);
    cairo_stroke(cr);
  }

  /* render control points handles */

  // draw number of scales arrow (bottom arrow)
  middle = box_w * (0.5f + (float)p->num_scales);
  if(g->lower_cursor || g->is_dragging == DT_IOP_RETOUCH_WDBAR_DRAG_BOTTOM)
  {
    cairo_set_source_rgb(cr, 0.67, 0.67, 0.67);
    dtgtk_cairo_paint_solid_triangle(cr, middle, box_h + 5.0f * lw,
                                     mk, mk, CPF_DIRECTION_UP, NULL);
  }
  else
  {
    cairo_set_source_rgb(cr, 0.54, 0.54, 0.54);
    dtgtk_cairo_paint_triangle(cr, middle, box_h + 5.0f * lw, mk, mk,
                               CPF_DIRECTION_UP, NULL);
  }

  // draw merge scales arrow (top arrow)
  middle = box_w * (0.5f + (float)p->merge_from_scale);
  if(g->upper_cursor || g->is_dragging == DT_IOP_RETOUCH_WDBAR_DRAG_TOP)
  {
    cairo_set_source_rgb(cr, 0.67, 0.67, 0.67);
    dtgtk_cairo_paint_solid_triangle(cr, middle, 3.0f * lw, mk, mk,
                                     CPF_DIRECTION_DOWN, NULL);
  }
  else
  {
    cairo_set_source_rgb(cr, 0.54, 0.54, 0.54);
    dtgtk_cairo_paint_triangle(cr, middle, 3.0f * lw, mk, mk,
                               CPF_DIRECTION_DOWN, NULL);
  }

  /* push mem surface into widget */
  cairo_destroy(cr);
  cairo_set_source_surface(crf, cst, 0, 0);
  cairo_paint(crf);
  cairo_surface_destroy(cst);

  return TRUE;
}

static float rt_gslider_scale_callback(GtkWidget *self,
                                       const float inval,
                                       const int dir)
{
  float outval;
  switch(dir)
  {
    case GRADIENT_SLIDER_SET:
      outval = (inval - RETOUCH_PREVIEW_LVL_MIN)
        / (RETOUCH_PREVIEW_LVL_MAX - RETOUCH_PREVIEW_LVL_MIN);
      break;
    case GRADIENT_SLIDER_GET:
      outval = (RETOUCH_PREVIEW_LVL_MAX - RETOUCH_PREVIEW_LVL_MIN)
        * inval + RETOUCH_PREVIEW_LVL_MIN;
      break;
    default:
      outval = inval;
  }
  return outval;
}


static void rt_gslider_changed(GtkDarktableGradientSlider *gslider,
                               dt_iop_module_t *self)
{
  dt_iop_retouch_params_t *p = self->params;

  double dlevels[3];

  if(DT_IN_GUI_UPDATE())
    return;

  dtgtk_gradient_slider_multivalue_get_values(gslider, dlevels);

  for(int i = 0; i < 3; i++)
    p->preview_levels[i] = dlevels[i];

  dt_dev_add_history_item(darktable.develop, self, TRUE);

}


void color_picker_apply(dt_iop_module_t *self,
                        GtkWidget *picker,
                        dt_dev_pixelpipe_t *pipe)
{
  dt_iop_retouch_gui_data_t *g = self->gui_data;
  dt_iop_retouch_params_t *p = self->params;

  if(fabsf(p->fill_color[0] - self->picked_output_color[0]) < 0.0001f
     && fabsf(p->fill_color[1] - self->picked_output_color[1]) < 0.0001f
     && fabsf(p->fill_color[2] - self->picked_output_color[2]) < 0.0001f)
  {
    // interrupt infinite loops
    return;
  }

  p->fill_color[0] = self->picked_output_color[0];
  p->fill_color[1] = self->picked_output_color[1];
  p->fill_color[2] = self->picked_output_color[2];

  const int index = rt_get_selected_shape_index(p);
  if(index >= 0)
  {
    if(p->rt_forms[index].algorithm == DT_IOP_RETOUCH_FILL)
    {
      p->rt_forms[index].fill_color[0] = p->fill_color[0];
      p->rt_forms[index].fill_color[1] = p->fill_color[1];
      p->rt_forms[index].fill_color[2] = p->fill_color[2];
    }
  }

  rt_display_selected_fill_color(g, p);

  dt_dev_add_history_item(darktable.develop, self, TRUE);
}

static void rt_copypaste_scale_callback(GtkGestureSingle *gesture,
                                        int n_press,
                                        double x,
                                        double y,
                                        dt_iop_module_t *self)
{
  if(dt_atomic_get_int(&darktable.gui->reset) != 0) return;

  GtkWidget *widget = dt_gui_get_widget(gesture);

  int scale_copied = 0;
  const int active = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
  dt_iop_retouch_params_t *p = self->params;
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  if(widget == g->bt_copy_scale)
  {
    g->copied_scale = (active) ? p->curr_scale : -1;
  }
  else if(widget == g->bt_paste_scale)
  {
    rt_paste_forms_from_scale(p, g->copied_scale, p->curr_scale);
    rt_show_forms_for_current_scale(self);

    scale_copied = 1;
    g->copied_scale = -1;
  }

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_copy_scale),
                               g->copied_scale >= 0);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_paste_scale),
                               g->copied_scale >= 0);
  gtk_widget_set_sensitive(g->bt_paste_scale,
                           g->copied_scale >= 0);

  if(scale_copied) dt_dev_add_history_item(darktable.develop, self, TRUE);
}

static void rt_display_wavelet_scale_callback(GtkGestureSingle *gesture,
                                              int n_press,
                                              double x,
                                              double y,
                                              dt_iop_module_t *self)
{
  if(dt_atomic_get_int(&darktable.gui->reset) != 0) return;

  GtkWidget *widget = dt_gui_get_widget(gesture);

  dt_iop_retouch_params_t *p = self->params;
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  // if blend module is displaying mask do not display wavelet scales
  if((self->request_mask_display != DT_DEV_PIXELPIPE_DISPLAY_NONE)
     && !g->mask_display)
  {
    dt_control_log(_("cannot display scales when the blending mask is displayed"));

    DT_ENTER_GUI_UPDATE();
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), FALSE);
    DT_LEAVE_GUI_UPDATE();
    return;
  }

  if(self->off)
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), 1);
  dt_iop_request_focus(self);

  g->display_wavelet_scale = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));

  rt_show_hide_controls(self);

  // compute auto levels only the first time display wavelet scale is used,
  // only if levels values are the default
  // and a detail scale is displayed
  dt_iop_gui_enter_critical_section(self);
  if(g->displayed_wavelet_scale == 0
     && p->preview_levels[0] == RETOUCH_PREVIEW_LVL_MIN
     && p->preview_levels[1] == 0.f
     && p->preview_levels[2] == RETOUCH_PREVIEW_LVL_MAX
     && g->preview_auto_levels == 0
     && p->curr_scale > 0 && p->curr_scale <= p->num_scales)
  {
    g->preview_auto_levels = 1;
    g->displayed_wavelet_scale = 1;
  }
  dt_iop_gui_leave_critical_section(self);

  dt_dev_reprocess_center(self->dev, self->iop_order);

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), g->display_wavelet_scale);
}

static void rt_develop_ui_pipe_finished_callback(gpointer instance,
                                                 dt_iop_module_t *self)
{
  dt_iop_retouch_params_t *p = self->params;
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  // FIXME: this doesn't seems the right place to update params and GUI ...
  // update auto levels
  dt_iop_gui_enter_critical_section(self);
  if(g->preview_auto_levels == 2)
  {
    g->preview_auto_levels = -1;

    dt_iop_gui_leave_critical_section(self);

    for(int i = 0; i < 3; i++)
      p->preview_levels[i] = g->preview_levels[i];

    dt_dev_add_history_item(darktable.develop, self, TRUE);

    dt_iop_gui_enter_critical_section(self);

    // update the gradient slider
    double dlevels[3];
    for(int i = 0; i < 3; i++)
      dlevels[i] = p->preview_levels[i];

    DT_ENTER_GUI_UPDATE();
    dtgtk_gradient_slider_multivalue_set_values(g->preview_levels_gslider, dlevels);
    DT_LEAVE_GUI_UPDATE();

    g->preview_auto_levels = 0;
  }
  dt_iop_gui_leave_critical_section(self);

  // just in case zoom level has changed
  gtk_widget_queue_draw(GTK_WIDGET(g->wd_bar));
}

static void rt_auto_levels_callback(GtkGestureSingle *gesture,
                                    int n_press,
                                    double x,
                                    double y,
                                    dt_iop_module_t *self)
{
  if(dt_atomic_get_int(&darktable.gui->reset) != 0) return;

  dt_iop_retouch_gui_data_t *g = self->gui_data;

  if(self->off) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), 1);
  dt_iop_request_focus(self);

  dt_iop_gui_enter_critical_section(self);
  if(g->preview_auto_levels == 0)
  {
    g->preview_auto_levels = 1;
  }
  dt_iop_gui_leave_critical_section(self);

  dt_iop_refresh_center(self);
}

static void rt_mask_opacity_callback(GtkWidget *slider,
                                     dt_iop_module_t *self)
{
  DT_GUARD_GUI_UPDATE();

  const dt_mask_id_t shape_id = rt_get_selected_shape_id();

  if(dt_is_valid_maskid(shape_id))
  {
    const float opacity = dt_bauhaus_slider_get(slider);
    rt_masks_form_change_opacity(self, shape_id, opacity);
  }
}

void gui_post_expose(dt_iop_module_t *self,
                     cairo_t *cr,
                     const float width,
                     const float height,
                     const float pointerx,
                     const float pointery,
                     const float zoom_scale)
{
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  const dt_mask_id_t shape_id = rt_get_selected_shape_id();

  if(dt_is_valid_maskid(shape_id))
  {
    DT_ENTER_GUI_UPDATE();
    dt_masks_point_group_t *grpt = rt_get_mask_point_group(self, shape_id);
    if(grpt)
      dt_bauhaus_slider_set(g->sl_mask_opacity, grpt->opacity);
    DT_LEAVE_GUI_UPDATE();
  }
}

static void rt_edit_masks_callback(GtkGestureSingle *gesture,
                                     int n_press,
                                     double x,
                                     double y,
                                     dt_iop_module_t *self)
{
  if(dt_atomic_get_int(&darktable.gui->reset) != 0) return;

  // if we don't have the focus, request for it and quit, gui_focus() do the rest
  if(darktable.develop->gui_module != self)
  {
    dt_iop_request_focus(self);
    return;
  }

  /* for a shortcut-activated press dt_gui_current_button/state return the
   * action effect's button/state, for a real click the event's */
  const guint button = dt_gui_current_button(gesture);
  const GdkModifierType state = dt_gui_current_state(gesture);

  dt_iop_gui_blend_data_t *bd = self->blend_data;
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  //hide all shapes and free if some are in creation
  if(darktable.develop->form_gui->creation
     && darktable.develop->form_gui->creation_module == self)
    dt_masks_change_form_gui(NULL);

  if(darktable.develop->form_gui->creation_continuous_module == self)
  {
    darktable.develop->form_gui->creation_continuous = FALSE;
    darktable.develop->form_gui->creation_continuous_module = NULL;
  }

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_path), FALSE);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_circle), FALSE);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_ellipse), FALSE);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_brush), FALSE);

  if(button == GDK_BUTTON_PRIMARY)
  {
    DT_ENTER_GUI_UPDATE();

    dt_iop_color_picker_reset(self, TRUE);

    dt_masks_form_t *grp =
      dt_masks_get_from_id(darktable.develop, self->blend_params->mask_id);
    if(grp && (grp->type & DT_MASKS_GROUP) && grp->points)
    {
      const gboolean control_button_pressed =
        dt_modifier_is(state, GDK_CONTROL_MASK);

      switch(bd->masks_shown)
      {
        case DT_MASKS_EDIT_FULL:
          bd->masks_shown = control_button_pressed
            ? DT_MASKS_EDIT_RESTRICTED
            : DT_MASKS_EDIT_OFF;
          break;

        case DT_MASKS_EDIT_RESTRICTED:
          bd->masks_shown = !control_button_pressed
            ? DT_MASKS_EDIT_FULL
            : DT_MASKS_EDIT_OFF;
          break;

        default:
        case DT_MASKS_EDIT_OFF:
          bd->masks_shown = control_button_pressed
            ? DT_MASKS_EDIT_RESTRICTED
            : DT_MASKS_EDIT_FULL;
          break;
      }
    }
    else
      bd->masks_shown = DT_MASKS_EDIT_OFF;

    rt_show_forms_for_current_scale(self);

    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_edit_masks),
                                 (bd->masks_shown != DT_MASKS_EDIT_OFF)
                                 && (darktable.develop->gui_module == self));

    DT_LEAVE_GUI_UPDATE();
  }
}

static void rt_add_shape_callback(GtkGestureSingle *gesture,
                                    int n_press,
                                    double x,
                                    double y,
                                    dt_iop_module_t *self)
{
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  if(dt_atomic_get_int(&darktable.gui->reset) != 0) return;

  GtkWidget *widget = dt_gui_get_widget(gesture);

  dt_iop_color_picker_reset(self, TRUE);

  const gboolean creation_continuous =
    dt_modifier_is(dt_gui_current_state(gesture), GDK_CONTROL_MASK);

  rt_add_shape(widget, creation_continuous, self);

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_circle),
                               rt_shape_is_being_added(self, DT_MASKS_CIRCLE));
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_path),
                               rt_shape_is_being_added(self, DT_MASKS_PATH));
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_ellipse),
                               rt_shape_is_being_added(self, DT_MASKS_ELLIPSE));
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_brush),
                               rt_shape_is_being_added(self, DT_MASKS_BRUSH));
}

static void rt_select_algorithm_callback(GtkGestureSingle *gesture,
                                         int n_press,
                                         double x,
                                         double y,
                                         dt_iop_module_t *self)
{
  if(dt_atomic_get_int(&darktable.gui->reset) != 0) return;

  GtkWidget *widget = dt_gui_get_widget(gesture);

  dt_iop_retouch_params_t *p = self->params;
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  dt_iop_retouch_algo_type_t new_algo = DT_IOP_RETOUCH_HEAL;

  if(widget == g->bt_blur)
    new_algo = DT_IOP_RETOUCH_BLUR;
  else if(widget == g->bt_clone)
    new_algo = DT_IOP_RETOUCH_CLONE;
  else if(widget == g->bt_heal)
    new_algo = DT_IOP_RETOUCH_HEAL;
  else if(widget == g->bt_fill)
    new_algo = DT_IOP_RETOUCH_FILL;

  // check if we have to do something
  gboolean accept = TRUE;

  const GdkModifierType state = dt_gui_current_state(gesture);

  const int index = rt_get_selected_shape_index(p);
  if(index >= 0 && dt_modifier_is(state, GDK_CONTROL_MASK))
  {
    if(new_algo != p->rt_forms[index].algorithm)
    {
      // we restrict changes to clone<->heal and blur<->fill
      if((new_algo == DT_IOP_RETOUCH_CLONE
          && p->rt_forms[index].algorithm != DT_IOP_RETOUCH_HEAL)
         || (new_algo == DT_IOP_RETOUCH_HEAL
             && p->rt_forms[index].algorithm != DT_IOP_RETOUCH_CLONE)
         || (new_algo == DT_IOP_RETOUCH_BLUR
             && p->rt_forms[index].algorithm != DT_IOP_RETOUCH_FILL)
         || (new_algo == DT_IOP_RETOUCH_FILL
             && p->rt_forms[index].algorithm != DT_IOP_RETOUCH_BLUR))
      {
        accept = FALSE;
      }
    }
  }

  if(accept)
    p->algorithm = new_algo;

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_clone),
                               (p->algorithm == DT_IOP_RETOUCH_CLONE));
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_heal),
                               (p->algorithm == DT_IOP_RETOUCH_HEAL));
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_blur),
                               (p->algorithm == DT_IOP_RETOUCH_BLUR));
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_fill),
                               (p->algorithm == DT_IOP_RETOUCH_FILL));

  rt_show_hide_controls(self);

  if(!accept)
    return;

  if(index >= 0 && dt_modifier_is(state, GDK_CONTROL_MASK))
  {
    if(p->algorithm != p->rt_forms[index].algorithm)
    {
      p->rt_forms[index].algorithm = p->algorithm;
      dt_control_queue_redraw_center();
    }
  }
  else if(darktable.develop->form_gui->creation
          && (darktable.develop->form_gui->creation_module == self))
  {
    dt_iop_request_focus(self);

    dt_masks_type_t type = DT_MASKS_CIRCLE;
    if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->bt_path)))
      type = DT_MASKS_PATH;
    else if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->bt_circle)))
      type = DT_MASKS_CIRCLE;
    else if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->bt_ellipse)))
      type = DT_MASKS_ELLIPSE;
    else if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(g->bt_brush)))
      type = DT_MASKS_BRUSH;

    dt_masks_form_t *spot = NULL;
    if(p->algorithm == DT_IOP_RETOUCH_CLONE || p->algorithm == DT_IOP_RETOUCH_HEAL)
      spot = dt_masks_create(type | DT_MASKS_CLONE);
    else
      spot = dt_masks_create(type | DT_MASKS_NON_CLONE);
    dt_masks_change_form_gui(spot);
    darktable.develop->form_gui->creation_module = self;
    dt_control_queue_redraw_center();
  }

  dt_dev_add_history_item(darktable.develop, self, TRUE);

  // if we have the shift key pressed, we set it as default
  if(dt_modifier_is(state, GDK_SHIFT_MASK))
  {
    dt_conf_set_int("plugins/darkroom/retouch/default_algo", p->algorithm);
    // and we show a toat msg to confirm
    if(p->algorithm == DT_IOP_RETOUCH_CLONE)
      dt_control_log(_("default tool changed to %s"), _("cloning"));
    else if(p->algorithm == DT_IOP_RETOUCH_HEAL)
      dt_control_log(_("default tool changed to %s"), _("healing"));
    else if(p->algorithm == DT_IOP_RETOUCH_FILL)
      dt_control_log(_("default tool changed to %s"), _("fill"));
    else if(p->algorithm == DT_IOP_RETOUCH_BLUR)
      dt_control_log(_("default tool changed to %s"), _("blur"));
  }
}

static void rt_showmask_callback(GtkGestureSingle *gesture,
                                 int n_press,
                                 double x,
                                 double y,
                                 dt_iop_module_t *self)
{
  if(dt_atomic_get_int(&darktable.gui->reset) != 0) return;

  GtkWidget *widget = dt_gui_get_widget(gesture);

  dt_iop_retouch_gui_data_t *g = self->gui_data;

  // if blend module is displaying mask do not display it here
  if((self->request_mask_display != DT_DEV_PIXELPIPE_DISPLAY_NONE)
     && !g->mask_display)
  {
    dt_control_log(_("cannot display masks when the blending mask is displayed"));

    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), FALSE);
    return;
  }

  g->mask_display = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));

  if(self->off)
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), 1);
  dt_iop_request_focus(self);

  dt_iop_refresh_center(self);

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), g->mask_display);
}

static void rt_suppress_callback(GtkGestureSingle *gesture,
                                 int n_press,
                                 double x,
                                 double y,
                                 dt_iop_module_t *self)
{
  if(dt_atomic_get_int(&darktable.gui->reset) != 0) return;

  GtkWidget *widget = dt_gui_get_widget(gesture);

  dt_iop_retouch_gui_data_t *g = self->gui_data;
  g->suppress_mask = !gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));

  if(self->off) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), 1);
  dt_iop_request_focus(self);

  dt_iop_refresh_center(self);

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget), g->suppress_mask);
}

void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
{
  dt_iop_retouch_params_t *p = self->params;
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  if(w == g->cmb_fill_mode)
  {
    DT_ENTER_GUI_UPDATE();
    rt_show_hide_controls(self);
    DT_LEAVE_GUI_UPDATE();
  }
  else
  {
    const int index = rt_get_selected_shape_index(p);
    if(index >= 0)
    {
      if(p->rt_forms[index].algorithm == DT_IOP_RETOUCH_BLUR)
      {
        p->rt_forms[index].blur_type = p->blur_type;
        p->rt_forms[index].blur_radius = p->blur_radius;
      }
      else if(p->rt_forms[index].algorithm == DT_IOP_RETOUCH_FILL)
      {
        p->rt_forms[index].fill_mode = p->fill_mode;
        p->rt_forms[index].fill_brightness = p->fill_brightness;
      }
    }
  }
}

//---------------------------------------------------------------------------------------
// GUI
//---------------------------------------------------------------------------------------

void masks_selection_changed(dt_iop_module_t *self, const int form_selected_id)
{
  dt_iop_retouch_gui_data_t *g = self->gui_data;
  if(!g) return;

  dt_iop_gui_enter_critical_section(self);
  rt_shape_selection_changed(self);
  dt_iop_gui_leave_critical_section(self);
}

void init(dt_iop_module_t *self)
{
  dt_iop_default_init(self);

  dt_iop_retouch_params_t *d = self->default_params;

  d->preview_levels[0] = RETOUCH_PREVIEW_LVL_MIN;
  d->preview_levels[1] = 0.f;
  d->preview_levels[2] = RETOUCH_PREVIEW_LVL_MAX;
  d->algorithm = dt_conf_get_int("plugins/darkroom/retouch/default_algo");
}

void init_global(dt_iop_module_so_t *self)
{
  const int program = 21; // retouch.cl, from programs.conf
  dt_iop_retouch_global_data_t *gd = malloc(sizeof(dt_iop_retouch_global_data_t));
  self->data = gd;
  gd->kernel_retouch_clear_alpha =
    dt_opencl_create_kernel(program, "retouch_clear_alpha");
  gd->kernel_retouch_copy_alpha =
    dt_opencl_create_kernel(program, "retouch_copy_alpha");
  gd->kernel_retouch_copy_buffer_to_buffer =
    dt_opencl_create_kernel(program, "retouch_copy_buffer_to_buffer");
  gd->kernel_retouch_copy_buffer_to_image =
    dt_opencl_create_kernel(program, "retouch_copy_buffer_to_image");
  gd->kernel_retouch_fill =
    dt_opencl_create_kernel(program, "retouch_fill");
  gd->kernel_retouch_copy_image_to_buffer_masked =
    dt_opencl_create_kernel(program, "retouch_copy_image_to_buffer_masked");
  gd->kernel_retouch_copy_buffer_to_buffer_masked =
    dt_opencl_create_kernel(program, "retouch_copy_buffer_to_buffer_masked");
  gd->kernel_retouch_image_rgb2lab =
    dt_opencl_create_kernel(program, "retouch_image_rgb2lab");
  gd->kernel_retouch_image_lab2rgb =
    dt_opencl_create_kernel(program, "retouch_image_lab2rgb");
  gd->kernel_retouch_copy_mask_to_alpha =
    dt_opencl_create_kernel(program, "retouch_copy_mask_to_alpha");
}

void cleanup_global(dt_iop_module_so_t *self)
{
  dt_iop_retouch_global_data_t *gd = self->data;

  dt_opencl_free_kernel(gd->kernel_retouch_clear_alpha);
  dt_opencl_free_kernel(gd->kernel_retouch_copy_alpha);
  dt_opencl_free_kernel(gd->kernel_retouch_copy_buffer_to_buffer);
  dt_opencl_free_kernel(gd->kernel_retouch_copy_buffer_to_image);
  dt_opencl_free_kernel(gd->kernel_retouch_fill);
  dt_opencl_free_kernel(gd->kernel_retouch_copy_image_to_buffer_masked);
  dt_opencl_free_kernel(gd->kernel_retouch_copy_buffer_to_buffer_masked);
  dt_opencl_free_kernel(gd->kernel_retouch_image_rgb2lab);
  dt_opencl_free_kernel(gd->kernel_retouch_image_lab2rgb);
  dt_opencl_free_kernel(gd->kernel_retouch_copy_mask_to_alpha);

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

void gui_focus(dt_iop_module_t *self, const gboolean in)
{
  if(self->enabled
     && !darktable.develop->full.pipe->loading)
  {
    dt_iop_retouch_gui_data_t *g = self->gui_data;

    if(in)
    {
      dt_iop_gui_blend_data_t *bd = self->blend_data;
      //only show shapes if shapes exist
      dt_masks_form_t *grp =
        dt_masks_get_from_id(darktable.develop, self->blend_params->mask_id);
      if(grp && (grp->type & DT_MASKS_GROUP) && grp->points)
      {
        // got focus, show all shapes
        if(bd->masks_shown == DT_MASKS_EDIT_OFF)
          dt_masks_set_edit_mode(self, DT_MASKS_EDIT_FULL);

        rt_show_forms_for_current_scale(self);

        gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_edit_masks),
                                     (bd->masks_shown != DT_MASKS_EDIT_OFF)
                                     && (darktable.develop->gui_module == self));
      }
    }
    else
    {
      // lost focus, hide all shapes and free if some are in creation
      if(darktable.develop->form_gui->creation
         && darktable.develop->form_gui->creation_module == self)
        dt_masks_change_form_gui(NULL);

      if(darktable.develop->form_gui->creation_continuous_module == self)
      {
        darktable.develop->form_gui->creation_continuous = FALSE;
        darktable.develop->form_gui->creation_continuous_module = NULL;
      }

      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_path), FALSE);
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_circle), FALSE);
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_ellipse), FALSE);
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_brush), FALSE);
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_edit_masks), FALSE);

      dt_masks_set_edit_mode(self, DT_MASKS_EDIT_OFF);
    }

    // if we are switching between display modes we have to reprocess the main image
    if(g->display_wavelet_scale
       || g->mask_display
       || g->suppress_mask)
      dt_iop_refresh_center(self);
  }
}

void tiling_callback(dt_iop_module_t *self,
                     dt_dev_pixelpipe_iop_t *piece,
                     const dt_iop_roi_t *roi_in,
                     const dt_iop_roi_t *roi_out,
                     dt_develop_tiling_t *tiling)
{
  dt_iop_retouch_params_t *p = self->params;
  const float require = 2.0f;
  const float require_cl = 1.0f  // in_retouch
     + ((p->num_scales > 0) ? 4.0f : 2.0f); // dwt_wavelet_decompose_cl
                                            // requires 4 buffers,
                                            // otherwise 2.0f is
                                            // enough

  // FIXME the above are worst case values, we might iterate through
  // the dt_iop_retouch_form_data_t to get the largest bounding box

  tiling->factor = 2.0f + require; // input & output buffers + internal requirements
  tiling->factor_cl = 2.0f + require_cl;
  tiling->maxbuf = 1.0f;
  tiling->maxbuf_cl = 1.0f;
  tiling->overhead = 0;
  tiling->overlap = 0;
  tiling->align = 1;
}

void init_pipe(dt_iop_module_t *self,
               dt_dev_pixelpipe_t *pipe,
               dt_dev_pixelpipe_iop_t *piece)
{
  piece->data = malloc(sizeof(dt_iop_retouch_data_t));
}

void cleanup_pipe(dt_iop_module_t *self,
                  dt_dev_pixelpipe_t *pipe,
                  dt_dev_pixelpipe_iop_t *piece)
{
  free(piece->data);
  piece->data = NULL;
}

void gui_update(dt_iop_module_t *self)
{
  dt_iop_retouch_gui_data_t *g = self->gui_data;
  dt_iop_retouch_params_t *p = self->params;

  // check if there is new or deleted forms
  rt_resynch_params(self);

  if(darktable.develop->form_gui->creation_continuous
     && darktable.develop->form_gui->creation_continuous_module == self
     && !rt_allow_create_form(self))
  {
    dt_masks_change_form_gui(NULL);
    darktable.develop->form_gui->creation_continuous = FALSE;
    darktable.develop->form_gui->creation_continuous_module = NULL;
  }

  // update clones count
  const dt_masks_form_t *grp =
    dt_masks_get_from_id(self->dev, self->blend_params->mask_id);
  guint nb = 0;
  if(grp && (grp->type & DT_MASKS_GROUP))
    nb = g_list_length(grp->points);
  gchar *str = g_strdup_printf("%u", nb);
  gtk_label_set_text(g->label_form, str);
  g_free(str);

  // update wavelet decompose labels
  rt_update_wd_bar_labels(p, g);

  // update selected shape label
  rt_display_selected_shapes_lbl(g);

  // show the shapes for the current scale
  rt_show_forms_for_current_scale(self);

  // update algorithm toolbar
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_clone),
                               p->algorithm == DT_IOP_RETOUCH_CLONE);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_blur),
                               p->algorithm == DT_IOP_RETOUCH_BLUR);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_heal),
                               p->algorithm == DT_IOP_RETOUCH_HEAL);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_fill),
                               p->algorithm == DT_IOP_RETOUCH_FILL);

  // update shapes toolbar
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_circle),
                               rt_shape_is_being_added(self, DT_MASKS_CIRCLE));
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_path),
                               rt_shape_is_being_added(self, DT_MASKS_PATH));
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_ellipse),
                               rt_shape_is_being_added(self, DT_MASKS_ELLIPSE));
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_brush),
                               rt_shape_is_being_added(self, DT_MASKS_BRUSH));

  // update masks related buttons
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_showmask), g->mask_display);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_suppress), g->suppress_mask);

  // update the rest of the fields
  gtk_widget_queue_draw(GTK_WIDGET(g->wd_bar));

  dt_bauhaus_combobox_set(g->cmb_blur_type, p->blur_type);
  dt_bauhaus_slider_set(g->sl_blur_radius, p->blur_radius);
  dt_bauhaus_slider_set(g->sl_fill_brightness, p->fill_brightness);
  dt_bauhaus_combobox_set(g->cmb_fill_mode, p->fill_mode);

  rt_display_selected_fill_color(g, p);

  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_display_wavelet_scale),
                               g->display_wavelet_scale);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_copy_scale),
                               g->copied_scale >= 0);
  gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_paste_scale),
                               g->copied_scale >= 0);
  gtk_widget_set_sensitive(g->bt_paste_scale,
                           g->copied_scale >= 0);

  // show/hide some fields
  rt_show_hide_controls(self);

  // update edit shapes status
  dt_iop_gui_blend_data_t *bd = self->blend_data;
  if(darktable.develop->history_updating)
    bd->masks_shown = DT_MASKS_EDIT_OFF;

  //only toggle shape show button if shapes exist
  if(grp && (grp->type & DT_MASKS_GROUP) && grp->points)
  {
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_edit_masks),
                                 (bd->masks_shown != DT_MASKS_EDIT_OFF)
                                 && (darktable.develop->gui_module == self));
  }
  else
  {
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->bt_edit_masks), FALSE);
  }

  // update the gradient slider
  double dlevels[3];
  for(int i = 0; i < 3; i++)
    dlevels[i] = p->preview_levels[i];
  dtgtk_gradient_slider_multivalue_set_values(g->preview_levels_gslider, dlevels);
}

void change_image(dt_iop_module_t *self)
{
  dt_iop_retouch_gui_data_t *g = self->gui_data;
  if(g)
  {
    g->copied_scale = -1;
    g->mask_display = FALSE;
    g->suppress_mask = FALSE;
    g->display_wavelet_scale = FALSE;
    g->displayed_wavelet_scale = 0;
    g->first_scale_visible = RETOUCH_MAX_SCALES + 1;

    g->preview_auto_levels = 0;
    g->preview_levels[0] = RETOUCH_PREVIEW_LVL_MIN;
    g->preview_levels[1] = 0.f;
    g->preview_levels[2] = RETOUCH_PREVIEW_LVL_MAX;

    g->is_dragging = 0;
    g->wdbar_mouse_x = g->wdbar_mouse_y = -1;
    g->curr_scale = -1;
    g->lower_cursor = g->upper_cursor = FALSE;
    g->lower_margin = g->upper_margin = FALSE;
  }
}

void gui_init(dt_iop_module_t *self)
{
  dt_iop_retouch_gui_data_t *g = IOP_GUI_ALLOC(retouch);
  const dt_iop_retouch_params_t *const p = self->default_params;

  change_image(self);

  // shapes toolbar
  g->label_form = GTK_LABEL(gtk_label_new("-1"));
  GtkWidget *hbox_shapes = dt_gui_hbox(dt_ui_label_new(_("shapes:")), g->label_form);
  gtk_widget_set_tooltip_text
    (hbox_shapes,
     _("to add a shape select an algorithm and a shape type and click on the image.\n"
       "shapes are added to the current scale"));

  g->bt_edit_masks = dt_iop_togglebutton_new
    (self, N_("editing"),
     N_("show and edit shapes on the current scale"),
     N_("show and edit shapes in restricted mode"),
     G_CALLBACK(rt_edit_masks_callback), TRUE, 0, 0,
     dtgtk_cairo_paint_masks_eye, hbox_shapes);

  g->bt_brush = dt_iop_togglebutton_new
    (self, N_("shapes"),
     N_("add brush"), N_("add multiple brush strokes"),
     G_CALLBACK(rt_add_shape_callback), TRUE, 0, 0,
     dtgtk_cairo_paint_masks_brush, hbox_shapes);

  g->bt_path = dt_iop_togglebutton_new
    (self, N_("shapes"), N_("add path"), N_("add multiple paths"),
     G_CALLBACK(rt_add_shape_callback), TRUE, 0, 0,
     dtgtk_cairo_paint_masks_path, hbox_shapes);

  g->bt_ellipse = dt_iop_togglebutton_new
    (self, N_("shapes"), N_("add ellipse"), N_("add multiple ellipses"),
     G_CALLBACK(rt_add_shape_callback), TRUE, 0, 0,
     dtgtk_cairo_paint_masks_ellipse, hbox_shapes);

  g->bt_circle = dt_iop_togglebutton_new
    (self, N_("shapes"), N_("add circle"), N_("add multiple circles"),
     G_CALLBACK(rt_add_shape_callback), TRUE, 0, 0,
     dtgtk_cairo_paint_masks_circle, hbox_shapes);

  // algorithm toolbar
  GtkWidget *hbox_algo = dt_gui_hbox(dt_ui_label_new(_("algorithms:")));

  g->bt_blur = dt_iop_togglebutton_new(
      self, N_("tools"), N_("activate blur tool"), NULL,
      G_CALLBACK(rt_select_algorithm_callback),
      TRUE, 0, 0, dtgtk_cairo_paint_tool_blur, hbox_algo);

  g->bt_fill = dt_iop_togglebutton_new(
      self, N_("tools"), N_("activate fill tool"), NULL,
      G_CALLBACK(rt_select_algorithm_callback),
      TRUE, 0, 0, dtgtk_cairo_paint_tool_fill, hbox_algo);

  g->bt_clone = dt_iop_togglebutton_new(
      self, N_("tools"), N_("activate cloning tool"), NULL,
      G_CALLBACK(rt_select_algorithm_callback),
      TRUE, 0, 0, dtgtk_cairo_paint_tool_clone, hbox_algo);

  g->bt_heal = dt_iop_togglebutton_new(
      self, N_("tools"), N_("activate healing tool"), NULL,
      G_CALLBACK(rt_select_algorithm_callback),
      TRUE, 0, 0, dtgtk_cairo_paint_tool_heal, hbox_algo);

  // overwrite tooltip ourself to handle shift+click
  gchar b[1000];
  gchar *c = _("ctrl+click to change tool for current form");
  gchar *s = _("shift+click to set the tool as default");
  gtk_widget_set_tooltip_text(g->bt_blur , dt_buf_printf(b, "%s\n%s\n%s", _("activate blur tool"), c, s));
  gtk_widget_set_tooltip_text(g->bt_fill , dt_buf_printf(b, "%s\n%s\n%s", _("activate fill tool"), c, s));
  gtk_widget_set_tooltip_text(g->bt_clone, dt_buf_printf(b, "%s\n%s\n%s", _("activate cloning tool"), c, s));
  gtk_widget_set_tooltip_text(g->bt_heal , dt_buf_printf(b, "%s\n%s\n%s", _("activate healing tool"), c, s));

  // wavelet decompose bar labels
  GtkWidget *grid_wd_labels = gtk_grid_new();
  gtk_grid_set_column_homogeneous(GTK_GRID(grid_wd_labels), FALSE);

  gtk_grid_attach(GTK_GRID(grid_wd_labels),
                  dt_ui_label_new(_("scales:")), 0, 0, 1, 1);
  g->lbl_num_scales = GTK_LABEL(dt_ui_label_new(NULL));
  gtk_label_set_width_chars(g->lbl_num_scales, 2);
  gtk_grid_attach(GTK_GRID(grid_wd_labels),
                  GTK_WIDGET(g->lbl_num_scales), 1, 0, 1, 1);

  gtk_grid_attach(GTK_GRID(grid_wd_labels),
                  dt_ui_label_new(_("current:")), 0, 1, 1, 1);
  g->lbl_curr_scale = GTK_LABEL(dt_ui_label_new(NULL));
  gtk_label_set_width_chars(g->lbl_curr_scale, 2);
  gtk_grid_attach(GTK_GRID(grid_wd_labels),
                  GTK_WIDGET(g->lbl_curr_scale), 1, 1, 1, 1);

  gtk_grid_attach(GTK_GRID(grid_wd_labels),
                  dt_ui_label_new(_("merge from:")), 0, 2, 1, 1);
  g->lbl_merge_from_scale = GTK_LABEL(dt_ui_label_new(NULL));
  gtk_label_set_width_chars(g->lbl_merge_from_scale, 2);
  gtk_grid_attach(GTK_GRID(grid_wd_labels),
                  GTK_WIDGET(g->lbl_merge_from_scale), 1, 2, 1, 1);

  // wavelet decompose bar
  g->wd_bar = gtk_drawing_area_new();

  gtk_widget_set_tooltip_text
    (g->wd_bar,
     _("top slider adjusts where the merge scales start\n"
       "bottom slider adjusts the number of scales\n"
       "dot indicates the current scale\n"
       "top line indicates that the scale is visible at current zoom level\n"
       "bottom line indicates that the scale has shapes on it"));

  g_signal_connect(G_OBJECT(g->wd_bar), "draw",
                   G_CALLBACK(rt_wdbar_draw), self);
  dt_gui_connect_click(g->wd_bar, rt_wdbar_button_press, rt_wdbar_button_release, self);
  dt_gui_connect_motion(g->wd_bar, rt_wdbar_motion_notify, NULL, rt_wdbar_leave_notify, self);
  dt_gui_connect_scroll(g->wd_bar, GTK_EVENT_CONTROLLER_SCROLL_BOTH_AXES
                                   | GTK_EVENT_CONTROLLER_SCROLL_DISCRETE,
                        rt_wdbar_scrolled, self);
  gtk_widget_set_size_request(g->wd_bar, -1, DT_PIXEL_APPLY_DPI(40));

  // toolbar display current scale / cut&paste / suppress&display masks

  // display & suppress masks
  GtkWidget *scale_end = dt_gui_hbox();
  g->bt_showmask = dt_iop_togglebutton_new
    (self, N_("editing"), N_("display masks"), NULL,
     G_CALLBACK(rt_showmask_callback), TRUE, 0, 0,
     dtgtk_cairo_paint_showmask, scale_end);
  dt_gui_add_class(g->bt_showmask, "dt_transparent_background");

  g->bt_suppress = dt_iop_togglebutton_new
    (self, N_("editing"), N_("temporarily switch off shapes"), NULL,
     G_CALLBACK(rt_suppress_callback), TRUE, 0, 0,
     dtgtk_cairo_paint_eye_toggle, scale_end);
  dt_gui_add_class(g->bt_suppress, "dt_transparent_background");

  // copy/paste shapes
  GtkWidget *scale_middle = dt_gui_hbox();
  g->bt_paste_scale = dt_iop_togglebutton_new
    (self, N_("editing"), N_("paste cut shapes to current scale"), NULL,
     G_CALLBACK(rt_copypaste_scale_callback), TRUE, 0, 0,
     dtgtk_cairo_paint_paste_forms, scale_middle);

  g->bt_copy_scale = dt_iop_togglebutton_new
    (self, N_("editing"), N_("cut shapes from current scale"), NULL,
     G_CALLBACK(rt_copypaste_scale_callback), TRUE, 0, 0,
     dtgtk_cairo_paint_cut_forms, scale_middle);

  // display final image/current scale
  GtkWidget *scale_start = dt_gui_hbox();
  g->bt_display_wavelet_scale = dt_iop_togglebutton_new
    (self, N_("editing"), N_("display wavelet scale"), NULL,
     G_CALLBACK(rt_display_wavelet_scale_callback), TRUE, 0, 0,
     dtgtk_cairo_paint_display_wavelet_scale, scale_start);
  dt_gui_add_class(g->bt_display_wavelet_scale, "dt_transparent_background");

  // preview single scale
  // gradient slider
  #define NEUTRAL_GRAY 0.5
  static const GdkRGBA _gradient_L[]
      = { { 0, 0, 0, 1.0 }, { NEUTRAL_GRAY, NEUTRAL_GRAY, NEUTRAL_GRAY, 1.0 } };
  GtkDarktableGradientSlider *gslider = g->preview_levels_gslider =
    DTGTK_GRADIENT_SLIDER_MULTIVALUE
    (dtgtk_gradient_slider_multivalue_new_with_color_and_name
     (_gradient_L[0], _gradient_L[1], 3, "preview-levels"));
  gtk_widget_set_tooltip_text(GTK_WIDGET(gslider), _("adjust preview levels"));
  dtgtk_gradient_slider_multivalue_set_marker(gslider, GRADIENT_SLIDER_MARKER_LOWER_OPEN_BIG, 0);
  dtgtk_gradient_slider_multivalue_set_marker(gslider, GRADIENT_SLIDER_MARKER_LOWER_FILLED_BIG, 1);
  dtgtk_gradient_slider_multivalue_set_marker(gslider, GRADIENT_SLIDER_MARKER_LOWER_OPEN_BIG, 2);
  (gslider)->scale_callback = rt_gslider_scale_callback;
  double vdefault[3] = {RETOUCH_PREVIEW_LVL_MIN,
                        (RETOUCH_PREVIEW_LVL_MIN + RETOUCH_PREVIEW_LVL_MAX) / 2.0,
                        RETOUCH_PREVIEW_LVL_MAX};
  dtgtk_gradient_slider_multivalue_set_values(gslider, vdefault);
  dtgtk_gradient_slider_multivalue_set_resetvalues(gslider, vdefault);
  (gslider)->markers_type = PROPORTIONAL_MARKERS;
  (gslider)->min_spacing = 0.05;
  g_signal_connect(G_OBJECT(gslider), "value-changed",
                   G_CALLBACK(rt_gslider_changed), self);

  // auto-levels button
  g->bt_auto_levels = dt_iop_togglebutton_new
    (self, N_("editing"), N_("auto levels"), NULL,
     G_CALLBACK(rt_auto_levels_callback), TRUE, 0, 0,
     dtgtk_cairo_paint_auto_levels, NULL);

  g->vbox_preview_scale = dt_gui_vbox(dt_ui_section_label_new(C_("section", "preview single scale")),
                                      dt_gui_hbox(dt_gui_expand(gslider), g->bt_auto_levels));

  // shapes selected (label)
  GtkWidget *label1 = gtk_label_new(_("shape selected:"));
  gtk_label_set_ellipsize(GTK_LABEL(label1), PANGO_ELLIPSIZE_START);
  g->label_form_selected = GTK_LABEL(gtk_label_new("-1"));
  GtkWidget *hbox_shape_sel = dt_gui_hbox(label1, g->label_form_selected);
  gtk_widget_set_tooltip_text
    (hbox_shape_sel,
     _("click on a shape to select it,\nto unselect click on an empty space"));

  // fill properties
  g->vbox_fill = self->widget = dt_gui_vbox();

  g->cmb_fill_mode = dt_bauhaus_combobox_from_params(self, "fill_mode");
  gtk_widget_set_tooltip_text
    (g->cmb_fill_mode, _("erase the detail or fills with chosen color"));

  // color for fill algorithm
  GdkRGBA color
      = (GdkRGBA){.red   = p->fill_color[0],
                  .green = p->fill_color[1],
                  .blue  = p->fill_color[2],
                  .alpha = 1.0 };

  g->colorpick = gtk_color_button_new_with_rgba(&color);
  gtk_color_chooser_set_use_alpha(GTK_COLOR_CHOOSER(g->colorpick), FALSE);
  gtk_color_button_set_title(GTK_COLOR_BUTTON(g->colorpick), _("select fill color"));
  gtk_widget_set_tooltip_text(g->colorpick, _("select fill color"));
  g_signal_connect(G_OBJECT(g->colorpick), "color-set",
                   G_CALLBACK(rt_colorpick_color_set_callback), self);

  g->colorpicker = dt_color_picker_new
    (self,
     DT_COLOR_PICKER_POINT | DT_COLOR_PICKER_IO,
     NULL);
  gtk_widget_set_tooltip_text(g->colorpicker, _("pick fill color from image"));
  dt_action_define_iop(self, NULL, N_("pick fill color"),
                       g->colorpicker, &dt_action_def_color_picker);

  g->hbox_color_pick = dt_gui_hbox(dt_ui_label_new(_("fill color: ")),
                                   dt_gui_expand(g->colorpick), g->colorpicker);
  dt_gui_box_add(g->vbox_fill, g->hbox_color_pick);

  g->sl_fill_brightness = dt_bauhaus_slider_from_params(self, "fill_brightness");
  dt_bauhaus_slider_set_digits(g->sl_fill_brightness, 4);
  dt_bauhaus_slider_set_format(g->sl_fill_brightness, "%");
  gtk_widget_set_tooltip_text
    (g->sl_fill_brightness,
     _("adjusts color brightness to fine-tune it. works with erase as well"));

  // blur properties
  g->vbox_blur = self->widget = dt_gui_vbox();

  g->cmb_blur_type = dt_bauhaus_combobox_from_params(self, "blur_type");
  gtk_widget_set_tooltip_text(g->cmb_blur_type, _("type for the blur algorithm"));

  g->sl_blur_radius = dt_bauhaus_slider_from_params(self, "blur_radius");
  dt_bauhaus_slider_set_format(g->sl_blur_radius, _(" px"));
  gtk_widget_set_tooltip_text(g->sl_blur_radius, _("radius of the selected blur type"));

  // mask opacity
  g->sl_mask_opacity = dt_bauhaus_slider_new_with_range(self, 0.0, 1.0, 0, 1., 3);
  dt_bauhaus_widget_set_label(g->sl_mask_opacity, NULL, N_("mask opacity"));
  dt_bauhaus_slider_set_format(g->sl_mask_opacity, "%");
  gtk_widget_set_tooltip_text(g->sl_mask_opacity,
                              _("set the opacity on the selected shape"));
  g_signal_connect(G_OBJECT(g->sl_mask_opacity), "value-changed",
                   G_CALLBACK(rt_mask_opacity_callback), self);

  // start building top level widget
  self->widget = dt_gui_vbox
    (dt_ui_section_label_new(C_("section", "retouch tools")),
     hbox_shapes, hbox_algo,
     dt_ui_section_label_new(C_("section", "wavelet decompose")),
     grid_wd_labels, g->wd_bar,
     dt_gui_hbox(scale_start, dt_gui_expand(scale_middle), dt_gui_expand(scale_end)),
     g->vbox_preview_scale,
     dt_ui_section_label_new(C_("section", "shapes")),
     hbox_shape_sel, g->vbox_blur, g->vbox_fill, g->sl_mask_opacity);

  /* add signal handler for preview pipe finish to redraw the preview */
  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_UI_PIPE_FINISHED, rt_develop_ui_pipe_finished_callback);
}

void gui_reset(dt_iop_module_t *self)
{
  // hide the previous masks
  dt_masks_reset_form_gui();
  // set the algo to the default one
  dt_iop_retouch_params_t *p = self->params;
  p->algorithm = dt_conf_get_int("plugins/darkroom/retouch/default_algo");
}

void reload_defaults(dt_iop_module_t *self)
{
  // set the algo to the default one
  dt_iop_retouch_params_t *p = self->default_params;
  p->algorithm = dt_conf_get_int("plugins/darkroom/retouch/default_algo");
}

static void rt_compute_roi_in(dt_iop_module_t *self,
                              dt_dev_pixelpipe_iop_t *piece,
                              dt_iop_roi_t *roi_in,
                              int *_roir,
                              int *_roib,
                              int *_roix,
                              int *_roiy)
{
  dt_iop_retouch_params_t *p = piece->data;
  dt_develop_blend_params_t *bp = piece->blendop_data;

  int roir = *_roir;
  int roib = *_roib;
  int roix = *_roix;
  int roiy = *_roiy;

  // We iterate through all forms
  const dt_masks_form_t *grp = dt_masks_get_from_id_ext(piece->pipe->forms, bp->mask_id);
  if(grp && (grp->type & DT_MASKS_GROUP))
  {
    for(const GList *forms = grp->points; forms; forms = g_list_next(forms))
    {
      const dt_masks_point_group_t *grpt = forms->data;
      if(grpt)
      {
        const dt_mask_id_t formid = grpt->formid;
        const int index = rt_get_index_from_formid(p, formid);
        if(p->rt_forms[index].algorithm == DT_IOP_RETOUCH_FILL)
        {
          continue;
        }

        // we get the spot
        dt_masks_form_t *form = dt_masks_get_from_id_ext(piece->pipe->forms, formid);
        if(form)
        {
          // if the form is outside the roi, we just skip it
          // we get the area for the form
          int fl, ft, fw, fh;
          if(!dt_masks_get_area(self, piece, form, &fw, &fh, &fl, &ft))
          {
            continue;
          }

          // is the form outside of the roi?
          fw *= roi_in->scale;
          fh *= roi_in->scale;
          fl *= roi_in->scale;
          ft *= roi_in->scale;

          if(ft >= roi_in->y + roi_in->height
             || ft + fh <= roi_in->y
             || fl >= roi_in->x + roi_in->width
             || fl + fw <= roi_in->x)
          {
            continue;
          }

          // heal need the entire area
          if(p->rt_forms[index].algorithm == DT_IOP_RETOUCH_HEAL)
          {
            // we enlarge the roi if needed
            roiy = fminf(ft, roiy);
            roix = fminf(fl, roix);
            roir = fmaxf(fl + fw, roir);
            roib = fmaxf(ft + fh, roib);
          }
          // blur need an overlap of 4 * radius (scaled)
          if(p->rt_forms[index].algorithm == DT_IOP_RETOUCH_BLUR)
          {
            if(index >= 0)
            {
              const int overlap =
                ceilf(4 * (p->rt_forms[index].blur_radius * roi_in->scale / piece->iscale));
              if(roiy > ft)
                roiy = MAX(roiy - overlap, ft);
              if(roix > fl)
                roix = MAX(roix - overlap, fl);
              if(roir < fl + fw)
                roir = MAX(roir + overlap, fl + fw);
              if(roib < ft + fh)
                roib = MAX(roib + overlap, ft + fh);
            }
          }
          // heal and clone need both source and destination areas
          if(p->rt_forms[index].algorithm == DT_IOP_RETOUCH_HEAL
             || p->rt_forms[index].algorithm == DT_IOP_RETOUCH_CLONE)
          {
            float dx = 0.f, dy = 0.f, angle = 0.f;
            if(rt_masks_get_transform_to_destination(
                 self, piece, roi_in, form, &dx, &dy, &angle, p->rt_forms[index].distort_mode))
            {
              // source region = destination box translated by (-dx, -dy)
              roiy = fminf(ft - dy, roiy);
              roix = fminf(fl - dx, roix);
              roir = fmaxf(fl + fw - dx, roir);
              roib = fmaxf(ft + fh - dy, roib);

              // when the source is rotated, the box above does not bound the
              // rotated footprint. dt_masks_get_source_area already applies the
              // source rotation (it goes through the source point generation), so
              // it gives the true rotated source bbox -- union it in.
              if(!feqf(angle, 0.0f, 0.01f))
              {
                int sfl, sft, sfw, sfh;
                if(dt_masks_get_source_area(self, piece, form, &sfw, &sfh, &sfl, &sft))
                {
                  sfw *= roi_in->scale;
                  sfh *= roi_in->scale;
                  sfl *= roi_in->scale;
                  sft *= roi_in->scale;
                  roiy = fminf(sft, roiy);
                  roix = fminf(sfl, roix);
                  roir = fmaxf(sfl + sfw, roir);
                  roib = fmaxf(sft + sfh, roib);
                }
              }
            }
          }
        }
      }
    }
  }

  *_roir = roir;
  *_roib = roib;
  *_roix = roix;
  *_roiy = roiy;
}

// for a given form, if a previous clone/heal destination intersects the source area,
// include that area in roi_in too
static void rt_extend_roi_in_from_source_clones(dt_iop_module_t *self,
                                                dt_dev_pixelpipe_iop_t *piece,
                                                dt_iop_roi_t *roi_in,
                                                const dt_mask_id_t formid_src,
                                                const int fl_src,
                                                const int ft_src,
                                                const int fw_src,
                                                const int fh_src,
                                                int *_roir,
                                                int *_roib,
                                                int *_roix,
                                                int *_roiy)
{
  dt_iop_retouch_params_t *p = piece->data;
  dt_develop_blend_params_t *bp = piece->blendop_data;

  int roir = *_roir;
  int roib = *_roib;
  int roix = *_roix;
  int roiy = *_roiy;

  // We iterate through all forms
  const dt_masks_form_t *grp = dt_masks_get_from_id_ext(piece->pipe->forms, bp->mask_id);

  if(grp && (grp->type & DT_MASKS_GROUP))
  {
    for(const GList *forms = grp->points; forms; forms = g_list_next(forms))
    {
      const dt_masks_point_group_t *grpt = forms->data;
      if(grpt)
      {
        const dt_mask_id_t formid = grpt->formid;

        // just need the previous forms
        if(formid == formid_src) break;

        const int index = rt_get_index_from_formid(p, formid);

        // only process clone and heal
        if(p->rt_forms[index].algorithm != DT_IOP_RETOUCH_HEAL
           && p->rt_forms[index].algorithm != DT_IOP_RETOUCH_CLONE)
        {
          continue;
        }

        // we get the spot
        dt_masks_form_t *form = dt_masks_get_from_id_ext(piece->pipe->forms, formid);
        if(form)
        {
          // we get the source area
          int fl, ft, fw, fh;
          if(!dt_masks_get_source_area(self, piece, form, &fw, &fh, &fl, &ft))
          {
            continue;
          }
          fw *= roi_in->scale;
          fh *= roi_in->scale;
          fl *= roi_in->scale;
          ft *= roi_in->scale;

          // get the destination area
          int fl_dest, ft_dest;
          float dx = 0.f, dy = 0.f;
          if(!rt_masks_get_transform_to_destination(self, piece, roi_in, form, &dx, &dy, NULL,
                                                p->rt_forms[index].distort_mode))
          {
            continue;
          }

          ft_dest = ft + dy;
          fl_dest = fl + dx;

          // check if the destination of this form intersects the source of the formid_src
          const int intersects = !(ft_dest + fh < ft_src
                                   || ft_src + fh_src < ft_dest
                                   || fl_dest + fw < fl_src
                                   || fl_src + fw_src < fl_dest);
          if(intersects)
          {
            // we enlarge the roi if needed
            roiy = fminf(ft, roiy);
            roix = fminf(fl, roix);
            roir = fmaxf(fl + fw, roir);
            roib = fmaxf(ft + fh, roib);

            // need both source and destination areas
            roiy = fminf(ft + dy, roiy);
            roix = fminf(fl + dx, roix);
            roir = fmaxf(fl + fw + dx, roir);
            roib = fmaxf(ft + fh + dy, roib);
          }
        }
      }
    }
  }

  *_roir = roir;
  *_roib = roib;
  *_roix = roix;
  *_roiy = roiy;
}

// for clone and heal, if the source area is the destination from another clone/heal,
// we also need the area from that previous clone/heal
static void rt_extend_roi_in_for_clone(dt_iop_module_t *self,
                                       dt_dev_pixelpipe_iop_t *piece,
                                       dt_iop_roi_t *roi_in,
                                       int *_roir,
                                       int *_roib,
                                       int *_roix,
                                       int *_roiy)
{
  dt_iop_retouch_params_t *p = piece->data;
  dt_develop_blend_params_t *bp = piece->blendop_data;

  int roir = *_roir;
  int roib = *_roib;
  int roix = *_roix;
  int roiy = *_roiy;

  // go through all clone and heal forms
  const dt_masks_form_t *grp = dt_masks_get_from_id_ext(piece->pipe->forms, bp->mask_id);
  if(grp && (grp->type & DT_MASKS_GROUP))
  {
    for(const GList *forms = grp->points; forms; forms = g_list_next(forms))
    {
      dt_masks_point_group_t *grpt = forms->data;
      if(grpt)
      {
        const dt_mask_id_t formid = grpt->formid;
        const int index = rt_get_index_from_formid(p, formid);

        if(p->rt_forms[index].algorithm != DT_IOP_RETOUCH_HEAL
           && p->rt_forms[index].algorithm != DT_IOP_RETOUCH_CLONE)
        {
          continue;
        }

        // we get the spot
        dt_masks_form_t *form = dt_masks_get_from_id_ext(piece->pipe->forms, formid);
        if(form == NULL)
        {
          continue;
        }

        // get the source area
        int fl_src, ft_src, fw_src, fh_src;
        if(!dt_masks_get_source_area(self, piece, form,
                                     &fw_src, &fh_src, &fl_src, &ft_src))
        {
          continue;
        }

        fw_src *= roi_in->scale;
        fh_src *= roi_in->scale;
        fl_src *= roi_in->scale;
        ft_src *= roi_in->scale;

        // we only want to process forms already in roi_in
        const int intersects
            = !(roib < ft_src
                || ft_src + fh_src < roiy
                || roir < fl_src
                || fl_src + fw_src < roix);
        if(intersects)
          rt_extend_roi_in_from_source_clones(self, piece, roi_in,
                                              formid, fl_src, ft_src, fw_src, fh_src,
                                              &roir, &roib, &roix, &roiy);
      }
    }
  }

  *_roir = roir;
  *_roib = roib;
  *_roix = roix;
  *_roiy = roiy;
}

// needed if mask dest is in roi and mask src is not
void modify_roi_in(dt_iop_module_t *self,
                   dt_dev_pixelpipe_iop_t *piece,
                   const dt_iop_roi_t *roi_out,
                   dt_iop_roi_t *roi_in)
{
  *roi_in = *roi_out;

  int roir = roi_in->width + roi_in->x;
  int roib = roi_in->height + roi_in->y;
  int roix = roi_in->x;
  int roiy = roi_in->y;

  rt_compute_roi_in(self, piece, roi_in, &roir, &roib, &roix, &roiy);

  int roir_prev = -1, roib_prev = -1, roix_prev = -1, roiy_prev = -1;

  while(roir != roir_prev
        || roib != roib_prev
        || roix != roix_prev
        || roiy != roiy_prev)
  {
    roir_prev = roir;
    roib_prev = roib;
    roix_prev = roix;
    roiy_prev = roiy;

    rt_extend_roi_in_for_clone(self, piece, roi_in, &roir, &roib, &roix, &roiy);
  }

  // now we set the values
  const float scwidth = piece->buf_in.width * roi_in->scale;
  const float scheight = piece->buf_in.height * roi_in->scale;

  roi_in->x = CLAMP(roix, 0, scwidth - 1);
  roi_in->y = CLAMP(roiy, 0, scheight - 1);
  roi_in->width = CLAMP(roir - roi_in->x, 1, scwidth + .5f - roi_in->x);
  roi_in->height = CLAMP(roib - roi_in->y, 1, scheight + .5f - roi_in->y);
}

//--------------------------------------------------------------------------------------------------
// process
//--------------------------------------------------------------------------------------------------

static void image_rgb2lab(float *img_src,
                          const int width,
                          const int height)
{
  const size_t npixels = (size_t)width * height;

  DT_OMP_FOR()
  for(size_t i = 0; i < npixels; i++)
  {
    dt_aligned_pixel_t XYZ;
    dt_linearRGB_to_XYZ(img_src + 4*i, XYZ);
    dt_XYZ_to_Lab(XYZ, img_src + 4*i);
  }
}

static void image_lab2rgb(float *img_src,
                          const int width,
                          const int height)
{
  const size_t npixels = (size_t)width * height;

  DT_OMP_FOR()
  for(size_t i = 0; i < npixels; i++)
  {
    dt_aligned_pixel_t XYZ;
    dt_Lab_to_XYZ(img_src + 4*i, XYZ);
    dt_XYZ_to_linearRGB(XYZ, img_src + 4*i);
  }
}

static void rt_process_stats(dt_iop_module_t *self,
                             dt_dev_pixelpipe_iop_t *piece,
                             float *const img_src,
                             const int width,
                             const int height,
                             const int ch,
                             float levels[3])
{
  const size_t size = (size_t)width * height * ch;
  float l_max = -FLT_MAX;
  float l_min = FLT_MAX;
  float l_sum = 0.f;
  int count = 0;
  const dt_iop_order_iccprofile_info_t *const work_profile =
    dt_ioppr_get_pipe_work_profile_info(piece->pipe);

  DT_OMP_FOR(reduction(+ : count, l_sum) reduction(max : l_max) reduction(min : l_min))
  for(int i = 0; i < size; i += ch)
  {
    dt_aligned_pixel_t Lab = { 0 };

    if(work_profile)
    {
      dt_ioppr_rgb_matrix_to_lab(img_src + i, Lab, work_profile->matrix_in_transposed,
                                  work_profile->lut_in, work_profile->unbounded_coeffs_in,
                                  work_profile->lutsize, work_profile->nonlinearlut);
    }
    else
    {
      dt_aligned_pixel_t XYZ;
      dt_linearRGB_to_XYZ(img_src + i, XYZ);
      dt_XYZ_to_Lab(XYZ, Lab);
    }

    l_max = MAX(l_max, Lab[0]);
    l_min = MIN(l_min, Lab[0]);
    l_sum += Lab[0];
    count++;
  }

  levels[0] = l_min / 100.f;
  levels[2] = l_max / 100.f;
  levels[1] = (l_sum / (float)count) / 100.f;
}

static void rt_adjust_levels(dt_iop_module_t *self,
                             dt_dev_pixelpipe_iop_t *piece,
                             float *img_src,
                             const int width,
                             const int height,
                             const int ch,
                             const float levels[3])
{
  const int size = width * height * ch;
  const dt_iop_order_iccprofile_info_t *const work_profile =
    dt_ioppr_get_pipe_work_profile_info(piece->pipe);

  const float left = levels[0];
  const float middle = levels[1];
  const float right = levels[2];

  if(left == RETOUCH_PREVIEW_LVL_MIN && middle == 0.f
     && right == RETOUCH_PREVIEW_LVL_MAX) return;

  const float delta = (right - left) / 2.0f;
  const float mid = left + delta;
  const float tmp = (middle - mid) / delta;
  const float in_inv_gamma = powf(10, tmp);

  DT_OMP_FOR()
  for(int i = 0; i < size; i += ch)
  {
    if(work_profile)
    {
      dt_ioppr_rgb_matrix_to_lab(img_src + i, img_src + i,
                                 work_profile->matrix_in_transposed,
                                 work_profile->lut_in,
                                 work_profile->unbounded_coeffs_in,
                                 work_profile->lutsize,
                                 work_profile->nonlinearlut);
    }
    else
    {
      dt_aligned_pixel_t XYZ;

      dt_linearRGB_to_XYZ(img_src + i, XYZ);
      dt_XYZ_to_Lab(XYZ, img_src + i);
    }

    for(int c = 0; c < 1; c++)
    {
      const float L_in = img_src[i + c] / 100.0f;

      if(L_in <= left)
      {
        img_src[i + c] = 0.f;
      }
      else
      {
        const float percentage = (L_in - left) / (right - left);
        img_src[i + c] = 100.0f * powf(percentage, in_inv_gamma);
      }
    }

    if(work_profile)
    {
      dt_ioppr_lab_to_rgb_matrix(img_src + i, img_src + i,
                                 work_profile->matrix_out_transposed,
                                 work_profile->lut_out,
                                 work_profile->unbounded_coeffs_out,
                                 work_profile->lutsize,
                                 work_profile->nonlinearlut);;
    }
    else
    {
      dt_aligned_pixel_t XYZ;

      dt_Lab_to_XYZ(img_src + i, XYZ);
      dt_XYZ_to_linearRGB(XYZ, img_src + i);
    }
  }
}

#undef RT_WDBAR_INSET

#undef RETOUCH_NO_FORMS
#undef RETOUCH_MAX_SCALES
#undef RETOUCH_NO_SCALES

#undef RETOUCH_PREVIEW_LVL_MIN
#undef RETOUCH_PREVIEW_LVL_MAX

static void rt_intersect_2_rois(dt_iop_roi_t *const roi_1,
                                dt_iop_roi_t *const roi_2,
                                const int dx,
                                const int dy,
                                const int padding,
                                dt_iop_roi_t *roi_dest)
{
  const int x_from = MAX(MAX((roi_1->x + 1 - padding),
                             roi_2->x),
                         (roi_2->x + dx));
  const int x_to
      = MIN(MIN((roi_1->x + roi_1->width + 1 + padding),
                roi_2->x + roi_2->width),
            (roi_2->x + roi_2->width + dx));

  const int y_from = MAX(MAX((roi_1->y + 1 - padding),
                             roi_2->y),
                         (roi_2->y + dy));
  const int y_to = MIN(MIN((roi_1->y + roi_1->height + 1 + padding),
                           (roi_2->y + roi_2->height)),
                       (roi_2->y + roi_2->height + dy));

  roi_dest->x = x_from;
  roi_dest->y = y_from;
  roi_dest->width = x_to - x_from;
  roi_dest->height = y_to - y_from;
}

// opacity-weighted centroid (center of mass) of a single-channel mask, in
// roi_mask_scaled-local coords. The centroid is rotation-invariant, which is why
// it -- not the bounding-box center -- is used as the rotation pivot: it stays
// put as the source is rotated, matching the overlay which pivots about the same
// (area) centroid (see _path_centroid / _brush_centroid).
static void rt_mask_centroid(const float *const mask_scaled,
                             const dt_iop_roi_t *const roi_mask_scaled,
                             float *const cx,
                             float *const cy)
{
  const int w = roi_mask_scaled->width;
  const int h = roi_mask_scaled->height;
  double sx = 0.0, sy = 0.0, sw = 0.0;

  for(int y = 0; y < h; y++)
  {
    const float *const m = mask_scaled + (size_t)y * w;
    for(int x = 0; x < w; x++)
    {
      const float v = m[x];
      sx += (double)v * x;
      sy += (double)v * y;
      sw += v;
    }
  }

  if(sw > 1e-6)
  {
    *cx = (float)(sx / sw);
    *cy = (float)(sy / sw);
  }
  else
  {
    *cx = w * 0.5f;
    *cy = h * 0.5f;
  }
}

// (cx, cy) is the rotation pivot in roi_out-local coords (the mask centroid);
// only used when angle != 0.
static void rt_copy_in_to_out(const float *const in,
                              const dt_iop_roi_t *const roi_in,
                              float *const out,
                              const dt_iop_roi_t *const roi_out,
                              const int ch,
                              const int dx,
                              const int dy,
                              const float angle,
                              const float cx,
                              const float cy)
{
  const size_t rowsize = sizeof(float) * ch * MIN(roi_out->width, roi_in->width);
  const int xoffs = roi_out->x - roi_in->x - dx;
  const int yoffs = roi_out->y - roi_in->y - dy;
  const int y_to = MIN(roi_out->height, roi_in->height);

  // treat a negligibly small rotation as no rotation: skips the resampling
  // path (cheaper) and avoids artifacts from repeated UI actions
  if(feqf(angle, 0.0f, 0.01f))
  {
    DT_OMP_FOR()
    for(int y = 0; y < y_to; y++)
    {
      const size_t iindex = ((size_t)(y + yoffs) * roi_in->width + xoffs) * ch;
      const size_t oindex = (size_t)y * roi_out->width * ch;
      const float *in1 = in + iindex;
      float *out1 = out + oindex;

      memcpy(out1, in1, rowsize);
    }
  }
  else
  {
    // Sampling with rotation: the destination pixel at offset (rx,ry) from the
    // mask centroid is filled with the image pixel that lies under the rotated
    // source overlay at the matching offset, i.e. centroid + R(angle)*(rx,ry).
    // This uses the SAME rotation and pivot as the on-screen source outline
    // (masks code), so the overlay marks the region being copied.
    const float c = cosf(angle);
    const float s = sinf(angle);
    // Rotation pivot: the mask centroid (passed in as cx, cy in roi_out-local
    // coords), mapped into the source region in roi_in coordinates.
    const float cx_source = cx + xoffs;
    const float cy_source = cy + yoffs;
    const int x_to = MIN(roi_out->width, roi_in->width);

    DT_OMP_FOR()
    for(int y = 0; y < y_to; y++)
    {
      for(int x = 0; x < x_to; x++)
      {
        // Translate to source, rotate around source center, get final position
        const float sx = x + xoffs;
        const float sy = y + yoffs;
        const float rx = sx - cx_source;
        const float ry = sy - cy_source;
        const float ix = cx_source + rx * c - ry * s;
        const float iy = cy_source + rx * s + ry * c;

        const size_t oindex = ((size_t)y * roi_out->width + x) * ch;
        float *out1 = out + oindex;

        // Edge-clamp (replicate) rather than zero-filling out-of-bounds samples:
        // rt_compute_roi_in grows roi_in to the rotation-aware source area, but it
        // cannot grow past the actual image border (and the box corners outside
        // the mask are not covered), so a rotated source near the image edge can
        // still sample just outside roi_in. Replicating the border keeps a benign
        // smear there instead of seeding black pixels, which would otherwise
        // create a hard seam for heal.
        const float ixc = CLAMPF(ix, 0.0f, roi_in->width - 1.0f);
        const float iyc = CLAMPF(iy, 0.0f, roi_in->height - 1.0f);

        const int x0 = CLAMP((int)ixc, 0, roi_in->width - 2);
        const int y0 = CLAMP((int)iyc, 0, roi_in->height - 2);
        const float dx0 = ixc - x0;
        const float dy0 = iyc - y0;

        const float *in00 = in + ((size_t)y0 * roi_in->width + x0) * ch;
        const float *in10 = in + ((size_t)y0 * roi_in->width + x0 + 1) * ch;
        const float *in01 = in + ((size_t)(y0 + 1) * roi_in->width + x0) * ch;
        const float *in11 = in + ((size_t)(y0 + 1) * roi_in->width + x0 + 1) * ch;

        for(int c_idx = 0; c_idx < ch; c_idx++)
        {
          const float val = in00[c_idx] * (1.0f - dx0) * (1.0f - dy0) +
                            in10[c_idx] * dx0 * (1.0f - dy0) + in01[c_idx] * (1.0f - dx0) * dy0 +
                            in11[c_idx] * dx0 * dy0;
          out1[c_idx] = val;
        }
      }
    }
  }
}

// Return TRUE in case of an error
static gboolean rt_build_scaled_mask(float *const mask,
                                 dt_iop_roi_t *const roi_mask,
                                 float **mask_scaled,
                                 dt_iop_roi_t *roi_mask_scaled,
                                 dt_iop_roi_t *const roi_in,
                                 const int dx,
                                 const int dy,
                                 const int algo)
{
  float *mask_tmp = NULL;

  const int padding = (algo == DT_IOP_RETOUCH_HEAL) ? 1 : 0;
  gboolean error = FALSE;
  *roi_mask_scaled = *roi_mask;

  roi_mask_scaled->x = roi_mask->x * roi_in->scale;
  roi_mask_scaled->y = roi_mask->y * roi_in->scale;
  roi_mask_scaled->width = ((roi_mask->width * roi_in->scale) + .5f);
  roi_mask_scaled->height = ((roi_mask->height * roi_in->scale) + .5f);
  roi_mask_scaled->scale = roi_in->scale;

  rt_intersect_2_rois(roi_mask_scaled, roi_in, dx, dy, padding, roi_mask_scaled);
  if(roi_mask_scaled->width < 1 || roi_mask_scaled->height < 1)
    goto cleanup;

  const int x_to = roi_mask_scaled->width + roi_mask_scaled->x;
  const int y_to = roi_mask_scaled->height + roi_mask_scaled->y;

  mask_tmp = dt_alloc_align_float((size_t)roi_mask_scaled->width * roi_mask_scaled->height);
  if(mask_tmp == NULL)
  {
    error = TRUE;
    dt_print(DT_DEBUG_ALWAYS, "[retouch] rt_build_scaled_mask: error allocating memory");
    goto cleanup;
  }
  dt_iop_image_fill(mask_tmp, 0.0f, roi_mask_scaled->width, roi_mask_scaled->height, 1);

  DT_OMP_FOR()
  for(int yy = roi_mask_scaled->y; yy < y_to; yy++)
  {
    const int mask_index = ((int)(yy / roi_in->scale)) - roi_mask->y;
    if(mask_index < 0 || mask_index >= roi_mask->height) continue;

    const int mask_scaled_index = (yy - roi_mask_scaled->y) * roi_mask_scaled->width;

    const float *m = mask + mask_index * roi_mask->width;
    float *ms = mask_tmp + mask_scaled_index;

    for(int xx = roi_mask_scaled->x; xx < x_to; xx++, ms++)
    {
      const int mx = ((int)(xx / roi_in->scale)) - roi_mask->x;
      if(mx < 0 || mx >= roi_mask->width) continue;

      *ms = m[mx];
    }
  }

cleanup:
  *mask_scaled = mask_tmp;
  return error;
}

// img_src and mask_scaled must have the same roi
static void rt_copy_image_masked(float *const img_src,
                                 float *img_dest,
                                 dt_iop_roi_t *const roi_dest,
                                 float *const mask_scaled,
                                 dt_iop_roi_t *const roi_mask_scaled,
                                 const float opacity)
{
  DT_OMP_FOR()
  for(int yy = 0; yy < roi_mask_scaled->height; yy++)
  {
    const int mask_index = yy * roi_mask_scaled->width;
    const int src_index = 4 * mask_index;
    const int dest_index
      = 4 * (((yy + roi_mask_scaled->y - roi_dest->y)
              * roi_dest->width) + (roi_mask_scaled->x - roi_dest->x));

    const float *s = img_src + src_index;
    const float *m = mask_scaled + mask_index;
    float *d = img_dest + dest_index;

    for(int xx = 0; xx < roi_mask_scaled->width; xx++)
    {
      const float f = m[xx] * opacity;
      const float f1 = (1.0f - f);

      for_each_channel(c,aligned(s,d))
      {
        d[4*xx + c] = d[4*xx + c] * f1 + s[4*xx + c] * f;
      }
    }
  }
}

static void rt_copy_mask_to_alpha(float *const img,
                                  dt_iop_roi_t *const roi_img,
                                  const int ch,
                                  float *const mask_scaled,
                                  dt_iop_roi_t *const roi_mask_scaled,
                                  const float opacity)
{
  DT_OMP_FOR()
  for(int yy = 0; yy < roi_mask_scaled->height; yy++)
  {
    const int mask_index = yy * roi_mask_scaled->width;
    const int dest_index
        = (((yy + roi_mask_scaled->y - roi_img->y) * roi_img->width)
           + (roi_mask_scaled->x - roi_img->x)) * ch;

    float *d = img + dest_index;
    const float *m = mask_scaled + mask_index;

    for(int xx = 0; xx < roi_mask_scaled->width; xx++, d += ch, m++)
    {
      const float f = (*m) * opacity;
      if(f > d[3]) d[3] = f;
    }
  }
}

static void _retouch_fill(float *const in,
                          dt_iop_roi_t *const roi_in,
                          float *const mask_scaled,
                          dt_iop_roi_t *const roi_mask_scaled,
                          const float opacity,
                          const float *const fill_color)
{
  DT_OMP_FOR()
  for(int yy = 0; yy < roi_mask_scaled->height; yy++)
  {
    const int mask_index = yy * roi_mask_scaled->width;
    const int dest_index
        = (((yy + roi_mask_scaled->y - roi_in->y) * roi_in->width)
           + (roi_mask_scaled->x - roi_in->x)) * 4;

    float *d = in + dest_index;
    const float *m = mask_scaled + mask_index;

    for(int xx = 0; xx < roi_mask_scaled->width; xx++)
    {
      const float f = m[xx] * opacity;

      for_each_channel(c,aligned(d,fill_color))
        d[4*xx + c] = d[4*xx + c] * (1.0f - f) + fill_color[c] * f;
    }
  }
}

static void _retouch_clone(float *const in,
                           dt_iop_roi_t *const roi_in,
                           float *const mask_scaled,
                           dt_iop_roi_t *const roi_mask_scaled,
                           const int dx,
                           const int dy,
                           const float angle,
                           const float cx,
                           const float cy,
                           const float opacity)
{
  // alloc temp image to avoid issues when areas self-intersects
  float *img_src = dt_alloc_align_float((size_t)4 * roi_mask_scaled->width * roi_mask_scaled->height);
  if(img_src == NULL)
  {
    dt_print(DT_DEBUG_ALWAYS, "[retouch] error allocating memory for cloning");
    goto cleanup;
  }

  // copy source image to tmp
  rt_copy_in_to_out(in, roi_in, img_src, roi_mask_scaled, 4, dx, dy, angle, cx, cy);

  // clone it
  rt_copy_image_masked(img_src, in, roi_in, mask_scaled, roi_mask_scaled, opacity);

cleanup:
  dt_free_align(img_src);
}

static void _retouch_blur(dt_iop_module_t *self,
                          float *const in,
                          dt_iop_roi_t *const roi_in,
                          float *const mask_scaled,
                          dt_iop_roi_t *const roi_mask_scaled,
                          const float opacity,
                          const int blur_type,
                          const float blur_radius,
                          dt_dev_pixelpipe_iop_t *piece)
{
  if(fabsf(blur_radius) <= 0.1f) return;

  const float sigma = blur_radius * roi_in->scale / piece->iscale;

  // alloc temp image to blur
  float *img_dest = dt_alloc_align_float((size_t)4 * roi_mask_scaled->width * roi_mask_scaled->height);
  if(img_dest == NULL)
  {
    dt_print(DT_DEBUG_ALWAYS, "[retouch] error allocating memory for blurring");
    goto cleanup;
  }

  // copy source image so we blur just the mask area (at least the
  // smallest rect that covers it)
  rt_copy_in_to_out(in, roi_in, img_dest, roi_mask_scaled, 4, 0, 0, 0.0f, 0.0f, 0.0f);

  if(blur_type == DT_IOP_RETOUCH_BLUR_GAUSSIAN && fabsf(blur_radius) > 0.1f)
  {
    static const float Labmax[] = { FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX };
    static const float Labmin[] = { -FLT_MAX, -FLT_MAX, -FLT_MAX, -FLT_MAX };

    dt_gaussian_t *g = dt_gaussian_init(roi_mask_scaled->width,
                                        roi_mask_scaled->height,
                                        4, Labmax, Labmin, sigma,
                                        DT_IOP_GAUSSIAN_ZERO);
    if(g)
    {
      dt_gaussian_blur_4c(g, img_dest, img_dest);
      dt_gaussian_free(g);
    }
  }
  else if(blur_type == DT_IOP_RETOUCH_BLUR_BILATERAL && fabsf(blur_radius) > 0.1f)
  {
    const float sigma_r = 100.0f; // does not depend on scale
    const float sigma_s = sigma;
    const float detail = -1.0f; // we want the bilateral base layer

    dt_bilateral_t *b = dt_bilateral_init(roi_mask_scaled->width,
                                          roi_mask_scaled->height, sigma_s, sigma_r);
    if(b)
    {
      int converted_cst;
      const dt_iop_order_iccprofile_info_t *const work_profile =
        dt_ioppr_get_pipe_work_profile_info(piece->pipe);

      if(work_profile)
        dt_ioppr_transform_image_colorspace(self, img_dest, img_dest,
                                            roi_mask_scaled->width,
                                            roi_mask_scaled->height,
                                            IOP_CS_RGB, IOP_CS_LAB,
                                            &converted_cst,
                                            work_profile);
      else
        image_rgb2lab(img_dest, roi_mask_scaled->width, roi_mask_scaled->height);

      dt_bilateral_splat(b, img_dest);
      dt_bilateral_blur(b);
      dt_bilateral_slice(b, img_dest, img_dest, detail);
      dt_bilateral_free(b);

      if(work_profile)
        dt_ioppr_transform_image_colorspace(self, img_dest, img_dest,
                                            roi_mask_scaled->width,
                                            roi_mask_scaled->height,
                                            IOP_CS_LAB, IOP_CS_RGB,
                                            &converted_cst,
                                            work_profile);
      else
        image_lab2rgb(img_dest, roi_mask_scaled->width, roi_mask_scaled->height);
    }
  }

  // copy blurred (temp) image to destination image
  rt_copy_image_masked(img_dest, in, roi_in, mask_scaled, roi_mask_scaled, opacity);

cleanup:
  dt_free_align(img_dest);
}

static void _retouch_heal(float *const in,
                          dt_iop_roi_t *const roi_in,
                          float *const mask_scaled,
                          dt_iop_roi_t *const roi_mask_scaled,
                          const int dx,
                          const int dy,
                          const float angle,
                          const float cx,
                          const float cy,
                          const float opacity,
                          const int max_iter)
{
  // alloc temp images for source and destination
  float *img_src  = dt_alloc_align_float((size_t)4 * roi_mask_scaled->width * roi_mask_scaled->height);
  float *img_dest = dt_alloc_align_float((size_t)4 * roi_mask_scaled->width * roi_mask_scaled->height);
  if((img_src == NULL) || (img_dest == NULL))
  {
    dt_print(DT_DEBUG_ALWAYS, "[retouch] error allocating memory for healing");
    goto cleanup;
  }

  // copy source and destination to temp images
  rt_copy_in_to_out(in, roi_in, img_src, roi_mask_scaled, 4, dx, dy, angle, cx, cy);
  rt_copy_in_to_out(in, roi_in, img_dest, roi_mask_scaled, 4, 0, 0, 0.0f, 0.0f, 0.0f);

  // heal it
  dt_heal(img_src, img_dest, mask_scaled,
          roi_mask_scaled->width, roi_mask_scaled->height, 4, max_iter);

  // copy healed (temp) image to destination image
  rt_copy_image_masked(img_dest, in, roi_in, mask_scaled, roi_mask_scaled, opacity);

cleanup:
  dt_free_align(img_src);
  dt_free_align(img_dest);
}

static void rt_process_forms(float *layer, dwt_params_t *const wt_p, const int scale1)
{
  int scale = scale1;
  retouch_user_data_t *usr_d = wt_p->user_data;
  dt_iop_module_t *self = usr_d->self;
  dt_dev_pixelpipe_iop_t *piece = usr_d->piece;

  // if preview a single scale, just process that scale and original image
  // unless merge is activated
  if(wt_p->merge_from_scale == 0
     && wt_p->return_layer > 0
     && scale != wt_p->return_layer
     && scale != 0)
    return;
  // do not process the reconstructed image
  if(scale > wt_p->scales + 1) return;

  dt_develop_blend_params_t *bp = piece->blendop_data;
  dt_iop_retouch_params_t *p = piece->data;
  dt_iop_roi_t *roi_layer = &usr_d->roi;
  const gboolean mask_display = usr_d->mask_display && (scale == usr_d->display_scale);

  // when the requested scales is grather than max scales the residual
  // image index will be different from the one defined by the user,
  // so we need to adjust it here, otherwise we will be using the
  // shapes from a scale on the residual image
  if(wt_p->scales < p->num_scales
     && wt_p->return_layer == 0
     && scale == wt_p->scales + 1)
  {
    scale = p->num_scales + 1;
  }

  // iterate through all forms
  if(!usr_d->suppress_mask)
  {
    const dt_masks_form_t *grp = dt_masks_get_from_id_ext(piece->pipe->forms, bp->mask_id);
    if(grp && (grp->type & DT_MASKS_GROUP))
    {
      for(const GList *forms = grp->points; forms; forms = g_list_next(forms))
      {
        const dt_masks_point_group_t *grpt = forms->data;
        if(grpt == NULL)
        {
          dt_print(DT_DEBUG_ALWAYS, "rt_process_forms: invalid form");
          continue;
        }
        const dt_mask_id_t formid = grpt->formid;
        const float form_opacity = grpt->opacity;
        if(!dt_is_valid_maskid(formid))
        {
          dt_print(DT_DEBUG_ALWAYS, "rt_process_forms: form is null");
          continue;
        }
        const int index = rt_get_index_from_formid(p, formid);
        if(index == -1)
        {
          // FIXME: we get this error when user go back in history, so
          // forms are the same but the array has changed
          dt_print(DT_DEBUG_ALWAYS,
                   "rt_process_forms: missing form=%i from array", formid);
          continue;
        }

        // only process current scale
        if(p->rt_forms[index].scale != scale)
        {
          continue;
        }

        // get the spot
        dt_masks_form_t *form = dt_masks_get_from_id_ext(piece->pipe->forms, formid);
        if(form == NULL)
        {
          dt_print(DT_DEBUG_ALWAYS,
                   "rt_process_forms: missing form=%i from masks", formid);
          continue;
        }

        // if the form is outside the roi, we just skip it
        if(!rt_masks_form_is_in_roi(self, piece, form, roi_layer, roi_layer))
        {
          continue;
        }

        // get the mask
        float *mask = NULL;
        dt_iop_roi_t roi_mask = { 0 };

        dt_masks_get_mask(self, piece, form, &mask,
                          &roi_mask.width, &roi_mask.height, &roi_mask.x, &roi_mask.y);
        if(mask == NULL)
        {
          dt_print(DT_DEBUG_ALWAYS, "rt_process_forms: error retrieving mask");
          continue;
        }

        // search the delta with the source
        const dt_iop_retouch_algo_type_t algo = p->rt_forms[index].algorithm;
        float dx = 0.f, dy = 0.f, angle = 0.f;

        if(algo != DT_IOP_RETOUCH_BLUR && algo != DT_IOP_RETOUCH_FILL)
        {
          if(!rt_masks_get_transform_to_destination(self, piece, roi_layer, form, &dx, &dy, &angle,
                                                p->rt_forms[index].distort_mode))
          {
            dt_free_align(mask);
            continue;
          }
        }

        // scale the mask
        float *mask_scaled = NULL;
        dt_iop_roi_t roi_mask_scaled = { 0 };

        rt_build_scaled_mask(mask, &roi_mask,
                             &mask_scaled, &roi_mask_scaled, roi_layer, dx, dy, algo);

        // we don't need the original mask anymore
        if(mask)
        {
          dt_free_align(mask);
          mask = NULL;
        }

        if(mask_scaled == NULL)
        {
          continue;
        }

        // rotation pivot: mask centroid in roi_mask_scaled-local coords
        float cx = 0.f, cy = 0.f;
        if(!feqf(angle, 0.0f, 0.01f))
          rt_mask_centroid(mask_scaled, &roi_mask_scaled, &cx, &cy);

        if((dx != 0
            || dy != 0
            || algo == DT_IOP_RETOUCH_BLUR
            || algo == DT_IOP_RETOUCH_FILL)
           && ((roi_mask_scaled.width > 2)
               && (roi_mask_scaled.height > 2)))
        {
          if(algo == DT_IOP_RETOUCH_CLONE)
          {
            _retouch_clone(layer, roi_layer, mask_scaled,
                           &roi_mask_scaled, dx, dy, angle, cx, cy, form_opacity);
          }
          else if(algo == DT_IOP_RETOUCH_HEAL)
          {
            _retouch_heal(layer, roi_layer, mask_scaled,
                          &roi_mask_scaled, dx, dy, angle, cx, cy, form_opacity, p->max_heal_iter);
          }
          else if(algo == DT_IOP_RETOUCH_BLUR)
          {
            _retouch_blur(self, layer, roi_layer, mask_scaled,
                          &roi_mask_scaled, form_opacity,
                          p->rt_forms[index].blur_type,
                          p->rt_forms[index].blur_radius, piece);
          }
          else if(algo == DT_IOP_RETOUCH_FILL)
          {
            // add a brightness to the color so it can be fine-adjusted by the user
            dt_aligned_pixel_t fill_color;

            if(p->rt_forms[index].fill_mode == DT_IOP_RETOUCH_FILL_ERASE)
            {
              fill_color[0] = fill_color[1] = fill_color[2] =
                p->rt_forms[index].fill_brightness;
            }
            else
            {
              fill_color[0] =
                p->rt_forms[index].fill_color[0] + p->rt_forms[index].fill_brightness;
              fill_color[1] =
                p->rt_forms[index].fill_color[1] + p->rt_forms[index].fill_brightness;
              fill_color[2] =
                p->rt_forms[index].fill_color[2] + p->rt_forms[index].fill_brightness;
            }
            fill_color[3] = 0.0f;

            _retouch_fill(layer, roi_layer, mask_scaled,
                          &roi_mask_scaled, form_opacity, fill_color);
          }
          else
            dt_print(DT_DEBUG_ALWAYS,
                     "rt_process_forms: unknown algorithm %i", algo);

          if(mask_display)
            rt_copy_mask_to_alpha(layer, roi_layer, wt_p->ch,
                                  mask_scaled, &roi_mask_scaled, form_opacity);
        }

        dt_free_align(mask);
        dt_free_align(mask_scaled);
      }
    }
  }
}

void process(dt_iop_module_t *self,
             dt_dev_pixelpipe_iop_t *piece,
             const void *const ivoid,
             void *const ovoid,
             const dt_iop_roi_t *const roi_in,
             const dt_iop_roi_t *const roi_out)
{
  if(!dt_iop_have_required_input_format(4 /*we need full-color pixels*/,
                                        self, piece->colors,
                                        ivoid, ovoid, roi_in, roi_out))
    return;

  dt_iop_retouch_params_t *p = piece->data;
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  float *in_retouch = NULL;

  dt_iop_roi_t roi_retouch = *roi_in;
  dt_iop_roi_t *roi_rt = &roi_retouch;

  retouch_user_data_t usr_data = { 0 };
  dwt_params_t *dwt_p = NULL;

  const gboolean display_wavelet_scale = g && dt_iop_has_focus(self) ? g->display_wavelet_scale : FALSE;

  // we will do all the clone, heal, etc on the input image,
  // this way the source for one algorithm can be the destination from a previous one
  in_retouch = dt_alloc_align_float((size_t)4 * roi_rt->width * roi_rt->height);
  if(in_retouch == NULL)
  {
    dt_print(DT_DEBUG_ALWAYS,"[retouch] out of memory");
    goto cleanup;
  }
  dt_iop_image_copy_by_size(in_retouch, ivoid, roi_rt->width, roi_rt->height, 4);

  // user data passed from the decompose routine to the one that process each scale
  usr_data.self = self;
  usr_data.piece = piece;
  usr_data.roi = *roi_rt;
  usr_data.mask_display = FALSE;
  usr_data.suppress_mask = (g
                            && g->suppress_mask
                            && dt_iop_has_focus(self)
                            && (piece->pipe == self->dev->full.pipe));
  usr_data.display_scale = p->curr_scale;

  // init the decompose routine
  dwt_p = dt_dwt_init(in_retouch, roi_rt->width, roi_rt->height, 4, p->num_scales,
                      (!display_wavelet_scale
                       || !dt_pipe_is_full(piece->pipe))
                      ? 0
                      : p->curr_scale,
                      p->merge_from_scale, &usr_data,
                      roi_in->scale / piece->iscale);
  if(dwt_p == NULL) goto cleanup;

  // check if this module should expose mask.
  if(dt_pipe_is_full(piece->pipe) && g
     && (g->mask_display || display_wavelet_scale)
     && dt_iop_has_focus(self) && (piece->pipe == self->dev->full.pipe))
  {
    for(size_t j = 0; j < (size_t)roi_rt->width * roi_rt->height * 4; j += 4)
      in_retouch[j + 3] = 0.f;

    piece->pipe->mask_display =
      g->mask_display ? DT_DEV_PIXELPIPE_DISPLAY_MASK : DT_DEV_PIXELPIPE_DISPLAY_PASSTHRU;
    piece->pipe->bypass_blendif = TRUE;
    usr_data.mask_display = TRUE;
  }

  if(dt_pipe_is_full(piece->pipe))
  {
    // check if the image support this number of scales
    if(dt_iop_has_focus(self))
    {
      const int max_scales = dwt_get_max_scale(dwt_p);
      if(dwt_p->scales > max_scales)
      {
        dt_control_log(_("max scale is %i for this image size"), max_scales);
      }
    }
    // get first scale visible at this zoom level
    if(g) g->first_scale_visible = dt_dwt_first_scale_visible(dwt_p);
  }

  // decompose it
  dwt_decompose(dwt_p, rt_process_forms);

  dt_aligned_pixel_t levels = { p->preview_levels[0],
                                p->preview_levels[1],
                                p->preview_levels[2] };

  // process auto levels
  if(g && dt_pipe_is_full(piece->pipe))
  {
    dt_iop_gui_enter_critical_section(self);
    if(g->preview_auto_levels == 1 && !DT_IN_GUI_UPDATE())
    {
      g->preview_auto_levels = -1;

      dt_iop_gui_leave_critical_section(self);

      levels[0] = levels[1] = levels[2] = 0;
      rt_process_stats(self, piece, in_retouch,
                       roi_rt->width, roi_rt->height, 4, levels);
      rt_clamp_minmax(levels, levels);

      for(int i = 0; i < 3; i++) g->preview_levels[i] = levels[i];

      dt_iop_gui_enter_critical_section(self);
      g->preview_auto_levels = 2;
    }
    dt_iop_gui_leave_critical_section(self);
  }

  // if user wants to preview a detail scale adjust levels
  if(dwt_p->return_layer > 0
     && dwt_p->return_layer < dwt_p->scales + 1)
  {
    rt_adjust_levels(self, piece, in_retouch,
                     roi_rt->width, roi_rt->height, 4, levels);
  }

  // copy alpha channel if needed
  if((piece->pipe->mask_display & DT_DEV_PIXELPIPE_DISPLAY_MASK)
     && g
     && !g->mask_display)
  {
    dt_iop_alpha_copy(ivoid, in_retouch, roi_rt->width, roi_rt->height);
  }

  // return final image
  rt_copy_in_to_out(in_retouch, roi_rt, ovoid, roi_out, 4, 0, 0, 0.0f, 0.0f, 0.0f);

cleanup:
  dt_free_align(in_retouch);
  dt_dwt_free(dwt_p);
}

void distort_mask(dt_iop_module_t *self,
                  dt_dev_pixelpipe_iop_t *piece,
                  const float *const in,
                  float *const out,
                  const dt_iop_roi_t *const roi_in,
                  const dt_iop_roi_t *const roi_out)
{
  rt_copy_in_to_out(in, roi_in, out, roi_out, 1, 0, 0, 0.0f, 0.0f, 0.0f);
}

#ifdef HAVE_OPENCL

cl_int rt_process_stats_cl(dt_iop_module_t *self,
                           dt_dev_pixelpipe_iop_t *piece,
                           const int devid,
                           cl_mem dev_img,
                           const int width,
                           const int height,
                           float levels[3])
{
  cl_int err = DT_OPENCL_SYSMEM_ALLOCATION;

  const int ch = 4;

  float *src_buffer = dt_alloc_align_float((size_t)ch * width * height);
  if(src_buffer == NULL)
  {
    dt_print(DT_DEBUG_ALWAYS, "[retouch] error allocating memory for healing (OpenCL)");
    goto cleanup;
  }

  err = dt_opencl_read_buffer_from_device(devid,
                                          (void *)src_buffer, dev_img, 0,
                                          (size_t)width * height * ch * sizeof(float), TRUE);
  if(err != CL_SUCCESS)
    goto cleanup;

  // just call the CPU version for now
  rt_process_stats(self, piece, src_buffer, width, height, ch, levels);

  err = dt_opencl_write_buffer_to_device(devid, src_buffer, dev_img, 0,
                                         sizeof(float) * ch * width * height, TRUE);
cleanup:
  dt_free_align(src_buffer);

  return err;
}

cl_int rt_adjust_levels_cl(dt_iop_module_t *self,
                           dt_dev_pixelpipe_iop_t *piece,
                           const int devid,
                           cl_mem dev_img,
                           const int width,
                           const int height,
                           const float levels[3])
{
  cl_int err = DT_OPENCL_SYSMEM_ALLOCATION;

  const int ch = 4;

  float *src_buffer = dt_alloc_align_float((size_t)ch * width * height);
  if(src_buffer == NULL)
  {
    dt_print(DT_DEBUG_ALWAYS,
             "[retouch] error allocating memory for healing (OpenCL)");
    goto cleanup;
  }

  err = dt_opencl_read_buffer_from_device(devid, (void *)src_buffer, dev_img, 0,
                                          (size_t)width * height * ch * sizeof(float), TRUE);
  if(err != CL_SUCCESS)
    goto cleanup;

  // just call the CPU version for now
  rt_adjust_levels(self, piece, src_buffer, width, height, ch, levels);

  err = dt_opencl_write_buffer_to_device(devid, src_buffer, dev_img, 0,
                                         sizeof(float) * ch * width * height, TRUE);

cleanup:
  dt_free_align(src_buffer);

  return err;
}

static cl_int rt_copy_in_to_out_cl(const int devid,
                                   cl_mem dev_in,
                                   const dt_iop_roi_t *const roi_in,
                                   cl_mem dev_out,
                                   const dt_iop_roi_t *const roi_out,
                                   const int dx,
                                   const int dy,
                                   const float angle,
                                   const float cx,
                                   const float cy,
                                   const int kernel)
{
  cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;

  const int xoffs = roi_out->x - roi_in->x - dx;
  const int yoffs = roi_out->y - roi_in->y - dy;

  cl_mem dev_roi_in = dt_opencl_copy_host_to_device_constant(devid, sizeof(dt_iop_roi_t), (void *)roi_in);
  cl_mem dev_roi_out = dt_opencl_copy_host_to_device_constant(devid, sizeof(dt_iop_roi_t), (void *)roi_out);
  if(dev_roi_in == NULL || dev_roi_out == NULL)
  {
    dt_print(DT_DEBUG_ALWAYS, "rt_copy_in_to_out_cl error 1");
    goto cleanup;
  }

  err = dt_opencl_enqueue_kernel_2d_args(devid, kernel, MIN(roi_out->width, roi_in->width), MIN(roi_out->height, roi_in->height),
     CLARG(dev_in), CLARG(dev_roi_in),
     CLARG(dev_out), CLARG(dev_roi_out),
     CLARG(xoffs), CLARG(yoffs), CLARG(angle), CLARG(cx), CLARG(cy));
  if(err != CL_SUCCESS)
    dt_print(DT_DEBUG_ALWAYS, "rt_copy_in_to_out_cl error 2");

cleanup:
  dt_opencl_release_mem_object(dev_roi_in);
  dt_opencl_release_mem_object(dev_roi_out);

  return err;
}

static cl_int rt_build_scaled_mask_cl(const int devid,
                                      float *const mask,
                                      dt_iop_roi_t *const roi_mask,
                                      float **mask_scaled,
                                      cl_mem *p_dev_mask_scaled,
                                      dt_iop_roi_t *roi_mask_scaled,
                                      dt_iop_roi_t *const roi_in,
                                      const int dx,
                                      const int dy,
                                      const int algo)
{
  cl_int err = rt_build_scaled_mask(mask, roi_mask, mask_scaled, roi_mask_scaled, roi_in, dx, dy, algo)
                ? CL_MEM_OBJECT_ALLOCATION_FAILURE : CL_SUCCESS;

  if(*mask_scaled == NULL)
    goto cleanup;

  const cl_mem dev_mask_scaled = dt_opencl_alloc_device_buffer(devid,
                                  sizeof(float) * roi_mask_scaled->width * roi_mask_scaled->height);
  if(dev_mask_scaled == NULL)
    goto cleanup;

  err = dt_opencl_write_buffer_to_device(devid, *mask_scaled, dev_mask_scaled, 0,
          sizeof(float) * roi_mask_scaled->width * roi_mask_scaled->height, TRUE);
  if(err != CL_SUCCESS)
    goto cleanup;

  *p_dev_mask_scaled = dev_mask_scaled;

cleanup:
  if(err != CL_SUCCESS) dt_print(DT_DEBUG_ALWAYS, "rt_build_scaled_mask_cl error");

  return err;
}

static cl_int rt_copy_image_masked_cl(const int devid,
                                      cl_mem dev_src,
                                      cl_mem dev_dest,
                                      dt_iop_roi_t *const roi_dest,
                                      cl_mem dev_mask_scaled,
                                      dt_iop_roi_t *const roi_mask_scaled,
                                      const float opacity, const int kernel)
{
  cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;

  const cl_mem dev_roi_dest = dt_opencl_copy_host_to_device_constant(devid, sizeof(dt_iop_roi_t), roi_dest);
  const cl_mem dev_roi_mask_scaled = dt_opencl_copy_host_to_device_constant(devid, sizeof(dt_iop_roi_t), roi_mask_scaled);

  if(dev_roi_dest == NULL || dev_roi_mask_scaled == NULL)
    goto cleanup;

  err = dt_opencl_enqueue_kernel_2d_args(devid, kernel,
                                         roi_mask_scaled->width,
                                         roi_mask_scaled->height,
                                         CLARG(dev_src), CLARG(dev_dest),
                                         CLARG(dev_roi_dest),
                                         CLARG(dev_mask_scaled),
                                         CLARG(dev_roi_mask_scaled),
                                         CLARG(opacity));

cleanup:
  dt_opencl_release_mem_object(dev_roi_dest);
  dt_opencl_release_mem_object(dev_roi_mask_scaled);

  return err;
}

static cl_int rt_copy_mask_to_alpha_cl(const int devid,
                                       cl_mem dev_layer,
                                       dt_iop_roi_t *const roi_layer,
                                       cl_mem dev_mask_scaled,
                                       dt_iop_roi_t *const roi_mask_scaled,
                                       const float opacity,
                                       dt_iop_retouch_global_data_t *gd)
{
  cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;

  // fill it
  const int kernel = gd->kernel_retouch_copy_mask_to_alpha;

  const cl_mem  dev_roi_layer = dt_opencl_copy_host_to_device_constant(devid, sizeof(dt_iop_roi_t), roi_layer);
  const cl_mem dev_roi_mask_scaled = dt_opencl_copy_host_to_device_constant(devid, sizeof(dt_iop_roi_t), roi_mask_scaled);
  if(dev_roi_layer == NULL || dev_roi_mask_scaled == NULL)
    goto cleanup;

  err = dt_opencl_enqueue_kernel_2d_args(devid, kernel,
                                         roi_mask_scaled->width,
                                         roi_mask_scaled->height,
                                         CLARG(dev_layer), CLARG(dev_roi_layer),
                                         CLARG(dev_mask_scaled),
                                         CLARG(dev_roi_mask_scaled),
                                         CLARG(opacity));

cleanup:
  dt_opencl_release_mem_object(dev_roi_layer);
  dt_opencl_release_mem_object(dev_roi_mask_scaled);

  return err;
}

static cl_int _retouch_clone_cl(const int devid,
                                cl_mem dev_layer,
                                dt_iop_roi_t *const roi_layer,
                                cl_mem dev_mask_scaled,
                                dt_iop_roi_t *const roi_mask_scaled,
                                const int dx,
                                const int dy,
                                const float angle,
                                const float cx,
                                const float cy,
                                const float opacity,
                                dt_iop_retouch_global_data_t *gd)
{
  cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;

  // alloc source temp image to avoid issues when areas self-intersects
  const cl_mem dev_src = dt_opencl_alloc_device_buffer(devid, sizeof(float)*4 * roi_mask_scaled->width * roi_mask_scaled->height);
  if(dev_src == NULL)
    goto cleanup;

  // copy source image to tmp
  err = rt_copy_in_to_out_cl(devid, dev_layer, roi_layer, dev_src, roi_mask_scaled, dx, dy, angle, cx, cy,
                             gd->kernel_retouch_copy_buffer_to_buffer);
  if(err != CL_SUCCESS)
    goto cleanup;

  // clone it
  err = rt_copy_image_masked_cl(devid, dev_src, dev_layer, roi_layer, dev_mask_scaled, roi_mask_scaled, opacity,
                              gd->kernel_retouch_copy_buffer_to_buffer_masked);

cleanup:
  dt_opencl_release_mem_object(dev_src);

  return err;
}

static cl_int _retouch_fill_cl(const int devid,
                               cl_mem dev_layer,
                               dt_iop_roi_t *const roi_layer,
                               cl_mem dev_mask_scaled,
                               dt_iop_roi_t *const roi_mask_scaled,
                               const float opacity,
                               float *color,
                               dt_iop_retouch_global_data_t *gd)
{
  cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;

  // fill it
  const int kernel = gd->kernel_retouch_fill;

  const cl_mem dev_roi_layer = dt_opencl_copy_host_to_device_constant(devid, sizeof(dt_iop_roi_t), roi_layer);
  const cl_mem dev_roi_mask_scaled = dt_opencl_copy_host_to_device_constant(devid, sizeof(dt_iop_roi_t), roi_mask_scaled);
  if(dev_roi_layer == NULL || dev_roi_mask_scaled == NULL)
    goto cleanup;

  err = dt_opencl_enqueue_kernel_2d_args(devid, kernel,
                                         roi_mask_scaled->width,
                                         roi_mask_scaled->height,
                                         CLARG(dev_layer), CLARG(dev_roi_layer),
                                         CLARG(dev_mask_scaled),
                                         CLARG(dev_roi_mask_scaled),
                                         CLARG(opacity),
                                         CLARG((color[0])),
                                         CLARG((color[1])),
                                         CLARG((color[2])));
cleanup:
  dt_opencl_release_mem_object(dev_roi_layer);
  dt_opencl_release_mem_object(dev_roi_mask_scaled);

  return err;
}

static cl_int _retouch_blur_cl(const int devid,
                               cl_mem dev_layer,
                               dt_iop_roi_t *const roi_layer,
                               cl_mem dev_mask_scaled,
                               dt_iop_roi_t *const roi_mask_scaled,
                               const float opacity,
                               const int blur_type,
                               const float blur_radius,
                               dt_dev_pixelpipe_iop_t *piece,
                               dt_iop_retouch_global_data_t *gd)
{
  if(fabsf(blur_radius) <= 0.1f) return CL_SUCCESS;

  const float sigma = blur_radius * roi_layer->scale / piece->iscale;

  const cl_mem dev_dest = dt_opencl_alloc_device(devid, roi_mask_scaled->width, roi_mask_scaled->height, sizeof(float) * 4);
  cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;

  if(dev_dest == NULL)
    goto cleanup;

  if(blur_type == DT_IOP_RETOUCH_BLUR_BILATERAL)
  {
    const int kernel = gd->kernel_retouch_image_rgb2lab;
    err = dt_opencl_enqueue_kernel_2d_args(devid, kernel, roi_layer->width,
                                           roi_layer->height,
                                           CLARG(dev_layer), CLARG((roi_layer->width)),
                                           CLARG((roi_layer->height)));
    if(err != CL_SUCCESS) goto cleanup;
  }

  err = rt_copy_in_to_out_cl(devid, dev_layer, roi_layer, dev_dest,
                             roi_mask_scaled, 0, 0, 0.0f, 0.0f, 0.0f,
                             gd->kernel_retouch_copy_buffer_to_image);
  if(err != CL_SUCCESS)
    goto cleanup;

  err = DT_OPENCL_PROCESS_CL;
  if(blur_type == DT_IOP_RETOUCH_BLUR_GAUSSIAN && fabsf(blur_radius) > 0.1f)
  {
    static const float Labmax[] = { FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX };
    static const float Labmin[] = { -FLT_MAX, -FLT_MAX, -FLT_MAX, -FLT_MAX };

    dt_gaussian_cl_t *g = dt_gaussian_init_cl(devid, roi_mask_scaled->width,
                                              roi_mask_scaled->height, 4, Labmax,
                                              Labmin, sigma, DT_IOP_GAUSSIAN_ZERO);
    if(g)
    {
      err = dt_gaussian_blur_cl(g, dev_dest, dev_dest);
      dt_gaussian_free_cl(g);
    }
    if(err != CL_SUCCESS) goto cleanup;
  }
  else if(blur_type == DT_IOP_RETOUCH_BLUR_BILATERAL && fabsf(blur_radius) > 0.1f)
  {
    const float sigma_r = 100.0f; // does not depend on scale
    const float sigma_s = sigma;
    const float detail = -1.0f; // we want the bilateral base layer

    dt_bilateral_cl_t *b = dt_bilateral_init_cl(devid, roi_mask_scaled->width,
                               roi_mask_scaled->height, sigma_s, sigma_r);
    if(b)
    {
      err = dt_bilateral_splat_cl(b, dev_dest);
      if(err == CL_SUCCESS) err = dt_bilateral_blur_cl(b);
      if(err == CL_SUCCESS) err = dt_bilateral_slice_cl(b, dev_dest, dev_dest, detail);
      dt_bilateral_free_cl(b);
    }
    if(err != CL_SUCCESS) goto cleanup;
  }

  // copy blurred (temp) image to destination image
  err = rt_copy_image_masked_cl(devid, dev_dest, dev_layer, roi_layer,
                                dev_mask_scaled, roi_mask_scaled, opacity,
                                gd->kernel_retouch_copy_image_to_buffer_masked);
  if(err != CL_SUCCESS)
    goto cleanup;

  if(blur_type == DT_IOP_RETOUCH_BLUR_BILATERAL)
  {
    const int kernel = gd->kernel_retouch_image_lab2rgb;
    err = dt_opencl_enqueue_kernel_2d_args(devid, kernel, roi_layer->width,
                                           roi_layer->height,
                                           CLARG(dev_layer),
                                           CLARG((roi_layer->width)),
                                           CLARG((roi_layer->height)));
  }

cleanup:
  dt_opencl_release_mem_object(dev_dest);

  return err;
}

static cl_int _retouch_heal_cl(const int devid,
                               cl_mem dev_layer,
                               dt_iop_roi_t *const roi_layer,
                               cl_mem dev_mask_scaled,
                               float *const mask_scaled,
                               dt_iop_roi_t *const roi_mask_scaled,
                               const int dx,
                               const int dy,
                               const float angle,
                               const float cx,
                               const float cy,
                               const float opacity,
                               const int max_iter,
                               dt_iop_retouch_global_data_t *gd)
{
  cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;

  cl_mem dev_src = dt_opencl_alloc_device_buffer(devid, sizeof(float) * 4 * roi_mask_scaled->width * roi_mask_scaled->height);
  cl_mem dev_dest = dt_opencl_alloc_device_buffer(devid, sizeof(float) * 4 * roi_mask_scaled->width * roi_mask_scaled->height);

  if(dev_dest == NULL || dev_src == NULL)
    goto cleanup;

  err = rt_copy_in_to_out_cl(devid, dev_layer, roi_layer, dev_src,
                             roi_mask_scaled, dx, dy, angle, cx, cy,
                             gd->kernel_retouch_copy_buffer_to_buffer);
  if(err != CL_SUCCESS)
    goto cleanup;

  err = rt_copy_in_to_out_cl(devid, dev_layer, roi_layer, dev_dest,
                             roi_mask_scaled, 0, 0, 0.0f, 0.0f, 0.0f,
                             gd->kernel_retouch_copy_buffer_to_buffer);
  if(err != CL_SUCCESS)
    goto cleanup;

  // heal it
  heal_params_cl_t *hp = dt_heal_init_cl(devid);
  if(hp)
  {
    err = dt_heal_cl(hp, dev_src, dev_dest, mask_scaled, dev_mask_scaled,
                     roi_mask_scaled->width, roi_mask_scaled->height, max_iter);
    dt_heal_free_cl(hp);

    dt_opencl_release_mem_object(dev_src);
    dev_src = NULL;

    if(err != CL_SUCCESS) goto cleanup;
  }

  // copy healed (temp) image to destination image
  err = rt_copy_image_masked_cl(devid, dev_dest, dev_layer, roi_layer,
                                dev_mask_scaled, roi_mask_scaled, opacity,
                                gd->kernel_retouch_copy_buffer_to_buffer_masked);

cleanup:
  dt_opencl_release_mem_object(dev_src);
  dt_opencl_release_mem_object(dev_dest);

  return err;
}

static cl_int rt_process_forms_cl(cl_mem dev_layer,
                                  dwt_params_cl_t *const wt_p,
                                  const int scale1)
{
  cl_int err = CL_SUCCESS;

  int scale = scale1;
  retouch_user_data_t *usr_d = wt_p->user_data;
  dt_iop_module_t *self = usr_d->self;
  dt_dev_pixelpipe_iop_t *piece = usr_d->piece;

  // if preview a single scale, just process that scale and original image
  // unless merge is activated
  if(wt_p->merge_from_scale == 0
     && wt_p->return_layer > 0
     && scale != wt_p->return_layer
     && scale != 0)
    return err;
  // do not process the reconstructed image
  if(scale > wt_p->scales + 1) return err;

  dt_develop_blend_params_t *bp = piece->blendop_data;
  dt_iop_retouch_params_t *p = piece->data;
  dt_iop_retouch_global_data_t *gd = self->global_data;
  const int devid = piece->pipe->devid;
  dt_iop_roi_t *roi_layer = &usr_d->roi;
  const gboolean mask_display = usr_d->mask_display && (scale == usr_d->display_scale);

  // when the requested scales is grather than max scales the residual
  // image index will be different from the one defined by the user,
  // so we need to adjust it here, otherwise we will be using the
  // shapes from a scale on the residual image
  if(wt_p->scales < p->num_scales
     && wt_p->return_layer == 0
     && scale == wt_p->scales + 1)
  {
    scale = p->num_scales + 1;
  }

  // iterate through all forms
  if(!usr_d->suppress_mask)
  {
    dt_masks_form_t *grp = dt_masks_get_from_id_ext(piece->pipe->forms, bp->mask_id);
    if(grp && (grp->type & DT_MASKS_GROUP))
    {
      for(const GList *forms = grp->points;
          forms && err == CL_SUCCESS;
          forms = g_list_next(forms))
      {
        dt_masks_point_group_t *grpt = forms->data;
        if(grpt == NULL)
        {
          dt_print(DT_DEBUG_ALWAYS, "rt_process_forms: invalid form");
          continue;
        }
        const dt_mask_id_t formid = grpt->formid;
        const float form_opacity = grpt->opacity;
        if(!dt_is_valid_maskid(formid))
        {
          dt_print(DT_DEBUG_ALWAYS, "rt_process_forms: form is null");
          continue;
        }
        const int index = rt_get_index_from_formid(p, formid);
        if(index == -1)
        {
          // FIXME: we get this error when user go back in history, so
          // forms are the same but the array has changed
          dt_print(DT_DEBUG_ALWAYS,
                   "rt_process_forms: missing form=%i from array", formid);
          continue;
        }

        // only process current scale
        if(p->rt_forms[index].scale != scale)
        {
          continue;
        }

        // get the spot
        dt_masks_form_t *form = dt_masks_get_from_id_ext(piece->pipe->forms, formid);
        if(form == NULL)
        {
          dt_print(DT_DEBUG_ALWAYS,
                   "rt_process_forms: missing form=%i from masks", formid);
          continue;
        }

        // if the form is outside the roi, we just skip it
        if(!rt_masks_form_is_in_roi(self, piece, form, roi_layer, roi_layer))
        {
          continue;
        }

        // get the mask
        float *mask = NULL;
        dt_iop_roi_t roi_mask = { 0 };

        dt_masks_get_mask(self, piece, form, &mask,
                          &roi_mask.width, &roi_mask.height,
                          &roi_mask.x, &roi_mask.y);
        if(mask == NULL)
        {
          dt_print(DT_DEBUG_ALWAYS, "rt_process_forms: error retrieving mask");
          continue;
        }

        float dx = 0.f, dy = 0.f, angle = 0.f;

        // search the delta with the source
        const dt_iop_retouch_algo_type_t algo = p->rt_forms[index].algorithm;
        if(algo != DT_IOP_RETOUCH_BLUR && algo != DT_IOP_RETOUCH_FILL)
        {
          if(!rt_masks_get_transform_to_destination(self, piece, roi_layer, form, &dx, &dy, &angle,
                                                p->rt_forms[index].distort_mode))
          {
            dt_free_align(mask);
            continue;
          }
        }

        // scale the mask
        cl_mem dev_mask_scaled = NULL;
        float *mask_scaled = NULL;
        dt_iop_roi_t roi_mask_scaled = { 0 };

        err = rt_build_scaled_mask_cl(devid, mask,
                                      &roi_mask,
                                      &mask_scaled,
                                      &dev_mask_scaled,
                                      &roi_mask_scaled,
                                      roi_layer, dx, dy, algo);

        // rotation pivot: mask centroid in roi_mask_scaled-local coords
        // (computed before mask_scaled may be freed below)
        float cx = 0.f, cy = 0.f;
        if(!feqf(angle, 0.0f, 0.01f) && mask_scaled != NULL)
          rt_mask_centroid(mask_scaled, &roi_mask_scaled, &cx, &cy);

        // only heal needs mask scaled
        if(algo != DT_IOP_RETOUCH_HEAL && mask_scaled != NULL)
        {
          dt_free_align(mask_scaled);
          mask_scaled = NULL;
        }

        // we don't need the original mask anymore
        dt_free_align(mask);
        mask = NULL;

        if(mask_scaled == NULL && algo == DT_IOP_RETOUCH_HEAL)
        {
          dt_opencl_release_mem_object(dev_mask_scaled);
          dev_mask_scaled = NULL;
          continue;
        }

        if((err == CL_SUCCESS)
           && (dx != 0
               || dy != 0
               || algo == DT_IOP_RETOUCH_BLUR
               || algo == DT_IOP_RETOUCH_FILL)
           && ((roi_mask_scaled.width > 2) && (roi_mask_scaled.height > 2)))
        {
          if(algo == DT_IOP_RETOUCH_CLONE)
          {
            err = _retouch_clone_cl(devid, dev_layer, roi_layer,
                                    dev_mask_scaled, &roi_mask_scaled, dx, dy, angle, cx, cy,
                                    form_opacity, gd);
          }
          else if(algo == DT_IOP_RETOUCH_HEAL)
          {
            err = _retouch_heal_cl(devid, dev_layer, roi_layer,
                                   dev_mask_scaled, mask_scaled, &roi_mask_scaled, dx,
                                   dy, angle, cx, cy, form_opacity, p->max_heal_iter, gd);
          }
          else if(algo == DT_IOP_RETOUCH_BLUR)
          {
            err = _retouch_blur_cl(devid, dev_layer, roi_layer,
                                   dev_mask_scaled, &roi_mask_scaled,
                                   form_opacity,
                                   p->rt_forms[index].blur_type,
                                   p->rt_forms[index].blur_radius, piece, gd);
          }
          else if(algo == DT_IOP_RETOUCH_FILL)
          {
            // add a brightness to the color so it can be fine-adjusted by the user
            dt_aligned_pixel_t fill_color;

            if(p->rt_forms[index].fill_mode == DT_IOP_RETOUCH_FILL_ERASE)
            {
              fill_color[0] = fill_color[1] = fill_color[2] =
                p->rt_forms[index].fill_brightness;
            }
            else
            {
              fill_color[0] =
                p->rt_forms[index].fill_color[0] + p->rt_forms[index].fill_brightness;
              fill_color[1] =
                p->rt_forms[index].fill_color[1] + p->rt_forms[index].fill_brightness;
              fill_color[2] =
                p->rt_forms[index].fill_color[2] + p->rt_forms[index].fill_brightness;
            }

            err = _retouch_fill_cl(devid, dev_layer, roi_layer,
                                   dev_mask_scaled, &roi_mask_scaled, form_opacity,
                                   fill_color, gd);
          }
          else
          {
            dt_print(DT_DEBUG_ALWAYS,
                     "rt_process_forms: unknown algorithm %i", algo);
          }

          if(mask_display)
          {
            err = rt_copy_mask_to_alpha_cl(devid, dev_layer, roi_layer,
                                     dev_mask_scaled, &roi_mask_scaled,
                                     form_opacity,
                                     gd);
          }
        }

        dt_free_align(mask);
        dt_free_align(mask_scaled);
        dt_opencl_release_mem_object(dev_mask_scaled);
      }
    }
  }

  return err;
}

int process_cl(dt_iop_module_t *self,
               dt_dev_pixelpipe_iop_t *piece,
               cl_mem dev_in,
               cl_mem dev_out,
               const dt_iop_roi_t *const roi_in,
               const dt_iop_roi_t *const roi_out)
{
  dt_iop_retouch_params_t *p = piece->data;
  dt_iop_retouch_global_data_t *gd = self->global_data;
  dt_iop_retouch_gui_data_t *g = self->gui_data;

  cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
  const int devid = piece->pipe->devid;

  dt_iop_roi_t roi_retouch = *roi_in;
  dt_iop_roi_t *roi_rt = &roi_retouch;

  retouch_user_data_t usr_data = { 0 };
  dwt_params_cl_t *dwt_p = NULL;

  const gboolean display_wavelet_scale = g && dt_iop_has_focus(self) ? g->display_wavelet_scale : FALSE;

  // we will do all the clone, heal, etc on the input image, this way
  // the source for one algorithm can be the destination from a
  // previous one
  const cl_mem in_retouch = dt_opencl_alloc_device_buffer(devid, sizeof(float) * 4 * roi_rt->width * roi_rt->height);
  if(in_retouch == NULL)
    goto cleanup;

  // copy input image to the new buffer
  {
    const size_t region[2] = { roi_rt->width, roi_rt->height };
    err = dt_opencl_enqueue_copy_image_to_buffer(devid, dev_in, in_retouch, CLIMG_ORIGIN, region, 0);
    if(err != CL_SUCCESS) goto cleanup;
  }

  // user data passed from the decompose routine to the one that process each scale
  usr_data.self = self;
  usr_data.piece = piece;
  usr_data.roi = *roi_rt;
  usr_data.mask_display = FALSE;
  usr_data.suppress_mask = (g
                            && g->suppress_mask
                            && dt_iop_has_focus(self)
                            && (piece->pipe == self->dev->full.pipe));
  usr_data.display_scale = p->curr_scale;

  // init the decompose routine
  dwt_p = dt_dwt_init_cl(devid, in_retouch, roi_rt->width, roi_rt->height, p->num_scales,
                         (!display_wavelet_scale || !dt_pipe_is_full(piece->pipe))
                         ? 0 : p->curr_scale,
                         p->merge_from_scale,
                         &usr_data,
                         roi_in->scale / piece->iscale);
  if(dwt_p == NULL)
  {
    err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
    goto cleanup;
  }

  // check if this module should expose mask.
  if(dt_pipe_is_full(piece->pipe)
     && g && g->mask_display
     && dt_iop_has_focus(self)
     && (piece->pipe == self->dev->full.pipe))
  {
    const int kernel = gd->kernel_retouch_clear_alpha;
    err = dt_opencl_enqueue_kernel_2d_args(devid, kernel,
                                           roi_rt->width, roi_rt->height,
                                           CLARG(in_retouch),
                                           CLARG((roi_rt->width)),
                                           CLARG((roi_rt->height)));
    if(err != CL_SUCCESS) goto cleanup;

    piece->pipe->mask_display = DT_DEV_PIXELPIPE_DISPLAY_MASK;
    piece->pipe->bypass_blendif = TRUE;
    usr_data.mask_display = TRUE;
  }

  if(dt_pipe_is_full(piece->pipe))
  {
    // check if the image support this number of scales
    if(dt_iop_has_focus(self))
    {
      const int max_scales = dwt_get_max_scale_cl(dwt_p);
      if(dwt_p->scales > max_scales)
      {
        dt_control_log(_("max scale is %i for this image size"), max_scales);
      }
    }
    // get first scale visible at this zoom level
    if(g) g->first_scale_visible = dt_dwt_first_scale_visible_cl(dwt_p);
  }

  // decompose it
  err = dwt_decompose_cl(dwt_p, rt_process_forms_cl);
  if(err != CL_SUCCESS) goto cleanup;

  dt_aligned_pixel_t levels = { p->preview_levels[0],
                                p->preview_levels[1],
                                p->preview_levels[2] };

  // process auto levels
  if(g && dt_pipe_is_full(piece->pipe))
  {
    dt_iop_gui_enter_critical_section(self);
    if(g->preview_auto_levels == 1 && !DT_IN_GUI_UPDATE())
    {
      g->preview_auto_levels = -1;

      dt_iop_gui_leave_critical_section(self);

      levels[0] = levels[1] = levels[2] = 0;
      err = rt_process_stats_cl(self, piece, devid, in_retouch,
                                roi_rt->width, roi_rt->height, levels);
      if(err != CL_SUCCESS) goto cleanup;

      rt_clamp_minmax(levels, levels);

      for(int i = 0; i < 3; i++) g->preview_levels[i] = levels[i];

      dt_iop_gui_enter_critical_section(self);
      g->preview_auto_levels = 2;
    }
    dt_iop_gui_leave_critical_section(self);
  }

  // if user wants to preview a detail scale adjust levels
  if(dwt_p->return_layer > 0
     && dwt_p->return_layer < dwt_p->scales + 1)
  {
    err = rt_adjust_levels_cl(self, piece, devid, in_retouch,
                              roi_rt->width, roi_rt->height, levels);
    if(err != CL_SUCCESS) goto cleanup;
  }

  // copy alpha channel if needed
  if((piece->pipe->mask_display & DT_DEV_PIXELPIPE_DISPLAY_MASK)
     && g
     && !g->mask_display)
  {
    const int kernel = gd->kernel_retouch_copy_alpha;
    err = dt_opencl_enqueue_kernel_2d_args(devid, kernel, roi_rt->width, roi_rt->height,
                                           CLARG(dev_in),
                                           CLARG(in_retouch),
                                           CLARG((roi_rt->width)),
                                           CLARG((roi_rt->height)));
    if(err != CL_SUCCESS) goto cleanup;
  }

  // return final image
  err = rt_copy_in_to_out_cl(devid, in_retouch, roi_in, dev_out, roi_out, 0, 0, 0.0f, 0.0f, 0.0f,
                             gd->kernel_retouch_copy_buffer_to_image);

cleanup:
  if(dwt_p) dt_dwt_free_cl(dwt_p);

  dt_opencl_release_mem_object(in_retouch);

  return err;
}
#endif

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