///// FIXME: Use path._rings instead of path._latlngs???
///// FIXME: Panic if this._map doesn't exist when called.
///// FIXME: Implement snakeOut()
///// FIXME: Implement layerGroup.snakeIn() / Out()

L.Polyline.include({
  // Hi-res timestamp indicating when the last calculations for vertices and
  // distance took place.
  _snakingTimestamp: 0,

  // How many rings and vertices we've already visited
  // Yeah, yeah, "rings" semantically only apply to polygons, but L.Polyline
  // internally uses that nomenclature.
  _snakingRings: 0,
  _snakingVertices: 0,

  // Distance to draw (in screen pixels) since the last vertex
  _snakingDistance: 0,

  // Flag
  _snaking: false,

  /// TODO: accept a 'map' parameter, fall back to addTo() in case
  /// performance.now is not available.
  snakeIn: function () {
    if (this._snaking) {
      return;
    }

    if (!('performance' in window) || !('now' in window.performance) || !this._map) {
      return;
    }

    this._snaking = true;
    this._snakingTime = performance.now();
    this._snakingVertices = this._snakingRings = this._snakingDistance = 0;

    if (!this._snakeLatLngs) {
      this._snakeLatLngs = L.LineUtil.isFlat(this._latlngs) ? [this._latlngs] : this._latlngs;
    }

    // Init with just the first (0th) vertex in a new ring
    // Twice because the first thing that this._snake is is chop the head.
    this._latlngs = [[this._snakeLatLngs[0][0], this._snakeLatLngs[0][0]]];

    this._update();
    this._snake();
    this.fire('snakestart');
    return this;
  },

  _snake: function () {
    var now = performance.now();
    var diff = now - this._snakingTime; // In milliseconds
    var forward = (diff * this.options.snakingSpeed) / 1000; // In pixels
    this._snakingTime = now;

    // Chop the head from the previous frame
    this._latlngs[this._snakingRings].pop();

    return this._snakeForward(forward);
  },

  _snakeForward: function (forward) {
    // If polyline has been removed from the map stop _snakeForward
    if (!this._map) return;
    // Calculate distance from current vertex to next vertex
    var currPoint = this._map.latLngToContainerPoint(this._snakeLatLngs[this._snakingRings][this._snakingVertices]);
    var nextPoint = this._map.latLngToContainerPoint(this._snakeLatLngs[this._snakingRings][this._snakingVertices + 1]);

    var distance = currPoint.distanceTo(nextPoint);
   
    // 		console.log('Distance to next point:', distance, '; Now at: ', this._snakingDistance, '; Must travel forward:', forward);
    // 		console.log('Vertices: ', this._latlngs);

    if (this._snakingDistance + forward > distance) {
      // Jump to next vertex
      this._snakingVertices++;
      this._latlngs[this._snakingRings].push(this._snakeLatLngs[this._snakingRings][this._snakingVertices]);

      if (this._snakingVertices >= this._snakeLatLngs[this._snakingRings].length - 1) {
        if (this._snakingRings >= this._snakeLatLngs.length - 1) {
          return this._snakeEnd();
        } else {
          this._snakingVertices = 0;
          this._snakingRings++;
          this._latlngs[this._snakingRings] = [this._snakeLatLngs[this._snakingRings][this._snakingVertices]];
        }
      }

      this._snakingDistance -= distance;
      return this._snakeForward(forward);
    }

    this._snakingDistance += forward;

    var percent = this._snakingDistance / distance;

    var headPoint = nextPoint.multiplyBy(percent).add(currPoint.multiplyBy(1 - percent));

    // Put a new head in place.
    var headLatLng = this._map.containerPointToLatLng(headPoint);
    this._latlngs[this._snakingRings].push(headLatLng);

    this.setLatLngs(this._latlngs);
    this.fire('snake');
    L.Util.requestAnimFrame(this._snake, this);
  },

  _snakeEnd: function () {
    this.setLatLngs(this._snakeLatLngs);
    this._snaking = false;
    this.fire('snakeend');
  }
});

L.Polyline.mergeOptions({
  snakingSpeed: 200 // In pixels/sec
});

L.LayerGroup.include({
  _snakingLayers: [],
  _snakingLayersDone: 0,

  snakeIn: function () {
    if (!('performance' in window) || !('now' in window.performance) || !this._map || this._snaking) {
      return;
    }

    this._snaking = true;
    this._snakingLayers = [];
    this._snakingLayersDone = 0;
    var keys = Object.keys(this._layers);
    for (var i in keys) {
      var key = keys[i];
      this._snakingLayers.push(this._layers[key]);
    }
    this.clearLayers();

    this.fire('snakestart');
    return this._snakeNext();
  },

  _snakeNext: function () {
    if (this._snakingLayersDone >= this._snakingLayers.length) {
      this.fire('snakeend');
      this._snaking = false;
      return;
    }

    var currentLayer = this._snakingLayers[this._snakingLayersDone];

    this._snakingLayersDone++;

    this.addLayer(currentLayer);
    if ('snakeIn' in currentLayer) {
      currentLayer.once(
        'snakeend',
        function () {
          setTimeout(this._snakeNext.bind(this), this.options.snakingPause);
        },
        this
      );
      currentLayer.snakeIn();
    } else {
      setTimeout(this._snakeNext.bind(this), this.options.snakingPause);
    }

    this.fire('snake');
    return this;
  }
});

L.LayerGroup.mergeOptions({
  snakingPause: 200
});
