/*
    This file is part of darktable,
    Copyright (C) 2019-2023 darktable developers.

    darktable is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    darktable is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with darktable.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "common/calculator.h"
#include "common/darktable.h"
#include "common/file_location.h"
#include "common/math.h"
#include "control/conf.h"

#include <glib.h>
#include <glib/gstdio.h>
#include <glib/gprintf.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>

typedef struct dt_conf_dreggn_t
{
  GSList *result;
  const char *match;
} dt_conf_dreggn_t;

/** return slot for this variable or newly allocated slot. */
static inline char *_conf_get_var(const char *name)
{
  char *str;

  dt_pthread_mutex_lock(&darktable.conf->mutex);

  str = (char *)g_hash_table_lookup(darktable.conf->override_entries, name);
  if(str) goto fin;

  str = (char *)g_hash_table_lookup(darktable.conf->table, name);
  if(str) goto fin;

  // not found, try defaults
  str = (char *)dt_confgen_get(name, DT_DEFAULT);
  if(str)
  {
    char *str_new = g_strdup(str);
    g_hash_table_insert(darktable.conf->table, g_strdup(name), str_new);
    str = str_new;
    goto fin;
  }

  // FIXME: why insert garbage?
  // still no luck? insert garbage:
  str = (char *)g_malloc0(sizeof(int32_t));
  g_hash_table_insert(darktable.conf->table, g_strdup(name), str);

fin:
  dt_pthread_mutex_unlock(&darktable.conf->mutex);
  return str;
}

/* set the value only if it hasn't been overridden from commandline
 * return 1 if key/value is still the one passed on commandline. */
static int _conf_set_if_not_overridden(const char *name, char *str)
{
  dt_pthread_mutex_lock(&darktable.conf->mutex);

  char *over = (char *)g_hash_table_lookup(darktable.conf->override_entries, name);
  const int is_overridden = (over && !strcmp(str, over));
  if(!is_overridden)
  {
    g_hash_table_insert(darktable.conf->table, g_strdup(name), str);
  }

  dt_pthread_mutex_unlock(&darktable.conf->mutex);

  return is_overridden;
}

void dt_conf_set_int(const char *name, const int val)
{
  char *str = g_strdup_printf("%d", val);
  if(_conf_set_if_not_overridden(name, str)) g_free(str);
}

void dt_conf_set_int64(const char *name, const int64_t val)
{
  char *str = g_strdup_printf("%" PRId64, val);
  if(_conf_set_if_not_overridden(name, str)) g_free(str);
}

void dt_conf_set_float(const char *name, const float val)
{
  char *str = (char *)g_malloc(G_ASCII_DTOSTR_BUF_SIZE);
  g_ascii_dtostr(str, G_ASCII_DTOSTR_BUF_SIZE, val);
  if(_conf_set_if_not_overridden(name, str)) g_free(str);
}

void dt_conf_set_bool(const char *name, const int val)
{
  char *str = g_strdup(val ? "TRUE" : "FALSE");
  if(_conf_set_if_not_overridden(name, str)) g_free(str);
}

void dt_conf_set_string(const char *name, const char *val)
{
  char *str = g_strdup(val);
  if(_conf_set_if_not_overridden(name, str)) g_free(str);
}

void dt_conf_set_folder_from_file_chooser(const char *name, GtkFileChooser *chooser)
{

#ifdef WIN32
  // for Windows native file chooser, gtk_file_chooser_get_current_folder()
  // does not work, so we workaround
  if(GTK_IS_FILE_CHOOSER_NATIVE(chooser))
  {
    gchar *pathname = gtk_file_chooser_get_filename(chooser);
    if(pathname)
    {
      gchar *folder = g_path_get_dirname(pathname);
      if(_conf_set_if_not_overridden(name, folder)) g_free(folder);
      g_free(pathname);
    }
    return;
  }
#endif

  gchar *folder = gtk_file_chooser_get_current_folder(chooser);
  if(_conf_set_if_not_overridden(name, folder)) g_free(folder);
}

static inline gboolean _conf_isnan(const double val)
{
  return val != val;
}

static int _conf_get_int_fast(const char *name)
{
  const char *str = _conf_get_var(name);
  double new_value = dt_calculator_solve(1, str);
  if(_conf_isnan(new_value))
  {
    //we've got garbage, check default
    const char *def_val = dt_confgen_get(name, DT_DEFAULT);
    if(def_val)
    {
      new_value = dt_calculator_solve(1, def_val);
      if(_conf_isnan(new_value))
        new_value = 0.0;
      else
      {
        char *fix_badval = g_strdup(def_val);
        if(_conf_set_if_not_overridden(name, fix_badval))
          g_free(fix_badval);
      }
    }
    else
    {
      new_value = 0.0;
    }
  }

  int val;
  if(new_value > 0)
    val = new_value + 0.5;
  else
    val = new_value - 0.5;
  return val;
}

int dt_conf_get_int(const char *name)
{
  const int min = dt_confgen_get_int(name, DT_MIN);
  const int max = dt_confgen_get_int(name, DT_MAX);
  const int val = _conf_get_int_fast(name);
  const int ret = CLAMP(val, min, max);
  return ret;
}

static int64_t _conf_get_int64_fast(const char *name)
{
  const char *str = _conf_get_var(name);
  double new_value = dt_calculator_solve(1, str);
  if(_conf_isnan(new_value))
  {
    //we've got garbage, check default
    const char *def_val = dt_confgen_get(name, DT_DEFAULT);
    if(def_val)
    {
      new_value = dt_calculator_solve(1, def_val);
      if(_conf_isnan(new_value))
        new_value = 0.0;
      else
      {
        char *fix_badval = g_strdup(def_val);
        if(_conf_set_if_not_overridden(name, fix_badval))
          g_free(fix_badval);
      }
    }
    else
    {
      new_value = 0.0;
    }
  }

  int64_t val;
  if(new_value > 0)
    val = new_value + 0.5;
  else
    val = new_value - 0.5;
  return val;
}

int64_t dt_conf_get_int64(const char *name)
{
  const int64_t min = dt_confgen_get_int64(name, DT_MIN);
  const int64_t max = dt_confgen_get_int64(name, DT_MAX);
  const int64_t val = _conf_get_int64_fast(name);
  const int64_t ret = CLAMP(val, min, max);
  return ret;
}

float _conf_get_float_fast(const char *name)
{
  const char *str = _conf_get_var(name);
  double new_value = dt_calculator_solve(1, str);
  if(_conf_isnan(new_value))
  {
    //we've got garbage, check default
    const char *def_val = dt_confgen_get(name, DT_DEFAULT);
    if(def_val)
    {
      new_value = dt_calculator_solve(1, def_val);
      if(_conf_isnan(new_value))
        new_value = 0.0;
      else
      {
        char *fix_badval = g_strdup(def_val);
        if(_conf_set_if_not_overridden(name, fix_badval))
          g_free(fix_badval);
      }
    }
    else
    {
      new_value = 0.0;
    }
  }
  return (float)new_value;
}

float dt_conf_get_float(const char *name)
{
  const float min = dt_confgen_get_float(name, DT_MIN);
  const float max = dt_confgen_get_float(name, DT_MAX);
  const float val = _conf_get_float_fast(name);
  const float ret = CLAMP(val, min, max);
  return ret;
}

int dt_conf_get_and_sanitize_int(const char *name,
                                 const int min,
                                 const int max)
{
  const int cmin = dt_confgen_get_int(name, DT_MIN);
  const int cmax = dt_confgen_get_int(name, DT_MAX);
  const int val = _conf_get_int_fast(name);
  const int ret = CLAMPS(val, MAX(min, cmin), MIN(max, cmax));
  dt_conf_set_int(name, ret);
  return ret;
}

int64_t dt_conf_get_and_sanitize_int64(const char *name,
                                       const int64_t min,
                                       const int64_t max)
{
  const int64_t cmin = dt_confgen_get_int64(name, DT_MIN);
  const int64_t cmax = dt_confgen_get_int64(name, DT_MAX);
  const int64_t val = _conf_get_int64_fast(name);
  const int64_t ret = CLAMPS(val, MAX(min, cmin), MIN(max, cmax));
  dt_conf_set_int64(name, ret);
  return ret;
}

float dt_conf_get_and_sanitize_float(const char *name,
                                     const float min,
                                     const float max)
{
  const float cmin = dt_confgen_get_float(name, DT_MIN);
  const float cmax = dt_confgen_get_float(name, DT_MAX);
  const float val = _conf_get_float_fast(name);
  const float ret = CLAMPS(val, MAX(min, cmin), MIN(max, cmax));
  dt_conf_set_float(name, ret);
  return ret;
}

gboolean dt_conf_get_bool(const char *name)
{
  const char *str = _conf_get_var(name);
  return (str[0] != 'F') && (str[0] != 'f') && (str[0] != '0') && (str[0] != '\0');
}

void dt_conf_set_path(const char *name, const char *val)
{
  dt_conf_set_string(name, val);
}

gchar *dt_conf_get_path(const char *name)
{
  const char *path = _conf_get_var(name);
  const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name);

  if(path[0]
     && item
     && item->type == DT_PATH
     && !g_file_test(path, G_FILE_TEST_IS_DIR | G_FILE_TEST_IS_SYMLINK))
  {
    path = dt_confgen_get(name, DT_DEFAULT);
    dt_conf_set_path(name, path);
  }
  return g_strdup(path);
}

gchar *dt_conf_get_string(const char *name)
{
  const char *str = _conf_get_var(name);
  return g_strdup(str);
}

const char *dt_conf_get_string_const(const char *name)
{
  return _conf_get_var(name);
}

gboolean dt_conf_key_not_empty(const char *name)
{
  const char *val = dt_conf_get_string_const(name);
  if(val == NULL)      return FALSE;
  if(strlen(val) == 0) return FALSE;
  return TRUE;
}

gboolean dt_conf_get_folder_to_file_chooser(const char *name, GtkFileChooser *chooser)
{
  const gchar *folder = dt_conf_get_string_const(name);
  if(folder)
  {
    gtk_file_chooser_set_current_folder(chooser, folder);
    return TRUE;
  }
  return FALSE;
}

gboolean dt_conf_is_equal(const char *name, const char *value)
{
  const char *str = _conf_get_var(name);
  return g_strcmp0(str, value) == 0;
}

static char *_sanitize_confgen(const char *name, const char *value)
{
  if(!darktable.conf->x_confgen)
    return g_strdup(value);

  const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name);

  if(!item) return g_strdup(value);

  char *result = NULL;

  switch(item->type)
  {
    case DT_INT:
    {
      double v = dt_calculator_solve(1, value);

      const int min = item->min ? (int)dt_calculator_solve(1, item->min) : INT_MIN;
      const int max = item->max ? (int)dt_calculator_solve(1, item->max) : INT_MAX;
      // if garbage, use default
      const int val = _conf_isnan(v) ? dt_confgen_get_int(name, DT_DEFAULT) : (int)v;
      result = g_strdup_printf("%d", CLAMP(val, min, max));
    }
    break;
    case DT_INT64:
    {
      double v = dt_calculator_solve(1, value);

      const int64_t min = item->min
        ? (int64_t)dt_calculator_solve(1, item->min)
        : INT64_MIN;
      const int64_t max = item->max
        ? (int64_t)dt_calculator_solve(1, item->max)
        : INT64_MAX;
      // if garbage, use default
      const int64_t val = _conf_isnan(v)
        ? dt_confgen_get_int64(name, DT_DEFAULT)
        : (int64_t)v;
      result = g_strdup_printf("%"PRId64, CLAMP(val, min, max));
    }
    break;
    case DT_FLOAT:
    {
      double v = dt_calculator_solve(1, value);

      const float min = item->min ? (float)dt_calculator_solve(1, item->min) : -FLT_MAX;
      const float max = item->max ? (float)dt_calculator_solve(1, item->max) : FLT_MAX;
      // if garbage, use default
      const float val = _conf_isnan(v)
        ? (float)dt_confgen_get_float(name, DT_DEFAULT)
        : (float)v;
      result = g_strdup_printf("%f", CLAMP(val, min, max));
    }
    break;
    case DT_BOOL:
    {
      if(strcasecmp(value, "true") && strcasecmp(value, "false"))
        result = g_strdup(dt_confgen_get(name, DT_DEFAULT));
      else
        result = g_strdup(value);
    }
    break;
    case DT_ENUM:
    {
      size_t n = strlen(value);
      char *v = item->enum_values;
      while(v++)
      {
        if(!g_ascii_strncasecmp(value, v, n) && v[n] == ']')
          return g_strndup(v, n);

        v = strchr(v, '[');
      }

      result = g_strdup(dt_confgen_get(name, DT_DEFAULT));
    }
    break;
    default:
      result = g_strdup(value);
      break;
  }

  return result;
}

gchar *dt_conf_read_values(const char *filename,
                          gchar* (*callback)(const gchar *key,
                                             const gchar *value))
{
#define LINE_SIZE 1023

  char line[LINE_SIZE + 1];

  // check for user config
  FILE *f = g_fopen(filename, "rb");

  // if file has been found, parse it

  if(f)
  {
    while(!feof(f))
    {
      const char* ret = fgets(line, LINE_SIZE, f);
      if(ret != NULL)
      {
        line[strcspn(line, "\r\n")] = 0;
        char *c = line;
        char *end = line + strlen(line);
        // check for '=' which is separator between the conf name and value
        while(*c != '=' && c < end) c++;

        if(*c == '=')
        {
          *c = '\0';

          const char *name = line;
          // ensure that numbers are properly clamped if min/max
          // defined and if not and garbage is read then the default
          // value is returned.
          char *value = _sanitize_confgen(name, (const char *)(c + 1));

          gchar *v = callback(name, value);
          g_free(value);
          if(v)
            return v;
        }
      }
    }
    fclose(f);
  }
  else if(darktable.conf->x_confgen)
  {
    // We initialize the conf table with default values.
    // For common keys, prefer the value already loaded from darktablerc-common
    // (if present) over the confgen default, so that a freshly-created
    // darktablerc inherits shared settings rather than resetting them.
    GHashTableIter iter;
    gpointer key, value;

    g_hash_table_iter_init (&iter, darktable.conf->x_confgen);
    while(g_hash_table_iter_next (&iter, &key, &value))
    {
      const char *name = (const char *)key;
      if(dt_confgen_is_common(name) && g_hash_table_lookup(darktable.conf->table, name))
        continue;
      const dt_confgen_value_t *entry = (dt_confgen_value_t *)value;
      gchar *v = callback(name, entry->def);
      if(v)
        return v;
    }
  }

  return NULL;
}

static gchar *_conf_insert_value(const gchar *key, const gchar *value)
{
  g_hash_table_insert(darktable.conf->table, g_strdup(key), g_strdup(value));
  return NULL;
}

void dt_conf_init(dt_conf_t *cf,
                  const char *filename,
                  const gboolean is_common,
                  GSList *override_entries)
{
  // should we initialize the storage
  if(!cf->table)
  {
    cf->table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    cf->override_entries = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    dt_pthread_mutex_init(&darktable.conf->mutex, NULL);
  }

  // init conf filename
  if(is_common)
  {
    g_strlcpy(cf->filename_common, filename, sizeof(cf->filename_common));
  }
  else
  {
    g_strlcpy(cf->filename, filename, sizeof(cf->filename));
  }

  dt_conf_read_values(filename, _conf_insert_value);

  if(override_entries)
  {
    for(GSList *p = override_entries; p; p = g_slist_next(p))
    {
      dt_conf_string_entry_t *entry = p->data;
      g_hash_table_insert(darktable.conf->override_entries,
                          g_strdup(entry->key),
                          g_strdup(entry->value));
    }
  }

#undef LINE_SIZE

  return;
}

/** check if key exists, return 1 if lookup succeeded, 0 if failed..*/
gboolean dt_conf_key_exists(const char *key)
{
  dt_pthread_mutex_lock(&darktable.conf->mutex);
  const gboolean res = (g_hash_table_lookup(darktable.conf->table, key) != NULL)
                  || (g_hash_table_lookup(darktable.conf->override_entries, key) != NULL);
  dt_pthread_mutex_unlock(&darktable.conf->mutex);
  return (res || dt_confgen_value_exists(key, DT_DEFAULT));
}

/** remove key from conf */
void dt_conf_remove_key(const char *key)
{
  dt_pthread_mutex_lock(&darktable.conf->mutex);
  g_hash_table_remove(darktable.conf->table, key);
  dt_pthread_mutex_unlock(&darktable.conf->mutex);
}

static void _conf_add(char *key, char *val, dt_conf_dreggn_t *d)
{
  if(strncmp(key, d->match, strlen(d->match)) == 0)
  {
    dt_conf_string_entry_t *nv = g_malloc(sizeof(dt_conf_string_entry_t));
    nv->key = g_strdup(key + strlen(d->match) + 1);
    nv->value = g_strdup(val);
    d->result = g_slist_append(d->result, nv);
  }
}

/** get all strings in */
GSList *dt_conf_all_string_entries(const char *dir)
{
  dt_pthread_mutex_lock(&darktable.conf->mutex);
  dt_conf_dreggn_t d;
  d.result = NULL;
  d.match = dir;
  g_hash_table_foreach(darktable.conf->table, (GHFunc)_conf_add, &d);
  dt_pthread_mutex_unlock(&darktable.conf->mutex);
  return d.result;
}

void dt_conf_string_entry_free(gpointer data)
{
  dt_conf_string_entry_t *nv = (dt_conf_string_entry_t *)data;
  g_free(nv->key);
  g_free(nv->value);
  nv->key = NULL;
  nv->value = NULL;
  g_free(nv);
}

gboolean dt_confgen_exists(const char *name)
{
  return g_hash_table_lookup(darktable.conf->x_confgen, name) != NULL;
}

dt_confgen_type_t dt_confgen_type(const char *name)
{
  const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name);

  if(item)
    return item->type;
  else
    return DT_STRING;
}

gboolean dt_confgen_value_exists(const char *name,
                                 const dt_confgen_value_kind_t kind)
{
  const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name);
  if(item == NULL)
    return FALSE;

  switch(kind)
  {
     case DT_DEFAULT:
       return item->def != NULL;
     case DT_MIN:
       return item->min != NULL;
     case DT_MAX:
       return item->max != NULL;
     case DT_VALUES:
       return item->enum_values != NULL;
  }
  return FALSE;
}

const char *dt_confgen_get(const char *name,
                           const dt_confgen_value_kind_t kind)
{
  const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name);

  if(item)
  {
    switch(kind)
    {
       case DT_DEFAULT:
         return item->def;
       case DT_MIN:
         return item->min;
       case DT_MAX:
         return item->max;
       case DT_VALUES:
         return item->enum_values;
    }
  }

  return "";
}

gboolean dt_confgen_is_common(const char *name)
{
  const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name);

  if(item)
  {
    return item->is_common;
  }

  return FALSE;
}

const char *dt_confgen_get_label(const char *name)
{
  const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name);

  if(item)
  {
    return item->shortdesc;
  }

  return "";
}

const char *dt_confgen_get_tooltip(const char *name)
{
  const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name);

  if(item)
  {
    return item->longdesc;
  }

  return "";
}

int dt_confgen_get_int(const char *name,
                       const dt_confgen_value_kind_t kind)
{
  if(!dt_confgen_value_exists(name, kind))
  {
    //early bail
    switch(kind)
    {
    case DT_MIN:
      return INT_MIN;
      break;
    case DT_MAX:
      return INT_MAX;
      break;
    default:
      return 0;
      break;
    }
  }
  const char *str = dt_confgen_get(name, kind);

  //if str is NULL or empty, dt_calculator_solve will return NAN
  const double value = dt_calculator_solve(1, str);

  switch(kind)
  {
  case DT_MIN:
    return _conf_isnan(value) ? INT_MIN : (value > 0 ? value + 0.5f : value - 0.5f);
    break;
  case DT_MAX:
    return _conf_isnan(value) ? INT_MAX : (value > 0 ? value + 0.5f : value - 0.5f);
    break;
  default:
    return _conf_isnan(value) ? 0.0f : (value > 0 ? value + 0.5f : value - 0.5f);
    break;
  }
  return (int)value;
}

int64_t dt_confgen_get_int64(const char *name,
                             const dt_confgen_value_kind_t kind)
{
  if(!dt_confgen_value_exists(name, kind))
  {
    //early bail
    switch(kind)
    {
    case DT_MIN:
      return INT64_MIN;
      break;
    case DT_MAX:
      return INT64_MAX;
      break;
    default:
      return 0;
      break;
    }
  }
  const char *str = dt_confgen_get(name, kind);

  //if str is NULL or empty, dt_calculator_solve will return NAN
  const double value = dt_calculator_solve(1, str);

  switch(kind)
  {
  case DT_MIN:
    return _conf_isnan(value) ? INT64_MIN : (value > 0 ? value + 0.5f : value - 0.5f);
    break;
  case DT_MAX:
    return _conf_isnan(value) ? INT64_MAX : (value > 0 ? value + 0.5f : value - 0.5f);
    break;
  default:
    return _conf_isnan(value) ? 0.0f : (value > 0 ? value + 0.5f : value - 0.5f);
    break;
  }
  return (int64_t)value;
}

gboolean dt_confgen_get_bool(const char *name,
                             const dt_confgen_value_kind_t kind)
{
  const char *str = dt_confgen_get(name, kind);
  return !strcmp(str, "true");
}

float dt_confgen_get_float(const char *name,
                           const dt_confgen_value_kind_t kind)
{
  if(!dt_confgen_value_exists(name, kind))
  {
    //early bail
    switch(kind)
    {
    case DT_MIN:
      return -FLT_MAX;
      break;
    case DT_MAX:
      return FLT_MAX;
      break;
    default:
      break;
    }
    return 0.0f;
  }

  const char *str = dt_confgen_get(name, kind);

  //if str is NULL or empty, dt_calculator_solve will return NAN
  const double value = dt_calculator_solve(1, str);

  switch(kind)
  {
  case DT_MIN:
    // to anyone askig FLT_MIN is superclose to 0, not furthest value
    // from 0 possible in float
    return _conf_isnan(value) ? -FLT_MAX : (float)value;
    break;
  case DT_MAX:
    return _conf_isnan(value) ? FLT_MAX : (float)value;
    break;
  default:
    break;
  }
  return _conf_isnan(value) ? 0.0f : (float)value;
}

gboolean dt_conf_is_default(const char *name)
{
  if(!dt_confgen_exists(name))
    return TRUE; // well if default doesn't know about it, it's default

  switch(dt_confgen_type(name))
  {
  case DT_INT:
    return dt_conf_get_int(name) == dt_confgen_get_int(name, DT_DEFAULT);
    break;
  case DT_INT64:
    return dt_conf_get_int64(name) == dt_confgen_get_int64(name, DT_DEFAULT);
    break;
  case DT_FLOAT:
    return dt_conf_get_float(name) == dt_confgen_get_float(name, DT_DEFAULT);
    break;
  case DT_BOOL:
    return dt_conf_get_bool(name) == dt_confgen_get_bool(name, DT_DEFAULT);
    break;
  case DT_PATH:
  case DT_STRING:
  case DT_ENUM:
  default:
    {
      const char *def_val = dt_confgen_get(name, DT_DEFAULT);
      const char *cur_val = _conf_get_var(name);
      return g_strcmp0(def_val, cur_val) == 0;
      break;
    }
  }
}

gchar* dt_conf_expand_default_dir(const char *dir)
{
  // expand special dirs
#define CONFIG_DIR "$(config)"
#define HOME_DIR   "$(home)"

  gchar *path = NULL;
  if(g_str_has_prefix(dir, CONFIG_DIR))
  {
    gchar configdir[PATH_MAX] = { 0 };
    dt_loc_get_user_config_dir(configdir, sizeof(configdir));
    path = g_strdup_printf("%s%s", configdir, dir + strlen(CONFIG_DIR));
  }
  else if(g_str_has_prefix(dir, HOME_DIR))
  {
    gchar *homedir = dt_loc_get_home_dir(NULL);
    path = g_strdup_printf("%s%s", homedir, dir + strlen(HOME_DIR));
    g_free(homedir);
  }
  else path = g_strdup(dir);

  gchar *normalized_path = dt_util_normalize_path(path);
  g_free(path);

  return normalized_path;
}

static void _conf_print(const gchar *key, const gchar *val, FILE *f)
{
  fprintf(f, "%s=%s\n", key, val);
}

static void _conf_save(dt_conf_t *cf)
{
  FILE *fc = g_fopen(cf->filename_common, "wb");
  FILE *fs = g_fopen(cf->filename, "wb");

  if(fs && fc)
  {
    GList *keys = g_hash_table_get_keys(cf->table);
    GList *sorted = g_list_sort(keys, (GCompareFunc)g_strcmp0);

    for(GList *iter = sorted; iter; iter = g_list_next(iter))
    {
      const gchar *key = (const gchar *)iter->data;
      const gchar *val = (const gchar *)g_hash_table_lookup(cf->table, key);

      FILE *f = dt_confgen_is_common(key) ? fc : fs;
      _conf_print(key, val, f);
    }

    g_list_free(sorted);
    fclose(fs);
    fclose(fc);
  }
}
void dt_conf_cleanup(dt_conf_t *cf)
{
  _conf_save(cf);
  g_hash_table_unref(cf->table);
  g_hash_table_unref(cf->override_entries);
  g_hash_table_unref(cf->x_confgen);
  dt_pthread_mutex_destroy(&darktable.conf->mutex);
}

// ── welcome-screen metadata ───────────────────────────────────────────────────

int dt_confgen_get_welcome_pagenum(const char *name)
{
  const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name);
  return item ? item->welcome_pagenum : 0;
}

gboolean dt_confgen_get_welcome_dirchooser(const char *name)
{
  const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name);
  return item ? item->welcome_dirchooser : FALSE;
}

const char *const *dt_confgen_get_welcome_options(const char *name)
{
  const dt_confgen_value_t *item = g_hash_table_lookup(darktable.conf->x_confgen, name);
  return item ? (const char *const *)item->welcome_options : NULL;
}

static gint _welcome_key_compare(gconstpointer a, gconstpointer b)
{
  const dt_confgen_value_t *ea =
    g_hash_table_lookup(darktable.conf->x_confgen, (const char *)a);
  const dt_confgen_value_t *eb =
    g_hash_table_lookup(darktable.conf->x_confgen, (const char *)b);
  if(!ea || !eb) return 0;
  if(ea->welcome_pagenum != eb->welcome_pagenum)
    return ea->welcome_pagenum - eb->welcome_pagenum;
  return ea->welcome_questionnum - eb->welcome_questionnum;
}

GList *dt_confgen_get_welcome_keys(void)
{
  GList *result = NULL;
  GHashTableIter iter;
  gpointer key, value;
  g_hash_table_iter_init(&iter, darktable.conf->x_confgen);
  while(g_hash_table_iter_next(&iter, &key, &value))
  {
    const dt_confgen_value_t *entry = value;
    if(entry->welcome_pagenum > 0)
      result = g_list_prepend(result, g_strdup((const char *)key));
  }
  result = g_list_sort(result, _welcome_key_compare);

  // validate page and question numbering: pages must start at 1 with no gaps;
  // questions on each page must start at 1 with no gaps.
  int expected_page = 1;
  int expected_question = 1;
  int cur_page = 0;
  for(GList *l = result; l; l = l->next)
  {
    const char *k = l->data;
    const dt_confgen_value_t *e = g_hash_table_lookup(darktable.conf->x_confgen, k);
    if(!e) continue;

    if(e->welcome_pagenum != cur_page)
    {
      if(e->welcome_pagenum != expected_page)
        fprintf(stderr, "[confgen] welcome screen: expected page %d but got page %d (key: %s)\n",
                expected_page, e->welcome_pagenum, k);
      cur_page = e->welcome_pagenum;
      expected_page = cur_page + 1;
      expected_question = 1;
    }

    if(e->welcome_questionnum != expected_question)
      fprintf(stderr, "[confgen] welcome screen: on page %d expected question %d but got %d (key: %s)\n",
              cur_page, expected_question, e->welcome_questionnum, k);
    expected_question = e->welcome_questionnum + 1;
  }

  return result;
}

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