/*
    This file is part of darktable,
    Copyright (C) 2017-2025 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/>.
*/

/*
    This module implements automatic single-image haze removal as
    described by K. He et al. in

    * Kaiming He, Jian Sun, and Xiaoou Tang, "Single Image Haze
      Removal Using Dark Channel Prior," IEEE Transactions on Pattern
      Analysis and Machine Intelligence, vol. 33, no. 12,
      pp. 2341-2353, Dec. 2011. DOI: 10.1109/TPAMI.2010.168

    * K. He, J. Sun, and X. Tang, "Guided Image Filtering," Lecture
      Notes in Computer Science, pp. 1-14, 2010. DOI:
      10.1007/978-3-642-15549-9_1
*/


#include "bauhaus/bauhaus.h"
#include "common/box_filters.h"
#include "common/darktable.h"
#include "common/guided_filter.h"
#include "develop/imageop.h"
#include "develop/imageop_math.h"
#include "develop/imageop_gui.h"
#include "gui/gtk.h"
#include "gui/accelerators.h"
#include "develop/tiling.h"
#include "iop/iop_api.h"

#ifdef HAVE_OPENCL
#include "common/opencl.h"
#endif

#include <float.h>
#include <gtk/gtk.h>
#include <math.h>
#include <stdlib.h>
#include <string.h>

//----------------------------------------------------------------------
// implement the module api
//----------------------------------------------------------------------

DT_MODULE_INTROSPECTION(3, dt_iop_hazeremoval_params_t)

typedef dt_aligned_pixel_t rgb_pixel;

typedef struct dt_iop_hazeremoval_params_t
{
  float strength; // $MIN: -1.0 $MAX: 1.0 $DEFAULT: 0.2
  float distance; // $MIN:  0.0 $MAX: 1.0 $DEFAULT: 0.2
  gboolean compatibility_mode; // $DEFAULT: FALSE
  gboolean adaptive; // $DEFAULT: TRUE
} dt_iop_hazeremoval_params_t;

// types  dt_iop_hazeremoval_params_t and dt_iop_hazeremoval_data_t are
// equal, thus no commit_params function needs to be implemented
typedef dt_iop_hazeremoval_params_t dt_iop_hazeremoval_data_t;

typedef struct dt_iop_hazeremoval_gui_data_t
{
  GtkWidget *strength;
  GtkWidget *distance;
  rgb_pixel A0;
  float distance_max;
  dt_hash_t hash;
} dt_iop_hazeremoval_gui_data_t;

typedef struct dt_iop_hazeremoval_global_data_t
{
  int kernel_hazeremoval_transision_map;
  int kernel_hazeremoval_box_min_x;
  int kernel_hazeremoval_box_min_y;
  int kernel_hazeremoval_box_max_x;
  int kernel_hazeremoval_box_max_y;
  int kernel_hazeremoval_dehaze;
} dt_iop_hazeremoval_global_data_t;


const char *name()
{
  return _("haze removal");
}


const char *aliases()
{
  return _("dehaze|defog|smoke|smog");
}

const char **description(dt_iop_module_t *self)
{
  return dt_iop_set_description(self, _("remove fog and atmospheric hazing from images"),
                                      _("corrective"),
                                      _("linear, RGB, scene-referred"),
                                      _("frequential, RGB"),
                                      _("linear, RGB, scene-referred"));
}

int flags()
{
  return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_SUPPORTS_BLENDING;
}


int default_group()
{
  return IOP_GROUP_CORRECT | IOP_GROUP_TECHNICAL;
}


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


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

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)
{
  if(old_version == 1)
  {
    typedef struct dt_iop_hazeremoval_params_v1_t
    {
      float strength;
      float distance;
    } dt_iop_hazeremoval_params_v1_t;
    const dt_iop_hazeremoval_params_v1_t *o = old_params;

    dt_iop_hazeremoval_params_t *n = malloc(sizeof(dt_iop_hazeremoval_params_t));
    memcpy(n, o, sizeof(dt_iop_hazeremoval_params_v1_t));

    n->compatibility_mode = TRUE;
    n->adaptive = FALSE;

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

  if(old_version == 2)
  {
    typedef struct dt_iop_hazeremoval_params_v2_t
    {
      float strength;
      float distance;
      gboolean compatibility_mode;
    } dt_iop_hazeremoval_params_v2_t;
    const dt_iop_hazeremoval_params_v2_t *o = old_params;

    dt_iop_hazeremoval_params_t *n = malloc(sizeof(dt_iop_hazeremoval_params_t));
    memcpy(n, o, sizeof(dt_iop_hazeremoval_params_v2_t));

    n->adaptive = FALSE;
    *new_params = n;
    *new_params_size = sizeof(dt_iop_hazeremoval_params_t);
    *new_version = 3;
    return 0;
  }

  return 1;
}

void init_global(dt_iop_module_so_t *self)
{
  dt_iop_hazeremoval_global_data_t *gd = malloc(sizeof(*gd));
  const int program = 27; // hazeremoval.cl, from programs.conf
  gd->kernel_hazeremoval_transision_map =
    dt_opencl_create_kernel(program, "hazeremoval_transision_map");
  gd->kernel_hazeremoval_box_min_x =
    dt_opencl_create_kernel(program, "hazeremoval_box_min_x");
  gd->kernel_hazeremoval_box_min_y =
    dt_opencl_create_kernel(program, "hazeremoval_box_min_y");
  gd->kernel_hazeremoval_box_max_x =
    dt_opencl_create_kernel(program, "hazeremoval_box_max_x");
  gd->kernel_hazeremoval_box_max_y =
    dt_opencl_create_kernel(program, "hazeremoval_box_max_y");
  gd->kernel_hazeremoval_dehaze =
    dt_opencl_create_kernel(program, "hazeremoval_dehaze");
  self->data = gd;
}


void cleanup_global(dt_iop_module_so_t *self)
{
  dt_iop_hazeremoval_global_data_t *gd = self->data;
  dt_opencl_free_kernel(gd->kernel_hazeremoval_transision_map);
  dt_opencl_free_kernel(gd->kernel_hazeremoval_box_min_x);
  dt_opencl_free_kernel(gd->kernel_hazeremoval_box_min_y);
  dt_opencl_free_kernel(gd->kernel_hazeremoval_box_max_x);
  dt_opencl_free_kernel(gd->kernel_hazeremoval_box_max_y);
  dt_opencl_free_kernel(gd->kernel_hazeremoval_dehaze);
  free(self->data);
  self->data = NULL;
}


void gui_update(dt_iop_module_t *self)
{
  dt_iop_hazeremoval_gui_data_t *g = self->gui_data;

  dt_iop_gui_enter_critical_section(self);
  g->distance_max = NAN;
  g->A0[0] = NAN;
  g->A0[1] = NAN;
  g->A0[2] = NAN;
  g->hash = DT_INVALID_HASH;
  dt_iop_gui_leave_critical_section(self);
}

void gui_changed(dt_iop_module_t *self,
                 GtkWidget *w,
                 void *previous)
{
  dt_iop_hazeremoval_params_t *p = self->params;
  if(w)
  {
    p->compatibility_mode = FALSE;
    p->adaptive = TRUE;
  }
}

void gui_init(dt_iop_module_t *self)
{
  dt_iop_hazeremoval_gui_data_t *g = IOP_GUI_ALLOC(hazeremoval);

  g->distance_max = NAN;
  g->A0[0] = NAN;
  g->A0[1] = NAN;
  g->A0[2] = NAN;
  g->hash = DT_INVALID_HASH;

  g->strength = dt_bauhaus_slider_from_params(self, N_("strength"));
  gtk_widget_set_tooltip_text(g->strength, _("amount of haze reduction"));

  g->distance = dt_bauhaus_slider_from_params(self, N_("distance"));
  dt_bauhaus_slider_set_digits(g->distance, 3);
  gtk_widget_set_tooltip_text(g->distance,
                              _("limit haze removal up to a specific spatial depth"));
}


//----------------------------------------------------------------------
// module local functions and structures required by process function
//----------------------------------------------------------------------

typedef struct tile
{
  int left, right, lower, upper;
} tile;


typedef struct rgb_image
{
  float *data;
  int width, height, stride;
} rgb_image;


typedef struct const_rgb_image
{
  const float *data;
  int width, height, stride;
} const_rgb_image;

// swap the two floats that the pointers point to
static inline void _pointer_swap_f(float *a, float *b)
{
  float t = *a;
  *a = *b;
  *b = t;
}

// calculate the dark channel (minimal color component over a box of
// size (2*w+1) x (2*w+1) )
static void _dark_channel(const const_rgb_image img1,
                          const gray_image img2,
                          const int w)
{
  const size_t size = (size_t)img1.height * img1.width;
  const float *const restrict in_data = img1.data;
  float *const restrict out_data = img2.data;
  DT_OMP_FOR_SIMD(aligned(in_data, out_data: 64))
  for(size_t i = 0; i < size; i++)
  {
    const float *pixel = in_data + 4*i;
    float m = MIN(MIN(pixel[0], pixel[1]),pixel[2]);
    out_data[i] = m;
  }
  dt_box_min(img2.data, img2.height, img2.width, 1, w);
}


// calculate the transition map
static void _transition_map(const const_rgb_image img1,
                            const gray_image img2,
                            const int w,
                            const float *const A0,
                            const float strength)
{
  const size_t size = (size_t)img1.height * img1.width;
  const float *const restrict in_data = img1.data;
  float *const restrict out_data = img2.data;
  const dt_aligned_pixel_t A0_inv = { 1.0f / A0[0], 1.0f / A0[1], 1.0f / A0[2], 1.0f };
  DT_OMP_FOR_SIMD(aligned(in_data, out_data: 64))

  for(size_t i = 0; i < size; i++)
  {
    const float *pixel = in_data + 4*i;
    const float m = MIN(MIN(pixel[0] * A0_inv[0], pixel[1] * A0_inv[1]),
                        pixel[2] * A0_inv[2]);
    out_data[i] = 1.f - m * strength;
  }
  dt_box_max(img2.data, img2.height, img2.width, 1, w);
}


// partition the array [first, last) using the pivot value val, i.e.,
// reorder the elements in the range [first, last) in such a way that
// all elements that are less than the pivot precede the elements
// which are larger or equal the pivot
static float *_partition(float *first,
                         float *last,
                         const float val,
                         const gboolean compatibility_mode)
{
  if(compatibility_mode)
  {
    for(; first != last; ++first)
    {
      if(!((*first) < val)) break;
    }
    if(first == last) return first;
    for(float *i = first + 1; i != last; ++i)
    {
      if((*i) < val)
      {
        _pointer_swap_f(i, first);
        ++first;
      }
    }
    return first;
  }
  last++;
  while(TRUE)
  {
    // scan from start until we find an element which needs to be swapped to the second half
    do
    {
      first++;
    } while(first < last && *first < val);
    // scan from end until we find an element which needs to be swapped to the first half
    do
    {
      last--;
    } while(first < last && *last > val);
    // if we didn't find anything to swap, return that position as the partitioning location
    if(first >= last)
      return first;
    // swap the out-of-place elements
    _pointer_swap_f(first, last);
  }
}

// quick select algorithm, arranges the range [first, last) such that
// the element pointed to by nth is the same as the element that would
// be in that position if the entire range [first, last) had been
// sorted, additionally, none of the elements in the range [nth, last)
// is less than any of the elements in the range [first, nth)
void _quick_select(float *first,
                   float *nth,
                   float *last,
                   const gboolean compatibility_mode)
{
  if(first == last) return;
  for(;last > first+1;)
  {
    // select pivot by median of three heuristic for better performance
    float *p1 = first;
    float *p3 = compatibility_mode ? first + (last - first) / 2 : nth;
    float *pivot = last - 1; // put median in last to avoid additional swap

    if(!(*p1 < *pivot))
      _pointer_swap_f(p1, pivot);
    if(!(*p1 < *p3))
      _pointer_swap_f(p1, p3);
    if(!(*pivot < *p3))
      _pointer_swap_f(pivot, p3);

    float *new_pivot = _partition(first, last - 1, last[-1], compatibility_mode);
    if(compatibility_mode)
      pivot = p3; // old code simply assumed pivot would end up in middle
    else
      pivot = new_pivot;

    _pointer_swap_f(last - 1, pivot); // move pivot to its final place
    if(nth == pivot)
      break;
    else if(nth < pivot)
      last = pivot;
    else
      first = pivot + 1;
  }
}

// calculate diffusive ambient light and the maximal depth in the image
// depth is estimated by the local amount of haze and given in units of the
// characteristic haze depth, i.e., the distance over which object light is
// reduced by the factor exp(-1)
static float _ambient_light(const const_rgb_image img,
                            const int w1,
                            rgb_pixel *pA0,
                            const gboolean compatibility_mode)
{
  const float dark_channel_quantil = 0.95f; // quantil for determining the most hazy pixels
  const float bright_quantil = 0.95f; // quantil for determining the
                                      // brightest pixels among the
                                      // most hazy pixels
  const int width = img.width;
  const int height = img.height;
  const size_t size = (size_t)width * height;
  // calculate dark channel, which is an estimate for local amount of haze
  gray_image dark_ch = new_gray_image(width, height);
  _dark_channel(img, dark_ch, w1);
  // determine the brightest pixels among the most hazy pixels
  gray_image bright_hazy = new_gray_image(width, height);
  // first determine the most hazy pixels
  copy_gray_image(dark_ch, bright_hazy);
  float *const restrict hazy_data = bright_hazy.data;
  size_t p = (size_t)(size * dark_channel_quantil);
  _quick_select(hazy_data, hazy_data + p, hazy_data + size, compatibility_mode);
  const float crit_haze_level = hazy_data[p];
  const float *const restrict img_data = img.data;
  const float *const restrict dark_data = dark_ch.data;
  size_t N_most_hazy_start = size/2;
  size_t N_most_hazy_end = size/2;
  DT_OMP_PRAGMA(parallel num_threads(2) default(firstprivate) \
                shared(N_most_hazy_start, N_most_hazy_end))
  DT_OMP_PRAGMA(sections)
  {
  for(size_t i = 0; i < size/2; i++)
    if(dark_data[i] >= crit_haze_level)
    {
      const float *pixel_in = img_data + 4*i;
      // The next line prevents full parallelization via OpenMP.  But we can use
      // two threads by growing outward from the center
      hazy_data[--N_most_hazy_start] = pixel_in[0] + pixel_in[1] + pixel_in[2];
    }
  DT_OMP_PRAGMA(section)
  for(size_t i = size/2; i < size; i++)
    if(dark_data[i] >= crit_haze_level)
    {
      const float *pixel_in = img_data + 4*i;
      // next line prevents full parallelization via OpenMP
      hazy_data[N_most_hazy_end++] = pixel_in[0] + pixel_in[1] + pixel_in[2];
    }
  }
  if(compatibility_mode)
  {
    // for backwards compatibility with the original broken
    // quick_select, we need to put all of the items in hazy_data in
    // the order in which they appear in the original image.  Our
    // first loop above put them in reverse order, so un-reverse.
    const size_t start = N_most_hazy_start;
    const size_t end = size/2;
    const size_t midpoint = start + (end-start)/2;
    for(size_t i = start; i < midpoint; i++)
    {
      const float tmp = hazy_data[i];
      hazy_data[i] = hazy_data[(end-1) - (i-start)];
      hazy_data[(end-1) - (i-start)] = tmp;
    }
  }
  size_t N_most_hazy = N_most_hazy_end - N_most_hazy_start;
  p = (size_t)(N_most_hazy * bright_quantil) + N_most_hazy_start;
  _quick_select(hazy_data + N_most_hazy_start,
                hazy_data + p,
                hazy_data + N_most_hazy_end,
                compatibility_mode);
  const float crit_brightness = hazy_data[p];
  free_gray_image(&bright_hazy);
  // average over the brightest pixels among the most hazy pixels to
  // estimate the diffusive ambient light
  dt_aligned_pixel_t A0 = { 0.0f, 0.0f, 0.0f, 0.0f };
  size_t N_bright_hazy = 0;
  const float *const restrict data = dark_ch.data;
  const float *const restrict in_data = img.data;
  DT_OMP_FOR(reduction(+ : A0[0:4]) reduction(+ : N_bright_hazy))
  for(size_t i = 0; i < size; i++)
  {
    const float *pixel_in = in_data + 4*i;
    if((data[i] >= crit_haze_level)
       && (pixel_in[0] + pixel_in[1] + pixel_in[2] >= crit_brightness))
    {
      for_each_channel(c,aligned(pixel_in))
        A0[c] += pixel_in[c];
      N_bright_hazy++;
    }
  }
  if(N_bright_hazy > 0)
  {
    for_each_channel(c)
      A0[c] /= N_bright_hazy;
  }
  (*pA0)[0] = A0[0];
  (*pA0)[1] = A0[1];
  (*pA0)[2] = A0[2];
  free_gray_image(&dark_ch);
  // for almost haze free images it may happen that crit_haze_level=0, this means
  // there is a very large image depth, in this case a large number is returned, that
  // is small enough to avoid overflow in later processing
  // the critical haze level is at dark_channel_quantil (not 100%) to be insensitive
  // to extreme outliners, compensate for that by some factor slightly larger than
  // unity when calculating the maximal image depth
  return crit_haze_level > 0
    ? -1.125f * logf(crit_haze_level)
    : logf(FLT_MAX) / 2; // return the maximal depth
}

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_hazeremoval_gui_data_t *const g = self->gui_data;
  dt_iop_hazeremoval_params_t *d = piece->data;

  const int width = roi_in->width;
  const int height = roi_in->height;
  const size_t size = (size_t)width * height;
  const float wscale = d->adaptive ? CLIP((float)roi_in->scale / (float)piece->pipe->iscale) : 1.0f;
  /*  To make the darkroom experience and data taken from preview pipe (not only internal but also pickers ...)
      more consistent we modify the windowing sizes (positive integers) according to the pipe scaling.
      w1: to determine the dark channel and the transition map
      w2: for the guided filter
  */
  const int w1 = 2 + (int)ceilf(4.0f * wscale);
  const int w2 = 3 + (int)ceilf(6.0f * wscale);

  // module parameters
  const float strength = d->strength; // strength of haze removal
  const float distance = d->distance; // maximal distance from camera to remove haze
  const float eps = sqrtf(0.025f);    // regularization parameter for guided filter
  const gboolean compatibility_mode = d->compatibility_mode;
  const gboolean gui = self->dev->gui_attached && g;
  const gboolean fullpipes = dt_pipe_is_canvas(piece->pipe);
  const gboolean hq = darktable.develop->late_scaling.enabled;
  const gboolean fullhq = hq && dt_pipe_is_full(piece->pipe);

  /*  max distance and A0 for ambient light are stored and kept for the opther pipes by the preview pipe.
      If we run in HQ mode let's keep the fullpipe data too for slightly improved results.
  */
  const gboolean storing = gui && (dt_pipe_is_preview(piece->pipe) || fullhq);

  const float *const restrict in = (float*)ivoid;
  float *const restrict out = (float*)ovoid;
  const const_rgb_image img_in = (const_rgb_image){ in, width, height, 4 };

  /*  max distance and A0 for ambient light are stored and kept for the opther pipes by the preview pipe.
      If we run in HQ mode let's keep the fullpipe data too for slightly improved results.
  */
  rgb_pixel A0 = { NAN, NAN, NAN, 0.0f };
  float distance_max = NAN;

  /*  hazeremoval module needs the color and the haziness (which yields distance_max) of the most hazy
      region of the image.
      In the full pixelpipes without using HQ processing mode we only see the roi_in area and thus we
      can't get reliable data.
      Therefore, we try to get A0 and distance_max from the PREVIEW pixelpipe.
  */
  if(gui && fullpipes && !hq)
  {
    dt_iop_gui_enter_critical_section(self);
    const dt_hash_t hash = g->hash;
    dt_iop_gui_leave_critical_section(self);
    // Note that the case 'hash == DT_INVALID_HASH' on first invocation in a session
    // implies that g->distance_max is NAN, which initiates special
    // handling below to avoid inconsistent results.  In all other
    // cases we make sure that the preview pipe has left us with
    // proper readings for distance_max and A0.  If data are not yet
    // there we need to wait (with timeout).
    if(hash != DT_INVALID_HASH
       && !dt_dev_sync_pixelpipe_hash(self->dev, piece->pipe, self->iop_order,
                                      DT_DEV_TRANSFORM_DIR_BACK_INCL,
                                      &self->gui_lock, &g->hash))
      dt_control_log(_("inconsistent output"));
    dt_iop_gui_enter_critical_section(self);
    A0[0] = g->A0[0];
    A0[1] = g->A0[1];
    A0[2] = g->A0[2];
    distance_max = g->distance_max;
    dt_iop_gui_leave_critical_section(self);
  }

  // As we can't receive data from preview we at least leave a note to the user
  if(dt_pipe_is_image(piece->pipe) && !hq)
    dt_control_log(_("inconsistent output"));

  // In all other cases we calculate distance_max and A0 here.
  if(dt_isnan(distance_max))
    distance_max = _ambient_light(img_in, w1, &A0, compatibility_mode);

  if(storing)
  {
    dt_hash_t hash = dt_dev_hash_plus(self->dev, piece->pipe,
                                      self->iop_order, DT_DEV_TRANSFORM_DIR_BACK_INCL);
    dt_iop_gui_enter_critical_section(self);
    g->A0[0] = A0[0];
    g->A0[1] = A0[1];
    g->A0[2] = A0[2];
    g->distance_max = distance_max;
    g->hash = hash;
    dt_iop_gui_leave_critical_section(self);
    if(distance_max <= 0.0f)
      dt_control_log(_("haze removal could not calculate ambient light due to image content"));
  }

  // calculate the transition map
  gray_image trans_map = new_gray_image(width, height);
  _transition_map(img_in, trans_map, w1, A0, strength);

  // refine the transition map
  dt_box_min(trans_map.data, trans_map.height, trans_map.width, 1, w1);
  gray_image trans_map_filtered = new_gray_image(width, height);
  // apply guided filter with no clipping
  guided_filter(img_in.data, trans_map.data, trans_map_filtered.data,
                width, height, 4, w2, eps, 1.f, -FLT_MAX, FLT_MAX);

  // finally, calculate the haze-free image, minimum allowed value for transition map
  const float t_min = CLAMP(expf(-distance * distance_max), 1.0f / 1024.0f, 1.0f);

  dt_print_pipe(DT_DEBUG_VERBOSE,
      "ambient data", piece->pipe, self, DT_DEVICE_CPU, roi_in, roi_out,
      "tmin=%.4f distance_max=%.4f A0=%.4f %.4f %.4f", t_min, distance_max, A0[0], A0[1], A0[2]);

  const dt_aligned_pixel_t c_A0 = { A0[0], A0[1], A0[2], A0[3] };
  const gray_image c_trans_map_filtered = trans_map_filtered;
  DT_OMP_FOR()
  for(size_t i = 0; i < size; i++)
  {
    const float t = MAX(c_trans_map_filtered.data[i], t_min);
    dt_aligned_pixel_t res;
    for_each_channel(c, aligned(in))
      res[c] =  (in[4*i + c] - c_A0[c]) / t + c_A0[c];
    copy_pixel_nontemporal(out + 4*i, res);
  }
  dt_omploop_sfence();

  free_gray_image(&trans_map);
  free_gray_image(&trans_map_filtered);
}

#ifdef HAVE_OPENCL

// calculate diffusive ambient light and the maximal depth in the image
// depth is estimated by the local amount of haze and given in units of the
// characteristic haze depth, i.e., the distance over which object light is
// reduced by the factor exp(-1)
// some parts of the calculation are not suitable for a parallel implementation,
// thus we copy data to host memory fall back to a cpu routine
static float _ambient_light_cl(dt_iop_module_t *self,
                               const int devid,
                               cl_mem img,
                               const int w1,
                               rgb_pixel *pA0,
                               const gboolean compatibility_mode)
{
  const int width = dt_opencl_get_image_width(img);
  const int height = dt_opencl_get_image_height(img);
  const int element_size = dt_opencl_get_image_element_size(img);
  float *in = dt_alloc_aligned((size_t)width * height * element_size);
  cl_int err = dt_opencl_copy_image_to_host(devid, in, img, width, height, element_size);
  if(err != CL_SUCCESS) goto error;

  const const_rgb_image img_in = (const_rgb_image) {in, width, height, element_size / sizeof(float)};
  const float max_depth = _ambient_light(img_in, w1, pA0, compatibility_mode);
  dt_free_align(in);
  return max_depth;
error:
  dt_print(DT_DEBUG_OPENCL, "[hazeremoval, ambient_light_cl] unknown error: %d", err);
  dt_free_align(in);
  return 0.f;
}

static int _box_min_cl(dt_iop_module_t *self,
                       int devid,
                       cl_mem in,
                       cl_mem out,
                       const int w)
{
  dt_iop_hazeremoval_global_data_t *gd = self->global_data;
  const int width = dt_opencl_get_image_width(in);
  const int height = dt_opencl_get_image_height(in);
  cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;

  cl_mem temp = dt_opencl_alloc_device(devid, width, height, sizeof(float));
  if(temp == NULL) goto error;

  err = dt_opencl_enqueue_kernel_1d_args(devid, gd->kernel_hazeremoval_box_min_x, height,
              CLARG(width), CLARG(height), CLARG(in), CLARG(temp), CLARG(w));
  if(err != CL_SUCCESS) goto error;

  err = dt_opencl_enqueue_kernel_1d_args(devid, gd->kernel_hazeremoval_box_min_y, width,
              CLARG(width), CLARG(height), CLARG(temp), CLARG(out), CLARG(w));

error:
  dt_opencl_release_mem_object(temp);
  return err;
}

static int _box_max_cl(dt_iop_module_t *self,
                       int devid,
                       cl_mem in,
                       cl_mem out,
                       const int w)
{
  dt_iop_hazeremoval_global_data_t *gd = self->global_data;
  const int width = dt_opencl_get_image_width(in);
  const int height = dt_opencl_get_image_height(in);
  cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
  cl_mem temp = dt_opencl_alloc_device(devid, width, height, (int)sizeof(float));
  if(temp == NULL) goto error;

  err = dt_opencl_enqueue_kernel_1d_args(devid, gd->kernel_hazeremoval_box_max_x, height,
            CLARG(width), CLARG(height), CLARG(in), CLARG(temp), CLARG(w));
  if(err != CL_SUCCESS) goto error;

  err = dt_opencl_enqueue_kernel_1d_args(devid, gd->kernel_hazeremoval_box_max_y, width,
            CLARG(width), CLARG(height), CLARG(temp), CLARG(out), CLARG(w));
error:
  dt_opencl_release_mem_object(temp);
  return err;
}


static int _transition_map_cl(dt_iop_module_t *self,
                              int devid,
                              cl_mem img1,
                              cl_mem img2,
                              const int w1,
                              const float strength,
                              const float *const A0)
{
  dt_iop_hazeremoval_global_data_t *gd = self->global_data;
  const int width = dt_opencl_get_image_width(img1);
  const int height = dt_opencl_get_image_height(img1);

  cl_int err = dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_hazeremoval_transision_map, width, height,
                                                CLARG(width), CLARG(height),
                                                CLARG(img1), CLARG(img2), CLARG(strength),
                                                CLARG(A0[0]), CLARG(A0[1]), CLARG(A0[2]));
  if(err != CL_SUCCESS)
    return err;

  return _box_max_cl(self, devid, img2, img2, w1);
}


static int _dehaze_cl(dt_iop_module_t *self,
                      int devid,
                      cl_mem img_in,
                      cl_mem trans_map,
                      cl_mem img_out,
                      const float t_min,
                      const float *const A)
{
  dt_iop_hazeremoval_global_data_t *gd = self->global_data;
  const int width = dt_opencl_get_image_width(img_in);
  const int height = dt_opencl_get_image_height(img_in);

  const dt_aligned_pixel_t A0 = { A[0], A[1], A[2], A[3]};
  return dt_opencl_enqueue_kernel_2d_args(devid, gd->kernel_hazeremoval_dehaze, width, height,
                                          CLARG(width), CLARG(height),
                                          CLARG(img_in), CLARG(trans_map),
                                          CLARG(img_out), CLARG(t_min), CLARG(A0));
}

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)
{
  tiling->factor = 2.5f;  // in + out + two single-channel temp buffers
  tiling->factor_cl = 5.0f;
  tiling->maxbuf = 1.0f;
  tiling->maxbuf_cl = 1.0f;
  tiling->overhead = 0;
  tiling->overlap = 0;
  tiling->align = 1;
}

int process_cl(dt_iop_module_t *self,
               dt_dev_pixelpipe_iop_t *piece,
               cl_mem img_in,
               cl_mem img_out,
               const dt_iop_roi_t *const roi_in,
               const dt_iop_roi_t *const roi_out)
{
  dt_iop_hazeremoval_gui_data_t *const g = (dt_iop_hazeremoval_gui_data_t*)self->gui_data;
  dt_iop_hazeremoval_params_t *d = piece->data;

  const int devid = piece->pipe->devid;
  const int width = roi_in->width;
  const int height = roi_in->height;
  const float wscale = d->adaptive ? CLIP((float)roi_in->scale / (float)piece->pipe->iscale) : 1.0f;
  /*  To make the darkroom experience and data taken from preview pipe (not only internal but also pickers ...)
      more consistent we modify the windowing sizes (positive integers) according to the pipe scaling.
      w1: to determine the dark channel and the transition map
      w2: for the guided filter
  */
  const int w1 = 2 + (int)ceilf(4.0f * wscale);
  const int w2 = 3 + (int)ceilf(6.0f * wscale);

  // module parameters
  const float strength = d->strength; // strength of haze removal
  const float distance = d->distance; // maximal distance from camera to remove haze
  const float eps = sqrtf(0.025f);    // regularization parameter for guided filter
  const gboolean compatibility_mode = d->compatibility_mode;
  const gboolean gui = self->dev->gui_attached && g;
  const gboolean fullpipes = dt_pipe_is_canvas(piece->pipe);
  const gboolean hq = darktable.develop->late_scaling.enabled;
  const gboolean fullhq = hq && dt_pipe_is_full(piece->pipe);
  /*  max distance and A0 for ambient light are stored and kept for the opther pipes by the preview pipe.
      If we run in HQ mode let's keep the fullpipe data too for slightly improved results.
  */
  const gboolean storing = gui && (dt_pipe_is_preview(piece->pipe) || fullhq);
  rgb_pixel A0 = { NAN, NAN, NAN, 0.0f };
  float distance_max = NAN;

  /*  hazeremoval module needs the color and the haziness (which yields distance_max) of the most hazy
      region of the image.
      In the full pixelpipes without using HQ processing mode we only see the roi_in area and thus we
      can't get reliable data.
      Therefore, we try to get A0 and distance_max from the PREVIEW pixelpipe.
  */
  if(gui && fullpipes && !hq)
  {
    dt_iop_gui_enter_critical_section(self);
    const dt_hash_t hash = g->hash;
    dt_iop_gui_leave_critical_section(self);
    // Note that the case 'hash == DT_INVALID_HASH' on first invocation in a session
    // implies that g->distance_max is NAN, which initiates special
    // handling below to avoid inconsistent results.  In all other
    // cases we make sure that the preview pipe has left us with
    // proper readings for distance_max and A0.  If data are not yet
    // there we need to wait (with timeout).
    if(hash != DT_INVALID_HASH
       && !dt_dev_sync_pixelpipe_hash(self->dev, piece->pipe,
                                      self->iop_order, DT_DEV_TRANSFORM_DIR_BACK_INCL,
                                      &self->gui_lock, &g->hash))
    {
      dt_control_log(_("inconsistent output"));
    }

    dt_iop_gui_enter_critical_section(self);
    A0[0] = g->A0[0];
    A0[1] = g->A0[1];
    A0[2] = g->A0[2];
    distance_max = g->distance_max;
    dt_iop_gui_leave_critical_section(self);
  }

  // As we can't receive data from preview we at least leave a note to the user
  if(dt_pipe_is_image(piece->pipe) && !hq)
    dt_control_log(_("inconsistent output"));

  // In all other cases we calculate distance_max and A0 here.
  if(dt_isnan(distance_max))
    distance_max = _ambient_light_cl(self, devid, img_in, w1, &A0, compatibility_mode);

  if(storing)
  {
    dt_hash_t hash = dt_dev_hash_plus(self->dev, piece->pipe,
                                      self->iop_order, DT_DEV_TRANSFORM_DIR_BACK_INCL);
    dt_iop_gui_enter_critical_section(self);
    g->A0[0] = A0[0];
    g->A0[1] = A0[1];
    g->A0[2] = A0[2];
    g->distance_max = distance_max;
    g->hash = hash;
    dt_iop_gui_leave_critical_section(self);
    if(distance_max <= 0.0f)
      dt_control_log(_("haze removal could not calculate ambient light due to image content"));
  }

  cl_mem trans_map = NULL;
  cl_mem trans_map_filtered = NULL;
  cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE;

  // calculate the transition map
  trans_map = dt_opencl_alloc_device(devid, width, height, (int)sizeof(float));
  if(trans_map == NULL) goto error;

  err = _transition_map_cl(self, devid, img_in, trans_map, w1, strength, A0);
  if(err != CL_SUCCESS) goto error;

  // refine the transition map
  err = _box_min_cl(self, devid, trans_map, trans_map, w1);
  if(err != CL_SUCCESS) goto error;

  trans_map_filtered = dt_opencl_alloc_device(devid, width, height, (int)sizeof(float));
  if(trans_map_filtered == NULL)
  {
    err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
    goto error;
  }

  // apply guided filter with no clipping
  err = guided_filter_cl(devid, img_in, trans_map, trans_map_filtered,
                         width, height, 4, w2, eps, 1.f, -CL_FLT_MAX, CL_FLT_MAX);
  if(err != CL_SUCCESS) goto error;

  // finally, calculate the haze-free image
  const float t_min = CLAMP(expf(-distance * distance_max), 1.0f / 1024.0f, 1.0f);

  dt_print_pipe(DT_DEBUG_VERBOSE,
      "ambient data", piece->pipe, self, devid, roi_in, roi_out,
      "tmin=%.4f distance_max=%.4f A0=%.4f %.4f %.4f", t_min, distance_max, A0[0], A0[1], A0[2]);

  err = _dehaze_cl(self, devid, img_in, trans_map_filtered, img_out, t_min, A0);

error:
  dt_opencl_release_mem_object(trans_map);
  dt_opencl_release_mem_object(trans_map_filtered);
  return err;
}
#endif

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