/*
    This file is part of darktable,
    Copyright (C) 2012-2024 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 "gtkentry.h"
#include "common/darktable.h"

typedef struct completion_spec
{
  gchar *varname;
  gchar *description;
} dt_gtkentry_completion_spec;

typedef enum
{
  COMPL_ID = 0,
  COMPL_VARNAME,
  COMPL_DESCRIPTION
} dtGtkEntryCompletionSpecCol;


static dt_gtkentry_completion_spec _default_path_compl_list[]
  = { { "ROLL.NAME[]", N_("$(ROLL.NAME[n]) - roll of the input image, n: levels used") },
      { "FILE.FOLDER", N_("$(FILE.FOLDER) - folder containing the input image") },
      { "FILE.NAME", N_("$(FILE.NAME) - basename of the input image") },
      { "FILE.EXTENSION", N_("$(FILE.EXTENSION) - extension of the input image") },
      { "VERSION", N_("$(VERSION) - duplicate version") },
      { "VERSION.IF_MULTI", N_("$(VERSION.IF_MULTI) - same as $(VERSION) but null string if only one version exists") },
      { "VERSION.NAME", N_("$(VERSION.NAME) - version name from metadata") },
      { "JOBCODE", N_("$(JOBCODE) - job code for import") },
      { "SEQUENCE[4,1]", N_("$(SEQUENCE[n,m]) - sequence number, n: number of digits, m: start number") },
      { "WIDTH.MAX", N_("$(WIDTH.MAX) - maximum image export width") },
      { "WIDTH.SENSOR", N_("$(WIDTH.SENSOR) - image sensor width") },
      { "WIDTH.RAW", N_("$(WIDTH.RAW) - RAW image width") },
      { "WIDTH.CROP", N_("$(WIDTH.CROP) - image width after crop") },
      { "WIDTH.EXPORT", N_("$(WIDTH.EXPORT) - exported image width") },
      { "HEIGHT.MAX", N_("$(HEIGHT.MAX) - maximum image export height") },
      { "HEIGHT.SENSOR", N_("$(HEIGHT.SENSOR) - image sensor height") },
      { "HEIGHT.RAW", N_("$(HEIGHT.RAW) - RAW image height") },
      { "HEIGHT.CROP", N_("$(HEIGHT.CROP) - image height after crop") },
      { "HEIGHT.EXPORT", N_("$(HEIGHT.EXPORT) - exported image height") },
      { "YEAR", N_("$(YEAR) - year") },
      { "YEAR.SHORT", N_("$(YEAR.SHORT) - year without century") },
      { "MONTH", N_("$(MONTH) - month") },
      { "MONTH.SHORT", N_("$(MONTH.SHORT) - abbreviated month name according to the current locale") },
      { "MONTH.LONG", N_("$(MONTH.LONG) - full month name according to the current locale") },
      { "DAY", N_("$(DAY) - day") },
      { "HOUR", N_("$(HOUR) - hour") },
      { "HOUR.AMPM", N_("$(HOUR.AMPM) - hour, 12-hour clock") },
      { "MINUTE", N_("$(MINUTE) - minute") },
      { "SECOND", N_("$(SECOND) - second") },
      { "MSEC", N_("$(MSEC) - millisecond") },
      { "EXIF.DATE.REGIONAL", N_("$(EXIF.DATE.REGIONAL) - localized EXIF date") },
      { "EXIF.TIME.REGIONAL", N_("$(EXIF.TIME.REGIONAL) - localized EXIF time") },
      { "EXIF.YEAR", N_("$(EXIF.YEAR) - EXIF year") },
      { "EXIF.YEAR.SHORT", N_("$(EXIF.YEAR.SHORT) - EXIF year without century") },
      { "EXIF.MONTH", N_("$(EXIF.MONTH) - EXIF month") },
      { "EXIF.MONTH.SHORT", N_("$(EXIF.MONTH.SHORT) - abbreviated EXIF month name according to the current locale") },
      { "EXIF.MONTH.LONG", N_("$(EXIF.MONTH.LONG) - full EXIF month name according to the current locale") },
      { "EXIF.DAY", N_("$(EXIF.DAY) - EXIF day") },
      { "EXIF.HOUR", N_("$(EXIF.HOUR) - EXIF hour") },
      { "EXIF.HOUR.AMPM", N_("$(EXIF.HOUR.AMPM) - EXIF hour, 12-hour clock") },
      { "EXIF.MINUTE", N_("$(EXIF.MINUTE) - EXIF minute") },
      { "EXIF.SECOND", N_("$(EXIF.SECOND) - EXIF second") },
      { "EXIF.MSEC", N_("$(EXIF.MSEC) - EXIF millisecond") },
      { "EXIF.ISO", N_("$(EXIF.ISO) - ISO value") },
      { "EXIF.EXPOSURE.BIAS", N_("$(EXIF.EXPOSURE.BIAS) - EXIF exposure bias") },
      { "EXIF.EXPOSURE.PROGRAM", N_("$(EXIF.EXPOSURE.PROGRAM) - EXIF exposure program") },
      { "EXIF.EXPOSURE", N_("$(EXIF.EXPOSURE) - EXIF exposure") },
      { "EXIF.APERTURE", N_("$(EXIF.APERTURE) - EXIF aperture") },
      { "EXIF.CROP_FACTOR", N_("$(EXIF.CROP_FACTOR) - EXIF crop factor") },
      { "EXIF.FOCAL.LENGTH", N_("$(EXIF.FOCAL.LENGTH) - EXIF focal length") },
      { "EXIF.FOCAL.LENGTH.EQUIV", N_("$(EXIF.FOCAL.LENGTH.EQUIV) - EXIF 35 mm equivalent focal length") },
      { "EXIF.FOCUS.DISTANCE", N_("$(EXIF.FOCUS.DISTANCE) - EXIF focal distance") },
      { "EXIF.MAKER", N_("$(EXIF.MAKER) - camera maker") },
      { "EXIF.MODEL", N_("$(EXIF.MODEL) - camera model") },
      { "EXIF.WHITEBALANCE", N_("$(EXIF.WHITEBALANCE) - EXIF selected white balance") },
      { "EXIF.METERING", N_("$(EXIF.METERING) - EXIF exposure metering mode") },
      { "EXIF.LENS", N_("$(EXIF.LENS) - lens") },
      { "EXIF.FLASH.ICON", N_("$(EXIF.FLASH.ICON) - icon indicating whether flash was used") },
      { "EXIF.FLASH", N_("$(EXIF.FLASH) - was flash used (yes/no/--)") },
      { "GPS.LONGITUDE", N_("$(GPS.LONGITUDE) - longitude") },
      { "GPS.LATITUDE", N_("$(GPS.LATITUDE) - latitude") },
      { "GPS.ELEVATION", N_("$(GPS.ELEVATION) - elevation") },
      { "GPS.LOCATION.ICON", N_("$(GPS.LOCATION.ICON) - icon indicating whether GPS location is known") },
      { "LONGITUDE", N_("$(LONGITUDE) - longitude") },
      { "LATITUDE", N_("$(LATITUDE) - latitude") },
      { "ELEVATION", N_("$(ELEVATION) - elevation") },
      { "STARS", N_("$(STARS) - star rating as number (-1 for rejected)") },
      { "RATING.ICONS", N_("$(RATING.ICONS) - star/reject rating in icon form") },
      { "LABELS", N_("$(LABELS) - color labels as text") },
      { "LABELS.ICONS", N_("$(LABELS.ICONS) - color labels as icons") },
      { "IMAGE.TAGS.HIERARCHY", N_("$(IMAGE.TAGS.HIERARCHY) - tags as set in metadata settings, preserving hierarchy") },
      { "IMAGE.TAGS", N_("$(IMAGE.TAGS) - tags as set in metadata settings, flattened") },
      { "IMAGE.ID", N_("$(IMAGE.ID) - image ID") },
      { "IMAGE.ID.NEXT", N_("$(IMAGE.ID.NEXT) - next image ID to be assigned on import") },
      { "ID", N_("$(ID) - image ID") },
      { "USERNAME", N_("$(USERNAME) - login name") },
      { "FOLDER.PICTURES", N_("$(FOLDER.PICTURES) - pictures folder") },
      { "FOLDER.HOME", N_("$(FOLDER.HOME) - home folder") },
      { "FOLDER.DESKTOP", N_("$(FOLDER.DESKTOP) - desktop folder") },
      { "OPENCL.ACTIVATED", N_("$(OPENCL.ACTIVATED) - whether OpenCL is activated") },
      { "CATEGORY[,]", N_("$(CATEGORY[n,category]) - subtag of level n in hierarchical tags") },
      { "TAGS", N_("$(TAGS) - tags as set in metadata settings") },
      { "DARKTABLE.NAME", N_("$(DARKTABLE.NAME) - darktable name") },
      { "DARKTABLE.VERSION", N_("$(DARKTABLE.VERSION) - current darktable version") },
      { "SIDECAR_TXT", N_("$(SIDECAR_TXT) - contents of .txt sidecar file, if present") },
      { "WORKSPACE.LABEL", N_("$(WORKSPACE.LABEL) - label of the current workspace") },
      { NULL, NULL } };


static GtkListStore *_completion_model = NULL;


/**
 * Called when the user selects an entry from the autocomplete list.
 *
 * @param[in] widget
 * @param[in] model     Data structure containing autocomplete strings.
 * @param[in] iter      Pointer into data structure.
 * @param[in] user_data unused here
 *
 * @return Currently always true
 */
static gboolean _on_match_select(GtkEntryCompletion *widget,
                                 GtkTreeModel *model,
                                 GtkTreeIter *iter,
                                 gpointer user_data)
{

  const gchar *varname;
  GtkEditable *e = (GtkEditable *)gtk_entry_completion_get_entry(widget);
  gchar *s = gtk_editable_get_chars(e, 0, -1);
  gint cur_pos = gtk_editable_get_position(e);
  gint p = cur_pos;

  GValue value = {
    0,
  };
  gtk_tree_model_get_value(model, iter, COMPL_VARNAME, &value);
  varname = g_value_get_string(&value);

  for(p = cur_pos; p - 2 > 0; p--)
  {
    if(strncmp(s + p - 2, "$(", 2) == 0)
    {
      break;
    }
  }

  size_t text_len = strlen(varname) + 2;
  gchar *addtext = (gchar *)g_malloc(text_len);
  snprintf(addtext, text_len, "%s)", varname);

  gtk_editable_delete_text(e, p, cur_pos);
  gtk_editable_insert_text(e, addtext, -1, &p);
  gtk_editable_set_position(e, p);
  g_value_unset(&value);
  g_free(addtext);
  g_free(s);
  return TRUE;
}

/**
 * Case insensitive substring search for a completion match.
 *
 * Based on the default matching function in GtkEntryCompletion.
 *
 * This function is called once for each iter in the GtkEntryCompletion's
 * list of completion entries (model).
 *
 * @param completion Completion object to apply this function on
 * @param key        Complete string from the GtkEntry.
 * @param iter       Item in list of autocomplete database to compare key against.
 * @param user_data  Unused.
 */
static gboolean _on_match_func(GtkEntryCompletion *completion,
                               const gchar *key,
                               GtkTreeIter *iter,
                               gpointer user_data)
{
  gboolean ret = FALSE;

  GtkEditable *e = (GtkEditable *)gtk_entry_completion_get_entry(completion);
  gint cur_pos = gtk_editable_get_position(e); /* returns 1..* */
  gint var_start = 0;
  gboolean var_present = FALSE;

  for(gint p = cur_pos; p >= 0; p--)
  {
    gchar *ss = gtk_editable_get_chars(e, p, cur_pos);
    if(strncmp(ss, "$(", 2) == 0)
    {
      var_start = p + 2;
      var_present = TRUE;
      g_free(ss);
      break;
    }
    g_free(ss);
  }

  if(var_present)
  {
    gchar *varname = gtk_editable_get_chars(e, var_start, cur_pos);

    GtkTreeModel *model = gtk_entry_completion_get_model(completion);
    gchar *item = NULL;
    gtk_tree_model_get(model, iter, COMPL_VARNAME, &item, -1);

    if(item != NULL)
    {
      // Do utf8-safe case insensitive string compare.
      // Shamelessly stolen from GtkEntryCompletion.
      gchar *normalized_string = g_utf8_normalize(item, -1, G_NORMALIZE_ALL);

      if(normalized_string != NULL)
      {
        gchar *case_normalized_string = g_utf8_casefold(normalized_string, -1);

        if(!g_ascii_strncasecmp(varname, case_normalized_string, strlen(varname))) ret = TRUE;

        g_free(case_normalized_string);
      }
      g_free(normalized_string);
    }
    g_free(varname);
    g_free(item);
  }

  return ret;
}

static void _init_completion_model()
{
  _completion_model = gtk_list_store_new(3,
                                         G_TYPE_INT,
                                         G_TYPE_STRING,
                                         G_TYPE_STRING);
  /* Populate the completion database. */
  for(const dt_gtkentry_completion_spec *l = _default_path_compl_list; l && l->varname; l++)
  {
    gtk_list_store_insert_with_values(_completion_model, NULL, -1,
                       COMPL_ID, -1,  // -1 = internal
                       COMPL_VARNAME, l->varname,
                       COMPL_DESCRIPTION, _(l->description),
                       -1);
  }

  // metadata
  dt_pthread_mutex_lock(&darktable.metadata_threadsafe);
  for(GList *md_iter = dt_metadata_get_list(); md_iter; md_iter = md_iter->next)
  {
    dt_metadata_t *metadata = (dt_metadata_t *)md_iter->data;
    if(!metadata->internal)
      dt_gtkentry_variables_add_metadata(metadata);
  }
  dt_pthread_mutex_unlock(&darktable.metadata_threadsafe);
}

void dt_gtkentry_setup_variables_completion(GtkEntry *entry)
{
  if(!_completion_model)
    _init_completion_model();

  GtkEntryCompletion *completion = gtk_entry_completion_new();
  gtk_entry_completion_set_text_column(completion, COMPL_DESCRIPTION);
  gtk_entry_set_completion(entry, completion);
  g_signal_connect(G_OBJECT(completion), "match-selected", G_CALLBACK(_on_match_select), NULL);

  gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(_completion_model));
  gtk_entry_completion_set_match_func(completion, _on_match_func, NULL, NULL);
}

void dt_gtkentry_variables_add_metadata(dt_metadata_t *metadata)
{
  gchar *varname = g_strdup(metadata->tagname);
  gchar *description = g_strdup_printf("$(%s) - %s", varname, _("from metadata"));
  gtk_list_store_insert_with_values(_completion_model, NULL, -1,
                      COMPL_ID, metadata->key,
                      COMPL_VARNAME, varname,
                      COMPL_DESCRIPTION, description,
                      -1);
  g_free(varname);
  g_free(description);
}

void dt_gtkentry_variables_remove_metadata(dt_metadata_t *metadata)
{
  GtkTreeIter iter;

  gboolean valid = gtk_tree_model_get_iter_first(GTK_TREE_MODEL(_completion_model), &iter);
  while(valid)
  {
    int32_t id;
    gtk_tree_model_get(GTK_TREE_MODEL(_completion_model), &iter,
                       COMPL_ID, &id,
                       -1);

    if(id == metadata->key)
    {
      gtk_list_store_remove(_completion_model, &iter);
      break;
    }

    valid = gtk_tree_model_iter_next(GTK_TREE_MODEL(_completion_model), &iter);
  }
}


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