/* GIMP - The GNU Image Manipulation Program
 * 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 <http://www.gnu.org/licenses/>.
 */

#include "config.h"

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

#include "libgimpbase/gimpbase.h"
#include "libgimpmath/gimpmath.h"
#include "libgimpwidgets/gimpwidgets.h"

#include "display-types.h"

#include "config/gimpdisplayconfig.h"

#include "core/gimp.h"
#include "core/gimpimage.h"

#include "gimpdisplay.h"
#include "gimpdisplayshell.h"
#include "gimpdisplayshell-draw.h"
#include "gimpdisplayshell-expose.h"
#include "gimpdisplayshell-scale.h"
#include "gimpdisplayshell-scroll.h"
#include "gimpdisplayshell-title.h"
#include "gimpdisplayshell-transform.h"
#include "gimpimagewindow.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      gimp_display_shell_scale_to             (GimpDisplayShell *shell,
                                                          gdouble           scale,
                                                          gint              viewport_x,
                                                          gint              viewport_y);

static gboolean  gimp_display_shell_scale_image_starts_to_fit
                                                         (GimpDisplayShell *shell,
                                                          gdouble           new_scale,
                                                          gdouble           current_scale,
                                                          gboolean         *vertically,
                                                          gboolean         *horizontally);
static gboolean  gimp_display_shell_scale_viewport_coord_almost_centered
                                                         (GimpDisplayShell *shell,
                                                          gint              x,
                                                          gint              y,
                                                          gboolean         *horizontally,
                                                          gboolean         *vertically);

static void      gimp_display_shell_scale_get_image_center_viewport
                                                         (GimpDisplayShell *shell,
                                                          gint             *image_center_x,
                                                          gint             *image_center_y);


static void      gimp_display_shell_scale_get_zoom_focus (GimpDisplayShell *shell,
                                                          gdouble           new_scale,
                                                          gdouble           current_scale,
                                                          gint             *x,
                                                          gint             *y,
                                                          GimpZoomFocus     zoom_focus);


/*  public functions  */

/**
 * gimp_display_shell_update_scrollbars_and_rulers:
 * @shell: the #GimpDisplayShell
 *
 **/
void
gimp_display_shell_update_scrollbars_and_rulers (GimpDisplayShell *shell)
{
  g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));

  gimp_display_shell_scale_update_scrollbars (shell);
  gimp_display_shell_scale_update_rulers (shell);
}

/**
 * gimp_display_shell_scale_update_scrollbars:
 * @shell:
 *
 **/
void
gimp_display_shell_scale_update_scrollbars (GimpDisplayShell *shell)
{
  g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));

  if (! shell->display)
    return;

  /* Horizontal scrollbar */

  g_object_freeze_notify (G_OBJECT (shell->hsbdata));

  /* Update upper and lower value before we set the new value */
  gimp_display_shell_scroll_setup_hscrollbar (shell, shell->offset_x);

  g_object_set (shell->hsbdata,
                "value",          (gdouble) shell->offset_x,
                "page-size",      (gdouble) shell->disp_width,
                "page-increment", (gdouble) shell->disp_width / 2,
                NULL);

  g_object_thaw_notify (G_OBJECT (shell->hsbdata)); /* emits "changed" */


  /* Vertcal scrollbar */

  g_object_freeze_notify (G_OBJECT (shell->vsbdata));

  /* Update upper and lower value before we set the new value */
  gimp_display_shell_scroll_setup_vscrollbar (shell, shell->offset_y);

  g_object_set (shell->vsbdata,
                "value",          (gdouble) shell->offset_y,
                "page-size",      (gdouble) shell->disp_height,
                "page-increment", (gdouble) shell->disp_height / 2,
                NULL);

  g_object_thaw_notify (G_OBJECT (shell->vsbdata)); /* emits "changed" */
}

/**
 * gimp_display_shell_scale_update_rulers:
 * @shell:
 *
 **/
void
gimp_display_shell_scale_update_rulers (GimpDisplayShell *shell)
{
  GimpImage *image;
  gint       image_width;
  gint       image_height;
  gdouble    resolution_x = 1.0;
  gdouble    resolution_y = 1.0;
  gdouble    horizontal_lower;
  gdouble    horizontal_upper;
  gdouble    horizontal_max_size;
  gdouble    vertical_lower;
  gdouble    vertical_upper;
  gdouble    vertical_max_size;

  if (! shell->display)
    return;

  image = gimp_display_get_image (shell->display);

  if (image)
    {
      image_width  = gimp_image_get_width  (image);
      image_height = gimp_image_get_height (image);

      gimp_image_get_resolution (image, &resolution_x, &resolution_y);
    }
  else
    {
      image_width  = shell->disp_width;
      image_height = shell->disp_height;
    }


  /* Initialize values */

  horizontal_lower = 0;
  vertical_lower   = 0;

  if (image)
    {
      horizontal_upper    = gimp_pixels_to_units (FUNSCALEX (shell,
                                                             shell->disp_width),
                                                  shell->unit,
                                                  resolution_x);
      horizontal_max_size = gimp_pixels_to_units (MAX (image_width,
                                                       image_height),
                                                  shell->unit,
                                                  resolution_x);

      vertical_upper      = gimp_pixels_to_units (FUNSCALEY (shell,
                                                             shell->disp_height),
                                                  shell->unit,
                                                  resolution_y);
      vertical_max_size   = gimp_pixels_to_units (MAX (image_width,
                                                       image_height),
                                                  shell->unit,
                                                  resolution_y);
    }
  else
    {
      horizontal_upper    = image_width;
      horizontal_max_size = MAX (image_width, image_height);

      vertical_upper      = image_height;
      vertical_max_size   = MAX (image_width, image_height);
    }


  /* Adjust due to scrolling */

  if (image)
    {
      gdouble offset_x;
      gdouble offset_y;

      offset_x = gimp_pixels_to_units (FUNSCALEX (shell,
                                                  (gdouble) shell->offset_x),
                                       shell->unit,
                                       resolution_x);

      offset_y = gimp_pixels_to_units (FUNSCALEX (shell,
                                                  (gdouble) shell->offset_y),
                                       shell->unit,
                                       resolution_y);

      horizontal_lower += offset_x;
      horizontal_upper += offset_x;

      vertical_lower   += offset_y;
      vertical_upper   += offset_y;
    }

  /* Finally setup the actual rulers */

  gimp_ruler_set_range (GIMP_RULER (shell->hrule),
                        horizontal_lower,
                        horizontal_upper,
                        horizontal_max_size);

  gimp_ruler_set_unit  (GIMP_RULER (shell->hrule),
                        shell->unit);

  gimp_ruler_set_range (GIMP_RULER (shell->vrule),
                        vertical_lower,
                        vertical_upper,
                        vertical_max_size);

  gimp_ruler_set_unit  (GIMP_RULER (shell->vrule),
                        shell->unit);
}

/**
 * gimp_display_shell_scale_revert:
 * @shell:     the #GimpDisplayShell
 *
 * Reverts the display to the previously used scale. If no previous
 * scale exist, then the call does nothing.
 *
 * Return value: %TRUE if the scale was reverted, otherwise %FALSE.
 **/
gboolean
gimp_display_shell_scale_revert (GimpDisplayShell *shell)
{
  g_return_val_if_fail (GIMP_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;

  gimp_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;
}

/**
 * gimp_display_shell_scale_can_revert:
 * @shell: the #GimpDisplayShell
 *
 * Return value: %TRUE if a previous display scale exists, otherwise %FALSE.
 **/
gboolean
gimp_display_shell_scale_can_revert (GimpDisplayShell *shell)
{
  g_return_val_if_fail (GIMP_IS_DISPLAY_SHELL (shell), FALSE);

  return (shell->last_scale > SCALE_EPSILON);
}

/**
 * gimp_display_shell_scale_set_dot_for_dot:
 * @shell:        the #GimpDisplayShell
 * @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
gimp_display_shell_scale_set_dot_for_dot (GimpDisplayShell *shell,
                                          gboolean          dot_for_dot)
{
  g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));

  if (dot_for_dot != shell->dot_for_dot)
    {
      /* freeze the active tool */
      gimp_display_shell_pause (shell);

      shell->dot_for_dot = dot_for_dot;

      gimp_display_shell_scale_changed (shell);

      gimp_display_shell_scale_resize (shell,
                                       shell->display->config->resize_windows_on_zoom,
                                       FALSE);

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

void
gimp_display_shell_get_screen_resolution (GimpDisplayShell *shell,
                                          gdouble          *xres,
                                          gdouble          *yres)
{
  gdouble x, y;

  g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));

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

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

/**
 * gimp_display_shell_scale:
 * @shell:     the #GimpDisplayShell
 * @zoom_type: whether to zoom in, our or to a specific scale
 * @scale:     ignored unless @zoom_type == %GIMP_ZOOM_TO
 *
 * This function figures out the context of the zoom and behaves
 * appropriatley thereafter.
 *
 **/
void
gimp_display_shell_scale (GimpDisplayShell *shell,
                          GimpZoomType      zoom_type,
                          gdouble           new_scale,
                          GimpZoomFocus     zoom_focus)
{
  gint    x, y;
  gdouble current_scale;
  gdouble real_new_scale;

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

  current_scale = gimp_zoom_model_get_factor (shell->zoom);

  if (zoom_type != GIMP_ZOOM_TO)
    {
      real_new_scale = gimp_zoom_model_zoom_step (zoom_type, current_scale);
    }
  else
    {
      real_new_scale = new_scale;
    }

  if (! SCALE_EQUALS (real_new_scale, current_scale))
    {
      if (shell->display->config->resize_windows_on_zoom)
        {
          GimpImageWindow *window = gimp_display_shell_get_window (shell);

          /* If the window is resized on zoom, simply do the zoom and
           * get things rolling
           */
          gimp_zoom_model_zoom (shell->zoom, GIMP_ZOOM_TO, real_new_scale);
          gimp_display_shell_scaled (shell);

          if (window && gimp_image_window_get_active_shell (window) == shell)
            {
              gimp_image_window_shrink_wrap (window, FALSE);
            }
        }
      else
        {
          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;
          gint     image_center_x;
          gint     image_center_y;

          gimp_display_shell_scale_get_zoom_focus (shell,
                                                   real_new_scale,
                                                   current_scale,
                                                   &x,
                                                   &y,
                                                   zoom_focus);
          gimp_display_shell_scale_get_image_center_viewport (shell,
                                                              &image_center_x,
                                                              &image_center_y);

          gimp_display_shell_scale_to (shell, real_new_scale, x, y);


          /* 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
           */
          gimp_display_shell_scale_image_starts_to_fit (shell,
                                                        real_new_scale,
                                                        current_scale,
                                                        &starts_fitting_horiz,
                                                        &starts_fitting_vert);

          gimp_display_shell_scale_viewport_coord_almost_centered (shell,
                                                                   x,
                                                                   y,
                                                                   &zoom_focus_almost_centered_horiz,
                                                                   &zoom_focus_almost_centered_vert);
          gimp_display_shell_scale_viewport_coord_almost_centered (shell,
                                                                   image_center_x,
                                                                   image_center_y,
                                                                   &image_center_almost_centered_horiz,
                                                                   &image_center_almost_centered_vert);
            
          gimp_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));
        }
    }
}

/**
 * gimp_display_shell_scale_fit_in:
 * @shell: the #GimpDisplayShell
 *
 * Sets the scale such that the entire image precisely fits in the display
 * area.
 **/
void
gimp_display_shell_scale_fit_in (GimpDisplayShell *shell)
{
  GimpImage *image;
  gint       image_width;
  gint       image_height;
  gdouble    xres;
  gdouble    yres;
  gdouble    zoom_factor;

  g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));

  image = gimp_display_get_image (shell->display);

  image_width  = gimp_image_get_width  (image);
  image_height = gimp_image_get_height (image);

  gimp_image_get_resolution (image, &xres, &yres);

  if (! shell->dot_for_dot)
    {
      image_width  = ROUND (image_width  * shell->monitor_xres / xres);
      image_height = ROUND (image_height * shell->monitor_yres / yres);
    }

  zoom_factor = MIN ((gdouble) shell->disp_width  / (gdouble) image_width,
                     (gdouble) shell->disp_height / (gdouble) image_height);

  gimp_display_shell_scale (shell,
                            GIMP_ZOOM_TO,
                            zoom_factor,
                            GIMP_ZOOM_FOCUS_BEST_GUESS);

  gimp_display_shell_scroll_center_image (shell, TRUE, TRUE);
}

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

  g_return_val_if_fail (GIMP_IS_DISPLAY_SHELL (shell), FALSE);

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

  gimp_display_shell_draw_get_scaled_image_size (shell, &sw, &sh);

  *horizontally = sw              <= shell->disp_width       &&
                  shell->offset_x <= 0                       &&
                  shell->offset_x >= sw - shell->disp_width;

  *vertically   = sh              <= shell->disp_height      &&
                  shell->offset_y <= 0                       &&
                  shell->offset_y >= sh - shell->disp_height;

  return *vertically && *horizontally;
}

/**
 * gimp_display_shell_scale_fill:
 * @shell: the #GimpDisplayShell
 *
 * Sets the scale such that the entire display area is precisely filled by the
 * image.
 **/
void
gimp_display_shell_scale_fill (GimpDisplayShell *shell)
{
  GimpImage *image;
  gint       image_width;
  gint       image_height;
  gdouble    xres;
  gdouble    yres;
  gdouble    zoom_factor;

  g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));

  image = gimp_display_get_image (shell->display);

  image_width  = gimp_image_get_width  (image);
  image_height = gimp_image_get_height (image);

  gimp_image_get_resolution (image, &xres, &yres);

  if (! shell->dot_for_dot)
    {
      image_width  = ROUND (image_width  * shell->monitor_xres / xres);
      image_height = ROUND (image_height * shell->monitor_yres / yres);
    }

  zoom_factor = MAX ((gdouble) shell->disp_width  / (gdouble) image_width,
                     (gdouble) shell->disp_height / (gdouble) image_height);

  gimp_display_shell_scale (shell,
                            GIMP_ZOOM_TO,
                            zoom_factor,
                            GIMP_ZOOM_FOCUS_BEST_GUESS);

  gimp_display_shell_scroll_center_image (shell, TRUE, TRUE);
}

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

  g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));

  now = time (NULL);

  if (now - shell->last_scale_time >= SCALE_TIMEOUT)
    {
      shell->last_scale    = gimp_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;
}

/**
 * gimp_display_shell_scale_by_values:
 * @shell:         the #GimpDisplayShell
 * @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
 * accomodate the image, see gimp_display_shell_shrink_wrap().
 **/
void
gimp_display_shell_scale_by_values (GimpDisplayShell *shell,
                                    gdouble           scale,
                                    gint              offset_x,
                                    gint              offset_y,
                                    gboolean          resize_window)
{
  g_return_if_fail (GIMP_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 (gimp_zoom_model_get_factor (shell->zoom), scale) &&
      shell->offset_x == offset_x &&
      shell->offset_y == offset_y)
    return;

  gimp_display_shell_scale_handle_zoom_revert (shell);

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

  gimp_zoom_model_zoom (shell->zoom, GIMP_ZOOM_TO, scale);

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

  gimp_display_shell_scale_resize (shell, resize_window, FALSE);

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

/**
 * gimp_display_shell_scale_shrink_wrap:
 * @shell: the #GimpDisplayShell
 *
 * Convenience function with the same functionality as
 * gimp_display_shell_scale_resize(@shell, TRUE, grow_only).
 **/
void
gimp_display_shell_scale_shrink_wrap (GimpDisplayShell *shell,
                                      gboolean          grow_only)
{
  g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));

  gimp_display_shell_scale_resize (shell, TRUE, grow_only);
}

/**
 * gimp_display_shell_scale_resize:
 * @shell:          the #GimpDisplayShell
 * @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 accomodate the display image as per
 * gimp_display_shell_shrink_wrap().
 **/
void
gimp_display_shell_scale_resize (GimpDisplayShell *shell,
                                 gboolean          resize_window,
                                 gboolean          grow_only)
{
  g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));

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

  if (resize_window)
    {
      GimpImageWindow *window = gimp_display_shell_get_window (shell);

      if (window && gimp_image_window_get_active_shell (window) == shell)
        {
          gimp_image_window_shrink_wrap (window, grow_only);
        }
    }

  gimp_display_shell_scroll_clamp_and_update (shell);
  gimp_display_shell_scaled (shell);

  gimp_display_shell_expose_full (shell);

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

/**
 * gimp_display_shell_calculate_scale_x_and_y:
 * @shell:
 * @scale:
 * @scale_x:
 * @scale_y:
 *
 **/
void
gimp_display_shell_calculate_scale_x_and_y (GimpDisplayShell *shell,
                                            gdouble           scale,
                                            gdouble          *scale_x,
                                            gdouble          *scale_y)
{
  GimpImage *image;
  gdouble    xres;
  gdouble    yres;
  gdouble    screen_xres;
  gdouble    screen_yres;

  g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));

  image = gimp_display_get_image (shell->display);

  g_return_if_fail (GIMP_IS_IMAGE (image));

  gimp_image_get_resolution (image, &xres, &yres);
  gimp_display_shell_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;
}

void
gimp_display_shell_set_initial_scale (GimpDisplayShell *shell,
                                      gdouble           scale,
                                      gint             *display_width,
                                      gint             *display_height)
{
  GimpImage *image;
  GdkScreen *screen;
  gint       image_width;
  gint       image_height;
  gint       shell_width;
  gint       shell_height;
  gint       screen_width;
  gint       screen_height;

  g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));

  image = gimp_display_get_image (shell->display);

  screen = gtk_widget_get_screen (GTK_WIDGET (shell));

  image_width  = gimp_image_get_width  (image);
  image_height = gimp_image_get_height (image);

  screen_width  = gdk_screen_get_width (screen)  * 0.75;
  screen_height = gdk_screen_get_height (screen) * 0.75;

  /* We need to zoom before we use SCALE[XY] */
  gimp_zoom_model_zoom (shell->zoom, GIMP_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 screen...  */
      if (shell_width > screen_width || shell_height > screen_height)
        {
          gdouble new_scale;
          gdouble current = gimp_zoom_model_get_factor (shell->zoom);

          new_scale = current * MIN (((gdouble) screen_height) / shell_height,
                                     ((gdouble) screen_width)  / shell_width);

          new_scale = gimp_zoom_model_zoom_step (GIMP_ZOOM_OUT, new_scale);

          /*  Since zooming out might skip a zoom step we zoom in
           *  again and test if we are small enough.
           */
          gimp_zoom_model_zoom (shell->zoom, GIMP_ZOOM_TO,
                                gimp_zoom_model_zoom_step (GIMP_ZOOM_IN,
                                                           new_scale));

          if (SCALEX (shell, image_width) > screen_width ||
              SCALEY (shell, image_height) > screen_height)
            gimp_zoom_model_zoom (shell->zoom, GIMP_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.
       */
      if (shell_width > screen_width)
        shell_width = screen_width;

      if (shell_height > screen_height)
        shell_height = screen_height;
    }

  if (display_width)
    *display_width = shell_width;

  if (display_height)
    *display_height = shell_height;
}

/**
 * gimp_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.
 **/
void
gimp_display_shell_push_zoom_focus_pointer_pos (GimpDisplayShell *shell,
                                                gint              x,
                                                gint              y)
{
  GdkPoint *point = g_slice_new (GdkPoint);
  point->x = x;
  point->y = y;

  g_queue_push_head (shell->zoom_focus_pointer_queue,
                     point);
}

/**
 * gimp_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
gimp_display_shell_scale_to (GimpDisplayShell *shell,
                             gdouble           scale,
                             gint              viewport_x,
                             gint              viewport_y)
{
  gdouble scale_x, scale_y;
  gdouble image_focus_x, image_focus_y;
  gint    target_offset_x, target_offset_y;

  g_return_if_fail (GIMP_IS_DISPLAY_SHELL (shell));

  if (! shell->display)
    return;

  gimp_display_shell_untransform_xy_f (shell,
                                       viewport_x,
                                       viewport_y,
                                       &image_focus_x,
                                       &image_focus_y);

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

  target_offset_x = scale_x * image_focus_x - viewport_x;
  target_offset_y = scale_y * image_focus_y - viewport_y;

  /* Note that we never come here if we need to
   * resize_windows_on_zoom
   */
  gimp_display_shell_scale_by_values (shell,
                                      scale,
                                      target_offset_x,
                                      target_offset_y,
                                      FALSE);
}

static gboolean
gimp_display_shell_scale_image_starts_to_fit (GimpDisplayShell *shell,
                                              gdouble           new_scale,
                                              gdouble           current_scale,
                                              gboolean         *vertically,
                                              gboolean         *horizontally)
{
  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)
    {
      *vertically   = FALSE;
      *horizontally = FALSE;
    }
  else
    {
      gint current_scale_width;
      gint current_scale_height;
      gint new_scale_width;
      gint new_scale_height;

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

      gimp_display_shell_draw_get_scaled_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
gimp_display_shell_scale_image_stops_to_fit (GimpDisplayShell *shell,
                                             gdouble           new_scale,
                                             gdouble           current_scale,
                                             gboolean         *vertically,
                                             gboolean         *horizontally)
{
  return gimp_display_shell_scale_image_starts_to_fit (shell,
                                                       current_scale,
                                                       new_scale,
                                                       vertically,
                                                       horizontally);
}

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

  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
gimp_display_shell_scale_get_image_center_viewport (GimpDisplayShell *shell,
                                                    gint             *image_center_x,
                                                    gint             *image_center_y)
{
  gint sw, sh;

  gimp_display_shell_draw_get_scaled_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;
}

/**
 * gimp_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
gimp_display_shell_scale_get_zoom_focus (GimpDisplayShell *shell,
                                         gdouble           new_scale,
                                         gdouble           current_scale,
                                         gint             *x,
                                         gint             *y,
                                         GimpZoomFocus     zoom_focus)
{
  GimpZoomFocus real_zoom_focus = zoom_focus;
  gint          image_center_x, image_center_y;
  gint          other_x, other_y;

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

  /* Calculate other focus point */
  {
    GdkEvent  *event;
    GtkWidget *window;
    gboolean   event_looks_sane;
    gboolean   cursor_within_canvas;
    gint       canvas_pointer_x, canvas_pointer_y;

    window = GTK_WIDGET (gimp_display_shell_get_window (shell));

    /*  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 ();

    event_looks_sane = (! event ||
                        gtk_get_event_widget (event) == shell->canvas ||
                        gtk_get_event_widget (event) == window);


    if (g_queue_peek_head (shell->zoom_focus_pointer_queue) == NULL)
      {
        gtk_widget_get_pointer (shell->canvas,
                                &canvas_pointer_x,
                                &canvas_pointer_y);
      }
    else
      {
        GdkPoint *point = g_queue_pop_head (shell->zoom_focus_pointer_queue);

        canvas_pointer_x = point->x;
        canvas_pointer_y = point->y;

        g_slice_free (GdkPoint, point);
      }

    cursor_within_canvas = canvas_pointer_x >= 0 &&
                           canvas_pointer_y >= 0 &&
                           canvas_pointer_x <  shell->disp_width &&
                           canvas_pointer_y <  shell->disp_height;


    if (event_looks_sane && cursor_within_canvas)
      {
        other_x = canvas_pointer_x;
        other_y = canvas_pointer_y;
      }
    else
      {
        other_x = shell->disp_width  / 2;
        other_y = shell->disp_height / 2;
      }
  }

  /* Decide which one to use for each axis */
  if (zoom_focus == GIMP_ZOOM_FOCUS_RETAIN_CENTERING_ELSE_BEST_GUESS)
    {
      gboolean centered;

      centered = gimp_display_shell_scale_viewport_coord_almost_centered (shell,
                                                                          image_center_x,
                                                                          image_center_y,
                                                                          NULL,
                                                                          NULL);
      real_zoom_focus = (centered ?
                         GIMP_ZOOM_FOCUS_IMAGE_CENTER :
                         GIMP_ZOOM_FOCUS_BEST_GUESS);
    }
  else
    {
      real_zoom_focus = zoom_focus;
    }

  switch (real_zoom_focus)
    {
    case GIMP_ZOOM_FOCUS_POINTER:
      *x = other_x;
      *y = other_y;
      break;

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

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

        gimp_display_shell_scale_image_is_within_viewport (shell,
                                                           &within_horizontally,
                                                           &within_vertically);

        gimp_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;
    }
}
