(function() {
  var SpanSkipList,
    __slice = [].slice;

  module.exports = SpanSkipList = (function() {
    SpanSkipList.prototype.maxHeight = 8;

    SpanSkipList.prototype.probability = .25;

    function SpanSkipList() {
      var dimensions, index;
      dimensions = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
      this.dimensions = dimensions;
      this.head = this.createNode(this.maxHeight, this.buildZeroDistance());
      this.tail = this.createNode(this.maxHeight, this.buildZeroDistance());
      index = 0;
      while (index < this.maxHeight) {
        this.head.next[index] = this.tail;
        this.head.distance[index] = this.buildZeroDistance();
        index++;
      }
    }

    SpanSkipList.prototype.totalTo = function(target, dimension) {
      var index, nextDistanceInTargetDimension, node, totalDistance, _ref;
      totalDistance = this.buildZeroDistance();
      node = this.head;
      index = this.maxHeight - 1;
      while (index >= 0) {
        while (true) {
          if (node.next[index] === this.tail) {
            break;
          }
          nextDistanceInTargetDimension = totalDistance[dimension] + node.distance[index][dimension] + ((_ref = node.next[index].element[dimension]) != null ? _ref : 1);
          if (nextDistanceInTargetDimension > target) {
            break;
          }
          this.incrementDistance(totalDistance, node.distance[index]);
          this.incrementDistance(totalDistance, node.next[index].element);
          node = node.next[index];
        }
        index--;
      }
      return totalDistance;
    };

    SpanSkipList.prototype.splice = function() {
      var count, dimension, elements, index;
      dimension = arguments[0], index = arguments[1], count = arguments[2], elements = 4 <= arguments.length ? __slice.call(arguments, 3) : [];
      return this.spliceArray(dimension, index, count, elements);
    };

    SpanSkipList.prototype.spliceArray = function(dimension, index, count, elements) {
      var i, newNode, nextNode, previous, previousDistances, removedElements;
      previous = this.buildPreviousArray();
      previousDistances = this.buildPreviousDistancesArray();
      nextNode = this.findClosestNode(dimension, index, previous, previousDistances);
      removedElements = [];
      while (count > 0 && nextNode !== this.tail) {
        removedElements.push(nextNode.element);
        nextNode = this.removeNode(nextNode, previous, previousDistances);
        count--;
      }
      i = elements.length - 1;
      while (i >= 0) {
        newNode = this.createNode(this.getRandomNodeHeight(), elements[i]);
        this.insertNode(newNode, previous, previousDistances);
        i--;
      }
      return removedElements;
    };

    SpanSkipList.prototype.getLength = function() {
      return this.getElements().length;
    };

    SpanSkipList.prototype.getElements = function() {
      var elements, node;
      elements = [];
      node = this.head;
      while (node.next[0] !== this.tail) {
        elements.push(node.next[0].element);
        node = node.next[0];
      }
      return elements;
    };

    SpanSkipList.prototype.findClosestNode = function(dimension, index, previous, previousDistances) {
      var i, nextHopDistance, node, totalDistance, _i, _ref, _ref1;
      totalDistance = this.buildZeroDistance();
      node = this.head;
      for (i = _i = _ref = this.maxHeight - 1; _ref <= 0 ? _i <= 0 : _i >= 0; i = _ref <= 0 ? ++_i : --_i) {
        while (true) {
          if (node.next[i] === this.tail) {
            break;
          }
          nextHopDistance = ((_ref1 = node.next[i].element[dimension]) != null ? _ref1 : 1) + node.distance[i][dimension];
          if (totalDistance[dimension] + nextHopDistance > index) {
            break;
          }
          this.incrementDistance(totalDistance, node.distance[i]);
          this.incrementDistance(totalDistance, node.next[i].element);
          this.incrementDistance(previousDistances[i], node.distance[i]);
          this.incrementDistance(previousDistances[i], node.next[i].element);
          node = node.next[i];
        }
        previous[i] = node;
      }
      return node.next[0];
    };

    SpanSkipList.prototype.insertNode = function(node, previous, previousDistances) {
      var coveredDistance, level;
      coveredDistance = this.buildZeroDistance();
      level = 0;
      while (level < node.height) {
        node.next[level] = previous[level].next[level];
        previous[level].next[level] = node;
        node.distance[level] = this.subtractDistances(previous[level].distance[level], coveredDistance);
        previous[level].distance[level] = this.cloneObject(coveredDistance);
        this.incrementDistance(coveredDistance, previousDistances[level]);
        level++;
      }
      level = node.height;
      while (level < this.maxHeight) {
        this.incrementDistance(previous[level].distance[level], node.element);
        level++;
      }
    };

    SpanSkipList.prototype.removeNode = function(node, previous) {
      var level;
      level = 0;
      while (level < node.height) {
        previous[level].next[level] = node.next[level];
        this.incrementDistance(previous[level].distance[level], node.distance[level]);
        level++;
      }
      level = node.height;
      while (level < this.maxHeight) {
        this.decrementDistance(previous[level].distance[level], node.element);
        level++;
      }
      return node.next[0];
    };

    SpanSkipList.prototype.buildPreviousArray = function() {
      var index, previous;
      previous = new Array(this.maxHeight);
      index = 0;
      while (index < this.maxHeight) {
        previous[index] = this.head;
        index++;
      }
      return previous;
    };

    SpanSkipList.prototype.buildPreviousDistancesArray = function() {
      var distances, index;
      distances = new Array(this.maxHeight);
      index = 0;
      while (index < this.maxHeight) {
        distances[index] = this.buildZeroDistance();
        index++;
      }
      return distances;
    };

    SpanSkipList.prototype.getRandomNodeHeight = function() {
      var height;
      height = 1;
      while (height < this.maxHeight && Math.random() < this.probability) {
        height++;
      }
      return height;
    };

    SpanSkipList.prototype.buildZeroDistance = function() {
      var dimension, _i, _len, _ref;
      if (this.zeroDistance == null) {
        this.zeroDistance = {
          elements: 0
        };
        _ref = this.dimensions;
        for (_i = 0, _len = _ref.length; _i < _len; _i++) {
          dimension = _ref[_i];
          this.zeroDistance[dimension] = 0;
        }
      }
      return this.cloneObject(this.zeroDistance);
    };

    SpanSkipList.prototype.incrementDistance = function(distance, delta) {
      var dimension, _i, _len, _ref, _ref1;
      distance.elements += (_ref = delta.elements) != null ? _ref : 1;
      _ref1 = this.dimensions;
      for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
        dimension = _ref1[_i];
        distance[dimension] += delta[dimension];
      }
    };

    SpanSkipList.prototype.decrementDistance = function(distance, delta) {
      var dimension, _i, _len, _ref, _ref1;
      distance.elements -= (_ref = delta.elements) != null ? _ref : 1;
      _ref1 = this.dimensions;
      for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
        dimension = _ref1[_i];
        distance[dimension] -= delta[dimension];
      }
    };

    SpanSkipList.prototype.addDistances = function(a, b) {
      var dimension, distance, _i, _len, _ref, _ref1, _ref2;
      distance = {
        elements: ((_ref = a.elements) != null ? _ref : 1) + ((_ref1 = b.elements) != null ? _ref1 : 1)
      };
      _ref2 = this.dimensions;
      for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
        dimension = _ref2[_i];
        distance[dimension] = a[dimension] + b[dimension];
      }
      return distance;
    };

    SpanSkipList.prototype.subtractDistances = function(a, b) {
      var dimension, distance, _i, _len, _ref, _ref1, _ref2;
      distance = {
        elements: ((_ref = a.elements) != null ? _ref : 1) - ((_ref1 = b.elements) != null ? _ref1 : 1)
      };
      _ref2 = this.dimensions;
      for (_i = 0, _len = _ref2.length; _i < _len; _i++) {
        dimension = _ref2[_i];
        distance[dimension] = a[dimension] - b[dimension];
      }
      return distance;
    };

    SpanSkipList.prototype.verifyDistanceInvariant = function() {
      var distanceOnPreviousLevel, distanceOnThisLevel, isEqual, level, node, _i, _ref, _results;
      isEqual = require('underscore').isEqual;
      _results = [];
      for (level = _i = _ref = this.maxHeight - 1; _ref <= 1 ? _i <= 1 : _i >= 1; level = _ref <= 1 ? ++_i : --_i) {
        node = this.head;
        _results.push((function() {
          var _results1;
          _results1 = [];
          while (node !== this.tail) {
            distanceOnThisLevel = this.addDistances(node.element, node.distance[level]);
            distanceOnPreviousLevel = this.distanceBetweenNodesAtLevel(node, node.next[level], level - 1);
            if (!isEqual(distanceOnThisLevel, distanceOnPreviousLevel)) {
              console.log(this.inspect());
              throw new Error("On level " + level + ": Distance " + (JSON.stringify(distanceOnThisLevel)) + " does not match " + (JSON.stringify(distanceOnPreviousLevel)));
            }
            _results1.push(node = node.next[level]);
          }
          return _results1;
        }).call(this));
      }
      return _results;
    };

    SpanSkipList.prototype.distanceBetweenNodesAtLevel = function(startNode, endNode, level) {
      var distance, node;
      distance = this.buildZeroDistance();
      node = startNode;
      while (node !== endNode) {
        this.incrementDistance(distance, node.element);
        this.incrementDistance(distance, node.distance[level]);
        node = node.next[level];
      }
      return distance;
    };

    SpanSkipList.prototype.createNode = function(height, element) {
      return {
        height: height,
        element: element,
        next: new Array(height),
        distance: new Array(height)
      };
    };

    SpanSkipList.prototype.cloneObject = function(object) {
      var cloned, key, value;
      cloned = {};
      for (key in object) {
        value = object[key];
        cloned[key] = value;
      }
      return cloned;
    };

    return SpanSkipList;

  })();

}).call(this);
