/*
 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.
*/

solrAdminApp.controller('CloudController',
    function($scope, $location, Zookeeper, Constants, Collections, System, Metrics, ZookeeperStatus) {

        $scope.showDebug = false;

        $scope.$on("cloud-dump", function(event) {
            $scope.showDebug = true;
        });

        $scope.closeDebug = function() {
            $scope.showDebug = false;
        };

        var view = $location.search().view ? $location.search().view : "nodes";
        if (view === "tree") {
            $scope.resetMenu("cloud-tree", Constants.IS_ROOT_PAGE);
            treeSubController($scope, Zookeeper);
        } else if (view === "graph") {
            $scope.resetMenu("cloud-graph", Constants.IS_ROOT_PAGE);
            graphSubController($scope, Zookeeper, false);
        } else if (view === "nodes") {
            $scope.resetMenu("cloud-nodes", Constants.IS_ROOT_PAGE);
            nodesSubController($scope, Collections, System, Metrics);
        } else if (view === "zkstatus") {
            $scope.resetMenu("cloud-zkstatus", Constants.IS_ROOT_PAGE);
            zkStatusSubController($scope, ZookeeperStatus, false);
        }
    }
);

function getOrCreateObj(name, object) {
  if (name in object) {
    entry = object[name];
  } else {
    entry = {};
    object[name] = entry;
  }
  return entry;
}

function getOrCreateList(name, object) {
  if (name in object) {
    entry = object[name];
  } else {
    entry = [];
    object[name] = entry;
  }
  return entry;
}

function ensureInList(string, list) {
  if (list.indexOf(string) === -1) {
    list.push(string);
  }
}

/* Puts a node name into the hosts structure */
function ensureNodeInHosts(node_name, hosts) {
  var hostName = node_name.split(":")[0];
  var host = getOrCreateObj(hostName, hosts);
  var hostNodes = getOrCreateList("nodes", host);
  ensureInList(node_name, hostNodes);
}

// from http://scratch99.com/web-development/javascript/convert-bytes-to-mb-kb/
function bytesToSize(bytes) {
  var sizes = ['b', 'Kb', 'Mb', 'Gb', 'Tb'];
  if (bytes === 0) return '0b';
  var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
  if (bytes === 0) return bytes + '' + sizes[i];
  return (bytes / Math.pow(1024, i)).toFixed(1) + '' + sizes[i];
}

function numDocsHuman(docs) {
  var sizes = ['', 'k', 'mn', 'bn', 'tn'];
  if (docs === 0) return '0';
  var i = parseInt(Math.floor(Math.log(docs) / Math.log(1000)));
  if (i === 0) return docs + '' + sizes[i];
  return (docs / Math.pow(1000, i)).toFixed(1) + '' + sizes[i];
}

/* Returns a style class depending on percentage */
var styleForPct = function (pct) {
  if (pct < 60) return "pct-normal";
  if (pct < 80) return "pct-warn";
  return "pct-critical"
};

function isNumeric(n) {
  return !isNaN(parseFloat(n)) && isFinite(n);
}

function coreNameToLabel(name) {
  return name.replace(/(.*?)_shard((\d+_?)+)_replica_?[ntp]?(\d+)/, '\$1_s\$2r\$4');
}

var nodesSubController = function($scope, Collections, System, Metrics) {
  $scope.pageSize = 10;
  $scope.showNodes = true;
  $scope.showTree = false;
  $scope.showGraph = false;
  $scope.showData = false;
  $scope.showAllDetails = false;
  $scope.showDetails = {};
  $scope.from = 0;
  $scope.to = $scope.pageSize - 1;
  $scope.filterType = "node"; // Pre-initialize dropdown

  $scope.toggleAllDetails = function() {
    $scope.showAllDetails = !$scope.showAllDetails;
    for (var node in $scope.nodes) {
      $scope.showDetails[node] = $scope.showAllDetails;
    }
    for (var host in $scope.hosts) {
      $scope.showDetails[host] = $scope.showAllDetails;
    }
  };

  $scope.toggleDetails = function(key) {
    $scope.showDetails[key] = !$scope.showDetails[key] === true;
  };

  $scope.toggleHostDetails = function(key) {
    $scope.showDetails[key] = !$scope.showDetails[key] === true;
    for (var nodeId in $scope.hosts[key].nodes) {
      var node = $scope.hosts[key].nodes[nodeId];
      $scope.showDetails[node] = $scope.showDetails[key];
    }
  };

  $scope.nextPage = function() {
    $scope.from += parseInt($scope.pageSize);
    $scope.reload();
  };

  $scope.previousPage = function() {
    $scope.from = Math.max(0, $scope.from - parseInt($scope.pageSize));
    $scope.reload();
  };
  
  // Checks if this node is the first (alphabetically) for a given host. Used to decide rowspan in table
  $scope.isFirstNodeForHost = function(node) {
    var hostName = node.split(":")[0]; 
    var nodesInHost = $scope.filteredNodes.filter(function (node) {
      return node.startsWith(hostName);
    });
    return nodesInHost[0] === node;
  };
  
  // Returns the first live node for this host, to make sure we pick host-level metrics from a live node
  $scope.firstLiveNodeForHost = function(key) {
    var hostName = key.split(":")[0]; 
    var liveNodesInHost = $scope.filteredNodes.filter(function (key) {
      return key.startsWith(hostName);
    }).filter(function (key) {
      return $scope.live_nodes.includes(key);
    });
    return liveNodesInHost.length > 0 ? liveNodesInHost[0] : key; 
  };

  // Initializes the cluster state, list of nodes, collections etc
  $scope.initClusterState = function() {
    var nodes = {};
    var hosts = {};
    var live_nodes = [];

    // We build a node-centric view of the cluster state which we can easily consume to render the table
    Collections.status(function (data) {
      // Fetch cluster state from collections API and invert to a nodes structure
      for (var name in data.cluster.collections) {
        var collection = data.cluster.collections[name];
        collection.name = name;
        var shards = collection.shards;
        collection.shards = [];
        for (var shardName in shards) {
          var shard = shards[shardName];
          shard.name = shardName;
          shard.collection = collection.name;
          var replicas = shard.replicas;
          shard.replicas = [];
          for (var replicaName in replicas) {
            var core = replicas[replicaName];
            core.name = replicaName;
            core.label = coreNameToLabel(core['core']);
            core.collection = collection.name;
            core.shard = shard.name;
            core.shard_state = shard.state;

            var node_name = core['node_name'];
            var node = getOrCreateObj(node_name, nodes);
            var cores = getOrCreateList("cores", node);
            cores.push(core);
            node['base_url'] = core.base_url;
            node['id'] = core.base_url.replace(/[^\w\d]/g, '');
            node['host'] = node_name.split(":")[0];
            var collections = getOrCreateList("collections", node);
            ensureInList(core.collection, collections);
            ensureNodeInHosts(node_name, hosts);
          }
        }
      }

      live_nodes = data.cluster.live_nodes;
      for (n in data.cluster.live_nodes) {
        node = data.cluster.live_nodes[n];
        if (!(node in nodes)) {
          var hostName = node.split(":")[0];
          nodes[node] = {};
          nodes[node]['host'] = hostName;
        }
        ensureNodeInHosts(node, hosts);
      }

      // Make sure nodes are sorted alphabetically to align with rowspan in table 
      for (var host in hosts) {
        hosts[host].nodes.sort();
      }

      $scope.nodes = nodes;
      $scope.hosts = hosts;
      $scope.live_nodes = live_nodes;

      $scope.Math = window.Math;
      $scope.reload();
    });
  };

  $scope.filterInput = function() {
    $scope.from = 0;
    $scope.to = $scope.pageSize - 1;
    $scope.reload();
  };

  /*
    Reload will fetch data for the current page of the table and thus refresh numbers.
    It is also called whenever a filter or paging action is executed 
   */
  $scope.reload = function() {
    var nodes = $scope.nodes;
    var node_keys = Object.keys(nodes);
    var hosts = $scope.hosts;
    var live_nodes = $scope.live_nodes;
    var hostNames = Object.keys(hosts);
    hostNames.sort();
    var pageSize = isNumeric($scope.pageSize) ? $scope.pageSize : 10;

    // Calculate what nodes that will show on this page
    var nodesToShow = [];
    var nodesParam;
    var hostsToShow = [];
    var filteredNodes;
    var filteredHosts;
    var isFiltered = false;
    switch ($scope.filterType) {
      case "node":  // Find what nodes match the node filter
        if ($scope.nodeFilter) {
          filteredNodes = node_keys.filter(function (node) {
            return node.indexOf($scope.nodeFilter) !== -1;
          });
        }
        break;

      case "collection": // Find what collections match the collection filter and what nodes that have these collections
        if ($scope.collectionFilter) {
          candidateNodes = {};
          nodesCollections = [];
          for (var i = 0 ; i < node_keys.length ; i++) {
            var node_name = node_keys[i];
            var node = nodes[node_name];
            nodeColl = {};
            nodeColl['node'] = node_name;
            collections = {};
            node.cores.forEach(function(core) {
              collections[core.collection] = true;
            });
            nodeColl['collections'] = Object.keys(collections);
            nodesCollections.push(nodeColl);
          }
          nodesCollections.forEach(function(nc) {
            matchingColls = nc['collections'].filter(function (collection) {
              return collection.indexOf($scope.collectionFilter) !== -1;
            });
            if (matchingColls.length > 0) {
              candidateNodes[nc.node] = true;
            }
          });
          filteredNodes = Object.keys(candidateNodes);
        }
        break;

      case "health":

    }
    
    if (filteredNodes) {
      // If filtering is active, calculate what hosts contain the nodes that match the filters
      isFiltered = true;
      filteredHosts = filteredNodes.map(function (node) {
        return node.split(":")[0];
      }).filter(function (item, index, self) {
        return self.indexOf(item) === index;
      });
    } else {
      filteredNodes = node_keys;
      filteredHosts = hostNames;
    }
    filteredNodes.sort();
    filteredHosts.sort();
    
    // Find what hosts & nodes (from the filtered set) that should be displayed on current page
    for (var id = $scope.from ; id < $scope.from + pageSize && filteredHosts[id] ; id++) {
      var hostName = filteredHosts[id];
      hostsToShow.push(hostName);
      if (isFiltered) { // Only show the nodes per host matching active filter
        nodesToShow = nodesToShow.concat(filteredNodes.filter(function (node) {
          return node.startsWith(hostName);
        }));
      } else {
        nodesToShow = nodesToShow.concat(hosts[hostName]['nodes']);
      }
    }
    nodesParam = nodesToShow.filter(function (node) {
      return live_nodes.includes(node); 
    }).join(',');
    var deadNodes = nodesToShow.filter(function (node) {
      return !live_nodes.includes(node);
    });
    deadNodes.forEach(function (node) {
      nodes[node]['dead'] = true;
    });
    $scope.nextEnabled = $scope.from + pageSize < filteredHosts.length;
    $scope.prevEnabled = $scope.from - pageSize >= 0;
    nodesToShow.sort();
    hostsToShow.sort();

    /*
     Fetch system info for all selected nodes
     Pick the data we want to display and add it to the node-centric data structure
      */
    System.get({"nodes": nodesParam}, function (systemResponse) {
      for (var node in systemResponse) {
        if (node in nodes) {
          var s = systemResponse[node];
          nodes[node]['system'] = s;
          var memTotal = s.system.totalPhysicalMemorySize;
          var memFree = s.system.freePhysicalMemorySize;
          var memPercentage = Math.floor((memTotal - memFree) / memTotal * 100);
          nodes[node]['memUsedPct'] = memPercentage;
          nodes[node]['memUsedPctStyle'] = styleForPct(memPercentage);
          nodes[node]['memTotal'] = bytesToSize(memTotal);
          nodes[node]['memFree'] = bytesToSize(memFree);
          nodes[node]['memUsed'] = bytesToSize(memTotal - memFree);

          var heapTotal = s.jvm.memory.raw.total;
          var heapFree = s.jvm.memory.raw.free;
          var heapPercentage = Math.floor((heapTotal - heapFree) / heapTotal * 100);
          nodes[node]['heapUsed'] = bytesToSize(heapTotal - heapFree);
          nodes[node]['heapUsedPct'] = heapPercentage;
          nodes[node]['heapUsedPctStyle'] = styleForPct(heapPercentage);
          nodes[node]['heapTotal'] = bytesToSize(heapTotal);
          nodes[node]['heapFree'] = bytesToSize(heapFree);

          var jvmUptime = s.jvm.jmx.upTimeMS / 1000; // Seconds
          nodes[node]['jvmUptime'] = secondsForHumans(jvmUptime);
          nodes[node]['jvmUptimeSec'] = jvmUptime;

          nodes[node]['uptime'] = s.system.uptime.replace(/.*up (.*?,.*?),.*/, "$1");
          nodes[node]['loadAvg'] = Math.round(s.system.systemLoadAverage * 100) / 100;
          nodes[node]['cpuPct'] = Math.ceil(s.system.processCpuLoad);
          nodes[node]['cpuPctStyle'] = styleForPct(Math.ceil(s.system.processCpuLoad));
          nodes[node]['maxFileDescriptorCount'] = s.system.maxFileDescriptorCount;
          nodes[node]['openFileDescriptorCount'] = s.system.openFileDescriptorCount;
        }
      }
    });

    /*
     Fetch metrics for all selected nodes. Only pull the metrics that we'll show to save bandwidth
     Pick the data we want to display and add it to the node-centric data structure
      */
    Metrics.get({
          "nodes": nodesParam,
          "prefix": "CONTAINER.fs,org.eclipse.jetty.server.handler.DefaultHandler.get-requests,INDEX.sizeInBytes,SEARCHER.searcher.numDocs,SEARCHER.searcher.deletedDocs,SEARCHER.searcher.warmupTime"
        },
        function (metricsResponse) {
          for (var node in metricsResponse) {
            if (node in nodes) {
              var m = metricsResponse[node];
              nodes[node]['metrics'] = m;
              var diskTotal = m.metrics['solr.node']['CONTAINER.fs.totalSpace'];
              var diskFree = m.metrics['solr.node']['CONTAINER.fs.usableSpace'];
              var diskPercentage = Math.floor((diskTotal - diskFree) / diskTotal * 100);
              nodes[node]['diskUsedPct'] = diskPercentage;
              nodes[node]['diskUsedPctStyle'] = styleForPct(diskPercentage);
              nodes[node]['diskTotal'] = bytesToSize(diskTotal);
              nodes[node]['diskFree'] = bytesToSize(diskFree);

              var r = m.metrics['solr.jetty']['org.eclipse.jetty.server.handler.DefaultHandler.get-requests'];
              nodes[node]['req'] = r.count;
              nodes[node]['req1minRate'] = Math.floor(r['1minRate'] * 100) / 100;
              nodes[node]['req5minRate'] = Math.floor(r['5minRate'] * 100) / 100;
              nodes[node]['req15minRate'] = Math.floor(r['15minRate'] * 100) / 100;
              nodes[node]['reqp75_ms'] = Math.floor(r['p75_ms']);
              nodes[node]['reqp95_ms'] = Math.floor(r['p95_ms']);
              nodes[node]['reqp99_ms'] = Math.floor(r['p99_ms']);

              var cores = nodes[node]['cores'];
              var indexSizeTotal = 0;
              var docsTotal = 0;
              var graphData = [];
              if (cores) {
                for (coreId in cores) {
                  var core = cores[coreId];
                  var keyName = "solr.core." + core['core'].replace(/(.*?)_(shard(\d+_?)+)_(replica.*?)/, '\$1.\$2.\$4');
                  var nodeMetric = m.metrics[keyName];
                  var size = nodeMetric['INDEX.sizeInBytes'];
                  size = (typeof size !== 'undefined') ? size : 0;
                  core['sizeInBytes'] = size;
                  core['size'] = bytesToSize(size);
                  if (core['shard_state'] !== 'active' || core['state'] !== 'active') {
                    // If core state is not active, display the real state, or if shard is inactive, display that
                    var labelState = (core['state'] !== 'active') ? core['state'] : core['shard_state'];
                    core['label'] += "_(" + labelState + ")";
                  }
                  indexSizeTotal += size;
                  var numDocs = nodeMetric['SEARCHER.searcher.numDocs'];
                  numDocs = (typeof numDocs !== 'undefined') ? numDocs : 0;
                  core['numDocs'] = numDocs;
                  core['numDocsHuman'] = numDocsHuman(numDocs);
                  core['avgSizePerDoc'] = bytesToSize(numDocs === 0 ? 0 : size / numDocs);
                  var deletedDocs = nodeMetric['SEARCHER.searcher.deletedDocs'];
                  deletedDocs = (typeof deletedDocs !== 'undefined') ? deletedDocs : 0;
                  core['deletedDocs'] = deletedDocs;
                  core['deletedDocsHuman'] = numDocsHuman(deletedDocs);
                  var warmupTime = nodeMetric['SEARCHER.searcher.warmupTime'];
                  warmupTime = (typeof warmupTime !== 'undefined') ? warmupTime : 0;
                  core['warmupTime'] = warmupTime;
                  docsTotal += core['numDocs'];
                }
                for (coreId in cores) {
                  core = cores[coreId];
                  var graphObj = {};
                  graphObj['label'] = core['label'];
                  graphObj['size'] = core['sizeInBytes'];
                  graphObj['sizeHuman'] = core['size'];
                  graphObj['pct'] = (core['sizeInBytes'] / indexSizeTotal) * 100;
                  graphData.push(graphObj);
                }
                cores.sort(function (a, b) {
                  return b.sizeInBytes - a.sizeInBytes
                });
              } else {
                cores = {};
              }
              graphData.sort(function (a, b) {
                return b.size - a.size
              });
              nodes[node]['graphData'] = graphData;
              nodes[node]['numDocs'] = numDocsHuman(docsTotal);
              nodes[node]['sizeInBytes'] = indexSizeTotal;
              nodes[node]['size'] = bytesToSize(indexSizeTotal);
              nodes[node]['sizePerDoc'] = docsTotal === 0 ? '0b' : bytesToSize(indexSizeTotal / docsTotal);

              // Build the d3 powered bar chart
              $('#chart' + nodes[node]['id']).empty();
              var chart = d3.select('#chart' + nodes[node]['id']).append('div').attr('class', 'chart');

              // Add one div per bar which will group together both labels and bars
              var g = chart.selectAll('div')
                  .data(nodes[node]['graphData']).enter()
                  .append('div');

              // Add the bars
              var bars = g.append("div")
                  .attr("class", "rect")
                  .text(function (d) {
                    return d.label + ':\u00A0\u00A0' + d.sizeHuman;
                  });

              // Execute the transition to show the bars
              bars.transition()
                  .ease('elastic')
                  .style('width', function (d) {
                    return d.pct + '%';
                  });
            }
          }
        });
    $scope.nodes = nodes;
    $scope.hosts = hosts;
    $scope.live_nodes = live_nodes;
    $scope.nodesToShow = nodesToShow;
    $scope.hostsToShow = hostsToShow;
    $scope.filteredNodes = filteredNodes;
    $scope.filteredHosts = filteredHosts;
  };
  $scope.initClusterState();
};

var zkStatusSubController = function($scope, ZookeeperStatus) {
    $scope.showZkStatus = true;
    $scope.showNodes = false;
    $scope.showTree = false;
    $scope.showGraph = false;
    $scope.tree = {};
    $scope.showData = false;
    $scope.showDetails = false;
    
    $scope.toggleDetails = function() {
      $scope.showDetails = !$scope.showDetails === true;
    };

    $scope.initZookeeper = function() {
      ZookeeperStatus.monitor({}, function(data) {
        $scope.zkState = data.zkStatus;
        $scope.mainKeys = ["ok", "clientPort", "zk_server_state", "zk_version", 
          "zk_approximate_data_size", "zk_znode_count", "zk_num_alive_connections"];
        $scope.detailKeys = ["dataDir", "dataLogDir", 
          "zk_avg_latency", "zk_max_file_descriptor_count", "zk_watch_count", 
          "zk_packets_sent", "zk_packets_received",
          "tickTime", "maxClientCnxns", "minSessionTimeout", "maxSessionTimeout"];
        $scope.ensembleMainKeys = ["serverId", "electionPort", "quorumPort"];
        $scope.ensembleDetailKeys = ["peerType", "electionAlg", "initLimit", "syncLimit",
          "zk_followers", "zk_synced_followers", "zk_pending_syncs"];
      });
    };

    $scope.initZookeeper();
};

var treeSubController = function($scope, Zookeeper) {
    $scope.showZkStatus = false;
    $scope.showTree = true;
    $scope.showGraph = false;
    $scope.tree = {};
    $scope.showData = false;

    $scope.showTreeLink = function(link) {
        var path = decodeURIComponent(link.replace(/.*[\\?&]path=([^&#]*).*/, "$1"));
        Zookeeper.detail({path: path}, function(data) {
            $scope.znode = data.znode;
            var path = data.znode.path.split( '.' );
            if(path.length >1) {
              $scope.lang = path.pop();
            } else {
              var lastPathElement = data.znode.path.split( '/' ).pop();
              if (lastPathElement == "managed-schema") {
                  $scope.lang = "xml";
              }
            }
            $scope.showData = true;
        });
    };

    $scope.hideData = function() {
        $scope.showData = false;
    };

    $scope.initTree = function() {
      Zookeeper.simple(function(data) {
        $scope.tree = data.tree;
      });
    };

    $scope.initTree();
};

/**
 * Translates seconds into human readable format of seconds, minutes, hours, days, and years
 * 
 * @param  {number} seconds The number of seconds to be processed
 * @return {string}         The phrase describing the the amount of time
 */
function secondsForHumans ( seconds ) {
    var levels = [
        [Math.floor(seconds / 31536000), 'y'],
        [Math.floor((seconds % 31536000) / 86400), 'd'],
        [Math.floor(((seconds % 31536000) % 86400) / 3600), 'h'],
        [Math.floor((((seconds % 31536000) % 86400) % 3600) / 60), 'm']
    ];
    var returntext = '';

    for (var i = 0, max = levels.length; i < max; i++) {
        if ( levels[i][0] === 0 ) continue;
        returntext += ' ' + levels[i][0] + levels[i][1];
    }
    return returntext.trim() === '' ? '0m' : returntext.trim();
}

var graphSubController = function ($scope, Zookeeper) {
    $scope.showZkStatus = false;
    $scope.showTree = false;
    $scope.showGraph = true;

    $scope.filterType = "status";

    $scope.helperData = {
        protocol: [],
        host: [],
        hostname: [],
        port: [],
        pathname: [],
        replicaType: [],
        base_url: [],
        core: [],
        node_name: [],
        state: [],
        core_node: []
    };

    $scope.next = function() {
        $scope.pos += $scope.rows;
        $scope.initGraph();
    };

    $scope.previous = function() {
        $scope.pos = Math.max(0, $scope.pos - $scope.rows);
        $scope.initGraph();
    };

    $scope.resetGraph = function() {
        $scope.pos = 0;
        $scope.initGraph();
    };

    $scope.initGraph = function() {
        Zookeeper.liveNodes(function (data) {
            var live_nodes = {};
            for (var c in data.tree[0].children) {
                live_nodes[data.tree[0].children[c].data.title] = true;
            }

            var params = {view: "graph"};
            if ($scope.rows) {
                params.start = $scope.pos;
                params.rows = $scope.rows;
            }

            var filter = ($scope.filterType=='status') ? $scope.pagingStatusFilter : $scope.pagingFilter;

            if (filter) {
                params.filterType = $scope.filterType;
                params.filter = filter;
            }

            Zookeeper.clusterState(params, function (data) {
                    eval("var state=" + data.znode.data); // @todo fix horrid means to parse JSON

                    var leaf_count = 0;
                    var graph_data = {
                        name: null,
                        children: []
                    };

                    for (var c in state) {
                        var shards = [];
                        for (var s in state[c].shards) {
                            var shard_status = state[c].shards[s].state;
                            shard_status = shard_status == 'inactive' ? 'shard-inactive' : shard_status;
                            var nodes = [];
                            for (var n in state[c].shards[s].replicas) {
                                leaf_count++;
                                var replica = state[c].shards[s].replicas[n]

                                var uri = replica.base_url;
                                var parts = uri.match(/^(\w+:)\/\/(([\w\d\.-]+)(:(\d+))?)(.+)$/);
                                var uri_parts = {
                                    protocol: parts[1],
                                    host: parts[2],
                                    hostname: parts[3],
                                    port: parseInt(parts[5] || 80, 10),
                                    pathname: parts[6],
                                    replicaType: replica.type,
                                    base_url: replica.base_url,
                                    core: replica.core,
                                    node_name: replica.node_name,
                                    state: replica.state,
                                    core_node: n
                                };

                                $scope.helperData.protocol.push(uri_parts.protocol);
                                $scope.helperData.host.push(uri_parts.host);
                                $scope.helperData.hostname.push(uri_parts.hostname);
                                $scope.helperData.port.push(uri_parts.port);
                                $scope.helperData.pathname.push(uri_parts.pathname);
                                $scope.helperData.replicaType.push(uri_parts.replicaType);
                                $scope.helperData.base_url.push(uri_parts.base_url);
                                $scope.helperData.core.push(uri_parts.core);
                                $scope.helperData.node_name.push(uri_parts.node_name);
                                $scope.helperData.state.push(uri_parts.state);
                                $scope.helperData.core_node.push(uri_parts.core_node);

                                var replica_status = replica.state;

                                if (!live_nodes[replica.node_name]) {
                                    replica_status = 'gone';
                                } else if(shard_status=='shard-inactive') {
                                    replica_status += ' ' + shard_status;
                                }

                                var node = {
                                    name: uri,
                                    data: {
                                        type: 'node',
                                        state: replica_status,
                                        leader: 'true' === replica.leader,
                                        uri: uri_parts
                                    }
                                };
                                nodes.push(node);
                            }

                            var shard = {
                                name: shard_status == "shard-inactive" ? s + ' (inactive)' : s,
                                data: {
                                    type: 'shard',
                                    state: shard_status
                                },
                                children: nodes
                            };
                            shards.push(shard);
                        }

                        var collection = {
                            name: c,
                            data: {
                                type: 'collection'
                            },
                            children: shards
                        };
                        graph_data.children.push(collection);
                    }

                    $scope.helperData.protocol = $.unique($scope.helperData.protocol);
                    $scope.helperData.host = $.unique($scope.helperData.host);
                    $scope.helperData.hostname = $.unique($scope.helperData.hostname);
                    $scope.helperData.port = $.unique($scope.helperData.port);
                    $scope.helperData.pathname = $.unique($scope.helperData.pathname);
                    $scope.helperData.replicaType = $.unique($scope.helperData.replicaType);
                    $scope.helperData.base_url = $.unique($scope.helperData.base_url);
                    $scope.helperData.core = $.unique($scope.helperData.core);
                    $scope.helperData.node_name = $.unique($scope.helperData.node_name);
                    $scope.helperData.state = $.unique($scope.helperData.state);
                    $scope.helperData.core_node = $.unique($scope.helperData.core_node);

                    if (data.znode && data.znode.paging) {
                        $scope.showPaging = true;

                        var parr = data.znode.paging.split('|');
                        if (parr.length < 3) {
                            $scope.showPaging = false;
                        } else {
                            $scope.start = Math.max(parseInt(parr[0]), 0);
                            $scope.prevEnabled = ($scope.start > 0);
                            $scope.rows = parseInt(parr[1]);
                            $scope.total = parseInt(parr[2]);

                            if ($scope.rows == -1) {
                                $scope.showPaging = false;
                            } else {
                                var filterType = parr.length > 3 ? parr[3] : '';

                                if (filterType == '' || filterType == 'none') filterType = 'status';

                                +$('#cloudGraphPagingFilterType').val(filterType);

                                var filter = parr.length > 4 ? parr[4] : '';
                                var page = Math.floor($scope.start / $scope.rows) + 1;
                                var pages = Math.ceil($scope.total / $scope.rows);
                                $scope.last = Math.min($scope.start + $scope.rows, $scope.total);
                                $scope.nextEnabled = ($scope.last < $scope.total);
                            }
                        }
                    }
                    else {
                        $scope.showPaging = false;
                    }
                    $scope.graphData = graph_data;
                    $scope.leafCount = leaf_count;
                });
        });
    };

    $scope.initGraph();
    $scope.pos = 0;
};

solrAdminApp.directive('graph', function(Constants) {
    return {
        restrict: 'EA',
        scope: {
            data: "=",
            leafCount: "=",
            helperData: "=",
        },
        link: function (scope, element, attrs) {
            var helper_path_class = function (p) {
                var classes = ['link'];
                classes.push('lvl-' + p.target.depth);

                if (p.target.data && p.target.data.leader) {
                    classes.push('leader');
                }

                if (p.target.data && p.target.data.state) {
                    classes.push(p.target.data.state);
                }

                return classes.join(' ');
            };

            var helper_node_class = function (d) {
                var classes = ['node'];
                classes.push('lvl-' + d.depth);

                if (d.data && d.data.leader) {
                    classes.push('leader');
                }

                if (d.data && d.data.state) {
                    if(!(d.data.type=='shard' && d.data.state=='active')){
                        classes.push(d.data.state);
                    }
                }

                return classes.join(' ');
            };

            var helper_tooltip_text = function (d) {
                if (!d.data || !d.data.uri) {
                    return tooltip;
                }

                var tooltip = d.data.uri.core_node + " {<br/>";

                if (0 !== scope.helperData.core.length) {
                    tooltip += "core: [" + d.data.uri.core + "],<br/>";
                }

                if (0 !== scope.helperData.node_name.length) {
                    tooltip += "node_name: [" + d.data.uri.node_name + "],<br/>";
                }

                tooltip += "}";

                return tooltip;
            };

            var helper_node_text = function (d) {
                if (!d.data || !d.data.uri) {
                    return d.name;
                }

                var name = d.data.uri.hostname;

                if (1 !== scope.helperData.protocol.length) {
                    name = d.data.uri.protocol + '//' + name;
                }

                if (1 !== scope.helperData.port.length) {
                    name += ':' + d.data.uri.port;
                }

                if (1 !== scope.helperData.pathname.length) {
                    name += d.data.uri.pathname;
                }

                if(0 !== scope.helperData.replicaType.length) {
                    name += ' (' + d.data.uri.replicaType[0] + ')';
                }

                return name;
            };

            scope.$watch("data", function(newValue, oldValue) {
                if (newValue) {
                    flatGraph(element, scope.data, scope.leafCount);
                }

                $('text').tooltip({
                    content: function() {
                        return $(this).attr('title');
                    }
                });
            });


            function setNodeNavigationBehavior(node, view){
                node
                .attr('data-href', function (d) {
                    if (d.type == "node"){
                        return getNodeUrl(d, view);
                    }
                    else{
                        return "";
                    }
                })
                .on('click', function(d) {
                    if (d.data.type == "node"){
                        location.href = getNodeUrl(d, view);
                    }
                });
            }

            function getNodeUrl(d, view){
                var url = d.name + Constants.ROOT_URL + "#/~cloud";
                if (view != undefined){
                    url += "?view=" + view;
                }
                return url;
            }

            var flatGraph = function(element, graphData, leafCount) {
                var w = element.width(),
                    h = leafCount * 20;

                var tree = d3.layout.tree().size([h, w - 400]);

                var diagonal = d3.svg.diagonal().projection(function (d) {
                    return [d.y, d.x];
                });

                d3.select('#canvas', element).html('');
                var vis = d3.select('#canvas', element).append('svg')
                    .attr('width', w)
                    .attr('height', h)
                    .append('g')
                    .attr('transform', 'translate(100, 0)');

                var nodes = tree.nodes(graphData);

                var link = vis.selectAll('path.link')
                    .data(tree.links(nodes))
                    .enter().append('path')
                    .attr('class', helper_path_class)
                    .attr('d', diagonal);

                var node = vis.selectAll('g.node')
                    .data(nodes)
                    .enter().append('g')
                    .attr('class', helper_node_class)
                    .attr('transform', function (d) {
                        return 'translate(' + d.y + ',' + d.x + ')';
                    })

                node.append('circle')
                    .attr('r', 4.5);

                node.append('text')
                    .attr('dx', function (d) {
                        return 0 === d.depth ? -8 : 8;
                    })
                    .attr('dy', function (d) {
                        return 5;
                    })
                    .attr('text-anchor', function (d) {
                        return 0 === d.depth ? 'end' : 'start';
                    })
                    .attr("title", helper_tooltip_text)
                    .text(helper_node_text);

                setNodeNavigationBehavior(node);
            };
        }
    };
});
