var _ = require('underscore');
var Q = require('q');

var GRAPHICS = require('../util/constants').GRAPHICS;
var GlobalStateStore = require('../stores/GlobalStateStore');

var VisNode = require('../visuals/visNode').VisNode;
var VisBranch = require('../visuals/visBranch').VisBranch;
var VisBranchCollection = require('../visuals/visBranch').VisBranchCollection;
var VisTag = require('../visuals/visTag').VisTag;
var VisTagCollection = require('../visuals/visTag').VisTagCollection;
var VisEdge = require('../visuals/visEdge').VisEdge;
var VisEdgeCollection = require('../visuals/visEdge').VisEdgeCollection;

function GitVisuals(options) {
  options = options || {};
  this.options = options;
  this.visualization = options.visualization;
  this.commitCollection = options.commitCollection;
  this.branchCollection = options.branchCollection;
  this.tagCollection = options.tagCollection;
  this.visNodeMap = {};

  this.visEdgeCollection = new VisEdgeCollection();
  this.visBranchCollection = new VisBranchCollection();
  this.visTagCollection = new VisTagCollection();
  this.commitMap = {};

  this.rootCommit = null;
  this.branchStackMap = null;
  this.tagStackMap = null;
  this.upstreamBranchSet = null;
  this.upstreamTagSet = null;
  this.upstreamHeadSet = null;

  this.paper = options.paper;
  this.gitReady = false;

  this.branchCollection.on('add', this.addBranchFromEvent, this);
  this.branchCollection.on('remove', this.removeBranch, this);
  
  this.tagCollection.on('add', this.addTagFromEvent, this);
  this.tagCollection.on('remove', this.removeTag, this);
  
  this.deferred = [];

  this.flipFraction = 0.65;

  var Main = require('../app');
  Main.getEvents().on('refreshTree', this.refreshTree, this);
}

GitVisuals.prototype.defer = function(action) {
  this.deferred.push(action);
};

GitVisuals.prototype.deferFlush = function() {
  _.each(this.deferred, function(action) {
    action();
  }, this);
  this.deferred = [];
};

GitVisuals.prototype.resetAll = function() {
  // make sure to copy these collections because we remove
  // items in place and underscore is too dumb to detect length change
  var edges = this.visEdgeCollection.toArray();
  _.each(edges, function(visEdge) {
    visEdge.remove();
  }, this);

  var branches = this.visBranchCollection.toArray();
  _.each(branches, function(visBranch) {
    visBranch.remove();
  }, this);

  var tags = this.visTagCollection.toArray();
  _.each(tags, function(visTag) {
    visTag.remove();
  }, this);

  _.each(this.visNodeMap, function(visNode) {
    visNode.remove();
  }, this);

  this.visEdgeCollection.reset();
  this.visBranchCollection.reset();
  this.visTagCollection.reset();

  this.visNodeMap = {};
  this.rootCommit = null;
  this.commitMap = {};
};

GitVisuals.prototype.tearDown = function() {
  this.resetAll();
  this.paper.remove();
};

GitVisuals.prototype.assignGitEngine = function(gitEngine) {
  this.gitEngine = gitEngine;
  this.initHeadBranch();
  this.deferFlush();
};

GitVisuals.prototype.getVisualization = function() {
  return this.visualization;
};

GitVisuals.prototype.initHeadBranch = function() {
  // it's unfortaunte we have to do this, but the head branch
  // is an edge case because it's not part of a collection so
  // we can't use events to load or unload it. thus we have to call
  // this ugly method which will be deleted one day

  // seed this with the HEAD pseudo-branch
  this.addBranchFromEvent(this.gitEngine.HEAD);
};

GitVisuals.prototype.getScreenPadding = function() {
  // if we are flipping the tree, the helper bar gets in the way
  var topFactor = (GlobalStateStore.getFlipTreeY()) ? 3 : 1.5;

  // for now we return the node radius subtracted from the walls
  return {
    widthPadding: GRAPHICS.nodeRadius * 1.5,
    topHeightPadding: GRAPHICS.nodeRadius * topFactor,
    // we pad the bottom a lot more so the branches wont go off screen
    bottomHeightPadding: GRAPHICS.nodeRadius * 5
  };
};

GitVisuals.prototype.getPosBoundaries = function() {
  if (this.gitEngine.hasOrigin()) {
    return {
      min: 0,
      max: 0.5
    };
  } else if (this.gitEngine.isOrigin()) {
    return {
      min: 0.5,
      max: 1
    };
  }
  return {
    min: 0,
    max: 1
  };
};

GitVisuals.prototype.getFlipPos = function() {
  var bounds = this.getPosBoundaries();
  var min = bounds.min;
  var max = bounds.max;
  return this.flipFraction * (max - min) + min;
};

GitVisuals.prototype.getIsGoalVis = function() {
  return !!this.options.isGoalVis;
};

GitVisuals.prototype.getLevelBlob = function() {
  return this.visualization.options.levelBlob || {};
};

GitVisuals.prototype.toScreenCoords = function(pos) {
  if (!this.paper.width) {
    throw new Error('being called too early for screen coords');
  }
  var padding = this.getScreenPadding();

  var shrink = function(frac, total, padding) {
    return padding + frac * (total - padding * 2);
  };

  var asymShrink = function(frac, total, paddingTop, paddingBelow) {
    return paddingTop + frac * (total - paddingBelow - paddingTop);
  };

  var x = shrink(pos.x, this.paper.width, padding.widthPadding);
  var y =
    asymShrink(pos.y, this.paper.height, padding.topHeightPadding, padding.bottomHeightPadding);

  if (GlobalStateStore.getFlipTreeY()) {
    y = this.paper.height - y;
  }

  return {x: x, y: y};
};

GitVisuals.prototype.animateAllAttrKeys = function(keys, attr, speed, easing) {
  var deferred = Q.defer();

  var animate = function(visObj) {
    visObj.animateAttrKeys(keys, attr, speed, easing);
  };

  this.visBranchCollection.each(animate);
  this.visEdgeCollection.each(animate);
  this.visTagCollection.each(animate);
  _.each(this.visNodeMap, animate);

  var time = (speed !== undefined) ? speed : GRAPHICS.defaultAnimationTime;
  setTimeout(function() {
    deferred.resolve();
  }, time);

  return deferred.promise;
};

GitVisuals.prototype.finishAnimation = function() {
  var _this = this;
  var deferred = Q.defer();
  var animationDone = Q.defer();
  var defaultTime = GRAPHICS.defaultAnimationTime;
  var nodeRadius = GRAPHICS.nodeRadius;

  var textString = 'Solved!!\n:D';
  var text = null;
  var makeText = function() {
    text = this.paper.text(
      this.paper.width / 2,
      this.paper.height / 2,
      textString
    );
    text.attr({
      opacity: 0,
      'font-weight': 500,
      'font-size': '32pt',
      'font-family': 'Monaco, Courier, font-monospace',
      stroke: '#000',
      'stroke-width': 2,
      fill: '#000'
    });
    text.animate({ opacity: 1 }, defaultTime);
  }.bind(this);

  // this is a BIG ANIMATION but it ends up just being
  // a sweet chain of promises but is pretty nice. this is
  // after I discovered promises / deferred's. Unfortunately
  // I wrote a lot of the git stuff before promises, so
  // that's somewhat ugly

  deferred.promise
  // first fade out everything but circles
  .then(function() {
    return this.animateAllAttrKeys(
      { exclude: ['circle'] },
      { opacity: 0 },
      defaultTime * 1.1
    );
  }.bind(this))
  // then make circle radii bigger
  .then(function() {
    return this.animateAllAttrKeys(
      { exclude: ['arrow', 'rect', 'path', 'text'] },
      { r: nodeRadius * 2 },
      defaultTime * 1.5
    );
  }.bind(this))
  // then shrink em super fast
  .then(function() {
    return this.animateAllAttrKeys(
      { exclude: ['arrow', 'rect', 'path', 'text'] },
      { r: nodeRadius * 0.75 },
      defaultTime * 0.5
    );
  }.bind(this))
  // then explode them and display text
  .then(function() {
    makeText();
    return this.explodeNodes();
  }.bind(this))
  .then(function() {
    return this.explodeNodes();
  }.bind(this))
  // then fade circles (aka everything) in and back
  .then(function() {
    return this.animateAllAttrKeys(
      { exclude: ['arrow', 'rect', 'path', 'text'] },
      {},
      defaultTime * 1.25
    );
  }.bind(this))
  // then fade everything in and remove text
  .then(function() {
    text.animate({ opacity: 0 }, defaultTime, undefined, undefined, function() {
      text.remove();
    });
    return this.animateAllAttrKeys(
      {},
      {}
    );
  }.bind(this))
  .then(function() {
    animationDone.resolve();
  })
  .fail(function(reason) {
    console.warn('animation error' + reason);
  })
  .done();

  // start our animation chain right away
  deferred.resolve();
  return animationDone.promise;
};

GitVisuals.prototype.explodeNodes = function() {
  var deferred = Q.defer();
  var funcs = [];
  _.each(this.visNodeMap, function(visNode) {
    funcs.push(visNode.getExplodeStepFunc());
  });

  var interval = setInterval(function() {
    // object creation here is a bit ugly inside a loop,
    // but the alternative is to just OR against a bunch
    // of booleans which means the other stepFuncs
    // are called unnecessarily when they have almost
    // zero speed. would be interesting to see performance differences
    var keepGoing = [];
    _.each(funcs, function(func) {
      if (func()) {
        keepGoing.push(func);
      }
    });

    if (!keepGoing.length) {
      clearInterval(interval);
      // next step :D wow I love promises
      deferred.resolve();
      return;
    }

    funcs = keepGoing;
  }, 1/40);

  return deferred.promise;
};

GitVisuals.prototype.animateAllFromAttrToAttr = function(fromSnapshot, toSnapshot, idsToOmit) {
  var animate = function(obj) {
    var id = obj.getID();
    if (_.include(idsToOmit, id)) {
      return;
    }

    if (!fromSnapshot[id] || !toSnapshot[id]) {
      // its actually ok it doesnt exist yet
      return;
    }
    obj.animateFromAttrToAttr(fromSnapshot[id], toSnapshot[id]);
  };

  this.visBranchCollection.each(animate);
  this.visEdgeCollection.each(animate);
  this.visTagCollection.each(animate);
  _.each(this.visNodeMap, animate);
};

/***************************************
     == BEGIN Tree Calculation Parts ==
       _  __    __  _
       \\/ /    \ \//_
        \ \     /   __|   __
         \ \___/   /_____/ /
          |        _______ \
          \  ( )   /      \_\
           \      /
            |    |
            |    |
  ____+-_=+-^    ^+-=_=__________

^^ I drew that :D

 **************************************/

GitVisuals.prototype.genSnapshot = function() {
  this.fullCalc();

  var snapshot = {};
  _.each(this.visNodeMap, function(visNode) {
    snapshot[visNode.get('id')] = visNode.getAttributes();
  }, this);

  this.visBranchCollection.each(function(visBranch) {
    snapshot[visBranch.getID()] = visBranch.getAttributes();
  }, this);

  this.visEdgeCollection.each(function(visEdge) {
    snapshot[visEdge.getID()] = visEdge.getAttributes();
  }, this);

  this.visTagCollection.each(function(visTag) {
    snapshot[visTag.getID()] = visTag.getAttributes();
  }, this);

  return snapshot;
};

GitVisuals.prototype.refreshTree = function(speed) {
  if (!this.gitReady || !this.gitEngine.rootCommit) {
    return;
  }

  // this method can only be called after graphics are rendered
  this.fullCalc();

  this.animateAll(speed);
};

GitVisuals.prototype.refreshTreeHarsh = function() {
  this.fullCalc();

  this.animateAll(0);
};

GitVisuals.prototype.animateAll = function(speed) {
  this.zIndexReflow();

  this.animateEdges(speed);
  this.animateNodePositions(speed);
  this.animateRefs(speed);
};

GitVisuals.prototype.fullCalc = function() {
  this.calcTreeCoords();
  this.calcGraphicsCoords();
};

GitVisuals.prototype.calcTreeCoords = function() {
  // this method can only contain things that dont rely on graphics
  if (!this.rootCommit) {
    throw new Error('grr, no root commit!');
  }

  this.calcUpstreamSets();
  this.calcBranchStacks();
  this.calcTagStacks();

  this.calcDepth();
  this.calcWidth();
};

GitVisuals.prototype.calcGraphicsCoords = function() {
  this.visBranchCollection.each(function(visBranch) {
    visBranch.updateName();
  });
  this.visTagCollection.each(function(visTag) {
    visTag.updateName();
  });
};

GitVisuals.prototype.calcUpstreamSets = function() {
  this.upstreamBranchSet = this.gitEngine.getUpstreamBranchSet();
  this.upstreamHeadSet = this.gitEngine.getUpstreamHeadSet();
  this.upstreamTagSet = this.gitEngine.getUpstreamTagSet();
};

GitVisuals.prototype.getCommitUpstreamBranches = function(commit) {
  return this.branchStackMap[commit.get('id')];
};

GitVisuals.prototype.getBlendedHuesForCommit = function(commit) {
  var branches = this.upstreamBranchSet[commit.get('id')];
  if (!branches) {
    throw new Error('that commit doesnt have upstream branches!');
  }

  return this.blendHuesFromBranchStack(branches);
};

GitVisuals.prototype.blendHuesFromBranchStack = function(branchStackArray) {
  var hueStrings = [];
  _.each(branchStackArray, function(branchWrapper) {
    var fill = branchWrapper.obj.get('visBranch').get('fill');

    if (fill.slice(0,3) !== 'hsb') {
      // crap! convert
      var color = Raphael.color(fill);
      fill = 'hsb(' + String(color.h) + ',' + String(color.l);
      fill = fill + ',' + String(color.s) + ')';
    }

    hueStrings.push(fill);
  });

  return blendHueStrings(hueStrings);
};

GitVisuals.prototype.getCommitUpstreamStatus = function(commit) {
  if (!this.upstreamBranchSet) {
    throw new Error("Can't calculate this yet!");
  }

  var id = commit.get('id');
  var branch = this.upstreamBranchSet;
  var head = this.upstreamHeadSet;
  var tag = this.upstreamTagSet;

  if (branch[id]) {
    return 'branch';
  } else if (tag[id]) {
    return 'tag';
  } else if (head[id]) {
    return 'head';
  } else {
    return 'none';
  }
};

GitVisuals.prototype.calcTagStacks = function() {
  var tags = this.gitEngine.getTags();
  var map = {};
  _.each(tags, function(tag) {
      var thisId = tag.target.get('id');
  
      map[thisId] = map[thisId] || [];
      map[thisId].push(tag);
      map[thisId].sort(function(a, b) {
          var aId = a.obj.get('id');
          var bId = b.obj.get('id');
          return aId.localeCompare(bId);
        });
    });
  this.tagStackMap = map;
};

GitVisuals.prototype.calcBranchStacks = function() {
  var branches = this.gitEngine.getBranches();
  var map = {};
  _.each(branches, function(branch) {
    var thisId = branch.target.get('id');

    map[thisId] = map[thisId] || [];
    map[thisId].push(branch);
    map[thisId].sort(function(a, b) {
      var aId = a.obj.get('id');
      var bId = b.obj.get('id');
      if (aId == 'master' || bId == 'master') {
        return aId == 'master' ? -1 : 1;
      }
      return aId.localeCompare(bId);
    });
  });
  this.branchStackMap = map;
};

GitVisuals.prototype.calcWidth = function() {
  this.maxWidthRecursive(this.rootCommit);

  var bounds = this.getPosBoundaries();
  this.assignBoundsRecursive(
    this.rootCommit,
    bounds.min,
    bounds.max
  );
};

GitVisuals.prototype.maxWidthRecursive = function(commit) {
  var childrenTotalWidth = 0;
  _.each(commit.get('children'), function(child) {
    // only include this if we are the "main" parent of
    // this child
    if (child.isMainParent(commit)) {
      var childWidth = this.maxWidthRecursive(child);
      childrenTotalWidth += childWidth;
    }
  }, this);

  var maxWidth = Math.max(1, childrenTotalWidth);
  commit.get('visNode').set('maxWidth', maxWidth);
  return maxWidth;
};

GitVisuals.prototype.assignBoundsRecursive = function(commit, min, max) {
  // I always position myself within my bounds
  var myWidthPos = (max + min) / 2.0;
  commit.get('visNode').get('pos').x = myWidthPos;

  if (commit.get('children').length === 0) {
    return;
  }

  // i have a certain length to divide up
  var myLength = max - min;
  // I will divide up that length based on my children's max width in a
  // basic box-flex model
  var totalFlex = 0;
  var children = commit.get('children');
  _.each(children, function(child) {
    if (child.isMainParent(commit)) {
      totalFlex += child.get('visNode').getMaxWidthScaled();
    }
  }, this);

  var prevBound = min;
  _.each(children, function(child, index) {
    if (!child.isMainParent(commit)) {
      return;
    }

    var flex = child.get('visNode').getMaxWidthScaled();
    var portion = (flex / totalFlex) * myLength;

    var childMin = prevBound;
    var childMax = childMin + portion;

    this.assignBoundsRecursive(child, childMin, childMax);
    prevBound = childMin + portion;
  }, this);
};

GitVisuals.prototype.calcDepth = function() {
  var maxDepth = this.calcDepthRecursive(this.rootCommit, 0);
  if (maxDepth > 15) {
    // issue warning
    console.warn('graphics are degrading from too many layers');
  }

  var depthIncrement = this.getDepthIncrement(maxDepth);
  _.each(this.visNodeMap, function(visNode) {
    visNode.setDepthBasedOn(depthIncrement, this.getHeaderOffset());
  }, this);
};

/***************************************
     == END Tree Calculation ==
       _  __    __  _
       \\/ /    \ \//_
        \ \     /   __|   __
         \ \___/   /_____/ /
          |        _______ \
          \  ( )   /      \_\
           \      /
            |    |
            |    |
  ____+-_=+-^    ^+-=_=__________

^^ I drew that :D

 **************************************/

GitVisuals.prototype.animateNodePositions = function(speed) {
  _.each(this.visNodeMap, function(visNode) {
    visNode.animateUpdatedPosition(speed);
  }, this);
};

GitVisuals.prototype.addBranchFromEvent = function(branch, collection, index) {
  var action = function() {
    this.addBranch(branch);
  }.bind(this);

  if (!this.gitEngine || !this.gitReady) {
    this.defer(action);
  } else {
    action();
  }
};

GitVisuals.prototype.addBranch = function(branch) {
  var visBranch = new VisBranch({
    branch: branch,
    gitVisuals: this,
    gitEngine: this.gitEngine
  });

  this.visBranchCollection.add(visBranch);
  if (this.gitReady) {
    visBranch.genGraphics(this.paper);
  } else {
    this.defer(function() {
      visBranch.genGraphics(this.paper);
    }.bind(this));
  }
};

GitVisuals.prototype.addTagFromEvent = function(tag, collection, index) {
  var action = function() {
    this.addTag(tag);
  }.bind(this);

  if (!this.gitEngine || !this.gitReady) {
    this.defer(action);
  } else {
    action();
  }
};

GitVisuals.prototype.addTag = function(tag) {
  var visTag = new VisTag({
    tag: tag,
    gitVisuals: this,
    gitEngine: this.gitEngine
  });

  this.visTagCollection.add(visTag);
  if (this.gitReady) {
    visTag.genGraphics(this.paper);
  } else {
    this.defer(function() {
      visTag.genGraphics(this.paper);
    }.bind(this));
  }
};

GitVisuals.prototype.removeVisBranch = function(visBranch) {
  this.visBranchCollection.remove(visBranch);
};

GitVisuals.prototype.removeVisTag = function(visTag) {
  this.visTagCollection.remove(visTag);
};


GitVisuals.prototype.removeVisNode = function(visNode) {
  delete this.visNodeMap[visNode.getID()];
};

GitVisuals.prototype.removeVisEdge = function(visEdge) {
  this.visEdgeCollection.remove(visEdge);
};

GitVisuals.prototype.animateRefs = function(speed) {
  this.visBranchCollection.each(function(visBranch) {
    visBranch.animateUpdatedPos(speed);
  }, this);
  this.visTagCollection.each(function(visTag) {
    visTag.animateUpdatedPos(speed);
  }, this);
};

GitVisuals.prototype.animateEdges = function(speed) {
  this.visEdgeCollection.each(function(edge) {
    edge.animateUpdatedPath(speed);
  }, this);
};

GitVisuals.prototype.getMinLayers = function() {
  return (this.options.smallCanvas) ? 2 : 7;
};

GitVisuals.prototype.getDepthIncrement = function(maxDepth) {
  // assume there are at least a number of layers until later
  // to have better visuals
  maxDepth = Math.max(maxDepth, this.getMinLayers());
  // if we have a header, reserve space for that
  var vSpace = 1.0 - this.getHeaderOffset();
  var increment = vSpace / maxDepth;
  return increment;
};

GitVisuals.prototype.shouldHaveHeader = function() {
  return this.gitEngine.isOrigin() || this.gitEngine.hasOrigin();
};

GitVisuals.prototype.getHeaderOffset = function() {
  return (this.shouldHaveHeader()) ? 0.05 : 0;
};

GitVisuals.prototype.calcDepthRecursive = function(commit, depth) {
  commit.get('visNode').setDepth(depth);

  var children = commit.get('children');
  var maxDepth = depth;
  _.each(children, function(child) {
    var d = this.calcDepthRecursive(child, depth + 1);
    maxDepth = Math.max(d, maxDepth);
  }, this);

  return maxDepth;
};

// we debounce here so we aren't firing a resize call on every resize event
// but only after they stop
GitVisuals.prototype.canvasResize = function(width, height) {
  if (!this.resizeFunc) {
    this.genResizeFunc();
  }
  this.resizeFunc(width, height);
};

GitVisuals.prototype.genResizeFunc = function() {
  this.resizeFunc = _.debounce(
    function(width, height) {
      this.refreshTree();
    }.bind(this),
    200,
    true
  );
};

GitVisuals.prototype.addNode = function(id, commit) {
  this.commitMap[id] = commit;
  if (commit.get('rootCommit')) {
    this.rootCommit = commit;
  }

  var visNode = new VisNode({
    id: id,
    commit: commit,
    gitVisuals: this,
    gitEngine: this.gitEngine
  });
  this.visNodeMap[id] = visNode;

  if (this.gitReady) {
    visNode.genGraphics(this.paper);
  }
  return visNode;
};

GitVisuals.prototype.addEdge = function(idTail, idHead) {
  var visNodeTail = this.visNodeMap[idTail];
  var visNodeHead = this.visNodeMap[idHead];

  if (!visNodeTail || !visNodeHead) {
    throw new Error('one of the ids in (' + idTail +
                    ', ' + idHead + ') does not exist');
  }

  var edge = new VisEdge({
    tail: visNodeTail,
    head: visNodeHead,
    gitVisuals: this,
    gitEngine: this.gitEngine
  });
  this.visEdgeCollection.add(edge);

  if (this.gitReady) {
    edge.genGraphics(this.paper);
  }
};

GitVisuals.prototype.zIndexReflow = function() {
  this.visNodesFront();
  this.visBranchesFront();
  this.visTagsFront();
};

GitVisuals.prototype.visNodesFront = function() {
  _.each(this.visNodeMap, function(visNode) {
    visNode.toFront();
  });
};

GitVisuals.prototype.visBranchesFront = function() {
  this.visBranchCollection.each(function(vBranch) {
    vBranch.nonTextToFront();
    vBranch.textToFront();
  });

  this.visBranchCollection.each(function(vBranch) {
    vBranch.textToFrontIfInStack();
  });
};

GitVisuals.prototype.visTagsFront = function() {
  this.visTagCollection.each(function(vTag) {
    vTag.nonTextToFront();
    vTag.textToFront();
  });

  this.visTagCollection.each(function(vTag) {
    vTag.textToFrontIfInStack();
  });
};

GitVisuals.prototype.drawTreeFromReload = function() {
  this.gitReady = true;
  // gen all the graphics we need
  this.deferFlush();

  this.calcTreeCoords();
};

GitVisuals.prototype.drawTreeFirstTime = function() {
  this.gitReady = true;
  this.calcTreeCoords();

  _.each(this.visNodeMap, function(visNode) {
    visNode.genGraphics(this.paper);
  }, this);

  this.visEdgeCollection.each(function(edge) {
    edge.genGraphics(this.paper);
  }, this);

  this.visBranchCollection.each(function(visBranch) {
    visBranch.genGraphics(this.paper);
  }, this);

  this.visTagCollection.each(function(visTag) {
    visTag.genGraphics(this.paper);
  }, this);

  this.zIndexReflow();
};


/************************
 * Random util functions, some from liquidGraph
 ***********************/
function blendHueStrings(hueStrings) {
  // assumes a sat of 0.7 and brightness of 1

  var x = 0;
  var y = 0;
  var totalSat = 0;
  var totalBright = 0;
  var length = hueStrings.length;

  _.each(hueStrings, function(hueString) {
    var exploded = hueString.split('(')[1];
    exploded = exploded.split(')')[0];
    exploded = exploded.split(',');

    totalSat += parseFloat(exploded[1]);
    totalBright += parseFloat(exploded[2]);
    var hue = parseFloat(exploded[0]);

    var angle = hue * Math.PI * 2;
    x += Math.cos(angle);
    y += Math.sin(angle);
  });

  x = x / length;
  y = y / length;
  totalSat = totalSat / length;
  totalBright = totalBright / length;

  var hue = Math.atan2(y, x) / (Math.PI * 2); // could fail on 0's
  if (hue < 0) {
    hue = hue + 1;
  }
  return 'hsb(' + String(hue) + ',' + String(totalSat) + ',' + String(totalBright) + ')';
}

exports.GitVisuals = GitVisuals;

