/* Dia -- a diagram creation/manipulation program
 * Copyright (C) 1998 Alexander Larsson
 *
 * $Id$
 *
 * navigation.c : a navigation popup window to browse large diagrams.
 * Copyright (C) 2003 Luc Pionchon
 *  
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *  
 */

#include <gtk/gtk.h>

#include "diagram.h"
#include "display.h"
#include "diagdkrenderer.h"

#include "navigation.h"


#define THUMBNAIL_MAX_SIZE 150 /*(pixels) this may be a preference*/


struct _NavigationWindow
{
  GtkWidget * popup_window;

  /*popup size (drawing_area)*/
  int width;
  int height;
  int max_size;

  gboolean is_first_expose;

  /*miniframe*/
  int frame_w;
  int frame_h;
  GdkGC * gc;
  GdkCursor * cursor;

  /*factors to translate thumbnail coordinates to adjustement values*/
  gdouble hadj_coef;
  gdouble vadj_coef;

  /*diagram thumbnail's buffer*/
  GdkPixmap * buffer;

  /*display to navigate*/
  DDisplay * ddisp;
};

typedef struct _NavigationWindow NavigationWindow;

static NavigationWindow _nav;
static NavigationWindow * nav = &_nav;


#define DIAGRAM_OFFSET 1 /*(diagram's unit) so we can see the little green boxes :)*/
#define FRAME_THICKNESS 2 /*(pixels)*/
#define STD_CURSOR_MIN 16 /*(pixels)*/


static void on_button_navigation_popup_pressed  (GtkButton * button, gpointer _ddisp);
static void on_button_navigation_popup_released (GtkButton * button, gpointer unused);

static void reset_sc_adj (GtkAdjustment * adj, gdouble lower, gdouble upper, gdouble page);

static gboolean on_da_expose_event         (GtkWidget * widget, GdkEventExpose * event, gpointer unused);
static gboolean on_da_motion_notify_event  (GtkWidget * widget, GdkEventMotion * event, gpointer unused);
static gboolean on_da_button_release_event (GtkWidget * widget, GdkEventButton * event, gpointer popup_window);


static char * nav_xpm[] = {
  "10 10 2 1",
  "  c None",
  ". c #000000",
  "    ..    ",
  "   ....   ",
  "    ..    ",
  " .  ..  . ",
  "..........",
  "..........",
  " .  ..  . ",
  "    ..    ",
  "   ....   ",
  "    ..    "
};

GtkWidget *
navigation_popup_new (DDisplay *ddisp)
{
  GtkWidget * button;

  GtkWidget * image;
  GdkPixmap * pixmap;
  GdkBitmap * mask = NULL;

  button = gtk_button_new ();
  gtk_container_set_border_width (GTK_CONTAINER (button), 0);
  gtk_button_set_relief (GTK_BUTTON(button), GTK_RELIEF_NONE);
  g_signal_connect (G_OBJECT (button), "pressed",
                    G_CALLBACK (on_button_navigation_popup_pressed), ddisp);
  /*if you are fast, the button catches it before the drawing_area:*/
  g_signal_connect (G_OBJECT (button), "released",
                    G_CALLBACK (on_button_navigation_popup_released), NULL);

  pixmap = gdk_pixmap_colormap_create_from_xpm_d(NULL,
                                                 gtk_widget_get_colormap(button),
                                                 &mask,
                                                 &(button->style->bg[GTK_STATE_NORMAL]),
                                                 nav_xpm);

  image = gtk_image_new_from_pixmap (pixmap, mask);
  g_object_unref(pixmap);
  g_object_unref(mask);

  gtk_container_add (GTK_CONTAINER (button), image);
  gtk_widget_show(image);


  return button;
}


static void
on_button_navigation_popup_pressed (GtkButton * button, gpointer _ddisp)
{
  GtkWidget * popup_window;
  GtkWidget * frame;

  GtkWidget * drawing_area;

  DiagramData * data;

  Rectangle rect;/*diagram's extents*/
  real zoom;/*zoom factor for thumbnail rendering*/


  memset (nav, 0, sizeof(NavigationWindow));
  /*--Retrieve the diagram's data*/
  nav->ddisp  = (DDisplay *) _ddisp;
  data = nav->ddisp->diagram->data;

  /*--Calculate sizes*/
  {
    int canvas_width, canvas_height;/*pixels*/
    int diagram_width, diagram_height;/*pixels*/
    GtkAdjustment * adj;

    nav->max_size = THUMBNAIL_MAX_SIZE;

    /*size: Diagram <--> thumbnail*/
    rect.top    = data->extents.top    - DIAGRAM_OFFSET;
    rect.left   = data->extents.left   - DIAGRAM_OFFSET;
    rect.bottom = data->extents.bottom + DIAGRAM_OFFSET + 1;
    rect.right  = data->extents.right  + DIAGRAM_OFFSET + 1;
    
    zoom = nav->max_size / MAX( (rect.right - rect.left) , (rect.bottom - rect.top) );

    nav->width  = MIN( nav->max_size, (rect.right  - rect.left) * zoom);
    nav->height = MIN( nav->max_size, (rect.bottom - rect.top)  * zoom);

    /*size: display canvas <--> frame cursor*/
    diagram_width  = (int) ddisplay_transform_length (nav->ddisp, (rect.right - rect.left));
    diagram_height = (int) ddisplay_transform_length (nav->ddisp, (rect.bottom - rect.top));

    if (diagram_width * diagram_height == 0)
      return; /* don't crash with no size, i.e. empty diagram */

    canvas_width   = nav->ddisp->canvas->allocation.width;
    canvas_height  = nav->ddisp->canvas->allocation.height;

    nav->frame_w = nav->width  * canvas_width  / diagram_width;
    nav->frame_h = nav->height * canvas_height / diagram_height;

    /*reset adjustements to diagram size,*/
    /*(dia allows to grow the canvas bigger than the diagram's actual size)    */
    /*and store the ratio thumbnail/adjustement(speedup on motion)*/
    adj = nav->ddisp->hsbdata;
    reset_sc_adj (adj, rect.left, rect.right, canvas_width / nav->ddisp->zoom_factor);
    nav->hadj_coef = (adj->upper - adj->page_size - adj->lower) / (nav->width - nav->frame_w);

    adj = nav->ddisp->vsbdata;
    reset_sc_adj (adj, rect.top, rect.bottom, canvas_height / nav->ddisp->zoom_factor);
    nav->vadj_coef = (adj->upper - adj->page_size - adj->lower) / (nav->height - nav->frame_h);
  }
  
  /*--GUI*/
  /*popup window, and cute frame*/
  popup_window = gtk_window_new (GTK_WINDOW_POPUP);
  nav->popup_window = popup_window;
  gtk_window_set_position (GTK_WINDOW (popup_window), GTK_WIN_POS_MOUSE);

  frame = gtk_frame_new (NULL);
  gtk_frame_set_shadow_type (GTK_FRAME (frame), GTK_SHADOW_ETCHED_OUT);

  /*drawing area*/
  drawing_area = gtk_drawing_area_new ();
  gtk_widget_set_size_request (drawing_area, nav->width, nav->height);

  gtk_widget_set_events (drawing_area, 0
                         | GDK_EXPOSURE_MASK
                         | GDK_POINTER_MOTION_MASK
                         | GDK_BUTTON_RELEASE_MASK
                         );

  g_signal_connect (G_OBJECT (drawing_area), "expose_event",
                    G_CALLBACK (on_da_expose_event), NULL);
  g_signal_connect (G_OBJECT (drawing_area), "motion_notify_event",
                    G_CALLBACK (on_da_motion_notify_event), NULL);
  g_signal_connect (G_OBJECT (drawing_area), "button_release_event",
                    G_CALLBACK (on_da_button_release_event), NULL);

  /*packing*/
  gtk_container_add (GTK_CONTAINER (frame), drawing_area);
  gtk_container_add (GTK_CONTAINER (popup_window), frame);
  gtk_widget_show (drawing_area);
  gtk_widget_show (frame);
  gtk_widget_show (popup_window);

  /*miniframe style*/
  nav->gc = gdk_gc_new (drawing_area->window);
  gdk_gc_set_line_attributes (nav->gc,
                              FRAME_THICKNESS,
                              GDK_LINE_SOLID, GDK_CAP_BUTT, GDK_JOIN_MITER);

  /*cursor*/
  if(MIN(nav->frame_h, nav->frame_w) > STD_CURSOR_MIN){
    nav->cursor = gdk_cursor_new (GDK_FLEUR);
  }
  else{/*the miniframe is very small, so we use a minimalist cursor*/
    unsigned char cursor_none_data[] = { 0x00 };
    GdkBitmap * bitmap;
    GdkColor fg = { 0, 65535, 65535, 65535};
    GdkColor bg = { 0, 0, 0, 0 };

    bitmap = gdk_bitmap_create_from_data(NULL, cursor_none_data, 1, 1);
    nav->cursor = gdk_cursor_new_from_pixmap(bitmap, bitmap, &fg, &bg, 1, 1);
    g_object_unref(bitmap);
  }

  /*grab the pointer*/
  gdk_pointer_grab (drawing_area->window, TRUE,
                    GDK_BUTTON_RELEASE_MASK | GDK_BUTTON_MOTION_MASK,
                    drawing_area->window,
                    nav->cursor,
                    GDK_CURRENT_TIME);

  /*buffer to draw the thumbnail on*/
  nav->buffer = gdk_pixmap_new (drawing_area->window,
                                nav->width, nav->height, -1);
  gdk_draw_rectangle (nav->buffer,
                      drawing_area->style->black_gc, TRUE,
                      0, 0, nav->width, nav->height);

  {/*--Render the thumbnail*/
    DiaGdkRenderer *renderer;
    GdkColor color;

    renderer = g_object_new (DIA_TYPE_GDK_RENDERER, NULL);
    renderer->transform = dia_transform_new (&rect, &zoom);
    renderer->pixmap = nav->buffer;/*render on the thumbnail buffer*/
    renderer->gc = gdk_gc_new (nav->buffer);

    /*Background color*/
    color_convert (&data->bg_color, &color);
    gdk_gc_set_foreground (renderer->gc, &color);
    gdk_draw_rectangle (renderer->pixmap, renderer->gc, 1, 0, 0, nav->width, nav->height);

    /*render the data*/
    data_render (data, DIA_RENDERER (renderer), NULL, NULL, NULL);

    g_object_ref (renderer->pixmap);
    g_object_unref (renderer);  
  }

  nav->is_first_expose = TRUE;/*set to request to draw the miniframe*/
}


/* resets adjustement to diagram size */
static void
reset_sc_adj (GtkAdjustment * adj, gdouble lower, gdouble upper, gdouble page)
{
  adj->page_size = page;

  adj->lower = lower;
  adj->upper = upper;

  if (adj->value < lower) adj->value = lower;
  if (adj->value > (upper - page)) adj->value = upper - page;

  gtk_adjustment_changed(adj);
}


static gboolean
on_da_expose_event (GtkWidget * widget, GdkEventExpose * event, gpointer unused)
{
  /*refresh the part outdated by the event*/
  gdk_draw_pixmap (widget->window,
                   widget->style->fg_gc[GTK_WIDGET_STATE (widget)],
                   GDK_PIXMAP(nav->buffer),
                   event->area.x, event->area.y,
                   event->area.x, event->area.y,
                   event->area.width, event->area.height);

  /*the first time, display the current display's state*/
  if(nav->is_first_expose){

    GtkAdjustment * adj;
    int x, y;

    adj = nav->ddisp->hsbdata;
    x = (adj->value - adj->lower) / (adj->upper - adj->lower) * (nav->width) +1;

    adj = nav->ddisp->vsbdata;
    y = (adj->value - adj->lower) / (adj->upper - adj->lower) * (nav->height) +1;

    /*draw directly on the window, do not buffer the miniframe*/
    gdk_draw_rectangle (widget->window,
                        nav->gc, FALSE,
                        x, y, nav->frame_w, nav->frame_h);

    nav->is_first_expose = FALSE;
  }
  return FALSE;
}


static gboolean
on_da_motion_notify_event (GtkWidget * drawing_area, GdkEventMotion * event, gpointer unused)
{
  GtkAdjustment * adj;
  gboolean value_changed;

  int w = nav->frame_w;
  int h = nav->frame_h;

  int x, y;/*top left of the miniframe*/

  /* Don't try to move if there's no room for it.*/
  if (w >= nav->width-1 && h >= nav->height-1) return FALSE;

  x = CLAMP (event->x - w/2 , 0, nav->width  - w);
  y = CLAMP (event->y - h/2 , 0, nav->height - h);

  adj = nav->ddisp->hsbdata;
  value_changed = FALSE;
  if (w/2 <= event->x && event->x <= (nav->width - w/2)){
    adj->value = adj->lower + x * nav->hadj_coef;
    value_changed = TRUE;
  }
  else if (x == 0 && adj->value != adj->lower){/*you've been too fast! :)*/
    adj->value = adj->lower;
    value_changed = TRUE;
  }
  else if (x == (nav->width - w) && adj->value != (adj->upper - adj->page_size)){/*idem*/
    adj->value = adj->upper - adj->page_size;
    value_changed = TRUE;
  }
  if (value_changed) gtk_adjustment_value_changed(adj);

  adj = nav->ddisp->vsbdata;
  value_changed = FALSE;
  if (h/2 <= event->y && event->y <= (nav->height - h/2)){
    adj->value = adj->lower + y * nav->vadj_coef;
    value_changed = TRUE;
  }
  else if (y == 0 && adj->value != adj->lower){/*you've been too fast! :)*/
    adj->value = adj->lower;
    value_changed = TRUE;
  }
  else if (y == (nav->height - h) && adj->value != (adj->upper - adj->page_size)){/*idem*/
    adj->value = adj->upper - adj->page_size;
    value_changed = TRUE;
  }
  if (value_changed) gtk_adjustment_value_changed(adj);


/*--Draw the miniframe*/
/*refresh from the buffer*/
  gdk_draw_pixmap (drawing_area->window,
                   drawing_area->style->fg_gc[GTK_WIDGET_STATE (drawing_area)],
                   GDK_PIXMAP(nav->buffer),
                   0, 0, 0, 0, nav->width, nav->height);
/*draw directly on the window, do not buffer the miniframe*/
  gdk_draw_rectangle (drawing_area->window,
                      nav->gc, FALSE,
                      x, y, w, h);
  return FALSE;
}


static gboolean
on_da_button_release_event (GtkWidget * widget, GdkEventButton * event, gpointer unused)
{
  /* Apparently there are circumstances where this is run twice for one popup 
   * Protected calls to avoid crashing on second pass.
   */
  if (nav->buffer)
    g_object_unref (nav->buffer);
  nav->buffer = NULL;

  if (nav->gc)
    g_object_unref (nav->gc);
  nav->gc = NULL;

  if (nav->cursor)
    gdk_cursor_unref (nav->cursor);
  nav->cursor = NULL;

  if (nav->popup_window)
    gtk_widget_destroy (nav->popup_window);
  nav->popup_window = NULL;

/*returns the focus on the canvas*/
  gtk_widget_grab_focus(nav->ddisp->canvas);
  return FALSE;
}

static void
on_button_navigation_popup_released (GtkButton * button, gpointer z)
{
  /* don't popdown before having drawn once */
  if (!nav->is_first_expose) /* needed for gtk+-2.6.x, but work for 2.6 too. */
    on_da_button_release_event (NULL, NULL, NULL);
}
