context: //
  // ZoomableNetworkMap.cs: Bringin' sexy back...
  //
  // Author:
  //   Idan Gazit <idan@fastmail.fm>
  //   Eric Butler <eric@extremeboredom.net>
  //
  // (C) 2006 FileFind.net (http://filefind.net/)
  // 
  
  using System;
  using System.Collections;
  using System.Net;
  using Cairo;
  using Meshwork.Client.GtkClient.Menus;
  using Gtk;
  using Meshwork.Backend.Core;
  using Meshwork.Backend.Core.Destination;
  
  namespace Meshwork.Client.GtkClient.Widgets
  {
  	public class NodeGroup
  	{
  		private ArrayList nodes;
  		private Cairo.PointD position;
  		private SizeD dimension;
  		private string name;
  
  		public NodeGroup ()
  		{
  			nodes = ArrayList.Synchronized(new ArrayList());
  			name = string.Empty;
  			position = new PointD(0, 0);
  			dimension = new SizeD(0, 0);
  		}
  
  		public NodeGroup (string name, PointD position)
  		{
  			nodes = ArrayList.Synchronized(new ArrayList());
  			this.name = name;
  			this.position = position;
  			this.dimension = new SizeD(0, 0);
  		}
  
  		public ArrayList Nodes {
  			get { return this.nodes; }
  		}
  
  		public PointD Position {
  			get { return this.position; }
  			set { this.position = value; }
  		}
  
  		public SizeD Dimension {
  			get { return this.dimension; }
  			set { this.dimension = value; }
  		}
  
  		public string Name {
  			get { return this.name; }
  			set { this.name = value; }
  		}
  	}
  
  	public struct SizeD
  	{
  		public double Width, Height;
  
  		public SizeD (double width, double height)
  		{
  			this.Width = width;
  			this.Height = height;
  		}
  	}
  
  	public class ZoomableNetworkMap : ZoomableCairoArea
  	{
  		bool debug = false;
  
  		#region members
  		private ArrayList networkZOrder;
  
  		private NodeGroup selectedGroup;
  		private Network selectedNetwork;
  
  		private Node selectedNode;
  		private Node hoverNode;
  		private bool nodeDragging;
  		private double nodeDraggingLastX, nodeDraggingLastY;
  		private Random rng;
  		#endregion
  
  		#region colors
  		private readonly Cairo.Color lightGray = new Cairo.Color(0.8, 0.8, 0.8, 0.8);
  		private readonly Cairo.Color mediumGray = new Cairo.Color(0.4, 0.4, 0.4, 1.0);
  		private readonly Cairo.Color darkGray = new Cairo.Color(0.3, 0.3, 0.3, 1.0);
  		private readonly Cairo.Color lightAlphaGray = new Cairo.Color(0.8, 0.8, 0.8, 0.5);
  		private readonly Cairo.Color mediumAlphaGray = new Cairo.Color(0.5, 0.5, 0.5, 0.5);
  		private readonly Cairo.Color darkAlphaGray = new Cairo.Color(0.1, 0.1, 0.1, 0.8);
  		private readonly Cairo.Color black = new Cairo.Color(0, 0, 0, 1);
  		private readonly Cairo.Color white = new Cairo.Color(1, 1, 1, 1);
  		private readonly Cairo.Color alphaRed = new Cairo.Color(1, 0, 0, 0.2);
  		private readonly Cairo.Color alphaBlue = new Cairo.Color(0, 0, 1, 0.2);
  		private readonly Cairo.Color connectionGreen = new Cairo.Color(0, 1, 0, 0.5);
  		private readonly Cairo.Color orangeOverlay = new Cairo.Color(1.0, 0.7, 0.0, 0.4);
  		private readonly Cairo.Color transparent = new Cairo.Color(0, 0, 0, 0);
  		private readonly Cairo.Color networkBG = new Cairo.Color(0.6, 0.6, 0.6, 1.0);
  		#endregion
  
  		#region constants
  		private static readonly double AvatarDimension = 40.0;
  		private static readonly SizeD AvatarSizeD = new SizeD(AvatarDimension, AvatarDimension);
  		private static readonly double HalfAvatarDimension = AvatarDimension / 2.0;
  		private static readonly double Padding = 4.0;
  		private static readonly double NodegroupNameFontSize = 14.0;
  		private static readonly double NetworkNameFontSize = 20.0;
  		private static Gdk.Pixbuf noticeIcon;
  		#endregion
  
  		public delegate void NodeEventHandler (Node selectedNode);
  		public event NodeEventHandler SelectedNodeChanged;
  		public event NodeEventHandler NodeDoubleClicked;
  		public event ErrorEventHandler Error;
  
  		public ZoomableNetworkMap ()
  		{
  			noticeIcon = Gui.LoadIcon(24, "dialog-information");
  			rng = new Random();
  			
  			networkZOrder = ArrayList.Synchronized(new ArrayList());
  			
  			selectedGroup = null;
  			nodeDragging = false;
  			nodeDraggingLastX = 0;
  			nodeDraggingLastY = 0;
  			
  			Runtime.Core.NetworkAdded += (NetworkEventHandler)DispatchService.GuiDispatch(new NetworkEventHandler(AddNetwork));
  		    Runtime.Core.NetworkRemoved += (NetworkEventHandler)DispatchService.GuiDispatch(new NetworkEventHandler(DeleteNetwork));
  			
  			foreach (Network network in Runtime.Core.Networks) {
  				AddNetwork(network);
  			}
  			
  			base.ButtonPressEvent += base_ButtonPressEvent;
  			base.ButtonReleaseEvent += base_ButtonReleaseEvent;
  			base.MotionNotifyEvent += base_MotionNotifyEvent;
  			
  			this.render = DrawContents;
  			this.overlay = DrawOverlay;
  			this.QueueDraw();
  		}
  
  		#region network add/delete/update
  		private void AddNetwork (Network network)
  		{
  			lock (this) {
  				
  				if (debug)
  					Console.Out.WriteLine("*** Adding network {0}.", network.NetworkName);
  				
  				PointD position = new PointD(rng.Next(200), rng.Next(200));
  				
  				Hashtable nodegroups = Hashtable.Synchronized(new Hashtable());
  				Hashtable node2nodegroup = Hashtable.Synchronized(new Hashtable());
  				network.Properties["nodegroups"] = nodegroups;
  				network.Properties["node2nodegroup"] = node2nodegroup;
  				network.Properties["rect"] = new Rectangle(position.X, position.Y, 0, 0);
  				networkZOrder.Add(network);
  				
  				foreach (Node node in network.Nodes.Values)
  					AddNode(node, network);
  				
  				network.NewIncomingConnection += delegate(Network eNetwork, LocalNodeConnection connection) { Gtk.Application.Invoke(delegate { this.QueueDraw(); }); };
  				
  				network.ConnectingTo += delegate(Network eNetwork, LocalNodeConnection connection) { Gtk.Application.Invoke(delegate { this.QueueDraw(); }); };
  				
  				network.UserOnline += (NodeOnlineOfflineEventHandler)DispatchService.GuiDispatch(new NodeOnlineOfflineEventHandler(network_UserOnline));
  				network.UserOffline += (NodeOnlineOfflineEventHandler)DispatchService.GuiDispatch(new NodeOnlineOfflineEventHandler(network_UserOffline));
  				network.UpdateNodeInfo += (UpdateNodeInfoEventHandler)DispatchService.GuiDispatch(new UpdateNodeInfoEventHandler(network_UpdateNodeInfo));
  				
  				network.ConnectionUp += delegate(INodeConnection connection) { Gtk.Application.Invoke(delegate { this.QueueDraw(); }); };
  				
  				network.ConnectionDown += delegate(INodeConnection connection) { Gtk.Application.Invoke(delegate { this.QueueDraw(); }); };
  				
  				network.CleanupFinished += delegate(object sender, EventArgs e) { Gtk.Application.Invoke(delegate { this.QueueDraw(); }); };
  			}
  			
  			this.QueueDraw();
  		}
  
  		private void network_UserOnline (Network eNetwork, Node node)
  		{
  			AddNode(node, eNetwork);
  			this.QueueDraw();
  		}
  
  		private void network_UserOffline (Network eNetwork, Node node)
  		{
  			DeleteNode(node, eNetwork);
  			this.QueueDraw();
  		}
  
  		private void network_UpdateNodeInfo (Network eNetwork, string oldNick, Node node)
  		{
  			UpdateNode(node, eNetwork);
  			this.QueueDraw();
  		}
  
  		private void DeleteNetwork (Network network)
  		{
  			lock (this) {
  				if (debug)
  					Console.Out.WriteLine("*** Deleting network {0}.", network.NetworkName);
  				
  				foreach (Node node in network.Nodes.Values)
  					DeleteNode(node, network);
  				
  				network.Properties.Remove("nodegroups");
  				network.Properties.Remove("node2nodegroup");
  				network.Properties.Remove("rect");
  				networkZOrder.Remove(network);
  			}
  			
  			this.QueueDraw();
  		}
  		#endregion
  
  		#region add/delete/update node
  		private void AddNode (Node node, Network network)
  		{
  			Rectangle networkRect = (Rectangle)network.Properties["rect"];
  			AddNode(node, network, new PointD(networkRect.X + rng.Next(75), networkRect.Y + rng.Next(75)));
  		}
  
  		private void AddNode (Node node, Network network, PointD position)
  		{
  			lock (network) {
  				if (debug)
  					Console.Out.WriteLine("*** Adding node {0} to network {1}...", node.NickName, network.NetworkName);
  				Hashtable nodegroups = network.Properties["nodegroups"] as Hashtable;
  				Hashtable node2nodegroup = network.Properties["node2nodegroup"] as Hashtable;
  				
  				IPAddress address = GetExternalIPv4Address(node);
  				string nodeIP = (address.Equals(IPAddress.None)) ? node.NickName : address.ToString();
  				
  				if (nodegroups.ContainsKey(nodeIP)) {
  					NodeGroup ng = nodegroups[nodeIP] as NodeGroup;
  					ng.Nodes.Add(node);
  					node2nodegroup.Add(node, ng);
  					if (debug)
  						Console.Out.WriteLine(" -> Added node {0} to network {1} / nodegroup {2}", node.NickName, network.NetworkName, ng.Name);
  				} else {
  					NodeGroup ng = new NodeGroup(nodeIP, position);
  					ng.Nodes.Add(node);
  					node2nodegroup.Add(node, ng);
  					nodegroups.Add(nodeIP, ng);
  					if (debug)
  						Console.Out.WriteLine(" -> Added node {0} to network {1} / new nodegroup {2}", node.NickName, network.NetworkName, ng.Name);
  				}
  			}
  		}
  
  		private void DeleteNode (Node node, Network network)
  		{
  			lock (network) {
  				if (debug)
  					Console.Out.WriteLine("*** Deleting node {0} from network {1}.", node.NickName, network.NetworkName);
  				Hashtable nodegroups = network.Properties["nodegroups"] as Hashtable;
  				Hashtable node2nodegroup = network.Properties["node2nodegroup"] as Hashtable;
  				//string nodeIP = (node.ExternalIP == string.Empty) ? node.NickName : node.ExternalIP;
  				
  				if (!node2nodegroup.ContainsKey(node))
  					throw new Exception("Unable to delete node: could not find the nodegroup which contains the specified node: " + node.NickName);
  				
  				NodeGroup ng = node2nodegroup[node] as NodeGroup;
  				if (debug)
  					Console.Out.WriteLine(" -> Found node {0} in nodegroup {1} / network {2}.", node.NickName, ng.Name, network.NetworkName);
  				ng.Nodes.Remove(node);
  				node2nodegroup.Remove(node);
  				if (debug)
  					Console.Out.WriteLine(" -> Deleted node {0} in nodegroup {1} / network {2}.", node.NickName, ng.Name, network.NetworkName);
  				
  				// If nodegroup is empty, delete that too.
  				if (ng.Nodes.Count == 0) {
  					if (debug)
  						Console.Out.WriteLine(" -> Nodegroup {0} / network {1} empty, deleting.", ng.Name, network.NetworkName);
  					nodegroups.Remove(ng.Name);
  					if (debug)
  						Console.Out.WriteLine(" -> Nodegroup {0} / network {1} deleted.", ng.Name, network.NetworkName);
  				}
  			}
  		}
  
  		private void UpdateNode (Node node, Network network)
  		{
  			try {
  				lock (network) {
  					if (debug)
  						Console.Out.WriteLine("*** Updating node {0} from network {1}.", node.NickName, network.NetworkName);
  					Hashtable node2nodegroup = network.Properties["node2nodegroup"] as Hashtable;
  					
  					if (!node2nodegroup.ContainsKey(node))
  						throw new Exception("Unable to modify node: could not find the nodegroup which contains the specified node: " + node.NickName);
  					
  					NodeGroup ng = node2nodegroup[node] as NodeGroup;
  					PointD position = ng.Position;
  					DeleteNode(node, network);
  					AddNode(node, network, position);
  					if (debug)
  						Console.Out.WriteLine("### Node update complete for node {0} from network {1}.", node.NickName, network.NetworkName);
  				}
  			} catch (Exception ex) {
  				OnError(ex);
  			}
  		}
  		#endregion
  
  		#region mouse/motion event handling
  		private void base_ButtonPressEvent (object sender, Gtk.ButtonPressEventArgs args)
  		{
  			Gdk.EventButton eb = args.Event;
  			double mouseX = eb.X;
  			double mouseY = eb.Y;
  			DeviceToUser(ref mouseX, ref mouseY);
  			if (debug)
  				Console.Out.WriteLine("Button Pressed {0}: user coords {1},{2}", eb.Button, mouseX, mouseY);
  			PointD thePoint = new PointD(mouseX, mouseY);
  			NodeGroup firstNodeGroupHit = null;
  			Node firstNodeHit = null;
  			
  			foreach (Network network in networkZOrder) {
  				Rectangle networkRect = (Rectangle)network.Properties["rect"];
  				if (Inside(thePoint, networkRect)) {
  					if (debug)
  						Console.Out.WriteLine("Click inside network {0}", network.NetworkName);
  					Hashtable nodegroups = (Hashtable)network.Properties["nodegroups"];
  					foreach (NodeGroup ng in nodegroups.Values) {
  						//PointD ngPosition = new PointD (ng.Position.X + networkRect.X,
  						//                              ng.Position.Y + networkRect.Y);
  						if (Inside(thePoint, ng.Position, ng.Dimension)) {
  							if (debug)
  								Console.Out.WriteLine("Click inside group {0}", ng.Name);
  							firstNodeGroupHit = ng;
  							foreach (Node n in ng.Nodes) {
  								PointD np = (PointD)n.Properties["position"];
  								//	np = new PointD (np.X + ngPosition.X, 
  								//	                 np.Y + ngPosition.Y);
  								if (Inside(thePoint, np, AvatarSizeD)) {
  									firstNodeHit = n;
  									if (debug)
  										Console.Out.WriteLine("Click inside node {0}", n);
  									break;
  									// clicked node n
  								}
  							}
  							break;
  							// click in ng but not any node
  						}
  						
  					}
  					break;
  					// click in network but not any ng
  				}
  			}
  			
  			if (eb.Button == 1 | eb.Button == 3) {
  				selectedNode = firstNodeHit;
  				
  				if (SelectedNodeChanged != null)
  					SelectedNodeChanged(selectedNode);
  			}
  			
  			switch (eb.Button) {
  			case 1:
  				selectedGroup = firstNodeGroupHit;
  				
  				if (selectedGroup != null) {
  					this.nodeDragging = true;
  					nodeDraggingLastX = eb.X;
  					nodeDraggingLastY = eb.Y;
  					
  					// we're dragging...
  					this.GdkWindow.Cursor = new Gdk.Cursor(Gdk.CursorType.Plus);
  				}
  				
  				if (selectedNode != null) {
  					if (eb.Type == Gdk.EventType.TwoButtonPress) {
  						ResetDrag();
  						if (NodeDoubleClicked != null)
  							NodeDoubleClicked(selectedNode);
  					}
  				}
  				
  				break;
  			
  			case 3:
  				if (firstNodeGroupHit == null) {
  					// clicked on map background
  					Menu mapMenu = (Menu)Runtime.UIManager.GetWidget("/MapPopupMenu");
  					mapMenu.Popup();
  					
  				} else {
  					if (firstNodeHit != null) {
  						UserMenu userMenu = new UserMenu(firstNodeHit.Network, firstNodeHit);
  						userMenu.Popup();
  					}
  				}
  				break;
  			}
  		}
  
  		private void base_ButtonReleaseEvent (object sender, Gtk.ButtonReleaseEventArgs args)
  		{
  			ResetDrag();
  		}
  
  		private void ResetDrag ()
  		{
  			this.nodeDragging = false;
  			nodeDraggingLastX = 0;
  			nodeDraggingLastY = 0;
  			this.GdkWindow.Cursor = new Gdk.Cursor(Gdk.CursorType.LeftPtr);
  		}
  
  		private void base_MotionNotifyEvent (object sender, Gtk.MotionNotifyEventArgs args)
  		{
  			Gdk.EventMotion em = args.Event;
  			if (debug)
  				Console.Out.WriteLine("Motion event: nodeDragging {0}, user coords {1},{2}", nodeDragging, em.X, em.Y);
  			double mouseX = em.X;
  			double mouseY = em.Y;
  			DeviceToUser(ref mouseX, ref mouseY);
  			PointD thePoint = new PointD(mouseX, mouseY);
  			hoverNode = null;
  			foreach (Network network in networkZOrder) {
  				foreach (Node n in network.Nodes.Values) {
  					if (n.Properties.ContainsKey("position")) {
  						PointD np = (PointD)n.Properties["position"];
  						if (Inside(thePoint, np, AvatarSizeD)) {
  							hoverNode = n;
  							break;
  						}
  					}
  				}
  			}
  			
  			//if (debug) Console.Out.WriteLine ("MotionNotifyEventHandler called!");
  			if (!nodeDragging) {
  				if (debug)
  					Console.Out.WriteLine("not nodedragging motion!");
  				this.QueueDraw();
  				return;
  			}
  			
  			mouseX = em.X;
  			mouseY = em.Y;
  			
  			double deltaX, deltaY;
  			deltaX = mouseX - nodeDraggingLastX;
  			deltaY = mouseY - nodeDraggingLastY;
  			PointD pos = selectedGroup.Position;
  			pos.X += (deltaX / this.ScaleFactor);
  			pos.Y += (deltaY / this.ScaleFactor);
  			nodeDraggingLastX = em.X;
  			nodeDraggingLastY = em.Y;
  			selectedGroup.Position = pos;
  			this.QueueDraw();
  		}
  
  		#endregion
  
  		private bool Inside (PointD thePoint, Rectangle rect)
  		{
  			bool tl = thePoint.X >= rect.X && thePoint.Y >= rect.Y;
  			bool br = thePoint.X < rect.X + rect.Width && thePoint.Y < rect.Y + rect.Height;
  			return tl && br;
  		}
  
  		private bool Inside (PointD thePoint, PointD rectOrigin, SizeD rectSize)
  		{
  			bool tl = thePoint.X >= rectOrigin.X && thePoint.Y >= rectOrigin.Y;
  			bool br = thePoint.X < rectOrigin.X + rectSize.Width && thePoint.Y < rectOrigin.Y + rectSize.Height;
  			return tl && br;
  		}
  
  		private SizeD CalculateNodeGroupTitleTextSize (NodeGroup ng, Cairo.Context gc)
  		{
  			Pango.Layout layout = new Pango.Layout(this.PangoContext);
  			layout.FontDescription = this.PangoContext.FontDescription.Copy();
  			layout.FontDescription.Size = Pango.Units.FromDouble(NodegroupNameFontSize);
  			layout.SetText(ng.Name);
  			
  			Gdk.Size size = GetTextSize(layout);
  			return new SizeD(size.Width, size.Height);
  		}
  
  		private SizeD CalculateNodeGroupSize (NodeGroup ng, Cairo.Context gc)
  		{
  			var titleTextSize = CalculateNodeGroupTitleTextSize(ng, gc);
  			var titleTextHeight = titleTextSize.Height + (Padding * 2.0);
  			var heightNodeCount = (ng.Nodes.Count < 3) ? 1 : ng.Nodes.Count;
  			
  			var width = Math.Max(titleTextSize.Width + (Padding * 2.0), 
  			                     (ng.Nodes.Count * AvatarDimension) + (Padding * 2.0) + (Padding * ng.Nodes.Count));
  			var height = (heightNodeCount * AvatarDimension) + titleTextHeight + (Padding * 2.0) + (Padding * heightNodeCount);
  			return new SizeD(width, height);
  		}
  
  		private void RenderNodeGroup (NodeGroup ng, Network network, Cairo.Context gc)
  		{
  			gc.Save();
  			
  			SizeD size = CalculateNodeGroupSize(ng, gc);
  			ng.Dimension = size;
  			CreateRoundedRectPath(gc, ng.Position.X, ng.Position.Y, size.Width, size.Height, 20);
  			gc.Color = new Cairo.Color(0, 0, 0, 0.5);
  			gc.FillPreserve();
  			
  			if (selectedGroup == ng) {
  				gc.Save();
  				gc.Color = orangeOverlay;
  				gc.StrokePreserve();
  				gc.Restore();
  			}
  			
  
gt: 			var titleTextSize = CalculateNodeGroupTitleTextSize(ng, gc);