/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements.  See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.  The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License.  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied.  See the License for the
specific language governing permissions and limitations
under the License.
*/
/**
 * @module QDR
 */
var QDR = (function (QDR) {


	QDR.module.controller('QDR.TopologyFormController', function ($scope, QDRService) {

		$scope.attributes = []
        var nameTemplate = '<div title="{{row.entity.description}}" class="ngCellText"><span>{{row.entity.attributeName}}</span></div>';
        var valueTemplate = '<div title="{{row.entity.attributeValue}}" class="ngCellText"><span>{{row.entity.attributeValue}}</span></div>';
        $scope.topoGridOptions = {
            data: 'attributes',
			enableColumnResize: true,
			multiSelect: false,
            columnDefs: [
            {
                field: 'attributeName',
                displayName: 'Attribute',
                cellTemplate: nameTemplate
            },
            {
                field: 'attributeValue',
                displayName: 'Value',
				cellTemplate: valueTemplate
            }
            ]
        };
		$scope.form = ''
		$scope.$on('showEntityForm', function (event, args) {
			var attributes = args.attributes;
			var entityTypes = QDRService.schema.entityTypes[args.entity].attributes;
			attributes.forEach( function (attr) {
				if (entityTypes[attr.attributeName] && entityTypes[attr.attributeName].description)
					attr.description = entityTypes[attr.attributeName].description
			})
			$scope.attributes = attributes;
			$scope.form = args.entity;
		})
		$scope.$on('showAddForm', function (event) {
			$scope.form = 'add';
		})
	})


  /**
   * @method TopologyController
   *
   * Controller that handles the QDR topology page
   */
    QDR.module.controller("QDR.TopologyController", ['$scope', '$rootScope', 'QDRService', '$location', '$timeout', '$dialog',
    function($scope, $rootScope, QDRService, $location, $timeout, $dialog) {

        $scope.multiData = []
        $scope.selectedClient = [];
        $scope.quiesceState = {}
        var dontHide = false;
		$scope.quiesceConnection = function (row) {
			var entity = row.entity;
			var state = $scope.quiesceState[entity.connectionId].state;
			if (state === 'enabled') {
				// start quiescing all links
				$scope.quiesceState[entity.connectionId].state = 'quiescing';
			} else if (state === 'quiesced') {
				// start reviving all links
				$scope.quiesceState[entity.connectionId].state = 'reviving';
			}
			$scope.multiDetails.updateState(entity);
			dontHide = true;
			$scope.multiDetails.selectRow(row.rowIndex, true);
			$scope.multiDetails.showLinksList(row)
		}
		$scope.quiesceDisabled = function (row) {
			return $scope.quiesceState[row.entity.connectionId].buttonDisabled;
		}
		$scope.quiesceText = function (row) {
			return $scope.quiesceState[row.entity.connectionId].buttonText;
		}
		$scope.quiesceClass = function (row) {
			var stateClassMap = {
				enabled: 'btn-primary',
				quiescing: 'btn-warning',
				reviving: 'btn-warning',
				quiesced: 'btn-danger'
			}
			return stateClassMap[$scope.quiesceState[row.entity.connectionId].state];
		}
        $scope.multiDetails = {
            data: 'multiData',
			selectedItems: $scope.selectedClient,
			multiSelect: false,
			afterSelectionChange: function (obj) {
				if (obj.selected && obj.orig) {
					var detailsDiv = d3.select('#link_details')
					var isVis = detailsDiv.style('display') === 'block';
					if (!dontHide && isVis && $scope.connectionId === obj.entity.connectionId) {
						hideLinkDetails();
						return;
					}
					dontHide = false;
					$scope.multiDetails.showLinksList(obj)
				}
			},
			showLinksList: function (obj) {
				$scope.linkData = obj.entity.linkData;
				$scope.connectionId = obj.entity.connectionId;
				var visibleLen = Math.min(obj.entity.linkData.length, 10)
				var left = parseInt(d3.select('#multiple_details').style("left"))
				var detailsDiv = d3.select('#link_details')
	            detailsDiv
	                .style({
	                    display: 'block',
	                    opacity: 1,
	                    left: (left + 20) + "px",
	                    top:  (mouseY + 20 + $(document).scrollTop()) + "px",
	                    height: ((visibleLen + 1) * 30) + 40 + "px", // +1 for the header row
	                    'overflow-y': obj.entity.linkData > 10 ? 'scroll' : 'hidden'})
			},
			updateState: function (entity) {
				var state = $scope.quiesceState[entity.connectionId].state

				// count enabled and disabled links for this connection
				var enabled = 0, disabled = 0;
				entity.linkData.forEach ( function (link) {
					if (link.adminStatus === 'enabled')
						++enabled;
					if (link.adminStatus === 'disabled')
						++disabled;
				})

				var linkCount = entity.linkData.length;
				// if state is quiescing and any links are enabled, button should say 'Quiescing' and be disabled
				if (state === 'quiescing' && (enabled > 0)) {
					$scope.quiesceState[entity.connectionId].buttonText = 'Quiescing';
					$scope.quiesceState[entity.connectionId].buttonDisabled = true;
				} else
				// if state is enabled and all links are disabled, button should say Revive and be enabled. set state to quisced
				// if state is quiescing and all links are disabled, button should say 'Revive' and be enabled. set state to quiesced
				if ((state === 'quiescing' || state === 'enabled') && (disabled === linkCount)) {
					$scope.quiesceState[entity.connectionId].buttonText = 'Revive';
					$scope.quiesceState[entity.connectionId].buttonDisabled = false;
					$scope.quiesceState[entity.connectionId].state = 'quiesced'
				} else
				// if state is reviving and any links are disabled, button should say 'Reviving' and be disabled
				if (state === 'reviving' && (disabled > 0)) {
					$scope.quiesceState[entity.connectionId].buttonText = 'Reviving';
					$scope.quiesceState[entity.connectionId].buttonDisabled = true;
				} else
				// if state is reviving or quiesced and all links are enabled, button should say 'Quiesce' and be enabled. set state to enabled
				if ((state === 'reviving' || state === 'quiesced') && (enabled === linkCount)) {
					$scope.quiesceState[entity.connectionId].buttonText = 'Quiesce';
					$scope.quiesceState[entity.connectionId].buttonDisabled = false;
					$scope.quiesceState[entity.connectionId].state = 'enabled'
				}
			},
            columnDefs: [
            {
                field: 'host',
                cellTemplate: "titleCellTemplate.html",
				headerCellTemplate: 'titleHeaderCellTemplate.html',
                displayName: 'Connection host'
            },
            {
                field: 'user',
                cellTemplate: "titleCellTemplate.html",
				headerCellTemplate: 'titleHeaderCellTemplate.html',
                displayName: 'User'
            },
			{
				field: 'properties',
                cellTemplate: "titleCellTemplate.html",
				headerCellTemplate: 'titleHeaderCellTemplate.html',
				displayName: 'Properties'
			}/*,
			{
				cellClass: 'gridCellButton',
				cellTemplate: '<button title="{{quiesceText(row)}} the links" type="button" ng-class="quiesceClass(row)" class="btn" ng-click="$event.stopPropagation();quiesceConnection(row)" ng-disabled="quiesceDisabled(row)">{{quiesceText(row)}}</button>'
			}*/
            ]
        };
		$scope.linkData = [];
		$scope.quiesceLinkClass = function (row) {
			var stateClassMap = {
				enabled: 'btn-primary',
				disabled: 'btn-danger'
			}
			return stateClassMap[row.entity.adminStatus]
		}
		$scope.quiesceLink = function (row) {
			QDRService.quiesceLink(row.entity.nodeId, row.entity.name);
		}
		$scope.quiesceLinkDisabled = function (row) {
			return (row.entity.operStatus !== 'up' && row.entity.operStatus !== 'down')
		}
		$scope.quiesceLinkText = function (row) {
			return row.entity.operStatus === 'down' ? "Revive" : "Quiesce";
		}
		$scope.linkDetails = {
			data: 'linkData',
            columnDefs: [
			{
				field: 'adminStatus',
                cellTemplate: "titleCellTemplate.html",
				headerCellTemplate: 'titleHeaderCellTemplate.html',
				displayName: 'Admin state'
			},
			{
				field: 'operStatus',
                cellTemplate: "titleCellTemplate.html",
				headerCellTemplate: 'titleHeaderCellTemplate.html',
				displayName: 'Oper state'
			},
			{
				field: 'dir',
                cellTemplate: "titleCellTemplate.html",
				headerCellTemplate: 'titleHeaderCellTemplate.html',
				displayName: 'dir'
			},
			{
				field: 'owningAddr',
                cellTemplate: "titleCellTemplate.html",
				headerCellTemplate: 'titleHeaderCellTemplate.html',
				displayName: 'Address'
			},
			{
				field: 'deliveryCount',
				displayName: 'Delivered',
				headerCellTemplate: 'titleHeaderCellTemplate.html',
				cellClass: 'grid-values'

			},
			{
				field: 'uncounts',
				displayName: 'Outstanding',
				headerCellTemplate: 'titleHeaderCellTemplate.html',
				cellClass: 'grid-values'
			}/*,
			{
				cellClass: 'gridCellButton',
				cellTemplate: '<button title="{{quiesceLinkText(row)}} this link" type="button" ng-class="quiesceLinkClass(row)" class="btn" ng-click="quiesceLink(row)" ng-disabled="quiesceLinkDisabled(row)">{{quiesceLinkText(row)}}</button>'
			}*/
			]
		}

		if (!QDRService.connected) {
			// we are not connected. we probably got here from a bookmark or manual page reload
			QDRService.redirectWhenConnected("topology");
			return;
		}
		// we are currently connected. setup a handler to get notified if we are ever disconnected
		QDRService.addDisconnectAction( function () {
			QDRService.redirectWhenConnected("topology");
			$scope.$apply();
		})

		var urlPrefix = $location.absUrl();
		urlPrefix = urlPrefix.split("#")[0]
		QDR.log.debug("started QDR.TopologyController with urlPrefix: " + urlPrefix);

		$scope.addingNode = {
			step: 0,
			hasLink: false,
			trigger: ''
		};

        $scope.cancel = function () {
            $scope.addingNode.step = 0;
        }
		$scope.editNewRouter = function () {
			$scope.addingNode.trigger = 'editNode';
		}

		var NewRouterName = "__NEW__";
	    // mouse event vars
	    var selected_node = null,
	        selected_link = null,
	        mousedown_link = null,
	        mousedown_node = null,
	        mouseup_node = null,
	        initial_mouse_down_position = null;

        $scope.schema = "Not connected";

	    $scope.modes = [
	    	{title: 'Topology view', name: 'Diagram', right: false},
	    	/* {title: 'Add a new router node', name: 'Add Router', right: true} */
	    	];
		$scope.mode = "Diagram";
		$scope.contextNode = null; // node that is associated with the current context menu

		$scope.isModeActive = function (name) {
			if ((name == 'Add Router' || name == 'Diagram') && $scope.addingNode.step > 0)
				return true;
			return ($scope.mode == name);
		}
		$scope.selectMode = function (name) {
			if (name == "Add Router") {
				name = 'Diagram';
				if ($scope.addingNode.step > 0) {
					$scope.addingNode.step = 0;
				} else {
					// start adding node mode
					$scope.addingNode.step = 1;
				}
			} else {
				$scope.addingNode.step = 0;
			}

			$scope.mode = name;
		}
		$scope.$watch(function () {return $scope.addingNode.step}, function (newValue, oldValue) {
			if (newValue == 0 && oldValue != 0) {
				// we are cancelling the add

				// find the New node
				nodes.every(function (n, i) {
					// for the placeholder node, the key will be __internal__
					if (QDRService.nameFromId(n.key) == '__internal__') {
						var newLinks = links.filter(function (e, i) {
							return e.source.id == n.id || e.target.id == n.id;
						})
						// newLinks is an array of links to remove
						newLinks.map(function (e) {
							links.splice(links.indexOf(e), 1);
						})
						// i is the index of the node to remove
						nodes.splice(i, 1);
						force.nodes(nodes).links(links).start();
		                restart(false);
						return false; // stop looping
					}
					return true;
				})
				updateForm(Object.keys(QDRService.topology.nodeInfo())[0], 'router', 0);

			} else if (newValue > 0) {
				// we are starting the add mode
				$scope.$broadcast('showAddForm')

				resetMouseVars();
                selected_node = null;
                selected_link = null;
                // add a new node
                var id = "amqp:/_topo/0/__internal__/$management";
                var x = radiusNormal * 4;
                var y = x;;
                if (newValue > 1) {   // add at current mouse position
                    var offset = jQuery('#topology').offset();
                    x = mouseX - offset.left + $(document).scrollLeft();
                    y = mouseY - offset.top + $(document).scrollTop();;
                }
                NewRouterName = genNewName();
                nodes.push( aNode(id, NewRouterName, "inter-router", undefined, nodes.length, x, y, undefined, true) );
                force.nodes(nodes).links(links).start();
                restart(false);
			}

		})
		$scope.isRight = function (mode) {
			return mode.right;
		}

		// for ng-grid that shows details for multiple consoles/clients
		// generate unique name for router and containerName
		var genNewName = function () {
			var nodeInfo = QDRService.topology.nodeInfo();
			var nameIndex = 1;
			var newName = "R." + nameIndex;

			var names = [];
			for (key in nodeInfo) {
				var node = nodeInfo[key];
				var router = node['.router'];
				var attrNames = router.attributeNames;
				var name = QDRService.valFor(attrNames, router.results[0], 'routerId')
				if (!name)
					name = QDRService.valFor(attrNames, router.results[0], 'name')
				names.push(name);
			}

			while (names.indexOf(newName) >= 0) {
				newName = "R." + nameIndex++;
			}
			return newName;
		}

		$scope.$watch(function () {return $scope.addingNode.trigger}, function (newValue, oldValue) {
			if (newValue == 'editNode') {
				$scope.addingNode.trigger = "";
				editNode();
			}
		})

	    function editNode() {
	        doAddDialog(NewRouterName);
	    };
		$scope.reverseLink = function () {
			if (!mousedown_link)
				return;
			var d = mousedown_link;
			var tmp = d.left;
			d.left = d.right;;
			d.right = tmp;
		    restart(false);
		    tick();
		}
		$scope.removeLink = function () {
			if (!mousedown_link)
				return;
			var d = mousedown_link;
			 links.every( function (l, i) {
				if (l.source.id == d.source.id && l.target.id == d.target.id) {
			        links.splice(i, 1);
					force.links(links).start();
					return false; // exit the 'every' loop
				}
				return true;
			});
		    restart(false);
		    tick();
		}
		$scope.setFixed = function (b) {
			if ($scope.contextNode) {
				$scope.contextNode.fixed = b;
			}
			restart();
		}
		$scope.isFixed = function () {
			if (!$scope.contextNode)
				return false;
			return ($scope.contextNode.fixed & 0b1);
		}

		var mouseX, mouseY;
		// event handlers for popup context menu
		$(document).mousemove(function (e) {
		    mouseX = e.clientX;
		    mouseY = e.clientY;
		});
		$(document).mousemove();
		$(document).click(function (e) {
			$scope.contextNode = null;
            $(".contextMenu").fadeOut(200);
        });

		// set up SVG for D3
	    var colors = {'inter-router': "#EAEAEA", 'normal': "#F0F000", 'on-demand': '#00F000'};
	    var radii = {'inter-router': 25, 'normal': 15, 'on-demand': 15};
	    var radius = 25;
	    var radiusNormal = 15;
	    var svg, lsvg;
		var force;
		var animate = false; // should the force graph organize itself when it is displayed
		var path, circle;
		var savedKeys = {};
		var dblckickPos = [0,0];
		var width = 0;
		var height = 0;

		var getSizes = function () {
			var legendWidth = 196;
		    var gap = 5;
			var width = $('#topology').width() - gap - legendWidth;
		    var top = $('#topology').offset().top
			var tpformHeight = $('#topologyForm').height()
		    var height = Math.max(window.innerHeight, tpformHeight + top) - top - gap;
			if (width < 10) {
				QDR.log.info("page width and height are abnormal w:" + width + " height:" + height)
				return [0,0];
			}
			return [width, height]
		}
		var resize = function () {
			var sizes = getSizes();
			width = sizes[0]
			height = sizes[1]
			if (width > 0) {
			    // set attrs and 'resume' force
			    svg.attr('width', width);
			    svg.attr('height', height);
			    force.size(sizes).resume();
			}
		}
		window.addEventListener('resize', resize);
		var sizes = getSizes()
		width = sizes[0]
		height = sizes[1]
		if (width <= 0 || height <= 0)
			return

	    // set up initial nodes and links
	    //  - nodes are known by 'id', not by index in array.
	    //  - selected edges are indicated on the node (as a bold red circle).
	    //  - links are always source < target; edge directions are set by 'left' and 'right'.
		var nodes = [];
		var links = [];

		var aNode = function (id, name, nodeType, nodeInfo, nodeIndex, x, y, resultIndex, fixed, properties) {
			properties = properties || {};
			var routerId;
			if (nodeInfo) {
				var node = nodeInfo[id];
				if (node) {
					var router = node['.router'];
					routerId = QDRService.valFor(router.attributeNames, router.results[0], 'id')
					if (!routerId)
						routerId = QDRService.valFor(router.attributeNames, router.results[0], 'routerId')
				}
			}
			return {   key: id,
				name: name,
				nodeType: nodeType,
				properties: properties,
				routerId: routerId,
				x: x,
				y: y,
				id: nodeIndex,
				resultIndex: resultIndex,
				fixed: fixed,
				cls: name == NewRouterName ? 'temp' : ''
			};
		};


        var initForm = function (attributes, results, entityType, formFields) {
        
            while(formFields.length > 0) {
                // remove all existing attributes
                    formFields.pop();
            }

            for (var i=0; i<attributes.length; ++i) {
                var name = attributes[i];
                var val = results[i];
                var desc = "";
                if (entityType.attributes[name])
                    if (entityType.attributes[name].description)
                        desc = entityType.attributes[name].description;

                formFields.push({'attributeName': name, 'attributeValue': val, 'description': desc});
            }
        }

		// initialize the nodes and links array from the QDRService.topology._nodeInfo object
		var initForceGraph = function () {
			nodes = [];
			links = [];

			svg = d3.select('#topology')
				.append('svg')
				.attr("id", "SVG_ID")
				.attr('width', width)
				.attr('height', height)
	            .on("contextmenu", function(d) {
	                if (d3.event.defaultPrevented)
	                    return;
                    d3.event.preventDefault();
					if ($scope.addingNode.step != 0)
						return;
					if (d3.select('#svg_context_menu').style('display') !== 'block')
	                    $(document).click();
                    d3.select('#svg_context_menu')
                      .style('left', (mouseX + $(document).scrollLeft()) + "px")
                      .style('top', (mouseY + $(document).scrollTop()) + "px")
                      .style('display', 'block');
                })
                .on('click', function (d) {
                    removeCrosssection()
                });

                $(document).keyup(function(e) {
                  if (e.keyCode === 27) {
                    removeCrosssection()
                  }
                });

			// the legend
			lsvg = d3.select("#svg_legend")
			 	.append('svg')
				.attr('id', 'svglegend')
			lsvg = lsvg.append('svg:g')
				.attr('transform', 'translate('+(radii['inter-router']+2)+','+(radii['inter-router']+2)+')')
				.selectAll('g');

			// mouse event vars
			selected_node = null;
			selected_link = null;
			mousedown_link = null;
			mousedown_node = null;
			mouseup_node = null;

			// initialize the list of nodes
			var yInit = 10;
			var nodeInfo = QDRService.topology.nodeInfo();
			var nodeCount = Object.keys(nodeInfo).length;
			for (var id in nodeInfo) {
				var name = QDRService.nameFromId(id);
                // if we have any new nodes, animate the force graph to position them
				var position = angular.fromJson(localStorage[name]);
				if (!angular.isDefined(position)) {
				    animate = true;
				    position = {x: width / 4 + ((width / 2)/nodeCount) * nodes.length,
                				y: 200 + yInit,
                				fixed: false};
				}
				if (position.y > height)
					position.y = 200 - yInit;
				nodes.push( aNode(id, name, "inter-router", nodeInfo, nodes.length, position.x, position.y, undefined, position.fixed) );
				yInit *= -1;
				//QDR.log.debug("adding node " + nodes.length-1);
			}

			// initialize the list of links
			var source = 0;
			var client = 1;
			for (var id in nodeInfo) {
				var onode = nodeInfo[id];
				var conns = onode['.connection'].results;
				var attrs = onode['.connection'].attributeNames;
				var parent = getNodeIndex(QDRService.nameFromId(id));
				//QDR.log.debug("external client parent is " + parent);
				var normalsParent = {console: undefined, client: undefined}; // 1st normal node for this parent

				for (var j = 0; j < conns.length; j++) {
                    var role = QDRService.valFor(attrs, conns[j], "role");
                    var properties = QDRService.valFor(attrs, conns[j], "properties") || {};
                    var dir = QDRService.valFor(attrs, conns[j], "dir");
					if (role == "inter-router") {
						var connId = QDRService.valFor(attrs, conns[j], "container");
						var target = getContainerIndex(connId);
						if (target >= 0)
							getLink(source, target, dir);
					} else if (role == "normal" || role == "on-demand") {
						// not a router, but an external client
						//QDR.log.debug("found an external client for " + id);
						var name = QDRService.nameFromId(id) + "." + client;
						//QDR.log.debug("external client name is  " + name + " and the role is " + role);

                        // if we have any new clients, animate the force graph to position them
                        var position = angular.fromJson(localStorage[name]);
                        if (!angular.isDefined(position)) {
                            animate = true;
                            position = {x: nodes[parent].x + 40 + Math.sin(Math.PI/2 * client),
                                        y: nodes[parent].y + 40 + Math.cos(Math.PI/2 * client),
                                        fixed: false};
                        }
						if (position.y > height)
							position.y = nodes[parent].y + 40 + Math.cos(Math.PI/2 * client)
						var node = aNode(id, name, role, nodeInfo, nodes.length, position.x, position.y, j, position.fixed, properties)
						var nodeType = QDRService.isAConsole(properties, QDRService.valFor(attrs, conns[j], "identity"), role, node.key)

						if (role === 'normal') {
							node.user = QDRService.valFor(attrs, conns[j], "user")
							node.isEncrypted = QDRService.valFor(attrs, conns[j], "isEncrypted")
							node.host = QDRService.valFor(attrs, conns[j], "host")
							node.connectionId = QDRService.valFor(attrs, conns[j], "identity")

							if (!normalsParent[nodeType]) {
								normalsParent[nodeType] = node;
								nodes.push(	node );
								node.normals = [node];
								// now add a link
								getLink(parent, nodes.length-1, dir);
								client++;
							} else {
								normalsParent[nodeType].normals.push(node)
							}
						} else {
							nodes.push( node)
							// now add a link
							getLink(parent, nodes.length-1, dir);
							client++;
						}
					}
				}
				source++;
			}

            $scope.schema = QDRService.schema;
			// init D3 force layout
			force = d3.layout.force()
				.nodes(nodes)
				.links(links)
				.size([width, height])
				.linkDistance(function(d) { return d.target.nodeType === 'inter-router' ? 150 : 65 })
				.charge(-1800)
				.friction(.10)
				.gravity(0.0001)
				.on('tick', tick)
				.start()

			svg.append("svg:defs").selectAll('marker')
				.data(["end-arrow", "end-arrow-selected"])      // Different link/path types can be defined here
				.enter().append("svg:marker")    // This section adds in the arrows
				.attr("id", String)
				.attr("viewBox", "0 -5 10 10")
				//.attr("refX", 25)
				.attr("markerWidth", 4)
				.attr("markerHeight", 4)
				.attr("orient", "auto")
				.append("svg:path")
				.attr('d', 'M 0 -5 L 10 0 L 0 5 z')

			svg.append("svg:defs").selectAll('marker')
				.data(["start-arrow", "start-arrow-selected"])      // Different link/path types can be defined here
				.enter().append("svg:marker")    // This section adds in the arrows
				.attr("id", String)
				.attr("viewBox", "0 -5 10 10")
				.attr("refX", 5)
				.attr("markerWidth", 4)
				.attr("markerHeight", 4)
				.attr("orient", "auto")
				.append("svg:path")
				.attr('d', 'M 10 -5 L 0 0 L 10 5 z');

			// handles to link and node element groups
			path = svg.append('svg:g').selectAll('path'),
			circle = svg.append('svg:g').selectAll('g');
            
			force.on('end', function() {
				//QDR.log.debug("force end called");
				circle
					.attr('cx', function(d) {
						localStorage[d.name] = angular.toJson({x: d.x, y: d.y, fixed: d.fixed});
						return d.x; });
			});

			// app starts here
			restart(false);
    	    force.start();
			setTimeout(function () {
	    	    updateForm(Object.keys(QDRService.topology.nodeInfo())[0], 'router', 0);
			}, 10)

		}

		function updateForm (key, entity, resultIndex) {
			var nodeInfo = QDRService.topology.nodeInfo();
			var onode = nodeInfo[key]
			if (onode) {
				var nodeResults = onode['.' + entity].results[resultIndex]
				var nodeAttributes = onode['.' + entity].attributeNames
				var attributes = nodeResults.map( function (row, i) {
					return {
						attributeName: nodeAttributes[i],
						attributeValue: row
					}
				})
				// sort by attributeName
				attributes.sort( function (a, b) { return a.attributeName.localeCompare(b.attributeName) })

				// move the Name first
				var nameIndex = attributes.findIndex ( function (attr) {
					return attr.attributeName === 'name'
				})
				if (nameIndex >= 0)
					attributes.splice(0, 0, attributes.splice(nameIndex, 1)[0]);
				// get the list of ports this router is listening on
				if (entity === 'router') {
					var listeners = onode['.listener'].results;
					var listenerAttributes = onode['.listener'].attributeNames;
					var normals = listeners.filter ( function (listener) {
						return QDRService.valFor( listenerAttributes, listener, 'role') === 'normal';
					})
					var ports = []
					normals.forEach (function (normalListener) {
						ports.push(QDRService.valFor( listenerAttributes, normalListener, 'port'))
					})
					// add as 2nd row
					if (ports.length)
						attributes.splice(1, 0, {attributeName: 'Listening on', attributeValue: ports, description: 'The port on which this router is listening for connections'});
				}

				$scope.$broadcast('showEntityForm', {entity: entity, attributes: attributes})
			}
			if (!$scope.$$phase) $scope.$apply()
		}

        function getContainerIndex(_id) {
            var nodeIndex = 0;
            var nodeInfo = QDRService.topology.nodeInfo();
            for (var id in nodeInfo) {
                var node = nodeInfo[id]['.router'];
                // there should be only one router entity for each node, so using results[0] should be fine
                if (QDRService.valFor( node.attributeNames, node.results[0], "id") === _id)
                    return nodeIndex;
                if (QDRService.valFor( node.attributeNames, node.results[0], "routerId") === _id)
                    return nodeIndex;
                nodeIndex++
            }
			// there was no router.id that matched, check deprecated router.routerId
            nodeIndex = 0;
            for (var id in nodeInfo) {
                var node = nodeInfo[id]['.container'];
				if (node) {
					if (QDRService.valFor ( node.attributeNames, node.results[0], "containerName") === _id)
						return nodeIndex;
				}
				nodeIndex++
			}
            //QDR.log.warn("unable to find containerIndex for " + _id);
            return -1;
        }

        function getNodeIndex (_id) {
            var nodeIndex = 0;
            var nodeInfo = QDRService.topology.nodeInfo();
            for (var id in nodeInfo) {
                if (QDRService.nameFromId(id) == _id) return nodeIndex;
                nodeIndex++
            }
            QDR.log.warn("unable to find nodeIndex for " + _id);
            return -1;
        }

        function getLink (_source, _target, dir, cls) {
            for (var i=0; i < links.length; i++) {
                var s = links[i].source, t = links[i].target;
                if (typeof links[i].source == "object") {
                    s = s.id;
                    t = t.id;
				}
                if (s == _source && t == _target) {
                    return i;
                }
				// same link, just reversed
                if (s == _target && t == _source) {
                    return -i;
				}
            }

            //QDR.log.debug("creating new link (" + (links.length) + ") between " + nodes[_source].name + " and " + nodes[_target].name);
            var link = {
                source: _source,
                target: _target,
                left: dir != "out",
                right: dir == "out",
                cls: cls
            };
            return links.push(link) - 1;
        }


	    function resetMouseVars() {
	        mousedown_node = null;
	        mouseup_node = null;
	        mousedown_link = null;
	    }

	    // update force layout (called automatically each iteration)
	    function tick() {
	        circle.attr('transform', function (d) {
                var cradius;
                if (d.nodeType == "inter-router") {
					cradius = d.left ? radius + 8  : radius;
                } else {
					cradius = d.left ? radiusNormal + 18  : radiusNormal;
                }
	            d.x = Math.max(d.x, radiusNormal * 2);
	            d.y = Math.max(d.y, radiusNormal * 2);
				d.x = Math.max(0, Math.min(width-cradius, d.x))
				d.y = Math.max(0, Math.min(height-cradius, d.y))
	            return 'translate(' + d.x + ',' + d.y + ')';
	        });

	        // draw directed edges with proper padding from node centers
	        path.attr('d', function (d) {
				//QDR.log.debug("in tick for d");
				//console.dump(d);
                var sourcePadding, targetPadding, r;

                if (d.target.nodeType == "inter-router") {
					r = radius;
					//                       right arrow  left line start
					sourcePadding = d.left ? radius + 8  : radius;
					//                      left arrow      right line start
					targetPadding = d.right ? radius + 16 : radius;
                } else {
					r = radiusNormal - 18;
					sourcePadding = d.left ? radiusNormal + 18  : radiusNormal;
					targetPadding = d.right ? radiusNormal + 16 : radiusNormal;
                }
				var dtx = Math.max(targetPadding, Math.min(width-r, d.target.x)),
				    dty = Math.max(targetPadding, Math.min(height-r, d.target.y)),
				    dsx = Math.max(sourcePadding, Math.min(width-r, d.source.x)),
					dsy = Math.max(sourcePadding, Math.min(height-r, d.source.y));

	            var deltaX = dtx - dsx,
	                deltaY = dty - dsy,
	                dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY),
	                normX = deltaX / dist,
	                normY = deltaY / dist;
	                var sourceX = dsx + (sourcePadding * normX),
	                sourceY = dsy + (sourcePadding * normY),
	                targetX = dtx - (targetPadding * normX),
	                targetY = dty - (targetPadding * normY);
					sourceX = Math.max(0, Math.min(width, sourceX))
					sourceY = Math.max(0, Math.min(width, sourceY))
					targetX = Math.max(0, Math.min(width, targetX))
					targetY = Math.max(0, Math.min(width, targetY))

	            return 'M' + sourceX + ',' + sourceY + 'L' + targetX + ',' + targetY;
	        });

	        if (!animate) {
	            animate = true;
	            force.stop();
	        }
	    }

        // highlight the paths between the selected node and the hovered node
        function findNextHopNode(from, d) {
            // d is the node that the mouse is over
            // from is the selected_node ....
            if (!from)
                return null;

            if (from == d)
                return selected_node;

            //QDR.log.debug("finding nextHop from: " + from.name + " to " + d.name);
            var sInfo = QDRService.topology.nodeInfo()[from.key];

            if (!sInfo) {
                QDR.log.warn("unable to find topology node info for " + from.key);
                return null;
            }

            // find the hovered name in the selected name's .router.node results
            if (!sInfo['.router.node'])
                return null;
            var aAr = sInfo['.router.node'].attributeNames;
            var vAr = sInfo['.router.node'].results;
            for (var hIdx=0; hIdx<vAr.length; ++hIdx) {
                var addrT = QDRService.valFor(aAr, vAr[hIdx], "id" );
                if (addrT == d.name) {
                    //QDR.log.debug("found " + d.name + " at " + hIdx);
                    var nextHop = QDRService.valFor(aAr, vAr[hIdx], "nextHop");
                    //QDR.log.debug("nextHop was " + nextHop);
                    return (nextHop == null) ? nodeFor(addrT) : nodeFor(nextHop);
                }
            }
            return null;
        }

        function nodeFor(name) {
            for (var i=0; i<nodes.length; ++i) {
                if (nodes[i].name == name)
                    return nodes[i];
            }
            return null;
        }

        function linkFor(source, target) {
            for (var i=0; i<links.length; ++i) {
                if ((links[i].source == source) && (links[i].target == target))
                    return links[i];
                if ((links[i].source == target) && (links[i].target == source))
                    return links[i];
            }
            // the selected node was a client/broker
            //QDR.log.debug("failed to find a link between ");
            //console.dump(source);
            //QDR.log.debug(" and ");
            //console.dump(target);
            return null;
        }

		function clearPopups() {
            d3.select("#crosssection").style("display", "none");
			$('.hastip').empty();
            d3.select("#multiple_details").style("display", "none")
            d3.select("#link_details").style("display", "none")
			d3.select('#node_context_menu').style('display', 'none');

		}
		function removeCrosssection() {
			setTimeout(function () {
				d3.select("[id^=tooltipsy]").remove()
				$('.hastip').empty();
			}, 1010);
			d3.select("#crosssection svg g").transition()
                .duration(1000)
				.attr("transform", "translate("+(dblckickPos[0]-140) + "," + (dblckickPos[1]-100) + ") scale(0)")
                .style("opacity", 0)
                .each("end", function (d) {
                    d3.select("#crosssection svg").remove();
                    d3.select("#crosssection").style("display","none");
                });
            d3.select("#multiple_details").transition()
                .duration(500)
                .style("opacity", 0)
                .each("end", function (d) {
                    d3.select("#multiple_details").style("display", "none")
                    stopUpdateConnectionsGrid();
                })
			hideLinkDetails();
		}
		function hideLinkDetails() {
            d3.select("#link_details").transition()
                .duration(500)
                .style("opacity", 0)
                .each("end", function (d) {
                    d3.select("#link_details").style("display", "none")
                })
		}

	    // takes the nodes and links array of objects and adds svg elements for everything that hasn't already
	    // been added
	    function restart(start) {
	        circle.call(force.drag);

	        // path (link) group
	        path = path.data(links);

			// update existing links
  			path.classed('selected', function(d) { return d === selected_link; })
  			    .classed('highlighted', function(d) { return d.highlighted; } )
  			    .classed('temp', function(d) { return d.cls == 'temp'; } )
                .attr('marker-start', function(d) {
                    var sel = d===selected_link ? '-selected' : '';
                    return d.left ? 'url('+urlPrefix+'#start-arrow' + sel + ')' : ''; })
                .attr('marker-end', function(d) {
                    var sel = d===selected_link ? '-selected' : '';
                    return d.right ? 'url('+urlPrefix+'#end-arrow' + sel +')' : ''; })


			// add new links. if links[] is longer than the existing paths, add a new path for each new element
			path.enter().append('svg:path')
				.attr('class', 'link')
                .attr('marker-start', function(d) {
                        var sel = d===selected_link ? '-selected' : '';
						return d.left ? 'url('+urlPrefix+'#start-arrow' + sel + ')' : ''; })
                .attr('marker-end', function(d) {
					var sel = d===selected_link ? '-selected' : '';
                    return d.right ? 'url('+urlPrefix+'#end-arrow' + sel + ')' : ''; })
  			    .classed('temp', function(d) { return d.cls == 'temp'; } )
	            .on('mouseover', function (d) {
				  if($scope.addingNode.step > 0) {
				    if (d.cls == 'temp') {
				        d3.select(this).classed('over', true);
				    }
				    return;
				  }
			        //QDR.log.debug("showing connections form");
					var resultIndex = 0; // the connection to use
                    var left = d.left ? d.target : d.source;
					// right is the node that the arrow points to, left is the other node
					var right = d.left ? d.source : d.target;
					var onode = QDRService.topology.nodeInfo()[left.key];
					// loop through all the connections for left, and find the one for right
					if (!onode || !onode['.connection'])
						return;
                    // update the info dialog for the link the mouse is over
                    if (!selected_node && !selected_link) {
                        for (resultIndex=0; resultIndex < onode['.connection'].results.length; ++resultIndex) {
                            var conn = onode['.connection'].results[resultIndex];
                            /// find the connection whose container is the right's name
                            var name = QDRService.valFor(onode['.connection'].attributeNames, conn, "container");
                            if (name == right.routerId) {
                                break;
                            }
                        }
                        // did not find connection. this is a connection to a non-interrouter node
                        if (resultIndex === onode['.connection'].results.length) {
                            // use the non-interrouter node's connection info
                            left = d.target;
                            resultIndex = left.resultIndex;
                        }
						if (resultIndex)
                            updateForm(left.key, 'connection', resultIndex);
                    }

					mousedown_link = d;
					selected_link = mousedown_link;
					restart();
				})
	            .on('mouseout', function (d) {
				  if($scope.addingNode.step > 0) {
				    if (d.cls == 'temp') {
				        d3.select(this).classed('over', false);
				    }
				    return;
				  }
			        //QDR.log.debug("showing connections form");
					selected_link = null;
					restart();
				})
	            .on("contextmenu", function(d) {
	                $(document).click();
                    d3.event.preventDefault();
	                if (d.cls !== "temp")
	                    return;

					mousedown_link = d;
                    d3.select('#link_context_menu')
                      .style('left', (mouseX + $(document).scrollLeft()) + "px")
                      .style('top', (mouseY + $(document).scrollTop()) + "px")
                      .style('display', 'block');
                })
                .on("click", function (d) {
                    dblckickPos = d3.mouse(this);
                    QDR.log.debug("dblckickPos is [" + dblckickPos[0] + ", " + dblckickPos[1] + "]")
                    d3.event.stopPropagation();
                    clearPopups();
                    var diameter = 400;
                    var format = d3.format(",d");
                    var pack = d3.layout.pack()
                        .size([diameter - 4, diameter - 4])
                        .padding(-10)
                        .value(function(d) { return d.size; });

                    d3.select("#crosssection svg").remove();
                    var svg = d3.select("#crosssection").append("svg")
                        .attr("width", diameter)
                        .attr("height", diameter)
                    var svgg = svg.append("g")
                        .attr("transform", "translate(2,2)");

					var root = {
						name: "links between " + d.source.name + " and " + d.target.name,
						children: []
					}
					var nodeInfo = QDRService.topology.nodeInfo();
					var connections = nodeInfo[d.source.key]['.connection'];
					var containerIndex = connections.attributeNames.indexOf('container');
					connections.results.some ( function (connection) {
                        if (connection[containerIndex] == d.target.routerId) {
                            root.attributeNames = connections.attributeNames;
                            root.obj = connection;
                            root.desc = "Connection";
                            return true;    // stop looping after 1 match
                        }
                        return false;
                    })

					// find router.links where link.remoteContainer is d.source.name
					var links = nodeInfo[d.source.key]['.router.link'];
					var identityIndex = connections.attributeNames.indexOf('identity')
					var roleIndex = connections.attributeNames.indexOf('role')
					var connectionIdIndex = links.attributeNames.indexOf('connectionId');
					var linkTypeIndex = links.attributeNames.indexOf('linkType');
					var nameIndex = links.attributeNames.indexOf('name');
					var linkDirIndex = links.attributeNames.indexOf('linkDir');

					if (roleIndex < 0 || identityIndex < 0 || connectionIdIndex < 0
						|| linkTypeIndex < 0 || nameIndex < 0 || linkDirIndex < 0)
						return;
					links.results.forEach ( function (link) {
						if (root.obj && link[connectionIdIndex] == root.obj[identityIndex] && link[linkTypeIndex] == root.obj[roleIndex])
							root.children.push (
								{ name: "(" + link[linkDirIndex] + ") " + link[nameIndex],
								size: 100,
								obj: link,
	                            desc: "Link",
								attributeNames: links.attributeNames
							})
					})
					if (root.children.length == 0)
						return;
	                var node = svgg.datum(root).selectAll(".node")
	                      .data(pack.nodes)
	                    .enter().append("g")
	                      .attr("class", function(d) { return d.children ? "parent node hastip" : "leaf node hastip"; })
	                      .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")" + (!d.children ? "scale(0.9)" : ""); })
	                      .attr("title", function (d) {
	                          var title = "<h4>" + d.desc + "</h4><table class='tiptable'><tbody>";
	                          if (d.attributeNames)
		                            d.attributeNames.forEach( function (n, i) {
		                                title += "<tr><td>" + n + "</td><td>";
		                                title += d.obj[i] != null ? d.obj[i] : '';
		                                title += '</td></tr>';
		                            })
		                      title += "</tbody></table>"
	                          return title
	                      })

	                node.append("circle")
	                      .attr("r", function(d) { return d.r; });

//	                node.filter(function(d) { return !d.children; }).append("text")
	                node.append("text")
	                      .attr("dy", function (d) { return d.children ? "-10em" : ".3em"})
	                      .style("text-anchor", "middle")
	                      .text(function(d) {
	                          return d.name.substring(0, d.r / 3);
	                      });

					$('.hastip').tooltipsy({ alignTo: 'cursor'});
					svgg.attr("transform", "translate("+(dblckickPos[0]-140) + "," + (dblckickPos[1]-100) + ") scale(0.01)")
	                d3.select("#crosssection").style("display","block");

					svgg.transition().attr("transform", "translate(2,2) scale(1)")
                })

	        // remove old links
	        path.exit().remove();


	        // circle (node) group
	        // nodes are known by id
	        circle = circle.data(nodes, function (d) {
	            return d.id;
	        });

	        // update existing nodes visual states
	        circle.selectAll('circle')
	            .classed('selected', function (d) { return (d === selected_node) })
	            .classed('fixed', function (d) { return (d.fixed & 0b1) })

			// add new circle nodes. if nodes[] is longer than the existing paths, add a new path for each new element
	        var g = circle.enter().append('svg:g')
  			    .classed('multiple', function(d) { return (d.normals && d.normals.length > 1)  } )

			var appendCircle = function (g) {
				// add new circles and set their attr/class/behavior
		        return g.append('svg:circle')
		            .attr('class', 'node')
		            .attr('r', function (d) { return radii[d.nodeType] } )
		            .classed('fixed', function (d) {return d.fixed})
	                .classed('temp', function(d) { return QDRService.nameFromId(d.key) == '__internal__'; } )
	                .classed('normal', function(d) { return d.nodeType == 'normal' } )
	                .classed('inter-router', function(d) { return d.nodeType == 'inter-router' } )
	                .classed('on-demand', function(d) { return d.nodeType == 'on-demand' } )
	                .classed('console', function(d) { return QDRService.isConsole(d) } )
	                .classed('artemis', function(d) { return QDRService.isArtemis(d) } )
	                .classed('qpid-cpp', function(d) { return QDRService.isQpid(d) } )
	                .classed('client', function(d) { return d.nodeType === 'normal' && !d.properties.console_identifier } )
			}
			appendCircle(g).on('mouseover', function (d) {
	                if ($scope.addingNode.step > 0) {
		                d3.select(this).attr('transform', 'scale(1.1)');
						return;
	                }
					if (!selected_node) {
                        if (d.nodeType === 'inter-router') {
                            //QDR.log.debug("showing general form");
                            updateForm(d.key, 'router', 0);
                        } else if (d.nodeType === 'normal' || d.nodeType === 'on-demand') {
                            //QDR.log.debug("showing connections form");
                            updateForm(d.key, 'connection', d.resultIndex);
                        }
					}

	                if (d === mousedown_node) 
	                    return;
	                //if (d === selected_node)
	                //    return;
	                // enlarge target node
	                d3.select(this).attr('transform', 'scale(1.1)');
                    // highlight the next-hop route from the selected node to this node
                    mousedown_node = null;

	                if (!selected_node) {
	                    return;
	                }
                    setTimeout(nextHop, 1, selected_node, d);
	            })
	            .on('mouseout', function (d) {
	                // unenlarge target node
	                d3.select(this).attr('transform', '');
                    for (var i=0; i<links.length; ++i) {
                        links[i]['highlighted'] = false;
                    }
                    restart();
	            })
	            .on('mousedown', function (d) {
	                if (d3.event.button !== 0) {   // ignore all but left button
	                    return;
	                }
	                mousedown_node = d;
	                // mouse position relative to svg
	                initial_mouse_down_position = d3.mouse(this.parentElement.parentElement.parentElement).slice();
	            })
	            .on('mouseup', function (d) {
	                if (!mousedown_node)
	                    return;

                    selected_link = null;
	                // unenlarge target node
	                d3.select(this).attr('transform', '');

	                // check for drag
	                mouseup_node = d;
	                var mySvg = this.parentElement.parentElement.parentElement;
                    // if we dragged the node, make it fixed
                    var cur_mouse = d3.mouse(mySvg);
                    if (cur_mouse[0] != initial_mouse_down_position[0] ||
                        cur_mouse[1] != initial_mouse_down_position[1]) {
						console.log("mouse pos changed. making this node fixed")
						d3.select(this).classed("fixed", d.fixed = true);
                        resetMouseVars();
                        return;
	                }

					// we didn't drag, we just clicked on the node
	                if ($scope.addingNode.step > 0) {
                        if (d.nodeType !== 'inter-router')
                            return;
						if (QDRService.nameFromId(d.key) == '__internal__')
							return;

						// add a link from the clicked node to the new node
						getLink(d.id, nodes.length-1, "in", "temp");
						$scope.addingNode.hasLink = true;
						if (!$scope.$$phase) $scope.$apply()
						// add new elements to the svg
						force.links(links).start();
						restart();
						return;

	                }

					// if this node was selected, unselect it
                    if (mousedown_node === selected_node) {
                        selected_node = null;
                    }
                    else {
                        if (d.nodeType !== 'normal' && d.nodeType !== 'on-demand')
                            selected_node = mousedown_node;
                    }
                    for (var i=0; i<links.length; ++i) {
                        links[i]['highlighted'] = false;
                    }
	                mousedown_node = null;
					if (!$scope.$$phase) $scope.$apply()
                    restart(false);

	            })
	            .on("dblclick", function (d) {
	                if (d.fixed) {
						d3.select(this).classed("fixed", d.fixed = false);
						force.start();  // let the nodes move to a new position
	                }
	                if (QDRService.nameFromId(d.key) == '__internal__') {
	                    editNode();
						if (!$scope.$$phase) $scope.$apply()
	                }
	            })
	            .on("contextmenu", function(d) {
	                $(document).click();
                    d3.event.preventDefault();
	                $scope.contextNode = d;
					if (!$scope.$$phase) $scope.$apply()     // we just changed a scope valiable during an async event
                    d3.select('#node_context_menu')
                      .style('left', (mouseX + $(document).scrollLeft()) + "px")
                      .style('top', (mouseY + $(document).scrollTop()) + "px")
                      .style('display', 'block');

                })
                .on("click", function (d) {
                    // clicked on a circle
                    clearPopups();
					if (!d.normals) {
						// circle was a router or a broker
			            if ( QDRService.isArtemis(d) && Core.ConnectionName === 'Artemis' ) {
							$location.path('/jmx/attributes?tab=artemis&con=Artemis')
						}
						return;
					}
                    clickPos = d3.mouse(this);
                    d3.event.stopPropagation();
					startUpdateConnectionsGrid(d);
				})

			var appendContent = function (g) {
		        // show node IDs
		        g.append('svg:text')
		            .attr('x', 0)
		            .attr('y', function (d) {
		                var y = 6;
		                if (QDRService.isArtemis(d))
		                    y = 8;
		                else if (QDRService.isQpid(d))
		                    y = 9;
		                else if (d.nodeType === 'inter-router')
		                    y = 4;
		                return y;})
		            .attr('class', 'id')
   	                .classed('console', function(d) { return QDRService.isConsole(d) } )
	                .classed('normal', function(d) { return d.nodeType === 'normal' } )
	                .classed('on-demand', function(d) { return d.nodeType === 'on-demand' } )
	                .classed('artemis', function(d) { return QDRService.isArtemis(d) } )
	                .classed('qpid-cpp', function(d) { return QDRService.isQpid(d) } )
		            .text(function (d) {
		                if (QDRService.isConsole(d)) {
		                    return '\uf108'; // icon-desktop for this console
		                }
						if (QDRService.isArtemis(d)) {
							return '\ue900'
						}
		                if (QDRService.isQpid(d)) {
		                    return '\ue901';
		                }
						if (d.nodeType === 'normal')
							return '\uf109'; // icon-laptop for clients
		                return d.name.length>7 ? d.name.substr(0,6)+'...' : d.name;
		        });
			}

			appendContent(g)

			var appendTitle = function (g) {
		        g.append("svg:title").text(function (d) {
	                var x = '';
	                if (d.normals && d.normals.length > 1)
	                    x = " x " + d.normals.length;
		            if (QDRService.isConsole(d)) {
	                    return 'Dispatch console' + x
	                }
		            if (d.properties.product == 'qpid-cpp') {
	                    return 'Broker - qpid-cpp' + x
	                }
		            if ( QDRService.isArtemis(d) ) {
	                    return 'Broker - Artemis' + x
	                }
		            return d.nodeType == 'normal' ? 'client' + x : (d.nodeType == 'on-demand' ? 'broker' : 'Router ' + d.name)
		        })
			}
			appendTitle(g);

	        // remove old nodes
	        circle.exit().remove();

			// add subcircles
			svg.selectAll('.subcircle').remove();
			var multiples = svg.selectAll('.multiple')
			multiples.each( function (d) {
				d.normals.forEach( function (n, i) {
					if (i<d.normals.length-1 && i<3) // only show a few shadow circles
						this.insert('svg:circle', ":first-child")
						.attr('class', 'subcircle node')
						.attr('r', 15 - i)
						.attr('transform', "translate("+ 4 * (i+1) +", 0)")
				}, d3.select(this))
			})

			// dynamically create the legend based on which node types are present
			var legendNodes = [];
			legendNodes.push(aNode("Router", "", "inter-router", undefined, 0, 0, 0, 0, false, {}))

			if (!svg.selectAll('circle.console').empty()) {
				legendNodes.push(aNode("Dispatch console", "", "normal", undefined, 1, 0, 0, 0, false, {console_identifier: 'Dispatch console'}))
			}
			if (!svg.selectAll('circle.client').empty()) {
				legendNodes.push(aNode("Client", "", "normal", undefined, 2, 0, 0, 0, false, {}))
			}
			if (!svg.selectAll('circle.qpid-cpp').empty()) {
				legendNodes.push(aNode("Qpid cpp broker", "", "on-demand", undefined, 3, 0, 0, 0, false, {product: 'qpid-cpp'}))
			}
			if (!svg.selectAll('circle.artemis').empty()) {
				legendNodes.push(aNode("Artemis broker", "", "on-demand", undefined, 4, 0, 0, 0, false, {}))
			}
		    lsvg = lsvg.data(legendNodes, function (d) {
	            return d.id;
            });
	        var lg = lsvg.enter().append('svg:g')
				.attr('transform', function (d, i) {
					// 45px between lines and add 10px space after 1st line
					return "translate(0, "+(45*i+(i>0?10:0))+")"
				})

			appendCircle(lg)
			appendContent(lg)
			appendTitle(lg)
			lg.append('svg:text')
				.attr('x', 35)
				.attr('y', 6)
				.attr('class', "label")
				.text(function (d) {return d.key })
			lsvg.exit().remove();
			var svgEl = document.getElementById('svglegend')
			if (svgEl) {
				var bb;
				// firefox can throw an exception on getBBox on an svg element
				try {
					bb = svgEl.getBBox();
				} catch (e) {
					bb = {y: 0, height: 200, x: 0, width: 200}
				}
				svgEl.style.height = (bb.y + bb.height) + 'px';
				svgEl.style.width = (bb.x + bb.width) + 'px';
			}

	        if (!mousedown_node || !selected_node)
	            return;

            if (!start)
                return;
	        // set the graph in motion
	        //QDR.log.debug("mousedown_node is " + mousedown_node);
	        force.start();

	    }

		var startUpdateConnectionsGrid = function (d) {
			var extendConnections = function () {
				$scope.multiData = []
				var normals = d.normals;
				// find updated normals for d
		        d3.selectAll('.normal')
                    .each(function(newd) {
                        if (newd.id == d.id && newd.name == d.name) {
                            normals = newd.normals;
                        }
                    });
				if (normals) {
					normals.forEach( function (n) {
						var nodeInfo = QDRService.topology.nodeInfo();
						var links = nodeInfo[n.key]['.router.link'];
						var linkTypeIndex = links.attributeNames.indexOf('linkType');
						var connectionIdIndex = links.attributeNames.indexOf('connectionId');
						n.linkData = [];
						links.results.forEach( function (link) {
							if (link[linkTypeIndex] === 'endpoint' && link[connectionIdIndex] === n.connectionId) {
								var l = {};
								l.owningAddr = QDRService.valFor(links.attributeNames, link, 'owningAddr');
								l.dir = QDRService.valFor(links.attributeNames, link, 'linkDir');
								if (l.owningAddr && l.owningAddr.length > 2)
									if (l.owningAddr[0] === 'M')
										l.owningAddr = l.owningAddr.substr(2)
									else
										l.owningAddr = l.owningAddr.substr(1)

								l.deliveryCount = QDRService.pretty(QDRService.valFor(links.attributeNames, link, 'deliveryCount'));
								l.uncounts = QDRService.pretty(QDRService.valFor(links.attributeNames, link, 'undeliveredCount') +
																QDRService.valFor(links.attributeNames, link, 'unsettledCount'))
								//l.undeliveredCount = QDRService.pretty(QDRService.valFor(links.attributeNames, link, 'undeliveredCount'));
								//l.unsettledCount = QDRService.pretty(QDRService.valFor(links.attributeNames, link, 'unsettledCount'));
								l.adminStatus = QDRService.valFor(links.attributeNames, link, 'adminStatus');
								l.operStatus = QDRService.valFor(links.attributeNames, link, 'operStatus');
								l.identity = QDRService.valFor(links.attributeNames, link, 'identity')
								l.connectionId = QDRService.valFor(links.attributeNames, link, 'connectionId')
								l.nodeId = n.key
								l.type = QDRService.valFor(links.attributeNames, link, 'type')
								l.name = QDRService.valFor(links.attributeNames, link, 'name')

								// TODO: remove this fake quiescing/reviving logic when the routers do the work
								initConnState(n.connectionId)
								if ($scope.quiesceState[n.connectionId].linkStates[l.identity])
									l.adminStatus = $scope.quiesceState[n.connectionId].linkStates[l.identity];
								if ($scope.quiesceState[n.connectionId].state == 'quiescing') {
									if (l.adminStatus === 'enabled') {
										// 25% chance of switching
										var chance = Math.floor(Math.random() * 2);
										if (chance == 1) {
											l.adminStatus = 'disabled';
											$scope.quiesceState[n.connectionId].linkStates[l.identity] = 'disabled';
										}
									}
								}
								if ($scope.quiesceState[n.connectionId].state == 'reviving') {
									if (l.adminStatus === 'disabled') {
										// 25% chance of switching
										var chance = Math.floor(Math.random() * 2);
										if (chance == 1) {
											l.adminStatus = 'enabled';
											$scope.quiesceState[n.connectionId].linkStates[l.identity] = 'enabled';
										}
									}
								}
								QDR.log.debug("pushing link state for " + l.owningAddr + " status: "+ l.adminStatus)


								n.linkData.push(l)
							}
						})
						$scope.multiData.push(n)
						if (n.connectionId == $scope.connectionId)
							$scope.linkData = n.linkData;
						initConnState(n.connectionId)
						$scope.multiDetails.updateState(n)
					})
				}
				$scope.$apply();

	            d3.select('#multiple_details')
	                .style({
	                    height: ((normals.length + 1) * 30) + 40 + "px",
	                    'overflow-y': normals.length > 10 ? 'scroll' : 'hidden'
					})
			}

	        QDRService.addUpdatedAction("normalsStats", extendConnections)
			extendConnections();
			clearPopups();
			var display = 'block'
			var left = mouseX + $(document).scrollLeft()
			if (d.normals.length === 1) {
				display = 'none'
				left = left - 30;
				mouseY = mouseY - 20
			}
            d3.select('#multiple_details')
                .style({
                    display: display,
                    opacity: 1,
                    left: (mouseX + $(document).scrollLeft()) + "px",
                    top:  (mouseY + $(document).scrollTop()) + "px"})
            if (d.normals.length === 1) {
				// simulate a click on the connection to popup the link details
                $scope.multiDetails.showLinksList( {entity: d} )
            }
		}
		var stopUpdateConnectionsGrid = function () {
	        QDRService.delUpdatedAction("normalsStats");
		}

		var initConnState = function (id) {
			if (!angular.isDefined($scope.quiesceState[id])) {
				$scope.quiesceState[id] = {
					state:          'enabled',
					buttonText:     'Quiesce',
					buttonDisabled: false,
					linkStates:     {}
				}
			}
		}

        function nextHop(thisNode, d) {
            if ((thisNode) && (thisNode != d)) {
                var target = findNextHopNode(thisNode, d);
                //QDR.log.debug("highlight link from node ");
                 //console.dump(nodeFor(selected_node.name));
                 //console.dump(target);
                if (target) {
                    var hlLink = linkFor(nodeFor(thisNode.name), target);
                    //QDR.log.debug("need to highlight");
                    //console.dump(hlLink);
                    if (hlLink)
                        hlLink['highlighted'] = true;
                    else
                        target = null;
                }
                setTimeout(nextHop, 1, target, d);
            }
            restart();
        }


	    function mousedown() {
	        // prevent I-bar on drag
	        //d3.event.preventDefault();

	        // because :active only works in WebKit?
	        svg.classed('active', true);
	    }

        QDRService.addUpdatedAction("topology", function() {
            //QDR.log.debug("Topology controller was notified that the model was updated");
            if (hasChanged()) {
                QDR.log.info("svg graph changed")
                saveChanged();
                // TODO: update graph nodes instead of rebuilding entire graph
                d3.select("#SVG_ID").remove();
                d3.select("#svg_legend svg").remove();
                animate = true;
                initForceGraph();
                //if ($location.path().startsWith("/topology"))
                //    Core.notification('info', "Qpid dispatch router topology changed");

            } else {
                //QDR.log.debug("no changes")
            }
        });

		function hasChanged () {
			// Don't update the underlying topology diagram if we are adding a new node.
			// Once adding is completed, the topology will update automatically if it has changed
			if ($scope.addingNode.step > 0)
				return false;
			var nodeInfo = QDRService.topology.nodeInfo();
			if (Object.keys(nodeInfo).length != Object.keys(savedKeys).length)
				return true;
			for (var key in nodeInfo) {
                // if this node isn't in the saved node list
                if (!savedKeys.hasOwnProperty(key))
                    return true;
                // if the number of connections for this node chaanged
                if (nodeInfo[key]['.connection'].results.length != savedKeys[key]) {
					/*
					QDR.log.debug("number of connections changed for " + key);
					QDR.log.debug("QDRService.topology._nodeInfo[key]['.connection'].results.length");
					console.dump(QDRService.topology._nodeInfo[key]['.connection'].results.length);
					QDR.log.debug("savedKeys[key]");
					console.dump(savedKeys[key]);
					*/
                    return true;
                }
			}
			return false;
		};
		function saveChanged () {
            savedKeys = {};
            var nodeInfo = QDRService.topology.nodeInfo();
            // save the number of connections per node
		    for (var key in nodeInfo) {
		        savedKeys[key] = nodeInfo[key]['.connection'].results.length;
		    }
			//QDR.log.debug("saving current keys");
			console.dump(savedKeys);
		};
		// we are about to leave the page, save the node positions
		$rootScope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
			//QDR.log.debug("locationChangeStart");
			nodes.forEach( function (d) {
	           localStorage[d.name] = angular.toJson({x: d.x, y: d.y, fixed: d.fixed});
			});
            $scope.addingNode.step = 0;

		});
		// When the DOM element is removed from the page,
        // AngularJS will trigger the $destroy event on
        // the scope
        $scope.$on("$destroy", function( event ) {
   			//QDR.log.debug("scope on destroy");
            QDRService.stopUpdating();
            QDRService.delUpdatedAction("topology");
			d3.select("#SVG_ID").remove();
			window.removeEventListener('resize', resize);
        });

		initForceGraph();
		saveChanged();
        QDRService.startUpdating();

	    function doAddDialog(NewRouterName) {
		    var d = $dialog.dialog({
				dialogClass: "modal dlg-large",
				backdrop: true,
				keyboard: true,
				backdropClick: true,
		        controller: 'QDR.NodeDialogController',
		        templateUrl: 'node-config-template.html',
		        resolve: {
		            newname: function () {
		                return NewRouterName;
		            }
		        }
		    });
		    d.open().then(function (result) {
				if (result)
					doDownloadDialog(result);
		    });
        };

	    function doDownloadDialog(result) {
		    d = $dialog.dialog({
				backdrop: true,
				keyboard: true,
				backdropClick: true,
				controller: 'QDR.DownloadDialogController',
		        templateUrl: 'download-dialog-template.html',
		        resolve: {
		            results: function () {
		                return result;
		            }
		        }
		    });
		    d.open().then(function (result) {
		    //QDR.log.debug("download dialog done")
		    })
            if (!$scope.$$phase) $scope.$apply()
        };
  }]);

  QDR.module.controller("QDR.NodeDialogController", function($scope, QDRService, dialog, newname) {
   		var schema = QDRService.schema;
   		var myEntities = ['router', 'log', 'listener' ];
   		var typeMap = {integer: 'number', string: 'text', path: 'text', boolean: 'boolean'};
		var newLinks = $('path.temp').toArray();    // jquery array of new links for the added router
		var nodeInfo = QDRService.topology.nodeInfo();
		var separatedEntities = []; // additional entities required if a link is reversed
		var myPort = 0, myAddr = '0.0.0.0'; // port and address for new router
   		$scope.entities = [];

		// find max port number that is used in all the listeners
		var getMaxPort = function (nodeInfo) {
			var maxPort = 5674;
			for (var key in nodeInfo) {
				var node = nodeInfo[key];
				var listeners = node['.listener'];
				var attrs = listeners.attributeNames;
				for (var i=0; i<listeners.results.length; ++i) {
					var res = listeners.results[i];
					var port = QDRService.valFor(attrs, res, 'port');
					if (parseInt(port, 10) > maxPort)
						maxPort = parseInt(port, 10);
				}
			}
			return maxPort;
		}
		var maxPort = getMaxPort(nodeInfo);

		// construct an object that contains all the info needed for a single tab's fields
		var entity = function (actualName, tabName, humanName, ent, icon, link) {
			var nameIndex = -1; // the index into attributes that the name field was placed
			var index = 0;
			var info = {
			    actualName: actualName,
				tabName:    tabName,
				humanName:  humanName,
				description:ent.description,
				icon:       angular.isDefined(icon) ? icon : '',
				references: ent.references,
				link:       link,

   		        attributes: $.map(ent.attributes, function (value, key) {
					// skip identity and depricated fields
   		            if (key == 'identity' || value.description.startsWith('Deprecated'))
   		                return null;
					var val = value['default'];
					if (key == 'name')
						nameIndex = index;
					index++;
					return {    name:       key,
								humanName:  QDRService.humanify(key),
                                description:value.description,
                                type:       typeMap[value.type],
                                rawtype:    value.type,
                                input:      typeof value.type == 'string' ? value.type == 'boolean' ? 'boolean' : 'input'
                                                                          : 'select',
                                selected:   val ? val : undefined,
                                'default':  value['default'],
                                value:      val,
                                required:   value.required,
                                unique:     value.unique
                    };
                })
			}
			// move the 'name' attribute to the 1st position
			if (nameIndex > -1) {
				var tmp = info.attributes[0];
				info.attributes[0] = info.attributes[nameIndex];
				info.attributes[nameIndex] = tmp;
			}
			return info;
		}

		// remove the annotation fields
		var stripAnnotations = function (entityName, ent, annotations) {
			if (ent.references) {
				var newEnt = {attributes: {}};
				ent.references.forEach( function (annoKey) {
					if (!annotations[annoKey])
						annotations[annoKey] = {};
					annotations[annoKey][entityName] = true;    // create the key/consolidate duplicates
					var keys = Object.keys(schema.annotations[annoKey].attributes);
					for (var attrib in ent.attributes) {
						if (keys.indexOf(attrib) == -1) {
							newEnt.attributes[attrib] = ent.attributes[attrib];
						}
					}
					// add a field for the reference name
					newEnt.attributes[annoKey] = {type: 'string',
							description: 'Name of the ' + annoKey + ' section.',
							'default': annoKey, required: true};
				})
				newEnt.references = ent.references;
				newEnt.description = ent.description;
				return newEnt;
			}
			return ent;
		}

		var annotations = {};
   		myEntities.forEach(function (entityName) {
   		    var ent = schema.entityTypes[entityName];
   		    var hName = QDRService.humanify(entityName);
   		    if (entityName == 'listener')
   		        hName = "Listener for clients";
   		    var noAnnotations = stripAnnotations(entityName, ent, annotations);
			var ediv = entity(entityName, entityName, hName, noAnnotations, undefined);
			if (ediv.actualName == 'router') {
				ediv.attributes.filter(function (attr) { return attr.name == 'name'})[0].value = newname;
				// if we have any new links (connectors), then the router's mode should be interior
				if (newLinks.length) {
					var roleAttr = ediv.attributes.filter(function (attr) { return attr.name == 'mode'})[0];
					roleAttr.value = roleAttr.selected = "interior";
				}
			}
			if (ediv.actualName == 'container') {
				ediv.attributes.filter(function (attr) { return attr.name == 'containerName'})[0].value = newname + "-container";
			}
			if (ediv.actualName == 'listener') {
				// find max port number that is used in all the listeners
				ediv.attributes.filter(function (attr) { return attr.name == 'port'})[0].value = ++maxPort;
			}
			// special case for required log.module since it doesn't have a default
			if (ediv.actualName == 'log') {
				var moduleAttr = ediv.attributes.filter(function (attr) { return attr.name == 'module'})[0];
				moduleAttr.value = moduleAttr.selected = "DEFAULT";
			}
			$scope.entities.push( ediv );
   		})

		// add a tab for each annotation that was found
		var annotationEnts = [];
		for (var key in annotations) {
			ent = angular.copy(schema.annotations[key]);
			ent.attributes.name = {type: "string", unique: true, description: "Unique name that is used to refer to this set of attributes."}
			var ediv = entity(key, key+'tab', QDRService.humanify(key), ent, undefined);
			ediv.attributes.filter(function (attr) { return attr.name == 'name'})[0].value = key;
			$scope.entities.push( ediv );
			annotationEnts.push( ediv );
		}

		// add an additional listener tab if any links are reversed
		ent = schema.entityTypes['listener'];
		newLinks.some(function (link) {
			if (link.__data__.right) {
	   		    var noAnnotations = stripAnnotations('listener', ent, annotations);
				var ediv = entity("listener", "listener0", "Listener (internal)", noAnnotations, undefined);
				ediv.attributes.filter(function (attr) { return attr.name == 'port'})[0].value = ++maxPort;
				// connectors from other routers need to connect to this addr:port
				myPort = maxPort;
				myAddr = ediv.attributes.filter(function (attr) { return attr.name == 'host'})[0].value

				// override the role. 'normal' is the default, but we want inter-router
				ediv.attributes.filter(function( attr ) { return attr.name == 'role'})[0].selected = 'inter-router';
				separatedEntities.push( ediv );
				return true; // stop looping
			}
			return false;   // continue looping
		})

		// Add connector tabs for each new link on the topology graph
		ent = schema.entityTypes['connector'];
		newLinks.forEach(function (link, i) {
   		    var noAnnotations = stripAnnotations('connector', ent, annotations);
			var ediv = entity('connector', 'connector' + i, " " + link.__data__.source.name, noAnnotations, link.__data__.right, link)

			// override the connector role. 'normal' is the default, but we want inter-router
			ediv.attributes.filter(function( attr ) { return attr.name == 'role'})[0].selected = 'inter-router';

			// find the addr:port of the inter-router listener to use
			var listener = nodeInfo[link.__data__.source.key]['.listener'];
			var attrs = listener.attributeNames;
			for (var i=0; i<listener.results.length; ++i) {
				var res = listener.results[i];
				var role = QDRService.valFor(attrs, res, 'role');
				if (role == 'inter-router') {
					ediv.attributes.filter(function( attr ) { return attr.name == 'host'})[0].value =
						QDRService.valFor(attrs, res, 'host')
					ediv.attributes.filter(function( attr ) { return attr.name == 'port'})[0].value =
						QDRService.valFor(attrs, res, 'port')
					break;
				}
			}
			if (link.__data__.right) {
				// connectors from other nodes need to connect to the new router's listener addr:port
   				ediv.attributes.filter(function (attr) { return attr.name == 'port'})[0].value = myPort;
   				ediv.attributes.filter(function (attr) { return attr.name == 'host'})[0].value = myAddr;

				separatedEntities.push(ediv)
			}
			else
				$scope.entities.push( ediv );
		})
		Array.prototype.push.apply($scope.entities, separatedEntities);

		// update the description on all the annotation tabs
		annotationEnts.forEach ( function (ent) {
			var shared = Object.keys(annotations[ent.actualName]);
			ent.description += " These fields are shared by " + shared.join(" and ") + ".";

		})

		$scope.testPattern = function (attr) {
			if (attr.rawtype == 'path')
				return /^(\/)?([^/\0]+(\/)?)+$/;
				//return /^(.*\/)([^/]*)$/;
			return /(.*?)/;
		}

		$scope.attributeDescription = '';
		$scope.attributeType = '';
		$scope.attributeRequired = '';
		$scope.attributeUnique = '';
		$scope.active = 'router'
		$scope.fieldsetDivs = "/fieldsetDivs.html"
		$scope.setActive = function (tabName) {
			$scope.active = tabName
		}
		$scope.isActive = function (tabName) {
			return $scope.active === tabName
		}
		$scope.showDescription = function (attr, e) {
			$scope.attributeDescription = attr.description;
			var offset = jQuery(e.currentTarget).offset()
			jQuery('.attr-description').offset({top: offset.top})

			$scope.attributeType = "Type: " + JSON.stringify(attr.rawtype);
			$scope.attributeRequired = attr.required ? 'required' : '';
			$scope.attributeUnique = attr.unique ? 'Must be unique' : '';
		}
        // handle the download button click
        // copy the dialog's values to the original node
        $scope.download = function () {
	        dialog.close({entities: $scope.entities, annotations: annotations});
        }
        $scope.cancel = function () {
            dialog.close()
        };

		$scope.selectAnnotationTab = function (tabName) {
            var tabs = $( "#tabs" ).tabs();
            tabs.tabs("select", tabName);
		}

        var initTabs = function () {
            var div = angular.element("#tabs");
            if (!div.width()) {
                setTimeout(initTabs, 100);
                return;
            }
            $( "#tabs" )
                .tabs()
                .addClass('ui-tabs-vertical ui-helper-clearfix');
        }
        // start the update loop
        initTabs();

  });

QDR.module.controller("QDR.DownloadDialogController", function($scope, QDRService, $templateCache, $window, dialog, results) {
		var result = results.entities;
		var annotations = results.annotations;
		var annotationKeys = Object.keys(annotations);
		var annotationSections = {};

		// use the router's name as the file name if present
		$scope.newRouterName = 'router';
		result.forEach( function (e) {
			if (e.actualName == 'router') {
				e.attributes.forEach( function (a) {
					if (a.name == 'name') {
						$scope.newRouterName = a.value;
					}
				})
			}
		})
		$scope.newRouterName = $scope.newRouterName + ".conf";

		var template = $templateCache.get('config-file-header.html');
		$scope.verbose = true;
		$scope.$watch('verbose', function (newVal) {
			if (newVal !== undefined) {
				// recreate output using current verbose setting
				getOutput();
			}
		})

		var getOutput = function () {
			$scope.output = template + '\n';
			$scope.parts = [];
			var commentChar = '#'
			result.forEach(function (entity) {
				// don't output a section for annotations, they get flattened into the entities
				var section = "";
				if (entity.icon) {
					section += "##\n## Add to " + entity.link.__data__.source.name + "'s configuration file\n##\n";
				}
				section += "##\n## " + QDRService.humanify(entity.actualName) + " - " + entity.description + "\n##\n";
				section += entity.actualName + " {\n";
				entity.attributes.forEach(function (attribute) {
					if (attribute.input == 'select')
						attribute.value = attribute.selected;

					// treat values with all spaces and empty strings as undefined
					attribute.value = String(attribute.value).trim();
					if (attribute.value === 'undefined' || attribute.value === '')
						attribute.value = undefined;

					if ($scope.verbose) {
						commentChar = attribute.required || attribute.value != attribute['default'] ? ' ' : '#';
						if (!attribute.value) {
							commentChar = '#';
							attribute.value = '';
						}
						section += commentChar + "    "
							+ attribute.name + ":" + Array(Math.max(20 - attribute.name.length, 1)).join(" ")
							+ attribute.value
						    + Array(Math.max(20 - ((attribute.value)+"").length, 1)).join(" ")
							+ '# ' + attribute.description
						    + "\n";
					} else {
						if (attribute.value) {
							if (attribute.value != attribute['default'] || attribute.required)
								section += "    "
									+ attribute.name + ":" + Array(20 - attribute.name.length).join(" ")
									+ attribute.value + "\n";

						}
					}
				})
				section += "}\n\n";
				// if entity.icon is true, this is a connector intended for another router
				if (entity.icon)
					$scope.parts.push({output: section,
								link: entity.link,
								name: entity.link.__data__.source.name,
								references: entity.references});
				else
					$scope.output += section;

				// if this section is actually an annotation
				if (annotationKeys.indexOf(entity.actualName) > -1) {
					annotationSections[entity.actualName] = section;
				}
			})
			// go back and add annotation sections to the parts
			$scope.parts.forEach (function (part) {
				for (var section in annotationSections) {
					if (part.references.indexOf(section) > -1) {
						part.output += annotationSections[section];
					}
				}
			})
			QDR.log.debug($scope.output);
		}

        // handle the download button click
        $scope.download = function () {
			var output = $scope.output + "\n\n"
			var blob = new Blob([output], { type: 'text/plain;charset=utf-16' });
			saveAs(blob, $scope.newRouterName);
        }

		$scope.downloadPart = function (part) {
			var linkName = part.link.__data__.source.name + 'additional.conf';
			var blob = new Blob([part.output], { type: 'text/plain;charset=utf-16' });
			saveAs(blob, linkName);
		}

		$scope.done = function () {
	        dialog.close();
		}
});

  return QDR;
}(QDR || {}));
