/* 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 <math.h>

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

#include "libpikabase/pikabase.h"
#include "libpikamath/pikamath.h"
#include "libpikawidgets/pikawidgets.h"

#include "display-types.h"

#include "config/pikaguiconfig.h"

#include "core/pika.h"
#include "core/pikaimage.h"

#include "pikadisplay.h"
#include "pikadisplayshell.h"
#include "pikadisplayshell-expose.h"
#include "pikadisplayshell-render.h"
#include "pikadisplayshell-rotate.h"
#include "pikadisplayshell-scale.h"
#include "pikadisplayshell-scroll.h"
#include "pikadisplayshell-transform.h"
#include "pikaimagewindow.h"


#define SCALE_TIMEOUT             2
#define SCALE_EPSILON             0.0001
#define ALMOST_CENTERED_THRESHOLD 2

#define SCALE_EQUALS(a,b) (fabs ((a) - (b)) < SCALE_EPSILON)


/*  local function prototypes  */

static void      pika_display_shell_scale_get_screen_resolution
                                                         (PikaDisplayShell *shell,
                                                          gdouble          *xres,
                                                          gdouble          *yres);
static void      pika_display_shell_scale_get_image_size_for_scale
                                                         (PikaDisplayShell *shell,
                                                          gdouble           scale,
                                                          gint             *w,
                                                          gint             *h);
static void      pika_display_shell_calculate_scale_x_and_y
                                                         (PikaDisplayShell *shell,
                                                          gdouble           scale,
                                                          gdouble          *scale_x,
                                                          gdouble          *scale_y);

static void      pika_display_shell_scale_to             (PikaDisplayShell *shell,
                                                          gdouble           scale,
                                                          gdouble           viewport_x,
                                                          gdouble           viewport_y);
static void      pika_display_shell_scale_fit_or_fill    (PikaDisplayShell *shell,
                                                          gboolean          fill);

static gboolean  pika_display_shell_scale_image_starts_to_fit
                                                         (PikaDisplayShell *shell,
                                                          gdouble           new_scale,
                                                          gdouble           current_scale,
                                                          gboolean         *horizontally,
                                                          gboolean         *vertically);
static gboolean  pika_display_shell_scale_image_stops_to_fit
                                                         (PikaDisplayShell *shell,
                                                          gdouble           new_scale,
                                                          gdouble           current_scale,
                                                          gboolean         *horizontally,
                                                          gboolean         *vertically);

static gboolean  pika_display_shell_scale_viewport_coord_almost_centered
                                                         (PikaDisplayShell *shell,
                                                          gint              x,
                                                          gint              y,
                                                          gboolean         *horizontally,
                                                          gboolean         *vertically);

static void      pika_display_shell_scale_get_image_center_viewport
                                                         (PikaDisplayShell *shell,
                                                          gint             *image_center_x,
                                                          gint             *image_center_y);


static void      pika_display_shell_scale_get_zoom_focus (PikaDisplayShell *shell,
                                                          gdouble           new_scale,
                                                          gdouble           current_scale,
                                                          gdouble          *x,
                                                          gdouble          *y,
                                                          PikaZoomFocus     zoom_focus);


/*  public functions  */

/**
 * pika_display_shell_scale_revert:
 * @shell:     the #PikaDisplayShell
 *
 * Reverts the display to the previously used scale. If no previous
 * scale exist, then the call does nothing.
 *
 * Returns: %TRUE if the scale was reverted, otherwise %FALSE.
 **/
gboolean
pika_display_shell_scale_revert (PikaDisplayShell *shell)
{
  g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), FALSE);

  /* don't bother if no scale has been set */
  if (shell->last_scale < SCALE_EPSILON)
    return FALSE;

  shell->last_scale_time = 0;

  pika_display_shell_scale_by_values (shell,
                                      shell->last_scale,
                                      shell->last_offset_x,
                                      shell->last_offset_y,
                                      FALSE);   /* don't resize the window */

  return TRUE;
}

/**
 * pika_display_shell_scale_can_revert:
 * @shell: the #PikaDisplayShell
 *
 * Returns: %TRUE if a previous display scale exists, otherwise %FALSE.
 **/
gboolean
pika_display_shell_scale_can_revert (PikaDisplayShell *shell)
{
  g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), FALSE);

  return (shell->last_scale > SCALE_EPSILON);
}

/**
 * pika_display_shell_scale_save_revert_values:
 * @shell:
 *
 * Handle the updating of the Revert Zoom variables.
 **/
void
pika_display_shell_scale_save_revert_values (PikaDisplayShell *shell)
{
  guint now;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  now = time (NULL);

  if (now - shell->last_scale_time >= SCALE_TIMEOUT)
    {
      shell->last_scale    = pika_zoom_model_get_factor (shell->zoom);
      shell->last_offset_x = shell->offset_x;
      shell->last_offset_y = shell->offset_y;
    }

  shell->last_scale_time = now;
}

/**
 * pika_display_shell_scale_set_dot_for_dot:
 * @shell:        the #PikaDisplayShell
 * @dot_for_dot:  whether "Dot for Dot" should be enabled
 *
 * If @dot_for_dot is set to %TRUE then the "Dot for Dot" mode (where image and
 * screen pixels are of the same size) is activated. Dually, the mode is
 * disabled if @dot_for_dot is %FALSE.
 **/
void
pika_display_shell_scale_set_dot_for_dot (PikaDisplayShell *shell,
                                          gboolean          dot_for_dot)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  if (dot_for_dot != shell->dot_for_dot)
    {
      PikaDisplayConfig *config = shell->display->config;
      gboolean           resize_window;

      /* Resize windows only in multi-window mode */
      resize_window = (config->resize_windows_on_zoom &&
                       ! PIKA_GUI_CONFIG (config)->single_window_mode);

      /* freeze the active tool */
      pika_display_shell_pause (shell);

      shell->dot_for_dot = dot_for_dot;

      pika_display_shell_scale_update (shell);

      pika_display_shell_scale_resize (shell, resize_window, FALSE);

      /* re-enable the active tool */
      pika_display_shell_resume (shell);
    }
}

/**
 * pika_display_shell_scale_get_image_size:
 * @shell:
 * @w:
 * @h:
 *
 * Gets the size of the rendered image after it has been scaled.
 *
 **/
void
pika_display_shell_scale_get_image_size (PikaDisplayShell *shell,
                                         gint             *w,
                                         gint             *h)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  pika_display_shell_scale_get_image_size_for_scale (shell,
                                                     pika_zoom_model_get_factor (shell->zoom),
                                                     w, h);
}

/**
 * pika_display_shell_scale_get_image_bounds:
 * @shell:
 * @x:
 * @y:
 * @w:
 * @h:
 *
 * Gets the screen-space boudning box of the image, after it has
 * been transformed (i.e., scaled, rotated, and scrolled).
 **/
void
pika_display_shell_scale_get_image_bounds (PikaDisplayShell *shell,
                                           gint             *x,
                                           gint             *y,
                                           gint             *w,
                                           gint             *h)
{
  PikaImage *image;
  gdouble    x1, y1;
  gdouble    x2, y2;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  image = pika_display_get_image (shell->display);

  pika_display_shell_transform_bounds (shell,
                                       0, 0,
                                       pika_image_get_width (image),
                                       pika_image_get_height (image),
                                       &x1, &y1,
                                       &x2, &y2);

  x1 = ceil (x1);
  y1 = ceil (y1);
  x2 = floor (x2);
  y2 = floor (y2);

  if (x) *x = x1 + shell->offset_x;
  if (y) *y = y1 + shell->offset_y;
  if (w) *w = x2 - x1;
  if (h) *h = y2 - y1;
}

/**
 * pika_display_shell_scale_get_image_unrotated_bounds:
 * @shell:
 * @x:
 * @y:
 * @w:
 * @h:
 *
 * Gets the screen-space boudning box of the image, after it has
 * been scaled and scrolled, but before it has been rotated.
 **/
void
pika_display_shell_scale_get_image_unrotated_bounds (PikaDisplayShell *shell,
                                                     gint             *x,
                                                     gint             *y,
                                                     gint             *w,
                                                     gint             *h)
{
  PikaImage *image;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  image = pika_display_get_image (shell->display);

  if (x) *x = -shell->offset_x;
  if (y) *y = -shell->offset_y;
  if (w) *w = floor (pika_image_get_width  (image) * shell->scale_x);
  if (h) *h = floor (pika_image_get_height (image) * shell->scale_y);
}

/**
 * pika_display_shell_scale_get_image_bounding_box:
 * @shell:
 * @x:
 * @y:
 * @w:
 * @h:
 *
 * Gets the screen-space boudning box of the image content, after it has
 * been transformed (i.e., scaled, rotated, and scrolled).
 **/
void
pika_display_shell_scale_get_image_bounding_box (PikaDisplayShell *shell,
                                                 gint             *x,
                                                 gint             *y,
                                                 gint             *w,
                                                 gint             *h)
{
  GeglRectangle bounding_box;
  gdouble       x1, y1;
  gdouble       x2, y2;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  bounding_box = pika_display_shell_get_bounding_box (shell);

  pika_display_shell_transform_bounds (shell,
                                       bounding_box.x,
                                       bounding_box.y,
                                       bounding_box.x + bounding_box.width,
                                       bounding_box.y + bounding_box.height,
                                       &x1, &y1,
                                       &x2, &y2);

  if (! shell->show_all)
    {
      x1 = ceil  (x1);
      y1 = ceil  (y1);
      x2 = floor (x2);
      y2 = floor (y2);
    }
  else
    {
      x1 = floor (x1);
      y1 = floor (y1);
      x2 = ceil  (x2);
      y2 = ceil  (y2);
    }

  if (x) *x = x1 + shell->offset_x;
  if (y) *y = y1 + shell->offset_y;
  if (w) *w = x2 - x1;
  if (h) *h = y2 - y1;
}

/**
 * pika_display_shell_scale_get_image_unrotated_bounding_box:
 * @shell:
 * @x:
 * @y:
 * @w:
 * @h:
 *
 * Gets the screen-space boudning box of the image content, after it has
 * been scaled and scrolled, but before it has been rotated.
 **/
void
pika_display_shell_scale_get_image_unrotated_bounding_box (PikaDisplayShell *shell,
                                                           gint             *x,
                                                           gint             *y,
                                                           gint             *w,
                                                           gint             *h)
{
  GeglRectangle bounding_box;
  gdouble       x1, y1;
  gdouble       x2, y2;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  bounding_box = pika_display_shell_get_bounding_box (shell);

  x1 = bounding_box.x * shell->scale_x -
       shell->offset_x;
  y1 = bounding_box.y * shell->scale_y -
       shell->offset_y;

  x2 = (bounding_box.x + bounding_box.width)  * shell->scale_x -
       shell->offset_x;
  y2 = (bounding_box.y + bounding_box.height) * shell->scale_y -
       shell->offset_y;

  if (! shell->show_all)
    {
      x1 = ceil  (x1);
      y1 = ceil  (y1);
      x2 = floor (x2);
      y2 = floor (y2);
    }
  else
    {
      x1 = floor (x1);
      y1 = floor (y1);
      x2 = ceil  (x2);
      y2 = ceil  (y2);
    }

  if (x) *x = x1;
  if (y) *y = y1;
  if (w) *w = x2 - x1;
  if (h) *h = y2 - y1;
}

/**
 * pika_display_shell_scale_image_is_within_viewport:
 * @shell:
 *
 * Returns: %TRUE if the (scaled) image is smaller than and within the
 *          viewport.
 **/
gboolean
pika_display_shell_scale_image_is_within_viewport (PikaDisplayShell *shell,
                                                   gboolean         *horizontally,
                                                   gboolean         *vertically)
{
  gboolean horizontally_dummy, vertically_dummy;

  g_return_val_if_fail (PIKA_IS_DISPLAY_SHELL (shell), FALSE);

  if (! horizontally) horizontally = &horizontally_dummy;
  if (! vertically)   vertically   = &vertically_dummy;

  if (! pika_display_shell_get_infinite_canvas (shell))
    {
      gint sx, sy;
      gint sw, sh;

      pika_display_shell_scale_get_image_bounding_box (shell,
                                                       &sx, &sy, &sw, &sh);

      sx -= shell->offset_x;
      sy -= shell->offset_y;

      *horizontally = sx >= 0 && sx + sw <= shell->disp_width;
      *vertically   = sy >= 0 && sy + sh <= shell->disp_height;
    }
  else
    {
      *horizontally = FALSE;
      *vertically   = FALSE;
    }

  return *vertically && *horizontally;
}

/* We used to calculate the scale factor in the SCALEFACTOR_X() and
 * SCALEFACTOR_Y() macros. But since these are rather frequently
 * called and the values rarely change, we now store them in the
 * shell and call this function whenever they need to be recalculated.
 */
void
pika_display_shell_scale_update (PikaDisplayShell *shell)
{
  PikaImage *image;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  image = pika_display_get_image (shell->display);

  if (image)
    {
      pika_display_shell_calculate_scale_x_and_y (shell,
                                                  pika_zoom_model_get_factor (shell->zoom),
                                                  &shell->scale_x,
                                                  &shell->scale_y);
    }
  else
    {
      shell->scale_x = 1.0;
      shell->scale_y = 1.0;
    }
}

/**
 * pika_display_shell_scale:
 * @shell:     the #PikaDisplayShell
 * @zoom_type: whether to zoom in, out or to a specific scale
 * @scale:     ignored unless @zoom_type == %PIKA_ZOOM_TO
 *
 * This function figures out the context of the zoom and behaves
 * appropriately thereafter.
 *
 **/
void
pika_display_shell_scale (PikaDisplayShell *shell,
                          PikaZoomType      zoom_type,
                          gdouble           new_scale,
                          PikaZoomFocus     zoom_focus)
{
  PikaDisplayConfig *config;
  gdouble            current_scale;
  gdouble            delta = 0.0;
  gboolean           resize_window;

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

  current_scale = pika_zoom_model_get_factor (shell->zoom);

  if (zoom_type == PIKA_ZOOM_SMOOTH)
    delta = -new_scale;

  if (zoom_type == PIKA_ZOOM_PINCH)
    delta = new_scale;

  if (zoom_type != PIKA_ZOOM_TO)
    new_scale = pika_zoom_model_zoom_step (zoom_type, current_scale, delta);

  if (SCALE_EQUALS (new_scale, current_scale))
    return;

  config = shell->display->config;

  /* Resize windows only in multi-window mode */
  resize_window = (config->resize_windows_on_zoom &&
                   ! PIKA_GUI_CONFIG (config)->single_window_mode);

  if (resize_window)
    {
      /* If the window is resized on zoom, simply do the zoom and get
       * things rolling
       */
      pika_zoom_model_zoom (shell->zoom, PIKA_ZOOM_TO, new_scale);

      pika_display_shell_scale_resize (shell, TRUE, FALSE);

      /* XXX The @zoom_focus policy is clearly not working in this code
       * path. This should be fixed.
       */
    }
  else
    {
      gdouble  x, y;
      gint     image_center_x;
      gint     image_center_y;

      pika_display_shell_scale_get_zoom_focus (shell,
                                               new_scale,
                                               current_scale,
                                               &x,
                                               &y,
                                               zoom_focus);
      pika_display_shell_scale_get_image_center_viewport (shell,
                                                          &image_center_x,
                                                          &image_center_y);

      pika_display_shell_scale_to (shell, new_scale, x, y);

      /* skip centering magic if pointer focus was requested */
      if (zoom_focus != PIKA_ZOOM_FOCUS_POINTER)
        {
          gboolean starts_fitting_horiz;
          gboolean starts_fitting_vert;
          gboolean zoom_focus_almost_centered_horiz;
          gboolean zoom_focus_almost_centered_vert;
          gboolean image_center_almost_centered_horiz;
          gboolean image_center_almost_centered_vert;

          /* If an image axis started to fit due to zooming out or if
           * the focus point is as good as in the center, center on
           * that axis
           */
          pika_display_shell_scale_image_starts_to_fit (shell,
                                                        new_scale,
                                                        current_scale,
                                                        &starts_fitting_horiz,
                                                        &starts_fitting_vert);

          pika_display_shell_scale_viewport_coord_almost_centered (shell,
                                                                   x,
                                                                   y,
                                                                   &zoom_focus_almost_centered_horiz,
                                                                   &zoom_focus_almost_centered_vert);
          pika_display_shell_scale_viewport_coord_almost_centered (shell,
                                                                   image_center_x,
                                                                   image_center_y,
                                                                   &image_center_almost_centered_horiz,
                                                                   &image_center_almost_centered_vert);

          pika_display_shell_scroll_center_image (shell,
                                                  starts_fitting_horiz ||
                                                  (zoom_focus_almost_centered_horiz &&
                                                   image_center_almost_centered_horiz),
                                                  starts_fitting_vert ||
                                                  (zoom_focus_almost_centered_vert &&
                                                   image_center_almost_centered_vert));
        }
    }
}

/**
 * pika_display_shell_scale_to_rectangle:
 * @shell:         the #PikaDisplayShell
 * @zoom_type:     whether to zoom in or out
 * @x:             retangle's x in image coordinates
 * @y:             retangle's y in image coordinates
 * @width:         retangle's width in image coordinates
 * @height:        retangle's height in image coordinates
 * @resize_window: whether the display window should be resized
 *
 * Scales and scrolls to a specific image rectangle
 **/
void
pika_display_shell_scale_to_rectangle (PikaDisplayShell *shell,
                                       PikaZoomType      zoom_type,
                                       gdouble           x,
                                       gdouble           y,
                                       gdouble           width,
                                       gdouble           height,
                                       gboolean          resize_window)
{
  gdouble current_scale;
  gdouble new_scale;
  gdouble factor   = 1.0;
  gint    offset_x = 0;
  gint    offset_y = 0;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  pika_display_shell_transform_bounds (shell,
                                       x, y,
                                       x + width, y + height,
                                       &x, &y,
                                       &width, &height);

  /* Convert scrolled (x1, y1, x2, y2) to unscrolled (x, y, width, height). */
  width  -= x;
  height -= y;
  x      += shell->offset_x;
  y      += shell->offset_y;

  width  = MAX (1.0, width);
  height = MAX (1.0, height);

  current_scale = pika_zoom_model_get_factor (shell->zoom);

  switch (zoom_type)
    {
    case PIKA_ZOOM_IN:
      factor = MIN ((shell->disp_width  / width),
                    (shell->disp_height / height));
      break;

    case PIKA_ZOOM_OUT:
      factor = MAX ((width  / shell->disp_width),
                    (height / shell->disp_height));
      break;

    default:
      g_return_if_reached ();
      break;
    }

  new_scale = current_scale * factor;

  switch (zoom_type)
    {
    case PIKA_ZOOM_IN:
      /*  move the center of the rectangle to the center of the
       *  viewport:
       *
       *  new_offset = center of rectangle in new scale screen coords
       *               including offset
       *               -
       *               center of viewport in screen coords without
       *               offset
       */
      offset_x = RINT (factor * (x + width  / 2.0) - (shell->disp_width  / 2));
      offset_y = RINT (factor * (y + height / 2.0) - (shell->disp_height / 2));
      break;

    case PIKA_ZOOM_OUT:
      /*  move the center of the viewport to the center of the
       *  rectangle:
       *
       *  new_offset = center of viewport in new scale screen coords
       *               including offset
       *               -
       *               center of rectangle in screen coords without
       *               offset
       */
      offset_x = RINT (factor * (shell->offset_x + shell->disp_width  / 2) -
                       ((x + width  / 2.0) - shell->offset_x));

      offset_y = RINT (factor * (shell->offset_y + shell->disp_height / 2) -
                       ((y + height / 2.0) - shell->offset_y));
      break;

    default:
      break;
    }

  if (new_scale != current_scale   ||
      offset_x  != shell->offset_x ||
      offset_y  != shell->offset_y)
    {
      pika_display_shell_scale_by_values (shell,
                                          new_scale,
                                          offset_x, offset_y,
                                          resize_window);
    }
}

/**
 * pika_display_shell_scale_fit_in:
 * @shell: the #PikaDisplayShell
 *
 * Sets the scale such that the entire image precisely fits in the
 * display area.
 **/
void
pika_display_shell_scale_fit_in (PikaDisplayShell *shell)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  pika_display_shell_scale_fit_or_fill (shell,
                                        /* fill = */ FALSE);
 }

/**
 * pika_display_shell_scale_fill:
 * @shell: the #PikaDisplayShell
 *
 * Sets the scale such that the entire display area is precisely
 * filled by the image.
 **/
void
pika_display_shell_scale_fill (PikaDisplayShell *shell)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  pika_display_shell_scale_fit_or_fill (shell,
                                        /* fill = */ TRUE);
}

/**
 * pika_display_shell_scale_by_values:
 * @shell:         the #PikaDisplayShell
 * @scale:         the new scale
 * @offset_x:      the new X offset
 * @offset_y:      the new Y offset
 * @resize_window: whether the display window should be resized
 *
 * Directly sets the image scale and image offsets used by the display. If
 * @resize_window is %TRUE then the display window is resized to better
 * accommodate the image, see pika_display_shell_shrink_wrap().
 **/
void
pika_display_shell_scale_by_values (PikaDisplayShell *shell,
                                    gdouble           scale,
                                    gint              offset_x,
                                    gint              offset_y,
                                    gboolean          resize_window)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  /*  Abort early if the values are all setup already. We don't
   *  want to inadvertently resize the window (bug #164281).
   */
  if (SCALE_EQUALS (pika_zoom_model_get_factor (shell->zoom), scale) &&
      shell->offset_x == offset_x &&
      shell->offset_y == offset_y)
    return;

  pika_display_shell_scale_save_revert_values (shell);

  /* freeze the active tool */
  pika_display_shell_pause (shell);

  pika_zoom_model_zoom (shell->zoom, PIKA_ZOOM_TO, scale);

  shell->offset_x = offset_x;
  shell->offset_y = offset_y;

  pika_display_shell_rotate_update_transform (shell);

  pika_display_shell_scale_resize (shell, resize_window, FALSE);

  /* re-enable the active tool */
  pika_display_shell_resume (shell);
}

void
pika_display_shell_scale_drag (PikaDisplayShell *shell,
                               gdouble           start_x,
                               gdouble           start_y,
                               gdouble           delta_x,
                               gdouble           delta_y)
{
  gdouble scale;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  scale = pika_zoom_model_get_factor (shell->zoom);

  if (delta_y != 0.0)
    {
      PikaDisplayConfig *config = shell->display->config;
      gdouble            speed  = config->drag_zoom_speed * 0.01;

      pika_display_shell_push_zoom_focus_pointer_pos (shell, start_x, start_y);

      if (config->drag_zoom_mode == PROP_DRAG_ZOOM_MODE_DISTANCE)
        {
          pika_display_shell_scale (shell,
                                    PIKA_ZOOM_TO,
                                    scale * exp (0.005 * speed * delta_y),
                                    PIKA_ZOOM_FOCUS_POINTER);
        }
      else if (delta_y > 0.0) /* drag_zoom_mode == PROP_DRAG_ZOOM_MODE_DURATION */
        {
          pika_display_shell_scale (shell,
                                    PIKA_ZOOM_TO,
                                    scale * (1 + 0.1 * speed),
                                    PIKA_ZOOM_FOCUS_POINTER);
        }
      else /* delta_y < 0.0 */
        {
          pika_display_shell_scale (shell,
                                    PIKA_ZOOM_TO,
                                    scale * (1 - 0.1 * speed),
                                    PIKA_ZOOM_FOCUS_POINTER);
        }


      if (shell->zoom_focus_point)
        {
          /* In case we hit one of the cases when the focus pointer
           * position was unused.
           */
          g_slice_free (GdkPoint, shell->zoom_focus_point);
          shell->zoom_focus_point = NULL;
        }
    }
}

/**
 * pika_display_shell_scale_shrink_wrap:
 * @shell: the #PikaDisplayShell
 *
 * Convenience function with the same functionality as
 * pika_display_shell_scale_resize(@shell, TRUE, grow_only).
 **/
void
pika_display_shell_scale_shrink_wrap (PikaDisplayShell *shell,
                                      gboolean          grow_only)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  pika_display_shell_scale_resize (shell, TRUE, grow_only);
}

/**
 * pika_display_shell_scale_resize:
 * @shell:          the #PikaDisplayShell
 * @resize_window:  whether the display window should be resized
 * @grow_only:      whether shrinking of the window is allowed or not
 *
 * Function commonly called after a change in display scale to make the changes
 * visible to the user. If @resize_window is %TRUE then the display window is
 * resized to accommodate the display image as per
 * pika_display_shell_shrink_wrap().
 **/
void
pika_display_shell_scale_resize (PikaDisplayShell *shell,
                                 gboolean          resize_window,
                                 gboolean          grow_only)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  /* freeze the active tool */
  pika_display_shell_pause (shell);

  if (resize_window)
    {
      PikaImageWindow *window = pika_display_shell_get_window (shell);

      if (window && pika_image_window_get_active_shell (window) == shell)
        {
          pika_image_window_shrink_wrap (window, grow_only);
        }
    }

  pika_display_shell_scroll_clamp_and_update (shell);
  pika_display_shell_scaled (shell);

  pika_display_shell_expose_full (shell);
  pika_display_shell_render_invalidate_full (shell);

  /* re-enable the active tool */
  pika_display_shell_resume (shell);
}

void
pika_display_shell_set_initial_scale (PikaDisplayShell *shell,
                                      gdouble           scale,
                                      gint             *display_width,
                                      gint             *display_height)
{
  PikaImage    *image;
  GdkRectangle  workarea;
  gint          image_width;
  gint          image_height;
  gint          monitor_width;
  gint          monitor_height;
  gint          shell_width;
  gint          shell_height;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  image = pika_display_get_image (shell->display);

  gdk_monitor_get_workarea (shell->initial_monitor, &workarea);

  image_width  = pika_image_get_width  (image);
  image_height = pika_image_get_height (image);

  monitor_width  = workarea.width  * 0.75;
  monitor_height = workarea.height * 0.75;

  /* We need to zoom before we use SCALE[XY] */
  pika_zoom_model_zoom (shell->zoom, PIKA_ZOOM_TO, scale);

  shell_width  = SCALEX (shell, image_width);
  shell_height = SCALEY (shell, image_height);

  if (shell->display->config->initial_zoom_to_fit)
    {
      /*  Limit to the size of the monitor...  */
      if (shell_width > monitor_width || shell_height > monitor_height)
        {
          gdouble new_scale;
          gdouble current = pika_zoom_model_get_factor (shell->zoom);

          new_scale = current * MIN (((gdouble) monitor_height) / shell_height,
                                     ((gdouble) monitor_width)  / shell_width);

          new_scale = pika_zoom_model_zoom_step (PIKA_ZOOM_OUT, new_scale, 0.0);

          /*  Since zooming out might skip a zoom step we zoom in
           *  again and test if we are small enough.
           */
          pika_zoom_model_zoom (shell->zoom, PIKA_ZOOM_TO,
                                pika_zoom_model_zoom_step (PIKA_ZOOM_IN,
                                                           new_scale, 0.0));

          if (SCALEX (shell, image_width)  > monitor_width ||
              SCALEY (shell, image_height) > monitor_height)
            pika_zoom_model_zoom (shell->zoom, PIKA_ZOOM_TO, new_scale);

          shell_width  = SCALEX (shell, image_width);
          shell_height = SCALEY (shell, image_height);
        }
    }
  else
    {
      /*  Set up size like above, but do not zoom to fit. Useful when
       *  working on large images.
       */
      shell_width  = MIN (shell_width,  monitor_width);
      shell_height = MIN (shell_height, monitor_height);
    }

  if (display_width)  *display_width  = shell_width;
  if (display_height) *display_height = shell_height;
}

/**
 * pika_display_shell_get_rotated_scale:
 * @shell:   the #PikaDisplayShell
 * @scale_x: horizontal scale output
 * @scale_y: vertical scale output
 *
 * Returns the screen space horizontal and vertical scaling
 * factors, taking rotation into account.
 **/
void
pika_display_shell_get_rotated_scale (PikaDisplayShell *shell,
                                      gdouble          *scale_x,
                                      gdouble          *scale_y)
{
  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  if (shell->rotate_angle == 0.0 || shell->scale_x == shell->scale_y)
    {
      if (scale_x) *scale_x = shell->scale_x;
      if (scale_y) *scale_y = shell->scale_y;
    }
  else
    {
      gdouble a     = G_PI * shell->rotate_angle / 180.0;
      gdouble cos_a = cos (a);
      gdouble sin_a = sin (a);

      if (scale_x) *scale_x = 1.0 / sqrt (SQR (cos_a / shell->scale_x) +
                                          SQR (sin_a / shell->scale_y));

      if (scale_y) *scale_y = 1.0 / sqrt (SQR (cos_a / shell->scale_y) +
                                          SQR (sin_a / shell->scale_x));
    }
}

/**
 * pika_display_shell_push_zoom_focus_pointer_pos:
 * @shell:
 * @x:
 * @y:
 *
 * When the zoom focus mechanism asks for the pointer the next time,
 * use @x and @y.
 *
 * It was primarily created for unit testing (see commit 7e3898da093).
 * Therefore it should not be used by our code. When it is, we should
 * make sure that @shell->zoom_focus_point has been properly used and
 * freed, if we don't want it to leak.
 * Just calling pika_display_shell_scale() is not enough as there are
 * currently some code paths where the values is not used.
 **/
void
pika_display_shell_push_zoom_focus_pointer_pos (PikaDisplayShell *shell,
                                                gint              x,
                                                gint              y)
{
  GdkPoint *point = g_slice_new (GdkPoint);
  point->x = x;
  point->y = y;

  g_slice_free (GdkPoint, shell->zoom_focus_point);
  shell->zoom_focus_point = point;
}


/*  private functions   */

static void
pika_display_shell_scale_get_screen_resolution (PikaDisplayShell *shell,
                                                gdouble          *xres,
                                                gdouble          *yres)
{
  gdouble x, y;

  if (shell->dot_for_dot)
    {
      pika_image_get_resolution (pika_display_get_image (shell->display),
                                 &x, &y);
    }
  else
    {
      x = shell->monitor_xres;
      y = shell->monitor_yres;
    }

  if (xres) *xres = x;
  if (yres) *yres = y;
}

/**
 * pika_display_shell_scale_get_image_size_for_scale:
 * @shell:
 * @scale:
 * @w:
 * @h:
 *
 **/
static void
pika_display_shell_scale_get_image_size_for_scale (PikaDisplayShell *shell,
                                                   gdouble           scale,
                                                   gint             *w,
                                                   gint             *h)
{
  PikaImage *image = pika_display_get_image (shell->display);
  gdouble    scale_x;
  gdouble    scale_y;

  pika_display_shell_calculate_scale_x_and_y (shell, scale, &scale_x, &scale_y);

  if (w) *w = scale_x * pika_image_get_width  (image);
  if (h) *h = scale_y * pika_image_get_height (image);
}

/**
 * pika_display_shell_calculate_scale_x_and_y:
 * @shell:
 * @scale:
 * @scale_x:
 * @scale_y:
 *
 **/
static void
pika_display_shell_calculate_scale_x_and_y (PikaDisplayShell *shell,
                                            gdouble           scale,
                                            gdouble          *scale_x,
                                            gdouble          *scale_y)
{
  PikaImage *image = pika_display_get_image (shell->display);
  gdouble    xres;
  gdouble    yres;
  gdouble    screen_xres;
  gdouble    screen_yres;

  pika_image_get_resolution (image, &xres, &yres);
  pika_display_shell_scale_get_screen_resolution (shell,
                                                  &screen_xres, &screen_yres);

  if (scale_x) *scale_x = scale * screen_xres / xres;
  if (scale_y) *scale_y = scale * screen_yres / yres;
}

/**
 * pika_display_shell_scale_to:
 * @shell:
 * @scale:
 * @viewport_x:
 * @viewport_y:
 *
 * Zooms. The display offsets are adjusted so that the point specified
 * by @x and @y doesn't change it's position on screen.
 **/
static void
pika_display_shell_scale_to (PikaDisplayShell *shell,
                             gdouble           scale,
                             gdouble           viewport_x,
                             gdouble           viewport_y)
{
  gdouble image_x, image_y;
  gdouble new_viewport_x, new_viewport_y;

  g_return_if_fail (PIKA_IS_DISPLAY_SHELL (shell));

  if (! shell->display)
    return;

  /* freeze the active tool */
  pika_display_shell_pause (shell);

  pika_display_shell_untransform_xy_f (shell,
                                       viewport_x,
                                       viewport_y,
                                       &image_x,
                                       &image_y);

  /* Note that we never come here if we need to resize_windows_on_zoom
   */
  pika_display_shell_scale_by_values (shell,
                                      scale,
                                      shell->offset_x,
                                      shell->offset_y,
                                      FALSE);

  pika_display_shell_transform_xy_f (shell,
                                     image_x,
                                     image_y,
                                     &new_viewport_x,
                                     &new_viewport_y);

  pika_display_shell_scroll (shell,
                             new_viewport_x - viewport_x,
                             new_viewport_y - viewport_y);

  /* re-enable the active tool */
  pika_display_shell_resume (shell);
}

/**
 * pika_display_shell_scale_fit_or_fill:
 * @shell: the #PikaDisplayShell
 * @fill:  whether to scale the image to fill the viewport,
 *         or fit inside the viewport
 *
 * A common implementation for pika_display_shell_scale_{fit_in,fill}().
 **/
static void
pika_display_shell_scale_fit_or_fill (PikaDisplayShell *shell,
                                      gboolean          fill)
{
  GeglRectangle bounding_box;
  gdouble       image_x;
  gdouble       image_y;
  gdouble       image_width;
  gdouble       image_height;
  gdouble       current_scale;
  gdouble       zoom_factor;

  if (! pika_display_shell_get_infinite_canvas (shell))
    {
      PikaImage *image = pika_display_get_image (shell->display);

      bounding_box.x      = 0;
      bounding_box.y      = 0;
      bounding_box.width  = pika_image_get_width  (image);
      bounding_box.height = pika_image_get_height (image);
    }
  else
    {
      bounding_box = pika_display_shell_get_bounding_box (shell);
    }

  pika_display_shell_transform_bounds (shell,
                                       bounding_box.x,
                                       bounding_box.y,
                                       bounding_box.x + bounding_box.width,
                                       bounding_box.y + bounding_box.height,
                                       &image_x,
                                       &image_y,
                                       &image_width,
                                       &image_height);

  image_width  -= image_x;
  image_height -= image_y;

  current_scale = pika_zoom_model_get_factor (shell->zoom);

  if (fill)
    {
      zoom_factor = MAX (shell->disp_width  / image_width,
                         shell->disp_height / image_height);
    }
  else
    {
      zoom_factor = MIN (shell->disp_width  / image_width,
                         shell->disp_height / image_height);
    }

  pika_display_shell_scale (shell,
                            PIKA_ZOOM_TO,
                            zoom_factor * current_scale,
                            PIKA_ZOOM_FOCUS_BEST_GUESS);

  pika_display_shell_scroll_center_content (shell, TRUE, TRUE);
}

static gboolean
pika_display_shell_scale_image_starts_to_fit (PikaDisplayShell *shell,
                                              gdouble           new_scale,
                                              gdouble           current_scale,
                                              gboolean         *horizontally,
                                              gboolean         *vertically)
{
  gboolean vertically_dummy;
  gboolean horizontally_dummy;

  if (! vertically)   vertically   = &vertically_dummy;
  if (! horizontally) horizontally = &horizontally_dummy;

  /* The image can only start to fit if we zoom out */
  if (new_scale > current_scale ||
      pika_display_shell_get_infinite_canvas (shell))
    {
      *vertically   = FALSE;
      *horizontally = FALSE;
    }
  else
    {
      gint current_scale_width;
      gint current_scale_height;
      gint new_scale_width;
      gint new_scale_height;

      pika_display_shell_scale_get_image_size_for_scale (shell,
                                                         current_scale,
                                                         &current_scale_width,
                                                         &current_scale_height);

      pika_display_shell_scale_get_image_size_for_scale (shell,
                                                         new_scale,
                                                         &new_scale_width,
                                                         &new_scale_height);

      *vertically   = (current_scale_width  >  shell->disp_width &&
                       new_scale_width      <= shell->disp_width);
      *horizontally = (current_scale_height >  shell->disp_height &&
                       new_scale_height     <= shell->disp_height);
    }

  return *vertically && *horizontally;
}

static gboolean
pika_display_shell_scale_image_stops_to_fit (PikaDisplayShell *shell,
                                             gdouble           new_scale,
                                             gdouble           current_scale,
                                             gboolean         *horizontally,
                                             gboolean         *vertically)
{
  return pika_display_shell_scale_image_starts_to_fit (shell,
                                                       current_scale,
                                                       new_scale,
                                                       horizontally,
                                                       vertically);
}

/**
 * pika_display_shell_scale_viewport_coord_almost_centered:
 * @shell:
 * @x:
 * @y:
 * @horizontally:
 * @vertically:
 *
 **/
static gboolean
pika_display_shell_scale_viewport_coord_almost_centered (PikaDisplayShell *shell,
                                                         gint              x,
                                                         gint              y,
                                                         gboolean         *horizontally,
                                                         gboolean         *vertically)
{
  gboolean local_horizontally = FALSE;
  gboolean local_vertically   = FALSE;
  gint     center_x           = shell->disp_width  / 2;
  gint     center_y           = shell->disp_height / 2;

  if (! pika_display_shell_get_infinite_canvas (shell))
    {
      local_horizontally = (x > center_x - ALMOST_CENTERED_THRESHOLD &&
                            x < center_x + ALMOST_CENTERED_THRESHOLD);

      local_vertically   = (y > center_y - ALMOST_CENTERED_THRESHOLD &&
                            y < center_y + ALMOST_CENTERED_THRESHOLD);
    }

  if (horizontally) *horizontally = local_horizontally;
  if (vertically)   *vertically   = local_vertically;

  return local_horizontally && local_vertically;
}

static void
pika_display_shell_scale_get_image_center_viewport (PikaDisplayShell *shell,
                                                    gint             *image_center_x,
                                                    gint             *image_center_y)
{
  gint sw, sh;

  pika_display_shell_scale_get_image_size (shell, &sw, &sh);

  if (image_center_x) *image_center_x = -shell->offset_x + sw / 2;
  if (image_center_y) *image_center_y = -shell->offset_y + sh / 2;
}

/**
 * pika_display_shell_scale_get_zoom_focus:
 * @shell:
 * @new_scale:
 * @x:
 * @y:
 *
 * Calculates the viewport coordinate to focus on when zooming
 * independently for each axis.
 **/
static void
pika_display_shell_scale_get_zoom_focus (PikaDisplayShell *shell,
                                         gdouble           new_scale,
                                         gdouble           current_scale,
                                         gdouble          *x,
                                         gdouble          *y,
                                         PikaZoomFocus     zoom_focus)
{
  GtkWidget *window = GTK_WIDGET (pika_display_shell_get_window (shell));
  GdkEvent  *event;
  gint       image_center_x;
  gint       image_center_y;
  gint       other_x;
  gint       other_y;

  /* Calculate stops-to-fit focus point */
  pika_display_shell_scale_get_image_center_viewport (shell,
                                                      &image_center_x,
                                                      &image_center_y);

  /* Calculate other focus point, default is the canvas center */
  other_x = shell->disp_width  / 2;
  other_y = shell->disp_height / 2;

  /*  Center on the mouse position instead of the display center if
   *  one of the following conditions are fulfilled and pointer is
   *  within the canvas:
   *
   *   (1) there's no current event (the action was triggered by an
   *       input controller)
   *   (2) the event originates from the canvas (a scroll event)
   *   (3) the event originates from the window (a key press event)
   *
   *  Basically the only situation where we don't want to center on
   *  mouse position is if the action is being called from a menu.
   */
  event = gtk_get_current_event ();

  if (! event ||
      gtk_get_event_widget (event) == shell->canvas ||
      gtk_get_event_widget (event) == window)
    {
      gint canvas_pointer_x;
      gint canvas_pointer_y;

      if (shell->zoom_focus_point)
        {
          canvas_pointer_x = shell->zoom_focus_point->x;
          canvas_pointer_y = shell->zoom_focus_point->y;

          g_slice_free (GdkPoint, shell->zoom_focus_point);
          shell->zoom_focus_point = NULL;
        }
      else
        {
          GdkDisplay *display = gtk_widget_get_display (shell->canvas);
          GdkSeat    *seat    = gdk_display_get_default_seat (display);

          gdk_window_get_device_position (gtk_widget_get_window (shell->canvas),
                                          gdk_seat_get_pointer (seat),
                                          &canvas_pointer_x,
                                          &canvas_pointer_y,
                                          NULL);
        }

      if (canvas_pointer_x >= 0 &&
          canvas_pointer_y >= 0 &&
          canvas_pointer_x <  shell->disp_width &&
          canvas_pointer_y <  shell->disp_height)
        {
          other_x = canvas_pointer_x;
          other_y = canvas_pointer_y;
        }
    }

  if (zoom_focus == PIKA_ZOOM_FOCUS_RETAIN_CENTERING_ELSE_BEST_GUESS)
    {
      if (pika_display_shell_scale_viewport_coord_almost_centered (shell,
                                                                   image_center_x,
                                                                   image_center_y,
                                                                   NULL,
                                                                   NULL))
        {
          zoom_focus = PIKA_ZOOM_FOCUS_IMAGE_CENTER;
        }
      else
        {
          zoom_focus = PIKA_ZOOM_FOCUS_BEST_GUESS;
        }
    }

  switch (zoom_focus)
    {
    case PIKA_ZOOM_FOCUS_POINTER:
      *x = other_x;
      *y = other_y;
      break;

    case PIKA_ZOOM_FOCUS_IMAGE_CENTER:
      *x = image_center_x;
      *y = image_center_y;
      break;

    case PIKA_ZOOM_FOCUS_BEST_GUESS:
    default:
      {
        gboolean within_horizontally, within_vertically;
        gboolean stops_horizontally, stops_vertically;

        pika_display_shell_scale_image_is_within_viewport (shell,
                                                           &within_horizontally,
                                                           &within_vertically);

        pika_display_shell_scale_image_stops_to_fit (shell,
                                                     new_scale,
                                                     current_scale,
                                                     &stops_horizontally,
                                                     &stops_vertically);

        *x = within_horizontally && ! stops_horizontally ? image_center_x : other_x;
        *y = within_vertically   && ! stops_vertically   ? image_center_y : other_y;
      }
      break;
    }
}
