/* eLectrix - a pdf viewer
 * Copyright (C) 2010, 2011 Martin Linder <mali2297@users.sf.net>
 * 
 * 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 2 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/gpl-2.0.html>.
 */
#include <gdk/gdkkeysyms.h>
#include "e6x-pref.h"
#include "e6x-view.h"

#define E6X_VIEW_GET_PRIVATE(o) \
  (G_TYPE_INSTANCE_GET_PRIVATE ((o), E6X_TYPE_VIEW, E6xViewPrivate))

struct _E6xViewPrivate
{
  E6xDocument *doc;
  cairo_surface_t *surface;
  GdkPoint offset;
  GdkPoint ref_point;
  gdouble scale;
  GList *history;
  gboolean in_history;
  gboolean awaits_expose;
};

static struct
{
  gdouble scroll_factor;
  gdouble zoom_min;
  gdouble zoom_max;
  gdouble zoom_step;
} e6x_view_settings = {
  0.1,
  0.25, 
  4.0, 
  0.25
};
  
G_DEFINE_TYPE (E6xView, e6x_view, GTK_TYPE_WIDGET)

/* Standard GObject methods */
static void e6x_view_class_init (E6xViewClass *klass);
static void e6x_view_init (E6xView *view);
static void e6x_view_finalize (GObject *object);
static void e6x_view_dispose (GObject *object);

/* Customized GtkWidget methods */
static void e6x_view_realize (GtkWidget *widget);
static void e6x_view_size_allocate (GtkWidget *widget, 
                                    GtkAllocation *allocation);
static gboolean e6x_view_expose (GtkWidget *widget, 
                                 GdkEventExpose *event);

/* Method to add scrolling ability */
static gboolean e6x_view_set_adjustments (E6xView *view,
                                          GtkAdjustment *hadj,
                                          GtkAdjustment *vadj);

/* Callbacks */
static gboolean e6x_view_tooltip_query (GtkWidget *widget,
                                        gint x,
                                        gint y,
                                        gboolean keyboard_mode,
                                        GtkTooltip *tooltip,
                                        gpointer data);
static gboolean e6x_view_button_press_event (GtkWidget *widget,
                                             GdkEventButton *event);
static gboolean e6x_view_button_release_event (GtkWidget *widget,
                                               GdkEventButton *event);
static gboolean e6x_view_motion_notify_event (GtkWidget *widget,
                                              GdkEventMotion *event);
static gboolean e6x_view_mouse_scroll_event (GtkWidget *widget,
                                             GdkEventScroll *event);
static void e6x_view_key_scroll_event (E6xView *view, 
                                       GtkScrollType scroll);
static void e6x_view_adjustment_value_changed (GtkAdjustment *adj, 
                                               E6xView *view);
static void e6x_view_doc_changed (E6xDocument *doc, 
                                  E6xView *view);
static void e6x_view_page_no_changed (E6xDocument *doc, 
                                      E6xView *view);
static void e6x_view_scale_changed (E6xDocument *doc, 
                                    E6xView *view);

/* Helpers */
static void e6x_view_draw (GtkWidget *widget);
static void e6x_view_draw_selection (GtkWidget *widget,
                                     const GdkRectangle *rect);
static void e6x_view_scroll (E6xView *view, 
                             gint dx, 
                             gint dy);
static void e6x_view_scroll_event (E6xView *view,
                                   gdouble h_value,
                                   gdouble v_value);
static void e6x_view_add_bindings (GtkBindingSet *binding_set);
static void e6x_view_load_settings ();

GtkWidget *
e6x_view_new ()
{
  return g_object_new (E6X_TYPE_VIEW, NULL);
}

void
e6x_view_set_document (E6xView *view, 
                       E6xDocument *doc)
{
  g_return_if_fail (view != NULL);
  E6xViewPrivate *priv = view->priv;
  
  if (doc == priv->doc)
  {
    return;
  }
  
  if (priv->history != NULL)
  {
    priv->history = g_list_first (priv->history);
    g_list_free (priv->history);
    priv->history = NULL;
  }
  
  if (priv->doc != NULL)
  {
    g_signal_handlers_disconnect_by_func (priv->doc,
                                          e6x_view_doc_changed,
                                          view);
    g_signal_handlers_disconnect_by_func (priv->doc,
                                          e6x_view_scale_changed,
                                          view);
    g_signal_handlers_disconnect_by_func (priv->doc,
                                          e6x_view_page_no_changed,
                                          view);
    g_object_unref (priv->doc);
  }

  priv->doc = doc;
  
  if (priv->doc != NULL)
  {
    g_object_ref (priv->doc);
  
    priv->scale = priv->doc->scale;
    priv->history = g_list_prepend (priv->history,
                                    GUINT_TO_POINTER (doc->page_no));

    g_signal_connect (priv->doc, "changed::page-no",
                      G_CALLBACK (e6x_view_page_no_changed), view);
    g_signal_connect (priv->doc, "changed::multiple",
                      G_CALLBACK (e6x_view_page_no_changed), view);
    g_signal_connect (priv->doc, "changed::scale",
                      G_CALLBACK (e6x_view_scale_changed), view);
    g_signal_connect (priv->doc, "changed::multiple",
                      G_CALLBACK (e6x_view_scale_changed), view);
    g_signal_connect_after (priv->doc, "changed",
                            G_CALLBACK (e6x_view_doc_changed), view);
    
    if (gtk_widget_get_realized (GTK_WIDGET (view)))
      e6x_view_refresh (view);
  }
}


E6xDocument *
e6x_view_get_document (E6xView *view)
{
  g_return_val_if_fail (view != NULL, NULL);
  return view->priv->doc;
}


void
e6x_view_refresh (E6xView *view)
{
  g_return_if_fail (view != NULL);
  E6xViewPrivate *priv = view->priv;
  GtkWidget *widget = GTK_WIDGET (view);
  GdkCursor *cursor;
  
  e6x_view_set_select_mode (view, FALSE);
  g_return_if_fail (priv->doc);

  cursor = gdk_cursor_new (GDK_WATCH);
  gdk_window_set_cursor (widget->window, cursor);
  gdk_cursor_unref(cursor);
  while (gtk_events_pending ())
    gtk_main_iteration ();

  if (priv->surface)
  {
    cairo_surface_destroy (priv->surface);
    priv->surface = NULL;
  }

  priv->surface = e6x_document_render_page (priv->doc);
  
  if (G_LIKELY (priv->surface))
  {
    view->hadj->lower = 0;
    view->hadj->upper = cairo_image_surface_get_width (priv->surface);
    gtk_adjustment_changed (view->hadj);

    view->vadj->lower = 0;
    view->vadj->upper = cairo_image_surface_get_height (priv->surface);
    gtk_adjustment_changed (view->vadj);
  }
  
  gdk_window_set_cursor (widget->window, NULL);
  gtk_widget_queue_draw (widget);
}


gboolean
e6x_view_show_nth_match (E6xView *view,
                         const gchar *string,
                         guint match_no)
{
  g_return_val_if_fail (view != NULL, FALSE);
  E6xViewPrivate *priv = view->priv;
  GdkRectangle *rect;
  
  rect = e6x_document_get_nth_match (priv->doc, priv->doc->page_no,
                                     string, match_no);
  
  if (G_LIKELY (rect))
  {
    GtkAdjustment *hadj = view->hadj;
    GtkAdjustment *vadj = view->vadj;
    gdouble h_value, v_value;
    
    h_value = CLAMP (rect->x - hadj->page_size / 2, 
                     hadj->lower, 
                     hadj->upper - hadj->page_size);
    v_value = CLAMP (rect->y - vadj->page_size / 2, 
                     vadj->lower, 
                     vadj->upper - vadj->page_size);
    e6x_view_scroll (view, hadj->value - h_value, vadj->value - v_value);
    e6x_view_draw (GTK_WIDGET (view));
    rect->x += priv->offset.x;
    rect->y += priv->offset.y;
    e6x_view_draw_selection (GTK_WIDGET (view), rect);

    g_free (rect);
    return TRUE;
  }
  else
    return FALSE;
}


gboolean
e6x_view_has_history (E6xView *view,
                      E6xDirection dir)
{
  g_return_val_if_fail (view != NULL, FALSE);
  E6xViewPrivate *priv = view->priv;

  if (priv->history == NULL)
    return FALSE;
  
  if (dir == BACKWARD)
    return priv->history->next != NULL;
  else
    return priv->history->prev != NULL;
}


void
e6x_view_walk_history (E6xView *view,
                       E6xDirection dir)
{
  g_return_if_fail (view != NULL);
  E6xViewPrivate *priv = view->priv;
  
  if (G_UNLIKELY (!e6x_view_has_history (view, dir)))
    return;
  
  priv->in_history = TRUE;
  if (dir == BACKWARD)
    priv->history = g_list_next (priv->history);
  else
    priv->history = g_list_previous (priv->history);
  e6x_document_set_page_no (priv->doc, 
                            GPOINTER_TO_UINT (priv->history->data));
}


void
e6x_view_set_select_mode (E6xView *view, gboolean mode)
{
  GtkWidget *widget = GTK_WIDGET (view);
  
  if (mode)
  {
    GdkCursor *cursor = NULL;

    cursor = gdk_cursor_new (GDK_TCROSS);
    gdk_window_set_cursor (widget->window, cursor);
    gdk_cursor_unref(cursor);
  }
  else
    gdk_window_set_cursor (widget->window, NULL);
  
  view->select_mode = mode;
}


static void
e6x_view_class_init (E6xViewClass *klass)
{
  g_type_class_add_private (klass, sizeof (E6xViewPrivate));

  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->dispose = e6x_view_dispose;
  object_class->finalize = e6x_view_finalize;

  widget_class->realize = e6x_view_realize;
  widget_class->expose_event = e6x_view_expose;
  widget_class->size_allocate = e6x_view_size_allocate;
  widget_class->scroll_event = e6x_view_mouse_scroll_event;
  widget_class->motion_notify_event = e6x_view_motion_notify_event;
  widget_class->button_press_event = e6x_view_button_press_event;  
  widget_class->button_release_event = e6x_view_button_release_event;
  
  klass->set_scroll_adjustments = e6x_view_set_adjustments;

  widget_class->set_scroll_adjustments_signal =
    g_signal_new ("set_scroll_adjustments",
                  G_TYPE_FROM_CLASS (object_class),
                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
                  G_STRUCT_OFFSET (E6xViewClass, set_scroll_adjustments),
                  NULL, NULL,
                  gtk_marshal_VOID__POINTER_POINTER,
                  G_TYPE_NONE, 2,
                  GTK_TYPE_ADJUSTMENT,
                  GTK_TYPE_ADJUSTMENT);
  
  g_signal_new ("key_scroll_event",
                G_TYPE_FROM_CLASS (object_class),
                G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
                0, NULL, NULL,
                g_cclosure_marshal_VOID__ENUM,
                G_TYPE_NONE, 1,
                GTK_TYPE_SCROLL_TYPE);  

  e6x_view_add_bindings (gtk_binding_set_by_class (klass));
  e6x_view_load_settings ();
}


static void
e6x_view_init (E6xView *view)
{
  E6xViewPrivate *priv = E6X_VIEW_GET_PRIVATE (view);

  priv->doc = NULL;
  priv->surface = NULL;
  priv->scale = 1.0;
  priv->history = NULL;
  priv->in_history = FALSE;
  
  view->priv = priv;
  view->select_mode = FALSE;
  
  g_signal_connect (G_OBJECT (view), "key_scroll_event",
                    G_CALLBACK (e6x_view_key_scroll_event), NULL);
  
  gtk_widget_set_has_tooltip (GTK_WIDGET (view), TRUE);  
  g_signal_connect (G_OBJECT (view), "query-tooltip",
                    G_CALLBACK (e6x_view_tooltip_query), NULL);
}


static void
e6x_view_finalize (GObject *object)
{
  G_OBJECT_CLASS (e6x_view_parent_class)->finalize (object);
}


static void 
e6x_view_dispose (GObject *object)
{
  E6xViewPrivate *priv = E6X_VIEW (object)->priv;

  e6x_view_set_adjustments (E6X_VIEW (object), NULL, NULL);
  e6x_view_set_document (E6X_VIEW (object), NULL);

  if (priv->surface)
  {
    cairo_surface_destroy (priv->surface);
    priv->surface = NULL;
  }

  G_OBJECT_CLASS (e6x_view_parent_class)->dispose (object);
}


static void
e6x_view_realize (GtkWidget *widget)
{
  GdkWindowAttr attributes;
  gint attributes_mask;
  
  attributes.window_type = GDK_WINDOW_CHILD;
  attributes.x = widget->allocation.x;
  attributes.y = widget->allocation.y;
  attributes.width = widget->allocation.width;
  attributes.height = widget->allocation.height;
  attributes.wclass = GDK_INPUT_OUTPUT;
  attributes.visual = gtk_widget_get_visual (widget);
  attributes.colormap = gtk_widget_get_colormap (widget);
  attributes.event_mask = gtk_widget_get_events (widget);
  attributes.event_mask |= (GDK_EXPOSURE_MASK |
                            GDK_POINTER_MOTION_MASK |
                            GDK_POINTER_MOTION_HINT_MASK |
                            GDK_BUTTON_PRESS_MASK |
                            GDK_BUTTON_RELEASE_MASK |
                            GDK_KEY_PRESS_MASK |
                            GDK_KEY_RELEASE_MASK);

  attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL | GDK_WA_COLORMAP;

  widget->window = gdk_window_new (gtk_widget_get_parent_window (widget),
                                   &attributes, attributes_mask);
  gdk_window_set_user_data (widget->window, E6X_VIEW (widget));

  widget->style = gtk_style_attach (widget->style, widget->window);
  gtk_style_set_background (widget->style, widget->window, GTK_STATE_ACTIVE);
  
  gtk_widget_set_realized (widget, TRUE);
  gtk_widget_set_can_focus (widget, TRUE);
  gtk_widget_grab_focus (widget);
}


static void
e6x_view_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
{
  widget->allocation = *allocation;
  
  if (G_LIKELY (gtk_widget_get_realized (widget)))
  {
    GtkAdjustment *hadj, *vadj;
    
    gdk_window_move_resize (widget->window,
                            allocation->x, allocation->y,
                            allocation->width, allocation->height);

    hadj = E6X_VIEW (widget)->hadj;
    if (G_LIKELY (hadj))
    {
      hadj->page_size = allocation->width;
      hadj->page_increment = hadj->page_size;
      hadj->step_increment = e6x_view_settings.scroll_factor * hadj->page_size;
      gtk_adjustment_changed (hadj);
    }
    
    vadj = E6X_VIEW (widget)->vadj;
    if (G_LIKELY (vadj))
    {
      vadj->page_size = allocation->height;
      vadj->page_increment = vadj->page_size;
      vadj->step_increment = e6x_view_settings.scroll_factor * vadj->page_size;
      gtk_adjustment_changed (vadj);
    }
  }
}


static gboolean
e6x_view_expose (GtkWidget *widget, 
                 GdkEventExpose *event)
{
  E6xViewPrivate *priv = E6X_VIEW (widget)->priv;
  GtkAdjustment *hadj, *vadj;
  
  /* Center surface horizontally if smaller than window width. */
  hadj = E6X_VIEW (widget)->hadj;
  if (hadj->page_size >= hadj->upper - hadj->lower)
    priv->offset.x = (hadj->page_size - hadj->upper + hadj->lower) / 2;
  else if (priv->offset.x > - hadj->lower)
    priv->offset.x = - hadj->lower;

  /* Synchronize horizontal adjustment value with offset. */
  gtk_adjustment_set_value (hadj,
                            CLAMP (- priv->offset.x, hadj->lower,
                                   hadj->upper - hadj->page_size));

  /* Center surface vertically if smaller than window height. */
  vadj = E6X_VIEW (widget)->vadj;
  if (vadj->page_size >= vadj->upper - vadj->lower)
    priv->offset.y = (vadj->page_size - vadj->upper + vadj->lower) / 2;
  else if (priv->offset.y > - vadj->lower)
    priv->offset.y = - vadj->lower;

  /* Synchronize vertical adjustment value with offset. */
  gtk_adjustment_set_value (vadj,
                            CLAMP (- priv->offset.y, vadj->lower,
                                   vadj->upper - vadj->page_size));

  /* Draw the exposed area. */
  if (G_LIKELY (priv->surface))
  {
    cairo_t *cr;
    
    cr = gdk_cairo_create (widget->window);
    cairo_set_source_surface (cr, priv->surface,
                              priv->offset.x, priv->offset.y);
    gdk_cairo_region (cr, event->region);
    cairo_fill (cr);
    cairo_destroy (cr);
  }

  /* Enable respond to new scroll event */
  priv->awaits_expose = FALSE;
  
  return FALSE;
}


static gboolean
e6x_view_set_adjustments (E6xView *view,
                          GtkAdjustment *hadj,
                          GtkAdjustment *vadj)
{
  if (view->hadj)
  {
    g_signal_handlers_disconnect_by_func (view->hadj,
                                          e6x_view_adjustment_value_changed,
                                          view);
    g_object_unref (view->hadj);
  }
  if (view->vadj)
  {
    g_signal_handlers_disconnect_by_func (view->vadj,
                                          e6x_view_adjustment_value_changed,
                                          view);
    g_object_unref (view->vadj);
  }

  view->hadj = hadj;
  view->vadj = vadj;

  if (view->hadj)
  {
    g_signal_connect (G_OBJECT(view->hadj), "value-changed",
                      G_CALLBACK (e6x_view_adjustment_value_changed), view);
    g_object_ref (view->hadj);
  }
  if (view->vadj)
  {
    g_signal_connect (G_OBJECT (view->vadj), "value-changed",
                      G_CALLBACK (e6x_view_adjustment_value_changed), view);
    g_object_ref (view->vadj);
  }

  return TRUE;
}


static gboolean 
e6x_view_tooltip_query (GtkWidget *widget,
                        gint x,
                        gint y,
                        gboolean keyboard_mode,
                        GtkTooltip *tooltip,
                        gpointer data)
{
  E6xViewPrivate *priv = E6X_VIEW (widget)->priv;
  GdkPoint pos = {0, 0};
  gchar *text = NULL;

  if (priv->doc == NULL)
    return FALSE;
  
  pos.x = x - priv->offset.x;
  pos.y = y - priv->offset.y;
  text = e6x_document_get_tip (priv->doc, pos);
  if (text != NULL)
  {
    gtk_tooltip_set_text (tooltip, text);
    g_free (text);
    return TRUE;
  }
  
  return FALSE;
}


static gboolean
e6x_view_button_press_event (GtkWidget *widget,
                             GdkEventButton *event)
{
  E6xViewPrivate *priv = E6X_VIEW (widget)->priv;
  
  if (!gtk_widget_has_focus (widget))
    gtk_widget_grab_focus (widget);

  if (event->button == 1 &&
      G_LIKELY (priv->doc != NULL) &&
      !E6X_VIEW (widget)->select_mode)
  {
    GdkPoint pos = {0, 0};
    gboolean at_link = FALSE;
    
    pos.x = event->x - priv->offset.x;
    pos.y = event->y - priv->offset.y;
    at_link = e6x_document_go_to_link (priv->doc, pos);
    if (!at_link)
    {
      GdkCursor *cursor = NULL;
      
      cursor = gdk_cursor_new (GDK_FLEUR);
      gdk_window_set_cursor (widget->window, cursor);
      gdk_cursor_unref(cursor);
    }
  }
  
  priv->ref_point.x = event->x;
  priv->ref_point.y = event->y;
  
  return FALSE;
}


static gboolean
e6x_view_button_release_event (GtkWidget *widget,
                               GdkEventButton *event)
{
  E6xViewPrivate *priv = E6X_VIEW (widget)->priv;
  
  if (event->button == 1 && 
      G_LIKELY (priv->doc) && 
      E6X_VIEW (widget)->select_mode)
  {
    GdkRectangle rect;
    gchar *string;
    
    rect.x = priv->ref_point.x - priv->offset.x;
    rect.y =  priv->ref_point.y - priv->offset.y;
    rect.width =  event->x - priv->ref_point.x;
    rect.height = event->y -  priv->ref_point.y;
    
    string = e6x_document_get_text (priv->doc, rect);
    if (string)
    {
      GtkClipboard *clipboard = gtk_clipboard_get (GDK_SELECTION_PRIMARY);
      gtk_clipboard_set_text (clipboard, string, -1);
      g_free (string);
    }
  }

  e6x_view_set_select_mode (E6X_VIEW (widget), FALSE);
  gdk_window_set_cursor (widget->window, NULL);
  e6x_view_draw (widget);

  return FALSE;
}


static gboolean 
e6x_view_motion_notify_event (GtkWidget *widget,
                              GdkEventMotion *event)
{
  E6xViewPrivate *priv = E6X_VIEW (widget)->priv;
  gint x, y;
  GdkModifierType state;
  E6xView *view = E6X_VIEW (widget);
  GtkAdjustment *hadj = view->hadj, *vadj = view->vadj;
  
  if (G_UNLIKELY (!priv->doc))
    return FALSE;

  if (event->is_hint)
    gdk_window_get_pointer (event->window, &x, &y, &state);
  else
  {
    x = event->x;
    y = event->y;
    state = event->state;
  }

  if (state & GDK_BUTTON1_MASK)
  {
    if (view->select_mode)
    {
      GdkRectangle rect;
    
      rect.x = priv->ref_point.x;
      rect.y =  priv->ref_point.y;
      rect.width =  x - priv->ref_point.x;
      rect.height = y -  priv->ref_point.y;
    
      e6x_view_draw_selection (widget, &rect);
    }
    else
    {
      gdouble h_value = hadj->value;
    
      if (hadj->upper - hadj->lower > hadj->page_size)
        h_value = CLAMP (h_value - x + priv->ref_point.x,
                         hadj->lower, hadj->upper - hadj->page_size);

      gdouble v_value = vadj->value;
    
      if (vadj->upper - vadj->lower > vadj->page_size)
        v_value = CLAMP (v_value - y + priv->ref_point.y,
                         vadj->lower, vadj->upper - vadj->page_size);

      e6x_view_scroll (view, 
                       (gint) (hadj->value - h_value), 
                       (gint) (vadj->value - v_value));

      priv->ref_point.x = x;
      priv->ref_point.y = y;
    }
  }
  else if (!view->select_mode)
  {
    GdkPoint pos = {0, 0};
    
    pos.x = x - priv->offset.x;
    pos.y = y - priv->offset.y;

    if (e6x_document_is_at_link (priv->doc, pos))
    {
      GdkCursor *cursor = gdk_cursor_new (GDK_HAND1);
      gdk_window_set_cursor (widget->window, cursor);
      gdk_cursor_unref(cursor);
    }
    else
      gdk_window_set_cursor(widget->window, NULL);
  }

  return FALSE;
}


static gboolean 
e6x_view_mouse_scroll_event (GtkWidget *widget,
                             GdkEventScroll *event)
{
  E6xView *view = E6X_VIEW (widget);
  E6xViewPrivate *priv = view->priv;
  
  if (priv->doc != NULL &&
      event->state & GDK_CONTROL_MASK)
  {
    gdouble scale = priv->doc->scale;
    
    switch (event->direction)
    {
    case GDK_SCROLL_LEFT:
      scale = e6x_view_settings.zoom_min;
      break;
    case GDK_SCROLL_RIGHT:
      scale = e6x_view_settings.zoom_max;
      break;
    case GDK_SCROLL_UP:
      scale += e6x_view_settings.zoom_step;
      break;
    case GDK_SCROLL_DOWN:
      scale -= e6x_view_settings.zoom_step;
      break;
    default:
      g_warn_if_reached ();
      break;
    }
    
    scale = CLAMP (scale, 
                   e6x_view_settings.zoom_min, 
                   e6x_view_settings.zoom_max);
    e6x_document_set_scale (priv->doc, scale);
  }
  else
  {
    GtkAdjustment *hadj = view->hadj, *vadj = view->vadj;
    gdouble h_value = hadj->value, v_value = vadj->value;
  
    switch (event->direction)
    {
    case GDK_SCROLL_LEFT:
      h_value -= hadj->step_increment;
      break;
    case GDK_SCROLL_RIGHT:
      h_value += hadj->step_increment;
      break;
    case GDK_SCROLL_UP:
      v_value -= vadj->step_increment;
      break;
    case GDK_SCROLL_DOWN:
      v_value += vadj->step_increment;
      break;
    default:
      g_warn_if_reached ();
      break;
    }
    
    e6x_view_scroll_event (view, h_value, v_value);
  }
  
  return FALSE;
}


static void 
e6x_view_key_scroll_event (E6xView *view, 
                           GtkScrollType scroll)
{
  GtkAdjustment *hadj = view->hadj, *vadj = view->vadj;
  gdouble h_value = hadj->value, v_value = vadj->value;
  
  switch (scroll)
  {
  case GTK_SCROLL_STEP_LEFT:
    h_value -= hadj->step_increment;
    break;
  case GTK_SCROLL_STEP_RIGHT:
    h_value += hadj->step_increment;
    break;
  case GTK_SCROLL_PAGE_LEFT:
    h_value -= hadj->page_increment;
    break;
  case GTK_SCROLL_PAGE_RIGHT:
    h_value += hadj->page_increment;
    break;
  case GTK_SCROLL_STEP_UP:
    /* Fall through */
  case GTK_SCROLL_STEP_BACKWARD:
    v_value -= vadj->step_increment;
    break;
  case GTK_SCROLL_STEP_DOWN:
    /* Fall through */
  case GTK_SCROLL_STEP_FORWARD:
    v_value += vadj->step_increment;
    break;
  case GTK_SCROLL_PAGE_UP:
    /* Fall through */
  case GTK_SCROLL_PAGE_BACKWARD:
    v_value -= vadj->page_increment;
    break;
  case GTK_SCROLL_PAGE_DOWN:
    /* Fall through */
  case GTK_SCROLL_PAGE_FORWARD:
    v_value += vadj->page_increment;
    break;
  case GTK_SCROLL_START:
    v_value = vadj->lower;
    break;
  case GTK_SCROLL_END:
    v_value = vadj->upper;
    break;
  default:
    g_warn_if_reached ();
    break;
  }
  
  e6x_view_scroll_event (view, h_value, v_value);
}


static void 
e6x_view_adjustment_value_changed (GtkAdjustment *adj, 
                                   E6xView *view)
{
  if (adj->upper - adj->lower <= adj->page_size)
    return;

  E6xViewPrivate *priv = view->priv;

  if (adj == view->hadj)
    e6x_view_scroll (view, - priv->offset.x - (gint) adj->value, 0);
  else if (adj == view->vadj)
    e6x_view_scroll (view, 0, - priv->offset.y - (gint) adj->value);
}

/**
 * e6x_view_doc_changed:
 * 
 * A callback after any change in the document.
 **/
static void
e6x_view_doc_changed (E6xDocument *doc, 
                      E6xView *view)
{
  e6x_view_refresh (view);
}


static void 
e6x_view_page_no_changed (E6xDocument *doc,
                          E6xView *view)
{
  E6xViewPrivate *priv = view->priv;
  GList *future = NULL;
  
  if (doc->page_no == GPOINTER_TO_UINT (priv->history->data))
    return;
  
  if (priv->in_history)
  {
    priv->in_history = FALSE;
    return;
  }
  
  if (priv->history != NULL)
    future = priv->history->prev;
  if (future != NULL)
  {
    priv->history->prev = NULL;
    future->next = NULL;
    future = g_list_first (future);
    g_list_free (future);
  }
  priv->history = g_list_prepend (priv->history,
                                  GUINT_TO_POINTER (doc->page_no));
}


/**
 * e6x_view_scale_changed:
 * 
 * A callback after a zoom event. It sets the offset so that the
 * center point remains constant. It should be called before 
 * e6x_view_doc_changed.
 **/
static void 
e6x_view_scale_changed (E6xDocument *doc, 
                        E6xView *view)
{
  E6xViewPrivate *priv = view->priv;
  GtkAdjustment *hadj = view->hadj, *vadj = view->vadj;
  gdouble scale_factor, h_value, v_value;
  
  if (doc->scale == priv->scale)
    return;
  
  scale_factor = doc->scale / priv->scale;

  h_value = scale_factor * (hadj->value + hadj->page_size / 2) 
    - hadj->page_size / 2;
  h_value = CLAMP (h_value, hadj->lower, hadj->upper - hadj->page_size);
  priv->offset.x = - (gint) h_value;

  v_value = scale_factor * (vadj->value + vadj->page_size / 2) 
    - vadj->page_size / 2;
  v_value = CLAMP (v_value, vadj->lower, vadj->upper - vadj->page_size);
  priv->offset.y = - (gint) v_value;
  
  priv->scale = doc->scale;
}


static void
e6x_view_draw (GtkWidget *widget)
{
  GdkRegion *region;
  
  region = gdk_drawable_get_clip_region (widget->window);
  gdk_window_invalidate_region (widget->window, region, TRUE);
  gdk_region_destroy (region);
  gdk_window_process_updates (widget->window, TRUE);
}


static void 
e6x_view_draw_selection (GtkWidget *widget,
                         const GdkRectangle *rect)
{
  cairo_t *cr = NULL;
  
  e6x_view_draw (widget);
  
  cr = gdk_cairo_create (widget->window);
  cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
  cairo_set_operator (cr, CAIRO_OPERATOR_DIFFERENCE);
  gdk_cairo_rectangle (cr, rect);
  cairo_fill (cr);
  cairo_destroy (cr);  
}


static void
e6x_view_scroll (E6xView *view, 
                 gint dx, 
                 gint dy)
{
  E6xViewPrivate *priv = view->priv;

  if (dx == 0 && dy == 0)
    return;

  priv->offset.x += dx;
  priv->offset.y += dy;

  gdk_window_scroll (GTK_WIDGET (view)->window, dx, dy);
}


static void
e6x_view_scroll_event (E6xView *view,
                       gdouble h_value,
                       gdouble v_value)
{
  E6xViewPrivate *priv = view->priv;
  E6xDocument *doc = priv->doc;
  GtkAdjustment *hadj = view->hadj, *vadj = view->vadj;

  if (priv->awaits_expose || G_UNLIKELY (!priv->doc))
    return;

  if (hadj->value <= hadj->lower 
      && h_value < hadj->lower 
      && doc->page_no > 1)
  {
    priv->offset.x = - hadj->upper + hadj->page_size;
    e6x_document_set_page_no (doc, doc->page_no - 1);
    priv->awaits_expose = TRUE;
  }
  else if (hadj->value >= hadj->upper - hadj->page_size 
           && h_value > MAX (hadj->upper - hadj->page_size, hadj->lower)
           && doc->page_no < doc->n_pages)
  {
    priv->offset.x = - hadj->lower;
    e6x_document_set_page_no (doc, doc->page_no + 1);
    priv->awaits_expose = TRUE;
  }
  else
  {    
    h_value = CLAMP (h_value, hadj->lower, hadj->upper - hadj->page_size);
    gtk_adjustment_set_value (hadj, h_value);
  }

  if (vadj->value <= vadj->lower 
      && v_value < vadj->lower 
      && doc->page_no > 1)
  {
    priv->offset.y = - vadj->upper + vadj->page_size;
    e6x_document_set_page_no (doc, doc->page_no - 1);
    priv->awaits_expose = TRUE;
  }
  else if (vadj->value >= vadj->upper - vadj->page_size 
           && v_value > MAX (vadj->upper - vadj->page_size, hadj->lower) 
           && doc->page_no < doc->n_pages)
  {
    priv->offset.y = - vadj->lower;
    e6x_document_set_page_no (doc, doc->page_no + 1);
    priv->awaits_expose = TRUE;
  }
  else
  {    
    v_value = CLAMP (v_value, vadj->lower, vadj->upper - vadj->page_size);
    gtk_adjustment_set_value (vadj, v_value);
  }
}


static void 
e6x_view_add_bindings (GtkBindingSet *binding_set)
{
  gtk_binding_entry_add_signal (binding_set, GDK_Left, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_STEP_LEFT);
  gtk_binding_entry_add_signal (binding_set, GDK_Right, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_STEP_RIGHT);
  gtk_binding_entry_add_signal (binding_set, GDK_Up, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_STEP_UP);
  gtk_binding_entry_add_signal (binding_set, GDK_Down, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_STEP_DOWN);
  gtk_binding_entry_add_signal (binding_set, GDK_Page_Up, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_PAGE_UP);
  gtk_binding_entry_add_signal (binding_set, GDK_Page_Down, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_PAGE_DOWN);
  gtk_binding_entry_add_signal (binding_set, GDK_Home, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_START);
  gtk_binding_entry_add_signal (binding_set, GDK_End, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_END);

  gtk_binding_entry_add_signal (binding_set, GDK_KP_Left, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_STEP_LEFT);
  gtk_binding_entry_add_signal (binding_set, GDK_KP_Right, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_STEP_RIGHT);
  gtk_binding_entry_add_signal (binding_set, GDK_KP_Up, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_STEP_UP);
  gtk_binding_entry_add_signal (binding_set, GDK_KP_Down, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_STEP_DOWN);
  gtk_binding_entry_add_signal (binding_set, GDK_KP_Page_Up, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_PAGE_UP);
  gtk_binding_entry_add_signal (binding_set, GDK_KP_Page_Down, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_PAGE_DOWN);
  gtk_binding_entry_add_signal (binding_set, GDK_KP_Home, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_START);
  gtk_binding_entry_add_signal (binding_set, GDK_KP_End, 
                                0, "key_scroll_event", 1,
                                GTK_TYPE_SCROLL_TYPE, GTK_SCROLL_END);
}


static void
e6x_view_load_settings ()
{
  GKeyFile *keyfile = e6x_pref_get_keyfile ();

  e6x_view_settings.zoom_min 
    = g_key_file_get_double (keyfile, "Zoom", "Min", NULL);
  e6x_view_settings.zoom_max 
    = g_key_file_get_double (keyfile, "Zoom", "Max", NULL);
  e6x_view_settings.zoom_step 
    = g_key_file_get_double (keyfile, "Zoom", "Step", NULL);
}
