/*
    This file is part of darktable,
    Copyright (C) 2010-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 "bauhaus/bauhaus.h"
#include "common/debug.h"
#include "common/eaw.h"
#include "common/imagebuf.h"
#include "common/opencl.h"
#include "control/conf.h"
#include "control/control.h"
#include "develop/imageop.h"
#include "develop/imageop_gui.h"
#include "develop/imageop_math.h"
#include "develop/tiling.h"
#include "dtgtk/drawingarea.h"
#include "gui/accelerators.h"
#include "gui/draw.h"
#include "gui/gtk.h"
#include "gui/presets.h"
#include "iop/iop_api.h"

#include <math.h>
#include <memory.h>
#include <stdlib.h>

//#define USE_NEW_CL  //uncomment to use the new, more memory-efficient OpenCL code (not yet finished)

#define INSET DT_PIXEL_APPLY_DPI(5)

DT_MODULE_INTROSPECTION(2, dt_iop_atrous_params_t)

#define BANDS 6
#define MAX_NUM_SCALES 8 // 2*2^(i+1) + 1 = 1025px support for i = 8
#define RES 64

#define dt_atrous_show_upper_label(cr, text, layout, ink)                                                    \
  pango_layout_set_text(layout, text, -1);                                                                   \
  pango_layout_get_pixel_extents(layout, &ink, NULL);                                                        \
  cairo_move_to(cr, .5 * (width - ink.width), (.08 * height) - ink.height);                                  \
  pango_cairo_show_layout(cr, layout);


#define dt_atrous_show_lower_label(cr, text, layout, ink)                                                    \
  pango_layout_set_text(layout, text, -1);                                                                   \
  pango_layout_get_pixel_extents(layout, &ink, NULL);                                                        \
  cairo_move_to(cr, .5 * (width - ink.width), (.98 * height) - ink.height);                                  \
  pango_cairo_show_layout(cr, layout);


typedef enum atrous_channel_t
{
  atrous_L = 0,  // luminance boost
  atrous_c = 1,  // chrominance boost
  atrous_s = 2,  // edge sharpness
  atrous_Lt = 3, // luminance noise threshold
  atrous_ct = 4, // chrominance noise threshold
  atrous_none = 5
} atrous_channel_t;

typedef struct dt_iop_atrous_params_t
{
  int32_t octaves;             // $DEFAULT: 3
  float x[atrous_none][BANDS];
  float y[atrous_none][BANDS]; // $DEFAULT: 0.5
  float mix;                   // $DEFAULT: 1.0 $MIN: -2.0 $MAX: 2.0
} dt_iop_atrous_params_t;

typedef struct dt_iop_atrous_gui_data_t
{
  GtkWidget *mix;
  GtkDrawingArea *area;
  GtkNotebook *channel_tabs;
  double mouse_x, mouse_y, mouse_pick;
  float mouse_radius;
  dt_iop_atrous_params_t drag_params;
  int dragging;
  int x_move;
  dt_draw_curve_t *minmax_curve;
  atrous_channel_t channel, channel2;
  float draw_xs[RES], draw_ys[RES];
  float draw_min_xs[RES], draw_min_ys[RES];
  float draw_max_xs[RES], draw_max_ys[RES];
  float band_hist[MAX_NUM_SCALES];
  float band_max;
  float sample[MAX_NUM_SCALES];
  int num_samples;
  gboolean in_curve;
} dt_iop_atrous_gui_data_t;

typedef struct dt_iop_atrous_global_data_t
{
  int kernel_zero;
  int kernel_decompose;
  int kernel_synthesize;
  int kernel_addbuffers;
} dt_iop_atrous_global_data_t;

typedef struct dt_iop_atrous_data_t
{
  // demosaic pattern
  int32_t octaves;
  dt_draw_curve_t *curve[atrous_none];
} dt_iop_atrous_data_t;


const char *name()
{
  return _("contrast equalizer");
}

const char *aliases()
{
  return _("sharpness|acutance|local contrast|clarity");
}

const char **description(dt_iop_module_t *self)
{
  return dt_iop_set_description(self, _("add or remove local contrast, sharpness, acutance"),
                                      _("corrective and creative"),
                                      _("linear, Lab, scene-referred"),
                                      _("frequential, RGB"),
                                      _("linear, Lab, scene-referred"));
}

int default_group()
{
  return IOP_GROUP_CORRECT | IOP_GROUP_EFFECTS;
}

int flags()
{
  return IOP_FLAGS_SUPPORTS_BLENDING | IOP_FLAGS_ALLOW_TILING;
}

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_LAB;
}

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_atrous_params_v2_t
  {
    int32_t octaves;
    float x[atrous_none][BANDS];
    float y[atrous_none][BANDS];
    float mix;
  } dt_iop_atrous_params_v2_t;

  if(old_version == 1)
  {
    typedef struct dt_iop_atrous_params_v1_t
    {
      int32_t octaves;             // $DEFAULT: 3
      float x[atrous_none][BANDS];
      float y[atrous_none][BANDS]; // $DEFAULT: 0.5
    } dt_iop_atrous_params_v1_t;

    const dt_iop_atrous_params_v1_t *o = (dt_iop_atrous_params_v1_t *)old_params;
    dt_iop_atrous_params_v2_t *n = malloc(sizeof(dt_iop_atrous_params_v2_t));

    memcpy(n, o, sizeof(dt_iop_atrous_params_v1_t));
    n->mix = 1.0f;

    *new_params = n;
    *new_params_size = sizeof(dt_iop_atrous_params_v2_t);
    *new_version = 2;
    return 0;
  }

  return 1;
}

static int get_samples(float *t,
                       const dt_iop_atrous_data_t *const d,
                       const dt_iop_roi_t *roi_in,
                       const dt_dev_pixelpipe_iop_t *const piece)
{
  const float scale = roi_in->scale;
  const float supp0 =
    MIN(2 * (2 << (MAX_NUM_SCALES - 1)) + 1,
        MAX(piece->buf_in.height,
            piece->buf_in.width) * 0.2f);
  const float i0 = dt_log2f((supp0 - 1.0f) * .5f);
  int i = 0;
  for(; i < MAX_NUM_SCALES; i++)
  {
    // actual filter support on scaled buffer
    const float supp = 2 * (2 << i) + 1;
    // approximates this filter size on unscaled input image:
    const float supp_in = supp * (1.0f / scale);
    const float i_in = dt_log2f((supp_in - 1) * .5f) - 1.0f;
    t[i] = 1.0f - (i_in + .5f) / i0;
    if(t[i] < 0.0f) break;
  }
  return i;
}

static int get_scales(float (*thrs)[4],
                      float (*boost)[4],
                      float *sharp, const
                      dt_iop_atrous_data_t *const d,
                      const dt_iop_roi_t *roi_in,
                      const dt_dev_pixelpipe_iop_t *const piece)
{
  // we want coeffs to span max 20% of the image
  // finest is 5x5 filter
  //
  // 1:1 : w=20% buf_in.width                     w=5x5
  //     : ^ ...            ....            ....  ^
  // buf :  17x17  9x9  5x5     2*2^k+1
  // .....
  // . . . . .
  // .   .   .   .   .
  // cut off too fine ones, if image is not detailed enough (due to roi_in->scale)
  const float scale = roi_in->scale / piece->iscale;
  // largest desired filter on input buffer (20% of input dim)
  const float supp0 =
    MIN(2 * (2 << (MAX_NUM_SCALES - 1)) + 1,
        MAX(piece->buf_in.height * piece->iscale,
            piece->buf_in.width * piece->iscale) * 0.2f);
  const float i0 = dt_log2f((supp0 - 1.0f) * .5f);
  int i = 0;
  for(; i < MAX_NUM_SCALES; i++)
  {
    // actual filter support on scaled buffer
    const float supp = 2 * (2 << i) + 1;
    // approximates this filter size on unscaled input image:
    const float supp_in = supp * (1.0f / scale);
    const float i_in = dt_log2f((supp_in - 1) * .5f) - 1.0f;
    // i_in = max_scale .. .. .. 0
    const float t = 1.0f - (i_in + .5f) / i0;
    boost[i][3] = boost[i][0] = 2.0f * dt_draw_curve_calc_value(d->curve[atrous_L], t);
    boost[i][1] = boost[i][2] = 2.0f * dt_draw_curve_calc_value(d->curve[atrous_c], t);
    for(int k = 0; k < 4; k++) boost[i][k] *= boost[i][k];
    thrs[i][0] = thrs[i][3] = powf(2.0f, -7.0f * (1.0f - t)) * 10.0f
                              * dt_draw_curve_calc_value(d->curve[atrous_Lt], t);
    thrs[i][1] = thrs[i][2] = powf(2.0f, -7.0f * (1.0f - t)) * 20.0f
                              * dt_draw_curve_calc_value(d->curve[atrous_ct], t);
    sharp[i] = 0.0025f * dt_draw_curve_calc_value(d->curve[atrous_s], t);
    // printf("scale %d boost %f %f thrs %f %f sharpen %f\n", i, boost[i][0], boost[i][2], thrs[i][0],
    // thrs[i][1], sharp[i]);
    if(t < 0.0f) break;
  }
  // ensure that return value max_scale is such that
  // 2 * 2 *(1 << max_scale) <= min(width, height)
  const int max_scale_roi =
    (int)floorf(dt_log2f((float)MIN(roi_in->width, roi_in->height))) - 2;
  return MIN(max_scale_roi, i);
}

/* just process the supplied image buffer, upstream
 * default_process_tiling() does the rest */
static void process_wavelets(dt_iop_module_t *self,
                             dt_dev_pixelpipe_iop_t *piece,
                             const void *const i,
                             void *const o,
                             const dt_iop_roi_t *const roi_in,
                             const dt_iop_roi_t *const roi_out)
{
  const dt_iop_atrous_data_t *d = piece->data;
  dt_aligned_pixel_t thrs[MAX_NUM_SCALES];
  dt_aligned_pixel_t boost[MAX_NUM_SCALES];
  float sharp[MAX_NUM_SCALES];
  const int max_scale = get_scales(thrs, boost, sharp, d, roi_in, piece);
  const int max_mult = 1u << (max_scale - 1);

  const int width = roi_out->width;
  const int height = roi_out->height;

  if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe))
  {
    dt_iop_atrous_gui_data_t *g = self->gui_data;
    g->num_samples = get_samples(g->sample, d, roi_in, piece);
    // tries to acquire gdk lock and this prone to deadlock:
    // dt_control_queue_draw(GTK_WIDGET(g->area));
  }

  // corner case of extremely small image. this is not really likely
  // to happen but would lead to out of bounds memory access
  if(width < 2 * max_mult || height < 2 * max_mult)
  {
    dt_iop_image_copy_by_size(o, i, width, height, 4);
    return;
  }

  float *const restrict out = (float*)o;
  float *restrict tmp = NULL;
  float *restrict tmp2 = NULL;

  if(!dt_iop_alloc_image_buffers(self, roi_in, roi_out, 4, &tmp, 4, &tmp2, 0))
  {
    dt_iop_copy_image_roi(out, i, piece->colors, roi_in, roi_out);
    return;
  }

  float *buf1 = (float *)i;
  float *buf2 = tmp;

  // clear the output buffer, which will be accumulating all of the detail scales
  dt_iop_image_fill(out, 0.0f, width, height, 4);

  // now do the wavelet decomposition, immediately synthesizing the
  // detail scale into the final output so that we don't need to store
  // it past the current scale's iteration
  for(int scale = 0; scale < max_scale; scale++)
  {
    eaw_decompose_and_synthesize(buf2, buf1, out, scale, sharp[scale], thrs[scale],
                                 boost[scale], width, height);
    if(scale == 0) buf1 = (float *)tmp2; // now switch to second
                                         // scratch for buffer
                                         // ping-pong between buf1 and
                                         // buf2
    float *buf3 = buf2;
    buf2 = buf1;
    buf1 = buf3;
  }

  // add in the final residue
  DT_OMP_FOR_SIMD(aligned(buf1, out : 64) num_threads(MIN(dt_get_num_threads(),16)))
  for(size_t k = 0; k < (size_t)4 * width * height; k++)
    out[k] += buf1[k];

  dt_free_align(tmp);
  dt_free_align(tmp2);
  return;
}

void process(dt_iop_module_t *self,
             dt_dev_pixelpipe_iop_t *piece,
             const void *const i,
             void *const o,
             const dt_iop_roi_t *const roi_in,
             const dt_iop_roi_t *const roi_out)
{
  process_wavelets(self, piece, i, o, roi_in, roi_out);
}

#ifdef HAVE_OPENCL

#ifdef USE_NEW_CL
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)
{
  const dt_iop_atrous_data_t *d = piece->data;
  const dt_iop_atrous_global_data_t *gd = self->global_data;
  const int devid = piece->pipe->devid;

  dt_aligned_pixel_t thrs[MAX_NUM_SCALES];
  dt_aligned_pixel_t boost[MAX_NUM_SCALES];
  float sharp[MAX_NUM_SCALES];
  const int max_scale = get_scales(thrs, boost, sharp, d, roi_in, piece);

  if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe))
  {
    dt_iop_atrous_gui_data_t *g = self->gui_data;
    g->num_samples = get_samples(g->sample, d, roi_in, piece);
    // dt_control_queue_redraw_widget(GTK_WIDGET(g->area));
    // tries to acquire gdk lock and this prone to deadlock:
    // dt_control_queue_draw(GTK_WIDGET(g->area));
  }

  cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
  cl_mem dev_filter = NULL;
  cl_mem dev_tmp = NULL;
  cl_mem dev_tmp2 = NULL;
  cl_mem dev_detail = NULL;

  const float m[5] = { 0.0625f, 0.25f, 0.375f, 0.25f, 0.0625f }; // 1/16, 4/16, 6/16, 4/16, 1/16
  float mm[5][5];
  for(int j = 0; j < 5; j++)
    for(int i = 0; i < 5; i++) mm[j][i] = m[i] * m[j];

  const int width = roi_out->width;
  const int height = roi_out->height;
  dev_filter = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 25, mm);

  /* allocate space for two temporary buffer to participate_in in the
     buffer ping-pong below.
     We need dev_out to accumulate the result
     and dev_in must stay unchanged
  */
  dev_tmp = dt_opencl_alloc_device(devid, width, height, sizeof(float) * 4);
  dev_tmp2 = dt_opencl_alloc_device(devid, width, height, sizeof(float) * 4);
  /* allocate a buffer for storing the detail information. */
  dev_detail = dt_opencl_alloc_device(devid, width, height, sizeof(float) * 4);
  if(!dev_detail || !dev_tmp || !dev_tmp2 || !dev_filter) goto error;

  // clear dev_out to zeros, as we will be incrementally accumulating results there
  err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_zero, width, height,
    CLARG(dev_out), CLARG(width), CLARG(height));
  if(err != CL_SUCCESS) goto error;

  // the buffers for the buffer ping-pong.  We start with dev_in as
  // the input half for the first scale, then switch to using dev_tmp
  // and dev_tmp2 as the two scratch buffers at the end of scaling loop
  cl_mem pp_in = dev_in;
  cl_mem pp_coarse = dev_tmp;

  /* decompose image into detail scales and coarse (the latter is left
     in dev_tmp or dev_out)
  */
  for(int s = 0; s < max_scale; s++)
  {
    const int scale = s;

    // run the decomposition
    err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_decompose, width, height,
                              CLARG(pp_in), CLARG(pp_coarse), CLARG(dev_detail),
                              CLARG(width), CLARG(height),
                              CLARG(scale), CLARG(sharp[s]), CLARG(dev_filter));
    if(err != CL_SUCCESS) goto error;

    // now immediately run the synthesis for the current scale, accumulating the details into dev_out
    // dev_out as the accumulator must be given twice as an OpenCL 1.2 workaround
    // Is this safe here? or would we need another temp buff and accumalate?
    err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_synthesize, width, height,
                              CLARG(dev_out), CLARG(pp_coarse), CLARG(dev_detail),
                              CLARG(width), CLARG(height),
                              CLARG(thrs[scale]), CLARG(boost[scale]));
    if(err != CL_SUCCESS) goto error;

    // swap scratch buffers but leave as is for the final round to keep pp_coarse correct
    if(s != max_scale -1)
    {
      cl_mem tmp = (s == 0) ? dev_tmp2 : pp_in;
      pp_in = pp_coarse;
      pp_coarse = tmp;
    }
  }

  /* Add the residue, the coarse scale from the final decomposition, to the accumulated
     details. The kernel reads its accumulator and writes the sum, so the two cannot be
     the same image: clSetKernelArg leaves the result undefined when one image object is
     bound as both a read_only and a write_only argument.

     After the loop pp_coarse holds the residue and is one of the two scratch buffers,
     which leaves the other free to receive the sum before it is copied out. */
  cl_mem residue_dst = (pp_coarse == dev_tmp) ? dev_tmp2 : dev_tmp;
  err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_addbuffers, width, height,
          CLARG(residue_dst), CLARG(dev_out), CLARG(pp_coarse),
          CLARG(width), CLARG(height));
  if(err != CL_SUCCESS) goto error;

  err = dt_opencl_enqueue_copy_image(devid, residue_dst, dev_out,
                                     CLIMG_ORIGIN, CLIMG_ORIGIN, area);
error:
  dt_opencl_release_mem_object(dev_filter);
  dt_opencl_release_mem_object(dev_tmp);
  dt_opencl_release_mem_object(dev_tmp2);
  dt_opencl_release_mem_object(dev_detail);
  return err;
}

#else // ======== old, memory-hungry implementation ========================================================

/* this version is adapted to the new global tiling mechanism. it no longer does tiling by itself. */
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)
{
  const dt_iop_atrous_data_t *d = piece->data;
  const dt_iop_atrous_global_data_t *gd = self->global_data;
  const int devid = piece->pipe->devid;

  dt_aligned_pixel_t thrs[MAX_NUM_SCALES];
  dt_aligned_pixel_t boost[MAX_NUM_SCALES];
  float sharp[MAX_NUM_SCALES];
  const int max_scale = get_scales(thrs, boost, sharp, d, roi_in, piece);

  if(self->dev->gui_attached && dt_pipe_is_full(piece->pipe))
  {
    dt_iop_atrous_gui_data_t *g = self->gui_data;
    g->num_samples = get_samples(g->sample, d, roi_in, piece);
    // dt_control_queue_redraw_widget(GTK_WIDGET(g->area));
    // tries to acquire gdk lock and this prone to deadlock:
    // dt_control_queue_draw(GTK_WIDGET(g->area));
  }

  cl_int err = DT_OPENCL_DEFAULT_ERROR;
  cl_mem dev_filter = NULL;
  cl_mem dev_tmp = NULL;
  cl_mem *dev_detail = calloc(max_scale, sizeof(cl_mem));

  const float m[5] = { 0.0625f, 0.25f, 0.375f, 0.25f, 0.0625f }; // 1/16, 4/16, 6/16, 4/16, 1/16
  float mm[5][5];
  for(int j = 0; j < 5; j++)
    for(int i = 0; i < 5; i++)
      mm[j][i] = m[i] * m[j];

  const int width = roi_out->width;
  const int height = roi_out->height;

  dev_filter = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 25, mm);

  /* allocate space for a temporary buffer. we don't want to use
     dev_in in the buffer ping-pong below, as we need to keep it for
     blendops */
  dev_tmp = dt_opencl_alloc_device(devid, width, height, sizeof(float) * 4);
  if(!dev_tmp || !dev_filter) goto error;

  /* allocate space to store detail information. Requires a number of
   * additional buffers, each with full image size */
  for(int k = 0; k < max_scale; k++)
  {
    dev_detail[k] = dt_opencl_alloc_device(devid, width, height, sizeof(float) * 4);
    if(dev_detail[k] == NULL) goto error;
  }

  const size_t region[2] = { width, height };
  // copy original input from dev_in -> dev_out as starting point
  err = dt_opencl_enqueue_copy_image(devid, dev_in, dev_out, CLIMG_ORIGIN, CLIMG_ORIGIN, region);
  if(err != CL_SUCCESS) goto error;

  /* decompose image into detail scales and coarse (the latter is left
   * in dev_tmp or dev_out) */
  for(int s = 0; s < max_scale; s++)
  {
    const int scale = s;

    if(s & 1)
      err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_decompose, width, height,
                                CLARG(dev_tmp), CLARG(dev_out),
                                CLARG(dev_detail[s]), CLARG(width), CLARG(height),
                                CLARG(scale), CLARG(sharp[s]), CLARG(dev_filter));
    else
      err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_decompose, width, height,
                                CLARG(dev_out), CLARG(dev_tmp),
                                CLARG(dev_detail[s]), CLARG(width), CLARG(height),
                                CLARG(scale), CLARG(sharp[s]), CLARG(dev_filter));
    if(err != CL_SUCCESS) goto error;
  }

  /* now synthesize again */
  for(int scale = max_scale - 1; scale >= 0; scale--)
  {
    if(scale & 1)
      err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_synthesize, width, height,
                              CLARG(dev_tmp), CLARG(dev_out),
                              CLARG(dev_detail[scale]),
                              CLARG(width), CLARG(height),
                              CLARG(thrs[scale]), CLARG(boost[scale]));

    else
      err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_synthesize, width, height,
                              CLARG(dev_out), CLARG(dev_tmp),
                              CLARG(dev_detail[scale]),
                              CLARG(width), CLARG(height),
                              CLARG(thrs[scale]), CLARG(boost[scale]));
    if(err != CL_SUCCESS) goto error;
  }
  dt_opencl_finish_sync_pipe(devid, piece->pipe->type);

error:
  dt_opencl_release_mem_object(dev_filter);
  dt_opencl_release_mem_object(dev_tmp);
  for(int k = 0; k < max_scale; k++)
    dt_opencl_release_mem_object(dev_detail[k]);
  free(dev_detail);
  return err;
}
#endif // USE_NEW_CL

#endif // HAVE_OPENCL

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_atrous_data_t *d = piece->data;
  dt_aligned_pixel_t thrs[MAX_NUM_SCALES];
  dt_aligned_pixel_t boost[MAX_NUM_SCALES];
  float sharp[MAX_NUM_SCALES];
  const int max_scale = get_scales(thrs, boost, sharp, d, roi_in, piece);
  const int max_filter_radius = 2 * (1 << max_scale); // 2 * 2^max_scale

  tiling->factor = 4.0f;                // in + out + 2*tmp
  //  tiling->factor_cl = 5.0f; // in + out + details + 2*tmp - new flipping code
  tiling->factor_cl = 3.0f + max_scale; // in + out + tmp + scale buffers
  tiling->maxbuf = 1.0f;
  tiling->maxbuf_cl = 1.0f;
  tiling->overhead = 0;
  tiling->overlap = max_filter_radius;
  tiling->align = 1;
  return;
}

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

  dt_iop_atrous_params_t *d = self->default_params;

  for(int k = 0; k < BANDS; k++)
  {
    d->y[atrous_Lt][k] = d->y[atrous_ct][k] = 0.0f;
    for(int c = atrous_L; c <= atrous_ct; c++)
      d->x[c][k] = k / (BANDS - 1.0f);
  }
}

void init_global(dt_iop_module_so_t *self)
{
  const int program = 1; // from programs.conf
  dt_iop_atrous_global_data_t *gd = malloc(sizeof(dt_iop_atrous_global_data_t));
  self->data = gd;
  gd->kernel_decompose = dt_opencl_create_kernel(program, "eaw_decompose");
  gd->kernel_synthesize = dt_opencl_create_kernel(program, "eaw_synthesize");
#ifdef USE_NEW_CL
  gd->kernel_zero = dt_opencl_create_kernel(program, "eaw_zero");
  gd->kernel_addbuffers = dt_opencl_create_kernel(program, "eaw_addbuffers");
#endif
}

void cleanup_global(dt_iop_module_so_t *self)
{
  dt_iop_atrous_global_data_t *gd = self->data;
  dt_opencl_free_kernel(gd->kernel_decompose);
  dt_opencl_free_kernel(gd->kernel_synthesize);
#ifdef USE_NEW_CL
  dt_opencl_free_kernel(gd->kernel_zero);
  dt_opencl_free_kernel(gd->kernel_addbuffers);
#endif
  free(self->data);
  self->data = NULL;
}

static inline void _apply_mix(dt_iop_module_t *self,
                              const int ch,
                              const int k,
                              const float mix,
                              const float px,
                              const float py,
                              float *x,
                              float *y)
{
  const dt_iop_atrous_params_t *const dp = self->default_params;
  *x = fminf(1.0f, fmaxf(0.0f, px + (mix - 1.0f) * (px - dp->x[ch][k])));
  *y = fminf(1.0f, fmaxf(0.0f, py + (mix - 1.0f) * (py - dp->y[ch][k])));
}

void commit_params(dt_iop_module_t *self,
                   dt_iop_params_t *params,
                   dt_dev_pixelpipe_t *pipe,
                   dt_dev_pixelpipe_iop_t *piece)
{
  dt_iop_atrous_params_t *p = (dt_iop_atrous_params_t *)params;
  dt_iop_atrous_data_t *d = piece->data;

  d->octaves = p->octaves;
  for(int ch = 0; ch < atrous_none; ch++)
    for(int k = 0; k < BANDS; k++)
    {
      float x, y;
      _apply_mix(self, ch, k, p->mix, p->x[ch][k], p->y[ch][k], &x, &y);
      dt_draw_curve_set_point(d->curve[ch], k, x, y);
    }
  int l = 0;
  for(int k = (int)MIN(pipe->iwidth * pipe->iscale, pipe->iheight * pipe->iscale);
      k;
      k >>= 1)
    l++;

  d->octaves = MIN(BANDS, l);
}

void init_pipe(dt_iop_module_t *self,
               dt_dev_pixelpipe_t *pipe,
               dt_dev_pixelpipe_iop_t *piece)
{
  dt_iop_atrous_data_t *d = malloc(sizeof(dt_iop_atrous_data_t));
  const dt_iop_atrous_params_t *const default_params = self->default_params;
  piece->data = (void *)d;
  for(int ch = 0; ch < atrous_none; ch++)
  {
    d->curve[ch] = dt_draw_curve_new(0.0, 1.0, CATMULL_ROM);
    for(int k = 0; k < BANDS; k++)
      (void)dt_draw_curve_add_point(d->curve[ch],
                                    default_params->x[ch][k], default_params->y[ch][k]);
  }
  int l = 0;
  for(int k = (int)MIN(pipe->iwidth * pipe->iscale, pipe->iheight * pipe->iscale);
      k;
      k >>= 1)
    l++;

  d->octaves = MIN(BANDS, l);
}

void cleanup_pipe(dt_iop_module_t *self,
                  dt_dev_pixelpipe_t *pipe,
                  dt_dev_pixelpipe_iop_t *piece)
{
  dt_iop_atrous_data_t *d = piece->data;
  for(int ch = 0; ch < atrous_none; ch++)
    dt_draw_curve_destroy(d->curve[ch]);
  free(piece->data);
  piece->data = NULL;
}

#define GAUSS(x, sigma) expf( -(1.0f - x) * (1.0f - x) / (sigma * sigma)) / (2.0 * sigma * powf(M_PI, 0.5f))

void init_presets(dt_iop_module_so_t *self)
{
  dt_database_start_transaction(darktable.db);
  dt_iop_atrous_params_t p;
  p.octaves = 7;
  p.mix = 1.0f;

  for(int k = 0; k < BANDS; k++)
  {
    p.x[atrous_L][k] = k / (BANDS - 1.0);
    p.x[atrous_c][k] = k / (BANDS - 1.0);
    p.x[atrous_s][k] = k / (BANDS - 1.0);
    p.y[atrous_L][k] = fmaxf(.5f, .75f - .5f * k / (BANDS - 1.0));
    p.y[atrous_c][k] = fmaxf(.5f, .55f - .5f * k / (BANDS - 1.0));
    p.y[atrous_s][k] = fminf(.5f, .2f + .35f * k / (BANDS - 1.0));
    p.x[atrous_Lt][k] = k / (BANDS - 1.0);
    p.x[atrous_ct][k] = k / (BANDS - 1.0);
    p.y[atrous_Lt][k] = 0.0f;
    p.y[atrous_ct][k] = 0.0f;
  }
  dt_gui_presets_add_generic(_("coarse"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);
  for(int k = 0; k < BANDS; k++)
  {
    p.x[atrous_L][k] = k / (BANDS - 1.0);
    p.x[atrous_c][k] = k / (BANDS - 1.0);
    p.x[atrous_s][k] = k / (BANDS - 1.0);
    p.y[atrous_L][k] = .5f + .25f * k / (float)BANDS;
    p.y[atrous_c][k] = .5f;
    p.y[atrous_s][k] = .5f;
    p.x[atrous_Lt][k] = k / (BANDS - 1.0);
    p.x[atrous_ct][k] = k / (BANDS - 1.0);
    p.y[atrous_Lt][k] = .2f * k / (float)BANDS;
    p.y[atrous_ct][k] = .3f * k / (float)BANDS;
  }
  dt_gui_presets_add_generic(_("denoise & sharpen"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);
  for(int k = 0; k < BANDS; k++)
  {
    p.x[atrous_L][k] = k / (BANDS - 1.0);
    p.x[atrous_c][k] = k / (BANDS - 1.0);
    p.x[atrous_s][k] = k / (BANDS - 1.0);
    p.y[atrous_L][k] = .5f + .25f * k / (float)BANDS;
    p.y[atrous_c][k] = .5f;
    p.y[atrous_s][k] = .5f;
    p.x[atrous_Lt][k] = k / (BANDS - 1.0);
    p.x[atrous_ct][k] = k / (BANDS - 1.0);
    p.y[atrous_Lt][k] = 0.0f;
    p.y[atrous_ct][k] = 0.0f;
  }
  dt_gui_presets_add_generic(_("sharpen"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);
  for(int k = 0; k < BANDS; k++)
  {
    p.x[atrous_L][k] = k / (BANDS - 1.0);
    p.x[atrous_c][k] = k / (BANDS - 1.0);
    p.x[atrous_s][k] = k / (BANDS - 1.0);
    p.y[atrous_L][k] = .5f;
    p.y[atrous_c][k] = .5f;
    p.y[atrous_s][k] = .0f;
    p.x[atrous_Lt][k] = k / (BANDS - 1.0);
    p.x[atrous_ct][k] = k / (BANDS - 1.0);
    p.y[atrous_Lt][k] = .0f;
    p.y[atrous_ct][k] = fmaxf(0.0f, (.60f * k / (float)BANDS) - 0.30f);
  }
  dt_gui_presets_add_generic(_("denoise chroma"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);
  for(int k = 0; k < BANDS; k++)
  {
    p.x[atrous_L][k] = k / (BANDS - 1.0);
    p.x[atrous_c][k] = k / (BANDS - 1.0);
    p.x[atrous_s][k] = k / (BANDS - 1.0);
    p.y[atrous_L][k] = .5f; //-.2f*k/(float)BANDS;
    p.y[atrous_c][k] = .5f; // fmaxf(0.0f, .5f-.3f*k/(float)BANDS);
    p.y[atrous_s][k] = .5f;
    p.x[atrous_Lt][k] = k / (BANDS - 1.0);
    p.x[atrous_ct][k] = k / (BANDS - 1.0);
    p.y[atrous_Lt][k] = .2f * k / (float)BANDS;
    p.y[atrous_ct][k] = .3f * k / (float)BANDS;
  }
  dt_gui_presets_add_generic(_("denoise"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);
  for(int k = 0; k < BANDS; k++)
  {
    p.x[atrous_L][k] = k / (BANDS - 1.0);
    p.x[atrous_c][k] = k / (BANDS - 1.0);
    p.x[atrous_s][k] = k / (BANDS - 1.0);
    p.y[atrous_L][k] = fminf(.5f, .3f + .35f * k / (BANDS - 1.0));
    p.y[atrous_c][k] = .5f;
    p.y[atrous_s][k] = .0f;
    p.x[atrous_Lt][k] = k / (BANDS - 1.0);
    p.x[atrous_ct][k] = k / (BANDS - 1.0);
    p.y[atrous_Lt][k] = 0.0f;
    p.y[atrous_ct][k] = 0.0f;
  }
  p.y[atrous_L][0] = .5f;
  dt_gui_presets_add_generic(_("bloom"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);
  for(int k = 0; k < BANDS; k++)
  {
    p.x[atrous_L][k] = k / (BANDS - 1.0);
    p.x[atrous_c][k] = k / (BANDS - 1.0);
    p.x[atrous_s][k] = k / (BANDS - 1.0);
    p.y[atrous_L][k] = 0.6f;
    p.y[atrous_c][k] = .55f;
    p.y[atrous_s][k] = .0f;
    p.x[atrous_Lt][k] = k / (BANDS - 1.0);
    p.x[atrous_ct][k] = k / (BANDS - 1.0);
    p.y[atrous_Lt][k] = 0.0f;
    p.y[atrous_ct][k] = 0.0f;
  }
  dt_gui_presets_add_generic(_("clarity"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);

  float sigma = 3.f / (float)(BANDS - 1);

  for(int k = 0; k < BANDS; k++)
  {
    const float x = k / (float)(BANDS - 1);
    const float fine = GAUSS(x, 0.5 * sigma);
    const float medium = GAUSS(x, sigma);
    const float coarse = GAUSS(x, 2 * sigma);
    const float coeff = 0.5f + (coarse + medium + fine) / 16.0f;
    const float noise = (coarse + medium + fine) / 128.f;

    p.x[atrous_L][k] = p.x[atrous_c][k] = p.x[atrous_s][k] = x;
    p.y[atrous_L][k] = p.y[atrous_s][k] = coeff;
    p.y[atrous_c][k] = 0.5f;
    p.x[atrous_Lt][k] = p.x[atrous_ct][k] = x;
    p.y[atrous_Lt][k] = p.y[atrous_ct][k] = noise;
  }
  dt_gui_presets_add_generic(_("deblur | large blur | strength 3"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);

  for(int k = 0; k < BANDS; k++)
  {
    const float x = k / (float)(BANDS - 1);
    const float fine = GAUSS(x, 0.5 * sigma);
    const float medium = GAUSS(x, sigma);
    const float coeff = 0.5f + (medium + fine) / 16.0f;
    const float noise = (medium + fine) / 128.f;

    p.x[atrous_L][k] = p.x[atrous_c][k] = p.x[atrous_s][k] = x;
    p.y[atrous_L][k] = p.y[atrous_s][k] = coeff;
    p.y[atrous_c][k] = 0.5f;
    p.x[atrous_Lt][k] = p.x[atrous_ct][k] = x;
    p.y[atrous_Lt][k] = p.y[atrous_ct][k] = noise;
  }
  dt_gui_presets_add_generic(_("deblur | medium blur | strength 3"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);

  for(int k = 0; k < BANDS; k++)
  {
    const float x =  k / (float)(BANDS - 1);
    const float fine = GAUSS(x, 0.5 * sigma);
    const float coeff = 0.5f + fine / 16.f;
    const float noise = fine / 128.f;

    p.x[atrous_L][k] = p.x[atrous_c][k] = p.x[atrous_s][k] = x;
    p.y[atrous_L][k] = p.y[atrous_s][k] = coeff;
    p.y[atrous_c][k] = 0.5f;
    p.x[atrous_Lt][k] = p.x[atrous_ct][k] = x;
    p.y[atrous_Lt][k] = p.y[atrous_ct][k] = noise;
  }
  dt_gui_presets_add_generic(_("deblur | fine blur | strength 3"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);

  for(int k = 0; k < BANDS; k++)
  {
    const float x =  k / (float)(BANDS - 1);
    const float fine = GAUSS(x, 0.5 * sigma);
    const float medium = GAUSS(x, sigma);
    const float coarse = GAUSS(x, 2 * sigma);
    const float coeff = 0.5f + (coarse + medium + fine) / 24.0f;
    const float noise = (coarse + medium + fine) / 192.f;

    p.x[atrous_L][k] = p.x[atrous_c][k] = p.x[atrous_s][k] = x;
    p.y[atrous_L][k] = p.y[atrous_s][k] = coeff;
    p.y[atrous_c][k] = 0.5f;
    p.x[atrous_Lt][k] = p.x[atrous_ct][k] = x;
    p.y[atrous_Lt][k] = p.y[atrous_ct][k] = noise;
  }
  dt_gui_presets_add_generic(_("deblur | large blur | strength 2"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);

  for(int k = 0; k < BANDS; k++)
  {
    const float x =  k / (float)(BANDS - 1);
    const float fine = GAUSS(x, 0.5 * sigma);
    const float medium = GAUSS(x, sigma);
    const float coeff = 0.5f + (medium + fine) / 24.0f;
    const float noise = (medium + fine) / 192.f;

    p.x[atrous_L][k] = p.x[atrous_c][k] = p.x[atrous_s][k] = x;
    p.y[atrous_L][k] = p.y[atrous_s][k] = coeff;
    p.y[atrous_c][k] = 0.5f;
    p.x[atrous_Lt][k] = p.x[atrous_ct][k] = x;
    p.y[atrous_Lt][k] = p.y[atrous_ct][k] = noise;
  }
  dt_gui_presets_add_generic(_("deblur | medium blur | strength 2"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);

  for(int k = 0; k < BANDS; k++)
  {
    const float x =  k / (float)(BANDS - 1);
    const float fine = GAUSS(x, 0.5 * sigma);
    const float coeff = 0.5f + fine / 24.0f;
    const float noise = fine / 192.f;

    p.x[atrous_L][k] = p.x[atrous_c][k] = p.x[atrous_s][k] = x;
    p.y[atrous_L][k] = p.y[atrous_s][k] = coeff;
    p.y[atrous_c][k] = 0.5f;
    p.x[atrous_Lt][k] = p.x[atrous_ct][k] = x;
    p.y[atrous_Lt][k] = p.y[atrous_ct][k] = noise;
  }
  dt_gui_presets_add_generic(_("deblur | fine blur | strength 2"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);

  for(int k = 0; k < BANDS; k++)
  {
    const float x =  k / (float)(BANDS - 1);
    const float fine = GAUSS(x, 0.5 * sigma);
    const float medium = GAUSS(x, sigma);
    const float coarse = GAUSS(x, 2 * sigma);
    const float coeff = 0.5f + (coarse + medium + fine) / 32.0f;
    const float noise = (coarse + medium + fine) / 128.f;

    p.x[atrous_L][k] = p.x[atrous_c][k] = p.x[atrous_s][k] = x;
    p.y[atrous_L][k] = p.y[atrous_s][k] = coeff;
    p.y[atrous_c][k] = 0.5f;
    p.x[atrous_Lt][k] = p.x[atrous_ct][k] = x;
    p.y[atrous_Lt][k] = p.y[atrous_ct][k] = noise;
  }
  dt_gui_presets_add_generic(_("deblur | large blur | strength 1"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);

  for(int k = 0; k < BANDS; k++)
  {
    const float x =  k / (float)(BANDS - 1);
    const float fine = GAUSS(x, 0.5 * sigma);
    const float medium = GAUSS(x, sigma);
    const float coeff = 0.5f + (medium + fine) / 32.0f;
    const float noise = (medium + fine) / 128.f;

    p.x[atrous_L][k] = p.x[atrous_c][k] = p.x[atrous_s][k] = x;
    p.y[atrous_L][k] = p.y[atrous_s][k] = coeff;
    p.y[atrous_c][k] = 0.5f;
    p.x[atrous_Lt][k] = p.x[atrous_ct][k] = x;
    p.y[atrous_Lt][k] = p.y[atrous_ct][k] = noise;
  }
  dt_gui_presets_add_generic(_("deblur | medium blur | strength 1"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);

  for(int k = 0; k < BANDS; k++)
  {
    const float x =  k / (float)(BANDS - 1);
    const float fine = GAUSS(x, 0.5 * sigma);
    const float coeff = 0.5f + fine / 32.f;
    const float noise = fine / 128.f;

    p.x[atrous_L][k] = p.x[atrous_c][k] = p.x[atrous_s][k] = x;
    p.y[atrous_L][k] = p.y[atrous_s][k] = coeff;
    p.y[atrous_c][k] = 0.5f;
    p.x[atrous_Lt][k] = p.x[atrous_ct][k] = x;
    p.y[atrous_Lt][k] = p.y[atrous_ct][k] = noise;
  }
  dt_gui_presets_add_generic(_("deblur | fine blur | strength 1"), self->op,
                             self->version(), &p, sizeof(p), TRUE,
                             DEVELOP_BLEND_CS_RGB_DISPLAY);

  dt_database_release_transaction(darktable.db);
}

static void reset_mix(dt_iop_module_t *self)
{
  dt_iop_atrous_gui_data_t *g = self->gui_data;
  dt_iop_atrous_params_t *p = self->params;
  g->drag_params = *p;
  DT_ENTER_GUI_UPDATE();
  dt_bauhaus_slider_set(g->mix, p->mix);
  DT_LEAVE_GUI_UPDATE();
  gtk_widget_queue_draw(GTK_WIDGET(g->area));
}

void gui_update(dt_iop_module_t *self)
{
  reset_mix(self);
}


// gui stuff:

static gboolean area_enter_leave_notify(GtkWidget *widget,
                                        GdkEventCrossing *event,
                                        dt_iop_module_t *self)
{
  dt_iop_atrous_gui_data_t *g = self->gui_data;
  g->in_curve = event->type == GDK_ENTER_NOTIFY;
  if(!g->dragging)
    g->x_move = -1;

  gtk_widget_queue_draw(widget);
  return FALSE;
}

// fills in new parameters based on mouse position (in 0,1)
static void get_params(dt_iop_atrous_params_t *p,
                       const int ch,
                       const double mouse_x,
                       const double mouse_y,
                       const float rad)
{
  for(int k = 0; k < BANDS; k++)
  {
    const float f = expf(-(mouse_x - p->x[ch][k]) * (mouse_x - p->x[ch][k]) / (rad * rad));
    p->y[ch][k] = MAX(0.0f, MIN(1.0f, (1 - f) * p->y[ch][k] + f * mouse_y));
  }
}

static gboolean area_draw(GtkWidget *widget,
                          cairo_t *crf,
                          dt_iop_module_t *self)
{
  dt_iop_atrous_gui_data_t *g = self->gui_data;
  dt_iop_atrous_params_t p = *(dt_iop_atrous_params_t *)self->params;

  const float mix = g->in_curve ? 1.0f : p.mix;

  for(int k = 0; k < BANDS; k++)
  {
    const int ch2 = (int)g->channel2;
    float x, y;
    _apply_mix(self, ch2, k, mix, p.x[ch2][k], p.y[ch2][k], &x, &y);
    dt_draw_curve_set_point(g->minmax_curve, k, x, y);
  }

  const int inset = INSET;
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  int width = allocation.width;
  int height = allocation.height - DT_RESIZE_HANDLE_SIZE;
  cairo_surface_t *cst = dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
  cairo_t *cr = cairo_create(cst);
  // clear bg, match color of the notebook tabs:
  GdkRGBA bright_bg_color, graph_bg;
  GtkStyleContext *context = gtk_widget_get_style_context(self->expander);
  gboolean color_found = gtk_style_context_lookup_color
    (context, "graph_overlay", &bright_bg_color);
  if(!color_found)
  {
    bright_bg_color.red = 1.0;
    bright_bg_color.green = 0.0;
    bright_bg_color.blue = 0.0;
    bright_bg_color.alpha = 1.0;
  }
  color_found = gtk_style_context_lookup_color
    (context, "graph_bg", &graph_bg);
  if(!color_found)
  {
    graph_bg.red = 1.0;
    graph_bg.green = 0.0;
    graph_bg.blue = 0.0;
    graph_bg.alpha = 1.0;
  }

  gdk_cairo_set_source_rgba(cr, &bright_bg_color);
  cairo_paint(cr);

  cairo_translate(cr, inset, inset);
  width -= 2 * inset;
  height -= 2 * inset;

  cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.0));
  gdk_cairo_set_source_rgba(cr, &graph_bg);
  cairo_rectangle(cr, 0, 0, width, height);
  cairo_stroke(cr);

  gdk_cairo_set_source_rgba(cr, &bright_bg_color);
  cairo_rectangle(cr, 0, 0, width, height);
  cairo_fill(cr);

  if(g->mouse_y > 0 || g->dragging)
  {
    const int ch2 = (int)g->channel2;

    // draw min/max curves:
    get_params(&p, ch2, g->mouse_x, 1., g->mouse_radius);
    for(int k = 0; k < BANDS; k++)
      dt_draw_curve_set_point(g->minmax_curve, k, p.x[ch2][k], p.y[ch2][k]);
    dt_draw_curve_calc_values(g->minmax_curve, 0.0, 1.0, RES,
                              g->draw_min_xs, g->draw_min_ys);

    p = *(dt_iop_atrous_params_t *)self->params;
    get_params(&p, ch2, g->mouse_x, .0, g->mouse_radius);
    for(int k = 0; k < BANDS; k++)
      dt_draw_curve_set_point(g->minmax_curve, k, p.x[ch2][k], p.y[ch2][k]);
    dt_draw_curve_calc_values(g->minmax_curve, 0.0, 1.0, RES,
                              g->draw_max_xs, g->draw_max_ys);
  }

  // draw grid
  cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(.4));
  gdk_cairo_set_source_rgba(cr, &graph_bg);
  dt_draw_grid(cr, 8, 0, 0, width, height);

  cairo_save(cr);

  // draw selected cursor
  cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.));
  cairo_translate(cr, 0, height);

// draw frequency histogram in bg.
#if 1
  if(g->num_samples > 0)
  {
    cairo_save(cr);
    for(int k = 1; k < g->num_samples; k += 2)
    {
      cairo_set_source_rgba(cr, graph_bg.red, graph_bg.green, graph_bg.blue, .3);
      cairo_move_to(cr, width * g->sample[k - 1], 0.0f);
      cairo_line_to(cr, width * g->sample[k - 1], -height);
      cairo_line_to(cr, width * g->sample[k], -height);
      cairo_line_to(cr, width * g->sample[k], 0.0f);
      cairo_fill(cr);
    }
    if(g->num_samples & 1)
    {
      cairo_move_to(cr, width * g->sample[g->num_samples - 1], 0.0f);
      cairo_line_to(cr, width * g->sample[g->num_samples - 1], -height);
      cairo_line_to(cr, 0.0f, -height);
      cairo_line_to(cr, 0.0f, 0.0f);
      cairo_fill(cr);
    }
    cairo_restore(cr);
  }
  if(g->band_max > 0)
  {
    cairo_save(cr);
    cairo_scale(cr, width / (BANDS - 1.0),
                -(height - DT_PIXEL_APPLY_DPI(5)) / g->band_max);
    cairo_set_source_rgba(cr, graph_bg.red, graph_bg.green, graph_bg.blue, .3);
    cairo_move_to(cr, 0, 0);
    for(int k = 0; k < BANDS; k++) cairo_line_to(cr, k, g->band_hist[k]);
    cairo_line_to(cr, BANDS - 1.0, 0.);
    cairo_close_path(cr);
    cairo_fill(cr);
    cairo_restore(cr);
  }
#endif

  // cairo_set_operator(cr, CAIRO_OPERATOR_ADD);
  cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
  cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.));
  for(int i = 0; i <= atrous_s; i++)
  {
    // draw curves, selected last.
    int ch = ((int)g->channel + i + 1) % (atrous_s + 1);
    int ch2 = -1;
    const float bgmul = i < atrous_s ? 0.5f : 1.0f;
    switch(ch)
    {
      case atrous_L:
        cairo_set_source_rgba(cr, .6, .6, .6, .3 * bgmul);
        ch2 = atrous_Lt;
        break;
      case atrous_c:
        cairo_set_source_rgba(cr, .4, .2, .0, .4 * bgmul);
        ch2 = atrous_ct;
        break;
      default: // case atrous_s:
        cairo_set_source_rgba(cr, .1, .2, .3, .4 * bgmul);
        break;
    }
    p = *(dt_iop_atrous_params_t *)self->params;

    // reverse order if bottom is active (to end up with correct
    // values in minmax_curve):
    if(g->channel2 == ch2)
    {
      ch2 = ch;
      ch = g->channel2;
    }

    if(ch2 >= 0)
    {
      for(int k = 0; k < BANDS; k++)
      {
        float x, y;
        _apply_mix(self, ch2, k, mix, p.x[ch2][k], p.y[ch2][k], &x, &y);
        dt_draw_curve_set_point(g->minmax_curve, k, x, y);
      }
      dt_draw_curve_calc_values(g->minmax_curve, 0.0, 1.0, RES, g->draw_xs, g->draw_ys);
      cairo_move_to(cr, width, -height * p.y[ch2][BANDS - 1]);
      for(int k = RES - 2; k >= 0; k--)
        cairo_line_to(cr, k * width / (float)(RES - 1), -height * g->draw_ys[k]);
    }
    else
      cairo_move_to(cr, 0, 0);
    for(int k = 0; k < BANDS; k++)
    {
      float x, y;
      _apply_mix(self, ch, k, mix, p.x[ch][k], p.y[ch][k], &x, &y);
      dt_draw_curve_set_point(g->minmax_curve, k, x, y);
    }
    dt_draw_curve_calc_values(g->minmax_curve, 0.0, 1.0, RES, g->draw_xs, g->draw_ys);
    for(int k = 0; k < RES; k++)
      cairo_line_to(cr, k * width / (float)(RES - 1), -height * g->draw_ys[k]);
    if(ch2 < 0)
      cairo_line_to(cr, width, 0);
    cairo_close_path(cr);
    cairo_stroke_preserve(cr);
    cairo_fill(cr);
  }

  if(g->mouse_y > 0 || g->dragging)
  {
    const int ch = (int)g->channel;
    const int ch2 = (int)g->channel2;

    // draw dots on knots
    cairo_save(cr);
    if(ch != ch2)
      cairo_set_source_rgb(cr, 0.1, 0.1, 0.1);
    else
      cairo_set_source_rgb(cr, 0.7, 0.7, 0.7);
    cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.));
    for(int k = 0; k < BANDS; k++)
    {
      float x, y;
      _apply_mix(self, ch, k, mix, p.x[ch2][k], p.y[ch2][k], &x, &y);
      cairo_arc(cr, width * x, -height * y, DT_PIXEL_APPLY_DPI(3.0), 0.0, 2.0 * M_PI);
      if(g->x_move == k)
        cairo_fill(cr);
      else
        cairo_stroke(cr);
    }
    cairo_restore(cr);
  }

  if(g->mouse_y > 0 || g->dragging)
  {
    // draw min/max, if selected
    // cairo_set_source_rgba(cr, .6, .6, .6, .5);
    cairo_move_to(cr, 0, -height * g->draw_min_ys[0]);
    for(int k = 1; k < RES; k++)
      cairo_line_to(cr, k * width / (float)(RES - 1), -height * g->draw_min_ys[k]);
    for(int k = RES - 1; k >= 0; k--)
      cairo_line_to(cr, k * width / (float)(RES - 1), -height * g->draw_max_ys[k]);
    cairo_close_path(cr);
    cairo_fill(cr);
    // draw mouse focus circle
    cairo_set_source_rgba(cr, .9, .9, .9, .5);
    const float pos = RES * g->mouse_x;
    int k = (int)pos;
    const float f = k - pos;
    if(k >= RES - 1) k = RES - 2;
    const float ht = -height * (f * g->draw_ys[k] + (1 - f) * g->draw_ys[k + 1]);
    cairo_arc(cr, g->mouse_x * width, ht, g->mouse_radius * width, 0, 2. * M_PI);
    cairo_stroke(cr);
  }

  cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);

  // draw x positions
  cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.));
  cairo_set_source_rgb(cr, 0.6, 0.6, 0.6);
  const float arrw = DT_PIXEL_APPLY_DPI(7.0f);
  for(int k = 1; k < BANDS - 1; k++)
  {
    cairo_move_to(cr, width * p.x[(int)g->channel][k], inset - DT_PIXEL_APPLY_DPI(1));
    cairo_rel_line_to(cr, -arrw * .5f, 0);
    cairo_rel_line_to(cr, arrw * .5f, -arrw);
    cairo_rel_line_to(cr, arrw * .5f, arrw);
    cairo_close_path(cr);
    if(g->x_move == k)
      cairo_fill(cr);
    else
      cairo_stroke(cr);
  }

  cairo_restore(cr);

  if(g->mouse_y > 0 || g->dragging)
  {
    // draw labels:
    PangoLayout *layout;
    PangoRectangle ink;
    PangoFontDescription *desc =
      pango_font_description_copy_static(darktable.bauhaus->pango_font_desc);
    pango_font_description_set_weight(desc, PANGO_WEIGHT_BOLD);
    pango_font_description_set_absolute_size(desc, (.06 * height) * PANGO_SCALE);
    layout = pango_cairo_create_layout(cr);
    pango_layout_set_font_description(layout, desc);
    gdk_cairo_set_source_rgba(cr, &graph_bg);
    cairo_set_font_size(cr, .06 * height);
    pango_layout_set_text(layout, C_("graph", "coarse"), -1);
    pango_layout_get_pixel_extents(layout, &ink, NULL);
    cairo_move_to(cr, .02 * width - ink.y, .14 * height + ink.width);
    cairo_save(cr);
    cairo_rotate(cr, -M_PI * .5f);
    pango_cairo_show_layout(cr, layout);
    cairo_restore(cr);
    pango_layout_set_text(layout, _("fine"), -1);
    pango_layout_get_pixel_extents(layout, &ink, NULL);
    cairo_move_to(cr, .98 * width - ink.height, .14 * height + ink.width);
    cairo_save(cr);
    cairo_rotate(cr, -M_PI * .5f);
    pango_cairo_show_layout(cr, layout);
    cairo_restore(cr);

    switch(g->channel2)
    {
      case atrous_L:
      case atrous_c:
        dt_atrous_show_upper_label(cr, _("contrasty"), layout, ink);
        dt_atrous_show_lower_label(cr, _("smooth"), layout, ink);
        break;
      case atrous_Lt:
      case atrous_ct:
        dt_atrous_show_upper_label(cr, _("smooth"), layout, ink);
        dt_atrous_show_lower_label(cr, _("noisy"), layout, ink);
        break;
      default: // case atrous_s:
        dt_atrous_show_upper_label(cr, _("bold"), layout, ink);
        dt_atrous_show_lower_label(cr, _("dull"), layout, ink);
        break;
    }
    pango_font_description_free(desc);
    g_object_unref(layout);
  }


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

static gboolean area_motion_notify(GtkWidget *widget,
                                   GdkEventMotion *event,
                                   dt_iop_module_t *self)
{
  dt_iop_atrous_gui_data_t *g = self->gui_data;
  dt_iop_atrous_params_t *p = self->params;
  const int inset = INSET;
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  const int height = allocation.height - 2 * inset - DT_RESIZE_HANDLE_SIZE;
  const int width = allocation.width - 2 * inset;
  if(!g->dragging) g->mouse_x = CLAMP(event->x - inset, 0, width) / (float)width;
  g->mouse_y = 1.0 - CLAMP(event->y - inset, 0, height) / (float)height;

  darktable.control->element = 0;

  int ch2 = g->channel;
  if(g->channel == atrous_L) ch2 = atrous_Lt;
  if(g->channel == atrous_c) ch2 = atrous_ct;

  if(g->dragging)
  {
    // drag y-positions
    *p = g->drag_params;
    if(g->x_move >= 0)
    {
      const float mx = CLAMP(event->x - inset, 0, width) / (float)width;
      if(g->x_move > 0 && g->x_move < BANDS - 1)
      {
        const float minx = p->x[g->channel][g->x_move - 1] + 0.001f;
        const float maxx = p->x[g->channel][g->x_move + 1] - 0.001f;
        p->x[ch2][g->x_move] = p->x[g->channel][g->x_move] = fminf(maxx, fmaxf(minx, mx));
      }
    }
    else
    {
      get_params(p, g->channel2, g->mouse_x, g->mouse_y + g->mouse_pick, g->mouse_radius);
    }
    gtk_widget_queue_draw(widget);
    dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget + g->channel);
  }
  else if(event->y > height)
  {
    // move x-positions
    g->x_move = 0;
    float dist = fabs(p->x[g->channel][0] - g->mouse_x);
    for(int k = 1; k < BANDS; k++)
    {
      const float d2 = fabs(p->x[g->channel][k] - g->mouse_x);
      if(d2 < dist)
      {
        g->x_move = k;
        dist = d2;
      }
    }
    darktable.control->element = g->x_move + 1;

    gtk_widget_queue_draw(widget);
  }
  else
  {
    // choose between bottom and top curve:
    const int ch = g->channel;
    float dist = 1000000.0f;
    for(int k = 0; k < BANDS; k++)
    {
      float d2 = fabs(p->x[g->channel][k] - g->mouse_x);
      if(d2 < dist)
      {
        if(fabs(g->mouse_y - p->y[ch][k]) < fabs(g->mouse_y - p->y[ch2][k]))
          g->channel2 = ch;
        else
          g->channel2 = ch2;
        dist = d2;
      }
    }
    // don't move x-positions:
    g->x_move = -1;
    gtk_widget_queue_draw(widget);
  }
  return TRUE;
}

static gboolean area_button_press(GtkWidget *widget,
                                  GdkEventButton *event,
                                  dt_iop_module_t *self)
{
  if(event->button == GDK_BUTTON_PRIMARY && event->type == GDK_2BUTTON_PRESS)
  {
    // reset current curve
    dt_iop_atrous_params_t *p = self->params;
    const dt_iop_atrous_params_t *const d = self->default_params;
    dt_iop_atrous_gui_data_t *g = self->gui_data;
    reset_mix(self);
    for(int k = 0; k < BANDS; k++)
    {
      p->x[g->channel2][k] = d->x[g->channel2][k];
      p->y[g->channel2][k] = d->y[g->channel2][k];
    }
    dt_dev_add_history_item_target(darktable.develop, self, TRUE, widget + g->channel2);
  }
  else if(event->button == GDK_BUTTON_PRIMARY)
  {
    // set active point
    dt_iop_atrous_gui_data_t *g = self->gui_data;
    reset_mix(self);
    const int inset = INSET;
    GtkAllocation allocation;
    gtk_widget_get_allocation(widget, &allocation);
    const int height = allocation.height - 2 * inset - DT_RESIZE_HANDLE_SIZE;
    const int width = allocation.width - 2 * inset;
    g->mouse_pick
        = dt_draw_curve_calc_value(g->minmax_curve,
                                   CLAMP(event->x - inset, 0, width) / (float)width);
    g->mouse_pick -= 1.0 - CLAMP(event->y - inset, 0, height) / (float)height;
    g->dragging = 1;
    return TRUE;
  }
  return FALSE;
}

static gboolean area_button_release(GtkWidget *widget,
                                    GdkEventButton *event,
                                    dt_iop_module_t *self)
{
  if(event->button == GDK_BUTTON_PRIMARY)
  {
    dt_iop_atrous_gui_data_t *g = self->gui_data;
    g->dragging = 0;
    reset_mix(self);
    return TRUE;
  }
  return FALSE;
}

static gboolean area_scrolled(GtkWidget *widget,
                              GdkEventScroll *event,
                              dt_iop_module_t *self)
{
  dt_iop_atrous_gui_data_t *g = self->gui_data;

  if(dt_gui_ignore_scroll(event)) return FALSE;

  if(dt_modifier_is(event->state, GDK_MOD1_MASK))
    return gtk_widget_event(GTK_WIDGET(g->channel_tabs), (GdkEvent*)event);

  int delta_y;
  if(dt_gui_get_scroll_unit_delta(event, &delta_y))
  {
    g->mouse_radius = CLAMP(g->mouse_radius * (1.0 + 0.1 * delta_y), 0.25 / BANDS, 1.0);
    gtk_widget_queue_draw(widget);
  }
  return TRUE;
}

static void tab_switch(GtkNotebook *notebook,
                       GtkWidget *page,
                       const guint page_num,
                       dt_iop_module_t *self)
{
  dt_iop_atrous_gui_data_t *g = self->gui_data;
  DT_GUARD_GUI_UPDATE();
  g->channel = g->channel2 = (atrous_channel_t)page_num;
  gtk_widget_queue_draw(GTK_WIDGET(g->area));
}

static void mix_callback(GtkWidget *slider,
                         dt_iop_module_t *self)
{
  DT_GUARD_GUI_UPDATE();
  dt_iop_atrous_params_t *p = self->params;
  dt_iop_atrous_gui_data_t *g = self->gui_data;
  p->mix = dt_bauhaus_slider_get(slider);
  gtk_widget_queue_draw(GTK_WIDGET(g->area));
  dt_dev_add_history_item_target(darktable.develop, self, TRUE, slider);
}

enum
{
  DT_ACTION_EFFECT_ATROUS_RESET = DT_ACTION_EFFECT_DEFAULT_KEY,
  DT_ACTION_EFFECT_BOOST = DT_ACTION_EFFECT_DEFAULT_UP,
  DT_ACTION_EFFECT_REDUCE = DT_ACTION_EFFECT_DEFAULT_DOWN,
  DT_ACTION_EFFECT_RAISE = 3,
  DT_ACTION_EFFECT_LOWER = 4,
  DT_ACTION_EFFECT_RIGHT = 5,
  DT_ACTION_EFFECT_LEFT = 6,
};

const gchar *dt_action_effect_equalizer[]
  = { N_("reset"),
      N_("boost"),
      N_("reduce"),
      N_("raise"),
      N_("lower"),
      N_("right"),
      N_("left"),
      NULL };

const dt_action_element_def_t _action_elements_equalizer[]
  = { { N_("radius"  ), dt_action_effect_value     },
      { N_("coarsest"), dt_action_effect_equalizer },
      { N_("coarser" ), dt_action_effect_equalizer },
      { NC_("graph", "coarse"), dt_action_effect_equalizer },
      { N_("fine"    ), dt_action_effect_equalizer },
      { N_("finer"   ), dt_action_effect_equalizer },
      { N_("finest"  ), dt_action_effect_equalizer },
      { } };

static float _action_process_equalizer(gpointer target,
                                       const dt_action_element_t element,
                                       const dt_action_effect_t effect,
                                       float move_size)
{
  if(element > BANDS) return DT_ACTION_NOT_VALID;

  dt_iop_module_t *self = g_object_get_data(G_OBJECT(target), "iop-instance");
  dt_iop_atrous_gui_data_t *g = self->gui_data;
  dt_iop_atrous_params_t *p = self->params;
  const dt_iop_atrous_params_t *const d = self->default_params;

  const int node = element - 1;
  const int ch1 = g->channel;
  const int ch2 = ch1 == atrous_L ? atrous_Lt
                : ch1 == atrous_c ? atrous_ct
                : ch1;

  if(DT_PERFORM_ACTION(move_size))
  {
    gchar *toast = NULL;

    if(element)
    {
      switch(effect)
      {
      case DT_ACTION_EFFECT_ATROUS_RESET:
        p->y[ch1][node] = d->y[ch1][node];
        p->y[ch2][node] = d->y[ch2][node];

        toast = g_strdup_printf("%s, %s",
                                _action_elements_equalizer[element].name, "reset");
        break;
      case DT_ACTION_EFFECT_REDUCE:
        move_size *= -1;
      case DT_ACTION_EFFECT_BOOST:
        get_params(p, ch1, p->x[ch1][node],
                   p->y[ch1][node] + move_size / 100, g->mouse_radius);

        toast = g_strdup_printf
          ("%s, %s %+.2f",
           _action_elements_equalizer[element].name,
           ch1 == atrous_s ? _("sharpness") : _("boost"), p->y[ch1][node] * 2. - 1.);
        break;
      case DT_ACTION_EFFECT_LOWER:
        move_size *= -1;
      case DT_ACTION_EFFECT_RAISE:
        get_params(p, ch2, p->x[ch2][node],
                   p->y[ch2][node] + move_size / 100, g->mouse_radius);

        toast = g_strdup_printf("%s, %s %.2f",
                                _action_elements_equalizer[element].name,
                                _("threshold"),
                                p->y[ch2][node]);
        break;
      case DT_ACTION_EFFECT_LEFT:
        move_size *= -1;
      case DT_ACTION_EFFECT_RIGHT:
        if(element > 1 && element < BANDS)
        {
          const float minx = p->x[ch1][node - 1] + 0.001f;
          const float maxx = p->x[ch1][node + 1] - 0.001f;
          p->x[ch1][node] = p->x[ch2][node]
            = fminf(maxx, fmaxf(minx, p->x[ch1][node] + move_size * (maxx - minx) / 100));
        }

        toast = g_strdup_printf("%s, %s %+.2f", _action_elements_equalizer[element].name,
                                _("x"), p->x[ch1][node]);
        break;
      default:
        dt_print(DT_DEBUG_ALWAYS,
                 "[_action_process_equalizer] unknown shortcut effect (%d)"
                 " for contrast equalizer node",
                 effect);
        break;
      }

      dt_dev_add_history_item_target(darktable.develop, self, TRUE, target + ch1);
    }
    else // radius
    {
      float bottop = -1e6;
      switch(effect)
      {
      case DT_ACTION_EFFECT_RESET:
        g->mouse_radius = 1.0 / BANDS;
        break;
      case DT_ACTION_EFFECT_BOTTOM:
        bottop *= -1;
      case DT_ACTION_EFFECT_TOP:
        move_size = bottop;
      case DT_ACTION_EFFECT_DOWN:
        move_size *= -1;
      case DT_ACTION_EFFECT_UP:
        g->mouse_radius = CLAMP(g->mouse_radius * (1.0 + 0.1 * move_size),
                                0.25 / BANDS, 1.0);
        break;
      default:
        dt_print(DT_DEBUG_ALWAYS,
                 "[_action_process_equalizer] unknown shortcut effect (%d)"
                 " for contrast equalizer radius", effect);
        break;
      }

      toast = g_strdup_printf("%s %+.2f",
                              _action_elements_equalizer[element].name, g->mouse_radius);
    }
    dt_action_widget_toast(DT_ACTION(self), target, toast);
    g_free(toast);

    gtk_widget_queue_draw(GTK_WIDGET(g->area));
  }

  return element
    ? effect >= DT_ACTION_EFFECT_RIGHT ? p->x[ch1][node]
    : effect >= DT_ACTION_EFFECT_RAISE ? p->y[ch2][node] + DT_VALUE_PATTERN_PERCENTAGE
    : effect >= DT_ACTION_EFFECT_BOOST ? p->y[ch1][node] + DT_VALUE_PATTERN_PLUS_MINUS
    : p->y[ch1][node] != d->y[ch1][node] || p->y[ch2][node] != d->y[ch2][node]
    : g->mouse_radius + DT_VALUE_PATTERN_PERCENTAGE;
}

static const dt_shortcut_fallback_t _action_fallbacks_equalizer[]
  = { { .mods = GDK_SHIFT_MASK,   .effect = DT_ACTION_EFFECT_RAISE },
      { .mods = GDK_CONTROL_MASK, .effect = DT_ACTION_EFFECT_RIGHT },
      { } };

const dt_action_def_t _action_def_equalizer
  = { N_("contrast equalizer"),
      _action_process_equalizer,
      _action_elements_equalizer,
      _action_fallbacks_equalizer };

static void _ui_pipe_done(gpointer instance, dt_iop_module_t *self)
{
  dt_iop_atrous_gui_data_t *g = self->gui_data;
  if(g && !DT_IN_GUI_UPDATE() && self->enabled && self->expanded)
    gtk_widget_queue_draw(GTK_WIDGET(g->area));
}

void gui_focus(dt_iop_module_t *self, const gboolean in)
{
  dt_iop_atrous_gui_data_t *g = self->gui_data;
  if(in && g)
    gtk_widget_queue_draw(GTK_WIDGET(g->area));
}

void gui_init(dt_iop_module_t *self)
{
  dt_iop_atrous_gui_data_t *g = IOP_GUI_ALLOC(atrous);
  const dt_iop_atrous_params_t *const p = self->default_params;

  g->num_samples = 0;
  g->band_max = 0;
  g->channel = g->channel2 = dt_conf_get_int("plugins/darkroom/atrous/gui_channel");
  int ch = (int)g->channel;
  g->minmax_curve = dt_draw_curve_new(0.0, 1.0, CATMULL_ROM);
  for(int k = 0; k < BANDS; k++)
    (void)dt_draw_curve_add_point(g->minmax_curve, p->x[ch][k], p->y[ch][k]);
  g->mouse_x = g->mouse_y = g->mouse_pick = -1.0;
  g->dragging = 0;
  g->x_move = -1;
  g->mouse_radius = 1.0 / BANDS;
  g->in_curve = FALSE;

  static struct dt_action_def_t notebook_def = { };
  g->channel_tabs = dt_ui_notebook_new(&notebook_def);
  dt_action_define_iop(self, NULL, N_("channel"),
                       GTK_WIDGET(g->channel_tabs), &notebook_def);
  dt_ui_notebook_page(g->channel_tabs, N_("luma"),
                      _("change lightness at each feature size"));
  dt_ui_notebook_page(g->channel_tabs, N_("chroma"),
                      _("change color saturation at each feature size"));
  dt_ui_notebook_page
    (g->channel_tabs, N_("edges"),
     _("change edge halos at each feature size\nonly changes results of luma"
       " and chroma tabs"));
  gtk_widget_show(gtk_notebook_get_nth_page(g->channel_tabs, g->channel));
  gtk_notebook_set_current_page(g->channel_tabs, g->channel);
  g_signal_connect(G_OBJECT(g->channel_tabs), "switch_page", G_CALLBACK(tab_switch), self);

  // graph
  g->area = GTK_DRAWING_AREA(dt_ui_resize_wrap
                             (NULL,
                              0,
                              "plugins/darkroom/atrous/graphheight"));

  g_object_set_data(G_OBJECT(g->area), "iop-instance", self);
  dt_action_define_iop(self, NULL, N_("graph"),
                       GTK_WIDGET(g->area), &_action_def_equalizer);
  g_signal_connect(G_OBJECT(g->area), "draw", G_CALLBACK(area_draw), self);
  g_signal_connect(G_OBJECT(g->area), "button-press-event",
                   G_CALLBACK(area_button_press), self);
  g_signal_connect(G_OBJECT(g->area), "button-release-event",
                   G_CALLBACK(area_button_release), self);
  g_signal_connect(G_OBJECT(g->area), "motion-notify-event",
                   G_CALLBACK(area_motion_notify), self);
  g_signal_connect(G_OBJECT(g->area), "leave-notify-event",
                   G_CALLBACK(area_enter_leave_notify), self);
  g_signal_connect(G_OBJECT(g->area), "enter-notify-event",
                   G_CALLBACK(area_enter_leave_notify), self);
  g_signal_connect(G_OBJECT(g->area), "scroll-event",
                   G_CALLBACK(area_scrolled), self);

  self->widget = dt_gui_vbox(g->channel_tabs, g->area);

  // mix slider
  g->mix = dt_bauhaus_slider_from_params(self, N_("mix"));
  gtk_widget_set_tooltip_text(g->mix, _("make effect stronger or weaker"));
  g_signal_connect(G_OBJECT(g->mix), "value-changed", G_CALLBACK(mix_callback), self);
  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_UI_PIPE_FINISHED, _ui_pipe_done);
}

void gui_cleanup(dt_iop_module_t *self)
{
  dt_iop_atrous_gui_data_t *g = self->gui_data;
  dt_conf_set_int("plugins/darkroom/atrous/gui_channel", g->channel);
  dt_draw_curve_destroy(g->minmax_curve);
}

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