/*
    This file is part of darktable,
    Copyright (C) 2011-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 "common/collection.h"
#include "common/darktable.h"
#include "common/debug.h"
#include "common/image_cache.h"
#include "common/ratings.h"
#include "common/undo.h"
#include "common/grouping.h"
#include "views/view.h"
#include "control/conf.h"
#include "control/control.h"
#include "gui/gtk.h"
#include "gui/accelerators.h"

#define DT_RATINGS_UPGRADE -1
#define DT_RATINGS_DOWNGRADE -2
#define DT_RATINGS_REJECT -3
#define DT_RATINGS_UNREJECT -4

typedef struct dt_undo_ratings_t
{
  dt_imgid_t imgid;
  int before;
  int after;
} dt_undo_ratings_t;

int dt_ratings_get(const dt_imgid_t imgid)
{
  int stars = 0;
  dt_image_t *image = dt_image_cache_get(imgid, 'r');
  if(image)
  {
    if(image->flags & DT_IMAGE_REJECTED)
      stars = DT_VIEW_REJECT;
    else
      stars = DT_VIEW_RATINGS_MASK & image->flags;
    dt_image_cache_read_release(image);
  }
  return stars;
}

static void _ratings_apply_to_image(const dt_imgid_t imgid, const int rating)
{
  int new_rating = rating;
  dt_image_t *image = dt_image_cache_get(imgid, 'w');

  if(image)
  {
    // apply or remove rejection
    if(new_rating == DT_RATINGS_REJECT)
      image->flags = (image->flags | DT_IMAGE_REJECTED);
    else if(new_rating == DT_RATINGS_UNREJECT)
      image->flags = (image->flags & ~DT_IMAGE_REJECTED);
    else
    {
      image->flags = (image->flags & ~(DT_IMAGE_REJECTED | DT_VIEW_RATINGS_MASK))
        | (DT_VIEW_RATINGS_MASK & new_rating);
    }
    // synch through:
    dt_image_cache_write_release_info(image, DT_IMAGE_CACHE_SAFE, "_ratings_apply_to_image");
  }
}

static void _pop_undo(gpointer user_data, dt_undo_type_t type, dt_undo_data_t data, dt_undo_action_t action, GList **imgs)
{
  if(type == DT_UNDO_RATINGS)
  {
    for(GList *list = (GList *)data; list; list = g_list_next(list))
    {
      dt_undo_ratings_t *ratings = list->data;
      _ratings_apply_to_image(ratings->imgid, (action == DT_ACTION_UNDO) ? ratings->before : ratings->after);
      *imgs = g_list_prepend(*imgs, GINT_TO_POINTER(ratings->imgid));
    }
    dt_collection_hint_message(darktable.collection);
  }
}

static void _ratings_undo_data_free(gpointer data)
{
  GList *l = (GList *)data;
  g_list_free(l);
}

// wrapper that does some precalculation to deal with toggle effects and rating increase/decrease
static void _ratings_apply(const GList *imgs,
                           const int rating,
                           GList **undo,
                           const gboolean undo_on)
{
  // REJECTION and SINGLE_STAR rating can have a toggle effect
  // but we only toggle off if ALL images have that rating
  // so we need to check every image first
  gboolean toggle = FALSE;

  if(rating == DT_VIEW_REJECT)
  {
    toggle = TRUE;
    for(const GList *images = imgs; images; images = g_list_next(images))
    {
      if(dt_ratings_get(GPOINTER_TO_INT(images->data)) != DT_VIEW_REJECT)
      {
        toggle = FALSE;
        break;
      }
    }
  }
  else if(!dt_conf_get_bool("rating_one_double_tap") && (rating == DT_VIEW_STAR_1))
  {
    toggle = TRUE;
    for(const GList *images = imgs; images; images = g_list_next(images))
    {
      if(dt_ratings_get(GPOINTER_TO_INT(images->data)) != DT_VIEW_STAR_1)
      {
        toggle = FALSE;
        break;
      }
    }
  }

  if(!g_list_shorter_than(imgs, 2)) // pop up a toast if rating multiple images at once
  {
    const guint count = g_list_length((GList *) imgs);
    if(rating == DT_VIEW_REJECT)
      dt_control_log(ngettext("rejecting %d image", "rejecting %d images", count), count);
    else
      dt_control_log(ngettext("applying rating %d to %d image", "applying rating %d to %d images", count),
                     rating, count);
    // process all pending events to ensure that the toast is actually shown right away
    dt_gui_process_events();
  }

  for(const GList *images = imgs; images; images = g_list_next(images))
  {
    const dt_imgid_t image_id = GPOINTER_TO_INT(images->data);
    const int old_rating = dt_ratings_get(image_id);
    if(undo_on)
    {
      dt_undo_ratings_t *undoratings = malloc(sizeof(dt_undo_ratings_t));
      undoratings->imgid = image_id;
      undoratings->before = old_rating;
      undoratings->after = rating;
      *undo = g_list_append(*undo, undoratings);
    }

    int new_rating = rating;
    // do not 'DT_RATINGS_UPGRADE' or 'DT_RATINGS_UPGRADE' if image was rejected
    if(old_rating == DT_VIEW_REJECT && rating < DT_VIEW_DESERT)
      new_rating = DT_VIEW_REJECT;
    else if(rating == DT_RATINGS_UPGRADE)
      new_rating = MIN(DT_VIEW_STAR_5, old_rating + 1);
    else if(rating == DT_RATINGS_DOWNGRADE)
      new_rating = MAX(DT_VIEW_DESERT, old_rating - 1);
    else if(rating == DT_VIEW_STAR_1 && toggle)
      new_rating = DT_VIEW_DESERT;
    else if(rating == DT_VIEW_REJECT && toggle)
      new_rating = DT_RATINGS_UNREJECT;
    else if(rating == DT_VIEW_REJECT && !toggle)
      new_rating = DT_RATINGS_REJECT;

    _ratings_apply_to_image(image_id, new_rating);
  }
}

void dt_ratings_apply_on_list(const GList *img,
                              const int rating,
                              const gboolean undo_on)
{
  if(!g_list_is_empty(img))
  {
    dt_gui_cursor_set_busy();
    GList *undo = NULL;
    if(undo_on) dt_undo_start_group(darktable.undo, DT_UNDO_RATINGS);

    _ratings_apply(img, rating, &undo, undo_on);

    if(undo_on)
    {
      dt_undo_record(darktable.undo, NULL, DT_UNDO_RATINGS, undo, _pop_undo, _ratings_undo_data_free);
      dt_undo_end_group(darktable.undo);
    }
    dt_gui_cursor_clear_busy();
    dt_collection_hint_message(darktable.collection);
  }
}

void dt_ratings_apply_on_image(const dt_imgid_t imgid,
                               const int rating,
                               const gboolean single_star_toggle,
                               const gboolean undo_on,
                               const gboolean group_on)
{
  GList *imgs = NULL;
  int new_rating = rating;

  if(dt_is_valid_imgid(imgid))
    imgs = g_list_prepend(imgs, GINT_TO_POINTER(imgid));

  if(!g_list_is_empty(imgs))
  {
    GList *undo = NULL;
    if(undo_on) dt_undo_start_group(darktable.undo, DT_UNDO_RATINGS);
    if(group_on) dt_grouping_add_grouped_images(&imgs);

    _ratings_apply(imgs, new_rating, &undo, undo_on);

    if(undo_on)
    {
      dt_undo_record(darktable.undo, NULL,
                     DT_UNDO_RATINGS, undo, _pop_undo, _ratings_undo_data_free);
      dt_undo_end_group(darktable.undo);
    }
    g_list_free(imgs);
  }
  else
    dt_control_log(_("no images selected to apply rating"));
}

enum
{
  DT_ACTION_EFFECT_SELECT = DT_ACTION_EFFECT_DEFAULT_KEY,
  DT_ACTION_EFFECT_UPGRADE = DT_ACTION_EFFECT_DEFAULT_UP,
  DT_ACTION_EFFECT_DOWNGRADE = DT_ACTION_EFFECT_DEFAULT_DOWN,
};

static float _action_process_rating(gpointer target,
                                    dt_action_element_t element,
                                    dt_action_effect_t effect,
                                    float move_size)
{
  float return_value = DT_ACTION_NOT_VALID;

  if(DT_PERFORM_ACTION(move_size))
  {
    dt_gui_cursor_set_busy();
    if(element != DT_VIEW_REJECT)
    {
      switch(effect)
      {
      case DT_ACTION_EFFECT_SELECT:
        break;
      case DT_ACTION_EFFECT_UPGRADE:
        element = DT_RATINGS_UPGRADE;
        break;
      case DT_ACTION_EFFECT_DOWNGRADE:
        element = DT_RATINGS_DOWNGRADE;
        break;
      default:
        dt_print(DT_DEBUG_ALWAYS,
                 "[_action_process_rating] unknown shortcut effect (%d) for rating", effect);
        break;
      }
    }

    GList *imgs = dt_act_on_get_images(FALSE, TRUE, FALSE);
    dt_ratings_apply_on_list(imgs, element, TRUE);

    // if we are in darkroom we show a message as there might be no other indication
    if(dt_view_get_current() == DT_VIEW_DARKROOM && g_list_is_singleton(imgs) && darktable.develop->preview_pipe)
    {
      // we verify that the image is the active one
      const int id = GPOINTER_TO_INT(imgs->data);
      if(id == darktable.develop->preview_pipe->output_imgid)
      {
        const dt_image_t *img = dt_image_cache_get(id, 'r');
        if(img)
        {
          const int r = img->flags & DT_IMAGE_REJECTED ? DT_VIEW_REJECT : (img->flags & DT_VIEW_RATINGS_MASK);
          dt_image_cache_read_release(img);

          // translate in human readable value
          if(r == DT_VIEW_REJECT)
            dt_toast_log(_("image rejected"));
          else if(r == 0)
            dt_toast_log(_("image rated to 0 star"));
          else
            dt_toast_log(_("image rated to %s"), r == 1 ? "★" :
                                                 r == 2 ? "★★" :
                                                 r == 3 ? "★★★" :
                                                 r == 4 ? "★★★★" :
                                                 r == 5 ? "★★★★★" :
                                                 _("unknown"));
          return_value = - r + (r >= element ? DT_VALUE_PATTERN_ACTIVE : 0);
        }
      }
    }

    dt_collection_update_query(darktable.collection, DT_COLLECTION_CHANGE_RELOAD,
                               DT_COLLECTION_PROP_RATING_RANGE, imgs);
    dt_gui_cursor_clear_busy();
  }
  else if(darktable.develop)
  {
    const dt_imgid_t image_id = darktable.develop->image_storage.id;
    if(dt_is_valid_imgid(image_id))
    {
      int rating = dt_ratings_get(image_id);
      return_value = - rating + (rating >= element ? DT_VALUE_PATTERN_ACTIVE : 0);
    }
  }

  return return_value + DT_VALUE_PATTERN_SUM;
}

const gchar *dt_action_effect_rating[]
  = { N_("select"),
      N_("upgrade"),
      N_("downgrade"),
      NULL };

const dt_action_element_def_t _action_elements_rating[]
  = { { N_("zero"  ), dt_action_effect_rating },
      { N_("one"   ), dt_action_effect_rating },
      { N_("two"   ), dt_action_effect_rating },
      { N_("three" ), dt_action_effect_rating },
      { N_("four"  ), dt_action_effect_rating },
      { N_("five"  ), dt_action_effect_rating },
      { N_("reject"), dt_action_effect_activate },
      { NULL } };

const dt_action_def_t dt_action_def_rating
  = { N_("rating"),
      _action_process_rating,
      _action_elements_rating };

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