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

#include "bauhaus/bauhaus.h"
#include "common/imagebuf.h"
#include "common/colorspaces.h"
#include "common/colorspaces_inline_conversions.h"
#include "common/file_location.h"
#include "common/iop_profile.h"
#include "develop/imageop.h"
#include "develop/imageop_gui.h"
#include "dtgtk/button.h"
#include "gui/gtk.h"
#include "gui/accelerators.h"
#include "imageio/imageio_png.h"
#include "iop/iop_api.h"

#include <gtk/gtk.h>
#include <libgen.h>
#include <png.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dirent.h>
#if defined (_WIN32)
#include "win/getdelim.h"
#include "win/scandir.h"
#endif // defined (_WIN32)

DT_MODULE_INTROSPECTION(3, dt_iop_lut3d_params_t)

#define DT_IOP_LUT3D_MAX_PATHNAME 512
#define DT_IOP_LUT3D_MAX_LUTNAME 128
#define DT_IOP_LUT3D_CLUT_LEVEL 48
#define DT_IOP_LUT3D_MAX_KEYPOINTS 2048

typedef enum dt_iop_lut3d_colorspace_t
{
  DT_IOP_SRGB = 0,    // $DESCRIPTION: "sRGB"
  DT_IOP_ARGB,        // $DESCRIPTION: "Adobe RGB"
  DT_IOP_REC709,      // $DESCRIPTION: "gamma Rec709 RGB"
  DT_IOP_LIN_REC709,  // $DESCRIPTION: "linear Rec709 RGB"
  DT_IOP_LIN_REC2020, // $DESCRIPTION: "linear Rec2020 RGB"
  DT_IOP_LIN_PROPHOTO,// $DESCRIPTION: "linear ProPhoto RGB"
} dt_iop_lut3d_colorspace_t;

typedef enum dt_iop_lut3d_interpolation_t
{
  DT_IOP_TETRAHEDRAL = 0, // $DESCRIPTION: "tetrahedral"
  DT_IOP_TRILINEAR = 1,   // $DESCRIPTION: "trilinear"
  DT_IOP_PYRAMID = 2,     // $DESCRIPTION: "pyramid"
} dt_iop_lut3d_interpolation_t;

typedef struct dt_iop_lut3d_params_t
{
  char filepath[DT_IOP_LUT3D_MAX_PATHNAME];
  dt_iop_lut3d_colorspace_t colorspace; // $DEFAULT: DT_IOP_SRGB $DESCRIPTION: "application color space"
  dt_iop_lut3d_interpolation_t interpolation; // $DEFAULT: DT_IOP_TETRAHEDRAL
  int nb_keypoints; // $DEFAULT: 0 >0 indicates the presence of compressed lut
  char c_clut[DT_IOP_LUT3D_MAX_KEYPOINTS*2*3];
  char lutname[DT_IOP_LUT3D_MAX_LUTNAME];
} dt_iop_lut3d_params_t;

typedef struct dt_iop_lut3d_gui_data_t
{
  GtkWidget *button;
  GtkWidget *filepath;
  GtkWidget *colorspace;
  GtkWidget *interpolation;
#ifdef HAVE_GMIC
  GtkWidget *lutentry;
  GtkWidget *lutname;
  GtkWidget *lutwindow;
  gulong lutname_handler_id;
#endif
} dt_iop_lut3d_gui_data_t;

typedef enum dt_lut3d_cols_t
{
  DT_LUT3D_COL_NAME = 0,
  DT_LUT3D_COL_VISIBLE,
  DT_LUT3D_NUM_COLS
} dt_lut3d_cols_t;

const char invalid_filepath_prefix[] = "INVALID >> ";

typedef struct dt_iop_lut3d_data_t
{
  dt_iop_lut3d_params_t params;
  float *clut;  // cube lut pointer
  uint16_t level; // cube_size
} dt_iop_lut3d_data_t;

typedef struct dt_iop_lut3d_global_data_t
{
  int kernel_lut3d_tetrahedral;
  int kernel_lut3d_trilinear;
  int kernel_lut3d_pyramid;
  int kernel_lut3d_none;
} dt_iop_lut3d_global_data_t;

#ifdef HAVE_GMIC
void lut3d_decompress_clut(const unsigned char *const input_keypoints, const unsigned int nb_input_keypoints,
                    const unsigned int output_resolution, float *const output_clut_data,
                    const char *const filename);

unsigned int lut3d_get_cached_clut(float *const output_clut_data, const unsigned int output_resolution,
                    const char *const filename);

gboolean lut3d_read_gmz(int *const nb_keypoints, unsigned char *const keypoints, const char *const filename,
              int *const nb_lut, void *g, const char *const lutname, const gboolean newlutname);

#endif // HAVE_GMIC

const char *name()
{
  return _("LUT 3D");
}

const char **description(dt_iop_module_t *self)
{
  return dt_iop_set_description(self, _("perform color space corrections and apply look"),
                                      _("corrective or creative"),
                                      _("linear, RGB, display-referred"),
                                      _("defined by profile, RGB"),
                                      _("linear or non-linear, RGB, display-referred"));
}

int flags()
{
  return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_SUPPORTS_BLENDING;
}

int default_group()
{
  return IOP_GROUP_COLOR | 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;
}

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_lut3d_params_v3_t
  {
    char filepath[DT_IOP_LUT3D_MAX_PATHNAME];
    dt_iop_lut3d_colorspace_t colorspace;
    dt_iop_lut3d_interpolation_t interpolation;
    int nb_keypoints;
    char c_clut[DT_IOP_LUT3D_MAX_KEYPOINTS*2*3];
    char lutname[DT_IOP_LUT3D_MAX_LUTNAME];
  } dt_iop_lut3d_params_v3_t;

  if(old_version == 1)
  {
    typedef struct dt_iop_lut3d_params_v1_t
    {
      char filepath[DT_IOP_LUT3D_MAX_PATHNAME];
      int colorspace;
      int interpolation;
    } dt_iop_lut3d_params_v1_t;

    const dt_iop_lut3d_params_v1_t *o = (dt_iop_lut3d_params_v1_t *)old_params;
    dt_iop_lut3d_params_v3_t *n = malloc(sizeof(dt_iop_lut3d_params_v3_t));
    g_strlcpy(n->filepath, o->filepath, sizeof(n->filepath));
    n->colorspace = o->colorspace;
    n->interpolation = o->interpolation;
    n->nb_keypoints = 0;
    memset(&n->c_clut, 0, sizeof(n->c_clut));
    memset(&n->lutname, 0, sizeof(n->lutname));

    *new_params = n;
    *new_params_size = sizeof(dt_iop_lut3d_params_v3_t);
    *new_version = 3;
    return 0;
  }
  if(old_version == 2)
  {
    typedef struct dt_iop_lut3d_params_v2_t
    {
      char filepath[DT_IOP_LUT3D_MAX_PATHNAME];
      int colorspace;
      int interpolation;
      int nb_keypoints; // >0 indicates the presence of compressed lut
      char c_clut[DT_IOP_LUT3D_MAX_KEYPOINTS*2*3];
      char lutname[DT_IOP_LUT3D_MAX_LUTNAME];
      uint32_t gmic_version;
    } dt_iop_lut3d_params_v2_t;

    const dt_iop_lut3d_params_v2_t *o = (dt_iop_lut3d_params_v2_t *)old_params;
    dt_iop_lut3d_params_v3_t *n = malloc(sizeof(dt_iop_lut3d_params_v3_t));
    memcpy(n, o, sizeof(dt_iop_lut3d_params_v3_t)); // v3 is smaller

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

  return 1;
}

// From `HaldCLUT_correct.c' by Eskil Steenberg (http://www.quelsolaar.com) (BSD licensed)
static void _correct_pixel_trilinear(const float *const in,
                                     float *const out,
                                     const size_t pixel_nb,
                                     const float *const restrict clut,
                                     const uint16_t level)
{
  const int level_minus_2 = (level - 2);
  const size_t level2 = level * level;
  const float flevel_1 = (float)(level - 1);
  const size_t level1_stride = 3 * level;
  const size_t level2_stride = 3 * level2;
  const size_t level12_stride = 3 * (level + level2);

  DT_OMP_FOR()
  for(size_t k = 0; k < (size_t)(pixel_nb * 4); k+=4)
  {
    const float *const input = in + k;
    float *const output = ((float *const)out) + k;

    DT_ALIGNED_PIXEL int rgbi[4];
    dt_aligned_pixel_t rgbd;
    dt_aligned_pixel_t tmp1, tmp2, tmp3;

    // scale the input according to grid size
    for_each_channel(c, aligned(input))
      rgbd[c] = CLIP(input[c]) * flevel_1;
    // quantize to grid
    for_each_channel(c)
      rgbi[c] = (int)rgbd[c];
    for_each_channel(c)
      rgbi[c] = CLAMP(rgbi[c], 0, level_minus_2);
    // compute deltas for each channel
    for_each_channel(c)
      rgbd[c] -= rgbi[c]; // delta red/green/blue

    size_t color = rgbi[0] + level * rgbi[1] + level2 * rgbi[2];

    const size_t i = color * 3;  // P000

    const float one_minus_rgbd0 = 1.0f - rgbd[0];
    const float one_minus_rgbd1 = 1.0f - rgbd[1];

    // process indexes of P000 to P111 in clut
    for_each_channel(c) // P000 and P100
      tmp1[c] = clut[i+c] * one_minus_rgbd0 + clut[i+3+c] * rgbd[0];

    for_each_channel(c) // P010 and P110
      tmp2[c] = clut[i+level1_stride+c] * one_minus_rgbd0 + clut[i+level1_stride+3+c] * rgbd[0];

    for_each_channel(c) // blend P000/P100 with P010/P110
      tmp3[c] = tmp1[c] * one_minus_rgbd1 + tmp2[c] * rgbd[1];

    for_each_channel(c) // P001 and P101
      tmp1[c] = clut[i+level2_stride+c] * one_minus_rgbd0 + clut[i+level2_stride+3+c] * rgbd[0];

    for_each_channel(c) // P011 and P111
      tmp2[c] = clut[i+level12_stride+c] * one_minus_rgbd0 + clut[i+level12_stride+3+c] * rgbd[0];

    for_each_channel(c) // blend P001/P101 and P011/P111
      tmp1[c] = tmp1[c] * one_minus_rgbd1 + tmp2[c] * rgbd[1];

    for_each_channel(c, aligned(output))
      output[c] = tmp3[c] * (1.0f - rgbd[2]) + tmp1[c] * rgbd[2];
    // not using non-temporal writes here, as those are substantially slower when in==out....
    // (which is the case when performing a colorspace conversion)
 }
}

// from OpenColorIO
// https://github.com/imageworks/OpenColorIO/blob/master/src/OpenColorIO/ops/Lut3D/Lut3DOp.cpp
static void _correct_pixel_tetrahedral(const float *const in,
                                       float *const out,
                                       const size_t pixel_nb,
                                       const float *const restrict clut,
                                       const uint16_t level)
{
  const size_t level2 = level * level;
  const size_t level1_stride = 3 * level;
  const size_t level2_stride = 3 * level2;
  const size_t level12_stride = 3 * (level + level2);
  const float flevel_1 = (float)(level - 1);

  DT_OMP_FOR()
  for(size_t k = 0; k < (size_t)(pixel_nb * 4); k+=4)
  {
    const float *const input = in + k;
    float *const output = ((float *const)out) + k;

    dt_aligned_pixel_t rgbi;
    dt_aligned_pixel_t rgbd;
    for_each_channel(c)
      rgbd[c] = CLIP(input[c]) * flevel_1;

    for_each_channel(c)
    {
      rgbi[c] = CLAMP((int)rgbd[c], 0, level - 2);
      rgbd[c] = rgbd[c] - rgbi[c]; // delta red/green/blue
    }

  // indexes of P000 to P111 in clut
    const size_t color = rgbi[0] + rgbi[1] * level + rgbi[2] * level2;
    const size_t i000 = color * 3;                     // P000
    const size_t i100 = i000 + 3;                      // P100
    const size_t i010 = i000 + level1_stride;          // P010
    const size_t i110 = i010 + 3;                      // P110
    const size_t i001 = i000 + level2_stride;          // P001
    const size_t i101 = i001 + 3;                      // P101
    const size_t i011 = i000 + level12_stride;         // P011
    const size_t i111 = i011 + 3;                      // P111

    if(rgbd[0] > rgbd[1])
    {
      if(rgbd[1] > rgbd[2])
      {
        // rgbd[0] > rgbd[1] > rgbd[2]
        for_each_channel(c, aligned(output))
          output[c] = ((1-rgbd[0])*clut[i000+c] + (rgbd[0]-rgbd[1])*clut[i100+c]
                      + (rgbd[1]-rgbd[2])*clut[i110+c] + rgbd[2]*clut[i111+c]);
      }
      else if(rgbd[0] > rgbd[2])
      {
        // rgbd[0] > rgbd[2] >= rgbd[1]
        for_each_channel(c, aligned(output))
          output[c] = ((1-rgbd[0])*clut[i000+c] + (rgbd[0]-rgbd[2])*clut[i100+c]
                      + (rgbd[2]-rgbd[1])*clut[i101+c] + rgbd[1]*clut[i111+c]);
      }
      else
      {
        // rgbd[2] >= rgbd[0] > rgbd[2]
        for_each_channel(c, aligned(output))
          output[c] = ((1-rgbd[2])*clut[i000+c] + (rgbd[2]-rgbd[0])*clut[i001+c]
                      + (rgbd[0]-rgbd[1])*clut[i101+c] + rgbd[1]*clut[i111+c]);
      }
    }
    else
    {
      if(rgbd[2] > rgbd[1])
      {
        // rgbd[2] > rgbd[1] >= rgbd[0]
        for_each_channel(c, aligned(output))
          output[c] = ((1-rgbd[2])*clut[i000+c] + (rgbd[2]-rgbd[1])*clut[i001+c]
                      + (rgbd[1]-rgbd[0])*clut[i011+c] + rgbd[0]*clut[i111+c]);
      }
      else if(rgbd[2] > rgbd[0])
      {
        // rgbd[1] >= rgbd[2] > rgbd[0]
        for_each_channel(c, aligned(output))
          output[c] = ((1-rgbd[1])*clut[i000+c] + (rgbd[1]-rgbd[2])*clut[i010+c]
                      + (rgbd[2]-rgbd[0])*clut[i011+c] + rgbd[0]*clut[i111+c]);
      }
      else
      {
        // rgbd[1] >= rgbd[0] >= rgbd[2]
        for_each_channel(c, aligned(output))
          output[c] = ((1-rgbd[1])*clut[i000+c] + (rgbd[1]-rgbd[0])*clut[i010+c]
                      + (rgbd[0]-rgbd[2])*clut[i110+c] + rgbd[2]*clut[i111+c]);
      }
    }
    // not using non-temporal writes here, as those are substantially slower when in==out....
    // (which is the case when performing a colorspace conversion)
  }
}

// from Study on the 3D Interpolation Models Used in Color Conversion
// http://ijetch.org/papers/318-T860.pdf
static void _correct_pixel_pyramid(const float *const in,
                                   float *const out,
                                   const size_t pixel_nb,
                                   const float *const restrict clut,
                                   const uint16_t level)
{
  const int level2 = level * level;
  const float flevel_1 = (float)(level - 1);

  DT_OMP_FOR()
  for(size_t k = 0; k < (size_t)(pixel_nb * 4); k+=4)
  {
    const float *const input = in + k;
    float *const output = ((float *const)out) + k;

    DT_ALIGNED_PIXEL int rgbi[4];
    dt_aligned_pixel_t rgbd;
    // scale the input according to grid size
    for_each_channel(c)
      rgbd[c] = CLIP(input[c]) * flevel_1;
    // clip coordinates to LUT grid
    for_each_channel(c)
      rgbi[c] = (int)rgbd[c];
    for_each_channel(c)
      rgbi[c] = CLAMP(rgbi[c], 0, level - 2);
    // compute deltas for each channel
    for_each_channel(c)
      rgbd[c] -= rgbi[c];

  // indexes of P000 to P111 in clut
    const int color = rgbi[0] + rgbi[1] * level + rgbi[2] * level2;
    const size_t i000 = color * 3;                     // P000
    const size_t i100 = i000 + 3;                      // P100
    const size_t i010 = (color + level) * 3;           // P010
    const size_t i110 = i010 + 3;                      // P110
    const size_t i001 = (color + level2) * 3;          // P001
    const size_t i101 = i001 + 3;                      // P101
    const size_t i011 = (color + level + level2) * 3;  // P011
    const size_t i111 = i011 + 3;                      // P111

    dt_aligned_pixel_t outpx;
    if(rgbd[1] > rgbd[0] && rgbd[2] > rgbd[0])
    {
      for_each_channel(c)
        outpx[c] = (clut[i000+c] + (clut[i111+c]-clut[i011+c])*rgbd[0]
                    + (clut[i010+c]-clut[i000+c])*rgbd[1] + (clut[i001+c]-clut[i000+c])*rgbd[2]
                    + (clut[i011+c]-clut[i001+c]-clut[i010+c]+clut[i000+c])*rgbd[1]*rgbd[2]);
    }
    else if(rgbd[0] > rgbd[1] && rgbd[2] > rgbd[1])
    {
      for_each_channel(c)
        outpx[c] = (clut[i000+c] + (clut[i100+c]-clut[i000+c])*rgbd[0]
                    + (clut[i111+c]-clut[i101+c])*rgbd[1] + (clut[i001+c]-clut[i000+c])*rgbd[2]
                    + (clut[i101+c]-clut[i001+c]-clut[i100+c]+clut[i000+c])*rgbd[0]*rgbd[2]);
    }
    else
    {
      for_each_channel(c)
         outpx[c] = clut[i000+c] + (clut[i100+c]-clut[i000+c])*rgbd[0]
                  + (clut[i010+c]-clut[i000+c])*rgbd[1] + (clut[i111+c]-clut[i110+c])*rgbd[2]
                  + (clut[i110+c]-clut[i100+c]-clut[i010+c]+clut[i000+c])*rgbd[0]*rgbd[1];
    }
    // not using non-temporal writes here, as those are substantially slower when in==out....
    // (which is the case when performing a colorspace conversion)
    copy_pixel(output, outpx);
  }
}

#ifdef HAVE_GMIC
static void _get_cache_filename(const char *const lutname, char *const cache_filename)
{
  char *cache_dir = g_build_filename(g_get_user_cache_dir(), "gmic", NULL);
  char *cache_file = g_build_filename(cache_dir, lutname, NULL);
  g_strlcpy(cache_filename, cache_file, DT_IOP_LUT3D_MAX_PATHNAME);
  g_strlcpy(&cache_filename[strlen(cache_filename)], ".cimgz", DT_IOP_LUT3D_MAX_PATHNAME-strlen(cache_file));
  g_free(cache_dir);
  g_free(cache_file);
}

static uint8_t _calculate_clut_compressed(dt_iop_lut3d_params_t *const p,
                                          const char *const filepath, float **clut)
{
  uint8_t level = DT_IOP_LUT3D_CLUT_LEVEL;
  float *lclut;
  lclut = NULL;
  char cache_filename[DT_IOP_LUT3D_MAX_PATHNAME];
  size_t buf_size_lut;

  _get_cache_filename(p->lutname, cache_filename);
  buf_size_lut = (size_t)(level * level * level * 3);
  lclut = dt_alloc_align_float(buf_size_lut);
  if(!lclut)
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] error allocating buffer for gmz LUT");
    dt_control_log(_("error allocating buffer for gmz LUT"));
    level = 0;
  }
  else
  {
    level = lut3d_get_cached_clut(lclut, level, cache_filename);
    if(!level)
    {  //clut not in cache
      char *c_clut = p->c_clut;
      level = DT_IOP_LUT3D_CLUT_LEVEL;
      lut3d_decompress_clut((const unsigned char *const)c_clut, p->nb_keypoints,
        level, lclut, cache_filename);
    }
  }
  *clut = lclut;
  return level;
}
#endif // HAVE_GMIC


static uint16_t _calculate_clut_haldclut(dt_iop_lut3d_params_t *const p,
                                         const char *const filepath, float **clut)
{
  dt_imageio_png_t png;
  if(!dt_imageio_png_read_header(filepath, &png))
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] invalid png file %s", filepath);
    dt_control_log(_("invalid png file %s"), filepath);
    return 0;
  }
  dt_print(DT_DEBUG_DEV, "[lut3d] png: width=%d, height=%d, color_type=%d, bit_depth=%d", png.width,
           png.height, png.color_type, png.bit_depth);
  if(png.bit_depth !=8 && png.bit_depth != 16)
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] png bit depth %d is not supported", png.bit_depth);
    dt_control_log(_("png bit depth %d is not supported"), png.bit_depth);
    fclose(png.f);
    png_destroy_read_struct(&png.png_ptr, &png.info_ptr, NULL);
    return 0;
  }

  // check the file sizes
  uint16_t level = 2;
  while(level * level * level < png.width) ++level;

  if(level * level * level != png.width)
  {
#ifdef HAVE_GMIC
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] invalid level in png file %d %d", level, png.width);
    dt_control_log(_("invalid level in png file %d %d"), level, png.width);
#else
    if(png.height == 2)
    {
      dt_print(DT_DEBUG_ALWAYS, "[lut3d] this darktable build is not compatible with compressed CLUT");
      dt_control_log(_("this darktable build is not compatible with compressed CLUT"));
    }
    else
    {
      dt_print(DT_DEBUG_ALWAYS, "[lut3d] invalid level in png file %d %d", level, png.width);
      dt_control_log(_("invalid level in png file %d %d"), level, png.width);
    }
#endif // HAVE_GMIC
    fclose(png.f);
    png_destroy_read_struct(&png.png_ptr, &png.info_ptr, NULL);
    return 0;
  }

  level *= level;  // to be equivalent to cube level
  if(level > 256)
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] error - LUT 3D size %d > 256", level);
    dt_control_log(_("error - LUT 3D size %d exceeds the maximum supported"), level);
    fclose(png.f);
    png_destroy_read_struct(&png.png_ptr, &png.info_ptr, NULL);
    return 0;
  }
  const size_t buf_size = (size_t)png.height * png_get_rowbytes(png.png_ptr, png.info_ptr);
  dt_print(DT_DEBUG_DEV, "[lut3d] allocating %zu bytes for png file", buf_size);
  uint8_t *buf = NULL;
  buf = dt_alloc_aligned(buf_size);
  if(!buf)
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] error allocating buffer for png LUT");
    dt_control_log(_("error allocating buffer for png LUT"));
    fclose(png.f);
    png_destroy_read_struct(&png.png_ptr, &png.info_ptr, NULL);
    return 0;
  }
  if(!dt_imageio_png_read_image(&png, buf))
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] error - could not read png image `%s'", filepath);
    dt_control_log(_("error - could not read png image %s"), filepath);
    dt_free_align(buf);
    return 0;
  }
  const size_t buf_size_lut = (size_t)png.height * png.height * 3;
  dt_print(DT_DEBUG_DEV, "[lut3d] allocating %zu floats for png LUT - level %d", buf_size_lut, level);
  // as we use for_each_channel() while reading clut data while doing the _correct variants we must over-allocate
  // by one float for channel=3
  float *lclut = dt_alloc_align_float(buf_size_lut+1);
  if(!lclut)
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] error - allocating buffer for png LUT");
    dt_control_log(_("error - allocating buffer for png LUT"));
    dt_free_align(buf);
    return 0;
  }
  // get clut values
  const float norm = 1.0f / (powf(2.f, png.bit_depth) - 1.0f);
  if(png.bit_depth == 8)
  {
    for(size_t i = 0; i < buf_size_lut; ++i)
      lclut[i] = (float)buf[i] * norm;
  }
  else
  {
    for(size_t i = 0; i < buf_size_lut; ++i)
      lclut[i] = (256.0f * (float)buf[2*i] + (float)buf[2*i+1]) * norm;
  }
  lclut[buf_size_lut] = 0.0f; // see comment about over-allocation
  dt_free_align(buf);
  *clut = lclut;
  return level;
}

// provided by @rabauke, atof replaces strtod & sccanf which are locale dependent
static double _dt_atof(const char *str)
{
  if(strncmp(str, "nan", 3) == 0 || strncmp(str, "NAN", 3) == 0)
    return NAN;
  double integral_result = 0;
  double fractional_result = 0;
  double sign = 1;
  if(*str == '+')
  {
    str++;
    sign = +1;
  } else if(*str == '-')
  {
    str++;
    sign = -1;
  }
  if(strncmp(str, "inf", 3) == 0 || strncmp(str, "INF", 3) == 0)
    return sign * INFINITY;
  // search for end of integral part and parse from
  // right to left for numerical stability
  const char * istr_back = str;
  while(*str >= '0' && *str <= '9')
    str++;
  const char * istr_2 = str;
  double imultiplier = 1;
  while(istr_2 != istr_back)
  {
    --istr_2;
    integral_result += (*istr_2 - '0') * imultiplier;
    imultiplier *= 10;
  }
  if(*str == '.')
  {
    str++;
  // search for end of fractional part and parse from
  // right to left for numerical stability
    const char * fstr_back = str;
    while(*str >= '0' && *str <= '9')
      str++;
    const char * fstr_2 = str;
    double fmultiplier = 1;
    while(fstr_2 != fstr_back)
    {
      --fstr_2;
      fractional_result += (*fstr_2 - '0') * fmultiplier;
      fmultiplier *= 10;
    }
    fractional_result /= fmultiplier;
  }
  double result = sign * (integral_result + fractional_result);
  if(*str == 'e' || *str == 'E')
  {
    str++;
    double power_sign = 1;
    if(*str == '+')
    {
      str++;
      power_sign = +1;
    }
    else if(*str == '-')
    {
      str++;
      power_sign = -1;
    }
    double power = 0;
    while(*str >= '0' && *str <= '9')
    {
      power *= 10;
      power += *str - '0';
      str++;
    }
    if(power_sign > 0)
      result *= pow(10, power);
    else
      result /= pow(10, power);
  }
  return result;
}

// return max 3 tokens from the line (separator = ' ' and token length = 50)
// if nb tokens > 3, the 3rd one captures the last input
static uint8_t _parse_cube_line(char *line, char (*token)[50])
{
  const int max_token_len = 50;
  uint8_t i = 0;
  uint8_t c = 0;
  char *t = &token[0][0];
  char *l = line;

  while(*l != 0 && i < max_token_len)
  {
    if(*l == '#' || *l == '\n' || *l == '\r')
    { // end of useful part of the line
      if(i > 0)
      {
        *t = 0;
        c++;
        return c;
      }
      else
      {
        *t = 0;
        return c;
      }
    }
    if(*l == ' ' || *l == '\t')
    { // separator
      if(i > 0)
      {
        *t = 0;
        c++;
        i = 0;
        t = &token[c > 2 ? 2 : c][0];
      }
    }
    else
    { // capture info
      *t = *l;
      t++;
      i++;
    }
    l++;
    // sometimes the last lf is missing
    if(*l == 0)
    {
      *t = 0;
      c++;
      return c;
    }
  }
  token[0][max_token_len - 1] = 0;
  token[1][max_token_len - 1] = 0;
  token[2][max_token_len - 1] = 0;
  return c;
}

static uint16_t _calculate_clut_cube(const char *const filepath, float **clut)
{
  char *line = NULL;
  size_t len = 0;
  ssize_t read;
  char token[3][50];
  uint16_t level = 0;
  float *lclut = NULL;
  uint32_t i = 0;
  size_t buf_size = 0;
  uint32_t out_of_range_nb = 0;

  FILE *cube_file = g_fopen(filepath, "r");

  if(!cube_file)
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] invalid cube file: %s", filepath);
    dt_control_log(_("error - invalid cube file: %s"), filepath);
    return 0;
  }
  while((read = getline(&line, &len, cube_file)) != -1)
  {
    const uint8_t nb_token = _parse_cube_line(line, token);
    if(nb_token)
    {
      if(token[0][0] == 'T') continue;
      else if(strcmp("DOMAIN_MIN", token[0]) == 0)
      {
        if(strtod(token[1], NULL) != 0.0f)
        {
          dt_print(DT_DEBUG_ALWAYS, "[lut3d] DOMAIN MIN other than 0 is not supported");
          dt_control_log(_("DOMAIN MIN other than 0 is not supported"));
          dt_free_align(lclut);
          free(line);
          fclose(cube_file);
          return 0;
        }
      }
      else if(strcmp("DOMAIN_MAX", token[0]) == 0)
      {
        if(strtod(token[1], NULL) != 1.0f)
        {
          dt_print(DT_DEBUG_ALWAYS, "[lut3d] DOMAIN MAX other than 1 is not supported");
          dt_control_log(_("DOMAIN MAX other than 1 is not supported"));
          dt_free_align(lclut);
          free(line);
          fclose(cube_file);
          return 0;
        }
      }
      else if(strcmp("LUT_1D_SIZE", token[0]) == 0)
      {
        dt_print(DT_DEBUG_ALWAYS, "[lut3d] 1D cube LUT is not supported");
        dt_control_log(_("1D cube LUT is not supported"));
        free(line);
        fclose(cube_file);
        return 0;
      }
      else if(strcmp("LUT_3D_SIZE", token[0]) == 0)
      {
        level = atoll(token[1]);
        if(level > 256)
        {
          dt_print(DT_DEBUG_ALWAYS, "[lut3d] error - LUT 3D size %d > 256", level);
          dt_control_log(_("error - LUT 3D size %d exceeds the maximum supported"), level);
          free(line);
          fclose(cube_file);
          return 0;
        }
        buf_size = level * level * level * 3;
        dt_print(DT_DEBUG_DEV, "[lut3d] allocating %zu bytes for cube LUT - level %d", buf_size, level);
        lclut = dt_alloc_align_float(buf_size);
        if(!lclut)
        {
          dt_print(DT_DEBUG_ALWAYS, "[lut3d] error - allocating buffer for cube LUT");
          dt_control_log(_("error - allocating buffer for cube LUT"));
          free(line);
          fclose(cube_file);
          return 0;
        }
      }
      else if(nb_token == 3)
      {
        if(!level)
        {
          dt_print(DT_DEBUG_ALWAYS, "[lut3d] error - cube LUT size is not defined");
          dt_control_log(_("error - cube LUT size is not defined"));
          free(line);
          fclose(cube_file);
          return 0;
        }
        for(int j=0; j < 3; j++)
        {
          lclut[i+j] = _dt_atof(token[j]);
          if(dt_isnan(lclut[i+j]))
          {
            dt_print(DT_DEBUG_ALWAYS, "[lut3d] error - invalid number line %d", (int)i/3);
            dt_control_log(_("error - cube LUT invalid number line %d"), (int)i/3);
            free(line);
            fclose(cube_file);
            return 0;
          }
          else if(lclut[i+j] < 0.0 || lclut[i+j] > 1.0)
            out_of_range_nb++;
        }
        i += 3;
      }
    }
  }
  if(i != buf_size || i == 0)
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] error - cube LUT lines number %d is not correct, should be %d",
             (int)i/3, (int)buf_size/3);
    dt_control_log(_("error - cube LUT lines number %d is not correct, should be %d"),
                   (int)i/3, (int)buf_size/3);
    dt_free_align(lclut);
    free(line);
    fclose(cube_file);
    return 0;
  }
  if(out_of_range_nb)
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] warning - %u values out of range [0,1]", out_of_range_nb);
    dt_control_log(_("warning - cube LUT has %d values out of range [0,1]"), out_of_range_nb);
  }
  *clut = lclut;
  free(line);
  fclose(cube_file);
  return level;
}

static uint16_t _calculate_clut_3dl(const char *const filepath, float **clut)
{
  char *line = NULL;
  size_t len = 0;
  ssize_t read;
  char token[3][50];
  uint16_t level = 0;
  float *lclut = NULL;
  int max_value = 0;
  uint32_t i = 0;
  size_t buf_size = 0;

  FILE *cube_file = g_fopen(filepath, "r");

  if(!cube_file)
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] invalid 3dl file: %s", filepath);
    dt_control_log(_("error - invalid 3dl file: %s"), filepath);
    return 0;
  }
  while((read = getline(&line, &len, cube_file)) != -1)
  {
    const uint8_t nb_token = _parse_cube_line(line, token);
    if(nb_token)
    {
      if(!level)
      {
        if(nb_token > 3)
        {
          // we assume the shaper is linear and gives the size of the cube (level)
          const int min_shaper = atoll(token[0]);
          const int max_shaper = atoll(token[2]);
          if(max_shaper > min_shaper)
          {
            level = nb_token; // max nb_token = 50 < 256
            if(max_shaper < 128)
            {
              dt_print(DT_DEBUG_ALWAYS, "[lut3d] error - the maximum shaper LUT value %d is too low", max_shaper);
              dt_control_log(_("error - the maximum shaper LUT value %d is too low"), max_shaper);
              free(line);
              fclose(cube_file);
              return 0;
            }
            buf_size = level * level * level * 3;
            dt_print(DT_DEBUG_DEV, "[lut3d] allocating %zu bytes for 3dl LUT - level %d", buf_size, level);
            lclut = dt_alloc_align_float(buf_size);
            if(!lclut)
            {
              dt_print(DT_DEBUG_ALWAYS, "[lut3d] error - allocating buffer for 3dl LUT");
              dt_control_log(_("error - allocating buffer for 3dl LUT"));
              free(line);
              fclose(cube_file);
              return 0;
            }
          }
        }
      }
      else if(nb_token == 3)
      {
        if(!level)
        {
          dt_print(DT_DEBUG_ALWAYS, "[lut3d] error - 3dl LUT size is not defined");
          dt_control_log(_("error - 3dl LUT size is not defined"));
          free(line);
          fclose(cube_file);
          return 0;
        }
        // indexing starts with blue instead of red. need to restore the right index
        const uint32_t level2 = level * level;
        const uint32_t red = i / level2;
        const uint32_t rr = i - red * level2;
        const uint32_t green = rr / level;
        const uint32_t blue = rr - green * level;
        const uint32_t k = red + level * green + level2 * blue;
        for(int j=0; j < 3; j++)
        {
          const uint32_t value = atoll(token[j]);
          lclut[k*3+j] = (float)value;
          if(value > max_value)
            max_value = value;
        }
        i++;
        if(i * 3 > buf_size)
          break;
      }
    }
  }
  if(i * 3 != buf_size || i == 0)
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] error - 3dl LUT lines number is not correct");
    dt_control_log(_("error - 3dl LUT lines number is not correct"));
    dt_free_align(lclut);
    free(line);
    fclose(cube_file);
    return 0;
  }
  free(line);
  fclose(cube_file);

  // search bit depth: min 2^x > max_value
  int inorm = 1;
  while((inorm < max_value) && (inorm < 65536))  // bit depth 16
    inorm <<= 1;
  if(inorm < 128)  // bit depth 7
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] error - the maximum LUT value does not match any valid bit depth");
    dt_control_log(_("error - the maximum LUT value does not match any valid bit depth"));
    dt_free_align(lclut);
    return 0;
  }
  const float norm = 1.0f / (float)(inorm - 1);
  // normalize the lut
  for(i =0; i < buf_size; i++)
    lclut[i] = CLAMP(lclut[i] * norm, 0.0f, 1.0f);
  *clut = lclut;
  return level;
}

#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_lut3d_data_t *d = piece->data;
  dt_iop_lut3d_global_data_t *gd = self->global_data;
  cl_int err = CL_SUCCESS;
  const float *const clut = (float *)d->clut;
  const int level = d->level;
  const int kernel = (d->params.interpolation == DT_IOP_TETRAHEDRAL) ? gd->kernel_lut3d_tetrahedral
    : (d->params.interpolation == DT_IOP_TRILINEAR) ? gd->kernel_lut3d_trilinear
    : gd->kernel_lut3d_pyramid;
  const int colorspace
    = (d->params.colorspace == DT_IOP_SRGB) ? DT_COLORSPACE_SRGB
    : (d->params.colorspace == DT_IOP_REC709) ? DT_COLORSPACE_REC709
    : (d->params.colorspace == DT_IOP_ARGB) ? DT_COLORSPACE_ADOBERGB
    : (d->params.colorspace == DT_IOP_LIN_PROPHOTO) ? DT_COLORSPACE_PROPHOTO_RGB
    : (d->params.colorspace == DT_IOP_LIN_REC709) ? DT_COLORSPACE_LIN_REC709
    : DT_COLORSPACE_LIN_REC2020;
  const dt_iop_order_iccprofile_info_t *const lut_profile
    = dt_ioppr_add_profile_info_to_list(self->dev, colorspace, "", INTENT_PERCEPTUAL);
  const dt_iop_order_iccprofile_info_t *const work_profile
    = dt_ioppr_get_iop_work_profile_info(self, self->dev->iop);
  gboolean transform = (work_profile != NULL && lut_profile != NULL) ? TRUE : FALSE;
  cl_mem clut_cl = NULL;
  const int devid = piece->pipe->devid;
  const int width = roi_in->width;
  const int height = roi_in->height;
  const size_t sizes[] = { ROUNDUPDWD(width, devid), ROUNDUPDHT(height, devid), 1 };

  if(clut && level)
  {
    clut_cl = dt_opencl_copy_host_to_device_constant(devid, sizeof(float) * 3 * level * level * level + 1, (void *)clut);
    if(clut_cl == NULL)
    {
      err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
      goto cleanup;
    }
    if(transform)
    {
      const gboolean success = dt_ioppr_transform_image_colorspace_rgb_cl(devid,
        dev_in, dev_out, width, height,
        work_profile, lut_profile, "work profile to LUT profile");
      if(!success)
       transform = FALSE;
    }
    if(transform)
      dt_opencl_set_kernel_args(devid, kernel, 0, CLARG(dev_out));
    else
      dt_opencl_set_kernel_args(devid, kernel, 0, CLARG(dev_in));
    dt_opencl_set_kernel_args(devid, kernel, 1, CLARG(dev_out), CLARG(width), CLARG(height), CLARG(clut_cl), CLARG(level));
    err = dt_opencl_enqueue_kernel_2d(devid, kernel, sizes);
    if(err != CL_SUCCESS)
      goto cleanup;

    if(transform)
    {
      if(!dt_ioppr_transform_image_colorspace_rgb_cl(devid, dev_out, dev_out, width, height,
        lut_profile, work_profile, "LUT profile to work profile"))
        err = DT_OPENCL_PROCESS_CL;
    }
  }
  else
  { // no lut: identity kernel
    dt_opencl_set_kernel_args(devid, gd->kernel_lut3d_none, 0, CLARG(dev_in), CLARG(dev_out), CLARG(width),
      CLARG(height));
    err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_lut3d_none, sizes);
  }

cleanup:
  dt_opencl_release_mem_object(clut_cl);
  return err;
}
#endif

void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const ibuf, void *const obuf,
             const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
{
  dt_iop_lut3d_data_t *d = piece->data;
  const int width = roi_in->width;
  const int height = roi_in->height;
  const int ch = piece->colors;
  const float *const clut = (float *)d->clut;
  const uint16_t level = d->level;
  const int interpolation = d->params.interpolation;
  const int colorspace
    = (d->params.colorspace == DT_IOP_SRGB) ? DT_COLORSPACE_SRGB
    : (d->params.colorspace == DT_IOP_REC709) ? DT_COLORSPACE_REC709
    : (d->params.colorspace == DT_IOP_ARGB) ? DT_COLORSPACE_ADOBERGB
    : (d->params.colorspace == DT_IOP_LIN_PROPHOTO) ? DT_COLORSPACE_PROPHOTO_RGB
    : (d->params.colorspace == DT_IOP_LIN_REC709) ? DT_COLORSPACE_LIN_REC709
    : DT_COLORSPACE_LIN_REC2020;
  const dt_iop_order_iccprofile_info_t *const lut_profile
    = dt_ioppr_add_profile_info_to_list(self->dev, colorspace, "", INTENT_PERCEPTUAL);
  const dt_iop_order_iccprofile_info_t *const work_profile
    = dt_ioppr_get_iop_work_profile_info(self, self->dev->iop);
  const gboolean transform = (work_profile != NULL && lut_profile != NULL) ? TRUE : FALSE;
  if(clut)
  {
    if(transform)
    {
      dt_ioppr_transform_image_colorspace_rgb(ibuf, obuf, width, height,
        work_profile, lut_profile, "work profile to LUT profile");
      if(interpolation == DT_IOP_TETRAHEDRAL)
        _correct_pixel_tetrahedral(obuf, obuf, (size_t)width * height, clut, level);
      else if(interpolation == DT_IOP_TRILINEAR)
        _correct_pixel_trilinear(obuf, obuf, (size_t)width * height, clut, level);
      else
        _correct_pixel_pyramid(obuf, obuf, (size_t)width * height, clut, level);
      dt_ioppr_transform_image_colorspace_rgb(obuf, obuf, width, height,
        lut_profile, work_profile, "LUT profile to work profile");
    }
    else
    {
      if(interpolation == DT_IOP_TETRAHEDRAL)
        _correct_pixel_tetrahedral(ibuf, obuf, (size_t)width * height, clut, level);
      else if(interpolation == DT_IOP_TRILINEAR)
        _correct_pixel_trilinear(ibuf, obuf, (size_t)width * height, clut, level);
      else
        _correct_pixel_pyramid(ibuf, obuf, (size_t)width * height, clut, level);
    }
  }
  else  // no clut
  {
    dt_iop_image_copy_by_size(obuf, ibuf, width, height, ch);
  }
}

void filepath_set_unix_separator(char *filepath)
{ // use the unix separator as it works also on windows
  const int len = strlen(filepath);
  for(int i=0; i<len; ++i)
    if(filepath[i]=='\\') filepath[i] = '/';
}

void init_global(dt_iop_module_so_t *self)
{
  const int program = 28; // rgbcurve.cl, from programs.conf
  dt_iop_lut3d_global_data_t *gd = malloc(sizeof(dt_iop_lut3d_global_data_t));
  self->data = gd;
  gd->kernel_lut3d_tetrahedral = dt_opencl_create_kernel(program, "lut3d_tetrahedral");
  gd->kernel_lut3d_trilinear = dt_opencl_create_kernel(program, "lut3d_trilinear");
  gd->kernel_lut3d_pyramid = dt_opencl_create_kernel(program, "lut3d_pyramid");
  gd->kernel_lut3d_none = dt_opencl_create_kernel(program, "lut3d_none");

#ifdef HAVE_GMIC
  // make sure the cache dir exists
  char *cache_dir = g_build_filename(g_get_user_cache_dir(), "gmic", NULL);
  char *cache_gmic_dir = dt_loc_init_generic(cache_dir, NULL, NULL);
  g_free(cache_dir);
  g_free(cache_gmic_dir);
#endif // HAVE_GMIC
}

void cleanup_global(dt_iop_module_so_t *self)
{
  dt_iop_lut3d_global_data_t *gd = self->data;
  dt_opencl_free_kernel(gd->kernel_lut3d_tetrahedral);
  dt_opencl_free_kernel(gd->kernel_lut3d_trilinear);
  dt_opencl_free_kernel(gd->kernel_lut3d_pyramid);
  dt_opencl_free_kernel(gd->kernel_lut3d_none);
  free(self->data);
  self->data = NULL;
}

static int _calculate_clut(dt_iop_lut3d_params_t *const p, float **clut)
{
  uint16_t level = 0;
  const char *filepath = p->filepath;
#ifdef HAVE_GMIC
  if(p->nb_keypoints && filepath[0])
  {
    // compressed in params. no need to read the file
    level = _calculate_clut_compressed(p, filepath, clut);
  }
  else
  { // read the file
#endif  // HAVE_GMIC
    gchar *lutfolder = dt_conf_get_string("plugins/darkroom/lut3d/def_path");
    if(filepath[0] && lutfolder[0])
    {
      char *fullpath = g_build_filename(lutfolder, filepath, NULL);
      if(g_str_has_suffix (filepath, ".png") || g_str_has_suffix (filepath, ".PNG"))
      {
        level = _calculate_clut_haldclut(p, fullpath, clut);
      }
      else if(g_str_has_suffix (filepath, ".cube") || g_str_has_suffix (filepath, ".CUBE"))
      {
        level = _calculate_clut_cube(fullpath, clut);
      }
      else if(g_str_has_suffix (filepath, ".3dl") || g_str_has_suffix (filepath, ".3DL"))
      {
        level = _calculate_clut_3dl(fullpath, clut);
      }
      g_free(fullpath);
    }
    g_free(lutfolder);
#ifdef HAVE_GMIC
  }
#endif // HAVE_GMIC
  return level;
}

#ifdef HAVE_GMIC
static gboolean _list_match_string(GtkTreeModel *model,
                                   GtkTreePath *path,
                                   GtkTreeIter *iter,
                                   dt_iop_lut3d_gui_data_t *g)
{
  gchar *str = NULL;
  gboolean visible;

  gtk_tree_model_get(model, iter, DT_LUT3D_COL_NAME, &str, -1);
  gchar *haystack = g_utf8_strdown(str, -1);
  gchar *needle = g_utf8_strdown(gtk_entry_get_text(GTK_ENTRY(g->lutentry)), -1);

  visible = (g_strrstr(haystack, needle) != NULL);

  g_free(haystack);
  g_free(needle);
  g_free(str);
  gtk_list_store_set((GtkListStore *)model, iter, DT_LUT3D_COL_VISIBLE, visible, -1);
  return FALSE;
}

static void _apply_filter_lutname_list(dt_iop_lut3d_gui_data_t *g)
{
  GtkTreeModel *modelf = gtk_tree_view_get_model((GtkTreeView *)g->lutname);
  GtkTreeModel *model = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(modelf));
  gtk_tree_model_foreach(model, (GtkTreeModelForeachFunc)_list_match_string, g);
}

void _lut3d_add_lutname_to_list(void *gv, const char *const lutname)
{
  dt_iop_lut3d_gui_data_t *g = (dt_iop_lut3d_gui_data_t *)gv;
  GtkTreeModel *modelf = gtk_tree_view_get_model((GtkTreeView *)g->lutname);
  GtkTreeModel *model = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(modelf));
  gtk_list_store_insert_with_values((GtkListStore *)model, NULL, -1,
                                     DT_LUT3D_COL_NAME, lutname, DT_LUT3D_COL_VISIBLE, TRUE, -1);
}

void _lut3d_clear_lutname_list(void *gv)
{
  dt_iop_lut3d_gui_data_t *g = (dt_iop_lut3d_gui_data_t *)gv;
  GtkTreeModel *modelf = gtk_tree_view_get_model((GtkTreeView *)g->lutname);
  GtkTreeModel *model = gtk_tree_model_filter_get_model(GTK_TREE_MODEL_FILTER(modelf));
  // keep _lutname_callback quiet while clearing the list
  GtkTreeSelection *selection = gtk_tree_view_get_selection((GtkTreeView *)g->lutname);
  g_signal_handler_block(G_OBJECT(selection), g->lutname_handler_id);
  gtk_list_store_clear((GtkListStore *)model);
  g_signal_handler_unblock(G_OBJECT(selection), g->lutname_handler_id);
}

static gboolean _select_lutname_in_list(dt_iop_lut3d_gui_data_t *g, const char *const lutname)
{
  GtkTreeIter iter;
  GtkTreeSelection *selection = gtk_tree_view_get_selection((GtkTreeView *)g->lutname);
  GtkTreeModel *model = gtk_tree_view_get_model((GtkTreeView *)g->lutname);
  if(lutname)
  {
  gboolean valid = gtk_tree_model_get_iter_first(model, &iter);
  while(valid)
    {
     gchar *name;
     gtk_tree_model_get(model, &iter, DT_LUT3D_COL_NAME, &name, -1);
     if(!g_strcmp0(lutname, name))
     {
       gtk_tree_selection_select_iter(selection, &iter);
       GtkTreePath *path = gtk_tree_model_get_path (model, &iter);
       gtk_tree_view_scroll_to_cell((GtkTreeView *)g->lutname, path, NULL, TRUE, 0.2, 0);
       gtk_tree_path_free(path);
       g_free(name);
       return TRUE;
     }
     g_free(name);
     valid = gtk_tree_model_iter_next(model, &iter);
    }
    return FALSE;
  }
  else  // select the first in the list
  {
    if(gtk_tree_model_iter_nth_child(model, &iter, NULL, 0))
    {
      gtk_tree_selection_select_iter(selection, &iter);
      return TRUE;
    }
    else
    {
      return FALSE;
    }
  }
}

static void _get_selected_lutname(dt_iop_lut3d_gui_data_t *g, char *const lutname)
{
  GtkTreeIter iter;
  GtkTreeSelection *selection = gtk_tree_view_get_selection((GtkTreeView *)g->lutname);
  GtkTreeModel *model = gtk_tree_view_get_model((GtkTreeView *)g->lutname);
  if(gtk_tree_selection_get_selected(selection, &model, &iter))
  {
    gchar *name;
    gtk_tree_model_get(model, &iter, DT_LUT3D_COL_NAME, &name, -1);
    g_strlcpy(lutname, name, DT_IOP_LUT3D_MAX_LUTNAME);
    g_free(name);
  }
  else lutname[0] = 0;
}

static void _get_compressed_clut(dt_iop_module_t *self, gboolean newlutname)
{
  dt_iop_lut3d_gui_data_t *g = self->gui_data;
  dt_iop_lut3d_params_t *p = self->params;
  int nb_lut = 0;
  char *lutfolder = dt_conf_get_string("plugins/darkroom/lut3d/def_path");
  if(p->filepath[0] && lutfolder[0])
  {
    if(g_str_has_suffix (p->filepath, ".gmz") || g_str_has_suffix (p->filepath, ".GMZ"))
    {
      char *fullpath = g_build_filename(lutfolder, p->filepath, NULL);
      gboolean lut_found = lut3d_read_gmz(&p->nb_keypoints, (unsigned char *const)p->c_clut, fullpath,
              &nb_lut, (void *)g, p->lutname, newlutname);
      // to be able to fix evolution issue, keep the gmic version with the compressed lut
      if(lut_found)
      {
        if(!newlutname)
          _select_lutname_in_list(g, p->lutname);
      }
      else if(nb_lut)
      {
        _select_lutname_in_list(g, NULL);
        _get_selected_lutname(g, p->lutname);
      }
      else if(p->lutname[0])
      { // read has failed - make sure lutname appear in the list (for user info)
        if(!_select_lutname_in_list(g, p->lutname))
        {
          _lut3d_add_lutname_to_list(g, p->lutname);
          _select_lutname_in_list(g, p->lutname);
        }
      }
      g_free(fullpath);
    }
  }
  g_free(lutfolder);
}

static void _show_hide_controls(dt_iop_module_t *self)
{
  dt_iop_lut3d_gui_data_t *g = self->gui_data;
  GtkTreeModel *model = gtk_tree_view_get_model((GtkTreeView *)g->lutname);
  const int nb_luts = gtk_tree_model_iter_n_children(model, NULL);
  if((nb_luts > 1) || ((nb_luts > 0) &&
       g_str_has_prefix(dt_bauhaus_combobox_get_text(g->filepath), invalid_filepath_prefix)))
  {
    int nb_pixels = (20*(nb_luts+1) > 200) ? 200 : 20*(nb_luts);
    if(nb_luts > 100)
      gtk_widget_set_visible(g->lutentry, TRUE);
    else
      gtk_widget_set_visible(g->lutentry, FALSE);
    gtk_widget_set_visible(g->lutwindow, TRUE);
    gtk_scrolled_window_set_min_content_height((GtkScrolledWindow *)g->lutwindow, DT_PIXEL_APPLY_DPI(nb_pixels));
  }
  else
  {
    gtk_widget_set_visible(g->lutentry, FALSE);
    gtk_widget_set_visible(g->lutwindow, FALSE);
  }
}
#endif  // HAVE_GMIC

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_lut3d_params_t *p = (dt_iop_lut3d_params_t *)p1;
  dt_iop_lut3d_data_t *d = piece->data;

  if(strcmp(p->filepath, d->params.filepath) != 0 || strcmp(p->lutname, d->params.lutname) != 0 )
  { // new clut file
    if(d->clut)
    { // reset current clut if any
      dt_free_align(d->clut);
      d->clut = NULL;
      d->level = 0;
    }
    d->level = _calculate_clut(p, &d->clut);
  }
  memcpy(&d->params, p, sizeof(dt_iop_lut3d_params_t));
}

void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
{
  piece->data = malloc(sizeof(dt_iop_lut3d_data_t));
  dt_iop_lut3d_data_t *d = piece->data;
  memcpy(&d->params, self->default_params, sizeof(dt_iop_lut3d_params_t));
  d->clut = NULL;
  d->level = 0;
  d->params.filepath[0] = '\0';
}

void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
{
  dt_iop_lut3d_data_t *d = piece->data;;
  if(d->clut)
    dt_free_align(d->clut);
  d->clut = NULL;
  d->level = 0;
  free(piece->data);
  piece->data = NULL;
}

static void _filepath_callback(GtkWidget *widget, dt_iop_module_t *self)
{
  if(darktable.gui->reset) return;
  dt_iop_lut3d_params_t *p = self->params;
  char filepath[DT_IOP_LUT3D_MAX_PATHNAME];
  g_strlcpy(filepath, dt_bauhaus_combobox_get_text(widget), sizeof(filepath));
  if(!g_str_has_prefix(filepath, invalid_filepath_prefix))
  {
    filepath_set_unix_separator(filepath);
#ifdef HAVE_GMIC
    dt_iop_lut3d_gui_data_t *g = self->gui_data;
    if(strcmp(filepath, p->filepath) != 0 && !(g_str_has_suffix(filepath, ".gmz") || g_str_has_suffix(filepath, ".GMZ")))
    {
      // if new file is gmz we try to keep the same lut
      p->nb_keypoints = 0;
      p->lutname[0] = 0;
      _lut3d_clear_lutname_list(g);
    }
    g_strlcpy(p->filepath, filepath, sizeof(p->filepath));
    _get_compressed_clut(self, FALSE);
    _show_hide_controls(self);
    gtk_entry_set_text(GTK_ENTRY(g->lutentry), "");
#else
    g_strlcpy(p->filepath, filepath, sizeof(p->filepath));
#endif // HAVE_GMIC
    dt_dev_add_history_item(darktable.develop, self, TRUE);
  }
}

#ifdef HAVE_GMIC
static void _entry_callback(GtkEntry *entry, dt_iop_module_t *self)
{
  dt_iop_lut3d_gui_data_t *g = self->gui_data;
  _apply_filter_lutname_list(g);
}

static void _lutname_callback(GtkTreeSelection *selection, dt_iop_module_t *self)
{
  if(darktable.gui->reset) return;
  dt_iop_lut3d_params_t *p = self->params;
  GtkTreeIter iter;
  GtkTreeModel *model;
  gchar *lutname;

  if(gtk_tree_selection_get_selected(selection, &model, &iter))
  {
    gtk_tree_model_get(model, &iter, DT_LUT3D_COL_NAME, &lutname, -1);
    if(lutname[0] && strcmp(lutname, p->lutname) != 0)
    {
      g_strlcpy(p->lutname, lutname, sizeof(p->lutname));
      _get_compressed_clut(self, TRUE);
      dt_dev_add_history_item(darktable.develop, self, TRUE);
    }
    g_free(lutname);
  }
}

static gboolean _mouse_scroll(GtkWidget *view, GdkEventScroll *event, dt_lib_module_t *self)
{
  GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(view));
  GtkTreeIter iter;
  GtkTreeModel *model = gtk_tree_view_get_model((GtkTreeView *)view);
  if(gtk_tree_selection_get_selected(selection, &model, &iter))
  {
    gboolean next = FALSE;
    if(event->delta_y > 0)
      next = gtk_tree_model_iter_next(model, &iter);
    else
      next = gtk_tree_model_iter_previous(model, &iter);
    if(next)
    {
      gtk_tree_selection_select_iter(selection, &iter);
      GtkTreePath *path = gtk_tree_model_get_path(model, &iter);
      gtk_tree_view_set_cursor((GtkTreeView *)view, path, NULL, FALSE);
      gtk_tree_path_free(path);
      return TRUE;
    }
  }
  return FALSE;
}
#endif // HAVE_GMIC

// remove root lut folder from path
static void _remove_root_from_path(const char *const lutfolder, char *const filepath)
{
  const int j = strlen(lutfolder) + 1;
  int i;
  for(i = 0; filepath[i+j] != '\0'; i++)
    filepath[i] = filepath[i+j];
  filepath[i] = '\0';
}

static int _check_extension(const struct dirent *namestruct)
{
  const char *filename = namestruct->d_name;
  int res = 0;
  if(!filename || !filename[0]) return res;
  char *p = g_strrstr(filename,".");
  if(!p) return res;
  char *fext = g_ascii_strdown(g_strdup(p), -1);
#ifdef HAVE_GMIC
  if(!g_strcmp0(fext, ".png") || !g_strcmp0(fext, ".cube") || !g_strcmp0(fext, ".3dl")
      || !g_strcmp0(fext, ".gmz")) res = 1;
#else
  if(!g_strcmp0(fext, ".png") || !g_strcmp0(fext, ".cube") || !g_strcmp0(fext, ".3dl") ) res = 1;
#endif // HAVE_GMIC
  g_free(fext);
  return res;
}

// update filepath combobox with all files in the current folder
static void _update_filepath_combobox(dt_iop_lut3d_gui_data_t *g, char *filepath, char *lutfolder)
{
  if(!filepath[0])
    dt_bauhaus_combobox_clear(g->filepath);
  else if(!dt_bauhaus_combobox_set_from_text(g->filepath, filepath))
  {
    // new folder -> update the files list
    char *relativepath = g_path_get_dirname(filepath);
    char *folder = g_build_filename(lutfolder, relativepath, NULL);

    struct dirent **entries;
    const int numentries = scandir(folder, &entries, _check_extension, alphasort);

    dt_bauhaus_combobox_clear(g->filepath);
    for(int i = 0; i < numentries; i++)
    {
      const char *file = entries[i]->d_name;
      char *ofilepath = (strcmp(relativepath, ".") != 0)
              ? g_build_filename(relativepath, file, NULL)
              : g_strdup(file);
      filepath_set_unix_separator(ofilepath);
      dt_bauhaus_combobox_add_aligned(g->filepath, ofilepath, DT_BAUHAUS_COMBOBOX_ALIGN_LEFT);
      g_free(ofilepath);
      free(entries[i]);
    }
    if(numentries != -1) free(entries);

    if(!dt_bauhaus_combobox_set_from_text(g->filepath, filepath))
    { // file may have disappeared - show it
      char *invalidfilepath = g_strconcat(invalid_filepath_prefix, filepath, NULL);
      dt_bauhaus_combobox_add_aligned(g->filepath, invalidfilepath, DT_BAUHAUS_COMBOBOX_ALIGN_LEFT);
      dt_bauhaus_combobox_set_from_text(g->filepath, invalidfilepath);
      g_free(invalidfilepath);
    }
    g_free(relativepath);
    g_free(folder);
  }
}

static void _button_clicked(GtkWidget *widget, dt_iop_module_t *self)
{
  dt_iop_lut3d_gui_data_t *g = self->gui_data;
  dt_iop_lut3d_params_t *p = self->params;
  gchar* lutfolder = dt_conf_get_string("plugins/darkroom/lut3d/def_path");
  if(strlen(lutfolder) == 0)
  {
    dt_print(DT_DEBUG_ALWAYS, "[lut3d] LUT root folder not defined");
    dt_control_log(_("LUT root folder not defined"));
    g_free(lutfolder);
    return;
  }
  GtkWidget *win = dt_ui_main_window(darktable.gui->ui);
  GtkFileChooserNative *filechooser = gtk_file_chooser_native_new(
        _("select LUT file"), GTK_WINDOW(win), GTK_FILE_CHOOSER_ACTION_OPEN,
        _("_select"), _("_cancel"));
  gtk_file_chooser_set_select_multiple(GTK_FILE_CHOOSER(filechooser), FALSE);

  char *composed = g_build_filename(lutfolder, p->filepath, NULL);
  if(strlen(p->filepath) == 0 || g_access(composed, F_OK) == -1)
    gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(filechooser), lutfolder);
  else
    gtk_file_chooser_select_filename(GTK_FILE_CHOOSER(filechooser), composed);
  g_free(composed);

  GtkFileFilter* filter = GTK_FILE_FILTER(gtk_file_filter_new());
  gtk_file_filter_add_pattern(filter, "*.png");
  gtk_file_filter_add_pattern(filter, "*.PNG");
  gtk_file_filter_add_pattern(filter, "*.cube");
  gtk_file_filter_add_pattern(filter, "*.CUBE");
  gtk_file_filter_add_pattern(filter, "*.3dl");
  gtk_file_filter_add_pattern(filter, "*.3DL");
#ifdef HAVE_GMIC
  gtk_file_filter_add_pattern(filter, "*.gmz");
  gtk_file_filter_add_pattern(filter, "*.GMZ");
  gtk_file_filter_set_name(filter, _("hald CLUT (png), 3D LUT (cube or 3dl) or gmic compressed LUT (gmz)"));
#else
  gtk_file_filter_set_name(filter, _("hald CLUT (png) or 3D LUT (cube or 3dl)"));
#endif // HAVE_GMIC
  gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(filechooser), filter);
  gtk_file_chooser_set_filter(GTK_FILE_CHOOSER(filechooser), filter);

  // let this option to allow the user to see the actual content of the folder
  // but any selected file with ext <> png or cube will be ignored
  filter = GTK_FILE_FILTER(gtk_file_filter_new());
  gtk_file_filter_add_pattern(filter, "*");
  gtk_file_filter_set_name(filter, _("all files"));
  gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(filechooser), filter);

  if(gtk_native_dialog_run(GTK_NATIVE_DIALOG(filechooser)) == GTK_RESPONSE_ACCEPT)
  {
    gchar *filepath = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(filechooser));
    if(strcmp(lutfolder, filepath) < 0)
    {
      _remove_root_from_path(lutfolder, filepath);
      filepath_set_unix_separator(filepath);
      _update_filepath_combobox(g, filepath, lutfolder);
    }
    else if(!filepath[0])// file chosen outside of root folder
    {
      dt_print(DT_DEBUG_ALWAYS, "[lut3d] select file outside LUT root folder is not allowed");
      dt_control_log(_("select file outside LUT root folder is not allowed"));
    }
    g_free(filepath);
    gtk_widget_set_sensitive(g->filepath, p->filepath[0]);
  }
  g_free(lutfolder);
  g_object_unref(filechooser);
}

static void _show_hide_colorspace(dt_iop_module_t *self)
{
  dt_iop_lut3d_gui_data_t *g = self->gui_data;
  GList *iop_order_list = self->dev->iop_order_list;
  const int order_lut3d = dt_ioppr_get_iop_order(iop_order_list, self->op, self->multi_priority);
  const int order_colorin = dt_ioppr_get_iop_order(iop_order_list, "colorin", -1);
  const int order_colorout = dt_ioppr_get_iop_order(iop_order_list, "colorout", -1);
  if(order_lut3d < order_colorin || order_lut3d > order_colorout)
  {
    gtk_widget_hide(g->colorspace);
  }
  else
  {
    gtk_widget_show(g->colorspace);
  }
}

void gui_update(dt_iop_module_t *self)
{
  dt_iop_lut3d_gui_data_t *g = self->gui_data;
  dt_iop_lut3d_params_t *p = self->params;
  gchar *lutfolder = dt_conf_get_string("plugins/darkroom/lut3d/def_path");
  if(!lutfolder[0])
  {
    gtk_widget_set_sensitive(g->button, FALSE);
    gtk_widget_set_sensitive(g->filepath, FALSE);
    dt_bauhaus_combobox_clear(g->filepath);
  }
  else
  {
    gtk_widget_set_sensitive(g->button, TRUE);
    gtk_widget_set_sensitive(g->filepath, p->filepath[0]);
    _update_filepath_combobox(g, p->filepath, lutfolder);
  }
  g_free(lutfolder);

  _show_hide_colorspace(self);

#ifdef HAVE_GMIC
  if(p->lutname[0])
  {
    _get_compressed_clut(self, FALSE);
  }
  _show_hide_controls(self);
#endif // HAVE_GMIC
}

static void _module_moved_callback(gpointer instance, dt_iop_module_t *self)
{
  _show_hide_colorspace(self);
}

void gui_init(dt_iop_module_t *self)
{
  dt_iop_lut3d_gui_data_t *g = IOP_GUI_ALLOC(lut3d);

  g->button = dtgtk_button_new(dtgtk_cairo_paint_directory, CPF_NONE, NULL);
  gtk_widget_set_name(g->button, "non-flat");
#ifdef HAVE_GMIC
  gtk_widget_set_tooltip_text(g->button, _("select a png (haldclut)"
      ", a cube, a 3dl or a gmz (compressed LUT) file "
      "CAUTION: 3D LUT folder must be set in preferences/processing before choosing the LUT file"));
#else
  gtk_widget_set_tooltip_text(g->button, _("select a png (haldclut)"
      ", a cube or a 3dl file "
      "CAUTION: 3D LUT folder must be set in preferences/processing before choosing the LUT file"));
#endif // HAVE_GMIC

  g->filepath = dt_bauhaus_combobox_new(self);
  dt_bauhaus_combobox_set_entries_ellipsis(g->filepath, PANGO_ELLIPSIZE_MIDDLE);
#ifdef HAVE_GMIC
  gtk_widget_set_tooltip_text(g->filepath,
    _("the file path (relative to LUT folder) is saved with image along with the LUT data if it's a compressed LUT (gmz)"));
#else
  gtk_widget_set_tooltip_text(g->filepath,
    _("the file path (relative to LUT folder) is saved with image (and not the LUT data themselves)"));
#endif // HAVE_GMIC
  self->widget = dt_gui_vbox(dt_gui_hbox(g->button, dt_gui_expand(g->filepath)));
  g_signal_connect(G_OBJECT(g->button), "clicked", G_CALLBACK(_button_clicked), self);
  g_signal_connect(G_OBJECT(g->filepath), "value-changed", G_CALLBACK(_filepath_callback), self);

#ifdef HAVE_GMIC
  // text entry
  g->lutentry = gtk_entry_new();
  gtk_widget_set_tooltip_text(g->lutentry, _("enter LUT name"));
  gtk_widget_add_events(g->lutentry, GDK_KEY_RELEASE_MASK);
  dt_gui_box_add(self->widget, g->lutentry);

  // treeview
  GtkTreeModel *lutmodel = (GtkTreeModel *)gtk_list_store_new(DT_LUT3D_NUM_COLS, G_TYPE_STRING, G_TYPE_BOOLEAN);
  GtkTreeModel *lutfilter = gtk_tree_model_filter_new(lutmodel, NULL);
  gtk_tree_model_filter_set_visible_column(GTK_TREE_MODEL_FILTER(lutfilter), DT_LUT3D_COL_VISIBLE);
  g_object_unref(lutmodel);

  g->lutname = (GtkWidget *)gtk_tree_view_new();
  gtk_widget_set_name(g->lutname, "lutname");
  gtk_tree_view_set_model((GtkTreeView *)g->lutname, lutfilter);
  gtk_tree_view_set_hover_selection((GtkTreeView *)g->lutname, FALSE);
  gtk_tree_view_set_headers_visible((GtkTreeView *)g->lutname, FALSE);
  gtk_widget_set_tooltip_text(g->lutname, _("select the LUT"));
  g->lutwindow = dt_gui_scroll_wrap(g->lutname);

  GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
  GtkTreeViewColumn *col = gtk_tree_view_column_new_with_attributes ("lutname", renderer,
                                                   "text", DT_LUT3D_COL_NAME, NULL);
  gtk_tree_view_append_column((GtkTreeView *)g->lutname, col);
  GtkTreeSelection *selection = gtk_tree_view_get_selection((GtkTreeView *)g->lutname);
  gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE);
  g->lutname_handler_id = g_signal_connect(G_OBJECT(selection), "changed", G_CALLBACK(_lutname_callback), self);
  dt_gui_box_add(self->widget, g->lutwindow);

  g_signal_connect(G_OBJECT(g->lutentry), "changed", G_CALLBACK(_entry_callback), self);
  g_signal_connect(G_OBJECT((GtkTreeView *)g->lutname), "scroll-event", G_CALLBACK(_mouse_scroll), (gpointer)self);
#endif // HAVE_GMIC

  g->colorspace = dt_bauhaus_combobox_from_params(self, "colorspace");
  gtk_widget_set_tooltip_text(g->colorspace, _("select the color space in which the LUT has to be applied"));

  g->interpolation = dt_bauhaus_combobox_from_params(self, N_("interpolation"));
  gtk_widget_set_tooltip_text(g->interpolation, _("select the interpolation method"));

  DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_MODULE_MOVED, _module_moved_callback);
}

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