/*
http://www.youtube.com/watch?v=JVoUgR6bhBc
 */

/*
    This file is part of darktable,
    Copyright (C) 2013-2023 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/>.
*/
// our includes go first:
#include "bauhaus/bauhaus.h"
#include "common/exif.h"
#include "common/colorspaces_inline_conversions.h"
#include "common/math.h"
#include "common/opencl.h"
#include "develop/blend.h"
#include "develop/imageop.h"
#include "develop/imageop_math.h"
#include "develop/imageop_gui.h"
#include "develop/openmp_maths.h"
#include "dtgtk/gradientslider.h"
#include "gui/accelerators.h"
#include "gui/gtk.h"
#include "gui/presets.h"
#include "gui/color_picker_proxy.h"
#include "iop/iop_api.h"

#include <stdlib.h>

// uncomment the following line to test auto-vectorized code on a system which supports SSE
//#undef __SSE2__

// these are not in a state to be useful. but they look nice. too bad i couldn't map the enhanced mode with
// negative values to the wheels :(
//#define SHOW_COLOR_WHEELS

DT_MODULE_INTROSPECTION(3, dt_iop_colorbalance_params_t)

/*

  Meaning of the values:
   0 --> 100%
  -1 -->   0%
   1 --> 200%
 */

typedef enum dt_iop_colorbalance_mode_t
{
  LIFT_GAMMA_GAIN = 0,    // $DESCRIPTION: "lift, gamma, gain (ProPhoto RGB)"
  SLOPE_OFFSET_POWER = 1, // $DESCRIPTION: "slope, offset, power (ProPhoto RGB)"
  LEGACY = 2              // $DESCRIPTION: "lift, gamma, gain (sRGB)"
} dt_iop_colorbalance_mode_t;

typedef enum _colorbalance_channel_t
{
  CHANNEL_FACTOR = 0,
  CHANNEL_RED,
  CHANNEL_GREEN,
  CHANNEL_BLUE,
  CHANNEL_SIZE
} _colorbalance_channel_t;

typedef enum _colorbalance_levels_t
{
  LIFT = 0,
  GAMMA,
  GAIN,
  LEVELS
} _colorbalance_levels_t;

typedef enum _controls_t
{
  HSL,
  RGBL,
  BOTH
} _controls_t;

typedef enum _colorbalance_patch_t
{
  INVALID,
  USER_SELECTED,
  AUTO_SELECTED
} _colorbalance_patch_t;

typedef struct dt_iop_colorbalance_params_t
{
  dt_iop_colorbalance_mode_t mode; // $DEFAULT: SLOPE_OFFSET_POWER
  float lift[CHANNEL_SIZE], gamma[CHANNEL_SIZE], gain[CHANNEL_SIZE]; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 1.0
  float saturation;     // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 1.0 $DESCRIPTION: "input saturation"
  float contrast;       // $MIN: 0.01 $MAX: 1.99 $DEFAULT: 1.0
  float grey;           // $MIN: 0.1 $MAX: 100.0 $DEFAULT: 18.0 $DESCRIPTION: "contrast fulcrum"
  float saturation_out; // $MIN: 0.0 $MAX: 2.0 $DEFAULT: 1.0 $DESCRIPTION: "output saturation"
} dt_iop_colorbalance_params_t;

typedef struct dt_iop_colorbalance_gui_data_t
{
  GtkWidget *master_box;
  GtkWidget *main_label;
  GtkWidget *main_box;
  GtkWidget *blocks[3];
  GtkWidget *optimizer_box;
  GtkWidget *mode;
  GtkWidget *controls;
  GtkWidget *hue_lift, *hue_gamma, *hue_gain;
  GtkWidget *sat_lift, *sat_gamma, *sat_gain;
  GtkWidget *lift_r, *lift_g, *lift_b, *lift_factor;
  GtkWidget *gamma_r, *gamma_g, *gamma_b, *gamma_factor;
  GtkWidget *gain_r, *gain_g, *gain_b, *gain_factor;
  GtkWidget *saturation, *contrast, *grey, *saturation_out;
  GtkWidget *auto_luma;
  GtkWidget *auto_color;
  float color_patches_lift[3];
  float color_patches_gamma[3];
  float color_patches_gain[3];
  _colorbalance_patch_t color_patches_flags[LEVELS];
  float luma_patches[LEVELS];
  _colorbalance_patch_t luma_patches_flags[LEVELS];
} dt_iop_colorbalance_gui_data_t;

typedef struct dt_iop_colorbalance_data_t
{
  dt_iop_colorbalance_mode_t mode;
  float lift[CHANNEL_SIZE], gamma[CHANNEL_SIZE], gain[CHANNEL_SIZE];
  float saturation, contrast, grey, saturation_out;
} dt_iop_colorbalance_data_t;

typedef struct dt_iop_colorbalance_global_data_t
{
  int kernel_colorbalance;
  int kernel_colorbalance_cdl;
  int kernel_colorbalance_lgg;
} dt_iop_colorbalance_global_data_t;


const char *name()
{
  return _("color balance");
}

const char *aliases()
{
  return _("lift gamma gain|cdl|color grading|contrast|saturation|hue");
}

const char **description(dt_iop_module_t *self)
{
  return dt_iop_set_description(self, _("shift colors selectively by luminance range"),
                                      _("corrective or creative"),
                                      _("linear, Lab, scene-referred"),
                                      _("non-linear, RGB"),
                                      _("non-linear, Lab, scene-referred"));
}

int flags()
{
  return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_SUPPORTS_BLENDING;
}

int default_group()
{
  return IOP_GROUP_COLOR | IOP_GROUP_GRADING;
}

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_colorbalance_params_v3_t
  {
    dt_iop_colorbalance_mode_t mode;
    float lift[CHANNEL_SIZE], gamma[CHANNEL_SIZE], gain[CHANNEL_SIZE];
    float saturation;
    float contrast;
    float grey;
    float saturation_out;
  } dt_iop_colorbalance_params_v3_t;

  if(old_version == 1)
  {
    typedef struct dt_iop_colorbalance_params_v1_t
    {
      float lift[CHANNEL_SIZE], gamma[CHANNEL_SIZE], gain[CHANNEL_SIZE];
    } dt_iop_colorbalance_params_v1_t;

    const dt_iop_colorbalance_params_v1_t *o = old_params;
    dt_iop_colorbalance_params_v3_t *n = malloc(sizeof(dt_iop_colorbalance_params_v3_t));

    for(int i = 0; i < CHANNEL_SIZE; i++)
    {
      n->lift[i] = o->lift[i];
      n->gamma[i] = o->gamma[i];
      n->gain[i] = o->gain[i];
    }
    n->mode = LEGACY;
    n->saturation = 1.0f;
    n->contrast = 1.0f;
    n->grey = 18.0f;
    n->saturation_out = 1.0f;

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

  if(old_version == 2)
  {
    typedef struct dt_iop_colorbalance_params_v2_t
    {
      dt_iop_colorbalance_mode_t mode;
      float lift[CHANNEL_SIZE], gamma[CHANNEL_SIZE], gain[CHANNEL_SIZE];
      float saturation, contrast, grey;
    } dt_iop_colorbalance_params_v2_t;

    const dt_iop_colorbalance_params_v2_t *o = old_params;
    dt_iop_colorbalance_params_v3_t *n = malloc(sizeof(dt_iop_colorbalance_params_v3_t));

    for(int i = 0; i < CHANNEL_SIZE; i++)
    {
      n->lift[i] = o->lift[i];
      n->gamma[i] = o->gamma[i];
      n->gain[i] = o->gain[i];
    }
    n->mode = o->mode;
    n->contrast = o->contrast;
    n->saturation = o->saturation;
    n->contrast = o->contrast;
    n->grey = o->grey;
    n->saturation_out = 1.0f;

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

// taken from denoiseprofile.c
static void add_preset(dt_iop_module_so_t *self, const char *name,
                       const char *pi, const int version, const char *bpi, const int blendop_version)
{
  int len, blen;
  uint8_t *p  = dt_exif_xmp_decode(pi, strlen(pi), &len);
  uint8_t *bp = dt_exif_xmp_decode(bpi, strlen(bpi), &blen);
  if(blendop_version != dt_develop_blend_version())
  {
    // update to current blendop params format
    void *bp_new = malloc(sizeof(dt_develop_blend_params_t));

    if(dt_develop_blend_legacy_params_from_so(self, bp, blendop_version, bp_new,
                                              dt_develop_blend_version(), blen) == FALSE)
    {
      free(bp);
      bp = bp_new;
      blen = sizeof(dt_develop_blend_params_t);
    }
    else
    {
      free(bp);
      free(bp_new);
      bp = NULL;
    }
  }

  if(p && bp)
  {
    gchar *prefixed_name = g_strdup_printf(BUILTIN_PREFIX "%s", name);
    dt_gui_presets_add_with_blendop(prefixed_name, self->op, version, p, len, bp, TRUE);
    g_free(prefixed_name);
  }
  free(bp);
  free(p);
}

void init_presets(dt_iop_module_so_t *self)
{
  // these blobs were exported as dtstyle and copied from there:
  add_preset(self, N_("split-toning teal-orange (2nd instance)"),
             "gz02eJxjZGBg8HhYZX99cYN9kkCDfdCOOnsGhgZ7ruvN9m8CK+yXFNTaz5w50z5PqBku9u9/PVjNv//9jqfP+NgDAHs0HIc=", 3,
             "gz05eJxjZWBgYGUAgRNODFDAzszAxMBQ5cwI4Tow4AUNdkBsD8E3gGwue9x8uB6q8s+c8bEF8Z9Y9Nnt2f3bbluCN03tg/EBIBckVg==", 8);
  add_preset(self, N_("split-toning teal-orange (1st instance)"),
             "gz02eJxjZACBBvugHXX2E3fU219f3GAP4n/TqLFvfd1oL8HZaH/2jI/9prn1cLHUtDSwGgaGCY7//tfbAwBRixpm", 3,
             "gz04eJxjZWBgYGUAgRNODFDApgwiq5wZIVyHD4E7bBnwggZ7CIYBRiBbBA8fXT1l/P5DX21i+pnA/Pfv8uw6OzzIMq9I5rgtSH//4wii1AMASbIlcw==", 8);

  add_preset(self, N_("generic film"),
             "gz02eJxjZACBBntN5gb7op/19u5AGsSX3dFgr+jYaL+vttb+0NcM+1Pnq+3XyFTZr/rYBJZPS0sD0hMcQDQA29kXSQ==", 3,
             "gz11eJxjYGBgkGAAgRNODGiAEV0AJ2iwh+CRxQcA5qIZBA==", 8);

  add_preset(self, N_("similar to Kodak Portra"),
             "gz02eJxjZACBBnsQfh3YYK8VU28P43s8rLKP6W+yP/Q1w36deyMYLymoBcsZGxcDaQGHs2d87AGnphWu", 3,
             "gz11eJxjYGBgkGAAgRNODGiAEV0AJ2iwh+CRxQcA5qIZBA==", 8);

  add_preset(self, N_("similar to Kodak Ektar"),
             "gz02eJxjZACBBvvrixvsrXIb7IN21NnD+CA2iOa6nmxvZFxsX15ebp+e1gaWNwbyGRgEHNLS0uwBE7wWhw==", 3,
             "gz11eJxjYGBgkGAAgRNODGiAEV0AJ2iwh+CRxQcA5qIZBA==", 8);

  add_preset(self, N_("similar to Kodachrome"),
             "gz02eJxjZACBBvvrixvsrXIb7IN21NnD+CA2iG59HWhvZFxsX15ebp+e1gaWT0tLA9ICDrNmRtoDACjOF7c=", 3,
             "gz11eJxjYGBgkGAAgRNODGiAEV0AJ2iwh+CRxQcA5qIZBA==", 8);
}

static const dt_aligned_pixel_t zero = { 0.0f, 0.0f, 0.0f, 0.0f };
static const dt_aligned_pixel_t one = { 1.0f, 1.0f, 1.0f, 1.0f };

DT_OMP_DECLARE_SIMD(simdlen(4))
static inline float CDL(float x, float slope, float offset, float power)
{
  float out;
  out = slope * x + offset;
//  out = (out <= 0.0f) ? 0.0f : powf(out, power);
  out = powf(MAX(out, 0.0f), power);
  return out;
}

static inline void _apply_CDL(dt_aligned_pixel_t x,
                              const dt_aligned_pixel_t slope,
                              const dt_aligned_pixel_t offset,
                              const dt_aligned_pixel_t power)
{
  dt_aligned_pixel_t res;
  for_each_channel(c)
    res[c] = slope[c] * x[c] + offset[c];
  dt_vector_max(res, res, zero);	// clip away negatives
  dt_vector_powf(res, power, x);
}

static inline void _apply_fulcrum_contrast(dt_aligned_pixel_t rgb,
                                           const dt_aligned_pixel_t grey,
                                           const dt_aligned_pixel_t contrast_power)
{
  dt_vector_max(rgb, rgb, zero);	// clip away negatives
  for_each_channel(c)
    rgb[c] /= grey[c];
  dt_vector_powf(rgb, contrast_power, rgb);
  for_each_channel(c)
    rgb[c] *= grey[c];
}

static void _process_legacy(const dt_aligned_pixel_t in,
                            dt_aligned_pixel_t out,
                            const size_t npixels,
                            const dt_aligned_pixel_t lift,
                            const dt_aligned_pixel_t gamma_inv,
                            const dt_aligned_pixel_t gain)
{
  for(size_t k = 0; k < npixels; k++)
  {
    // transform the pixel to sRGB:
    // Lab -> XYZ
    dt_aligned_pixel_t XYZ = { 0.0f };
    dt_Lab_to_XYZ(in + 4*k, XYZ);

    // XYZ -> sRGB
    dt_aligned_pixel_t rgb = { 0.0f };
    dt_XYZ_to_sRGB(XYZ, rgb);

    // do the calculation in RGB space
    for_each_channel(c)
    {
      // lift gamma gain - apply lift and gain
      rgb[c] = ((( rgb[c] - one[c]) * lift[c]) + one[c]) * gain[c];
    }
    dt_vector_max(rgb, rgb, zero);  // clip away negatives
    // lift gamma gain - apply gamma
    dt_vector_powf(rgb, gamma_inv, rgb);

    // transform the result back to Lab
    // sRGB -> XYZ
    dt_sRGB_to_XYZ(rgb, XYZ);

    // XYZ -> Lab
    dt_aligned_pixel_t res;
    dt_XYZ_to_Lab(XYZ, res);
    copy_pixel_nontemporal(out + 4*k, res);
  }
}

#ifndef __SSE2__
static void _apply_lgg(dt_aligned_pixel_t rgb,
                       const dt_aligned_pixel_t lift,
                       const dt_aligned_pixel_t gamma_inv,
                       const dt_aligned_pixel_t gain)
{
  dt_vector_max(rgb, rgb, zero);  	// clip away negatives
  // RGB gamma correction
  static const dt_aligned_pixel_t power = { 1.0f/2.2f, 1.0f/2.2f, 1.0f/2.2f, 1.0f/2.2f };
  dt_vector_powf(rgb, power, rgb);
  for_each_channel(c)
  {
    // lift gamma gain - apply lift and gain
    rgb[c] = ((( rgb[c] - one[c]) * lift[c]) + one[c]) * gain[c];
  }
  dt_vector_max(rgb, rgb, zero);	// clip away negatives
  dt_vector_powf(rgb, gamma_inv, rgb);
}
#endif

static void _process_lgg(const dt_aligned_pixel_t in,
                         dt_aligned_pixel_t out,
                         const size_t npixels,
                         const dt_aligned_pixel_t lift_,
                         const dt_aligned_pixel_t gamma_inv_,
                         const dt_aligned_pixel_t gain_,
                         const float grey,
                         const float saturation,
                         const float saturation_out,
                         const dt_aligned_pixel_t contrast_power)
{
  const int run_saturation = fabsf(saturation - 1.0f) > 1e-6;
  const int run_saturation_out = fabsf(saturation_out - 1.0f) > 1e-6;
  const int run_contrast = fabsf(contrast_power[0] - 1.0f) > 1e-6;
#ifdef __SSE2__
  const __m128 mm_zero = _mm_setzero_ps();
  const __m128 mm_one = _mm_set1_ps(1.0);
  const __m128 gamma_inv_RGB = _mm_set1_ps(1.0f/2.2f);
  const __m128 lift = _mm_load_ps(lift_);
  const __m128 gamma_inv = _mm_load_ps(gamma_inv_);
  const __m128 gain = _mm_load_ps(gain_);
  const __m128 contrast = _mm_load_ps(contrast_power);
  for(size_t k = 0; k < npixels; k++)
  {
    // transform the pixel to sRGB:
    // Lab -> XYZ
    __m128 XYZ = dt_Lab_to_XYZ_sse2(_mm_load_ps(in + 4*k));
    // XYZ -> sRGB
    __m128 rgb = dt_XYZ_to_prophotoRGB_sse2(XYZ);

    __m128 luma;

    // adjust main saturation input
    if(run_saturation)
    {
      luma = _mm_set1_ps(XYZ[1]); // the Y channel is the relative luminance
      rgb = luma + saturation * (rgb - luma);
    }

    // RGB gamma adjustment
    rgb = _mm_pow_ps(_mm_max_ps(rgb, mm_zero), gamma_inv_RGB);

    // regular lift gamma gain
    rgb = ((rgb - mm_one) * lift + mm_one) * gain;
    rgb = _mm_max_ps(rgb, mm_zero);
    rgb = _mm_pow_ps(rgb, gamma_inv);

    // adjust main saturation output
    if(run_saturation_out)
    {
      XYZ = dt_prophotoRGB_to_XYZ_sse2(rgb);
      luma = _mm_set1_ps(XYZ[1]); // the Y channel is the relative luminance
      rgb = luma + saturation_out * (rgb - luma);
    }

    // fulcrum contrast
    if(run_contrast)
    {
      rgb = _mm_max_ps(rgb, mm_zero);
      rgb = _mm_pow_ps(rgb / grey, contrast) * grey;
    }

    // transform the result back to Lab
    // sRGB -> XYZ
    XYZ = dt_prophotoRGB_to_XYZ_sse2(rgb);
    // XYZ -> Lab
    _mm_stream_ps(out + 4*k, dt_XYZ_to_Lab_sse2(XYZ));
  }
#else
  if(!run_saturation && !run_saturation_out && !run_contrast)
  {
    // fully neutral settings, only apply the curves
    for(size_t k = 0; k < npixels; k++)
    {
      // transform the pixel to ProphotoRGB:
      // Lab -> XYZ -> RGB, return Y as luma
      dt_aligned_pixel_t rgb;
      (void)dt_Lab_to_prophotorgb(in + 4*k, rgb);
      _apply_lgg(rgb, lift_, gamma_inv_, gain_);
      // transform the result back to Lab
      // ProphotoRGB -> XYZ -> Lab
      dt_aligned_pixel_t res;
      dt_prophotorgb_to_Lab(rgb, res);
      copy_pixel_nontemporal(out + 4*k, res);
    }
    return;
  }

  const dt_aligned_pixel_t grey4 = { grey, grey, grey, grey };
  const dt_aligned_pixel_t saturation4 = { saturation, saturation, saturation, saturation };
  const dt_aligned_pixel_t saturation_out4
    = { saturation_out, saturation_out, saturation_out, saturation_out };

  for(size_t k = 0; k < npixels; k++)
  {
    // transform the pixel to ProphotoRGB:
    // Lab -> XYZ -> RGB, return Y as luma
    dt_aligned_pixel_t rgb;
    float luma = dt_Lab_to_prophotorgb(in + 4*k, rgb);
    if(run_saturation)
    {
      // main saturation input
      for_each_channel(c)
        rgb[c] = luma + saturation4[c] * (rgb[c] - luma);
    }
    _apply_lgg(rgb, lift_, gamma_inv_, gain_);
    // main saturation output
    if(run_saturation_out)
    {
      luma = dt_prophotorgb_to_XYZ_luma(rgb);
      for_each_channel(c)
        rgb[c] = luma + saturation_out4[c] * (rgb[c] - luma);
    }

    // fulcrum contrat
    if(run_contrast)
    {
      _apply_fulcrum_contrast(rgb, grey4, contrast_power);
    }
    // transform the result back to Lab
    // ProphotoRGB -> XYZ -> Lab
    dt_aligned_pixel_t res;
    dt_prophotorgb_to_Lab(rgb, res);
    copy_pixel_nontemporal(out + 4*k, res);
  }
#endif /* __SSE2__ */
}

static void _process_sop(const dt_aligned_pixel_t in,
                         dt_aligned_pixel_t out,
                         const size_t npixels,
                         const dt_aligned_pixel_t lift,
                         const dt_aligned_pixel_t gamma,
                         const dt_aligned_pixel_t gain,
                         const float grey,
                         const float saturation,
                         const float saturation_out,
                         const float contrast_amt,
                         const dt_aligned_pixel_t contrast_power)
{
  const int run_saturation = fabsf(saturation - 1.0f) > 1e-6;
  const int run_saturation_out = fabsf(saturation_out - 1.0f) > 1e-6;
  const int run_contrast = fabsf(contrast_amt - 1.0f) > 1e-6;
#ifdef __SSE2__
  const __m128 mm_zero = _mm_setzero_ps();
  const __m128 lift4 = _mm_load_ps(lift);
  const __m128 gamma4 = _mm_load_ps(gamma);
  const __m128 gain4 = _mm_load_ps(gain);
  const __m128 contrast = _mm_load_ps(contrast_power);
  for(size_t k = 0; k < npixels; k++)
  {
    // transform the pixel to sRGB:
    // Lab -> XYZ
    __m128 XYZ = dt_Lab_to_XYZ_sse2(_mm_load_ps(in + 4*k));
    // XYZ -> sRGB
    __m128 rgb = dt_XYZ_to_prophotoRGB_sse2(XYZ);

    __m128 luma;

    // adjust main saturation
    if(run_saturation)
    {
      luma = _mm_set1_ps(XYZ[1]); // the Y channel is the relative luminance
      rgb = luma + saturation * (rgb - luma);
    }

    // slope offset
    rgb = rgb * gain4 + lift4;

    //power
    rgb = _mm_max_ps(rgb, mm_zero);
    rgb = _mm_pow_ps(rgb, gamma4);

    // adjust main saturation output
    if(run_saturation_out)
    {
      XYZ = dt_prophotoRGB_to_XYZ_sse2(rgb);
      luma = _mm_set1_ps(XYZ[1]); // the Y channel is the relative luminance
      rgb = luma + saturation_out * (rgb - luma);
    }

    // fulcrum contrast
    if(run_contrast)
    {
      rgb = _mm_max_ps(rgb, mm_zero);
      rgb = _mm_pow_ps(rgb / grey, contrast) * grey;
    }

    // transform the result back to Lab
    // sRGB -> XYZ
    XYZ = dt_prophotoRGB_to_XYZ_sse2(rgb);
    // XYZ -> Lab
    _mm_stream_ps(out + 4*k, dt_XYZ_to_Lab_sse2(XYZ));
  }
#else
  if(!run_saturation && !run_saturation_out && !run_contrast)
  {
    // fully neutral settings, only apply the curves
    for(size_t k = 0; k < npixels; k++)
    {
    // transform the pixel to RGB:
    // Lab -> XYZ -> RGB, return Y as luma
    dt_aligned_pixel_t rgb;
    (void)dt_Lab_to_prophotorgb(in + 4*k, rgb);
    _apply_CDL(rgb, gain, lift, gamma);
    // transform the result back to Lab
    // ProphotoRGB -> XYZ -> Lab
    dt_aligned_pixel_t res;
    dt_prophotorgb_to_Lab(rgb , res);
    copy_pixel_nontemporal(out + 4*k, res);
    }
    return;
  }

  const dt_aligned_pixel_t grey4 = { grey, grey, grey, grey };
  const dt_aligned_pixel_t saturation4 = { saturation, saturation, saturation, saturation };
  const dt_aligned_pixel_t saturation_out4
    = { saturation_out, saturation_out, saturation_out, saturation_out };

  for(size_t k = 0; k < npixels; k++)
  {
    // transform the pixel to RGB:
    // Lab -> XYZ -> RGB, return Y as luma
    dt_aligned_pixel_t rgb;
    float luma = dt_Lab_to_prophotorgb(in + 4*k, rgb);
    if(run_saturation)
    {
      // main saturation input
      for_each_channel(c)
        rgb[c] = luma + saturation4[c] * (rgb[c] - luma);
    }
    _apply_CDL(rgb, gain, lift, gamma);

    // main saturation output
    if(run_saturation_out)
    {
      luma = dt_prophotorgb_to_XYZ_luma(rgb);
      for_each_channel(c)
        rgb[c] = luma + saturation_out4[c] * (rgb[c] - luma);
    }

    // fulcrum contrast
    if(run_contrast)
    {
      _apply_fulcrum_contrast(rgb, grey4, contrast_power);
    }

    // transform the result back to Lab
    // ProphotoRGB -> XYZ -> Lab
    dt_aligned_pixel_t res;
    dt_prophotorgb_to_Lab(rgb , res);
    copy_pixel_nontemporal(out + 4*k, res);
  }
#endif /* __SSE2__ */
}

// see http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html for the transformation matrices
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_colorbalance_data_t *d = piece->data;
  const float contrast = (d->contrast != 0.0f) ? 1.0f / d->contrast : 1000000.0f,
              grey = d->grey / 100.0f;
  const float saturation = d->saturation;
  const float saturation_out = d->saturation_out;

  const size_t npixels = (size_t)roi_out->width * roi_out->height;
  const float *const restrict in = ivoid;
  float *const restrict out = ovoid;
  const dt_aligned_pixel_t contrast_power = { contrast, contrast, contrast, contrast };

  // these are RGB values!
  const dt_aligned_pixel_t lift =
    { 2.0f - (d->lift[CHANNEL_RED] * d->lift[CHANNEL_FACTOR]),
      2.0f - (d->lift[CHANNEL_GREEN] * d->lift[CHANNEL_FACTOR]),
      2.0f - (d->lift[CHANNEL_BLUE] * d->lift[CHANNEL_FACTOR]),
      0.0f };
  const dt_aligned_pixel_t lift_sop =
    { ( d->lift[CHANNEL_RED] + d->lift[CHANNEL_FACTOR] - 2.0f),
      ( d->lift[CHANNEL_GREEN] + d->lift[CHANNEL_FACTOR] - 2.0f),
      ( d->lift[CHANNEL_BLUE] + d->lift[CHANNEL_FACTOR] - 2.0f),
      0.0f };
  const dt_aligned_pixel_t gamma =
    { d->gamma[CHANNEL_RED] * d->gamma[CHANNEL_FACTOR],
      d->gamma[CHANNEL_GREEN] * d->gamma[CHANNEL_FACTOR],
      d->gamma[CHANNEL_BLUE] * d->gamma[CHANNEL_FACTOR],
      1.0f };
  const dt_aligned_pixel_t gamma_sop =
    { (2.0f - d->gamma[CHANNEL_RED]) * (2.0f - d->gamma[CHANNEL_FACTOR]),
      (2.0f - d->gamma[CHANNEL_GREEN]) * (2.0f - d->gamma[CHANNEL_FACTOR]),
      (2.0f - d->gamma[CHANNEL_BLUE]) * (2.0f - d->gamma[CHANNEL_FACTOR]),
      1.0f };
  const dt_aligned_pixel_t gamma_inv_legacy =
    { (gamma[0] != 0.0f) ? 1.0f / gamma[0] : 1000000.0f,
      (gamma[1] != 0.0f) ? 1.0f / gamma[1] : 1000000.0f,
      (gamma[2] != 0.0f) ? 1.0f / gamma[2] : 1000000.0f,
      1.0f };
  const dt_aligned_pixel_t gamma_inv_lgg =
    { 2.2f * ((gamma[0] != 0.0f) ? 1.0f / gamma[0] : 1000000.0f),
      2.2f * ((gamma[1] != 0.0f) ? 1.0f / gamma[1] : 1000000.0f),
      2.2f * ((gamma[2] != 0.0f) ? 1.0f / gamma[2] : 1000000.0f),
      1.0f };
  const dt_aligned_pixel_t gain =
    { d->gain[CHANNEL_RED] * d->gain[CHANNEL_FACTOR],
      d->gain[CHANNEL_GREEN] * d->gain[CHANNEL_FACTOR],
      d->gain[CHANNEL_BLUE] * d->gain[CHANNEL_FACTOR],
      1.0f };

  const int mode = d->mode;
  // figure out the number of pixels each thread needs to process,
  // rounded up to a multiple of the CPU's cache line size
  const size_t nthreads = dt_get_num_threads();
  const size_t chunksize = dt_cacheline_chunks(npixels, nthreads);
  DT_OMP_FOR()
  for(size_t chunkstart = 0; chunkstart < npixels; chunkstart += chunksize)
  {
    size_t end = MIN(chunkstart + chunksize, npixels);
    switch(mode)
    {
      case LEGACY:
        _process_legacy(in + 4*chunkstart, out + 4*chunkstart, end-chunkstart, lift, gamma_inv_legacy, gain);
        break;
      case LIFT_GAMMA_GAIN:
        _process_lgg(in + 4*chunkstart, out + 4*chunkstart, end-chunkstart, lift, gamma_inv_lgg, gain,
                     grey, saturation, saturation_out, contrast_power);
        break;
      case SLOPE_OFFSET_POWER:
        _process_sop(in + 4*chunkstart, out + 4*chunkstart, end-chunkstart,
                     lift_sop, gamma_sop, gain, grey, saturation,
                     saturation_out, contrast, contrast_power);
        break;
    }
  }
  dt_omploop_sfence();
}

#ifdef HAVE_OPENCL
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_colorbalance_data_t *d = piece->data;
  dt_iop_colorbalance_global_data_t *gd = self->global_data;

  cl_int err = DT_OPENCL_DEFAULT_ERROR;
  const int devid = piece->pipe->devid;
  const int width = roi_in->width;
  const int height = roi_in->height;
  size_t sizes[] = { ROUNDUPDWD(width, devid), ROUNDUPDHT(height, devid), 1 };

  switch(d->mode)
  {
    case LEGACY:
    {
      const float lift[4] = { 2.0f - (d->lift[CHANNEL_RED] * d->lift[CHANNEL_FACTOR]),
                              2.0f - (d->lift[CHANNEL_GREEN] * d->lift[CHANNEL_FACTOR]),
                              2.0f - (d->lift[CHANNEL_BLUE] * d->lift[CHANNEL_FACTOR]), 0.0f },
                  gamma[4] = { d->gamma[CHANNEL_RED] * d->gamma[CHANNEL_FACTOR],
                               d->gamma[CHANNEL_GREEN] * d->gamma[CHANNEL_FACTOR],
                               d->gamma[CHANNEL_BLUE] * d->gamma[CHANNEL_FACTOR], 0.0f },
                  gamma_inv[4] = { (gamma[0] != 0.0f) ? 1.0f / gamma[0] : 1000000.0f,
                                   (gamma[1] != 0.0f) ? 1.0f / gamma[1] : 1000000.0f,
                                   (gamma[2] != 0.0f) ? 1.0f / gamma[2] : 1000000.0f, 0.0f },
                  gain[4] = { d->gain[CHANNEL_RED] * d->gain[CHANNEL_FACTOR],
                              d->gain[CHANNEL_GREEN] * d->gain[CHANNEL_FACTOR],
                              d->gain[CHANNEL_BLUE] * d->gain[CHANNEL_FACTOR], 0.0f },
                  contrast = (d->contrast != 0.0f) ? 1.0f / d->contrast : 1000000.0f,
                  grey = d->grey / 100.0f,
                  saturation = d->saturation;

      dt_opencl_set_kernel_args(devid, gd->kernel_colorbalance, 0, CLARG(dev_in), CLARG(dev_out), CLARG(width),
        CLARG(height), CLARG(lift), CLARG(gain), CLARG(gamma_inv), CLARG(saturation), CLARG(contrast),
        CLARG(grey));
      err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_colorbalance, sizes);
      if(err != CL_SUCCESS) goto error;
      return CL_SUCCESS;

      break;
    }
    case LIFT_GAMMA_GAIN:
    {
      const float lift[4] = { 2.0f - (d->lift[CHANNEL_RED] * d->lift[CHANNEL_FACTOR]),
                              2.0f - (d->lift[CHANNEL_GREEN] * d->lift[CHANNEL_FACTOR]),
                              2.0f - (d->lift[CHANNEL_BLUE] * d->lift[CHANNEL_FACTOR]), 0.0f },
                  gamma[4] = { d->gamma[CHANNEL_RED] * d->gamma[CHANNEL_FACTOR],
                               d->gamma[CHANNEL_GREEN] * d->gamma[CHANNEL_FACTOR],
                               d->gamma[CHANNEL_BLUE] * d->gamma[CHANNEL_FACTOR], 0.0f },
                  gamma_inv[4] = { (gamma[0] != 0.0f) ? 1.0f / gamma[0] : 1000000.0f,
                                   (gamma[1] != 0.0f) ? 1.0f / gamma[1] : 1000000.0f,
                                   (gamma[2] != 0.0f) ? 1.0f / gamma[2] : 1000000.0f, 0.0f },
                  gain[4] = { d->gain[CHANNEL_RED] * d->gain[CHANNEL_FACTOR],
                              d->gain[CHANNEL_GREEN] * d->gain[CHANNEL_FACTOR],
                              d->gain[CHANNEL_BLUE] * d->gain[CHANNEL_FACTOR], 0.0f },
                  contrast = (d->contrast != 0.0f) ? 1.0f / d->contrast : 1000000.0f,
                  grey = d->grey / 100.0f,
                  saturation = d->saturation,
                  saturation_out = d->saturation_out;

      dt_opencl_set_kernel_args(devid, gd->kernel_colorbalance_lgg, 0, CLARG(dev_in), CLARG(dev_out),
        CLARG(width), CLARG(height), CLARG(lift), CLARG(gain), CLARG(gamma_inv), CLARG(saturation), CLARG(contrast),
        CLARG(grey), CLARG(saturation_out));
      err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_colorbalance_lgg, sizes);
      if(err != CL_SUCCESS) goto error;
      return CL_SUCCESS;

      break;
    }
    case SLOPE_OFFSET_POWER:
    {
      const float lift[4] = { ( d->lift[CHANNEL_RED] + d->lift[CHANNEL_FACTOR] - 2.0f ),
                              ( d->lift[CHANNEL_GREEN] + d->lift[CHANNEL_FACTOR] - 2.0f ),
                              ( d->lift[CHANNEL_BLUE] + d->lift[CHANNEL_FACTOR] - 2.0f ),
                              0.0f },
                  gamma[4] = { (2.0f - d->gamma[CHANNEL_RED]) * (2.0f - d->gamma[CHANNEL_FACTOR]),
                               (2.0f - d->gamma[CHANNEL_GREEN]) * (2.0f - d->gamma[CHANNEL_FACTOR]),
                               (2.0f - d->gamma[CHANNEL_BLUE]) * (2.0f - d->gamma[CHANNEL_FACTOR]),
                               0.0f },
                  gain[4] = { d->gain[CHANNEL_RED] * d->gain[CHANNEL_FACTOR],
                              d->gain[CHANNEL_GREEN] * d->gain[CHANNEL_FACTOR],
                              d->gain[CHANNEL_BLUE] * d->gain[CHANNEL_FACTOR],
                              0.0f },
                  contrast = (d->contrast != 0.0f) ? 1.0f / d->contrast : 1000000.0f,
                  grey = d->grey / 100.0f,
                  saturation = d->saturation,
                  saturation_out = d->saturation_out;

      dt_opencl_set_kernel_args(devid, gd->kernel_colorbalance_cdl, 0, CLARG(dev_in), CLARG(dev_out),
        CLARG(width), CLARG(height), CLARG(lift), CLARG(gain), CLARG(gamma), CLARG(saturation), CLARG(contrast),
        CLARG(grey), CLARG(saturation_out));
      err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_colorbalance_cdl, sizes);
      if(err != CL_SUCCESS) goto error;
      return CL_SUCCESS;

      break;
    }
  }

error:
  return err;
}
#endif

static inline void update_saturation_slider_color(GtkWidget *slider, float hue)
{
  dt_aligned_pixel_t rgb;
  if(hue != -1)
  {
    hsl2rgb(rgb, hue, 1.0, 0.5);
    dt_bauhaus_slider_set_stop(slider, 1.0, rgb[0], rgb[1], rgb[2]);
    hsl2rgb(rgb, hue, 0.0, 0.5);
    dt_bauhaus_slider_set_stop(slider, 0.0, rgb[0], rgb[1], rgb[2]);
    gtk_widget_queue_draw(GTK_WIDGET(slider));
  }
}

static inline void set_RGB_sliders(GtkWidget *R, GtkWidget *G, GtkWidget *B, float hsl[3], float *p, int mode)
{

  dt_aligned_pixel_t rgb = { 0.0f };
  hsl2rgb(rgb, hsl[0], hsl[1], hsl[2]);

  if(hsl[0] != -1)
  {
    p[CHANNEL_RED] = rgb[0] * 2.0f;
    p[CHANNEL_GREEN] = rgb[1] * 2.0f;
    p[CHANNEL_BLUE] = rgb[2] * 2.0f;

    ++darktable.gui->reset;
    dt_bauhaus_slider_set(R, p[CHANNEL_RED]);
    dt_bauhaus_slider_set(G, p[CHANNEL_GREEN]);
    dt_bauhaus_slider_set(B, p[CHANNEL_BLUE]);
    --darktable.gui->reset;
  }
}

static inline void set_HSL_sliders(GtkWidget *hue, GtkWidget *sat, float RGB[4])
{
  /** HSL sliders are set from the RGB values at any time.
  * Only the RGB values are saved and used in the computations.
  * The HSL sliders are merely an interface.
  */
  dt_aligned_pixel_t RGB_norm = { (RGB[CHANNEL_RED] / 2.0f), (RGB[CHANNEL_GREEN] / 2.0f), (RGB[CHANNEL_BLUE] / 2.0f) };

  float h, s, l;
  rgb2hsl(RGB_norm, &h, &s, &l);

  if(h != -1.0f)
  {
    dt_bauhaus_slider_set(hue, h * 360.0f);
    dt_bauhaus_slider_set(sat, s * 100.0f);
    update_saturation_slider_color(GTK_WIDGET(sat), h);
    gtk_widget_queue_draw(GTK_WIDGET(sat));
  }
  else
  {
    dt_bauhaus_slider_set(hue, -1.0f);
    dt_bauhaus_slider_set(sat, 0.0f);
    gtk_widget_queue_draw(GTK_WIDGET(sat));
  }
}

static inline void _check_tuner_picker_labels(dt_iop_module_t *self)
{
  dt_iop_colorbalance_gui_data_t *g = self->gui_data;

  if(g->luma_patches_flags[GAIN] == USER_SELECTED && g->luma_patches_flags[GAMMA] == USER_SELECTED
     && g->luma_patches_flags[LIFT] == USER_SELECTED)
    dt_bauhaus_widget_set_label(g->auto_luma, NULL, N_("optimize luma from patches"));
  else
    dt_bauhaus_widget_set_label(g->auto_luma, NULL, N_("optimize luma"));

  if(g->color_patches_flags[GAIN] == USER_SELECTED && g->color_patches_flags[GAMMA] == USER_SELECTED
     && g->color_patches_flags[LIFT] == USER_SELECTED)
    dt_bauhaus_widget_set_label(g->auto_color, NULL, N_("neutralize colors from patches"));
  else
    dt_bauhaus_widget_set_label(g->auto_color, NULL, N_("neutralize colors"));
}


static void apply_autogrey(dt_iop_module_t *self)
{
  if(darktable.gui->reset) return;
  dt_iop_colorbalance_params_t *p = self->params;
  dt_iop_colorbalance_gui_data_t *g = self->gui_data;

  dt_aligned_pixel_t XYZ = { 0.0f };
  dt_aligned_pixel_t rgb = { 0.0f };
  dt_Lab_to_XYZ((const float *)self->picked_color, XYZ);
  dt_XYZ_to_prophotorgb((const float *)XYZ, rgb);

  const dt_aligned_pixel_t lift
      = { (p->lift[CHANNEL_RED] + p->lift[CHANNEL_FACTOR] - 2.0f),
          (p->lift[CHANNEL_GREEN] + p->lift[CHANNEL_FACTOR] - 2.0f),
          (p->lift[CHANNEL_BLUE] + p->lift[CHANNEL_FACTOR] - 2.0f) },
      gamma
      = { 2.0f - p->gamma[CHANNEL_RED] * p->gamma[CHANNEL_FACTOR],
          2.0f - p->gamma[CHANNEL_GREEN] * p->gamma[CHANNEL_FACTOR],
          2.0f - p->gamma[CHANNEL_BLUE] * p->gamma[CHANNEL_FACTOR] },
      gain = { p->gain[CHANNEL_RED] * p->gain[CHANNEL_FACTOR], p->gain[CHANNEL_GREEN] * p->gain[CHANNEL_FACTOR],
               p->gain[CHANNEL_BLUE] * p->gain[CHANNEL_FACTOR] };

  _apply_CDL(rgb, gain, lift, gamma);
  for_each_channel(c)
  {
    rgb[c] = CLAMP(rgb[c], 0.0f, 1.0f);
  }

  dt_prophotorgb_to_XYZ((const float *)rgb, XYZ);

  p->grey = XYZ[1] * 100.0f;

  ++darktable.gui->reset;
  dt_bauhaus_slider_set(g->grey, p->grey);
  --darktable.gui->reset;

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

static void apply_lift_neutralize(dt_iop_module_t *self)
{
  if(darktable.gui->reset) return;
  dt_iop_colorbalance_params_t *p = self->params;
  dt_iop_colorbalance_gui_data_t *g = self->gui_data;

  dt_aligned_pixel_t XYZ = { 0.0f };
  dt_Lab_to_XYZ((const float *)self->picked_color, XYZ);
  dt_aligned_pixel_t RGB = { 0.0f };
  dt_XYZ_to_prophotorgb((const float *)XYZ, RGB);

// Save the patch color for the optimization
  for(int c = 0; c < 3; ++c) g->color_patches_lift[c] = RGB[c];
  g->color_patches_flags[LIFT] = USER_SELECTED;

  // Compute the RGB values after the CDL factors
  for(int c = 0; c < 3; ++c)
    RGB[c] = CDL(RGB[c], p->gain[CHANNEL_FACTOR], p->lift[CHANNEL_FACTOR] - 1.0f, 2.0f - p->gamma[CHANNEL_FACTOR]);

  // Compute the luminance of the average grey
  dt_XYZ_to_prophotorgb((const float *)XYZ, RGB);

  // Get the parameter
  for(int c = 0; c < 3; ++c)
    RGB[c] = powf(XYZ[1], 1.0f/(2.0f - p->gamma[c+1])) - RGB[c] * p->gain[c+1];

  p->lift[CHANNEL_RED] = RGB[0] + 1.0f;
  p->lift[CHANNEL_GREEN] = RGB[1] + 1.0f;
  p->lift[CHANNEL_BLUE] = RGB[2] + 1.0f;

  ++darktable.gui->reset;
  dt_bauhaus_slider_set(g->lift_r, p->lift[CHANNEL_RED]);
  dt_bauhaus_slider_set(g->lift_g, p->lift[CHANNEL_GREEN]);
  dt_bauhaus_slider_set(g->lift_b, p->lift[CHANNEL_BLUE]);
  set_HSL_sliders(g->hue_lift, g->sat_lift, p->lift);
  --darktable.gui->reset;

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

static void apply_gamma_neutralize(dt_iop_module_t *self)
{
  if(darktable.gui->reset) return;
  dt_iop_colorbalance_params_t *p = self->params;
  dt_iop_colorbalance_gui_data_t *g = self->gui_data;

  dt_aligned_pixel_t XYZ = { 0.0f };
  dt_Lab_to_XYZ((const float *)self->picked_color, XYZ);
  dt_aligned_pixel_t RGB = { 0.0f };
  dt_XYZ_to_prophotorgb((const float *)XYZ, RGB);

// Save the patch color for the optimization
  for(int c = 0; c < 3; ++c) g->color_patches_gamma[c] = RGB[c];
  g->color_patches_flags[GAMMA] = USER_SELECTED;

  // Compute the RGB values after the CDL factors
  for(int c = 0; c < 3; ++c)
    RGB[c] = CDL(RGB[c], p->gain[CHANNEL_FACTOR], p->lift[CHANNEL_FACTOR] - 1.0f, 2.0f - p->gamma[CHANNEL_FACTOR]);

  // Compute the luminance of the average grey
  dt_XYZ_to_prophotorgb((const float *)XYZ, RGB);

  // Get the parameter
  for(int c = 0; c < 3; ++c) RGB[c] = logf(XYZ[1])/ logf(RGB[c] * p->gain[c + 1] + p->lift[c + 1] - 1.0f);

  p->gamma[CHANNEL_RED] = CLAMP(2.0 - RGB[0], 0.0001f, 2.0f);
  p->gamma[CHANNEL_GREEN] = CLAMP(2.0 - RGB[1], 0.0001f, 2.0f);
  p->gamma[CHANNEL_BLUE] = CLAMP(2.0 - RGB[2], 0.0001f, 2.0f);

  ++darktable.gui->reset;
  dt_bauhaus_slider_set(g->gamma_r, p->gamma[CHANNEL_RED]);
  dt_bauhaus_slider_set(g->gamma_g, p->gamma[CHANNEL_GREEN]);
  dt_bauhaus_slider_set(g->gamma_b, p->gamma[CHANNEL_BLUE]);
  set_HSL_sliders(g->hue_gamma, g->sat_gamma, p->gamma);
  --darktable.gui->reset;

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

static void apply_gain_neutralize(dt_iop_module_t *self)
{
  if(darktable.gui->reset) return;
  dt_iop_colorbalance_params_t *p = self->params;
  dt_iop_colorbalance_gui_data_t *g = self->gui_data;

  dt_aligned_pixel_t XYZ = { 0.0f };
  dt_Lab_to_XYZ((const float *)self->picked_color, XYZ);
  dt_aligned_pixel_t RGB = { 0.0f };
  dt_XYZ_to_prophotorgb((const float *)XYZ, RGB);

// Save the patch color for the optimization
  for(int c = 0; c < 3; c++) g->color_patches_gain[c] = RGB[c];
  g->color_patches_flags[GAIN] = USER_SELECTED;

  // Compute the RGB values after the CDL factors
  for(int c = 0; c < 3; ++c)
    RGB[c] = CDL(RGB[c], p->gain[CHANNEL_FACTOR], p->lift[CHANNEL_FACTOR] - 1.0f, 2.0f - p->gamma[CHANNEL_FACTOR]);

  // Compute the luminance of the average grey
  dt_XYZ_to_prophotorgb((const float *)XYZ, RGB);

  // Get the parameter
  for(int c = 0; c < 3; ++c)
    RGB[c] = (powf(XYZ[1], 1.0f/(2.0f - p->gamma[c+1])) - p->lift[c+1] + 1.0f) / MAX(RGB[c], 0.000001f);

  p->gain[CHANNEL_RED] = RGB[0];
  p->gain[CHANNEL_GREEN] = RGB[1];
  p->gain[CHANNEL_BLUE] = RGB[2];

  ++darktable.gui->reset;
  dt_bauhaus_slider_set(g->gain_r, p->gain[CHANNEL_RED]);
  dt_bauhaus_slider_set(g->gain_g, p->gain[CHANNEL_GREEN]);
  dt_bauhaus_slider_set(g->gain_b, p->gain[CHANNEL_BLUE]);
  set_HSL_sliders(g->hue_gain, g->sat_gain, p->gain);
  --darktable.gui->reset;

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

static void apply_lift_auto(dt_iop_module_t *self)
{
  if(darktable.gui->reset) return;
  dt_iop_colorbalance_params_t *p = self->params;
  dt_iop_colorbalance_gui_data_t *g = self->gui_data;

  dt_aligned_pixel_t XYZ = { 0.0f };
  dt_Lab_to_XYZ((const float *)self->picked_color_min, XYZ);

  g->luma_patches[LIFT] = XYZ[1];
  g->luma_patches_flags[LIFT] = USER_SELECTED;

  dt_aligned_pixel_t RGB = { 0.0f };
  dt_XYZ_to_prophotorgb((const float *)XYZ, RGB);

  p->lift[CHANNEL_FACTOR] = -p->gain[CHANNEL_FACTOR] * XYZ[1] + 1.0f;

  ++darktable.gui->reset;
  dt_bauhaus_slider_set(g->lift_factor, p->lift[CHANNEL_FACTOR]);
  --darktable.gui->reset;

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

static void apply_gamma_auto(dt_iop_module_t *self)
{
  if(darktable.gui->reset) return;
  dt_iop_colorbalance_params_t *p = self->params;
  dt_iop_colorbalance_gui_data_t *g = self->gui_data;

  dt_aligned_pixel_t XYZ = { 0.0f };
  dt_Lab_to_XYZ((const float *)self->picked_color, XYZ);

  g->luma_patches[GAMMA] = XYZ[1];
  g->luma_patches_flags[GAMMA] = USER_SELECTED;

  dt_aligned_pixel_t RGB = { 0.0f };
  dt_XYZ_to_prophotorgb((const float *)XYZ, RGB);

  p->gamma[CHANNEL_FACTOR]
      = 2.0f - logf(0.1842f) / logf(MAX(p->gain[CHANNEL_FACTOR] * XYZ[1] + p->lift[CHANNEL_FACTOR] - 1.0f, 0.000001f));

  ++darktable.gui->reset;
  dt_bauhaus_slider_set(g->gamma_factor, p->gamma[CHANNEL_FACTOR]);
  --darktable.gui->reset;

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

static void apply_gain_auto(dt_iop_module_t *self)
{
  if(darktable.gui->reset) return;
  dt_iop_colorbalance_params_t *p = self->params;
  dt_iop_colorbalance_gui_data_t *g = self->gui_data;

  dt_aligned_pixel_t XYZ = { 0.0f };
  dt_Lab_to_XYZ((const float *)self->picked_color_max, XYZ);

  g->luma_patches[GAIN] = XYZ[1];
  g->luma_patches_flags[GAIN] = USER_SELECTED;

  dt_aligned_pixel_t RGB = { 0.0f };
  dt_XYZ_to_prophotorgb((const float *)XYZ, RGB);

  p->gain[CHANNEL_FACTOR] = p->lift[CHANNEL_FACTOR] / (XYZ[1]);

  ++darktable.gui->reset;
  dt_bauhaus_slider_set(g->gain_factor, p->gain[CHANNEL_FACTOR]);
  --darktable.gui->reset;

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

static void apply_autocolor(dt_iop_module_t *self)
{
  dt_iop_colorbalance_params_t *p = self->params;
  dt_iop_colorbalance_gui_data_t *g = self->gui_data;

  if(g->color_patches_flags[GAIN] == INVALID || g->color_patches_flags[GAMMA] == INVALID
     || g->color_patches_flags[LIFT] == INVALID)
  {
    /*
     * Some color patches were not picked by the user. Take a
     * picture-wide patch for these.
     */
    dt_aligned_pixel_t XYZ = { 0.0f };
    dt_Lab_to_XYZ((const float *)self->picked_color, XYZ);
    dt_aligned_pixel_t RGB = { 0.0f };
    dt_XYZ_to_prophotorgb((const float *)XYZ, RGB);

    // Save the patch color for the optimization
    if(g->color_patches_flags[LIFT] == INVALID)
    {
      for(int c = 0; c < 3; c++) g->color_patches_lift[c] = RGB[c];
      g->color_patches_flags[LIFT] = AUTO_SELECTED;
    }
    if(g->color_patches_flags[GAMMA] == INVALID)
    {
      for(int c = 0; c < 3; c++) g->color_patches_gamma[c] = RGB[c];
      g->color_patches_flags[GAMMA] = AUTO_SELECTED;
    }
    if(g->color_patches_flags[GAIN] == INVALID)
    {
      for(int c = 0; c < 3; c++) g->color_patches_gain[c] = RGB[c];
      g->color_patches_flags[GAIN] = AUTO_SELECTED;
    }
  }

  dt_iop_color_picker_reset(self, TRUE);

  // Build the CDL-corrected samples (after the factors)
  dt_aligned_pixel_t samples_lift = { 0.f };
  dt_aligned_pixel_t samples_gamma = { 0.f };
  dt_aligned_pixel_t samples_gain = { 0.f };

  for(int c = 0; c < 3; ++c)
  {
    samples_lift[c] = CDL(g->color_patches_lift[c], p->gain[CHANNEL_FACTOR], p->lift[CHANNEL_FACTOR] - 1.0f, 2.0f - p->gamma[CHANNEL_FACTOR]);
    samples_gamma[c] = CDL(g->color_patches_gamma[c], p->gain[CHANNEL_FACTOR], p->lift[CHANNEL_FACTOR] - 1.0f, 2.0f - p->gamma[CHANNEL_FACTOR]);
    samples_gain[c] = CDL(g->color_patches_gain[c], p->gain[CHANNEL_FACTOR], p->lift[CHANNEL_FACTOR] - 1.0f, 2.0f - p->gamma[CHANNEL_FACTOR]);
  }

  // Get the average patches luma value (= neutral grey equivalents) after the CDL factors
  dt_aligned_pixel_t greys = { 0.0 };
  dt_aligned_pixel_t XYZ = { 0.0 };
  dt_prophotorgb_to_XYZ((const float *)samples_lift, (float *)XYZ);
  greys[0] = XYZ[1];
  dt_prophotorgb_to_XYZ((const float *)samples_gamma, (float *)XYZ);
  greys[1] = XYZ[1];
  dt_prophotorgb_to_XYZ((const float *)samples_gain, (float *)XYZ);
  greys[2] = XYZ[1];

  // Get the current params
  dt_aligned_pixel_t RGB_lift = { p->lift[CHANNEL_RED] - 1.0f, p->lift[CHANNEL_GREEN] - 1.0f, p->lift[CHANNEL_BLUE] - 1.0f };
  dt_aligned_pixel_t RGB_gamma = { p->gamma[CHANNEL_RED], p->gamma[CHANNEL_GREEN], p->gamma[CHANNEL_BLUE] };
  dt_aligned_pixel_t RGB_gain = { p->gain[CHANNEL_RED], p->gain[CHANNEL_GREEN], p->gain[CHANNEL_BLUE] };

  /** Optimization loop :
  * We try to find the CDL curves that neutralize the 3 input color patches, while not affecting the overall lightness.
  * But this is a non-linear overconstrained problem with tainted inputs, so the best we can do is a numerical optimization.
  * To do so, we compute each parameter of each RGB curve from the input color and the 2 other parameters.
  * Then, we loop over the previous optimization until the difference between 2 updates is insignificant.
  * This would need a proper stopping criterion based on convergence analysis, but it would be overkill here since
  * it should converge usually in 20 iterations, and maximum in 100.
  * Also, the convergence has not been proven formally.
  * For better color accuracy, we compute on luminance corrected RGB values (after the main factors corrections).
  * To avoid divergence, we constrain the parameters between +- 0.25 around the neutral value.
  * Experimentally, nothing good happens out of these bounds.
  */
  for(int runs = 0 ; runs < 1000 ; ++runs)
  {
    // compute RGB slope/gain (powf(XYZ[1], 1.0f/(2.0f - p->gamma[c+1])) - p->lift[c+1] + 1.0f) / MAX(RGB[c], 0.000001f);
    for(int c = 0; c < 3; ++c)
      RGB_gain[c] = CLAMP((powf(greys[GAIN], 1.0f / (2.0f - RGB_gamma[c])) - RGB_lift[c]) / MAX(samples_gain[c], 0.000001f), 0.75f, 1.25f);
    // compute RGB offset/lift powf(XYZ[1], 1.0f/(2.0f - p->gamma[c+1])) - RGB[c] * p->gain[c+1];
    for(int c = 0; c < 3; ++c)
      RGB_lift[c] = CLAMP(powf(greys[LIFT], 1.0f / (2.0f - RGB_gamma[c])) - samples_lift[c] * RGB_gain[c], -0.025f, 0.025f);
    // compute  power/gamma 2.0f - logf(0.1842f) / logf(MAX(p->gain[CHANNEL_FACTOR] * XYZ[1] + p->lift[CHANNEL_FACTOR] - 1.0f, 0.000001f));
    for(int c = 0; c < 3; ++c)
      RGB_gamma[c] = 2.0f - CLAMP(logf(MAX(greys[GAMMA], 0.000001f)) / logf(MAX(RGB_gain[c] * samples_gamma[c] + RGB_lift[c], 0.000001f)), 0.75f, 1.25f);
  }

  // save
  p->lift[CHANNEL_RED] = RGB_lift[0] + 1.0f;
  p->lift[CHANNEL_GREEN] = RGB_lift[1] + 1.0f;
  p->lift[CHANNEL_BLUE] = RGB_lift[2] + 1.0f;
  p->gamma[CHANNEL_RED] = RGB_gamma[0];
  p->gamma[CHANNEL_GREEN] = RGB_gamma[1];
  p->gamma[CHANNEL_BLUE] = RGB_gamma[2];
  p->gain[CHANNEL_RED] = RGB_gain[0];
  p->gain[CHANNEL_GREEN] = RGB_gain[1];
  p->gain[CHANNEL_BLUE] = RGB_gain[2];

  ++darktable.gui->reset;
  dt_bauhaus_slider_set(g->lift_r, p->lift[CHANNEL_RED]);
  dt_bauhaus_slider_set(g->lift_g, p->lift[CHANNEL_GREEN]);
  dt_bauhaus_slider_set(g->lift_b, p->lift[CHANNEL_BLUE]);

  dt_bauhaus_slider_set(g->gamma_r, p->gamma[CHANNEL_RED]);
  dt_bauhaus_slider_set(g->gamma_g, p->gamma[CHANNEL_GREEN]);
  dt_bauhaus_slider_set(g->gamma_b, p->gamma[CHANNEL_BLUE]);

  dt_bauhaus_slider_set(g->gain_r, p->gain[CHANNEL_RED]);
  dt_bauhaus_slider_set(g->gain_g, p->gain[CHANNEL_GREEN]);
  dt_bauhaus_slider_set(g->gain_b, p->gain[CHANNEL_BLUE]);

  set_HSL_sliders(g->hue_lift, g->sat_lift, p->lift);
  set_HSL_sliders(g->hue_gamma, g->sat_gamma, p->gamma);
  set_HSL_sliders(g->hue_gain, g->sat_gain, p->gain);
  --darktable.gui->reset;

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

static void apply_autoluma(dt_iop_module_t *self)
{
  dt_iop_colorbalance_params_t *p = self->params;
  dt_iop_colorbalance_gui_data_t *g = self->gui_data;

  /*
   * If some luma patches were not picked by the user, take a
   * picture-wide patch for these.
   */
  if(g->luma_patches_flags[LIFT] == INVALID)
  {
    dt_aligned_pixel_t XYZ = { 0.0f };
    dt_Lab_to_XYZ((const float *)self->picked_color_min, XYZ);
    g->luma_patches[LIFT] = XYZ[1];
    g->luma_patches_flags[LIFT] = AUTO_SELECTED;
  }
  if(g->luma_patches_flags[GAMMA] == INVALID)
  {
    dt_aligned_pixel_t XYZ = { 0.0f };
    dt_Lab_to_XYZ((const float *)self->picked_color, XYZ);
    g->luma_patches[GAMMA] = XYZ[1];
    g->luma_patches_flags[GAMMA] = AUTO_SELECTED;
  }
  if(g->luma_patches_flags[GAIN] == INVALID)
  {
    dt_aligned_pixel_t XYZ = { 0.0f };
    dt_Lab_to_XYZ((const float *)self->picked_color_max, XYZ);
    g->luma_patches[GAIN] = XYZ[1];
    g->luma_patches_flags[GAIN] = AUTO_SELECTED;
  }

  dt_iop_color_picker_reset(self, TRUE);

  /** Optimization loop :
  * We try to find the CDL curves that neutralize the 3 input luma patches
  */
  for(int runs = 0 ; runs < 100 ; ++runs)
  {
    p->gain[CHANNEL_FACTOR] = CLAMP(p->lift[CHANNEL_FACTOR] / g->luma_patches[GAIN], 0.0f, 2.0f);
    p->lift[CHANNEL_FACTOR] = CLAMP(-p->gain[CHANNEL_FACTOR] * g->luma_patches[LIFT] + 1.0f, 0.0f, 2.0f);
    p->gamma[CHANNEL_FACTOR] = CLAMP(2.0f - logf(0.1842f) / logf(MAX(p->gain[CHANNEL_FACTOR] * g->luma_patches[GAMMA] + p->lift[CHANNEL_FACTOR] - 1.0f, 0.000001f)), 0.0f, 2.0f);
  }

  ++darktable.gui->reset;
  dt_bauhaus_slider_set(g->lift_factor, p->lift[CHANNEL_FACTOR]);
  dt_bauhaus_slider_set(g->gamma_factor, p->gamma[CHANNEL_FACTOR]);
  dt_bauhaus_slider_set(g->gain_factor, p->gain[CHANNEL_FACTOR]);
  --darktable.gui->reset;

  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_colorbalance_gui_data_t *g = self->gui_data;
  if     (picker == g->hue_lift)
    apply_lift_neutralize(self);
  else if(picker == g->hue_gamma)
    apply_gamma_neutralize(self);
  else if(picker == g->hue_gain)
    apply_gain_neutralize(self);
  else if(picker == g->lift_factor)
    apply_lift_auto(self);
  else if(picker == g->gamma_factor)
    apply_gamma_auto(self);
  else if(picker == g->gain_factor)
    apply_gain_auto(self);
  else if(picker == g->grey)
    apply_autogrey(self);
  else if(picker == g->auto_luma)
    apply_autoluma(self);
  else if(picker == g->auto_color)
    apply_autocolor(self);
  else
    dt_print(DT_DEBUG_ALWAYS, "[colorbalance] unknown color picker");

  _check_tuner_picker_labels(self);
}

void init_global(dt_iop_module_so_t *self)
{
  const int program = 8; // extended.cl, from programs.conf
  dt_iop_colorbalance_global_data_t *gd = malloc(sizeof(dt_iop_colorbalance_global_data_t));
  self->data = gd;
  gd->kernel_colorbalance = dt_opencl_create_kernel(program, "colorbalance");
  gd->kernel_colorbalance_lgg = dt_opencl_create_kernel(program, "colorbalance_lgg");
  gd->kernel_colorbalance_cdl = dt_opencl_create_kernel(program, "colorbalance_cdl");
}

void cleanup_global(dt_iop_module_so_t *self)
{
  dt_iop_colorbalance_global_data_t *gd = self->data;
  dt_opencl_free_kernel(gd->kernel_colorbalance);
  dt_opencl_free_kernel(gd->kernel_colorbalance_lgg);
  dt_opencl_free_kernel(gd->kernel_colorbalance_cdl);
  free(self->data);
  self->data = NULL;
}

void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe,
                   dt_dev_pixelpipe_iop_t *piece)
{
  dt_iop_colorbalance_data_t *d = piece->data;
  dt_iop_colorbalance_params_t *p = (dt_iop_colorbalance_params_t *)p1;

  d->mode = p->mode;

  const dt_aligned_pixel_t lift = { p->lift[CHANNEL_RED], p->lift[CHANNEL_GREEN], p->lift[CHANNEL_BLUE] };
  const dt_aligned_pixel_t gamma = { p->gamma[CHANNEL_RED], p->gamma[CHANNEL_GREEN], p->gamma[CHANNEL_BLUE] };
  const dt_aligned_pixel_t gain = { p->gain[CHANNEL_RED], p->gain[CHANNEL_GREEN], p->gain[CHANNEL_BLUE] };

  switch(d->mode)
  {
    case SLOPE_OFFSET_POWER:
    {
      // Correct the luminance in RGB parameters so we don't affect it
      dt_aligned_pixel_t XYZ;

      dt_prophotorgb_to_XYZ(lift, XYZ);
      d->lift[CHANNEL_FACTOR] = p->lift[CHANNEL_FACTOR];
      d->lift[CHANNEL_RED] = (p->lift[CHANNEL_RED] - XYZ[1]) + 1.f;
      d->lift[CHANNEL_GREEN] = (p->lift[CHANNEL_GREEN] - XYZ[1]) + 1.f;
      d->lift[CHANNEL_BLUE] = (p->lift[CHANNEL_BLUE] - XYZ[1]) + 1.f;

      dt_prophotorgb_to_XYZ(gamma, XYZ);
      d->gamma[CHANNEL_FACTOR] = p->gamma[CHANNEL_FACTOR];
      d->gamma[CHANNEL_RED] = (p->gamma[CHANNEL_RED] - XYZ[1]) + 1.f;
      d->gamma[CHANNEL_GREEN] = (p->gamma[CHANNEL_GREEN] - XYZ[1]) + 1.f;
      d->gamma[CHANNEL_BLUE] = (p->gamma[CHANNEL_BLUE] - XYZ[1]) + 1.f;

      dt_prophotorgb_to_XYZ(gain, XYZ);
      d->gain[CHANNEL_FACTOR] = p->gain[CHANNEL_FACTOR];
      d->gain[CHANNEL_RED] = (p->gain[CHANNEL_RED] - XYZ[1]) + 1.f;
      d->gain[CHANNEL_GREEN] = (p->gain[CHANNEL_GREEN] - XYZ[1]) + 1.f;
      d->gain[CHANNEL_BLUE] = (p->gain[CHANNEL_BLUE] - XYZ[1]) + 1.f;

      break;
    }

    case LEGACY:
    {
      // Luminance is not corrected in lift/gamma/gain for compatibility
      for(int i = 0; i < CHANNEL_SIZE; i++)
      {
        d->lift[i] = p->lift[i];
        d->gamma[i] = p->gamma[i];
        d->gain[i] = p->gain[i];
      }

      break;
    }

    case LIFT_GAMMA_GAIN:
    {
      // Correct the luminance in RGB parameters so we don't affect it
      dt_aligned_pixel_t XYZ;

      dt_prophotorgb_to_XYZ(lift, XYZ);
      d->lift[CHANNEL_FACTOR] = p->lift[CHANNEL_FACTOR];
      d->lift[CHANNEL_RED] = (p->lift[CHANNEL_RED] - XYZ[1]) + 1.f;
      d->lift[CHANNEL_GREEN] = (p->lift[CHANNEL_GREEN] - XYZ[1]) + 1.f;
      d->lift[CHANNEL_BLUE] = (p->lift[CHANNEL_BLUE] - XYZ[1]) + 1.f;

      dt_prophotorgb_to_XYZ(gamma, XYZ);
      d->gamma[CHANNEL_FACTOR] = p->gamma[CHANNEL_FACTOR];
      d->gamma[CHANNEL_RED] = (p->gamma[CHANNEL_RED] - XYZ[1]) + 1.f;
      d->gamma[CHANNEL_GREEN] = (p->gamma[CHANNEL_GREEN] - XYZ[1]) + 1.f;
      d->gamma[CHANNEL_BLUE] = (p->gamma[CHANNEL_BLUE] - XYZ[1]) + 1.f;

      dt_prophotorgb_to_XYZ(gain, XYZ);
      d->gain[CHANNEL_FACTOR] = p->gain[CHANNEL_FACTOR];
      d->gain[CHANNEL_RED] = (p->gain[CHANNEL_RED] - XYZ[1]) + 1.f;
      d->gain[CHANNEL_GREEN] = (p->gain[CHANNEL_GREEN] - XYZ[1]) + 1.f;
      d->gain[CHANNEL_BLUE] = (p->gain[CHANNEL_BLUE] - XYZ[1]) + 1.f;

      break;
    }
  }

  d->grey = p->grey;
  d->saturation = p->saturation;
  d->saturation_out = p->saturation_out;
  d->contrast = p->contrast;
}

void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
{
  piece->data = calloc(1, sizeof(dt_iop_colorbalance_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 set_visible_widgets(dt_iop_colorbalance_gui_data_t *g)
{
  const int mode = dt_bauhaus_combobox_get(g->mode);
  const int control_mode = dt_bauhaus_combobox_get(g->controls);

  gtk_widget_set_visible(g->master_box, mode != LEGACY);

  dt_conf_set_string("plugins/darkroom/colorbalance/controls",
                     control_mode == RGBL ? "RGBL" :
                     control_mode == BOTH ? "BOTH" : "HSL");
  gboolean show_rgbl = (control_mode == RGBL) || (control_mode == BOTH);
  gboolean show_hsl  = (control_mode == HSL)  || (control_mode == BOTH);

  gtk_widget_set_visible(g->lift_r,  show_rgbl);
  gtk_widget_set_visible(g->lift_g,  show_rgbl);
  gtk_widget_set_visible(g->lift_b,  show_rgbl);
  gtk_widget_set_visible(g->gamma_r, show_rgbl);
  gtk_widget_set_visible(g->gamma_g, show_rgbl);
  gtk_widget_set_visible(g->gamma_b, show_rgbl);
  gtk_widget_set_visible(g->gain_r,  show_rgbl);
  gtk_widget_set_visible(g->gain_g,  show_rgbl);
  gtk_widget_set_visible(g->gain_b,  show_rgbl);

  gtk_widget_set_visible(g->hue_lift,  show_hsl);
  gtk_widget_set_visible(g->sat_lift,  show_hsl);
  gtk_widget_set_visible(g->hue_gamma, show_hsl);
  gtk_widget_set_visible(g->sat_gamma, show_hsl);
  gtk_widget_set_visible(g->hue_gain,  show_hsl);
  gtk_widget_set_visible(g->sat_gain,  show_hsl);

  gtk_widget_set_visible(g->optimizer_box, mode == SLOPE_OFFSET_POWER);
}

void gui_update(dt_iop_module_t *self)
{
  dt_iop_color_picker_reset(self, TRUE);
  _check_tuner_picker_labels(self);

  gui_changed(self, NULL, NULL);
}

void gui_reset(dt_iop_module_t *self)
{
  dt_iop_colorbalance_gui_data_t *g = self->gui_data;

  for(int k=0; k<LEVELS; k++)
  {
    g->color_patches_flags[k] = INVALID;
    g->luma_patches_flags[k] = INVALID;
  }
  _check_tuner_picker_labels(self);

  dt_bauhaus_combobox_set(g->controls, HSL);

  set_visible_widgets(g);

  dt_iop_color_picker_reset(self, TRUE);
}

static void _configure_slider_blocks(gpointer instance, dt_iop_module_t *self);

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

  if(!w || w == g->mode)
  {
    set_visible_widgets(g);
    _configure_slider_blocks(NULL, self);
  }

  ++darktable.gui->reset;

  if(!w || w == g->lift_r  || w == g->lift_g  || w == g->lift_b)
    set_HSL_sliders(g->hue_lift, g->sat_lift, p->lift);
  if(!w || w == g->gamma_r || w == g->gamma_g || w == g->gamma_b)
    set_HSL_sliders(g->hue_gamma, g->sat_gamma, p->gamma);
  if(!w || w == g->gain_r  || w == g->gain_g  || w == g->gain_b)
    set_HSL_sliders(g->hue_gain, g->sat_gain, p->gain);

  --darktable.gui->reset;
}

static void controls_callback(GtkWidget *combo, dt_iop_module_t *self)
{
  if(darktable.gui->reset) return;

  dt_iop_colorbalance_gui_data_t *g = self->gui_data;

  set_visible_widgets(g);

  dt_iop_color_picker_reset(self, TRUE);
}

#ifdef SHOW_COLOR_WHEELS
static gboolean dt_iop_area_draw(GtkWidget *widget, cairo_t *cr, dt_iop_module_t *self)
{
  float flt_bg = 0.5;
  if(gtk_widget_get_state_flags(widget) & GTK_STATE_FLAG_SELECTED) flt_bg = 0.6;
  float flt_dark = flt_bg / 1.5, flt_light = flt_bg * 1.5;

  uint32_t bg = ((255 << 24) | ((int)floor(flt_bg * 255 + 0.5) << 16) | ((int)floor(flt_bg * 255 + 0.5) << 8)
                 | (int)floor(flt_bg * 255 + 0.5));
  // bg = 0xffffffff;
  //   uint32_t dark = ((255 << 24) |
  //                  ((int)floor(flt_dark * 255 + 0.5) << 16) |
  //                  ((int)floor(flt_dark * 255 + 0.5) << 8) |
  //                  (int)floor(flt_dark * 255 + 0.5));
  uint32_t light = ((255 << 24) | ((int)floor(flt_light * 255 + 0.5) << 16)
                    | ((int)floor(flt_light * 255 + 0.5) << 8) | (int)floor(flt_light * 255 + 0.5));

  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  int width = allocation.width, height = allocation.height;
  if(width % 2 == 0) width--;
  if(height % 2 == 0) height--;
  double center_x = (float)width / 2.0, center_y = (float)height / 2.0;
  double diameter = MIN(width, height) - 4;
  double r_outside = diameter / 2.0, r_inside = r_outside * 0.87;
  double r_outside_2 = r_outside * r_outside, r_inside_2 = r_inside * r_inside;

  // clear the background
  cairo_set_source_rgb(cr, flt_bg, flt_bg, flt_bg);
  cairo_paint(cr);

  /* Create an image initialized with the ring colors */
  gint stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, width);
  guint32 *buf = malloc(sizeof(guint32) * height * stride / 4);

  for(int y = 0; y < height; y++)
  {
    guint32 *p = buf + y * width;

    double dy = -(y + 0.5 - center_y);

    for(int x = 0; x < width; x++)
    {
      double dx = x + 0.5 - center_x;
      double dist = dx * dx + dy * dy;
      if(dist < r_inside_2 || dist > r_outside_2)
      {
        uint32_t col = bg;
        if((abs(dx) < 1 && abs(dy) < 3) || (abs(dx) < 3 && abs(dy) < 1)) col = light;
        *p++ = col;
        continue;
      }

      double angle = atan2(dy, dx) - M_PI_2;
      if(angle < 0.0) angle += 2.0 * M_PI;

      double hue = angle / (2.0 * M_PI);

      dt_aligned_pixel_t rgb;
      hsl2rgb(rgb, hue, 1.0, 0.5);

      *p++ = (((int)floor(rgb[0] * 255 + 0.5) << 16) | ((int)floor(rgb[1] * 255 + 0.5) << 8)
              | (int)floor(rgb[2] * 255 + 0.5));
    }
  }

  cairo_surface_t *source
      = cairo_image_surface_create_for_data((unsigned char *)buf, CAIRO_FORMAT_RGB24, width, height, stride);

  cairo_set_source_surface(cr, source, 0.0, 0.0);
  cairo_paint(cr);
  free(buf);

  // draw border
  float line_width = 1;
  cairo_set_line_width(cr, line_width);

  cairo_set_source_rgb(cr, flt_bg, flt_bg, flt_bg);
  cairo_new_path(cr);
  cairo_arc(cr, center_x, center_y, r_outside, 0.0, 2.0 * M_PI);
  cairo_stroke(cr);
  cairo_arc(cr, center_x, center_y, r_inside, 0.0, 2.0 * M_PI);
  cairo_stroke(cr);

  cairo_set_source_rgb(cr, flt_dark, flt_dark, flt_dark);
  cairo_new_path(cr);
  cairo_arc(cr, center_x, center_y, r_outside, M_PI, 1.5 * M_PI);
  cairo_stroke(cr);
  cairo_arc(cr, center_x, center_y, r_inside, 0.0, 0.5 * M_PI);
  cairo_stroke(cr);

  cairo_set_source_rgb(cr, flt_light, flt_light, flt_light);
  cairo_new_path(cr);
  cairo_arc(cr, center_x, center_y, r_outside, 0.0, 0.5 * M_PI);
  cairo_stroke(cr);
  cairo_arc(cr, center_x, center_y, r_inside, M_PI, 1.5 * M_PI);
  cairo_stroke(cr);

  // draw selector
  double r = 255 / 255.0, g = 155 / 255.0, b = 40 / 255.0;
  double h, s, v;

  gtk_rgb_to_hsv(r, g, b, &h, &s, &v);

  cairo_save(cr);
  cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 0.7);

  cairo_translate(cr, center_x, center_y);
  cairo_rotate(cr, h * 2.0 * M_PI - M_PI_2);

  cairo_arc(cr, r_inside * v, 0.0, 3.0, 0, 2.0 * M_PI);
  cairo_stroke(cr);

  cairo_restore(cr);

  cairo_surface_destroy(source);

  return TRUE;
}
#endif

static void _configure_slider_blocks(gpointer instance, dt_iop_module_t *self)
{
  dt_iop_colorbalance_params_t *p = self->params;
  dt_iop_colorbalance_gui_data_t *g = self->gui_data;

  GtkWidget *new_container = NULL;
  GtkWidget *old_container = gtk_bin_get_child(GTK_BIN(g->main_box));

  for(int i=0; i<3; i++)
  {
    g_object_ref(G_OBJECT(g->blocks[i]));
    if(old_container) gtk_container_remove(GTK_CONTAINER(old_container), g->blocks[i]);
  }

  if(old_container) gtk_widget_destroy(old_container);

  const gchar *short_label_ops[] = { C_("color", "offset"), C_("color", "power"), C_("color", "slope") };
  const gchar *short_label_lgg[] = { C_("color", "lift"), C_("color", "gamma"), C_("color", "gain") };
  const gchar **short_label = (p->mode == SLOPE_OFFSET_POWER) ? short_label_ops : short_label_lgg;
  const gchar *long_label[]
    = { NC_("section", "shadows: lift / offset"),
        NC_("section", "mid-tones: gamma / power"),
        NC_("section", "highlights: gain / slope") };

  gchar *layout = dt_conf_get_string("plugins/darkroom/colorbalance/layout");

  if(!g_strcmp0(layout, "list"))
  {
    new_container = dt_gui_vbox();

    for(int i=0; i<3; i++)
    {
      if(i == 0)
        gtk_label_set_text(GTK_LABEL(g->main_label), Q_(long_label[0]));
      else
      {
        GtkWidget *label = dt_ui_section_label_new(Q_(long_label[i]));
        gtk_container_add(GTK_CONTAINER(new_container), label);
        if(old_container) gtk_widget_show(label);
      }

      gtk_container_add(GTK_CONTAINER(new_container), g->blocks[i]);
    }
  }
  else
  {
    gtk_label_set_text(GTK_LABEL(g->main_label), _("shadows / mid-tones / highlights"));

    GtkWidget *label[3];
    for(int i=0; i<3; i++)
    {
      label[i] = gtk_label_new(_(short_label[i]));
      gtk_widget_set_tooltip_text(label[i], _(long_label[i]));
      gtk_label_set_ellipsize(GTK_LABEL(label[i]), PANGO_ELLIPSIZE_END);
      gtk_widget_set_hexpand(label[i], TRUE);
    }

    if(!g_strcmp0(layout, "columns"))
    {
      new_container = gtk_grid_new();

      gtk_grid_set_column_homogeneous(GTK_GRID(new_container), TRUE);
      gtk_grid_set_column_spacing(GTK_GRID(new_container), 8);

      for(int i=0; i<3; i++)
      {
        dt_gui_add_class(label[i], "dt_section_label");

        gtk_container_add(GTK_CONTAINER(new_container), label[i]);
        if(old_container) gtk_widget_show(label[i]);
        gtk_grid_attach_next_to(GTK_GRID(new_container), g->blocks[i], label[i], GTK_POS_BOTTOM, 1, 1);
      }
    }
    else
    {
      new_container = gtk_notebook_new();

      for(int i=0; i<3; i++) gtk_notebook_append_page(GTK_NOTEBOOK(new_container), g->blocks[i], label[i]);
    }
  }

  g_free(layout);

  for(int i=0; i<3; i++) g_object_unref(G_OBJECT(g->blocks[i]));

  gtk_container_add(GTK_CONTAINER(g->main_box), new_container);
  if(old_container) gtk_widget_show(new_container);
}

static void _cycle_layout_callback(GtkWidget *label, GdkEventButton *event, dt_iop_module_t *self)
{
  gchar *layout = dt_conf_get_string("plugins/darkroom/colorbalance/layout");

  dt_conf_set_string("plugins/darkroom/colorbalance/layout",
                     !g_strcmp0(layout, "columns") ? "tabs" :
                     !g_strcmp0(layout, "list") ? "columns" : "list");

  g_free(layout);

  _configure_slider_blocks(NULL, self);
}

#define HSL_CALLBACK(which)                                                             \
static void which##_callback(GtkWidget *slider, dt_iop_module_t *self)                  \
{                                                                                       \
  dt_iop_colorbalance_params_t *p = self->params;       \
  dt_iop_colorbalance_gui_data_t *g = self->gui_data; \
                                                                                        \
  if(darktable.gui->reset) return;                                                      \
                                                                                        \
  dt_iop_color_picker_reset(self, TRUE);                                                \
                                                                                        \
  float hsl[3] = {dt_bauhaus_slider_get(g->hue_##which) / 360.0f,                       \
                  dt_bauhaus_slider_get(g->sat_##which) / 100.0f,                       \
                  0.5f};                                                                \
                                                                                        \
  if(slider == g->hue_##which)                                                          \
    update_saturation_slider_color(g->sat_##which, hsl[0]);                             \
  set_RGB_sliders(g->which##_r, g->which##_g, g->which##_b, hsl, p->which, p->mode);    \
  dt_dev_add_history_item(darktable.develop, self, TRUE);                               \
}

HSL_CALLBACK(lift)
HSL_CALLBACK(gamma)
HSL_CALLBACK(gain)

void gui_init(dt_iop_module_t *self)
{
  dt_iop_colorbalance_gui_data_t *g = IOP_GUI_ALLOC(colorbalance);

  g->mode = NULL;

  for(int k=0; k<LEVELS; k++)
  {
    g->color_patches_flags[k] = INVALID;
    g->luma_patches_flags[k] = INVALID;
  }

  GtkWidget *mode_box = self->widget = dt_gui_vbox();

  // mode choice
  g->mode = dt_bauhaus_combobox_from_params(self, N_("mode"));
  gtk_widget_set_tooltip_text(g->mode, _("color-grading mapping method"));

  // control choice
  g->controls = dt_bauhaus_combobox_new(self);
  dt_bauhaus_widget_set_label(g->controls, NULL, N_("color control sliders"));
  dt_bauhaus_combobox_add(g->controls, _("HSL"));
  dt_bauhaus_combobox_add(g->controls, _("RGBL"));
  dt_bauhaus_combobox_add(g->controls, _("both"));
  dt_gui_box_add(self->widget, g->controls);
  gtk_widget_set_tooltip_text(g->controls, _("color-grading mapping method"));
  g_signal_connect(G_OBJECT(g->controls), "value-changed", G_CALLBACK(controls_callback), self);

  const char *mode = dt_conf_get_string_const("plugins/darkroom/colorbalance/controls");
  dt_bauhaus_combobox_set(g->controls, !g_strcmp0(mode, "RGBL") ? RGBL :
                                       !g_strcmp0(mode, "BOTH") ? BOTH : HSL);

  g->master_box = self->widget = dt_gui_vbox(dt_ui_section_label_new(C_("section", "master")));

  g->saturation = dt_bauhaus_slider_from_params(self, "saturation");
  dt_bauhaus_slider_set_soft_range(g->saturation, 0.5f, 1.5f);
  dt_bauhaus_slider_set_digits(g->saturation, 4);
  dt_bauhaus_slider_set_format(g->saturation, "%");
  gtk_widget_set_tooltip_text(g->saturation, _("saturation correction before the color balance"));

  g->saturation_out = dt_bauhaus_slider_from_params(self, "saturation_out");
  dt_bauhaus_slider_set_soft_range(g->saturation_out, 0.5f, 1.5f);
  dt_bauhaus_slider_set_digits(g->saturation_out, 4);
  dt_bauhaus_slider_set_format(g->saturation_out, "%");
  gtk_widget_set_tooltip_text(g->saturation_out, _("saturation correction after the color balance"));

  g->grey = dt_color_picker_new(self, DT_COLOR_PICKER_AREA,
            dt_bauhaus_slider_from_params(self, "grey"));
  dt_bauhaus_slider_set_format(g->grey, "%");
  gtk_widget_set_tooltip_text(g->grey, _("adjust to match a neutral tone"));

  g->contrast = dt_bauhaus_slider_from_params(self, N_("contrast"));
  dt_bauhaus_slider_set_soft_range(g->contrast, 0.5f, 1.5f);
  dt_bauhaus_slider_set_digits(g->contrast, 4);
  dt_bauhaus_slider_set_factor(g->contrast, -100.0f);
  dt_bauhaus_slider_set_offset(g->contrast, 100.0f);
  dt_bauhaus_slider_set_format(g->contrast, "%");
  gtk_widget_set_tooltip_text(g->contrast, _("contrast"));

  g->main_label = dt_ui_section_label_new(""); // is set in _configure_slider_blocks
  gtk_widget_set_tooltip_text(g->main_label, _("click to cycle layout"));
  GtkWidget *main_label_box = gtk_event_box_new();
  gtk_container_add(GTK_CONTAINER(main_label_box), g->main_label);
  g_signal_connect(G_OBJECT(main_label_box), "button-release-event", G_CALLBACK(_cycle_layout_callback), self);

  g->main_box = gtk_event_box_new(); // is filled in _configure_slider_blocks

  char field_name[10];

#define ADD_CHANNEL(which, section, c, n, N, text, span)                    \
  sprintf(field_name, "%s[%d]", #which, CHANNEL_##N);                       \
  g->which##_##c = dt_bauhaus_slider_from_params(self, field_name);         \
  dt_bauhaus_slider_set_soft_range(g->which##_##c, -span+1.0, span+1.0);    \
  dt_bauhaus_slider_set_digits(g->which##_##c, 5);                          \
  dt_bauhaus_slider_set_offset(g->which##_##c, -1.0);                       \
  dt_bauhaus_slider_set_feedback(g->which##_##c, 0);                        \
  gtk_widget_set_tooltip_text(g->which##_##c, _(text[CHANNEL_##N]));        \
  dt_bauhaus_widget_set_label(g->which##_##c, section, #n);                 \

#define ADD_BLOCK(blk, which, section, text, span, satspan)                 \
  g->blocks[blk] = self->widget = dt_gui_vbox();                            \
                                                                            \
  sprintf(field_name, "%s[%d]", #which, CHANNEL_FACTOR);                    \
  g->which##_factor = dt_color_picker_new(self, DT_COLOR_PICKER_AREA,       \
                      dt_bauhaus_slider_from_params(self, field_name));     \
  dt_bauhaus_slider_set_soft_range(g->which##_factor, -span+1.0, span+1.0); \
  dt_bauhaus_slider_set_digits(g->which##_factor, 4);                       \
  dt_bauhaus_slider_set_factor(g->which##_factor, 100.0);                   \
  dt_bauhaus_slider_set_offset(g->which##_factor, - 100.0);                 \
  dt_bauhaus_slider_set_format(g->which##_factor,"%");                      \
  dt_bauhaus_slider_set_feedback(g->which##_factor, 0);                     \
  dt_bauhaus_slider_set_stop(g->which##_factor, 0.0, 0.0, 0.0, 0.0);        \
  dt_bauhaus_slider_set_stop(g->which##_factor, 1.0, 1.0, 1.0, 1.0);        \
  gtk_widget_set_tooltip_text(g->which##_factor, _(text[CHANNEL_FACTOR]));  \
  dt_bauhaus_widget_set_label(g->which##_factor, section, N_("factor"));    \
                                                                            \
  g->hue_##which = dt_color_picker_new(self, DT_COLOR_PICKER_AREA,          \
                   dt_bauhaus_slider_new_with_range_and_feedback(self,      \
                   0.0f, 360.0f, 0, 0.0f, 2, 0));                           \
  dt_bauhaus_widget_set_label(g->hue_##which, section, N_("hue"));          \
  dt_bauhaus_slider_set_format(g->hue_##which, "°");                        \
  dt_bauhaus_slider_set_stop(g->hue_##which, 0.0f,   1.0f, 0.0f, 0.0f);     \
  dt_bauhaus_slider_set_stop(g->hue_##which, 0.166f, 1.0f, 1.0f, 0.0f);     \
  dt_bauhaus_slider_set_stop(g->hue_##which, 0.322f, 0.0f, 1.0f, 0.0f);     \
  dt_bauhaus_slider_set_stop(g->hue_##which, 0.498f, 0.0f, 1.0f, 1.0f);     \
  dt_bauhaus_slider_set_stop(g->hue_##which, 0.664f, 0.0f, 0.0f, 1.0f);     \
  dt_bauhaus_slider_set_stop(g->hue_##which, 0.830f, 1.0f, 0.0f, 1.0f);     \
  dt_bauhaus_slider_set_stop(g->hue_##which, 1.0f,   1.0f, 0.0f, 0.0f);     \
  gtk_widget_set_tooltip_text(g->hue_##which, _("select the hue"));         \
  g_signal_connect(G_OBJECT(g->hue_##which), "value-changed",               \
                   G_CALLBACK(which##_callback), self);                     \
                                                                            \
  g->sat_##which = dt_bauhaus_slider_new_with_range_and_feedback(self,      \
                   0.0f, 100.0f, 0, 0.0f, 2, 0);                            \
  dt_bauhaus_slider_set_soft_max(g->sat_##which, satspan);                  \
  dt_bauhaus_widget_set_label(g->sat_##which, section, N_("saturation"));   \
  dt_bauhaus_slider_set_format(g->sat_##which, "%");                        \
  dt_bauhaus_slider_set_stop(g->sat_##which, 0.0f, 0.2f, 0.2f, 0.2f);       \
  dt_bauhaus_slider_set_stop(g->sat_##which, 1.0f, 1.0f, 1.0f, 1.0f);       \
  gtk_widget_set_tooltip_text(g->sat_##which, _("select the saturation"));  \
  g_signal_connect(G_OBJECT(g->sat_##which), "value-changed",               \
                   G_CALLBACK(which##_callback), self);                     \
                                                                            \
  dt_gui_box_add(self->widget, g->hue_##which, g->sat_##which);             \
                                                                            \
  ADD_CHANNEL(which, section, r, red, RED, text, span)                      \
  dt_bauhaus_slider_set_stop(g->which##_r, 0.0, 0.0, 1.0, 1.0);             \
  dt_bauhaus_slider_set_stop(g->which##_r, 0.5, 1.0, 1.0, 1.0);             \
  dt_bauhaus_slider_set_stop(g->which##_r, 1.0, 1.0, 0.0, 0.0);             \
  ADD_CHANNEL(which, section, g, green, GREEN, text, span)                  \
  dt_bauhaus_slider_set_stop(g->which##_g, 0.0, 1.0, 0.0, 1.0);             \
  dt_bauhaus_slider_set_stop(g->which##_g, 0.5, 1.0, 1.0, 1.0);             \
  dt_bauhaus_slider_set_stop(g->which##_g, 1.0, 0.0, 1.0, 0.0);             \
  ADD_CHANNEL(which, section, b, blue, BLUE, text, span)                    \
  dt_bauhaus_slider_set_stop(g->which##_b, 0.0, 1.0, 1.0, 0.0);             \
  dt_bauhaus_slider_set_stop(g->which##_b, 0.5, 1.0, 1.0, 1.0);             \
  dt_bauhaus_slider_set_stop(g->which##_b, 1.0, 0.0, 0.0, 1.0);             \

  static const char *lift_messages[]
    = { N_("factor of lift/offset"),
        N_("factor of red for lift/offset"),
        N_("factor of green for lift/offset"),
        N_("factor of blue for lift/offset") };

  static const char *gamma_messages[]
    = { N_("factor of gamma/power"),
        N_("factor of red for gamma/power"),
        N_("factor of green for gamma/power"),
        N_("factor of blue for gamma/power") };

  static const char *gain_messages[]
    = { N_("factor of gain/slope"),
        N_("factor of red for gain/slope"),
        N_("factor of green for gain/slope"),
        N_("factor of blue for gain/slope") };

  ADD_BLOCK(0, lift,  N_("shadows"), lift_messages, 0.05f,  5.0f)
  ADD_BLOCK(1, gamma, N_("mid-tones"), gamma_messages, 0.5f, 20.0f)
  ADD_BLOCK(2, gain,  N_("highlights"), gain_messages,  0.5f, 25.0f)
  _configure_slider_blocks(NULL, self);

  g->auto_luma = dt_color_picker_new(self, DT_COLOR_PICKER_AREA,
                 dt_bauhaus_combobox_new(self));
  dt_bauhaus_widget_set_label(g->auto_luma, NULL, N_("optimize luma"));
  gtk_widget_set_tooltip_text(g->auto_luma, _("fit the whole histogram and center the average luma"));

  g->auto_color = dt_color_picker_new(self, DT_COLOR_PICKER_AREA,
                  dt_bauhaus_combobox_new(self));
  dt_bauhaus_widget_set_label(g->auto_color, NULL, N_("neutralize colors"));
  gtk_widget_set_tooltip_text(g->auto_color, _("optimize the RGB curves to remove color casts"));

  g->optimizer_box = dt_gui_vbox(dt_ui_section_label_new(C_("section", "auto optimizers")),
                                 g->auto_luma, g->auto_color);

  // start building top level widget
  self->widget = dt_gui_vbox(mode_box,
                             g->master_box,
                             main_label_box,
                             g->main_box,
                             g->optimizer_box);

  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_PREFERENCES_CHANGE, _configure_slider_blocks);
}

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