var DEPTH_HEIGHT = 100;
var DEPTH_WIDTH = 132;

/**
 * Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock)
 */
function ImageHistoryTree(namespace, name, images, formatComment, formatTime) {
    /**
     * The namespace of the repo.
     */
    this.repoNamespace_ = namespace;

    /**
     * The name of the repo.
     */
    this.repoName_ = name;

    /**
     * The images to display.
     */
    this.images_ = images;

    /**
     * Method to invoke to format a comment for an image.
     */
    this.formatComment_ = formatComment;

    /**
     * Method to invoke to format the time for an image.
     */
    this.formatTime_ = formatTime;

    /**
     * The current tag (if any).
     */
    this.currentTag_ = null;

    /**
     * The current image (if any).
     */
    this.currentImage_ = null;

    /**
     * Counter for creating unique IDs.
     */
    this.idCounter_ = 0;
}


/**
 * Calculates the dimensions of the tree.
 */
ImageHistoryTree.prototype.calculateDimensions_ = function(container) {
    var cw = Math.max(document.getElementById(container).clientWidth, this.maxWidth_ * DEPTH_WIDTH);
    var ch = this.maxHeight_ * (DEPTH_HEIGHT + 10);

    var margin = { top: 40, right: 20, bottom: 20, left: 40 };
    var m = [margin.top, margin.right, margin.bottom, margin.left];
    var w = cw - m[1] - m[3];
    var h = ch - m[0] - m[2];

    return {
        'w': w,
        'h': h,
        'm': m,
        'cw': cw,
        'ch': ch
    };
};


/**
 * Updates the dimensions of the tree.
 */
ImageHistoryTree.prototype.updateDimensions_ = function() {
    var container = this.container_;
    var dimensions = this.calculateDimensions_(container);

    var m = dimensions.m;
    var w = dimensions.w;
    var h = dimensions.h;
    var cw = dimensions.cw;
    var ch = dimensions.ch;

    // Set the height of the container so that it never goes offscreen.
    $('#' + container).removeOverscroll();
    var viewportHeight = $(window).height();
    var boundingBox = document.getElementById(container).getBoundingClientRect();
    document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 30) + 'px';
    $('#' + container).overscroll();

    // Update the tree.
    var rootSvg = this.rootSvg_;
    var tree = this.tree_;
    var vis = this.vis_;

    rootSvg
        .attr("width", w + m[1] + m[3])
        .attr("height", h + m[0] + m[2]);

    tree.size([w, h]);
    vis.attr("transform", "translate(" + m[3] + "," + m[0] + ")");

    return dimensions;
};


/**
 * Draws the tree.
 */
ImageHistoryTree.prototype.draw = function(container) {
    // Build the root of the tree.
    var result = this.buildRoot_();
    this.maxWidth_ = result['maxWidth'];
    this.maxHeight_ = result['maxHeight'];

    // Save the container.
    this.container_ = container;
 
    // Create the tree and all its components.
    var tree = d3.layout.tree()
        .separation(function() { return 2; });

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

    var rootSvg =  d3.select("#" + container).append("svg:svg")
        .attr("class", "image-tree");

    var vis = rootSvg.append("svg:g");

    var formatComment = this.formatComment_;
    var formatTime = this.formatTime_;
    var tip = d3.tip()
        .attr('class', 'd3-tip')
        .offset([-1, 24])
        .direction('e')
        .html(function(d) {
            var html = '';
            if (d.virtual) {
                return d.name;
            }

            if (d.collapsed) {
                for (var i = 1; i < d.encountered.length; ++i) {
                    html += '<span>' + d.encountered[i].image.id.substr(0, 12) + '</span>';
                    html += '<span class="created">' + formatTime(d.encountered[i].image.created) + '</span>';
                }
                return html;
            }

            if (!d.image) {
                return '(This repository is empty)';
            }

            if (d.image.comment) {
                html += '<span class="comment">' + formatComment(d.image.comment) + '</span>';
            }
            html += '<span class="created">' + formatTime(d.image.created) + '</span>';
            html += '<span class="full-id">' + d.image.id + '</span>';
            return html;
        })

    vis.call(tip);

    // Save all the state created.
    this.diagonal_ = diagonal;
    this.vis_ = vis;
    this.rootSvg_ = rootSvg;
    this.tip_ = tip;
    this.tree_ = tree;

    // Update the dimensions of the tree.
    var dimensions = this.updateDimensions_();

    // Populate the tree.
    this.root_.x0 = dimensions.cw / 2;
    this.root_.y0 = 0;

    this.setTag_(this.currentTag_);

    $('#' + container).overscroll();
};


/**
 * Redraws the image history to fit the new size.
 */
ImageHistoryTree.prototype.notifyResized = function() {
  this.updateDimensions_();
  this.update_(this.root_);
};


/**
 * Sets the current tag displayed in the tree.
 */
ImageHistoryTree.prototype.setTag = function(tagName) {
  this.setTag_(tagName);
};


/**
 * Sets the current image displayed in the tree.
 */
ImageHistoryTree.prototype.setImage = function(imageId) {
  this.setImage_(imageId);
};


/**
 * Returns the ancestors of the given image.
 */
ImageHistoryTree.prototype.getAncestors_ = function(image) {
  var ancestorsString = image.ancestors;

  // Remove the starting and ending /s.
  ancestorsString = ancestorsString.substr(1, ancestorsString.length - 2);

  // Split based on /.
  ancestors = ancestorsString.split('/');
  return ancestors;
};


/**
 * Sets the current tag displayed in the tree and raises the event that the tag
 * was changed.
 */
ImageHistoryTree.prototype.changeTag_ = function(tagName) {
  $(this).trigger({
   'type': 'tagChanged',
   'tag': tagName
  });
  this.setTag_(tagName);
};


/**
 * Sets the current image displayed in the tree and raises the event that the image
 * was changed.
 */
ImageHistoryTree.prototype.changeImage_ = function(imageId) {
  $(this).trigger({
   'type': 'imageChanged',
   'image': this.findImage_(function(image) { return image.id == imageId; })
  });
  this.setImage_(imageId);
};


/**
 * Builds the root node for the tree.
 */
ImageHistoryTree.prototype.buildRoot_ = function() {
    // Build the formatted JSON block for the tree. It must be of the form:
    //  {
    //    "name": "...",
    //    "children": [...]
    //  }
    var formatted = {"name": "No images found"};

    // Build a node for each image.
    var imageByDBID = {};
    for (var i = 0; i < this.images_.length; ++i) {
        var image = this.images_[i];
        var imageNode = {
            "name": image.id.substr(0, 12),
            "children": [],
            "image": image,
            "tags": image.tags
        };
        imageByDBID[image.dbid] = imageNode;
    }
    this.imageByDBID_ = imageByDBID;

    // For each node, attach it to its immediate parent. If there is no immediate parent,
    // then the node is the root.
    var roots = [];
    for (var i = 0; i < this.images_.length; ++i) {
        var image = this.images_[i];
        var imageNode = imageByDBID[image.dbid];
        var ancestors = this.getAncestors_(image);
        var immediateParent = ancestors[ancestors.length - 1] * 1;
        var parent = imageByDBID[immediateParent];
        if (parent) {	   
            // Add a reference to the parent. This makes walking the tree later easier.
            imageNode.parent = parent;
            parent.children.push(imageNode);
        } else {
            roots.push(imageNode);
        }
    }

    // If there are multiple root nodes, then there is at least one branch without shared
    // ancestry and we use the virtual node. Otherwise, we use the root node found.
    var root = {
        'name': '',
        'children': roots,
        'virtual': true
    };

    if (roots.length == 1) {
        root = roots[0];
    }

    // Determine the maximum number of nodes at a particular level. This is used to size
    // the width of the tree properly.
    var maxChildCount = 0;
    for (var i = 0; i < this.images_.length; ++i) {
        var image = this.images_[i];
        var imageNode = imageByDBID[image.dbid];
        maxChildCount = Math.max(maxChildCount, this.determineMaximumChildCount_(imageNode));
    }

    // Compact the graph so that any single chain of three (or more) images becomes a collapsed
    // section. We only do this if the max width is > 1 (since for a single width tree, no long
    // chain will hide a branch).
    if (maxChildCount > 1) {
        this.collapseNodes_(root);
    }

    // Determine the maximum height of the tree.
    var maxHeight = this.determineMaximumHeight_(root);

    // Finally, set the root node and return.
    this.root_ = root;

    return {
        'maxWidth': maxChildCount + 1,
        'maxHeight': maxHeight
    };
};


/**
 * Collapses long single chains of nodes (3 or more) into single nodes to make the graph more
 * compact.
 */
ImageHistoryTree.prototype.determineMaximumHeight_ = function(node) {
   var maxHeight = 0;
   if (node.children) {
     for (var i = 0; i < node.children.length; ++i) {
       maxHeight = Math.max(this.determineMaximumHeight_(node.children[i]), maxHeight);
     }
   }
   return maxHeight + 1;
};


/**
 * Collapses long single chains of nodes (3 or more) into single nodes to make the graph more
 * compact.
 */
ImageHistoryTree.prototype.collapseNodes_ = function(node) {
    if (node.children.length == 1) {
        // Keep searching downward until we find a node with more than a single child.
        var current = node;
        var previous = node;
        var encountered = [];
        while (current.children.length == 1) {
            encountered.push(current);
            previous = current;
            current = current.children[0];
        }

        if (encountered.length >= 3) {
            // Collapse the node.
            var collapsed = {
                "name": '(' + (encountered.length - 1) + ' images)',
                "children": [current],
                "collapsed": true,
                "encountered": encountered
            };
            node.children = [collapsed];

            // Update the parent relationships.
            collapsed.parent = node;
            current.parent = collapsed;
            return;
        }
    }

    for (var i = 0; i < node.children.length; ++i) {
        this.collapseNodes_(node.children[i]);
    }
};


/**
 * Determines the maximum child count for the node and its children.
 */
ImageHistoryTree.prototype.determineMaximumChildCount_ = function(node) {
    var children = node.children;
    var myLevelCount = children.length;
    var nestedCount = 0;

    for (var i = 0; i < children.length; ++i) {
        nestedCount += children[i].children.length;
    }

    return Math.max(myLevelCount, nestedCount);
};


/**
 * Finds the image where the checker function returns true and returns it or null
 * if none.
 */
ImageHistoryTree.prototype.findImage_ = function(checker) {
    for (var i = 0; i < this.images_.length; ++i) {
        var image = this.images_[i];
        if (checker(image)) {
            return image;
        }
    }

    return null;
};


/**
 * Marks the full node path from the given starting node on whether it is highlighted.
 */
ImageHistoryTree.prototype.markPath_ = function(startingNode, isHighlighted) {
    var currentNode = startingNode;
    currentNode.current = isHighlighted;
    while (currentNode != null) {
        currentNode.highlighted = isHighlighted;
        currentNode = currentNode.parent;
    }
};


/**
 * Sets the current tag displayed in the tree.
 */
ImageHistoryTree.prototype.setTag_ = function(tagName) {
    if (tagName == this.currentTag_) {
      return;
    }

    var imageByDBID = this.imageByDBID_;
   
    // Save the current tag.
    var previousTagName = this.currentTag_;
    this.currentTag_ = tagName;
   
    // Update the state of each existing node to no longer be highlighted.
    var previousImage = this.findImage_(function(image) {
      return image.tags.indexOf(previousTagName || '(no tag specified)') >= 0;
    });

    if (previousImage) {
      var currentNode = imageByDBID[previousImage.dbid];
      this.markPath_(currentNode, false);
    }

    // Find the new current image (if any).
    this.currentImage_ = this.findImage_(function(image) {
      return image.tags.indexOf(tagName || '(no tag specified)') >= 0;
    });

    // Update the state of the new node path.
    if (this.currentImage_) {
      var currentNode = imageByDBID[this.currentImage_.dbid];
      this.markPath_(currentNode, true);
    }

    // Ensure that the children are in the correct order.
    for (var i = 0; i < this.images_.length; ++i) {
        var image = this.images_[i];
        var imageNode = this.imageByDBID_[image.dbid];
        var ancestors = this.getAncestors_(image);
        var immediateParent = ancestors[ancestors.length - 1] * 1;
        var parent = imageByDBID[immediateParent];
        if (parent && imageNode.highlighted) {
            var arr = parent.children;
            if (parent._children) {
                arr = parent._children;
            }

            if (arr[0] != imageNode) {
                var index = arr.indexOf(imageNode);
                if (index > 0) {
                    arr.splice(index, 1);
                    arr.splice(0, 0, imageNode);
                }
            }
        }
    }

    // Update the tree.
    this.update_(this.root_);
};


/**
 * Sets the current image highlighted in the tree.
 */
ImageHistoryTree.prototype.setImage_ = function(imageId) {
    // Find the new current image.
    var newImage = this.findImage_(function(image) {
        return image.id == imageId;
    });

    if (newImage == this.currentImage_) {
        return;
    }

    this.currentImage_ = newImage;
    this.update_(this.root_);
};


/**
 * Updates the tree in response to a click on a node.
 */
ImageHistoryTree.prototype.update_ = function(source) {
  var tree = this.tree_;
  var vis = this.vis_;
  var diagonal = this.diagonal_;
  var tip = this.tip_;
  var currentTag = this.currentTag_;
  var currentImage = this.currentImage_;
  var repoNamespace = this.repoNamespace_;
  var repoName = this.repoName_;
  var maxHeight = this.maxHeight_;

  var that = this;

  var duration = 500;

  // Compute the new tree layout.
  var nodes = tree.nodes(this.root_).reverse();

  // Normalize for fixed-depth.
  nodes.forEach(function(d) { d.y = (maxHeight - d.depth - 1) * DEPTH_HEIGHT; });

  // Update the nodes...
  var node = vis.selectAll("g.node")
      .data(nodes, function(d) { return d.id || (d.id = that.idCounter_++); });

  // Enter any new nodes at the parent's previous position.
  var nodeEnter = node.enter().append("svg:g")
      .attr("class", "node")
      .attr("transform", function(d) { return "translate(" + source.x0 + "," + source.y0 + ")"; });

  nodeEnter.append("svg:circle")
      .attr("r", 1e-6)
      .style("fill", function(d) { return d._children ? "lightsteelblue" : "#fff"; })
      .on("click", function(d) { that.toggle_(d); that.update_(d); });
 
  // Create the group that will contain the node name and its tags.
  var g = nodeEnter.append("svg:g").style("fill-opacity", 1e-6);

  // Add the repo ID.
  g.append("svg:text")
      .attr("x", function(d) { return d.children || d._children ? -10 : 10; })
      .attr("dy", ".35em")
      .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; })
      .text(function(d) { return d.name; })
      .on("click", function(d) { if (d.image) { that.changeImage_(d.image.id); } })
      .on('mouseover', tip.show)
      .on('mouseout', tip.hide);

    nodeEnter.selectAll("tags")
        .append("svg:text")
        .text("bar");

  // Create the foreign object to hold the tags (if any).
  var fo = g.append("svg:foreignObject")
      .attr("class", "fo")
      .attr("x", 14)
      .attr("y", 12)
      .attr("width", 110)
      .attr("height", DEPTH_HEIGHT - 20);
  
  // Add the tags container.
  fo.append('xhtml:div')
    .attr("class", "tags")
    .style("display", "none");

  // Translate the foreign object so the tags are under the ID.
  fo.attr("transform", function(d, i) {
    return "translate(" + [-130, 0 ] + ")";
  });

  // Transition nodes to their new position.
  var nodeUpdate = node.transition()
      .duration(duration)
      .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

  // Update the node circle.
  nodeUpdate.select("circle")
      .attr("r", 4.5)
      .attr("class", function(d) {
          return (d._children ? "closed " : "open ") + (d.current ? "current " : "") + (d.highlighted ? "highlighted " : "");
      })
      .style("fill", function(d) {
        if (d.current) {
            return "";
        }
        return d._children ? "lightsteelblue" : "#fff";
      });

  // Update the repo text.
  nodeUpdate.select("text")
      .attr("class", function(d) {
          if (d.collapsed) {
              return 'collapsed';
          }
          if (d.virtual) {
              return 'virtual';
          }
	  if (!currentImage) {
	      return '';
	  }
          return d.image.id == currentImage.id ? 'current' : '';
      });

  // Ensure that the node is visible.
  nodeUpdate.select("g")
      .style("fill-opacity", 1);

  // Update the tags.
  node.select(".tags")
      .html(function(d) {
          if (!d.tags) {
              return '';
          }

          var html = '';
          for (var i = 0; i < d.tags.length; ++i) {
              var tag = d.tags[i];
              var kind = 'default';
              if (tag == currentTag) {
                  kind = 'success';
              }
              html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '">' + tag + '</span>';
          }
          return html;
      });

  // Listen for click events on the labels.
  node.selectAll(".tag")
      .on("click", function(d, e) {
          var tag = this.getAttribute('data-tag');
          if (tag) {
              that.changeTag_(tag);
          }
      });

  // Ensure the tags are visible.
  nodeUpdate.select(".tags")
    .style("display", "")

  // There is a bug in Chrome which sometimes prevents the foreignObject from redrawing. To that end,
  // we force a redraw by adjusting the height of the object ever so slightly.
  nodeUpdate.select(".fo")
    .attr('height', function(d) {
        return DEPTH_HEIGHT - 20 + Math.random() / 10;
    });

  // Transition exiting nodes to the parent's new position.
  var nodeExit = node.exit().transition()
      .duration(duration)
      .attr("transform", function(d) { return "translate(" + source.x + "," + source.y + ")"; })
      .remove();

  nodeExit.select("circle")
      .attr("r", 1e-6);

  nodeExit.select(".tags")
      .style("display", "none");

  nodeExit.select("g")
      .style("fill-opacity", 1e-6);

  // Update the links...
  var link = vis.selectAll("path.link")
      .data(tree.links(nodes), function(d) { return d.target.id; });

  // Enter any new links at the parent's previous position.
  link.enter().insert("svg:path", "g")
      .attr("class", function(d) {
          var isHighlighted = d.target.highlighted;
          return "link " + (isHighlighted ? "highlighted": "");
      })
      .attr("d", function(d) {
        var o = {x: source.x0, y: source.y0};
        return diagonal({source: o, target: o});
      })
    .transition()
      .duration(duration)
      .attr("d", diagonal);

  // Transition links to their new position.
  link.transition()
      .duration(duration)
      .attr("d", diagonal)
      .attr("class", function(d) {
          var isHighlighted = d.target.highlighted;
          return "link " + (isHighlighted ? "highlighted": "");
      });

  // Transition exiting nodes to the parent's new position.
  link.exit().transition()
      .duration(duration)
      .attr("d", function(d) {
        var o = {x: source.x, y: source.y};
        return diagonal({source: o, target: o});
      })
      .remove();

  // Stash the old positions for transition.
  nodes.forEach(function(d) {
    d.x0 = d.x;
    d.y0 = d.y;
  });
};


/**
 * Toggles children of a node.
 */
ImageHistoryTree.prototype.toggle_ = function(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else {
    d.children = d._children;
    d._children = null;
  }
};


/**
 * Disposes of the tree.
 */
ImageHistoryTree.prototype.dispose = function() {
  var container =  this.container_ ;
  $('#' + container).removeOverscroll();
  document.getElementById(container).innerHTML = '';
};

////////////////////////////////////////////////////////////////////////////////

/**
 * Based off of http://bl.ocks.org/mbostock/1093025 by Mike Bostock (@mbostock)
 */
function ImageFileChangeTree(image, changes) {
    /**
     * The parent image.
     */
    this.image_ = image;

    /**
     * The changes being drawn.
     */
    this.changes_ = changes;

    /**
     * Counter for creating unique IDs.
     */
    this.idCounter_ = 0;
   
    /**
     * Map from file path to associated tree node.
     */
    this.nodeMap_ = {};
}


/**
 * Calculates the dimensions of the tree.
 */
ImageFileChangeTree.prototype.calculateDimensions_ = function(container) {
    var cw = document.getElementById(container).clientWidth;
    var barHeight = 20;
    var ch = (this.changes_.length * barHeight) + 40;

    var margin = { top: 40, right: 00, bottom: 20, left: 20 };
    var m = [margin.top, margin.right, margin.bottom, margin.left];
    var w = cw - m[1] - m[3];
    var h = ch - m[0] - m[2];

    var barWidth = cw * 0.8 - m[1] - m[3];

    return {
        'w': w,
        'h': h,
        'm': m,
        'cw': cw,
        'ch': ch,
        'bw': barWidth,
        'bh': barHeight
    };
};


/**
 * Updates the dimensions of the tree.
 */
ImageFileChangeTree.prototype.updateDimensions_ = function() {
    var container = this.container_;
    var dimensions = this.calculateDimensions_(container);

    var w = dimensions.w;
    var h = dimensions.h;
    var m = dimensions.m;

    // Update the tree.
    var rootSvg = this.rootSvg_;
    var tree = this.tree_;
    var vis = this.vis_;

    rootSvg
        .attr("width", w + m[1] + m[3])
        .attr("height", h + m[0] + m[2]);

    tree.size([h, 100]);
    vis.attr("transform", "translate(" + m[3] + "," + m[0] + ")");

    this.barWidth_ = dimensions.bw;
    this.barHeight_ = dimensions.bh;
  
    return dimensions;
};


/**
 * Redraws the image change tree to fit the new size.
 */
ImageFileChangeTree.prototype.notifyResized = function() {
  this.updateDimensions_();
  this.update_(this.root_);
};


/**
 * Disposes of the tree.
 */
ImageFileChangeTree.prototype.dispose = function() {
  var container =  this.container_ ;
  document.getElementById(container).innerHTML = '';
};


/**
 * Draws the tree.
 */
ImageFileChangeTree.prototype.draw = function(container) {
  this.container_ = container;  
    
  var dimensions = this.calculateDimensions_(container);
  
  var w = dimensions.w;
  var h = dimensions.h;
  var m = dimensions.m;

  this.barWidth_ = dimensions.bw;
  this.barHeight_ = dimensions.bh;

  var tree = d3.layout.tree()
    .size([h, 100]);

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

  var rootSvg = d3.select("#" + container).append("svg:svg")
    .attr("width", w)
    .attr("height", h);

  var vis = rootSvg
        .append("svg:g")
        .attr("transform", "translate(" + m[3] + "," + m[0] + ")");

  this.rootSvg_ = rootSvg;
  this.tree_ = tree;
  this.diagonal_ = diagonal;
  this.vis_ = vis;

  this.populateAndDraw_();
};


/**
 * Populates the tree and then draws it.
 */
ImageFileChangeTree.prototype.populateAndDraw_ = function() {
  // For each change, generate all the node(s) needed for the folders, as well
  // as the final node (if any) for the file.
  for (var i = 0; i < this.changes_.length; ++i) {
    var filepath = this.changes_[i].file;
    var node = this.buildNodes_(filepath);
    node.kind = this.changes_[i].kind;
  }

  // Sort the children of each node so that folders are on top.
  var sortByName = function (a, b) {
    if (a.name > b.name) {
      return 1;
    }
    if (a.name < b.name) {
      return -1;
    }
    return 0;
  };

  var sortFunction = function(a, b) {
    var hasA = a._children.length > 0;
    var hasB = b._children.length > 0;
    if (hasA == hasB) {
      // Sort alphabetically.
      return sortByName(a, b);
    }
    if (hasA) { return -1; }
    return 1;
  };

  for (var path in this.nodeMap_) {
    if (!this.nodeMap_.hasOwnProperty(path) || !this.nodeMap_[path]._children) {
      continue;
    }

    this.nodeMap_[path]._children.sort(sortFunction);
  }

  this.root_ = this.nodeMap_[''];
  this.root_.x0 = 0;
  this.root_.y0 = 0;
  this.toggle_(this.root_);
  this.update_(this.root_);
};


/**
 * Builds all the nodes in the tree.
 */
ImageFileChangeTree.prototype.buildNodes_ = function(path) {
  var parts = path.split('/');
  for (var i = 0; i < parts.length; ++i) {
    var currentPath = parts.slice(0, i + 1).join('/');
    if (!this.nodeMap_[currentPath]) {
      this.nodeMap_[currentPath] = { 'name': parts[i] || '/', 'path': currentPath, '_children': [] };

      if (currentPath.length > 0) {
        var parentPath = parts.slice(0, i).join('/');
        this.nodeMap_[parentPath]._children.push(this.nodeMap_[currentPath]);
      }
    }
  }
  return this.nodeMap_[path];
};


/**
 * Calculates the count of visible nodes.
 */
ImageFileChangeTree.prototype.getVisibleCount_ = function(node) {
  if (node.children) {
    var count = 1;
    for (var  i = 0; i < node.children.length; ++i) {
      count += this.getVisibleCount_(node.children[i]);
    }
    return count;
  }
  return 1;
};


/**
 * Calculates the height for the container.
 */
ImageFileChangeTree.prototype.getContainerHeight_ = function() {
  var dimensions = this.calculateDimensions_(this.container_);
  var barHeight = this.barHeight_;
  var height = (this.getVisibleCount_(this.root_) * (barHeight + 2)); 
  return height + dimensions.m[0] + dimensions.m[2];
};


/**
 * Updates the tree starting at the given source node.
 */
ImageFileChangeTree.prototype.update_ = function(source) {
  var that = this;
  var tree = this.tree_;
  var vis = this.vis_;
  var svg = this.rootSvg_;
  var diagonal = this.diagonal_;
  var barWidth = this.barWidth_;
  var barHeight = this.barHeight_;

  var duration = 400;

  var color = function(d) {
      if (d.kind) {
        return '';
      }
      return d._children ? "#E9E9E9" : "#c6dbef";
  };

  // Update the height of the container and the SVG.
  document.getElementById(this.container_).style.height = this.getContainerHeight_() + 'px';
  svg.attr('height', this.getContainerHeight_());

  // Compute the flattened node list.
  var nodes = tree.nodes(this.root_);
  
  // Compute the "layout".
  nodes.forEach(function(n, i) {
    n.x = i * barHeight;
  });
  
  // Update the nodes...
  var node = vis.selectAll("g.node")
      .data(nodes, function(d) { return d.id || (d.id = that.idCounter_++); });
  
  var nodeEnter = node.enter().append("svg:g")
      .attr("class", function(d) {
          return "node " + (d.kind ? d.kind : 'folder');
      })
      .attr("transform", function(d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
      .style("opacity", 1e-6);

  // Enter any new nodes at the parent's previous position.
  nodeEnter.append("svg:rect")
      .attr("class", "main-rect")
      .attr("y", -barHeight / 2)
      .attr("height", barHeight)
      .attr("width", barWidth)
      .style("fill", color)
      .on("click", function(d) { that.toggle_(d); that.update_(source); });
 
  nodeEnter.append("svg:text")
      .attr("dy", 3.5)
      .attr("dx", 5.5 + 18)
      .text(function(d) { return d.name; });

  var body = nodeEnter.append('svg:foreignObject')
      .attr("class", "fo")
      .attr("width", 18)
      .attr("height", barHeight)
      .append('xhtml:body');

  body.append('div')
      .attr('class', 'node-icon');

  // Transition nodes to their new position.
  nodeEnter.transition()
      .duration(duration)
      .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
      .style("opacity", 1);
 
  node.transition()
      .duration(duration)
       // TODO: reenable for full animation
       //.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; })
      .style("opacity", 1)
    .select("rect")
      .style("fill", color);

  // TODO: remove if full animation.
  node.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });

  node.select('.main-rect')
      .attr("y", -barHeight / 2)
      .attr("height", barHeight)
      .attr("width", barWidth)

  node.select('.fo')
      .attr("x", function(d) { return d.kind ? barWidth - 18 : 0; })
      .attr("y", -10)
  
  node.select('.node-icon')
      .html(function(d) {
            if (!d.kind) {
                var folder = d._children ? 'fa fa-folder' : 'fa fa-folder-open';
                return '<i class="' + folder + '"></i>';
            }

            var icon = {
                'added': 'plus-square',
                'removed': 'minus-square',
                'changed': 'pencil-square'
            };

            return '<i class="change-icon fa fa-' + icon[d.kind]  + '"></i>';
        });
  
  // Transition exiting nodes to the parent's new position.
  node.exit().transition()
      .duration(duration)
     // TODO: reenable for full animation
     // .attr("transform", function(d) { return "translate(" + source.y + "," + source.x + ")"; })
      .style("opacity", 1e-6)
      .remove();
  
  // Update the links...
  var link = vis.selectAll("path.link")
      .data(tree.links(nodes), function(d) { return d.target.id; });
  
  // Enter any new links at the parent's previous position.
  link.enter().insert("svg:path", "g")
      .attr("class", "link")
      .attr("d", function(d) {
        var o = {x: source.x0, y: source.y0};
        return diagonal({source: o, target: o});
      })
    .transition()
      .duration(duration)
      .attr("d", diagonal);
  
  // Transition links to their new position.
  link.transition()
      .duration(duration)
      .attr("d", function(d) {
        var s = {x: d.source.x + 14, y: d.source.y + 9};
        var t = d.target;
        return diagonal({source: s, target: t});
      });
  
  // Transition exiting nodes to the parent's new position.
  link.exit().transition()
      .duration(duration)
      .attr("d", function(d) {
        var o = {x: source.x, y: source.y};
        return diagonal({source: o, target: o});
      })
      .remove();
  
  // Stash the old positions for transition.
  nodes.forEach(function(d) {
    d.x0 = d.x;
    d.y0 = d.y;
  });
};

/**
 * Toggles children of a node.
 */
ImageFileChangeTree.prototype.toggle_ = function(d) {
  if (d.children) {
    d._children = d.children;
    d.children = null;
  } else {
    d.children = d._children;
    d._children = null;
  }
};


////////////////////////////////////////////////////////////////////////////////

/**
 * Based off of http://bl.ocks.org/mbostock/1346410
 */
function RepositoryUsageChart() {
  this.total_ = null;
  this.count_ = null;
  this.drawn_ = false;
}


/**
 * Updates the chart with the given count and total of number of repositories.
 */
RepositoryUsageChart.prototype.update = function(count, total) {
  if (!this.g_) { return; }
  this.total_ = total;
  this.count_ = count;
  this.drawInternal_();
};


/**
 * Conducts the actual draw or update (if applicable).
 */
RepositoryUsageChart.prototype.drawInternal_ = function() {
  // If the total is null, then we have not yet set the proper counts.
  if (this.total_ === null) { return; }

  var duration = 750;

  var arc = this.arc_;
  var pie = this.pie_;
  var arcTween = this.arcTween_;

  var color = d3.scale.category20();
  var count = this.count_;
  var total = this.total_;

  var data = [Math.max(count, 1), Math.max(0, total - count)];

  var arcTween = function(a) {
    var i = d3.interpolate(this._current, a);
    this._current = i(0);
    return function(t) {
      return arc(i(t));
    };
  };

  if (!this.drawn_) {
    var text = this.g_.append("svg:text")
      .attr("dy", 10)
      .attr("dx", 0)
      .attr('dominant-baseline', 'auto')
      .attr('text-anchor', 'middle')
      .attr('class', 'count-text')
      .text(this.count_ + ' / ' + this.total_);

    var path = this.g_.datum(data).selectAll("path")
       .data(pie)
          .enter().append("path")
            .attr("fill", function(d, i) { return color(i); })
            .attr("class", function(d, i) { return 'arc-' + i; })
            .attr("d", arc)
            .each(function(d) { this._current = d; }); // store the initial angles

    this.path_ = path;
    this.text_ = text;
  } else {
    pie.value(function(d, i) { return data[i]; }); // change the value function
    this.path_ = this.path_.data(pie); // compute the new angles
    this.path_.transition().duration(duration).attrTween("d", arcTween); // redraw the arcs

    // Update the text.
    this.text_.text(this.count_ + ' / ' + this.total_);
  }
    
  this.drawn_ = true;
};


/**
 * Draws the chart in the given container.
 */
RepositoryUsageChart.prototype.draw = function(container) {
  var cw = 200;
  var ch = 200;
  var radius = Math.min(cw, ch) / 2;

  var pie = d3.layout.pie().sort(null);

  var arc = d3.svg.arc()
    .innerRadius(radius - 50)
    .outerRadius(radius - 25);

  var svg = d3.select("#" + container).append("svg:svg")
    .attr("width", cw)
    .attr("height", ch);

  var g = svg.append("g")
    .attr("transform", "translate(" + cw / 2 + "," + ch / 2 + ")");

  this.svg_ = svg;
  this.g_ = g;
  this.pie_ = pie;
  this.arc_ = arc;
  this.width_ = cw;
  this.drawInternal_();
};