/* 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 "display-types.h"

#include "config/pikadisplayconfig.h"

#include "core/pika.h"
#include "core/pika-cairo.h"
#include "core/pikaboundary.h"
#include "core/pikachannel.h"
#include "core/pikaimage.h"

#include "pikadisplay.h"
#include "pikadisplayshell.h"
#include "pikadisplayshell-appearance.h"
#include "pikadisplayshell-draw.h"
#include "pikadisplayshell-expose.h"
#include "pikadisplayshell-selection.h"
#include "pikadisplayshell-transform.h"


struct _Selection
{
  PikaDisplayShell *shell;            /*  shell that owns the selection     */

  PikaSegment      *segs_in;          /*  segments of area boundary         */
  gint              n_segs_in;        /*  number of segments in segs_in     */

  PikaSegment      *segs_out;         /*  segments of area boundary         */
  gint              n_segs_out;       /*  number of segments in segs_out    */

  guint             index;            /*  index of current stipple pattern  */
  gint              paused;           /*  count of pause requests           */
  gboolean          shell_visible;    /*  visility of the display shell     */
  gboolean          show_selection;   /*  is the selection visible?         */
  guint             timeout;          /*  timer for successive draws        */
  cairo_pattern_t  *segs_in_mask;     /*  cache for rendered segments       */
};


/*  local function prototypes  */

static void      selection_start          (Selection          *selection);
static void      selection_stop           (Selection          *selection);

static void      selection_undraw         (Selection          *selection);

static void      selection_render_mask    (Selection          *selection);

static void      selection_zoom_segs      (Selection          *selection,
                                           const PikaBoundSeg *src_segs,
                                           PikaSegment        *dest_segs,
                                           gint                n_segs,
                                           gint                canvas_offset_x,
                                           gint                canvas_offset_y);
static void      selection_generate_segs  (Selection          *selection);
static void      selection_free_segs      (Selection          *selection);

static gboolean  selection_timeout        (Selection          *selection);

static gboolean  selection_window_state_event      (GtkWidget           *shell,
                                                    GdkEventWindowState *event,
                                                    Selection           *selection);
static gboolean  selection_visibility_notify_event (GtkWidget           *shell,
                                                    GdkEventVisibility  *event,
                                                    Selection           *selection);


/*  public functions  */

void
pika_display_shell_selection_init (PikaDisplayShell *shell)
{
  Selection *selection;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (shell->selection == NULL);

  selection = g_slice_new0 (Selection);

  selection->shell          = shell;
  selection->shell_visible  = TRUE;
  selection->show_selection = pika_display_shell_get_show_selection (shell);

  shell->selection = selection;
  shell->selection_update = g_get_monotonic_time ();

  g_signal_connect (shell, "window-state-event",
                    G_CALLBACK (selection_window_state_event),
                    selection);
  g_signal_connect (shell, "visibility-notify-event",
                    G_CALLBACK (selection_visibility_notify_event),
                    selection);
}

void
pika_display_shell_selection_free (PikaDisplayShell *shell)
{
  Selection *selection;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (shell->selection != NULL);

  selection = shell->selection;

  selection_stop (selection);

  g_signal_handlers_disconnect_by_func (shell,
                                        selection_window_state_event,
                                        selection);
  g_signal_handlers_disconnect_by_func (shell,
                                        selection_visibility_notify_event,
                                        selection);

  selection_free_segs (selection);

  g_slice_free (Selection, selection);

  shell->selection = NULL;
}

void
pika_display_shell_selection_undraw (PikaDisplayShell *shell)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (shell->selection != NULL);

  if (pika_display_get_image (shell->display))
    {
      selection_undraw (shell->selection);
    }
  else
    {
      selection_stop (shell->selection);
      selection_free_segs (shell->selection);
    }
}

void
pika_display_shell_selection_restart (PikaDisplayShell *shell)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (shell->selection != NULL);

  if (pika_display_get_image (shell->display))
    {
      selection_start (shell->selection);
    }
}

void
pika_display_shell_selection_pause (PikaDisplayShell *shell)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (shell->selection != NULL);

  if (pika_display_get_image (shell->display))
    {
      if (shell->selection->paused == 0)
        selection_stop (shell->selection);

      shell->selection->paused++;
    }
}

void
pika_display_shell_selection_resume (PikaDisplayShell *shell)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (shell->selection != NULL);

  if (pika_display_get_image (shell->display))
    {
      shell->selection->paused--;

      if (shell->selection->paused == 0)
        selection_start (shell->selection);
    }
}

void
pika_display_shell_selection_set_show (PikaDisplayShell *shell,
                                       gboolean          show)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));
  g_return_if_fail (shell->selection != NULL);

  if (pika_display_get_image (shell->display))
    {
      Selection *selection = shell->selection;

      if (show != selection->show_selection)
        {
          selection_undraw (selection);

          selection->show_selection = show;

          selection_start (selection);
        }
    }
}


/*  private functions  */

static void
selection_start (Selection *selection)
{
  selection_stop (selection);

  /*  If this selection is paused, do not start it  */
  if (selection->paused == 0                             &&
      pika_display_get_image (selection->shell->display) &&
      selection->show_selection)
    {
      /*  Draw the ants once  */
      selection_timeout (selection);

      if (selection->segs_in && selection->shell_visible)
        {
          PikaDisplayConfig *config = selection->shell->display->config;

          selection->timeout = g_timeout_add_full (G_PRIORITY_DEFAULT_IDLE,
                                                   config->marching_ants_speed,
                                                   (GSourceFunc) selection_timeout,
                                                   selection, NULL);
        }
    }
}

static void
selection_stop (Selection *selection)
{
  if (selection->timeout)
    {
      g_source_remove (selection->timeout);
      selection->timeout = 0;
    }
}

void
pika_display_shell_selection_draw (PikaDisplayShell *shell,
                                   cairo_t          *cr)
{
  if (pika_display_get_image (shell->display) &&
      shell->selection && shell->selection->show_selection)
    {
      PikaDisplayConfig *config = shell->display->config;
      gint64             time   = g_get_monotonic_time ();

      if ((time - shell->selection_update) / 1000 > config->marching_ants_speed &&
          shell->selection->paused == 0)
        {
          shell->selection_update = time;
          shell->selection->index++;
        }

      selection_generate_segs (shell->selection);

      if (shell->selection->segs_in)
        {
          pika_display_shell_draw_selection_in (shell->selection->shell, cr,
                                                shell->selection->segs_in_mask,
                                                shell->selection->index % 8);
        }

      if (shell->selection->segs_out)
        {
          if (shell->selection->shell->rotate_transform)
            cairo_transform (cr, shell->selection->shell->rotate_transform);

          pika_display_shell_draw_selection_out (shell->selection->shell, cr,
                                                 shell->selection->segs_out,
                                                 shell->selection->n_segs_out);
        }
    }
}

static void
selection_undraw (Selection *selection)
{
  gint x, y, w, h;

  selection_stop (selection);

  if (pika_display_shell_mask_bounds (selection->shell, &x, &y, &w, &h))
    {
      /* expose will restart the selection */
      pika_display_shell_expose_area (selection->shell, x, y, w, h);
    }
  else
    {
      selection_start (selection);
    }
}

static void
selection_render_mask (Selection *selection)
{
  GdkWindow       *window;
  cairo_surface_t *surface;
  cairo_t         *cr;

  window = gtk_widget_get_window (GTK_WIDGET (selection->shell));
  surface = gdk_window_create_similar_surface (window, CAIRO_CONTENT_ALPHA,
                                               gdk_window_get_width  (window),
                                               gdk_window_get_height (window));
  cr = cairo_create (surface);

  cairo_set_line_cap (cr, CAIRO_LINE_CAP_SQUARE);
  cairo_set_line_width (cr, 1.0);

  if (selection->shell->rotate_transform)
    cairo_transform (cr, selection->shell->rotate_transform);

  pika_cairo_segments (cr,
                       selection->segs_in,
                       selection->n_segs_in);
  cairo_stroke (cr);

  selection->segs_in_mask = cairo_pattern_create_for_surface (surface);

  cairo_destroy (cr);
  cairo_surface_destroy (surface);
}

static void
selection_zoom_segs (Selection          *selection,
                     const PikaBoundSeg *src_segs,
                     PikaSegment        *dest_segs,
                     gint                n_segs,
                     gint                canvas_offset_x,
                     gint                canvas_offset_y)
{
  const gint xclamp = selection->shell->disp_width + 1;
  const gint yclamp = selection->shell->disp_height + 1;
  gint       i;

  pika_display_shell_zoom_segments (selection->shell,
                                    src_segs, dest_segs, n_segs,
                                    0.0, 0.0);

  for (i = 0; i < n_segs; i++)
    {
      if (! selection->shell->rotate_transform)
        {
          dest_segs[i].x1 = CLAMP (dest_segs[i].x1, -1, xclamp) + canvas_offset_x;
          dest_segs[i].y1 = CLAMP (dest_segs[i].y1, -1, yclamp) + canvas_offset_y;

          dest_segs[i].x2 = CLAMP (dest_segs[i].x2, -1, xclamp) + canvas_offset_x;
          dest_segs[i].y2 = CLAMP (dest_segs[i].y2, -1, yclamp) + canvas_offset_y;
        }

      /*  If this segment is a closing segment && the segments lie inside
       *  the region, OR if this is an opening segment and the segments
       *  lie outside the region...
       *  we need to transform it by one display pixel
       */
      if (! src_segs[i].open)
        {
          /*  If it is vertical  */
          if (dest_segs[i].x1 == dest_segs[i].x2)
            {
              dest_segs[i].x1 -= 1;
              dest_segs[i].x2 -= 1;
            }
          else
            {
              dest_segs[i].y1 -= 1;
              dest_segs[i].y2 -= 1;
            }
        }
    }
}

static void
selection_generate_segs (Selection *selection)
{
  PikaImage          *image = pika_display_get_image (selection->shell->display);
  const PikaBoundSeg *segs_in;
  const PikaBoundSeg *segs_out;
  gint                canvas_offset_x = 0;
  gint                canvas_offset_y = 0;

  selection_free_segs (selection);

  /*  Ask the image for the boundary of its selected region...
   *  Then transform that information into a new buffer of PikaSegments
   */
  pika_channel_boundary (pika_image_get_mask (image),
                         &segs_in, &segs_out,
                         &selection->n_segs_in, &selection->n_segs_out,
                         0, 0, 0, 0);

  if (selection->n_segs_in)
    {
      selection->segs_in = g_new (PikaSegment, selection->n_segs_in);
      selection_zoom_segs (selection, segs_in,
                           selection->segs_in, selection->n_segs_in,
                           canvas_offset_x, canvas_offset_y);

      selection_render_mask (selection);
    }

  /*  Possible secondary boundary representation  */
  if (selection->n_segs_out)
    {
      selection->segs_out = g_new (PikaSegment, selection->n_segs_out);
      selection_zoom_segs (selection, segs_out,
                           selection->segs_out, selection->n_segs_out,
                           canvas_offset_x, canvas_offset_y);
    }
}

static void
selection_free_segs (Selection *selection)
{
  g_clear_pointer (&selection->segs_in, g_free);
  selection->n_segs_in = 0;

  g_clear_pointer (&selection->segs_out, g_free);
  selection->n_segs_out = 0;

  g_clear_pointer (&selection->segs_in_mask, cairo_pattern_destroy);
}

static gboolean
selection_timeout (Selection *selection)
{
  PikaDisplayConfig *config = selection->shell->display->config;
  gint64             time   = g_get_monotonic_time ();

  if ((time - selection->shell->selection_update) / 1000 > config->marching_ants_speed)
    {
      GdkWindow             *window;
      cairo_rectangle_int_t  rect;
      cairo_region_t        *region;

      window = gtk_widget_get_window (GTK_WIDGET (selection->shell));

      rect.x      = 0;
      rect.y      = 0;
      rect.width  = gdk_window_get_width  (window);
      rect.height = gdk_window_get_height (window);

      region = cairo_region_create_rectangle (&rect);
      gtk_widget_queue_draw_region (GTK_WIDGET (selection->shell), region);
      cairo_region_destroy (region);
    }

  return G_SOURCE_CONTINUE;
}

static void
selection_set_shell_visible (Selection *selection,
                             gboolean   shell_visible)
{
  if (selection->shell_visible != shell_visible)
    {
      selection->shell_visible = shell_visible;

      if (shell_visible)
        selection_start (selection);
      else
        selection_stop (selection);
    }
}

static gboolean
selection_window_state_event (GtkWidget           *shell,
                              GdkEventWindowState *event,
                              Selection           *selection)
{
  selection_set_shell_visible (selection,
                               (event->new_window_state & (GDK_WINDOW_STATE_WITHDRAWN |
                                                           GDK_WINDOW_STATE_ICONIFIED)) == 0);

  return FALSE;
}

static gboolean
selection_visibility_notify_event (GtkWidget          *shell,
                                   GdkEventVisibility *event,
                                   Selection          *selection)
{
  selection_set_shell_visible (selection,
                               event->state != GDK_VISIBILITY_FULLY_OBSCURED);

  return FALSE;
}
