/* EtherApe
 * Copyright (C) 2001 Juan Toledo, Riccardo Ghetta
 *
 * 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/>.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

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

#include "appdata.h"
#include "diagram.h"
#include "pref_dialog.h"
#include "stats/node.h"
#include "info_windows.h"
#include "stats/protocols.h"
#include "datastructs.h"
#include "names/ip-cache.h"
#include "menus.h"
#include "capture/capctl.h"
#include "stats/conversations.h"
#include "preferences.h"
#include "export.h"
#include "stats/util.h"

// uncomment to scale the diagram according to node sizes
// instead of considering only node centers 
//#define SCALE_TO_NODE_SIZE

/* maximum node and link size */
#define MAX_NODE_SIZE  5000
#define MAX_LINK_SIZE  (MAX_NODE_SIZE/4)

gboolean already_updating;
gboolean stop_requested;

/**** search helper ****/
typedef struct
{
  gdouble x;
  gdouble y;
  void *item;
} search_position_t;

/**** node structs and helpers ****/
typedef struct
{
  node_id_t node_id;
  gdouble x;                    /* x position */
  gdouble y;                    /* y position */
  gdouble radius;               /* node radius */
  GdkRGBA color;                /* node color */
  gchar *text;                  /* node text */
  gboolean is_new;
  gboolean shown;               /* True if it is to be displayed. */
  gboolean centered;            /* true if is a center node */

  /* For '-P' mode (columnar layout) */
  guint column; /* Which column this goes in */
  guint column_idx; /* Which position within its column this node is */
} canvas_node_t;

struct node_ring
{
  gfloat angle;
  guint node_i;
  guint n_nodes;
};

static gint canvas_node_compare(const node_id_t *a, const node_id_t *b,
                                gpointer dummy);
static void canvas_node_delete(canvas_node_t *cn);
static gint canvas_node_update(node_id_t  *ether_addr,
                               canvas_node_t *canvas_node,
                               GList * *delete_list);
static canvas_node_t *find_node_at_position(gdouble diagram_x, gdouble diagram_y);
static gboolean check_node_at_position(node_id_t *node_id, canvas_node_t *node, search_position_t *data);
static gboolean draw_canvas_node(node_id_t *node_id, canvas_node_t *node, cairo_t *cr);

static GHashTable *legend_protocols = NULL;   /* Key / Value name for active Legend protocol names -> GtkLabel widget */

/**** link structs and helpers ****/
typedef struct
{
  link_id_t link_id; /* id of the link */
  gdouble src_x, src_y;     /* source coordinates */
  gdouble dst_x, dst_y;     /* destination coordinates */
  gdouble src_size;         /* source triangle size */
  gdouble dst_size;         /* destination triangle size */
  GdkRGBA color;            /* link color */
  gdouble alpha;            /* transparency factor */
} canvas_link_t;
static gint canvas_link_compare(const link_id_t *a, const link_id_t *b,
                                gpointer dummy);
static void canvas_link_delete(canvas_link_t *canvas_link);
static gint canvas_link_update(link_id_t *link_id,
                               canvas_link_t *canvas_link,
                               GList * *delete_list);
static canvas_link_t *find_link_at_position(gdouble diagram_x, gdouble diagram_y);
static gboolean check_link_at_position(link_id_t *link_id, canvas_link_t *link, search_position_t *data);
static gboolean draw_canvas_link(link_id_t *link_id, canvas_link_t *link, cairo_t *cr);

/**** repositioning structs ****/
typedef struct
{
  GtkDrawingArea *canvas;

  struct node_ring outer;
  struct node_ring center;

  gdouble xmin;
  gdouble ymin;
  gdouble xmax;
  gdouble ymax;
  gdouble x_rad_max;
  gdouble y_rad_max;
  gdouble x_inner_rad_max;
  gdouble y_inner_rad_max;

  guint *column_populations;
} reposition_node_t;

/**** background structs ****/

typedef struct
{
  gboolean use_image;

  struct
  {
    GdkPixbuf *image;
    gchar *path;
    cairo_surface_t *surface;
  } image;
} canvas_background_t;


/***************************************************************************
 *
 * local variables
 *
 **************************************************************************/

static GTree *canvas_nodes = NULL;      /* We don't use the nodes tree directly in order to
                                 * separate data from presentation: that is, we need to
                                 * keep a list of CanvasItems, but we do not want to keep
                                 * that info on the nodes tree itself */
static GTree *canvas_links = NULL;     /* See canvas_nodes */
static guint known_protocols = 0;
static canvas_background_t canvas_background;
static guint displayed_nodes;
static gboolean need_reposition = TRUE; /* Force a diagram relayout */
static gboolean need_font_refresh = TRUE; /* Force font refresh during layout */
static gint diagram_timeout = 0;        /* Descriptor of the diagram timeout function
                                        * (Used to change the refresh_period in the callback */

static long canvas_obj_count = 0; /* counter of canvas objects */
static GtkDrawingArea *gcanvas_ = NULL; /* drawing canvas */
static GtkContainer *garea_ = NULL; /* drawing container */
static GdkRectangle canvas_rect; /* canvas dimensions */
static char *pcap_stats_text = NULL; /* stats text content */

/** diagram scaling */
static gdouble current_scale = 1.0;
static gdouble current_translate_x = 0.0;
static gdouble current_translate_y = 0.0;

/***************************************************************************
 *
 * local Function definitions
 *
 **************************************************************************/
static void init_canvas_background(void);
static void update_diagram(GtkDrawingArea *canvas);       /* full diagram update */
static void diagram_update_nodes(GtkDrawingArea *canvas); /* updates ALL nodes */
static void diagram_update_links(GtkDrawingArea *canvas); /* updates ALL links */
static void diagram_update_background_image(GtkDrawingArea *canvas); /* updates background image */
static void diagram_reposition(GtkDrawingArea *canvas); /* reposition nodes */

static void check_new_protocol(GtkWidget *prot_table, const protostack_t *pstk);
static gint check_new_node(node_t *node, GtkDrawingArea *canvas);
static gboolean display_node(node_t *node);
static void limit_nodes(void);
static gint add_ordered_node(node_id_t *node_id,
                             canvas_node_t *canvas_node,
                             GTree *ordered_nodes);
static gint check_ordered_node(gdouble *traffic, canvas_node_t *node,
                               guint *count);
static gint traffic_compare(gconstpointer a, gconstpointer b);
static gint reposition_canvas_nodes(node_id_t *node_id,
                                    canvas_node_t *canvas_node,
                                    reposition_node_t *data);
static gint reposition_canvas_nodes_prep(const node_id_t *node_id,
                                         canvas_node_t *canvas_node,
                                         reposition_node_t *data);
static gint check_new_link(link_id_t *link_id,
                           link_t *link,
                           GtkDrawingArea *canvas);
static gdouble get_node_size(gdouble average);
static gdouble get_link_size(const basic_stats_t *link_stats);
static gboolean link_item_event(GdkEventType type, const canvas_link_t *canvas_link);
static gboolean node_item_event(GdkEventType type, const canvas_node_t *canvas_node);

static void update_legend(void);
static void init_reposition(reposition_node_t *data,
                            GtkDrawingArea *canvas,
                            guint total_nodes);
static void clear_reposition(reposition_node_t *rdata);
static void redraw_canvas_background(GtkDrawingArea *canvas);
static gboolean diagram_resize_event(GtkWidget *widget,
                                     const GdkEventConfigure *event,
                                     GtkDrawingArea *canvas);
static gboolean on_canvas_draw(GtkWidget *widget, cairo_t *cr, gpointer data);
static gboolean on_canvas_button_event(GtkWidget *widget, GdkEventButton *event, gpointer data);
static gboolean on_canvas_motion_event(GtkWidget *widget, GdkEventMotion *event, gpointer data);
static gboolean point_in_rect(gdouble x, gdouble y, GdkRectangle *rect);
static void draw_pcap_stats(cairo_t *cr);

/***************************************************************************
 *
 * implementation
 *
 **************************************************************************/
GtkWidget *canvas_widget()
{
  return GTK_WIDGET(gcanvas_);
}

/* Convert screen coordinates to diagram coordinates */
static void screen_to_diagram_coords(gdouble screen_x, gdouble screen_y,
                                     gdouble *diagram_x, gdouble *diagram_y)
{
  *diagram_x = (screen_x - current_translate_x) / current_scale;
  *diagram_y = (screen_y - current_translate_y) / current_scale;
}

/* Convert diagram coordinates to screen coordinates */
static void diagram_to_screen_coords(gdouble diagram_x, gdouble diagram_y,
                                     gdouble *screen_x, gdouble *screen_y)
{
  *screen_x = diagram_x * current_scale + current_translate_x;
  *screen_y = diagram_y * current_scale + current_translate_y;
}

void ask_reposition(gboolean r_font)
{
  need_reposition = TRUE;
  need_font_refresh = r_font;
}

void dump_stats(guint32 diff_msecs)
{
  gchar *status_string;
  long ipc = ipcache_active_entries();
  status_string = g_strdup_printf(
    _("Nodes: %d (on canvas: %d, shown: %u), Links: %d, Conversations: %ld, "
      "names %ld, protocols %ld. Total Packets seen: %lu (in memory: %ld, "
      "on list %ld). IP cache entries %ld. Canvas objs: %ld. Refreshed: %u ms"),
    node_count(),
    g_tree_nnodes(canvas_nodes), displayed_nodes,
    links_catalog_size(), active_conversations(),
    active_names(), protocol_summary_size(),
    appdata.n_packets, appdata.total_mem_packets,
    packet_list_item_count(), ipc,
    canvas_obj_count,
    (unsigned int)diff_msecs);

  g_my_info("%s", status_string);
  g_free(status_string);
}

/* called when a watched object is finalized */
static void finalize_callback(gpointer data, GObject *obj)
{
  --canvas_obj_count;
}

/* It updates controls from values of variables, and connects control
 * signals to callback functions */
void init_diagram(GtkBuilder *xml)
{
  GtkAllocation windowsize;
  gulong sig_id;

  g_assert(gcanvas_ == NULL);

  /* get containing window and size */
  garea_ = GTK_CONTAINER(gtk_builder_get_object(xml, "diagramwindow"));
  g_assert(garea_ != NULL);
  gtk_widget_get_allocation(GTK_WIDGET(garea_), &windowsize);

  /* Set inital window size */
  GtkWidget *toplevel = gtk_widget_get_toplevel(GTK_WIDGET(garea_));
  if (gtk_widget_is_toplevel(toplevel)) {
    gtk_window_resize(GTK_WINDOW(toplevel), pref.window_width, pref.window_height);
  }

  /* Creates trees */
  canvas_nodes = g_tree_new_full((GCompareDataFunc)canvas_node_compare,
                                 NULL, NULL, (GDestroyNotify)canvas_node_delete);
  canvas_links = g_tree_new_full((GCompareDataFunc)canvas_link_compare,
                                 NULL, NULL, (GDestroyNotify)canvas_link_delete);

  initialize_pref_controls();

  /* canvas */
  gcanvas_ = GTK_DRAWING_AREA(gtk_drawing_area_new());
  g_assert(gcanvas_ != NULL);

  /* Set black background */
  GtkStyleContext *style_context = gtk_widget_get_style_context(GTK_WIDGET(gcanvas_));
  GtkCssProvider *provider = gtk_css_provider_new();
  gtk_css_provider_load_from_data(provider,
                                 "drawing { background-color: black; }",
                                 -1,
                                 NULL);
  gtk_style_context_add_provider(style_context,
                                GTK_STYLE_PROVIDER(provider),
                                GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
  g_object_unref(provider);

  /* Set initial canvas dimensions */
  canvas_rect.x = -windowsize.width/2;
  canvas_rect.y = -windowsize.height/2;
  canvas_rect.width = windowsize.width;
  canvas_rect.height = windowsize.height;

  gtk_widget_show(GTK_WIDGET(gcanvas_));
  gtk_container_add(garea_, GTK_WIDGET(gcanvas_));

  /* Initialize background image */
  init_canvas_background();

  /* Initialize pcap stats text */
  pcap_stats_text = NULL;

  /* Connect signals */
  sig_id = g_signal_connect(G_OBJECT(garea_), "size-allocate",
                            G_CALLBACK(diagram_resize_event), gcanvas_);
  g_assert(sig_id > 0);
  
  sig_id = g_signal_connect(G_OBJECT(gcanvas_), "draw",
                            G_CALLBACK(on_canvas_draw), NULL);
  g_assert(sig_id > 0);
  
  sig_id = g_signal_connect(G_OBJECT(gcanvas_), "button-release-event",
                            G_CALLBACK(on_canvas_button_event), NULL);
  g_assert(sig_id > 0);
  
  sig_id = g_signal_connect(G_OBJECT(gcanvas_), "motion-notify-event",
                            G_CALLBACK(on_canvas_motion_event), NULL);
  g_assert(sig_id > 0);

  /* Enable events on drawing area */
  gtk_widget_add_events(GTK_WIDGET(gcanvas_), 
                       GDK_BUTTON_PRESS_MASK | 
                       GDK_BUTTON_RELEASE_MASK |
                       GDK_POINTER_MOTION_MASK |
                       GDK_ENTER_NOTIFY_MASK |
                       GDK_LEAVE_NOTIFY_MASK);

  /* Initialize the known_protocols table */
  delete_gui_protocols();

  /* Init legend hash table */
  init_legend();

  /* Set the already_updating global flag */
  already_updating = FALSE;
  stop_requested = FALSE;

  /* Added action for hide / show main menu
   * while in fullscreen mode. */
  add_hide_show_main_menu_action();
}

typedef struct find_bounds_data_struct {
    gdouble min_x;
    gdouble min_y;
    gdouble max_x;
    gdouble max_y;
} find_bounds_data;

 /* Helper function to find bounds */
static gboolean find_bounds(node_id_t *node_id, canvas_node_t *node, gpointer user_data) 
{
    if (!node->shown)
      return FALSE;

    find_bounds_data *data = (find_bounds_data *)user_data;

#if defined(SCALE_TO_NODE_SIZE)
      const gdouble padding = node->radius;
#else
      const gdouble padding = 20;
#endif
    if (node->x - padding < data->min_x) data->min_x = node->x - padding;
    if (node->y - padding < data->min_y) data->min_y = node->y - padding;
    if (node->x + padding > data->max_x) data->max_x = node->x + padding;
    if (node->y + padding > data->max_y) data->max_y = node->y + padding;
    
    return FALSE;
}


/* calculate the bounding box of all nodes */
static void calculate_diagram_bounds(gdouble *min_x, gdouble *min_y, 
                                    gdouble *max_x, gdouble *max_y) 
{

  *min_x = G_MAXDOUBLE;
  *min_y = G_MAXDOUBLE;
  *max_x = -G_MAXDOUBLE;
  *max_y = -G_MAXDOUBLE;
  
  find_bounds_data data = { *min_x, *min_y, *max_x, *max_y };
  g_tree_foreach(canvas_nodes, (GTraverseFunc)find_bounds, &data);

  /* take the bounds and add some padding */
#if defined(SCALE_TO_NODE_SIZE)
  const gdouble padding = node->radius;
#else
  const gdouble padding = 20;
#endif

*min_x = data.min_x - padding;
  *min_y = data.min_y - padding;
  *max_x = data.max_x + padding;
  *max_y = data.max_y + padding;
}

/* update the diagram */
static gboolean on_canvas_draw(GtkWidget *widget, cairo_t *cr, gpointer data)
{
  /* Clear the background */
  cairo_set_source_rgb(cr, 0, 0, 0);
  cairo_paint(cr);

  /* Draw background image if enabled */
  if (canvas_background.use_image && canvas_background.image.surface)
  {
    cairo_set_source_surface(cr, canvas_background.image.surface, 0, 0);
    cairo_paint(cr);
  }

  /* Get canvas dimensions */
  GtkAllocation allocation;
  gtk_widget_get_allocation(widget, &allocation);
  gdouble canvas_width = allocation.width;
  gdouble canvas_height = allocation.height;

  /* Calculate diagram bounds */
  gdouble min_x, min_y, max_x, max_y;
  calculate_diagram_bounds(&min_x, &min_y, &max_x, &max_y);

  /* Skip scaling if we don't have any nodes */
  if (min_x >= max_x || min_y >= max_y)
  {
    current_scale = 1.0;
    current_translate_x = 0.0;
    current_translate_y = 0.0;
    return FALSE;
  }

  /* Calculate scaling factors */
  gdouble diagram_width = max_x - min_x;
  gdouble diagram_height = max_y - min_y;
  gdouble scale_x = canvas_width / diagram_width;
  gdouble scale_y = canvas_height / diagram_height;
  gdouble scale = MIN(scale_x, scale_y) * 0.9; /* Use 90% to leave some margin */

  /* Store the current transformation parameters */
  current_scale = scale;
  if (current_scale >2)
    current_scale = 1.0; // in some degenerate cases, scale can be too high
  current_translate_x = canvas_width / 2 - (min_x + diagram_width / 2) * scale;
  current_translate_y = canvas_height / 2 - (min_y + diagram_height / 2) * scale;

  /* Save the current transformation */
  cairo_save(cr);

  /* Apply scaling transformation */
  cairo_translate(cr, current_translate_x, current_translate_y);
  cairo_scale(cr, current_scale, current_scale);

  /* Draw all links */
  g_tree_foreach(canvas_links, (GTraverseFunc)draw_canvas_link, cr);

  /* Draw all nodes */
  g_tree_foreach(canvas_nodes, (GTraverseFunc)draw_canvas_node, cr);

  /* Restore the original transformation */
  cairo_restore(cr);

  /* Draw pcap stats text if enabled (not scaled) */
  draw_pcap_stats(cr);

  return FALSE;
}

static void draw_pcap_stats(cairo_t *cr)
{
  if (pref.pcap_stats_pos == STATSPOS_NONE || !pcap_stats_text)
    return;

  cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
  cairo_set_font_size(cr, 12.0);

  cairo_text_extents_t extents;
  cairo_text_extents(cr, pcap_stats_text, &extents);

  gdouble xpos, ypos;
  switch (pref.pcap_stats_pos)
  {
  case STATSPOS_UPPER_LEFT:
    xpos = 0;
    ypos = extents.height;
    break;

  case STATSPOS_UPPER_RIGHT:
    xpos = canvas_rect.width - extents.x_advance;
    ypos = extents.height;
    break;

  case STATSPOS_LOWER_LEFT:
    xpos = 0;
    ypos = canvas_rect.height - extents.height;
    break;

  case STATSPOS_LOWER_RIGHT:
    xpos = canvas_rect.width - extents.x_advance;
    ypos = canvas_rect.height - extents.height;
    break;

  default:
    g_error(_("Bogus statspos_t (%d) pref.pcap_stats_pos"), pref.pcap_stats_pos);
    return;
  }

  /* Parse text color from preferences */
  GdkRGBA text_color;
  gdk_rgba_parse(&text_color, pref.text_color);

  cairo_set_source_rgba(cr, text_color.red, text_color.green, text_color.blue, text_color.alpha);
  cairo_move_to(cr, xpos, ypos);
  cairo_show_text(cr, pcap_stats_text);
}

/* Handle button events on canvas */
static gboolean on_canvas_button_event(GtkWidget *widget, GdkEventButton *event, gpointer data)
{
  /* Convert screen coordinates to diagram coordinates */
  gdouble diagram_x, diagram_y;
  screen_to_diagram_coords(event->x, event->y, &diagram_x, &diagram_y);

  /* Check if click is on a node */
  canvas_node_t *clicked_node = find_node_at_position(diagram_x, diagram_y);
  if (clicked_node) {
    node_item_event(event->type, clicked_node);
    return TRUE;
  }
  
  /* Check if click is on a link */
  canvas_link_t *clicked_link = find_link_at_position(diagram_x, diagram_y);
  if (clicked_link) {
    link_item_event(event->type, clicked_link);
    return TRUE;
  }
  
  return FALSE;
}

/* Handle motion events on canvas */
static gboolean on_canvas_motion_event(GtkWidget *widget, GdkEventMotion *event, gpointer data)
{
return FALSE;

  /* Convert screen coordinates to diagram coordinates */
  gdouble diagram_x, diagram_y;
  screen_to_diagram_coords(event->x, event->y, &diagram_x, &diagram_y);

  /* Check if mouse is over a link */
  canvas_link_t *hovered_link = find_link_at_position(diagram_x, diagram_y);
  if (hovered_link) {
    link_item_event(event->type, hovered_link);
    return TRUE;
  }
  
  return FALSE;
}

/* Helper function to check if point is in rectangle */
static gboolean point_in_rect(gdouble x, gdouble y, GdkRectangle *rect)
{
  return (x >= rect->x && x <= rect->x + rect->width &&
          y >= rect->y && y <= rect->y + rect->height);
}

/* Find node at given position - uses diagram coordinates */
static canvas_node_t *find_node_at_position(gdouble diagram_x, gdouble diagram_y)
{
  search_position_t data = {diagram_x, diagram_y, NULL};
  g_tree_foreach(canvas_nodes, (GTraverseFunc)check_node_at_position, &data);
  return data.item;
}

/* Check if node is at given position */
static gboolean check_node_at_position(node_id_t *node_id, canvas_node_t *node, search_position_t *data)
{
  if (!node->shown)
    return FALSE;

  gdouble dx = data->x - node->x;
  gdouble dy = data->y - node->y;
  gdouble distance = sqrt(dx*dx + dy*dy);
  
  if (distance <= node->radius) {
    data->item = node;
    return TRUE; /* Stop traversal */
  }
  
  return FALSE; /* Continue traversal */
}

/* Find link at given position - uses diagram coordinates */
static canvas_link_t *find_link_at_position(gdouble diagram_x, gdouble diagram_y)
{
  search_position_t data = {diagram_x, diagram_y, NULL};
  g_tree_foreach(canvas_links, (GTraverseFunc)check_link_at_position, &data);
  return data.item;
}

/* Check if point is inside a triangle */
static gboolean point_in_triangle(gdouble px, gdouble py,
                                  gdouble ax, gdouble ay,
                                  gdouble bx, gdouble by,
                                  gdouble cx, gdouble cy)
{
  /* Compute vectors */
  gdouble v0x = cx - ax;
  gdouble v0y = cy - ay;
  gdouble v1x = bx - ax;
  gdouble v1y = by - ay;
  gdouble v2x = px - ax;
  gdouble v2y = py - ay;

  /* Compute dot products */
  gdouble dot00 = v0x * v0x + v0y * v0y;
  gdouble dot01 = v0x * v1x + v0y * v1y;
  gdouble dot02 = v0x * v2x + v0y * v2y;
  gdouble dot11 = v1x * v1x + v1y * v1y;
  gdouble dot12 = v1x * v2x + v1y * v2y;

  /* Check for degenerate triangle */
  gdouble denom = dot00 * dot11 - dot01 * dot01;
  if (fabs(denom) < 1e-10)
    return FALSE;  // Degenerate triangle

  /* Compute barycentric coordinates */
  gdouble inv_denom = 1.0 / denom;
  gdouble u = (dot11 * dot02 - dot01 * dot12) * inv_denom;
  gdouble v = (dot00 * dot12 - dot01 * dot02) * inv_denom;

  /* Check if point is inside triangle */
  return (u >= 0) && (v >= 0) && (u + v <= 1.0);
}
/* Check if link is at given position */
static gboolean check_link_at_position(link_id_t *link_id, canvas_link_t *link, search_position_t *data)
{
  if (!link->alpha)
    return FALSE;

  /* Calculate vector along the link line */
  gdouble dx = link->dst_x - link->src_x;
  gdouble dy = link->dst_y - link->src_y;
  gdouble line_length = sqrt(dx*dx + dy*dy);
  
  /* Check for zero-length links */
  if (line_length < 1e-10)
    return FALSE;
  
  /* Normalize direction vector */
  gdouble nx = dx / line_length;
  gdouble ny = dy / line_length;
  
  /* Create perpendicular vector for thickness */
  gdouble thickness = MAX(10.0, link->src_size + link->dst_size); /* Base thickness plus link size */
  gdouble perpx = -ny * thickness;
  gdouble perpy = nx * thickness;
  
  /* Add padding along the line length */
  gdouble padding = 5.0; /* Adjust as needed */
  gdouble padx = nx * padding;
  gdouble pady = ny * padding;
  
  /* Extended source and destination points for better hit detection */
  gdouble ext_src_x = link->src_x - padx;
  gdouble ext_src_y = link->src_y - pady;
  gdouble ext_dst_x = link->dst_x + padx;
  gdouble ext_dst_y = link->dst_y + pady;
  
  /* Check if point is inside any of the four triangles that make up the link area */
  gboolean is_inside = 
    /* Top triangle */
    point_in_triangle(data->x, data->y,
                     ext_src_x, ext_src_y,
                     ext_dst_x, ext_dst_y,
                     ext_src_x + perpx, ext_src_y + perpy) ||
    /* Bottom triangle */                 
    point_in_triangle(data->x, data->y,
                     ext_src_x, ext_src_y,
                     ext_dst_x, ext_dst_y,
                     ext_src_x - perpx, ext_src_y - perpy) ||
    /* Left triangle */
    point_in_triangle(data->x, data->y,
                     ext_src_x + perpx, ext_src_y + perpy,
                     ext_src_x - perpx, ext_src_y - perpy,
                     ext_dst_x - perpx, ext_dst_y - perpy) ||
    /* Right triangle */
    point_in_triangle(data->x, data->y,
                     ext_src_x + perpx, ext_src_y + perpy,
                     ext_dst_x + perpx, ext_dst_y + perpy,
                     ext_dst_x - perpx, ext_dst_y - perpy);

  if (is_inside) {
    data->item = link;
    return TRUE; /* Stop traversal */
  }
  
  return FALSE; /* Continue traversal */
}

/* Draw a node on the canvas */
static gboolean draw_canvas_node(node_id_t *node_id, canvas_node_t *node, cairo_t *cr)
{
  /* Make sure the node can be shown and
   * its position has be calculated. */
  if (!node->shown || node->is_new)
    return FALSE;
  
  /* Draw the node circle */
  cairo_set_source_rgba(cr, node->color.red, node->color.green, node->color.blue, 1.0);
  cairo_arc(cr, node->x, node->y, node->radius, 0, 2 * M_PI);
  cairo_fill(cr);
  
  /* Draw the node text if not in diagram-only mode */
  if (!pref.diagram_only && node->text) {
    /* Parse text color from preferences */
    GdkRGBA text_color;
    gdk_rgba_parse(&text_color, pref.text_color);
    
    cairo_select_font_face(cr, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
    cairo_set_font_size(cr, 12.0);
    cairo_set_source_rgba(cr, text_color.red, text_color.green, text_color.blue, text_color.alpha);
    
    /* Center text on node */
    cairo_text_extents_t extents;
    cairo_text_extents(cr, node->text, &extents);
    cairo_move_to(cr, 
                 node->x - extents.width/2,
                 node->y + extents.height/2);
    cairo_show_text(cr, node->text);
  }
  
  return FALSE; /* Continue traversal */
}

/* Draw a link on the canvas */
static gboolean draw_canvas_link(link_id_t *link_id, canvas_link_t *link, cairo_t *cr)
{
  /* Draw source triangle */
  if (link->src_size > 0) {
    /* Calculate triangle points */
    gdouble versorx = -(link->dst_y - link->src_y);
    gdouble versory = link->dst_x - link->src_x;
    gdouble modulus = sqrt(pow(versorx, 2) + pow(versory, 2));
    
    if (modulus > 0) {
      cairo_set_source_rgba(cr, link->color.red, link->color.green, link->color.blue, link->alpha);
      
      cairo_move_to(cr, link->dst_x, link->dst_y);
      cairo_line_to(cr, link->src_x + versorx * link->src_size / modulus,
                   link->src_y + versory * link->src_size / modulus);
      cairo_line_to(cr, link->src_x - versorx * link->src_size / modulus,
                   link->src_y - versory * link->src_size / modulus);
      cairo_close_path(cr);
      cairo_fill(cr);
    }
  }
  
  /* Draw destination triangle */
  if (link->dst_size > 0) {
    /* Calculate triangle points */
    gdouble versorx = -(link->src_y - link->dst_y);
    gdouble versory = link->src_x - link->dst_x;
    gdouble modulus = sqrt(pow(versorx, 2) + pow(versory, 2));
    
    if (modulus > 0) {
      cairo_set_source_rgba(cr, link->color.red, link->color.green, link->color.blue, link->alpha);
      
      cairo_move_to(cr, link->src_x, link->src_y);
      cairo_line_to(cr, link->dst_x + versorx * link->dst_size / modulus,
                   link->dst_y + versory * link->dst_size / modulus);
      cairo_line_to(cr, link->dst_x - versorx * link->dst_size / modulus,
                   link->dst_y - versory * link->dst_size / modulus);
      cairo_close_path(cr);
      cairo_fill(cr);
    }
  }
  
  return FALSE; /* Continue traversal */
}

void cleanup_diagram()
{
  if (canvas_nodes)
    g_tree_destroy(canvas_nodes);
  if (canvas_links)
    g_tree_destroy(canvas_links);
}

/*
 * Initialize the canvas_background structure.
 */
static void init_canvas_background()
{
  canvas_background.use_image = pref.bck_image_enabled;
  canvas_background.image.path = g_strdup(pref.bck_image_path);
  canvas_background.image.image = NULL;
  canvas_background.image.surface = NULL;
}

/*
 * Called by on_canvas1_size_allocate.
 * Try to load image from path.
 * On success image is scaled to the canvas size and printed.
 * On error a blank black image is made and printed.
 */
static void redraw_canvas_background(GtkDrawingArea *canvas)
{
  GError *error = NULL;
  GtkAllocation canvas_size;

  if (canvas_background.use_image) {
    /* Get canvas dimensions */
    gtk_widget_get_allocation(GTK_WIDGET(canvas), &canvas_size);

    /* Clean up old image if it exists */
    if (canvas_background.image.image) {
      g_object_unref(G_OBJECT(canvas_background.image.image));
      canvas_background.image.image = NULL;
    }
    
    if (canvas_background.image.surface) {
      cairo_surface_destroy(canvas_background.image.surface);
      canvas_background.image.surface = NULL;
    }

    /* Load new image if path is valid */
    if (canvas_background.image.path && strlen(canvas_background.image.path)) {
      canvas_background.image.image = gdk_pixbuf_new_from_file_at_scale(
          canvas_background.image.path,
          canvas_size.width, canvas_size.height,
          FALSE, &error);
          
      if (canvas_background.image.image) {
        /* Convert pixbuf to cairo surface */
        canvas_background.image.surface = gdk_cairo_surface_create_from_pixbuf(
            canvas_background.image.image, 0, NULL);
      }
    }
  }
  
  /* Force redraw */
  gtk_widget_queue_draw(GTK_WIDGET(canvas));
}

/*
 * Update the background image.  Load new image if user selected another path
 * in preferences.  Place the background image behind the nodes and links.
 */
static void diagram_update_background_image(GtkDrawingArea *canvas)
{
  /*
   * If the background image enable toggle or the image path has changed, we
   * need to update the background.
   */
  if (pref.bck_image_enabled != canvas_background.use_image ||
      g_strcmp0(canvas_background.image.path, pref.bck_image_path)) {
    canvas_background.use_image = pref.bck_image_enabled;
    g_free(canvas_background.image.path);
    canvas_background.image.path = g_strdup(pref.bck_image_path);
    redraw_canvas_background(canvas);
  }
}

static gboolean diagram_resize_event(GtkWidget *widget,
                                     const GdkEventConfigure *event,
                                     GtkDrawingArea *canvas)
{
  GtkAllocation windowsize;
  g_assert(widget != NULL);
  g_assert(canvas == gcanvas_);

  gtk_widget_get_allocation(GTK_WIDGET(widget), &windowsize);
  
  /* Update canvas dimensions */
  canvas_rect.x = -windowsize.width/2;
  canvas_rect.y = -windowsize.height/2;
  canvas_rect.width = windowsize.width;
  canvas_rect.height = windowsize.height;

  redraw_canvas_background(canvas);
  diagram_reposition(canvas);
  diagram_update_links(canvas);
  
  /* Force redraw */
  gtk_widget_queue_draw(GTK_WIDGET(canvas));

  /* Update the Preference Dialog Window Size value if displayed. */
  if (is_visible_pref_dialog()) {
    update_label_value("win_size_label_value");
  }

  return FALSE;
}


/* delete the specified canvas node */
static void canvas_node_delete(canvas_node_t *canvas_node)
{
  if (canvas_node->text) {
    g_free(canvas_node->text);
    canvas_node->text = NULL;
  }

  g_free(canvas_node);
}

/* used to remove nodes */
static void gfunc_remove_canvas_node(gpointer data, gpointer user_data)
{
  g_tree_remove(canvas_nodes, (const node_id_t *)data);
}

/* used to remove links */
static void gfunc_remove_canvas_link(gpointer data, gpointer user_data)
{
  g_tree_remove(canvas_links, (const link_id_t *)data);
}

static void diagram_update_nodes(GtkDrawingArea *canvas)
{
  GList *delete_list = NULL;
  node_t *new_node = NULL;

  /* Check if there are any new nodes */
  while ((new_node = new_nodes_pop()))
    check_new_node(new_node, canvas);

  /* Update nodes look and queue outdated canvas_nodes for deletion */
  g_tree_foreach(canvas_nodes,
                 (GTraverseFunc)canvas_node_update,
                 &delete_list);

  /* delete all canvas nodes queued */
  g_list_foreach(delete_list, gfunc_remove_canvas_node, NULL);

  /* free the list - list items are already destroyed */
  g_list_free(delete_list);

  /* Limit the number of nodes displayed, if a limit has been set */
  /* TODO check whether this is the right function to use, now that we have a more
   * general display_node called in update_canvas_nodes */
  limit_nodes();

  /* Reposition canvas_nodes */
  if (need_reposition)
    diagram_reposition(canvas);
}

/* handle node repositioning */
static void diagram_reposition(GtkDrawingArea *canvas)
{
  reposition_node_t rdata;

  if (pref.headless)
    return;

  init_reposition(&rdata, canvas, displayed_nodes);

  g_tree_foreach(canvas_nodes,
                 (GTraverseFunc)reposition_canvas_nodes_prep,
                 &rdata);

  rdata.center.node_i = rdata.center.n_nodes;
  rdata.outer.node_i = rdata.outer.n_nodes;

  g_tree_foreach(canvas_nodes,
                 (GTraverseFunc)reposition_canvas_nodes,
                 &rdata);

  clear_reposition(&rdata);

  need_reposition = FALSE;
  need_font_refresh = FALSE;
}

static void diagram_update_links(GtkDrawingArea *canvas)
{
  GList *delete_list = NULL;

  if (pref.headless)
    return;

  /* Check if there are any new links */
  links_catalog_foreach((GTraverseFunc)check_new_link, canvas);

  /* Update links look
   * We also queue timedout links for deletion */
  delete_list = NULL;
  g_tree_foreach(canvas_links,
                 (GTraverseFunc)canvas_link_update,
                 &delete_list);

  /* delete all canvas links queued */
  g_list_foreach(delete_list, gfunc_remove_canvas_link, NULL);

  /* free the list - list items are already destroyed */
  g_list_free(delete_list);
}

/* Update libpcap stats counters display */
static void update_pcap_stats_text(GtkDrawingArea *canvas)
{
  if (pcap_stats_text)
  {
    g_free(pcap_stats_text);
    pcap_stats_text = NULL;
  }

  if (pref.pcap_stats_pos == STATSPOS_NONE || get_capture_status() != PLAY)
    return;

  if (appdata.source.type == ST_FILE)
    pcap_stats_text = g_strdup(_("(Capture statistics unavailable in offline mode.)"));
  else
  {
    struct pcap_stat stats;
    get_capture_stats(&stats);

    pcap_stats_text = g_strdup_printf("%-12s %12u  %-12s %12u  %-12s %12u", "Recv:",
                                      stats.ps_recv, "Drop:", stats.ps_drop,
                                      "IfDrop:", stats.ps_ifdrop);
  }

  gtk_widget_queue_draw(GTK_WIDGET(canvas)); // queue redraw
}

/* Refreshes the diagram. Called each refresh_period ms
 * 1. Checks for new protocols and displays them
 * 2. Updates nodes looks
 * 3. Updates links looks
 */
static void update_diagram(GtkDrawingArea *canvas)
{
  capstatus_t status;

  /* if requested and enabled, dump to xml or json */
  if (appdata.request_dump && appdata.export_file_signal) {
    g_warning(_("SIGUSR1 received: exporting to %s"), appdata.export_file_signal);
    export_data(appdata.export_file_signal, appdata.n_packets);
    appdata.request_dump = FALSE;
  }

  status = get_capture_status();
  if (status == PAUSE)
    return;

  /* Helps with lingering nodes and links to expire during
   * low capture rates or using a strict capture filter.*/
  if (!node_count() && (appdata.source.type == ST_LIVE)) {
    refresh_diagram();
  }

  if (status == CAP_EOF) {
    gui_eof_capture();

    if (pref.headless || appdata.export_file_final)
      gtk_main_quit();  // special modes always end after replay

#if DEBUG_TIMINGS
    /* after capture stops, update the time in 100ms steps for debugging */
    static struct timeval incr;
    incr.tv_sec = 0;
    incr.tv_usec = 100000;
    timeradd(&appdata.now, &incr, &appdata.now);
#else
    return;  /* after replaying do not update the display anymore */
#endif        
  }

  /*
   * It could happen that during an intensive calculation, in order
   * to update the GUI and make the application responsive gtk_main_iteration
   * is called. But that could also trigger this very function's timeout.
   * If we let it run twice many problems could come up. Thus,
   * we are preventing it with the already_updating variable
   */

  if (already_updating) {
    g_my_debug("update_diagram called while already updating");
    return;
  }

  already_updating = TRUE;

  if (!pref.headless || appdata.max_delay) {
    /* headless with max delay zero disables also node expiration */ 

    /* Deletes all nodes and updates traffic values */
    nodes_catalog_update_all();

    /* Delete old capture links and update capture link variables */
    links_catalog_update_all();

    /* Update global protocol information */
    protocol_summary_update_all();
  }

  if (!pref.headless) {

    /* update background image */
    diagram_update_background_image(canvas);

    /* update nodes */
    diagram_update_nodes(canvas);

    /* update links */
    diagram_update_links(canvas);

    /* update proto legend */
    update_legend();

    /* Now update info windows */
    update_info_windows(NULL);

    update_pcap_stats_text(canvas);
  }

  /* Force redraw */
  while (gtk_events_pending())
    gtk_main_iteration();

  already_updating = FALSE;

  if (stop_requested)
    gui_stop_capture();
}

/* Initialize the legend_protocols hash table */
void init_legend(void)
{
  if (legend_protocols) {
    cleanup_legend();
  }
  if (!legend_protocols) {
    legend_protocols = g_hash_table_new_full(g_str_hash, 
                                             g_str_equal, 
                                             g_free,      /* key destroy */
                                             NULL);       /* value destroy - we don't own the widget */
  }
}

/* Cleanup function call: legend_protocols */
void cleanup_legend(void)
{
  if (legend_protocols) {
    g_hash_table_destroy(legend_protocols);
    legend_protocols = NULL;
  }
}

/* Purge expired legend protocol widget */
static void purge_expired_legend_protocol(GtkWidget *widget, gpointer data)
{
  GtkLabel *lab = GTK_LABEL(widget);
  if (!GTK_IS_LABEL(lab))
    return;

  const gchar *label_text = gtk_label_get_text(lab);
  /* Only remove protocol if it no longer exists in the current protocol summary */
  if (!protocol_summary_find(pref.stack_level, label_text)) {
    g_my_debug("Purging expired protocol: %s", label_text);

    /* Remove from hash table ONLY when we're actually destroying the widget */
    if (legend_protocols && g_hash_table_contains(legend_protocols, label_text)) {
      g_hash_table_remove(legend_protocols, label_text);
    }
    /* Destroy the widget */
    gtk_widget_destroy(widget);
    known_protocols--;
  }
}

/* Update legend protocol widgets */
static void update_legend(void)
{
  GtkWidget *prot_table;
    
  prot_table = GTK_WIDGET(gtk_builder_get_object(appdata.xml, "prot_table"));
  if (!prot_table)
    return;

  /* First, remove expired protocols from both the UI and our hash table */
  gtk_container_foreach(GTK_CONTAINER(prot_table),
                        (GtkCallback)purge_expired_legend_protocol, NULL);

  /* Then add any new protocols */
  check_new_protocol(prot_table, protocol_summary_stack());
}

/* Checks whether there is already a legend entry for each known protocol.
 * If not, create it. Uses GHashTable for fast and reliable lookup. */
static void check_new_protocol(GtkWidget *prot_table, const protostack_t *pstk)
{
  if (!pstk || !prot_table)
    return;

  if (!legend_protocols)
    init_legend();
/*
    g_my_debug("=== Legend hash table size BEFORE processing: %u ===",
               g_hash_table_size(legend_protocols));
    GHashTableIter iter;
    gpointer key, value;
    g_hash_table_iter_init(&iter, legend_protocols);
    while (g_hash_table_iter_next(&iter, &key, &value)) {
        g_my_debug("  Legend entry -> Key: '%s'  Widget: %p", 
                   (const char*)key, value);
    }
*/
  const GList *protocol_item = pstk->protostack[pref.stack_level];
  const protocol_t *protocol;
  while (protocol_item) {
    protocol = protocol_item->data;
    protocol_item = protocol_item->next;
    if (g_hash_table_lookup(legend_protocols, protocol->name) != NULL) {
      continue;		/* skip - all ready created */
    }

    g_my_debug("Protocol: '%s' not found in legend. Creating new entry.",
               protocol->name);

    GtkWidget *newlab = gtk_label_new(NULL);
    gtk_widget_show(newlab);

    const GdkRGBA *color = protohash_color(protocol->name);
    gchar *mkupbuf = g_markup_printf_escaped(
      "<span foreground=\"#%02x%02x%02x\">%s</span>",
      (unsigned int)(color->red * 255),
      (unsigned int)(color->green * 255),
      (unsigned int)(color->blue * 255),
      protocol->name);
    /* Apply markup */
    gtk_label_set_markup(GTK_LABEL(newlab), mkupbuf);
    g_free(mkupbuf);

    /* Add widget */
    gtk_container_add(GTK_CONTAINER(prot_table), newlab);

    /* Insert in hash table with strdup'd key */
    g_hash_table_insert(legend_protocols,
                        g_strdup(protocol->name),
                        newlab);
    /* New protocol legend widget added */
    known_protocols++;
  }
}

/* empties the table of protocols */
void delete_gui_protocols(void)
{
  GList *item;
  GtkContainer *prot_table;

  known_protocols = 0;

  /* restart color cycle */
  protohash_reset_cycle();

  /* remove proto labels from legend */
  prot_table = GTK_CONTAINER(gtk_builder_get_object(appdata.xml, "prot_table"));
  item = gtk_container_get_children(GTK_CONTAINER(prot_table));
  while (item) {
    gtk_container_remove(prot_table, item->data);
    item = item->next;
  }

  gtk_widget_queue_resize(GTK_WIDGET(appdata.app1));
}                               /* delete_gui_protocols */

/* Checks if there is a canvas_node per each node. If not, one canvas_node
 * must be created and initiated */
static gint check_new_node(node_t *node, GtkDrawingArea *canvas)
{
  canvas_node_t *new_canvas_node;

  if (!node)
    return FALSE;

  if (display_node(node) && !g_tree_lookup(canvas_nodes, &node->node_id)) {
    new_canvas_node = g_malloc(sizeof(canvas_node_t));
    g_assert(new_canvas_node);
    
    new_canvas_node->node_id = node->node_id;
    new_canvas_node->x = 100.0;
    new_canvas_node->y = 100.0;
    new_canvas_node->radius = 0.0;
    new_canvas_node->text = g_strdup(node->name->str);
    new_canvas_node->is_new = TRUE;
    new_canvas_node->shown = TRUE;
    new_canvas_node->centered = FALSE;

    g_tree_insert(canvas_nodes,
                  &new_canvas_node->node_id, new_canvas_node);
    g_log(G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG,
          _("Creating canvas_node: %s. Number of nodes %d"),
          node->name->str, g_tree_nnodes(canvas_nodes));

    need_reposition = TRUE;
  }

  return FALSE;                 /* False to keep on traversing */
}                               /* check_new_node */


/* - updates sizes, names, etc */
static gint canvas_node_update(node_id_t *node_id, canvas_node_t *canvas_node,
                               GList * *delete_list)
{
  node_t *node;
  gdouble node_size;
  static clock_t start = 0;
  clock_t end;
  gdouble cpu_time_used;
  const gchar *main_prot;

  node = nodes_catalog_find(node_id);

  /* Remove node if node is too old or if capture is stopped */
  if (!node || !display_node(node)) {
    /* adds current to list of canvas nodes to delete */
    *delete_list = g_list_prepend(*delete_list, node_id);
    g_my_debug("Queueing canvas node to remove.");
    need_reposition = TRUE;
    return FALSE;
  }

  switch (pref.node_size_variable)
  {
      case INST_TOTAL:
        node_size = get_node_size(node->node_stats.stats.average);
        break;
      case INST_INBOUND:
        node_size = get_node_size(node->node_stats.stats_in.average);
        break;
      case INST_OUTBOUND:
        node_size = get_node_size(node->node_stats.stats_out.average);
        break;
      case INST_PACKETS:
        node_size = get_node_size(node->node_stats.pkt_list.length);
        break;
      case ACCU_TOTAL:
        node_size = get_node_size(node->node_stats.stats.accumulated);
        break;
      case ACCU_INBOUND:
        node_size = get_node_size(node->node_stats.stats_in.accumulated);
        break;
      case ACCU_OUTBOUND:
        node_size = get_node_size(node->node_stats.stats_out.accumulated);
        break;
      case ACCU_PACKETS:
        node_size = get_node_size(node->node_stats.stats.accu_packets);
        break;
      case ACCU_AVG_SIZE:
        node_size = get_node_size(node->node_stats.stats.avg_size);
        break;
      default:
        node_size = get_node_size(node->node_stats.stats_out.average);
        g_warning(_("Unknown value or node_size_variable"));
  }

  /* limit the maximum size to avoid overload */
  if (node_size > MAX_NODE_SIZE)
    node_size = MAX_NODE_SIZE;

  /* Update node size */
  canvas_node->radius = node_size / 2;

  /* Update node color based on main protocol */
  main_prot = traffic_stats_most_used_proto(&node->node_stats, pref.stack_level);
  if (main_prot) {
    canvas_node->color = *protohash_color(main_prot);
  } else {
    /* Default to black if no protocol */
    canvas_node->color.red = 0;
    canvas_node->color.green = 0;
    canvas_node->color.blue = 0;
    canvas_node->color.alpha = 1.0;
  }

  /* We check the name of the node, and update the canvas node name
   * if it has changed (useful for non blocking dns resolving) */
  if (strcmp(canvas_node->text, node->name->str)) {
    g_free(canvas_node->text);
    canvas_node->text = g_strdup(node->name->str);
  }

  /* Processor time check. If too much time has passed, update the GUI */
  end = clock();
  cpu_time_used = ((gdouble)(end - start)) / CLOCKS_PER_SEC;
  if (cpu_time_used > 0.05) {
    /* Force redraw */
    gtk_widget_queue_draw(GTK_WIDGET(gcanvas_));
    while (gtk_events_pending())
      gtk_main_iteration();
    start = end;
  }
  
  return FALSE;                 /* False means keep on calling the function */
}                               /* update_canvas_nodes */


/* Returns whether the node in question should be displayed in the
 * diagram or not */
static gboolean display_node(node_t *node)
{
  double diffms;

  if (!node)
    return FALSE;

  diffms = subtract_times_ms(&appdata.now, &node->node_stats.stats.last_time);

  /* There are problems if a canvas_node is deleted if it still
   * has packets, so we have to check that as well */

  /* Remove canvas_node if node is too old */
  if (diffms >= pref.gui_node_timeout_time &&
      pref.gui_node_timeout_time &&
      !node->node_stats.pkt_list.length)
    return FALSE;

#if 1
  if ((pref.gui_node_timeout_time == 1) && !node->node_stats.pkt_list.length)
    g_my_critical("Impossible situation in display node");
#endif

  return TRUE;
}                               /* display_node */

/* Sorts canvas nodes with the criterium set in preferences and sets
 * which will be displayed in the diagram */
static void limit_nodes(void)
{
  GTree *ordered_nodes = NULL;
  guint limit;

  displayed_nodes = 0;          /* We'll increment for each node we don't
                                 * limit */

  if (appdata.node_limit < 0) {
    displayed_nodes = g_tree_nnodes(canvas_nodes);
    return;
  }

  limit = appdata.node_limit;

  ordered_nodes = g_tree_new(traffic_compare);

  g_tree_foreach(canvas_nodes, (GTraverseFunc)add_ordered_node, ordered_nodes);
  g_tree_foreach(ordered_nodes, (GTraverseFunc)check_ordered_node,
                 &limit);
  g_tree_destroy(ordered_nodes);
}                               /* limit_nodes */

static gint add_ordered_node(node_id_t *node_id, canvas_node_t *node,
                             GTree *ordered_nodes)
{
  g_tree_insert(ordered_nodes, node, node);
  g_my_debug("Adding ordered node. Number of nodes: %d",
             g_tree_nnodes(ordered_nodes));
  return FALSE;                 /* keep on traversing */
}                               /* add_ordered_node */

static gint check_ordered_node(gdouble *traffic, canvas_node_t *node, guint *count)
{
  /* TODO We can probably optimize this by stopping the traversion once
   * the limit has been reached */
  if (*count) {
    if (!node->shown)
      need_reposition = TRUE;
    node->shown = TRUE;
    ++displayed_nodes;
    (*count)--;
  }
  else {
    if (node->shown)
      need_reposition = TRUE;
    node->shown = FALSE;
  }
  return FALSE;            /* keep on traversing */
}

/* Comparison function used to order the (GTree *) nodes
 * and canvas_nodes heard on the network */
static gint traffic_compare(gconstpointer a, gconstpointer b)
{
  node_t *node_a, *node_b;

  g_assert(a != NULL);
  g_assert(b != NULL);

  node_a = (node_t *)a;
  node_b = (node_t *)b;

  if (node_a->node_stats.stats.average < node_b->node_stats.stats.average)
    return 1;
  if (node_a->node_stats.stats.average > node_b->node_stats.stats.average)
    return -1;

  /* If two nodes have the same traffic, we still have
   * to distinguish them somehow. We use the node_id */

  return (node_id_compare(&node_a->node_id, &node_b->node_id));
}                               /* traffic_compare */

/* initialize reposition struct */
static void init_reposition(reposition_node_t *data, GtkDrawingArea *canvas, guint total_nodes)
{
  gdouble text_compensation = 50;

  data->canvas = canvas;
  memset(&data->center, 0, sizeof(data->center));
  memset(&data->outer, 0, sizeof(data->outer));

  /*
   * Offset the starting angle on the center ring so that when there are
   * relatively few nodes displayed (e.g. 4 central and 4 outer) the links
   * obscure things less (by not overlapping node labels and other links).
   */
  data->center.angle += M_PI / 4.0;

  if (appdata.column_patterns)
    data->column_populations = g_malloc0(appdata.column_patterns->len + 1 *
                                         sizeof(*data->column_populations));

  /* Get canvas bounds from the canvas_rect global variable */
  data->xmin = canvas_rect.x;
  data->ymin = canvas_rect.y;
  data->xmax = canvas_rect.x + canvas_rect.width;
  data->ymax = canvas_rect.y + canvas_rect.height;


  data->xmin += text_compensation;
  data->xmax -= text_compensation;      /* Reduce the drawable area so that
                                 * the node name is not lost
                                 * TODO: Need a function to calculate
                                 * text_compensation depending on font size */
  data->x_rad_max = 0.9 * (data->xmax - data->xmin) / 2;
  data->y_rad_max = 0.9 * (data->ymax - data->ymin) / 2;
  data->x_inner_rad_max = data->x_rad_max / 2;
  data->y_inner_rad_max = data->y_rad_max / 2;
}

static void clear_reposition(reposition_node_t *rdata)
{
  if (appdata.column_patterns)
    g_free(rdata->column_populations);
}

static guint find_node_column(node_t *node)
{
  guint i;

  /* This should only be called if we're in columnar-positioning mode */
  g_assert(appdata.column_patterns);

  for (i = 0; i < appdata.column_patterns->len; i++) {
    if (node_matches_spec_list(node, g_ptr_array_index(appdata.column_patterns, i)))
      return i;
  }

  /*
   * If no explicit match was found it goes in the rightmost column (with an
   * implicit "match-all" pattern).
   */
  return appdata.column_patterns->len;
}

/*
 * A preparatory pass to count how many nodes are centered and how many are on
 * the outer ring (and mark each appropriately).  Also does analogous work for
 * columnar-positioning mode (count nodes in each column and mark each node with
 * its column).
 */
static gint reposition_canvas_nodes_prep(const node_id_t *node_id,
                                         canvas_node_t *canvas_node,
                                         reposition_node_t *rdata)
{
  node_t *node;

  if (!canvas_node->shown)
    return FALSE;

  node = nodes_catalog_find(node_id);
  if (appdata.column_patterns) {
    canvas_node->column = find_node_column(node);
    canvas_node->column_idx = rdata->column_populations[canvas_node->column]++;
  }
  else if (node && node_matches_spec_list(node, centered_node_speclist)) {
    canvas_node->centered = TRUE;
    rdata->center.n_nodes++;
  }
  else {
    canvas_node->centered = FALSE;
    rdata->outer.n_nodes++;
  }

  return FALSE;
}

/*
 * Return a point between 'min' and 'max' appropriate for position number 'pos'
 * out of 'num' total possible positions (basically pos/num of the way between
 * min and max, though with some tweaking to keep things away from the very
 * edges of the range).
 */
static gdouble scale_within(gdouble min, gdouble max, guint pos, guint num)
{
  return min + (((max - min) / (num + 1)) * (pos + 1));
}

/* Called from update_diagram if the global need_reposition
 * is set. It rearranges the nodes*/
static gint reposition_canvas_nodes(node_id_t *node_id,
                                    canvas_node_t *canvas_node,
                                    reposition_node_t *data)
{
  struct node_ring *ring;
  gdouble center_x, center_y, oddAngle;
  gdouble x = 0, y = 0;

  if (!canvas_node->shown) {
    return FALSE;
  }

  ring = canvas_node->centered ? &data->center : &data->outer;

  center_x = (data->xmax - data->xmin) / 2 + data->xmin;
  center_y = (data->ymax - data->ymin) / 2 + data->ymin;

  /* TODO I've done all the stationary changes in a hurry
   * I should review it an tidy up all this stuff */
  if (appdata.stationary_layout) {
    if (canvas_node->is_new) {
      static guint count = 0, base = 1;
      gdouble s_angle = 0;

      if (count == 0) {
        s_angle = M_PI * 2.0f;
        count++;
      }
      else {
        if (count > 2 * base) {
          base *= 2;
          count = 1;
        }
        s_angle = M_PI * (gdouble)count / ((gdouble)base);
        count += 2;
      }
      x = data->x_rad_max * cos(s_angle);
      y = data->y_rad_max * sin(s_angle);
    }
  }
  else if (appdata.column_patterns) {
    guint col = canvas_node->column;

    x = scale_within(data->xmin, data->xmax, canvas_node->column,
                     appdata.column_patterns->len + 1);
    y = scale_within(data->ymin, data->ymax, canvas_node->column_idx,
                     data->column_populations[col]);
  }
  else {
    if (canvas_node->centered && data->center.n_nodes == 1) {
      /* one centered node, reset coordinates */
      x = center_x;
      y = center_y;
      ring->angle -= 2 * M_PI / ring->n_nodes;
    }
    else {
      if (ring->n_nodes % 2 == 0) /* spacing is better when n_nodes is odd and Y is linear */
        oddAngle = (ring->angle * ring->n_nodes) / (ring->n_nodes + 1);
      else
        oddAngle = ring->angle;

      if (ring->n_nodes > 7) {
        x = data->x_rad_max * cos(oddAngle);
        y = data->y_rad_max * asin(sin(oddAngle)) / (M_PI / 2);
      }
      else {
        x = data->x_rad_max * cos(ring->angle);
        y = data->y_rad_max * sin(ring->angle);
      }

      if (canvas_node->centered && data->center.n_nodes > 1) {
        /* For the inner ring, just move it proportionally closer the the center point. */
        x = center_x + ((x - center_x) * pref.inner_ring_scale);
        y = center_y + ((y - center_y) * pref.inner_ring_scale);
      }
    }
  }

  if (!appdata.stationary_layout || canvas_node->is_new) {
    canvas_node->x = x;
    canvas_node->y = y;
    canvas_node->is_new = FALSE;
  }

  ring->node_i--;

  if (ring->node_i)
    ring->angle += 2 * M_PI / ring->n_nodes;
  else {
    ring->angle = 0.0;
    ring->n_nodes = 0;
  }

  return FALSE;
}


/* Goes through all known links and checks whether there already exists
 * a corresponding canvas_link. If not, create it.*/
static gint check_new_link(link_id_t *link_id, link_t *link, GtkDrawingArea *canvas)
{
  canvas_link_t *new_canvas_link;

  if (!g_tree_lookup(canvas_links, link_id)) {
    new_canvas_link = g_malloc(sizeof(canvas_link_t));
    g_assert(new_canvas_link);
    
    new_canvas_link->link_id = *link_id;
    new_canvas_link->src_x = 0.0;
    new_canvas_link->src_y = 0.0;
    new_canvas_link->dst_x = 0.0;
    new_canvas_link->dst_y = 0.0;
    new_canvas_link->src_size = 0.0;
    new_canvas_link->dst_size = 0.0;
    new_canvas_link->alpha = 1.0;
    
    /* Default color (will be updated in canvas_link_update) */
    new_canvas_link->color.red = 0.5;
    new_canvas_link->color.green = 0.5;
    new_canvas_link->color.blue = 0.5;
    new_canvas_link->color.alpha = 1.0;

    g_tree_insert(canvas_links,
                  &new_canvas_link->link_id, new_canvas_link);
  }

  return FALSE;
}


/* - calls update_links, so that the related link updates its average
 *   traffic and main protocol, and old links are deleted
 * - caculates link size and color fading */
static gint canvas_link_update(link_id_t *link_id, canvas_link_t *canvas_link,
                               GList * *delete_list)
{
  const link_t *link;
  const canvas_node_t *canvas_dst;
  const canvas_node_t *canvas_src;
  double scale;
  const gchar *main_prot;

  /* We used to run update_link here, but that was a major performance penalty,
   * and now it is done in update_diagram */
  link = links_catalog_find(link_id);
  if (!link) {
    *delete_list = g_list_prepend(*delete_list, link_id);
    g_my_debug("Queueing canvas link to remove.");
    return FALSE;
  }

  /* If either source or destination has disappeared, we hide the link
   * until it can be shown again */

  /* We get coords for the destination node */
  canvas_dst = g_tree_lookup(canvas_nodes, &link_id->dst);
  if (!canvas_dst || !canvas_dst->shown) {
    return FALSE;
  }

  /* We get coords from source node */
  canvas_src = g_tree_lookup(canvas_nodes, &link_id->src);
  if (!canvas_src || !canvas_src->shown) {
    return FALSE;
  }

  /* Update link coordinates from nodes */
  canvas_link->src_x = canvas_src->x;
  canvas_link->src_y = canvas_src->y;
  canvas_link->dst_x = canvas_dst->x;
  canvas_link->dst_y = canvas_dst->y;

  /* What if there never is a protocol?
   * I have to initialize canvas_link->color to a known value */
  main_prot = traffic_stats_most_used_proto(&link->link_stats, pref.stack_level);
  if (main_prot) {
    double diffms;
    double ratio;
    canvas_link->color = *protohash_color(main_prot);

    diffms = subtract_times_ms(&appdata.now, &link->link_stats.stats.last_time);
    ratio = diffms / pref.averaging_time;
    scale = pow(0.5, ratio);
    if (scale < 0.1) {
      /* too dark, just hide */
      canvas_link->alpha = 0;
      return FALSE;
    }

    canvas_link->alpha = scale;
  }
  else {
    /* black */
    canvas_link->color.red = 0;
    canvas_link->color.green = 0;
    canvas_link->color.blue = 0;
    canvas_link->color.alpha = 1;
    canvas_link->alpha = 1;
  }

  /* Calculate link sizes */
  canvas_link->src_size = get_link_size(&(link->link_stats.stats_out)) / 2;
  canvas_link->dst_size = get_link_size(&(link->link_stats.stats_in)) / 2;

  /* Limit maximum sizes */
  if (canvas_link->src_size > MAX_LINK_SIZE)
    canvas_link->src_size = MAX_LINK_SIZE;
  if (canvas_link->dst_size > MAX_LINK_SIZE)
    canvas_link->dst_size = MAX_LINK_SIZE;

  return FALSE;
} 


/* Returs the radius in pixels given average traffic and size mode */
static gdouble get_node_size(gdouble average)
{
  gdouble result = 0.0;
  switch (pref.size_mode)
  {
      case LINEAR:
        result = average + 1;
        break;
      case LOG:
        result = log(average + 1);
        break;
      case SQRT:
        result = sqrt(average + 1);
        break;
  }
  return 5.0 + pref.node_radius_multiplier * result;
}

/* Returs the width in pixels given average traffic and size mode */
static gdouble get_link_size(const basic_stats_t *link_stats)
{
  gdouble result = 0.0;
  gdouble data;

  /* since links are one-sided, there's no distinction between inbound/outbound
     data.   */
  switch (pref.node_size_variable)
  {
      case INST_TOTAL:
      case INST_INBOUND:
      case INST_OUTBOUND:
      case INST_PACKETS: /* active packets stat not available */
        data = link_stats->average;
        break;
      case ACCU_TOTAL:
      case ACCU_INBOUND:
      case ACCU_OUTBOUND:
        data = link_stats->accumulated;
        break;
      case ACCU_PACKETS:
        data = link_stats->accu_packets;
        break;
      case ACCU_AVG_SIZE:
        data = link_stats->avg_size;
        break;
      default:
        data = link_stats->average;
        g_warning(_("Unknown value for link_size_variable"));
  }
  switch (pref.size_mode)
  {
      case LINEAR:
        result = data + 1;
        break;
      case LOG:
        result = log(data + 1);
        break;
      case SQRT:
        result = sqrt(data + 1);
        break;
  }
  return 1.0 + pref.node_radius_multiplier * result;
}



/* Called for every event a link receives. Right now it's used to
 * set a message in the statusbar and launch the popup */
static gboolean link_item_event(GdkEventType type, const canvas_link_t *canvas_link)
{
  gchar *str;
  const gchar *main_prot = NULL;
  const link_t *link = NULL;

  switch (type)
  {
      case GDK_BUTTON_PRESS:
      case GDK_BUTTON_RELEASE:
      case GDK_2BUTTON_PRESS:
      case GDK_3BUTTON_PRESS:
        if (canvas_link)
          link_info_window_create(&canvas_link->link_id);
        break;

      case GDK_ENTER_NOTIFY:
        if (canvas_link)
          link = links_catalog_find(&canvas_link->link_id);
        if (link)
          main_prot = traffic_stats_most_used_proto(&link->link_stats, pref.stack_level);
        if (main_prot)
          str = g_strdup_printf(_("Link main protocol: %s"), main_prot);
        else
          str = g_strdup_printf(_("Link main protocol: unknown"));
        gtk_statusbar_push(appdata.statusbar, 1, str);
        g_free(str);
        break;
      case GDK_LEAVE_NOTIFY:
        gtk_statusbar_pop(appdata.statusbar, 1);
        break;
      default:
        break;
  }

  return FALSE;
}


/* Called for every event a node receives. Right now it's used to
 * launch the popup */
static gint node_item_event(GdkEventType type, const canvas_node_t *canvas_node)
{
  const node_t *node = NULL;

  switch (type)
  {
      case GDK_BUTTON_PRESS:
      case GDK_BUTTON_RELEASE:
      case GDK_2BUTTON_PRESS:
      case GDK_3BUTTON_PRESS:
        if (canvas_node)
          node = nodes_catalog_find(&canvas_node->node_id);
        if (node) {
          node_protocols_window_create(&canvas_node->node_id);
          g_my_info("Nodes: %d (shown %u)", nodes_catalog_size(),
                    displayed_nodes);
          if (DEBUG_ENABLED) {
            gchar *msg = node_dump(node);
            g_my_debug("%s", msg);
            g_free(msg);
          }
        }
        break;
      default:
        break;
  }

  return FALSE;
}

/* Pushes a string into the statusbar stack */
void set_statusbar_msg(gchar *str)
{
  static gchar *status_string = NULL;

  if (status_string)
    g_free(status_string);

  status_string = g_strdup(str);

  gtk_statusbar_pop(appdata.statusbar, 0);
  gtk_statusbar_push(appdata.statusbar, 0, status_string);
}                               /* set_statusbar_msg */


static gint canvas_node_compare(const node_id_t *a, const node_id_t *b,
                                gpointer dummy)
{
  g_assert(a != NULL);
  g_assert(b != NULL);
  return node_id_compare(a, b);
}

static gint canvas_link_compare(const link_id_t *a, const link_id_t *b,
                                gpointer dummy)
{
  g_assert(a != NULL);
  g_assert(b != NULL);
  return link_id_compare(a, b);
}

static void canvas_link_delete(canvas_link_t *canvas_link)
{
  g_free(canvas_link);
}

/* diagram timeout was changed. Remove old timer and register new */
void diagram_timeout_changed(void)
{
  if (diagram_timeout)
    g_source_remove(diagram_timeout);
  diagram_timeout = g_timeout_add(pref.refresh_period,
                                  update_diagram_callback,
                                  NULL);
}

void resize_diagram(const GtkAllocation *allocation)
{
  canvas_rect.x = -allocation->width / 2;
  canvas_rect.y = -allocation->height / 2;
  canvas_rect.width = allocation->width;
  canvas_rect.height = allocation->height;
  
  ask_reposition(FALSE);
  redraw_canvas_background(gcanvas_);
  update_diagram(gcanvas_);
}

gboolean update_diagram_callback(gpointer data)
{
  update_diagram(gcanvas_);
  return TRUE;
}

gboolean refresh_diagram(void)
{
  GtkAllocation windowsize;
  /* Simulate a window resize */
  g_my_debug("repaint diagram requested");
  gtk_widget_get_allocation(GTK_WIDGET(garea_), &windowsize);
  
  canvas_rect.x = -windowsize.width/2;
  canvas_rect.y = -windowsize.height/2;
  canvas_rect.width = windowsize.width;
  canvas_rect.height = windowsize.height;
  
  redraw_canvas_background(gcanvas_);
  diagram_reposition(gcanvas_);
  diagram_update_links(gcanvas_);
  
  /* Force redraw */
  gtk_widget_queue_draw(GTK_WIDGET(gcanvas_));
  
  return TRUE;
}

/* Update the appdata.now timestamp.
 * Used for low capture packet rate conditions
 * or using a strict capture filter. */
gboolean update_now_timestamp(gpointer dummy)
{
  if (appdata.source.type == ST_LIVE) {
    time_t timenow = time(NULL); 
    static struct timeval now;
    now.tv_sec = timenow; 
    now.tv_usec = 0;
    appdata.now = now;
  } 
  return TRUE;
}

