/* PIKA - Photo and Image Kooker Application
 * a rebranding of The GNU Image Manipulation Program (created with heckimp)
 * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
 *
 * Original copyright, applying to most contents (license remains unchanged): 
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <gegl.h>
#include <gtk/gtk.h>

#include "libpikawidgets/pikawidgets.h"

#include "tools-types.h"

#include "core/pikachannel.h"
#include "core/pikaerror.h"
#include "core/pikaimage.h"
#include "core/pikaimage-pick-item.h"
#include "core/pikaimage-undo.h"
#include "core/pikapickable.h"
#include "core/pikaundostack.h"

#include "display/pikadisplay.h"
#include "display/pikadisplayshell-appearance.h"

#include "widgets/pikawidgets-utils.h"

#include "pikaeditselectiontool.h"
#include "pikaselectiontool.h"
#include "pikaselectionoptions.h"
#include "pikatoolcontrol.h"
#include "pikatools-utils.h"

#include "pika-intl.h"


static void       pika_selection_tool_control             (PikaTool           *tool,
                                                           PikaToolAction      action,
                                                           PikaDisplay        *display);
static void       pika_selection_tool_modifier_key        (PikaTool           *tool,
                                                           GdkModifierType     key,
                                                           gboolean            press,
                                                           GdkModifierType     state,
                                                           PikaDisplay        *display);
static void       pika_selection_tool_oper_update         (PikaTool           *tool,
                                                           const PikaCoords   *coords,
                                                           GdkModifierType     state,
                                                           gboolean            proximity,
                                                           PikaDisplay        *display);
static void       pika_selection_tool_cursor_update       (PikaTool           *tool,
                                                           const PikaCoords   *coords,
                                                           GdkModifierType     state,
                                                           PikaDisplay        *display);

static gboolean   pika_selection_tool_real_have_selection (PikaSelectionTool  *sel_tool,
                                                           PikaDisplay        *display);

static void       pika_selection_tool_commit              (PikaSelectionTool  *sel_tool);
static void       pika_selection_tool_halt                (PikaSelectionTool  *sel_tool,
                                                           PikaDisplay        *display);

static gboolean   pika_selection_tool_check               (PikaSelectionTool  *sel_tool,
                                                           PikaDisplay        *display,
                                                           GError            **error);

static gboolean   pika_selection_tool_have_selection      (PikaSelectionTool  *sel_tool,
                                                           PikaDisplay        *display);


G_DEFINE_TYPE (PikaSelectionTool, pika_selection_tool, PIKA_TYPE_DRAW_TOOL)

#define parent_class pika_selection_tool_parent_class


static void
pika_selection_tool_class_init (PikaSelectionToolClass *klass)
{
  PikaToolClass *tool_class = PIKA_TOOL_CLASS (klass);

  tool_class->control       = pika_selection_tool_control;
  tool_class->modifier_key  = pika_selection_tool_modifier_key;
  tool_class->key_press     = pika_edit_selection_tool_key_press;
  tool_class->oper_update   = pika_selection_tool_oper_update;
  tool_class->cursor_update = pika_selection_tool_cursor_update;

  klass->have_selection     = pika_selection_tool_real_have_selection;
}

static void
pika_selection_tool_init (PikaSelectionTool *selection_tool)
{
  selection_tool->function             = SELECTION_SELECT;
  selection_tool->saved_operation      = PIKA_CHANNEL_OP_REPLACE;

  selection_tool->saved_show_selection = FALSE;
  selection_tool->undo                 = NULL;
  selection_tool->redo                 = NULL;
  selection_tool->idle_id              = 0;

  selection_tool->allow_move           = TRUE;
}

static void
pika_selection_tool_control (PikaTool       *tool,
                             PikaToolAction  action,
                             PikaDisplay    *display)
{
  PikaSelectionTool *selection_tool = PIKA_SELECTION_TOOL (tool);

  switch (action)
    {
    case PIKA_TOOL_ACTION_PAUSE:
    case PIKA_TOOL_ACTION_RESUME:
      break;

    case PIKA_TOOL_ACTION_HALT:
      pika_selection_tool_halt (selection_tool, display);
      break;

    case PIKA_TOOL_ACTION_COMMIT:
      pika_selection_tool_commit (selection_tool);
      break;
    }

  PIKA_TOOL_CLASS (parent_class)->control (tool, action, display);
}

static void
pika_selection_tool_modifier_key (PikaTool        *tool,
                                  GdkModifierType  key,
                                  gboolean         press,
                                  GdkModifierType  state,
                                  PikaDisplay     *display)
{
  PikaSelectionTool    *selection_tool = PIKA_SELECTION_TOOL (tool);
  PikaSelectionOptions *options        = PIKA_SELECTION_TOOL_GET_OPTIONS (tool);
  GdkModifierType       extend_mask;
  GdkModifierType       modify_mask;

  extend_mask = pika_get_extend_selection_mask ();
  modify_mask = pika_get_modify_selection_mask ();

  if (key == extend_mask ||
      key == modify_mask ||
      key == GDK_MOD1_MASK)
    {
      PikaChannelOps button_op = options->operation;

      state &= extend_mask |
               modify_mask |
               GDK_MOD1_MASK;

      if (press)
        {
          if (key == state ||
              /* PikaPolygonSelectTool may mask-out part of the state, which
               * can cause the wrong mode to be restored on release if we don't
               * init saved_operation here.
               *
               * see issue #4992.
               */
              ! state)
            {
              /*  first modifier pressed  */

              selection_tool->saved_operation = options->operation;
            }
        }
      else
        {
          if (! state)
            {
              /*  last modifier released  */

              button_op = selection_tool->saved_operation;
            }
        }

      if (state & GDK_MOD1_MASK)
        {
          /*  if alt is down, pretend that neither
           *  shift nor control are down
           */
          button_op = selection_tool->saved_operation;
        }
      else if (state & (extend_mask |
                        modify_mask))
        {
          /*  else get the operation from the modifier state, but only
           *  if there is actually a modifier pressed, so we don't
           *  override the "last modifier released" assignment above
           */
          button_op = pika_modifiers_to_channel_op (state);
        }

      if (button_op != options->operation)
        {
          g_object_set (options, "operation", button_op, NULL);
        }
    }
}

static void
pika_selection_tool_oper_update (PikaTool         *tool,
                                 const PikaCoords *coords,
                                 GdkModifierType   state,
                                 gboolean          proximity,
                                 PikaDisplay      *display)
{
  PikaSelectionTool    *selection_tool = PIKA_SELECTION_TOOL (tool);
  PikaSelectionOptions *options        = PIKA_SELECTION_TOOL_GET_OPTIONS (tool);
  PikaImage            *image;
  GList                *drawables;
  PikaLayer            *layer;
  PikaLayer            *floating_sel;
  GdkModifierType       extend_mask;
  GdkModifierType       modify_mask;
  gboolean              have_selection;
  gboolean              move_layer        = FALSE;
  gboolean              move_floating_sel = FALSE;

  image        = pika_display_get_image (display);
  drawables    = pika_image_get_selected_drawables (image);
  layer        = pika_image_pick_layer (image, coords->x, coords->y, NULL);
  floating_sel = pika_image_get_floating_selection (image);

  extend_mask = pika_get_extend_selection_mask ();
  modify_mask = pika_get_modify_selection_mask ();

  have_selection = pika_selection_tool_have_selection (selection_tool, display);

  if (drawables)
    {
      if (floating_sel)
        {
          if (layer == floating_sel)
            move_floating_sel = TRUE;
        }
      else if (have_selection)
        {
          GList *iter;

          for (iter = drawables; iter; iter = iter->next)
            {
              if (pika_item_mask_intersect (PIKA_ITEM (iter->data),
                                            NULL, NULL, NULL, NULL))
                {
                  move_layer = TRUE;
                  break;
                }
            }
        }

      g_list_free (drawables);
    }

  selection_tool->function = SELECTION_SELECT;

  if (selection_tool->allow_move &&
      (state & GDK_MOD1_MASK) && (state & modify_mask) && move_layer)
    {
      /* move the selection */
      selection_tool->function = SELECTION_MOVE;
    }
  else if (selection_tool->allow_move &&
           (state & GDK_MOD1_MASK) && (state & extend_mask) && move_layer)
    {
      /* move a copy of the selection */
      selection_tool->function = SELECTION_MOVE_COPY;
    }
  else if (selection_tool->allow_move &&
           (state & GDK_MOD1_MASK) && have_selection)
    {
      /* move the selection mask */
      selection_tool->function = SELECTION_MOVE_MASK;
    }
  else if (selection_tool->allow_move &&
           ! (state & (extend_mask | modify_mask)) &&
           move_floating_sel)
    {
      /* move the selection */
      selection_tool->function = SELECTION_MOVE;
    }
  else if ((state & modify_mask) || (state & extend_mask))
    {
      /* select */
      selection_tool->function = SELECTION_SELECT;
    }
  else if (floating_sel)
    {
      /* anchor the selection */
      selection_tool->function = SELECTION_ANCHOR;
    }

  pika_tool_pop_status (tool, display);

  if (proximity)
    {
      const gchar     *status      = NULL;
      gboolean         free_status = FALSE;
      GdkModifierType  modifiers   = (extend_mask | modify_mask);

      if (have_selection)
        modifiers |= GDK_MOD1_MASK;

      switch (selection_tool->function)
        {
        case SELECTION_SELECT:
          switch (options->operation)
            {
            case PIKA_CHANNEL_OP_REPLACE:
              if (have_selection)
                {
                  status = pika_suggest_modifiers (_("Click-Drag to replace the "
                                                     "current selection"),
                                                   modifiers & ~state,
                                                   NULL, NULL, NULL);
                  free_status = TRUE;
                }
              else
                {
                  status = _("Click-Drag to create a new selection");
                }
              break;

            case PIKA_CHANNEL_OP_ADD:
              status = pika_suggest_modifiers (_("Click-Drag to add to the "
                                                 "current selection"),
                                               modifiers
                                               & ~(state | extend_mask),
                                               NULL, NULL, NULL);
              free_status = TRUE;
              break;

            case PIKA_CHANNEL_OP_SUBTRACT:
              status = pika_suggest_modifiers (_("Click-Drag to subtract from the "
                                                 "current selection"),
                                               modifiers
                                               & ~(state | modify_mask),
                                               NULL, NULL, NULL);
              free_status = TRUE;
              break;

            case PIKA_CHANNEL_OP_INTERSECT:
              status = pika_suggest_modifiers (_("Click-Drag to intersect with "
                                                 "the current selection"),
                                               modifiers & ~state,
                                               NULL, NULL, NULL);
              free_status = TRUE;
              break;
            }
          break;

        case SELECTION_MOVE_MASK:
          status = pika_suggest_modifiers (_("Click-Drag to move the "
                                             "selection mask"),
                                           modifiers & ~state,
                                           NULL, NULL, NULL);
          free_status = TRUE;
          break;

        case SELECTION_MOVE:
          status = _("Click-Drag to move the selected pixels");
          break;

        case SELECTION_MOVE_COPY:
          status = _("Click-Drag to move a copy of the selected pixels");
          break;

        case SELECTION_ANCHOR:
          status = _("Click to anchor the floating selection");
          break;

        default:
          g_return_if_reached ();
        }

      if (status)
        pika_tool_push_status (tool, display, "%s", status);

      if (free_status)
        g_free ((gchar *) status);
    }
}

static void
pika_selection_tool_cursor_update (PikaTool         *tool,
                                   const PikaCoords *coords,
                                   GdkModifierType   state,
                                   PikaDisplay      *display)
{
  PikaSelectionTool    *selection_tool = PIKA_SELECTION_TOOL (tool);
  PikaSelectionOptions *options;
  PikaToolCursorType    tool_cursor;
  PikaCursorModifier    modifier;

  options = PIKA_SELECTION_TOOL_GET_OPTIONS (tool);

  tool_cursor = pika_tool_control_get_tool_cursor (tool->control);
  modifier    = PIKA_CURSOR_MODIFIER_NONE;

  switch (selection_tool->function)
    {
    case SELECTION_SELECT:
      switch (options->operation)
        {
        case PIKA_CHANNEL_OP_REPLACE:
          break;
        case PIKA_CHANNEL_OP_ADD:
          modifier = PIKA_CURSOR_MODIFIER_PLUS;
          break;
        case PIKA_CHANNEL_OP_SUBTRACT:
          modifier = PIKA_CURSOR_MODIFIER_MINUS;
          break;
        case PIKA_CHANNEL_OP_INTERSECT:
          modifier = PIKA_CURSOR_MODIFIER_INTERSECT;
          break;
        }
      break;

    case SELECTION_MOVE_MASK:
      modifier = PIKA_CURSOR_MODIFIER_MOVE;
      break;

    case SELECTION_MOVE:
    case SELECTION_MOVE_COPY:
      tool_cursor = PIKA_TOOL_CURSOR_MOVE;
      break;

    case SELECTION_ANCHOR:
      modifier = PIKA_CURSOR_MODIFIER_ANCHOR;
      break;
    }

  /*  our subclass might have set a BAD modifier, in which case we leave it
   *  there, since it's more important than what we have to say.
   */
  if (pika_tool_control_get_cursor_modifier (tool->control) ==
      PIKA_CURSOR_MODIFIER_BAD                              ||
      ! pika_selection_tool_check (selection_tool, display, NULL))
    {
      modifier = PIKA_CURSOR_MODIFIER_BAD;
    }

  pika_tool_set_cursor (tool, display,
                        pika_tool_control_get_cursor (tool->control),
                        tool_cursor,
                        modifier);
}

static gboolean
pika_selection_tool_real_have_selection (PikaSelectionTool *sel_tool,
                                         PikaDisplay       *display)
{
  PikaImage   *image     = pika_display_get_image (display);
  PikaChannel *selection = pika_image_get_mask (image);

  return ! pika_channel_is_empty (selection);
}

static void
pika_selection_tool_commit (PikaSelectionTool *sel_tool)
{
  /* make sure pika_selection_tool_halt() doesn't undo the change, if any */
  g_clear_weak_pointer (&sel_tool->undo);
}

static void
pika_selection_tool_halt (PikaSelectionTool *sel_tool,
                          PikaDisplay       *display)
{
  g_warn_if_fail (sel_tool->change_count == 0);

  if (display)
    {
      PikaTool      *tool       = PIKA_TOOL (sel_tool);
      PikaImage     *image      = pika_display_get_image (display);
      PikaUndoStack *undo_stack = pika_image_get_undo_stack (image);
      PikaUndo      *undo       = pika_undo_stack_peek (undo_stack);

      /* if we have an existing selection in the current display, then
       * we have already "executed", and need to undo at this point,
       * unless the user has done something in the meantime
       */
      if (undo && sel_tool->undo == undo)
        {
          /* prevent this change from halting the tool */
          pika_tool_control_push_preserve (tool->control, TRUE);

          pika_image_undo (image);
          pika_image_flush (image);

          pika_tool_control_pop_preserve (tool->control);
        }

      /* reset the automatic undo/redo mechanism */
      g_clear_weak_pointer (&sel_tool->undo);
      g_clear_weak_pointer (&sel_tool->redo);
    }
}

static gboolean
pika_selection_tool_check (PikaSelectionTool  *sel_tool,
                           PikaDisplay        *display,
                           GError            **error)
{
  PikaSelectionOptions *options   = PIKA_SELECTION_TOOL_GET_OPTIONS (sel_tool);
  PikaImage            *image     = pika_display_get_image (display);

  switch (sel_tool->function)
    {
    case SELECTION_SELECT:
      switch (options->operation)
        {
        case PIKA_CHANNEL_OP_ADD:
        case PIKA_CHANNEL_OP_REPLACE:
          break;

        case PIKA_CHANNEL_OP_SUBTRACT:
          if (! pika_item_bounds (PIKA_ITEM (pika_image_get_mask (image)),
                                  NULL, NULL, NULL, NULL))
            {
              g_set_error (error, PIKA_ERROR, PIKA_FAILED,
                           _("Cannot subtract from an empty selection."));

              return FALSE;
            }
          break;

        case PIKA_CHANNEL_OP_INTERSECT:
          if (! pika_item_bounds (PIKA_ITEM (pika_image_get_mask (image)),
                                  NULL, NULL, NULL, NULL))
            {
              g_set_error (error, PIKA_ERROR, PIKA_FAILED,
                           _("Cannot intersect with an empty selection."));

              return FALSE;
            }
          break;
        }
      break;

    case SELECTION_MOVE:
    case SELECTION_MOVE_COPY:
        {
          GList    *drawables   = pika_image_get_selected_drawables (image);
          PikaItem *locked_item = NULL;
          GList    *iter;

          for (iter = drawables; iter; iter = iter->next)
            {
              if (pika_viewable_get_children (iter->data))
                {
                  g_set_error (error, PIKA_ERROR, PIKA_FAILED,
                               _("Cannot modify the pixels of layer groups."));

                  g_list_free (drawables);
                  return FALSE;
                }
              else if (pika_item_is_content_locked (iter->data, &locked_item))
                {
                  g_set_error (error, PIKA_ERROR, PIKA_FAILED,
                               _("A selected item's pixels are locked."));

                  if (error)
                    pika_tools_blink_lock_box (display->pika, locked_item);

                  g_list_free (drawables);
                  return FALSE;
                }
            }

          g_list_free (drawables);
        }
      break;

    default:
      break;
    }

  return TRUE;
}

static gboolean
pika_selection_tool_have_selection (PikaSelectionTool *sel_tool,
                                    PikaDisplay       *display)
{
  return PIKA_SELECTION_TOOL_GET_CLASS (sel_tool)->have_selection (sel_tool,
                                                                   display);
}


/*  public functions  */

gboolean
pika_selection_tool_start_edit (PikaSelectionTool *sel_tool,
                                PikaDisplay       *display,
                                const PikaCoords  *coords)
{
  PikaTool             *tool;
  PikaSelectionOptions *options;
  GError               *error = NULL;

  g_return_val_if_fail (PIKA_IS_SELECTION_TOOL (sel_tool), FALSE);
  g_return_val_if_fail (PIKA_IS_DISPLAY (display), FALSE);
  g_return_val_if_fail (coords != NULL, FALSE);

  tool    = PIKA_TOOL (sel_tool);
  options = PIKA_SELECTION_TOOL_GET_OPTIONS (sel_tool);

  g_return_val_if_fail (pika_tool_control_is_active (tool->control) == FALSE,
                        FALSE);

  if (! pika_selection_tool_check (sel_tool, display, &error))
    {
      pika_tool_message_literal (tool, display, error->message);

      pika_tools_show_tool_options (display->pika);
      pika_widget_blink (options->mode_box);

      g_clear_error (&error);

      return TRUE;
    }

  switch (sel_tool->function)
    {
    case SELECTION_MOVE_MASK:
      pika_edit_selection_tool_start (tool, display, coords,
                                      PIKA_TRANSLATE_MODE_MASK, FALSE);
      return TRUE;

    case SELECTION_MOVE:
    case SELECTION_MOVE_COPY:
      {
        PikaTranslateMode edit_mode;

        pika_tool_control (tool, PIKA_TOOL_ACTION_COMMIT, display);

        if (sel_tool->function == SELECTION_MOVE)
          edit_mode = PIKA_TRANSLATE_MODE_MASK_TO_LAYER;
        else
          edit_mode = PIKA_TRANSLATE_MODE_MASK_COPY_TO_LAYER;

        pika_edit_selection_tool_start (tool, display, coords,
                                        edit_mode, FALSE);

        return TRUE;
      }

    default:
      break;
    }

  return FALSE;
}

static gboolean
pika_selection_tool_idle (PikaSelectionTool *sel_tool)
{
  PikaTool         *tool  = PIKA_TOOL (sel_tool);
  PikaDisplayShell *shell = pika_display_get_shell (tool->display);

  pika_display_shell_set_show_selection (shell, FALSE);

  sel_tool->idle_id = 0;

  return G_SOURCE_REMOVE;
}

void
pika_selection_tool_start_change (PikaSelectionTool *sel_tool,
                                  gboolean           create,
                                  PikaChannelOps     operation)
{
  PikaTool         *tool;
  PikaDisplayShell *shell;
  PikaImage        *image;
  PikaUndoStack    *undo_stack;

  g_return_if_fail (PIKA_IS_SELECTION_TOOL (sel_tool));

  tool = PIKA_TOOL (sel_tool);

  g_return_if_fail (tool->display != NULL);

  if (sel_tool->change_count++ > 0)
    return;

  shell      = pika_display_get_shell (tool->display);
  image      = pika_display_get_image (tool->display);
  undo_stack = pika_image_get_undo_stack (image);

  sel_tool->saved_show_selection =
    pika_display_shell_get_show_selection (shell);

  if (create)
    {
      g_clear_weak_pointer (&sel_tool->undo);
    }
  else
    {
      PikaUndoStack *redo_stack = pika_image_get_redo_stack (image);
      PikaUndo      *undo;

      undo = pika_undo_stack_peek (undo_stack);

      if (undo && undo == sel_tool->undo)
        {
          /* prevent this change from halting the tool */
          pika_tool_control_push_preserve (tool->control, TRUE);

          pika_image_undo (image);

          pika_tool_control_pop_preserve (tool->control);

          g_clear_weak_pointer (&sel_tool->undo);

          /* we will need to redo if the user cancels or executes */
          g_set_weak_pointer (&sel_tool->redo,
                              pika_undo_stack_peek (redo_stack));
        }

      /* if the operation is "Replace", turn off the marching ants,
       * because they are confusing ...
       */
      if (operation == PIKA_CHANNEL_OP_REPLACE)
        {
          /* ... however, do this in an idle function, to avoid unnecessarily
           * restarting the selection if we don't visit the main loop between
           * the start_change() and end_change() calls.
           */
          sel_tool->idle_id = g_idle_add_full (
            G_PRIORITY_HIGH_IDLE,
            (GSourceFunc) pika_selection_tool_idle,
            sel_tool, NULL);
        }
    }

  g_set_weak_pointer (&sel_tool->undo,
                      pika_undo_stack_peek (undo_stack));
}

void
pika_selection_tool_end_change (PikaSelectionTool *sel_tool,
                                gboolean           cancel)
{
  PikaTool         *tool;
  PikaDisplayShell *shell;
  PikaImage        *image;
  PikaUndoStack    *undo_stack;

  g_return_if_fail (PIKA_IS_SELECTION_TOOL (sel_tool));
  g_return_if_fail (sel_tool->change_count > 0);

  tool = PIKA_TOOL (sel_tool);

  g_return_if_fail (tool->display != NULL);

  if (--sel_tool->change_count > 0)
    return;

  shell      = pika_display_get_shell (tool->display);
  image      = pika_display_get_image (tool->display);
  undo_stack = pika_image_get_undo_stack (image);

  if (cancel)
    {
      PikaUndoStack *redo_stack = pika_image_get_redo_stack (image);
      PikaUndo      *redo       = pika_undo_stack_peek (redo_stack);

      if (redo && redo == sel_tool->redo)
        {
          /* prevent this from halting the tool */
          pika_tool_control_push_preserve (tool->control, TRUE);

          pika_image_redo (image);

          pika_tool_control_pop_preserve (tool->control);

          g_set_weak_pointer (&sel_tool->undo,
                              pika_undo_stack_peek (undo_stack));
        }
      else
        {
          g_clear_weak_pointer (&sel_tool->undo);
        }
    }
  else
    {
      PikaUndo *undo = pika_undo_stack_peek (undo_stack);

      /* save the undo that we got when executing, but only if
       * we actually selected something
       */
      if (undo && undo != sel_tool->undo)
        g_set_weak_pointer (&sel_tool->undo, undo);
      else
        g_clear_weak_pointer (&sel_tool->undo);
    }

  g_clear_weak_pointer (&sel_tool->redo);

  if (sel_tool->idle_id)
    {
      g_source_remove (sel_tool->idle_id);
      sel_tool->idle_id = 0;
    }
  else
    {
      pika_display_shell_set_show_selection (shell,
                                             sel_tool->saved_show_selection);
    }

  pika_image_flush (image);
}
